Skip to content

Commit 561422d

Browse files
committed
feat: add logging option to Braintree payment provider for enhanced debugging
- Introduced a `logging` option in Braintree options to enable console logging for important operations. - Implemented logging functionality in both BraintreeBase and BraintreeImport classes to capture debug information and error details. - Updated README.md to document the new logging feature.
1 parent 526f29d commit 561422d

4 files changed

Lines changed: 166 additions & 6 deletions

File tree

plugins/braintree-payment/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ dependencies:[Modules.CACHE]
6969
enable3DSecure: process.env.BRAINTREE_ENABLE_3D_SECURE === 'true',
7070
savePaymentMethod: true, // Save payment methods for future use
7171
autoCapture: true, // Automatically capture payments
72+
logging: process.env.NODE_ENV !== 'production', // Enable debug logs (e.g. for development)
7273
}
7374
}
7475
```
@@ -84,6 +85,7 @@ dependencies:[Modules.CACHE]
8485
- **savePaymentMethod**: Save payment methods for future use (default: `true`).
8586
- **autoCapture**: Automatically capture payments (default: `true`).
8687
- **allowRefundOnRefunded**: Allow refund attempts on already-refunded imported transactions (default: `false`).
88+
- **logging**: When `true`, logs important operations (initiate, authorize, capture, refund, etc.) to the console for debugging (default: `false`).
8789

8890
> **Note:**
8991
> - `autoCapture`: If set to `true`, payments are captured automatically after authorization.

plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts

Lines changed: 118 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,24 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> {
135135
return token;
136136
}
137137

138+
/** Logs to console when options.logging is true. Use for debugging. */
139+
protected logDebug(message: string, context?: Record<string, unknown>): void {
140+
if (this.options_.logging) {
141+
const msg = context ? `${message} ${JSON.stringify(context)}` : message;
142+
this.logger.info(`[Braintree] ${msg}`);
143+
}
144+
}
145+
146+
/** When options.logging is true, logs error details to help debug vague failures. */
147+
protected logErrorDetail(operation: string, error: unknown, context?: Record<string, unknown>): void {
148+
if (!this.options_.logging) return;
149+
const msg = error instanceof Error ? error.message : String(error);
150+
const stack = error instanceof Error ? error.stack : undefined;
151+
const ctx = context ? ` ${JSON.stringify(context)}` : '';
152+
const stackLine = stack ? ` stack: ${stack}` : '';
153+
this.logger.info(`[Braintree] ERROR ${operation}: ${msg}${ctx}${stackLine}`);
154+
}
155+
138156
async getValidClientToken(
139157
medusaCustomerId: string | undefined,
140158
accountHolder: PaymentAccountHolderDTO | undefined,
@@ -199,6 +217,8 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> {
199217
publicKey: this.options_.publicKey!,
200218
privateKey: this.options_.privateKey!,
201219
});
220+
221+
this.logDebug(`Gateway initialized (environment: ${envKey})`);
202222
}
203223

204224
static validateOptions(options: BraintreeOptions): void {
@@ -225,8 +245,9 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> {
225245
options.savePaymentMethod = options.savePaymentMethod ?? false;
226246
options.autoCapture = options.autoCapture ?? false;
227247
options.allowRefundOnRefunded = options.allowRefundOnRefunded ?? false;
248+
options.logging = options.logging ?? false;
228249

229-
const booleanFields = ['enable3DSecure', 'savePaymentMethod', 'autoCapture', 'allowRefundOnRefunded'];
250+
const booleanFields = ['enable3DSecure', 'savePaymentMethod', 'autoCapture', 'allowRefundOnRefunded', 'logging'];
230251
for (const field of booleanFields) {
231252
if (isDefined(options[field]) && typeof options[field] !== 'boolean') {
232253
throw new MedusaError(
@@ -241,6 +262,8 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> {
241262
const sessionData = await this.parsePaymentSessionData(input.data ?? {});
242263
const transaction = sessionData.transaction;
243264

265+
this.logDebug('capturePayment', { transactionId: transaction?.id });
266+
244267
if (!transaction) {
245268
throw new MedusaError(MedusaError.Types.NOT_FOUND, 'Braintree transaction not found');
246269
}
@@ -287,6 +310,10 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> {
287310
}
288311

289312
async authorizePayment(input: AuthorizePaymentInput): Promise<AuthorizePaymentOutput> {
313+
this.logDebug('authorizePayment', {
314+
amount: (input.data as { amount?: number })?.amount,
315+
currency_code: (input.data as { currency_code?: string })?.currency_code,
316+
});
290317
try {
291318
const sessionData = await this.parsePaymentSessionData(input.data ?? {});
292319

@@ -321,13 +348,18 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> {
321348
status: finalStatus,
322349
};
323350
} catch (error) {
324-
this.logger.error(`Error authorizing transaction: ${error.message}`, error);
325-
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.message ?? 'Unknown error');
351+
this.logErrorDetail('authorizePayment', error, {
352+
amount: (input.data as { amount?: number })?.amount,
353+
currency_code: (input.data as { currency_code?: string })?.currency_code,
354+
});
355+
this.logger.error(`Error authorizing transaction: ${(error as Error).message}`, error as Error);
356+
throw new MedusaError(MedusaError.Types.INVALID_DATA, (error as Error).message ?? 'Unknown error');
326357
}
327358
}
328359

329360
async cancelPayment(input: CancelPaymentInput): Promise<CancelPaymentOutput> {
330361
const sessionData = await this.parsePaymentSessionData(input.data ?? {});
362+
this.logDebug('cancelPayment', { transactionId: sessionData.transaction?.id });
331363
const transaction = await this.retrieveTransaction(sessionData.transaction?.id as string);
332364

333365
if (!transaction) return {};
@@ -461,6 +493,11 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> {
461493
}
462494

463495
async initiatePayment(input: InitiatePaymentInput): Promise<InitiatePaymentOutput> {
496+
this.logDebug('initiatePayment', {
497+
amount: input.amount,
498+
currency_code: input.currency_code,
499+
idempotency_key: input.context?.idempotency_key,
500+
});
464501
const data = this.validateInitiatePaymentData(input.data ?? {});
465502

466503
let transaction: Transaction | undefined;
@@ -507,18 +544,36 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> {
507544
customer: input.context?.customer,
508545
});
509546
try {
547+
this.logDebug('createTransaction (sale)', {
548+
amount: transactionCreateRequest.amount,
549+
orderId: _context?.orderId,
550+
});
510551
const saleResponse = await this.gateway.transaction.sale(transactionCreateRequest);
511552

512553
if (!saleResponse.success) {
554+
this.logErrorDetail(
555+
'transaction.sale failed',
556+
new Error(saleResponse.transaction?.gatewayRejectionReason ?? saleResponse.message ?? 'Unknown error'),
557+
{
558+
transactionId: saleResponse.transaction?.id,
559+
gatewayRejectionReason: saleResponse.transaction?.gatewayRejectionReason,
560+
processorResponseCode: saleResponse.transaction?.processorResponseCode,
561+
processorResponseText: saleResponse.transaction?.processorResponseText,
562+
errors: saleResponse.errors,
563+
},
564+
);
513565
throw new MedusaError(
514566
MedusaError.Types.PAYMENT_AUTHORIZATION_ERROR,
515-
saleResponse.transaction?.gatewayRejectionReason ?? 'Unknown error',
567+
saleResponse.transaction?.gatewayRejectionReason ?? saleResponse.message ?? 'Unknown error',
516568
);
517569
}
518570

519571
try {
520572
return await this.retrieveTransaction(saleResponse.transaction.id);
521573
} catch (error) {
574+
this.logErrorDetail('sync payment session (retrieveTransaction)', error, {
575+
transactionId: saleResponse.transaction?.id,
576+
});
522577
if (saleResponse.transaction?.id) {
523578
await this.gateway.transaction.void(saleResponse.transaction.id);
524579
}
@@ -527,13 +582,18 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> {
527582
});
528583
}
529584
} catch (error) {
585+
this.logErrorDetail('create Braintree transaction', error, {
586+
amount: transactionCreateRequest.amount,
587+
orderId: _context?.orderId,
588+
});
530589
throw buildBraintreeError(error, 'create Braintree transaction', this.logger);
531590
}
532591
}
533592

534593
async deletePayment(input: DeletePaymentInput): Promise<DeletePaymentOutput> {
535594
const sessionData = await this.parsePaymentSessionData(input.data ?? {});
536595
const transaction = sessionData.transaction;
596+
this.logDebug('deletePayment', { transactionId: transaction?.id });
537597

538598
if (transaction) {
539599
try {
@@ -546,6 +606,7 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> {
546606
},
547607
};
548608
} catch (e) {
609+
this.logErrorDetail('delete Braintree payment', e, { transactionId: transaction?.id });
549610
throw buildBraintreeError(e, 'delete Braintree payment', this.logger);
550611
}
551612
} else {
@@ -562,6 +623,7 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> {
562623
// Support both `data.transaction` and `data.braintreeTransaction` without requiring full session parsing
563624
const tx = (input.data?.transaction ?? input.data?.braintreeTransaction) as Transaction | undefined;
564625
const id = tx?.id as string | undefined;
626+
this.logDebug('getPaymentStatus', { transactionId: id });
565627

566628
if (!id) {
567629
return { status: PaymentSessionStatus.PENDING };
@@ -571,6 +633,7 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> {
571633
try {
572634
transaction = await this.gateway.transaction.find(id);
573635
} catch (e) {
636+
this.logErrorDetail('getPaymentStatus (transaction.find)', e, { transactionId: id });
574637
this.logger.warn('received payment data from session not transaction data');
575638
throw e;
576639
}
@@ -579,6 +642,9 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> {
579642
}
580643

581644
async savePaymentMethod(input: SavePaymentMethodInput): Promise<SavePaymentMethodOutput> {
645+
this.logDebug('savePaymentMethod', {
646+
accountHolderId: input.context?.account_holder?.data?.id,
647+
});
582648
const sessionData = await this.parsePaymentSessionData(input.data ?? {});
583649

584650
const braintreeCustomerId = validateString(input.context?.account_holder?.data?.id, 'Braintree customer ID');
@@ -595,6 +661,14 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> {
595661
});
596662

597663
if (!paymentMethodResult.success) {
664+
this.logErrorDetail(
665+
'savePaymentMethod (paymentMethod.create)',
666+
new Error(JSON.stringify(paymentMethodResult.errors)),
667+
{
668+
customerId: braintreeCustomerId,
669+
errors: paymentMethodResult.errors,
670+
},
671+
);
598672
throw new MedusaError(
599673
MedusaError.Types.INVALID_DATA,
600674
`Failed to save payment method: ${JSON.stringify(paymentMethodResult.errors)}`,
@@ -611,6 +685,10 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> {
611685

612686
async refundPayment(input: RefundPaymentInput): Promise<RefundPaymentOutput> {
613687
const sessionData = await this.parsePaymentSessionData(input.data ?? {});
688+
this.logDebug('refundPayment', {
689+
transactionId: sessionData.transaction?.id,
690+
amount: input.amount,
691+
});
614692

615693
const refundAmountBN = MathBN.convert(input.amount, 2);
616694
const refundAmount = refundAmountBN.toNumber();
@@ -631,8 +709,14 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> {
631709
const voidResponse = await this.gateway.transaction.void(transaction.id);
632710
const voidSucceeded = voidResponse.success ?? false;
633711

634-
if (!voidSucceeded)
712+
if (!voidSucceeded) {
713+
this.logErrorDetail('refundPayment (void)', new Error(voidResponse.message ?? 'Void failed'), {
714+
transactionId: transaction.id,
715+
message: voidResponse.message,
716+
errors: (voidResponse as { errors?: unknown }).errors,
717+
});
635718
throw new MedusaError(MedusaError.Types.PAYMENT_AUTHORIZATION_ERROR, 'Failed to void transaction');
719+
}
636720

637721
const voidedTransaction = voidResponse?.transaction ?? (await this.retrieveTransaction(transaction.id));
638722

@@ -673,11 +757,18 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> {
673757
const refundResponse = await this.gateway.transaction.refund(transaction.id, refundAmountDecimal);
674758

675759
const refundSucceeded = refundResponse.success ?? false;
676-
if (!refundSucceeded)
760+
if (!refundSucceeded) {
761+
this.logErrorDetail('refundPayment (refund)', new Error(refundResponse.message ?? 'Refund failed'), {
762+
transactionId: transaction.id,
763+
refundAmount: refundAmountDecimal,
764+
message: refundResponse.message,
765+
errors: (refundResponse as { errors?: unknown }).errors,
766+
});
677767
throw new MedusaError(
678768
MedusaError.Types.INVALID_DATA,
679769
`Failed to create Braintree refund: ${refundResponse.message}`,
680770
);
771+
}
681772

682773
const refundTransaction = refundResponse.transaction ?? (await this.retrieveTransaction(transaction.id));
683774

@@ -690,6 +781,10 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> {
690781
};
691782
return refundResult;
692783
} catch (e) {
784+
this.logErrorDetail('create Braintree refund', e, {
785+
transactionId: transaction.id,
786+
refundAmount: refundAmountDecimal,
787+
});
693788
throw buildBraintreeError(e, 'create Braintree refund', this.logger);
694789
}
695790
}
@@ -699,6 +794,7 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> {
699794

700795
async retrievePayment(input: RetrievePaymentInput): Promise<RetrievePaymentOutput> {
701796
const paymentSessionData = await this.parsePaymentSessionData(input.data ?? {});
797+
this.logDebug('retrievePayment', { transactionId: paymentSessionData.transaction?.id });
702798

703799
if (!paymentSessionData.transaction?.id) {
704800
throw new MedusaError(MedusaError.Types.NOT_FOUND, 'Braintree transaction not found');
@@ -715,6 +811,7 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> {
715811
}
716812

717813
async updatePayment(input: UpdatePaymentInput): Promise<UpdatePaymentOutput> {
814+
this.logDebug('updatePayment', { amount: input.amount, currency_code: input.currency_code });
718815
return Promise.resolve({
719816
data: {
720817
...input.data,
@@ -725,6 +822,7 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> {
725822
}
726823

727824
async createAccountHolder(input: CreateAccountHolderInput): Promise<CreateAccountHolderOutput> {
825+
this.logDebug('createAccountHolder', { customerEmail: input.context.customer?.email });
728826
const customer = await this.createBraintreeCustomer(input.context.customer);
729827

730828
return {
@@ -738,6 +836,7 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> {
738836
async updateAccountHolder(input: UpdateAccountHolderInput): Promise<UpdateAccountHolderOutput> {
739837
const { context } = input;
740838
const accountHolderId = context.account_holder?.data?.id as string;
839+
this.logDebug('updateAccountHolder', { accountHolderId });
741840
if (!accountHolderId) {
742841
throw new MedusaError(MedusaError.Types.INVALID_DATA, `Account holder id is required`);
743842
}
@@ -756,6 +855,10 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> {
756855
const updateResult = await this.gateway.customer.update(accountHolder.id, customerUpdateRequest);
757856

758857
if (!updateResult.success) {
858+
this.logErrorDetail('updateAccountHolder (customer.update)', new Error(JSON.stringify(updateResult.errors)), {
859+
accountHolderId,
860+
errors: updateResult.errors,
861+
});
759862
throw new MedusaError(
760863
MedusaError.Types.INVALID_DATA,
761864
`Failed to update account holder: ${JSON.stringify(updateResult.errors)}`,
@@ -766,6 +869,7 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> {
766869
data: { ...updateResult.customer },
767870
};
768871
} catch (e) {
872+
this.logErrorDetail('update account holder', e, { accountHolderId });
769873
throw buildBraintreeError(e, 'update account holder', this.logger);
770874
}
771875
}
@@ -774,6 +878,7 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> {
774878
const { context } = input;
775879

776880
const accountHolderId = context.account_holder?.data?.id as string;
881+
this.logDebug('deleteAccountHolder', { accountHolderId });
777882

778883
if (!accountHolderId) throw new MedusaError(MedusaError.Types.INVALID_DATA, `Account holder id is required`);
779884

@@ -789,13 +894,15 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> {
789894
data: {},
790895
};
791896
} catch (e) {
897+
this.logErrorDetail('delete account holder', e, { accountHolderId });
792898
throw buildBraintreeError(e, 'delete account holder', this.logger);
793899
}
794900
}
795901

796902
async getWebhookActionAndData(webhookData: ProviderWebhookPayload['payload']): Promise<WebhookActionResult> {
797903
const logger = this.logger;
798904

905+
this.logDebug('getWebhookActionAndData', { hasData: !!webhookData?.data });
799906
logger.info(`Received Braintree webhook with data: ${!!webhookData.data}`);
800907

801908
const decodedPayload = new URLSearchParams(webhookData.data as unknown as string);
@@ -811,6 +918,7 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> {
811918
return { action: PaymentActions.FAILED };
812919
}
813920
} catch (error) {
921+
this.logErrorDetail('webhook validation', error, { hasPayload: !!webhookData?.data });
814922
logger.error(`Braintree webhook validation failed : ${error}`);
815923

816924
return { action: PaymentActions.FAILED };
@@ -853,6 +961,10 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> {
853961
});
854962

855963
if (!customerResult.success) {
964+
this.logErrorDetail('createBraintreeCustomer', new Error(JSON.stringify(customerResult.errors)), {
965+
email: customer.email,
966+
errors: customerResult.errors,
967+
});
856968
throw new MedusaError(
857969
MedusaError.Types.INVALID_DATA,
858970
`Failed to create Braintree customer: ${JSON.stringify(customerResult.errors)}`,

0 commit comments

Comments
 (0)