Aller au contenu

Story OP-001: Backend cohort éligibilité — GET /api/operator/contracts?eligible=true

Module: operator-console Slice: Architecture §11 OP-001a + sous-set de §3.3 Contrats Side: [BACKEND] Version target: V0.0.0 Priority: 2 Depends on: OP-000 (ContractEligibilityQueryAdapter, RBAC filter, exception handler) Can develop concurrently with: OP-002, OP-015 Merge order: Any order after OP-000 ; avant OP-005 (frontend liste) Estimated complexity: S PRD User Stories: V0 envelope V0.0.0 (cohort simplifiée renewal_window_days) Wireframe: N/A — backend only


Objective

Exposer l'endpoint REST que le frontend (OP-005) et l'UI confirmation modale appellent pour afficher le compte de contrats éligibles AVANT déclencher la création + envoi d'une campagne. La logique d'éligibilité réelle vit dans ContractQueryService.findEligible (ING) — cette story ne fait que la projection REST scoped tenant.


Backend Scope

Entities

Aucune entité OP créée. Lit contracts (ING owner).

Migrations

Aucune (toutes les tables ont été créées en OP-000).

Service Layer

  • ContractEligibilityController (@RestController("/api/operator/contracts")) — méthode eligibleContracts(...).
  • Délègue à ContractEligibilityQueryAdapter.findEligible(tenantId, agencyId, windowDays) de OP-000.
  • windowDays lu depuis TenantConfigCache.getRenewalWindowDays(tenantId) (default 30). Pas de paramètre query — la fenêtre est settings-driven (FR-OP-01).
  • Pagination curseur (D6, 25/page par défaut). cursor query param opaque base64.

API Endpoints

Method Path Request Response Auth
GET /api/operator/contracts?eligible=true&cursor=&pageSize=25 { data: ContractSummary[], nextCursor, hasMore, totalEligibleCount } agency_operator, supervisor, cabinet_admin

ContractSummary JSON :

{
  "id": "uuid",
  "customerName": "KOUAKOU Aya",
  "phone": "+225 07 02 11 22",
  "productBranch": "Auto",
  "amount": 85000,
  "dueDate": "2026-05-29",
  "agencyId": "uuid"
}

Validation Rules

  • eligible doit être true (V0.0.0 ne supporte que cette valeur). Toute autre valeur → 400.
  • pageSize ∈ [1, 100], default 25.
  • cursor : base64 valide ou absent → sinon 400 OP_INVALID_CURSOR.

Multi-Tenant Considerations

  • tenant_id du JWT, scope obligatoire.
  • agency_id : si role=AGENCY_OPERATOR, ContractEligibilityQueryAdapter reçoit l'agencyId depuis le contexte (V0.0.0 : pas de filtrage agence actif — AgencyScopeFilter désactivé ; tous les contrats du tenant remontent). En V0.0.2 (OP-010), le filtrage s'active automatiquement.
  • Aucun lookup cross-tenant possible.

Audit & Logging

  • Pas d'audit event pour cette lecture (per V1 PRD §B.6, seuls les writes sont auditrés).
  • MDC enrichi : tenant_id, endpoint=GET /contracts.

Acceptance Criteria

AC-001: L'endpoint retourne les contrats éligibles dans la fenêtre `renewal_window_days`
Given un tenant avec renewal_window_days=30 et 12 contrats dont 8 dans [today, today+30]
And aucun de ces contrats n'est dans une campagne active (FR-OP-04)
When GET /api/operator/contracts?eligible=true est appelé
Then la réponse contient 8 entrées avec totalEligibleCount=8 et hasMore=false.

AC-002: Les contrats déjà actifs dans une campagne sont exclus (FR-OP-04)
Given 5 contrats éligibles
And 2 d'entre eux ont une campaign_contract en statut INCLUS d'une campagne BROUILLON
When GET /api/operator/contracts?eligible=true est appelé
Then la réponse contient les 3 contrats restants seulement.

AC-003: Isolation tenant
Given un tenant A avec 5 contrats éligibles et un tenant B avec 7
When un operator du tenant A appelle l'endpoint
Then il reçoit exactement les 5 contrats de A ; aucun de B (vérifié par ID).

Compliance Rules

  • PRD V0 §4 ligne OP : cohort simplifiée renewal_window_daysVÉRIFIÉ.
  • ARTCI minimisation : la réponse n'inclut pas de DOB (FR-OP-06 — DOB masqué en liste). VÉRIFIÉ.

Standards & Conventions

  • docs/standards/critical-rules.md — rules #1, #2 (XOF integer minor units).
  • docs/standards/api-guidelines.md — pagination curseur, error envelope.
  • docs/standards/database-guidelines.md — index (tenant_id, due_date) sur contracts (déclaré dans TENANT/ING).

Testing Requirements

Unit Tests

  • ContractEligibilityControllerTest : 400 si eligible=false ; 400 si cursor invalide ; pageSize bornes.

Integration Tests

  • EligibleContractsIT (AC-001, AC-002, AC-003) avec Postgres Testcontainer + Keycloak Testcontainer + un set ING contracts fixture.
  • Test pagination : 30 contrats éligibles → 2 pages de 25 + 5 ; cursor opaque round-trip.

What QA Will Validate

  • curl avec JWT operator → 200 + JSON conforme.
  • Performance : p99 < 500 ms pour 1000 contrats (index (tenant_id, due_date) couvre, idx_cc_tenant_contract_status couvre le NOT EXISTS).

Out of Scope

  • Création de campagne → OP-002.
  • Filtres avancés (branche, plage montant, plage date) → V1 (US-OP-03 ad hoc selector).
  • Recherche client → V1 (US-OP-10).
  • Cohort planifiée @Scheduled → V1 (FR-OP-01 / OP-011 arch).

Definition of Done

  • [ ] Endpoint GET /api/operator/contracts?eligible=true retourne 200 + JSON conforme
  • [ ] AC-001 / AC-002 / AC-003 passent en intégration
  • [ ] Tenant isolation prouvée (AC-003)
  • [ ] AgencyScopeFilter inactif (V0.0.0) — pas d'erreur si role=AGENCY_OPERATOR
  • [ ] p99 < 500 ms sur dataset 1000 contrats (test perf optionnel)
  • [ ] Pagination curseur idempotente (round-trip)
  • [ ] DOB jamais présent dans la réponse
  • [ ] Aucune string FR en hardcoded (l'endpoint est headless)
  • [ ] Unit + integration tests passent