tenseleyflow/shithub / 70c62bd

Browse files

migrations: user_billing_states table (mirrors org shape minus seat columns)

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
70c62bd04f47f7f1a59fad3edd492f1f1ce321ff
Parents
75b7d7c
Tree
2e3c57e

1 changed file

StatusFile+-
A internal/migrationsfs/migrations/0072_user_billing_states.sql 92 0
internal/migrationsfs/migrations/0072_user_billing_states.sqladded
@@ -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;