@mj-biz-apps/orders-core-entities-server 5.36.0 → 5.37.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/dist/CheckoutSessionService.d.ts +44 -2
- package/dist/CheckoutSessionService.d.ts.map +1 -1
- package/dist/CheckoutSessionService.js +156 -39
- package/dist/CheckoutSessionService.js.map +1 -1
- package/dist/ConcessionApprovalListener.js +6 -1
- package/dist/ConcessionApprovalListener.js.map +1 -1
- package/dist/ConcessionApprovalTask.d.ts +40 -1
- package/dist/ConcessionApprovalTask.d.ts.map +1 -1
- package/dist/ConcessionApprovalTask.js +139 -15
- package/dist/ConcessionApprovalTask.js.map +1 -1
- package/dist/EntitlementEngine.d.ts +7 -1
- package/dist/EntitlementEngine.d.ts.map +1 -1
- package/dist/EntitlementEngine.js +70 -60
- package/dist/EntitlementEngine.js.map +1 -1
- package/dist/ExternalInvoiceBehavior.d.ts +4 -0
- package/dist/ExternalInvoiceBehavior.d.ts.map +1 -1
- package/dist/ExternalInvoiceBehavior.js +34 -3
- package/dist/ExternalInvoiceBehavior.js.map +1 -1
- package/dist/InvoiceDisplay.d.ts +2 -0
- package/dist/InvoiceDisplay.d.ts.map +1 -1
- package/dist/InvoiceDisplay.js +7 -3
- package/dist/InvoiceDisplay.js.map +1 -1
- package/dist/OrderConcessionEntityServer.d.ts +13 -3
- package/dist/OrderConcessionEntityServer.d.ts.map +1 -1
- package/dist/OrderConcessionEntityServer.js +64 -11
- package/dist/OrderConcessionEntityServer.js.map +1 -1
- package/dist/OrderEntityServer.d.ts +5 -0
- package/dist/OrderEntityServer.d.ts.map +1 -1
- package/dist/OrderEntityServer.js +120 -45
- package/dist/OrderEntityServer.js.map +1 -1
- package/dist/OrderJournalEntryFactory.d.ts +5 -3
- package/dist/OrderJournalEntryFactory.d.ts.map +1 -1
- package/dist/OrderJournalEntryFactory.js +56 -49
- package/dist/OrderJournalEntryFactory.js.map +1 -1
- package/dist/OrderLineEntityServer.d.ts +13 -0
- package/dist/OrderLineEntityServer.d.ts.map +1 -1
- package/dist/OrderLineEntityServer.js +91 -0
- package/dist/OrderLineEntityServer.js.map +1 -1
- package/dist/OrdersSettings.d.ts +9 -0
- package/dist/OrdersSettings.d.ts.map +1 -1
- package/dist/OrdersSettings.js +11 -0
- package/dist/OrdersSettings.js.map +1 -1
- package/dist/PaymentWebhookHandler.d.ts.map +1 -1
- package/dist/PaymentWebhookHandler.js +2 -1
- package/dist/PaymentWebhookHandler.js.map +1 -1
- package/dist/RevenueRecognition.d.ts +1 -1
- package/dist/RevenueRecognition.d.ts.map +1 -1
- package/dist/RevenueRecognition.js +12 -7
- package/dist/RevenueRecognition.js.map +1 -1
- package/dist/SubscriptionBehavior.d.ts +91 -9
- package/dist/SubscriptionBehavior.d.ts.map +1 -1
- package/dist/SubscriptionBehavior.js +134 -11
- package/dist/SubscriptionBehavior.js.map +1 -1
- package/dist/TermLengthExceptions.d.ts +84 -0
- package/dist/TermLengthExceptions.d.ts.map +1 -0
- package/dist/TermLengthExceptions.js +119 -0
- package/dist/TermLengthExceptions.js.map +1 -0
- package/dist/checkoutCaptureAlert.d.ts +3 -0
- package/dist/checkoutCaptureAlert.d.ts.map +1 -1
- package/dist/checkoutCaptureAlert.js +21 -3
- package/dist/checkoutCaptureAlert.js.map +1 -1
- package/dist/displaced-start-event.d.ts +1 -1
- package/dist/displaced-start-event.d.ts.map +1 -1
- package/dist/displaced-start-event.js +3 -1
- package/dist/displaced-start-event.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/isolatedProvider.d.ts +22 -0
- package/dist/isolatedProvider.d.ts.map +1 -0
- package/dist/isolatedProvider.js +39 -0
- package/dist/isolatedProvider.js.map +1 -0
- package/package.json +2 -2
|
@@ -238,8 +238,20 @@ export declare class CheckoutSessionService {
|
|
|
238
238
|
* requires a company match).
|
|
239
239
|
*/
|
|
240
240
|
private static resolveAllowedProductIds;
|
|
241
|
-
/**
|
|
241
|
+
/**
|
|
242
|
+
* Marks expired-but-still-Open sessions Expired. Bounded; best-effort.
|
|
243
|
+
*
|
|
244
|
+
* A session whose payment settled has charged the buyer with no order, so expiring it raises the
|
|
245
|
+
* "settled with no order" alert: otherwise it would leave nothing for staff to see (#724).
|
|
246
|
+
*/
|
|
242
247
|
static ReapExpiredOpenSessions(contextUser?: UserInfo, limit?: number): Promise<number>;
|
|
248
|
+
/** True when the session's payment intent is recorded as settled. Reads our row, not the gateway. */
|
|
249
|
+
private static sessionPaymentSettled;
|
|
250
|
+
/**
|
|
251
|
+
* Waits, up to `PROCESSING_WAIT_MS`, for a session another caller latched to Processing to leave
|
|
252
|
+
* that state, reloading it in place. Returns at once when it is not Processing.
|
|
253
|
+
*/
|
|
254
|
+
private static awaitProcessingFinished;
|
|
243
255
|
/**
|
|
244
256
|
* Verifies a caller-presented client session key against the session row's stored key.
|
|
245
257
|
* The session GUID alone must never be sufficient to drive a session — both values are
|
|
@@ -397,6 +409,30 @@ export declare class CheckoutSessionService {
|
|
|
397
409
|
* refuses a sale (#480).
|
|
398
410
|
*/
|
|
399
411
|
static CompleteCheckout(sessionID: string, clientSessionKey: string, contextUser?: UserInfo, evidence?: CheckoutLocationEvidence): Promise<CompleteCheckoutResult>;
|
|
412
|
+
/**
|
|
413
|
+
* Completes a session paid for at the gateway whose browser never asked (#724): the buyer closed
|
|
414
|
+
* the tab, or lost the page, after the payment succeeded. Reached from the payment webhook, so there
|
|
415
|
+
* is no client session key: the signature-verified event is the proof, and the completion checks
|
|
416
|
+
* the intent's state and amount against the re-priced order exactly as the browser's call does.
|
|
417
|
+
*
|
|
418
|
+
* The session expiry is not enforced: the buyer paid inside it, and expiry stops a session from
|
|
419
|
+
* being driven further, not a paid one from being booked.
|
|
420
|
+
*
|
|
421
|
+
* Any refusal raises the "settled with no order" alert, since nobody is on the page to see it.
|
|
422
|
+
*/
|
|
423
|
+
private static completeFromWebhook;
|
|
424
|
+
/**
|
|
425
|
+
* The existing order of a Confirmed session, booking its payment if the first completion did
|
|
426
|
+
* not. CapturePayment is idempotent on `checkout-complete:${session.ID}`, so this is one of the
|
|
427
|
+
* retries; `payment_intent.succeeded` is the other (BookSettledCheckoutPaymentIfNeeded, including
|
|
428
|
+
* on AlreadyApplied webhook deliveries so a 500 after applyEvent still converges).
|
|
429
|
+
*/
|
|
430
|
+
private static replayConfirmedSession;
|
|
431
|
+
/**
|
|
432
|
+
* Builds and confirms the order for an Open session — the work behind both the browser's
|
|
433
|
+
* complete call and the payment webhook. The caller has checked the session may be completed.
|
|
434
|
+
*/
|
|
435
|
+
private static completeOpenSession;
|
|
400
436
|
/** The session's MetadataJSON, or an empty snapshot when it is absent or unreadable. */
|
|
401
437
|
private static readSnapshot;
|
|
402
438
|
/** The billing location the draft stored, re-checked — null when it is missing or no longer valid. */
|
|
@@ -447,9 +483,15 @@ export declare class CheckoutSessionService {
|
|
|
447
483
|
* 2. This method, from `payment_intent.succeeded` — including AlreadyApplied deliveries,
|
|
448
484
|
* so a 500 after the intent row is stamped still converges on the next Stripe retry.
|
|
449
485
|
*
|
|
450
|
-
*
|
|
486
|
+
* A session the payment settled for that is still Open was never completed: the buyer's page
|
|
487
|
+
* closed before it asked (#724). This method completes it, as the browser would have. One that
|
|
488
|
+
* expired first is past completing, and raises the "settled with no order" alert.
|
|
489
|
+
*
|
|
490
|
+
* No-ops when no checkout session owns the intent (back-office captures).
|
|
451
491
|
*/
|
|
452
492
|
static BookSettledCheckoutPaymentIfNeeded(paymentIntentID: string, contextUser: UserInfo): Promise<BookCheckoutPaymentResult>;
|
|
493
|
+
/** The checkout session that opened this payment intent, among those `statusFilter` admits. */
|
|
494
|
+
private static sessionIDForIntent;
|
|
453
495
|
/**
|
|
454
496
|
* Operator replay of the Capture step (`Orders.ReplayCheckoutStep`, #326). The same idempotent
|
|
455
497
|
* CapturePayment as the complete call and the webhook, recorded as a Replay by the operator.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CheckoutSessionService.d.ts","sourceRoot":"","sources":["../src/CheckoutSessionService.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAiG,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAQ/I,OAAO,EAgBH,KAAK,2BAA2B,EAOhC,KAAK,oBAAoB,EASzB,KAAK,oBAAoB,EAC5B,MAAM,8BAA8B,CAAC;AAGtC,OAAO,EAAwB,KAAK,eAAe,EAA2B,MAAM,+BAA+B,CAAC;AAWpH,OAAO,EAKH,KAAK,2BAA2B,EACnC,MAAM,qCAAqC,CAAC;
|
|
1
|
+
{"version":3,"file":"CheckoutSessionService.d.ts","sourceRoot":"","sources":["../src/CheckoutSessionService.ts"],"names":[],"mappings":"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"}
|
|
@@ -22,6 +22,7 @@ import { ResolveCheckoutPublishableKey } from './CheckoutPublishableKey.js';
|
|
|
22
22
|
import { FindReusableProviderCustomerRef, SnapshotSellsSubscription, SaveCheckoutInstrumentForRenewals, } from './CheckoutSavedInstrument.js';
|
|
23
23
|
import { CheckoutMemberDiscountNotConfiguredError, DEFAULT_TYPED_CODE_PRECEDENCE, IsRegisteredMemberPromotionCode, ResolveCheckoutMemberDiscountResolver, } from './CheckoutMemberDiscountResolver.js';
|
|
24
24
|
import { CapturePaymentOperation } from './CapturePaymentOperation.js';
|
|
25
|
+
import { WithIsolatedProvider } from './isolatedProvider.js';
|
|
25
26
|
import { ToIsoCountryCode } from './VatLocationEvidence.js';
|
|
26
27
|
import { raiseCheckoutCaptureTerminalAlert, raiseCheckoutSettledNotBookedAlert } from './checkoutCaptureAlert.js';
|
|
27
28
|
import { CheckoutStepLog } from './CheckoutStepLog.js';
|
|
@@ -135,6 +136,13 @@ function SettledButShortMessage(cause) {
|
|
|
135
136
|
return `${what}, so your payment does not cover the total and the order was not placed. `
|
|
136
137
|
+ 'Our team has been notified and will arrange a refund.';
|
|
137
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* How long a caller that finds a session Processing waits for the other completion to finish. The
|
|
141
|
+
* browser's complete call and the payment webhook can both arrive for one payment (#724); the one
|
|
142
|
+
* that loses the latch waits for the winner and answers with its order.
|
|
143
|
+
*/
|
|
144
|
+
const PROCESSING_WAIT_MS = 30_000;
|
|
145
|
+
const PROCESSING_POLL_MS = 1_000;
|
|
138
146
|
export class CheckoutSessionService {
|
|
139
147
|
/**
|
|
140
148
|
* Resolves a public distribution slug, validates widget status, and mints an active CheckoutSession.
|
|
@@ -450,7 +458,12 @@ export class CheckoutSessionService {
|
|
|
450
458
|
}
|
|
451
459
|
return allowed;
|
|
452
460
|
}
|
|
453
|
-
/**
|
|
461
|
+
/**
|
|
462
|
+
* Marks expired-but-still-Open sessions Expired. Bounded; best-effort.
|
|
463
|
+
*
|
|
464
|
+
* A session whose payment settled has charged the buyer with no order, so expiring it raises the
|
|
465
|
+
* "settled with no order" alert: otherwise it would leave nothing for staff to see (#724).
|
|
466
|
+
*/
|
|
454
467
|
static async ReapExpiredOpenSessions(contextUser, limit = 50) {
|
|
455
468
|
const rv = new RunView();
|
|
456
469
|
const nowUtc = new Date().toISOString();
|
|
@@ -478,6 +491,9 @@ export class CheckoutSessionService {
|
|
|
478
491
|
session.Status = 'Expired';
|
|
479
492
|
if (await session.Save()) {
|
|
480
493
|
n++;
|
|
494
|
+
if (await this.sessionPaymentSettled(session, md, contextUser)) {
|
|
495
|
+
void raiseCheckoutSettledNotBookedAlert(session.ID, session.PaymentIntentID, 'The checkout expired after its payment settled, and no order was created.', contextUser);
|
|
496
|
+
}
|
|
481
497
|
}
|
|
482
498
|
}
|
|
483
499
|
catch (err) {
|
|
@@ -486,6 +502,27 @@ export class CheckoutSessionService {
|
|
|
486
502
|
}
|
|
487
503
|
return n;
|
|
488
504
|
}
|
|
505
|
+
/** True when the session's payment intent is recorded as settled. Reads our row, not the gateway. */
|
|
506
|
+
static async sessionPaymentSettled(session, md, contextUser) {
|
|
507
|
+
if (!session.PaymentIntentID) {
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
const intent = await md.GetEntityObject(PAYMENT_INTENT_ENTITY, contextUser);
|
|
511
|
+
return (await intent.Load(session.PaymentIntentID)) && SETTLED_INTENT_STATUSES.includes(intent.Status);
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Waits, up to `PROCESSING_WAIT_MS`, for a session another caller latched to Processing to leave
|
|
515
|
+
* that state, reloading it in place. Returns at once when it is not Processing.
|
|
516
|
+
*/
|
|
517
|
+
static async awaitProcessingFinished(session) {
|
|
518
|
+
const deadline = Date.now() + PROCESSING_WAIT_MS;
|
|
519
|
+
while (session.Status === 'Processing' && Date.now() < deadline) {
|
|
520
|
+
await new Promise((resolve) => setTimeout(resolve, PROCESSING_POLL_MS));
|
|
521
|
+
if (!(await session.Load(session.ID))) {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
489
526
|
/**
|
|
490
527
|
* Verifies a caller-presented client session key against the session row's stored key.
|
|
491
528
|
* The session GUID alone must never be sufficient to drive a session — both values are
|
|
@@ -1784,27 +1821,13 @@ export class CheckoutSessionService {
|
|
|
1784
1821
|
Status: session.Status
|
|
1785
1822
|
};
|
|
1786
1823
|
}
|
|
1824
|
+
// The payment webhook may be completing this session right now (#724): answer with its order.
|
|
1825
|
+
await this.awaitProcessingFinished(session);
|
|
1787
1826
|
// Replay safety: a session that already completed returns its existing order rather
|
|
1788
1827
|
// than an error — a network retry after a successful booking must never double-book
|
|
1789
1828
|
// and must never strand the buyer without their order id.
|
|
1790
1829
|
if (session.Status === 'Confirmed' && session.DraftOrderID) {
|
|
1791
|
-
|
|
1792
|
-
const orderLoaded = await existingOrder.Load(session.DraftOrderID);
|
|
1793
|
-
// A first complete can confirm the order and then fail to book the payment.
|
|
1794
|
-
// Replay is one retry: CapturePayment is idempotent on `checkout-complete:${session.ID}`.
|
|
1795
|
-
// `payment_intent.succeeded` is the other — BookSettledCheckoutPaymentIfNeeded, including
|
|
1796
|
-
// on AlreadyApplied webhook deliveries so a 500 after applyEvent still converges.
|
|
1797
|
-
if (orderLoaded) {
|
|
1798
|
-
await this.applySettledPaymentToOrder(session, existingOrder, contextUser, { Source: 'Checkout' });
|
|
1799
|
-
}
|
|
1800
|
-
return {
|
|
1801
|
-
Success: true,
|
|
1802
|
-
SessionID: sessionID,
|
|
1803
|
-
Status: 'Confirmed',
|
|
1804
|
-
OrderID: session.DraftOrderID,
|
|
1805
|
-
OrderNumber: orderLoaded ? (existingOrder.OrderNumber || session.DraftOrderID) : session.DraftOrderID,
|
|
1806
|
-
TotalGross: orderLoaded ? (existingOrder.TotalGross ?? undefined) : undefined
|
|
1807
|
-
};
|
|
1830
|
+
return this.replayConfirmedSession(session, md, contextUser, { Source: 'Checkout', Report: {} });
|
|
1808
1831
|
}
|
|
1809
1832
|
if (session.Status !== 'Open') {
|
|
1810
1833
|
return {
|
|
@@ -1822,12 +1845,77 @@ export class CheckoutSessionService {
|
|
|
1822
1845
|
Status: 'Expired'
|
|
1823
1846
|
};
|
|
1824
1847
|
}
|
|
1848
|
+
return this.completeOpenSession(session, md, contextUser, { Source: 'Checkout', Evidence: evidence, Report: {} });
|
|
1849
|
+
}
|
|
1850
|
+
/**
|
|
1851
|
+
* Completes a session paid for at the gateway whose browser never asked (#724): the buyer closed
|
|
1852
|
+
* the tab, or lost the page, after the payment succeeded. Reached from the payment webhook, so there
|
|
1853
|
+
* is no client session key: the signature-verified event is the proof, and the completion checks
|
|
1854
|
+
* the intent's state and amount against the re-priced order exactly as the browser's call does.
|
|
1855
|
+
*
|
|
1856
|
+
* The session expiry is not enforced: the buyer paid inside it, and expiry stops a session from
|
|
1857
|
+
* being driven further, not a paid one from being booked.
|
|
1858
|
+
*
|
|
1859
|
+
* Any refusal raises the "settled with no order" alert, since nobody is on the page to see it.
|
|
1860
|
+
*/
|
|
1861
|
+
static async completeFromWebhook(session, md, contextUser) {
|
|
1862
|
+
const trigger = { Source: 'Webhook', Report: {} };
|
|
1863
|
+
const result = await this.completeOpenSession(session, md, contextUser, trigger);
|
|
1864
|
+
if (result.Success) {
|
|
1865
|
+
return trigger.Report.Capture ?? { Attempted: true, Booked: true };
|
|
1866
|
+
}
|
|
1867
|
+
const reason = result.ErrorMessage ?? 'the checkout could not be completed';
|
|
1868
|
+
if (trigger.Report.Retryable) {
|
|
1869
|
+
return { Attempted: true, Booked: false, Retryable: true, ErrorMessage: reason };
|
|
1870
|
+
}
|
|
1871
|
+
if (!trigger.Report.Alerted) {
|
|
1872
|
+
void raiseCheckoutSettledNotBookedAlert(session.ID, session.PaymentIntentID, reason, contextUser);
|
|
1873
|
+
}
|
|
1874
|
+
return { Attempted: true, Booked: false, Retryable: false, ErrorMessage: reason };
|
|
1875
|
+
}
|
|
1876
|
+
/**
|
|
1877
|
+
* The existing order of a Confirmed session, booking its payment if the first completion did
|
|
1878
|
+
* not. CapturePayment is idempotent on `checkout-complete:${session.ID}`, so this is one of the
|
|
1879
|
+
* retries; `payment_intent.succeeded` is the other (BookSettledCheckoutPaymentIfNeeded, including
|
|
1880
|
+
* on AlreadyApplied webhook deliveries so a 500 after applyEvent still converges).
|
|
1881
|
+
*/
|
|
1882
|
+
static async replayConfirmedSession(session, md, contextUser, trigger) {
|
|
1883
|
+
const orderID = session.DraftOrderID ?? '';
|
|
1884
|
+
const existingOrder = await md.GetEntityObject(ORDER_HEADER_ENTITY, contextUser);
|
|
1885
|
+
const orderLoaded = await existingOrder.Load(orderID);
|
|
1886
|
+
if (orderLoaded) {
|
|
1887
|
+
trigger.Report.Capture = await this.applySettledPaymentToOrder(session, existingOrder, contextUser, { Source: trigger.Source });
|
|
1888
|
+
}
|
|
1889
|
+
return {
|
|
1890
|
+
Success: true,
|
|
1891
|
+
SessionID: session.ID,
|
|
1892
|
+
Status: 'Confirmed',
|
|
1893
|
+
OrderID: orderID,
|
|
1894
|
+
OrderNumber: orderLoaded ? (existingOrder.OrderNumber || orderID) : orderID,
|
|
1895
|
+
TotalGross: orderLoaded ? (existingOrder.TotalGross ?? undefined) : undefined
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
/**
|
|
1899
|
+
* Builds and confirms the order for an Open session — the work behind both the browser's
|
|
1900
|
+
* complete call and the payment webhook. The caller has checked the session may be completed.
|
|
1901
|
+
*/
|
|
1902
|
+
static async completeOpenSession(session, md, contextUser, trigger) {
|
|
1903
|
+
const sessionID = session.ID;
|
|
1904
|
+
const evidence = trigger.Evidence;
|
|
1825
1905
|
if (session.DraftOrderID) {
|
|
1826
|
-
return this.completeRenewalPayment(session, md, contextUser);
|
|
1906
|
+
return this.completeRenewalPayment(session, md, contextUser, trigger);
|
|
1827
1907
|
}
|
|
1828
1908
|
// Atomically latch status to Processing to prevent concurrent duplicate checkout processing
|
|
1829
1909
|
const latched = await this.latchSessionProcessingAtomic(sessionID, md, contextUser);
|
|
1830
1910
|
if (!latched) {
|
|
1911
|
+
// The other caller for this payment (browser or webhook) holds the latch: wait for it.
|
|
1912
|
+
if (await session.Load(sessionID)) {
|
|
1913
|
+
await this.awaitProcessingFinished(session);
|
|
1914
|
+
if (session.Status === 'Confirmed' && session.DraftOrderID) {
|
|
1915
|
+
return this.replayConfirmedSession(session, md, contextUser, trigger);
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
trigger.Report.Retryable = true;
|
|
1831
1919
|
return {
|
|
1832
1920
|
Success: false,
|
|
1833
1921
|
ErrorMessage: 'Session is not in an Open status or was concurrently processed',
|
|
@@ -2126,6 +2214,7 @@ export class CheckoutSessionService {
|
|
|
2126
2214
|
? `Promotion code ${typedCodeDeclined.Code} was declined on re-price (${typedCodeDeclined.Reason}). ${paymentFailure.Message}`
|
|
2127
2215
|
: paymentFailure.Message;
|
|
2128
2216
|
void raiseCheckoutSettledNotBookedAlert(sessionID, session.PaymentIntentID, reason, contextUser);
|
|
2217
|
+
trigger.Report.Alerted = true;
|
|
2129
2218
|
}
|
|
2130
2219
|
return {
|
|
2131
2220
|
Success: false,
|
|
@@ -2141,7 +2230,7 @@ export class CheckoutSessionService {
|
|
|
2141
2230
|
order.CardIssuingCountry = paymentCheck.CardIssuingCountry;
|
|
2142
2231
|
// Payment has checked out, so from here a failure leaves a paid checkout without
|
|
2143
2232
|
// an order. Record the attempt; Confirm rolls back on failure, the record does not.
|
|
2144
|
-
confirmStep = await CheckoutStepLog.Begin(sessionID, 'Confirm',
|
|
2233
|
+
confirmStep = await CheckoutStepLog.Begin(sessionID, 'Confirm', trigger.Source, contextUser);
|
|
2145
2234
|
}
|
|
2146
2235
|
// Payment has checked out (or none was due): create the Persons the checkout needs.
|
|
2147
2236
|
if (newPayerFields) {
|
|
@@ -2184,7 +2273,7 @@ export class CheckoutSessionService {
|
|
|
2184
2273
|
}
|
|
2185
2274
|
// Book the PaymentHeader + allocation now that the order exists. retrieve-on-complete
|
|
2186
2275
|
// only stamps PaymentIntent.Status = Succeeded; AmountPaid stays 0 until CapturePayment.
|
|
2187
|
-
await this.applySettledPaymentToOrder(session, order, contextUser, { Source:
|
|
2276
|
+
trigger.Report.Capture = await this.applySettledPaymentToOrder(session, order, contextUser, { Source: trigger.Source });
|
|
2188
2277
|
// Mint the GuestOrder identity claim for the buyer's email so a later account
|
|
2189
2278
|
// (MJ core's IdentityClaimEngineServer) can attach the order + its entitlement
|
|
2190
2279
|
// grants on redemption or claim-on-login. Best-effort: a claim failure must never
|
|
@@ -2241,6 +2330,8 @@ export class CheckoutSessionService {
|
|
|
2241
2330
|
}
|
|
2242
2331
|
await CheckoutSessionService.revertSessionOpenAtomic(sessionID, md, contextUser);
|
|
2243
2332
|
session.Status = 'Open';
|
|
2333
|
+
// A thrown save or read may pass on a later attempt; a refusal above would not.
|
|
2334
|
+
trigger.Report.Retryable = true;
|
|
2244
2335
|
LogError(`[CheckoutSessionService] CompleteCheckout failed for session ${sessionID}: ${msg}`);
|
|
2245
2336
|
return {
|
|
2246
2337
|
Success: false,
|
|
@@ -2305,7 +2396,7 @@ export class CheckoutSessionService {
|
|
|
2305
2396
|
* booked through the ordinary checkout capture, which also replaces the renewal card. No order is
|
|
2306
2397
|
* created or confirmed here — the renewal order already is.
|
|
2307
2398
|
*/
|
|
2308
|
-
static async completeRenewalPayment(session, md, contextUser) {
|
|
2399
|
+
static async completeRenewalPayment(session, md, contextUser, trigger) {
|
|
2309
2400
|
const refuse = (message, status = session.Status) => ({
|
|
2310
2401
|
Success: false,
|
|
2311
2402
|
ErrorMessage: message,
|
|
@@ -2324,16 +2415,18 @@ export class CheckoutSessionService {
|
|
|
2324
2415
|
return refuse(paymentProblem.Message);
|
|
2325
2416
|
}
|
|
2326
2417
|
if (!(await this.latchSessionProcessingAtomic(session.ID, md, contextUser))) {
|
|
2418
|
+
trigger.Report.Retryable = true;
|
|
2327
2419
|
return refuse('Session is not in an Open status or was concurrently processed', 'Unknown');
|
|
2328
2420
|
}
|
|
2329
2421
|
if (!(await this.stampSessionConfirmedAtomic(session.ID, renewal.OrderID, md, contextUser))) {
|
|
2330
2422
|
await this.revertSessionOpenAtomic(session.ID, md, contextUser);
|
|
2423
|
+
trigger.Report.Retryable = true;
|
|
2331
2424
|
return refuse('The renewal payment could not be recorded. Please try again.', 'Open');
|
|
2332
2425
|
}
|
|
2333
2426
|
session.Status = 'Confirmed';
|
|
2334
2427
|
const order = await md.GetEntityObject(ORDER_HEADER_ENTITY, contextUser);
|
|
2335
2428
|
if (await order.Load(renewal.OrderID)) {
|
|
2336
|
-
await this.applySettledPaymentToOrder(session, order, contextUser, { Source:
|
|
2429
|
+
trigger.Report.Capture = await this.applySettledPaymentToOrder(session, order, contextUser, { Source: trigger.Source });
|
|
2337
2430
|
}
|
|
2338
2431
|
return {
|
|
2339
2432
|
Success: true,
|
|
@@ -2433,26 +2526,36 @@ export class CheckoutSessionService {
|
|
|
2433
2526
|
* 2. This method, from `payment_intent.succeeded` — including AlreadyApplied deliveries,
|
|
2434
2527
|
* so a 500 after the intent row is stamped still converges on the next Stripe retry.
|
|
2435
2528
|
*
|
|
2436
|
-
*
|
|
2529
|
+
* A session the payment settled for that is still Open was never completed: the buyer's page
|
|
2530
|
+
* closed before it asked (#724). This method completes it, as the browser would have. One that
|
|
2531
|
+
* expired first is past completing, and raises the "settled with no order" alert.
|
|
2532
|
+
*
|
|
2533
|
+
* No-ops when no checkout session owns the intent (back-office captures).
|
|
2437
2534
|
*/
|
|
2438
2535
|
static async BookSettledCheckoutPaymentIfNeeded(paymentIntentID, contextUser) {
|
|
2439
2536
|
if (!paymentIntentID) {
|
|
2440
2537
|
return { Attempted: false, Booked: false };
|
|
2441
2538
|
}
|
|
2442
|
-
const rv = new RunView();
|
|
2443
|
-
const found = await rv.RunView({
|
|
2444
|
-
EntityName: CHECKOUT_SESSION_ENTITY,
|
|
2445
|
-
ExtraFilter: `PaymentIntentID = '${EscapeText(paymentIntentID)}' AND Status = 'Confirmed'`,
|
|
2446
|
-
MaxRows: 1,
|
|
2447
|
-
ResultType: 'simple',
|
|
2448
|
-
}, contextUser);
|
|
2449
|
-
const sessionID = found?.Results?.[0]?.ID;
|
|
2450
|
-
if (!sessionID) {
|
|
2451
|
-
return { Attempted: false, Booked: false };
|
|
2452
|
-
}
|
|
2453
2539
|
const md = new Metadata();
|
|
2454
2540
|
const session = await md.GetEntityObject(CHECKOUT_SESSION_ENTITY, contextUser);
|
|
2455
|
-
|
|
2541
|
+
const sessionID = (await this.sessionIDForIntent(paymentIntentID, `Status = 'Confirmed'`, contextUser)) ??
|
|
2542
|
+
(await this.sessionIDForIntent(paymentIntentID, `Status IN ('Open', 'Processing', 'Expired')`, contextUser));
|
|
2543
|
+
if (!sessionID || !(await session.Load(sessionID))) {
|
|
2544
|
+
return { Attempted: false, Booked: false };
|
|
2545
|
+
}
|
|
2546
|
+
await this.awaitProcessingFinished(session);
|
|
2547
|
+
if (session.Status === 'Open') {
|
|
2548
|
+
return this.completeFromWebhook(session, md, contextUser);
|
|
2549
|
+
}
|
|
2550
|
+
if (session.Status === 'Processing') {
|
|
2551
|
+
return { Attempted: true, Booked: false, Retryable: true, ErrorMessage: 'the checkout is still being completed' };
|
|
2552
|
+
}
|
|
2553
|
+
if (session.Status === 'Expired') {
|
|
2554
|
+
const reason = 'The checkout expired before it was completed, so its settled payment has no order.';
|
|
2555
|
+
void raiseCheckoutSettledNotBookedAlert(session.ID, session.PaymentIntentID, reason, contextUser);
|
|
2556
|
+
return { Attempted: true, Booked: false, Retryable: false, ErrorMessage: reason };
|
|
2557
|
+
}
|
|
2558
|
+
if (session.Status !== 'Confirmed' || !session.DraftOrderID) {
|
|
2456
2559
|
return { Attempted: false, Booked: false };
|
|
2457
2560
|
}
|
|
2458
2561
|
const order = await md.GetEntityObject(ORDER_HEADER_ENTITY, contextUser);
|
|
@@ -2461,6 +2564,17 @@ export class CheckoutSessionService {
|
|
|
2461
2564
|
}
|
|
2462
2565
|
return this.applySettledPaymentToOrder(session, order, contextUser, { Source: 'Webhook' });
|
|
2463
2566
|
}
|
|
2567
|
+
/** The checkout session that opened this payment intent, among those `statusFilter` admits. */
|
|
2568
|
+
static async sessionIDForIntent(paymentIntentID, statusFilter, contextUser) {
|
|
2569
|
+
const rv = new RunView();
|
|
2570
|
+
const found = await rv.RunView({
|
|
2571
|
+
EntityName: CHECKOUT_SESSION_ENTITY,
|
|
2572
|
+
ExtraFilter: `PaymentIntentID = '${EscapeText(paymentIntentID)}' AND ${statusFilter}`,
|
|
2573
|
+
MaxRows: 1,
|
|
2574
|
+
ResultType: 'simple',
|
|
2575
|
+
}, contextUser);
|
|
2576
|
+
return found?.Results?.[0]?.ID;
|
|
2577
|
+
}
|
|
2464
2578
|
/**
|
|
2465
2579
|
* Operator replay of the Capture step (`Orders.ReplayCheckoutStep`, #326). The same idempotent
|
|
2466
2580
|
* CapturePayment as the complete call and the webhook, recorded as a Replay by the operator.
|
|
@@ -2577,7 +2691,10 @@ export class CheckoutSessionService {
|
|
|
2577
2691
|
}
|
|
2578
2692
|
try {
|
|
2579
2693
|
const op = new CapturePaymentOperation();
|
|
2580
|
-
|
|
2694
|
+
// On its own transaction, never the shared provider's (#927): the page and the webhook
|
|
2695
|
+
// capture the same session within a second of each other, and on one provider their
|
|
2696
|
+
// transactions nest, so a rollback on one side undid the other's payment.
|
|
2697
|
+
const result = await WithIsolatedProvider((captureProvider) => op.ExecuteServer({
|
|
2581
2698
|
PaymentIntentID: intent.ID,
|
|
2582
2699
|
Amount: due,
|
|
2583
2700
|
ReceivingCompanyID: order.CompanyID,
|
|
@@ -2594,10 +2711,10 @@ export class CheckoutSessionService {
|
|
|
2594
2711
|
: null,
|
|
2595
2712
|
IdempotencyKey: `checkout-complete:${session.ID}`,
|
|
2596
2713
|
}, {
|
|
2597
|
-
provider:
|
|
2714
|
+
provider: captureProvider,
|
|
2598
2715
|
user: contextUser,
|
|
2599
2716
|
emitProgress: () => undefined,
|
|
2600
|
-
});
|
|
2717
|
+
}), mdProvider);
|
|
2601
2718
|
const output = result.Output;
|
|
2602
2719
|
if (!result.Success || !output?.Success) {
|
|
2603
2720
|
const detail = result.ErrorMessage
|