Story OP-012: Backend annulation campagne — POST /campaigns/{id}/cancel¶
Module: operator-console
Slice: Architecture §11 OP-004a + §6.4 (FR-OP-05)
Side: [BACKEND]
Version target: V0.0.3
Priority: 13
Depends on: OP-003 (campagne ENVOYÉE), AUTH (SignedLinkService.revokeLinks), NOTIF (NotificationDispatchService.cancelPending)
Can develop concurrently with: OP-013, OP-014, OP-015
Merge order: Before OP-016 (frontend annulation)
Estimated complexity: M
PRD User Stories: US-OP-07 AC1+AC2, FR-OP-05
Wireframe: N/A — backend only
Objective¶
Implémenter l'annulation d'une campagne (BROUILLON ou ENVOYÉE) avec révocation atomique des liens signés AUTH et annulation des envois NOTIF en queue, transitions cohérentes vers ANNULÉE / LIEN_RÉVOQUÉ, audit CAMPAIGN_CANCELLED. Cabinet_admin uniquement V0.0.3 (élargissement RBAC reporté V1).
Backend Scope¶
Service Layer¶
CampaignCancelController.cancel(id):POST /api/operator/campaigns/{id}/cancel(RBAC : cabinet_admin uniquement V0.0.3).- Charge campagne → 404 sinon ; valide statut ∈ {BROUILLON, ENVOYÉE} sinon 400
OP_CAMPAIGN_WRONG_STATUS. - Charge
nonTerminalCcIds = SELECT id FROM campaign_contracts WHERE campaign_id=:c AND tenant_id=:t AND status NOT IN ('PAYÉ','RÉGLÉ_HORS_PLATEFORME','EXCLU','LIEN_RÉVOQUÉ'). -
Délègue à
CampaignCancellationService.cancel(...). -
CampaignCancellationService.cancel(...)(chemin orchestration §6.4) : NotificationDispatchService.cancelPending(campaignId)→{cancelledCount}.SignedLinkService.revokeLinks(nonTerminalCcIds)→{revokedCount}. Atomique côté AUTH (bulk).- TX_final : UPDATE
campaignsSETstatus='ANNULÉE', cancelled_at=now(), cancelled_by=:userId. - UPDATE
campaign_contractsSETstatus='LIEN_RÉVOQUÉ'WHERE id IN (:nonTerminalCcIds). - Audit
CAMPAIGN_CANCELLED {campaignId, cancelledBy, linksRevokedCount, notifCancelledCount, timestamp}. -
Retour :
{campaignId, status:"ANNULÉE", linksRevokedCount, notifCancelledCount}. -
Failure handling (§12 arch) :
- Si AUTH.revokeLinks échoue → throw 502
OP_AUTH_UNAVAILABLE; campagne NON flippée (l'utilisateur retentera). - Si NOTIF.cancelPending échoue (mais AUTH OK) → log + audit error, continue le flip (les notifs déjà en queue partiront, c'est moins grave que des liens non révoqués).
- Job de réconciliation V1 détecte campagnes ENVOYÉE avec tous cc en LIEN_RÉVOQUÉ et complète le flip.
API Endpoints¶
| Method | Path | Request | Response | Auth |
|---|---|---|---|---|
POST |
/api/operator/campaigns/{id}/cancel |
{} |
200 { campaignId, status:"ANNULÉE", linksRevokedCount, notifCancelledCount } |
cabinet_admin |
Validation Rules¶
- Statut courant ∈ {BROUILLON, ENVOYÉE} ; CLÔTURÉE/ANNULÉE → 400.
Multi-Tenant Considerations¶
- Scope
tenant_idstrict.campaign_iddoit appartenir au tenant. agency_id: cabinet_admin a accès cross-agences (RBAC OP-010).
Audit & Logging¶
CAMPAIGN_CANCELLEDaveccorrelation_id = campaignId, payload complet.- MDC enrichi.
Acceptance Criteria¶
AC-001: Annulation campagne ENVOYÉE révoque liens + annule NOTIF queue + flip statut (FR-OP-05)
Given une campagne ENVOYÉE avec 12 cc dont 8 non-terminaux et 4 PAYÉ
When cabinet_admin POST /campaigns/{id}/cancel
Then NotificationDispatchService.cancelPending est appelé
And SignedLinkService.revokeLinks est appelé avec les 8 ccIds non-terminaux
And campaigns.status='ANNULÉE', cancelled_at!=null
And les 8 cc passent à LIEN_RÉVOQUÉ
And les 4 PAYÉ restent intacts
And 1 audit event CAMPAIGN_CANCELLED est créé.
AC-002: Annulation BROUILLON (avant envoi) flip simple sans appels AUTH/NOTIF
Given une campagne BROUILLON avec 5 cc INCLUS
When POST /cancel
Then NOTIF/AUTH ne sont PAS appelés (rien à révoquer)
And campagne → ANNULÉE, les 5 cc → LIEN_RÉVOQUÉ.
AC-003: AUTH down → campagne NON flippée
Given SignedLinkService.revokeLinks lève 503
When POST /cancel
Then 502 OP_AUTH_UNAVAILABLE
And la campagne reste ENVOYÉE (vérifié DB)
And aucun cc n'est touché (pas de mutation partielle).
AC-004: agency_operator → 403
Given agency_operator
When POST /cancel
Then 403 OP_ROLE_FORBIDDEN.
Compliance Rules¶
- FR-OP-05 : modale "{X} liens" côté front + révocation synchronique + flip campagne.
VÉRIFIÉ. - PRD §C.3 :
revokeLinkssynchrone avant flip.VÉRIFIÉ. - Audit critical rule #3 :
CAMPAIGN_CANCELLED.VÉRIFIÉ.
Standards & Conventions¶
docs/standards/critical-rules.mddocs/standards/java-spring-guidelines.md— orchestration sync inter-modules.docs/standards/api-guidelines.md— 502 pour upstream failure.
Testing Requirements¶
Unit Tests¶
CampaignCancellationServiceTest: ordre des appels (NOTIF puis AUTH puis flip).- Failure NOTIF post-AUTH : continue flip.
- Failure AUTH : abort.
Integration Tests¶
CancelCampaignITAC-001..AC-004 avec WireMock NOTIF + AUTH stubs.
What QA Will Validate¶
- Audit log contient CAMPAIGN_CANCELLED avec linksRevokedCount + notifCancelledCount exacts.
- Aucun cc PAYÉ touché.
Out of Scope¶
- Frontend modale annulation → OP-016.
- Job de réconciliation campagnes "à moitié annulées" → V1.
- Élargissement RBAC à agency_operator → V1.
Definition of Done¶
- [ ]
POST /campaigns/{id}/cancelretourne 200 conforme - [ ] AC-001..AC-004 passent
- [ ] CAMPAIGN_CANCELLED émis avec payload complet
- [ ] Ordre orchestration : NOTIF.cancelPending → AUTH.revokeLinks → flip
- [ ] AUTH failure NE flippe PAS (AC-003)
- [ ] cc PAYÉ/HORS_PLAT non touchés
- [ ] Tenant isolation
- [ ] Tests intégration verts