fix(billing): read the payload that was actually sent

Deliveries arrived intact, verified correctly, and applied to nobody.

Checking the signature here rather than through the SDK means the body is
exactly what was sent, and what is sent is snake_case. The SDK's parser renames
fields to camelCase on the way through, so the reader written against it looked
for `externalId` in a payload that says `external_id` — found nothing, decided
the delivery was about a customer we did not create, and acknowledged it.

That is the worst shape a bug of this kind can take. Every visible signal was
healthy: a 200 back to the provider, no retries, no errors, and a plan that
silently never changed.

Both spellings are now read, so neither path can regress the other.
This commit is contained in:
Wanjohi
2026-09-19 01:42:42 +03:00
parent abd27f2d5f
commit a09ad1e09a
2 changed files with 44 additions and 5 deletions

View File

@@ -341,18 +341,27 @@ export namespace Polar {
throw error;
}
// Both spellings, for both paths. The SDK's parser renames fields to
// camelCase on the way through; verifying the signature ourselves
// hands back exactly what was sent, which is snake_case. Reading only
// one spelling makes every delivery arrive intact, verify correctly,
// and then quietly apply to nobody.
const data = event.data ?? {};
const customer = data.customer as { externalId?: string | null } | undefined;
const customer = data.customer as
| { externalId?: string | null; external_id?: string | null }
| undefined;
// `externalId` is the team id we put on the customer. A delivery
// without one is about a customer created some other way — by hand in
// their dashboard, most likely — and there is nothing here it can
// change.
const teamId = customer?.externalId ?? null;
const teamId = customer?.externalId ?? customer?.external_id ?? null;
// Both spellings, because which one a payload carries depends on
// whether the product was expanded into it.
const product = data.product as { id?: string } | undefined;
const productId = (data.productId as string | undefined) ?? product?.id ?? null;
const productId =
(data.productId as string | undefined) ??
(data.product_id as string | undefined) ??
product?.id ??
null;
return { type: event.type, teamId, standing: standingFor(event.type, productId) };
}