Aller au contenu

Story AUTH-006: Signed-link auto-revocation when all contracts are paid

Module: identity Slice: S4 (arch §11, AllContractsPaid handler) — V0 envelope AUTH-002 (arch §0.1) Side: [BACKEND] Version target: V0 — v0.0.1 Priority: 6 Depends on: AUTH-005; consumes PAY AllContractsPaid Can develop concurrently with: AUTH-007 Merge order: After AUTH-005 Estimated complexity: S PRD User Stories: AUTH-CP-07 (customer link auto-revocation triggers) Wireframe: N/A — backend only (the "already paid" customer screen is served by the CP module; AUTH provides the state + French copy)


Objective

v0.0.1 introduces merchant payment. Once a customer has paid all the contracts a signed link covers, the link must stop working so it can't be reused. This story adds the AllContractsPaid event handler that auto-revokes the link, and makes sure a partial payment does NOT revoke it. It also confirms the v0.0.1 security posture: the signed-link token is stored only as a SHA-256 hash at rest (never in cleartext) now that the link gates a real payment journey.


Backend Scope

Entities

Updates signed_links.stateAUTO_REVOKED_PAID. No new tables.

Service Layer

  • @ApplicationModuleListener void on(AllContractsPaid e) (arch §10.4) → signedLinkService.autoRevoke(e.linkId(), RevocationReason.ALL_PAID).
  • autoRevoke sets state AUTO_REVOKED_PAID and emits the audit event. A subsequent verifyLink on an auto-revoked link returns a terminal "already paid" result instead of VALID.
  • Partial payment does NOT revoke (R-CP-015): the link remains usable for the unpaid contracts. Only AllContractsPaid (every eligible contract paid) triggers revocation.
  • In-flight payment at the moment of revocation: PAY's idempotency key lets the running payment finish; no further interaction is accepted on the link afterwards (R-CP-007c equivalent).
  • Confirm SHA-256-hash-at-rest: signed_links.token_hash holds only the SHA-256 hash; the cleartext token lives solely in the URL (arch §7.7). This is the v0.0.1 hardening milestone called out in the V0 sub-version plan.

API Endpoints

None new. Reached via the existing CP boundary: a click on an auto-revoked link routes through SignedLinkService.verifyLink(...) which returns the terminal status.

Validation Rules

  • Only AllContractsPaid (all eligible contracts paid) revokes. Any partial-payment event must NOT revoke.
  • autoRevoke is idempotent (re-delivery of the event leaves the link in AUTO_REVOKED_PAID).

Multi-Tenant Considerations

The handler revokes only the link identified in the event, which is tenant-scoped on signed_links. AUTH uses the platform datasource. No cross-tenant effect.

Audit & Logging

  • signed_link_auto_revoked_paidcampaign_id, file_id.
  • AUTH_LINK_REJECTED_REVOKEDcampaign_id, file_id, revoke_reason (emitted when a customer clicks the now-revoked link).

Frontend Scope

N/A — backend only. The customer "already paid" screen is a CP story; AUTH supplies the state and the French copy below.

French copy provided to CP (design §4.7 / §10.1)

  • « Tous vos contrats sont en règle. Vous avez déjà payé l'ensemble des contrats de cette échéance. Merci. »
  • Optional line: « Un reçu vous a été envoyé par e-mail. »

UI States (ALL REQUIRED)

N/A — backend only.


Acceptance Criteria

AC-001: All contracts paid revokes the link
Given a link covers contracts {C1, C2, C3} and all three are paid
When PAY publishes AllContractsPaid for that link
Then the link state becomes AUTO_REVOKED_PAID
And signed_link_auto_revoked_paid is audit-logged
AC-002: Partial payment does not revoke
Given a link covers {C1, C2, C3} and only C1 and C2 are paid
When the customer returns
Then the link still verifies as usable for C3
And no revocation occurs
AC-003: Clicking an auto-revoked-paid link shows the "already paid" result
Given a link in state AUTO_REVOKED_PAID
When the customer clicks it
Then verifyLink returns the terminal "already paid" status (copy: "Tous vos contrats sont en règle. Vous avez déjà payé l'ensemble des contrats de cette échéance. Merci.")
And AUTH_LINK_REJECTED_REVOKED is audit-logged with revoke_reason

Compliance Rules

  • CIMA 001-2024 art. 26 single-use link = VÉRIFIÉ.
  • [LEGAL-REVIEW]: the link remains an authentication mechanism, not proof of consent (red flag #5 unchanged). Auto-revocation does not change this posture.

Standards & Conventions

  • docs/standards/java-spring-guidelines.md (@ApplicationModuleListener, idempotent handlers).
  • docs/standards/critical-rules.md (SHA-256 token at rest; tenant isolation).
  • docs/standards/api-guidelines.md.

Testing Requirements

Unit Tests

  • autoRevoke is idempotent.
  • Partial-payment input does not revoke.

Integration Tests

  • AllContractsPaid → state AUTO_REVOKED_PAID + audit; subsequent verifyLink returns the terminal "already paid" status + AUTH_LINK_REJECTED_REVOKED.
  • Token stored only as SHA-256 hash (assert no cleartext column/value).

What QA Will Validate

  • Paying everything kills the link; paying part of it does not; the revoked link shows the "déjà payé" message; tokens are hash-only at rest.

Out of Scope

  • Expiry / TTL enforcement — v0.1.0 (AUTH-010).
  • Attempt-limit / lockout / permanent revoke — v0.1.0 (AUTH-011).
  • Operator manual re-issue — V1 (AUTH-CP-08).
  • The payment flow itself and the customer "already paid" React screen — PAY / CP modules.

Definition of Done

  • [ ] AllContractsPaid handler auto-revokes the link (state AUTO_REVOKED_PAID)
  • [ ] Partial payment never revokes
  • [ ] Clicking a revoked-paid link returns the terminal "already paid" result with exact French copy
  • [ ] Handler is idempotent
  • [ ] Token confirmed SHA-256-hash-at-rest (no cleartext)
  • [ ] Audit entries signed_link_auto_revoked_paid and AUTH_LINK_REJECTED_REVOKED
  • [ ] Unit + integration tests written and passing
  • [ ] Tenant isolation verified (link scoped by tenant_id; platform DB)
  • [ ] No silent failures on event handling