Aller au contenu

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 400 OP_CAMPAIGN_WRONG_STATUS).
  • Validation : amount > 0 && integer, phone E.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é par CampaignSenderService de OP-003 — extension) :

  • checkBlockingAnomalies(campaignId) : appelle AnomalyService.countOpen(tenantId, campaignContractIds[], type=BLOCKING).
  • Si count > 0 → throw BlockingAnomaliesException(count) → 400 OP_CAMPAIGN_BLOCKING_ANOMALIES avec 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 CampaignSendController appelle guard.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.
  • ccId doit appartenir à la campagne {id} du tenant courant (sinon 404).

Multi-Tenant Considerations

  • Scope tenant_id strict.
  • cabinet_admin peut é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 : amount entier XOF, jamais float. VÉRIFIÉ.
  • Critical rule #3 : audit FIELD_CORRECTED enregistré.

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

  • PatchContractIT AC-001..AC-003.
  • SendGuardIT AC-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