Aller au contenu

Story NOTIF-001d: Country-profile resolver + provider strategy bean wiring (CI seeded)

Module: notifications Slice: NOTIF-001 sub-slice d (of NOTIF-001 in architecture §11) Side: [BACKEND] Version target: V0.0.0 Sub-version: v0.0.0 (architecture-backed) Priority: 2 (parallelizable with NOTIF-001a) Depends on: NOTIF-S0, TENANT must own the country_profile columns + CI seed row (architecture §9.1 step 4 — TENANT-owned migration) Can develop concurrently with: NOTIF-001a Merge order: Before NOTIF-001b (NOTIF-001b's SmsDispatcher resolves the provider via this slice) Estimated complexity: S PRD User Stories: N/A — Technical Story (supports NOTIF-US-01 / NFR-10 provider-agnostic) Wireframe: N/A — backend only


Objective

Implement the country-profile-driven provider resolver. At dispatch time, NOTIF looks up country_profile.notif_provider_sms and notif_provider_wa from the tenant's country_code, and selects the corresponding Spring @Component strategy bean keyed by provider_id (letstalk_sms_ci, meta_wa_cloud_ci, …). Only CI is seeded in v0.0.0 — but the wiring is Day-1 multi-country so adding a country in V1 is one row + one adapter class, zero NOTIF code change (NOTIF-ADR-05 / NFR-10).


Backend Scope

Entities

None (this story reads country_profile but does not own its schema — see Migrations).

Migrations

None owned by this story. TENANT owns V0_0_0__notif_seed_country_profile_notif_columns.sql (architecture §9.1 step 4 — listed in NOTIF architecture for traceability; the migration adds notif_provider_sms, notif_provider_wa, sms_sender_id_default columns to country_profile and seeds the CI row). This story HARD-DEPENDS on that migration being available before development starts.

Service Layer

  • CountryProfileResolver:
  • String resolveSmsProviderId(String countryCode) — reads country_profile.notif_provider_sms for the given country. Cache via the existing TenantConfigCache infrastructure (the country-profile cache is shared with TENANT — architecture §1.2 row 3). Returns the provider key (e.g. letstalk_sms_ci).
  • String resolveWaProviderId(String countryCode) — same for notif_provider_wa.
  • String resolveSmsSenderIdDefault(String countryCode) — for adapter use at submit time.
  • Optional<...> API — empty for an unseeded country (NOTIF-001a maps empty → 400 tenant_country_unsupported).
  • SmsProviderRegistry — Spring @Component that holds a Map<String, SmsProvider> keyed by provider_id (Spring auto-collects beans implementing SmsProvider and discriminates via a String providerId() method on the interface).
  • SmsProvider get(String providerId) — returns the bean or throws UnknownProviderException (500 — operational bug, not a user-facing error).
  • WaProviderRegistry — symmetric for WhatsAppProvider (the interface itself ships in NOTIF-003; the registry hook is wired here so NOTIF-003 only adds the adapter).
  • Wiring point in NotificationService.dispatch(...) (NOTIF-001a): the SMS delivery_attempt.provider_id is set from CountryProfileResolver.resolveSmsProviderId(tenant.country_code) at insert time — NOT hard-coded. NOTIF-001b's SmsDispatcher resolves the adapter from SmsProviderRegistry.get(provider_id) at submit time.

API Endpoints

None.

Validation Rules

  • tenant.country_code MUST have a country_profile row with non-null notif_provider_sms (and notif_provider_wa even though v0.0.0 doesn't use WA at dispatch time — the WA resolver returning empty would block NOTIF-003 at runtime). Validation belongs in NOTIF-001a's request path; this story provides the resolver.

Multi-Tenant Considerations

  • country_profile is global (not tenant-scoped) — it is keyed by country_code and shared across tenants of the same country. Critical-rule #1 doesn't apply to this table (it's reference data, not customer data).
  • Both V1 DB profiles supported: country_profile lives on platformDataSource (TENANT owns it).
  • No tenant impersonation possible — the resolver only consumes a country_code, which itself is resolved from the JWT-scoped tenant in NOTIF-001a.

Audit & Logging

  • No audit event owned by this story.
  • A failed resolver call (unseeded country) is surfaced as a 400 from NOTIF-001a; no audit event (per OQ-NOTIF-08 — validation rejects don't audit).
  • A failed registry lookup at submit time (UnknownProviderException) is logged as ERROR with correlation_id and triggers an alert — this is operational data integrity, not a customer-facing flow.

Frontend Scope

N/A — backend only.


Acceptance Criteria

AC-001d.1: CI resolver returns seeded providers
Given the `country_profile` table has a seeded row for `country_code='CI'` with `(notif_provider_sms='letstalk_sms_ci', notif_provider_wa='meta_wa_cloud_ci', sms_sender_id_default='PAPILLON')`
When `CountryProfileResolver.resolveSmsProviderId("CI")` is called
Then it returns `Optional.of("letstalk_sms_ci")`
And `resolveWaProviderId("CI")` returns `Optional.of("meta_wa_cloud_ci")`
And `resolveSmsSenderIdDefault("CI")` returns `Optional.of("PAPILLON")`

AC-001d.2: Unseeded country returns empty + adding a country requires zero NOTIF source change
Given a tenant with `country_code='SN'` and NO `country_profile.SN` row
When the resolver is called with "SN"
Then it returns `Optional.empty()`
And NOTIF-001a maps this to a 400 `tenant_country_unsupported`
When (forward-compat verification) a second country row is inserted (`SN`, `letstalk_sms_sn`, `meta_wa_cloud_sn`, 'PAPILLON_SN') AND a `letstalk_sms_sn` SmsProvider bean is added to the Spring context
Then `SmsProviderRegistry.get("letstalk_sms_sn")` returns the new bean without any source change to `CountryProfileResolver`, `NotificationService`, or `SmsDispatcher`

Compliance Rules

  • None directly. The country-profile resolver is the architectural seam that lets ARTCI mitigations (configured per country) stay per-country without spreading if (country == "CI") checks through NOTIF. Status discipline: when a second country is added, its mitigation set must be VÉRIFIÉ before the seed row goes in.

Standards & Conventions

  • docs/standards/critical-rules.md
  • docs/standards/java-spring-guidelines.md (@Component strategy pattern, Spring auto-collect via Map<String, T> injection)
  • docs/standards/database-guidelines.md
  • docs/standards/multi-datasource-patterns.md
  • docs/standards/multi-tenant-model.md (country-profile resolution rules)
  • docs/standards/tech-stack.md

Testing Requirements

Unit Tests

  • CountryProfileResolverTest — CI seeded → hits; SN unseeded → empty.
  • SmsProviderRegistryTest — known provider_id → returns bean; unknown → throws UnknownProviderException.
  • CacheBehaviorTest — second lookup for the same country hits the cache, not the DB (assert via a query counter).

Integration Tests

  • ForwardCompatMultiCountryTest — inserts an SN country_profile row + registers a stub SmsProvider bean with providerId() returning letstalk_sms_sn in test context; asserts NotificationService.dispatch for an SN tenant routes to the new adapter without any NOTIF source change (AC-001d.2 forward-compat verification).
  • MissingCountryProfileTest — tenant with country_code='SN', no country_profile.SN row → POST /dispatch returns 400 tenant_country_unsupported.

What QA Will Validate

  • The only place letstalk_sms_ci appears in source is the adapter's providerId() return + the country_profile.CI seed row. No string literal in NotificationService / SmsDispatcher.
  • Adding a country in V1 will be a doc-only PR (review checklist item).

Out of Scope

  • The actual country_profile table schema and the CI seed row — TENANT-owned (architecture §9.1 step 4).
  • WhatsApp provider adapter — NOTIF-003.
  • Per-tenant provider account override (V2 BYO-provider per V1 PRD §A.5).
  • Provider-credentials KMS — v0.1.0 (architecture §7 row "Secrets").

Definition of Done

  • [ ] CountryProfileResolver implemented + cached via the TENANT-shared cache
  • [ ] SmsProviderRegistry (and stub WaProviderRegistry for NOTIF-003 to fill in) wired
  • [ ] SmsProvider interface carries a String providerId() discriminator method
  • [ ] AC-001d.1 and AC-001d.2 pass (including the forward-compat multi-country test)
  • [ ] No string literal "letstalk_sms_ci" outside the adapter class and the seed migration (grep-asserted in CI)
  • [ ] Both V1 DB profiles tested
  • [ ] country_profile reference data correctly NOT scoped by tenant_id (it is global per architecture §4)
  • [ ] No silent failures: unknown provider_id at registry lookup throws a typed exception with a clear error log + correlation_id