@mj-biz-apps/orders-core-entities-server 5.36.0 → 5.36.1
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 +149 -36
- package/dist/CheckoutSessionService.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/PaymentWebhookHandler.d.ts.map +1 -1
- package/dist/PaymentWebhookHandler.js +2 -1
- package/dist/PaymentWebhookHandler.js.map +1 -1
- 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/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":"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
|
|
1
|
+
{"version":3,"file":"CheckoutSessionService.d.ts","sourceRoot":"","sources":["../src/CheckoutSessionService.ts"],"names":[],"mappings":"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"}
|
|
@@ -135,6 +135,13 @@ function SettledButShortMessage(cause) {
|
|
|
135
135
|
return `${what}, so your payment does not cover the total and the order was not placed. `
|
|
136
136
|
+ 'Our team has been notified and will arrange a refund.';
|
|
137
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* How long a caller that finds a session Processing waits for the other completion to finish. The
|
|
140
|
+
* browser's complete call and the payment webhook can both arrive for one payment (#724); the one
|
|
141
|
+
* that loses the latch waits for the winner and answers with its order.
|
|
142
|
+
*/
|
|
143
|
+
const PROCESSING_WAIT_MS = 30_000;
|
|
144
|
+
const PROCESSING_POLL_MS = 1_000;
|
|
138
145
|
export class CheckoutSessionService {
|
|
139
146
|
/**
|
|
140
147
|
* Resolves a public distribution slug, validates widget status, and mints an active CheckoutSession.
|
|
@@ -450,7 +457,12 @@ export class CheckoutSessionService {
|
|
|
450
457
|
}
|
|
451
458
|
return allowed;
|
|
452
459
|
}
|
|
453
|
-
/**
|
|
460
|
+
/**
|
|
461
|
+
* Marks expired-but-still-Open sessions Expired. Bounded; best-effort.
|
|
462
|
+
*
|
|
463
|
+
* A session whose payment settled has charged the buyer with no order, so expiring it raises the
|
|
464
|
+
* "settled with no order" alert: otherwise it would leave nothing for staff to see (#724).
|
|
465
|
+
*/
|
|
454
466
|
static async ReapExpiredOpenSessions(contextUser, limit = 50) {
|
|
455
467
|
const rv = new RunView();
|
|
456
468
|
const nowUtc = new Date().toISOString();
|
|
@@ -478,6 +490,9 @@ export class CheckoutSessionService {
|
|
|
478
490
|
session.Status = 'Expired';
|
|
479
491
|
if (await session.Save()) {
|
|
480
492
|
n++;
|
|
493
|
+
if (await this.sessionPaymentSettled(session, md, contextUser)) {
|
|
494
|
+
void raiseCheckoutSettledNotBookedAlert(session.ID, session.PaymentIntentID, 'The checkout expired after its payment settled, and no order was created.', contextUser);
|
|
495
|
+
}
|
|
481
496
|
}
|
|
482
497
|
}
|
|
483
498
|
catch (err) {
|
|
@@ -486,6 +501,27 @@ export class CheckoutSessionService {
|
|
|
486
501
|
}
|
|
487
502
|
return n;
|
|
488
503
|
}
|
|
504
|
+
/** True when the session's payment intent is recorded as settled. Reads our row, not the gateway. */
|
|
505
|
+
static async sessionPaymentSettled(session, md, contextUser) {
|
|
506
|
+
if (!session.PaymentIntentID) {
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
const intent = await md.GetEntityObject(PAYMENT_INTENT_ENTITY, contextUser);
|
|
510
|
+
return (await intent.Load(session.PaymentIntentID)) && SETTLED_INTENT_STATUSES.includes(intent.Status);
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Waits, up to `PROCESSING_WAIT_MS`, for a session another caller latched to Processing to leave
|
|
514
|
+
* that state, reloading it in place. Returns at once when it is not Processing.
|
|
515
|
+
*/
|
|
516
|
+
static async awaitProcessingFinished(session) {
|
|
517
|
+
const deadline = Date.now() + PROCESSING_WAIT_MS;
|
|
518
|
+
while (session.Status === 'Processing' && Date.now() < deadline) {
|
|
519
|
+
await new Promise((resolve) => setTimeout(resolve, PROCESSING_POLL_MS));
|
|
520
|
+
if (!(await session.Load(session.ID))) {
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
489
525
|
/**
|
|
490
526
|
* Verifies a caller-presented client session key against the session row's stored key.
|
|
491
527
|
* The session GUID alone must never be sufficient to drive a session — both values are
|
|
@@ -1784,27 +1820,13 @@ export class CheckoutSessionService {
|
|
|
1784
1820
|
Status: session.Status
|
|
1785
1821
|
};
|
|
1786
1822
|
}
|
|
1823
|
+
// The payment webhook may be completing this session right now (#724): answer with its order.
|
|
1824
|
+
await this.awaitProcessingFinished(session);
|
|
1787
1825
|
// Replay safety: a session that already completed returns its existing order rather
|
|
1788
1826
|
// than an error — a network retry after a successful booking must never double-book
|
|
1789
1827
|
// and must never strand the buyer without their order id.
|
|
1790
1828
|
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
|
-
};
|
|
1829
|
+
return this.replayConfirmedSession(session, md, contextUser, { Source: 'Checkout', Report: {} });
|
|
1808
1830
|
}
|
|
1809
1831
|
if (session.Status !== 'Open') {
|
|
1810
1832
|
return {
|
|
@@ -1822,12 +1844,77 @@ export class CheckoutSessionService {
|
|
|
1822
1844
|
Status: 'Expired'
|
|
1823
1845
|
};
|
|
1824
1846
|
}
|
|
1847
|
+
return this.completeOpenSession(session, md, contextUser, { Source: 'Checkout', Evidence: evidence, Report: {} });
|
|
1848
|
+
}
|
|
1849
|
+
/**
|
|
1850
|
+
* Completes a session paid for at the gateway whose browser never asked (#724): the buyer closed
|
|
1851
|
+
* the tab, or lost the page, after the payment succeeded. Reached from the payment webhook, so there
|
|
1852
|
+
* is no client session key: the signature-verified event is the proof, and the completion checks
|
|
1853
|
+
* the intent's state and amount against the re-priced order exactly as the browser's call does.
|
|
1854
|
+
*
|
|
1855
|
+
* The session expiry is not enforced: the buyer paid inside it, and expiry stops a session from
|
|
1856
|
+
* being driven further, not a paid one from being booked.
|
|
1857
|
+
*
|
|
1858
|
+
* Any refusal raises the "settled with no order" alert, since nobody is on the page to see it.
|
|
1859
|
+
*/
|
|
1860
|
+
static async completeFromWebhook(session, md, contextUser) {
|
|
1861
|
+
const trigger = { Source: 'Webhook', Report: {} };
|
|
1862
|
+
const result = await this.completeOpenSession(session, md, contextUser, trigger);
|
|
1863
|
+
if (result.Success) {
|
|
1864
|
+
return trigger.Report.Capture ?? { Attempted: true, Booked: true };
|
|
1865
|
+
}
|
|
1866
|
+
const reason = result.ErrorMessage ?? 'the checkout could not be completed';
|
|
1867
|
+
if (trigger.Report.Retryable) {
|
|
1868
|
+
return { Attempted: true, Booked: false, Retryable: true, ErrorMessage: reason };
|
|
1869
|
+
}
|
|
1870
|
+
if (!trigger.Report.Alerted) {
|
|
1871
|
+
void raiseCheckoutSettledNotBookedAlert(session.ID, session.PaymentIntentID, reason, contextUser);
|
|
1872
|
+
}
|
|
1873
|
+
return { Attempted: true, Booked: false, Retryable: false, ErrorMessage: reason };
|
|
1874
|
+
}
|
|
1875
|
+
/**
|
|
1876
|
+
* The existing order of a Confirmed session, booking its payment if the first completion did
|
|
1877
|
+
* not. CapturePayment is idempotent on `checkout-complete:${session.ID}`, so this is one of the
|
|
1878
|
+
* retries; `payment_intent.succeeded` is the other (BookSettledCheckoutPaymentIfNeeded, including
|
|
1879
|
+
* on AlreadyApplied webhook deliveries so a 500 after applyEvent still converges).
|
|
1880
|
+
*/
|
|
1881
|
+
static async replayConfirmedSession(session, md, contextUser, trigger) {
|
|
1882
|
+
const orderID = session.DraftOrderID ?? '';
|
|
1883
|
+
const existingOrder = await md.GetEntityObject(ORDER_HEADER_ENTITY, contextUser);
|
|
1884
|
+
const orderLoaded = await existingOrder.Load(orderID);
|
|
1885
|
+
if (orderLoaded) {
|
|
1886
|
+
trigger.Report.Capture = await this.applySettledPaymentToOrder(session, existingOrder, contextUser, { Source: trigger.Source });
|
|
1887
|
+
}
|
|
1888
|
+
return {
|
|
1889
|
+
Success: true,
|
|
1890
|
+
SessionID: session.ID,
|
|
1891
|
+
Status: 'Confirmed',
|
|
1892
|
+
OrderID: orderID,
|
|
1893
|
+
OrderNumber: orderLoaded ? (existingOrder.OrderNumber || orderID) : orderID,
|
|
1894
|
+
TotalGross: orderLoaded ? (existingOrder.TotalGross ?? undefined) : undefined
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
/**
|
|
1898
|
+
* Builds and confirms the order for an Open session — the work behind both the browser's
|
|
1899
|
+
* complete call and the payment webhook. The caller has checked the session may be completed.
|
|
1900
|
+
*/
|
|
1901
|
+
static async completeOpenSession(session, md, contextUser, trigger) {
|
|
1902
|
+
const sessionID = session.ID;
|
|
1903
|
+
const evidence = trigger.Evidence;
|
|
1825
1904
|
if (session.DraftOrderID) {
|
|
1826
|
-
return this.completeRenewalPayment(session, md, contextUser);
|
|
1905
|
+
return this.completeRenewalPayment(session, md, contextUser, trigger);
|
|
1827
1906
|
}
|
|
1828
1907
|
// Atomically latch status to Processing to prevent concurrent duplicate checkout processing
|
|
1829
1908
|
const latched = await this.latchSessionProcessingAtomic(sessionID, md, contextUser);
|
|
1830
1909
|
if (!latched) {
|
|
1910
|
+
// The other caller for this payment (browser or webhook) holds the latch: wait for it.
|
|
1911
|
+
if (await session.Load(sessionID)) {
|
|
1912
|
+
await this.awaitProcessingFinished(session);
|
|
1913
|
+
if (session.Status === 'Confirmed' && session.DraftOrderID) {
|
|
1914
|
+
return this.replayConfirmedSession(session, md, contextUser, trigger);
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
trigger.Report.Retryable = true;
|
|
1831
1918
|
return {
|
|
1832
1919
|
Success: false,
|
|
1833
1920
|
ErrorMessage: 'Session is not in an Open status or was concurrently processed',
|
|
@@ -2126,6 +2213,7 @@ export class CheckoutSessionService {
|
|
|
2126
2213
|
? `Promotion code ${typedCodeDeclined.Code} was declined on re-price (${typedCodeDeclined.Reason}). ${paymentFailure.Message}`
|
|
2127
2214
|
: paymentFailure.Message;
|
|
2128
2215
|
void raiseCheckoutSettledNotBookedAlert(sessionID, session.PaymentIntentID, reason, contextUser);
|
|
2216
|
+
trigger.Report.Alerted = true;
|
|
2129
2217
|
}
|
|
2130
2218
|
return {
|
|
2131
2219
|
Success: false,
|
|
@@ -2141,7 +2229,7 @@ export class CheckoutSessionService {
|
|
|
2141
2229
|
order.CardIssuingCountry = paymentCheck.CardIssuingCountry;
|
|
2142
2230
|
// Payment has checked out, so from here a failure leaves a paid checkout without
|
|
2143
2231
|
// an order. Record the attempt; Confirm rolls back on failure, the record does not.
|
|
2144
|
-
confirmStep = await CheckoutStepLog.Begin(sessionID, 'Confirm',
|
|
2232
|
+
confirmStep = await CheckoutStepLog.Begin(sessionID, 'Confirm', trigger.Source, contextUser);
|
|
2145
2233
|
}
|
|
2146
2234
|
// Payment has checked out (or none was due): create the Persons the checkout needs.
|
|
2147
2235
|
if (newPayerFields) {
|
|
@@ -2184,7 +2272,7 @@ export class CheckoutSessionService {
|
|
|
2184
2272
|
}
|
|
2185
2273
|
// Book the PaymentHeader + allocation now that the order exists. retrieve-on-complete
|
|
2186
2274
|
// only stamps PaymentIntent.Status = Succeeded; AmountPaid stays 0 until CapturePayment.
|
|
2187
|
-
await this.applySettledPaymentToOrder(session, order, contextUser, { Source:
|
|
2275
|
+
trigger.Report.Capture = await this.applySettledPaymentToOrder(session, order, contextUser, { Source: trigger.Source });
|
|
2188
2276
|
// Mint the GuestOrder identity claim for the buyer's email so a later account
|
|
2189
2277
|
// (MJ core's IdentityClaimEngineServer) can attach the order + its entitlement
|
|
2190
2278
|
// grants on redemption or claim-on-login. Best-effort: a claim failure must never
|
|
@@ -2241,6 +2329,8 @@ export class CheckoutSessionService {
|
|
|
2241
2329
|
}
|
|
2242
2330
|
await CheckoutSessionService.revertSessionOpenAtomic(sessionID, md, contextUser);
|
|
2243
2331
|
session.Status = 'Open';
|
|
2332
|
+
// A thrown save or read may pass on a later attempt; a refusal above would not.
|
|
2333
|
+
trigger.Report.Retryable = true;
|
|
2244
2334
|
LogError(`[CheckoutSessionService] CompleteCheckout failed for session ${sessionID}: ${msg}`);
|
|
2245
2335
|
return {
|
|
2246
2336
|
Success: false,
|
|
@@ -2305,7 +2395,7 @@ export class CheckoutSessionService {
|
|
|
2305
2395
|
* booked through the ordinary checkout capture, which also replaces the renewal card. No order is
|
|
2306
2396
|
* created or confirmed here — the renewal order already is.
|
|
2307
2397
|
*/
|
|
2308
|
-
static async completeRenewalPayment(session, md, contextUser) {
|
|
2398
|
+
static async completeRenewalPayment(session, md, contextUser, trigger) {
|
|
2309
2399
|
const refuse = (message, status = session.Status) => ({
|
|
2310
2400
|
Success: false,
|
|
2311
2401
|
ErrorMessage: message,
|
|
@@ -2324,16 +2414,18 @@ export class CheckoutSessionService {
|
|
|
2324
2414
|
return refuse(paymentProblem.Message);
|
|
2325
2415
|
}
|
|
2326
2416
|
if (!(await this.latchSessionProcessingAtomic(session.ID, md, contextUser))) {
|
|
2417
|
+
trigger.Report.Retryable = true;
|
|
2327
2418
|
return refuse('Session is not in an Open status or was concurrently processed', 'Unknown');
|
|
2328
2419
|
}
|
|
2329
2420
|
if (!(await this.stampSessionConfirmedAtomic(session.ID, renewal.OrderID, md, contextUser))) {
|
|
2330
2421
|
await this.revertSessionOpenAtomic(session.ID, md, contextUser);
|
|
2422
|
+
trigger.Report.Retryable = true;
|
|
2331
2423
|
return refuse('The renewal payment could not be recorded. Please try again.', 'Open');
|
|
2332
2424
|
}
|
|
2333
2425
|
session.Status = 'Confirmed';
|
|
2334
2426
|
const order = await md.GetEntityObject(ORDER_HEADER_ENTITY, contextUser);
|
|
2335
2427
|
if (await order.Load(renewal.OrderID)) {
|
|
2336
|
-
await this.applySettledPaymentToOrder(session, order, contextUser, { Source:
|
|
2428
|
+
trigger.Report.Capture = await this.applySettledPaymentToOrder(session, order, contextUser, { Source: trigger.Source });
|
|
2337
2429
|
}
|
|
2338
2430
|
return {
|
|
2339
2431
|
Success: true,
|
|
@@ -2433,26 +2525,36 @@ export class CheckoutSessionService {
|
|
|
2433
2525
|
* 2. This method, from `payment_intent.succeeded` — including AlreadyApplied deliveries,
|
|
2434
2526
|
* so a 500 after the intent row is stamped still converges on the next Stripe retry.
|
|
2435
2527
|
*
|
|
2436
|
-
*
|
|
2528
|
+
* A session the payment settled for that is still Open was never completed: the buyer's page
|
|
2529
|
+
* closed before it asked (#724). This method completes it, as the browser would have. One that
|
|
2530
|
+
* expired first is past completing, and raises the "settled with no order" alert.
|
|
2531
|
+
*
|
|
2532
|
+
* No-ops when no checkout session owns the intent (back-office captures).
|
|
2437
2533
|
*/
|
|
2438
2534
|
static async BookSettledCheckoutPaymentIfNeeded(paymentIntentID, contextUser) {
|
|
2439
2535
|
if (!paymentIntentID) {
|
|
2440
2536
|
return { Attempted: false, Booked: false };
|
|
2441
2537
|
}
|
|
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
2538
|
const md = new Metadata();
|
|
2454
2539
|
const session = await md.GetEntityObject(CHECKOUT_SESSION_ENTITY, contextUser);
|
|
2455
|
-
|
|
2540
|
+
const sessionID = (await this.sessionIDForIntent(paymentIntentID, `Status = 'Confirmed'`, contextUser)) ??
|
|
2541
|
+
(await this.sessionIDForIntent(paymentIntentID, `Status IN ('Open', 'Processing', 'Expired')`, contextUser));
|
|
2542
|
+
if (!sessionID || !(await session.Load(sessionID))) {
|
|
2543
|
+
return { Attempted: false, Booked: false };
|
|
2544
|
+
}
|
|
2545
|
+
await this.awaitProcessingFinished(session);
|
|
2546
|
+
if (session.Status === 'Open') {
|
|
2547
|
+
return this.completeFromWebhook(session, md, contextUser);
|
|
2548
|
+
}
|
|
2549
|
+
if (session.Status === 'Processing') {
|
|
2550
|
+
return { Attempted: true, Booked: false, Retryable: true, ErrorMessage: 'the checkout is still being completed' };
|
|
2551
|
+
}
|
|
2552
|
+
if (session.Status === 'Expired') {
|
|
2553
|
+
const reason = 'The checkout expired before it was completed, so its settled payment has no order.';
|
|
2554
|
+
void raiseCheckoutSettledNotBookedAlert(session.ID, session.PaymentIntentID, reason, contextUser);
|
|
2555
|
+
return { Attempted: true, Booked: false, Retryable: false, ErrorMessage: reason };
|
|
2556
|
+
}
|
|
2557
|
+
if (session.Status !== 'Confirmed' || !session.DraftOrderID) {
|
|
2456
2558
|
return { Attempted: false, Booked: false };
|
|
2457
2559
|
}
|
|
2458
2560
|
const order = await md.GetEntityObject(ORDER_HEADER_ENTITY, contextUser);
|
|
@@ -2461,6 +2563,17 @@ export class CheckoutSessionService {
|
|
|
2461
2563
|
}
|
|
2462
2564
|
return this.applySettledPaymentToOrder(session, order, contextUser, { Source: 'Webhook' });
|
|
2463
2565
|
}
|
|
2566
|
+
/** The checkout session that opened this payment intent, among those `statusFilter` admits. */
|
|
2567
|
+
static async sessionIDForIntent(paymentIntentID, statusFilter, contextUser) {
|
|
2568
|
+
const rv = new RunView();
|
|
2569
|
+
const found = await rv.RunView({
|
|
2570
|
+
EntityName: CHECKOUT_SESSION_ENTITY,
|
|
2571
|
+
ExtraFilter: `PaymentIntentID = '${EscapeText(paymentIntentID)}' AND ${statusFilter}`,
|
|
2572
|
+
MaxRows: 1,
|
|
2573
|
+
ResultType: 'simple',
|
|
2574
|
+
}, contextUser);
|
|
2575
|
+
return found?.Results?.[0]?.ID;
|
|
2576
|
+
}
|
|
2464
2577
|
/**
|
|
2465
2578
|
* Operator replay of the Capture step (`Orders.ReplayCheckoutStep`, #326). The same idempotent
|
|
2466
2579
|
* CapturePayment as the complete call and the webhook, recorded as a Replay by the operator.
|