Skip to content

Subscription Plans Case Matrix ​

One row per behavioral case for every use case named in SUBSCRIPTION_PLANS_USE_CASES.md. Each case names the test that would prove it, quoted verbatim as a spec file path, then the separator "›", then the exact test title, exactly like ORGANIZATION_CASE_MATRIX.md and IDENTITY_CASE_MATRIX.md. A case with no proving test answers — and is listed under Gaps, together with the lowest layer that could prove it honestly.

Group 0 (SP2, SP3, and SP5's entitlement window) shipped in baseline revision 28 and is proven against real PostgreSQL by database/tests/prepare/prepare-database-contracts.ts. NestJS application code now exists for SP13 (staff actor), SP14 (public catalog), SP15 (plan/version/offering/market administration), SP16 (assign/change a plan, extend a trial), SP17 (suspend/reactivate an organization), SP18 (the renewal lifecycle sweep job), SP19 (record a payment) and SP20 (idempotency and audit for staff commands), under src/modules/platform/. The row counts in Totals below were recounted across SP1-SP20 and npm run lint:case-matrix now fails when they drift from the tables.


Totals ​

Use cases20 (SP1 to SP20)
Per-case scenario rows210
Per-case scenario rows proven143 (Group 0: SP2 seed shape, SP3 caller-scoped provisioning, SP5 trial-entitlement window; SP4 hardening rows; the shipped SP13 to SP20 rows over the database contracts and the e2e catalog)
Cross-tenant and concurrency invariants10 (1 proven)
Gaps76

Plan catalog as data ​

SP1 — Define and publish a plan ​

CaseExpected answerProving test
Happy path: a draft plan version with attached capabilities/limits is publishedplan_versions.status = 'active', published_at/effective_from set—
A second version of the same plan attempts active while the first is still activeRefused — publish_plan_version_as_staff raises 23514 named plan_versions_another_version_active; through the API 422 platform.plan_version_command_refused with That plan already has an active plan version., the second version stays draft and the Idempotency-Key stays free; plan_versions_one_active_per_plan_uq stays the backstopdatabase/tests/concurrency/subscription-plans-unique-races-concurrency.ts › sequentialMistakesAreRefusals (NINAKU_SEQUENTIAL_PLAN_MISTAKES_REFUSED_OK); test/e2e/platform-admin-plan-unique-refusals.e2e-spec.ts › refuses publishing a second version while another is active with 422 and changes nothing
Editing capabilities/limits of a version already activeRefused by require_draft_plan_version—
Attaching a deprecated capability or limit definition to a draft versionRefused by validate_plan_version_capability/validate_plan_version_limit—
Publishing an offering whose plan or plan version is not activeRefused by validate_plan_offering—
Publishing an offering for a market that is not beta/active/pausedRefused by validate_plan_offering—
A plan version retires while its offerings stay activeNot refused by any trigger today — a documented gap, not a passing case—

SP2 — Seed the first real Plan and its Market Offerings ​

CaseExpected answerProving test
Happy path: the seed migration produces active Colombia/Ecuador Markets, one active pionero plan version, and one active, priced Offering per Market with trial_days = 30platform.provision_organization_for_caller-shaped calls can reference a real plan_offering_id per countrydatabase/tests/prepare/prepare-database-contracts.ts › organizationRuntimeProvisionsItselfPerCountry (CO and EC)
trial_days outside 0–365Refused by plan_offerings_trial_days_chk—
Active market missing default locale/currencyRefused by markets_operational_defaults_chk—
A second Offering for the same Plan/Market is added without retiring the firstNot refused — an ops-discipline gap, not a schema guarantee—
An organization attempts a second trial after its first subscription endsNot refused by any schema object — bounded only by the cost of creating a second organization—

Subscription lifecycle at provisioning ​

SP3 — Start a subscription when provisioning an organization ​

CaseExpected answerProving test
Happy path: platform.provision_organization_for_caller inserts a trialing subscription with trial_ends_at = starts_at + trial_days201 Created, subscription row present, ended_at IS NULLdatabase/tests/prepare/prepare-database-contracts.ts › organizationRuntimeProvisionsItselfPerCountry
Happy path, zero-trial offeringSubscription inserted active with a computed current_period_end—
ninaku_runtime executes platform.provision_organization directlyRefused — permission denied for function provision_organization (already proven today; must keep passing unchanged)database/tests/prepare/prepare-database-contracts.ts › organizationRuntimeCannotProvisionOrganizations
ninaku_runtime executes platform.provision_organization_for_caller for itself200/201 — this is the new, intentionally reachable pathdatabase/tests/prepare/prepare-database-contracts.ts › organizationRuntimeProvisionsItselfPerCountry
Chosen plan offering belongs to another country's MarketRefused — "Plan Offering does not belong to the requested country Market"database/tests/prepare/prepare-database-contracts.ts › organizationRuntimeRefusesAnOfferingFromAnotherCountry
Chosen plan offering is not activeRefused before any insertdatabase/tests/prepare/prepare-database-contracts.ts › organizationRuntimeRefusesAnInactiveOffering
Offering's availability window excludes starts_atRefused before any insertdatabase/tests/prepare/prepare-database-contracts.ts › organizationRuntimeRefusesAnInactiveOffering
Unknown country/currency/localeRefused before any insertdatabase/tests/prepare/prepare-database-contracts.ts › organizationRuntimeRefusesAnUnknownCountry/...Currency/...Locale
Country with no active MarketRefused before any insertdatabase/tests/prepare/prepare-database-contracts.ts › organizationRuntimeRefusesACountryWithNoMarket
Owner identity is blocked, with no partial garbage left behindRefused; no organization, membership, subscription or provisioning-request row survivesdatabase/tests/prepare/prepare-database-contracts.ts › organizationRuntimeCannotProvisionForABlockedIdentity
Caller cannot name another Identity or Role as ownerThe function has no such parameter (structural) and the resulting organization's sole member is the caller (behavioral)database/tests/prepare/prepare-database-contracts.ts › organizationRuntimeCannotNameAnotherOwner
Two provisioning attempts with the same idempotency key and payloadSame organization_id, no duplicate subscriptiondatabase/tests/prepare/prepare-database-contracts.ts › organizationRuntimeProvisionsItselfPerCountry
Same idempotency key, different payloadRefused — fingerprint mismatchdatabase/tests/prepare/prepare-database-contracts.ts › organizationRuntimeProvisionsItselfPerCountry

Reading a subscription ​

SP4 — Read my organization's subscription and plan ​

CaseExpected answerProving test
Happy path: member reads their organization's current subscription, plan name, version, and entitlement summary200 OKtest/e2e/my-subscription.e2e-spec.ts › answers the organization current plan, price, period and entitlements; src/modules/platform/infrastructure/subscription-reading/platform-my-subscription.repository.integration-spec.ts › returns the current plan, price, period and entitlements of the tenant organization; test/e2e/my-subscription.e2e-spec.ts › lets a member without any role read the subscription
Caller's X-Organization-Id names a foreign organizationNever reaches this query — refused by the tenant-context guard (Organization O3)test/e2e/my-subscription.e2e-spec.ts › refuses every reason to lack a tenant context with the same body; src/modules/platform/infrastructure/subscription-reading/platform-my-subscription.repository.integration-spec.ts › never shows another organization's subscription
Ended/cancelled subscriptionReturned as historical fact, not an errorsrc/modules/platform/infrastructure/subscription-reading/platform-my-subscription.repository.integration-spec.ts › returns an ended subscription as historical fact when nothing newer exists; src/modules/platform/infrastructure/subscription-reading/platform-my-subscription.repository.integration-spec.ts › shows the current row, not the one a plan change ended
ninaku_runtime attempts to join through to platform.plans/plan_versions todayAllowed — SELECT shipped in Group 0 (revision 28); the read model is GET /subscriptionsrc/modules/platform/infrastructure/subscription-reading/platform-my-subscription.repository.integration-spec.ts › returns the current plan, price, period and entitlements of the tenant organization
ETag stability: two identical readsSame body, same strong ETag (quoted SHA-256 of the body)test/e2e/my-subscription-etag.e2e-spec.ts › gives two identical reads the same body and the same ETag
ETag changes when a limit override changes what the organization is held toThe old If-None-Match answers 200 with the new limit; the new tag answers 304test/e2e/my-subscription-etag.e2e-spec.ts › changes the ETag when a limit override changes what the organization is held to, so the old tag no longer answers 304
If-None-Match variants: *, a list holding the tag, a weak validator, an unquoted copy, another tag, a list of other tags, an empty value304 with no body and Cache-Control: private, no-cache for the first three; 200 with the body for the rest (Express freshness and the shared helper agree)test/e2e/my-subscription-etag.e2e-spec.ts › treats %s as If-None-Match and answers %i
One organization's tag presented for another organization of the same callerNever 304; a different tag and a different plantest/e2e/my-subscription-etag.e2e-spec.ts › never lets one organization's tag answer 304 for another organization of the same caller
A caller outside the organization presents the exact current tag or *403 · tenant.context_rejected, never 304, no ETag header, no plan datatest/e2e/my-subscription-etag.e2e-spec.ts › answers a caller outside the organization 403 even when it presents the exact current tag or a wildcard
A member of one organization names another organization's idThe same 403 · tenant.context_rejected body as an unknown id, without the organization's idtest/e2e/my-subscription-isolation.e2e-spec.ts › answers a member of one organization naming another's id exactly as it answers an unknown id
Membership ended or suspended after the token was issued; organization suspended403 · tenant.context_rejected, no datatest/e2e/my-subscription-isolation.e2e-spec.ts › refuses a member whose membership became %s after the token was issued; test/e2e/my-subscription-isolation.e2e-spec.ts › refuses even the owner once the organization is suspended
Only an ended subscription exists200 as history: ended, endedAt set, no capabilities, the last limit ceiling kepttest/e2e/my-subscription-isolation.e2e-spec.ts › answers an ended subscription as history: ended, no entitlements, the last ceiling kept
Trial whose trial_ends_at has passedCapabilities are empty, the limits keep the ceiling they hadtest/e2e/my-subscription-isolation.e2e-spec.ts › drops the capabilities of a lapsed trial but keeps the limits it was held to
Price values 0.1, 19.99, 0.000001, 9999999999999.999999, 59000A JSON string with six decimals (0.100000, 19.990000, 0.000001, 9999999999999.999999, 59000.000000), never a numbertest/e2e/my-subscription-isolation.e2e-spec.ts › answers the price %s as the exact decimal string %s and never as a number
Reading never writesNo subscription, organization, membership or role assignment row is touched, and the read transaction never gets a transaction idtest/e2e/my-subscription-isolation.e2e-spec.ts › reads without writing: no subscription, organization, membership or role assignment row is touched; src/modules/platform/infrastructure/subscription-reading/platform-my-subscription.repository.integration-spec.ts › reads inside a transaction that never wrote anything
Twenty parallel reads while staff change the planEvery answer is one complete snapshot, the old plan in full or the new plan in full, never a mixtest/e2e/my-subscription-coherence.e2e-spec.ts › answers every one of 20 parallel reads as one complete snapshot, the old plan in full or the new plan in full
Override windows: an override that expired, one that starts tomorrow, and an unlimited overrideExpired and future overrides are ignored (the plan ceiling shows); an unlimited override shows no value and isUnlimited: truesrc/modules/platform/infrastructure/subscription-reading/platform-my-subscription.repository.integration-spec.ts › ignores a limit override that is %s and keeps the plan ceiling; src/modules/platform/infrastructure/subscription-reading/platform-my-subscription.repository.integration-spec.ts › answers an unlimited override as no value and unlimited

Trial and lapse ​

SP5 — Trial expiry: what the product does when it ends ​

CaseExpected answerProving test
Happy path: trial_ends_at passes with no plan changeplatform.subscription_is_entitling returns true before, false from that instant, with no application code involveddatabase/tests/prepare/prepare-database-contracts.ts › organizationTrialEntitlementFollowsTrialEndsAt
A capability-gated write attempted after trial expiryRefused by assert_operation_allowed (effective_capability resolves disabled)—
A permission-only operation (no capability check) attempted after trial expiryStill succeeds — unaffected by entitlement lapse—
Trial subscriber changes plan before trial_ends_atNo lapse ever observed; old row ends as part of the plan-change transition (SP6)—
A trialing subscription attempts to hold a grace_until valueRefused by subscriptions_grace_chk — structurally impossible for a trial—
Scheduled job detects a lapsed trialing row at trial_ends_atTransitions to past_due (not ended), backfilling current_period_start/current_period_end/grace_until so the existing grace window applies — full rows under SP18 belowdatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP18-01 a lapsed trial moves to past_due with a 3-day grace backfilled onto its period

Changing plan ​

SP6 — Change plan (upgrade or downgrade) ​

CaseExpected answerProving test
Happy path: caller with organization.account.manage moves to a new active offeringOld subscription ended_at set, new subscription inserted, exactly one current row—
Concurrent plan-change commands on the same organizationExactly one wins; the loser's conditional UPDATE affects zero rows and is refused, not retried into a duplicate—
Downgrade below current usage of a limited resource (e.g., active memberships)Downgrade succeeds; existing usage is grandfathered; the next admission against the new, lower limit fails at the owning domain's own check—
Target offering's plan version is retiredRefused by validate_subscription_creation's active-offering check—
Target offering itself is retired/paused/draftRefused by the same check—
Caller lacks organization.account.manageRefused by the permission check—
Change to the exact same offeringRecommended no-op 200, not a wasted end-and-reopen write—
ninaku_runtime attempts a raw INSERT/UPDATE on platform.subscriptions todayRefused — no grant exists beyond SELECT—

SP7 — Request cancellation of a subscription ​

CaseExpected answerProving test
Happy path: immediate cancellationSubscription ended_at set, status = 'ended', no replacement row—
Happy path: cancel at period endcancel_requested_at set now, cancel_at_period_end = true; actual end deferred to current_period_end—
cancel_at_period_end = true without cancel_requested_at setRefused by subscriptions_cancel_at_period_end_chk—
Cancelling an already-cancelled (pending) requestIdempotent, not an error—
Immediate cancellation of a trialing subscriptionOrganization left with no current subscription; every capability-gated operation afterward fails closed the same way SP5 describes—
Caller lacks organization.account.manageRefused by the permission check—

Entitlement contract — how other modules ask ​

SP8 — Ask whether a capability is granted (effective_capability) ​

CaseExpected answerProving test
Happy path: capability included in the current entitling subscription's plan version'enabled'—
Organization itself is not active'disabled', checked before the capability lookup—
Unknown capability code'disabled', not an error—
Market-scoped capability with no market support row, or a paused/retired one'disabled', independent of plan inclusion—
Explicit deny override present'disabled', overriding an otherwise-included capability—
Explicit grant override present, no entitling subscription'enabled' — a grant does not require a subscription—
No entitling subscription, no override, capability included in plan'disabled'—

SP9 — Ask a plan limit (effective_limit) ​

CaseExpected answerProving test
Happy path: no override, current subscription's plan defines the limitReturns { limitValue, isUnlimited: false, limitSource: 'plan' }—
Valid, currently-effective override presentReturns the override's value, limitSource: 'override', regardless of the plan—
Limit code never definedNo row returned; caller decides how to treat "undefined"—
Subscription ended, no overrideStill returns the most recently ended subscription's plan limit, limitSource: 'plan'—
identity.assert_membership_seat_available calling this for identity.active_membershipsRefuses the (N+1)th active membership once count(*) > limit_value—

SP10 — Authorize an operation combining permission and capability (assert_operation_allowed) ​

CaseExpected answerProving test
Happy path: permission and capability both grantedSucceeds—
Permission present, capability absentRefused, distinctly from a plain permission refusal—
Capability present, permission absentRefused—
Target outlet inactive or has an inactive ancestorRefused—
Vertical named but not active for the resolved business unitRefused—
No active tenant contextRefused, 42501—

Versioning and grandfathering ​

SP11 — Plan version changes after an organization has subscribed ​

CaseExpected answerProving test
Happy path: V1 retires while an organization still subscribes through an offering pointing at V1Existing subscriber's entitlement is unaffected; effective_capability/effective_limit still resolve through the unchanged plan_offering_id—
An offering's plan_version_id is attempted to change after leaving draftRefused by protect_plan_offering_terms—
A new offering under V2 is created for the same plan/marketCoexists with the old offering under V1; no organization is moved without an explicit SP6 command—

Out of scope ​

SP12 — Billing and payment collection ​

CaseExpected answerProving test
Any attempt to charge a card, call a payment processor, or reconcile a payment result through platform.*No code path exists — explicitly deferred to payments/integrations—
A paid (amount > 0) offering is created without a billing integrationNot refused by any trigger — plan_offerings_amount_chk only requires amount >= 0; nothing ties the offering to a payment capability—

Platform administration and the super admin actor ​

SP13 — Platform super admin as an actor ​

CaseExpected answerProving test
Happy path: an identity with an active platform.staff_members row calls a staff-gated functionSucceeds past the staff checkdatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP13-01 an active staff grant is recognized
Identity has no platform.staff_members rowRefused, 42501, before any table is toucheddatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP13-03 an ordinary identity with no staff row at all is refused
Staff row exists but status = 'revoked'Refused — checked at call time, not at session-issue timedatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP13-02 a revoked staff grant is refused, even naming its own identity
An identity attempts to hold two simultaneously active staff grantsRefused — staff_members_one_active_per_identity_uqdatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP13-04 an identity cannot hold two simultaneously active staff grants
An organization owner's role_assignments row is presented as if it were staffRefused — the two tables share no column, no trigger, no function—
ninaku_runtime attempts INSERT on platform.staff_members directlyRefused — no such grant is proposed; the first row is a migration insert by ninaku_migrator—

SP14 — List plans available in a country before signup ​

CaseExpected answerProving test
Happy path: an unauthenticated caller lists a country's plans200, active plans/offerings for that market onlytest/e2e/plans-catalog.e2e-spec.ts › needs no session and shows the price for the chosen country; src/modules/platform/application/plan-catalog/list-public-plans.use-case.integration-spec.ts › shows the price seeded for the offering in the chosen country
Unknown or unsupported country codeEmpty list, not an errorsrc/modules/platform/application/plan-catalog/list-public-plans.use-case.integration-spec.ts › returns an empty catalog for a country with no active Market
Market exists but is planned/paused/retiredEmpty list—
A plan version is active with zero active offerings anywherePlan omitted from the response—
A draft/paused/retired offering, plan, or versionNever returned, at any join—

SP15 — Create and manage plans, versions, prices, and markets ​

CaseExpected answerProving test
Happy path: staff creates a plan, drafts a version, publishes itplans/plan_versions rows active as expecteddatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP15-06 staff creates a plan, SP15-07 staff drafts a plan version, SP15-09 publishing succeeds once every active Limit Definition is declared
A draft version missing an active Limit Definition attempts to publishRefused — every active Limit Definition must be declared firstdatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP15-08 a draft version missing an active Limit Definition cannot publish
Two staff calls race to publish two versions of the same plan activeExactly one wins; the loser blocks on the Plan row lock and is then refused with Plan already has an active Plan Version, never a unique violation; one version active, the other still draftdatabase/tests/concurrency/subscription-plans-unique-races-concurrency.ts › concurrentPublishesOfOnePlan (NINAKU_CONCURRENT_PUBLISHES_ONE_ACTIVE_OK)
Two staff calls race to draft a version of the same planBoth succeed with distinct numbers (1,2): the second blocks on the Plan row lock before it computes MAX(version_number)+1database/tests/concurrency/subscription-plans-unique-races-concurrency.ts › concurrentDraftsForOnePlan (NINAKU_CONCURRENT_DRAFTS_DISTINCT_NUMBERS_OK)
Staff creates a plan whose code already exists, in any letter case422 platform.plan_version_command_refused with A plan with that code already exists.; no second plan is storedtest/e2e/platform-admin-plan-unique-refusals.e2e-spec.ts › refuses a duplicate plan code with 422 and creates no second plan
Staff retires a version with active/paused offeringsVersion retires and every one of its offerings retires in the same transaction — closes SP1's documented cascade gapdatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP15-10 staff retires a plan version, SP15-11 retiring a plan version cascades to retire its offerings atomically
Staff retires a version with zero offeringsThe cascading UPDATE affects zero rows; not an error—
Staff attempts to change an existing active offering's amount directlyRefused — protect_plan_offering_terms—
Staff adds a new offering for the same plan version/market insteadSucceeds — INSERT, never UPDATEdatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP15-12 staff drafts an offering
Staff creates an offering for a market, a currency or a target organization that does not exist422 platform.plan_offering_command_refused whose detail names only the missing reference (That market does not exist., That currency does not exist., The target organization does not exist.); no offering is stored and no constraint or SQL text is exposed. The repository maps each foreign key by its constraint name (plan_offerings_market_fk, plan_offerings_currency_fk, plan_offerings_target_organization_fk), never by the SQLSTATE class; the market and the plan version are named by validate_plan_offering itself (CONSTRAINT added in migration 0042)test/e2e/platform-admin-offering-references.e2e-spec.ts › refuses an unknown $reference with 422 and a safe reason, and stores no offering; src/modules/platform/infrastructure/plan-administration/platform-plan-administration.repository.spec.ts › refuses a missing reference named $constraint with its own reason and reveals nothing else; database/tests/contracts/subscription-plans-administration-contracts.ts › SP15-12b an unknown Market names its foreign key, SP15-12c an unknown currency names its foreign key, SP15-12d an unknown target organization names its foreign key
Staff creates an offering under a plan version that does not exist404 platform.plan_offering_not_found; no offering is stored. Publishing and withdrawing an offering keep answering 404 for any foreign key violationtest/e2e/platform-admin-offering-references.e2e-spec.ts › answers an unknown plan version with 404 and stores no offering; src/modules/platform/infrastructure/plan-administration/platform-plan-administration.repository.spec.ts › keeps an unknown plan version a not found; src/modules/platform/infrastructure/plan-administration/platform-plan-administration.repository.spec.ts › keeps any foreign key violation a not found, whatever the constraint; database/tests/contracts/subscription-plans-administration-contracts.ts › SP15-12a an unknown Plan Version names its foreign key
Staff publishes a draft offeringdraft → active, sellable from available_fromdatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP15-13 staff publishes the offering
Staff withdraws (pauses) an active offeringactive → paused, allowed by protect_plan_offering_termsdatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP15-14 staff pauses (not retires) a withdrawn offering by default
Staff withdraws (retires) a paused offeringpaused → retired, alloweddatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP15-15 staff retires the offering for good
Staff opens a market with status = 'active' and no default locale/currencyRefused — markets_operational_defaults_chk—
Staff opens a market for a country that already has one, or two staff race to open the same new market concurrentlyIdempotent — open_market_as_staff is a single atomic INSERT ... ON CONFLICT DO UPDATE; both callers converge on the same row, never a markets_country_uq violationdatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP15-17 staff re-opens an already-open market idempotently, updating it in place; database/tests/concurrency/subscription-plans-concurrency.ts › marketOpeningRace (NINAKU_MARKET_OPENING_CONCURRENCY_OK)
Staff publishes an offering whose plan version retired between the draft and publish calls, including the exact race where the retire commits while the publish is waiting on the same rowRefused — publish_plan_offering_as_staff locks the Plan Version FOR SHARE, blocking on a concurrent retire and re-reading its committed status; validate_plan_offering's active-plan/version check is the same-transaction backstopdatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP15-18 an offering cannot publish once its Plan Version is retired; database/tests/concurrency/subscription-plans-concurrency.ts › publishOfferingVersusRetireVersionRace (NINAKU_PUBLISH_OFFERING_VERSUS_RETIRE_VERSION_CONCURRENCY_OK)
An offering amount at or beyond the numeric(19,6) boundaryAccepted up to 13 integer digits and 6 decimals; refused with 422 beyond that, never a raw database overflow errorsrc/modules/platform/presentation/dtos/create-plan-offering-request.schema.spec.ts
A non-staff caller invokes any of the eleven functions aboveRefused, 42501database/tests/contracts/subscription-plans-administration-contracts.ts › SP15-05 a non-staff identity cannot create a plan, SP15-16 a non-staff identity cannot open a market

SP16 — Assign a plan, extend a trial, or change an organization's plan (staff override) ​

Shipped. platform.assign_plan_as_staff, platform.change_subscription_plan_as_staff and platform.extend_subscription_trial_as_staff share platform.assert_offering_assignable_to_organization, which closes the country-mismatch gap this row used to flag as an open question: the target Offering's Market must match platform.organization_country_code(organization_id) (the country of the Organization's most recent Subscription — organization.organizations itself carries no country column), a private Offering with a target_organization_id must name this exact Organization, and a trial Offering (trial_days > 0) is refused outright on the staff path (a trial is a self-service acquisition benefit; there is no owner identity here to attribute identity_trial_grants to).

CaseExpected answerProving test
Happy path: staff assigns a plan to an organization with no current SubscriptionINSERT, guarded by validate_subscription_creation exactly as SP3's insert isdatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP16-01 staff assigns a plan to an organization with no current Subscription
Happy path: staff changes an organization's plan to a new active OfferingOld Subscription ends, new one is inserted, exactly one current rowdatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP16-07 staff changes an organization plan, ending the old Subscription and starting a new one
Happy path: staff extends a trialing Subscription's trial_ends_at forwardUpdated; subscriptions_trial_chk holdsdatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP16-08 staff extends a trialing Subscription forward
Staff attempts to extend a trial that is not currently trialing, or attempts to move trial_ends_at backward/sidewaysRefused as a no-op — the function's own WHERE status='trialing' AND p_new_trial_ends_at > trial_ends_at matches zero rowsdatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP16-09 extending a trial backward or sideways is refused as a no-op
Staff assigns or changes to a trial Offering (trial_days > 0)Refused — a trial is a self-service acquisition benefit, never staff-assigneddatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP16-02 staff cannot assign a trial Offering
Staff assigns or changes to an Offering from a different country's Market than the organization's ownRefused — platform.organization_country_code mismatchdatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP16-03 staff cannot assign an Offering from a different country than the organization own
Staff assigns or changes to a private Offering targeted to a different OrganizationRefuseddatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP16-04 staff cannot assign a private Offering targeted to a different organization
Staff assigns a private Offering to its own declared targetSucceedsdatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP16-04b a targeted private Offering assigns cleanly to its own target
Staff assigns a plan to an organization that already has a current SubscriptionRefuseddatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP16-05 assigning a plan is refused for an organization that already has a current Subscription
Staff assigns or changes to a draft (unpublished) OfferingRefused — validate_subscription_creation requires an active Offeringdatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP16-06 assigning a draft (unpublished) Offering is refused
Two staff members race to change the same organization's planExactly one wins — same conditional-UPDATE signal as SP6—
Two staff calls with different Idempotency-Keys race to assign a plan to one organization that has no current SubscriptionExactly one creates the Subscription; the loser blocks on the Organization row lock and is refused with Organization already has a current Subscription (422 platform.subscription_command_refused through the API), never a unique violationdatabase/tests/concurrency/subscription-plans-unique-races-concurrency.ts › concurrentAssignmentsForOneOrganization (NINAKU_CONCURRENT_ASSIGNMENTS_ONE_CURRENT_OK); test/e2e/platform-admin-plan-unique-refusals.e2e-spec.ts › refuses a second assignment to an organization that already has a current subscription
Staff assigns a plan to an archived organizationRefused — validate_subscription_creation's organization-status check—
A non-staff caller invokes any of the three functions aboveRefused, 42501database/tests/contracts/subscription-plans-administration-contracts.ts › SP16-10a/SP16-10b/SP16-10c

SP17 — Suspend and reactivate an organization ​

Archival, the one-way third transition, follows this same staff-command shape but is catalogued and proven under O6 in ORGANIZATION_CASE_MATRIX.md; an archived organization can no longer be reactivated by the staff command below, by a payment, or by the lifecycle sweep.

Shipped. platform.suspend_organization_as_staff closes the mid-provisioning gap this row used to flag as an open question: it refuses when the Organization has no platform.subscriptions row at all (every real Organization gets one atomically with provision_organization/provision_organization_for_caller, so a missing one means provisioning never finished). platform.reactivate_organization_as_staff adds no new trigger — D21's existing deferred check (identity.assert_administration_preserved, 0400_cross_domain_contracts.sql) already fires on organization.organizations regardless of which caller ran the UPDATE, so the staff path inherits the same last-administrator guarantee O6 already proves for the member-authored path.

CaseExpected answerProving test
Happy path: staff suspends an active organizationstatus = 'suspended'database/tests/contracts/subscription-plans-administration-contracts.ts › SP17-01 staff suspends an active organization
Happy path: staff reactivates a suspended organization that still has an active administratorstatus = 'active'database/tests/contracts/subscription-plans-administration-contracts.ts › SP17-03 staff reactivates a suspended organization that still has its administrator
Staff reactivates a suspended organization that lost its last administrator while suspended (D21 does not apply to a non-active organization, so the last role assignment can be removed while suspended)Refused at commit — D21, 23514 · identity_last_administrator_requireddatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP17-04 reactivating a suspended organization that lost its last administrator is refused
Staff suspends an organization mid-provisioning (no completed Subscription yet)Refused — platform.subscriptions has no row for it yetdatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP17-05 suspending an organization mid-provisioning is refused
Staff suspends an already-suspended organizationZero-row conditional UPDATE; refused as a no-op, not silently re-applieddatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP17-02 suspending an already-suspended organization is refused as a no-op
Staff suspends an archived organizationZero-row conditional UPDATE; archived stays archived—
Staff reactivates a suspended organization whose current Subscription is suspendedRefused (23514, organization_reactivation_subscription_suspended); the organization stays suspended with its source unchanged; a payment is the way to restore itdatabase/tests/contracts/organization-suspension-source-contracts.ts › OSS-03 staff reactivation is refused while the subscription is suspended; database/tests/contracts/organization-suspension-source-contracts.ts › OSS-03b the refused reactivation leaves the organization suspended by the subscription
The same refusal over HTTP422 · platform.organization_command_refused with a safe body naming no constraint, one denied audit record with reason code subscription_suspended, the organization still suspended; a payment then restores both the Subscription and the organizationtest/e2e/platform-admin-organization-suspension-source.e2e-spec.ts › refuses to reactivate an organization whose subscription is suspended, audits the denial and lets a payment restore it; src/modules/platform/infrastructure/organization-administration/platform-organization-administration.repository.spec.ts › refuses a suspended subscription as a command refusal that names its reason
Staff suspends, reactivates or archives an organizationSuspension records suspension_source = 'staff'; reactivation of a staff-suspended organization (Subscription active, or past_due inside its grace) and archival clear itdatabase/tests/contracts/organization-suspension-source-contracts.ts › OSS-04 staff suspension records the staff source and reactivation of an active subscription clears it; database/tests/contracts/organization-suspension-source-contracts.ts › OSS-05 a subscription that still entitles, past_due inside its grace, does not block a staff reactivation; database/tests/contracts/organization-suspension-source-contracts.ts › OSS-06 archival clears the suspension source
A suspend, reactivate or archive command meets a transaction holding the Subscription rowThe command waits for the Subscription row before it touches the organization (the common lock order of the payment and the sweep) and then appliesdatabase/tests/concurrency/subscription-staff-commands-concurrency.ts › everyStaffCommandWaitsForTheSubscriptionRow
Staff reactivation races a payment, in both ordersReactivation after a payment on a suspended Subscription is refused as no longer suspended; reactivation and payment on a staff-suspended organization both apply in either order; always active/active with one payment and no deadlockdatabase/tests/concurrency/subscription-staff-commands-concurrency.ts › reactivationWaitsForAPaymentOnASuspendedSubscription; database/tests/concurrency/subscription-staff-commands-concurrency.ts › reactivationAndPaymentOnAStaffSuspendedOrganization
Staff suspension races a payment, and races the grace-expiry sweep, in both ordersA payment renews the Subscription and leaves the organization suspended with source staff; against the sweep the final state is suspended/suspended with source subscription when the sweep goes first (the suspension is refused as no longer active) and staff when the suspension goes firstdatabase/tests/concurrency/subscription-staff-commands-concurrency.ts › staffSuspensionVersusPayment; database/tests/concurrency/subscription-staff-commands-concurrency.ts › staffSuspensionVersusTheGraceExpirySweep
A non-staff caller invokes suspend/reactivateRefused, 42501database/tests/contracts/subscription-plans-administration-contracts.ts › SP17-06a/SP17-06b
Staff suspends an organization204; the Audit Record platform.organization.suspended is sealed with the staff member's reasonTexttest/e2e/platform-admin-organization-lifecycle.e2e-spec.ts › suspends an organization and records who did it
Staff reactivates a suspended organization204; the Audit Record platform.organization.reactivated is sealedtest/e2e/platform-admin-organization-lifecycle.e2e-spec.ts › reactivates a suspended organization and records who did it
Staff suspends without a reason, or with an empty or blank one400; nothing changes, because the action type requires a reasontest/e2e/platform-admin-organization-lifecycle.e2e-spec.ts › refuses to suspend with a $label reason and leaves nothing behind
A suspended organization's own member attempts to establish tenant contextRefused today — identity.has_active_tenant_context() requires organization.status = 'active'—

SP20 — Idempotency-Key and audit for staff commands ​

Every staff write built so far (create_plan, create_draft_plan_version, publish_plan_version, retire_plan_version, create_plan_offering, publish_plan_offering, withdraw_plan_offering, open_market, assign_plan, change_subscription_plan, extend_subscription_trial, suspend_organization, reactivate_organization, record_subscription_payment) requires an Idempotency-Key header and records a platform Audit Record on the same transaction it executes in. SP16-SP19's five commands share this exact same staff-command-idempotency.ts helper and PlatformAuditContract.record call, proven the same way as the original eight.

CaseExpected answerProving test
A brand new Idempotency-Key reserves the commandreserved = true; the command executes and its result is storeddatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP20-01 a brand new idempotency key is reserved for its caller
The same key and the same request body are retried200/201/204 with Idempotent-Replayed: true and the original result, never a second executiondatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP20-02 the same key and fingerprint replays the completed result instead of executing again
The same key is retried with a different request body409 idempotency.key_reused, the original request's fingerprint is what is compared, not the new onedatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP20-03 the same key with a different fingerprint still reports the ORIGINAL fingerprint, not the new one
The same key and request body are reused for a different command (for example a suspension key sent to the archival route, whose payload fingerprint is identical)409 idempotency.key_reused; the second command does not execute, no stored result is replayed and no archive Audit Record is written. Defended twice: platform.reserve_staff_command_idempotency answers a foreign command with no stored fingerprint and no result, and claimStaffCommand binds the command name into the fingerprint it reserves and comparesdatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP20-04 the same key and fingerprint for a DIFFERENT command is a mismatch and never returns the stored result; database/tests/contracts/subscription-plans-administration-contracts.ts › SP20-05 a foreign command reusing the key leaves the original command able to replay its result; src/modules/platform/application/staff/staff-command-idempotency.spec.ts › refuses the same key and payload fingerprint for a different command with $description and does not run it; test/e2e/platform-admin-organization-archival-refusals.e2e-spec.ts › refuses an archival that reuses the key of a suspension with the same reason and neither archives nor audits
Two callers race to claim the exact same brand-new key concurrentlyExactly one executes; the loser blocks on the winner's row lock and then replays its committed result — never a double execution, never a raw 23505database/tests/concurrency/subscription-plans-concurrency.ts › staffCommandIdempotencyRace (NINAKU_STAFF_COMMAND_IDEMPOTENCY_CONCURRENCY_OK)
A staff command is refused by the domainCommands that opt in with @AuditsRefusal leave an Audit Record with the same action code, outcome = denied and a reason_code, written in a second transaction because the refusal rolls the first back. If that record cannot be written, the caller still gets the refusal and the gap is logged at error level (a refused attempt changed nothing; a success keeps failing closed because its record shares its transaction). Today only the security message reissue opts insrc/modules/platform/presentation/audit/platform-refusal-audit.interceptor.spec.ts; against the real database, test/e2e/platform-admin-reissue-refusal-audit.e2e-spec.ts › records one denied attempt per refused request, so two refusals of the same message leave two records; test/e2e/platform-admin-reissue-refusal-audit.e2e-spec.ts › still answers the same refusal body whether or not the attempt was recorded; test/e2e/platform-admin-reissue-refusal-audit.e2e-spec.ts › writes no denied record when a non-staff caller is refused before the handler, and answers as it does for a message that does not exist; test/e2e/platform-admin-reissue-refusal-audit.e2e-spec.ts › writes no denied record when a staff member has no fresh elevation; test/e2e/platform-admin-reissue-refusal-audit.e2e-spec.ts › writes no denied record for a message that does not exist
A staff command executes for the first timeA platform Audit Record is written in the same transaction, actor = the staff identity, target = the created/changed Plan, Plan Version, Offering or Marketsrc/modules/platform/application/plan-administration/*.use-case.ts (each calls PlatformAuditContract.record only on IDEMPOTENT_OUTCOME.EXECUTED) — proven end-to-end, one sealed record with the staff actor and the changed target per command (a market is upserted by country, so its record is matched by the staff actor), by test/e2e/platform-admin-catalog-audit.e2e-spec.ts (plan, version, offering and market commands), test/e2e/platform-admin-subscription-audit.e2e-spec.ts (assign, change, extend trial, record payment) and test/e2e/platform-admin-organization-lifecycle.e2e-spec.ts (suspend, reactivate)
A retried (replayed) staff commandNo second Audit Record is writtenSame use-case guard (if outcome === EXECUTED) — unit-level only, not re-proven with a dedicated integration test in this iteration (disclosed gap)
Void (204) routes (publish/retire version, publish/withdraw offering) on replay204 without a body and with Idempotent-Replayed: true; the first execution is 204 without the header and exactly one platform Audit Record exists after the replay. The routes return HttpIdempotentResponse with no data and the envelope interceptor answers it as a 204test/e2e/platform-admin-catalog-replay-header.e2e-spec.ts › marks the replay of every 204 command of the plan catalog lifecycle and does nothing twice; src/foundation/http/envelope/http-envelope.interceptor.spec.ts › answers 204 with no envelope and sets Idempotent-Replayed when a replayed HttpIdempotentResponse carries no data

Step-up (elevation) on money-changing commands ​

Shipped for the three commands that move or redirect money: assign_plan, change_subscription_plan and record_subscription_payment. Reuses 100% of Identity's existing elevation mechanism (POST /step-ups, POST /step-ups/confirmations, the stateless JWT ElevationTokenPort) rather than building a second one: IdentitySessionContract (Identity's own public cross-module contract, now reached through the foundation SessionAuthenticator port) gained one new method, verifyElevation(elevationToken, caller), implemented by the same AuthenticateCallerUseCase that answers SessionAuthenticator.authenticate(). A new PlatformElevationGuard mirrors Identity's own ElevationGuard exactly, reading X-Elevation-Token and the caller the foundation SessionGuard already placed on the request, and is applied at the method level (never class level) on assignments, plan-changes and payments, never on trial-extensions (extending a trial moves no money). It is also applied, still at the method level, on the five plan administration commands that change what is sold, at what price and where: publishing a plan version, retiring a plan version, publishing an offering, withdrawing an offering and opening or updating a market. The routes that only create drafts (POST /platform-admin/plans, .../plans/{planId}/versions and .../plan-versions/{versionId}/offerings) stay open to a staff session without an elevation, because a draft is not sellable. Clients must now run a step-up before those five actions. The routes share one documentation decorator, ApiPlatformElevation, which declares the required X-Elevation-Token header and the 403 answer in the OpenAPI document. A staff identity signs in and steps up through the exact same Identity routes any tenant member uses — no new elevation route, no new token format, no duplicated verification logic — because an elevation token is bound only to identityId/sessionId, never to which guard authenticated the request.

CaseExpected answerProving test
A staff caller with a live session but no elevation token calls a money-changing commandRefused, 403 platform.elevation_requiredsrc/modules/platform/presentation/guards/platform-elevation.guard.spec.ts; end-to-end: test/e2e/platform-admin-elevation.e2e-spec.ts › refuses assigning a plan with no elevation token, then succeeds once elevated
A staff caller completes step-up and presents a matching elevation tokenCommand executesSame e2e test, second half of the same case
An elevation token minted for a different session (or a different identity) than the one calling nowRefused, 403 platform.elevation_requiredauthenticate-caller.use-case.spec.ts (contract-level); platform-elevation.guard.spec.ts (guard-level); test/e2e/platform-admin-elevation.e2e-spec.ts › refuses an elevation token minted for a different session than the one calling now (end-to-end)
PlatformElevationGuard runs before the foundation SessionGuard populated a callerRefused, 401 identity.session_required, never an elevation-specific messageplatform-elevation.guard.spec.ts
extend_subscription_trial, suspend_organization, reactivate_organizationNo elevation required — none of these three moves moneyDeliberate scope decision, not a gap
Staff price administration: publishing or retiring a plan version, publishing or withdrawing an offering, opening or updating a marketElevation required, same guard as the money commands. Owner-delegated decision, reversible: a stolen staff access token could otherwise change what is sold, at what price and wheretest/e2e/platform-admin-catalog-elevation.e2e-spec.ts (the rows below)
A staff caller with a session but no elevation token, with a token nobody minted, or with a token minted for another session calls one of those five commands403 platform.elevation_required; the plan version, offering or market is unchanged, no idempotency record is spent and no Audit Record is writtentest/e2e/platform-admin-catalog-elevation.e2e-spec.ts › answers 403 platform.elevation_required and changes nothing when no elevation token is sent, answers the same 403 and changes nothing for an elevation token nobody minted, answers 403 and changes nothing for a token minted for another session (each runs once per route: publish version, retire version, publish offering, withdraw offering, open market)
A staff caller with a live elevation withdraws an offering, retires a plan version with its offerings, or opens a marketWorks as before: 204 / 204 / 200test/e2e/platform-admin-catalog-elevation.e2e-spec.ts › withdraws an offering with a live elevation, retires a plan version and its offerings with a live elevation, opens a market with a live elevation; publishing a plan version and an offering with an elevation: test/e2e/platform-admin-catalog-audit.e2e-spec.ts › audits every command of the plan catalog lifecycle with its actor and target
A staff caller without an elevation creates a plan, drafts a version or drafts an offeringStill works: 201 for each, a draft is not sellabletest/e2e/platform-admin-catalog-elevation.e2e-spec.ts › keeps the draft routes open to a staff session without an elevation

Renewal lifecycle and payments ​

SP18 — Renewal lifecycle: trial to active, grace, automatic block, automatic reactivation ​

Shipped. platform.apply_trial_lapse, apply_period_lapse and apply_grace_expiry compose into platform.apply_subscription_lifecycle_transitions — the one function a periodic job calls. apply_grace_expiry also suspends the underlying organization.organizations row (mirroring SP17's staff suspension automatically, no new trigger) — in the same statement as the Subscription UPDATE, and only the active Organizations of the Subscriptions that statement returned (migration 0043), so "blocked" means the same thing here as it does for a staff-suspended organization: no tenant context at all, not a read-only mode. SubscriptionLifecycleSweepJob (@nestjs/schedule, src/modules/platform/presentation/jobs/) runs it on a configurable interval (SUBSCRIPTION_LIFECYCLE_SWEEP_INTERVAL_MS, default 300000ms) and takes a Postgres advisory transaction lock (pg_try_advisory_xact_lock) first, skipping the tick entirely if another replica already holds it — an efficiency measure, not the correctness backstop: each function's own conditional UPDATE ... WHERE <preconditions> is already idempotent by construction (a row that already transitioned stops matching), so even without the lock two replicas could never double-apply, only waste work contending on the same rows.

CaseExpected answerProving test
Happy path, Case A: an active period lapses with no renewalJob sets past_due, grace_until = current_period_end + 3 days; subscription_is_entitling stays true through grace, false afterdatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP18-02 a lapsed paid period moves to past_due with a 3-day grace
Happy path, Case B: a trialing subscription lapses with no successorJob sets past_due, backfilling current_period_start/current_period_end/grace_until in the same UPDATE; the same 3-day grace window now applies to a trial-derived lapse, with no ALTER TABLEdatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP18-01 a lapsed trial moves to past_due with a 3-day grace backfilled onto its period
Day 3 of grace: past_due, grace not yet expiredStill subscription_is_entitling = true, organization still active — "still works"database/tests/contracts/subscription-plans-administration-contracts.ts › SP18-03 day 3 of grace still works: past_due, still entitling, organization still active
Day 4: grace expired with no payment recordedpast_due → suspended; subscription_is_entitling has no branch for suspended so entitlement is false; the organization is suspended too — "blocks"database/tests/contracts/subscription-plans-administration-contracts.ts › SP18-04 day 4 blocks: suspended, no longer entitling, organization suspended too
The sweep suspends an organization at day 4 while a staff suspension is already in forceThe sweep records suspension_source = 'subscription' on the organization it suspends and never overwrites a staff suspensiondatabase/tests/contracts/organization-suspension-source-contracts.ts › OSS-07 the sweep records the subscription source and never overwrites a staff suspension
Re-running the composed sweep once every lapse already settledNo-op — 0 rows for every transitiondatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP18-05 re-running the composed sweep on an already-settled state is a no-op
Two replicas' job ticks race the same sweep windowThe second replica's pg_try_advisory_xact_lock call returns false immediately and it skips its tick entirely; releasing the lock makes it available againdatabase/tests/concurrency/subscription-plans-concurrency.ts › subscriptionLifecycleSweepLockRace (NINAKU_SUBSCRIPTION_LIFECYCLE_SWEEP_LOCK_CONCURRENCY_OK)
The job runs late — current_period_end has passed but status is still activesubscription_is_entitling's active branch (p_at < current_period_end) already evaluates false; no over-granting while status lags—
A subscriber changes plan (SP16) between two job runsThe job's WHERE clause no longer matches; no lapse is misapplied to a row that moved on—
A payment is recorded exactly at the grace_until boundary, racing apply_grace_expiry()The payment's own SELECT ... FOR UPDATE holds the row for the whole transaction; the racing sweep blocks on its UPDATE of the Subscription, re-evaluates its WHERE clause against the now-active row and matches zero, and the Organization is suspended only for the Subscriptions that same statement suspended, so it matches zero too — the final state is Subscription active and Organization activedatabase/tests/concurrency/subscription-grace-expiry-concurrency.ts › paymentVersusGraceExpiryRace (NINAKU_PAYMENT_VERSUS_GRACE_EXPIRY_CONCURRENCY_OK)
The sweep holds the Subscription lock first and a payment for the same Subscription waits for itAfter both commit the Subscription is active, the Organization the sweep suspended is active again and exactly one payment row existsdatabase/tests/concurrency/subscription-grace-expiry-concurrency.ts › graceExpiryVersusPaymentRace (NINAKU_GRACE_EXPIRY_VERSUS_PAYMENT_CONCURRENCY_OK)
One sweep over an expired past_due Subscription, one still inside its grace, one whose Organization is archived and one already suspended whose Organization is activeOnly the expired one is suspended together with its Organization; the one inside its grace stays past_due with an active Organization; the archived Organization is never touched (the Subscription is still suspended); the already suspended Subscription does not suspend its active Organization againdatabase/tests/concurrency/subscription-grace-expiry-concurrency.ts › graceExpirySweepOutcomes (NINAKU_GRACE_EXPIRY_SWEEP_OUTCOMES_OK)
A subscription has no current row at all (SP7's immediate cancellation)No job function's WHERE clause matches a row that does not exist; nothing to lapse—

SP19 — Record a payment ​

Shipped. platform.subscription_payments is insert-only — ninaku_runtime holds no direct grant on it at all, only EXECUTE on platform.record_subscription_payment_as_staff, the same isolation platform.staff_members already uses. The function locks the target Subscription with SELECT ... FOR UPDATE before reading its current_period_end, compares the amount and currency with those of the offering the Subscription is on (numeric equality: a manual staff payment is a full-period payment) and refuses a mismatch before anything is written, then inserts the payment row and renews the period from that captured value and clears grace_until: an early payment on a live period keeps current_period_start and adds one interval at the end, a payment inside grace starts the new period at the old end, and a payment after a long lapse anchors the period at the payment so the customer always ends entitled (migration 0049); the function refuses with subscription_payment_not_entitling a payment that would leave the Subscription without access. The function reactivates the Organization only when it is suspended with suspension_source = 'subscription' (the value apply_grace_expiry writes); a suspension applied by platform.suspend_organization_as_staff (SP17) records the staff source and is never undone by a payment, whatever the Subscription's own status was (migration 0045). A payment may carry an optional external_reference (the id read on the bank transfer or the processor receipt); the same reference is never recorded twice, so the same real-world payment cannot renew a Subscription twice whatever Idempotency-Key it arrives under (migration 0047). The reference is global, one namespace across every payment, because the model has no notion of a payment source; two different sources that happen to issue the same reference would collide, a known simplification.

CaseExpected answerProving test
Happy path: staff records a payment for a past_due subscriptionsubscription_payments row inserted; subscription reactivates to active with a recomputed period anchored to the old current_period_end, never to now()database/tests/contracts/subscription-plans-administration-contracts.ts › SP19-01 a payment on a past_due Subscription reactivates it, anchoring the new period on its own current_period_end, never now()
Happy path: staff records a payment for a suspended subscriptionSubscription and the Organization apply_grace_expiry suspended both reactivate to activedatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP19-02 a payment on a suspended Subscription also reactivates the Organization apply_grace_expiry had suspended
Happy path: staff records an ordinary early renewal of an active subscription, before it lapsesAccepted; the period start is kept and one interval is added at the end, no requirement to wait for a lapsedatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP19-03 an ordinary early renewal of an active Subscription is accepted, keeping its current_period_start and adding one interval to its current_period_end
A payment is recorded for an organization the subscription lapse suspendedThe Subscription renews and the organization reactivates with its source cleareddatabase/tests/contracts/organization-suspension-source-contracts.ts › OSS-01 a payment reactivates an organization the subscription lapse suspended and clears the source
A payment is recorded for an organization suspended by staff whose Subscription later lapsed to suspendedThe Subscription renews to active; the organization stays suspended with source staff until staff reactivate it, over HTTP toodatabase/tests/contracts/organization-suspension-source-contracts.ts › OSS-02 a payment renews the subscription of a staff-suspended organization but never undoes the staff suspension; test/e2e/platform-admin-organization-suspension-source.e2e-spec.ts › keeps an organization suspended by staff suspended after a payment and lets staff reactivate it afterwards
A payment is recorded for an Organization suspended by staff policy (SP17), whose Subscription itself stayed activeThe payment renews the Subscription but never touches organization.organizations.status — a policy suspension is never silently undone by a paymentdatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP19-04 a payment never lifts a staff policy suspension that never suspended the Subscription itself
An active subscription in a live period is paid earlyStays entitled right after the payment and at the old period end; current_period_start unchanged and current_period_end moved by exactly one intervaldatabase/tests/contracts/subscription-payment-renewal-contracts.ts › EP-01 an early payment on an active Subscription adds one interval at the end and keeps the period start; database/tests/contracts/subscription-payment-renewal-contracts.ts › EP-01b the Subscription stays entitled right after the early payment and at the old period end; test/e2e/platform-admin-subscription-payment-renewal.e2e-spec.ts › keeps an active subscription entitled when it is paid early: the period start stays and one interval is added at the end
Two early payments in a rowTwo intervals added, no gap, entitled throughoutdatabase/tests/contracts/subscription-payment-renewal-contracts.ts › EP-02 two early payments in a row add two intervals and never open a gap; test/e2e/platform-admin-subscription-payment-renewal.e2e-spec.ts › adds two intervals and opens no gap when the same subscription is paid early twice
A past_due subscription pays inside its graceUnchanged: the new period starts at the old end and the subscription is entitleddatabase/tests/contracts/subscription-payment-renewal-contracts.ts › EP-03 a past_due payment inside grace still starts the new period at the old end and is entitled; test/e2e/platform-admin-subscription-payment-renewal.e2e-spec.ts › still starts the new period at the old end when a past_due subscription pays inside its grace
A subscription suspended long ago (old end plus one interval still in the past) is paid onceThe period is anchored at the payment (start about now, end one interval later), the subscription is entitled and the reactivated organization sits over an entitled subscriptiondatabase/tests/contracts/subscription-payment-renewal-contracts.ts › EP-04 a payment after a long lapse anchors the new period at the payment and gives a full interval; database/tests/contracts/subscription-payment-renewal-contracts.ts › EP-04b the reactivated Organization sits over an entitled Subscription; test/e2e/platform-admin-subscription-payment-renewal.e2e-spec.ts › anchors the period at the payment after a long lapse, so the reactivated organization has access
A payment for an organization suspended by staff whose subscription is liveThe subscription is renewed and entitled; the organization stays suspended with source staffdatabase/tests/contracts/subscription-payment-renewal-contracts.ts › EP-05 a payment renews a staff-suspended Organization Subscription and leaves the Organization suspended; test/e2e/platform-admin-subscription-payment-renewal.e2e-spec.ts › renews the subscription of a staff-suspended organization and leaves the organization suspended
A wrong amount on an early paymentRefused, both period bounds untoucheddatabase/tests/contracts/subscription-payment-renewal-contracts.ts › EP-06 a wrong amount on an early payment is refused; database/tests/contracts/subscription-payment-renewal-contracts.ts › EP-06b the refusal leaves both period bounds untouched
A payment that would leave the subscription without access (a scheduled subscription whose period has not started)Refused, 23514, subscription_payment_not_entitling; no payment row, no period moveddatabase/tests/contracts/subscription-payment-renewal-contracts.ts › EP-07 a payment that would leave the Subscription without access is refused; database/tests/contracts/subscription-payment-renewal-contracts.ts › EP-07b the refusal records no payment and moves no period bound
Staff records a payment for an organization with no active/past_due/suspended subscription at all (e.g. only an ended one)Refused, 23514 — nothing to resolvedatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP19-05 an Organization with no active, past-due or suspended Subscription has no lapse to resolve
Staff records a zero amountRefused as a mismatch with the offering price (23514, subscription_payment_amount_mismatch), because zero never equals a price; nothing is writtendatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP19-06 a zero amount is refused as a mismatch with the offering price, never silently accepted; database/tests/contracts/subscription-plans-administration-contracts.ts › ${caseName.slice(0, 7)} the refusal writes nothing: no payment row, no renewal, no reactivation
A non-staff caller invokes record_subscription_payment_as_staffRefused, 42501database/tests/contracts/subscription-plans-administration-contracts.ts › SP19-07 a non-staff identity cannot record a payment
Staff records an amount equal to the offering's price, in the offering's currencyAccepted, whatever scale the amount is written in: the comparison is numeric, never text or floatdatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP19-08 a payment equal to the offering price in the offering currency is accepted whatever scale it is written in; test/e2e/platform-admin-subscription-payment.e2e-spec.ts › accepts a payment equal to the offering price whatever scale the amount is written in
Staff records an amount below, or above, the price of the offering the Subscription is onRefused (23514, subscription_payment_amount_mismatch) before anything is written: no payment row, no renewal, no reactivationdatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP19-09 an amount below the offering price is refused; database/tests/contracts/subscription-plans-administration-contracts.ts › SP19-10 an amount above the offering price is refused; database/tests/contracts/subscription-plans-administration-contracts.ts › ${caseName.slice(0, 7)} the refusal writes nothing: no payment row, no renewal, no reactivation
Staff records the right amount in a currency other than the offering'sRefused (23514, subscription_payment_currency_mismatch), nothing written; a wrong amount in a wrong currency is refused as a currency mismatch firstdatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP19-11 the right amount in another currency is refused; database/tests/contracts/subscription-plans-administration-contracts.ts › SP19-12 a wrong amount in a wrong currency is refused as a currency mismatch first
Staff records a payment that is not the offering price for a suspended Subscription whose Organization is suspendedRefused; the Subscription and the Organization stay suspended. A payment equal to the price reactivates bothdatabase/tests/contracts/subscription-plans-administration-contracts.ts › SP19-13 a wrong amount never renews a suspended Subscription nor reactivates its suspended Organization; database/tests/contracts/subscription-plans-administration-contracts.ts › SP19-14 after the refused payment the Subscription and the Organization are still suspended; test/e2e/platform-admin-subscription-payment.e2e-spec.ts › never reactivates a suspended organization with a payment that is not the offering price; test/e2e/platform-admin-subscription-payment.e2e-spec.ts › reactivates a suspended organization with a payment equal to the offering price
The same refusals over HTTP422 · platform.organization_command_refused with a safe body that names no price, no constraint and no SQL, and one denied platform audit record (action platform.subscription.payment_recorded, actor = the staff member, target = the organization) whose reason code is payment_amount_mismatch or payment_currency_mismatch; the subscription state is unchangedtest/e2e/platform-admin-subscription-payment.e2e-spec.ts › refuses $what, writes nothing and audits the denied attempt; src/modules/platform/infrastructure/subscription-payment/platform-subscription-payment.repository.spec.ts › refuses $constraint as a command refusal that names its reason and reveals nothing else; src/modules/platform/presentation/audit/organization-command-refusal-reason.spec.ts › names the %s refusal so its denied attempt can be audited
A refused payment and the Idempotency-KeyThe refusal does not spend the key: the same key then pays the right price and records one payment; the use case lets the refusal reach the caller without a success audit record or a stored idempotent resulttest/e2e/platform-admin-subscription-payment.e2e-spec.ts › does not spend the Idempotency-Key on a refused payment, so the same key can then pay the right price; src/modules/platform/application/subscription-payment/record-subscription-payment.use-case.spec.ts › lets a %s refusal reach the caller, audits no success and completes no idempotent result
A repeated payment request, and a key reused with a different paymentThe same key and payment answers the same way with one payment row; a different payment on the same key is 409 · idempotency.key_reused and the first payment standstest/e2e/platform-admin-subscription-payment.e2e-spec.ts › answers a repeated request with the same key and payment the same way and records one payment; test/e2e/platform-admin-subscription-payment.e2e-spec.ts › refuses to reuse a key with a different payment and keeps the first one; src/modules/platform/application/subscription-payment/record-subscription-payment.use-case.spec.ts › replays the same key and payment without recording or auditing a second time; src/modules/platform/application/subscription-payment/record-subscription-payment.use-case.spec.ts › refuses the same key reused with a different amount and keeps the first payment
The same real-world payment is registered a second time with the same external reference, under another Idempotency-KeyRefused before anything is written, with a unique violation naming subscription_payments_external_reference_ci_uq: one payment row and one renewal, not twodatabase/tests/contracts/subscription-payment-reference-contracts.ts › RP-02 the same reference registered a second time is refused; database/tests/contracts/subscription-payment-reference-contracts.ts › ${caseName.slice(0, 5)}b the refusal changes nothing: no payment row, no renewal
The reference differs only in case or surrounding whitespace, or another organization already recorded itStill the same reference: the comparison is lower(btrim(...)) and globaldatabase/tests/contracts/subscription-payment-reference-contracts.ts › RP-03 a case and whitespace variant of the reference collides; database/tests/contracts/subscription-payment-reference-contracts.ts › RP-04 the reference is global: another organization cannot reuse it; test/e2e/platform-admin-subscription-payment-reference.e2e-spec.ts › treats a reference that differs only in case and surrounding whitespace as the same one; test/e2e/platform-admin-subscription-payment-reference.e2e-spec.ts › refuses a reference another organization already recorded, because the reference is global
Distinct references, a blank reference, and the shape of the guaranteeDistinct references on one organization are all recorded; a blank reference is refused by subscription_payments_external_reference_chk; the unique index is partial on lower(btrim(external_reference)) where it is not null, and the four-argument function signature no longer existsdatabase/tests/contracts/subscription-payment-reference-contracts.ts › RP-06 distinct references on one organization are all recorded; database/tests/contracts/subscription-payment-reference-contracts.ts › RP-07 the unique index is on the trimmed lowercase reference and ignores missing references; database/tests/contracts/subscription-payment-reference-contracts.ts › RP-08 a blank reference is refused by the check; database/tests/contracts/subscription-payment-reference-contracts.ts › RP-09 the four-argument signature no longer exists
A payment row names a Subscription that belongs to another Organization, written directly by an administratorRefused by subscription_payments_subscription_fk, which is the composite foreign key (subscription_id, organization_id) onto platform.subscriptions (id, organization_id); no row is written, a payment whose Subscription belongs to its own Organization is accepted, the plain subscription_payments_organization_fk is kept, and the staff function still records a payment end to enddatabase/tests/contracts/payment-and-session-schema-integrity-contracts.ts › SI-01 a payment whose subscription belongs to another organization is refused by the composite foreign key; database/tests/contracts/payment-and-session-schema-integrity-contracts.ts › SI-01b the refused cross-organization payment wrote no row; database/tests/contracts/payment-and-session-schema-integrity-contracts.ts › SI-02 a payment whose subscription belongs to its own organization is accepted; database/tests/contracts/payment-and-session-schema-integrity-contracts.ts › SI-03 the staff recording function still records a payment end to end; database/tests/contracts/payment-and-session-schema-integrity-contracts.ts › SI-04 the subscription foreign key is composite on the subscription and its organization; database/tests/contracts/payment-and-session-schema-integrity-contracts.ts › SI-05 the plain organization foreign key is kept
Two payments with the same reference at the same timeExactly one is recorded. On one Subscription the second waits on the Subscription lock and then gets the clean refusal from the function's own check, never a unique violation; on two Subscriptions it waits on the unique index entry and is refused on that constraint, and nothing is renewed for itdatabase/tests/concurrency/subscription-payment-reference-concurrency.ts › sameReferenceOnOneSubscription; database/tests/concurrency/subscription-payment-reference-concurrency.ts › sameReferenceOnTwoSubscriptions
The same refusal over HTTP422 · platform.organization_command_refused with a safe body that names no constraint and no SQL, one denied platform audit record with the reason payment_already_recorded, the subscription state unchanged, and the Idempotency-Key still free for another payment; a payment that commits while the request is in flight is refused the same way (the repository maps the unique violation by constraint name, and no other unique violation) rather than answering 500test/e2e/platform-admin-subscription-payment-reference.e2e-spec.ts › refuses the same reference registered again under another Idempotency-Key and changes nothing; test/e2e/platform-admin-subscription-payment-reference.e2e-spec.ts › does not spend the Idempotency-Key on a refused duplicate, so the same key can then record another payment; test/e2e/platform-admin-subscription-payment-reference.e2e-spec.ts › answers the clean refusal, not a 500, when a payment with the same reference commits while the request is in flight; test/e2e/platform-admin-subscription-payment-reference.e2e-spec.ts › lets exactly one of two simultaneous payments with one reference win, the other a clean refusal and never a 500; src/modules/platform/infrastructure/subscription-payment/platform-subscription-payment.repository.spec.ts › refuses a unique violation of the external reference index as an already recorded payment, revealing nothing else; src/modules/platform/infrastructure/subscription-payment/platform-subscription-payment.repository.spec.ts › does not map $what, which stays the original failure; src/modules/platform/application/subscription-payment/record-subscription-payment.use-case.spec.ts › lets a %s refusal reach the caller, audits no success and completes no idempotent result; src/modules/platform/presentation/audit/organization-command-refusal-reason.spec.ts › names the %s refusal so its denied attempt can be audited
The reference on the request, in the stored payment and in the success audit recordOptional, trimmed, at most 200 characters, never blank, never NUL; stored as written after trimming; the success audit record carries it as the context attribute external_reference; a key reused with another reference is 409 · idempotency.key_reusedsrc/modules/platform/presentation/dtos/record-subscription-payment-request.schema.spec.ts › is optional and reads as null when the request leaves it out; src/modules/platform/presentation/dtos/record-subscription-payment-request.schema.spec.ts › trims the reference the staff member typed; src/modules/platform/presentation/dtos/record-subscription-payment-request.schema.spec.ts › accepts a reference of exactly the maximum length and refuses one more; src/modules/platform/presentation/dtos/record-subscription-payment-request.schema.spec.ts › refuses $what; test/unit/free-text-request-fields.spec.ts › $name: refuses a NUL character; test/e2e/platform-admin-subscription-payment-reference.e2e-spec.ts › records a payment with a reference, stores it trimmed and audits it; test/e2e/platform-admin-subscription-payment-reference.e2e-spec.ts › answers a blank reference with a validation failure and records nothing; test/e2e/platform-admin-subscription-payment-reference.e2e-spec.ts › refuses to reuse a key with another reference and keeps the first payment; src/modules/platform/application/subscription-payment/record-subscription-payment.use-case.spec.ts › hands the external reference to the repository and audits it as a context attribute; src/modules/platform/application/subscription-payment/record-subscription-payment.use-case.spec.ts › refuses the same key reused with a different reference and keeps the first payment; src/modules/platform/infrastructure/subscription-payment/platform-subscription-payment.repository.spec.ts › hands the external reference to the function as its fifth argument, and null when there is none
An organization with no subscription to renew, a caller who is not active staff, a staff member with no live elevationUnchanged: 422 · platform.subscription_payment_refused (no mismatch audit record), 403 · platform.staff_required, 403 · platform.elevation_required; nothing writtentest/e2e/platform-admin-subscription-payment.e2e-spec.ts › answers 422 platform.subscription_payment_refused when the organization has no subscription to renew, and audits no mismatch refusal; test/e2e/platform-admin-subscription-payment.e2e-spec.ts › answers 403 to a caller who is not active staff and records nothing; test/e2e/platform-admin-subscription-payment.e2e-spec.ts › answers 403 to a staff member with no live elevation and records nothing
Two staff members record two payments for the same lapsed period, with no external referenceBoth rows persist; the second reactivation UPDATE is a no-op, not an error: a payment without a reference is never deduplicateddatabase/tests/contracts/subscription-payment-reference-contracts.ts › RP-05 payments without a reference are never deduplicated; test/e2e/platform-admin-subscription-payment-reference.e2e-spec.ts › records payments without a reference as before, however many there are
A payment is recorded racing SP18's apply_grace_expiry() job on the same rowThe payment's SELECT ... FOR UPDATE wins the race; see the Renewal lifecycle concurrency row abovedatabase/tests/concurrency/subscription-grace-expiry-concurrency.ts › paymentVersusGraceExpiryRace; database/tests/concurrency/subscription-grace-expiry-concurrency.ts › graceExpiryVersusPaymentRace

Cross-tenant and concurrency invariants ​

InvariantExpected answerProving test
Organization A reads Organization B's subscription0 rows — subscriptions_tenant_select_policy scopes SELECT to organization_id = current_organization_id() (SP4)—
Direct cross-tenant INSERT/UPDATE on platform.subscriptions as ninaku_runtimeRefused today at the grant level (only SELECT exists); once Group 3 ships, refused by the new function's own identity.current_organization_id() scoping (SP6, SP7)—
Concurrent plan-change commands for the same organizationExactly one current subscription survives; the conditional UPDATE's zero-row result is the losing side's signal (SP6)—
Two organizations sharing the same plan_offering_idEach keeps an independent subscriptions row; subscriptions_one_current_per_organization_uq is scoped per organization, not per offering—
Retrying organization provisioning with the same idempotency keyReturns the same organization_id and the same subscription, never a duplicate (SP3, shared with Organization O1)test/e2e/organizations-provisioning-concurrency.e2e-spec.ts › answers the same organization to every concurrent replay of one key and creates exactly one
A capability/limit override effective-dated window overlapping an existing one for the same organization+capability (or +limit)Refused by the EXCLUDE USING gist constraint on organization_capability_overrides/organization_limit_overrides—
effective_limit/effective_capability called with an organization that has no current or historical subscription at alleffective_capability returns 'disabled'; effective_limit returns no row from the subscription branch (an override, if any, still applies)—
Two platform-admin functions (SP15-SP19) race to change the same target row (a version, an offering, a subscription, an organization's status)Exactly one wins; every staff function repeats the conditional-UPDATE pattern SP6 established, never a bare unconditional UPDATE—
A platform-admin function is called with an organization_id/plan_id/plan_version_id/offering_id naming nothing that existsRefused by the function's own FOUND check, uniformly across SP15-SP19, before any table is touched—
An identity holds both a tenant role_assignments row and an active platform.staff_members row at the same timeThe two authorities never compose — a staff function ignores role_assignments entirely, an org-scoped function ignores staff_members entirely; two unrelated grants on the same person, not a privilege-escalation path—

Gaps ​

NestJS application code now exists for SP13-SP20 (see below); every route in the Derived API's platform-admin group has a controller, use case, port and repository. Group 0's DB-layer seed and provision_organization_for_caller shipped in revision 28 and are proven by database/tests/prepare/prepare-database-contracts.ts. Grouped by family, each remaining line names the lowest layer that could prove it honestly once the corresponding code exists, or the specific rows still missing a test where the family has already shipped:

  • SP1 Define and publish a plan — integration (the version/offering lifecycle triggers are proven against real PostgreSQL, not mockable); the seed-shape half of it is proven by Group 0.
  • SP2 Seed the first real Plan and its Market Offerings — proven by Group 0 (organizationRuntimeProvisionsItselfPerCountry); the GET /plans read model is proven under SP14 (test/e2e/plans-catalog.e2e-spec.ts).
  • SP3 Start a subscription when provisioning an organization — proven by Group 0: caller-scoped happy path per country, idempotent retry, replay mismatch, offering-country mismatch, inactive offering, blocked identity with no partial garbage, and the no-owner-parameter guard. organizationRuntimeCannotProvisionOrganizations still proves platform.provision_organization stays barred. The POST /organizations route is served by the Organization module and its HTTP proof lives under O1 in ORGANIZATION_CASE_MATRIX.md; the zero-trial row of this table still has no dedicated test.
  • SP4 Read my organization's subscription and plan — integration (RLS-dependent read) + e2e (response contract).
  • SP5 Trial expiry — the half-open window is proven by Group 0 (organizationTrialEntitlementFollowsTrialEndsAt); the revised main path (trialing → past_due, not trialing → ended) is proven by SP18-01 (the scheduled-job row of SP5).
  • SP6 Change plan — integration (immutability triggers, conditional-UPDATE concurrency) + a dedicated two-connection concurrency test for the race case, the same pattern test/integration/sign-out.integration-spec.ts already uses for Identity.
  • SP7 Request cancellation of a subscription — integration.
  • SP8 Ask whether a capability is granted — unit (contract composition) + integration (the underlying effective_capability call, already exercised informally by assert_operation_allowed's own tests once they exist).
  • SP9 Ask a plan limit — unit + integration; identity.assert_membership_seat_available is the existing example to extend, not a new pattern to invent.
  • SP10 Authorize an operation — unit (contract composition) + integration; shared proving surface with Organization O34.
  • SP11 Plan version changes after an organization has subscribed — integration (prove the grandfathering property directly, not by absence of a bug report).
  • SP12 Billing and payment collection — not schedulable; no owner, no module, explicitly deferred.
  • SP13 Platform super admin as an actor — shipped and proven by database/tests/contracts/subscription-plans-administration-contracts.ts (SP13-01..SP13-04), plus the foundation SessionGuard in src/foundation/auth/ and PlatformStaffGuard in src/modules/platform/presentation/guards/. Remaining gap: the organization-owner-impersonation and direct-INSERT-by-ninaku_runtime rows have no dedicated test.
  • SP14 List plans available in a country before signup — shipped and proven by test/e2e/plans-catalog.e2e-spec.ts and list-public-plans.use-case.integration-spec.ts; the only case in this revision needing zero new grants.
  • SP15 Create and manage plans, versions, prices, and markets — shipped and proven by database/tests/contracts/subscription-plans-administration-contracts.ts (SP15-05..SP15-18) and database/tests/concurrency/subscription-plans-concurrency.ts, plus src/modules/platform/application/plan-administration/ and PlatformAdminPlansController. Remaining gaps: the two-versions-race-to-publish row, the missing-locale/currency-on-open row, and the offering-amount-immutability row have no dedicated test.
  • SP16 Assign a plan, extend a trial, or change an organization's plan — shipped and proven by subscription-plans-administration-contracts.ts (SP16-01..SP16-10c), plus AssignPlanUseCase/ChangeSubscriptionPlanUseCase/ExtendSubscriptionTrialUseCase and PlatformAdminSubscriptionsController. Remaining gaps: the concurrent-plan-change race and the archived-organization row have no dedicated test.
  • SP17 Suspend and reactivate an organization — shipped and proven by subscription-plans-administration-contracts.ts (SP17-01..SP17-06b), reusing D21's existing triggers (already proven for the member-authored path by administration-contracts.ts) now also exercised from a staff-authored caller. Remaining gap: the archived-organization row has no dedicated test.
  • SP18 Renewal lifecycle — shipped and proven by subscription-plans-administration-contracts.ts (SP18-01..SP18-05) and database/tests/concurrency/subscription-plans-concurrency.ts › subscriptionLifecycleSweepLockRace and database/tests/concurrency/subscription-grace-expiry-concurrency.ts › paymentVersusGraceExpiryRace/graceExpiryVersusPaymentRace/graceExpirySweepOutcomes, confirming the "no ALTER TABLE needed" schema claim directly. Remaining gaps: the late-job-run row, the plan-change-mid-sweep row, and the subscription-does-not-exist row have no dedicated test.
  • SP19 Record a payment — shipped and proven by subscription-plans-administration-contracts.ts (SP19-01..SP19-14) and database/tests/concurrency/subscription-grace-expiry-concurrency.ts › paymentVersusGraceExpiryRace/graceExpiryVersusPaymentRace, plus RecordSubscriptionPaymentUseCase and the payments route on PlatformAdminSubscriptionsController. The optional external reference that stops the same real-world payment renewing twice is proven by database/tests/contracts/subscription-payment-reference-contracts.ts (RP-01..RP-09), database/tests/concurrency/subscription-payment-reference-concurrency.ts and test/e2e/platform-admin-subscription-payment-reference.e2e-spec.ts. Remaining gap: the reference is global and optional, so a payment recorded without one is still not deduplicated, and two payment sources that issue the same reference would collide.
  • SP20 Idempotency-Key and audit for staff commands — shipped and proven for all 14 commands that exist today (database/tests/contracts/subscription-plans-administration-contracts.ts › SP20-01..SP20-03, database/tests/concurrency/subscription-plans-concurrency.ts › staffCommandIdempotencyRace). Remaining gaps: no dedicated integration test that a replayed HTTP call skips the second Audit Record; the Idempotent-Replayed header is not surfaced on the four 204 routes.
  • Cross-tenant and concurrency invariants (all 10 rows) — integration, with the concurrent-plan-change and GiST-exclusion rows still gaps; the platform-admin-race row is now partly covered (offering-publish-versus-version-retire and market-open-versus-market-open both have a dedicated two-connection test), but a subscription-assignment or organization-status race (SP16/SP17, which now exist) has no dedicated two-connection test.

Application Foundation in progress. Tracked in issue #13.