diff --git a/src/commands/member.ts b/src/commands/member.ts index 662d71f..1811a1a 100644 --- a/src/commands/member.ts +++ b/src/commands/member.ts @@ -66,7 +66,7 @@ export function registerMemberCommands(program: Command): void { .option('--limit ', 'Number of members per page or "all"') .option('--page ', 'Page number') .option('--filter ', 'NQL filter') - .option('--status ', 'Member status (free|paid|comped)') + .option('--status ', 'Member status (free|paid|comped|gift)') .option('--search ', 'Search term') .option('--include ', 'Include relationships') .option('--fields ', 'Select output fields') diff --git a/src/lib/stats.ts b/src/lib/stats.ts index b3f673e..f72a19a 100644 --- a/src/lib/stats.ts +++ b/src/lib/stats.ts @@ -607,7 +607,8 @@ function normalizeMembersSeries(rows: Record[]): StatsSeriesPoi row.count ?? getNumber(row.free_members ?? row.free) + getNumber(row.paid_members ?? row.paid) + - getNumber(row.comped), + getNumber(row.comped) + + getNumber(row.gift), ), mrr: null, subscriptions: null, diff --git a/src/schemas/member.ts b/src/schemas/member.ts index c7e8cc6..6c83f47 100644 --- a/src/schemas/member.ts +++ b/src/schemas/member.ts @@ -7,7 +7,7 @@ export const MemberListInputSchema = z.object({ limit: z.union([z.number().int().positive().max(100), z.literal('all')]).optional(), page: z.number().int().positive().optional(), filter: z.string().optional(), - status: z.enum(['free', 'paid', 'comped']).optional(), + status: z.enum(['free', 'paid', 'comped', 'gift']).optional(), search: z.string().optional(), include: z.string().optional(), fields: z.string().optional(), diff --git a/tests/commands-and-run.test.ts b/tests/commands-and-run.test.ts index 2ca9297..2a81b3e 100644 --- a/tests/commands-and-run.test.ts +++ b/tests/commands-and-run.test.ts @@ -1186,6 +1186,9 @@ describe('run + commands', () => { await expect(run(['node', 'ghst', 'member', 'list', '--status', 'paid'])).resolves.toBe( ExitCode.SUCCESS, ); + await expect(run(['node', 'ghst', 'member', 'list', '--status', 'gift'])).resolves.toBe( + ExitCode.SUCCESS, + ); await expect(run(['node', 'ghst', 'member', 'get', fixtureIds.memberId])).resolves.toBe( ExitCode.SUCCESS, ); diff --git a/tests/lib-stats.test.ts b/tests/lib-stats.test.ts index 24d04ae..c38f346 100644 --- a/tests/lib-stats.test.ts +++ b/tests/lib-stats.test.ts @@ -245,6 +245,31 @@ describe('stats library', () => { ]); }); + test('includes gift members in the total_members fallback when the row carries a gift bucket', async () => { + installGhostFixtureFetchMock({ + onRequest: ({ pathname, method }) => { + if (pathname.endsWith('/ghost/api/admin/stats/member_count/') && method === 'GET') { + return new Response( + JSON.stringify({ + stats: [{ date: '2026-02-06', free: 100, paid: 5, comped: 2, gift: 3 }], + meta: { totals: { free: 100, paid: 5, comped: 2, gift: 3 } }, + }), + { + status: 200, + headers: { 'content-type': 'application/json' }, + }, + ); + } + + return undefined; + }, + }); + + const payload = await getStatsGrowth({}, { from: '2026-02-06', to: '2026-02-06' }); + + expect(payload.members[0]?.total_members).toBe(110); + }); + test('clips growth histories client-side to the selected range', async () => { installGhostFixtureFetchMock(); diff --git a/tests/schemas.test.ts b/tests/schemas.test.ts index d2c9d01..18f2831 100644 --- a/tests/schemas.test.ts +++ b/tests/schemas.test.ts @@ -19,6 +19,7 @@ import { MemberBulkInputSchema, MemberCreateInputSchema, MemberGetInputSchema, + MemberListInputSchema, MemberUpdateInputSchema, } from '../src/schemas/member.js'; import { @@ -222,6 +223,13 @@ describe('member schemas', () => { expectInvalid(MemberUpdateInputSchema, { id: 'member-1' }, 'Provide at least one update field'); }); + test('accepts gift alongside the existing member statuses on list input', () => { + for (const status of ['free', 'paid', 'comped', 'gift'] as const) { + expectValid<{ status?: string }>(MemberListInputSchema, { status }); + } + expectInvalid(MemberListInputSchema, { status: 'unknown' }); + }); + test('enforce member selectors and bulk destructive safeguards', () => { expectValid<{ email?: string }>(MemberGetInputSchema, { email: 'x@example.com' }); expectValid<{ update?: boolean; labels?: string }>(MemberBulkInputSchema, {