Skip to content

Add back-office tenant overview with dashboard, accounts, users, billing events, and Stripe reconciliation#888

Open
tjementum wants to merge 50 commits intomainfrom
back-office-tenant-overview
Open

Add back-office tenant overview with dashboard, accounts, users, billing events, and Stripe reconciliation#888
tjementum wants to merge 50 commits intomainfrom
back-office-tenant-overview

Conversation

@tjementum
Copy link
Copy Markdown
Member

@tjementum tjementum commented May 8, 2026

Summary & Motivation

Builds the cross-tenant back-office surface that platform operators use to monitor revenue, search and inspect customer accounts, audit subscription lifecycle activity, and reconcile local state with Stripe. The base back-office shell and Easy Auth wiring landed earlier in #876; this branch adds every operator-facing page on top of that shell, the append-only BillingEvent aggregate that captures subscription transitions with deterministic IDs, three data-quality banners that surface when Stripe and the local log disagree, and a refactor of the dashboard MRR computation to read history from the event log rather than projecting today's snapshot backward. Sync with Stripe is gated to admins via the existing BackOfficeIdentityDefaults.AdminPolicyName.

Back-office shell additions

  • Side menu entries for the new pages (Accounts, Users, Billing events) wired into the existing BackOfficeSideMenu, plus a Coming soon group for Feature flags, Support, and Wait list.
  • Kiosk mode toggle in BackOfficeAvatarMenu that hides the side menu rail and the mobile floating menu trigger so the dashboard fills the screen for unattended displays. Toggle persists per-browser via the same path used by the zoom level.
  • The data-quality banner stack (described below) is wired into __root.tsx through the shared BannerPortal.
  • An authenticated tenant-logo blob proxy (BackOfficeBlobProxy) is added so the new accounts list and side pane can render tenant logos without leaking signed URLs.

Dashboard

Operator landing page with a period selector (Last 7 / Last 30 / Last 90 days) that drives every card. Each card has skeleton loaders, an empty state, and shares a DashboardCardShell for consistent borders, padding, and section headings. KPI tiles use LinkCard so the full tile is a navigation target; non-link tiles use Card.

  • KPI tiles (/api/back-office/dashboard/kpis): Total accounts (with +N new in last <period> days subtitle and delta vs prior period), Blended MRR (forward-MRR sum across all paid subscriptions; clicks through to /billing-events), Users active in period, Active sessions in last 24 hours.
  • MRR trend (/api/back-office/dashboard/mrr-trend): area chart with Current period vs Prior period overlay. Each daily point is reconstructed from the BillingEvent log per subscription — the latest event with NewAmount set up to end-of-day. The card header shows the latest blended value.
  • Plan distribution (/api/back-office/dashboard/plan-distribution): donut chart of paid vs trial vs canceled accounts.
  • Account growth (/api/back-office/dashboard/trends): stacked bar chart of new tenant signups per day for the period, with prior-period comparison.
  • User logins (/api/back-office/dashboard/trends): area chart of distinct user logins per day with success/failure split.
  • Recent signups (/api/back-office/dashboard/recent-signups): table of the most recent 5 paid signups with tenant name, owner email, plan, and time-since.
  • Recent Stripe events (/api/back-office/dashboard/recent-stripe-events): table of the most recent 10 BillingEvent rows across all tenants — date, event type, plan transition, MRR impact, MRR after — using the same row template as the standalone Billing events page so behaviour is consistent.
  • All charts are rendered through new shared wrappers (Chart.tsx) that default accessibilityLayer={true} so Tab focuses data points instead of the SVG, and apply a global outline fix that suppresses Chrome's blue ring on <svg class="recharts-surface">. Direct recharts imports are now lint-blocked.

Accounts list (/accounts)

  • Toolbar with search-by-name, multi-select Plan filter (Premium / Standard / Basis), multi-select Status filter (Active / Downgrading / Canceling / Canceled / Free), and a Clear filters button. Filter state is persisted in the URL and survives navigation.
  • Table with sortable columns: Name (default sort), Plan, MRR (with strike-through display when a downgrade is scheduled or the subscription is cancelling at period end), Renewal date, Status. Column headers use a shared SortableTableHead primitive.
  • Each row has a tenant logo (resolved through a new authenticated blob proxy at /back-office/blob), a TenantStatusBadge (combines plan + planned change), and a Subscribed since value.
  • Click a row to open the side pane preview (AccountSidePane): tenant logo, plan badge, status, country flag, and AccountSidePaneSections:
    • Plan & revenue: Renewal date · MRR (with forward-MRR strike-through for cancellations and downgrades), Subscribed since · Lifetime value, Last invoice date · Last invoice amount.
    • Owners list with avatar, name, email.
    • Users preview (top three with SidePaneUsersRow) plus a total · active · inactive · pending summary bar.
    • Created with formatted date and a relative <n> days ago.
    • Footer action: "Open account" navigates to the full detail page.
  • Side pane debounces the detail fetch by 200 ms to avoid hammering the API while a user arrows through rows.

Account detail (/accounts/$tenantId)

Header with tenant logo, name, plan and status badges, country flag, "Created" date, and an AccountActionsMenu kebab (admin-only) with the Sync with Stripe action. Three KPI tiles below the header (AccountHealthTiles):

  • Users: <active> / <total> with activation progress bar and <percent>% activation subtitle.
  • Lifetime value: sum of AmountExcludingTax over Succeeded payment transactions, with Since <subscribed date> subtitle.
  • MRR: forward-MRR with strike-through display when downgrading or cancelling, Renews <date> subtitle.

Tabs:

  • Overview: Owners list, Current plan card (price, period end, subscribed since, billing address, status indicator), Invoices preview (latest 2 with a "View all invoices" link), Billing events preview (latest with a link to the full tab).
  • Users: paginated table of all users on the tenant — name, email, role, last sign-in, status (AccountUsersTab).
  • Invoices: full payment history (AccountBillingHistorySection + AccountPaymentRow) with Date, Plan, Amount excluding VAT, VAT, Total, Status, plus action buttons for Invoice PDF and Credit note PDF when present. One row per Stripe transaction (Stripe is the source of truth — refunds are not split into synthetic rows).
  • Billing events: full lifecycle log filtered to this tenant (AccountBillingEventsSection + AccountBillingEventRow) with date, event type badge, plan transition, MRR impact (signed delta), and MRR after.

The detail page is reachable from the accounts list, the side pane, the dashboard's recent signups, the dashboard's recent Stripe events, and a user's tenant memberships.

Users list and user detail (/users, /users/$userId)

  • List toolbar with search by email / first name / last name / account name (debounced, minimum 2 characters), with empty and loading states. Pagination uses a shared TablePagination component.
  • Table columns: Name + email, Account, Role, Last sign-in, Status (UsersTable + UsersTableRow).
  • Detail page header: avatar, full name, email, last-seen relative date.
  • KPI tiles (UserActivityTiles): Tenant memberships count, Active sessions, Logins in last 30 days.
  • Tabs:
    • Overview: Tenant memberships table (UserTenantsSection) with one row per tenant — logo, name, plan, role, MRR contribution, scheduled price (when downgrading), renewal date — clickable to the account detail page. Login history table (UserLoginHistorySection) for the last 30 days, every attempt successful or failed, across email and external providers.
    • Logins: full login history with method (Email, Google, Microsoft), outcome, IP, when, device.
    • Sessions: Active sessions table (UserSessionsSection) — device, browser, IP, last seen, with a Revoked badge for invalidated sessions.

Billing events page (/billing-events)

Cross-tenant lifecycle log scoped to the entire platform.

  • Toolbar (BillingEventsToolbar): search by account name, multi-select Event type filter (covers all 17 BillingEventType values), period selector reusing the dashboard's date range.
  • Table (BillingEventsTable): Date, Event (badge), Account, Plan transition, MRR impact (signed delta), MRR after, with deep-link to the source account on click.
  • Reachable from the dashboard's "View all events" link, the dashboard MRR tile click-through, and direct navigation.

BillingEvent aggregate and Stripe reconciliation pipeline

Append-only audit log of every subscription, payment, and billing transition.

  • Aggregate (Core/Features/Subscriptions/Domain/BillingEvent.cs): BillingEventId is a deterministic SHA-256 hash of (SubscriptionId, EventType, StripeReference, OccurredAt) — webhook redelivery and the historical backfill produce the same ID, so insert is naturally idempotent (PK conflict skipped).
  • Event types (17 total): SubscriptionCreated/Renewed/Upgraded/DowngradeScheduled/DowngradeCancelled/Downgraded/Cancelled/Reactivated/Expired/ImmediatelyCancelled/Suspended, PaymentFailed/Recovered/Refunded, BillingInfoAdded/Updated, PaymentMethodUpdated.
  • Live sync (ProcessPendingStripeEvents): consumes pending stripe_events rows, advances the local subscription state, and appends BillingEvents in lockstep. Each emission carries PreviousAmount, NewAmount, AmountDelta, Currency, plus event-type-specific metadata (days on previous plan, days until effective, scheduled-for date, cancellation reason, suspension reason).
  • Replayer (StripeEventReplayer): backfills BillingEvents from previously stored stripe_events for subscriptions that predate the log. Implemented as a state machine that walks events chronologically and tracks running plan, scheduled plan, cancel-at-period-end flag, and committed MRR — so historical rows reflect the truth at the time they happened.
  • Backfill trigger (BackfillLegacyBillingEventsAsync on ProcessPendingStripeEvents): runs the first time a customer's subscription is synced and has no BillingEvent rows yet. Two-layer idempotency guard: skip-if-any-events at the function level, plus the per-event SHA-256 ID check.
  • Sync command (SyncTenantWithStripe): admin-gated back-office action that runs the live sync forcefully (even with no pending events), then runs the drift detector. Returns BillingEventsAppended, HasDriftDetected, DriftDiscrepancyCount, SyncedAt. Result dialog shows a green check on a clean sync, an amber alert when drift is detected.
  • Plan resolution (StripeClient): PaymentTransaction is enriched with the active plan resolved from the Stripe price catalog — for proration upgrade/downgrade invoices, the algorithm picks the line with the largest positive amount so the row reflects the new plan rather than the credited old plan.

Drift detection and acknowledgment

BillingDriftDetector compares the local subscription state against Stripe history after each sync.

  • Discrepancies are categorized by DriftDiscrepancyKind (MissingEvent, ExtraEvent, plus type-specific cases) with severity (Warning, Critical).
  • Detection result is stored on the subscription as HasDriftDetected + DriftCheckedAt + DriftDiscrepancies (JSONB) so the next sync starts from a known baseline.
  • A MissingEvent discrepancy is raised when a subscription has payment transactions but zero BillingEvents — the back-office Sync action triggers the backfill described above.
  • AcknowledgeBillingDrift command lets an admin clear the flag once the drift is reviewed; it does not modify the underlying discrepancy data.

Data-quality banners

Three global banners portal into a fixed-top slot above the sidebar via the shared BannerPortal, all using the warning palette and the same row template:

  • BillingDriftBanner (/api/back-office/billing-drift/summary): "{N} accounts have billing drift detected." Click-through to /accounts. Fires when any subscription has HasDriftDetected = true.
  • UnsyncedAccountsBanner (/api/back-office/billing-drift/unsynced-summary): "{N} accounts have not been synced yet — MRR trend is incomplete." Fires when any paid subscription has zero billing_events rows. The dashboard MRR trend reads from the event log, so unsynced subscriptions silently under-count it; the banner makes the gap visible.
  • MrrMismatchBanner (/api/back-office/billing-drift/mrr-consistency-summary): "Dashboard MRR mismatch: KPI shows {X}, trend latest shows {Y}." Click-through to /billing-events. Fires when the KPI tile's forward-MRR sum disagrees with the trend's latest blended value — guards against a regression in either calculation, a missing event emission, or direct DB mutation without an event.

All three poll every 60 seconds while the user is signed in and disappear when their condition clears.

MRR trend rewrite, LTV fix, forward-MRR helper

  • MRR trend now reads the BillingEvent log: GetDashboardMrrTrendHandler.ComputeDailyMrr groups events by subscription, sorts by OccurredAt, and for each day picks the latest event with NewAmount set up to end-of-day. Subscriptions backfilled via BackfillLegacyBillingEventsAsync are covered the same way. Replaces the prior approach that summed subscription.CurrentPriceAmount for every day in the period — that approach changed historical points whenever a subscription was cancelled or reactivated.
  • Cancel-at-period-end emission unified: both StripeEventReplayer and the live ProcessPendingStripeEvents paths now emit SubscriptionCancelled with newAmount: 0m. Previously the live path defaulted to null, which made the trend's latest-event-with-NewAmount lookup miss the cancellation.
  • Lifetime value fix (GetTenantDetailHandler): filter to Status == Succeeded, sum AmountExcludingTax. Refunded charges contribute zero (paid + refunded = wash). The previous formula multiplied refunds by -1, which double-counted the refund — under-reported LTV by the refund amount per refunded charge.
  • MrrCalculator.ForwardMrr (new shared helper): per-subscription forward MRR — 0 if cancelling at period end, scheduled price if a downgrade is queued, otherwise the current price. Used by GetDashboardKpisHandler, GetDashboardMrrConsistencySummaryHandler, and the per-account MrrAmount tile in the front-end. Funnelling all callers through one method keeps the KPI and the consistency check from drifting by formula divergence.

Tax breakdown invariant

  • PaymentTransaction.AmountExcludingTax and TaxAmount are non-nullable decimal in the C# domain record. The TenantPaymentTransaction DTO mirrors the change.
  • A CHECK constraint on subscriptions.payment_transactions JSONB enforces the invariant at the database level: NOT jsonb_path_exists(payment_transactions, '$[*] ? (!(@.AmountExcludingTax.type() == "number") || !(@.TaxAmount.type() == "number"))').
  • All construction sites (StripeClient, MockStripeClient, eight test files) updated to pass values. The previous ?? Amount fallback in the LTV formula is gone.

Admin authorization on Sync with Stripe

/{id}/sync-with-stripe chains .RequireAuthorization(BackOfficeIdentityDefaults.AdminPolicyName) on top of the group's regular BackOfficeIdentityDefaults.PolicyName. Authenticated non-admins receive 403; the front-end AccountActionsMenu mirrors the gate by hiding the kebab trigger when me.isAdmin is false. Banner endpoints remain on the regular policy so every authenticated back-office user still sees the warnings.

Banner portal infrastructure

  • The shared BannerPortal renders a <div id="banner-root"> fixed at the top of the viewport with z-40 and measures its own height into a --banner-offset CSS variable.
  • BackOfficeBanners portals into that slot via createPortal, with a useEffect-based target lookup that defers the DOM read until after the parent's commit so the synchronous mount doesn't race the portal target.
  • AppLayout and Sidebar viewport-height calcs (min-h-dvh / min-h-svh) now subtract --banner-offset, so side-pane footers and other bottom-pinned elements remain visible when a banner is active. The fix benefits the user-facing app's existing banner stack as well.

Shared UI primitives

  • New LinkCard wraps Card with a TanStack Router <Link> so an entire card is a navigable surface.
  • Card consolidated to a single primitive used by both apps.
  • Chart wrapper around recharts defaulting accessibilityLayer={true} and re-exporting Bar, Line, Area, Pie, Donut, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer. Direct recharts imports are lint-blocked. A global tailwind.css rule suppresses the unstyleable Chrome focus ring on svg.recharts-surface.
  • Table styling consolidated; TablePagination accepts trackingTitle for telemetry.
  • TenantLogo resolves and proxies tenant blobs through BackOfficeBlobProxy (host-scoped, admin-policy-protected) so logos load with admin auth instead of leaking signed URLs.
  • MultiSelect gains clearAllLabel and an "Apply" affordance. Sidebar gains the --banner-offset-aware height calc described above. Alert adds an info variant.
  • useSmartDate adds locale-aware formatLongDate for headers and a relative-time formatter ("4 days ago") for side-pane and recent-signups rows.

Translations

Comprehensive en-US and da-DK catalogs under application/account/BackOffice/shared/translations/locale/ covering every back-office surface — roughly 350 strings each, including all banner copy, KPI labels, table headers, status badges, dialog text, and tooltips.

Backend test coverage

  • GetTenantsTests, GetTenantDetailTests (incl. refunded LTV), GetTenantPaymentHistoryTests, GetTenantUsersTests, GetTenantUserCountsTests, GetTenantActivityTests.
  • GetBackOfficeUsersTests, GetBackOfficeUserDetailTests.
  • GetDashboardKpisTests (incl. forward-MRR scenarios), GetDashboardMrrTrendTests, GetDashboardPlanDistributionTests, GetDashboardRecentSignupsTests, GetDashboardRecentStripeEventsTests, GetDashboardTrendsTests.
  • GetBackOfficeBillingEventsTests.
  • GetBillingDriftSummaryTests, GetUnsyncedSubscriptionsSummaryTests, GetDashboardMrrConsistencySummaryTests (incl. the divergence path).
  • BillingDriftDetectorTests, BillingEventAppendTests (idempotency), StripeClientTests (proration plan resolution).
  • SyncTenantWithStripeTests (incl. admin-required / non-admin-forbidden / unconfigured-Stripe cases) with TelemetryEventsCollectorSpy assertions on success.

E2E coverage

  • back-office-flows.spec.ts: dashboard, accounts list with filters, account detail tabs, users list and detail, navigation through the side menu.
  • billing-events-flows.spec.ts: filter, search, paginate, click-through to account detail.
  • subscription-flows.spec.ts extended with reconciliation paths.

Database migration

A single consolidated migration AddBackOfficeBillingTracking:

  • Adds subscribed_since and scheduled_price_amount columns to subscriptions.
  • Adds has_drift_detected, drift_checked_at, drift_discrepancies (JSONB) columns and a filtered index on has_drift_detected = true.
  • Backfills subscribed_since = created_at for active paid subscriptions (those with a Stripe subscription id and a non-Basis plan).
  • Creates the billing_events table — strongly-typed-string PK, the 18 columns described in the BillingEvent aggregate, and indexes on subscription_id, tenant_id+occurred_at DESC, and occurred_at DESC.
  • Adds a CHECK constraint on subscriptions.payment_transactions enforcing AmountExcludingTax and TaxAmount are present on every JSONB element.

Dev tooling

  • .mcp.json adds a stripe-development server pointed at the dev sandbox, with the API key sourced from dotnet user-secrets via a small wrapper script so no key is committed.
  • .claude/skills/db-query/SKILL.md adds a guided way to run psql against the local Aspire Postgres for inspection — read-only by default with explicit-permission rules for any write.

Checklist

  • I have added tests, or done manual regression tests
  • I have updated the documentation, if necessary

@tjementum tjementum self-assigned this May 8, 2026
@tjementum tjementum requested a review from a team as a code owner May 8, 2026 02:08
@tjementum tjementum added the Enhancement New feature or request label May 8, 2026
@tjementum tjementum added the Deploy to Staging Set this label on pull requests to deploy code or infrastructure to the Staging environment label May 8, 2026
@tjementum tjementum force-pushed the back-office-tenant-overview branch 2 times, most recently from 6ef04b8 to edc9f2d Compare May 8, 2026 10:10
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 8, 2026

Approve Database Migration account database on stage

The following pending migration(s) will be applied to the database when approved:

  • AddBillingEventsAndDriftDetection (20260508021500_AddBillingEventsAndDriftDetection)

Migration Script

START TRANSACTION;

DO $EF$
BEGIN
    IF NOT EXISTS(SELECT 1 FROM __ef_migrations_history WHERE "migration_id" = '20260508021500_AddBillingEventsAndDriftDetection') THEN
    ALTER TABLE subscriptions ADD subscribed_since timestamptz;
    END IF;
END $EF$;

DO $EF$
BEGIN
    IF NOT EXISTS(SELECT 1 FROM __ef_migrations_history WHERE "migration_id" = '20260508021500_AddBillingEventsAndDriftDetection') THEN
    ALTER TABLE subscriptions ADD scheduled_price_amount numeric(18,2);
    END IF;
END $EF$;

DO $EF$
BEGIN
    IF NOT EXISTS(SELECT 1 FROM __ef_migrations_history WHERE "migration_id" = '20260508021500_AddBillingEventsAndDriftDetection') THEN
    ALTER TABLE subscriptions ADD has_drift_detected boolean NOT NULL DEFAULT FALSE;
    END IF;
END $EF$;

DO $EF$
BEGIN
    IF NOT EXISTS(SELECT 1 FROM __ef_migrations_history WHERE "migration_id" = '20260508021500_AddBillingEventsAndDriftDetection') THEN
    ALTER TABLE subscriptions ADD drift_checked_at timestamptz;
    END IF;
END $EF$;

DO $EF$
BEGIN
    IF NOT EXISTS(SELECT 1 FROM __ef_migrations_history WHERE "migration_id" = '20260508021500_AddBillingEventsAndDriftDetection') THEN
    ALTER TABLE subscriptions ADD drift_discrepancies jsonb NOT NULL DEFAULT '[]';
    END IF;
END $EF$;

DO $EF$
BEGIN
    IF NOT EXISTS(SELECT 1 FROM __ef_migrations_history WHERE "migration_id" = '20260508021500_AddBillingEventsAndDriftDetection') THEN
    CREATE INDEX ix_subscriptions_has_drift_detected ON subscriptions (has_drift_detected) WHERE has_drift_detected = true;
    END IF;
END $EF$;

DO $EF$
BEGIN
    IF NOT EXISTS(SELECT 1 FROM __ef_migrations_history WHERE "migration_id" = '20260508021500_AddBillingEventsAndDriftDetection') THEN
    UPDATE subscriptions
    SET subscribed_since = created_at
    WHERE subscribed_since IS NULL
      AND stripe_subscription_id IS NOT NULL
      AND plan <> 'Basis';
    END IF;
END $EF$;

DO $EF$
BEGIN
    IF NOT EXISTS(SELECT 1 FROM __ef_migrations_history WHERE "migration_id" = '20260508021500_AddBillingEventsAndDriftDetection') THEN
    UPDATE subscriptions
    SET payment_transactions = (
        SELECT jsonb_agg(
            e || jsonb_build_object(
                'AmountExcludingTax', COALESCE((e->>'AmountExcludingTax')::numeric, (e->>'Amount')::numeric, 0),
                'TaxAmount', COALESCE((e->>'TaxAmount')::numeric, 0)
            )
        )
        FROM jsonb_array_elements(payment_transactions) e
    )
    WHERE jsonb_array_length(payment_transactions) > 0
      AND jsonb_path_exists(payment_transactions, '$[*] ? (!(@.AmountExcludingTax.type() == "number") || !(@.TaxAmount.type() == "number"))');
    END IF;
END $EF$;

DO $EF$
BEGIN
    IF NOT EXISTS(SELECT 1 FROM __ef_migrations_history WHERE "migration_id" = '20260508021500_AddBillingEventsAndDriftDetection') THEN
    ALTER TABLE subscriptions ADD CONSTRAINT chk_subscriptions_payment_transactions_tax_breakdown CHECK (NOT jsonb_path_exists(payment_transactions, '$[*] ? (!(@.AmountExcludingTax.type() == "number") || !(@.TaxAmount.type() == "number"))'));
    END IF;
END $EF$;

DO $EF$
BEGIN
    IF NOT EXISTS(SELECT 1 FROM __ef_migrations_history WHERE "migration_id" = '20260508021500_AddBillingEventsAndDriftDetection') THEN
    CREATE TABLE billing_events (
        tenant_id bigint NOT NULL,
        id text NOT NULL,
        subscription_id text NOT NULL,
        created_at timestamptz NOT NULL,
        modified_at timestamptz,
        event_type text NOT NULL,
        from_plan text,
        to_plan text,
        previous_amount numeric(18,2),
        new_amount numeric(18,2),
        amount_delta numeric(18,2),
        currency text,
        days_on_previous_plan integer,
        days_until_effective integer,
        days_since_cancelled integer,
        scheduled_for timestamptz,
        effective_at timestamptz,
        occurred_at timestamptz NOT NULL,
        cancellation_reason text,
        suspension_reason text,
        stripe_reference text NOT NULL,
        CONSTRAINT pk_billing_events PRIMARY KEY (id)
    );
    END IF;
END $EF$;

DO $EF$
BEGIN
    IF NOT EXISTS(SELECT 1 FROM __ef_migrations_history WHERE "migration_id" = '20260508021500_AddBillingEventsAndDriftDetection') THEN
    CREATE INDEX ix_billing_events_tenant_id_occurred_at ON billing_events (tenant_id, occurred_at DESC);
    END IF;
END $EF$;

DO $EF$
BEGIN
    IF NOT EXISTS(SELECT 1 FROM __ef_migrations_history WHERE "migration_id" = '20260508021500_AddBillingEventsAndDriftDetection') THEN
    CREATE INDEX ix_billing_events_occurred_at ON billing_events (occurred_at DESC);
    END IF;
END $EF$;

DO $EF$
BEGIN
    IF NOT EXISTS(SELECT 1 FROM __ef_migrations_history WHERE "migration_id" = '20260508021500_AddBillingEventsAndDriftDetection') THEN
    CREATE INDEX ix_billing_events_subscription_id ON billing_events (subscription_id);
    END IF;
END $EF$;

DO $EF$
BEGIN
    IF NOT EXISTS(SELECT 1 FROM __ef_migrations_history WHERE "migration_id" = '20260508021500_AddBillingEventsAndDriftDetection') THEN
    INSERT INTO __ef_migrations_history (migration_id, product_version)
    VALUES ('20260508021500_AddBillingEventsAndDriftDetection', '10.0.7');
    END IF;
END $EF$;
COMMIT;

@tjementum tjementum force-pushed the back-office-tenant-overview branch 2 times, most recently from 54f9b4e to fc0b457 Compare May 8, 2026 19:52
@tjementum tjementum removed the Deploy to Staging Set this label on pull requests to deploy code or infrastructure to the Staging environment label May 8, 2026
tjementum added 25 commits May 8, 2026 23:40
…illing event log, drift detection, forward MRR, and non-nullable tax breakdown invariant
@tjementum tjementum force-pushed the back-office-tenant-overview branch from fc0b457 to ac23c03 Compare May 8, 2026 21:46
@tjementum tjementum force-pushed the back-office-tenant-overview branch from ac23c03 to 8b2c185 Compare May 8, 2026 22:51
@tjementum tjementum moved this to 🏗 In Progress in Kanban board May 8, 2026
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented May 8, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Enhancement New feature or request

Projects

Status: 🏗 In Progress

Development

Successfully merging this pull request may close these issues.

1 participant