@@ -67,9 +67,30 @@ WITH state AS ( |
| 67 | WHEN EXCLUDED.subscription_status = 'past_due' THEN COALESCE(org_billing_states.past_due_at, now()) | 67 | WHEN EXCLUDED.subscription_status = 'past_due' THEN COALESCE(org_billing_states.past_due_at, now()) |
| 68 | ELSE NULL | 68 | ELSE NULL |
| 69 | END, | 69 | END, |
| 70 | - locked_at = NULL, | 70 | + -- PRO08 D1: never unconditionally NULL the lock columns. |
| 71 | - lock_reason = NULL, | 71 | + -- past_due -> preserve any existing lock (MarkPastDue |
| 72 | - grace_until = NULL, | 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, |
| 73 | updated_at = now() | 94 | updated_at = now() |
| 74 | 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 | 95 | 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 |
| 75 | ), org_update AS ( | 96 | ), org_update AS ( |
@@ -217,9 +238,27 @@ WITH state AS ( |
| 217 | WHEN EXCLUDED.subscription_status = 'past_due' THEN COALESCE(user_billing_states.past_due_at, now()) | 238 | WHEN EXCLUDED.subscription_status = 'past_due' THEN COALESCE(user_billing_states.past_due_at, now()) |
| 218 | ELSE NULL | 239 | ELSE NULL |
| 219 | END, | 240 | END, |
| 220 | - locked_at = NULL, | 241 | + -- PRO08 D1: never unconditionally NULL the lock columns |
| 221 | - lock_reason = NULL, | 242 | + -- (mirror of the org-side fix). The Mark* paths own |
| 222 | - grace_until = NULL, | 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, |
| 223 | updated_at = now() | 262 | updated_at = now() |
| 224 | 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 | 263 | 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 |
| 225 | ), user_update AS ( | 264 | ), user_update AS ( |