@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.
Files changed (74) hide show
  1. package/dist/CheckoutSessionService.d.ts +44 -2
  2. package/dist/CheckoutSessionService.d.ts.map +1 -1
  3. package/dist/CheckoutSessionService.js +156 -39
  4. package/dist/CheckoutSessionService.js.map +1 -1
  5. package/dist/ConcessionApprovalListener.js +6 -1
  6. package/dist/ConcessionApprovalListener.js.map +1 -1
  7. package/dist/ConcessionApprovalTask.d.ts +40 -1
  8. package/dist/ConcessionApprovalTask.d.ts.map +1 -1
  9. package/dist/ConcessionApprovalTask.js +139 -15
  10. package/dist/ConcessionApprovalTask.js.map +1 -1
  11. package/dist/EntitlementEngine.d.ts +7 -1
  12. package/dist/EntitlementEngine.d.ts.map +1 -1
  13. package/dist/EntitlementEngine.js +70 -60
  14. package/dist/EntitlementEngine.js.map +1 -1
  15. package/dist/ExternalInvoiceBehavior.d.ts +4 -0
  16. package/dist/ExternalInvoiceBehavior.d.ts.map +1 -1
  17. package/dist/ExternalInvoiceBehavior.js +34 -3
  18. package/dist/ExternalInvoiceBehavior.js.map +1 -1
  19. package/dist/InvoiceDisplay.d.ts +2 -0
  20. package/dist/InvoiceDisplay.d.ts.map +1 -1
  21. package/dist/InvoiceDisplay.js +7 -3
  22. package/dist/InvoiceDisplay.js.map +1 -1
  23. package/dist/OrderConcessionEntityServer.d.ts +13 -3
  24. package/dist/OrderConcessionEntityServer.d.ts.map +1 -1
  25. package/dist/OrderConcessionEntityServer.js +64 -11
  26. package/dist/OrderConcessionEntityServer.js.map +1 -1
  27. package/dist/OrderEntityServer.d.ts +5 -0
  28. package/dist/OrderEntityServer.d.ts.map +1 -1
  29. package/dist/OrderEntityServer.js +120 -45
  30. package/dist/OrderEntityServer.js.map +1 -1
  31. package/dist/OrderJournalEntryFactory.d.ts +5 -3
  32. package/dist/OrderJournalEntryFactory.d.ts.map +1 -1
  33. package/dist/OrderJournalEntryFactory.js +56 -49
  34. package/dist/OrderJournalEntryFactory.js.map +1 -1
  35. package/dist/OrderLineEntityServer.d.ts +13 -0
  36. package/dist/OrderLineEntityServer.d.ts.map +1 -1
  37. package/dist/OrderLineEntityServer.js +91 -0
  38. package/dist/OrderLineEntityServer.js.map +1 -1
  39. package/dist/OrdersSettings.d.ts +9 -0
  40. package/dist/OrdersSettings.d.ts.map +1 -1
  41. package/dist/OrdersSettings.js +11 -0
  42. package/dist/OrdersSettings.js.map +1 -1
  43. package/dist/PaymentWebhookHandler.d.ts.map +1 -1
  44. package/dist/PaymentWebhookHandler.js +2 -1
  45. package/dist/PaymentWebhookHandler.js.map +1 -1
  46. package/dist/RevenueRecognition.d.ts +1 -1
  47. package/dist/RevenueRecognition.d.ts.map +1 -1
  48. package/dist/RevenueRecognition.js +12 -7
  49. package/dist/RevenueRecognition.js.map +1 -1
  50. package/dist/SubscriptionBehavior.d.ts +91 -9
  51. package/dist/SubscriptionBehavior.d.ts.map +1 -1
  52. package/dist/SubscriptionBehavior.js +134 -11
  53. package/dist/SubscriptionBehavior.js.map +1 -1
  54. package/dist/TermLengthExceptions.d.ts +84 -0
  55. package/dist/TermLengthExceptions.d.ts.map +1 -0
  56. package/dist/TermLengthExceptions.js +119 -0
  57. package/dist/TermLengthExceptions.js.map +1 -0
  58. package/dist/checkoutCaptureAlert.d.ts +3 -0
  59. package/dist/checkoutCaptureAlert.d.ts.map +1 -1
  60. package/dist/checkoutCaptureAlert.js +21 -3
  61. package/dist/checkoutCaptureAlert.js.map +1 -1
  62. package/dist/displaced-start-event.d.ts +1 -1
  63. package/dist/displaced-start-event.d.ts.map +1 -1
  64. package/dist/displaced-start-event.js +3 -1
  65. package/dist/displaced-start-event.js.map +1 -1
  66. package/dist/index.d.ts +1 -1
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +1 -1
  69. package/dist/index.js.map +1 -1
  70. package/dist/isolatedProvider.d.ts +22 -0
  71. package/dist/isolatedProvider.d.ts.map +1 -0
  72. package/dist/isolatedProvider.js +39 -0
  73. package/dist/isolatedProvider.js.map +1 -0
  74. 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
- /** Marks expired-but-still-Open sessions Expired. Bounded; best-effort. */
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
- * No-ops when the intent is not a confirmed checkout session (back-office captures).
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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"}
@@ -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
- /** Marks expired-but-still-Open sessions Expired. Bounded; best-effort. */
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
- const existingOrder = await md.GetEntityObject(ORDER_HEADER_ENTITY, contextUser);
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', 'Checkout', contextUser);
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: 'Checkout' });
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: 'Checkout' });
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
- * No-ops when the intent is not a confirmed checkout session (back-office captures).
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
- if (!(await session.Load(sessionID)) || !session.DraftOrderID) {
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
- const result = await op.ExecuteServer({
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: mdProvider,
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