tenseleyflow/shithub / 0c3c4d9

Browse files

web/orgs/billing_webhook: record resolved principal on receipt after each resolve

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
0c3c4d9bfa29d2892293e466ce1dec4d5ba19dd1
Parents
7296411
Tree
ac50e13

1 changed file

StatusFile+-
M internal/web/handlers/orgs/billing_webhook.go 21 0
internal/web/handlers/orgs/billing_webhook.gomodified
@@ -102,6 +102,7 @@ func (h *Handlers) applyStripeCheckoutCompleted(ctx context.Context, event strip
102102
 	if err != nil {
103103
 		return err
104104
 	}
105
+	h.recordWebhookSubject(ctx, event.ID, principal)
105106
 	_, err = orgbilling.SetStripeCustomerForPrincipal(ctx, orgbilling.Deps{Pool: h.d.Pool}, principal, customerID)
106107
 	return err
107108
 }
@@ -147,6 +148,7 @@ func (h *Handlers) applyStripeSubscriptionEvent(ctx context.Context, event strip
147148
 	if err != nil {
148149
 		return err
149150
 	}
151
+	h.recordWebhookSubject(ctx, event.ID, principal)
150152
 	// Cross-kind price-id check: if the subscription's first item
151153
 	// price doesn't match the expected price for the resolved kind,
152154
 	// refuse to apply. A Pro price on an org subject (or Team on
@@ -262,6 +264,7 @@ func (h *Handlers) applyStripeInvoiceEvent(ctx context.Context, event stripeapi.
262264
 	if err != nil {
263265
 		return err
264266
 	}
267
+	h.recordWebhookSubject(ctx, event.ID, principalState.Principal)
265268
 	status, err := stripeInvoiceStatus(inv.Status)
266269
 	if err != nil {
267270
 		return err
@@ -451,6 +454,24 @@ func unixTime(ts int64) time.Time {
451454
 	return time.Unix(ts, 0).UTC()
452455
 }
453456
 
457
+// recordWebhookSubject persists the resolved principal on the receipt
458
+// row so failed events keep their audit trail. Logs and continues on
459
+// error — the subject is auxiliary; the state-mutation path is the
460
+// load-bearing thing. A zero principal (invalid kind / ID) is treated
461
+// as a programmer error and silently dropped: the both-or-neither
462
+// CHECK constraint on the receipt table would reject the write.
463
+func (h *Handlers) recordWebhookSubject(ctx context.Context, eventID string, p orgbilling.Principal) {
464
+	if err := p.Validate(); err != nil {
465
+		h.d.Logger.WarnContext(ctx, "org billing: webhook subject record skipped — invalid principal",
466
+			"event_id", eventID, "error", err)
467
+		return
468
+	}
469
+	if err := orgbilling.SetWebhookEventSubjectForPrincipal(ctx, orgbilling.Deps{Pool: h.d.Pool}, eventID, p); err != nil {
470
+		h.d.Logger.WarnContext(ctx, "org billing: webhook subject record failed",
471
+			"event_id", eventID, "principal", p.String(), "error", err)
472
+	}
473
+}
474
+
454475
 func unmarshalStripeEventObject[T any](event stripeapi.Event, out *T) error {
455476
 	if event.Data == nil || len(event.Data.Raw) == 0 {
456477
 		return errors.New("stripe webhook missing event data")