Billing: a returning customer's subscription reaches their team (#398)

A customer the payment provider already knew under the same email kept
their old customer record, without our team id, so their paid
subscription was acknowledged and applied to no team. Checkout now
carries the team in its metadata and the webhook falls back to it; a
paid delivery that matches no team is logged.
This commit is contained in:
Wanjohi
2026-10-13 18:59:24 +00:00
committed by GitHub
3 changed files with 52 additions and 5 deletions
+5
View File
@@ -84,6 +84,11 @@ export namespace BillingApi {
// would have them retry it for days against a thing that will
// never become actionable.
if (!delivery.teamId || !delivery.standing) {
// Said, because a paid subscription that matches no team is
// somebody paying for nothing, and a 200 alone hides it.
if (delivery.standing?.plan === 'paid') {
console.error('a paid subscription matched no team', { type: delivery.type });
}
return c.json({ data: { applied: false, type: delivery.type } });
}
+33
View File
@@ -202,6 +202,39 @@ describe('Both signing schemes', () => {
expect(delivery.standing).toEqual({ plan: 'free', status: 'revoked' });
});
test('a customer who existed before checkout is found by the checkout metadata', async () => {
// The provider reuses a customer it already has under the same email and
// does not put our id on it, so the subscription arrives with a customer
// that has no external id. A paid subscription was dropped this way.
const { Webhook } = await import('standardwebhooks');
const secret = `whsec_${Buffer.from('c'.repeat(32)).toString('base64')}`;
configure({ POLAR_WEBHOOK_SECRET: secret });
const body = JSON.stringify({
type: 'subscription.active',
data: {
customer: { external_id: null },
metadata: { teamId: 'tem_returning' },
product_id: PAID
}
});
const id = 'msg_returning';
const timestamp = new Date();
const signature = new Webhook(secret).sign(id, timestamp, body);
const delivery = Polar.receive({
body,
headers: {
'webhook-id': id,
'webhook-timestamp': Math.floor(timestamp.getTime() / 1000).toString(),
'webhook-signature': signature
}
});
expect(delivery.teamId).toBe('tem_returning');
expect(delivery.standing).toEqual({ plan: 'paid', status: 'active' });
});
test('a tampered body under a valid-looking signature is refused', () => {
const secret = `whsec_${Buffer.from('a'.repeat(32)).toString('base64')}`;
configure({ POLAR_WEBHOOK_SECRET: secret });
+14 -5
View File
@@ -187,6 +187,11 @@ export namespace Polar {
const created = await client().checkouts.create({
products: [productId],
externalCustomerId: input.teamId,
// Also on the checkout itself, which the provider copies onto the
// subscription. A customer who already existed under this email
// keeps the customer record they had, without our id on it, and
// this is then the only place the team survives.
metadata: { teamId: input.teamId },
customerEmail: input.email,
successUrl: input.successUrl
});
@@ -350,11 +355,15 @@ export namespace Polar {
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 ?? customer?.external_id ?? null;
// `externalId` is the team id we put on the customer. A customer who
// existed before checkout keeps the record they had, without it, so
// the team then comes from the checkout's metadata, which the
// subscription carries. A delivery with neither was not started here.
const metadata = data.metadata as { teamId?: unknown } | undefined;
const teamId =
customer?.externalId ??
customer?.external_id ??
(typeof metadata?.teamId === 'string' ? metadata.teamId : null);
const product = data.product as { id?: string } | undefined;
const productId =