Story OP-013: Backend exclusion en lot + marquage hors-plateforme¶
Module: operator-console Slice: Architecture §11 OP-004b + FR-OP-08 Side: [BACKEND] Version target: V0.0.3 Priority: 14 Depends on: OP-007 (PATCH cc + édition pré-envoi infra) Can develop concurrently with: OP-012, OP-014, OP-015 Merge order: Before OP-017 (frontend bulk + HP modale) Estimated complexity: S PRD User Stories: US-OP-04 AC4 (bulk exclude), US-OP-06 AC1+AC2+AC3 (HP), FR-OP-08 Wireframe: N/A — backend only
Objective¶
Permettre 2 actions backend : (a) exclure en lot N contrats d'une campagne BROUILLON (statut INCLUS → EXCLU + raison) ; (b) marquer un contrat de campagne ENVOYÉE comme RÉGLÉ_HORS_PLATEFORME avec méthode/date/montant/référence.
Backend Scope¶
Service Layer¶
CampaignContractsBulkController.exclude(id, body):POST /api/operator/campaigns/{id}/contracts/bulk-exclude(RBAC : agency_operator de l'agence OU cabinet_admin).- Body :
{ "ccIds": ["uuid1", "uuid2", ...], "reason"?: "string ≤ 500 chars" }. - Vérifie : campagne BROUILLON (sinon 400
OP_CAMPAIGN_WRONG_STATUS), tous les ccIds appartiennent à la campagne (sinon 400OP_INVALID_CC_IDS). - UPDATE
campaign_contractsSETstatus='EXCLU', excluded_by=:user, excluded_at=now(), exclusion_reason=:reasonWHERE id IN (...). - 1 audit event
CONTRACT_EXCLUDEDpar cc exclu (correlation_id = cc.id, payload :{contractId, campaignId, reason, operatorId}). -
Retour :
{ excludedCount }. -
CampaignContractsHpController.markOffPlatform(id, ccId, body):POST /api/operator/campaigns/{id}/contracts/{ccId}/mark-offplatform(RBAC : agency_operator de l'agence OU cabinet_admin). - Body :
{ "method": "MOBILE_MONEY"|"CASH"|"CHEQUE"|"VIREMENT", "date": "ISO date ≤ today", "amount": int>0 XOF, "reference"?: "≤ 200 chars" }. - Précondition :
campaign.status='ENVOYÉE'ETcc.status NOT IN ('PAYÉ','LIEN_RÉVOQUÉ','EXCLU','RÉGLÉ_HORS_PLATEFORME')(sinon 400OP_HP_NOT_ALLOWED). - UPDATE
campaign_contractsSETstatus='RÉGLÉ_HORS_PLATEFORME', hp_method=..., hp_date=..., hp_amount=..., hp_reference=..., hp_marked_by=:user, hp_marked_at=now(). - Audit
HORS_PLATEFORME_MARKED {contractId, campaignId, method, amount, date, reference?, operatorId, timestamp}. - Retour :
{ campaignContractId, status:"RÉGLÉ_HORS_PLATEFORME" }.
API Endpoints¶
| Method | Path | Request | Response | Auth |
|---|---|---|---|---|
POST |
/api/operator/campaigns/{id}/contracts/bulk-exclude |
{ ccIds[], reason? } |
200 { excludedCount } |
agency_operator(agence)+cabinet_admin |
POST |
/api/operator/campaigns/{id}/contracts/{ccId}/mark-offplatform |
{ method, date, amount, reference? } |
200 { campaignContractId, status } |
agency_operator(agence)+cabinet_admin |
Validation Rules¶
ccIds: array non vide, ≤ 100 ids par appel (limite bulk).reason: optionnel, ≤ 500 caractères.method∈ enum strict.date: ISO valide, ≤ today.amount: entier strict > 0.reference: ≤ 200 chars.
Multi-Tenant Considerations¶
- Scope
tenant_id. agency_id: agency_operator ne peut exclure/marquer que sur les campagnes de son agence (via AgencyScopeFilter activé OP-010).
Audit & Logging¶
CONTRACT_EXCLUDED× N par bulk (un event par contrat — traçabilité fine).HORS_PLATEFORME_MARKED× 1 par appel.- MDC :
campaign_id,cc_id.
Acceptance Criteria¶
AC-001: Bulk exclude flip statuts + audit N events (US-OP-04 AC4)
Given une campagne BROUILLON avec 5 cc INCLUS
When POST /campaigns/{id}/contracts/bulk-exclude body={"ccIds":["1","2","3"],"reason":"Doublons"}
Then la réponse 200 contient excludedCount=3
And les 3 cc passent à EXCLU avec excluded_by + reason
And les 2 autres restent INCLUS
And 3 audit events CONTRACT_EXCLUDED apparaissent.
AC-002: Bulk exclude sur ENVOYÉE → 400 (statut interdit)
Given une campagne ENVOYÉE
When POST .../bulk-exclude
Then 400 OP_CAMPAIGN_WRONG_STATUS.
AC-003: HP marquage flip + audit (FR-OP-08)
Given un cc NOTIF_LIVRÉ d'une campagne ENVOYÉE
When POST .../mark-offplatform body={"method":"MOBILE_MONEY","date":"2026-05-22","amount":85000}
Then le cc passe à RÉGLÉ_HORS_PLATEFORME avec hp_method/date/amount peuplés
And audit HORS_PLATEFORME_MARKED contient method/amount/date/operatorId/timestamp.
AC-004: HP sur cc PAYÉ → 400
Given un cc PAYÉ
When POST .../mark-offplatform
Then 400 OP_HP_NOT_ALLOWED.
AC-005: Validation amount entier > 0
Given body={"method":"CASH","date":"2026-05-22","amount":0}
When POST .../mark-offplatform
Then 400 avec validation error "Le montant doit être supérieur à 0."
Given body={"amount":85000.5}
Then 400 "Le montant doit être un entier (pas de décimale)."
Compliance Rules¶
- FR-OP-08 : précondition, méthode, audit.
VÉRIFIÉ. - Critical rule #2 : amount entier XOF strict.
VÉRIFIÉ. - Critical rule #3 : audit CONTRACT_EXCLUDED + HORS_PLATEFORME_MARKED.
VÉRIFIÉ.
Standards & Conventions¶
docs/standards/critical-rules.mddocs/standards/api-guidelines.md— bulk limit, validation messages FR.
Testing Requirements¶
Unit Tests¶
- Validations : amount float, ≤ 0, date future ; method enum strict.
markOffPlatformprécondition statuts.
Integration Tests¶
BulkExcludeITAC-001 + AC-002.MarkHpITAC-003..AC-005.- Scope agence : agency_operator B ne peut pas exclure cc de campagne agence A → 403.
What QA Will Validate¶
- Audit log contient N events après bulk (pas 1 seul agrégé).
- HP fields tous peuplés correctement.
Out of Scope¶
- Frontend bulk action bar / HP modale → OP-017.
- Rappel + renvoi lien (US-OP-05 AC4 / FR-OP-09) → V1.
Definition of Done¶
- [ ] Les 2 endpoints retournent 200 conformes
- [ ] AC-001..AC-005 passent
- [ ] CONTRACT_EXCLUDED × N + HORS_PLATEFORME_MARKED × 1
- [ ] Validation enum + amount strict
- [ ] RBAC scope agence respecté
- [ ] Tenant isolation
- [ ] Tests verts