@@ -857,6 +857,71 @@ func (q *Queries) GetWebhookEventReceipt(ctx context.Context, db DBTX, providerE |
| 857 | 857 | return i, err |
| 858 | 858 | } |
| 859 | 859 | |
| 860 | +const listFailedWebhookEvents = `-- name: ListFailedWebhookEvents :many |
| 861 | +SELECT id, provider, provider_event_id, event_type, api_version, |
| 862 | + received_at, processed_at, processing_attempts, process_error, |
| 863 | + subject_kind, subject_id |
| 864 | + FROM billing_webhook_events |
| 865 | + WHERE provider = 'stripe' |
| 866 | + AND ( |
| 867 | + process_error <> '' |
| 868 | + OR (processed_at IS NULL AND processing_attempts > 0) |
| 869 | + ) |
| 870 | + ORDER BY received_at DESC |
| 871 | + LIMIT $1 |
| 872 | +` |
| 873 | + |
| 874 | +type ListFailedWebhookEventsRow struct { |
| 875 | + ID int64 |
| 876 | + Provider BillingProvider |
| 877 | + ProviderEventID string |
| 878 | + EventType string |
| 879 | + ApiVersion string |
| 880 | + ReceivedAt pgtype.Timestamptz |
| 881 | + ProcessedAt pgtype.Timestamptz |
| 882 | + ProcessingAttempts int32 |
| 883 | + ProcessError string |
| 884 | + SubjectKind NullBillingSubjectKind |
| 885 | + SubjectID pgtype.Int8 |
| 886 | +} |
| 887 | + |
| 888 | +// Operator query for "events we received but failed to process." |
| 889 | +// A row is "failed" when it has a non-empty process_error OR when |
| 890 | +// it has never been processed (processed_at NULL) and has at least |
| 891 | +// one processing attempt. Rows that are merely new and untouched |
| 892 | +// (attempts=0, processed_at NULL, no error) are excluded. |
| 893 | +func (q *Queries) ListFailedWebhookEvents(ctx context.Context, db DBTX, limit int32) ([]ListFailedWebhookEventsRow, error) { |
| 894 | + rows, err := db.Query(ctx, listFailedWebhookEvents, limit) |
| 895 | + if err != nil { |
| 896 | + return nil, err |
| 897 | + } |
| 898 | + defer rows.Close() |
| 899 | + items := []ListFailedWebhookEventsRow{} |
| 900 | + for rows.Next() { |
| 901 | + var i ListFailedWebhookEventsRow |
| 902 | + if err := rows.Scan( |
| 903 | + &i.ID, |
| 904 | + &i.Provider, |
| 905 | + &i.ProviderEventID, |
| 906 | + &i.EventType, |
| 907 | + &i.ApiVersion, |
| 908 | + &i.ReceivedAt, |
| 909 | + &i.ProcessedAt, |
| 910 | + &i.ProcessingAttempts, |
| 911 | + &i.ProcessError, |
| 912 | + &i.SubjectKind, |
| 913 | + &i.SubjectID, |
| 914 | + ); err != nil { |
| 915 | + return nil, err |
| 916 | + } |
| 917 | + items = append(items, i) |
| 918 | + } |
| 919 | + if err := rows.Err(); err != nil { |
| 920 | + return nil, err |
| 921 | + } |
| 922 | + return items, nil |
| 923 | +} |
| 924 | + |
| 860 | 925 | const listInvoicesForOrg = `-- name: ListInvoicesForOrg :many |
| 861 | 926 | 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 |
| 862 | 927 | WHERE subject_kind = 'org' AND subject_id = $1 |
@@ -1602,6 +1667,30 @@ func (q *Queries) SetUserStripeCustomer(ctx context.Context, db DBTX, arg SetUse |
| 1602 | 1667 | return i, err |
| 1603 | 1668 | } |
| 1604 | 1669 | |
| 1670 | +const setWebhookEventSubject = `-- name: SetWebhookEventSubject :exec |
| 1671 | +UPDATE billing_webhook_events |
| 1672 | + SET subject_kind = $1::billing_subject_kind, |
| 1673 | + subject_id = $2::bigint |
| 1674 | + WHERE provider = 'stripe' |
| 1675 | + AND provider_event_id = $3::text |
| 1676 | +` |
| 1677 | + |
| 1678 | +type SetWebhookEventSubjectParams struct { |
| 1679 | + SubjectKind BillingSubjectKind |
| 1680 | + SubjectID int64 |
| 1681 | + ProviderEventID string |
| 1682 | +} |
| 1683 | + |
| 1684 | +// Records the resolved subject on the receipt row after a successful |
| 1685 | +// subject-resolution step. Called from the apply path before guard + |
| 1686 | +// state mutation so the receipt carries the audit trail even if the |
| 1687 | +// subsequent apply fails. Migration 0075's CHECK constraint enforces |
| 1688 | +// both-or-neither; callers must pass a non-zero subject. |
| 1689 | +func (q *Queries) SetWebhookEventSubject(ctx context.Context, db DBTX, arg SetWebhookEventSubjectParams) error { |
| 1690 | + _, err := db.Exec(ctx, setWebhookEventSubject, arg.SubjectKind, arg.SubjectID, arg.ProviderEventID) |
| 1691 | + return err |
| 1692 | +} |
| 1693 | + |
| 1605 | 1694 | const upsertInvoice = `-- name: UpsertInvoice :one |
| 1606 | 1695 | |
| 1607 | 1696 | INSERT INTO billing_invoices ( |