fix(billing): a customer may already exist, and may belong to somebody else

Registering a team assumed creating a subscription would create the customer it
names. It does not — the customer has to exist first, and creating one fails if
the address is already taken, which it often is: a checkout taken before the
team existed leaves one behind, and so does making one by hand.

So the lookup is now in three steps. A customer already carrying this team id is
used. Otherwise one is found by address and adopted. Otherwise one is made.

**A customer carrying a different team's id is left alone.** Taking it would
move where that subscription is billed, and the team that lost it would go quiet
rather than fail — which is the kind of thing found a month later in a revenue
figure that does not add up.

A team whose owner has no address yet gets no customer, reported rather than
guessed around: an invented address makes a customer nobody can be reached at,
and the next call fixes it once there is a real one.

Checked against the provider rather than only in tests: all four paths — new,
repeated, taken by another team, and no address — behave as written.
This commit is contained in:
Wanjohi
2026-09-19 01:39:56 +03:00
parent 5b90bd0f0c
commit abd27f2d5f
2 changed files with 78 additions and 23 deletions
+7 -1
View File
@@ -7,6 +7,7 @@ import { Database } from '../db/index.js';
import { Examples } from '../examples.js';
import { fn } from '../fn.js';
import { Identifier } from '../id.js';
import { User } from '../user/index.js';
import { TeamMemberTable } from './member.sql.js';
import { TeamTable } from './team.sql.js';
@@ -91,7 +92,12 @@ export namespace Team {
// idempotent.
Database.effect(async () => {
try {
await Polar.ensureFree({ teamId: input.id });
// The owner's address, so a customer can be found or made. A team
// created by somebody with no verified address gets no customer
// yet, which is a state `ensureFree` reports rather than guesses
// its way out of.
const owner = await User.fromID(ownerId);
await Polar.ensureFree({ teamId: input.id, email: owner?.email ?? undefined });
} catch (error) {
// eslint-disable-next-line no-console
console.error('could not register team with the payment provider:', error);