Aller au contenu

Story OP-010: Backend — Activation du routing multi-agence (AgencyScopeFilter)

Module: operator-console Slice: Architecture §11 OP-003 Side: [BACKEND] Version target: V0.0.2 Priority: 11 Depends on: OP-002 (création), OP-007 (édition), TENANT-002 (agency_id sur contracts ING) Can develop concurrently with: aucune (active une fonctionnalité transverse) Merge order: Before OP-011 (frontend TopBar selector) Estimated complexity: S PRD User Stories: US-OP-02 AC3 (filtre par agence), V0.0.2 envelope (routing multi-agence) Wireframe: N/A — backend only


Objective

Activer AgencyScopeFilter (codé désactivé en OP-000) sur toutes les requêtes OP : un agency_operator ne voit / modifie que les données de sa propre agence ; les cabinet_admin / supervisor voient toutes les agences. Toutes les colonnes agency_id sur événements audit OP sont propagées. Élargit le RBAC édition d'OP-007 pour autoriser agency_operator sur son agence.


Backend Scope

Entities

Aucune modification de schéma (l'agency_id est déjà présent sur campaigns et operator_notes depuis OP-000).

Migrations

Aucune.

Service Layer

  • AgencyScopeFilter (déjà codé OP-000) — activation via property op.agency-scope.enabled=true en dev/staging/prod. Le test OP-000 vérifiait enabled=false ; OP-010 ajoute les tests enabled=true.
  • CampaignService.createDraft(...) : lit agencyId depuis currentTenant.agencyId() au lieu du fallback "agence par défaut" (TenantConfigCache.getDefaultAgencyId(tenantId)). Pour cabinet_admin/supervisor (sans agency_id JWT), validation requise : body doit contenir agencyId explicite (sinon 400 OP_AGENCY_REQUIRED).
  • CampaignListController.list(...) : filtre agencyId ajouté en query param (cabinet_admin/supervisor) + retourne la colonne agencyName (déjà présente).
  • CampaignContractEditController.patch(...) : RBAC étendu — agency_operator peut éditer SI campaign.agency_id == currentTenant.agencyId(). Sinon 403.
  • CampaignSenderService.send(...) : même RBAC.
  • ContractEligibilityQueryAdapter (OP-000) — lit agencyId du contexte ; appel ING findEligible(tenantId, agencyId, windowDays) avec agency_id réel.
  • AnomalyQueryAdapter (consommé par OP-007 garde et OP-014) — pareil.

API Endpoints

Endpoints existants étendus : - GET /campaigns?agencyId=... (cabinet_admin/supervisor uniquement ; agency_operator ignore le param) - GET /contracts?eligible=true (filtré agence pour agency_operator) - POST /campaigns body : { "type":"SCHEDULED", "agencyId"?: "uuid" }agencyId requis pour cabinet_admin/supervisor.

Nouveau : | Method | Path | Request | Response | Auth | |--------|------|---------|----------|------| | GET | /api/operator/agencies | — | { data: [{id, name}] } (toutes les agences du tenant) | Tous OP |

Validation Rules

  • agencyId UUID valide.
  • Pour cabinet_admin/supervisor : agencyId doit appartenir au tenant courant (validation TENANT).
  • Erreur OP_AGENCY_REQUIRED 400 si cabinet_admin/supervisor appelle POST /campaigns sans agencyId.

Multi-Tenant Considerations

  • L'AgencyScopeFilter (AOP @AgencyScoped) intercepte toutes les requêtes JdbcClient OP annotées :
  • findEligibleContracts → annotée.
  • findCampaignsByStatus → annotée.
  • findCampaignContracts → annotée.
  • findAnomalies → annotée.
  • CRITIQUE : si filter activé mais currentTenant.agencyId() == null (role non-AGENCY_OPERATOR), le filter est bypass (transparent).
  • Profil BYO : même comportement (datasource résolue par tenant ; filter purement WHERE clause).

Audit & Logging

  • Tous les events OP émis incluent agency_id dans le payload (OperatorAuditPublisher enrichit automatiquement). Tester sur 14 types.

Acceptance Criteria

AC-001: agency_operator ne voit que les campagnes de son agence
Given un tenant avec agence A (3 campagnes) et agence B (2 campagnes)
When un agency_operator d'agence A appelle GET /campaigns
Then la réponse contient les 3 campagnes de A
And aucune campagne de B n'est dans la réponse.

AC-002: cabinet_admin peut filtrer par agence ou voir tout
Given le même tenant
When cabinet_admin appelle GET /campaigns sans filtre
Then 5 campagnes retournées (3+2)
When cabinet_admin appelle GET /campaigns?agencyId=B
Then 2 campagnes retournées.

AC-003: agency_operator peut éditer son contrat, pas celui d'une autre agence
Given un cc d'une campagne agence A
When agency_operator d'agence A appelle PATCH → 200 OK
When agency_operator d'agence B appelle PATCH → 403 OP_ROLE_FORBIDDEN.

AC-004: POST /campaigns sans agencyId par cabinet_admin → 400
Given cabinet_admin
When POST /campaigns body={"type":"SCHEDULED"} (pas d'agencyId)
Then 400 OP_AGENCY_REQUIRED message "L'agence cible est obligatoire pour ce rôle."

Compliance Rules

  • ARTCI minimisation + scoping : agency_operator ne peut accéder aux données d'autres agences. VÉRIFIÉ.
  • OP-ADR-04 : agency_id activé V0.0.2 sans migration. VÉRIFIÉ.

Standards & Conventions

  • docs/standards/multi-tenant-model.md — agency scoping pattern.
  • docs/standards/critical-rules.md — rule #1 + agency variant.
  • docs/standards/java-spring-guidelines.md@PreAuthorize SpEL.

Testing Requirements

Unit Tests

  • AgencyScopeFilterTest (extension OP-000) : enabled=true + role=AGENCY_OPERATOR → injection ; enabled=true + role=CABINET_ADMIN → bypass.
  • CampaignServiceCreateAgencyTest : agencyId requis pour cabinet_admin.

Integration Tests

  • AgencyIsolationIT AC-001..AC-004 (réutilise §13.5 arch).
  • Test : 14 types audit avec agency_id présent dans payload.

What QA Will Validate

  • Smoke : 2 agency_operator de 2 agences ne voient jamais les données de l'autre.
  • cabinet_admin peut switcher via param.

Out of Scope

  • Frontend TopBar selector → OP-011.
  • Cross-agence consolidation V2.
  • ContractMutationService côté ING (changements ING gérés par ING module).

Definition of Done

  • [ ] op.agency-scope.enabled=true activé en dev/staging/prod
  • [ ] AC-001..AC-004 passent
  • [ ] AgencyScopeFilter intercepte tous les repositories OP annotés (5 méthodes min)
  • [ ] Tous les audit events incluent agency_id
  • [ ] GET /agencies endpoint disponible
  • [ ] POST /campaigns refuse cabinet_admin sans agencyId
  • [ ] PATCH RBAC étendu agency_operator
  • [ ] Tenant isolation préservée
  • [ ] Tests intégration verts