@endora-commerce/mod-returns 0.100.0
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.
- package/LICENSE +21 -0
- package/README.md +60 -0
- package/dist/admin/api/returns-client.d.ts +69 -0
- package/dist/admin/api/returns-client.d.ts.map +1 -0
- package/dist/admin/api/returns-client.js +36 -0
- package/dist/admin/api/returns-client.js.map +1 -0
- package/dist/admin/index.d.ts +37 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +57 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/ReturnDeliveryMethodsPage.d.ts +5 -0
- package/dist/admin/pages/ReturnDeliveryMethodsPage.d.ts.map +1 -0
- package/dist/admin/pages/ReturnDeliveryMethodsPage.js +44 -0
- package/dist/admin/pages/ReturnDeliveryMethodsPage.js.map +1 -0
- package/dist/admin/pages/ReturnDetail.d.ts +5 -0
- package/dist/admin/pages/ReturnDetail.d.ts.map +1 -0
- package/dist/admin/pages/ReturnDetail.js +115 -0
- package/dist/admin/pages/ReturnDetail.js.map +1 -0
- package/dist/admin/pages/ReturnReasonsPage.d.ts +5 -0
- package/dist/admin/pages/ReturnReasonsPage.d.ts.map +1 -0
- package/dist/admin/pages/ReturnReasonsPage.js +44 -0
- package/dist/admin/pages/ReturnReasonsPage.js.map +1 -0
- package/dist/admin/pages/ReturnStatusesConfigPage.d.ts +5 -0
- package/dist/admin/pages/ReturnStatusesConfigPage.d.ts.map +1 -0
- package/dist/admin/pages/ReturnStatusesConfigPage.js +71 -0
- package/dist/admin/pages/ReturnStatusesConfigPage.js.map +1 -0
- package/dist/admin/pages/ReturnsList.d.ts +5 -0
- package/dist/admin/pages/ReturnsList.d.ts.map +1 -0
- package/dist/admin/pages/ReturnsList.js +189 -0
- package/dist/admin/pages/ReturnsList.js.map +1 -0
- package/dist/backend/domain/default-reasons.d.ts +14 -0
- package/dist/backend/domain/default-reasons.d.ts.map +1 -0
- package/dist/backend/domain/default-reasons.js +9 -0
- package/dist/backend/domain/default-reasons.js.map +1 -0
- package/dist/backend/domain/free-return-window.d.ts +2 -0
- package/dist/backend/domain/free-return-window.d.ts.map +1 -0
- package/dist/backend/domain/free-return-window.js +19 -0
- package/dist/backend/domain/free-return-window.js.map +1 -0
- package/dist/backend/domain/refund-math.d.ts +19 -0
- package/dist/backend/domain/refund-math.d.ts.map +1 -0
- package/dist/backend/domain/refund-math.js +27 -0
- package/dist/backend/domain/refund-math.js.map +1 -0
- package/dist/backend/domain/return-status-graph.d.ts +91 -0
- package/dist/backend/domain/return-status-graph.d.ts.map +1 -0
- package/dist/backend/domain/return-status-graph.js +143 -0
- package/dist/backend/domain/return-status-graph.js.map +1 -0
- package/dist/backend/email-templates/return-authorized.d.ts +11 -0
- package/dist/backend/email-templates/return-authorized.d.ts.map +1 -0
- package/dist/backend/email-templates/return-authorized.js +18 -0
- package/dist/backend/email-templates/return-authorized.js.map +1 -0
- package/dist/backend/email-templates/return-rejected.d.ts +11 -0
- package/dist/backend/email-templates/return-rejected.d.ts.map +1 -0
- package/dist/backend/email-templates/return-rejected.js +20 -0
- package/dist/backend/email-templates/return-rejected.js.map +1 -0
- package/dist/backend/email-templates/transactional-defaults.d.ts +11 -0
- package/dist/backend/email-templates/transactional-defaults.d.ts.map +1 -0
- package/dist/backend/email-templates/transactional-defaults.js +64 -0
- package/dist/backend/email-templates/transactional-defaults.js.map +1 -0
- package/dist/backend/entities/refund.entity.d.ts +41 -0
- package/dist/backend/entities/refund.entity.d.ts.map +1 -0
- package/dist/backend/entities/refund.entity.js +122 -0
- package/dist/backend/entities/refund.entity.js.map +1 -0
- package/dist/backend/entities/return-case-attachment.entity.d.ts +16 -0
- package/dist/backend/entities/return-case-attachment.entity.d.ts.map +1 -0
- package/dist/backend/entities/return-case-attachment.entity.js +53 -0
- package/dist/backend/entities/return-case-attachment.entity.js.map +1 -0
- package/dist/backend/entities/return-case-comment.entity.d.ts +20 -0
- package/dist/backend/entities/return-case-comment.entity.d.ts.map +1 -0
- package/dist/backend/entities/return-case-comment.entity.js +70 -0
- package/dist/backend/entities/return-case-comment.entity.js.map +1 -0
- package/dist/backend/entities/return-case-item.entity.d.ts +25 -0
- package/dist/backend/entities/return-case-item.entity.d.ts.map +1 -0
- package/dist/backend/entities/return-case-item.entity.js +87 -0
- package/dist/backend/entities/return-case-item.entity.js.map +1 -0
- package/dist/backend/entities/return-case.entity.d.ts +41 -0
- package/dist/backend/entities/return-case.entity.d.ts.map +1 -0
- package/dist/backend/entities/return-case.entity.js +155 -0
- package/dist/backend/entities/return-case.entity.js.map +1 -0
- package/dist/backend/entities/return-delivery-method.entity.d.ts +18 -0
- package/dist/backend/entities/return-delivery-method.entity.d.ts.map +1 -0
- package/dist/backend/entities/return-delivery-method.entity.js +63 -0
- package/dist/backend/entities/return-delivery-method.entity.js.map +1 -0
- package/dist/backend/entities/return-list-saved-view.entity.d.ts +24 -0
- package/dist/backend/entities/return-list-saved-view.entity.d.ts.map +1 -0
- package/dist/backend/entities/return-list-saved-view.entity.js +74 -0
- package/dist/backend/entities/return-list-saved-view.entity.js.map +1 -0
- package/dist/backend/entities/return-reason.entity.d.ts +21 -0
- package/dist/backend/entities/return-reason.entity.d.ts.map +1 -0
- package/dist/backend/entities/return-reason.entity.js +65 -0
- package/dist/backend/entities/return-reason.entity.js.map +1 -0
- package/dist/backend/entities/return-shipment.entity.d.ts +22 -0
- package/dist/backend/entities/return-shipment.entity.d.ts.map +1 -0
- package/dist/backend/entities/return-shipment.entity.js +71 -0
- package/dist/backend/entities/return-shipment.entity.js.map +1 -0
- package/dist/backend/entities/return-status-transition.entity.d.ts +17 -0
- package/dist/backend/entities/return-status-transition.entity.d.ts.map +1 -0
- package/dist/backend/entities/return-status-transition.entity.js +55 -0
- package/dist/backend/entities/return-status-transition.entity.js.map +1 -0
- package/dist/backend/entities/return-status.entity.d.ts +29 -0
- package/dist/backend/entities/return-status.entity.d.ts.map +1 -0
- package/dist/backend/entities/return-status.entity.js +90 -0
- package/dist/backend/entities/return-status.entity.js.map +1 -0
- package/dist/backend/events/return-status-events.d.ts +44 -0
- package/dist/backend/events/return-status-events.d.ts.map +1 -0
- package/dist/backend/events/return-status-events.js +50 -0
- package/dist/backend/events/return-status-events.js.map +1 -0
- package/dist/backend/index.d.ts +106 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +116 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/plugin.d.ts +33 -0
- package/dist/backend/plugin.d.ts.map +1 -0
- package/dist/backend/plugin.js +124 -0
- package/dist/backend/plugin.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +38 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +181 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/routes.customer.d.ts +22 -0
- package/dist/backend/routes.customer.d.ts.map +1 -0
- package/dist/backend/routes.customer.js +56 -0
- package/dist/backend/routes.customer.js.map +1 -0
- package/dist/backend/services/notification-context.d.ts +47 -0
- package/dist/backend/services/notification-context.d.ts.map +1 -0
- package/dist/backend/services/notification-context.js +49 -0
- package/dist/backend/services/notification-context.js.map +1 -0
- package/dist/backend/services/return-attachment-service.d.ts +20 -0
- package/dist/backend/services/return-attachment-service.d.ts.map +1 -0
- package/dist/backend/services/return-attachment-service.js +48 -0
- package/dist/backend/services/return-attachment-service.js.map +1 -0
- package/dist/backend/services/return-authorization-service.d.ts +53 -0
- package/dist/backend/services/return-authorization-service.d.ts.map +1 -0
- package/dist/backend/services/return-authorization-service.js +72 -0
- package/dist/backend/services/return-authorization-service.js.map +1 -0
- package/dist/backend/services/return-case-service.d.ts +44 -0
- package/dist/backend/services/return-case-service.d.ts.map +1 -0
- package/dist/backend/services/return-case-service.js +260 -0
- package/dist/backend/services/return-case-service.js.map +1 -0
- package/dist/backend/services/return-comment-service.d.ts +43 -0
- package/dist/backend/services/return-comment-service.d.ts.map +1 -0
- package/dist/backend/services/return-comment-service.js +109 -0
- package/dist/backend/services/return-comment-service.js.map +1 -0
- package/dist/backend/services/return-delivery-method-service.d.ts +44 -0
- package/dist/backend/services/return-delivery-method-service.d.ts.map +1 -0
- package/dist/backend/services/return-delivery-method-service.js +114 -0
- package/dist/backend/services/return-delivery-method-service.js.map +1 -0
- package/dist/backend/services/return-email-notifier.d.ts +89 -0
- package/dist/backend/services/return-email-notifier.d.ts.map +1 -0
- package/dist/backend/services/return-email-notifier.js +128 -0
- package/dist/backend/services/return-email-notifier.js.map +1 -0
- package/dist/backend/services/return-export-service.d.ts +16 -0
- package/dist/backend/services/return-export-service.d.ts.map +1 -0
- package/dist/backend/services/return-export-service.js +29 -0
- package/dist/backend/services/return-export-service.js.map +1 -0
- package/dist/backend/services/return-list-service.d.ts +27 -0
- package/dist/backend/services/return-list-service.d.ts.map +1 -0
- package/dist/backend/services/return-list-service.js +114 -0
- package/dist/backend/services/return-list-service.js.map +1 -0
- package/dist/backend/services/return-list-view-service.d.ts +36 -0
- package/dist/backend/services/return-list-view-service.d.ts.map +1 -0
- package/dist/backend/services/return-list-view-service.js +82 -0
- package/dist/backend/services/return-list-view-service.js.map +1 -0
- package/dist/backend/services/return-reason-service.d.ts +33 -0
- package/dist/backend/services/return-reason-service.d.ts.map +1 -0
- package/dist/backend/services/return-reason-service.js +90 -0
- package/dist/backend/services/return-reason-service.js.map +1 -0
- package/dist/backend/services/return-settlement-service.d.ts +67 -0
- package/dist/backend/services/return-settlement-service.d.ts.map +1 -0
- package/dist/backend/services/return-settlement-service.js +295 -0
- package/dist/backend/services/return-settlement-service.js.map +1 -0
- package/dist/backend/services/return-shipment-service.d.ts +40 -0
- package/dist/backend/services/return-shipment-service.d.ts.map +1 -0
- package/dist/backend/services/return-shipment-service.js +113 -0
- package/dist/backend/services/return-shipment-service.js.map +1 -0
- package/dist/backend/services/return-status-graph-service.d.ts +59 -0
- package/dist/backend/services/return-status-graph-service.d.ts.map +1 -0
- package/dist/backend/services/return-status-graph-service.js +197 -0
- package/dist/backend/services/return-status-graph-service.js.map +1 -0
- package/dist/backend/services/return-transition-service.d.ts +48 -0
- package/dist/backend/services/return-transition-service.d.ts.map +1 -0
- package/dist/backend/services/return-transition-service.js +116 -0
- package/dist/backend/services/return-transition-service.js.map +1 -0
- package/dist/backend/services/returns-seeder.d.ts +18 -0
- package/dist/backend/services/returns-seeder.d.ts.map +1 -0
- package/dist/backend/services/returns-seeder.js +63 -0
- package/dist/backend/services/returns-seeder.js.map +1 -0
- package/dist/backend/services/rma-number-generator.d.ts +35 -0
- package/dist/backend/services/rma-number-generator.d.ts.map +1 -0
- package/dist/backend/services/rma-number-generator.js +45 -0
- package/dist/backend/services/rma-number-generator.js.map +1 -0
- package/dist/manifest.d.ts +182 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +216 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260625T144227_returns_init.d.ts +15 -0
- package/dist/migrations/20260625T144227_returns_init.d.ts.map +1 -0
- package/dist/migrations/20260625T144227_returns_init.js +241 -0
- package/dist/migrations/20260625T144227_returns_init.js.map +1 -0
- package/dist/migrations/20260817T203206_returns_refund_corrective_invoice_outcome.d.ts +25 -0
- package/dist/migrations/20260817T203206_returns_refund_corrective_invoice_outcome.d.ts.map +1 -0
- package/dist/migrations/20260817T203206_returns_refund_corrective_invoice_outcome.js +46 -0
- package/dist/migrations/20260817T203206_returns_refund_corrective_invoice_outcome.js.map +1 -0
- package/dist/migrations/index.d.ts +28 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +31 -0
- package/dist/migrations/index.js.map +1 -0
- package/docs/returns.md +103 -0
- package/i18n/en.json +28 -0
- package/i18n/pl.json +28 -0
- package/package.json +101 -0
- package/tailwind.css +14 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { ERROR_CODES } from '@endora-commerce/contracts';
|
|
2
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
3
|
+
import { recordAuditFromContext } from '@endora-commerce/platform/commands';
|
|
4
|
+
import { ReturnReason } from '../entities/return-reason.entity.js';
|
|
5
|
+
/**
|
|
6
|
+
* ReturnReasonService — feature 046 (US7, FR-030).
|
|
7
|
+
*
|
|
8
|
+
* Manages the return/complaint reason list. The storefront form reads the active
|
|
9
|
+
* reasons (optionally filtered by case kind) in `weight` order.
|
|
10
|
+
*/
|
|
11
|
+
export class ReturnReasonService {
|
|
12
|
+
emFactory;
|
|
13
|
+
auditLog;
|
|
14
|
+
constructor(emFactory, auditLog) {
|
|
15
|
+
this.emFactory = emFactory;
|
|
16
|
+
this.auditLog = auditLog;
|
|
17
|
+
}
|
|
18
|
+
#audit(em, action, objectId, stateBefore, stateAfter) {
|
|
19
|
+
if (this.auditLog) {
|
|
20
|
+
recordAuditFromContext(this.auditLog, em, {
|
|
21
|
+
action,
|
|
22
|
+
objectType: 'return_reason',
|
|
23
|
+
objectId,
|
|
24
|
+
stateBefore,
|
|
25
|
+
stateAfter,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** All reasons (admin view), ordered by weight. */
|
|
30
|
+
async listAll() {
|
|
31
|
+
const em = this.emFactory();
|
|
32
|
+
const rows = await em.find(ReturnReason, {}, { orderBy: { weight: 'asc' } });
|
|
33
|
+
return rows.map(toDto);
|
|
34
|
+
}
|
|
35
|
+
/** Active reasons for a case kind (storefront), ordered by weight. */
|
|
36
|
+
async listActive(kind) {
|
|
37
|
+
const em = this.emFactory();
|
|
38
|
+
const rows = await em.find(ReturnReason, { isActive: true }, { orderBy: { weight: 'asc' } });
|
|
39
|
+
const applicable = kind
|
|
40
|
+
? rows.filter((r) => r.appliesTo === kind || r.appliesTo === 'both')
|
|
41
|
+
: rows;
|
|
42
|
+
return applicable.map(toDto);
|
|
43
|
+
}
|
|
44
|
+
async create(input) {
|
|
45
|
+
const em = this.emFactory();
|
|
46
|
+
const row = em.create(ReturnReason, {
|
|
47
|
+
label: input.label,
|
|
48
|
+
appliesTo: input.appliesTo,
|
|
49
|
+
isActive: input.isActive ?? true,
|
|
50
|
+
weight: input.weight ?? 100,
|
|
51
|
+
});
|
|
52
|
+
em.persist(row);
|
|
53
|
+
this.#audit(em, 'return_reason.create', row.id, null, { appliesTo: row.appliesTo, isActive: row.isActive });
|
|
54
|
+
await em.flush();
|
|
55
|
+
return toDto(row);
|
|
56
|
+
}
|
|
57
|
+
async update(id, patch) {
|
|
58
|
+
const em = this.emFactory();
|
|
59
|
+
const row = await em.findOne(ReturnReason, { id });
|
|
60
|
+
if (!row)
|
|
61
|
+
throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'Return reason not found.');
|
|
62
|
+
if (patch.label !== undefined)
|
|
63
|
+
row.label = patch.label;
|
|
64
|
+
if (patch.appliesTo !== undefined)
|
|
65
|
+
row.appliesTo = patch.appliesTo;
|
|
66
|
+
if (patch.isActive !== undefined)
|
|
67
|
+
row.isActive = patch.isActive;
|
|
68
|
+
if (patch.weight !== undefined)
|
|
69
|
+
row.weight = patch.weight;
|
|
70
|
+
this.#audit(em, 'return_reason.update', row.id, null, {
|
|
71
|
+
appliesTo: row.appliesTo,
|
|
72
|
+
isActive: row.isActive,
|
|
73
|
+
weight: row.weight,
|
|
74
|
+
});
|
|
75
|
+
await em.flush();
|
|
76
|
+
return toDto(row);
|
|
77
|
+
}
|
|
78
|
+
async remove(id) {
|
|
79
|
+
const em = this.emFactory();
|
|
80
|
+
const row = await em.findOne(ReturnReason, { id });
|
|
81
|
+
if (!row)
|
|
82
|
+
throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'Return reason not found.');
|
|
83
|
+
this.#audit(em, 'return_reason.delete', row.id, { appliesTo: row.appliesTo }, null);
|
|
84
|
+
await em.removeAndFlush(row);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function toDto(r) {
|
|
88
|
+
return { id: r.id, label: r.label, appliesTo: r.appliesTo, isActive: r.isActive, weight: r.weight };
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=return-reason-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"return-reason-service.js","sourceRoot":"","sources":["../../../src/backend/services/return-reason-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import type { CorrectiveInvoicePort, CreditTopupPort, PaymentRefundPort, SettlementPrefill, SettlementRequest, SettlementResult } from '@endora-commerce/contracts';
|
|
3
|
+
import type { EventBus } from '@endora-commerce/platform/events';
|
|
4
|
+
import type { AuditPort } from '@endora-commerce/platform/kernel';
|
|
5
|
+
import type { ReturnTransitionService } from './return-transition-service.js';
|
|
6
|
+
import type { ReturnStatusGraphService } from './return-status-graph-service.js';
|
|
7
|
+
export interface ReturnSettlementServiceDeps {
|
|
8
|
+
emFactory: () => EntityManager;
|
|
9
|
+
events: EventBus;
|
|
10
|
+
transitions: ReturnTransitionService;
|
|
11
|
+
graphService: ReturnStatusGraphService;
|
|
12
|
+
paymentRefund: PaymentRefundPort;
|
|
13
|
+
correctiveInvoice: CorrectiveInvoicePort;
|
|
14
|
+
creditTopup: CreditTopupPort;
|
|
15
|
+
auditLog?: AuditPort;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* ReturnSettlementService — feature 046 (US5).
|
|
19
|
+
*
|
|
20
|
+
* Validates per-line refund amounts (never exceeding the paid amount), advances
|
|
21
|
+
* the case to `resolved`, and orchestrates the financial side-effects through
|
|
22
|
+
* the documented ports: money refund (payments), store credit (credit_limits),
|
|
23
|
+
* and the corrective invoice (invoices). Replacement/repair resolutions move no
|
|
24
|
+
* money.
|
|
25
|
+
*
|
|
26
|
+
* **The ordering law (D-91, issue #157). Every external effect is attempted
|
|
27
|
+
* before any state is written, and the case reaches `resolved` in the same unit
|
|
28
|
+
* of work as the `Refund` row that records what happened.** In order:
|
|
29
|
+
*
|
|
30
|
+
* 1. **Validate** — the graph transition, the refund method, the per-line
|
|
31
|
+
* caps. Nothing is assigned and nothing is flushed.
|
|
32
|
+
* 2. **Attempt every external effect: money, then document.** The gateway
|
|
33
|
+
* refund or the store credit, then the corrective invoice. Outcomes are
|
|
34
|
+
* held in memory. A refusal here — a `ModuleDisabledError` from a
|
|
35
|
+
* switched-off `payments`, `credit_limits` or `invoices`, a gateway
|
|
36
|
+
* rejection — propagates, and because nothing has been written the case is
|
|
37
|
+
* still `received` and the whole settlement is retryable.
|
|
38
|
+
* 3. **Build the `Refund` row from those outcomes**, including its
|
|
39
|
+
* `settlementState` and its provider details.
|
|
40
|
+
* 4. **One unit of work** — the item amounts, the `Refund` row, the audit
|
|
41
|
+
* entry and the transition to `resolved`, in the transition's single flush.
|
|
42
|
+
* Either the case is resolved with a complete record of what happened, or
|
|
43
|
+
* it is untouched.
|
|
44
|
+
* 5. **After commit** — the settled event.
|
|
45
|
+
*
|
|
46
|
+
* The gateway arm has followed this since feature 046 ("a PSP rejection must
|
|
47
|
+
* not leave the RMA looking successfully settled"); the `credit` and
|
|
48
|
+
* corrective-invoice arms called their ports *after* the case was resolved and
|
|
49
|
+
* flushed, so a refusal arrived too late to prevent anything and left "case
|
|
50
|
+
* resolved, refund row absent, money possibly already moved" behind. That is a
|
|
51
|
+
* fact about the relationship between an irreversible effect and a state
|
|
52
|
+
* change, not a fact about PSPs — so it holds for a store credit and for a VAT
|
|
53
|
+
* document, and any arm added later follows it too.
|
|
54
|
+
*
|
|
55
|
+
* The retry the law makes reachable is what makes the two non-gateway effects
|
|
56
|
+
* take a key: `creditFromReturn` applies once per return case and
|
|
57
|
+
* `createCorrection` takes `idempotencyKey: rc.id`, the same key
|
|
58
|
+
* `paymentRefund.refund` has always taken.
|
|
59
|
+
*/
|
|
60
|
+
export declare class ReturnSettlementService {
|
|
61
|
+
private readonly deps;
|
|
62
|
+
constructor(deps: ReturnSettlementServiceDeps);
|
|
63
|
+
getPrefill(id: string): Promise<SettlementPrefill>;
|
|
64
|
+
settle(id: string, adminUserId: string, input: SettlementRequest): Promise<SettlementResult>;
|
|
65
|
+
private emitSettled;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=return-settlement-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"return-settlement-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/return-settlement-service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,KAAK,EACV,qBAAqB,EACrB,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EACjB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AACjE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAOlE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAC9E,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AAEjF,MAAM,WAAW,2BAA2B;IAC1C,SAAS,EAAE,MAAM,aAAa,CAAC;IAC/B,MAAM,EAAE,QAAQ,CAAC;IACjB,WAAW,EAAE,uBAAuB,CAAC;IACrC,YAAY,EAAE,wBAAwB,CAAC;IACvC,aAAa,EAAE,iBAAiB,CAAC;IACjC,iBAAiB,EAAE,qBAAqB,CAAC;IACzC,WAAW,EAAE,eAAe,CAAC;IAC7B,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,qBAAa,uBAAuB;IACtB,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,2BAA2B;IAExD,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAkBlD,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA8OlG,OAAO,CAAC,WAAW;CAapB"}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { ERROR_CODES } from '@endora-commerce/contracts';
|
|
3
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
4
|
+
import { ReturnCase } from '../entities/return-case.entity.js';
|
|
5
|
+
import { ReturnCaseItem } from '../entities/return-case-item.entity.js';
|
|
6
|
+
import { Refund } from '../entities/refund.entity.js';
|
|
7
|
+
import { RETURN_STATUS_RESOLVED } from '../domain/return-status-graph.js';
|
|
8
|
+
import { exceedsCap, sumApproved } from '../domain/refund-math.js';
|
|
9
|
+
/**
|
|
10
|
+
* ReturnSettlementService — feature 046 (US5).
|
|
11
|
+
*
|
|
12
|
+
* Validates per-line refund amounts (never exceeding the paid amount), advances
|
|
13
|
+
* the case to `resolved`, and orchestrates the financial side-effects through
|
|
14
|
+
* the documented ports: money refund (payments), store credit (credit_limits),
|
|
15
|
+
* and the corrective invoice (invoices). Replacement/repair resolutions move no
|
|
16
|
+
* money.
|
|
17
|
+
*
|
|
18
|
+
* **The ordering law (D-91, issue #157). Every external effect is attempted
|
|
19
|
+
* before any state is written, and the case reaches `resolved` in the same unit
|
|
20
|
+
* of work as the `Refund` row that records what happened.** In order:
|
|
21
|
+
*
|
|
22
|
+
* 1. **Validate** — the graph transition, the refund method, the per-line
|
|
23
|
+
* caps. Nothing is assigned and nothing is flushed.
|
|
24
|
+
* 2. **Attempt every external effect: money, then document.** The gateway
|
|
25
|
+
* refund or the store credit, then the corrective invoice. Outcomes are
|
|
26
|
+
* held in memory. A refusal here — a `ModuleDisabledError` from a
|
|
27
|
+
* switched-off `payments`, `credit_limits` or `invoices`, a gateway
|
|
28
|
+
* rejection — propagates, and because nothing has been written the case is
|
|
29
|
+
* still `received` and the whole settlement is retryable.
|
|
30
|
+
* 3. **Build the `Refund` row from those outcomes**, including its
|
|
31
|
+
* `settlementState` and its provider details.
|
|
32
|
+
* 4. **One unit of work** — the item amounts, the `Refund` row, the audit
|
|
33
|
+
* entry and the transition to `resolved`, in the transition's single flush.
|
|
34
|
+
* Either the case is resolved with a complete record of what happened, or
|
|
35
|
+
* it is untouched.
|
|
36
|
+
* 5. **After commit** — the settled event.
|
|
37
|
+
*
|
|
38
|
+
* The gateway arm has followed this since feature 046 ("a PSP rejection must
|
|
39
|
+
* not leave the RMA looking successfully settled"); the `credit` and
|
|
40
|
+
* corrective-invoice arms called their ports *after* the case was resolved and
|
|
41
|
+
* flushed, so a refusal arrived too late to prevent anything and left "case
|
|
42
|
+
* resolved, refund row absent, money possibly already moved" behind. That is a
|
|
43
|
+
* fact about the relationship between an irreversible effect and a state
|
|
44
|
+
* change, not a fact about PSPs — so it holds for a store credit and for a VAT
|
|
45
|
+
* document, and any arm added later follows it too.
|
|
46
|
+
*
|
|
47
|
+
* The retry the law makes reachable is what makes the two non-gateway effects
|
|
48
|
+
* take a key: `creditFromReturn` applies once per return case and
|
|
49
|
+
* `createCorrection` takes `idempotencyKey: rc.id`, the same key
|
|
50
|
+
* `paymentRefund.refund` has always taken.
|
|
51
|
+
*/
|
|
52
|
+
export class ReturnSettlementService {
|
|
53
|
+
deps;
|
|
54
|
+
constructor(deps) {
|
|
55
|
+
this.deps = deps;
|
|
56
|
+
}
|
|
57
|
+
async getPrefill(id) {
|
|
58
|
+
const em = this.deps.emFactory();
|
|
59
|
+
const rc = await em.findOne(ReturnCase, { id });
|
|
60
|
+
if (!rc)
|
|
61
|
+
throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'Return case not found.');
|
|
62
|
+
const items = await em.find(ReturnCaseItem, { returnCaseId: id });
|
|
63
|
+
return {
|
|
64
|
+
currency: rc.currency,
|
|
65
|
+
resolutionOptions: ['refund', 'credit', 'replacement', 'repair'],
|
|
66
|
+
items: items.map((it) => ({
|
|
67
|
+
returnCaseItemId: it.id,
|
|
68
|
+
productName: it.productName,
|
|
69
|
+
quantity: it.quantity,
|
|
70
|
+
defaultRefundAmount: Number(it.defaultRefundAmount),
|
|
71
|
+
approvedRefundAmount: Number(it.approvedRefundAmount),
|
|
72
|
+
})),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
async settle(id, adminUserId, input) {
|
|
76
|
+
const em = this.deps.emFactory();
|
|
77
|
+
const rc = await em.findOne(ReturnCase, { id });
|
|
78
|
+
if (!rc)
|
|
79
|
+
throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'Return case not found.');
|
|
80
|
+
// Fail fast if the case cannot reach `resolved` before mutating anything.
|
|
81
|
+
const graph = await this.deps.graphService.loadGraph();
|
|
82
|
+
if (!graph.canTransition(rc.statusCode, RETURN_STATUS_RESOLVED)) {
|
|
83
|
+
throw new HttpError(409, ERROR_CODES.INVALID_TRANSITION, `Cannot settle a case in status "${rc.statusCode}".`);
|
|
84
|
+
}
|
|
85
|
+
if (input.resolutionType === 'refund' && !input.refundPaymentMethodId) {
|
|
86
|
+
throw new HttpError(422, ERROR_CODES.VALIDATION_FAILED, 'A refund payment method is required.', {
|
|
87
|
+
code: 'payment_method_required',
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
// Step 1 — validate. The approved amounts are computed into a map and
|
|
91
|
+
// **not** assigned: an assignment on a managed entity is a write the next
|
|
92
|
+
// flush carries, and under the ordering law nothing is written until step 4.
|
|
93
|
+
const items = await em.find(ReturnCaseItem, { returnCaseId: id });
|
|
94
|
+
const byId = new Map(items.map((it) => [it.id, it]));
|
|
95
|
+
const approvedByItemId = new Map();
|
|
96
|
+
const perLine = [];
|
|
97
|
+
for (const line of input.lines) {
|
|
98
|
+
const item = byId.get(line.returnCaseItemId);
|
|
99
|
+
if (!item) {
|
|
100
|
+
throw new HttpError(422, ERROR_CODES.VALIDATION_FAILED, 'Unknown return line.', {
|
|
101
|
+
code: 'unknown_line',
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
if (exceedsCap(line.approvedRefundAmount, Number(item.defaultRefundAmount))) {
|
|
105
|
+
throw new HttpError(422, ERROR_CODES.VALIDATION_FAILED, 'Refund amount cannot exceed the amount paid for the line.', { code: 'refund_exceeds_paid' });
|
|
106
|
+
}
|
|
107
|
+
approvedByItemId.set(line.returnCaseItemId, line.approvedRefundAmount);
|
|
108
|
+
perLine.push(line.approvedRefundAmount);
|
|
109
|
+
}
|
|
110
|
+
const total = sumApproved(perLine);
|
|
111
|
+
const movesMoney = input.resolutionType === 'refund' || input.resolutionType === 'credit';
|
|
112
|
+
// Step 2 — every external effect, money first, then the document. A refusal
|
|
113
|
+
// from any of them propagates with the case still in `received` and nothing
|
|
114
|
+
// written, which is what makes the settlement retryable.
|
|
115
|
+
let gatewayOutcome = null;
|
|
116
|
+
let creditApplied = null;
|
|
117
|
+
let correction = null;
|
|
118
|
+
if (movesMoney) {
|
|
119
|
+
if (input.resolutionType === 'refund') {
|
|
120
|
+
gatewayOutcome = await this.deps.paymentRefund.refund({
|
|
121
|
+
orderId: rc.orderId,
|
|
122
|
+
amount: total,
|
|
123
|
+
currency: rc.currency,
|
|
124
|
+
...(input.refundPaymentMethodId ? { paymentMethodId: input.refundPaymentMethodId } : {}),
|
|
125
|
+
idempotencyKey: rc.id,
|
|
126
|
+
});
|
|
127
|
+
if (gatewayOutcome.state === 'failed') {
|
|
128
|
+
throw new HttpError(502, ERROR_CODES.VALIDATION_FAILED, gatewayOutcome.failureReason ?? 'Payment gateway rejected the refund.', {
|
|
129
|
+
code: 'gateway_refund_failed',
|
|
130
|
+
settlementState: gatewayOutcome.state,
|
|
131
|
+
providerDetails: gatewayOutcome.providerDetails ?? null,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
// `credit_limits` applies once per return case, so this survives the
|
|
137
|
+
// retry the law above makes reachable.
|
|
138
|
+
const credit = rc.organizationId
|
|
139
|
+
? await this.deps.creditTopup.creditFromReturn({
|
|
140
|
+
organizationId: rc.organizationId,
|
|
141
|
+
amount: total,
|
|
142
|
+
currency: rc.currency,
|
|
143
|
+
returnCaseId: rc.id,
|
|
144
|
+
})
|
|
145
|
+
: { applied: false };
|
|
146
|
+
creditApplied = credit.applied;
|
|
147
|
+
}
|
|
148
|
+
if (input.createCorrectiveInvoice ?? true) {
|
|
149
|
+
correction = await this.deps.correctiveInvoice.createCorrection({
|
|
150
|
+
orderId: rc.orderId,
|
|
151
|
+
lines: items.map((it) => ({
|
|
152
|
+
// The order item is what ties this credit to the invoice line it
|
|
153
|
+
// corrects, so the correction can mirror that line's VAT rate.
|
|
154
|
+
orderItemId: it.orderItemId,
|
|
155
|
+
productName: it.productName,
|
|
156
|
+
quantity: it.quantity,
|
|
157
|
+
// The approved amount for this settlement, which is not on the row
|
|
158
|
+
// yet: the rows are written in step 4.
|
|
159
|
+
amount: approvedByItemId.get(it.id) ?? Number(it.approvedRefundAmount),
|
|
160
|
+
})),
|
|
161
|
+
total,
|
|
162
|
+
currency: rc.currency,
|
|
163
|
+
// One correction per return case, however often the settlement is
|
|
164
|
+
// retried. Not per order: a second partial return against the same
|
|
165
|
+
// order is a different case and gets its own document.
|
|
166
|
+
idempotencyKey: rc.id,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Step 3 — the outcomes, as the values the row and the response carry.
|
|
171
|
+
const result = { totalRefundAmount: total };
|
|
172
|
+
const refundFields = movesMoney
|
|
173
|
+
? {
|
|
174
|
+
returnCaseId: id,
|
|
175
|
+
resolutionType: (input.resolutionType === 'refund' ? 'refund' : 'credit'),
|
|
176
|
+
amount: total.toFixed(2),
|
|
177
|
+
currency: rc.currency,
|
|
178
|
+
paymentMethodId: input.refundPaymentMethodId ?? null,
|
|
179
|
+
settlementState: 'pending_manual',
|
|
180
|
+
externalReference: null,
|
|
181
|
+
providerDetails: null,
|
|
182
|
+
failureReason: null,
|
|
183
|
+
creditLimitTopupApplied: false,
|
|
184
|
+
correctiveInvoiceId: null,
|
|
185
|
+
// D-92 — the three-way answer on the row, so it survives a page
|
|
186
|
+
// reload. `not_requested` is the default because the caller can
|
|
187
|
+
// switch the correction off; the two other values are set below,
|
|
188
|
+
// inside the branch that asked for one.
|
|
189
|
+
correctiveInvoiceOutcome: 'not_requested',
|
|
190
|
+
}
|
|
191
|
+
: null;
|
|
192
|
+
if (refundFields) {
|
|
193
|
+
if (gatewayOutcome) {
|
|
194
|
+
refundFields.settlementState = gatewayOutcome.state;
|
|
195
|
+
refundFields.externalReference = gatewayOutcome.externalReference ?? null;
|
|
196
|
+
refundFields.providerDetails = gatewayOutcome.providerDetails ?? null;
|
|
197
|
+
refundFields.failureReason = gatewayOutcome.failureReason ?? null;
|
|
198
|
+
result.refund = {
|
|
199
|
+
settlementState: gatewayOutcome.state,
|
|
200
|
+
externalReference: gatewayOutcome.externalReference ?? null,
|
|
201
|
+
failureReason: gatewayOutcome.failureReason ?? null,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
refundFields.settlementState = creditApplied ? 'issued' : 'pending_manual';
|
|
206
|
+
refundFields.creditLimitTopupApplied = creditApplied ?? false;
|
|
207
|
+
result.creditLimitTopupApplied = creditApplied ?? false;
|
|
208
|
+
result.refund = { settlementState: refundFields.settlementState };
|
|
209
|
+
}
|
|
210
|
+
if (correction) {
|
|
211
|
+
// An order that was never invoiced has nothing to correct (#135). The
|
|
212
|
+
// settlement still stands — the refund is recorded on the return case
|
|
213
|
+
// and on the payment record — and the result says which of the two
|
|
214
|
+
// happened, because a silent absence would read as a lost document.
|
|
215
|
+
if (correction.issued) {
|
|
216
|
+
refundFields.correctiveInvoiceId = correction.invoiceId;
|
|
217
|
+
refundFields.correctiveInvoiceOutcome = 'issued';
|
|
218
|
+
result.correctiveInvoiceId = correction.invoiceId;
|
|
219
|
+
result.correctiveInvoice = {
|
|
220
|
+
issued: true,
|
|
221
|
+
invoiceId: correction.invoiceId,
|
|
222
|
+
number: correction.number,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
refundFields.correctiveInvoiceOutcome = 'not_due';
|
|
227
|
+
result.correctiveInvoiceId = null;
|
|
228
|
+
result.correctiveInvoice = { issued: false, reason: correction.reason };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// Step 4 — one unit of work. `transitions.apply` flushes once, so the item
|
|
233
|
+
// amounts, the `Refund` row, the audit entry and the resolution commit
|
|
234
|
+
// together or not at all.
|
|
235
|
+
let refund = null;
|
|
236
|
+
await this.deps.transitions.apply(id, RETURN_STATUS_RESOLVED, { kind: 'admin', adminUserId, source: 'settlement' }, {
|
|
237
|
+
mutate: async (target, tx) => {
|
|
238
|
+
target.resolutionType = input.resolutionType;
|
|
239
|
+
target.refundPaymentMethodId = input.refundPaymentMethodId ?? null;
|
|
240
|
+
target.totalRefundAmount = total.toFixed(2);
|
|
241
|
+
target.resolvedAt = new Date();
|
|
242
|
+
for (const it of await tx.find(ReturnCaseItem, { returnCaseId: id })) {
|
|
243
|
+
const approved = approvedByItemId.get(it.id);
|
|
244
|
+
if (approved !== undefined)
|
|
245
|
+
it.approvedRefundAmount = approved.toFixed(2);
|
|
246
|
+
}
|
|
247
|
+
if (refundFields) {
|
|
248
|
+
refund = tx.create(Refund, refundFields);
|
|
249
|
+
tx.persist(refund);
|
|
250
|
+
}
|
|
251
|
+
// The audit entry is part of the write, not a best-effort call after
|
|
252
|
+
// it: it used to sit behind `catch { /* ignore audit failures */ }`,
|
|
253
|
+
// which is a comment rather than a decision (Principle XIII's
|
|
254
|
+
// co-transactional clause).
|
|
255
|
+
this.deps.auditLog?.recordWithin(tx, {
|
|
256
|
+
actorAdminUserId: adminUserId,
|
|
257
|
+
action: 'return.settled',
|
|
258
|
+
objectType: 'return_case',
|
|
259
|
+
objectId: rc.id,
|
|
260
|
+
stateAfter: {
|
|
261
|
+
resolutionType: input.resolutionType,
|
|
262
|
+
amount: total.toFixed(2),
|
|
263
|
+
currency: rc.currency,
|
|
264
|
+
settlementState: refundFields?.settlementState ?? null,
|
|
265
|
+
// Says which of the three happened — issued, not due, or never
|
|
266
|
+
// asked for — so "no corrective invoice" is readable after the
|
|
267
|
+
// fact instead of being an absent field (#135).
|
|
268
|
+
correctiveInvoice: result.correctiveInvoice
|
|
269
|
+
? result.correctiveInvoice.issued
|
|
270
|
+
? 'issued'
|
|
271
|
+
: result.correctiveInvoice.reason
|
|
272
|
+
: 'not_requested',
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
// Step 5 — after commit.
|
|
278
|
+
this.emitSettled(rc, refund, total);
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
281
|
+
emitSettled(rc, refund, total) {
|
|
282
|
+
const payload = {
|
|
283
|
+
eventId: randomUUID(),
|
|
284
|
+
occurredAt: new Date().toISOString(),
|
|
285
|
+
returnCaseId: rc.id,
|
|
286
|
+
refundId: refund?.id ?? null,
|
|
287
|
+
resolutionType: rc.resolutionType,
|
|
288
|
+
amount: total,
|
|
289
|
+
currency: rc.currency,
|
|
290
|
+
settlementState: refund?.settlementState ?? null,
|
|
291
|
+
};
|
|
292
|
+
this.deps.events.emit('return.refund.settled.v1', payload);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
//# sourceMappingURL=return-settlement-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"return-settlement-service.js","sourceRoot":"","sources":["../../../src/backend/services/return-settlement-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import type { ReturnShipmentDto } from '@endora-commerce/contracts';
|
|
3
|
+
import type { AuditPort } from '@endora-commerce/platform/kernel';
|
|
4
|
+
import type { ReturnTransitionService } from './return-transition-service.js';
|
|
5
|
+
export interface ReturnShipmentServiceDeps {
|
|
6
|
+
emFactory: () => EntityManager;
|
|
7
|
+
transitions: ReturnTransitionService;
|
|
8
|
+
/** Feature 054 — audits shipment writes co-transactionally when provided. */
|
|
9
|
+
auditLog?: AuditPort;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* ReturnShipmentService — feature 046 (US6).
|
|
13
|
+
*
|
|
14
|
+
* Records inbound return shipments (customer → shop) and replacement shipments
|
|
15
|
+
* (shop → customer) against a case. Receiving an inbound shipment drives the
|
|
16
|
+
* case `authorized → received` (FR-024).
|
|
17
|
+
*/
|
|
18
|
+
export declare class ReturnShipmentService {
|
|
19
|
+
#private;
|
|
20
|
+
private readonly deps;
|
|
21
|
+
constructor(deps: ReturnShipmentServiceDeps);
|
|
22
|
+
listForCase(caseId: string): Promise<ReturnShipmentDto[]>;
|
|
23
|
+
create(caseId: string, input: {
|
|
24
|
+
direction: 'inbound' | 'replacement';
|
|
25
|
+
deliveryMethodId?: string | undefined;
|
|
26
|
+
externalReference?: string | undefined;
|
|
27
|
+
}): Promise<ReturnShipmentDto>;
|
|
28
|
+
/**
|
|
29
|
+
* Mark an inbound shipment received and advance the case to `received`.
|
|
30
|
+
*
|
|
31
|
+
* The case read comes first, and that ordering is the point rather than a
|
|
32
|
+
* tidiness: the shipment row is flushed before the transition is attempted,
|
|
33
|
+
* so a caller that reached the transition's own refusal had already written
|
|
34
|
+
* `received` onto a case in an organization it cannot see, and been told 404.
|
|
35
|
+
* Both ids on this route are the caller's, and neither entity carries a
|
|
36
|
+
* filter.
|
|
37
|
+
*/
|
|
38
|
+
receive(caseId: string, shipmentId: string, adminUserId: string): Promise<ReturnShipmentDto>;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=return-shipment-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"return-shipment-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/return-shipment-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAGpE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAIlE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAE9E,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,MAAM,aAAa,CAAC;IAC/B,WAAW,EAAE,uBAAuB,CAAC;IACrC,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED;;;;;;GAMG;AACH,qBAAa,qBAAqB;;IACpB,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,yBAAyB;IAoBtD,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAOzD,MAAM,CACV,MAAM,EAAE,MAAM,EACd,KAAK,EAAE;QACL,SAAS,EAAE,SAAS,GAAG,aAAa,CAAC;QACrC,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QACtC,iBAAiB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;KACxC,GACA,OAAO,CAAC,iBAAiB,CAAC;IAwB7B;;;;;;;;;OASG;IACG,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC;CAyBnG"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { ERROR_CODES } from '@endora-commerce/contracts';
|
|
2
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
3
|
+
import { recordAuditFromContext } from '@endora-commerce/platform/commands';
|
|
4
|
+
import { ReturnCase } from '../entities/return-case.entity.js';
|
|
5
|
+
import { ReturnShipment } from '../entities/return-shipment.entity.js';
|
|
6
|
+
import { RETURN_STATUS_RECEIVED } from '../domain/return-status-graph.js';
|
|
7
|
+
/**
|
|
8
|
+
* ReturnShipmentService — feature 046 (US6).
|
|
9
|
+
*
|
|
10
|
+
* Records inbound return shipments (customer → shop) and replacement shipments
|
|
11
|
+
* (shop → customer) against a case. Receiving an inbound shipment drives the
|
|
12
|
+
* case `authorized → received` (FR-024).
|
|
13
|
+
*/
|
|
14
|
+
export class ReturnShipmentService {
|
|
15
|
+
deps;
|
|
16
|
+
constructor(deps) {
|
|
17
|
+
this.deps = deps;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the case every method below is keyed on.
|
|
21
|
+
*
|
|
22
|
+
* `ReturnShipment` is `@GlobalEntity` — no organization column, no filter —
|
|
23
|
+
* so a read keyed on `returnCaseId` answers for every case on the platform.
|
|
24
|
+
* `ReturnCase` is `@OrgScoped`, so this read *is* the tenant boundary: an
|
|
25
|
+
* assignment-scoped administrator gets nothing back and the caller stops.
|
|
26
|
+
*
|
|
27
|
+
* `create` has made this read since it was written, for its own reasons;
|
|
28
|
+
* `listForCase` and `receive` had no reason of their own to make it, which is
|
|
29
|
+
* exactly how they came to have none.
|
|
30
|
+
*/
|
|
31
|
+
async #loadCase(em, caseId) {
|
|
32
|
+
const rc = await em.findOne(ReturnCase, { id: caseId });
|
|
33
|
+
if (!rc)
|
|
34
|
+
throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'Return case not found.');
|
|
35
|
+
return rc;
|
|
36
|
+
}
|
|
37
|
+
async listForCase(caseId) {
|
|
38
|
+
const em = this.deps.emFactory();
|
|
39
|
+
await this.#loadCase(em, caseId);
|
|
40
|
+
const rows = await em.find(ReturnShipment, { returnCaseId: caseId }, { orderBy: { createdAt: 'asc' } });
|
|
41
|
+
return rows.map(toDto);
|
|
42
|
+
}
|
|
43
|
+
async create(caseId, input) {
|
|
44
|
+
const em = this.deps.emFactory();
|
|
45
|
+
await this.#loadCase(em, caseId);
|
|
46
|
+
const shipment = em.create(ReturnShipment, {
|
|
47
|
+
returnCaseId: caseId,
|
|
48
|
+
direction: input.direction,
|
|
49
|
+
deliveryMethodId: input.deliveryMethodId ?? null,
|
|
50
|
+
externalReference: input.externalReference ?? null,
|
|
51
|
+
status: 'pending',
|
|
52
|
+
});
|
|
53
|
+
em.persist(shipment);
|
|
54
|
+
if (this.deps.auditLog) {
|
|
55
|
+
recordAuditFromContext(this.deps.auditLog, em, {
|
|
56
|
+
action: 'return_shipment.create',
|
|
57
|
+
objectType: 'return_shipment',
|
|
58
|
+
objectId: shipment.id,
|
|
59
|
+
stateBefore: null,
|
|
60
|
+
stateAfter: { returnCaseId: caseId, direction: shipment.direction },
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
await em.flush();
|
|
64
|
+
return toDto(shipment);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Mark an inbound shipment received and advance the case to `received`.
|
|
68
|
+
*
|
|
69
|
+
* The case read comes first, and that ordering is the point rather than a
|
|
70
|
+
* tidiness: the shipment row is flushed before the transition is attempted,
|
|
71
|
+
* so a caller that reached the transition's own refusal had already written
|
|
72
|
+
* `received` onto a case in an organization it cannot see, and been told 404.
|
|
73
|
+
* Both ids on this route are the caller's, and neither entity carries a
|
|
74
|
+
* filter.
|
|
75
|
+
*/
|
|
76
|
+
async receive(caseId, shipmentId, adminUserId) {
|
|
77
|
+
const em = this.deps.emFactory();
|
|
78
|
+
await this.#loadCase(em, caseId);
|
|
79
|
+
const shipment = await em.findOne(ReturnShipment, { id: shipmentId, returnCaseId: caseId });
|
|
80
|
+
if (!shipment)
|
|
81
|
+
throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'Return shipment not found.');
|
|
82
|
+
shipment.status = 'received';
|
|
83
|
+
if (this.deps.auditLog) {
|
|
84
|
+
recordAuditFromContext(this.deps.auditLog, em, {
|
|
85
|
+
action: 'return_shipment.receive',
|
|
86
|
+
objectType: 'return_shipment',
|
|
87
|
+
objectId: shipment.id,
|
|
88
|
+
stateBefore: { status: 'pending' },
|
|
89
|
+
stateAfter: { status: 'received' },
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
await em.flush();
|
|
93
|
+
if (shipment.direction === 'inbound') {
|
|
94
|
+
await this.deps.transitions.apply(caseId, RETURN_STATUS_RECEIVED, {
|
|
95
|
+
kind: 'admin',
|
|
96
|
+
adminUserId,
|
|
97
|
+
source: 'shipment',
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return toDto(shipment);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function toDto(s) {
|
|
104
|
+
return {
|
|
105
|
+
id: s.id,
|
|
106
|
+
direction: s.direction,
|
|
107
|
+
deliveryMethodId: s.deliveryMethodId ?? null,
|
|
108
|
+
externalReference: s.externalReference ?? null,
|
|
109
|
+
status: s.status,
|
|
110
|
+
createdAt: s.createdAt.toISOString(),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=return-shipment-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"return-shipment-service.js","sourceRoot":"","sources":["../../../src/backend/services/return-shipment-service.ts"],"names":[],"mappings":"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"}
|