tenseleyflow/shithub / 443e03c

Browse files

web/orgs/billing_webhook: guardPriceKindMatch refuses empty-items when prices configured

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
443e03c311177468402fc356dbf39be634011ee0
Parents
a00f957
Tree
65f6f1f

1 changed file

StatusFile+-
M internal/web/handlers/orgs/billing_webhook.go 20 4
internal/web/handlers/orgs/billing_webhook.gomodified
@@ -229,13 +229,29 @@ func (h *Handlers) resolvePrincipalFromSubscription(ctx context.Context, sub *st
229229
 // rejecting org events. PRO-disabled instances never see Pro
230230
 // events, so the org path is unaffected.
231231
 func (h *Handlers) guardPriceKindMatch(kind orgbilling.SubjectKind, sub *stripeapi.Subscription) error {
232
-	if sub == nil || sub.Items == nil || len(sub.Items.Data) == 0 || sub.Items.Data[0] == nil || sub.Items.Data[0].Price == nil {
233
-		// No price on the event — nothing to validate. Subsequent
234
-		// apply logic surfaces the missing-data error if needed.
232
+	teamPrice, proPrice := h.d.BillingPriceIDs()
233
+	// PRO08 A1: when ANY price is configured we MUST be able to
234
+	// inspect the event's price-id to enforce cross-kind separation.
235
+	// A subscription event with empty Items can otherwise bypass the
236
+	// guard entirely — a Pro-priced subscription with `subject_kind=org`
237
+	// metadata would silently write Team to the org-side table. Refuse
238
+	// the apply so Stripe retries (and the operator notices).
239
+	if teamPrice != "" || proPrice != "" {
240
+		if sub == nil || sub.Items == nil || len(sub.Items.Data) == 0 || sub.Items.Data[0] == nil || sub.Items.Data[0].Price == nil {
241
+			id := ""
242
+			if sub != nil {
243
+				id = strings.TrimSpace(sub.ID)
244
+			}
245
+			return fmt.Errorf("stripe subscription %q: no line items in event — refusing apply (cross-kind price guard cannot run)", id)
246
+		}
247
+	} else if sub == nil || sub.Items == nil || len(sub.Items.Data) == 0 || sub.Items.Data[0] == nil || sub.Items.Data[0].Price == nil {
248
+		// No prices configured AND no items — nothing to validate.
249
+		// The instance has billing disabled or runs Pro-only / Team-
250
+		// only without the other tier's price wired; let the apply
251
+		// flow handle the rest.
235252
 		return nil
236253
 	}
237254
 	priceID := strings.TrimSpace(sub.Items.Data[0].Price.ID)
238
-	teamPrice, proPrice := h.d.BillingPriceIDs()
239255
 	switch kind {
240256
 	case orgbilling.SubjectKindOrg:
241257
 		if teamPrice != "" && priceID != "" && priceID != teamPrice {