@@ -161,6 +161,155 @@ func (q *Queries) ApplySubscriptionSnapshot(ctx context.Context, db DBTX, arg Ap |
| 161 | return i, err | 161 | return i, err |
| 162 | } | 162 | } |
| 163 | | 163 | |
| | 164 | +const applyUserSubscriptionSnapshot = `-- name: ApplyUserSubscriptionSnapshot :one |
| | 165 | +WITH state AS ( |
| | 166 | + INSERT INTO user_billing_states ( |
| | 167 | + user_id, |
| | 168 | + provider, |
| | 169 | + plan, |
| | 170 | + subscription_status, |
| | 171 | + stripe_subscription_id, |
| | 172 | + stripe_subscription_item_id, |
| | 173 | + current_period_start, |
| | 174 | + current_period_end, |
| | 175 | + cancel_at_period_end, |
| | 176 | + trial_end, |
| | 177 | + canceled_at, |
| | 178 | + last_webhook_event_id, |
| | 179 | + past_due_at, |
| | 180 | + locked_at, |
| | 181 | + lock_reason, |
| | 182 | + grace_until |
| | 183 | + ) |
| | 184 | + VALUES ( |
| | 185 | + $1::bigint, |
| | 186 | + 'stripe', |
| | 187 | + $2::user_plan, |
| | 188 | + $3::billing_subscription_status, |
| | 189 | + $4::text, |
| | 190 | + $5::text, |
| | 191 | + $6::timestamptz, |
| | 192 | + $7::timestamptz, |
| | 193 | + $8::boolean, |
| | 194 | + $9::timestamptz, |
| | 195 | + $10::timestamptz, |
| | 196 | + $11::text, |
| | 197 | + CASE |
| | 198 | + WHEN $3::billing_subscription_status = 'past_due' THEN now() |
| | 199 | + ELSE NULL |
| | 200 | + END, |
| | 201 | + NULL, |
| | 202 | + NULL, |
| | 203 | + NULL |
| | 204 | + ) |
| | 205 | + ON CONFLICT (user_id) DO UPDATE |
| | 206 | + SET plan = EXCLUDED.plan, |
| | 207 | + subscription_status = EXCLUDED.subscription_status, |
| | 208 | + stripe_subscription_id = EXCLUDED.stripe_subscription_id, |
| | 209 | + stripe_subscription_item_id = EXCLUDED.stripe_subscription_item_id, |
| | 210 | + current_period_start = EXCLUDED.current_period_start, |
| | 211 | + current_period_end = EXCLUDED.current_period_end, |
| | 212 | + cancel_at_period_end = EXCLUDED.cancel_at_period_end, |
| | 213 | + trial_end = EXCLUDED.trial_end, |
| | 214 | + canceled_at = EXCLUDED.canceled_at, |
| | 215 | + last_webhook_event_id = EXCLUDED.last_webhook_event_id, |
| | 216 | + past_due_at = CASE |
| | 217 | + WHEN EXCLUDED.subscription_status = 'past_due' THEN COALESCE(user_billing_states.past_due_at, now()) |
| | 218 | + ELSE NULL |
| | 219 | + END, |
| | 220 | + locked_at = NULL, |
| | 221 | + lock_reason = NULL, |
| | 222 | + grace_until = NULL, |
| | 223 | + 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 |
| | 225 | +), user_update AS ( |
| | 226 | + UPDATE users |
| | 227 | + SET plan = $2::user_plan, |
| | 228 | + updated_at = now() |
| | 229 | + WHERE id = $1::bigint |
| | 230 | + RETURNING id |
| | 231 | +) |
| | 232 | +SELECT 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 FROM state |
| | 233 | +` |
| | 234 | + |
| | 235 | +type ApplyUserSubscriptionSnapshotParams struct { |
| | 236 | + UserID int64 |
| | 237 | + Plan UserPlan |
| | 238 | + SubscriptionStatus BillingSubscriptionStatus |
| | 239 | + StripeSubscriptionID pgtype.Text |
| | 240 | + StripeSubscriptionItemID pgtype.Text |
| | 241 | + CurrentPeriodStart pgtype.Timestamptz |
| | 242 | + CurrentPeriodEnd pgtype.Timestamptz |
| | 243 | + CancelAtPeriodEnd bool |
| | 244 | + TrialEnd pgtype.Timestamptz |
| | 245 | + CanceledAt pgtype.Timestamptz |
| | 246 | + LastWebhookEventID string |
| | 247 | +} |
| | 248 | + |
| | 249 | +type ApplyUserSubscriptionSnapshotRow struct { |
| | 250 | + UserID int64 |
| | 251 | + Provider BillingProvider |
| | 252 | + StripeCustomerID pgtype.Text |
| | 253 | + StripeSubscriptionID pgtype.Text |
| | 254 | + StripeSubscriptionItemID pgtype.Text |
| | 255 | + Plan UserPlan |
| | 256 | + SubscriptionStatus BillingSubscriptionStatus |
| | 257 | + CurrentPeriodStart pgtype.Timestamptz |
| | 258 | + CurrentPeriodEnd pgtype.Timestamptz |
| | 259 | + CancelAtPeriodEnd bool |
| | 260 | + TrialEnd pgtype.Timestamptz |
| | 261 | + PastDueAt pgtype.Timestamptz |
| | 262 | + CanceledAt pgtype.Timestamptz |
| | 263 | + LockedAt pgtype.Timestamptz |
| | 264 | + LockReason NullBillingLockReason |
| | 265 | + GraceUntil pgtype.Timestamptz |
| | 266 | + LastWebhookEventID string |
| | 267 | + CreatedAt pgtype.Timestamptz |
| | 268 | + UpdatedAt pgtype.Timestamptz |
| | 269 | +} |
| | 270 | + |
| | 271 | +// Mirrors ApplySubscriptionSnapshot for orgs minus the seat columns |
| | 272 | +// and with `user_plan` as the plan enum. The same CTE pattern keeps |
| | 273 | +// users.plan and user_billing_states.plan atomic. |
| | 274 | +func (q *Queries) ApplyUserSubscriptionSnapshot(ctx context.Context, db DBTX, arg ApplyUserSubscriptionSnapshotParams) (ApplyUserSubscriptionSnapshotRow, error) { |
| | 275 | + row := db.QueryRow(ctx, applyUserSubscriptionSnapshot, |
| | 276 | + arg.UserID, |
| | 277 | + arg.Plan, |
| | 278 | + arg.SubscriptionStatus, |
| | 279 | + arg.StripeSubscriptionID, |
| | 280 | + arg.StripeSubscriptionItemID, |
| | 281 | + arg.CurrentPeriodStart, |
| | 282 | + arg.CurrentPeriodEnd, |
| | 283 | + arg.CancelAtPeriodEnd, |
| | 284 | + arg.TrialEnd, |
| | 285 | + arg.CanceledAt, |
| | 286 | + arg.LastWebhookEventID, |
| | 287 | + ) |
| | 288 | + var i ApplyUserSubscriptionSnapshotRow |
| | 289 | + err := row.Scan( |
| | 290 | + &i.UserID, |
| | 291 | + &i.Provider, |
| | 292 | + &i.StripeCustomerID, |
| | 293 | + &i.StripeSubscriptionID, |
| | 294 | + &i.StripeSubscriptionItemID, |
| | 295 | + &i.Plan, |
| | 296 | + &i.SubscriptionStatus, |
| | 297 | + &i.CurrentPeriodStart, |
| | 298 | + &i.CurrentPeriodEnd, |
| | 299 | + &i.CancelAtPeriodEnd, |
| | 300 | + &i.TrialEnd, |
| | 301 | + &i.PastDueAt, |
| | 302 | + &i.CanceledAt, |
| | 303 | + &i.LockedAt, |
| | 304 | + &i.LockReason, |
| | 305 | + &i.GraceUntil, |
| | 306 | + &i.LastWebhookEventID, |
| | 307 | + &i.CreatedAt, |
| | 308 | + &i.UpdatedAt, |
| | 309 | + ) |
| | 310 | + return i, err |
| | 311 | +} |
| | 312 | + |
| 164 | const clearBillingLock = `-- name: ClearBillingLock :one | 313 | const clearBillingLock = `-- name: ClearBillingLock :one |
| 165 | WITH state AS ( | 314 | WITH state AS ( |
| 166 | UPDATE org_billing_states | 315 | UPDATE org_billing_states |
@@ -242,6 +391,83 @@ func (q *Queries) ClearBillingLock(ctx context.Context, db DBTX, orgID int64) (C |
| 242 | return i, err | 391 | return i, err |
| 243 | } | 392 | } |
| 244 | | 393 | |
| | 394 | +const clearUserBillingLock = `-- name: ClearUserBillingLock :one |
| | 395 | +WITH state AS ( |
| | 396 | + UPDATE user_billing_states |
| | 397 | + SET plan = CASE |
| | 398 | + WHEN subscription_status = 'canceled' THEN 'free' |
| | 399 | + ELSE plan |
| | 400 | + END, |
| | 401 | + subscription_status = CASE |
| | 402 | + WHEN subscription_status = 'canceled' THEN 'none' |
| | 403 | + ELSE subscription_status |
| | 404 | + END, |
| | 405 | + locked_at = NULL, |
| | 406 | + lock_reason = NULL, |
| | 407 | + grace_until = NULL, |
| | 408 | + updated_at = now() |
| | 409 | + WHERE user_id = $1 |
| | 410 | + 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 |
| | 411 | +), user_update AS ( |
| | 412 | + UPDATE users |
| | 413 | + SET plan = state.plan, |
| | 414 | + updated_at = now() |
| | 415 | + FROM state |
| | 416 | + WHERE users.id = state.user_id |
| | 417 | + RETURNING users.id |
| | 418 | +) |
| | 419 | +SELECT 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 FROM state |
| | 420 | +` |
| | 421 | + |
| | 422 | +type ClearUserBillingLockRow struct { |
| | 423 | + UserID int64 |
| | 424 | + Provider BillingProvider |
| | 425 | + StripeCustomerID pgtype.Text |
| | 426 | + StripeSubscriptionID pgtype.Text |
| | 427 | + StripeSubscriptionItemID pgtype.Text |
| | 428 | + Plan UserPlan |
| | 429 | + SubscriptionStatus BillingSubscriptionStatus |
| | 430 | + CurrentPeriodStart pgtype.Timestamptz |
| | 431 | + CurrentPeriodEnd pgtype.Timestamptz |
| | 432 | + CancelAtPeriodEnd bool |
| | 433 | + TrialEnd pgtype.Timestamptz |
| | 434 | + PastDueAt pgtype.Timestamptz |
| | 435 | + CanceledAt pgtype.Timestamptz |
| | 436 | + LockedAt pgtype.Timestamptz |
| | 437 | + LockReason NullBillingLockReason |
| | 438 | + GraceUntil pgtype.Timestamptz |
| | 439 | + LastWebhookEventID string |
| | 440 | + CreatedAt pgtype.Timestamptz |
| | 441 | + UpdatedAt pgtype.Timestamptz |
| | 442 | +} |
| | 443 | + |
| | 444 | +func (q *Queries) ClearUserBillingLock(ctx context.Context, db DBTX, userID int64) (ClearUserBillingLockRow, error) { |
| | 445 | + row := db.QueryRow(ctx, clearUserBillingLock, userID) |
| | 446 | + var i ClearUserBillingLockRow |
| | 447 | + err := row.Scan( |
| | 448 | + &i.UserID, |
| | 449 | + &i.Provider, |
| | 450 | + &i.StripeCustomerID, |
| | 451 | + &i.StripeSubscriptionID, |
| | 452 | + &i.StripeSubscriptionItemID, |
| | 453 | + &i.Plan, |
| | 454 | + &i.SubscriptionStatus, |
| | 455 | + &i.CurrentPeriodStart, |
| | 456 | + &i.CurrentPeriodEnd, |
| | 457 | + &i.CancelAtPeriodEnd, |
| | 458 | + &i.TrialEnd, |
| | 459 | + &i.PastDueAt, |
| | 460 | + &i.CanceledAt, |
| | 461 | + &i.LockedAt, |
| | 462 | + &i.LockReason, |
| | 463 | + &i.GraceUntil, |
| | 464 | + &i.LastWebhookEventID, |
| | 465 | + &i.CreatedAt, |
| | 466 | + &i.UpdatedAt, |
| | 467 | + ) |
| | 468 | + return i, err |
| | 469 | +} |
| | 470 | + |
| 245 | const countBillableOrgMembers = `-- name: CountBillableOrgMembers :one | 471 | const countBillableOrgMembers = `-- name: CountBillableOrgMembers :one |
| 246 | SELECT count(*)::integer | 472 | SELECT count(*)::integer |
| 247 | FROM org_members | 473 | FROM org_members |
@@ -363,7 +589,7 @@ VALUES ( |
| 363 | $4::jsonb | 589 | $4::jsonb |
| 364 | ) | 590 | ) |
| 365 | ON CONFLICT (provider, provider_event_id) DO NOTHING | 591 | ON CONFLICT (provider, provider_event_id) DO NOTHING |
| 366 | -RETURNING id, provider, provider_event_id, event_type, api_version, payload, received_at, processed_at, process_error, processing_attempts | 592 | +RETURNING id, provider, provider_event_id, event_type, api_version, payload, received_at, processed_at, process_error, processing_attempts, subject_kind, subject_id |
| 367 | ` | 593 | ` |
| 368 | | 594 | |
| 369 | type CreateWebhookEventReceiptParams struct { | 595 | type CreateWebhookEventReceiptParams struct { |
@@ -393,6 +619,8 @@ func (q *Queries) CreateWebhookEventReceipt(ctx context.Context, db DBTX, arg Cr |
| 393 | &i.ProcessedAt, | 619 | &i.ProcessedAt, |
| 394 | &i.ProcessError, | 620 | &i.ProcessError, |
| 395 | &i.ProcessingAttempts, | 621 | &i.ProcessingAttempts, |
| | 622 | + &i.SubjectKind, |
| | 623 | + &i.SubjectID, |
| 396 | ) | 624 | ) |
| 397 | return i, err | 625 | return i, err |
| 398 | } | 626 | } |
@@ -504,8 +732,107 @@ func (q *Queries) GetOrgBillingStateByStripeSubscription(ctx context.Context, db |
| 504 | return i, err | 732 | return i, err |
| 505 | } | 733 | } |
| 506 | | 734 | |
| | 735 | +const getUserBillingState = `-- name: GetUserBillingState :one |
| | 736 | + |
| | 737 | +SELECT 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 FROM user_billing_states WHERE user_id = $1 |
| | 738 | +` |
| | 739 | + |
| | 740 | +// ─── user_billing_states (PRO03) ────────────────────────────────── |
| | 741 | +func (q *Queries) GetUserBillingState(ctx context.Context, db DBTX, userID int64) (UserBillingState, error) { |
| | 742 | + row := db.QueryRow(ctx, getUserBillingState, userID) |
| | 743 | + var i UserBillingState |
| | 744 | + err := row.Scan( |
| | 745 | + &i.UserID, |
| | 746 | + &i.Provider, |
| | 747 | + &i.StripeCustomerID, |
| | 748 | + &i.StripeSubscriptionID, |
| | 749 | + &i.StripeSubscriptionItemID, |
| | 750 | + &i.Plan, |
| | 751 | + &i.SubscriptionStatus, |
| | 752 | + &i.CurrentPeriodStart, |
| | 753 | + &i.CurrentPeriodEnd, |
| | 754 | + &i.CancelAtPeriodEnd, |
| | 755 | + &i.TrialEnd, |
| | 756 | + &i.PastDueAt, |
| | 757 | + &i.CanceledAt, |
| | 758 | + &i.LockedAt, |
| | 759 | + &i.LockReason, |
| | 760 | + &i.GraceUntil, |
| | 761 | + &i.LastWebhookEventID, |
| | 762 | + &i.CreatedAt, |
| | 763 | + &i.UpdatedAt, |
| | 764 | + ) |
| | 765 | + return i, err |
| | 766 | +} |
| | 767 | + |
| | 768 | +const getUserBillingStateByStripeCustomer = `-- name: GetUserBillingStateByStripeCustomer :one |
| | 769 | +SELECT 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 FROM user_billing_states |
| | 770 | +WHERE provider = 'stripe' |
| | 771 | + AND stripe_customer_id = $1 |
| | 772 | +` |
| | 773 | + |
| | 774 | +func (q *Queries) GetUserBillingStateByStripeCustomer(ctx context.Context, db DBTX, stripeCustomerID pgtype.Text) (UserBillingState, error) { |
| | 775 | + row := db.QueryRow(ctx, getUserBillingStateByStripeCustomer, stripeCustomerID) |
| | 776 | + var i UserBillingState |
| | 777 | + err := row.Scan( |
| | 778 | + &i.UserID, |
| | 779 | + &i.Provider, |
| | 780 | + &i.StripeCustomerID, |
| | 781 | + &i.StripeSubscriptionID, |
| | 782 | + &i.StripeSubscriptionItemID, |
| | 783 | + &i.Plan, |
| | 784 | + &i.SubscriptionStatus, |
| | 785 | + &i.CurrentPeriodStart, |
| | 786 | + &i.CurrentPeriodEnd, |
| | 787 | + &i.CancelAtPeriodEnd, |
| | 788 | + &i.TrialEnd, |
| | 789 | + &i.PastDueAt, |
| | 790 | + &i.CanceledAt, |
| | 791 | + &i.LockedAt, |
| | 792 | + &i.LockReason, |
| | 793 | + &i.GraceUntil, |
| | 794 | + &i.LastWebhookEventID, |
| | 795 | + &i.CreatedAt, |
| | 796 | + &i.UpdatedAt, |
| | 797 | + ) |
| | 798 | + return i, err |
| | 799 | +} |
| | 800 | + |
| | 801 | +const getUserBillingStateByStripeSubscription = `-- name: GetUserBillingStateByStripeSubscription :one |
| | 802 | +SELECT 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 FROM user_billing_states |
| | 803 | +WHERE provider = 'stripe' |
| | 804 | + AND stripe_subscription_id = $1 |
| | 805 | +` |
| | 806 | + |
| | 807 | +func (q *Queries) GetUserBillingStateByStripeSubscription(ctx context.Context, db DBTX, stripeSubscriptionID pgtype.Text) (UserBillingState, error) { |
| | 808 | + row := db.QueryRow(ctx, getUserBillingStateByStripeSubscription, stripeSubscriptionID) |
| | 809 | + var i UserBillingState |
| | 810 | + err := row.Scan( |
| | 811 | + &i.UserID, |
| | 812 | + &i.Provider, |
| | 813 | + &i.StripeCustomerID, |
| | 814 | + &i.StripeSubscriptionID, |
| | 815 | + &i.StripeSubscriptionItemID, |
| | 816 | + &i.Plan, |
| | 817 | + &i.SubscriptionStatus, |
| | 818 | + &i.CurrentPeriodStart, |
| | 819 | + &i.CurrentPeriodEnd, |
| | 820 | + &i.CancelAtPeriodEnd, |
| | 821 | + &i.TrialEnd, |
| | 822 | + &i.PastDueAt, |
| | 823 | + &i.CanceledAt, |
| | 824 | + &i.LockedAt, |
| | 825 | + &i.LockReason, |
| | 826 | + &i.GraceUntil, |
| | 827 | + &i.LastWebhookEventID, |
| | 828 | + &i.CreatedAt, |
| | 829 | + &i.UpdatedAt, |
| | 830 | + ) |
| | 831 | + return i, err |
| | 832 | +} |
| | 833 | + |
| 507 | const getWebhookEventReceipt = `-- name: GetWebhookEventReceipt :one | 834 | const getWebhookEventReceipt = `-- name: GetWebhookEventReceipt :one |
| 508 | -SELECT id, provider, provider_event_id, event_type, api_version, payload, received_at, processed_at, process_error, processing_attempts FROM billing_webhook_events | 835 | +SELECT id, provider, provider_event_id, event_type, api_version, payload, received_at, processed_at, process_error, processing_attempts, subject_kind, subject_id FROM billing_webhook_events |
| 509 | WHERE provider = 'stripe' | 836 | WHERE provider = 'stripe' |
| 510 | AND provider_event_id = $1 | 837 | AND provider_event_id = $1 |
| 511 | ` | 838 | ` |
@@ -524,24 +851,92 @@ func (q *Queries) GetWebhookEventReceipt(ctx context.Context, db DBTX, providerE |
| 524 | &i.ProcessedAt, | 851 | &i.ProcessedAt, |
| 525 | &i.ProcessError, | 852 | &i.ProcessError, |
| 526 | &i.ProcessingAttempts, | 853 | &i.ProcessingAttempts, |
| | 854 | + &i.SubjectKind, |
| | 855 | + &i.SubjectID, |
| 527 | ) | 856 | ) |
| 528 | return i, err | 857 | return i, err |
| 529 | } | 858 | } |
| 530 | | 859 | |
| 531 | const listInvoicesForOrg = `-- name: ListInvoicesForOrg :many | 860 | const listInvoicesForOrg = `-- name: ListInvoicesForOrg :many |
| 532 | -SELECT id, org_id, provider, stripe_invoice_id, stripe_customer_id, stripe_subscription_id, status, number, currency, amount_due_cents, amount_paid_cents, amount_remaining_cents, hosted_invoice_url, invoice_pdf_url, period_start, period_end, due_at, paid_at, voided_at, created_at, updated_at FROM billing_invoices | 861 | +SELECT id, org_id, provider, stripe_invoice_id, stripe_customer_id, stripe_subscription_id, status, number, currency, amount_due_cents, amount_paid_cents, amount_remaining_cents, hosted_invoice_url, invoice_pdf_url, period_start, period_end, due_at, paid_at, voided_at, created_at, updated_at, subject_kind, subject_id FROM billing_invoices |
| 533 | -WHERE org_id = $1 | 862 | +WHERE subject_kind = 'org' AND subject_id = $1 |
| 534 | ORDER BY created_at DESC, id DESC | 863 | ORDER BY created_at DESC, id DESC |
| 535 | LIMIT $2 | 864 | LIMIT $2 |
| 536 | ` | 865 | ` |
| 537 | | 866 | |
| 538 | type ListInvoicesForOrgParams struct { | 867 | type ListInvoicesForOrgParams struct { |
| 539 | - OrgID int64 | 868 | + SubjectID int64 |
| 540 | Limit int32 | 869 | Limit int32 |
| 541 | } | 870 | } |
| 542 | | 871 | |
| | 872 | +// PRO03: filters on the polymorphic subject columns so the index |
| | 873 | +// billing_invoices_subject_created_idx services this query. The |
| | 874 | +// legacy `org_id` column is kept populated by UpsertInvoice for the |
| | 875 | +// transitional window; this query no longer reads it. |
| 543 | func (q *Queries) ListInvoicesForOrg(ctx context.Context, db DBTX, arg ListInvoicesForOrgParams) ([]BillingInvoice, error) { | 876 | func (q *Queries) ListInvoicesForOrg(ctx context.Context, db DBTX, arg ListInvoicesForOrgParams) ([]BillingInvoice, error) { |
| 544 | - rows, err := db.Query(ctx, listInvoicesForOrg, arg.OrgID, arg.Limit) | 877 | + rows, err := db.Query(ctx, listInvoicesForOrg, arg.SubjectID, arg.Limit) |
| | 878 | + if err != nil { |
| | 879 | + return nil, err |
| | 880 | + } |
| | 881 | + defer rows.Close() |
| | 882 | + items := []BillingInvoice{} |
| | 883 | + for rows.Next() { |
| | 884 | + var i BillingInvoice |
| | 885 | + if err := rows.Scan( |
| | 886 | + &i.ID, |
| | 887 | + &i.OrgID, |
| | 888 | + &i.Provider, |
| | 889 | + &i.StripeInvoiceID, |
| | 890 | + &i.StripeCustomerID, |
| | 891 | + &i.StripeSubscriptionID, |
| | 892 | + &i.Status, |
| | 893 | + &i.Number, |
| | 894 | + &i.Currency, |
| | 895 | + &i.AmountDueCents, |
| | 896 | + &i.AmountPaidCents, |
| | 897 | + &i.AmountRemainingCents, |
| | 898 | + &i.HostedInvoiceUrl, |
| | 899 | + &i.InvoicePdfUrl, |
| | 900 | + &i.PeriodStart, |
| | 901 | + &i.PeriodEnd, |
| | 902 | + &i.DueAt, |
| | 903 | + &i.PaidAt, |
| | 904 | + &i.VoidedAt, |
| | 905 | + &i.CreatedAt, |
| | 906 | + &i.UpdatedAt, |
| | 907 | + &i.SubjectKind, |
| | 908 | + &i.SubjectID, |
| | 909 | + ); err != nil { |
| | 910 | + return nil, err |
| | 911 | + } |
| | 912 | + items = append(items, i) |
| | 913 | + } |
| | 914 | + if err := rows.Err(); err != nil { |
| | 915 | + return nil, err |
| | 916 | + } |
| | 917 | + return items, nil |
| | 918 | +} |
| | 919 | + |
| | 920 | +const listInvoicesForSubject = `-- name: ListInvoicesForSubject :many |
| | 921 | +SELECT id, org_id, provider, stripe_invoice_id, stripe_customer_id, stripe_subscription_id, status, number, currency, amount_due_cents, amount_paid_cents, amount_remaining_cents, hosted_invoice_url, invoice_pdf_url, period_start, period_end, due_at, paid_at, voided_at, created_at, updated_at, subject_kind, subject_id FROM billing_invoices |
| | 922 | +WHERE subject_kind = $1::billing_subject_kind |
| | 923 | + AND subject_id = $2::bigint |
| | 924 | +ORDER BY created_at DESC, id DESC |
| | 925 | +LIMIT $3::integer |
| | 926 | +` |
| | 927 | + |
| | 928 | +type ListInvoicesForSubjectParams struct { |
| | 929 | + SubjectKind BillingSubjectKind |
| | 930 | + SubjectID int64 |
| | 931 | + Lim int32 |
| | 932 | +} |
| | 933 | + |
| | 934 | +// Polymorphic invoice listing for PRO04+ callers. The org-flavored |
| | 935 | +// ListInvoicesForOrg above is the same query with subject_kind |
| | 936 | +// hard-coded; this surface lets a user-side caller pass kind='user' |
| | 937 | +// without forking the helper. |
| | 938 | +func (q *Queries) ListInvoicesForSubject(ctx context.Context, db DBTX, arg ListInvoicesForSubjectParams) ([]BillingInvoice, error) { |
| | 939 | + rows, err := db.Query(ctx, listInvoicesForSubject, arg.SubjectKind, arg.SubjectID, arg.Lim) |
| 545 | if err != nil { | 940 | if err != nil { |
| 546 | return nil, err | 941 | return nil, err |
| 547 | } | 942 | } |
@@ -571,6 +966,8 @@ func (q *Queries) ListInvoicesForOrg(ctx context.Context, db DBTX, arg ListInvoi |
| 571 | &i.VoidedAt, | 966 | &i.VoidedAt, |
| 572 | &i.CreatedAt, | 967 | &i.CreatedAt, |
| 573 | &i.UpdatedAt, | 968 | &i.UpdatedAt, |
| | 969 | + &i.SubjectKind, |
| | 970 | + &i.SubjectID, |
| 574 | ); err != nil { | 971 | ); err != nil { |
| 575 | return nil, err | 972 | return nil, err |
| 576 | } | 973 | } |
@@ -844,13 +1241,224 @@ func (q *Queries) MarkPaymentSucceeded(ctx context.Context, db DBTX, arg MarkPay |
| 844 | return i, err | 1241 | return i, err |
| 845 | } | 1242 | } |
| 846 | | 1243 | |
| | 1244 | +const markUserCanceled = `-- name: MarkUserCanceled :one |
| | 1245 | +WITH state AS ( |
| | 1246 | + UPDATE user_billing_states |
| | 1247 | + SET plan = 'free', |
| | 1248 | + subscription_status = 'canceled', |
| | 1249 | + canceled_at = COALESCE(canceled_at, now()), |
| | 1250 | + locked_at = now(), |
| | 1251 | + lock_reason = 'canceled', |
| | 1252 | + grace_until = NULL, |
| | 1253 | + cancel_at_period_end = false, |
| | 1254 | + last_webhook_event_id = $1::text, |
| | 1255 | + updated_at = now() |
| | 1256 | + WHERE user_id = $2::bigint |
| | 1257 | + 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 |
| | 1258 | +), user_update AS ( |
| | 1259 | + UPDATE users |
| | 1260 | + SET plan = 'free', |
| | 1261 | + updated_at = now() |
| | 1262 | + WHERE id = $2::bigint |
| | 1263 | + RETURNING id |
| | 1264 | +) |
| | 1265 | +SELECT 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 FROM state |
| | 1266 | +` |
| | 1267 | + |
| | 1268 | +type MarkUserCanceledParams struct { |
| | 1269 | + LastWebhookEventID string |
| | 1270 | + UserID int64 |
| | 1271 | +} |
| | 1272 | + |
| | 1273 | +type MarkUserCanceledRow struct { |
| | 1274 | + UserID int64 |
| | 1275 | + Provider BillingProvider |
| | 1276 | + StripeCustomerID pgtype.Text |
| | 1277 | + StripeSubscriptionID pgtype.Text |
| | 1278 | + StripeSubscriptionItemID pgtype.Text |
| | 1279 | + Plan UserPlan |
| | 1280 | + SubscriptionStatus BillingSubscriptionStatus |
| | 1281 | + CurrentPeriodStart pgtype.Timestamptz |
| | 1282 | + CurrentPeriodEnd pgtype.Timestamptz |
| | 1283 | + CancelAtPeriodEnd bool |
| | 1284 | + TrialEnd pgtype.Timestamptz |
| | 1285 | + PastDueAt pgtype.Timestamptz |
| | 1286 | + CanceledAt pgtype.Timestamptz |
| | 1287 | + LockedAt pgtype.Timestamptz |
| | 1288 | + LockReason NullBillingLockReason |
| | 1289 | + GraceUntil pgtype.Timestamptz |
| | 1290 | + LastWebhookEventID string |
| | 1291 | + CreatedAt pgtype.Timestamptz |
| | 1292 | + UpdatedAt pgtype.Timestamptz |
| | 1293 | +} |
| | 1294 | + |
| | 1295 | +func (q *Queries) MarkUserCanceled(ctx context.Context, db DBTX, arg MarkUserCanceledParams) (MarkUserCanceledRow, error) { |
| | 1296 | + row := db.QueryRow(ctx, markUserCanceled, arg.LastWebhookEventID, arg.UserID) |
| | 1297 | + var i MarkUserCanceledRow |
| | 1298 | + err := row.Scan( |
| | 1299 | + &i.UserID, |
| | 1300 | + &i.Provider, |
| | 1301 | + &i.StripeCustomerID, |
| | 1302 | + &i.StripeSubscriptionID, |
| | 1303 | + &i.StripeSubscriptionItemID, |
| | 1304 | + &i.Plan, |
| | 1305 | + &i.SubscriptionStatus, |
| | 1306 | + &i.CurrentPeriodStart, |
| | 1307 | + &i.CurrentPeriodEnd, |
| | 1308 | + &i.CancelAtPeriodEnd, |
| | 1309 | + &i.TrialEnd, |
| | 1310 | + &i.PastDueAt, |
| | 1311 | + &i.CanceledAt, |
| | 1312 | + &i.LockedAt, |
| | 1313 | + &i.LockReason, |
| | 1314 | + &i.GraceUntil, |
| | 1315 | + &i.LastWebhookEventID, |
| | 1316 | + &i.CreatedAt, |
| | 1317 | + &i.UpdatedAt, |
| | 1318 | + ) |
| | 1319 | + return i, err |
| | 1320 | +} |
| | 1321 | + |
| | 1322 | +const markUserPastDue = `-- name: MarkUserPastDue :one |
| | 1323 | +UPDATE user_billing_states |
| | 1324 | + SET subscription_status = 'past_due', |
| | 1325 | + past_due_at = COALESCE(past_due_at, now()), |
| | 1326 | + locked_at = now(), |
| | 1327 | + lock_reason = 'past_due', |
| | 1328 | + grace_until = $1::timestamptz, |
| | 1329 | + last_webhook_event_id = $2::text, |
| | 1330 | + updated_at = now() |
| | 1331 | + WHERE user_id = $3::bigint |
| | 1332 | +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 |
| | 1333 | +` |
| | 1334 | + |
| | 1335 | +type MarkUserPastDueParams struct { |
| | 1336 | + GraceUntil pgtype.Timestamptz |
| | 1337 | + LastWebhookEventID string |
| | 1338 | + UserID int64 |
| | 1339 | +} |
| | 1340 | + |
| | 1341 | +func (q *Queries) MarkUserPastDue(ctx context.Context, db DBTX, arg MarkUserPastDueParams) (UserBillingState, error) { |
| | 1342 | + row := db.QueryRow(ctx, markUserPastDue, arg.GraceUntil, arg.LastWebhookEventID, arg.UserID) |
| | 1343 | + var i UserBillingState |
| | 1344 | + err := row.Scan( |
| | 1345 | + &i.UserID, |
| | 1346 | + &i.Provider, |
| | 1347 | + &i.StripeCustomerID, |
| | 1348 | + &i.StripeSubscriptionID, |
| | 1349 | + &i.StripeSubscriptionItemID, |
| | 1350 | + &i.Plan, |
| | 1351 | + &i.SubscriptionStatus, |
| | 1352 | + &i.CurrentPeriodStart, |
| | 1353 | + &i.CurrentPeriodEnd, |
| | 1354 | + &i.CancelAtPeriodEnd, |
| | 1355 | + &i.TrialEnd, |
| | 1356 | + &i.PastDueAt, |
| | 1357 | + &i.CanceledAt, |
| | 1358 | + &i.LockedAt, |
| | 1359 | + &i.LockReason, |
| | 1360 | + &i.GraceUntil, |
| | 1361 | + &i.LastWebhookEventID, |
| | 1362 | + &i.CreatedAt, |
| | 1363 | + &i.UpdatedAt, |
| | 1364 | + ) |
| | 1365 | + return i, err |
| | 1366 | +} |
| | 1367 | + |
| | 1368 | +const markUserPaymentSucceeded = `-- name: MarkUserPaymentSucceeded :one |
| | 1369 | +WITH state AS ( |
| | 1370 | + UPDATE user_billing_states |
| | 1371 | + SET plan = CASE |
| | 1372 | + WHEN subscription_status IN ('past_due', 'unpaid', 'incomplete') THEN 'pro' |
| | 1373 | + ELSE plan |
| | 1374 | + END, |
| | 1375 | + subscription_status = CASE |
| | 1376 | + WHEN subscription_status IN ('past_due', 'unpaid', 'incomplete') THEN 'active' |
| | 1377 | + ELSE subscription_status |
| | 1378 | + END, |
| | 1379 | + past_due_at = CASE |
| | 1380 | + WHEN subscription_status IN ('past_due', 'unpaid', 'incomplete') THEN NULL |
| | 1381 | + ELSE past_due_at |
| | 1382 | + END, |
| | 1383 | + locked_at = NULL, |
| | 1384 | + lock_reason = NULL, |
| | 1385 | + grace_until = NULL, |
| | 1386 | + last_webhook_event_id = $1::text, |
| | 1387 | + updated_at = now() |
| | 1388 | + WHERE user_id = $2::bigint |
| | 1389 | + 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 |
| | 1390 | +), user_update AS ( |
| | 1391 | + UPDATE users |
| | 1392 | + SET plan = state.plan, |
| | 1393 | + updated_at = now() |
| | 1394 | + FROM state |
| | 1395 | + WHERE users.id = state.user_id |
| | 1396 | + RETURNING users.id |
| | 1397 | +) |
| | 1398 | +SELECT 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 FROM state |
| | 1399 | +` |
| | 1400 | + |
| | 1401 | +type MarkUserPaymentSucceededParams struct { |
| | 1402 | + LastWebhookEventID string |
| | 1403 | + UserID int64 |
| | 1404 | +} |
| | 1405 | + |
| | 1406 | +type MarkUserPaymentSucceededRow struct { |
| | 1407 | + UserID int64 |
| | 1408 | + Provider BillingProvider |
| | 1409 | + StripeCustomerID pgtype.Text |
| | 1410 | + StripeSubscriptionID pgtype.Text |
| | 1411 | + StripeSubscriptionItemID pgtype.Text |
| | 1412 | + Plan UserPlan |
| | 1413 | + SubscriptionStatus BillingSubscriptionStatus |
| | 1414 | + CurrentPeriodStart pgtype.Timestamptz |
| | 1415 | + CurrentPeriodEnd pgtype.Timestamptz |
| | 1416 | + CancelAtPeriodEnd bool |
| | 1417 | + TrialEnd pgtype.Timestamptz |
| | 1418 | + PastDueAt pgtype.Timestamptz |
| | 1419 | + CanceledAt pgtype.Timestamptz |
| | 1420 | + LockedAt pgtype.Timestamptz |
| | 1421 | + LockReason NullBillingLockReason |
| | 1422 | + GraceUntil pgtype.Timestamptz |
| | 1423 | + LastWebhookEventID string |
| | 1424 | + CreatedAt pgtype.Timestamptz |
| | 1425 | + UpdatedAt pgtype.Timestamptz |
| | 1426 | +} |
| | 1427 | + |
| | 1428 | +func (q *Queries) MarkUserPaymentSucceeded(ctx context.Context, db DBTX, arg MarkUserPaymentSucceededParams) (MarkUserPaymentSucceededRow, error) { |
| | 1429 | + row := db.QueryRow(ctx, markUserPaymentSucceeded, arg.LastWebhookEventID, arg.UserID) |
| | 1430 | + var i MarkUserPaymentSucceededRow |
| | 1431 | + err := row.Scan( |
| | 1432 | + &i.UserID, |
| | 1433 | + &i.Provider, |
| | 1434 | + &i.StripeCustomerID, |
| | 1435 | + &i.StripeSubscriptionID, |
| | 1436 | + &i.StripeSubscriptionItemID, |
| | 1437 | + &i.Plan, |
| | 1438 | + &i.SubscriptionStatus, |
| | 1439 | + &i.CurrentPeriodStart, |
| | 1440 | + &i.CurrentPeriodEnd, |
| | 1441 | + &i.CancelAtPeriodEnd, |
| | 1442 | + &i.TrialEnd, |
| | 1443 | + &i.PastDueAt, |
| | 1444 | + &i.CanceledAt, |
| | 1445 | + &i.LockedAt, |
| | 1446 | + &i.LockReason, |
| | 1447 | + &i.GraceUntil, |
| | 1448 | + &i.LastWebhookEventID, |
| | 1449 | + &i.CreatedAt, |
| | 1450 | + &i.UpdatedAt, |
| | 1451 | + ) |
| | 1452 | + return i, err |
| | 1453 | +} |
| | 1454 | + |
| 847 | const markWebhookEventFailed = `-- name: MarkWebhookEventFailed :one | 1455 | const markWebhookEventFailed = `-- name: MarkWebhookEventFailed :one |
| 848 | UPDATE billing_webhook_events | 1456 | UPDATE billing_webhook_events |
| 849 | SET process_error = $2, | 1457 | SET process_error = $2, |
| 850 | processing_attempts = processing_attempts + 1 | 1458 | processing_attempts = processing_attempts + 1 |
| 851 | WHERE provider = 'stripe' | 1459 | WHERE provider = 'stripe' |
| 852 | AND provider_event_id = $1 | 1460 | AND provider_event_id = $1 |
| 853 | -RETURNING id, provider, provider_event_id, event_type, api_version, payload, received_at, processed_at, process_error, processing_attempts | 1461 | +RETURNING id, provider, provider_event_id, event_type, api_version, payload, received_at, processed_at, process_error, processing_attempts, subject_kind, subject_id |
| 854 | ` | 1462 | ` |
| 855 | | 1463 | |
| 856 | type MarkWebhookEventFailedParams struct { | 1464 | type MarkWebhookEventFailedParams struct { |
@@ -872,6 +1480,8 @@ func (q *Queries) MarkWebhookEventFailed(ctx context.Context, db DBTX, arg MarkW |
| 872 | &i.ProcessedAt, | 1480 | &i.ProcessedAt, |
| 873 | &i.ProcessError, | 1481 | &i.ProcessError, |
| 874 | &i.ProcessingAttempts, | 1482 | &i.ProcessingAttempts, |
| | 1483 | + &i.SubjectKind, |
| | 1484 | + &i.SubjectID, |
| 875 | ) | 1485 | ) |
| 876 | return i, err | 1486 | return i, err |
| 877 | } | 1487 | } |
@@ -883,7 +1493,7 @@ UPDATE billing_webhook_events |
| 883 | processing_attempts = processing_attempts + 1 | 1493 | processing_attempts = processing_attempts + 1 |
| 884 | WHERE provider = 'stripe' | 1494 | WHERE provider = 'stripe' |
| 885 | AND provider_event_id = $1 | 1495 | AND provider_event_id = $1 |
| 886 | -RETURNING id, provider, provider_event_id, event_type, api_version, payload, received_at, processed_at, process_error, processing_attempts | 1496 | +RETURNING id, provider, provider_event_id, event_type, api_version, payload, received_at, processed_at, process_error, processing_attempts, subject_kind, subject_id |
| 887 | ` | 1497 | ` |
| 888 | | 1498 | |
| 889 | func (q *Queries) MarkWebhookEventProcessed(ctx context.Context, db DBTX, providerEventID string) (BillingWebhookEvent, error) { | 1499 | func (q *Queries) MarkWebhookEventProcessed(ctx context.Context, db DBTX, providerEventID string) (BillingWebhookEvent, error) { |
@@ -900,6 +1510,8 @@ func (q *Queries) MarkWebhookEventProcessed(ctx context.Context, db DBTX, provid |
| 900 | &i.ProcessedAt, | 1510 | &i.ProcessedAt, |
| 901 | &i.ProcessError, | 1511 | &i.ProcessError, |
| 902 | &i.ProcessingAttempts, | 1512 | &i.ProcessingAttempts, |
| | 1513 | + &i.SubjectKind, |
| | 1514 | + &i.SubjectID, |
| 903 | ) | 1515 | ) |
| 904 | return i, err | 1516 | return i, err |
| 905 | } | 1517 | } |
@@ -948,10 +1560,54 @@ func (q *Queries) SetStripeCustomer(ctx context.Context, db DBTX, arg SetStripeC |
| 948 | return i, err | 1560 | return i, err |
| 949 | } | 1561 | } |
| 950 | | 1562 | |
| | 1563 | +const setUserStripeCustomer = `-- name: SetUserStripeCustomer :one |
| | 1564 | +INSERT INTO user_billing_states (user_id, provider, stripe_customer_id) |
| | 1565 | +VALUES ($1, 'stripe', $2) |
| | 1566 | +ON CONFLICT (user_id) DO UPDATE |
| | 1567 | + SET stripe_customer_id = EXCLUDED.stripe_customer_id, |
| | 1568 | + provider = 'stripe', |
| | 1569 | + updated_at = now() |
| | 1570 | +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 |
| | 1571 | +` |
| | 1572 | + |
| | 1573 | +type SetUserStripeCustomerParams struct { |
| | 1574 | + UserID int64 |
| | 1575 | + StripeCustomerID pgtype.Text |
| | 1576 | +} |
| | 1577 | + |
| | 1578 | +func (q *Queries) SetUserStripeCustomer(ctx context.Context, db DBTX, arg SetUserStripeCustomerParams) (UserBillingState, error) { |
| | 1579 | + row := db.QueryRow(ctx, setUserStripeCustomer, arg.UserID, arg.StripeCustomerID) |
| | 1580 | + var i UserBillingState |
| | 1581 | + err := row.Scan( |
| | 1582 | + &i.UserID, |
| | 1583 | + &i.Provider, |
| | 1584 | + &i.StripeCustomerID, |
| | 1585 | + &i.StripeSubscriptionID, |
| | 1586 | + &i.StripeSubscriptionItemID, |
| | 1587 | + &i.Plan, |
| | 1588 | + &i.SubscriptionStatus, |
| | 1589 | + &i.CurrentPeriodStart, |
| | 1590 | + &i.CurrentPeriodEnd, |
| | 1591 | + &i.CancelAtPeriodEnd, |
| | 1592 | + &i.TrialEnd, |
| | 1593 | + &i.PastDueAt, |
| | 1594 | + &i.CanceledAt, |
| | 1595 | + &i.LockedAt, |
| | 1596 | + &i.LockReason, |
| | 1597 | + &i.GraceUntil, |
| | 1598 | + &i.LastWebhookEventID, |
| | 1599 | + &i.CreatedAt, |
| | 1600 | + &i.UpdatedAt, |
| | 1601 | + ) |
| | 1602 | + return i, err |
| | 1603 | +} |
| | 1604 | + |
| 951 | const upsertInvoice = `-- name: UpsertInvoice :one | 1605 | const upsertInvoice = `-- name: UpsertInvoice :one |
| 952 | | 1606 | |
| 953 | INSERT INTO billing_invoices ( | 1607 | INSERT INTO billing_invoices ( |
| 954 | org_id, | 1608 | org_id, |
| | 1609 | + subject_kind, |
| | 1610 | + subject_id, |
| 955 | provider, | 1611 | provider, |
| 956 | stripe_invoice_id, | 1612 | stripe_invoice_id, |
| 957 | stripe_customer_id, | 1613 | stripe_customer_id, |
@@ -971,6 +1627,8 @@ INSERT INTO billing_invoices ( |
| 971 | voided_at | 1627 | voided_at |
| 972 | ) | 1628 | ) |
| 973 | VALUES ( | 1629 | VALUES ( |
| | 1630 | + $1::bigint, |
| | 1631 | + 'org'::billing_subject_kind, |
| 974 | $1::bigint, | 1632 | $1::bigint, |
| 975 | 'stripe', | 1633 | 'stripe', |
| 976 | $2::text, | 1634 | $2::text, |
@@ -1008,7 +1666,7 @@ ON CONFLICT (provider, stripe_invoice_id) DO UPDATE |
| 1008 | paid_at = EXCLUDED.paid_at, | 1666 | paid_at = EXCLUDED.paid_at, |
| 1009 | voided_at = EXCLUDED.voided_at, | 1667 | voided_at = EXCLUDED.voided_at, |
| 1010 | updated_at = now() | 1668 | updated_at = now() |
| 1011 | -RETURNING id, org_id, provider, stripe_invoice_id, stripe_customer_id, stripe_subscription_id, status, number, currency, amount_due_cents, amount_paid_cents, amount_remaining_cents, hosted_invoice_url, invoice_pdf_url, period_start, period_end, due_at, paid_at, voided_at, created_at, updated_at | 1669 | +RETURNING id, org_id, provider, stripe_invoice_id, stripe_customer_id, stripe_subscription_id, status, number, currency, amount_due_cents, amount_paid_cents, amount_remaining_cents, hosted_invoice_url, invoice_pdf_url, period_start, period_end, due_at, paid_at, voided_at, created_at, updated_at, subject_kind, subject_id |
| 1012 | ` | 1670 | ` |
| 1013 | | 1671 | |
| 1014 | type UpsertInvoiceParams struct { | 1672 | type UpsertInvoiceParams struct { |
@@ -1032,6 +1690,11 @@ type UpsertInvoiceParams struct { |
| 1032 | } | 1690 | } |
| 1033 | | 1691 | |
| 1034 | // ─── billing_invoices ────────────────────────────────────────────── | 1692 | // ─── billing_invoices ────────────────────────────────────────────── |
| | 1693 | +// PRO03: writes both legacy `org_id` and polymorphic |
| | 1694 | +// `(subject_kind, subject_id)`. Callers continue to bind org_id only; |
| | 1695 | +// the subject columns are derived. After PRO04 migrates all callers |
| | 1696 | +// to the polymorphic shape, a follow-up migration drops `org_id` and |
| | 1697 | +// this query loses the legacy column from its INSERT list. |
| 1035 | func (q *Queries) UpsertInvoice(ctx context.Context, db DBTX, arg UpsertInvoiceParams) (BillingInvoice, error) { | 1698 | func (q *Queries) UpsertInvoice(ctx context.Context, db DBTX, arg UpsertInvoiceParams) (BillingInvoice, error) { |
| 1036 | row := db.QueryRow(ctx, upsertInvoice, | 1699 | row := db.QueryRow(ctx, upsertInvoice, |
| 1037 | arg.OrgID, | 1700 | arg.OrgID, |
@@ -1075,6 +1738,8 @@ func (q *Queries) UpsertInvoice(ctx context.Context, db DBTX, arg UpsertInvoiceP |
| 1075 | &i.VoidedAt, | 1738 | &i.VoidedAt, |
| 1076 | &i.CreatedAt, | 1739 | &i.CreatedAt, |
| 1077 | &i.UpdatedAt, | 1740 | &i.UpdatedAt, |
| | 1741 | + &i.SubjectKind, |
| | 1742 | + &i.SubjectID, |
| 1078 | ) | 1743 | ) |
| 1079 | return i, err | 1744 | return i, err |
| 1080 | } | 1745 | } |