mirror of
https://github.com/nestriness/nestri.git
synced 2026-11-03 22:22:39 +02:00
fix(billing): a returning customer's subscription reaches their team
The provider reuses a customer it already has under the same email and leaves our team id off it, so the subscription arrived with no team, was acknowledged, and changed nothing: a paid plan never applied. The checkout now carries the team in its metadata, which the subscription keeps, and the webhook reads it there when the customer has none. A paid delivery that still matches no team is logged instead of dropped in silence.
This commit is contained in:
@@ -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 });
|
||||
|
||||
@@ -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 =
|
||||
|
||||
Reference in New Issue
Block a user