Skip to main content

Return & RMA Module

The strict, 3-stage reverse-logistics engine managing customer returns, financial refunds, and atomic inventory restocking.

MongoDB Razorpay Zod

1. Overview

The Return Module (src/modules/returns/) handles the complex lifecycle of reversing an order – moving money backward and restoring inventory. It operates on a strictly enforced 3‑stage state machine and uses proportional discounting to calculate refund amounts based on what the customer actually paid (not the live catalog price).

Base Route: /api/v1/returns
RMA lifecycle stages: PENDING_APPROVALAPPROVED / REJECTEDREFUNDED


2. The 3‑Stage State Machine (Sequence Diagram)


3. State Machine & Status Transitions

StatusDescriptionWho Can TransitionNext Possible Statuses
PENDING_APPROVALCustomer request submitted, awaiting admin review.AdminAPPROVED, REJECTED
APPROVEDReturn approved; customer must ship item back.Admin (via process)REFUNDED
REJECTEDReturn declined. Order reverts to DELIVERED.Admin(terminal)
REFUNDEDRefund issued and inventory restocked. Order becomes RETURNED.System (auto after gateway success)(terminal)

Note: A unique index on order prevents duplicate return requests for the same order (double‑refund protection).


4. Financial Taint Severing & Proportional Discounting

The system never uses the live product price for refunds. Instead, it looks at the historical order snapshot and applies proportional discounting:

  • Formula: refundAmount = (rawItemTotal / rawOrderTotal) * actualPaidAmount
  • Why: If a user bought a ₹1000 item and a ₹1500 item, used a ₹500 coupon, and paid ₹2000, then returning the ₹1000 item should refund ₹800 (not ₹1000). The discount is proportionally distributed.

Implementation: ReturnService.initiateReturn

const rawSubtotal = order.items.reduce((sum, item) => sum + item.priceAtPurchase * item.quantity, 0);
const discountRatio = order.pricing.totalAmount / rawSubtotal;
const refundEstimate = rawItemTotal * discountRatio;

This prevents coupon‑abuse refund exploits and keeps financial records accurate for GST audits.


5. Eligibility Firewalls (Stage 1)

ConditionEnforcementError Response
Order ownershipOrder.findOne({ _id, user: userId })404 Not Found
Order deliveredorder.orderStatus === "DELIVERED"400 Bad Request
7‑day windowdaysSinceDelivery > 7 → reject400 Bad Request
Hygiene policy (INNERWEAR)liveProduct.itemType === "INNERWEAR"403 Forbidden
Fragile items require imagesisFragile === true and no images array400 Bad Request
Duplicate returnUnique index on order (MongoDB 11000)409 Conflict

6. Refund & Restock (Stage 3 – Two‑Phase Execution)

To avoid database connection pool starvation, the refund is processed outside the ACID transaction:

  1. Gateway handshake – Call Razorpay refund API (may take a few seconds).
  2. If success – Open a MongoDB transaction to atomically:
    • Restock inventory using bulkWrite ($inc on each product).
    • Update return status to REFUNDED.
    • Update order status to RETURNED.
  3. If gateway fails – Throw error; no database changes.

This pattern prevents long‑held database locks and ensures that a failed refund does not corrupt stock data.


7. Security & Firewalls

ThreatMitigation
IDOR (return on someone else’s order)Order.findOne({ _id, user: userId }) before creating return.
NoSQL injectionAll $eq wrappers + Zod regex validation for ObjectId.
Mass assignment / prototype pollutionZod .strict() schemas drop undocumented fields.
Double refundUnique index on order in Return collection.
Refund amount manipulationRefund estimate calculated on backend from historical order snapshot, never trusted from frontend.
Log injectionsafeLog() strips \r\n from logs (CWE‑117).
Rate limitingstandardLimiter applied before authentication on public endpoints.

FilePurpose
return.public.controller.tsCustomer endpoints – initiate, history.
return.admin.controller.tsAdmin endpoints – list, arbitrate, process refund.
return.service.tsCore logic – eligibility, proportional refund, arbitration, refund+restock transaction.
return.model.tsMongoose schema, unique index on order.
return.interface.tsTypeScript enums (ReturnStatus, ReturnReason) and interfaces.
return.dto.tsZod validation (ObjectId regex, conditional adminRejectionReason).
return.route.tsRoute definitions with rate limiting, auth, RBAC.

See Also


The Reshma-Core Team