Tenant billing completion + activation/impersonation fixes#1270
Merged
Conversation
…nt chip Activate/deactivate now goes through a styled ConfirmDialog (important, blast- radius-y operation) instead of firing immediately. Removes the topbar TENANT pill since the user dropdown already shows the active tenant. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When an operator started impersonation from another app (no dashboard session stashed), ending it minted + installed the operator's account here — dropping a root-tenant SuperAdmin into the tenant dashboard, which login otherwise forbids. Now, with no stash, end-impersonation logs out cleanly instead of restoring. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sessions + Audit trail require elevated permissions (Sessions.ViewAll / AuditTrails.View), so impersonated or limited users landed on a 403 page. The shared SidebarNavBody now hides nav items whose permission the user lacks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The server end call has a 30s timeout, so an intermittently-slow response left the banner stuck on "Ending…". For a cross-app handoff the local impersonation token is disposable, so log out immediately and fire the server end best-effort in the background (grant revocation + audit) instead of blocking the UI on it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Design spec for completing the tenant billing feature to production grade: backend hardening (clock fix, grace header, indexes, AdjustTenantValidity), Phase 3 notifications (scan job + expiry/invoice events + email handlers), Phase 4 PDF invoices (QuestPDF behind IInvoicePdfRenderer), dashboard self-serve view + expiry banners, and the full regression/integration test plan. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… exist) Add the task-decomposed implementation plan for the production-hardening pass and correct the spec: the UsageSnapshot unique index and Subscription partial-unique active index already exist (audit was wrong), so no index migration is needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
RenewAsync derived "now" from DateTime.UtcNow instead of the injected TimeProvider the rest of the service uses, making the renewal stacking math uncontrollable in tests. Add a unit test that pins the clock and asserts the period starts from the injected provider. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Emit X-Subscription-Grace (days left) during the grace window via Response.OnStarting so the signal survives error responses too. - Add AdjustTenantValidityCommand: a root-only operator override that sets a tenant's ValidUpto to an explicit date (backdating allowed) with no invoice, subscription, or renewal event — for comps, support extensions, or immediate expiry. Logs the change for audit. - Tests: grace-header present in-grace / absent when active; adjust-validity future + backdate-to-expire, no-billing-side-effect, 400/401/403 authz; GetStatusAsync expiry-state boundary unit tests (at ValidUpto / grace end). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…art) Cover renewing twice stacks two terms, and renewing a tenant lapsed 30 days ago restarts the term from now rather than the past validity. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- New integration events: TenantNearingExpiry / TenantEnteredGrace / TenantExpired (Multitenancy.Contracts) and InvoiceIssued (Billing.Contracts). - BillingService publishes InvoiceIssued when a subscription invoice is issued. - TenantExpiryScanJob: daily Hangfire scan classifies each active tenant's state, dedups via a new TenantExpiryNotice ledger in TenantDbContext (one notice per tenant/state/validity period, re-arms on renewal), and publishes the matching event. - Notifications module gains email handlers (IMailService) for the 4 events. - Config: Billing:ExpiryNotificationLeadDays (default 7). Critical fix: background-published lifecycle events must set the Finbuckle tenant context before publishing — BaseDbContext (e.g. WebhookDbContext via the webhook fan-out handler) captures the ambient tenant at construction, so a no-context background scope made its query filter NRE. The scan job now installs the tenant context before publish; the webhook fan-out + email handlers then run correctly. Tests: scan-job records the right notice + dedups on re-run + emails the admin; issuing a subscription invoice emails the admin. NoOpMailService now captures sent mail so email behavior is assertable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- IInvoicePdfRenderer + QuestPDF implementation (A4 invoice: header, bill-to,
period, line-items table, subtotal, notes). License set to Community; the
dependency is isolated behind the interface so it's swappable.
- GET /api/v1/billing/invoices/{id}/pdf streams application/pdf. Reuses the
caller-tenant scoping of GetInvoiceById (View is basic), so one endpoint serves
both operators and tenant self-service; cross-tenant ids return 404.
- Tests: renderer produces a valid %PDF for full + empty invoices; endpoint
returns the tenant's own invoice as PDF and 404 for another tenant's.
Note: QuestPDF Community license is free under $1M USD/yr revenue; documented for
downstream commercial users.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GET /api/v1/tenants/me/status resolves the calling tenant from context and returns its plan, validity, and expiry/grace state — powering the dashboard's plan view and expiry warning banner. Tests cover own-status resolution and the unauthenticated 401. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…etion) Dashboard (tenant-facing): - /subscription page: current plan, validity, expiry badge, usage with limits/ overage, recent invoices. - Global expiry/grace banner in the AppShell (InGrace warning + nearing-expiry info), driven by GET /tenants/me/status; dismissible per session. - Invoice detail page with line items + Download PDF. - Fixed SubscriptionStatus enum to match backend; typed invoice line items; getMyInvoices now consumes the paged envelope. Admin (operator): - Download PDF button on invoice detail. - Adjust-validity dialog on tenant detail (operator override, no invoice), gated by the same permission as Renew. - Plan-form client-side validation: non-negative prices + overage rates. Tests: route-mocked Playwright for the dashboard subscription page, expiry banner states, invoice detail + PDF download, admin PDF button, adjust-validity POST, and plan-form negative-price rejection. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| } | ||
| catch (Exception ex) | ||
| { | ||
| logger.LogWarning(ex, "Failed to send {Context} email to {Email}", context, email); |
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
Completes the SaaS tenant billing feature end-to-end and bundles a few pre-existing tenant activation / impersonation fixes that were already on this branch.
Billing completion (the main feature):
TenantService.RenewAsyncnow uses the injectedTimeProvider(wasDateTime.UtcNow);X-Subscription-Grace: <daysLeft>response header during the grace window (set viaResponse.OnStartingso it survives error responses); new operator-onlyAdjustTenantValidityCommand(POST /tenants/{id}/adjust-validity) to set a tenant's expiry directly with no invoice (comps/corrections, backdating allowed).tenant-expiry-scanHangfire job (02:00 UTC) classifies each active tenant as nearing expiry / in grace / expired and emails the tenant admin, deduped once per state per validity window (re-arms on renewal) via aTenantExpiryNoticeledger. Issuing an invoice also emails the tenant. New config keyBilling:ExpiryNotificationLeadDays(default 7).IInvoicePdfRenderer(QuestPDF, Community license set, swappable behind the interface) + tenant-scopedGET /api/v1/billing/invoices/{id}/pdf(404 cross-tenant; one endpoint serves both operator console and tenant self-service).GET /api/v1/tenants/me/status; dashboard/subscriptionpage (plan, validity, usage, recent invoices), global expiry/grace warning banner, invoice detail + PDF download; admin invoice-PDF button, client-side plan-form validation, and an Adjust-validity operator dialog.Production bug fixed along the way: background-published integration events crashed the generic webhook fan-out (it captures the ambient tenant at DbContext construction, which is null in a no-HTTP scope). Background publishers now install the Finbuckle tenant context before publishing.
Also on this branch (pre-existing, unrelated): admin tenant activate/deactivate confirmation + redundant tenant-chip removal, dashboard permission-gating of admin-only nav, and end-impersonation-on-cross-app-handoff fixes.
Test plan
TreatWarningsAsErrors🤖 Generated with Claude Code