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