Aller au contenu

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 400 OP_INVALID_CC_IDS).
  • UPDATE campaign_contracts SET status='EXCLU', excluded_by=:user, excluded_at=now(), exclusion_reason=:reason WHERE id IN (...).
  • 1 audit event CONTRACT_EXCLUDED par 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' ET cc.status NOT IN ('PAYÉ','LIEN_RÉVOQUÉ','EXCLU','RÉGLÉ_HORS_PLATEFORME') (sinon 400 OP_HP_NOT_ALLOWED).
  • UPDATE campaign_contracts SET status='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.md
  • docs/standards/api-guidelines.md — bulk limit, validation messages FR.

Testing Requirements

Unit Tests

  • Validations : amount float, ≤ 0, date future ; method enum strict.
  • markOffPlatform précondition statuts.

Integration Tests

  • BulkExcludeIT AC-001 + AC-002.
  • MarkHpIT AC-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