# Customer update delivery plan

This plan delivers the accepted decisions in ADRs 0001–0004. Complete milestones in order because later behavior depends on the contracts, configuration, and persistence established earlier.

Feature status: In progress. Completing Milestones 0–6 does not close the feature until the Redis validation gate passes.

## Milestone 0: Run the service locally

Status: Complete.

- Restore and build the solution.
- Run SQL Server, Redis, RabbitMQ, Redis Insight, and the API through Aspire.
- Load local secrets from ignored `appsettings.Local.json` files.
- Use local authentication and the NewGen stub when the environment is `Local`.

Proof: Aspire reports every required resource as running, Swagger loads, and the existing profile-management stub endpoint returns a successful response.

## Milestone 1: Lock contracts and configuration

Status: Complete.

- Treat `newgen-assessment-v1.example.json` as the current NewGen response contract.
- Model requirement groups as named parameter dictionaries. Each parameter contains `value`, `required`, and `editable`; it has no `missing` property.
- Select customer input with `required && editable && value is empty`.
- Return `actionRequired` for an empty, required, non-editable parameter.
- Reject unknown groups, unknown required parameters, duplicate `sourceReference` values, duplicate selected names in the flat submission object, missing forms, and invalid workflow step references.
- Keep `sourceReference` internal to Form Engine configuration.
- Use the confirmed identity claims: `customerId`, `bankId`, and `onboardingType`, where digital onboarding is `DigitalOnboarding`. The first version used `CustomerId` and `5`, which returned `401` for real tokens.
- Define the eight authenticated client routes and their request and response shapes without exposing endpoints yet. Delete became the ninth route after Milestone 5.
- Stub the current NewGen submission as one flat object of collected values.
- Version the customer-information journey and the email form instead of changing active version 1 definitions.

Proof: deserialize the checked-in NewGen and flat-submission fixtures, validate their mappings against form properties, exercise every rejection rule, and read journey version 3 from the running API.

Verified in `DigitalOnboardingSvc`:

- The contract tests deserialize both fixtures and cover `complete`, `incomplete`, and `actionRequired` assessment outcomes.
- A Form Engine integration test reads journey version 3 and validates the confirmed NewGen requirements against the actual configured form schemas.
- Configuration tests reject unknown groups, unknown required parameters, duplicate mappings, duplicate flat-property names, missing main or branch forms, and invalid workflow step references.
- An Aspire-hosted HTTP test reads customer-information journey version 3 and confirms that the public response does not expose `sourceReference`.
- Clean-database seeding retains customer-information journey versions 1, 2, and 3. Version 3 adds source-driven review and confirmation without changing the published version 2 definition.

Later change: customer-information journey version 4 adds the `sourceOfIncome` step. New instances resolve version 4. Instances started on version 3 keep their fixed version.

Exit condition: the current client and NewGen fixtures, DTOs, route shapes, form properties, and journey configuration agree. If NewGen changes the nesting, update the fixture, DTO, projection, and tests together before closing this milestone.

## Milestone 2: Persist customer-owned instances

Status: Complete.

- Add the customer-update instance, resolved step, draft, submission, and reconciliation records described in ADR 0002.
- Enforce at most one active instance for `(bankId, CustomerId)`.
- Store only the resolved allowlist, drafts, versions, state, counters, and correlation references required for resumability and audit.
- Add migrations and repository tests.

Proof: apply migrations to a clean database, start and resume one owned instance, reject a second active instance, and prove that another customer receives `404`.

Verified in `DigitalOnboardingSvc`:

- A clean SQL Server database applies the customer-update persistence migration.
- The instance, resolved-step allowlist, draft, submission, and reconciliation records round-trip through EF Core.
- The repository resumes the active instance for its `(bankId, CustomerId)` owner.
- A filtered unique database index rejects a second active instance for the same customer.
- An ownership-scoped lookup returns no record for another customer, which the milestone 3 API maps to `404`.

Dependency: local database access is required to apply and inspect the real migration.

## Milestone 3: Resolve status and start

Status: Complete.

- Implement authenticated `GET /customer-updates/status` and `POST /customer-updates`.
- Query NewGen on every status request, map requirement groups through `sourceReference`, and filter configured form schemas.
- Return `complete`, `incomplete`, `actionRequired`, or `temporarilyUnavailable` according to ADR 0001.
- Issue a short-lived start token and make replay return the active instance.
- Reconcile an existing instance without writing when its resolved state is unchanged.

Proof: run status and start against the NewGen stub and SQL Server, including complete, action-required, contract-drift, outage, expiry, and replay cases.

Verified in `DigitalOnboardingSvc`:

- The authenticated status route resolves each eligible request from NewGen and Form Engine configuration.
- A repeated status request with the same effective requirements queries NewGen again without adding a reconciliation record.
- The protected start token binds the customer, the fixed journey and form versions, and the resolved field allowlists. An expired token returns `400`.
- Starting the journey persists one customer-owned instance in SQL Server. Replaying the valid token returns the same instance.
- The integration test covers `complete`, `actionRequired`, contract drift, retained-catalog outage fallback, token expiry, and replay.
- An Aspire-hosted HTTP test calls both authenticated routes with the local NewGen stub and the seeded Form Engine configuration.

## Milestone 4: Navigate and validate the journey

Status: Complete.

- Implement journey configuration, current-step, step, step-submission, and OTP routes.
- Enforce ownership and resolved-schema allowlists on every request.
- Save drafts, validate forms, invalidate review after an earlier change, and preserve compatible drafts during reconciliation.
- Store a durable request receipt before returning a successful step-submission response.
- Populate review and confirmation forms from validated non-email steps through configured `x-source` values.
- Calculate customer-update progress independently from onboarding progress.

Proof: complete the email OTP and preferred-address steps, resume after restart, reject malformed and extra properties, replay a durable request receipt, replace a validated earlier value, and observe the documented progress transitions.

Verified in `DigitalOnboardingSvc`:

- All six instance routes enforce ownership with `(bankId, CustomerId)` and return `404` for another customer.
- The resolved form schema is the submission allowlist. The service rejects extra properties before Form Engine validation.
- Email submission persists an OTP-pending step. OTP regeneration and verification use the retained `workitemName`, and verification advances the journey.
- If OTP verification succeeds upstream but the local commit is uncertain, the step remains OTP-pending. The customer must regenerate and verify another OTP.
- A request receipt is unique within the customer-update instance. An exact replay returns the stored response, while reuse with a different request returns `409`.
- Email submission replay with the same request regenerates the OTP, as required by the client contract.
- Form Engine validation failures use the onboarding validation envelope, including translated field errors, `statusCode`, and `statusMessage`.
- Review and confirmation forms contain only configured fields whose `x-source` values match validated non-email steps. The service overwrites submitted display values with the validated draft values before validation.
- Drafts, step status, and customer-update progress survive a process restart.
- A correction to an earlier validated step keeps review current and replaces the saved value.
- Reconciliation preserves compatible draft properties, removes properties that NewGen has populated, and invalidates affected validated steps and review.
- A SQL-backed integration test covers progress from `0/4` to `1/4` and `2/4`. An Aspire-hosted HTTP test exercises all six routes with the NewGen stub and the seeded Form Engine configuration.

Deferred enhancement: resolve and expose conditional customer-update branches. Journey version 3 has no branches, so this enhancement does not block Milestone 4.

## Milestone 5: Submit and reconcile with NewGen

Status: Complete.

- Freeze the flat payload when the customer confirms.
- Prevent concurrent external submissions.
- Send the values with the retained `workitemName` correlation reference.
- Mark success only after core confirmation, as required by ADR 0004.
- Return `202` with a stable submission reference when confirmation is still pending.
- Make polling and the stubbed callback update the same durable state idempotently.

Proof: exercise immediate success, delayed success, duplicate confirmation, callback replay, terminal failure, timeout, and recovery after process restart.

Verified in `DigitalOnboardingSvc`:

- Confirmation freezes one flat payload from validated data-step drafts and retains the assessment `workitemName` for NewGen correlation.
- The service commits the submission and `processing` lifecycle before it calls the submission gateway.
- Concurrent confirmation requests return the same durable submission. Only the request that commits the lifecycle transition calls NewGen.
- Immediate core confirmation completes the instance. A pending or unknown result returns HTTP `202` with a stable submission reference and `/customer-updates/status` polling URL.
- Status polling resumes a processing submission after a service restart and applies the same durable transition used by callbacks.
- Repeated confirmation and callback results are idempotent. Confirmed success cannot move back to processing, active, or failed.
- A terminal failure preserves the frozen attempt, returns the journey to its confirmation step, and permits a new retry with the saved drafts intact.
- SQL-backed integration tests cover immediate success, delayed polling success, concurrent confirmation, callback replay, terminal failure, timeout, and restart recovery.

## Post-Milestone 5: Align the client contract and add deletion

Status: Complete.

- Align the API with the client documentation in `docs/api/cif-update`. The envelope `statusCode` is a string business code, separate from the HTTP status. We changed the documentation, not the service.
- Return a translated `statusMessage` for every customer-update response.
- Return `400`, not `403`, for a start token issued to another customer.
- Return the translated OTP rejection message for every rejected OTP.
- Translate journey step titles for the instance channel.
- Return `409` from final submission when reconciliation has invalidated a data step since review.
- Report `InProgress`, not `Active`, as `onboardingStatus` after a failed submission, with envelope `statusCode` `99`.
- Add `POST /customer-updates/{customerUpdateId}/delete`. It deletes one owned instance and its reconciliation records, and discards the NewGen work item of an active instance in the same transaction. It returns `409` while the instance is processing and `503` when NewGen does not confirm the discard.

Verified in `DigitalOnboardingSvc`:

- SQL-backed integration tests cover deletion of an active instance, deletion of a completed instance without a discard, another customer's instance, a rejected discard that keeps local data, and a processing instance.
- Contract tests confirm the claim names and the nine routes.

Known deviations and open items from the Milestone 5 review:

- The service measures outage catalog retention from the last reconciliation write. A customer whose requirements have not changed for longer than `CatalogRetentionMinutes` receives `503` during an outage.
- The instance routes do not check NewGen health, so local draft work continues during an outage.
- A requirement satisfied through another channel leaves the progress total instead of counting as externally completed.
- ADR 0001 now describes the three items above as intended behavior.
- `existingCustomerUpdateId` is reserved and always `null`.
- Confirmation-step submissions store no request receipt. A retry with the same `requestId` after a failed submission starts a new submission attempt.
- The operations do not use `CustomerUpdateRepository`. Remove it or adopt it during cleanup.
- The legacy `CustomerInformation` module exposes `customer-information/status`, which overlaps the customer-update status route. It stays until the feature is complete. We remove it during cleanup.

## Milestone 6: Integrate and release

Status: Not started.

- Replace the local assessment and submission stubs with the agreed NewGen contracts.
- Expose the NewGen submission callback. `ReconcileCustomerUpdateSubmission` exists but has no route.
- Run the Postman collection and automated integration tests against a clean database.
- Add telemetry for catalog resolution, mapping failures, submissions, callbacks, and reconciliation.
- Confirm migration rollback and feature-disable procedures.

Proof: complete one customer update end to end in an integration environment and verify the resulting core state through the authoritative read path.

## Post-milestone completion gate: Validate Redis-backed start-token protection

Status: Open. Redis behavior needs more investigation before the feature can be declared complete.

The `startToken` is short-lived and self-contained. The service does not need to retain the token as a long-term business record. A multi-replica deployment does need shared protection state so that a token issued by one pod remains valid on another pod and during a rolling deployment.

Before choosing the Redis model, decide whether Redis stores only the shared Data Protection key ring or also stores a short-lived record for each issued token. Per-token records are required only if the service needs explicit revocation, one-time consumption, or immediate destruction before the cryptographic expiry time. Every per-token record must use a Redis TTL that does not exceed the token lifetime.

Do not mark this feature complete until an integration environment proves all of the following behavior:

- A token issued by one pod can start a journey through another pod.
- A valid token remains usable after the issuing pod restarts or a rolling deployment replaces it.
- A token fails after its configured expiry time.
- Redis removes any per-token state after expiry. The service retains no long-term token record.
- Token replay returns the existing active instance and cannot create a second active instance.
- Redis key rotation does not invalidate unexpired tokens unexpectedly.
- The service fails closed when it cannot read the protection state required to issue or consume a token.
- A Redis outage does not cause the service to create an independent per-pod key ring.
- The deployed Azure Redis hostname, credentials, TLS settings, firewall rules, and private network path work from every application pod.
- Logs, health checks, and alerts distinguish a Redis failure from an invalid or expired token.

Proof: run cross-pod issuance and consumption, pod restart, rolling deployment, expiry, replay, key rotation, and Redis outage tests against the deployed Azure Redis instance. Inspect Redis to confirm that any per-token state expires at the configured TTL.

Exit condition: document the selected Redis persistence model and outage behavior, then attach the integration results. Until this exit condition passes, the completed milestones prove application behavior only. They do not prove production-safe token persistence.

## External decisions still required

- Confirm whether NewGen will replace the current top-level requirement groups with a more consistently nested response.
- Confirm the final submission and callback envelopes. Until then, the service uses the documented flat values object and an internal stubbed outcome.
- Obtain database access on the development computer before Milestone 2 acceptance testing.
- Decide whether Redis stores only the shared Data Protection key ring or also stores expiring per-token records.
- Define the API and deployment behavior when Redis is unavailable during token issuance or consumption.
