Skip to content

[Enhancement] Non-custodial refund and dispute flow: signed reverse payments with atomic access revocation #62

Description

@zeemscript

Summary

Design a non-custodial refund and dispute flow for course/book purchases. Because DeenBridge payments go buyer → creator directly on Stellar (the platform never holds funds), a refund can't be a database toggle — it has to be a real on-chain reverse payment from the educator back to the buyer, tracked through a proper lifecycle and reconciled against Horizon. Today there is no refund concept at all: once a Transaction is confirmed and access is granted, there is no supported way to reverse it, and no dispute state a buyer can open.

Current state

  • Payments are non-custodial and peer-to-peer: buildPaymentTransaction sends from the buyer's wallet to creator.stellarWallet.publicKey; the platform wallet is never in the path (src/services/stellar/stellarService.js, src/controllers/stellar/paymentController.js). So a refund must originate from the creator's wallet, signed by the creator — mirroring the existing buyer-side build/submit flow in reverse.
  • Transaction.status enum is ["pending", "submitted", "confirmed", "failed", "expired"] (src/models/Transaction.js) — there is no refund_requested, refunded, disputed, or refund_rejected. There is no field linking a refund back to its original transaction.
  • On confirmed, submitPayment grants access by pushing to buyer.purchasedBooks/purchasedCourses and Course.enrolledUsers, and bumps stat counters. Nothing revokes any of that.
  • cancelTransaction only marks a still-pending transaction expired; it does nothing on-chain and can't touch a confirmed purchase.
  • There is no admin/arbitration surface (User.role is ["student", "tutor"] only), so disputes have no mediator today.

What to build

  1. Model (extend Transaction.js or a new Refund model referencing the original): add refund lifecycle states and links. Recommended Refund model — originalTransaction (ref, indexed), requestedBy (buyer), reason, status (requested → approved → signed → submitted → confirmed | rejected | failed), refundTxHash, refundLedger, amount, resolvedBy, timestamps. Add a disputed/refunded flag or state on the original transaction.
  2. Buyer: request a refundPOST /api/stellar/payment/transactions/:id/refund-request with a reason. Only the original buyer, only on a confirmed transaction, within a configurable window; idempotent (one open request per transaction).
  3. Educator: approve + sign the reverse payment — because it's non-custodial, the creator must sign. Provide POST .../refund/build that returns an unsigned reverse-payment XDR (creator → buyer, same asset/amount, memo referencing the original tx hash) and POST .../refund/submit that submits the signed XDR via submitTransaction, verifies it, and marks the refund confirmed. Reuse the exact build/submit/verify plumbing the forward payment already uses.
  4. Access revocation on confirmed refund: atomically (Mongo session, like the purchase path) pull the item from buyer.purchasedBooks/purchasedCourses, remove the buyer from Course.enrolledUsers, and decrement the relevant stat counter — reversing exactly what submitPayment granted.
  5. Dispute path: if the educator rejects or doesn't act within a window, the buyer can escalate to disputed; an admin/arbiter endpoint (role-gated, aligned with [Enhancement] Introduce role-based authorization and fix registration privilege escalation #20) can record a resolution. The platform cannot move funds itself (non-custodial), so resolution either nudges the educator's signed refund or records an off-chain outcome — document this honestly.
  6. On-chain truth: a refund is only confirmed after verifyTransaction confirms the reverse payment on Horizon; never trust client-reported success alone.

Acceptance criteria

  • A buyer can request a refund only on their own confirmed transaction, within the window, with at most one open request (idempotent); others get 403/400.
  • The refund reverse-payment is built as an unsigned XDR from the creator to the buyer for the correct asset/amount, and is only marked confirmed after Horizon verification — never on client claim alone.
  • On a confirmed refund, access is revoked atomically: item removed from the buyer's purchased list, buyer pulled from Course.enrolledUsers, and the matching stat counter decremented — with no partial state on failure (transaction session).
  • Refund and original transaction are cross-linked and queryable; refund state transitions are enforced (no confirmed without submitted, etc.).
  • Dispute escalation and an admin/arbiter resolution endpoint exist and are role-gated; the non-custodial limitation (platform can't unilaterally move funds) is documented in the endpoint responses/README.
  • Jest + supertest tests cover: request authorization, reverse-XDR build correctness (mocked Stellar SDK), access-revocation atomicity, and rejection/dispute transitions.

Pointers

Difficulty

High — it spans a new lifecycle model, a second signed on-chain flow (reverse payment), atomic multi-document access revocation, dispute/arbitration with role gating, and honest handling of non-custodial constraints.


🏆 GrantFox OSS — Official Campaign | FWC26. Apply for this issue through the GrantFox campaign page. The maintainer assigns one contributor before work starts; unassigned PRs may not be reviewed. PRs target the dev branch. Quality bar: CI must stay green.

💬 Questions or need help? Reach the maintainers and other contributors on the DeenBridge Telegram: https://t.me/+nst9lXNj1wc4ZDE0

Metadata

Metadata

Assignees

Labels

GrantFox OSSPart of the GrantFox OSS programMaybe RewardedPotential reward for completionOfficial Campaign | FWC26GrantFox official campaign FWC26complexity:highMaps to Drips Wave High tier (200 pts)enhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions