Skip to content

Tenant billing lifecycle — plans, subscriptions, invoices, expiry#1269

Merged
iammukeshm merged 21 commits into
mainfrom
feat/tenant-billing-lifecycle
May 28, 2026
Merged

Tenant billing lifecycle — plans, subscriptions, invoices, expiry#1269
iammukeshm merged 21 commits into
mainfrom
feat/tenant-billing-lifecycle

Conversation

@iammukeshm
Copy link
Copy Markdown
Member

Summary

  • Plan-driven subscriptions — creating a tenant subscribes it to a plan; renewing extends by one plan term (stacking on remaining time) or switches plans, issuing a term invoice.
  • Subscription expiry — configurable grace window; in-grace / expired state is enforced (alongside the tenant-deactivation enforcement on this branch).
  • Richer billing model — plans gain a billing interval + annual price; invoices gain a purpose + term period span; collision-free invoice numbers.
  • Admin billing UI — plan selector, intervals, a renew/change-plan dialog, and plan create/edit reworked as a dialog.
  • Dropdown modernization — all admin form <select>s replaced with the Radix dropdown.

Backend

  • Create-tenant subscribes to a plan; plan-driven renew replaces the explicit-date upgrade.
  • Subscription invoice service + overage-only monthly job; tenant subscribed/renewed events; GetPlanTerm cross-module query.
  • Default-plan + grace-window options; expiry enforcement; seed default plans (free, pro, pro-annual); migration for interval + invoice purpose.

Frontend (admin)

  • Billing lifecycle UI (plan selector, intervals, renew); plan interval + invoice purpose surfaced in billing views.
  • Plan create/edit moved from page-forms to a PlanFormDialog (dead routes removed).
  • components/list/select.tsx rewritten on the Radix DropdownMenu primitive (same API) → renew/create-tenant pickers, plan interval, audits/notifications filters, and the invoices status filter all modernized.

Tests

  • Full admin Playwright suite green locally (96 passing); lint + build clean.
  • New tenant-billing.spec.ts covers create-plan, renew, and the plan dialog (interval-reveals-annual-price, edit-prefill); affected specs realigned from native-<select> to the dropdown's button/menuitem; obsolete plan page-form tests removed.
  • Arch test allows Renew as an endpoint action verb.

Notes

Test plan

  • Backend CI green
  • Frontend CI green (lint + build + Playwright)
  • Integration tests (Testcontainers / Docker)
  • Manual: create tenant → subscribes to plan; renew & switch plan issue term invoices; expiry grace window blocks access after lapse

🤖 Generated with Claude Code

iammukeshm and others added 21 commits May 28, 2026 11:43
Wire the Multitenancy tenant lifecycle to the Billing module: plan-driven
subscriptions + invoices on create/renew, billing-interval term model,
grace-windowed expiry enforcement, expiry notifications, and PDF invoices.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
BillingPlan gains Interval (Monthly/Yearly) + AnnualPrice with TermMonths/
TermPrice helpers; persisted, exposed via DTO/GetPlans, and accepted by
Create/Update plan commands + validators.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Invoice gains Purpose (Subscription/Usage) + PeriodStartUtc/PeriodEndUtc via a
CreateDraft overload; the per-month unique index now includes Purpose so the
subscription and usage streams never collide.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The monthly job (GenerateInvoiceForPeriodAsync) now bills metered overage only
(USG- prefix, Usage purpose) — the plan base fee moves to a new
CreateSubscriptionInvoiceAsync (SUB- prefix, Subscription purpose) invoked on
tenant create/renew, idempotent per term. Updates the two integration tests
that asserted the old base-fee-in-monthly behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Read-only Mediator query in Billing.Contracts returning a plan's interval, term
length, and unit price so Multitenancy can compute tenant validity without a
runtime dependency on Billing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
TenantSubscribed/TenantRenewed integration events (Multitenancy.Contracts) are
handled in Billing to start/swap the active subscription and issue the term's
subscription invoice. Adds a Subscription term-end overload and registers the
handlers via AddIntegrationEventHandlers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Seeds the platform plan catalogue once so the trial fallback ('free') resolves
on a fresh install; includes a yearly plan to exercise the new interval.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Plans.Interval/AnnualPrice and Invoices.Purpose/PeriodStartUtc/PeriodEndUtc;
replaces the per-month unique invoice index with one that includes Purpose.
Existing rows backfill Purpose=Usage and Interval=Monthly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a shared "Billing" config section (DefaultPlanKey, GraceWindowDays) bound
locally in Multitenancy (TenantBillingOptions) and Identity (TenantGraceOptions),
with sensible defaults so config is optional.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CreateTenant now resolves a plan (defaulting to the trial plan), sets ValidUpto
from the plan term, refreshes the tenant cache, and publishes
TenantSubscribedIntegrationEvent so Billing creates the subscription + term
invoice. PlanKey is optional on the command with a slug validator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
RenewTenant extends ValidUpto by one plan term (stacking on remaining time),
optionally switches plan, refreshes the tenant cache (fixing the old upgrade
cache-staleness gap), and publishes TenantRenewedIntegrationEvent. Removes the
explicit-date UpgradeTenant endpoint/handler and its tests; TenantStatusDto now
carries Plan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The post-auth tenant guard now blocks requests past ValidUpto + grace (not just
deactivated tenants); the login/refresh check honors the same grace window so a
lapsed tenant can still authenticate during dunning. GetStatus exposes
Plan/ExpiryState/GraceEndsUtc. Adds expiry-enforcement + end-to-end
tenant-billing integration tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e default

Invoice numbers now derive from a SHA-256 tenant token instead of an 8-char
prefix, so the monthly job no longer clashes on the unique InvoiceNumber index
when many tenants share a prefix. Reorders InvoicePurpose so Usage=0 (the CLR
default) — Subscription(1) is always written explicitly, fixing subscription
invoices being stored as Usage via EF's store-default behavior. Regenerates the
migration and updates two integration tests whose assumptions (no subscription
for new tenants; global generate count) the auto-subscription feature changed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Create-tenant dialog gains a plan selector (preselects the trial plan; optional
so creation never hard-fails if plans are unavailable). Plan form gains billing
interval + annual price. Tenant detail shows plan + expiry/grace badges and a
Renew / change-plan dialog. Extends the billing + tenants API clients.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The admin app's design unification with the dashboard changed copy,
headings, and DOM structure, leaving 42 of 95 Playwright E2E tests
asserting the old UI (the "E2E (admin)" job, and the "Frontend CI"
gate that depends on it, were red).

Update all 16 admin spec files to match the new components:
- removed "// SECTION" mono crumbs -> real EntityPageHeader/SettingsSection headings
- Console -> Overview/Platform Admin; KPI labels via the Stat ".meta" class
- password show-toggle made getByLabel ambiguous -> { exact: true }
- create forms are now modal dialogs and audit detail a side sheet
  -> drive via the trigger + scope to getByRole("dialog")
- /health hard-nav 500s (Vite proxies /health -> API) -> serve the SPA shell
- assert main-page content before opening a modal (Radix aria-hides main)

Selectors verified against the actual components; full admin suite now
95/95 green, eslint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plans list shows the billing interval + per-term price; invoices list and detail
show the invoice purpose (Subscription/Usage) and the term span for subscription
invoices.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the native <select> in components/list/select.tsx with the Radix
DropdownMenu primitive already used by the filter Select, keeping the public
API (value/onValueChange/options/emptyLabel) so every consumer is untouched:
the renew + create-tenant plan pickers, the plan-form interval, and the
audits/notifications filters. Also swap the raw <select> status filter in the
invoices list. Realign the affected E2E specs (audits, notifications,
invoices) from native-select assertions to the dropdown's button/menuitem.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move plan create/edit from the /billing/plans/new and /:id page-forms to a
PlanFormDialog opened from the plans list, and remove the now-dead routes.
Cover the dialog flows (create, interval-reveals-annual-price, edit-prefill)
with new tests in tenant-billing.spec.ts and drop the obsolete page-form
tests from plans.spec.ts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@iammukeshm iammukeshm merged commit a4c9737 into main May 28, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant