mirror of
https://github.com/nestriness/nestri.git
synced 2026-11-03 14:12:39 +02:00
fix(api): billing portal answers 404 when a team has no Polar customer
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
import { afterAll, afterEach, describe, expect, test } from 'bun:test';
|
||||
|
||||
import { AccessToken } from '@nestri/core/access-token/index';
|
||||
import { Polar } from '@nestri/core/billing/polar';
|
||||
import { Fixtures } from '@nestri/core/db/fixtures';
|
||||
import { testDb } from '@nestri/core/db/test';
|
||||
import { Env } from '@nestri/core/env';
|
||||
import { Identifier } from '@nestri/core/id';
|
||||
|
||||
import { app } from '../app/index';
|
||||
import './setup';
|
||||
|
||||
const sql = testDb();
|
||||
const createdUserIds: string[] = [];
|
||||
const realFetch = globalThis.fetch;
|
||||
|
||||
async function person(label: string) {
|
||||
const owner = await Fixtures.owner(label);
|
||||
createdUserIds.push(owner.userId);
|
||||
const pat = await AccessToken.create({
|
||||
id: Identifier.ascending('accessToken'),
|
||||
ownerUserId: owner.userId,
|
||||
teamId: null,
|
||||
name: label
|
||||
});
|
||||
return { authorization: `Bearer ${pat.token}` };
|
||||
}
|
||||
|
||||
function configure() {
|
||||
Env.init({
|
||||
NODE_ENV: 'test',
|
||||
FRONTEND_URL: 'http://localhost:5173',
|
||||
POLAR_ACCESS_TOKEN: 'polar_test',
|
||||
POLAR_PRODUCT_ID: 'prod_test'
|
||||
});
|
||||
Polar.reset();
|
||||
}
|
||||
|
||||
/** Stand in for Polar only; every other request goes where it was going. */
|
||||
function polarAnswers(status: number, body: unknown) {
|
||||
globalThis.fetch = (async (input: Request | string | URL, init?: RequestInit) => {
|
||||
const url = input instanceof Request ? input.url : String(input);
|
||||
if (url.includes('polar.sh')) {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { 'content-type': 'application/json' }
|
||||
});
|
||||
}
|
||||
return realFetch(input as never, init);
|
||||
}) as typeof fetch;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = realFetch;
|
||||
Polar.reset();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (createdUserIds.length > 0) {
|
||||
await sql`delete from "user" where id in ${sql(createdUserIds)}`;
|
||||
}
|
||||
});
|
||||
|
||||
describe('GET /billing/portal', () => {
|
||||
test('a team Polar has never heard of gets a 404, not a 500', async () => {
|
||||
const headers = await person('portal-none');
|
||||
configure();
|
||||
polarAnswers(404, { error: 'ResourceNotFound', detail: 'Customer not found' });
|
||||
const res = await app.request('/billing/portal', { headers });
|
||||
expect(res.status).toBe(404);
|
||||
expect((await res.json()).message).toBe('This team has no billing yet');
|
||||
});
|
||||
|
||||
test('an unknown external id answered with a 422 is the same 404', async () => {
|
||||
const headers = await person('portal-422');
|
||||
configure();
|
||||
polarAnswers(422, { detail: [{ loc: ['body'], msg: 'Customer does not exist', type: 'x' }] });
|
||||
const res = await app.request('/billing/portal', { headers });
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
});
|
||||
@@ -207,10 +207,25 @@ export namespace Polar {
|
||||
* them, which is the one thing this integration exists to avoid.
|
||||
*/
|
||||
export const portal = fn(z.object({ teamId: z.string() }), async (input) => {
|
||||
const session = await client().customerSessions.create({
|
||||
externalCustomerId: input.teamId
|
||||
});
|
||||
return { url: session.customerPortalUrl };
|
||||
try {
|
||||
const session = await client().customerSessions.create({
|
||||
externalCustomerId: input.teamId
|
||||
});
|
||||
return { url: session.customerPortalUrl };
|
||||
} catch (err) {
|
||||
// A team that never checked out has no customer on their side, and
|
||||
// they refuse the session (404, or 422 for an unknown external id).
|
||||
// That is not our failure, so it must not surface as a 500.
|
||||
const status = (err as { statusCode?: number } | null)?.statusCode;
|
||||
if (status === 404 || status === 422) {
|
||||
throw new VisibleError(
|
||||
'not_found',
|
||||
ErrorCodes.NotFound.RESOURCE_NOT_FOUND,
|
||||
'This team has no billing yet'
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
/** The plan and status a team is on, as far as the provider is concerned. */
|
||||
|
||||
Reference in New Issue
Block a user