Skip to content

Commit 3ec9df8

Browse files
authored
Merge pull request #98 from TryGhost/gift-member-status
Update to support member.status = `gift`
2 parents d738c6d + b768e8c commit 3ec9df8

6 files changed

Lines changed: 40 additions & 3 deletions

File tree

src/commands/member.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export function registerMemberCommands(program: Command): void {
6666
.option('--limit <numberOrAll>', 'Number of members per page or "all"')
6767
.option('--page <number>', 'Page number')
6868
.option('--filter <nql>', 'NQL filter')
69-
.option('--status <status>', 'Member status (free|paid|comped)')
69+
.option('--status <status>', 'Member status (free|paid|comped|gift)')
7070
.option('--search <term>', 'Search term')
7171
.option('--include <relations>', 'Include relationships')
7272
.option('--fields <fields>', 'Select output fields')

src/lib/stats.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -607,7 +607,8 @@ function normalizeMembersSeries(rows: Record<string, unknown>[]): StatsSeriesPoi
607607
row.count ??
608608
getNumber(row.free_members ?? row.free) +
609609
getNumber(row.paid_members ?? row.paid) +
610-
getNumber(row.comped),
610+
getNumber(row.comped) +
611+
getNumber(row.gift),
611612
),
612613
mrr: null,
613614
subscriptions: null,

src/schemas/member.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export const MemberListInputSchema = z.object({
77
limit: z.union([z.number().int().positive().max(100), z.literal('all')]).optional(),
88
page: z.number().int().positive().optional(),
99
filter: z.string().optional(),
10-
status: z.enum(['free', 'paid', 'comped']).optional(),
10+
status: z.enum(['free', 'paid', 'comped', 'gift']).optional(),
1111
search: z.string().optional(),
1212
include: z.string().optional(),
1313
fields: z.string().optional(),

tests/commands-and-run.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1186,6 +1186,9 @@ describe('run + commands', () => {
11861186
await expect(run(['node', 'ghst', 'member', 'list', '--status', 'paid'])).resolves.toBe(
11871187
ExitCode.SUCCESS,
11881188
);
1189+
await expect(run(['node', 'ghst', 'member', 'list', '--status', 'gift'])).resolves.toBe(
1190+
ExitCode.SUCCESS,
1191+
);
11891192
await expect(run(['node', 'ghst', 'member', 'get', fixtureIds.memberId])).resolves.toBe(
11901193
ExitCode.SUCCESS,
11911194
);

tests/lib-stats.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,31 @@ describe('stats library', () => {
245245
]);
246246
});
247247

248+
test('includes gift members in the total_members fallback when the row carries a gift bucket', async () => {
249+
installGhostFixtureFetchMock({
250+
onRequest: ({ pathname, method }) => {
251+
if (pathname.endsWith('/ghost/api/admin/stats/member_count/') && method === 'GET') {
252+
return new Response(
253+
JSON.stringify({
254+
stats: [{ date: '2026-02-06', free: 100, paid: 5, comped: 2, gift: 3 }],
255+
meta: { totals: { free: 100, paid: 5, comped: 2, gift: 3 } },
256+
}),
257+
{
258+
status: 200,
259+
headers: { 'content-type': 'application/json' },
260+
},
261+
);
262+
}
263+
264+
return undefined;
265+
},
266+
});
267+
268+
const payload = await getStatsGrowth({}, { from: '2026-02-06', to: '2026-02-06' });
269+
270+
expect(payload.members[0]?.total_members).toBe(110);
271+
});
272+
248273
test('clips growth histories client-side to the selected range', async () => {
249274
installGhostFixtureFetchMock();
250275

tests/schemas.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
MemberBulkInputSchema,
2020
MemberCreateInputSchema,
2121
MemberGetInputSchema,
22+
MemberListInputSchema,
2223
MemberUpdateInputSchema,
2324
} from '../src/schemas/member.js';
2425
import {
@@ -222,6 +223,13 @@ describe('member schemas', () => {
222223
expectInvalid(MemberUpdateInputSchema, { id: 'member-1' }, 'Provide at least one update field');
223224
});
224225

226+
test('accepts gift alongside the existing member statuses on list input', () => {
227+
for (const status of ['free', 'paid', 'comped', 'gift'] as const) {
228+
expectValid<{ status?: string }>(MemberListInputSchema, { status });
229+
}
230+
expectInvalid(MemberListInputSchema, { status: 'unknown' });
231+
});
232+
225233
test('enforce member selectors and bulk destructive safeguards', () => {
226234
expectValid<{ email?: string }>(MemberGetInputSchema, { email: 'x@example.com' });
227235
expectValid<{ update?: boolean; labels?: string }>(MemberBulkInputSchema, {

0 commit comments

Comments
 (0)