@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.
@@ -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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"}
@@ -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
- /** Marks expired-but-still-Open sessions Expired. Bounded; best-effort. */
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
- 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
- };
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', 'Checkout', contextUser);
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: 'Checkout' });
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: 'Checkout' });
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
- * No-ops when the intent is not a confirmed checkout session (back-office captures).
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
- if (!(await session.Load(sessionID)) || !session.DraftOrderID) {
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.