Public Access
1
0

feat: launch Stripe billing flows with lifecycle hardening and analytics

add Stripe checkout, portal, webhook ingestion, and idempotent event persistence

add billing lifecycle state (grace/sync/timeline/admin visibility) and stronger entitlement handling

add analytics event tracking and admin summary APIs plus account/pricing UI integration
This commit is contained in:
pguerrerox
2026-05-22 22:55:04 +00:00
parent 94b8c357b4
commit 5508e15da1
35 changed files with 2851 additions and 50 deletions
+28
View File
@@ -0,0 +1,28 @@
create table if not exists public.billing_webhook_events (
id uuid primary key default gen_random_uuid(),
provider text not null check (provider in ('stripe')),
external_event_id text not null,
event_type text not null,
status text not null check (status in ('received', 'processed', 'failed', 'ignored')) default 'received',
workspace_id uuid references public.workspaces (id) on delete set null,
external_customer_ref text,
external_subscription_ref text,
payload_json jsonb not null,
error_message text,
received_at timestamptz not null default now(),
processed_at timestamptz,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint billing_webhook_events_provider_event_key unique (provider, external_event_id)
);
create index if not exists billing_webhook_events_status_idx on public.billing_webhook_events (status);
create index if not exists billing_webhook_events_workspace_id_idx on public.billing_webhook_events (workspace_id);
create index if not exists billing_webhook_events_customer_ref_idx on public.billing_webhook_events (external_customer_ref);
create index if not exists billing_webhook_events_subscription_ref_idx on public.billing_webhook_events (external_subscription_ref);
drop trigger if exists set_billing_webhook_events_updated_at on public.billing_webhook_events;
create trigger set_billing_webhook_events_updated_at
before update on public.billing_webhook_events
for each row
execute function public.set_updated_at();
@@ -0,0 +1,47 @@
alter table public.workspace_billing_accounts
add column if not exists grace_period_ends_at timestamptz,
add column if not exists pending_plan_code text,
add column if not exists pending_plan_effective_at timestamptz,
add column if not exists billing_sync_status text not null default 'ok' check (billing_sync_status in ('ok', 'stale', 'error')),
add column if not exists last_stripe_sync_at timestamptz;
create table if not exists public.workspace_billing_timeline_events (
id uuid primary key default gen_random_uuid(),
workspace_id uuid not null references public.workspaces (id) on delete cascade,
billing_account_id uuid references public.workspace_billing_accounts (id) on delete set null,
event_type text not null check (
event_type in (
'checkout_completed',
'subscription_created',
'subscription_updated',
'subscription_deleted',
'invoice_paid',
'invoice_payment_failed',
'portal_returned',
'checkout_returned',
'addon_purchased',
'billing_status_changed',
'plan_change_scheduled'
)
),
source text not null check (source in ('stripe', 'app', 'system')),
payload_json jsonb not null default '{}'::jsonb,
external_event_id text,
external_customer_ref text,
external_subscription_ref text,
occurred_at timestamptz not null default now(),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists workspace_billing_accounts_pending_plan_idx on public.workspace_billing_accounts (pending_plan_code);
create index if not exists workspace_billing_accounts_sync_status_idx on public.workspace_billing_accounts (billing_sync_status);
create index if not exists workspace_billing_timeline_events_workspace_id_idx on public.workspace_billing_timeline_events (workspace_id, occurred_at desc);
create index if not exists workspace_billing_timeline_events_event_type_idx on public.workspace_billing_timeline_events (event_type);
create index if not exists workspace_billing_timeline_events_subscription_ref_idx on public.workspace_billing_timeline_events (external_subscription_ref);
drop trigger if exists set_workspace_billing_timeline_events_updated_at on public.workspace_billing_timeline_events;
create trigger set_workspace_billing_timeline_events_updated_at
before update on public.workspace_billing_timeline_events
for each row
execute function public.set_updated_at();
+20
View File
@@ -0,0 +1,20 @@
create table if not exists public.analytics_events (
id uuid primary key default gen_random_uuid(),
event_name text not null,
event_source text not null check (event_source in ('web_app', 'api', 'stripe_webhook', 'system')),
user_id uuid references public.users (id) on delete set null,
workspace_id uuid references public.workspaces (id) on delete set null,
plan_code text,
addon_code text,
resource text,
amount numeric,
currency text,
metadata_json jsonb not null default '{}'::jsonb,
occurred_at timestamptz not null default now(),
created_at timestamptz not null default now()
);
create index if not exists analytics_events_occurred_at_idx on public.analytics_events (occurred_at desc);
create index if not exists analytics_events_event_name_idx on public.analytics_events (event_name);
create index if not exists analytics_events_workspace_id_idx on public.analytics_events (workspace_id);
create index if not exists analytics_events_plan_code_idx on public.analytics_events (plan_code);