dodopayments-cli 2.0.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.ts DELETED
@@ -1,494 +0,0 @@
1
- #!/usr/bin/env node
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import DodoPayments from 'dodopayments';
5
- import { input, select } from '@inquirer/prompts';
6
- import open from 'open';
7
- import { CurrencyToSymbolMap } from './utils/currency-to-symbol-map';
8
- import fs from 'node:fs';
9
-
10
- const DodoCliLogo = `
11
- /$$$$$$$ /$$ /$$$$$$ /$$ /$$$$$$
12
- | $$__ $$ | $$ /$$__ $$| $$ |_ $$_/
13
- | $$ \\ $$ /$$$$$$ /$$$$$$$ /$$$$$$ | $$ \\__/| $$ | $$
14
- | $$ | $$ /$$__ $$ /$$__ $$ /$$__ $$ | $$ | $$ | $$
15
- | $$ | $$| $$ \\ $$| $$ | $$| $$ \\ $$ | $$ | $$ | $$
16
- | $$ | $$| $$ | $$| $$ | $$| $$ | $$ | $$ $$| $$ | $$
17
- | $$$$$$$/| $$$$$$/| $$$$$$$| $$$$$$/ | $$$$$$/| $$$$$$$$ /$$$$$$
18
- |_______/ \\______/ \\_______/ \\______/ \\______/ |________/|______/
19
-
20
- The CLI to manage Dodo Payments!
21
- `;
22
-
23
- // The below is used to check if the error is a Dodo Payments error or not in the API Request
24
- type DodoPaymentsAPIError = {
25
- error: {
26
- code: 'NOT_FOUND';
27
- message: string;
28
- }
29
- }
30
-
31
- function isDodoPaymentsAPIError(e: unknown): e is DodoPaymentsAPIError {
32
- return (
33
- typeof e === "object" &&
34
- e !== null &&
35
- "error" in e &&
36
- typeof (e as any).error?.code === "string"
37
- );
38
- }
39
-
40
- // Function to add hyperlinked text
41
- const link = (text: string, url: string) =>
42
- `\u001b]8;;${url}\u001b\\${text}\u001b]8;;\u001b\\`;
43
-
44
- // For help commands
45
- const usage: {
46
- [key: string]: {
47
- command: string,
48
- description: string
49
- }[]
50
- } = {
51
- products: [
52
- { command: 'list', description: 'List your products' },
53
- { command: 'create', description: 'Create a new product' },
54
- { command: 'info', description: 'Get info about a product' }
55
- ],
56
- payments: [
57
- { command: 'list', description: 'List your payments' },
58
- { command: 'info', description: 'Information about a payment' },
59
- ],
60
- customers: [
61
- { command: 'list', description: 'List your customers' },
62
- { command: 'create', description: 'Create a customer' },
63
- { command: 'update', description: 'Update a customer' },
64
- ],
65
- discounts: [
66
- { command: 'list', description: 'List your discounts' },
67
- { command: 'create', description: 'Create a discount' },
68
- { command: 'delete', description: 'Remove a discount' },
69
- ],
70
- wh: [
71
- { command: '', description: 'Send a webhook event' },
72
- ]
73
- }
74
-
75
-
76
- const args = process.argv;
77
- const category = args[2];
78
- const subCommand = args[3];
79
- const homedir = os.homedir();
80
-
81
- const RenderHelp = () => {
82
- console.log(DodoCliLogo);
83
- // List all available methods
84
- // todo: Add more comments to make it clear what's being done
85
- Object.keys(usage).forEach(e => {
86
- console.log(`Category: ${e}`);
87
- (usage as any)[e].forEach((y: { command: string, description: string }) => {
88
- console.log(`dodo ${e} ${y.command} - ${y.description}`)
89
- });
90
- // Blank space as a separator
91
- console.log("");
92
- });
93
- }
94
-
95
- // Added this to the top so that it can bypass all further auth that happens for the login route
96
- if (category === 'login') {
97
- open('https://app.dodopayments.com/developer/api-keys');
98
- const API_KEY = await input({ message: 'Enter your Dodo Payments API Key:', required: true });
99
- const MODE = await select({
100
- choices: [{ name: "Test Mode", value: 'test_mode' }, { name: "Live Mode", value: 'live_mode' }],
101
- message: 'Choose the environment:'
102
- });
103
-
104
- // Initialize the Dodo Payment client to test the API key
105
- const newDodoClient = new DodoPayments({
106
- bearerToken: API_KEY,
107
- environment: (MODE as 'test_mode' | 'live_mode') // as 'test_mode' | 'live_mode' used to bypass ts error
108
- });
109
-
110
- console.log("Verifying Dodo Payments API Key");
111
-
112
- try {
113
- // Make this request just to confirm whether API key is correct or not
114
- await newDodoClient.products.list({ page_size: 1 });
115
- console.log('Successfully verified your Dodo Payments API Key!');
116
- } catch (err) {
117
- console.log("Something went wrong while authenticating:", err);
118
- process.exit(1);
119
- };
120
-
121
-
122
- console.log("Storing / Updating existing configuration...")
123
- let existingConfig;
124
- try {
125
- existingConfig = Object.create(JSON.parse(fs.readFileSync(path.join(homedir, '.dodopayments', 'api-key'), 'utf-8')));
126
- } catch {
127
- existingConfig = {};
128
- }
129
-
130
- existingConfig[MODE] = API_KEY;
131
- // Make the ~/.dodopayments directory if it's not present
132
- if (!fs.existsSync(path.join(homedir, '.dodopayments'))) {
133
- fs.mkdirSync(path.join(homedir, '.dodopayments'));
134
- }
135
- fs.writeFileSync(path.join(homedir, '.dodopayments', 'api-key'), JSON.stringify(existingConfig));
136
-
137
- // Mode will always be either test_mode or live_mode
138
- existingConfig[MODE] = API_KEY;
139
- console.log("Setup complete successfully!");
140
- process.exit(0);
141
- }
142
-
143
- // Webhook is managed completely by another file
144
- if (category === 'wh') {
145
- await import('./dodo-webhooks/index.ts');
146
- }
147
-
148
- // Normal functions which require the API key to be present start from here
149
- // Authentication part
150
- // Read the API key config
151
- if (!fs.existsSync(path.join(homedir, '.dodopayments', 'api-key'))) {
152
- if (category && subCommand) {
153
- console.log('Please login using `dodo login` command first!');
154
- process.exit(0);
155
- } else if (category) {
156
- if (category in usage) {
157
- console.log(`Category: ${category}`);
158
- (usage as any)[category]!.forEach((e: { command: string, description: string }) => {
159
- console.log(`dodo ${category} ${e.command} - ${e.description}`)
160
- });
161
- console.log('\nPlease login using `dodo login` command first!');
162
- } else {
163
- RenderHelp();
164
- }
165
- process.exit(0);
166
- } else {
167
- RenderHelp();
168
- console.log('Please login using `dodo login` command first!');
169
- process.exit(0);
170
- }
171
- }
172
-
173
- // Parse the API key config
174
- let existingAPIKeyConfigParsed;
175
- try {
176
- existingAPIKeyConfigParsed = JSON.parse(fs.readFileSync(path.join(homedir, '.dodopayments', 'api-key'), 'utf-8'));
177
- } catch {
178
- // Delete API config if something fails with parsing
179
- fs.rmSync(path.join(homedir, '.dodopayments', 'api-key'), { force: true });
180
- console.log("Failed to decode API Key configuration. Your config has been reset. Please log in again using `dodo login`");
181
- process.exit(0);
182
- }
183
-
184
-
185
- // Final variables
186
- let API_KEY;
187
- let MODE;
188
-
189
- // Retrive the keys of the parsed API key config to auto determine the environment if possible.
190
- const existingAPIKeyConfigParsedKeys = Object.keys(existingAPIKeyConfigParsed);
191
-
192
- // If there is only one mode auth mehtod then
193
- if (existingAPIKeyConfigParsedKeys.length === 1) {
194
- MODE = existingAPIKeyConfigParsedKeys[0]
195
- API_KEY = existingAPIKeyConfigParsed[MODE!];
196
- }
197
- else {
198
- // If there are multiple modes (i.e. both test mode & live mode) then prompt the user to select one environment to continue
199
- MODE = await select({
200
- choices: [{ name: "Test Mode", value: 'test_mode' }, { name: "Live Mode", value: 'live_mode' }],
201
- message: 'Choose the environment:'
202
- });
203
- API_KEY = existingAPIKeyConfigParsed[MODE];
204
- }
205
-
206
- // Initialize the Dodo Payments SDK to be used from now on
207
- const DodoClient = new DodoPayments({
208
- bearerToken: API_KEY,
209
- environment: (MODE as 'test_mode' | 'live_mode') // as 'test_mode' | 'live_mode' used to bypass ts error
210
- });
211
-
212
- // Continuation of other functions that require api key
213
- if (category === 'products') {
214
- if (subCommand === 'list') {
215
- const page = await input({
216
- message: 'Enter page:',
217
- default: "1",
218
- validate: (e => e.trim() !== '')
219
- });
220
-
221
- const fetchedData = await DodoClient.products.list({ page_number: parseInt(page) - 1, page_size: 100 });
222
- const table = fetchedData.items.map(e => ({
223
- name: e.name,
224
- product_id: e.product_id,
225
- created_at: new Date(e.created_at).toLocaleString(),
226
- ...e.is_recurring ? {
227
- price: `${CurrencyToSymbolMap[e.price_detail!.currency] || (e.price_detail!.currency + ' ')}${(e.price! * 0.01).toFixed(2)} Every ${e.price_detail?.payment_frequency_count} ${e.price_detail?.payment_frequency_interval}`,
228
- } : {
229
- price: `${CurrencyToSymbolMap[e.price_detail!.currency] || (e.price_detail!.currency + ' ')}${(e.price! * 0.01).toFixed(2)} (One Time)`,
230
- },
231
- edit_url: link('CTRL + Click to open', `https://app.dodopayments.com/products/edit?id=${e.product_id}`),
232
- }));
233
-
234
- console.table(table);
235
- } else if (subCommand === 'create') {
236
- open('https://app.dodopayments.com/products/create');
237
- } else if (subCommand === 'info') {
238
- try {
239
- const product_id = await input({
240
- message: "Enter product ID:",
241
- validate: (e => e.startsWith('pdt_') || 'Please enter a valid product ID!')
242
- });
243
-
244
- const info = await DodoClient.products.retrieve(product_id);
245
- console.table({
246
- product_id: info.product_id,
247
- name: info.name,
248
- ...info.description?.trim() !== '' && { description: info.description },
249
- created_at: new Date(info.created_at).toLocaleString(),
250
- updated_at: new Date(info.updated_at).toLocaleString(),
251
- ...info.is_recurring ? {
252
- // .fixed_price for usage based billing
253
- price: `${CurrencyToSymbolMap[info.price.currency] || (info.price.currency + ' ')}${((info.price.price || info.price.fixed_price) * 0.01).toFixed(2)} Every ${info.price.payment_frequency_count} ${info.price.payment_frequency_interval}`,
254
- } : {
255
- price: `${CurrencyToSymbolMap[info.price.currency] || (info.price.currency + ' ')}${((info.price.price || info.price.fixed_price) * 0.01).toFixed(2)} (One Time)`,
256
- },
257
- tax_category: info.tax_category,
258
- edit_url: link('CTRL + Click to open', `https://app.dodopayments.com/products/edit?id=${info.product_id}`)
259
- });
260
- } catch (e) {
261
- if (isDodoPaymentsAPIError(e) && e.error.code === "NOT_FOUND") {
262
- console.log("Incorrect product ID!");
263
- } else {
264
- console.error(e);
265
- }
266
- }
267
- } else {
268
- usage.products!.forEach(e => {
269
- console.log(`dodo products ${e.command} - ${e.description}`)
270
- });
271
- }
272
- } else if (category === 'payments') {
273
- if (subCommand === 'list') {
274
- const page = await input({
275
- message: 'Enter page:',
276
- default: "1",
277
- validate: (e => e.trim() !== '')
278
- });
279
- const payments = (await DodoClient.payments.list({ page_number: parseInt(page) - 1, page_size: 100 })).items;
280
- const paymentsTable = payments.map(payment => {
281
- return {
282
- 'payment id': payment.payment_id,
283
- 'created at': new Date(payment.created_at).toLocaleString(),
284
- 'subscription id': payment.subscription_id,
285
- 'total amount': `${CurrencyToSymbolMap[payment.currency] || (payment.currency + ' ')}${(payment.total_amount * 0.01).toFixed(2)}`,
286
- status: payment.status,
287
- 'more info': link('CTRL + Click to open', `https://app.dodopayments.com/transactions/payments/${payment.payment_id}`)
288
- };
289
- });
290
- console.table(paymentsTable);
291
- } else if (subCommand === 'info') {
292
- try {
293
- const payment_id = 'pay_0NWiGvZPWxeWeNWISbfat';
294
- const payment_info = await DodoClient.payments.retrieve(payment_id);
295
- console.log(payment_info);
296
- const payment_table = {
297
- 'payment id': payment_info.payment_id,
298
- status: payment_info.status,
299
- 'total amount': `${CurrencyToSymbolMap[payment_info.currency] || payment_info.currency + ' '}${(payment_info.total_amount * 0.01).toFixed(2)}`,
300
- 'payment method': payment_info.payment_method,
301
- createdAt: new Date(payment_info.created_at).toLocaleString(),
302
- customer: payment_info.customer.customer_id,
303
- 'customer email': payment_info.customer.email,
304
- ...payment_info.subscription_id && {
305
- 'subscription id': payment_info.subscription_id
306
- },
307
- 'billing address street': `${payment_info.billing.street}`,
308
- 'billing address state': `${payment_info.billing.state}`,
309
- 'billing address city': `${payment_info.billing.city}`,
310
- 'billing address country': `${payment_info.billing.country}`,
311
- 'billing address zipcode': `${payment_info.billing.zipcode}`,
312
- 'more info': link('Ctrl + Click to open', `https://app.dodopayments.com/transactions/payments/${payment_info.payment_id}`)
313
- }
314
- console.table(payment_table);
315
- } catch (e) {
316
- if (isDodoPaymentsAPIError(e) && e.error.code === 'NOT_FOUND') {
317
- console.log("Incorrect payment ID!")
318
- } else {
319
- console.error(e);
320
- }
321
- }
322
- } else {
323
- usage.payments!.forEach(e => {
324
- console.log(`dodo payments ${e.command} - ${e.description}`)
325
- });
326
- }
327
- } else if (category === 'customers') {
328
- if (subCommand === 'list') {
329
- const page = await input({
330
- message: 'Enter page:',
331
- default: "1",
332
- validate: (e => e.trim() !== '')
333
- });
334
- console.table((await DodoClient.customers.list({ page_number: parseInt(page) - 1, page_size: 100 })).items, ['customer_id', 'name', 'email', 'phone_number']);
335
- } else if (subCommand === 'create') {
336
- const name = await input({
337
- message: "Enter Name: ",
338
- validate: (e => e.trim() !== '')
339
- });
340
-
341
- const email = await input({
342
- message: "Enter Email: ",
343
- validate: (e => e.trim() !== '')
344
- });
345
-
346
- const phone = await input({
347
- message: "Enter Phone Number: ",
348
- });
349
-
350
- const creation = await DodoClient.customers.create({
351
- name,
352
- email,
353
- phone_number: phone.trim() !== '' ? phone : null
354
- });
355
-
356
- console.log('Customer Successfully Created!');
357
- console.table([creation], ['customer_id', 'name', 'email', 'phone_number']);
358
- } else if (subCommand === 'update') {
359
- const customer_id = await input({
360
- message: "Enter customer ID:",
361
- validate: (e => e.startsWith('cus_') || 'Please enter a valid customer ID!')
362
- });
363
-
364
- try {
365
- const existingInfo = await DodoClient.customers.retrieve(customer_id);
366
- const name = await input({
367
- message: "Enter customer name:",
368
- default: existingInfo.name
369
- });
370
-
371
- const phone = await input({
372
- message: "Enter customer phone:",
373
- default: existingInfo.phone_number?.toString()
374
- });
375
-
376
- const updated = await DodoClient.customers.update(customer_id, {
377
- name: name,
378
- phone_number: phone.trim() !== '' ? phone : null
379
- });
380
-
381
- console.table([updated], ['customer_id', 'name', 'email', 'phone_number']);
382
- } catch (e) {
383
- if (isDodoPaymentsAPIError(e) && e.error.code === "NOT_FOUND") {
384
- console.log("Incorrect customer ID!");
385
- } else {
386
- console.error(e);
387
- }
388
- }
389
- } else {
390
- usage.products!.forEach(e => {
391
- console.log(`dodo customers ${e.command} - ${e.description}`)
392
- });
393
- }
394
- } else if (category === 'discounts') {
395
- if (subCommand === 'list') {
396
- const page = await input({
397
- message: 'Enter page:',
398
- default: "1",
399
- validate: (e => e.trim() !== '')
400
- });
401
- const discounts = await DodoClient.discounts.list({ page_number: parseInt(page) - 1, page_size: 100 });
402
- const discountsTable = discounts.items.map(e => (
403
- {
404
- name: e.name,
405
- code: e.code,
406
- 'discount id': e.discount_id,
407
- 'created at': new Date(e.created_at).toLocaleString(),
408
- ...e.type === 'percentage' ? {
409
- amount: `${(e.amount * 0.01).toFixed(2)}%`
410
- } : {
411
- // I just added this in case of a breaking change in the future
412
- amount: e.amount
413
- },
414
- 'more info': link('Ctrl + Click for more info', `https://app.dodopayments.com/sales/discounts/edit?id=${e.discount_id}`)
415
- }
416
- ));
417
-
418
- console.table(discountsTable);
419
- } else if (subCommand === 'create') {
420
- const name = await input({
421
- message: "Enter discount name:",
422
- validate: (e => e.trim() !== '')
423
- });
424
-
425
- const percentage = await input({
426
- message: "Enter discount percentage:",
427
- // Make sure user enters valid value
428
- validate: (e => {
429
- const parsed = parseFloat(e);
430
- if (!Number.isNaN(parsed) && parsed > 0 && parsed <= 100) {
431
- return true;
432
- } else {
433
- return false;
434
- }
435
- })
436
- });
437
-
438
- const code = await input({
439
- message: "Enter discount code (Optional):"
440
- });
441
-
442
- const cycles = await input({
443
- message: "Enter discount cycles (Optional):"
444
- });
445
-
446
- const newDiscount = await DodoClient.discounts.create({
447
- name,
448
- code: code.trim() !== '' ? code : null,
449
- amount: parseFloat(percentage) * 100,
450
- type: 'percentage',
451
- // If the subscription cycles is provided
452
- ...cycles.trim() !== '' && {
453
- subscription_cycles: parseInt(cycles)
454
- }
455
- });
456
-
457
- console.log('Discount created successfully!');
458
- console.table({
459
- name: newDiscount.name,
460
- code: newDiscount.code,
461
- 'discount id': newDiscount.discount_id,
462
- ...cycles.trim() !== '' && {
463
- 'subscription cycles': newDiscount.subscription_cycles
464
- }
465
- });
466
- } else if (subCommand === 'delete') {
467
- await DodoClient.discounts.delete(await input({
468
- message: "Enter discount ID to be deleted:",
469
- validate: (e => e.startsWith('dsc_'))
470
- }));
471
-
472
- console.log("Successfully deleted discount!");
473
- } else {
474
- usage.discounts!.forEach(e => {
475
- console.log(`dodo discounts ${e.command} - ${e.description}`)
476
- });
477
- }
478
- } else if (category === 'licences') {
479
- if (subCommand === 'list') {
480
- const page = await input({
481
- message: 'Enter page:',
482
- default: "1",
483
- validate: (e => e.trim() !== '')
484
- });
485
- const licences = await DodoClient.licenseKeys.list({ page_number: parseInt(page) - 1, page_size: 100 });
486
- console.log(licences.items);
487
- } else {
488
- usage.licences!.forEach(e => {
489
- console.log(`dodo licences ${e.command} - ${e.description}`)
490
- });
491
- }
492
- } else {
493
- RenderHelp();
494
- }
@@ -1,8 +0,0 @@
1
- export const CurrencyToSymbolMap: {
2
- [key: string]: string
3
- } = {
4
- "USD": "$",
5
- "INR": "₹",
6
- "EUR": "€",
7
- "GBP": "£"
8
- }