paymongo-cli 1.4.7 → 1.4.8

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.
@@ -0,0 +1,293 @@
1
+ import Table from 'cli-table3';
2
+ import chalk from 'chalk';
3
+ import { CommandError } from '../../utils/errors.js';
4
+ import { AVAILABLE_TRIGGER_EVENTS, createTriggerContext, failTriggerCommand, generateWebhookPayload, printJsonResponse, sendWebhookRequest, } from './helpers.js';
5
+ export async function sendWebhookEvent(options) {
6
+ const { spinner, configManager, logger, store } = createTriggerContext();
7
+ try {
8
+ const config = await configManager.load();
9
+ let selectedEvent = options.event;
10
+ let webhookUrl = options.url || config?.webhooks?.url;
11
+ if (!selectedEvent) {
12
+ spinner.stop();
13
+ const { select, input } = await import('@inquirer/prompts');
14
+ selectedEvent = await select({
15
+ message: 'Select webhook event to trigger:',
16
+ choices: AVAILABLE_TRIGGER_EVENTS.map((event) => ({
17
+ name: event,
18
+ value: event,
19
+ })),
20
+ });
21
+ const urlInput = await input({
22
+ message: 'Webhook URL:',
23
+ default: webhookUrl || '',
24
+ validate: (value) => {
25
+ try {
26
+ new URL(value);
27
+ return true;
28
+ }
29
+ catch {
30
+ return 'Please enter a valid URL';
31
+ }
32
+ },
33
+ });
34
+ webhookUrl = urlInput || webhookUrl;
35
+ }
36
+ if (!webhookUrl) {
37
+ console.error(chalk.red('❌ No webhook URL provided. Use --url option or configure in .paymongo file'));
38
+ throw new CommandError();
39
+ }
40
+ if (!selectedEvent) {
41
+ console.error(chalk.red('❌ No event selected'));
42
+ throw new CommandError();
43
+ }
44
+ spinner.start('Generating webhook payload...');
45
+ const webhookPayload = generateWebhookPayload(selectedEvent);
46
+ if (options.json) {
47
+ console.log(JSON.stringify(webhookPayload, null, 2));
48
+ return;
49
+ }
50
+ spinner.succeed('Webhook event generated');
51
+ console.log(chalk.bold.blue('\n🚀 Webhook Event Trigger'));
52
+ console.log(chalk.gray('─'.repeat(50)));
53
+ console.log(`${chalk.bold('Event:')} ${chalk.cyan(selectedEvent)}`);
54
+ console.log(`${chalk.bold('URL:')} ${chalk.yellow(webhookUrl)}`);
55
+ console.log(`${chalk.bold('Timestamp:')} ${new Date().toISOString()}`);
56
+ console.log(chalk.gray('\nPayload:'));
57
+ console.log(chalk.gray('─'.repeat(30)));
58
+ console.log(JSON.stringify(webhookPayload, null, 2));
59
+ await store.storeEvent({
60
+ id: webhookPayload.data.id,
61
+ event: selectedEvent,
62
+ url: webhookUrl,
63
+ payload: webhookPayload,
64
+ timestamp: Math.floor(Date.now() / 1000),
65
+ status: 'delivered',
66
+ });
67
+ spinner.start('Sending webhook...');
68
+ try {
69
+ const response = await sendWebhookRequest(config, webhookUrl, webhookPayload);
70
+ if (response.statusCode >= 200 && response.statusCode < 300) {
71
+ spinner.succeed(`Webhook delivered successfully (HTTP ${response.statusCode})`);
72
+ const responseData = await printJsonResponse(response);
73
+ if (responseData) {
74
+ console.log(chalk.gray('\nResponse:'));
75
+ console.log(chalk.gray('─'.repeat(30)));
76
+ console.log(JSON.stringify(responseData, null, 2));
77
+ }
78
+ return;
79
+ }
80
+ if (response.statusCode === 404) {
81
+ spinner.fail('Webhook endpoint not found (HTTP 404)');
82
+ console.log('');
83
+ console.log(chalk.red('❌ The webhook URL returned 404 Not Found'));
84
+ console.log('');
85
+ console.log(chalk.yellow('💡 Possible causes:'));
86
+ console.log(chalk.gray(' • The webhook endpoint path is incorrect'));
87
+ console.log(chalk.gray(' • Your server is not running'));
88
+ console.log(chalk.gray(' • The route is not registered in your application'));
89
+ console.log('');
90
+ console.log(chalk.yellow('💡 To fix:'));
91
+ console.log(chalk.gray(` • Verify your server has a POST handler at: ${webhookUrl}`));
92
+ console.log(chalk.gray(' • Check that your server is running and accessible'));
93
+ throw new CommandError();
94
+ }
95
+ if (response.statusCode >= 400 && response.statusCode < 500) {
96
+ spinner.fail(`Webhook rejected by server (HTTP ${response.statusCode})`);
97
+ console.log('');
98
+ console.log(chalk.red(`❌ Server returned client error: ${response.statusCode}`));
99
+ const responseData = await printJsonResponse(response);
100
+ if (responseData) {
101
+ console.log(chalk.gray('\nServer response:'));
102
+ console.log(JSON.stringify(responseData, null, 2));
103
+ }
104
+ console.log('');
105
+ console.log(chalk.yellow('💡 Common causes:'));
106
+ console.log(chalk.gray(' • Invalid request format or headers'));
107
+ console.log(chalk.gray(' • Authentication/authorization failure'));
108
+ console.log(chalk.gray(' • Webhook signature verification failed'));
109
+ throw new CommandError();
110
+ }
111
+ if (response.statusCode >= 500) {
112
+ spinner.fail(`Webhook endpoint error (HTTP ${response.statusCode})`);
113
+ console.log('');
114
+ console.log(chalk.red(`❌ Server returned error: ${response.statusCode}`));
115
+ const responseData = await printJsonResponse(response);
116
+ if (responseData) {
117
+ console.log(chalk.gray('\nServer response:'));
118
+ console.log(JSON.stringify(responseData, null, 2));
119
+ }
120
+ console.log('');
121
+ console.log(chalk.yellow('💡 This is a server-side error. Check:'));
122
+ console.log(chalk.gray(' • Server logs for the specific error'));
123
+ console.log(chalk.gray(' • Webhook handler code for exceptions'));
124
+ throw new CommandError();
125
+ }
126
+ }
127
+ catch (error) {
128
+ const err = error;
129
+ if (err.code === 'ECONNREFUSED') {
130
+ spinner.fail('Connection refused');
131
+ console.log('');
132
+ console.log(chalk.red('❌ Could not connect to webhook URL'));
133
+ console.log('');
134
+ console.log(chalk.yellow('💡 Possible causes:'));
135
+ console.log(chalk.gray(' • Server is not running'));
136
+ console.log(chalk.gray(' • Wrong port number'));
137
+ console.log(chalk.gray(' • Firewall blocking the connection'));
138
+ console.log('');
139
+ console.log(chalk.yellow('💡 To fix:'));
140
+ console.log(chalk.gray(' • Start your local server'));
141
+ console.log(chalk.gray(' • Verify the server is listening on the correct port'));
142
+ throw new CommandError();
143
+ }
144
+ if (err.code === 'ENOTFOUND') {
145
+ spinner.fail('Host not found');
146
+ console.log('');
147
+ console.log(chalk.red('❌ Could not resolve webhook URL hostname'));
148
+ console.log('');
149
+ console.log(chalk.yellow('💡 Check:'));
150
+ console.log(chalk.gray(' • The URL is spelled correctly'));
151
+ console.log(chalk.gray(' • Your internet connection is working'));
152
+ console.log(chalk.gray(' • DNS is resolving correctly'));
153
+ throw new CommandError();
154
+ }
155
+ if (err.code === 'ETIMEDOUT' || err.message.includes('timeout')) {
156
+ spinner.fail('Request timed out');
157
+ console.log('');
158
+ console.log(chalk.red('❌ Webhook request timed out after 10 seconds'));
159
+ console.log('');
160
+ console.log(chalk.yellow('💡 Possible causes:'));
161
+ console.log(chalk.gray(' • Server is taking too long to respond'));
162
+ console.log(chalk.gray(' • Network latency issues'));
163
+ console.log(chalk.gray(' • Server is stuck or deadlocked'));
164
+ console.log('');
165
+ console.log(chalk.yellow('💡 To fix:'));
166
+ console.log(chalk.gray(' • Check your webhook handler for slow operations'));
167
+ console.log(chalk.gray(' • Ensure async operations are handled properly'));
168
+ throw new CommandError();
169
+ }
170
+ spinner.fail(`Webhook delivery failed: ${err.message}`);
171
+ console.log('');
172
+ console.log(chalk.red('❌ Unexpected error occurred'));
173
+ console.log(chalk.gray(` Error: ${err.message}`));
174
+ if (err.code) {
175
+ console.log(chalk.gray(` Code: ${err.code}`));
176
+ }
177
+ throw new CommandError();
178
+ }
179
+ }
180
+ catch (error) {
181
+ failTriggerCommand(logger, spinner, error);
182
+ }
183
+ }
184
+ export async function replayWebhookEvent(eventId, options) {
185
+ const { configManager, store } = createTriggerContext();
186
+ const config = await configManager.load();
187
+ try {
188
+ if (options.list || (!eventId && !options.event)) {
189
+ const events = await store.loadEvents();
190
+ if (options.json) {
191
+ console.log(JSON.stringify(events, null, 2));
192
+ return;
193
+ }
194
+ if (events.length === 0) {
195
+ console.log(chalk.yellow('No webhook events stored yet.'));
196
+ console.log(chalk.gray('Use "paymongo trigger send" to send events first.'));
197
+ return;
198
+ }
199
+ console.log(chalk.bold.blue('\n📋 Stored Webhook Events'));
200
+ console.log(chalk.gray('─'.repeat(95)));
201
+ const table = new Table({
202
+ head: [chalk.bold('ID'), chalk.bold('Event'), chalk.bold('Timestamp')],
203
+ colWidths: [25, 30, 25],
204
+ style: {
205
+ head: [],
206
+ border: [],
207
+ },
208
+ });
209
+ events.slice(0, 10).forEach((event) => {
210
+ const id = event.id.substring(0, 22) + (event.id.length > 22 ? '...' : '');
211
+ table.push([
212
+ chalk.cyan(id),
213
+ chalk.yellow(event.event),
214
+ chalk.gray(new Date(event.timestamp * 1000).toLocaleString()),
215
+ ]);
216
+ });
217
+ console.log(table.toString());
218
+ if (events.length > 10) {
219
+ console.log(chalk.gray(`\n... and ${events.length - 10} more events. Use --list to see all.`));
220
+ }
221
+ console.log(chalk.gray('\n💡 Use "paymongo trigger replay <eventId>" to replay a specific event'));
222
+ return;
223
+ }
224
+ if (options.event && !eventId) {
225
+ const events = await store.loadEvents();
226
+ const matchingEvents = events.filter((event) => event.event === options.event);
227
+ if (matchingEvents.length === 0) {
228
+ console.log(chalk.yellow(`No events found for type: ${options.event}`));
229
+ return;
230
+ }
231
+ if (options.json) {
232
+ console.log(JSON.stringify(matchingEvents, null, 2));
233
+ return;
234
+ }
235
+ console.log(chalk.bold.blue(`\n📋 Recent "${options.event}" Events`));
236
+ console.log(chalk.gray('─'.repeat(60)));
237
+ matchingEvents.slice(0, 5).forEach((event, index) => {
238
+ console.log(`${chalk.cyan((index + 1).toString() + '.')} ${chalk.yellow(event.id)} - ${chalk.gray(new Date(event.timestamp * 1000).toLocaleString())}`);
239
+ });
240
+ console.log(chalk.gray('\n💡 Use "paymongo trigger replay <eventId>" to replay a specific event'));
241
+ return;
242
+ }
243
+ if (eventId) {
244
+ const event = await store.getEventById(eventId);
245
+ if (!event) {
246
+ console.log(chalk.red(`❌ Event not found: ${eventId}`));
247
+ console.log(chalk.gray('Use "paymongo trigger replay --list" to see available events.'));
248
+ throw new CommandError();
249
+ }
250
+ const webhookUrl = options.url || event.url;
251
+ console.log(chalk.bold.blue('\n🔄 Replaying Webhook Event'));
252
+ console.log(chalk.gray('─'.repeat(50)));
253
+ console.log(`${chalk.bold('Event ID:')} ${chalk.cyan(event.id)}`);
254
+ console.log(`${chalk.bold('Event Type:')} ${chalk.yellow(event.event)}`);
255
+ console.log(`${chalk.bold('URL:')} ${chalk.yellow(webhookUrl)}`);
256
+ console.log(`${chalk.bold('Original Time:')} ${chalk.gray(new Date(event.timestamp * 1000).toISOString())}`);
257
+ const { spinner } = createTriggerContext();
258
+ spinner.start('Sending webhook...');
259
+ try {
260
+ const response = await sendWebhookRequest(config, webhookUrl, event.payload);
261
+ if (response.statusCode >= 200 && response.statusCode < 300) {
262
+ spinner.succeed(`Webhook replayed successfully (HTTP ${response.statusCode})`);
263
+ const responseData = await printJsonResponse(response);
264
+ if (responseData && !options.json) {
265
+ console.log(chalk.gray('\nResponse:'));
266
+ console.log(chalk.gray('─'.repeat(30)));
267
+ console.log(JSON.stringify(responseData, null, 2));
268
+ }
269
+ return;
270
+ }
271
+ spinner.fail(`Webhook replay failed (HTTP ${response.statusCode})`);
272
+ console.log(chalk.red(`Server responded with: ${response.statusCode}`));
273
+ throw new CommandError();
274
+ }
275
+ catch (error) {
276
+ const err = error;
277
+ spinner.fail('Webhook replay failed');
278
+ if (err.code === 'ECONNREFUSED') {
279
+ console.log(chalk.red('❌ Could not connect to webhook URL'));
280
+ }
281
+ else {
282
+ console.log(chalk.red(`❌ Error: ${err.message}`));
283
+ }
284
+ throw new CommandError();
285
+ }
286
+ }
287
+ }
288
+ catch (error) {
289
+ const err = error;
290
+ console.error(chalk.red(`❌ Failed to replay webhook: ${err.message}`));
291
+ throw new CommandError();
292
+ }
293
+ }
@@ -0,0 +1,230 @@
1
+ import crypto from 'crypto';
2
+ import ConfigManager from '../../services/config/manager.js';
3
+ import Spinner from '../../utils/spinner.js';
4
+ import Logger from '../../utils/logger.js';
5
+ import WebhookEventStore from '../../utils/webhook-store.js';
6
+ import { CLI_VERSION } from '../../utils/constants.js';
7
+ import { CommandError } from '../../utils/errors.js';
8
+ export function createTriggerContext() {
9
+ return {
10
+ spinner: new Spinner(),
11
+ configManager: new ConfigManager(),
12
+ logger: new Logger(),
13
+ store: new WebhookEventStore(),
14
+ };
15
+ }
16
+ export const AVAILABLE_TRIGGER_EVENTS = [
17
+ 'payment.paid',
18
+ 'payment.failed',
19
+ 'payment.refunded',
20
+ 'payment.refund.updated',
21
+ 'source.chargeable',
22
+ 'checkout_session.payment.paid',
23
+ 'link.payment.paid',
24
+ 'qrph.expired',
25
+ ];
26
+ export function buildSignatureHeader(config, webhookUrl, body) {
27
+ if (!config?.webhookSecrets || Object.keys(config.webhookSecrets).length === 0) {
28
+ return undefined;
29
+ }
30
+ const registered = config.registeredWebhooks || [];
31
+ const match = registered.find((webhook) => webhook.url === webhookUrl);
32
+ const webhookId = match?.id;
33
+ let secret;
34
+ if (webhookId && config.webhookSecrets[webhookId]) {
35
+ secret = config.webhookSecrets[webhookId];
36
+ }
37
+ else {
38
+ const secrets = Object.values(config.webhookSecrets).filter((value) => typeof value === 'string' && value.length > 0);
39
+ secret = secrets[0];
40
+ }
41
+ if (!secret) {
42
+ return undefined;
43
+ }
44
+ const timestamp = Math.floor(Date.now() / 1000).toString();
45
+ const signature = crypto.createHmac('sha256', secret).update(`${timestamp}.${body}`).digest('hex');
46
+ const parts = [`t=${timestamp}`, `te=${signature}`];
47
+ if (webhookId) {
48
+ parts.push(`li=${webhookId}`);
49
+ }
50
+ return parts.join(',');
51
+ }
52
+ export async function sendWebhookRequest(config, webhookUrl, payload) {
53
+ const { request } = await import('undici');
54
+ const body = JSON.stringify(payload);
55
+ const signatureHeader = buildSignatureHeader(config, webhookUrl, body);
56
+ return request(webhookUrl, {
57
+ method: 'POST',
58
+ headers: {
59
+ 'Content-Type': 'application/json',
60
+ 'User-Agent': `PayMongo-CLI/${CLI_VERSION}`,
61
+ ...(signatureHeader ? { 'paymongo-signature': signatureHeader } : {}),
62
+ },
63
+ body,
64
+ signal: AbortSignal.timeout(10000),
65
+ });
66
+ }
67
+ export function generateId() {
68
+ return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
69
+ }
70
+ export function generateWebhookPayload(eventType) {
71
+ const now = Math.floor(Date.now() / 1000);
72
+ const basePayload = {
73
+ data: {
74
+ id: `evt_${generateId()}`,
75
+ type: 'event',
76
+ attributes: {
77
+ type: eventType,
78
+ livemode: false,
79
+ created_at: now,
80
+ updated_at: now,
81
+ data: {},
82
+ },
83
+ },
84
+ };
85
+ switch (eventType) {
86
+ case 'payment.paid':
87
+ basePayload.data.attributes.data = {
88
+ id: `pay_${generateId()}`,
89
+ type: 'payment',
90
+ attributes: {
91
+ amount: 100000,
92
+ currency: 'PHP',
93
+ description: 'Test Payment',
94
+ status: 'paid',
95
+ external_reference_number: null,
96
+ paid_at: now,
97
+ created_at: now,
98
+ updated_at: now,
99
+ fees: 2950,
100
+ net_amount: 97050,
101
+ payment_intent_id: `pi_${generateId()}`,
102
+ source: {
103
+ id: `src_${generateId()}`,
104
+ type: 'source',
105
+ attributes: {
106
+ amount: 100000,
107
+ currency: 'PHP',
108
+ status: 'paid',
109
+ type: 'gcash',
110
+ created_at: now,
111
+ updated_at: now,
112
+ },
113
+ },
114
+ },
115
+ };
116
+ break;
117
+ case 'payment.failed':
118
+ basePayload.data.attributes.data = {
119
+ id: `pay_${generateId()}`,
120
+ type: 'payment',
121
+ attributes: {
122
+ amount: 50000,
123
+ currency: 'PHP',
124
+ description: 'Failed Test Payment',
125
+ status: 'failed',
126
+ external_reference_number: null,
127
+ created_at: now,
128
+ updated_at: now,
129
+ fees: 0,
130
+ net_amount: 0,
131
+ payment_intent_id: `pi_${generateId()}`,
132
+ source: {
133
+ id: `src_${generateId()}`,
134
+ type: 'source',
135
+ attributes: {
136
+ amount: 50000,
137
+ currency: 'PHP',
138
+ status: 'failed',
139
+ type: 'card',
140
+ created_at: now,
141
+ updated_at: now,
142
+ },
143
+ },
144
+ },
145
+ };
146
+ break;
147
+ case 'source.chargeable':
148
+ basePayload.data.attributes.data = {
149
+ id: `src_${generateId()}`,
150
+ type: 'source',
151
+ attributes: {
152
+ amount: 150000,
153
+ currency: 'PHP',
154
+ status: 'chargeable',
155
+ type: 'gcash',
156
+ billing: {
157
+ address: {
158
+ city: 'Manila',
159
+ country: 'PH',
160
+ line1: '123 Test Street',
161
+ line2: null,
162
+ postal_code: '1000',
163
+ state: 'Metro Manila',
164
+ },
165
+ email: 'test@example.com',
166
+ name: 'Test User',
167
+ phone: '+639123456789',
168
+ },
169
+ created_at: now,
170
+ updated_at: now,
171
+ },
172
+ };
173
+ break;
174
+ case 'checkout_session.payment.paid':
175
+ basePayload.data.attributes.data = {
176
+ id: `cs_${generateId()}`,
177
+ type: 'checkout_session',
178
+ attributes: {
179
+ amount: 200000,
180
+ currency: 'PHP',
181
+ description: 'Test Checkout Session',
182
+ status: 'paid',
183
+ payment_intent_id: `pi_${generateId()}`,
184
+ created_at: now,
185
+ updated_at: now,
186
+ },
187
+ };
188
+ break;
189
+ case 'link.payment.paid':
190
+ basePayload.data.attributes.data = {
191
+ id: `plink_${generateId()}`,
192
+ type: 'link',
193
+ attributes: {
194
+ amount: 75000,
195
+ currency: 'PHP',
196
+ description: 'Test Payment Link',
197
+ status: 'paid',
198
+ archived: false,
199
+ payment_intent_id: `pi_${generateId()}`,
200
+ created_at: now,
201
+ updated_at: now,
202
+ },
203
+ };
204
+ break;
205
+ default:
206
+ basePayload.data.attributes.data = {
207
+ id: `${eventType.split('.')[1]}_${generateId()}`,
208
+ type: eventType.split('.')[0],
209
+ attributes: {
210
+ status: 'test',
211
+ created_at: now,
212
+ updated_at: now,
213
+ },
214
+ };
215
+ }
216
+ return basePayload;
217
+ }
218
+ export async function printJsonResponse(response) {
219
+ const contentType = response.headers['content-type'];
220
+ if (contentType && contentType.includes('application/json')) {
221
+ return response.body.json();
222
+ }
223
+ return null;
224
+ }
225
+ export function failTriggerCommand(logger, spinner, error) {
226
+ const err = error;
227
+ spinner.fail('Failed to trigger webhook event');
228
+ logger.error('Trigger command error:', err.message);
229
+ throw new CommandError();
230
+ }