tenseleyflow/shithub / 77c4e77

Browse files

billing: snapshot CTE preserves lock columns under past_due transitions

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
77c4e77e3c42caf7cd1efee3a2890957e1d308b6
Parents
d8c2140
Tree
e5ab788

2 changed files

StatusFile+-
M internal/billing/queries/billing.sql 45 6
M internal/billing/sqlc/billing.sql.go 45 6
internal/billing/queries/billing.sqlmodified
@@ -80,9 +80,30 @@ WITH state AS (
8080
                WHEN EXCLUDED.subscription_status = 'past_due' THEN COALESCE(org_billing_states.past_due_at, now())
8181
                ELSE NULL
8282
            END,
83
-           locked_at = NULL,
84
-           lock_reason = NULL,
85
-           grace_until = NULL,
83
+           -- PRO08 D1: never unconditionally NULL the lock columns.
84
+           --   past_due -> preserve any existing lock (MarkPastDue
85
+           --     sets fresh grace_until on the invoice.payment_failed
86
+           --     path; if that hasn't arrived yet, leave NULL).
87
+           --   active / trialing recovering from past_due/unpaid -> clear.
88
+           --   any other transition -> preserve existing values.
89
+           locked_at = CASE
90
+               WHEN EXCLUDED.subscription_status = 'past_due' THEN COALESCE(org_billing_states.locked_at, now())
91
+               WHEN EXCLUDED.subscription_status IN ('active', 'trialing')
92
+                    AND org_billing_states.subscription_status IN ('past_due', 'unpaid', 'incomplete') THEN NULL
93
+               ELSE org_billing_states.locked_at
94
+           END,
95
+           lock_reason = CASE
96
+               WHEN EXCLUDED.subscription_status = 'past_due' THEN COALESCE(org_billing_states.lock_reason, 'past_due'::billing_lock_reason)
97
+               WHEN EXCLUDED.subscription_status IN ('active', 'trialing')
98
+                    AND org_billing_states.subscription_status IN ('past_due', 'unpaid', 'incomplete') THEN NULL
99
+               ELSE org_billing_states.lock_reason
100
+           END,
101
+           grace_until = CASE
102
+               WHEN EXCLUDED.subscription_status = 'past_due' THEN org_billing_states.grace_until
103
+               WHEN EXCLUDED.subscription_status IN ('active', 'trialing')
104
+                    AND org_billing_states.subscription_status IN ('past_due', 'unpaid', 'incomplete') THEN NULL
105
+               ELSE org_billing_states.grace_until
106
+           END,
86107
            updated_at = now()
87108
     RETURNING *
88109
 ), org_update AS (
@@ -571,9 +592,27 @@ WITH state AS (
571592
                WHEN EXCLUDED.subscription_status = 'past_due' THEN COALESCE(user_billing_states.past_due_at, now())
572593
                ELSE NULL
573594
            END,
574
-           locked_at = NULL,
575
-           lock_reason = NULL,
576
-           grace_until = NULL,
595
+           -- PRO08 D1: never unconditionally NULL the lock columns
596
+           -- (mirror of the org-side fix). The Mark* paths own
597
+           -- transitions into/out of the locked state.
598
+           locked_at = CASE
599
+               WHEN EXCLUDED.subscription_status = 'past_due' THEN COALESCE(user_billing_states.locked_at, now())
600
+               WHEN EXCLUDED.subscription_status IN ('active', 'trialing')
601
+                    AND user_billing_states.subscription_status IN ('past_due', 'unpaid', 'incomplete') THEN NULL
602
+               ELSE user_billing_states.locked_at
603
+           END,
604
+           lock_reason = CASE
605
+               WHEN EXCLUDED.subscription_status = 'past_due' THEN COALESCE(user_billing_states.lock_reason, 'past_due'::billing_lock_reason)
606
+               WHEN EXCLUDED.subscription_status IN ('active', 'trialing')
607
+                    AND user_billing_states.subscription_status IN ('past_due', 'unpaid', 'incomplete') THEN NULL
608
+               ELSE user_billing_states.lock_reason
609
+           END,
610
+           grace_until = CASE
611
+               WHEN EXCLUDED.subscription_status = 'past_due' THEN user_billing_states.grace_until
612
+               WHEN EXCLUDED.subscription_status IN ('active', 'trialing')
613
+                    AND user_billing_states.subscription_status IN ('past_due', 'unpaid', 'incomplete') THEN NULL
614
+               ELSE user_billing_states.grace_until
615
+           END,
577616
            updated_at = now()
578617
     RETURNING *
579618
 ), user_update AS (
internal/billing/sqlc/billing.sql.gomodified
@@ -67,9 +67,30 @@ WITH state AS (
6767
                WHEN EXCLUDED.subscription_status = 'past_due' THEN COALESCE(org_billing_states.past_due_at, now())
6868
                ELSE NULL
6969
            END,
70
-           locked_at = NULL,
71
-           lock_reason = NULL,
72
-           grace_until = NULL,
70
+           -- PRO08 D1: never unconditionally NULL the lock columns.
71
+           --   past_due -> preserve any existing lock (MarkPastDue
72
+           --     sets fresh grace_until on the invoice.payment_failed
73
+           --     path; if that hasn't arrived yet, leave NULL).
74
+           --   active / trialing recovering from past_due/unpaid -> clear.
75
+           --   any other transition -> preserve existing values.
76
+           locked_at = CASE
77
+               WHEN EXCLUDED.subscription_status = 'past_due' THEN COALESCE(org_billing_states.locked_at, now())
78
+               WHEN EXCLUDED.subscription_status IN ('active', 'trialing')
79
+                    AND org_billing_states.subscription_status IN ('past_due', 'unpaid', 'incomplete') THEN NULL
80
+               ELSE org_billing_states.locked_at
81
+           END,
82
+           lock_reason = CASE
83
+               WHEN EXCLUDED.subscription_status = 'past_due' THEN COALESCE(org_billing_states.lock_reason, 'past_due'::billing_lock_reason)
84
+               WHEN EXCLUDED.subscription_status IN ('active', 'trialing')
85
+                    AND org_billing_states.subscription_status IN ('past_due', 'unpaid', 'incomplete') THEN NULL
86
+               ELSE org_billing_states.lock_reason
87
+           END,
88
+           grace_until = CASE
89
+               WHEN EXCLUDED.subscription_status = 'past_due' THEN org_billing_states.grace_until
90
+               WHEN EXCLUDED.subscription_status IN ('active', 'trialing')
91
+                    AND org_billing_states.subscription_status IN ('past_due', 'unpaid', 'incomplete') THEN NULL
92
+               ELSE org_billing_states.grace_until
93
+           END,
7394
            updated_at = now()
7495
     RETURNING org_id, provider, stripe_customer_id, stripe_subscription_id, stripe_subscription_item_id, plan, subscription_status, billable_seats, seat_snapshot_at, current_period_start, current_period_end, cancel_at_period_end, trial_end, past_due_at, canceled_at, locked_at, lock_reason, grace_until, last_webhook_event_id, created_at, updated_at
7596
 ), org_update AS (
@@ -217,9 +238,27 @@ WITH state AS (
217238
                WHEN EXCLUDED.subscription_status = 'past_due' THEN COALESCE(user_billing_states.past_due_at, now())
218239
                ELSE NULL
219240
            END,
220
-           locked_at = NULL,
221
-           lock_reason = NULL,
222
-           grace_until = NULL,
241
+           -- PRO08 D1: never unconditionally NULL the lock columns
242
+           -- (mirror of the org-side fix). The Mark* paths own
243
+           -- transitions into/out of the locked state.
244
+           locked_at = CASE
245
+               WHEN EXCLUDED.subscription_status = 'past_due' THEN COALESCE(user_billing_states.locked_at, now())
246
+               WHEN EXCLUDED.subscription_status IN ('active', 'trialing')
247
+                    AND user_billing_states.subscription_status IN ('past_due', 'unpaid', 'incomplete') THEN NULL
248
+               ELSE user_billing_states.locked_at
249
+           END,
250
+           lock_reason = CASE
251
+               WHEN EXCLUDED.subscription_status = 'past_due' THEN COALESCE(user_billing_states.lock_reason, 'past_due'::billing_lock_reason)
252
+               WHEN EXCLUDED.subscription_status IN ('active', 'trialing')
253
+                    AND user_billing_states.subscription_status IN ('past_due', 'unpaid', 'incomplete') THEN NULL
254
+               ELSE user_billing_states.lock_reason
255
+           END,
256
+           grace_until = CASE
257
+               WHEN EXCLUDED.subscription_status = 'past_due' THEN user_billing_states.grace_until
258
+               WHEN EXCLUDED.subscription_status IN ('active', 'trialing')
259
+                    AND user_billing_states.subscription_status IN ('past_due', 'unpaid', 'incomplete') THEN NULL
260
+               ELSE user_billing_states.grace_until
261
+           END,
223262
            updated_at = now()
224263
     RETURNING user_id, provider, stripe_customer_id, stripe_subscription_id, stripe_subscription_item_id, plan, subscription_status, current_period_start, current_period_end, cancel_at_period_end, trial_end, past_due_at, canceled_at, locked_at, lock_reason, grace_until, last_webhook_event_id, created_at, updated_at
225264
 ), user_update AS (