Skip to content

feat: add refund reason handling to payment order responses and webhook#683

Open
onahprosper wants to merge 3 commits intomainfrom
feat/display-refund-reason
Open

feat: add refund reason handling to payment order responses and webhook#683
onahprosper wants to merge 3 commits intomainfrom
feat/display-refund-reason

Conversation

@onahprosper
Copy link
Collaborator

@onahprosper onahprosper commented Feb 6, 2026

Description

This PR adds refund reason to the payment_order.refunding and payment_order.refunded webhooks and to the transaction details/list API when status is refunded. The reason is derived from existing cancellation_reasons on the PaymentOrder table; no schema or migration.

  • Stale-ops: Set default cancellation reason (e.g. "Order timed out") before refund when order has no reasons.
  • Webhook: Add refundReason to PaymentOrderWebhookData; set from joined CancellationReasons when status is refunding or refunded.
  • API: Add refundReason to SenderOrderResponse; set in GetPaymentOrderByID and list when status is refunded.

References

Testing

  • Existing sender tests pass. Added assertions: when refundReason is present in GetPaymentOrderByID or in list order items, it is a string.

  • Manual: trigger a refund (stale-ops or provider cancel), verify webhook payload and GET /sender/orders/:id and list include refundReason when status is refunded/refunding.

  • This change adds test coverage for new/changed/fixed functionality

Checklist

  • I have added documentation and tests for new/changed functionality in this PR
  • All active GitHub checks for tests, formatting, and security are passing
  • The correct base branch is being used, if not main

By submitting a PR, I agree to Paycrest's Contributor Code of Conduct and Contribution Guide.

Summary by CodeRabbit

  • New Features

    • Payment orders and webhook payloads now include an optional refundReason field that shows joined cancellation reasons when present.
  • Bug Fixes

    • Orders that expire without a cancellation reason are auto-assigned "Order timed out" before refund processing so refunds proceed with context.
  • Tests

    • Added validation for refundReason presence/type, improved timestamp comparisons, and enhanced test payloads for provider-related scenarios.

- Introduced RefundReason field in SenderOrderResponse and PaymentOrderWebhookData types to include refund reasons in API responses.
- Implemented refundReasonFromOrder function to conditionally format refund reasons based on payment order status and cancellation reasons.
- Updated relevant test cases to validate the inclusion of refund reasons in responses.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 6, 2026

📝 Walkthrough

Walkthrough

Adds RefundReason propagation: new optional RefundReason fields in response and webhook structs, helper logic to compute a semicolon-joined refund reason from CancellationReasons, tests updated to validate refundReason type, and a pre-refund step that ensures CancellationReasons is populated before issuing refunds.

Changes

Cohort / File(s) Summary
Data structures
types/types.go
Added RefundReason string \json:"refundReason,omitempty"`toSenderOrderResponseandPaymentOrderWebhookData`.
Sender controller & response builders
controllers/sender/sender.go
Added refundReasonFromOrder(status, reasons) helper and integrated it when building payment order responses to populate RefundReason.
Webhook payload construction
utils/utils.go
Compute refundReason by joining CancellationReasons when status is refunded/refunding and include it in PaymentOrderWebhookData.
Refund retry task
tasks/stale_ops.go
Before calling RefundOrder in RetryStaleUserOperations, conditionally update orders with empty CancellationReasons to ["Order timed out"] so refunds always have a reason.
Tests
controllers/sender/sender_test.go
Added runtime checks asserting refundReason is a string when present; adjusted test payload declarations and timestamp parsing; added providerId in a v2 test payload.

Sequence Diagram(s)

sequenceDiagram
  participant Aggregator
  participant SenderController as Sender Controller
  participant Utils
  participant Storage
  participant StaleOps as StaleOps Task
  participant RefundService as Refund Service

  Aggregator->>SenderController: webhook (may include CancellationReasons)
  SenderController->>Utils: build webhook/payment payload
  Utils-->>SenderController: payload with RefundReason (if refunded)
  SenderController->>Storage: persist webhook/order (includes RefundReason)
  StaleOps->>Storage: find stale orders
  StaleOps->>Storage: conditional update CancellationReasons if empty ("Order timed out")
  StaleOps->>RefundService: RefundOrder(orderID, ... with CancellationReasons)
  RefundService-->>Storage: record refund outcome
  RefundService-->>SenderController: emit update/response (may include RefundReason)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 I hop through logs with whiskers bright,
I stitch the reasons, join them tight;
A timeout note, a webhook song,
Refund reasons where they belong,
Hooray — receipts sing clear tonight! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main purpose of the PR: adding refund reason handling to payment order responses and webhooks.
Description check ✅ Passed The description is comprehensive and addresses all template sections: it explains the purpose, provides references (#680), covers testing approach with assertions and manual verification steps, and includes completed checklist items.
Linked Issues check ✅ Passed The PR addresses all coding requirements from #680: adds refundReason to webhook payload [utils/utils.go], includes it in API responses [controllers/sender/sender.go, types/types.go], and sets default cancellation reasons [tasks/stale_ops.go]. Frontend conditional rendering is a UI responsibility outside this PR's scope.
Out of Scope Changes check ✅ Passed All changes are directly aligned with #680 objectives: refundReason field additions, webhook payload construction, API response enhancement, and default cancellation reason logic. Test updates support the implementation and are within scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/display-refund-reason

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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@tasks/stale_ops.go`:
- Around line 332-344: The current UpdateOneID(...).SetCancellationReasons(...)
unconditionally overwrites cancellation reasons from the in-memory snapshot and
can clobber a concurrent update; change the update to be conditional so it only
sets the default when the DB row still has empty reasons. Replace the
unconditional
storage.Client.PaymentOrder.UpdateOneID(order.ID).SetCancellationReasons(...)
call with a conditional update (e.g.
storage.Client.PaymentOrder.Update().Where(paymentorder.IDEQ(order.ID),
paymentorder.CancellationReasonsLenEQ(0) or an equivalent "is empty" predicate).
Keep the same error handling path but skip/continue when the conditional update
affects 0 rows so you don’t overwrite newer reasons for
order.CancellationReasons.
🧹 Nitpick comments (2)
controllers/sender/sender.go (1)

1710-1716: Centralize refund-reason derivation to avoid drift.
You now compute refundReason here and separately in webhook construction. Consider extracting a shared helper (e.g., utils.RefundReasonFromOrder) and reusing it to keep status rules in sync.

utils/utils.go (1)

302-309: Deduplicate refundReason logic across webhook and sender responses.
Consider reusing a shared helper (e.g., from utils) so webhook and API responses stay aligned if status rules evolve.

Also applies to: 339-339

- Updated the RetryStaleUserOperations function to log errors when setting cancellation reasons for payment orders, ensuring that refunds still proceed even if an error occurs.
- Refactored the update logic to use a more explicit query structure for better clarity and maintainability.
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

- Refactored the query in the RetryStaleUserOperations function to remove unnecessary conditions, enhancing clarity and maintainability.
- Ensured that the cancellation reasons are set correctly for payment orders with a streamlined update process.
Comment on lines +307 to +309
if (paymentOrder.Status == paymentorder.StatusRefunded || paymentOrder.Status == paymentorder.StatusRefunding) && len(reasons) > 0 {
refundReason = strings.Join(reasons, "; ")
}
Copy link
Contributor

Choose a reason for hiding this comment

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

@onahprosper this could contain duplicate reasons

if status != paymentorder.StatusRefunded || reasons == nil || len(reasons) == 0 {
return ""
}
return strings.Join(reasons, "; ")
Copy link
Contributor

Choose a reason for hiding this comment

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

@onahprosper this could contain duplicate reasons

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.

Display Refund reason

2 participants

Comments