Story OP-007: Backend édition pré-envoi + garde anomalies bloquantes¶
Module: operator-console Slice: Architecture §11 OP-002 (édition + garde) + FR-OP-02 + FR-OP-07 Side: [BACKEND] Version target: V0.0.1 Priority: 8 Depends on: OP-002 (campagne BROUILLON), ING (AnomalyService.findOpen pour la garde) Can develop concurrently with: OP-006 Merge order: Before OP-008 (frontend BROUILLON v2 dépend du PATCH + de la garde) Estimated complexity: M PRD User Stories: US-OP-04 AC2 (garde) + AC3 (édition + audit), FR-OP-02, FR-OP-07 Wireframe: N/A — backend only
Objective¶
Permettre l'édition pré-envoi (téléphone, montant, date d'échéance) sur les contrats d'une campagne BROUILLON par cabinet_admin uniquement (FR-OP-07), avec audit FIELD_CORRECTED. Bloquer la transition BROUILLON → ENVOYÉE si ≥ 1 anomalie bloquante non résolue sur un contrat INCLUS (FR-OP-02).
Backend Scope¶
Entities¶
Édite contracts (table ING) via délégation à ContractMutationService du module ING (qui propage la modification + audit ING). Lit anomalies via AnomalyService.findOpen(tenantId, contractId, type=BLOCKING).
Migrations¶
Aucune.
Service Layer¶
CampaignContractEditController.patch(...):PATCH /api/operator/campaigns/{id}/contracts/{ccId}(RBAC : cabinet_admin uniquement en V0.0.1 —@PreAuthorize("hasRole('CABINET_ADMIN')")).- Body :
{ "phone"?, "amount"?, "dueDate"? }— seuls ces 3 champs (FR-OP-07). - Vérifie
campaign.status == BROUILLON(sinon 400OP_CAMPAIGN_WRONG_STATUS). - Validation :
amount > 0 && integer,phoneE.164 CI+225XXXXXXXXXX,dueDate > today. - Délègue à
ContractMutationService.updateFields(tenantId, contractId, fields)du module ING (qui applique + journalise côté ING). -
Émet audit OP
FIELD_CORRECTED {contractId, campaignId, fieldName, oldValue, newValue, operatorId, timestamp}— UN événement par champ modifié (donc jusqu'à 3 events par appel). -
CampaignSendGuardService(consommé parCampaignSenderServicede OP-003 — extension) : checkBlockingAnomalies(campaignId): appelleAnomalyService.countOpen(tenantId, campaignContractIds[], type=BLOCKING).- Si count > 0 → throw
BlockingAnomaliesException(count)→ 400OP_CAMPAIGN_BLOCKING_ANOMALIESavec payload{ count, contractIds[] }et message FR : « {N} contrat(s) présentent des anomalies bloquantes. Corrigez les données ou excluez les contrats avant d'envoyer. » - Modification de OP-003 : le
CampaignSendControllerappelleguard.check(...)AVANT de muter le statut.
API Endpoints¶
| Method | Path | Request | Response | Auth |
|---|---|---|---|---|
PATCH |
/api/operator/campaigns/{id}/contracts/{ccId} |
{ "phone"?, "amount"?, "dueDate"? } |
200 { campaignContractId, updatedFields: {...} } |
cabinet_admin |
POST /send (déjà existant OP-003) renvoie désormais 400 OP_CAMPAIGN_BLOCKING_ANOMALIES si garde violée.
Validation Rules¶
phone:^\+225\d{10}$.amount: entier strict > 0 (pas de float).dueDate: ISO date > today.ccIddoit appartenir à la campagne{id}du tenant courant (sinon 404).
Multi-Tenant Considerations¶
- Scope
tenant_idstrict. cabinet_adminpeut éditer tout contrat du tenant V0.0.1 (toutes agences).
Audit & Logging¶
FIELD_CORRECTED× N champs modifiés (1 event par champ pour traçabilité fine).- MDC :
campaign_id,contract_id,cc_id. - Garde violée → log WARN avec
blockingCount.
Acceptance Criteria¶
AC-001: PATCH édite uniquement phone/amount/dueDate
Given un cc d'une campagne BROUILLON
When PATCH /campaigns/{id}/contracts/{ccId} body={"phone":"+22507020303","amount":95000}
Then la réponse 200 confirme les 2 champs modifiés
And 2 audit events FIELD_CORRECTED apparaissent avec oldValue/newValue corrects
And le contrat ING est effectivement mis à jour.
AC-002: agency_operator ne peut PAS éditer en V0.0.1 (RBAC)
Given un agency_operator
When PATCH /campaigns/{id}/contracts/{ccId}
Then 403 OP_ROLE_FORBIDDEN.
AC-003: Édition sur campagne ENVOYÉE → refusé
Given une campagne ENVOYÉE
When PATCH ...
Then 400 OP_CAMPAIGN_WRONG_STATUS, aucune mutation.
AC-004: POST /send bloqué si anomalies bloquantes (FR-OP-02)
Given une campagne BROUILLON avec 2 cc INCLUS ayant chacun ≥1 anomalie BLOCKING non résolue
When POST /campaigns/{id}/send
Then 400 OP_CAMPAIGN_BLOCKING_ANOMALIES avec payload {count:2, contractIds:[...]}
And la campagne reste BROUILLON (vérifié DB).
Compliance Rules¶
- FR-OP-02 : garde anomalies bloquantes.
VÉRIFIÉ. - FR-OP-07 : 3 champs éditables uniquement.
VÉRIFIÉ. - Critical rule #2 :
amountentier XOF, jamais float.VÉRIFIÉ. - Critical rule #3 : audit
FIELD_CORRECTEDenregistré.
Standards & Conventions¶
docs/standards/critical-rules.md— règles #2, #3.docs/standards/api-guidelines.md— PATCH partial update.docs/standards/java-spring-guidelines.md—@PreAuthorize.
Testing Requirements¶
Unit Tests¶
- Validation : phone invalide, amount float, amount ≤ 0, dueDate past → 400.
CampaignSendGuardServiceTest: garde vide → OK ; garde > 0 → exception.
Integration Tests¶
PatchContractITAC-001..AC-003.SendGuardITAC-004.- Test : un audit par champ modifié (3 fields → 3 events).
What QA Will Validate¶
- Édition d'un contrat existant côté ING propage les changements (vue DB ING).
- Aucun champ hors {phone, amount, dueDate} accepté.
Out of Scope¶
- Édition par
agency_operator→ arrive en V0.0.2 (OP-010 ouvre le RBAC à l'opérateur de l'agence du contrat). - Exclusion bulk → OP-013.
- Frontend drawer édition → OP-008.
Definition of Done¶
- [ ]
PATCH /campaigns/{id}/contracts/{ccId}retourne 200 conforme - [ ] AC-001..AC-004 passent
- [ ] FIELD_CORRECTED audité (1 event par champ)
- [ ] Garde anomalies intégrée dans /send (CampaignSenderService modifié)
- [ ] RBAC : seul cabinet_admin en V0.0.1
- [ ] Validation FR-OP-07 stricte (3 champs uniquement)
- [ ] Validation amount entier > 0
- [ ] Tenant isolation prouvée
- [ ] Tests intégration verts