@@ -229,13 +229,29 @@ func (h *Handlers) resolvePrincipalFromSubscription(ctx context.Context, sub *st |
| 229 | 229 | // rejecting org events. PRO-disabled instances never see Pro |
| 230 | 230 | // events, so the org path is unaffected. |
| 231 | 231 | 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. |
| 235 | 252 | return nil |
| 236 | 253 | } |
| 237 | 254 | priceID := strings.TrimSpace(sub.Items.Data[0].Price.ID) |
| 238 | | - teamPrice, proPrice := h.d.BillingPriceIDs() |
| 239 | 255 | switch kind { |
| 240 | 256 | case orgbilling.SubjectKindOrg: |
| 241 | 257 | if teamPrice != "" && priceID != "" && priceID != teamPrice { |