diff --git a/plugins/braintree-payment/README.md b/plugins/braintree-payment/README.md index ed60167c..575625cb 100644 --- a/plugins/braintree-payment/README.md +++ b/plugins/braintree-payment/README.md @@ -61,6 +61,7 @@ dependencies:[Modules.CACHE] id: 'braintree', options: { environment: process.env.BRAINTREE_ENVIRONMENT || (process.env.NODE_ENV !== 'production' ? 'sandbox' : 'production'), + defaultCurrencyCode: "USD", merchantId: process.env.BRAINTREE_MERCHANT_ID, publicKey: process.env.BRAINTREE_PUBLIC_KEY, privateKey: process.env.BRAINTREE_PRIVATE_KEY, @@ -75,6 +76,7 @@ dependencies:[Modules.CACHE] #### Options - **merchantId**: Your Braintree Merchant ID. +- **defaultCurrencyCode**: An optional field to indicate default currency code - **publicKey**: Your Braintree Public Key. - **privateKey**: Your Braintree Private Key. - **webhookSecret**: Secret for validating Braintree webhooks. diff --git a/plugins/braintree-payment/package.json b/plugins/braintree-payment/package.json index 6ac55208..eae236a2 100644 --- a/plugins/braintree-payment/package.json +++ b/plugins/braintree-payment/package.json @@ -1,6 +1,6 @@ { "name": "@lambdacurry/medusa-payment-braintree", - "version": "0.0.15", + "version": "0.0.16", "description": "Braintree plugin for Medusa", "author": "Lambda Curry (https://lambdacurry.dev)", "license": "MIT", diff --git a/plugins/braintree-payment/src/providers/payment-braintree/README.md b/plugins/braintree-payment/src/providers/payment-braintree/README.md index a0fa510b..9b677d82 100644 --- a/plugins/braintree-payment/src/providers/payment-braintree/README.md +++ b/plugins/braintree-payment/src/providers/payment-braintree/README.md @@ -35,6 +35,7 @@ Add the following configuration to the `payment` section of your `medusa-config. id: 'braintree', options: { environment: process.env.NODE_ENV !== 'production' ? 'sandbox' : 'production', + defaultCurrencyCode: "USD", merchantId: process.env.BRAINTREE_MERCHANT_ID, publicKey: process.env.BRAINTREE_PUBLIC_KEY, privateKey: process.env.BRAINTREE_PRIVATE_KEY, @@ -56,6 +57,7 @@ Add the following configuration to the `payment` section of your `medusa-config. - **enable3DSecure**: Enable 3D Secure authentication (`true` or `false`). - **savePaymentMethod**: Save payment methods for future use (default: `true`). - **autoCapture**: Automatically capture payments (default: `true`). +- **defaultCurrencyCode**: The default currency to use. This is optional - **customFields**: Array of Braintree custom field API names permitted to be forwarded from `data.custom_fields`. If empty or omitted, no user-provided custom fields are sent. ## Features diff --git a/plugins/braintree-payment/src/providers/payment-braintree/src/core/__tests__/braintree-base.spec.ts b/plugins/braintree-payment/src/providers/payment-braintree/src/core/__tests__/braintree-base.spec.ts index 6cc33a1c..c2da1579 100644 --- a/plugins/braintree-payment/src/providers/payment-braintree/src/core/__tests__/braintree-base.spec.ts +++ b/plugins/braintree-payment/src/providers/payment-braintree/src/core/__tests__/braintree-base.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals'; +import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals'; import BraintreeProviderService from '../../services/braintree-provider'; import { BraintreeConstructorArgs, BraintreePaymentSessionData } from '../braintree-base'; diff --git a/plugins/braintree-payment/src/providers/payment-braintree/src/core/__tests__/braintree-import.spec.ts b/plugins/braintree-payment/src/providers/payment-braintree/src/core/__tests__/braintree-import.spec.ts index 1e4848cc..4fbf1d1e 100644 --- a/plugins/braintree-payment/src/providers/payment-braintree/src/core/__tests__/braintree-import.spec.ts +++ b/plugins/braintree-payment/src/providers/payment-braintree/src/core/__tests__/braintree-import.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, jest, beforeEach } from '@jest/globals'; +import { beforeEach, describe, expect, it, jest } from '@jest/globals'; import BraintreeImportService from '../../services/braintree-import'; import { BraintreeConstructorArgs } from '../braintree-base'; diff --git a/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts b/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts index 35b315b9..2aaaf84a 100644 --- a/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts +++ b/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts @@ -4,12 +4,10 @@ import { MathBN, MedusaError, Modules, - Order, PaymentActions, PaymentSessionStatus, isDefined, } from '@medusajs/framework/utils'; -import { formatToTwoDecimalString } from '../../../../utils/format-amount'; import type { AddressDTO, AuthorizePaymentInput, @@ -18,7 +16,6 @@ import type { CancelPaymentOutput, CapturePaymentInput, CapturePaymentOutput, - CartDTO, CartLineItemDTO, CreateAccountHolderInput, CreateAccountHolderOutput, @@ -29,12 +26,9 @@ import type { GetPaymentStatusInput, GetPaymentStatusOutput, ICacheService, - ICartModuleService, InitiatePaymentInput, InitiatePaymentOutput, Logger, - MedusaContainer, - OrderDTO, PaymentAccountHolderDTO, PaymentCustomerDTO, PaymentProviderContext, @@ -54,6 +48,7 @@ import type { import type { Transaction, TransactionNotification, TransactionStatus } from 'braintree'; import Braintree from 'braintree'; import { z } from 'zod'; +import { formatToTwoDecimalString } from '../../../../utils/format-amount'; import type { BraintreeOptions, CustomFields } from '../types'; export type BraintreeConstructorArgs = Record & { @@ -62,6 +57,7 @@ export type BraintreeConstructorArgs = Record & { }; export interface ExtendedPaymentProviderContext extends PaymentProviderContext { + order_currency_code?: string; custom_fields?: Record; order_id?: string; shipping_address?: AddressDTO; @@ -109,12 +105,17 @@ const validateOptionalString = (value: unknown, fieldName: string): string | und }; // Error handling utility that preserves full error context -export const buildBraintreeError = (error: unknown, operation: string, logger: Logger, context?: Record): MedusaError => { +export const buildBraintreeError = ( + error: unknown, + operation: string, + logger: Logger, + context?: Record, +): MedusaError => { const errorMessage = error instanceof Error ? error.message : String(error); - + // Preserve full error context in logging logger.error(`Braintree ${operation} failed: ${errorMessage}`, error instanceof Error ? error : undefined); - + return new MedusaError(MedusaError.Types.INVALID_DATA, `Failed to ${operation}: ${errorMessage}`); }; @@ -124,7 +125,6 @@ class BraintreeBase extends AbstractPaymentProvider { protected gateway: Braintree.BraintreeGateway; logger: Logger; cache: ICacheService; - cartService: ICartModuleService; protected constructor(container: BraintreeConstructorArgs, options: BraintreeOptions) { super(container, options); @@ -132,7 +132,6 @@ class BraintreeBase extends AbstractPaymentProvider { this.options_ = options; this.logger = container[ContainerRegistrationKeys.LOGGER]; this.cache = container[Modules.CACHE]; - this.cartService = container[Modules.CART] as ICartModuleService; this.init(); } @@ -216,7 +215,6 @@ class BraintreeBase extends AbstractPaymentProvider { } private formatToTwoDecimalStringIfFinite(amount: unknown): string | undefined { - const n = Number(amount); if (!Number.isFinite(n)) return undefined; return formatToTwoDecimalString(n); @@ -442,14 +440,7 @@ class BraintreeBase extends AbstractPaymentProvider { countryCodeAlpha2: this.sanitizeCountryCodeAlpha2(context.shipping_address?.country_code), } : undefined; - let cart: CartDTO | undefined; - if(context.items?.[0]?.cart_id) { - cart = await this.cartService.retrieveCart(context.items?.[0]?.cart_id as string); - if (!cart) { - throw new MedusaError(MedusaError.Types.NOT_FOUND, 'Cart not found'); - } - } - + const lineItems = context.items ?.slice(0, 249) .map((item) => { @@ -459,15 +450,15 @@ class BraintreeBase extends AbstractPaymentProvider { const discount = Number(item.discount_total); const discountAmount = Number.isFinite(discount) && discount > 0 ? formatToTwoDecimalString(discount) : undefined; - + const currencyCode = context.order_currency_code ?? this.options_.defaultCurrencyCode ?? 'USD'; const li: Braintree.TransactionLineItem = { kind: 'debit', name: name.substring(0, 127), // Max 127 characters productCode: ((item.metadata?.productCode as string) ?? item.product_id).substring(0, 12), // Max 12 characters - commodityCode: ((item.metadata?.commodityCode as string) ?? "placeholder").substring(0, 12), // Max 12 characters - description: (item.product_description ?? "Healthcare").substring(0, 127), // Max 127 characters + commodityCode: ((item.metadata?.commodityCode as string) ?? 'placeholder').substring(0, 12), // Max 12 characters + description: (item.product_description ?? 'Healthcare').substring(0, 127), // Max 127 characters url: `/${item.product_handle}`.substring(0, 255), // Max 255 characters - unitOfMeasure: ((cart?.currency_code as string)?.toUpperCase() ?? 'USD').substring(0, 12), // Max 12 characters + unitOfMeasure: (currencyCode as string)?.toUpperCase().substring(0, 12), // Max 12 characters taxAmount: this.formatToTwoDecimalStringIfFinite(Number(item.tax_total)), // Must be decimal string discountAmount: this.formatToTwoDecimalStringIfFinite(Number(item.discount_total)), // Must be decimal string quantity: Math.max(1, Math.floor(Number(item.quantity) ?? 1)).toString(), // Must be positive integer @@ -552,14 +543,16 @@ class BraintreeBase extends AbstractPaymentProvider { const schema = z.object({ paymentMethodNonce: z.string().optional(), payment_method_nonce: z.string().optional(), - cardDetails:z.object({ - cardType: z.string().optional(), - lastFour: z.string().optional(), - lastTwo: z.string().optional(), - expirationMonth: z.string().optional(), - expirationYear: z.string().optional(), - cardholderName: z.string().optional(), - }).optional(), + cardDetails: z + .object({ + cardType: z.string().optional(), + lastFour: z.string().optional(), + lastTwo: z.string().optional(), + expirationMonth: z.string().optional(), + expirationYear: z.string().optional(), + cardholderName: z.string().optional(), + }) + .optional(), }); const result = schema.safeParse(data); @@ -636,7 +629,9 @@ class BraintreeBase extends AbstractPaymentProvider { if (saleResponse.transaction?.id) { await this.gateway.transaction.void(saleResponse.transaction.id); } - throw buildBraintreeError(error, 'sync payment session', this.logger, { transactionId: saleResponse.transaction?.id }); + throw buildBraintreeError(error, 'sync payment session', this.logger, { + transactionId: saleResponse.transaction?.id, + }); } } catch (error) { throw buildBraintreeError(error, 'create Braintree transaction', this.logger); @@ -660,7 +655,6 @@ class BraintreeBase extends AbstractPaymentProvider { } catch (e) { throw buildBraintreeError(e, 'delete Braintree payment', this.logger); } - } else { return { data: { @@ -734,12 +728,12 @@ class BraintreeBase extends AbstractPaymentProvider { let shouldVoid = ['submitted_for_settlement', 'authorized'].includes(transaction.status); - if(process.env.TEST_FORCE_SETTLED === 'true') { + if (process.env.TEST_FORCE_SETTLED === 'true') { shouldVoid = false; await this.gateway.testing.settle(transaction.id); transaction = await this.retrieveTransaction(transaction.id); } - + if (shouldVoid) { const voidResponse = await this.gateway.transaction.void(transaction.id); @@ -785,8 +779,6 @@ class BraintreeBase extends AbstractPaymentProvider { `Failed to create Braintree refund: ${refundResponse.message}`, ); - - const refundResult: RefundPaymentOutput = { data: { ...input.data, @@ -896,7 +888,6 @@ class BraintreeBase extends AbstractPaymentProvider { } catch (e) { throw buildBraintreeError(e, 'delete account holder', this.logger); } - } async getWebhookActionAndData(webhookData: ProviderWebhookPayload['payload']): Promise { diff --git a/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-import.ts b/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-import.ts index 66fd4f8d..18c47096 100644 --- a/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-import.ts +++ b/plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-import.ts @@ -6,39 +6,39 @@ import { PaymentActions, PaymentSessionStatus, } from '@medusajs/framework/utils'; -import { formatToTwoDecimalString } from '../../../../utils/format-amount'; -import { buildBraintreeError } from './braintree-base'; -import { z } from 'zod'; -import { BraintreeOptions, PaymentProviderKeys } from '../types'; -import type { BraintreeConstructorArgs } from './braintree-base'; -import Braintree from 'braintree'; import { - CapturePaymentInput, - CapturePaymentOutput, AuthorizePaymentInput, AuthorizePaymentOutput, CancelPaymentInput, CancelPaymentOutput, - InitiatePaymentInput, - InitiatePaymentOutput, + CapturePaymentInput, + CapturePaymentOutput, + CreateAccountHolderInput, + CreateAccountHolderOutput, + DeleteAccountHolderInput, + DeleteAccountHolderOutput, DeletePaymentInput, DeletePaymentOutput, GetPaymentStatusInput, GetPaymentStatusOutput, + InitiatePaymentInput, + InitiatePaymentOutput, + Logger, + ProviderWebhookPayload, RefundPaymentInput, RefundPaymentOutput, RetrievePaymentInput, RetrievePaymentOutput, UpdatePaymentInput, UpdatePaymentOutput, - ProviderWebhookPayload, WebhookActionResult, - DeleteAccountHolderOutput, - CreateAccountHolderInput, - CreateAccountHolderOutput, - DeleteAccountHolderInput, - Logger, } from '@medusajs/types'; +import Braintree from 'braintree'; +import { z } from 'zod'; +import { formatToTwoDecimalString } from '../../../../utils/format-amount'; +import { BraintreeOptions, PaymentProviderKeys } from '../types'; +import { buildBraintreeError } from './braintree-base'; +import type { BraintreeConstructorArgs } from './braintree-base'; export interface BraintreeImportInitiatePaymentData { transactionId?: string; @@ -106,7 +106,6 @@ class BraintreeImport extends AbstractPaymentProvider { return result.data as BraintreeImportPaymentSessionData; } - private truncate(value: unknown, max: number): string | undefined { if (value === null || value === undefined) return undefined; const str = String(value); @@ -200,10 +199,7 @@ class BraintreeImport extends AbstractPaymentProvider { // Explicit guard to verify transaction and transaction.id exist if (!transaction || !transaction.id) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `Braintree transaction not found: ${transactionId}` - ); + throw new MedusaError(MedusaError.Types.NOT_FOUND, `Braintree transaction not found: ${transactionId}`); } const shouldVoid = ['submitted_for_settlement', 'authorized'].includes(transaction.status); @@ -212,12 +208,9 @@ class BraintreeImport extends AbstractPaymentProvider { const cancelResponse = await this.gateway.transaction.void(transaction.id); if (!cancelResponse.success) { - throw buildBraintreeError( - new Error(cancelResponse.message), - 'void Braintree transaction', - this.logger, - { transactionId: transaction.id } - ); + throw buildBraintreeError(new Error(cancelResponse.message), 'void Braintree transaction', this.logger, { + transactionId: transaction.id, + }); } return { @@ -243,12 +236,10 @@ class BraintreeImport extends AbstractPaymentProvider { const refundResponse = await this.gateway.transaction.refund(transaction.id, refundAmountDecimal); if (!refundResponse.success) { - throw buildBraintreeError( - new Error(refundResponse.message), - 'create Braintree refund', - this.logger, - { transactionId: transaction.id, refundAmount: refundAmountDecimal } - ); + throw buildBraintreeError(new Error(refundResponse.message), 'create Braintree refund', this.logger, { + transactionId: transaction.id, + refundAmount: refundAmountDecimal, + }); } return { diff --git a/plugins/braintree-payment/src/providers/payment-braintree/src/index.ts b/plugins/braintree-payment/src/providers/payment-braintree/src/index.ts index a6364277..d36c7a0f 100644 --- a/plugins/braintree-payment/src/providers/payment-braintree/src/index.ts +++ b/plugins/braintree-payment/src/providers/payment-braintree/src/index.ts @@ -1,5 +1,5 @@ import { ModuleProvider, Modules } from '@medusajs/framework/utils'; -import { BraintreeProviderService, BraintreeImportService } from './services'; +import { BraintreeImportService, BraintreeProviderService } from './services'; const services = [BraintreeProviderService, BraintreeImportService]; diff --git a/plugins/braintree-payment/src/providers/payment-braintree/src/services/braintree-import.ts b/plugins/braintree-payment/src/providers/payment-braintree/src/services/braintree-import.ts index 3e3de9e8..f92051be 100644 --- a/plugins/braintree-payment/src/providers/payment-braintree/src/services/braintree-import.ts +++ b/plugins/braintree-payment/src/providers/payment-braintree/src/services/braintree-import.ts @@ -1,7 +1,7 @@ -import type { BraintreeOptions } from '../types'; -import { PaymentProviderKeys } from '../types'; import { BraintreeConstructorArgs } from '../core/braintree-base'; import BraintreeImport from '../core/braintree-import'; +import type { BraintreeOptions } from '../types'; +import { PaymentProviderKeys } from '../types'; class BraintreeImportService extends BraintreeImport { static identifier = PaymentProviderKeys.IMPORTED; diff --git a/plugins/braintree-payment/src/providers/payment-braintree/src/types/index.ts b/plugins/braintree-payment/src/providers/payment-braintree/src/types/index.ts index 5a498598..0eaae683 100644 --- a/plugins/braintree-payment/src/providers/payment-braintree/src/types/index.ts +++ b/plugins/braintree-payment/src/providers/payment-braintree/src/types/index.ts @@ -1,6 +1,7 @@ import type Braintree from 'braintree'; export interface BraintreeOptions extends Braintree.ClientGatewayConfig { + defaultCurrencyCode?: string; environment: 'production' | 'sandbox' | 'development' | 'qa'; merchantId: string; publicKey: string; diff --git a/plugins/braintree-payment/src/utils/format-amount.ts b/plugins/braintree-payment/src/utils/format-amount.ts index 3928cbe0..5f969560 100644 --- a/plugins/braintree-payment/src/utils/format-amount.ts +++ b/plugins/braintree-payment/src/utils/format-amount.ts @@ -3,7 +3,7 @@ import { MedusaError } from '@medusajs/framework/utils'; /** * Formats a number or string to a two-decimal string representation. * Validates the input is parseable to a number and throws MedusaError on NaN. - * + * * @param amount - The amount to format (number or string) * @returns A string representation with exactly 2 decimal places * @throws MedusaError if the amount is not a valid number @@ -12,9 +12,9 @@ export function formatToTwoDecimalString(amount: number | string): string { if (typeof amount !== 'string') { amount = amount.toString(); } - + const num = Number.parseFloat(amount); - + if (Number.isNaN(num)) { throw new MedusaError(MedusaError.Types.INVALID_ARGUMENT, 'Invalid amount'); }