Skip to content

Commit d963262

Browse files
authored
feat: add prices command for managing plan prices (#13)
Add `prices` command with create, update, activate, deactivate, and delete subcommands for managing plan prices. Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
1 parent 94bed09 commit d963262

5 files changed

Lines changed: 509 additions & 0 deletions

File tree

ARCHITECTURE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ memberstack-cli/
2020
│ │ ├── custom-fields.ts # Custom field listing
2121
│ │ ├── members.ts # Member CRUD, search, pagination
2222
│ │ ├── plans.ts # Plan CRUD, ordering, redirects, permissions
23+
│ │ ├── prices.ts # Price management (create, update, activate, deactivate, delete)
2324
│ │ ├── records.ts # Record CRUD, query, find, import/export, bulk ops
2425
│ │ ├── skills.ts # Agent skill add/remove (wraps npx skills)
2526
│ │ ├── tables.ts # Data table CRUD, describe
@@ -43,6 +44,7 @@ memberstack-cli/
4344
│ │ ├── custom-fields.test.ts
4445
│ │ ├── members.test.ts
4546
│ │ ├── plans.test.ts
47+
│ │ ├── prices.test.ts
4648
│ │ ├── records.test.ts
4749
│ │ ├── skills.test.ts
4850
│ │ ├── tables.test.ts

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ memberstack skills add memberstack-cli
6060
| `apps` | View, create, update, delete, and restore apps |
6161
| `members` | List, create, update, delete, import/export, bulk ops |
6262
| `plans` | List, create, update, delete, and reorder plans |
63+
| `prices` | Create, update, activate, deactivate, and delete prices |
6364
| `tables` | List, create, update, delete, and describe schema |
6465
| `records` | CRUD, query, import/export, bulk ops |
6566
| `custom-fields` | List, create, update, and delete custom fields |

src/commands/prices.ts

Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
import { Command, Option } from "commander";
2+
import yoctoSpinner from "yocto-spinner";
3+
import { graphqlRequest } from "../lib/graphql-client.js";
4+
import { printError, printRecord, printSuccess } from "../lib/utils.js";
5+
6+
interface Price {
7+
active: boolean;
8+
amount: number;
9+
currency: string;
10+
id: string;
11+
memberCount: number | null;
12+
name: string | null;
13+
status: string;
14+
type: string;
15+
}
16+
17+
const PRICE_FIELDS = `
18+
id
19+
name
20+
status
21+
active
22+
amount
23+
type
24+
currency
25+
memberCount
26+
`;
27+
28+
const PRICE_TYPES = ["SUBSCRIPTION", "ONETIME"];
29+
const INTERVAL_TYPES = ["YEARLY", "MONTHLY", "WEEKLY"];
30+
const EXPIRATION_INTERVALS = ["MONTHS", "DAYS"];
31+
32+
const parseNumber = (value: string): number => {
33+
const num = Number(value);
34+
if (Number.isNaN(num)) {
35+
throw new Error(`Invalid number: ${value}`);
36+
}
37+
return num;
38+
};
39+
40+
export const pricesCommand = new Command("prices")
41+
.usage("<command> [options]")
42+
.description("Manage prices for plans");
43+
44+
pricesCommand
45+
.command("create")
46+
.description("Create a price for a plan")
47+
.requiredOption("--plan-id <id>", "Plan ID to add the price to")
48+
.requiredOption("--name <name>", "Price name")
49+
.requiredOption("--amount <amount>", "Price amount", parseNumber)
50+
.addOption(
51+
new Option("--type <type>", "Price type")
52+
.choices(PRICE_TYPES)
53+
.makeOptionMandatory()
54+
)
55+
.option("--currency <currency>", "Currency code (e.g. USD, EUR, GBP)", "usd")
56+
.addOption(
57+
new Option("--interval-type <type>", "Billing interval").choices(
58+
INTERVAL_TYPES
59+
)
60+
)
61+
.option(
62+
"--interval-count <count>",
63+
"Number of intervals between billings",
64+
parseNumber
65+
)
66+
.option("--setup-fee-amount <amount>", "Setup fee amount", parseNumber)
67+
.option("--setup-fee-name <name>", "Setup fee name")
68+
.option("--setup-fee-enabled", "Enable setup fee")
69+
.option("--free-trial-enabled", "Enable free trial")
70+
.option("--free-trial-requires-card", "Require card for free trial")
71+
.option(
72+
"--free-trial-days <days>",
73+
"Free trial duration in days",
74+
parseNumber
75+
)
76+
.option("--expiration-count <count>", "Expiration count", parseNumber)
77+
.addOption(
78+
new Option(
79+
"--expiration-interval <interval>",
80+
"Expiration interval"
81+
).choices(EXPIRATION_INTERVALS)
82+
)
83+
.option("--cancel-at-period-end", "Cancel at period end")
84+
.action(async (opts) => {
85+
const spinner = yoctoSpinner({ text: "Creating price..." }).start();
86+
try {
87+
const input: Record<string, unknown> = {
88+
planId: opts.planId,
89+
name: opts.name,
90+
amount: opts.amount,
91+
type: opts.type,
92+
currency: opts.currency,
93+
};
94+
if (opts.intervalType) {
95+
input.intervalType = opts.intervalType;
96+
}
97+
if (opts.intervalCount !== undefined) {
98+
input.intervalCount = opts.intervalCount;
99+
}
100+
if (opts.setupFeeAmount !== undefined) {
101+
input.setupFeeAmount = opts.setupFeeAmount;
102+
}
103+
if (opts.setupFeeName) {
104+
input.setupFeeName = opts.setupFeeName;
105+
}
106+
if (opts.setupFeeEnabled) {
107+
input.setupFeeEnabled = true;
108+
}
109+
if (opts.freeTrialEnabled) {
110+
input.freeTrialEnabled = true;
111+
}
112+
if (opts.freeTrialRequiresCard) {
113+
input.freeTrialRequiresCard = true;
114+
}
115+
if (opts.freeTrialDays !== undefined) {
116+
input.freeTrialDays = opts.freeTrialDays;
117+
}
118+
if (opts.expirationCount !== undefined) {
119+
input.expirationCount = opts.expirationCount;
120+
}
121+
if (opts.expirationInterval) {
122+
input.expirationInterval = opts.expirationInterval;
123+
}
124+
if (opts.cancelAtPeriodEnd) {
125+
input.cancelAtPeriodEnd = true;
126+
}
127+
128+
const result = await graphqlRequest<{ createPrice: Price }>({
129+
query: `mutation($input: CreatePriceInput!) {
130+
createPrice(input: $input) {
131+
${PRICE_FIELDS}
132+
}
133+
}`,
134+
variables: { input },
135+
});
136+
spinner.stop();
137+
printSuccess("Price created successfully.");
138+
printRecord(result.createPrice);
139+
} catch (error) {
140+
spinner.stop();
141+
printError(
142+
error instanceof Error ? error.message : "An unknown error occurred"
143+
);
144+
process.exitCode = 1;
145+
}
146+
});
147+
148+
pricesCommand
149+
.command("update")
150+
.description("Update a price")
151+
.argument("<priceId>", "Price ID to update")
152+
.option("--name <name>", "Price name")
153+
.option("--amount <amount>", "Price amount", parseNumber)
154+
.addOption(new Option("--type <type>", "Price type").choices(PRICE_TYPES))
155+
.option("--currency <currency>", "Currency code")
156+
.addOption(
157+
new Option("--interval-type <type>", "Billing interval").choices(
158+
INTERVAL_TYPES
159+
)
160+
)
161+
.option(
162+
"--interval-count <count>",
163+
"Number of intervals between billings",
164+
parseNumber
165+
)
166+
.option("--setup-fee-amount <amount>", "Setup fee amount", parseNumber)
167+
.option("--setup-fee-name <name>", "Setup fee name")
168+
.option("--setup-fee-enabled", "Enable setup fee")
169+
.option("--no-setup-fee-enabled", "Disable setup fee")
170+
.option("--free-trial-enabled", "Enable free trial")
171+
.option("--no-free-trial-enabled", "Disable free trial")
172+
.option("--free-trial-requires-card", "Require card for free trial")
173+
.option(
174+
"--free-trial-days <days>",
175+
"Free trial duration in days",
176+
parseNumber
177+
)
178+
.option("--expiration-count <count>", "Expiration count", parseNumber)
179+
.addOption(
180+
new Option(
181+
"--expiration-interval <interval>",
182+
"Expiration interval"
183+
).choices(EXPIRATION_INTERVALS)
184+
)
185+
.option("--cancel-at-period-end", "Cancel at period end")
186+
.option("--no-cancel-at-period-end", "Do not cancel at period end")
187+
.action(async (priceId: string, opts) => {
188+
const input: Record<string, unknown> = { priceId };
189+
if (opts.name) {
190+
input.name = opts.name;
191+
}
192+
if (opts.amount !== undefined) {
193+
input.amount = opts.amount;
194+
}
195+
if (opts.type) {
196+
input.type = opts.type;
197+
}
198+
if (opts.currency) {
199+
input.currency = opts.currency;
200+
}
201+
if (opts.intervalType) {
202+
input.intervalType = opts.intervalType;
203+
}
204+
if (opts.intervalCount !== undefined) {
205+
input.intervalCount = opts.intervalCount;
206+
}
207+
if (opts.setupFeeAmount !== undefined) {
208+
input.setupFeeAmount = opts.setupFeeAmount;
209+
}
210+
if (opts.setupFeeName) {
211+
input.setupFeeName = opts.setupFeeName;
212+
}
213+
if (opts.setupFeeEnabled !== undefined) {
214+
input.setupFeeEnabled = opts.setupFeeEnabled;
215+
}
216+
if (opts.freeTrialEnabled !== undefined) {
217+
input.freeTrialEnabled = opts.freeTrialEnabled;
218+
}
219+
if (opts.freeTrialRequiresCard) {
220+
input.freeTrialRequiresCard = true;
221+
}
222+
if (opts.freeTrialDays !== undefined) {
223+
input.freeTrialDays = opts.freeTrialDays;
224+
}
225+
if (opts.expirationCount !== undefined) {
226+
input.expirationCount = opts.expirationCount;
227+
}
228+
if (opts.expirationInterval) {
229+
input.expirationInterval = opts.expirationInterval;
230+
}
231+
if (opts.cancelAtPeriodEnd !== undefined) {
232+
input.cancelAtPeriodEnd = opts.cancelAtPeriodEnd;
233+
}
234+
235+
if (Object.keys(input).length <= 1) {
236+
printError(
237+
"No update options provided. Use --help to see available options."
238+
);
239+
process.exitCode = 1;
240+
return;
241+
}
242+
243+
const spinner = yoctoSpinner({ text: "Updating price..." }).start();
244+
try {
245+
const result = await graphqlRequest<{ updatePrice: Price }>({
246+
query: `mutation($input: UpdatePriceInput!) {
247+
updatePrice(input: $input) {
248+
${PRICE_FIELDS}
249+
}
250+
}`,
251+
variables: { input },
252+
});
253+
spinner.stop();
254+
printSuccess("Price updated successfully.");
255+
printRecord(result.updatePrice);
256+
} catch (error) {
257+
spinner.stop();
258+
printError(
259+
error instanceof Error ? error.message : "An unknown error occurred"
260+
);
261+
process.exitCode = 1;
262+
}
263+
});
264+
265+
pricesCommand
266+
.command("activate")
267+
.description("Reactivate a price")
268+
.argument("<priceId>", "Price ID to reactivate")
269+
.action(async (priceId: string) => {
270+
const spinner = yoctoSpinner({ text: "Reactivating price..." }).start();
271+
try {
272+
const result = await graphqlRequest<{ reactivatePrice: Price }>({
273+
query: `mutation($input: ReactivatePriceInput!) {
274+
reactivatePrice(input: $input) {
275+
${PRICE_FIELDS}
276+
}
277+
}`,
278+
variables: { input: { priceId } },
279+
});
280+
spinner.stop();
281+
printSuccess("Price reactivated.");
282+
printRecord(result.reactivatePrice);
283+
} catch (error) {
284+
spinner.stop();
285+
printError(
286+
error instanceof Error ? error.message : "An unknown error occurred"
287+
);
288+
process.exitCode = 1;
289+
}
290+
});
291+
292+
pricesCommand
293+
.command("deactivate")
294+
.description("Deactivate a price")
295+
.argument("<priceId>", "Price ID to deactivate")
296+
.action(async (priceId: string) => {
297+
const spinner = yoctoSpinner({ text: "Deactivating price..." }).start();
298+
try {
299+
const result = await graphqlRequest<{ deactivatePrice: Price }>({
300+
query: `mutation($input: DeactivatePriceInput!) {
301+
deactivatePrice(input: $input) {
302+
${PRICE_FIELDS}
303+
}
304+
}`,
305+
variables: { input: { priceId } },
306+
});
307+
spinner.stop();
308+
printSuccess("Price deactivated.");
309+
printRecord(result.deactivatePrice);
310+
} catch (error) {
311+
spinner.stop();
312+
printError(
313+
error instanceof Error ? error.message : "An unknown error occurred"
314+
);
315+
process.exitCode = 1;
316+
}
317+
});
318+
319+
pricesCommand
320+
.command("delete")
321+
.description("Delete a price")
322+
.argument("<priceId>", "Price ID to delete")
323+
.action(async (priceId: string) => {
324+
const spinner = yoctoSpinner({ text: "Deleting price..." }).start();
325+
try {
326+
const result = await graphqlRequest<{ deletePrice: Price }>({
327+
query: `mutation($input: DeletePriceInput!) {
328+
deletePrice(input: $input) {
329+
${PRICE_FIELDS}
330+
}
331+
}`,
332+
variables: { input: { priceId } },
333+
});
334+
spinner.stop();
335+
printSuccess(`Price "${result.deletePrice.id}" deleted.`);
336+
} catch (error) {
337+
spinner.stop();
338+
printError(
339+
error instanceof Error ? error.message : "An unknown error occurred"
340+
);
341+
process.exitCode = 1;
342+
}
343+
});

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { authCommand } from "./commands/auth.js";
88
import { customFieldsCommand } from "./commands/custom-fields.js";
99
import { membersCommand } from "./commands/members.js";
1010
import { plansCommand } from "./commands/plans.js";
11+
import { pricesCommand } from "./commands/prices.js";
1112
import { recordsCommand } from "./commands/records.js";
1213
import { skillsCommand } from "./commands/skills.js";
1314
import { tablesCommand } from "./commands/tables.js";
@@ -60,6 +61,7 @@ program.addCommand(authCommand);
6061
program.addCommand(whoamiCommand);
6162
program.addCommand(membersCommand);
6263
program.addCommand(plansCommand);
64+
program.addCommand(pricesCommand);
6365
program.addCommand(tablesCommand);
6466
program.addCommand(recordsCommand);
6567
program.addCommand(customFieldsCommand);

0 commit comments

Comments
 (0)