From d199540b7dc1999746916ab16c6f57e0544dbf2f Mon Sep 17 00:00:00 2001 From: govind Date: Fri, 7 Aug 2026 12:59:31 +0530 Subject: [PATCH 1/2] fix: enable no-void --- plugins/braintree-payment/CHANGELOG.md | 6 +++ plugins/braintree-payment/README.md | 3 ++ plugins/braintree-payment/package.json | 2 +- .../src/core/__tests__/braintree-base.spec.ts | 41 +++++++++++++++++++ .../core/__tests__/braintree-import.spec.ts | 18 +++++++- .../src/core/braintree-base.ts | 20 ++++++++- .../src/core/braintree-import.ts | 10 +++++ .../payment-braintree/src/types/index.ts | 6 +++ 8 files changed, 102 insertions(+), 4 deletions(-) diff --git a/plugins/braintree-payment/CHANGELOG.md b/plugins/braintree-payment/CHANGELOG.md index 2740130c..0a2fc57e 100644 --- a/plugins/braintree-payment/CHANGELOG.md +++ b/plugins/braintree-payment/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.1.9-next + +### Improvements + +- Add `disableVoidTransactions` option: when enabled, refunds never void and only proceed for `settled`/`settling` transactions (late requirement for future partial order refunds and order edits). Unsettled refunds throw `INVALID_DATA` with “cannot be refunded right now”. + ## 0.1.8 ### Fixes diff --git a/plugins/braintree-payment/README.md b/plugins/braintree-payment/README.md index d44a55aa..445dd81b 100644 --- a/plugins/braintree-payment/README.md +++ b/plugins/braintree-payment/README.md @@ -90,6 +90,7 @@ dependencies:[Modules.CACHE] savePaymentMethod: true, // Save payment methods for future use autoCapture: true, // Automatically capture payments allowRefundOnRefunded: false, + disableVoidTransactions: false, logging: process.env.BRAINTREE_LOGGING === 'true', // Enable plugin debug logs } } @@ -106,6 +107,7 @@ dependencies:[Modules.CACHE] - **savePaymentMethod**: Save payment methods for future use (default: `true`). - **autoCapture**: Automatically capture payments (default: `true`). - **allowRefundOnRefunded**: Allow refund attempts on already-refunded imported transactions (default: `false`). +- **disableVoidTransactions**: When `true`, refunds never void; only `settled`/`settling` transactions may be refunded. Late requirement so future partial order refunds and order edits can be supported (void cancels the full authorization). Default: `false`. With this enabled, refunds on unsettled transactions fail with “cannot be refunded right now”. - **logging**: Enable verbose plugin debug logging (`true` or `false`, default: `false`). When `true`, the provider logs operation details (initiate, authorize, capture, refund, etc.) and expanded Braintree error context via Medusa's logger with a `[Braintree]` prefix. Set via `BRAINTREE_LOGGING=true` in `.env` or pass `logging: true` directly in provider options. Disable in production unless actively debugging. ### Debug logging @@ -140,6 +142,7 @@ Earlier README examples used `logging: process.env.NODE_ENV !== 'production'` (a > - `autoCapture`: If set to `true`, payments are captured automatically after authorization. > - `savePaymentMethod`: If set to `true`, customer payment methods are saved for future use. > - `allowRefundOnRefunded`: If set to `true`, the imported payment provider will gracefully handle refund attempts on transactions that have already been refunded in Braintree. Instead of throwing an error, it will log a warning and record the refund locally only. This is useful when orders are imported and later refunded directly in Braintree. +> - `disableVoidTransactions`: Late additional requirement so future partial order refunds and order edits can be supported. When `true`, the provider waits for `settled`/`settling` before refunding; otherwise refund fails with “cannot be refunded right now”. `cancelPayment` may still void. ### 3D Secure Setup diff --git a/plugins/braintree-payment/package.json b/plugins/braintree-payment/package.json index 002af7c9..69a71a2a 100644 --- a/plugins/braintree-payment/package.json +++ b/plugins/braintree-payment/package.json @@ -1,6 +1,6 @@ { "name": "@lambdacurry/medusa-payment-braintree", - "version": "0.1.8", + "version": "0.1.10-next", "description": "Braintree plugin for Medusa", "author": "Lambda Curry (https://lambdacurry.dev)", "license": "MIT", 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 dd715a89..a39d0dc1 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 @@ -233,6 +233,47 @@ describe('BraintreeProviderService core behaviors', () => { expect(entry?.transaction?.id).toBe('t1'); }); + it('refundPayment throws when disableVoidTransactions and status is authorized', async () => { + const { service, gateway } = buildService({ disableVoidTransactions: true }); + + const input: RefundPaymentInput = { + amount: 5, + data: { + client_token: 'ct', + amount: 1000, + currency_code: 'USD', + braintreeTransaction: { id: 't1' }, + }, + }; + + gateway.transaction.find.mockResolvedValueOnce({ id: 't1', status: 'authorized' }); + + await expect(service.refundPayment(input)).rejects.toMatchObject({ + type: MedusaError.Types.INVALID_DATA, + message: 'Braintree transaction with ID t1 cannot be refunded right now', + }); + expect(gateway.transaction.void).not.toHaveBeenCalled(); + expect(gateway.transaction.refund).not.toHaveBeenCalled(); + }); + + it('refundPayment refunds settled transactions when disableVoidTransactions is enabled', async () => { + const { service, gateway } = buildService({ disableVoidTransactions: true }); + + gateway.transaction.find + .mockResolvedValueOnce({ id: 't-settled', status: 'settled' }) + .mockResolvedValueOnce({ id: 't-settled', status: 'settled' }); + gateway.transaction.refund.mockResolvedValueOnce({ + success: true, + transaction: { id: 'r-settled', status: 'submitted_for_settlement' }, + }); + + const result = await service.refundPayment(settledRefundInput(10)); + + expect(gateway.transaction.void).not.toHaveBeenCalled(); + expect(gateway.transaction.refund).toHaveBeenCalledWith('t-settled', '10.00'); + expect(lastRefundEntry(result.data)?.type).toBe('refund'); + }); + it('refundPayment appends to existing braintreeRefund history', async () => { const { service, gateway } = buildService(); const priorEntry = { 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 7dc3e84c..ed7db4b2 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 @@ -3,7 +3,7 @@ import { MedusaError } from '@medusajs/framework/utils'; import BraintreeImportService from '../../services/braintree-import'; import { BraintreeConstructorArgs } from '../braintree-base'; -const buildService = () => { +const buildService = (overrideOptions?: Record) => { const logger = { info: jest.fn(), warn: jest.fn(), error: jest.fn() } as any; const cache = { get: jest.fn(), set: jest.fn() } as any; @@ -18,6 +18,7 @@ const buildService = () => { savePaymentMethod: false, webhookSecret: 'whsec', autoCapture: true, + ...overrideOptions, } as any; const service = new BraintreeImportService(container, options); @@ -33,7 +34,7 @@ const buildService = () => { (service as any).gateway = gateway; - return { service, gateway }; + return { service, gateway, logger }; }; describe('BraintreeImportService', () => { @@ -81,6 +82,19 @@ describe('BraintreeImportService', () => { expect((res.data as any).refundedTotal).toBe(10); }); + it('throws when disableVoidTransactions and status is authorized', async () => { + const { service, gateway } = buildService({ disableVoidTransactions: true }); + const session = { transactionId: 't2', importedAsRefunded: false, refundedTotal: 0, status: 'captured' } as any; + gateway.transaction.find.mockResolvedValueOnce({ id: 't2', status: 'authorized' }); + + await expect(service.refundPayment({ amount: 10, data: session } as any)).rejects.toMatchObject({ + type: MedusaError.Types.INVALID_DATA, + message: 'Braintree transaction with ID t2 cannot be refunded right now', + }); + expect(gateway.transaction.void).not.toHaveBeenCalled(); + expect(gateway.transaction.refund).not.toHaveBeenCalled(); + }); + it('performs real refund for settled/settling when not imported-refunded', async () => { const { service, gateway } = buildService(); const session = { transactionId: 't3', importedAsRefunded: false, refundedTotal: 1.25, status: 'captured' } as any; 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 45dc4cc7..9e68880a 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 @@ -527,9 +527,17 @@ class BraintreeBase extends AbstractPaymentProvider { options.savePaymentMethod = options.savePaymentMethod ?? false; options.autoCapture = options.autoCapture ?? false; options.allowRefundOnRefunded = options.allowRefundOnRefunded ?? false; + options.disableVoidTransactions = options.disableVoidTransactions ?? false; options.logging = options.logging ?? false; - const booleanFields = ['enable3DSecure', 'savePaymentMethod', 'autoCapture', 'allowRefundOnRefunded', 'logging']; + const booleanFields = [ + 'enable3DSecure', + 'savePaymentMethod', + 'autoCapture', + 'allowRefundOnRefunded', + 'disableVoidTransactions', + 'logging', + ]; for (const field of booleanFields) { if (isDefined(options[field as keyof BraintreeOptions]) && typeof options[field as keyof BraintreeOptions] !== 'boolean') { throw new MedusaError( @@ -1186,12 +1194,22 @@ class BraintreeBase extends AbstractPaymentProvider { /** * Chooses void vs refund based on transaction status (after optional test settle). * @param transaction - Live Braintree transaction + * @throws {MedusaError} `INVALID_DATA` when void is disabled and status is voidable * @throws {MedusaError} `NOT_FOUND` when status is neither voidable nor refundable */ private async resolveRefundAction(transaction: Transaction): Promise { const resolved = await this.applyTestForceSettled(transaction); if (isVoidableRefundStatus(resolved.status)) { + if (this.options.disableVoidTransactions) { + this.logger.error( + `Braintree transaction with ID ${resolved.id} cannot be refunded right now because it's in status ${resolved.status}`, + ); + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Braintree transaction with ID ${resolved.id} cannot be refunded right now`, + ); + } return { kind: 'voided', transaction: resolved }; } 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 a83f9d5f..38594e62 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 @@ -285,6 +285,16 @@ class BraintreeImport extends AbstractPaymentProvider { const shouldVoid = ['submitted_for_settlement', 'authorized'].includes(transaction.status); if (shouldVoid) { + if (this.options.disableVoidTransactions) { + this.logger.error( + `Braintree transaction with ID ${transaction.id} cannot be refunded right now because it's in status ${transaction.status}`, + ); + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Braintree transaction with ID ${transaction.id} cannot be refunded right now`, + ); + } + const cancelResponse = await this.gateway.transaction.void(transaction.id); if (isBraintreeFailureResponse(cancelResponse)) { 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 899dc49c..d0ddb45b 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 @@ -11,6 +11,12 @@ export interface BraintreeOptions extends Braintree.ClientGatewayConfig { webhookSecret: string; autoCapture: boolean; allowRefundOnRefunded?: boolean; + /** + * When true, refundPayment never voids. Only settled/settling transactions may be refunded. + * Late requirement so future partial order refunds and order edits can be supported + * (void cancels the full authorization). + */ + disableVoidTransactions?: boolean; /** When true, logs important operations to the console for debugging. */ logging?: boolean; } From 5a473b4e7a98df74189a8d058c17975a576cf858 Mon Sep 17 00:00:00 2001 From: govind Date: Mon, 10 Aug 2026 16:20:15 +0530 Subject: [PATCH 2/2] fix: improving operations --- plugins/braintree-payment/CHANGELOG.md | 1 + plugins/braintree-payment/README.md | 18 +++++++++++++---- plugins/braintree-payment/package.json | 2 +- .../src/core/__tests__/braintree-base.spec.ts | 20 +++++-------------- .../src/core/braintree-base.ts | 12 ++++++----- .../payment-braintree/src/types/index.ts | 6 ++++++ 6 files changed, 34 insertions(+), 25 deletions(-) diff --git a/plugins/braintree-payment/CHANGELOG.md b/plugins/braintree-payment/CHANGELOG.md index 0a2fc57e..dba1fcd4 100644 --- a/plugins/braintree-payment/CHANGELOG.md +++ b/plugins/braintree-payment/CHANGELOG.md @@ -5,6 +5,7 @@ ### Improvements - Add `disableVoidTransactions` option: when enabled, refunds never void and only proceed for `settled`/`settling` transactions (late requirement for future partial order refunds and order edits). Unsettled refunds throw `INVALID_DATA` with “cannot be refunded right now”. +- Move sandbox settle-before-refund from reading `process.env.TEST_FORCE_SETTLED` inside the provider to a `testForceSettled` option (wire `TEST_FORCE_SETTLED` in `medusa-config` if you still use the env var). ## 0.1.8 diff --git a/plugins/braintree-payment/README.md b/plugins/braintree-payment/README.md index 445dd81b..0646ac2d 100644 --- a/plugins/braintree-payment/README.md +++ b/plugins/braintree-payment/README.md @@ -41,8 +41,8 @@ BRAINTREE_PRIVATE_KEY= BRAINTREE_WEBHOOK_SECRET= BRAINTREE_ENVIRONMENT=sandbox|development|production|qa BRAINTREE_ENABLE_3D_SECURE=true|false -TEST_FORCE_SETTLED=true|false BRAINTREE_LOGGING=true|false +TEST_FORCE_SETTLED=true|false ``` - `BRAINTREE_PUBLIC_KEY`: Your Braintree public key. @@ -51,8 +51,8 @@ BRAINTREE_LOGGING=true|false - `BRAINTREE_WEBHOOK_SECRET`: Secret for validating Braintree webhooks. - `BRAINTREE_ENVIRONMENT`: One of `sandbox`, `development`, `production`, or `qa`. - `BRAINTREE_ENABLE_3D_SECURE`: Set to `true` to enable 3D Secure authentication, otherwise `false`. -- `TEST_FORCE_SETTLED`: **Sandbox only.** When set to `true` **and** `BRAINTREE_ENVIRONMENT=sandbox`, the refund flow settles the Braintree transaction via the sandbox testing API before attempting a refund. Use this to exercise the **refund** path (settled/settling) instead of the **void** path (authorized/submitted_for_settlement). Defaults to `false`. Ignored (with a warning) outside sandbox. Do not enable in production. - `BRAINTREE_LOGGING`: Optional. Set to `true` to enable plugin debug logging. Wire this to the provider `logging` option in `medusa-config.ts` (see below). Defaults to `false`. +- `TEST_FORCE_SETTLED`: Optional. **Sandbox only.** Wire this to the provider `testForceSettled` option in `medusa-config.ts` (see below). Defaults to `false`. Do not enable in production. ### Testing refunds in sandbox @@ -61,14 +61,22 @@ In Braintree sandbox, transactions often remain in `authorized` or `submitted_fo - **Void path:** `authorized`, `submitted_for_settlement` - **Refund path:** `settled`, `settling` -To test the refund path locally without waiting for settlement, set: +To test the refund path locally without waiting for settlement, set `environment: 'sandbox'` and `testForceSettled: true` in provider options (optionally via env): ```env BRAINTREE_ENVIRONMENT=sandbox TEST_FORCE_SETTLED=true ``` -When both are set, `refundPayment` calls Braintree's sandbox `testing.settle` on the transaction, re-fetches it, then proceeds with `transaction.refund`. If `TEST_FORCE_SETTLED=true` but the provider environment is not `sandbox`, the settle step is skipped and a warning is logged. +```javascript +options: { + environment: process.env.BRAINTREE_ENVIRONMENT || 'sandbox', + testForceSettled: process.env.TEST_FORCE_SETTLED === 'true', + // ... +} +``` + +When both are set, `refundPayment` calls Braintree's sandbox `testing.settle` on the transaction, re-fetches it, then proceeds with `transaction.refund`. If `testForceSettled` is `true` but the provider environment is not `sandbox`, the settle step is skipped and a warning is logged. ### Medusa Configuration @@ -92,6 +100,7 @@ dependencies:[Modules.CACHE] allowRefundOnRefunded: false, disableVoidTransactions: false, logging: process.env.BRAINTREE_LOGGING === 'true', // Enable plugin debug logs + testForceSettled: process.env.TEST_FORCE_SETTLED === 'true', // Sandbox: settle before refund } } ``` @@ -109,6 +118,7 @@ dependencies:[Modules.CACHE] - **allowRefundOnRefunded**: Allow refund attempts on already-refunded imported transactions (default: `false`). - **disableVoidTransactions**: When `true`, refunds never void; only `settled`/`settling` transactions may be refunded. Late requirement so future partial order refunds and order edits can be supported (void cancels the full authorization). Default: `false`. With this enabled, refunds on unsettled transactions fail with “cannot be refunded right now”. - **logging**: Enable verbose plugin debug logging (`true` or `false`, default: `false`). When `true`, the provider logs operation details (initiate, authorize, capture, refund, etc.) and expanded Braintree error context via Medusa's logger with a `[Braintree]` prefix. Set via `BRAINTREE_LOGGING=true` in `.env` or pass `logging: true` directly in provider options. Disable in production unless actively debugging. +- **testForceSettled**: **Sandbox only.** When `true` **and** `environment` is `sandbox`, the refund flow settles the Braintree transaction via the sandbox testing API before attempting a refund. Use this to exercise the **refund** path (settled/settling) instead of the **void** path (authorized/submitted_for_settlement). Defaults to `false`. Ignored (with a warning) outside sandbox. Set via `TEST_FORCE_SETTLED=true` in `.env` wired to this option, or pass `testForceSettled: true` directly. Do not enable in production. ### Debug logging diff --git a/plugins/braintree-payment/package.json b/plugins/braintree-payment/package.json index 69a71a2a..ec2117a8 100644 --- a/plugins/braintree-payment/package.json +++ b/plugins/braintree-payment/package.json @@ -1,6 +1,6 @@ { "name": "@lambdacurry/medusa-payment-braintree", - "version": "0.1.10-next", + "version": "0.2.0-next", "description": "Braintree plugin for Medusa", "author": "Lambda Curry (https://lambdacurry.dev)", "license": "MIT", 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 a39d0dc1..7a4380b3 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 @@ -75,20 +75,12 @@ const settledRefundInput = (amount: number, transactionId = 't-settled'): Refund }); describe('BraintreeProviderService core behaviors', () => { - const originalTestForceSettled = process.env.TEST_FORCE_SETTLED; - beforeEach(() => { jest.resetAllMocks(); - delete process.env.TEST_FORCE_SETTLED; }); afterEach(() => { jest.useRealTimers(); - if (originalTestForceSettled === undefined) { - delete process.env.TEST_FORCE_SETTLED; - } else { - process.env.TEST_FORCE_SETTLED = originalTestForceSettled; - } }); it('returns cached client token when available', async () => { @@ -538,9 +530,8 @@ describe('BraintreeProviderService core behaviors', () => { }); }); - it('refundPayment settles then refunds when TEST_FORCE_SETTLED is enabled in sandbox', async () => { - process.env.TEST_FORCE_SETTLED = 'true'; - const { service, gateway } = buildService({ environment: 'sandbox' }); + it('refundPayment settles then refunds when testForceSettled is enabled in sandbox', async () => { + const { service, gateway } = buildService({ environment: 'sandbox', testForceSettled: true }); gateway.transaction.find .mockResolvedValueOnce({ id: 't-force', status: 'authorized' }) @@ -561,9 +552,8 @@ describe('BraintreeProviderService core behaviors', () => { expect(forceEntry?.transaction?.id).toBe('r-force'); }); - it('refundPayment ignores TEST_FORCE_SETTLED outside sandbox and voids authorized transactions', async () => { - process.env.TEST_FORCE_SETTLED = 'true'; - const { service, gateway, logger } = buildService({ environment: 'production' }); + it('refundPayment ignores testForceSettled outside sandbox and voids authorized transactions', async () => { + const { service, gateway, logger } = buildService({ environment: 'production', testForceSettled: true }); gateway.transaction.find .mockResolvedValueOnce({ id: 't-prod', status: 'authorized' }) @@ -576,7 +566,7 @@ describe('BraintreeProviderService core behaviors', () => { expect(gateway.transaction.void).toHaveBeenCalledWith('t-prod'); expect(gateway.transaction.refund).not.toHaveBeenCalled(); expect(logger.warn).toHaveBeenCalledWith( - '[Braintree refund] TEST_FORCE_SETTLED ignored — only supported when environment is sandbox', + '[Braintree refund] testForceSettled ignored — only supported when environment is sandbox', ); const prodEntry = lastRefundEntry(result.data); expect(prodEntry?.type).toBe('voided'); 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 9e68880a..08adc733 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 @@ -402,11 +402,11 @@ class BraintreeBase extends AbstractPaymentProvider { } /** - * Whether sandbox test settlement is enabled (`TEST_FORCE_SETTLED=true` and env is sandbox). + * Whether sandbox test settlement is enabled (`testForceSettled` option and env is sandbox). */ private isTestForceSettledEnabled(): boolean { return ( - process.env.TEST_FORCE_SETTLED === 'true' && this.options_.environment.toLowerCase() === 'sandbox' + !!this.options_.testForceSettled && this.options_.environment.toLowerCase() === 'sandbox' ); } @@ -529,6 +529,7 @@ class BraintreeBase extends AbstractPaymentProvider { options.allowRefundOnRefunded = options.allowRefundOnRefunded ?? false; options.disableVoidTransactions = options.disableVoidTransactions ?? false; options.logging = options.logging ?? false; + options.testForceSettled = options.testForceSettled ?? false; const booleanFields = [ 'enable3DSecure', @@ -537,6 +538,7 @@ class BraintreeBase extends AbstractPaymentProvider { 'allowRefundOnRefunded', 'disableVoidTransactions', 'logging', + 'testForceSettled', ]; for (const field of booleanFields) { if (isDefined(options[field as keyof BraintreeOptions]) && typeof options[field as keyof BraintreeOptions] !== 'boolean') { @@ -1174,15 +1176,15 @@ class BraintreeBase extends AbstractPaymentProvider { /** * Sandbox-only: force settle so refund paths can be exercised in tests. - * No-ops unless `TEST_FORCE_SETTLED=true` and environment is sandbox. + * No-ops unless `testForceSettled` is true and environment is sandbox. * @param transaction - Transaction to optionally settle */ private async applyTestForceSettled(transaction: Transaction): Promise { - if (process.env.TEST_FORCE_SETTLED !== 'true') return transaction; + if (!this.options_.testForceSettled) return transaction; if (!this.isTestForceSettledEnabled()) { this.logger.warn( - '[Braintree refund] TEST_FORCE_SETTLED ignored — only supported when environment is sandbox', + '[Braintree refund] testForceSettled ignored — only supported when environment is sandbox', ); return transaction; } 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 d0ddb45b..802bc467 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 @@ -19,6 +19,12 @@ export interface BraintreeOptions extends Braintree.ClientGatewayConfig { disableVoidTransactions?: boolean; /** When true, logs important operations to the console for debugging. */ logging?: boolean; + /** + * Sandbox only. When true, refundPayment settles the transaction via the + * Braintree testing API before refunding (exercises refund vs void path). + * Ignored outside sandbox. Default: false. + */ + testForceSettled?: boolean; } export const PaymentProviderKeys = {