order-management 0.0.82 → 0.0.83

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. package/.medusa/server/src/admin/index.js +9 -9
  2. package/.medusa/server/src/admin/index.mjs +9 -9
  3. package/.medusa/server/src/api/admin/return/[id]/reject/route.js +6 -2
  4. package/.medusa/server/src/api/admin/swaps/[id]/cancel/route.js +5 -1
  5. package/.medusa/server/src/api/admin/swaps/[id]/reject/route.js +5 -1
  6. package/.medusa/server/src/api/store/guest-orders/[id]/returns/route.js +56 -41
  7. package/.medusa/server/src/api/store/guest-orders/[id]/swaps/[swap_id]/cancel/route.js +3 -1
  8. package/.medusa/server/src/api/store/guest-orders/[id]/swaps/route.js +25 -15
  9. package/.medusa/server/src/api/store/returns/[id]/cancel/route.js +3 -1
  10. package/.medusa/server/src/api/store/returns/route.js +24 -19
  11. package/.medusa/server/src/api/store/swaps/[id]/cancel/route.js +5 -1
  12. package/.medusa/server/src/api/store/swaps/route.js +26 -15
  13. package/.medusa/server/src/helpers/index.js +2 -1
  14. package/.medusa/server/src/helpers/return-exchange-guard.js +129 -0
  15. package/.medusa/server/src/modules/order-rma-gate/constants.js +21 -0
  16. package/.medusa/server/src/modules/order-rma-gate/index.js +20 -0
  17. package/.medusa/server/src/modules/order-rma-gate/migrations/Migration20260724100000.js +34 -0
  18. package/.medusa/server/src/modules/order-rma-gate/models/order-rma-gate.js +11 -0
  19. package/.medusa/server/src/modules/order-rma-gate/service.js +202 -0
  20. package/.medusa/server/src/modules/order-rma-gate/stale-reclaim.js +18 -0
  21. package/.medusa/server/src/modules/order-rma-gate/types.js +3 -0
  22. package/.medusa/server/src/subscribers/order-return-received.js +45 -0
  23. package/README.md +13 -4
  24. package/package.json +4 -2
@@ -0,0 +1,202 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const utils_1 = require("@medusajs/framework/utils");
7
+ const crypto_1 = require("crypto");
8
+ const order_rma_gate_1 = __importDefault(require("./models/order-rma-gate"));
9
+ const constants_1 = require("./constants");
10
+ const stale_reclaim_1 = require("./stale-reclaim");
11
+ function isUniqueViolation(error) {
12
+ if (!error || typeof error !== "object") {
13
+ return false;
14
+ }
15
+ const e = error;
16
+ if (e.code === "23505") {
17
+ return true;
18
+ }
19
+ const msg = `${e.message ?? ""} ${e.name ?? ""}`.toLowerCase();
20
+ return msg.includes("unique") || msg.includes("duplicate");
21
+ }
22
+ function newGateId() {
23
+ return `orgate_${(0, crypto_1.randomUUID)().replace(/-/g, "")}`;
24
+ }
25
+ class OrderRmaGateModuleService extends (0, utils_1.MedusaService)({
26
+ OrderRmaGate: order_rma_gate_1.default,
27
+ }) {
28
+ constructor(container) {
29
+ super(container);
30
+ this.manager_ = container.manager;
31
+ }
32
+ get moduleKey() {
33
+ return constants_1.ORDER_RMA_GATE_MODULE;
34
+ }
35
+ normalize(raw) {
36
+ if (!raw || typeof raw !== "object" || !("id" in raw) || !("order_id" in raw)) {
37
+ throw new Error("Invalid order_rma_gate data");
38
+ }
39
+ const row = raw;
40
+ const kind = row.kind === "exchange" ? "exchange" : "return";
41
+ return {
42
+ id: row.id,
43
+ order_id: row.order_id,
44
+ kind,
45
+ reference_id: row.reference_id ?? null,
46
+ created_at: new Date(row.created_at ?? new Date()),
47
+ updated_at: new Date(row.updated_at ?? new Date()),
48
+ deleted_at: row.deleted_at ? new Date(row.deleted_at) : null,
49
+ };
50
+ }
51
+ getKnex() {
52
+ const manager = this.manager_;
53
+ if (typeof manager.getKnex !== "function") {
54
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.UNEXPECTED_STATE, "EntityManager.getKnex is unavailable for order_rma_gate");
55
+ }
56
+ return manager.getKnex();
57
+ }
58
+ async getActiveByOrderId(orderId) {
59
+ const knex = this.getKnex();
60
+ const row = await knex("order_rma_gate")
61
+ .where({ order_id: orderId })
62
+ .whereNull("deleted_at")
63
+ .first();
64
+ return row ? this.normalize(row) : null;
65
+ }
66
+ /**
67
+ * Insert a gate for the order. On unique conflict, attempt atomic stale reclaim
68
+ * via SELECT … FOR UPDATE in a single transaction.
69
+ */
70
+ async acquire(orderId, kind, hasActiveMedusaRma, now = new Date()) {
71
+ const knex = this.getKnex();
72
+ const id = newGateId();
73
+ const stamp = now;
74
+ try {
75
+ await knex("order_rma_gate").insert({
76
+ id,
77
+ order_id: orderId,
78
+ kind,
79
+ reference_id: null,
80
+ created_at: stamp,
81
+ updated_at: stamp,
82
+ deleted_at: null,
83
+ });
84
+ const gate = await this.getActiveByOrderId(orderId);
85
+ if (!gate) {
86
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.UNEXPECTED_STATE, "Gate insert succeeded but row not found");
87
+ }
88
+ return { ok: true, gate };
89
+ }
90
+ catch (error) {
91
+ if (!isUniqueViolation(error)) {
92
+ throw error;
93
+ }
94
+ return this.reclaimStaleOrConflict(orderId, kind, hasActiveMedusaRma, now);
95
+ }
96
+ }
97
+ /**
98
+ * Atomic reclaim: lock existing live gate, evaluate staleness, soft-delete + insert
99
+ * in the same transaction so concurrent reclaimers cannot both win.
100
+ */
101
+ async reclaimStaleOrConflict(orderId, kind, hasActiveMedusaRma, now) {
102
+ const knex = this.getKnex();
103
+ const conflictMessage = "This order already has an active return or exchange in progress. Cancel or complete the existing one before creating a new request.";
104
+ return knex.transaction(async (trx) => {
105
+ const locked = await trx("order_rma_gate")
106
+ .where({ order_id: orderId })
107
+ .whereNull("deleted_at")
108
+ .forUpdate()
109
+ .first();
110
+ if (!locked) {
111
+ // Previous holder released between unique violation and lock — insert fresh
112
+ const id = newGateId();
113
+ await trx("order_rma_gate").insert({
114
+ id,
115
+ order_id: orderId,
116
+ kind,
117
+ reference_id: null,
118
+ created_at: now,
119
+ updated_at: now,
120
+ deleted_at: null,
121
+ });
122
+ return {
123
+ ok: true,
124
+ gate: this.normalize({
125
+ id,
126
+ order_id: orderId,
127
+ kind,
128
+ reference_id: null,
129
+ created_at: now,
130
+ updated_at: now,
131
+ deleted_at: null,
132
+ }),
133
+ };
134
+ }
135
+ const existing = this.normalize(locked);
136
+ const medusaActive = await hasActiveMedusaRma();
137
+ const decision = (0, stale_reclaim_1.evaluateStaleGateReclaim)({
138
+ gateCreatedAt: existing.created_at,
139
+ referenceId: existing.reference_id,
140
+ hasActiveMedusaRma: medusaActive,
141
+ now,
142
+ staleTtlMs: constants_1.STALE_GATE_TTL_MS,
143
+ });
144
+ if (decision !== "reclaim") {
145
+ return { ok: false, message: conflictMessage, type: "NOT_ALLOWED" };
146
+ }
147
+ await trx("order_rma_gate")
148
+ .where({ id: existing.id })
149
+ .update({
150
+ deleted_at: now,
151
+ updated_at: now,
152
+ });
153
+ const newId = newGateId();
154
+ await trx("order_rma_gate").insert({
155
+ id: newId,
156
+ order_id: orderId,
157
+ kind,
158
+ reference_id: null,
159
+ created_at: now,
160
+ updated_at: now,
161
+ deleted_at: null,
162
+ });
163
+ return {
164
+ ok: true,
165
+ gate: this.normalize({
166
+ id: newId,
167
+ order_id: orderId,
168
+ kind,
169
+ reference_id: null,
170
+ created_at: now,
171
+ updated_at: now,
172
+ deleted_at: null,
173
+ }),
174
+ };
175
+ });
176
+ }
177
+ async release(orderId) {
178
+ const knex = this.getKnex();
179
+ const now = new Date();
180
+ await knex("order_rma_gate")
181
+ .where({ order_id: orderId })
182
+ .whereNull("deleted_at")
183
+ .update({
184
+ deleted_at: now,
185
+ updated_at: now,
186
+ });
187
+ }
188
+ async setReference(orderId, referenceId) {
189
+ const knex = this.getKnex();
190
+ const now = new Date();
191
+ await knex("order_rma_gate")
192
+ .where({ order_id: orderId })
193
+ .whereNull("deleted_at")
194
+ .update({
195
+ reference_id: referenceId,
196
+ updated_at: now,
197
+ });
198
+ return this.getActiveByOrderId(orderId);
199
+ }
200
+ }
201
+ exports.default = OrderRmaGateModuleService;
202
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VydmljZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uL3NyYy9tb2R1bGVzL29yZGVyLXJtYS1nYXRlL3NlcnZpY2UudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7QUFBQSxxREFBc0U7QUFFdEUsbUNBQW1DO0FBQ25DLDZFQUFrRDtBQUNsRCwyQ0FJb0I7QUFFcEIsbURBR3dCO0FBd0J4QixTQUFTLGlCQUFpQixDQUFDLEtBQWM7SUFDdkMsSUFBSSxDQUFDLEtBQUssSUFBSSxPQUFPLEtBQUssS0FBSyxRQUFRLEVBQUUsQ0FBQztRQUN4QyxPQUFPLEtBQUssQ0FBQTtJQUNkLENBQUM7SUFDRCxNQUFNLENBQUMsR0FBRyxLQUEyRCxDQUFBO0lBQ3JFLElBQUksQ0FBQyxDQUFDLElBQUksS0FBSyxPQUFPLEVBQUUsQ0FBQztRQUN2QixPQUFPLElBQUksQ0FBQTtJQUNiLENBQUM7SUFDRCxNQUFNLEdBQUcsR0FBRyxHQUFHLENBQUMsQ0FBQyxPQUFPLElBQUksRUFBRSxJQUFJLENBQUMsQ0FBQyxJQUFJLElBQUksRUFBRSxFQUFFLENBQUMsV0FBVyxFQUFFLENBQUE7SUFDOUQsT0FBTyxHQUFHLENBQUMsUUFBUSxDQUFDLFFBQVEsQ0FBQyxJQUFJLEdBQUcsQ0FBQyxRQUFRLENBQUMsV0FBVyxDQUFDLENBQUE7QUFDNUQsQ0FBQztBQUVELFNBQVMsU0FBUztJQUNoQixPQUFPLFVBQVUsSUFBQSxtQkFBVSxHQUFFLENBQUMsT0FBTyxDQUFDLElBQUksRUFBRSxFQUFFLENBQUMsRUFBRSxDQUFBO0FBQ25ELENBQUM7QUFFRCxNQUFxQix5QkFBMEIsU0FBUSxJQUFBLHFCQUFhLEVBQUM7SUFDbkUsWUFBWSxFQUFaLHdCQUFZO0NBQ2IsQ0FBQztJQUdBLFlBQVksU0FBK0I7UUFDekMsS0FBSyxDQUFDLFNBQVMsQ0FBQyxDQUFBO1FBQ2hCLElBQUksQ0FBQyxRQUFRLEdBQUcsU0FBUyxDQUFDLE9BQU8sQ0FBQTtJQUNu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
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.evaluateStaleGateReclaim = evaluateStaleGateReclaim;
4
+ function evaluateStaleGateReclaim(input) {
5
+ if (input.hasActiveMedusaRma) {
6
+ return "conflict";
7
+ }
8
+ const ageMs = input.now.getTime() - input.gateCreatedAt.getTime();
9
+ if (input.referenceId == null || input.referenceId === "") {
10
+ if (ageMs >= input.staleTtlMs) {
11
+ return "reclaim";
12
+ }
13
+ return "conflict";
14
+ }
15
+ // reference_id set but Medusa reports no active RMA → orphan / terminal entity
16
+ return "reclaim";
17
+ }
18
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3RhbGUtcmVjbGFpbS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uL3NyYy9tb2R1bGVzL29yZGVyLXJtYS1nYXRlL3N0YWxlLXJlY2xhaW0udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFhQSw0REFrQkM7QUFsQkQsU0FBZ0Isd0JBQXdCLENBQ3RDLEtBQW9DO0lBRXBDLElBQUksS0FBSyxDQUFDLGtCQUFrQixFQUFFLENBQUM7UUFDN0IsT0FBTyxVQUFVLENBQUE7SUFDbkIsQ0FBQztJQUVELE1BQU0sS0FBSyxHQUFHLEtBQUssQ0FBQyxHQUFHLENBQUMsT0FBTyxFQUFFLEdBQUcsS0FBSyxDQUFDLGFBQWEsQ0FBQyxPQUFPLEVBQUUsQ0FBQTtJQUVqRSxJQUFJLEtBQUssQ0FBQyxXQUFXLElBQUksSUFBSSxJQUFJLEtBQUssQ0FBQyxXQUFXLEtBQUssRUFBRSxFQUFFLENBQUM7UUFDMUQsSUFBSSxLQUFLLElBQUksS0FBSyxDQUFDLFVBQVUsRUFBRSxDQUFDO1lBQzlCLE9BQU8sU0FBUyxDQUFBO1FBQ2xCLENBQUM7UUFDRCxPQUFPLFVBQVUsQ0FBQTtJQUNuQixDQUFDO0lBRUQsK0VBQStFO0lBQy9FLE9BQU8sU0FBUyxDQUFBO0FBQ2xCLENBQUMifQ==
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHlwZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi9zcmMvbW9kdWxlcy9vcmRlci1ybWEtZ2F0ZS90eXBlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiIn0=
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.config = void 0;
4
+ exports.default = orderReturnReceivedHandler;
5
+ const utils_1 = require("@medusajs/framework/utils");
6
+ const return_exchange_guard_1 = require("../helpers/return-exchange-guard");
7
+ /**
8
+ * Unlock the order RMA gate when a return is actually received.
9
+ *
10
+ * Note: older Medusa `createAndCompleteReturnOrderWorkflow` may emit
11
+ * `order.return_received` even when `receive_now` is false. We only release
12
+ * when the return entity status is `received`.
13
+ */
14
+ async function orderReturnReceivedHandler({ event, container, }) {
15
+ const data = event.data ?? {};
16
+ const returnId = data.return_id ?? data.id;
17
+ try {
18
+ const orderModule = container.resolve(utils_1.Modules.ORDER);
19
+ let orderId = data.order_id;
20
+ let status;
21
+ if (returnId) {
22
+ const ret = await orderModule.retrieveReturn(returnId).catch(() => null);
23
+ if (!ret) {
24
+ return;
25
+ }
26
+ orderId = orderId ?? ret.order_id;
27
+ status = ret.status;
28
+ }
29
+ if (!orderId) {
30
+ return;
31
+ }
32
+ // Only unlock on true received status (not spurious REQUESTED-time emits)
33
+ if (status !== "received") {
34
+ return;
35
+ }
36
+ await (0, return_exchange_guard_1.releaseOrderRmaGate)(orderId, container);
37
+ }
38
+ catch (error) {
39
+ console.error(`[order.return_received] Failed to release RMA gate:`, error);
40
+ }
41
+ }
42
+ exports.config = {
43
+ event: "order.return_received",
44
+ };
45
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3JkZXItcmV0dXJuLXJlY2VpdmVkLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vc3JjL3N1YnNjcmliZXJzL29yZGVyLXJldHVybi1yZWNlaXZlZC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFrQkEsNkNBcUNDO0FBdERELHFEQUFtRDtBQUVuRCw0RUFBc0U7QUFRdEU7Ozs7OztHQU1HO0FBQ1ksS0FBSyxVQUFVLDBCQUEwQixDQUFDLEVBQ3ZELEtBQUssRUFDTCxTQUFTLEdBQytCO0lBQ3hDLE1BQU0sSUFBSSxHQUFHLEtBQUssQ0FBQyxJQUFJLElBQUksRUFBRSxDQUFBO0lBQzdCLE1BQU0sUUFBUSxHQUFHLElBQUksQ0FBQyxTQUFTLElBQUksSUFBSSxDQUFDLEVBQUUsQ0FBQTtJQUUxQyxJQUFJLENBQUM7UUFDSCxNQUFNLFdBQVcsR0FBRyxTQUFTLENBQUMsT0FBTyxDQUFzQixlQUFPLENBQUMsS0FBSyxDQUFDLENBQUE7UUFDekUsSUFBSSxPQUFPLEdBQUcsSUFBSSxDQUFDLFFBQVEsQ0FBQTtRQUMzQixJQUFJLE1BQTBCLENBQUE7UUFFOUIsSUFBSSxRQUFRLEVBQUUsQ0FBQztZQUNiLE1BQU0sR0FBRyxHQUFHLE1BQU0sV0FBVyxDQUFDLGNBQWMsQ0FBQyxRQUFRLENBQUMsQ0FBQyxLQUFLLENBQUMsR0FBRyxFQUFFLENBQUMsSUFBSSxDQUFDLENBQUE7WUFDeEUsSUFBSSxDQUFDLEdBQUcsRUFBRSxDQUFDO2dCQUNULE9BQU07WUFDUixDQUFDO1lBQ0QsT0FBTyxHQUFHLE9BQU8sSUFBSSxHQUFHLENBQUMsUUFBUSxDQUFBO1lBQ2pDLE1BQU0sR0FBRyxHQUFHLENBQUMsTUFBTSxDQUFBO1FBQ3JCLENBQUM7UUFFRCxJQUFJLENBQUMsT0FBTyxFQUFFLENBQUM7WUFDYixPQUFNO1FBQ1IsQ0FBQztRQUVELDBFQUEwRTtRQUMxRSxJQUFJLE1BQU0sS0FBSyxVQUFVLEVBQUUsQ0FBQztZQUMxQixPQUFNO1FBQ1IsQ0FBQztRQUVELE1BQU0sSUFBQSwyQ0FBbUIsRUFBQyxPQUFPLEVBQUUsU0FBUyxDQUFDLENBQUE7SUFDL0MsQ0FBQztJQUFDLE9BQU8sS0FBSyxFQUFFLENBQUM7UUFDZixPQUFPLENBQUMsS0FBSyxDQUNYLHFEQUFxRCxFQUNyRCxLQUFLLENBQ04sQ0FBQTtJQUNILENBQUM7QUFDSCxDQUFDO0FBRVksUUFBQSxNQUFNLEdBQXFCO0lBQ3RDLEtBQUssRUFBRSx1QkFBdUI7Q0FDL0IsQ0FBQSJ9
package/README.md CHANGED
@@ -321,6 +321,7 @@ This plugin is compatible with Medusa v2 (e.g. `@medusajs/framework` and `@medus
321
321
  - **Order Confirmation Emails**: Optional email notifications when orders are placed (with "Claim Order" support for registered users)
322
322
  - **Status-Based Notifications**: Configurable email, SMS, and push notifications for order status changes (pending, shipped, delivered, canceled, etc.)
323
323
  - **Return / Exchange Delivery Window**: Configurable per-type windows (`returnValidInDays`, `exchangeValidInDays`) that block return or exchange requests submitted after N days from the fulfillment's `delivered_at` timestamp. Defaults to 7 days each if not configured. The window is checked only when the order has been marked as delivered; undelivered orders are unaffected.
324
+ - **Return / Exchange mutual exclusion**: Order-level lock via `order_rma_gate` — an active return blocks creating an exchange (and a second return), and vice versa. Same helper on store and guest create routes. Gate uses a partial unique index on `order_id` and atomic stale reclaim (`SELECT … FOR UPDATE`). Unlock on cancel/reject or when return status is actually `received`.
324
325
  - **Return Orders Admin Panel**: Admin UI and API for return orders (list, filter, search, detail, reject); returns are stored in the Medusa ORDER module and created via `createAndCompleteReturnOrderWorkflow`
325
326
  - **Exchanges Admin Panel**: Admin UI and API for exchanges (list, detail, reject, cancel); exchanges are ORDER module order changes (type `exchange`) created by customers via Medusa core workflows (`beginExchangeOrderWorkflow`, etc.)
326
327
  - **Medusa workflows**: Customer returns use `createAndCompleteReturnOrderWorkflow`; customer exchanges use `beginExchangeOrderWorkflow`, `orderExchangeRequestItemReturnWorkflow`, `orderExchangeAddNewItemWorkflow`, `confirmExchangeRequestWorkflow`, and `cancelBeginOrderExchangeWorkflow`. Admin actions use `confirmReturnReceiveWorkflow`, `cancelReturnWorkflow`, and `cancelBeginOrderExchangeWorkflow`.
@@ -334,8 +335,8 @@ This plugin is compatible with Medusa v2 (e.g. `@medusajs/framework` and `@medus
334
335
  | **Store (authenticated)** | `/store/orders/cancel|reorder/:id`, `/store/returns`, `/store/swaps`, `/store/payment-details`, `/store/refund-payment-mapping/:return_id`, `/store/orders/:order_id/refund-payment-mappings` |
335
336
  | **Store (guest)** | `/store/otp/request|verify`, `/store/guest-orders`, `/store/guest-orders/:id/returns|cancel|reorder|swaps` |
336
337
  | **Admin** | `/admin/returns`, `/admin/swaps`, `/admin/refund-payment-mapping/:return_id`, `/admin/refund-payment-mapping/:return_id/mark-refunded`, `/admin/orders/:order_id/refund-context` |
337
- | **Modules** | `payment_detail`, `refund_payment_mapping`; returns/exchanges use Medusa ORDER module |
338
- | **Subscribers** | Order confirmation email, status notifications, return/swap event sync |
338
+ | **Modules** | `payment_detail`, `refund_payment_mapping`, `order_rma_gate`; returns/exchanges use Medusa ORDER module |
339
+ | **Subscribers** | Order confirmation email, status notifications, return/swap event sync, `order.return_received` RMA gate release |
339
340
 
340
341
  ### Important APIs (quick reference)
341
342
 
@@ -632,7 +633,9 @@ Status updates are tracked in the return's metadata with timestamps and admin us
632
633
 
633
634
  ### One active return or exchange per item (Medusa v2)
634
635
 
635
- Medusa v2 does **not** allow multiple active return/exchange requests for the same order item at the same time. Once an item is part of a return or a swap (exchange), that quantity is reserved until the request is completed or canceled. Duplicate requests for the same quantity will fail validation (prevents double refunds / double fulfillment).
636
+ **Active standalone return** = `status ∈ {open, requested, partially_received}` and no `exchange_id`.
637
+
638
+ Medusa v2 does **not** allow multiple active return/exchange requests for the **same order item** at the same time. Once an item is part of a return or a swap (exchange), that quantity is reserved until the request is completed or canceled. This plugin also enforces **order-level** mutual exclusion via `order_rma_gate` (see Eligibility and Validation).
636
639
 
637
640
  **To allow a new request**, the first one must be closed or canceled:
638
641
 
@@ -1449,7 +1452,13 @@ Returns are validated for:
1449
1452
  - **Default Payment Method**: Customer must have a default payment method before creating a return
1450
1453
  - **Delivery Window**: Return is blocked if more than `returnValidInDays` days have passed since `fulfillment.delivered_at`. If the fulfillment has no `delivered_at` the check is skipped. Configure in plugin options (default: 7 days)
1451
1454
  - **Exchange Window**: Exchange is blocked if more than `exchangeValidInDays` days have passed since `fulfillment.delivered_at` (default: 7 days)
1452
- - **Duplicate Prevention**: Cannot create a new return or exchange while one is already pending or requested for the same order
1455
+ - **Mutual exclusion (order-level)**: While any **active** return or exchange exists on the order, neither a second return nor a cross-type request may be created (intentional: one open RMA per order). Active return statuses are `open` | `requested` | `partially_received` (not `received` / `canceled`). Exchange-linked returns do not count as standalone refund returns. Enforced by:
1456
+ - Medusa entity allowlists (`assertNoActiveConflictingRma`)
1457
+ - `order_rma_gate` row with `UNIQUE (order_id) WHERE deleted_at IS NULL` (race-safe create)
1458
+ - Atomic stale reclaim with `SELECT … FOR UPDATE` when a gate is abandoned (default TTL `STALE_GATE_TTL_MS` = 15 minutes; tune if crash-recovery timing requires it)
1459
+ - Release on store/guest/admin cancel+reject, and on `order.return_received` only when return `status === "received"`
1460
+ - Create workflow failure releases the gate (compensation)
1461
+ - **Retry UX (v1):** a client retry of a successful create may get `400 NOT_ALLOWED` while the gate is held — check the order for an existing return/exchange before escalating support tickets
1453
1462
 
1454
1463
  ### Helper Functions
1455
1464
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "order-management",
3
- "version": "0.0.82",
3
+ "version": "0.0.83",
4
4
  "description": "A starter for Medusa plugins.",
5
5
  "author": "Medusa (https://medusajs.com)",
6
6
  "license": "MIT",
@@ -30,6 +30,8 @@
30
30
  "scripts": {
31
31
  "build": "medusa plugin:build",
32
32
  "dev": "medusa plugin:develop",
33
+ "test": "vitest run",
34
+ "test:watch": "vitest",
33
35
  "prepublishOnly": "medusa plugin:build"
34
36
  },
35
37
  "devDependencies": {
@@ -46,7 +48,7 @@
46
48
  "@types/react": "^18.3.2",
47
49
  "@types/react-dom": "^18.2.25",
48
50
  "jsonwebtoken": "^9.0.3",
49
- "medusa-plugin-dynamic-config": "^0.0.36",
51
+ "medusa-plugin-dynamic-config": "0.0.36",
50
52
  "prop-types": "^15.8.1",
51
53
  "react": "^18.2.0",
52
54
  "react-dom": "^18.2.0",