mirror of
https://github.com/HeyPuter/puter.git
synced 2026-09-21 04:36:18 +00:00
feat: list the uuids of the seats a team is billed for
Per-tier quantities need to know *which* seats are active, not just how many: the count has to be split by the tier each one is on. `countActiveSeats` answers the old question and stays for the cap and the console copy. Keyed by `uid` rather than the numeric id, because the billing side holds uids and would otherwise have to look the team up twice. Same suspension filter, and a test asserts the two agree so they cannot drift apart. Falsified: dropping the suspension clause fails "leaves out a suspended seat, matching countActiveSeats".
This commit is contained in:
@@ -515,6 +515,39 @@ describe('TeamStore', () => {
|
||||
expect(await store.countActiveSeats(team.id)).toBe(2);
|
||||
});
|
||||
|
||||
it('lists the uuids of the seats it bills for', async () => {
|
||||
const { team, members } = await seatedTeam(2);
|
||||
const uuids = await store.listActiveSeatUuids(team.uid);
|
||||
const expected = await Promise.all(
|
||||
members.map(async (m) =>
|
||||
(await server.stores.user.getById(m.id))!.uuid,
|
||||
),
|
||||
);
|
||||
expect(uuids.sort()).toEqual(expected.sort());
|
||||
});
|
||||
|
||||
it('leaves out a suspended seat, matching countActiveSeats', async () => {
|
||||
const { team, members } = await seatedTeam(2);
|
||||
await suspend(members[0].id);
|
||||
const uuids = await store.listActiveSeatUuids(team.uid);
|
||||
expect(uuids).toHaveLength(1);
|
||||
expect(uuids).toHaveLength(
|
||||
await store.countActiveSeats(team.id),
|
||||
);
|
||||
});
|
||||
|
||||
it('leaves out the payer, who is not a seat', async () => {
|
||||
const { team } = await seatedTeam(1);
|
||||
const payer = await makeUser();
|
||||
await server.stores.user.update(payer.id, { password: 'hashed' });
|
||||
await store.addMember(team.uid, payer.id, { orgOwned: false });
|
||||
expect(await store.listActiveSeatUuids(team.uid)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('is empty for an unknown team, not an error', async () => {
|
||||
expect(await store.listActiveSeatUuids('no-such-team')).toEqual([]);
|
||||
});
|
||||
|
||||
it('is zero for a team with no seats, not an error', async () => {
|
||||
const { team } = await seatedTeam(0);
|
||||
expect(await store.countActiveSeats(team.id)).toBe(0);
|
||||
|
||||
@@ -597,6 +597,24 @@ export class TeamStore extends PuterStore {
|
||||
return Number(rows[0]?.n ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uuids of the seats a team is actually billed for, keyed by `uid` because
|
||||
* that is what the billing side holds. Same filter as `countActiveSeats`;
|
||||
* the quantities are per tier now, so a count is no longer enough.
|
||||
*/
|
||||
async listActiveSeatUuids(teamUid: string): Promise<string[]> {
|
||||
const rows = (await this.clients.db.read(
|
||||
'SELECT u.`uuid` AS `uuid` FROM `jct_user_group` ug ' +
|
||||
'JOIN `user` u ON u.`id` = ug.`user_id` ' +
|
||||
'JOIN `group` g ON g.`id` = ug.`group_id` ' +
|
||||
`WHERE g.\`uid\` = ? AND g.${this.#live()} ` +
|
||||
'AND ug.`org_owned` = 1 ' +
|
||||
'AND (u.`suspended` IS NULL OR u.`suspended` = 0)',
|
||||
[teamUid, TEAM_KIND],
|
||||
)) as unknown as { uuid: string }[];
|
||||
return rows.map((r) => r.uuid).filter(Boolean);
|
||||
}
|
||||
|
||||
/** Live teams this user owns. Soft-deleted ones do not count. */
|
||||
async countOwned(ownerUserId: number): Promise<number> {
|
||||
const rows = (await this.clients.db.read(
|
||||
|
||||
Reference in New Issue
Block a user