Architecture - Cross-Module Progress Log¶
Living document. Each ARCHITECT session appends a module entry and updates the cross-module decision table at the top.
Read this BEFORE every new architecture session - do NOT re-decide locked patterns.
Cross-Module Decisions (LOCKED)¶
| # | Decision | Locked in | Rationale |
|---|---|---|---|
| D1 | Two logical DBs: platformDataSource (tenant directory, country profile, billing master) + tenantDataSource (business data). |
TENANT-arch section 2.1 | Per multi-datasource-patterns.md. Tenant directory cannot live in a tenant DB (chicken-and-egg). |
| D2 | Platform tables accessed via @Qualifier("platformJdbcClient") JdbcClient only - never CrudRepository. |
TENANT-arch section 4.3 | Spring Data JDBC silently routes CrudRepository to @Primary (tenant DB). |
| D3 | Tenant resolution: Keycloak JWT claim tenant_id (operator); signed-link decode (customer, owned by AUTH). |
blueprint section 3.2 + TENANT section 4.1 | Platform-admin sentinel context for cross-tenant queries; emits PLATFORM_ADMIN_QUERY audit. |
| D4 | Domain events: Spring ApplicationEventPublisher + @TransactionalEventListener(AFTER_COMMIT) + spring-modulith-events-jdbc outbox for cross-module reactions that must survive a crash. |
TENANT-arch section 1.3 | Single deployable, no broker on critical path. |
| D5 | API namespaces: /api/control/... for platform-admin (cross-tenant); /api/tenant/me/... for tenant-scoped self-service; /api/... for operator-scoped business modules. (API version travels in the X-API-Version header per D31 — no /v1 path segment; the namespace structure stands) |
TENANT-arch section 3.1 | Discoverability + role gating + audit clarity. |
| D6 | Cursor-based pagination on every list endpoint. Default 25, max 100. Cursor opaque base64 of (sort_key, id). |
TENANT-arch section 3.2 | api-guidelines.md rule. |
| D7 | Audit writes via outbox; failure to audit MUST NOT roll back the business action. | TENANT-arch section 7.3 | NFR-09 across modules. |
| D8 | BYO-DB credentials envelope-encrypted (KEK env / hosted KMS in prod; per-tenant DEK). Pattern reused by AUDIT for per-tenant pseudonymization salts. | TENANT-arch section 7.2 + AUDIT-arch section 7.3 | Defence-in-depth; no plaintext anywhere. |
| D9 | Flyway split: db/migration/platform/ and db/migration/tenant/. Tenant migrations applied to shared tenant DB on startup AND to BYO-DB during provisioning. AUDIT contributes only to platform/ (AUDIT-ADR-01). |
TENANT-arch section 9.1 + AUDIT-arch section 9.1 | Identical schema across V1 profiles. |
| D10 | Daily lifecycle / compliance jobs run at 02:00 Africa/Abidjan (UTC+0, no DST). |
TENANT-arch section 6.3 | Stable timezone for all CI cron jobs. |
| D11 | Frontend design system: custom CSS Modules + design tokens. No MUI/Tailwind/Shadcn. Locked in docs/standards/design-system.md. |
standards (pre-existing) | Dual-brand support, low-bandwidth, customization. |
| D12 | Audit data lives on the platform DB for every tenant, regardless of db_profile. BYO-DB tenants still have their audit rows centralized. V2 may revisit (TV-05). |
AUDIT-arch section 4.2 (AUDIT-ADR-01) | Operational reliability (no hostage to tenant-side outage); cross-tenant platform-admin queries need a single source; single regulator-facing export. |
| D13 | Audit event_publication outbox redelivery is idempotent via composite (producer_module, producer_outbox_id) unique partial index on audit_log. Replay safety is structural. |
AUDIT-arch section 2.3 (FR-06) | Crash-mid-deliver does not duplicate rows; spring-modulith library retries without coordination. |
| D14 | AUDIT_SCHEMA_MIGRATED is emitted by a Flyway Java callback running under the deploy-only papillon_audit_admin Postgres role. The runtime application role (papillon_app) structurally cannot write this event_type. |
AUDIT-arch section 9.2 (AUDIT-ADR-06) | AC-08.3: "the application path cannot write this event; only the deploy pipeline can" - enforced at the DB role level, not by policy. |
| D15 | Three Postgres roles for audit data: papillon_app (INSERT+SELECT on audit_log, no UPDATE/DELETE, v0.0.0); papillon_audit_reader (SELECT only, V1); papillon_audit_admin (ALL incl. DDL, deploy-only, v0.0.0). |
AUDIT-arch section 7.2 (AUDIT-ADR-07) | DB-level append-only is a Day-1 structural guarantee, not an application convention. The PRD V0 banner said "convention applicative in v0.0.0"; the architecture goes one step further. |
| D16 | correlation_id origin = OP at OP_CAMPAIGN_VALIDATED, propagated downstream through AUTH (signed-link issued) -> NOTIF (sent) -> CP (link opened) -> PAY (initiated/confirmed). Operator/admin standalone events do NOT carry one. Confirmed by NOTIF (NOTIF-ADR-07). |
AUDIT-arch section 1.5 (AUDIT-ADR-08) + NOTIF-arch Appendix A | Resolves PRD A.6 OQ-05 + OQ-X3 (NOTIF leg). |
| D17 | NOTIF cascade (v0.0.0) is SMS-first at T0 → WhatsApp at T+5d if no link click, NOT parallel send. Cancels on customer.session_opened Spring event. WA carries a freshly re-issued signed link via AUTH.SignedLinkService.reissueForCascade. |
NOTIF-arch section 0 + ADR-02 + ADR-03 | Founder decision 2026-05-21. Cost-positive (one channel per renewal in happy case), reduces cross-border WA noise (ARTCI gap #6). Overrides NOTIF PRD V0 envelope line 19 — ANALYST PRD rework owed (OQ-NOTIF-12). |
| D18 | NOTIF tables on platform DB for both SHARED and BYO profiles (TV-05; mirrors AUDIT D12). |
NOTIF-arch ADR-01 | Operational resilience: a BYO-DB outage must not stop ARTCI-required outbound notifications. |
| D19 | NOTIF dispatch substrate = @Async post-commit SMS submit (no polling for general work) + single scheduled task WhatsAppCascadeFirer (every 5 min, partial-index scan on AWAITING_CASCADE) + SmsSubmitRecovery startup job. |
NOTIF-arch ADR-04 + section 5 | No new infra in v0.0.0. v0.1.0 layers Redis token-bucket on the same substrate without rewrite. |
| D20 | Two frontend apps, not three: frontend/customer/ (assuré portal) + frontend/operator/ (broker console). Platform admin is a role-gated section within frontend/operator/ revealed when Keycloak JWT contains PLATFORM_ADMIN role. No separate frontend/platform/ app. |
chore/frontend-scaffold (PR #35) | Platform admin surface is small; sharing the operator shell avoids a third deploy target and a third design system instantiation. |
| D21 | Frontend stack (locked): Vite + React 19.2 + TypeScript + React Router v7 + CSS Modules. No framework beyond React. No Next.js, no Remix, no Astro. | chore/frontend-scaffold (PR #35) | Low-bandwidth constraint, simple static deploy on Coolify, no SSR needed for either portal. |
| D22 | Canonical design tokens in frontend/_shared/tokens.css. Each app copies this file into src/theme/tokens.css and adds a per-frontend overlay (theme-customer.css / theme-operator.css). No symlinks, no monorepo tooling, no shared npm package. |
chore/frontend-scaffold (PR #35) | Simplest sharing model for a small team; avoids Turborepo/pnpm workspaces overhead. If token drift becomes a problem, a pre-commit script can enforce parity. |
| D23 | Customer portal gzip budget baseline: 74.55 KB (React + React Router, zero other dependencies). Budget ceiling: 200 KB. Every new dependency for frontend/customer/ must be justified against this ceiling. Operator app has no hard ceiling (desktop network assumed). |
chore/frontend-scaffold (PR #35) | Architecture constraint: V1 PRD §30 low-bandwidth requirement. |
| D28 | CP customer XHR auth = HttpOnly cp_session cookie (SHA-256 hash stored on customer_sessions.session_token_hash), issued only after AUTH.SignedLinkService.verifyAndOpen. SameSite=Lax, Secure, 72 h Max-Age ceiling. CP-13 receipt re-download uses a separate cp_readonly cookie backed by cp_readonly_sessions (5-min sliding, IP+UA-hash bound, parent-link-TTL ceiling). |
CP-arch ADR-CP-01 + ADR-CP-04 | Server-stored opaque tokens; no JWT refresh dance; trivially revokable; mirrors main-session pattern post-AUTH-revoke. |
| D29 | CP session storage = Postgres only on customer_sessions row (selection UUID[], consent, in-flight tx). No Redis fast-path in V0/V1. Partial UNIQUE on inflight_transaction_id enforces ≤1 non-terminal tx per session (CP-NFR-15). |
CP-arch ADR-CP-02 | Negligible write rate at V0 scale; removes a coherence problem; one fewer failure mode. CP-TV-06 explicitly leaves this to ARCHITECT. |
| D30 | CP polling = TanStack Query v5 refetchInterval (5 s × 12 then 30 s) with refetchIntervalInBackground=false. Adds ~13 KB gzipped to the customer bundle (105 KB total landing path budget, well under 200 KB ceiling). |
CP-arch ADR-CP-03 | Battle-tested cadence + visibility handling; foundation reusable across V1 file-detail surfaces; visibility-aware to spare data/battery. |
| D31 | API versioning: Header-based (X-API-Version: <integer>) via Spring Boot 4 native ApiVersionConfigurer. Default to latest when header absent (V0/V1 = version 1). Deprecation via Deprecation + Sunset response headers (RFC 8594); removed versions return 410 Gone. Exception: webhook ingress URLs registered with external providers (PSP, FNE) retain URI versioning — documented carve-out for third-party contracts. React clients inject header via single apiClient.ts wrapper; raw fetch() to /api/... is a lint violation. |
customer-portal-arch §3.0 (this session) | REST principles (resource identity unchanged across versions); Spring Boot 4 native support (no custom RequestCondition); operationally clean for internal APIs. Webhook exception: external providers register callback URLs as third-party contracts, cannot reliably inject custom headers. |
| D24 | ING v0.0.0 = cron-only, no HTTP endpoint. Single Spring @Scheduled at 0 0 2 * * * Africa/Abidjan iterating active tenants sequentially. File source = platform MinIO bucket papillon-ingestion-inbox with per-tenant prefix /<tenant_id>/{inbox,processed,rejected,duplicate}/. UI upload + atelier deferred to v0.0.3. |
ingestion-arch §0 + ING-ADR-02/03 | V0 PRD §4 ING simplification. Reuses existing MinIO (no new infra). |
| D25 | ING v0.0.0 anomaly handling = all-or-nothing reject. Any blocking anomaly → entire file moved to /<tid>/rejected/<ts>__<filename> with .errors.json sidecar, ING_PARSE_FAILED audit, no commit. Operator feedback = the sidecar JSON until v0.0.3 ships the atelier. |
ingestion-arch §0 + ING-ADR-01 | No atelier in v0.0.0; matches V1 ING-R-014 all-or-nothing commit semantics. |
| D26 | ING audit correlation_id = import_id (batch scope), does not extend D16 renewal chain. OP mints a fresh correlation_id when consuming ImportCommitted. ING events occur upstream of any campaign — there is no renewal correlation to inherit. |
ingestion-arch §1.3 + ING-ADR-09 | Clarifies D16 boundary. |
| D27 | ING tables (ingestion_import, ingestion_import_row) on tenant DB (business data per D1). ING audit emissions land on platform DB per D12. v0.0.0 schema is forward-compatible: ingestion_staged_row table arrives additively v0.0.3; agency_id column on contract arrives v0.0.2 via TENANT-002. |
ingestion-arch §2 + ING-ADR-04/10 | D1 + D12 + AUDIT-ADR-03 pattern. |
| D32 | Spring Modulith business modules are Java packages within the single :backend Gradle subproject (the one JVM Spring Boot application), NOT separate Gradle subprojects each. One backend/build.gradle.kts, one src/main, one src/test; module boundaries enforced by package structure + ApplicationModules.verify(). Build/test gate = ./gradlew :backend:check (no per-business-module :<module>: Gradle task). Per-business-module Gradle subprojects rejected. |
java-spring-guidelines.md §6 + blueprint §1/§11 (this session) | Canonical Spring Modulith model; lighter to refactor boundaries pre-MVP; matches ingestion/tenant-admin/notifications arch docs (all use :backend:). Corrects the earlier :audit-log: / :customer-portal: subproject framing in audit-log-arch + customer-portal-arch (this session). NOTE follow-up: NOTIF-S0 still has a nested :backend:notif:test of the same defect family. |
Shared Entities (canonical owners)¶
| Entity | Owner module | DB | Notes |
|---|---|---|---|
tenant |
TENANT | Platform | Tenant directory. Read by every module via TenantConfigCache. |
country_profile |
TENANT | Platform | Seed-only V1 (CI). Read by PAY (provider list), BILL (e-invoicing), NOTIF, CP. |
agency |
TENANT | Tenant | Read by ING, OP, AUTH, CP for branding. |
tenant_settings |
TENANT | Tenant | Read by OP (renewal_window_days), NOTIF (default_channel, whatsapp_template_id), AUTH (min_auth_level). |
tenant_compliance_doc |
TENANT | Tenant | DPA, agreement, FNE registration. Metadata in DB, binary in MinIO. |
audit_log |
AUDIT | Platform | Append-only, tenant-scoped. PRD section B.5 schema frozen Day 1 (v0.0.0). No UPDATE/DELETE from app role. |
audit_tenant_salt |
AUDIT | Platform | 1:1 with tenant. Envelope-encrypted (DEK per tenant, KEK env / KMS). Consumed via PiiHasher named-interface. |
audit_export |
AUDIT | Platform | Export job tracking (status, MinIO object keys, SHA-256). Bundles materialized in MinIO bucket audit-exports. |
event_publication |
(spring-modulith-events-jdbc library) | Platform | Outbox table. AUDIT consumes via @ApplicationModuleListener. Not owned by any module. |
notification |
NOTIF | Platform | One row per dispatch request. Carries tenant_id, country_code (snapshot), correlation_id, signed-link refs, status. v0.0.0+. |
delivery_attempt |
NOTIF | Platform | 1..N per notification, one per channel (SMS, WA). UNIQUE (provider_id, provider_message_id) enforces webhook idempotency. v0.0.0+. |
suppression |
NOTIF | Platform | Per-tenant opt-out (channel-scoped). Phone-keyed (customer_id nullable). v0.0.0+. STOP detection + admin override. |
ingestion_import |
ING | Tenant | Header row per file picked up by the cron. UNIQUE (tenant_id, file_hash_sha256). Status FSM: PICKED_UP → COMMITTED / PARSE_FAILED / DUPLICATE. v0.0.0+. |
ingestion_import_row |
ING | Tenant | One row per committed line (trace). FK to customer + contract. v0.0.0+. |
ingestion_staged_row |
ING | Tenant | NOT created in v0.0.0. Schema arrives v0.0.3 with the atelier de correction (UI upload + per-row corrections). |
customer |
TENANT (schema) / ING (writer) | Tenant | ING writes source_customer_id (dedup key), nom, prenoms, telephone (E.164), email, date_naissance. UNIQUE (tenant_id, source_customer_id). |
contract |
TENANT (schema) / ING (writer) | Tenant | ING writes source_contract_id (dedup key with tenant_id + customer_id), date_echeance, montant_du, numero_police, etc. UNIQUE (tenant_id, customer_id, source_contract_id). agency_id column added by TENANT-002 v0.0.2. |
customer_sessions |
CP | Tenant | One row per signed-link visit. Holds selection UUID[], consent, in-flight tx ref, session cookie SHA-256 hash, correlation_id (from signed_link), country_code. Partial UNIQUE on inflight_transaction_id (CP-NFR-15). v0.0.0+. |
payment_attempts (CP projection) |
CP | Tenant | Local projection of PAY's transaction for CP screen routing without round-trips. Updated by PaymentConfirmed / PaymentFailed / PaymentExpiredPending listeners. v0.0.1+. |
cp_readonly_sessions |
CP | Tenant | Ephemeral 5-min sliding cookie-bound row for receipt re-download after AUTH revoked link (reason=ALL_PAID). Bounded by parent link 72 h TTL. V1 (CP-006). |
mentions_legales_versions |
CP | Tenant | Read-only projection from TENANT; consumed at consent-tick time per CP-OQ-07 default. v0.0.0+. |
Event Contracts (canonical registry)¶
Inbound to AUDIT (consumed)¶
| Event | Owner | Payload | Available |
|---|---|---|---|
TenantOnboarded |
TENANT | {tenantId, countryCode, plan, primaryContact, defaultAgencyId, byoDb, occurredAt} |
v0.0.0 (TENANT-001) |
TenantSuspended |
TENANT | {tenantId, reasonCode, suspendedAt} |
v0.1.0 |
TenantReactivated |
TENANT | {tenantId, reactivatedAt} |
v0.1.0 |
TenantSoftDeleted |
TENANT | {tenantId, deletedAt} |
V1 |
AgencyDisabled |
TENANT | {tenantId, agencyId, disabledAt} |
v0.0.2 |
TenantSettingsChanged |
TENANT | {tenantId, changedKeys[], occurredAt} |
v0.1.0 |
| (all V1 PRD section 18 audit event types, via outbox) | every producer | per AUDIT-arch section B.3 decision table | Phased per producer (see AUDIT V0 banner) |
NOTIF_SENDING, NOTIF_SENT, NOTIF_FAILURE, NOTIF_SUPPRESSED_OPT_OUT |
NOTIF | per audit-log §B.3 NOTIF row, with PiiHasher applied to phone + name |
v0.0.0 (AUDIT-002 ships NOTIF_SUPPRESSED_OPT_OUT catalog entry; others land at v0.1.0: NOTIF_WEBHOOK_REJECTED, NOTIF_OPTED_OUT, NOTIF_CAP_EXCEEDED, NOTIF_KILL_SWITCH_*, NOTIF_MANUAL_RETRY, NOTIF_SUPPRESSION_REMOVED) |
ING_UPLOAD_RECEIVED, ING_PARSE_FAILED, ING_DUPLICATE_FILE, ING_COMMITTED, ING_CUSTOMER_CREATED, ING_CUSTOMER_UPDATED, ING_CONTRACT_CREATED, ING_CONTRACT_UPDATED |
ING | per ingestion-arch §7.4; PiiHasher applied to name + phone; DOB never plaintext (only year in V1, omitted in v0.0.0) |
v0.0.0 (AUDIT catalog must admit these before ING-001 dev-start — OQ-X10) |
Outbound from AUDIT (emitted)¶
| Event | Trigger | Consumers | Available |
|---|---|---|---|
AUDIT_INGEST_DEGRADED |
FR-10 threshold breach | ops (alert) | v0.1.0 |
AUDIT_RECONCILED |
Drain queue back to 0 | ops (auto-resolve) | v0.1.0 |
AUDIT_EXPORT_COMPLETED |
Export worker finishes | self-audit + admin | v0.1.0 |
AUDIT_QUERY_EXECUTED |
Query API hit (deduplicated 60s, FR-09) | self-audit | V1 |
AUDIT_SCHEMA_MIGRATED |
Flyway callback fires under papillon_audit_admin |
self-audit | v0.0.0 |
TENANT outbound (unchanged, retained for reference)¶
| Event | Consumers |
|---|---|
TenantOnboarded |
AUTH, BILL, NOTIF, AUDIT |
TenantSuspended |
AUTH, CP, PAY, OP, AUDIT |
TenantReactivated |
AUTH, CP, PAY, OP, AUDIT |
TenantSoftDeleted |
ING, CP (purge), AUDIT |
AgencyDisabled |
OP, ING, AUTH, AUDIT |
TenantSettingsChanged |
OP, AUTH, NOTIF, AUDIT |
API Namespaces (claimed)¶
Note (D31): API version travels in the
X-API-Versionrequest header — not in the URL path. The namespaces below are the canonical path shapes (no/v1segment). Per-module arch docs have been aligned to the header-versioned shape (no/v1path segment); the registry below is the source of truth. Webhook ingress URLs registered with external providers (PSP, FNE, SMS gateways) are the documented carve-out and DO retain URI versioning — they are marked explicitly below.
| Namespace | Module | Available |
|---|---|---|
/api/control/tenants/... |
TENANT | v0.0.0+ |
/api/tenant/me/... |
TENANT | v0.0.0+ |
/api/control/tenants/{id}/compliance-docs/... |
TENANT | V1 |
/api/control/audit/exports, /exports/{id} |
AUDIT | v0.1.0 (CSV stub) / V1 (JSONL+manifest) |
/api/control/audit/query |
AUDIT | V1 |
/api/control/audit/cases/{type}/{key} |
AUDIT | V1 |
/api/notifications/dispatch |
NOTIF | v0.0.0 |
/api/notifications/{id} |
NOTIF | v0.0.0 |
/api/v1/notifications/webhooks/{providerId}/{status\|inbound} (webhook carve-out — URI-versioned) |
NOTIF | v0.0.0 |
/api/portail/{token} (entry) + /api/portail/session/... (cookie-bound XHRs) |
CP | v0.0.0 |
/api/portail/{token}/recus (CP-13 readonly re-download) |
CP | V1 |
Reserved for upcoming modules (do not collide):
- /api/auth/..., /api/control/auth/... -> AUTH
- /api/control/billing/... -> BILL
- /api/notifications/admin/... -> NOTIF (v0.1.0 — kill-switch + suppression admin)
- /api/notifications/customers/{id}/timeline -> NOTIF (V1 — operator timeline)
- /api/ingestion/... -> ING (claimed but unused in v0.0.0 — cron only; first endpoints land v0.0.3 with UI upload + atelier)
- /api/operator/... -> OP
- /api/payments/... -> PAY
- PSP webhook ingress URLs (PAY) and FNE webhook ingress URLs (BILL) will be registered under /api/v1/... as additional webhook carve-outs when those modules land.
Shared Beans / Named Interfaces (Spring Modulith)¶
| Bean | Owner | Consumers | Available |
|---|---|---|---|
PiiHasher |
AUDIT | NOTIF, CP, PAY, AUTH, ING, OP, TENANT | v0.0.0 |
AuditEventType enum + AuditEventBuilder |
AUDIT | every producer | v0.0.0 |
SignedLinkService |
AUTH | CP, OP, ING | v0.0.0 |
TenantConfigCache |
TENANT | every module | v0.0.0 |
SignedLinkService.reissueForCascade(originalLinkId, ttl) |
AUTH | NOTIF (cascade firer) | v0.0.0 (NEW — flagged in NOTIF-arch; AUTH must add a slice) |
customer.session_opened Spring application event |
CP | NOTIF (cascade cancel listener) | v0.0.0 (confirmed — CP-arch §1.2 + CP-001 publishes on first authenticated contract-list render; payload {tenant_id, customer_id, signed_link_id, opened_at, correlation_id}) |
CustomerSessionService (in-process Spring named interface for CP session create/lookup/revoke) |
CP | (internal to CP; AUTH cooperates via SignedLinkService) |
v0.0.0 |
NotificationDispatchService (in-process Spring named interface) |
NOTIF | OP, PAY (V1), AUTH (V1) | v0.0.0 |
Open Questions (cross-module)¶
| ID | Question | Origin | Owner |
|---|---|---|---|
| OQ-A1 | Retain tenant_provisioning_run indefinitely vs prune to audit at 90d? |
TENANT-arch section 16 | Founder |
| OQ-A2 | BYO-DB: ship Flyway DDL pack to tenant DBA, or apply blind under contract clause? | TENANT-arch section 16 | Founder + Legal |
| OQ-A3 | papillon-platform-admin realm role vs separate papillon-admin realm? |
TENANT-arch section 16 | Founder |
| OQ-X1 | Will AUTH consume TenantOnboarded synchronously (block ACTIVE) or asynchronously (initial admin user appears later)? |
TENANT <-> AUTH | RESOLVED: AUTH consumes async; brief gap window documented in identity-arch. |
| OQ-X2 | Will BILL need a country_profile.fne_endpoint field, or hard-code the CI FNE URL? (V1 = CI only.) |
TENANT <-> BILL | BILL ARCHITECT |
| OQ-X3 | correlation_id origin = OP at OP_CAMPAIGN_VALIDATED, propagated through AUTH -> NOTIF -> CP -> PAY. Confirm in each downstream module's arch session. |
AUDIT <-> OP/NOTIF/CP/PAY/AUTH | NOTIF + CP + OP confirmed (NOTIF-ADR-07, customer-portal-arch §1.3, OP-ADR-05): correlation_id = campaign_contract.id; minted at row-insert by OP; propagated through AUTH + NOTIF + CP. Still pending: PAY/AUTH ARCHITECTS. |
| OQ-X6 | AUTH must expose SignedLinkService.reissueForCascade(originalLinkId, ttl) for NOTIF's T+5d WA cascade. AUTH session needs a new slice. |
NOTIF <-> AUTH | AUTH ARCHITECT (next session) |
| OQ-X7 | CP must publish customer.session_opened Spring application event on first signed-link-authenticated portal view, with {tenant_id, customer_id, signed_link_id, opened_at}. CP session needs a slice. |
NOTIF <-> CP | RESOLVED: customer-portal-arch §1.2 + CP-001 publishes the event in v0.0.0; payload extended with correlation_id. |
| OQ-X8 | NOTIF PRD V0 envelope line 19 ("envoi parallèle SMS+WA") is overridden by founder decision to SMS-first → WA at T+5d. ANALYST PRD rework owed. | NOTIF <-> ANALYST | ANALYST (next NOTIF PRD pass) |
| OQ-X9 | AUDIT V1 catalog must add NOTIF_SUPPRESSED_OPT_OUT to the v0.0.0 slice (folded into AUDIT-002). Other NOTIF self-events (NOTIF_WEBHOOK_REJECTED, NOTIF_OPTED_OUT, NOTIF_CAP_EXCEEDED, NOTIF_KILL_SWITCH_*, NOTIF_MANUAL_RETRY, NOTIF_SUPPRESSION_REMOVED) land at v0.1.0. |
NOTIF <-> AUDIT | AUDIT ANALYST (next AUDIT rework — blocks NOTIF-001 dev-start) |
| OQ-X10 | AUDIT catalog must admit ING event types ING_UPLOAD_RECEIVED, ING_PARSE_FAILED, ING_DUPLICATE_FILE, ING_COMMITTED, ING_CUSTOMER_CREATED, ING_CUSTOMER_UPDATED, ING_CONTRACT_CREATED, ING_CONTRACT_UPDATED before ING-001 dev-start. |
ING <-> AUDIT | AUDIT ARCHITECT/ANALYST (catalog rework — blocks ING-001 dev-start) |
| OQ-X11 | TENANT S0 must provision MinIO bucket papillon-ingestion-inbox and issue per-tenant scoped credentials (prefix-bound RW on /<tid>/inbox/, RO on /<tid>/{processed,rejected,duplicate}/). Currently TENANT provisions MinIO for compliance docs only. |
ING <-> TENANT | TENANT ARCHITECT (next session) |
| OQ-X12 | Ownership + migration order for customer.source_customer_id / contract.source_contract_id / contract.last_import_id + their UNIQUE indexes. ING needs them but does not own the base schema. Coordinate Flyway versioning between TENANT-001 (base tables) and ING-001 (additive ALTER). |
ING <-> TENANT | TENANT ARCHITECT (next session) |
| OQ-X13 | OP must declare a (potentially no-op in v0.0.0) @ApplicationModuleListener for ImportCommitted {importId, tenantId, rowsCreated, rowsUpdated, rowsSkipped, occurredAt}. Useful for cohort cache invalidation when OP UI lands v0.0.1. |
ING <-> OP | RESOLVED: no-op listener delivered in OP-S0 (docs/architecture/operator-console/part2-integrations-build-ops.md §16 Tier 0). |
| OQ-X14 | AUDIT catalog (AuditEventType enum) must admit the 14 OP event types (OP_CAMPAIGN_VALIDATED, CAMPAIGN_CANCELLED, CONTRACT_EXCLUDED, CONTRACT_ADDED_TO_CAMPAIGN, FIELD_CORRECTED, HORS_PLATEFORME_MARKED, NOTIFICATION_RESENT, ANOMALY_RESOLVED, OP_USER_CREATED, OP_USER_ROLE_CHANGED, OP_USER_DEACTIVATED, SETTINGS_CHANGED, DATA_EXPORTED, IMPORT_TRIGGERED) before OP-001 dev-start. Same pattern as OQ-X9 (NOTIF) and OQ-X10 (ING). |
OP <-> AUDIT | AUDIT ARCHITECT/ANALYST (catalog rework — blocks OP-001 dev-start) |
| OQ-X4 | AUDIT query API filter semantics on acting_on_tenant_id: response includes rows where tenant_id = :scope OR acting_on_tenant_id = :scope. Confirm at V1 implementation time. |
AUDIT internal | ARCHITECT review at V1 (AUDIT-004 story) |
| OQ-X5 | Defensive raw-PII scan at AUDIT INSERT boundary (FR-03 alinea 2): V1 only (AUDIT-008). Confirm pilot acceptance of v0.0.0-v0.1.0 lacking this backstop. | AUDIT internal | Founder |
Carried PRD-level OQs:
- TENANT: OQ-01..OQ-06 in docs/prd/tenant-admin-prd.md section A.6.
- AUTH: AUTH-OQ-01..06, AUTH-OQ-09 in docs/architecture/identity-arch.md Appendix B.
- AUDIT: A.6 OQ-01 (BILL fiscal events), OQ-02 (erasure-right policy), OQ-03 (file-level vs per-field on manual modification), OQ-04 (alert routing recipient), OQ-06 (DB-level read auditing) in docs/prd/audit-log-prd.md.
- OP: OQ-OP-01..05 in docs/architecture/operator-console/part2-integrations-build-ops.md Appendix B.
Module Entries¶
TENANT (tenant-admin) - V1 (V0 deltas at section 0)¶
- Architecture doc:
docs/architecture/tenant-admin-arch.md - PRD:
docs/prd/tenant-admin-prd.md - Status: Awaiting founder approval (workflow stage 4).
- Vertical slices: 11 slices (S0-S11) + V0 deltas (TENANT-001 v0.0.0, TENANT-002 v0.0.2, TENANT-003 v0.0.4). S3 marked split candidate (3 sub-slices proposed).
- External deps introduced: Postgres x2 logical DBs, Keycloak realm, Redis, MinIO bucket - all in S0.
- Cross-module impacts: 6 outbound events (see registry); Keycloak admin client port + MinIO object store port shared with later modules.
- V2 forward-compat checks:
country_codeNOT NULL no-default;group_idUUID NULL present; no UNIQUE onlegal_name; partial UK onncc WHERE status<>'DELETED'.
AUTH (identity) - V1 (V0 deltas at section 0)¶
- Architecture doc:
docs/architecture/identity-arch.md - PRD:
docs/prd/identity-prd.md - Status: Draft - awaiting founder approval (workflow stage 4).
- Vertical slices: 8 slices (S0-S7) + V0 deltas. S1, S3, S6 marked split candidates.
- External deps introduced: Keycloak realm config (on top of TENANT S0's docker-compose).
- Cross-module impacts: consumes 6 TENANT events +
AllContractsPaidfrom PAY; exposesSignedLinkService,ChallengeVerificationService,CustomerSessionServiceto CP via Spring boundary; exposesAuthProvisioningService.retriggerCabinetAdminInvitationto TENANT. - Locked decisions (AUTH-specific):
- All AUTH tables on platform DB (incl.
signed_links,customer_auth_attempts) - ADR-AUTH-01. - Signed-link token = 32-byte SecureRandom -> Base64url; SHA-256 stored; no HMAC - ADR-AUTH-02.
- Attempt counters Postgres-only + optimistic concurrency (
versioncol) - ADR-AUTH-03. - Keycloak Direct Grant (ROPC, confidential client) for operator login - ADR-AUTH-04.
- Keycloak access token TTL = 5 minutes; remember-device is application-layer (NOT Keycloak's keep-me-logged-in).
challenge_hmacstored onsigned_links(computed by ING/OP at issuance; AUTH never holds raw DOB/RCCM)./api/portail/{token}/...URL shape owned by CP; AUTH exposes in-process Spring module APIs.- OQ-X1 resolved: AUTH consumes
TenantOnboardedasynchronously; brief gap window documented. - Open OQs carried: AUTH-OQ-01..06, AUTH-OQ-09 (see arch doc Appendix B).
- Feature flags:
feature.whatsapp.operator-mfa.enabled=false[LEGAL-REVIEW] until ARTCI authorization (AUTH-OQ-04).
AUDIT (audit-log) - V1 (V0 deltas at section 0)¶
- Architecture doc:
docs/architecture/audit-log-arch.md - PRD:
docs/prd/audit-log-prd.md - Background KB:
docs/kb/audit-salt-storage-options.md(per-tenant salt store decision). - Status: Awaiting founder approval (workflow stage 4).
- Vertical slices V0: AUDIT-S0 (Tier 0: dev env + testcontainers), AUDIT-001 (v0.0.0 schema + outbox + PiiHasher + Flyway callback), AUDIT-002 (v0.0.1 real emission on PAY + NOTIF), AUDIT-003 (v0.1.0 export CSV stub + metrics).
- Vertical slices V1: AUDIT-004 (query API), AUDIT-005 (by-case lookup), AUDIT-006 (full JSONL+manifest export), AUDIT-007 (papillon_audit_reader role), AUDIT-008 (defensive PII scan), AUDIT-009 (real ingest-degradation alerting).
- External deps introduced: MinIO bucket
audit-exports; new Postgres rolespapillon_audit_admin(v0.0.0) andpapillon_audit_reader(V1). No new container. - Cross-module impacts:
- Consumes
TenantOnboarded(provisions salt row). - Consumes ALL V1 PRD section 18 events from every producer module via outbox.
- Exposes
PiiHashernamed-interface bean +AuditEventTypeenum +AuditEventBuilderto every producer. - Emits 5 self-events (
AUDIT_INGEST_DEGRADED,AUDIT_RECONCILED,AUDIT_EXPORT_COMPLETED,AUDIT_QUERY_EXECUTED,AUDIT_SCHEMA_MIGRATED). - Locked decisions (AUDIT-specific):
- AUDIT-ADR-01: audit data on platform DB regardless of tenant
db_profile(TV-05; V2 may revisit). -> D12 - AUDIT-ADR-02: producer-to-AUDIT via spring-modulith-events-jdbc outbox (D4 + D7).
- AUDIT-ADR-03: full V1 (PRD section B.5) schema frozen Day 1 (v0.0.0). No
ALTER TABLE audit_logbetween v0.0.0 and v0.1.0. - AUDIT-ADR-04: per-tenant salt in AUDIT-owned
audit_tenant_salttable, envelope-encrypted (D8 pattern). KB rationale indocs/kb/audit-salt-storage-options.md. - AUDIT-ADR-05: export bundles in MinIO bucket
audit-exports, streaming digest, presigned URL (15 min TTL). - AUDIT-ADR-06:
AUDIT_SCHEMA_MIGRATEDemitted by Flyway Java callback under deploy-onlypapillon_audit_adminrole. -> D14 - AUDIT-ADR-07: three Postgres roles (papillon_app INSERT+SELECT; papillon_audit_reader SELECT V1; papillon_audit_admin DDL deploy-only). -> D15
- AUDIT-ADR-08:
correlation_idorigin = OP at OP_CAMPAIGN_VALIDATED. -> D16 (pending OQ-X3 confirmation downstream). - Open OQs raised: OQ-X3, OQ-X4, OQ-X5 (see cross-module table above).
- PRD OQs carried: A.6 OQ-01..04, OQ-06 (OQ-05 resolved via AUDIT-ADR-08).
- Honest-scope notes (PRD AC-05.4): V1 does NOT defend against malicious DBA, compromised infrastructure operator, or KEK exfiltration. V2 introduces hash-chain tamper-evidence + HSM-backed key custody.
NOTIF (notifications) - v0.0.0¶
- Architecture doc:
docs/architecture/notifications-arch.md - PRD:
docs/prd/notifications-prd.md(V0 envelope §"V0 Envelope Scope" + V1 §§A/B/C as forward-compat reference) - Status: Awaiting founder approval (workflow stage 4). Document covers v0.0.0 only; v0.0.1 → V1 features designed-for but not architected in detail.
- Vertical slices v0.0.0: NOTIF-S0 (Tier 0: dev env + WireMock + testcontainers), NOTIF-001 (dispatch + SMS — split candidate into 4 sub-slices), NOTIF-002 (inbound STOP), NOTIF-003 (WhatsApp cascade at T+5d), NOTIF-004 (
SmsSubmitRecoverycrash-recovery startup job). - External deps introduced: WireMock service in docker-compose (test-only); SMS provider HTTPS endpoint (LetsTalk or Hub2 — OQ-NOTIF-01); WhatsApp Cloud API endpoint (Meta direct or via BSP — OQ-NOTIF-01). No new persistent infra beyond what TENANT S0 + AUDIT S0 already provision.
- Cross-module impacts (NEW from this session):
- AUDIT (blocker): catalog must accept
NOTIF_SUPPRESSED_OPT_OUTbefore NOTIF-001 dev-start (OQ-X9, folded into AUDIT-002). - AUTH (blocker for NOTIF-003): add
SignedLinkService.reissueForCascade(originalLinkId, ttl)Spring named interface (OQ-X6). Re-issued links carrylineage_of=<original>and samecorrelation_idper D16 chain. - CP (blocker for NOTIF-003): publish
customer.session_opened {tenant_id, customer_id, signed_link_id, opened_at}Spring application event (OQ-X7). - TENANT (closure): country_profile must seed
notif_provider_sms,notif_provider_wa,sms_sender_id_defaultforCI. Listed in TENANT migration path; flagged for TENANT ARCHITECT confirmation. - ANALYST (PRD rework): NOTIF PRD V0 envelope line 19 ("envoi parallèle") overridden by SMS-first cascade decision (OQ-X8).
- OP: subscribes to
notification.dispatched/sent/failed/cascade_cancelledSpring events for the file timeline (UI in V1; events emitted v0.0.0). - Locked decisions (NOTIF-specific):
- NOTIF-ADR-01: NOTIF tables on platform DB for both
SHAREDandBYOprofiles. -> D18 - NOTIF-ADR-02: cascade is SMS-first → WA at T+5d. -> D17
- NOTIF-ADR-03: WA carries a re-issued signed link (AUTH
reissueForCascade). - NOTIF-ADR-04: dispatch substrate =
@Asyncpost-commit +WhatsAppCascadeFirerscheduled task (no continuous polling) +SmsSubmitRecoverystartup job. -> D19 - NOTIF-ADR-05:
country_profileprovider lookup wired Day 1;CIseeded only. - NOTIF-ADR-06:
NOTIF_WEBHOOK_REJECTEDis log-only in v0.0.0 (audit catalog admits it at v0.1.0). - NOTIF-ADR-07:
correlation_idpropagates through MDC + async + scheduled tasks; never minted by NOTIF. Confirms D16 chain. - NOTIF-ADR-08: inbound STOP with no recent notification for the phone → log + drop, no suppression row (avoids cross-tenant leak).
- OQ-X3 resolved (NOTIF leg): confirmed D16 chain.
- OQs raised: OQ-X6, OQ-X7, OQ-X8, OQ-X9 (see cross-module table).
- PRD OQs carried: OQ-NOTIF-01 (provider choice — blocks NOTIF-001 dev-start), OQ-NOTIF-02 (ARTCI WA filing — feature-flagged), OQ-NOTIF-03 (mandatory-clause in body — non-blocking), OQ-NOTIF-04 (resolved for v0.0.0 scope, remaining 7 self-events land v0.1.0).
- Feature flags:
FEATURE_NOTIF_WHATSAPP_CASCADE_ENABLED(default true; flip to false if ARTCI B5+B10 filing pending — keeps SMS-only path live). - Honest-scope notes: v0.0.0 has NO transient-error retry, NO webhook-loss reconciliation polling, NO operator UI, NO manual retry. A stuck
SMS_SENTrow without delivery webhook stays stuck until V1 NOTIF-US-06 lands; the cascade still fires at T+5d if nocustomer_clicked_at. ARTCI proof obligation is satisfied byNOTIF_SENTaudit (proof of submission), not delivery.
ING (ingestion) - v0.0.0¶
- Architecture doc:
docs/architecture/ingestion-arch.md - PRD:
docs/prd/ingestion-prd.md(V0 envelope §"V0 Envelope Scope" + V1 §§A/B/C as forward-compat reference) - Status: Awaiting founder approval (workflow stage 4). Document covers v0.0.0 only; v0.0.1 → V1 features (UI upload, atelier de correction, full validation set, history UI, template DL, multi-agency code_agence) designed-for but not architected in detail.
- Vertical slices v0.0.0: ING-S0 (Tier 0: dev env + MinIO bucket layout + Flyway scaffolding + TestContainers fixtures), ING-001 (cron + pickup + parse + validate + commit + audit + reject — split candidate into 5 sub-slices a..e).
- External deps introduced: none new. Reuses MinIO (TENANT S0), Postgres x2 (TENANT S0), spring-modulith outbox (AUDIT S0).
- Cross-module impacts (NEW from this session):
- AUDIT (blocker for ING-001 dev-start): catalog must admit 8 new
ING_*event types (OQ-X10). - TENANT (blocker for ING-001 dev-start): provision MinIO bucket
papillon-ingestion-inbox+ per-tenant scoped credentials + per-tenant prefix layout (OQ-X11); confirm Flyway migration ordering for additive ALTER oncustomer/contractfor ING dedup columns (OQ-X12). - OP: declare listener for
ImportCommitted(no-op in v0.0.0; used v0.0.1+ for cohort cache invalidation) (OQ-X13). - AUTH: none. ING v0.0.0 cron runs as
SYSTEM, no JWT, noSignedLinkServiceuse. - Locked decisions (ING-specific):
- ING-ADR-01: v0.0.0 = all-or-nothing reject; no atelier. -> D25
- ING-ADR-02: file source = platform MinIO
papillon-ingestion-inboxper-tenant prefix. -> D24 - ING-ADR-03: cron = single
@Schedulediterating active tenants. -> D24 - ING-ADR-04: ING tables on tenant DB; audit on platform DB. -> D27 (reaffirms D1 + D12)
- ING-ADR-05: all-or-nothing single-transaction commit (V1 ING-R-014 semantics).
- ING-ADR-06: idempotency via SHA-256 dedup + per-file FSM; no extra outbox/queue beyond spring-modulith.
- ING-ADR-07: XLSX parse via Apache POI SAX; CSV via Jackson CSV streaming.
- ING-ADR-08: production gate via
feature.artci.phone-auth.granted(default false on prod profile, ignored elsewhere). - ING-ADR-09:
correlation_idfor ING audit =import_id(batch scope); does not extend D16 renewal chain. -> D26 - ING-ADR-10: v0.0.0 schema forward-compatible (
ingestion_staged_rowtable additive in v0.0.3;agency_idcolumn oncontractadditive in v0.0.2). -> D27 - OQ-X3 status: N/A for ING — ING events upstream of campaign chain; ING does not propagate the OP-originated
correlation_id. - OQs raised: OQ-X10 (AUDIT catalog), OQ-X11 (TENANT MinIO bucket), OQ-X12 (TENANT migration ordering), OQ-X13 (OP listener).
- PRD OQs carried: A.6 #1 (echeance dépassée — v0.0.0 keeps required + parseable only, skips >30d-past rule), #2 (branche referential — v0.0.0 makes
brancheoptional), #3 (notification channel — N/A, no notifications in ING), #4 (row delete in atelier — N/A, no atelier in v0.0.0). - Feature flags:
feature.artci.phone-auth.granted=false(default; BLOQUANT V0/V1 production per V0 PRD §6.4). Pre-prod ingestion authorized.[LEGAL-REVIEW]startup warning emitted while flag is false. - Honest-scope notes: v0.0.0 has NO upload UI, NO atelier de correction, NO history UI, NO template download, NO operator feedback channel except the
.errors.jsonsidecar in/rejected/. Any blocking anomaly on any row rejects the entire file. Operator notification of rejection is out of band (operator must check the MinIO/rejected/prefix). v0.0.1 brings the badge; v0.0.3 brings the full atelier.date_naissanceandbrancheare optional in v0.0.0 validation (will become required again v0.0.2 per V0 PRD §4 ING "v0.0.2 reintroduit règles V1").
CP (customer-portal) - v0.0.0 → V1¶
- Architecture doc:
docs/architecture/customer-portal-arch.md - PRD:
docs/prd/customer-portal-prd.md(V0 envelope §0 + V1 §§A/B/C) - Status: Awaiting founder approval (workflow stage 4). Document covers v0.0.0 → V1 with explicit sub-version deltas at §0.
- Vertical slices: CP-S0 (Tier 0: dev env + WireMock for PAY sandbox), CP-001 (v0.0.0 landing + list + cookie +
customer.session_openedemission), CP-002 (v0.0.1 payment + return + reçu — split candidate into 002a/002b/002c), CP-003 (v0.0.2 polling + phone-last-4 + per-contract toggle), CP-004 (v0.0.3 lazy-load + bundle budget gate), CP-005 (v0.1.0 TTL + attempt counter + rate-limit), CP-006 (V1 hardening: lien remplacé + déjà réglé CP-13 + mentions-légales page). - External deps introduced: WireMock service in
docker-compose.yml(test-only profile) for PAY sandbox. No new persistent infra (reuses TENANT S0 Postgres + Keycloak + MinIO; Redis present but unused by CP per D29). - Cross-module impacts:
- NOTIF: CP-001 publishes
customer.session_openedSpring event (closes OQ-X7). - AUTH: CP relies on
SignedLinkService.verifyAndOpen(token)(existing v0.0.0 named iface). - PAY: CP-002+ consumes
PaymentClient(initiate, status, receipts) and listens toPaymentConfirmed/PaymentFailed/PaymentExpiredPending. PAY ARCHITECT to confirm event payloads at PAY-arch session. - TENANT: CP reads
TenantConfigCachefor broker identity surface; invalidates onTenantSettingsChanged. - ING: CP reads-only via
ContractRepository.findEligibleForFile. - AUDIT: 17 event types emitted via outbox (PRD §B.3). All carry
correlation_idfrom signed_link (closes OQ-X3 CP leg). - OP: indirect — OP queries AUDIT for CP timeline events (no API surface from CP).
- Locked decisions (CP-specific):
- ADR-CP-01: HttpOnly
cp_sessioncookie + server-stored SHA-256 hash oncustomer_sessions.session_token_hash(no JWT, no token-replay on every XHR). → D28. - ADR-CP-02: Selection + consent + in-flight tx on a single
customer_sessionsrow in Postgres; no Redis fast-path in V0/V1. → D29. - ADR-CP-03: Polling via TanStack Query v5
refetchInterval(5 s × 12 then 30 s,refetchIntervalInBackground=false). +13 KB gzip on customer bundle (landing path ~105 KB, under 200 KB ceiling per D23). → D30. - ADR-CP-04: CP-13 read-only re-download via
cp_readonly_sessionsrow + separate HttpOnlycp_readonlycookie, 5-min sliding, IP+UA-hash bound, parent-link-TTL ceiling. → D28. - ADR-CP-05: Schema frozen v0.0.0 → V1 (no
ALTER TABLEbetween sub-versions); unused columns nullable. Mirrors AUDIT-ADR-03. - OQ-X3 (CP leg) RESOLVED:
correlation_idread from signed_link → stamped oncustomer_sessions.correlation_id→ propagated to PAY initiation requests + every CP audit event. - OQ-X7 RESOLVED:
customer.session_openedevent emission spec'd in §1.2 + slice CP-001. - OQs raised: none (delegates CP-OQ-03 receipt retention to PAY ARCHITECT and CP-OQ-07 content-version timing decided as latest-at-tick).
- PRD OQs carried: CP-OQ-01 (CIMA mandatory fields ⇒ lawyer), CP-OQ-02 (probative value of checkbox consent ⇒ lawyer), CP-OQ-05 (anonymous mandataire receipt acceptability ⇒ lawyer), CP-OQ-06 (refund/dispute UI ⇒ DESIGNER, default operational-only).
- Feature flags: none CP-side in V0; bundle-budget enforcement is a CI gate, not a runtime flag.
- Honest-scope notes: v0.0.0 has NO payment, NO polling, NO link TTL enforcement, NO challenge replay (AUTH still ships AUTH-CP-02/03 DOB/RCCM challenges at V1). v0.0.1 brings payment + reconciliation. v0.0.2 adds polling + per-contract toggle (replaces all-or-none from v0.0.0). v0.1.0 brings TTL + rate-limit hardening. CP-13 receipt re-download ships V1 (CP-006). Refund/dispute flow remains "call the agency" through V1.
OP (operator-console) - V1 (V0 envelope active)¶
- Architecture doc:
docs/architecture/operator-console/README.md(split into part 1 — domain & API and part 2 — integrations, build & ops) - PRD:
docs/prd/operator-console-prd.md - Status: Draft — awaiting founder approval (workflow stage 4). Document covers full V1 spec; V0 sub-versions OP-001..OP-005 designed-for in detail. 2026-05-23 review pass against
prd-v0.md: (a) merged two parallel section-numbering streams into a single canonical TOC (§0–§21 + Appendices A/B); (b) liftedagency_operatorinline-edit RBAC at v0.0.2 (OP-003) per PRD V0 §4 OP "branch operators can edit"; (c) movedPOST /campaigns/{id}/contracts/{ccId}/remindfrom v0.0.3 to V1 (consistent with OP-013); (d) movedPOST /campaigns/{id}/contracts/{ccId}/mark-offplatformfrom v0.0.3 to V1 (consistent with OP-014); (e) OP-004 split candidate reduced from 4 to 3 sub-slices (cancel, anomalies, import UI). Document version 1.1. - Vertical slices V0: OP-S0 (Tier 0: Flyway migration + module boundary + no-op ImportCommitted listener + Recovery stub), OP-001 (v0.0.0: campaign create + send, CampaignSendWorker + CampaignDispatchRecovery — split candidate into 3 sub-slices), OP-002 (v0.0.1: pre-send edit + status polling — admin only), OP-003 (v0.0.2: multi-agency routing + extend inline-edit to agency_operator), OP-004 (v0.0.3: cancel + anomalies + import UI — split candidate 3 sub-slices; remind & mark-offplatform deferred to V1), OP-005 (v0.1.0: observability).
- Vertical slices V1: OP-006 (dashboard) through OP-017 (notes offline sync).
- External deps introduced: None new. Reuses Postgres x2 (TENANT S0), Keycloak (AUTH S0), spring-modulith outbox (AUDIT S0), WireMock (NOTIF S0).
- Cross-module impacts (NEW from this session):
- AUDIT (blocker for OP-001 dev-start): catalog must admit 14 OP event types (OQ-X14).
- AUTH:
SignedLinkService.createLinkcalled at campaign send (v0.0.0);reissueForCascadeat send-reminder (V1, OQ-X6 already raised). - NOTIF:
NotificationDispatchService.dispatch()must be idempotent viaUNIQUE(campaign_contract_id)onnotificationstable (OQ-OP-04 — confirm with NOTIF ARCHITECT before OP-001 dev-start). - ING: OQ-X13 resolved — no-op
ImportCommittedlistener in OP-S0. - TENANT:
cabinet_settingstable dropped from OP; OP readsrenewal_window_days+ CIMA fields viaTenantConfigCache(OP-ADR-03).SETTINGS_CHANGEDaudit event emitted by OP when operator updates settings via TENANT API. - OP-specific ADRs:
- OP-ADR-01: Fan-out =
CampaignSendWorker(async@ApplicationModuleListeneroutbox-backed,TX_iper contract) +CampaignDispatchRecovery(startup + every 5 min, idempotent viaUNIQUE notifications.campaign_contract_id). - OP-ADR-02: Full V1 status CHECK constraints on
campaigns+campaign_contractsfrom v0.0.0 (forward-compat, mirrors AUDIT-ADR-03). - OP-ADR-03: Settings owned by TENANT; OP reads via
TenantConfigCache; nocabinet_settingstable. - OP-ADR-04: v0.0.0 cohort = operator on-demand;
@Scheduledcron deferred to v0.0.2+. - OP-ADR-05:
correlation_id = campaign_contract.id. D16 chain confirmed (OQ-X3 OP leg resolved). - OP-ADR-06: V1 real-time = SSE; v0.0.0–v0.0.5 = 30s polling.
- OQ-X3 status: OP + CP + NOTIF confirmed (see OQ-X3 table entry). Still pending: PAY/AUTH.
- OQ-X13 status: RESOLVED — no-op listener in OP-S0.
- OQs raised: OQ-X14 (AUDIT catalog for 14 OP event types — blocks OP-001 dev-start), OQ-OP-01..05 (see arch Appendix B).
- PRD OQs carried: OQ-01..06 from
docs/prd/operator-console-prd.md§A.6. OQ-04 resolved (cohort timezone = UTC+0 v0.0.0,ZoneIdfromTenantConfigCachewired from day 1 for V2 readiness). OQ-06 resolved (≥ 1 activecabinet_adminguard at service layer). - Tables owned (Tenant DB per D1):
campaigns,campaign_contracts,operator_notes.cabinet_settingsdropped (OP-ADR-03). - V2 forward-compat: No UNIQUE on
campaigns.name;cohort_params JSONBabsorbs V2 scheduling fields;send_attempt_countsupports V2 auto-retry; full status enums from v0.0.0.
Update this file after every ARCHITECT session.