@@ -0,0 +1,92 @@ |
| 1 | +-- SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | +-- |
| 3 | +-- PAYMENTS PRO03 — user billing state table. |
| 4 | +-- |
| 5 | +-- Mirrors org_billing_states minus the seat-specific columns |
| 6 | +-- (billable_seats, seat_snapshot_at). Pro is a single-seat plan by |
| 7 | +-- design; there's no seat reconciliation worker for users. The |
| 8 | +-- billing_subscription_status and billing_lock_reason enums from |
| 9 | +-- 0061 are subject-agnostic and reused as-is. |
| 10 | +-- |
| 11 | +-- Stripe customer-id namespace is global per Stripe account; the |
| 12 | +-- partial-unique indexes on each table (org_billing_states + |
| 13 | +-- user_billing_states) prevent a single customer-id from existing |
| 14 | +-- on both tables. Defensive cross-table validation lands in PRO04's |
| 15 | +-- webhook handler. |
| 16 | + |
| 17 | +-- +goose Up |
| 18 | + |
| 19 | +CREATE TABLE user_billing_states ( |
| 20 | + user_id bigint PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, |
| 21 | + provider billing_provider NOT NULL DEFAULT 'stripe', |
| 22 | + stripe_customer_id text, |
| 23 | + stripe_subscription_id text, |
| 24 | + stripe_subscription_item_id text, |
| 25 | + plan user_plan NOT NULL DEFAULT 'free', |
| 26 | + subscription_status billing_subscription_status NOT NULL DEFAULT 'none', |
| 27 | + current_period_start timestamptz, |
| 28 | + current_period_end timestamptz, |
| 29 | + cancel_at_period_end boolean NOT NULL DEFAULT false, |
| 30 | + trial_end timestamptz, |
| 31 | + past_due_at timestamptz, |
| 32 | + canceled_at timestamptz, |
| 33 | + locked_at timestamptz, |
| 34 | + lock_reason billing_lock_reason, |
| 35 | + grace_until timestamptz, |
| 36 | + last_webhook_event_id text NOT NULL DEFAULT '', |
| 37 | + created_at timestamptz NOT NULL DEFAULT now(), |
| 38 | + updated_at timestamptz NOT NULL DEFAULT now(), |
| 39 | + |
| 40 | + CONSTRAINT user_billing_states_customer_id_not_blank CHECK ( |
| 41 | + stripe_customer_id IS NULL OR char_length(stripe_customer_id) > 0 |
| 42 | + ), |
| 43 | + CONSTRAINT user_billing_states_subscription_id_not_blank CHECK ( |
| 44 | + stripe_subscription_id IS NULL OR char_length(stripe_subscription_id) > 0 |
| 45 | + ), |
| 46 | + CONSTRAINT user_billing_states_subscription_item_id_not_blank CHECK ( |
| 47 | + stripe_subscription_item_id IS NULL OR char_length(stripe_subscription_item_id) > 0 |
| 48 | + ), |
| 49 | + CONSTRAINT user_billing_states_lock_reason_requires_locked CHECK ( |
| 50 | + lock_reason IS NULL OR locked_at IS NOT NULL |
| 51 | + ), |
| 52 | + CONSTRAINT user_billing_states_grace_requires_locked CHECK ( |
| 53 | + grace_until IS NULL OR locked_at IS NOT NULL |
| 54 | + ), |
| 55 | + CONSTRAINT user_billing_states_period_order CHECK ( |
| 56 | + current_period_start IS NULL |
| 57 | + OR current_period_end IS NULL |
| 58 | + OR current_period_start <= current_period_end |
| 59 | + ) |
| 60 | +); |
| 61 | + |
| 62 | +CREATE UNIQUE INDEX user_billing_states_stripe_customer_unique |
| 63 | + ON user_billing_states (stripe_customer_id) |
| 64 | + WHERE stripe_customer_id IS NOT NULL; |
| 65 | + |
| 66 | +CREATE UNIQUE INDEX user_billing_states_stripe_subscription_unique |
| 67 | + ON user_billing_states (stripe_subscription_id) |
| 68 | + WHERE stripe_subscription_id IS NOT NULL; |
| 69 | + |
| 70 | +CREATE UNIQUE INDEX user_billing_states_stripe_subscription_item_unique |
| 71 | + ON user_billing_states (stripe_subscription_item_id) |
| 72 | + WHERE stripe_subscription_item_id IS NOT NULL; |
| 73 | + |
| 74 | +CREATE INDEX user_billing_states_status_idx |
| 75 | + ON user_billing_states (subscription_status, updated_at DESC); |
| 76 | + |
| 77 | +CREATE INDEX user_billing_states_locked_idx |
| 78 | + ON user_billing_states (locked_at) |
| 79 | + WHERE locked_at IS NOT NULL; |
| 80 | + |
| 81 | +CREATE TRIGGER set_updated_at BEFORE UPDATE ON user_billing_states |
| 82 | + FOR EACH ROW EXECUTE FUNCTION tg_set_updated_at(); |
| 83 | + |
| 84 | +-- +goose Down |
| 85 | + |
| 86 | +DROP TRIGGER IF EXISTS set_updated_at ON user_billing_states; |
| 87 | +DROP INDEX IF EXISTS user_billing_states_locked_idx; |
| 88 | +DROP INDEX IF EXISTS user_billing_states_status_idx; |
| 89 | +DROP INDEX IF EXISTS user_billing_states_stripe_subscription_item_unique; |
| 90 | +DROP INDEX IF EXISTS user_billing_states_stripe_subscription_unique; |
| 91 | +DROP INDEX IF EXISTS user_billing_states_stripe_customer_unique; |
| 92 | +DROP TABLE IF EXISTS user_billing_states; |