Add billing settings data helpers
Authored by
mfwolffe <wolffemf@dukes.jmu.edu>
- SHA
4f220ece8822e56657c331bab699c30cbd397228- Parents
-
b9f1094 - Tree
fcd95d0
4f220ec
4f220ece8822e56657c331bab699c30cbd397228b9f1094
fcd95d0| Status | File | + | - |
|---|---|---|---|
| M |
internal/billing/billing.go
|
25 | 0 |
| M |
internal/billing/billing_test.go
|
22 | 0 |
| M |
internal/billing/queries/billing.sql
|
9 | 0 |
| M |
internal/billing/sqlc/billing.sql.go
|
17 | 0 |
| M |
internal/billing/sqlc/querier.go
|
1 | 0 |
internal/billing/billing.gomodified@@ -263,6 +263,17 @@ func MarkWebhookEventFailed(ctx context.Context, deps Deps, providerEventID, pro | ||
| 263 | 263 | }) |
| 264 | 264 | } |
| 265 | 265 | |
| 266 | +func GetWebhookEventReceipt(ctx context.Context, deps Deps, providerEventID string) (billingdb.BillingWebhookEvent, error) { | |
| 267 | + if err := validateDeps(deps); err != nil { | |
| 268 | + return billingdb.BillingWebhookEvent{}, err | |
| 269 | + } | |
| 270 | + providerEventID = strings.TrimSpace(providerEventID) | |
| 271 | + if providerEventID == "" { | |
| 272 | + return billingdb.BillingWebhookEvent{}, ErrWebhookEventID | |
| 273 | + } | |
| 274 | + return billingdb.New().GetWebhookEventReceipt(ctx, deps.Pool, providerEventID) | |
| 275 | +} | |
| 276 | + | |
| 266 | 277 | func UpsertInvoice(ctx context.Context, deps Deps, snap InvoiceSnapshot) (billingdb.BillingInvoice, error) { |
| 267 | 278 | if err := validateDeps(deps); err != nil { |
| 268 | 279 | return billingdb.BillingInvoice{}, err |
@@ -363,6 +374,20 @@ func CountBillableOrgMembers(ctx context.Context, deps Deps, orgID int64) (int, | ||
| 363 | 374 | return int(n), nil |
| 364 | 375 | } |
| 365 | 376 | |
| 377 | +func CountPendingOrgInvitations(ctx context.Context, deps Deps, orgID int64) (int, error) { | |
| 378 | + if err := validateDeps(deps); err != nil { | |
| 379 | + return 0, err | |
| 380 | + } | |
| 381 | + if orgID == 0 { | |
| 382 | + return 0, ErrOrgIDRequired | |
| 383 | + } | |
| 384 | + n, err := billingdb.New().CountPendingOrgInvitations(ctx, deps.Pool, orgID) | |
| 385 | + if err != nil { | |
| 386 | + return 0, err | |
| 387 | + } | |
| 388 | + return int(n), nil | |
| 389 | +} | |
| 390 | + | |
| 366 | 391 | func MarkPastDue(ctx context.Context, deps Deps, orgID int64, graceUntil time.Time, lastWebhookEventID string) (State, error) { |
| 367 | 392 | if err := validateDeps(deps); err != nil { |
| 368 | 393 | return State{}, err |
internal/billing/billing_test.gomodified@@ -185,6 +185,13 @@ func TestRecordWebhookEventIsIdempotent(t *testing.T) { | ||
| 185 | 185 | if _, err := billing.MarkWebhookEventProcessed(ctx, deps, event.ProviderEventID); err != nil { |
| 186 | 186 | t.Fatalf("MarkWebhookEventProcessed: %v", err) |
| 187 | 187 | } |
| 188 | + receipt, err := billing.GetWebhookEventReceipt(ctx, deps, event.ProviderEventID) | |
| 189 | + if err != nil { | |
| 190 | + t.Fatalf("GetWebhookEventReceipt: %v", err) | |
| 191 | + } | |
| 192 | + if receipt.ProviderEventID != event.ProviderEventID || !receipt.ProcessedAt.Valid { | |
| 193 | + t.Fatalf("unexpected receipt lookup: %+v", receipt) | |
| 194 | + } | |
| 188 | 195 | dup, created, err = billing.RecordWebhookEvent(ctx, deps, event) |
| 189 | 196 | if err != nil { |
| 190 | 197 | t.Fatalf("RecordWebhookEvent after processed: %v", err) |
@@ -231,6 +238,21 @@ func TestSyncSeatSnapshotUpdatesBillingState(t *testing.T) { | ||
| 231 | 238 | if count != 1 { |
| 232 | 239 | t.Fatalf("billable members: got %d, want 1", count) |
| 233 | 240 | } |
| 241 | + | |
| 242 | + if _, err := deps.Pool.Exec(ctx, ` | |
| 243 | + INSERT INTO org_invitations (org_id, target_email, role, token_hash, expires_at) | |
| 244 | + VALUES ($1, 'pending@example.com', 'member', '\x010203', now() + interval '1 day'), | |
| 245 | + ($1, 'expired@example.com', 'member', '\x040506', now() - interval '1 day') | |
| 246 | + `, org.ID); err != nil { | |
| 247 | + t.Fatalf("insert invitations: %v", err) | |
| 248 | + } | |
| 249 | + pending, err := billing.CountPendingOrgInvitations(ctx, deps, org.ID) | |
| 250 | + if err != nil { | |
| 251 | + t.Fatalf("CountPendingOrgInvitations: %v", err) | |
| 252 | + } | |
| 253 | + if pending != 1 { | |
| 254 | + t.Fatalf("pending invitations: got %d, want 1", pending) | |
| 255 | + } | |
| 234 | 256 | } |
| 235 | 257 | |
| 236 | 258 | func TestStripeLookupsAndInvoiceSnapshot(t *testing.T) { |
internal/billing/queries/billing.sqlmodified@@ -231,6 +231,15 @@ SELECT count(*)::integer | ||
| 231 | 231 | FROM org_members |
| 232 | 232 | WHERE org_id = $1; |
| 233 | 233 | |
| 234 | +-- name: CountPendingOrgInvitations :one | |
| 235 | +SELECT count(*)::integer | |
| 236 | +FROM org_invitations | |
| 237 | +WHERE org_id = $1 | |
| 238 | + AND accepted_at IS NULL | |
| 239 | + AND declined_at IS NULL | |
| 240 | + AND canceled_at IS NULL | |
| 241 | + AND expires_at > now(); | |
| 242 | + | |
| 234 | 243 | -- ─── billing_invoices ────────────────────────────────────────────── |
| 235 | 244 | |
| 236 | 245 | -- name: UpsertInvoice :one |
internal/billing/sqlc/billing.sql.gomodified@@ -255,6 +255,23 @@ func (q *Queries) CountBillableOrgMembers(ctx context.Context, db DBTX, orgID in | ||
| 255 | 255 | return column_1, err |
| 256 | 256 | } |
| 257 | 257 | |
| 258 | +const countPendingOrgInvitations = `-- name: CountPendingOrgInvitations :one | |
| 259 | +SELECT count(*)::integer | |
| 260 | +FROM org_invitations | |
| 261 | +WHERE org_id = $1 | |
| 262 | + AND accepted_at IS NULL | |
| 263 | + AND declined_at IS NULL | |
| 264 | + AND canceled_at IS NULL | |
| 265 | + AND expires_at > now() | |
| 266 | +` | |
| 267 | + | |
| 268 | +func (q *Queries) CountPendingOrgInvitations(ctx context.Context, db DBTX, orgID int64) (int32, error) { | |
| 269 | + row := db.QueryRow(ctx, countPendingOrgInvitations, orgID) | |
| 270 | + var column_1 int32 | |
| 271 | + err := row.Scan(&column_1) | |
| 272 | + return column_1, err | |
| 273 | +} | |
| 274 | + | |
| 258 | 275 | const createSeatSnapshot = `-- name: CreateSeatSnapshot :one |
| 259 | 276 | |
| 260 | 277 | WITH snapshot AS ( |
internal/billing/sqlc/querier.gomodified@@ -14,6 +14,7 @@ type Querier interface { | ||
| 14 | 14 | ApplySubscriptionSnapshot(ctx context.Context, db DBTX, arg ApplySubscriptionSnapshotParams) (ApplySubscriptionSnapshotRow, error) |
| 15 | 15 | ClearBillingLock(ctx context.Context, db DBTX, orgID int64) (ClearBillingLockRow, error) |
| 16 | 16 | CountBillableOrgMembers(ctx context.Context, db DBTX, orgID int64) (int32, error) |
| 17 | + CountPendingOrgInvitations(ctx context.Context, db DBTX, orgID int64) (int32, error) | |
| 17 | 18 | // ─── billing_seat_snapshots ──────────────────────────────────────── |
| 18 | 19 | CreateSeatSnapshot(ctx context.Context, db DBTX, arg CreateSeatSnapshotParams) (CreateSeatSnapshotRow, error) |
| 19 | 20 | // ─── billing_webhook_events ──────────────────────────────────────── |