Skip to content

feat(core, dashboard): Support pluggable refund destinations#4594

Open
oliverstreissi wants to merge 8 commits into
minorfrom
feat/4563-refund-destinations
Open

feat(core, dashboard): Support pluggable refund destinations#4594
oliverstreissi wants to merge 8 commits into
minorfrom
feat/4563-refund-destinations

Conversation

@oliverstreissi
Copy link
Copy Markdown
Collaborator

@oliverstreissi oliverstreissi commented Mar 31, 2026

Description

Add RefundDestinationStrategy interface allowing plugins to define alternative refund destinations (store credit, gift cards, etc.) instead of only refunding to the original payment method.

Backend:

  • New RefundDestinationStrategy interface with isAvailable/createRefund methods
  • refundDestinations query to discover available destinations per order
  • destination field on RefundOrderInput and Refund entity
  • Routes PaymentService.createRefund through destination strategy when specified
  • Startup validation rejecting strategies using reserved default code
  • Uses refundable payment capacity (not just Settled state) for availability checks

Dashboard:

  • Refund dialog split into "Payment methods" and "Other refund destinations" sections
  • Independent allocation with payments prioritized before destinations
  • Fix multi-target refund sending duplicate RefundLine records
  • Remove orphaned refundFragment and dead utility functions

Fixes #4563

Breaking changes

None. Omitting destination in RefundOrderInput preserves existing behavior.

Screenshots

Screen.Recording.2026-03-31.at.10.21.26.mov

Checklist

📌 Always:

  • I have set a clear title
  • My PR is small and contains a single feature
  • I have checked my own PR

👍 Most of the time:

  • I have added or updated test cases
  • I have updated the README if needed

@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 31, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
vendure-storybook Ready Ready Preview, Comment Mar 31, 2026 11:47am

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 31, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 77dc345d-2bf6-4d6f-96ab-6834acbc20c5

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/4563-refund-destinations

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 31, 2026

Dashboard Preview: https://admin-dashboard-51vnluwnj-vendure.vercel.app

oliverstreissi and others added 2 commits March 31, 2026 12:07
The fix for modified order lines not appearing in the refund dialog
needs further investigation. Reverting to keep the PR clean.
* The returned {@link CreateRefundResult} determines the refund state
* and any associated transaction ID or metadata.
*/
createRefund(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this not lead to duplication of logic between eg the StoreCreditPaymentMethodHandler and the StoreCreditRefundDestination?

Why do we not delegate this logic to the existing payment method handler?

@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown
Member

@michaelbromley michaelbromley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dumping some notes from Claude here for when we revisit.

Architectural Review

Thanks for the solid work on this — the feature is clearly needed, and the strategy interface, e2e tests, and dashboard UI refactor are all well done. However, there are some architectural concerns worth discussing before merge.

1. Payment coupling for non-payment destinations

This is the main one. Every Refund is forced to reference a Payment, even when the destination (store credit, gift card) has nothing to do with the original payment method. The dashboard grabs firstRefundablePaymentId just to satisfy the FK — making Refund.payment semantically meaningless for destination refunds.

Both createRefund() and isAvailable() take a Payment parameter that's irrelevant for non-payment destinations. This will cause confusion for anyone querying refund data downstream.

Suggestion: Make Refund.payment nullable for destination-based refunds, and remove the Payment parameter from the strategy interface (pass Order only). If payment context is genuinely needed for some destinations, it could be optional.

2. No database migration

The PR adds @Column({ nullable: true, type: 'varchar' }) destination to the Refund entity but includes no migration file. This column won't exist until someone generates and runs a migration.

3. isAvailable inconsistency between query and mutation

getRefundDestinations() checks availability against ALL refundable payments (any match = available):

for (const payment of refundablePayments) {
    if (await strategy.isAvailable(ctx, order, payment)) {
        available = true;
        break;
    }
}

But createRefund() checks against the specific paymentId the dashboard sent — which for destinations is just firstRefundablePaymentId. This means the query could report a destination as available (because payment B passes), but the mutation rejects it (because it checks payment A).

4. Non-atomic split refunds

Multi-target refunds send sequential mutations from the dashboard. If the first succeeds and the second fails, you get a partial refund with no rollback. Only the first target gets refundLines, so subsequent Refund entities have no line references — which will cause issues in reporting and reconciliation.

Suggestion: Consider a single-mutation API that accepts multiple targets, so the server can handle this atomically.

5. Hardcoded constant duplication

// Must match DEFAULT_REFUND_DESTINATION_CODE from @vendure/common
const DEFAULT_REFUND_DESTINATION_CODE = 'default';

The CJS/ESM issue is understood, but this will go stale silently. Could the refundDestinations query mark which destination is default, avoiding the need for the dashboard to know the constant?

Minor items

  • No validation for duplicate strategy codes at startup
  • getRefundDestinations lives on OrderService but deals with payment/refund concerns — PaymentService might be a more natural home
  • ~60 lines of unrelated formatting changes (import reordering, ternary indentation) make the diff noisier than needed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants