-
Notifications
You must be signed in to change notification settings - Fork 6
Fix/multiple refunds optional void #43
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: fix/multiple-refunds
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,8 +41,8 @@ BRAINTREE_PRIVATE_KEY=<your_private_key> | |
| BRAINTREE_WEBHOOK_SECRET=<your_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 | ||
|
|
||
|
|
@@ -90,7 +98,9 @@ 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 | ||
| testForceSettled: process.env.TEST_FORCE_SETTLED === 'true', // Sandbox: settle before refund | ||
| } | ||
| } | ||
| ``` | ||
|
|
@@ -106,7 +116,9 @@ 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”. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Document the actual refund status and error contract. The implementation returns
📍 Affects 2 files
🤖 Prompt for AI Agents |
||
| - **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 | ||
|
|
||
|
|
@@ -140,6 +152,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 | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -402,11 +402,11 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 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' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -527,9 +527,19 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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']; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| options.testForceSettled = options.testForceSettled ?? false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const booleanFields = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'enable3DSecure', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'savePaymentMethod', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'autoCapture', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'allowRefundOnRefunded', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'disableVoidTransactions', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'logging', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'testForceSettled', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const field of booleanFields) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isDefined(options[field as keyof BraintreeOptions]) && typeof options[field as keyof BraintreeOptions] !== 'boolean') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new MedusaError( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1166,15 +1176,15 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 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<Transaction> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1186,12 +1196,22 @@ class BraintreeBase extends AbstractPaymentProvider<BraintreeOptions> { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 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<RefundAction> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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 }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
1202
to
1216
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Confirm whether AbstractPaymentProvider declares an `options` member.
fd -t f 'abstract-payment-provider*' node_modules/@medusajs 2>/dev/null | head
rg -nP --type=ts -C4 '\b(protected|public|readonly)?\s*options\s*[:;=]' \
$(fd -t f -p 'abstract-payment-provider' node_modules/@medusajs 2>/dev/null | head -5) 2>/dev/null
# Show every option read inside braintree-base.ts for consistency.
rg -nP --type=ts '\bthis\.options_?\.' plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.tsRepository: lambda-curry/medusa-plugins Length of output: 165 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- candidate files ---'
git ls-files | rg '(^|/)(abstract-payment-provider|braintree-base|package\.json|pnpm-lock|yarn\.lock|package-lock\.json)' | head -80
printf '%s\n' '--- BraintreeBase option declarations and reads ---'
rg -n -C3 'options_|this\.options\b|disableVoidTransactions' plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts
printf '%s\n' '--- provider base references in source and manifests ---'
rg -n -C3 'AbstractPaymentProvider|`@medusajs/medusa`|disableVoidTransactions' plugins/braintree-payment/src plugins/braintree-payment/package.json package.json 2>/dev/null | head -240Repository: lambda-curry/medusa-plugins Length of output: 17921 🌐 Web query:
💡 Result: In Medusa 2.x, the Citations:
🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- constructor and test setup ---'
sed -n '320,355p' plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts
sed -n '1,75p' plugins/braintree-payment/src/providers/payment-braintree/src/core/__tests__/braintree-base.spec.ts
sed -n '220,270p' plugins/braintree-payment/src/providers/payment-braintree/src/core/__tests__/braintree-base.spec.ts
printf '%s\n' '--- dependency lock entries ---'
rg -n -C2 '(`@medusajs/medusa`|`@medusajs/framework`)' plugins/braintree-payment/package.json yarn.lock | head -160Repository: lambda-curry/medusa-plugins Length of output: 14556 🏁 Script executed: #!/bin/bash
set -e
url='https://raw.githubusercontent.com/medusajs/medusa/v2.15.2/packages/core/utils/src/payment/abstract-payment-provider.ts'
src="$(curl -fsSL "$url")"
printf '%s\n' '--- AbstractPaymentProvider option members ---'
printf '%s\n' "$src" | rg -n -C5 '\b(options|config)\b|constructor'
printf '%s\n' '--- BraintreeBase option access classification ---'
python3 - <<'PY'
from pathlib import Path
p = Path("plugins/braintree-payment/src/providers/payment-braintree/src/core/braintree-base.ts")
text = p.read_text()
for i, line in enumerate(text.splitlines(), 1):
if "this.options" in line or "options_" in line:
print(f"{i}: {line}")
PYRepository: lambda-curry/medusa-plugins Length of output: 4983 Use
♻️ Proposed change if (isVoidableRefundStatus(resolved.status)) {
- if (this.options.disableVoidTransactions) {
+ if (this.options_.disableVoidTransactions) {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -285,6 +285,16 @@ class BraintreeImport extends AbstractPaymentProvider<BraintreeOptions> { | |
| 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`, | ||
| ); | ||
| } | ||
|
|
||
|
Comment on lines
+288
to
+297
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Exclude this policy error from the already-refunded fallback. When Use a structured gateway-error check for already-refunded cases. Re-throw the local 🤖 Prompt for AI Agents |
||
| const cancelResponse = await this.gateway.transaction.void(transaction.id); | ||
|
|
||
| if (isBraintreeFailureResponse(cancelResponse)) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Align release metadata and upgrade documentation.
The package declares
0.1.10-next, the changelog entry says0.1.9-next, and the README places the new option underUpgrading to 0.1.2. This can publish one version with release notes and migration guidance for different versions.plugins/braintree-payment/CHANGELOG.md#L3-L7: use the intended package version in the top heading.plugins/braintree-payment/README.md#L137-L145: move thedisableVoidTransactionsnote under the release that introduces it.plugins/braintree-payment/package.json#L3-L3: keep the package version consistent with the changelog heading.📍 Affects 3 files
plugins/braintree-payment/CHANGELOG.md#L3-L7(this comment)plugins/braintree-payment/README.md#L137-L145plugins/braintree-payment/package.json#L3-L3🤖 Prompt for AI Agents