Aller au contenu

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 campaigns SET status='ANNULÉE', cancelled_at=now(), cancelled_by=:userId.
  • UPDATE campaign_contracts SET status='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_id strict. campaign_id doit appartenir au tenant.
  • agency_id : cabinet_admin a accès cross-agences (RBAC OP-010).

Audit & Logging

  • CAMPAIGN_CANCELLED avec correlation_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 : revokeLinks synchrone avant flip. VÉRIFIÉ.
  • Audit critical rule #3 : CAMPAIGN_CANCELLED. VÉRIFIÉ.

Standards & Conventions

  • docs/standards/critical-rules.md
  • docs/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

  • CancelCampaignIT AC-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}/cancel retourne 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