@mj-biz-apps/orders-core-entities-server 0.0.1 → 5.2.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 +183 -0
- package/README.md +141 -2
- package/dist/AccountingBridge.d.ts +65 -0
- package/dist/AccountingBridge.d.ts.map +1 -0
- package/dist/AccountingBridge.js +102 -0
- package/dist/AccountingBridge.js.map +1 -0
- package/dist/AdvanceOrderStateOperation.d.ts +80 -0
- package/dist/AdvanceOrderStateOperation.d.ts.map +1 -0
- package/dist/AdvanceOrderStateOperation.js +267 -0
- package/dist/AdvanceOrderStateOperation.js.map +1 -0
- package/dist/ApplyAccountCreditOperation.d.ts +97 -0
- package/dist/ApplyAccountCreditOperation.d.ts.map +1 -0
- package/dist/ApplyAccountCreditOperation.js +275 -0
- package/dist/ApplyAccountCreditOperation.js.map +1 -0
- package/dist/BaseDeliveryChannel.d.ts +106 -0
- package/dist/BaseDeliveryChannel.d.ts.map +1 -0
- package/dist/BaseDeliveryChannel.js +36 -0
- package/dist/BaseDeliveryChannel.js.map +1 -0
- package/dist/BasePaymentProvider.d.ts +239 -0
- package/dist/BasePaymentProvider.d.ts.map +1 -0
- package/dist/BasePaymentProvider.js +91 -0
- package/dist/BasePaymentProvider.js.map +1 -0
- package/dist/BundleBehavior.d.ts +139 -0
- package/dist/BundleBehavior.d.ts.map +1 -0
- package/dist/BundleBehavior.js +173 -0
- package/dist/BundleBehavior.js.map +1 -0
- package/dist/BundleEngine.d.ts +60 -0
- package/dist/BundleEngine.d.ts.map +1 -0
- package/dist/BundleEngine.js +197 -0
- package/dist/BundleEngine.js.map +1 -0
- package/dist/CancelSubscriptionOperation.d.ts +98 -0
- package/dist/CancelSubscriptionOperation.d.ts.map +1 -0
- package/dist/CancelSubscriptionOperation.js +317 -0
- package/dist/CancelSubscriptionOperation.js.map +1 -0
- package/dist/CapturePaymentOperation.d.ts +62 -0
- package/dist/CapturePaymentOperation.d.ts.map +1 -0
- package/dist/CapturePaymentOperation.js +463 -0
- package/dist/CapturePaymentOperation.js.map +1 -0
- package/dist/CheckEntitlementOperation.d.ts +27 -0
- package/dist/CheckEntitlementOperation.d.ts.map +1 -0
- package/dist/CheckEntitlementOperation.js +45 -0
- package/dist/CheckEntitlementOperation.js.map +1 -0
- package/dist/CheckoutSessionService.d.ts +258 -0
- package/dist/CheckoutSessionService.d.ts.map +1 -0
- package/dist/CheckoutSessionService.js +1557 -0
- package/dist/CheckoutSessionService.js.map +1 -0
- package/dist/DeliveryBehavior.d.ts +121 -0
- package/dist/DeliveryBehavior.d.ts.map +1 -0
- package/dist/DeliveryBehavior.js +145 -0
- package/dist/DeliveryBehavior.js.map +1 -0
- package/dist/DeliveryRecipientResolver.d.ts +38 -0
- package/dist/DeliveryRecipientResolver.d.ts.map +1 -0
- package/dist/DeliveryRecipientResolver.js +93 -0
- package/dist/DeliveryRecipientResolver.js.map +1 -0
- package/dist/DeliveryResolver.d.ts +14 -0
- package/dist/DeliveryResolver.d.ts.map +1 -0
- package/dist/DeliveryResolver.js +45 -0
- package/dist/DeliveryResolver.js.map +1 -0
- package/dist/EmailDeliveryChannel.d.ts +31 -0
- package/dist/EmailDeliveryChannel.d.ts.map +1 -0
- package/dist/EmailDeliveryChannel.js +158 -0
- package/dist/EmailDeliveryChannel.js.map +1 -0
- package/dist/EntitlementBehavior.d.ts +235 -0
- package/dist/EntitlementBehavior.d.ts.map +1 -0
- package/dist/EntitlementBehavior.js +330 -0
- package/dist/EntitlementBehavior.js.map +1 -0
- package/dist/EntitlementEngine.d.ts +97 -0
- package/dist/EntitlementEngine.d.ts.map +1 -0
- package/dist/EntitlementEngine.js +338 -0
- package/dist/EntitlementEngine.js.map +1 -0
- package/dist/EntitlementGrantClaimDriver.d.ts +42 -0
- package/dist/EntitlementGrantClaimDriver.d.ts.map +1 -0
- package/dist/EntitlementGrantClaimDriver.js +158 -0
- package/dist/EntitlementGrantClaimDriver.js.map +1 -0
- package/dist/EntitlementRead.d.ts +82 -0
- package/dist/EntitlementRead.d.ts.map +1 -0
- package/dist/EntitlementRead.js +368 -0
- package/dist/EntitlementRead.js.map +1 -0
- package/dist/FulfillOrderLinesOperation.d.ts +34 -0
- package/dist/FulfillOrderLinesOperation.d.ts.map +1 -0
- package/dist/FulfillOrderLinesOperation.js +208 -0
- package/dist/FulfillOrderLinesOperation.js.map +1 -0
- package/dist/FulfillmentBehavior.d.ts +101 -0
- package/dist/FulfillmentBehavior.d.ts.map +1 -0
- package/dist/FulfillmentBehavior.js +145 -0
- package/dist/FulfillmentBehavior.js.map +1 -0
- package/dist/GLAccountResolver.d.ts +123 -0
- package/dist/GLAccountResolver.d.ts.map +1 -0
- package/dist/GLAccountResolver.js +175 -0
- package/dist/GLAccountResolver.js.map +1 -0
- package/dist/GetFulfillmentQueueOperation.d.ts +35 -0
- package/dist/GetFulfillmentQueueOperation.d.ts.map +1 -0
- package/dist/GetFulfillmentQueueOperation.js +221 -0
- package/dist/GetFulfillmentQueueOperation.js.map +1 -0
- package/dist/GetOverdueWorklistOperation.d.ts +41 -0
- package/dist/GetOverdueWorklistOperation.d.ts.map +1 -0
- package/dist/GetOverdueWorklistOperation.js +210 -0
- package/dist/GetOverdueWorklistOperation.js.map +1 -0
- package/dist/GiftCardBehavior.d.ts +97 -0
- package/dist/GiftCardBehavior.d.ts.map +1 -0
- package/dist/GiftCardBehavior.js +121 -0
- package/dist/GiftCardBehavior.js.map +1 -0
- package/dist/GiftCardEngine.d.ts +59 -0
- package/dist/GiftCardEngine.d.ts.map +1 -0
- package/dist/GiftCardEngine.js +197 -0
- package/dist/GiftCardEngine.js.map +1 -0
- package/dist/GuestOrderClaimDriver.d.ts +36 -0
- package/dist/GuestOrderClaimDriver.d.ts.map +1 -0
- package/dist/GuestOrderClaimDriver.js +161 -0
- package/dist/GuestOrderClaimDriver.js.map +1 -0
- package/dist/InvoiceBehavior.d.ts +394 -0
- package/dist/InvoiceBehavior.d.ts.map +1 -0
- package/dist/InvoiceBehavior.js +496 -0
- package/dist/InvoiceBehavior.js.map +1 -0
- package/dist/InvoiceBuilder.d.ts +48 -0
- package/dist/InvoiceBuilder.d.ts.map +1 -0
- package/dist/InvoiceBuilder.js +352 -0
- package/dist/InvoiceBuilder.js.map +1 -0
- package/dist/InvoiceDisplay.d.ts +96 -0
- package/dist/InvoiceDisplay.d.ts.map +1 -0
- package/dist/InvoiceDisplay.js +121 -0
- package/dist/InvoiceDisplay.js.map +1 -0
- package/dist/ListEntitlementsOperation.d.ts +21 -0
- package/dist/ListEntitlementsOperation.d.ts.map +1 -0
- package/dist/ListEntitlementsOperation.js +39 -0
- package/dist/ListEntitlementsOperation.js.map +1 -0
- package/dist/ManualPaymentProvider.d.ts +19 -0
- package/dist/ManualPaymentProvider.d.ts.map +1 -0
- package/dist/ManualPaymentProvider.js +92 -0
- package/dist/ManualPaymentProvider.js.map +1 -0
- package/dist/OrderEntityServer.d.ts +542 -0
- package/dist/OrderEntityServer.d.ts.map +1 -0
- package/dist/OrderEntityServer.js +2100 -0
- package/dist/OrderEntityServer.js.map +1 -0
- package/dist/OrderJournalEntryFactory.d.ts +140 -0
- package/dist/OrderJournalEntryFactory.d.ts.map +1 -0
- package/dist/OrderJournalEntryFactory.js +466 -0
- package/dist/OrderJournalEntryFactory.js.map +1 -0
- package/dist/OrderLineEntityServer.d.ts +90 -0
- package/dist/OrderLineEntityServer.d.ts.map +1 -0
- package/dist/OrderLineEntityServer.js +260 -0
- package/dist/OrderLineEntityServer.js.map +1 -0
- package/dist/OrdersSettings.d.ts +36 -0
- package/dist/OrdersSettings.d.ts.map +1 -0
- package/dist/OrdersSettings.js +147 -0
- package/dist/OrdersSettings.js.map +1 -0
- package/dist/PaymentAllocationFactory.d.ts +128 -0
- package/dist/PaymentAllocationFactory.d.ts.map +1 -0
- package/dist/PaymentAllocationFactory.js +235 -0
- package/dist/PaymentAllocationFactory.js.map +1 -0
- package/dist/PaymentHeaderEntityServer.d.ts +180 -0
- package/dist/PaymentHeaderEntityServer.d.ts.map +1 -0
- package/dist/PaymentHeaderEntityServer.js +656 -0
- package/dist/PaymentHeaderEntityServer.js.map +1 -0
- package/dist/PaymentIntentService.d.ts +101 -0
- package/dist/PaymentIntentService.d.ts.map +1 -0
- package/dist/PaymentIntentService.js +150 -0
- package/dist/PaymentIntentService.js.map +1 -0
- package/dist/PaymentJournalEntryFactory.d.ts +99 -0
- package/dist/PaymentJournalEntryFactory.d.ts.map +1 -0
- package/dist/PaymentJournalEntryFactory.js +127 -0
- package/dist/PaymentJournalEntryFactory.js.map +1 -0
- package/dist/PaymentLineEntityServer.d.ts +73 -0
- package/dist/PaymentLineEntityServer.d.ts.map +1 -0
- package/dist/PaymentLineEntityServer.js +303 -0
- package/dist/PaymentLineEntityServer.js.map +1 -0
- package/dist/PaymentProviderBehavior.d.ts +222 -0
- package/dist/PaymentProviderBehavior.d.ts.map +1 -0
- package/dist/PaymentProviderBehavior.js +368 -0
- package/dist/PaymentProviderBehavior.js.map +1 -0
- package/dist/PaymentProviderResolver.d.ts +84 -0
- package/dist/PaymentProviderResolver.d.ts.map +1 -0
- package/dist/PaymentProviderResolver.js +220 -0
- package/dist/PaymentProviderResolver.js.map +1 -0
- package/dist/PaymentReversalFactory.d.ts +107 -0
- package/dist/PaymentReversalFactory.d.ts.map +1 -0
- package/dist/PaymentReversalFactory.js +174 -0
- package/dist/PaymentReversalFactory.js.map +1 -0
- package/dist/PaymentSettlement.d.ts +56 -0
- package/dist/PaymentSettlement.d.ts.map +1 -0
- package/dist/PaymentSettlement.js +243 -0
- package/dist/PaymentSettlement.js.map +1 -0
- package/dist/PaymentTermsBehavior.d.ts +109 -0
- package/dist/PaymentTermsBehavior.d.ts.map +1 -0
- package/dist/PaymentTermsBehavior.js +172 -0
- package/dist/PaymentTermsBehavior.js.map +1 -0
- package/dist/PaymentWebhookHandler.d.ts +103 -0
- package/dist/PaymentWebhookHandler.d.ts.map +1 -0
- package/dist/PaymentWebhookHandler.js +246 -0
- package/dist/PaymentWebhookHandler.js.map +1 -0
- package/dist/PreviewPriceOperation.d.ts +62 -0
- package/dist/PreviewPriceOperation.d.ts.map +1 -0
- package/dist/PreviewPriceOperation.js +161 -0
- package/dist/PreviewPriceOperation.js.map +1 -0
- package/dist/PriceOrderOperation.d.ts +93 -0
- package/dist/PriceOrderOperation.d.ts.map +1 -0
- package/dist/PriceOrderOperation.js +146 -0
- package/dist/PriceOrderOperation.js.map +1 -0
- package/dist/ProductPriceEntityServer.d.ts +34 -0
- package/dist/ProductPriceEntityServer.d.ts.map +1 -0
- package/dist/ProductPriceEntityServer.js +97 -0
- package/dist/ProductPriceEntityServer.js.map +1 -0
- package/dist/RefundPaymentOperation.d.ts +61 -0
- package/dist/RefundPaymentOperation.d.ts.map +1 -0
- package/dist/RefundPaymentOperation.js +177 -0
- package/dist/RefundPaymentOperation.js.map +1 -0
- package/dist/RevenueRecognition.d.ts +76 -0
- package/dist/RevenueRecognition.d.ts.map +1 -0
- package/dist/RevenueRecognition.js +133 -0
- package/dist/RevenueRecognition.js.map +1 -0
- package/dist/ReversalBehavior.d.ts +82 -0
- package/dist/ReversalBehavior.d.ts.map +1 -0
- package/dist/ReversalBehavior.js +100 -0
- package/dist/ReversalBehavior.js.map +1 -0
- package/dist/ReversalResolver.d.ts +37 -0
- package/dist/ReversalResolver.d.ts.map +1 -0
- package/dist/ReversalResolver.js +96 -0
- package/dist/ReversalResolver.js.map +1 -0
- package/dist/SpawnRenewalsOperation.d.ts +109 -0
- package/dist/SpawnRenewalsOperation.d.ts.map +1 -0
- package/dist/SpawnRenewalsOperation.js +295 -0
- package/dist/SpawnRenewalsOperation.js.map +1 -0
- package/dist/StoredValuePaymentProvider.d.ts +52 -0
- package/dist/StoredValuePaymentProvider.d.ts.map +1 -0
- package/dist/StoredValuePaymentProvider.js +205 -0
- package/dist/StoredValuePaymentProvider.js.map +1 -0
- package/dist/StripeACHPaymentProvider.d.ts +50 -0
- package/dist/StripeACHPaymentProvider.d.ts.map +1 -0
- package/dist/StripeACHPaymentProvider.js +211 -0
- package/dist/StripeACHPaymentProvider.js.map +1 -0
- package/dist/StripePaymentProvider.d.ts +83 -0
- package/dist/StripePaymentProvider.d.ts.map +1 -0
- package/dist/StripePaymentProvider.js +442 -0
- package/dist/StripePaymentProvider.js.map +1 -0
- package/dist/SubscriptionBehavior.d.ts +197 -0
- package/dist/SubscriptionBehavior.d.ts.map +1 -0
- package/dist/SubscriptionBehavior.js +415 -0
- package/dist/SubscriptionBehavior.js.map +1 -0
- package/dist/checkoutCaptureAlert.d.ts +16 -0
- package/dist/checkoutCaptureAlert.d.ts.map +1 -0
- package/dist/checkoutCaptureAlert.js +55 -0
- package/dist/checkoutCaptureAlert.js.map +1 -0
- package/dist/checkoutCaptureRetry.d.ts +27 -0
- package/dist/checkoutCaptureRetry.d.ts.map +1 -0
- package/dist/checkoutCaptureRetry.js +50 -0
- package/dist/checkoutCaptureRetry.js.map +1 -0
- package/dist/claimDriverHelpers.d.ts +15 -0
- package/dist/claimDriverHelpers.d.ts.map +1 -0
- package/dist/claimDriverHelpers.js +34 -0
- package/dist/claimDriverHelpers.js.map +1 -0
- package/dist/entity-names.d.ts +15 -0
- package/dist/entity-names.d.ts.map +1 -0
- package/dist/entity-names.js +15 -0
- package/dist/entity-names.js.map +1 -0
- package/dist/identityClaimContracts.d.ts +118 -0
- package/dist/identityClaimContracts.d.ts.map +1 -0
- package/dist/identityClaimContracts.js +58 -0
- package/dist/identityClaimContracts.js.map +1 -0
- package/dist/index.d.ts +126 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +115 -0
- package/dist/index.js.map +1 -0
- package/dist/sql-guards.d.ts +78 -0
- package/dist/sql-guards.d.ts.map +1 -0
- package/dist/sql-guards.js +115 -0
- package/dist/sql-guards.js.map +1 -0
- package/package.json +50 -5
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* @fileoverview `Orders.AdvanceOrderState` — move a booked order up the status ladder.
|
|
9
|
+
*
|
|
10
|
+
* WHAT THIS IS FOR. Back-office entry of something that has ALREADY happened: a sale taken at a
|
|
11
|
+
* counter, a shipment that went out before anyone opened the system, a migration from whatever came
|
|
12
|
+
* before. The order needs to land in its final state without a human clicking through
|
|
13
|
+
* Confirmed → Posted → Fulfilled (D17).
|
|
14
|
+
*
|
|
15
|
+
* WHAT IT REPLACED, AND WHY IT SHRANK. This was `Orders.CreateOrderInState`, which took an
|
|
16
|
+
* `OrderDraft` — a hand-maintained mirror of the order entity — created the order by delegating to
|
|
17
|
+
* `Orders.ConfirmOrder`, and only then advanced it. Both halves of that are gone: composing and
|
|
18
|
+
* booking an order is now `order.Save()` through MJ's entity graph, which runs the identical booking
|
|
19
|
+
* walk on the server subclass. What is left is the part a save cannot do, and that part is this file.
|
|
20
|
+
*
|
|
21
|
+
* WHY THE LADDER STILL NEEDS AN OPERATION. Advancing to Posted is a plain save. Advancing to
|
|
22
|
+
* Fulfilled is not: the fulfillable LINES have to be marked first, and whether the order may move at
|
|
23
|
+
* all depends on how many could not be. That is a decision over a set of rows, taken on the server,
|
|
24
|
+
* with a caller-supplied override — a save on one header has nowhere to put it.
|
|
25
|
+
*
|
|
26
|
+
* NOTHING HERE BOOKS. Journal entries, subscriptions and entitlements are the confirm's business and
|
|
27
|
+
* have already happened by the time this runs. This operation reads the ledger back only to REPORT
|
|
28
|
+
* it, so a caller can tell "advanced an order that booked" from "advanced an order that did not".
|
|
29
|
+
*
|
|
30
|
+
* @module @mj-biz-apps/orders-core-entities-server
|
|
31
|
+
*/
|
|
32
|
+
import { BaseRemotableOperation, RunView, } from '@memberjunction/core';
|
|
33
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
34
|
+
import { IsAwaitingFulfillment } from './FulfillmentBehavior.js';
|
|
35
|
+
import { ORDER_HEADER_ENTITY } from './entity-names.js';
|
|
36
|
+
import { RequireUUID } from './sql-guards.js';
|
|
37
|
+
const ORDER_LINE_ENTITY = 'MJ_BizApps_Orders: Order Lines';
|
|
38
|
+
const PRODUCT_ENTITY = 'MJ_BizApps_Orders: Products';
|
|
39
|
+
const PRODUCT_TYPE_ENTITY = 'MJ_BizApps_Orders: Product Types';
|
|
40
|
+
const key = (id) => (id ?? '').toLowerCase();
|
|
41
|
+
const quote = (ids) => [...new Set(ids.map((i) => `'${i}'`))].join(',');
|
|
42
|
+
/** The status ladder, in the order an order climbs it. */
|
|
43
|
+
const LADDER = ['Draft', 'Quoted', 'Confirmed'];
|
|
44
|
+
/** Where this operation will take an order. Everything else is somebody else's job. */
|
|
45
|
+
const ACCEPTED_TARGETS = new Set(['Fulfilled']);
|
|
46
|
+
let AdvanceOrderStateOperation = class AdvanceOrderStateOperation extends BaseRemotableOperation {
|
|
47
|
+
constructor() {
|
|
48
|
+
super(...arguments);
|
|
49
|
+
this.OperationKey = 'Orders.AdvanceOrderState';
|
|
50
|
+
}
|
|
51
|
+
async InternalExecute(input, provider, user) {
|
|
52
|
+
const target = (input?.TargetStatus ?? '').trim();
|
|
53
|
+
if (!ACCEPTED_TARGETS.has(target)) {
|
|
54
|
+
return this.refuse(target, [
|
|
55
|
+
this.blocker('UNSUPPORTED_TARGET', `'${target || '(none)'}' is not a state this operation advances to. Use Fulfilled.`, target === 'Draft' || target === 'Quoted' || target === 'Confirmed'
|
|
56
|
+
? 'Those states are reached by saving the order itself — set Status and call Save(). ' +
|
|
57
|
+
'Confirmed runs the booking walk on the way through, which is where it belongs.'
|
|
58
|
+
: 'Voiding is a separate decision about an existing order, not a rung on the ladder.'),
|
|
59
|
+
]);
|
|
60
|
+
}
|
|
61
|
+
if (!input?.OrderHeaderID) {
|
|
62
|
+
return this.refuse(target, [this.blocker('NO_ORDER', 'An OrderHeaderID is required.')]);
|
|
63
|
+
}
|
|
64
|
+
// Reaches SQL filter text below.
|
|
65
|
+
RequireUUID(input.OrderHeaderID, 'OrderHeaderID');
|
|
66
|
+
const header = await provider.GetEntityObject(ORDER_HEADER_ENTITY, user);
|
|
67
|
+
if (!(await header.Load(input.OrderHeaderID))) {
|
|
68
|
+
return this.refuse(target, [
|
|
69
|
+
this.blocker('NOT_FOUND', `Order ${input.OrderHeaderID} could not be loaded.`),
|
|
70
|
+
]);
|
|
71
|
+
}
|
|
72
|
+
const orderID = header.ID;
|
|
73
|
+
const status = header.Status ?? 'Draft';
|
|
74
|
+
const transitions = [];
|
|
75
|
+
let unfulfilled = 0;
|
|
76
|
+
// AN ORDER THAT HAS NOT BOOKED CANNOT BE ADVANCED.
|
|
77
|
+
if (status !== 'Confirmed') {
|
|
78
|
+
return this.refuse(target, [
|
|
79
|
+
this.blocker('NOT_CONFIRMED', `The order is ${status}. Only a Confirmed order can be advanced.`, 'Set Status to Confirmed and save the order — that runs the booking walk. Then advance it.'),
|
|
80
|
+
], orderID, header.OrderNumber, status);
|
|
81
|
+
}
|
|
82
|
+
const rv = new RunView(provider);
|
|
83
|
+
if (header.FulfillmentStatus === 'Fulfilled') {
|
|
84
|
+
return {
|
|
85
|
+
Success: true,
|
|
86
|
+
Message: `The order is already Fulfilled.`,
|
|
87
|
+
OrderHeaderID: orderID,
|
|
88
|
+
OrderNumber: header.OrderNumber ?? null,
|
|
89
|
+
Status: 'Fulfilled',
|
|
90
|
+
RequestedStatus: target,
|
|
91
|
+
Transitions: [],
|
|
92
|
+
...(await this.readBookedEntries(orderID, provider)),
|
|
93
|
+
UnfulfilledLineCount: 0,
|
|
94
|
+
Blockers: [],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
// Mark the fulfillable lines FIRST.
|
|
98
|
+
unfulfilled = await this.fulfillLines(orderID, rv, provider, user);
|
|
99
|
+
if (unfulfilled > 0 && !input?.ForceFulfillment) {
|
|
100
|
+
transitions.push({
|
|
101
|
+
From: header.FulfillmentStatus ?? 'Pending',
|
|
102
|
+
To: 'Fulfilled',
|
|
103
|
+
Applied: false,
|
|
104
|
+
Reason: `${unfulfilled} fulfillable line(s) could not be marked Fulfilled. The order sits in ` +
|
|
105
|
+
`the fulfillment queue. Set ForceFulfillment to advance anyway.`,
|
|
106
|
+
});
|
|
107
|
+
return this.stopped(orderID, header, target, header.FulfillmentStatus ?? 'Pending', transitions, unfulfilled, provider);
|
|
108
|
+
}
|
|
109
|
+
header.FulfillmentStatus = 'Fulfilled';
|
|
110
|
+
if (input?.Reason) {
|
|
111
|
+
const existing = header.Description ?? '';
|
|
112
|
+
header.Description = existing ? `${existing} — ${input.Reason}` : input.Reason;
|
|
113
|
+
}
|
|
114
|
+
const saved = await header.Save();
|
|
115
|
+
transitions.push({
|
|
116
|
+
From: 'Pending',
|
|
117
|
+
To: 'Fulfilled',
|
|
118
|
+
Applied: saved,
|
|
119
|
+
Reason: saved && unfulfilled > 0
|
|
120
|
+
? `Forced with ${unfulfilled} line(s) still Pending.`
|
|
121
|
+
: header.LatestResult?.CompleteMessage ?? null,
|
|
122
|
+
});
|
|
123
|
+
const booked = await this.readBookedEntries(orderID, provider);
|
|
124
|
+
return {
|
|
125
|
+
Success: saved,
|
|
126
|
+
Message: saved ? undefined : `The order could not be advanced to Fulfilled: ${header.LatestResult?.CompleteMessage ?? 'Save failed'}`,
|
|
127
|
+
OrderHeaderID: orderID,
|
|
128
|
+
OrderNumber: header.OrderNumber ?? null,
|
|
129
|
+
Status: saved ? 'Fulfilled' : (header.FulfillmentStatus ?? 'Pending'),
|
|
130
|
+
RequestedStatus: target,
|
|
131
|
+
Transitions: transitions,
|
|
132
|
+
EntryCount: booked.EntryCount,
|
|
133
|
+
AllBalanced: booked.AllBalanced,
|
|
134
|
+
UnfulfilledLineCount: unfulfilled,
|
|
135
|
+
Blockers: [],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* What booked against this order's lines, read back rather than assumed.
|
|
140
|
+
*
|
|
141
|
+
* The confirm happened on a different call — possibly on a different day, for a migration — so
|
|
142
|
+
* this operation has no first-hand knowledge of it. Reporting zero entries because nobody looked
|
|
143
|
+
* is indistinguishable from an order that never booked, and this whole file exists to keep those
|
|
144
|
+
* two apart.
|
|
145
|
+
*/
|
|
146
|
+
async readBookedEntries(orderID, provider) {
|
|
147
|
+
const rows = (await provider.ExecuteSQL(`
|
|
148
|
+
SELECT je.ID AS EntryID,
|
|
149
|
+
SUM(ISNULL(jel.DebitAmount, 0)) AS D,
|
|
150
|
+
SUM(ISNULL(jel.CreditAmount, 0)) AS C
|
|
151
|
+
FROM __mj_BizAppsAccounting.JournalEntry je
|
|
152
|
+
JOIN __mj_BizAppsAccounting.JournalEntryLine jel ON jel.JournalEntryID = je.ID
|
|
153
|
+
WHERE je.LinkedRecordID IN (
|
|
154
|
+
SELECT CAST(ol.ID AS NVARCHAR(400)) FROM __mj_BizAppsOrders.OrderLine ol
|
|
155
|
+
WHERE ol.OrderHeaderID = '${orderID}')
|
|
156
|
+
GROUP BY je.ID`)) ?? [];
|
|
157
|
+
return {
|
|
158
|
+
EntryCount: rows.length,
|
|
159
|
+
AllBalanced: rows.every((r) => Math.abs(Number(r.D) - Number(r.C)) < 0.005),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/** Move the header, reporting a refusal rather than throwing — a stalled advance is still a result. */
|
|
163
|
+
async setStatus(header,
|
|
164
|
+
// Derived from the entity: `Status` is CHECK-constrained, so CodeGen owns this union and it
|
|
165
|
+
// widens with the constraint. Restating it here would freeze it at today's six values.
|
|
166
|
+
status, reason) {
|
|
167
|
+
header.Status = status;
|
|
168
|
+
if (reason) {
|
|
169
|
+
const existing = header.Description ?? '';
|
|
170
|
+
header.Description = existing ? `${existing} — ${reason}` : reason;
|
|
171
|
+
}
|
|
172
|
+
if (!(await header.Save())) {
|
|
173
|
+
return { ok: false, reason: header.LatestResult?.CompleteMessage ?? 'no reason given' };
|
|
174
|
+
}
|
|
175
|
+
return { ok: true, reason: null };
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Mark every line that is awaiting fulfilment, and report how many would not go.
|
|
179
|
+
*
|
|
180
|
+
* The three lookups are per-order rather than per-line on purpose: a migration advances orders in
|
|
181
|
+
* bulk, and a per-line product/type round trip turns a hundred-line order into three hundred
|
|
182
|
+
* queries.
|
|
183
|
+
*/
|
|
184
|
+
async fulfillLines(orderID, rv, provider, user) {
|
|
185
|
+
const lines = await rv.RunView({ EntityName: ORDER_LINE_ENTITY, ExtraFilter: `OrderHeaderID = '${orderID}'`, ResultType: 'simple' }, user);
|
|
186
|
+
const rows = lines.Results ?? [];
|
|
187
|
+
if (!rows.length)
|
|
188
|
+
return 0;
|
|
189
|
+
const products = await rv.RunView({
|
|
190
|
+
EntityName: PRODUCT_ENTITY,
|
|
191
|
+
ExtraFilter: `ID IN (${quote(rows.map((l) => l.ProductID))})`,
|
|
192
|
+
ResultType: 'simple',
|
|
193
|
+
}, user);
|
|
194
|
+
const typeIDs = [...new Set((products.Results ?? []).map((p) => p.ProductTypeID))].filter(Boolean);
|
|
195
|
+
const types = typeIDs.length
|
|
196
|
+
? await rv.RunView({ EntityName: PRODUCT_TYPE_ENTITY, ExtraFilter: `ID IN (${quote(typeIDs)})`, ResultType: 'simple' }, user)
|
|
197
|
+
: { Results: [] };
|
|
198
|
+
const requiresByType = new Map((types.Results ?? []).map((t) => [key(t.ID), !!t.RequiresFulfillment]));
|
|
199
|
+
const typeByProduct = new Map((products.Results ?? []).map((p) => [key(p.ID), key(p.ProductTypeID)]));
|
|
200
|
+
let remaining = 0;
|
|
201
|
+
for (const row of rows) {
|
|
202
|
+
const shaped = {
|
|
203
|
+
ID: row.ID,
|
|
204
|
+
RequiresFulfillment: requiresByType.get(typeByProduct.get(key(row.ProductID)) ?? '') ?? false,
|
|
205
|
+
FulfillmentStatus: row.FulfillmentStatus ?? null,
|
|
206
|
+
ReversesOrderLineID: row.ReversesOrderLineID,
|
|
207
|
+
IsRollupParent: !!row.IsRollupParent,
|
|
208
|
+
};
|
|
209
|
+
if (!IsAwaitingFulfillment(shaped))
|
|
210
|
+
continue;
|
|
211
|
+
const entity = await provider.GetEntityObject(ORDER_LINE_ENTITY, user);
|
|
212
|
+
if (!(await entity.Load(row.ID))) {
|
|
213
|
+
remaining++;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
entity.FulfillmentStatus = 'Fulfilled';
|
|
217
|
+
if (!(await entity.Save()))
|
|
218
|
+
remaining++;
|
|
219
|
+
}
|
|
220
|
+
return remaining;
|
|
221
|
+
}
|
|
222
|
+
/** The order is booked and moved as far as it could, but not as far as asked. */
|
|
223
|
+
async stopped(orderID, header, target, status, transitions, unfulfilled, provider) {
|
|
224
|
+
return {
|
|
225
|
+
Success: false,
|
|
226
|
+
Message: `The order stopped at ${status} rather than ${target}.`,
|
|
227
|
+
OrderHeaderID: orderID,
|
|
228
|
+
OrderNumber: header.OrderNumber ?? null,
|
|
229
|
+
Status: status,
|
|
230
|
+
RequestedStatus: target,
|
|
231
|
+
Transitions: transitions,
|
|
232
|
+
...(await this.readBookedEntries(orderID, provider)),
|
|
233
|
+
UnfulfilledLineCount: unfulfilled,
|
|
234
|
+
Blockers: [],
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
blocker(code, message, hint) {
|
|
238
|
+
return { Code: code, Message: message, ResolutionHint: hint ?? null, LineNumber: null };
|
|
239
|
+
}
|
|
240
|
+
refuse(target, blockers, orderID = null, orderNumber = null, status = null) {
|
|
241
|
+
return {
|
|
242
|
+
Success: false,
|
|
243
|
+
Message: blockers.map((b) => b.Message).join(' '),
|
|
244
|
+
OrderHeaderID: orderID,
|
|
245
|
+
OrderNumber: orderNumber,
|
|
246
|
+
Status: status,
|
|
247
|
+
RequestedStatus: target,
|
|
248
|
+
Transitions: [],
|
|
249
|
+
EntryCount: 0,
|
|
250
|
+
AllBalanced: true,
|
|
251
|
+
UnfulfilledLineCount: 0,
|
|
252
|
+
Blockers: blockers,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
AdvanceOrderStateOperation = __decorate([
|
|
257
|
+
RegisterClass(BaseRemotableOperation, 'Orders.AdvanceOrderState')
|
|
258
|
+
], AdvanceOrderStateOperation);
|
|
259
|
+
export { AdvanceOrderStateOperation };
|
|
260
|
+
/**
|
|
261
|
+
* Force the class registration. Tree-shaking removes a class nobody imports, and the decorator only
|
|
262
|
+
* runs if the module is loaded — so the server's bootstrap calls this.
|
|
263
|
+
*/
|
|
264
|
+
export function LoadAdvanceOrderStateOperation() {
|
|
265
|
+
// no-op by design
|
|
266
|
+
}
|
|
267
|
+
//# sourceMappingURL=AdvanceOrderStateOperation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AdvanceOrderStateOperation.js","sourceRoot":"","sources":["../src/AdvanceOrderStateOperation.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orders.ApplyAccountCredit — spend an order's credit balance on another order (plan D68).
|
|
3
|
+
*
|
|
4
|
+
* WHAT AN ACCOUNT CREDIT IS
|
|
5
|
+
* Not a separate instrument. When a customer pays more than an order is worth, the surplus stays on
|
|
6
|
+
* that order as a NEGATIVE balance, and that negative balance IS the credit. Nothing else holds it —
|
|
7
|
+
* deliberately, because a second record carrying the same balance is a second thing that can
|
|
8
|
+
* disagree with the first. (A gift card is different: `StoredValueAccount` exists because nothing
|
|
9
|
+
* else could hold that money.)
|
|
10
|
+
*
|
|
11
|
+
* WHAT THIS OPERATION WRITES
|
|
12
|
+
* A payment whose `Amount` is ZERO, carrying two offsetting allocations:
|
|
13
|
+
*
|
|
14
|
+
* Payment PAY-000042 Amount 0.00 type AccountCredit
|
|
15
|
+
* PaymentDetail.SourceOrderHeaderID = <the credit order>
|
|
16
|
+
* line 1: <credit order> -100.00 the credit is consumed
|
|
17
|
+
* line 2: <target order> +100.00 and lands here
|
|
18
|
+
* SUM = 0.00 == Amount
|
|
19
|
+
*
|
|
20
|
+
* Zero is not a degenerate case here, it is the truth: no new cash entered the business. The money
|
|
21
|
+
* arrived earlier, on the payment that over-paid the first order; this only re-attributes it. The
|
|
22
|
+
* D68 invariant (`Amount` = sum of lines) therefore holds exactly, with nothing special-cased.
|
|
23
|
+
*
|
|
24
|
+
* WHY NOT JUST EDIT THE ORIGINAL PAYMENT'S LINES
|
|
25
|
+
* Because a credit is not always traceable to one payment — it can accumulate from several, or
|
|
26
|
+
* survive a partial refund — and because a captured payment is frozen (51005/51010/51011). Rewriting
|
|
27
|
+
* history to express a new event would destroy the audit trail of money that genuinely moved.
|
|
28
|
+
*
|
|
29
|
+
* THE LEDGER FALLS OUT OF THE EXISTING MACHINERY
|
|
30
|
+
* `PaymentLineEntityServer` already books each allocation and already treats a negative amount as a
|
|
31
|
+
* reversal, so the two lines produce a mirrored pair and an ordinary pair:
|
|
32
|
+
*
|
|
33
|
+
* credit order: Dr A/R 100 Cr Cash 100 (mirrored — the reversal)
|
|
34
|
+
* target order: Dr Cash 100 Cr A/R 100
|
|
35
|
+
*
|
|
36
|
+
* Cash nets to zero across them; A/R moves from one order to the other. No new booking code exists
|
|
37
|
+
* for this operation, and none should — if it needed its own entries, that would mean the allocation
|
|
38
|
+
* path did not really understand negative applications.
|
|
39
|
+
*
|
|
40
|
+
* CROSS-COMPANY IS NOT A SPECIAL CASE EITHER. When the two orders belong to different companies the
|
|
41
|
+
* same path raises the intercompany legs, because the allocation factory splits per owning company.
|
|
42
|
+
* Note this is not merely convenient: a single `Dr A/R / Cr A/R` entry spanning two companies could
|
|
43
|
+
* not be booked at all (accounting refuses cross-company entries, D6), so the per-company shape is
|
|
44
|
+
* the only bookable form rather than an implementation preference.
|
|
45
|
+
*
|
|
46
|
+
* FAILURE MODEL: logical refusals come back INSIDE the output as `Success: false` with a reason —
|
|
47
|
+
* the same contract `Orders.RefundPayment` and `Orders.CancelSubscription` use. Only genuine faults
|
|
48
|
+
* throw.
|
|
49
|
+
*
|
|
50
|
+
* CONNECTS TO:
|
|
51
|
+
* BOOKING: PaymentLineEntityServer.Save — both legs book through the ordinary allocation path
|
|
52
|
+
* TABLES: __mj_BizAppsOrders.{PaymentHeader,PaymentLine,PaymentDetail,OrderHeader}
|
|
53
|
+
* DOC: plans/archive/bizapps-orders-master.md D68
|
|
54
|
+
*/
|
|
55
|
+
import { BaseRemotableOperation, IMetadataProvider, UserInfo } from '@memberjunction/core';
|
|
56
|
+
export interface ApplyAccountCreditInput {
|
|
57
|
+
/** The order carrying the credit (its Balance is negative). */
|
|
58
|
+
SourceOrderHeaderID: string;
|
|
59
|
+
/** The order to spend it on. */
|
|
60
|
+
TargetOrderHeaderID: string;
|
|
61
|
+
/** Omit to apply as much as both sides allow. */
|
|
62
|
+
Amount?: number;
|
|
63
|
+
Reason?: string;
|
|
64
|
+
/** Compute and validate without writing — for a confirmation screen. */
|
|
65
|
+
Preview?: boolean;
|
|
66
|
+
}
|
|
67
|
+
export interface ApplyAccountCreditOutput {
|
|
68
|
+
Success: boolean;
|
|
69
|
+
Message?: string;
|
|
70
|
+
/** What was (or would be) applied. */
|
|
71
|
+
AppliedAmount?: number;
|
|
72
|
+
/** The source order's credit still available AFTER this application. */
|
|
73
|
+
RemainingCredit?: number;
|
|
74
|
+
/** The target order's balance AFTER this application. */
|
|
75
|
+
TargetBalanceAfter?: number;
|
|
76
|
+
PaymentHeaderID?: string;
|
|
77
|
+
PaymentNumber?: string;
|
|
78
|
+
}
|
|
79
|
+
export declare class ApplyAccountCreditOperation extends BaseRemotableOperation<ApplyAccountCreditInput, ApplyAccountCreditOutput> {
|
|
80
|
+
OperationKey: string;
|
|
81
|
+
protected InternalExecute(input: ApplyAccountCreditInput, provider: IMetadataProvider, user: UserInfo): Promise<ApplyAccountCreditOutput>;
|
|
82
|
+
private loadOrder;
|
|
83
|
+
/**
|
|
84
|
+
* The `AccountCredit` tender's ID.
|
|
85
|
+
*
|
|
86
|
+
* Cache first, then the query it always did. The row is seeded metadata and the cache answers it
|
|
87
|
+
* without a round trip; the fallback stays because a deployment that has not seeded this tender
|
|
88
|
+
* yet, or one that added it in the transaction now running, must still resolve it — and the
|
|
89
|
+
* caller's whole operation depends on finding it.
|
|
90
|
+
*/
|
|
91
|
+
private accountCreditTypeID;
|
|
92
|
+
/** Gap-free payment numbering, same sequence the capture path uses. */
|
|
93
|
+
private nextPaymentNumber;
|
|
94
|
+
}
|
|
95
|
+
/** Tree-shaking anchor — called from the server bootstrap so the registration is retained. */
|
|
96
|
+
export declare function LoadApplyAccountCreditOperation(): void;
|
|
97
|
+
//# sourceMappingURL=ApplyAccountCreditOperation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ApplyAccountCreditOperation.d.ts","sourceRoot":"","sources":["../src/ApplyAccountCreditOperation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,OAAO,EAEH,sBAAsB,EAEtB,iBAAiB,EAIjB,QAAQ,EACX,MAAM,sBAAsB,CAAC;AAsB9B,MAAM,WAAW,uBAAuB;IACpC,+DAA+D;IAC/D,mBAAmB,EAAE,MAAM,CAAC;IAC5B,gCAAgC;IAChC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,OAAO,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,wBAAwB;IACrC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sCAAsC;IACtC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,wEAAwE;IACxE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,yDAAyD;IACzD,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,aAAa,CAAC,EAAE,MAAM,CAAC;CAC1B;AAaD,qBACa,2BAA4B,SAAQ,sBAAsB,CAAC,uBAAuB,EAAE,wBAAwB,CAAC;IAC/G,YAAY,SAA+B;cAElC,eAAe,CAC3B,KAAK,EAAE,uBAAuB,EAC9B,QAAQ,EAAE,iBAAiB,EAC3B,IAAI,EAAE,QAAQ,GACf,OAAO,CAAC,wBAAwB,CAAC;YA2JtB,SAAS;IAevB;;;;;;;OAOG;YACW,mBAAmB;IAmBjC,uEAAuE;YACzD,iBAAiB;CAelC;AAED,8FAA8F;AAC9F,wBAAgB,+BAA+B,IAAI,IAAI,CAEtD"}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Orders.ApplyAccountCredit — spend an order's credit balance on another order (plan D68).
|
|
9
|
+
*
|
|
10
|
+
* WHAT AN ACCOUNT CREDIT IS
|
|
11
|
+
* Not a separate instrument. When a customer pays more than an order is worth, the surplus stays on
|
|
12
|
+
* that order as a NEGATIVE balance, and that negative balance IS the credit. Nothing else holds it —
|
|
13
|
+
* deliberately, because a second record carrying the same balance is a second thing that can
|
|
14
|
+
* disagree with the first. (A gift card is different: `StoredValueAccount` exists because nothing
|
|
15
|
+
* else could hold that money.)
|
|
16
|
+
*
|
|
17
|
+
* WHAT THIS OPERATION WRITES
|
|
18
|
+
* A payment whose `Amount` is ZERO, carrying two offsetting allocations:
|
|
19
|
+
*
|
|
20
|
+
* Payment PAY-000042 Amount 0.00 type AccountCredit
|
|
21
|
+
* PaymentDetail.SourceOrderHeaderID = <the credit order>
|
|
22
|
+
* line 1: <credit order> -100.00 the credit is consumed
|
|
23
|
+
* line 2: <target order> +100.00 and lands here
|
|
24
|
+
* SUM = 0.00 == Amount
|
|
25
|
+
*
|
|
26
|
+
* Zero is not a degenerate case here, it is the truth: no new cash entered the business. The money
|
|
27
|
+
* arrived earlier, on the payment that over-paid the first order; this only re-attributes it. The
|
|
28
|
+
* D68 invariant (`Amount` = sum of lines) therefore holds exactly, with nothing special-cased.
|
|
29
|
+
*
|
|
30
|
+
* WHY NOT JUST EDIT THE ORIGINAL PAYMENT'S LINES
|
|
31
|
+
* Because a credit is not always traceable to one payment — it can accumulate from several, or
|
|
32
|
+
* survive a partial refund — and because a captured payment is frozen (51005/51010/51011). Rewriting
|
|
33
|
+
* history to express a new event would destroy the audit trail of money that genuinely moved.
|
|
34
|
+
*
|
|
35
|
+
* THE LEDGER FALLS OUT OF THE EXISTING MACHINERY
|
|
36
|
+
* `PaymentLineEntityServer` already books each allocation and already treats a negative amount as a
|
|
37
|
+
* reversal, so the two lines produce a mirrored pair and an ordinary pair:
|
|
38
|
+
*
|
|
39
|
+
* credit order: Dr A/R 100 Cr Cash 100 (mirrored — the reversal)
|
|
40
|
+
* target order: Dr Cash 100 Cr A/R 100
|
|
41
|
+
*
|
|
42
|
+
* Cash nets to zero across them; A/R moves from one order to the other. No new booking code exists
|
|
43
|
+
* for this operation, and none should — if it needed its own entries, that would mean the allocation
|
|
44
|
+
* path did not really understand negative applications.
|
|
45
|
+
*
|
|
46
|
+
* CROSS-COMPANY IS NOT A SPECIAL CASE EITHER. When the two orders belong to different companies the
|
|
47
|
+
* same path raises the intercompany legs, because the allocation factory splits per owning company.
|
|
48
|
+
* Note this is not merely convenient: a single `Dr A/R / Cr A/R` entry spanning two companies could
|
|
49
|
+
* not be booked at all (accounting refuses cross-company entries, D6), so the per-company shape is
|
|
50
|
+
* the only bookable form rather than an implementation preference.
|
|
51
|
+
*
|
|
52
|
+
* FAILURE MODEL: logical refusals come back INSIDE the output as `Success: false` with a reason —
|
|
53
|
+
* the same contract `Orders.RefundPayment` and `Orders.CancelSubscription` use. Only genuine faults
|
|
54
|
+
* throw.
|
|
55
|
+
*
|
|
56
|
+
* CONNECTS TO:
|
|
57
|
+
* BOOKING: PaymentLineEntityServer.Save — both legs book through the ordinary allocation path
|
|
58
|
+
* TABLES: __mj_BizAppsOrders.{PaymentHeader,PaymentLine,PaymentDetail,OrderHeader}
|
|
59
|
+
* DOC: plans/archive/bizapps-orders-master.md D68
|
|
60
|
+
*/
|
|
61
|
+
import { BaseRemotableOperation, LogError, RunView, } from '@memberjunction/core';
|
|
62
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
63
|
+
import { RequireUUID } from './sql-guards.js';
|
|
64
|
+
import { LoadOrdersEngine, OrdersEngine } from '@mj-biz-apps/orders-entities';
|
|
65
|
+
const PAYMENT_HEADER_ENTITY = 'MJ_BizApps_Orders: Payment Headers';
|
|
66
|
+
const PAYMENT_LINE_ENTITY = 'MJ_BizApps_Orders: Payment Lines';
|
|
67
|
+
const PAYMENT_DETAIL_ENTITY = 'MJ_BizApps_Orders: Payment Details';
|
|
68
|
+
const PAYMENT_TYPE_ENTITY = 'MJ_BizApps_Orders: Payment Types';
|
|
69
|
+
const ORDER_HEADER_ENTITY = 'MJ_BizApps_Orders: Order Headers';
|
|
70
|
+
/** The payment type this operation writes. Seeded in metadata/payment-types. */
|
|
71
|
+
const ACCOUNT_CREDIT_TYPE_CODE = 'AccountCredit';
|
|
72
|
+
/** Half a cent — the same tolerance the allocation guard uses. */
|
|
73
|
+
const TOLERANCE = 0.005;
|
|
74
|
+
const money = (v) => Math.round((v + Number.EPSILON) * 100) / 100;
|
|
75
|
+
let ApplyAccountCreditOperation = class ApplyAccountCreditOperation extends BaseRemotableOperation {
|
|
76
|
+
constructor() {
|
|
77
|
+
super(...arguments);
|
|
78
|
+
this.OperationKey = 'Orders.ApplyAccountCredit';
|
|
79
|
+
}
|
|
80
|
+
async InternalExecute(input, provider, user) {
|
|
81
|
+
if (!input?.SourceOrderHeaderID || !input?.TargetOrderHeaderID) {
|
|
82
|
+
return { Success: false, Message: 'Both SourceOrderHeaderID and TargetOrderHeaderID are required.' };
|
|
83
|
+
}
|
|
84
|
+
// Caller-supplied ids reach SQL filter text downstream. Validated here,
|
|
85
|
+
// at the boundary, so every frame below this one can trust them.
|
|
86
|
+
RequireUUID(input.SourceOrderHeaderID, 'SourceOrderHeaderID');
|
|
87
|
+
RequireUUID(input.TargetOrderHeaderID, 'TargetOrderHeaderID');
|
|
88
|
+
if (input.SourceOrderHeaderID.toLowerCase() === input.TargetOrderHeaderID.toLowerCase()) {
|
|
89
|
+
return {
|
|
90
|
+
Success: false,
|
|
91
|
+
Message: 'The source and target orders are the same. Moving a credit onto the order that already holds it would change nothing.',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const source = await this.loadOrder(provider, user, input.SourceOrderHeaderID);
|
|
95
|
+
if (!source)
|
|
96
|
+
return { Success: false, Message: `Order ${input.SourceOrderHeaderID} was not found.` };
|
|
97
|
+
const target = await this.loadOrder(provider, user, input.TargetOrderHeaderID);
|
|
98
|
+
if (!target)
|
|
99
|
+
return { Success: false, Message: `Order ${input.TargetOrderHeaderID} was not found.` };
|
|
100
|
+
// The credit is the source order's negative balance, expressed positive.
|
|
101
|
+
const available = money(-Number(source.Balance ?? 0));
|
|
102
|
+
if (available <= TOLERANCE) {
|
|
103
|
+
return {
|
|
104
|
+
Success: false,
|
|
105
|
+
Message: `Order ${source.OrderNumber} has no credit to spend — its balance is ${money(Number(source.Balance ?? 0))}. ` +
|
|
106
|
+
`A credit exists only when an order has been paid MORE than it is worth, which shows as a negative balance.`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
// Nothing to pay toward is a refusal rather than a no-op: the caller believes it is settling
|
|
110
|
+
// something, and silently writing a zero-value payment would hide that it did not.
|
|
111
|
+
const owing = money(Number(target.Balance ?? 0));
|
|
112
|
+
if (owing <= TOLERANCE) {
|
|
113
|
+
return {
|
|
114
|
+
Success: false,
|
|
115
|
+
Message: `Order ${target.OrderNumber} has nothing outstanding (balance ${owing}), so there is nothing to apply the credit to.`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
// Default to as much as both sides allow; an explicit amount is checked against both.
|
|
119
|
+
const requested = input.Amount === undefined ? Math.min(available, owing) : money(Number(input.Amount));
|
|
120
|
+
if (!(requested > TOLERANCE)) {
|
|
121
|
+
return { Success: false, Message: `Amount must be greater than zero (received ${input.Amount}).` };
|
|
122
|
+
}
|
|
123
|
+
if (requested > available + TOLERANCE) {
|
|
124
|
+
return {
|
|
125
|
+
Success: false,
|
|
126
|
+
Message: `Order ${source.OrderNumber} only has ${available} of credit; ${requested} was requested.`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (requested > owing + TOLERANCE) {
|
|
130
|
+
return {
|
|
131
|
+
Success: false,
|
|
132
|
+
Message: `Order ${target.OrderNumber} only owes ${owing}; applying ${requested} would over-pay it. ` +
|
|
133
|
+
`Apply ${owing} instead, or spend the rest of the credit on another order.`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const remainingCredit = money(available - requested);
|
|
137
|
+
const targetBalanceAfter = money(owing - requested);
|
|
138
|
+
if (input.Preview) {
|
|
139
|
+
return {
|
|
140
|
+
Success: true,
|
|
141
|
+
Message: `${requested} of order ${source.OrderNumber}'s credit would settle order ${target.OrderNumber}, ` +
|
|
142
|
+
`leaving ${remainingCredit} of credit and a balance of ${targetBalanceAfter}.`,
|
|
143
|
+
AppliedAmount: requested,
|
|
144
|
+
RemainingCredit: remainingCredit,
|
|
145
|
+
TargetBalanceAfter: targetBalanceAfter,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
const typeID = await this.accountCreditTypeID(provider, user);
|
|
149
|
+
if (!typeID) {
|
|
150
|
+
return {
|
|
151
|
+
Success: false,
|
|
152
|
+
Message: `The '${ACCOUNT_CREDIT_TYPE_CODE}' payment type is not present. Push metadata/payment-types before applying credits.`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const db = provider;
|
|
156
|
+
await db.BeginTransaction();
|
|
157
|
+
try {
|
|
158
|
+
const detail = await provider.GetEntityObject(PAYMENT_DETAIL_ENTITY, user);
|
|
159
|
+
detail.NewRecord();
|
|
160
|
+
detail.CompanyID = target.CompanyID;
|
|
161
|
+
detail.PaymentTypeID = typeID;
|
|
162
|
+
detail.SourceOrderHeaderID = source.ID;
|
|
163
|
+
detail.Notes = input.Reason ?? `Credit from order ${source.OrderNumber}`;
|
|
164
|
+
if (!(await detail.Save())) {
|
|
165
|
+
throw new Error(`Could not record the credit tender: ${detail.LatestResult?.CompleteMessage ?? 'unknown error'}`);
|
|
166
|
+
}
|
|
167
|
+
const payment = await provider.GetEntityObject(PAYMENT_HEADER_ENTITY, user);
|
|
168
|
+
payment.NewRecord();
|
|
169
|
+
const paymentNumber = await this.nextPaymentNumber(db);
|
|
170
|
+
payment.PaymentNumber = paymentNumber;
|
|
171
|
+
payment.ReceivingCompanyID = target.CompanyID;
|
|
172
|
+
payment.PaymentTypeID = typeID;
|
|
173
|
+
payment.PaymentDetailID = detail.ID;
|
|
174
|
+
// ZERO on purpose — no new cash entered the business, this re-attributes money already
|
|
175
|
+
// received. The D68 invariant holds exactly: 0 == (-requested) + (+requested).
|
|
176
|
+
payment.Amount = 0;
|
|
177
|
+
payment.ProcessingFeeAmount = 0;
|
|
178
|
+
payment.PaymentDate = new Date();
|
|
179
|
+
payment.Status = 'Captured';
|
|
180
|
+
payment.Notes = input.Reason ?? `Applied ${requested} of order ${source.OrderNumber}'s credit`;
|
|
181
|
+
// Both legs ride the header's Lines collection so they land inside this transaction and
|
|
182
|
+
// the invariant is checked against the pair, never against a half-written payment.
|
|
183
|
+
const consume = await provider.GetEntityObject(PAYMENT_LINE_ENTITY, user);
|
|
184
|
+
consume.NewRecord();
|
|
185
|
+
consume.OrderHeaderID = source.ID;
|
|
186
|
+
consume.Amount = -requested;
|
|
187
|
+
consume.AllocatedByUserID = user?.ID ?? null;
|
|
188
|
+
const apply = await provider.GetEntityObject(PAYMENT_LINE_ENTITY, user);
|
|
189
|
+
apply.NewRecord();
|
|
190
|
+
apply.OrderHeaderID = target.ID;
|
|
191
|
+
apply.Amount = requested;
|
|
192
|
+
apply.AllocatedByUserID = user?.ID ?? null;
|
|
193
|
+
// Attached, not assigned: `Lines` is a RelatedRecordCollection and `Add()` stamps
|
|
194
|
+
// PaymentHeaderID for us — correct even though the header has not been saved yet.
|
|
195
|
+
payment.Lines.Add(consume);
|
|
196
|
+
payment.Lines.Add(apply);
|
|
197
|
+
if (!(await payment.Save())) {
|
|
198
|
+
throw new Error(`Could not save the credit application: ${payment.LatestResult?.CompleteMessage ?? 'unknown error'}`);
|
|
199
|
+
}
|
|
200
|
+
await db.CommitTransaction();
|
|
201
|
+
return {
|
|
202
|
+
Success: true,
|
|
203
|
+
Message: `Applied ${requested} of order ${source.OrderNumber}'s credit to order ${target.OrderNumber} as ${paymentNumber}.`,
|
|
204
|
+
AppliedAmount: requested,
|
|
205
|
+
RemainingCredit: remainingCredit,
|
|
206
|
+
TargetBalanceAfter: targetBalanceAfter,
|
|
207
|
+
PaymentHeaderID: payment.ID,
|
|
208
|
+
PaymentNumber: paymentNumber,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
await db.RollbackTransaction();
|
|
213
|
+
LogError(err);
|
|
214
|
+
throw err;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async loadOrder(provider, user, id) {
|
|
218
|
+
const rv = new RunView(provider);
|
|
219
|
+
const res = await rv.RunView({
|
|
220
|
+
EntityName: ORDER_HEADER_ENTITY,
|
|
221
|
+
ExtraFilter: `ID='${id}'`,
|
|
222
|
+
Fields: ['ID', 'OrderNumber', 'Status', 'Balance', 'TotalGross', 'CompanyID'],
|
|
223
|
+
ResultType: 'simple',
|
|
224
|
+
BypassCache: true,
|
|
225
|
+
}, user);
|
|
226
|
+
return res?.Results?.[0] ?? null;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* The `AccountCredit` tender's ID.
|
|
230
|
+
*
|
|
231
|
+
* Cache first, then the query it always did. The row is seeded metadata and the cache answers it
|
|
232
|
+
* without a round trip; the fallback stays because a deployment that has not seeded this tender
|
|
233
|
+
* yet, or one that added it in the transaction now running, must still resolve it — and the
|
|
234
|
+
* caller's whole operation depends on finding it.
|
|
235
|
+
*/
|
|
236
|
+
async accountCreditTypeID(provider, user) {
|
|
237
|
+
await LoadOrdersEngine(provider, user);
|
|
238
|
+
const cached = OrdersEngine.Instance.PaymentTypeByCode(ACCOUNT_CREDIT_TYPE_CODE);
|
|
239
|
+
if (cached?.ID)
|
|
240
|
+
return cached.ID;
|
|
241
|
+
const rv = new RunView(provider);
|
|
242
|
+
const res = await rv.RunView({
|
|
243
|
+
EntityName: PAYMENT_TYPE_ENTITY,
|
|
244
|
+
ExtraFilter: `Code='${ACCOUNT_CREDIT_TYPE_CODE}'`,
|
|
245
|
+
Fields: ['ID'],
|
|
246
|
+
ResultType: 'simple',
|
|
247
|
+
BypassCache: true,
|
|
248
|
+
}, user);
|
|
249
|
+
return res?.Results?.[0]?.ID ?? null;
|
|
250
|
+
}
|
|
251
|
+
/** Gap-free payment numbering, same sequence the capture path uses. */
|
|
252
|
+
async nextPaymentNumber(db) {
|
|
253
|
+
const rows = (await db.ExecuteSQL(`
|
|
254
|
+
DECLARE @seq TABLE (Seq INT);
|
|
255
|
+
UPDATE __mj_BizAppsOrders.PaymentSequence WITH (UPDLOCK, HOLDLOCK)
|
|
256
|
+
SET NextSequenceNumber = NextSequenceNumber + 1
|
|
257
|
+
OUTPUT deleted.NextSequenceNumber INTO @seq(Seq)
|
|
258
|
+
WHERE ID = 1;
|
|
259
|
+
SELECT Seq FROM @seq;`));
|
|
260
|
+
const seq = rows?.[0]?.Seq;
|
|
261
|
+
if (!seq) {
|
|
262
|
+
throw new Error('Could not obtain the next payment number — PaymentSequence (ID=1) is missing.');
|
|
263
|
+
}
|
|
264
|
+
return `PAY-${String(seq).padStart(6, '0')}`;
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
ApplyAccountCreditOperation = __decorate([
|
|
268
|
+
RegisterClass(BaseRemotableOperation, 'Orders.ApplyAccountCredit')
|
|
269
|
+
], ApplyAccountCreditOperation);
|
|
270
|
+
export { ApplyAccountCreditOperation };
|
|
271
|
+
/** Tree-shaking anchor — called from the server bootstrap so the registration is retained. */
|
|
272
|
+
export function LoadApplyAccountCreditOperation() {
|
|
273
|
+
// intentionally empty
|
|
274
|
+
}
|
|
275
|
+
//# sourceMappingURL=ApplyAccountCreditOperation.js.map
|