Tenant billing lifecycle — plans, subscriptions, invoices, expiry#1269
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
<select>s replaced with the Radix dropdown.Backend
GetPlanTermcross-module query.Frontend (admin)
PlanFormDialog(dead routes removed).components/list/select.tsxrewritten on the Radix DropdownMenu primitive (same API) → renew/create-tenant pickers, plan interval, audits/notifications filters, and the invoices status filter all modernized.Tests
tenant-billing.spec.tscovers 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.Renewas an endpoint action verb.Notes
fullstackhero/docsrepo) still need updating for this user-facing feature.Test plan
🤖 Generated with Claude Code