@tellescope/sdk 1.256.6 → 1.256.8

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 (128) hide show
  1. package/lib/cjs/enduser.d.ts +1 -1
  2. package/lib/cjs/enduser.d.ts.map +1 -1
  3. package/lib/cjs/enduser.js +1 -0
  4. package/lib/cjs/enduser.js.map +1 -1
  5. package/lib/cjs/tests/api_tests/calendar_event_create_and_bulk_triggers.test.d.ts +6 -0
  6. package/lib/cjs/tests/api_tests/calendar_event_create_and_bulk_triggers.test.d.ts.map +1 -0
  7. package/lib/cjs/tests/api_tests/calendar_event_create_and_bulk_triggers.test.js +505 -0
  8. package/lib/cjs/tests/api_tests/calendar_event_create_and_bulk_triggers.test.js.map +1 -0
  9. package/lib/cjs/tests/api_tests/configurations_public_read.test.d.ts +6 -0
  10. package/lib/cjs/tests/api_tests/configurations_public_read.test.d.ts.map +1 -0
  11. package/lib/cjs/tests/api_tests/configurations_public_read.test.js +173 -0
  12. package/lib/cjs/tests/api_tests/configurations_public_read.test.js.map +1 -0
  13. package/lib/cjs/tests/api_tests/form_started_trigger.test.d.ts.map +1 -1
  14. package/lib/cjs/tests/api_tests/form_started_trigger.test.js +87 -24
  15. package/lib/cjs/tests/api_tests/form_started_trigger.test.js.map +1 -1
  16. package/lib/cjs/tests/api_tests/form_translations.test.d.ts +6 -0
  17. package/lib/cjs/tests/api_tests/form_translations.test.d.ts.map +1 -0
  18. package/lib/cjs/tests/api_tests/form_translations.test.js +259 -0
  19. package/lib/cjs/tests/api_tests/form_translations.test.js.map +1 -0
  20. package/lib/cjs/tests/api_tests/group_event_attendee_self_update.test.d.ts +6 -0
  21. package/lib/cjs/tests/api_tests/group_event_attendee_self_update.test.d.ts.map +1 -0
  22. package/lib/cjs/tests/api_tests/group_event_attendee_self_update.test.js +293 -0
  23. package/lib/cjs/tests/api_tests/group_event_attendee_self_update.test.js.map +1 -0
  24. package/lib/cjs/tests/api_tests/group_event_attendee_status_triggers.test.d.ts.map +1 -1
  25. package/lib/cjs/tests/api_tests/group_event_attendee_status_triggers.test.js +340 -124
  26. package/lib/cjs/tests/api_tests/group_event_attendee_status_triggers.test.js.map +1 -1
  27. package/lib/cjs/tests/api_tests/mdi_trigger_refill.test.d.ts +6 -0
  28. package/lib/cjs/tests/api_tests/mdi_trigger_refill.test.d.ts.map +1 -0
  29. package/lib/cjs/tests/api_tests/mdi_trigger_refill.test.js +277 -0
  30. package/lib/cjs/tests/api_tests/mdi_trigger_refill.test.js.map +1 -0
  31. package/lib/cjs/tests/api_tests/purchase_made_trigger.test.d.ts.map +1 -1
  32. package/lib/cjs/tests/api_tests/purchase_made_trigger.test.js +262 -78
  33. package/lib/cjs/tests/api_tests/purchase_made_trigger.test.js.map +1 -1
  34. package/lib/cjs/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.d.ts +31 -0
  35. package/lib/cjs/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.d.ts.map +1 -0
  36. package/lib/cjs/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.js +315 -0
  37. package/lib/cjs/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.js.map +1 -0
  38. package/lib/cjs/tests/api_tests/security/public_endpoint_auth_field_lock.test.d.ts.map +1 -1
  39. package/lib/cjs/tests/api_tests/security/public_endpoint_auth_field_lock.test.js +206 -198
  40. package/lib/cjs/tests/api_tests/security/public_endpoint_auth_field_lock.test.js.map +1 -1
  41. package/lib/cjs/tests/api_tests/stripe_webhook_triggers.test.d.ts.map +1 -1
  42. package/lib/cjs/tests/api_tests/stripe_webhook_triggers.test.js +343 -80
  43. package/lib/cjs/tests/api_tests/stripe_webhook_triggers.test.js.map +1 -1
  44. package/lib/cjs/tests/api_tests/welle_partner_token.test.d.ts.map +1 -1
  45. package/lib/cjs/tests/api_tests/welle_partner_token.test.js +74 -35
  46. package/lib/cjs/tests/api_tests/welle_partner_token.test.js.map +1 -1
  47. package/lib/cjs/tests/stripe_webhook_helpers.d.ts +134 -0
  48. package/lib/cjs/tests/stripe_webhook_helpers.d.ts.map +1 -1
  49. package/lib/cjs/tests/stripe_webhook_helpers.js +126 -1
  50. package/lib/cjs/tests/stripe_webhook_helpers.js.map +1 -1
  51. package/lib/cjs/tests/tests.d.ts.map +1 -1
  52. package/lib/cjs/tests/tests.js +242 -212
  53. package/lib/cjs/tests/tests.js.map +1 -1
  54. package/lib/esm/enduser.d.ts +2 -1
  55. package/lib/esm/enduser.d.ts.map +1 -1
  56. package/lib/esm/enduser.js +1 -0
  57. package/lib/esm/enduser.js.map +1 -1
  58. package/lib/esm/sdk.d.ts +3 -2
  59. package/lib/esm/sdk.d.ts.map +1 -1
  60. package/lib/esm/session.d.ts +1 -0
  61. package/lib/esm/session.d.ts.map +1 -1
  62. package/lib/esm/tests/api_tests/calendar_event_create_and_bulk_triggers.test.d.ts +6 -0
  63. package/lib/esm/tests/api_tests/calendar_event_create_and_bulk_triggers.test.d.ts.map +1 -0
  64. package/lib/esm/tests/api_tests/calendar_event_create_and_bulk_triggers.test.js +501 -0
  65. package/lib/esm/tests/api_tests/calendar_event_create_and_bulk_triggers.test.js.map +1 -0
  66. package/lib/esm/tests/api_tests/configurations_public_read.test.d.ts +6 -0
  67. package/lib/esm/tests/api_tests/configurations_public_read.test.d.ts.map +1 -0
  68. package/lib/esm/tests/api_tests/configurations_public_read.test.js +169 -0
  69. package/lib/esm/tests/api_tests/configurations_public_read.test.js.map +1 -0
  70. package/lib/esm/tests/api_tests/form_started_trigger.test.d.ts.map +1 -1
  71. package/lib/esm/tests/api_tests/form_started_trigger.test.js +87 -24
  72. package/lib/esm/tests/api_tests/form_started_trigger.test.js.map +1 -1
  73. package/lib/esm/tests/api_tests/form_translations.test.d.ts +6 -0
  74. package/lib/esm/tests/api_tests/form_translations.test.d.ts.map +1 -0
  75. package/lib/esm/tests/api_tests/form_translations.test.js +255 -0
  76. package/lib/esm/tests/api_tests/form_translations.test.js.map +1 -0
  77. package/lib/esm/tests/api_tests/group_event_attendee_self_update.test.d.ts +6 -0
  78. package/lib/esm/tests/api_tests/group_event_attendee_self_update.test.d.ts.map +1 -0
  79. package/lib/esm/tests/api_tests/group_event_attendee_self_update.test.js +289 -0
  80. package/lib/esm/tests/api_tests/group_event_attendee_self_update.test.js.map +1 -0
  81. package/lib/esm/tests/api_tests/group_event_attendee_status_triggers.test.d.ts.map +1 -1
  82. package/lib/esm/tests/api_tests/group_event_attendee_status_triggers.test.js +340 -124
  83. package/lib/esm/tests/api_tests/group_event_attendee_status_triggers.test.js.map +1 -1
  84. package/lib/esm/tests/api_tests/mdi_trigger_refill.test.d.ts +6 -0
  85. package/lib/esm/tests/api_tests/mdi_trigger_refill.test.d.ts.map +1 -0
  86. package/lib/esm/tests/api_tests/mdi_trigger_refill.test.js +273 -0
  87. package/lib/esm/tests/api_tests/mdi_trigger_refill.test.js.map +1 -0
  88. package/lib/esm/tests/api_tests/purchase_made_trigger.test.d.ts.map +1 -1
  89. package/lib/esm/tests/api_tests/purchase_made_trigger.test.js +262 -78
  90. package/lib/esm/tests/api_tests/purchase_made_trigger.test.js.map +1 -1
  91. package/lib/esm/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.d.ts +31 -0
  92. package/lib/esm/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.d.ts.map +1 -0
  93. package/lib/esm/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.js +311 -0
  94. package/lib/esm/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.js.map +1 -0
  95. package/lib/esm/tests/api_tests/security/public_endpoint_auth_field_lock.test.d.ts.map +1 -1
  96. package/lib/esm/tests/api_tests/security/public_endpoint_auth_field_lock.test.js +206 -198
  97. package/lib/esm/tests/api_tests/security/public_endpoint_auth_field_lock.test.js.map +1 -1
  98. package/lib/esm/tests/api_tests/stripe_webhook_triggers.test.d.ts.map +1 -1
  99. package/lib/esm/tests/api_tests/stripe_webhook_triggers.test.js +344 -81
  100. package/lib/esm/tests/api_tests/stripe_webhook_triggers.test.js.map +1 -1
  101. package/lib/esm/tests/api_tests/welle_partner_token.test.d.ts.map +1 -1
  102. package/lib/esm/tests/api_tests/welle_partner_token.test.js +74 -35
  103. package/lib/esm/tests/api_tests/welle_partner_token.test.js.map +1 -1
  104. package/lib/esm/tests/stripe_webhook_helpers.d.ts +134 -0
  105. package/lib/esm/tests/stripe_webhook_helpers.d.ts.map +1 -1
  106. package/lib/esm/tests/stripe_webhook_helpers.js +119 -0
  107. package/lib/esm/tests/stripe_webhook_helpers.js.map +1 -1
  108. package/lib/esm/tests/tests.d.ts.map +1 -1
  109. package/lib/esm/tests/tests.js +242 -212
  110. package/lib/esm/tests/tests.js.map +1 -1
  111. package/lib/tsconfig.tsbuildinfo +1 -1
  112. package/package.json +10 -10
  113. package/src/enduser.ts +4 -2
  114. package/src/tests/api_tests/calendar_event_create_and_bulk_triggers.test.ts +293 -0
  115. package/src/tests/api_tests/configurations_public_read.test.ts +116 -0
  116. package/src/tests/api_tests/form_started_trigger.test.ts +49 -1
  117. package/src/tests/api_tests/form_translations.test.ts +205 -0
  118. package/src/tests/api_tests/group_event_attendee_self_update.test.ts +161 -0
  119. package/src/tests/api_tests/group_event_attendee_status_triggers.test.ts +135 -0
  120. package/src/tests/api_tests/mdi_trigger_refill.test.ts +184 -0
  121. package/src/tests/api_tests/purchase_made_trigger.test.ts +137 -0
  122. package/src/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.ts +204 -0
  123. package/src/tests/api_tests/security/public_endpoint_auth_field_lock.test.ts +16 -6
  124. package/src/tests/api_tests/stripe_webhook_triggers.test.ts +221 -0
  125. package/src/tests/api_tests/welle_partner_token.test.ts +49 -6
  126. package/src/tests/stripe_webhook_helpers.ts +116 -0
  127. package/src/tests/tests.ts +29 -12
  128. package/test_generated.pdf +0 -0
@@ -4,7 +4,13 @@ import { Session } from "../../sdk"
4
4
  import { assert, log_header, wait } from "@tellescope/testing"
5
5
  import { setup_tests } from "../setup"
6
6
  import {
7
+ charge_succeeded_event,
7
8
  cleanup_stripe_test_integrations,
9
+ discount_with_coupon,
10
+ discount_with_promo_code,
11
+ discount_with_promo_code_ref,
12
+ invoice_paid_event,
13
+ invoice_paid_line,
8
14
  invoice_payment_failed_event,
9
15
  item_with_expanded_price_product,
10
16
  item_with_plan_product,
@@ -640,6 +646,221 @@ export const stripe_webhook_triggers_tests = async ({ sdk, sdkNonAdmin } : { sdk
640
646
  }
641
647
  assert(rejectedInvalidPausedProductId, 'expected a non-ObjectId productId to be rejected', 'invalid Subscription Paused productId rejected')
642
648
 
649
+ /* ============ invoice.paid / charge.succeeded — coupon codes ============ */
650
+
651
+ // Purchases are created by the webhook handler itself, so persistence is polled the same way
652
+ // tags are. Cleaned up automatically when the enduser is deleted (onDependencyDelete: 'delete').
653
+ const poll_for_purchase = async (enduserId: string, iterations = 20) => {
654
+ for (let i = 0; i < iterations; i++) {
655
+ await wait(undefined, 500)
656
+ const purchases = await sdk.api.purchases.getSome({ filter: { enduserId } })
657
+ if (purchases.length) return purchases
658
+ }
659
+ return []
660
+ }
661
+
662
+ // One webhook POST per case: a Purchase Made trigger (coupon-scoped unless couponCodes is
663
+ // undefined) and an invoice.paid built by the caller.
664
+ const run_coupon_invoice_case = async ({ label, couponCodes, discounts, discount, shouldFire, title } : {
665
+ label: string,
666
+ couponCodes?: string[],
667
+ discounts?: (object | string)[],
668
+ discount?: object,
669
+ shouldFire: boolean,
670
+ title: string,
671
+ }) => {
672
+ const caseTag = tag(label)
673
+ const enduser = await new_enduser(label)
674
+ await new_trigger({
675
+ title: `Stripe purchase made (${label}) ${RUN}`,
676
+ event: { type: 'Purchase Made', info: couponCodes !== undefined ? { couponCodes } : {} },
677
+ tags: [caseTag],
678
+ })
679
+
680
+ const res = await post_stripe_webhook({
681
+ stripePublicKey,
682
+ secret: webhooksSecret,
683
+ event: invoice_paid_event({
684
+ stripeCustomerId: enduser.stripeCustomerId,
685
+ lines: [invoice_paid_line({ description: `Coupon Test ${label} ${RUN}` })],
686
+ discounts,
687
+ discount,
688
+ }),
689
+ })
690
+ assert(res.status === 200, `expected 200, got ${res.status} (${res.data})`, `${label} webhook accepted`)
691
+
692
+ if (shouldFire) await expect_fired(enduser.id, caseTag, title)
693
+ else await expect_not_fired(enduser.id, caseTag, title)
694
+
695
+ return enduser
696
+ }
697
+
698
+ // C1: the feature — a coupon-scoped trigger fires for an invoice discounted with that promo
699
+ // code, and the code is persisted on the purchase record. The expanded promotion_code object
700
+ // reflects the post-expansion shape (and the checkout-session breakdown shape), not the raw
701
+ // webhook — it's the only way to E2E-prove the promo-code match without outbound Stripe calls;
702
+ // C1b covers the raw shape.
703
+ const c1Enduser = await run_coupon_invoice_case({
704
+ label: 'c1-matching-promo',
705
+ couponCodes: ['SUMMER20'],
706
+ discounts: [discount_with_promo_code('SUMMER20')],
707
+ shouldFire: true,
708
+ title: 'coupon-scoped Purchase Made trigger fires for a matching promo code',
709
+ })
710
+ const c1Purchases = await poll_for_purchase(c1Enduser.id)
711
+ assert(
712
+ c1Purchases.some(p => (p as any).couponCodes?.includes('SUMMER20')),
713
+ `expected a purchase with couponCodes containing SUMMER20, got ${JSON.stringify(c1Purchases.map(p => (p as any).couponCodes))}`,
714
+ 'couponCodes persisted on the webhook-created purchase',
715
+ )
716
+
717
+ // C1b: the raw webhook shape, verified against a live sandbox invoice.paid event — the single
718
+ // discount embedded at invoice.discount, only its bare "di_..." string id in invoice.discounts
719
+ // (the dedupe must not trigger a re-fetch), and promotion_code as a bare promo_... string whose
720
+ // retrieve fails under this suite's fake API key. The coupon name + id are still captured
721
+ // call-free, a name-scoped trigger fires, and the failed promo retrieve is proven non-fatal.
722
+ // (No absence assertion on the promo code: in production the retrieve succeeds.)
723
+ const c1bDiscount = discount_with_promo_code_ref({ couponId: `coupon_test_c1b_${RUN}`, couponName: 'Test coupon' })
724
+ const c1bEnduser = await run_coupon_invoice_case({
725
+ label: 'c1b-real-shape',
726
+ couponCodes: ['Test coupon'],
727
+ discount: c1bDiscount,
728
+ discounts: [(c1bDiscount as any).id],
729
+ shouldFire: true,
730
+ title: 'real-shaped invoice.paid payload matches via coupon name',
731
+ })
732
+ const c1bPurchases = await poll_for_purchase(c1bEnduser.id)
733
+ assert(
734
+ c1bPurchases.some(p =>
735
+ (p as any).couponCodes?.includes('Test coupon') && (p as any).couponCodes?.includes(`coupon_test_c1b_${RUN}`)
736
+ ),
737
+ `expected couponCodes with the coupon name and id, got ${JSON.stringify(c1bPurchases.map(p => (p as any).couponCodes))}`,
738
+ 'coupon name and id persisted from a real-shaped payload',
739
+ )
740
+
741
+ // C2: matching is case-sensitive — Stripe sends the canonical merchant-defined code, so a
742
+ // differently-cased trigger value must stay quiet (staff can list multiple variants if needed)
743
+ await run_coupon_invoice_case({
744
+ label: 'c2-case-mismatch',
745
+ couponCodes: ['summer20'],
746
+ discounts: [discount_with_promo_code('SUMMER20')],
747
+ shouldFire: false,
748
+ title: 'coupon match is case-sensitive: a differently-cased code does not fire',
749
+ })
750
+
751
+ // C3: a different code stays quiet
752
+ await run_coupon_invoice_case({
753
+ label: 'c3-nonmatching',
754
+ couponCodes: ['SUMMER20'],
755
+ discounts: [discount_with_promo_code('WINTER10')],
756
+ shouldFire: false,
757
+ title: 'coupon-scoped trigger does not fire for a different code',
758
+ })
759
+
760
+ // C4: an undiscounted invoice never matches a coupon-scoped trigger
761
+ await run_coupon_invoice_case({
762
+ label: 'c4-no-discounts',
763
+ couponCodes: ['SUMMER20'],
764
+ shouldFire: false,
765
+ title: 'an invoice with no discounts does not fire a coupon-scoped trigger',
766
+ })
767
+
768
+ // C5: admin-applied coupons carry a name but no promotion code, and must still match. Built
769
+ // once and referenced by id from `discounts`, mirroring the real single-discount payload shape.
770
+ const c5Discount = discount_with_coupon('Spring Promo')
771
+ await run_coupon_invoice_case({
772
+ label: 'c5-coupon-name',
773
+ couponCodes: ['Spring Promo'],
774
+ discount: c5Discount,
775
+ discounts: [(c5Discount as any).id],
776
+ shouldFire: true,
777
+ title: 'coupon-scoped trigger matches via coupon name (no promotion code)',
778
+ })
779
+
780
+ // C6: backward compat — an unscoped Purchase Made trigger fires for discounted and
781
+ // undiscounted invoices alike
782
+ await run_coupon_invoice_case({
783
+ label: 'c6-unscoped-discounted',
784
+ discounts: [discount_with_promo_code('SUMMER20')],
785
+ shouldFire: true,
786
+ title: 'unscoped Purchase Made trigger fires for a discounted invoice',
787
+ })
788
+ await run_coupon_invoice_case({
789
+ label: 'c6b-unscoped-undiscounted',
790
+ shouldFire: true,
791
+ title: 'unscoped Purchase Made trigger fires for an undiscounted invoice',
792
+ })
793
+
794
+ // C7: charge.succeeded with payment_intent null — the coupon lookup is skipped entirely, so
795
+ // the purchase is created, the unscoped trigger fires, and the coupon-scoped one does not
796
+ const C7_TAG = tag('c7-charge-unscoped')
797
+ const C7_SCOPED_TAG = tag('c7-charge-coupon-scoped')
798
+ const c7Enduser = await new_enduser('c7')
799
+ await new_trigger({
800
+ title: `Stripe purchase made (c7 unscoped) ${RUN}`,
801
+ event: { type: 'Purchase Made', info: {} },
802
+ tags: [C7_TAG],
803
+ })
804
+ await new_trigger({
805
+ title: `Stripe purchase made (c7 coupon-scoped) ${RUN}`,
806
+ event: { type: 'Purchase Made', info: { couponCodes: ['SUMMER20'] } },
807
+ tags: [C7_SCOPED_TAG],
808
+ })
809
+ const c7Res = await post_stripe_webhook({
810
+ stripePublicKey,
811
+ secret: webhooksSecret,
812
+ event: charge_succeeded_event({
813
+ customer: {
814
+ id: c7Enduser.stripeCustomerId,
815
+ email: `stripe-webhook-c7-${RUN}@tellescope.com`,
816
+ name: 'Stripe Webhook c7',
817
+ },
818
+ }),
819
+ })
820
+ assert(c7Res.status === 200, `expected 200, got ${c7Res.status} (${c7Res.data})`, 'charge.succeeded webhook accepted')
821
+ await expect_fired(c7Enduser.id, C7_TAG, 'unscoped Purchase Made trigger fires for charge.succeeded')
822
+ await expect_not_fired(c7Enduser.id, C7_SCOPED_TAG, 'coupon-scoped trigger does not fire for a charge with no coupon data')
823
+ const c7Purchases = await poll_for_purchase(c7Enduser.id)
824
+ assert(c7Purchases.length > 0, 'expected charge.succeeded to create a purchase', 'charge.succeeded purchase created')
825
+
826
+ // C8: graceful degradation — a payment_intent string triggers the checkout-session lookup,
827
+ // which fails against real Stripe under this suite's fake API key. That failure must be caught:
828
+ // the purchase is still created and the unscoped trigger still fires.
829
+ const C8_TAG = tag('c8-charge-failed-lookup')
830
+ const c8Enduser = await new_enduser('c8')
831
+ await new_trigger({
832
+ title: `Stripe purchase made (c8 failed session lookup) ${RUN}`,
833
+ event: { type: 'Purchase Made', info: {} },
834
+ tags: [C8_TAG],
835
+ })
836
+ const c8Res = await post_stripe_webhook({
837
+ stripePublicKey,
838
+ secret: webhooksSecret,
839
+ event: charge_succeeded_event({
840
+ customer: {
841
+ id: c8Enduser.stripeCustomerId,
842
+ email: `stripe-webhook-c8-${RUN}@tellescope.com`,
843
+ name: 'Stripe Webhook c8',
844
+ },
845
+ payment_intent: `pi_test_${RUN}`,
846
+ }),
847
+ })
848
+ assert(c8Res.status === 200, `expected 200, got ${c8Res.status} (${c8Res.data})`, 'charge.succeeded with failing session lookup still accepted')
849
+ await expect_fired(c8Enduser.id, C8_TAG, 'a failing checkout-session lookup does not block the purchase or its triggers')
850
+
851
+ // C9: couponCodes must round-trip on the trigger's event.info
852
+ const c9Trigger = await new_trigger({
853
+ title: `Stripe purchase made (validator round-trip) ${RUN}`,
854
+ event: { type: 'Purchase Made', info: { couponCodes: ['SUMMER20'] } },
855
+ tags: [tag('c9')],
856
+ })
857
+ const c9Fetched = await sdk.api.automation_triggers.getOne(c9Trigger.id)
858
+ assert(
859
+ JSON.stringify((c9Fetched.event as any).info?.couponCodes) === JSON.stringify(['SUMMER20']),
860
+ `expected couponCodes to round-trip, got ${JSON.stringify(c9Fetched.event)}`,
861
+ 'couponCodes round-trips through the validator',
862
+ )
863
+
643
864
  console.log("All Stripe webhook trigger tests passed!")
644
865
  } finally {
645
866
  // Best-effort only: the harness exits the process on the first failed assertion, which skips
@@ -23,9 +23,13 @@ const decode_jwt_header = (token: string) => (
23
23
  )
24
24
  const decode_jwt_payload = (token: string) => (
25
25
  JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString()) as {
26
- patientId: string,
26
+ sub: string,
27
+ iss: string,
28
+ aud: string,
29
+ jti: string,
27
30
  iat: number,
28
31
  exp: number,
32
+ target?: string,
29
33
  }
30
34
  )
31
35
  const sign_hs256 = (signingInput: string, key: string) => (
@@ -99,16 +103,55 @@ export const welle_partner_token_tests = async ({ sdk, sdkNonAdmin } : { sdk: Se
99
103
 
100
104
  const header = decode_jwt_header(issuedToken)
101
105
  const payload = decode_jwt_payload(issuedToken)
102
- const expectedExp = Math.floor(Date.now() / 1000) + 3600
106
+ const expectedExp = Math.floor(Date.now() / 1000) + 60
103
107
 
104
108
  return (
105
109
  header.alg === 'HS256'
106
- && payload.patientId === HEALTHIE_PATIENT_ID
107
- && Math.abs(payload.exp - expectedExp) < 60
110
+ && payload.sub === HEALTHIE_PATIENT_ID
111
+ && payload.iss === 'tellescope'
112
+ && payload.aud === 'welle:sso-launch'
113
+ && typeof payload.jti === 'string' && payload.jti.length > 0
114
+ && Math.abs(payload.exp - expectedExp) < 10
108
115
  )
109
116
  }},
110
117
  )
111
118
 
119
+ // each launch gets a unique jti
120
+ await async_test(
121
+ `back-to-back tokens carry distinct jti values`,
122
+ () => linkedSDK.api.integrations.proxy_read({ integration: WELLE_TITLE, type: 'partner-redirect-token' }),
123
+ { onResult: (r: any) => {
124
+ const second = r?.data?.token
125
+ if (!second) return false
126
+ return decode_jwt_payload(second).jti !== decode_jwt_payload(issuedToken).jti
127
+ }},
128
+ )
129
+
130
+ // optional deep-link target — allowlisted value is echoed as a claim
131
+ await async_test(
132
+ `target: 'plan' is included as a target claim`,
133
+ () => linkedSDK.api.integrations.proxy_read({ integration: WELLE_TITLE, type: 'partner-redirect-token', target: 'plan' }),
134
+ { onResult: (r: any) => (
135
+ !!r?.data?.token && decode_jwt_payload(r.data.token).target === 'plan'
136
+ )},
137
+ )
138
+ // omitted target → no target claim at all (the happy-path token above was minted without one)
139
+ assert(
140
+ decode_jwt_payload(issuedToken).target === undefined,
141
+ "token minted without a target has a target claim",
142
+ "token minted without a target has no target claim",
143
+ )
144
+ // non-allowlisted target is rejected at the API boundary by schema validation
145
+ await async_test(
146
+ `invalid target is rejected by schema validation`,
147
+ () => linkedSDK.api.integrations.proxy_read({
148
+ integration: WELLE_TITLE,
149
+ type: 'partner-redirect-token',
150
+ target: 'admin' as any,
151
+ }),
152
+ { shouldError: true, onError: () => true },
153
+ )
154
+
112
155
  // 2) the signature is real — verifies against the stored signing key, and only that key
113
156
  const [header, payload, signature] = issuedToken.split('.')
114
157
  assert(
@@ -131,7 +174,7 @@ export const welle_partner_token_tests = async ({ sdk, sdkNonAdmin } : { sdk: Se
131
174
  id: unlinkedId,
132
175
  }),
133
176
  { onResult: (r: any) => (
134
- !!r?.data?.token && decode_jwt_payload(r.data.token).patientId === HEALTHIE_PATIENT_ID
177
+ !!r?.data?.token && decode_jwt_payload(r.data.token).sub === HEALTHIE_PATIENT_ID
135
178
  )},
136
179
  )
137
180
 
@@ -195,7 +238,7 @@ export const welle_partner_token_tests = async ({ sdk, sdkNonAdmin } : { sdk: Se
195
238
  if (!token) return false
196
239
  const [h, p, s] = token.split('.')
197
240
  return sign_hs256(`${h}.${p}`, SIGNING_KEY) === s
198
- && decode_jwt_payload(token).patientId === HEALTHIE_PATIENT_ID
241
+ && decode_jwt_payload(token).sub === HEALTHIE_PATIENT_ID
199
242
  }},
200
243
  )
201
244
 
@@ -151,6 +151,122 @@ export const invoice_payment_failed_event = ({ stripeCustomerId, lines = [], has
151
151
  },
152
152
  })
153
153
 
154
+ /**
155
+ * Embedded Stripe.Discount-shaped object with an *expanded* promotion_code object. Real webhook
156
+ * payloads carry promotion_code as a bare promo_... string (see discount_with_promo_code_ref);
157
+ * the expanded variant reflects the post-expansion shape (and the checkout-session breakdown
158
+ * shape), and matters here because this suite signs payloads with a real HMAC but a fake Stripe
159
+ * API key, so any fallback fetch (promotionCodes.retrieve on a promo_... string) would fail —
160
+ * expanded objects prove the handler extracts codes without outbound Stripe calls.
161
+ */
162
+ export const discount_with_promo_code = (code: string, couponName?: string) => ({
163
+ id: `di_test_${unique_suffix()}`,
164
+ object: 'discount',
165
+ coupon: {
166
+ id: `coupon_test_${unique_suffix()}`,
167
+ object: 'coupon',
168
+ name: couponName ?? null,
169
+ },
170
+ promotion_code: {
171
+ id: `promo_test_${unique_suffix()}`,
172
+ object: 'promotion_code',
173
+ code,
174
+ },
175
+ })
176
+
177
+ /**
178
+ * Mirrors a real invoice.paid webhook discount (verified against a live sandbox event): the
179
+ * embedded coupon carries id + name, but promotion_code is a bare promo_... string reference that
180
+ * only the retrieve fallback can resolve — which fails under this suite's fake API key, so a
181
+ * payload built from this captures the coupon name + id and (in tests) not the promo code.
182
+ */
183
+ export const discount_with_promo_code_ref = ({ couponId, couponName } : { couponId: string, couponName: string }) => ({
184
+ id: `di_test_${unique_suffix()}`,
185
+ object: 'discount',
186
+ coupon: { id: couponId, object: 'coupon', name: couponName },
187
+ promotion_code: `promo_test_${unique_suffix()}`,
188
+ })
189
+
190
+ /** An admin-applied coupon: a name but no promotion code. */
191
+ export const discount_with_coupon = (name: string) => ({
192
+ id: `di_test_${unique_suffix()}`,
193
+ object: 'discount',
194
+ coupon: {
195
+ id: `coupon_test_${unique_suffix()}`,
196
+ object: 'coupon',
197
+ name,
198
+ },
199
+ promotion_code: null,
200
+ })
201
+
202
+ /**
203
+ * An invoice line item for invoice.paid. `description` feeds purchase.title; the expanded
204
+ * price.product object (when a product id is given) avoids the handler's product-name
205
+ * stripe.products.retrieve network fallback.
206
+ */
207
+ export const invoice_paid_line = ({ description, stripeProductId } : {
208
+ description?: string,
209
+ stripeProductId?: string,
210
+ }) => ({
211
+ description: description ?? null,
212
+ price: stripeProductId
213
+ ? { product: { id: stripeProductId, object: 'product', name: description ?? 'Test Product' } }
214
+ : null,
215
+ })
216
+
217
+ /**
218
+ * Minimal invoice.paid envelope. The handler looks up the enduser by stripeCustomerId, so no
219
+ * customer_email/name are needed. Real webhooks embed the single-discount case at `discount` and
220
+ * carry only bare "di_..." string ids in `discounts` (verified against live sandbox events);
221
+ * cases may instead pass expanded objects in `discounts` to prove call-free extraction — see
222
+ * discount_with_promo_code.
223
+ */
224
+ export const invoice_paid_event = ({ stripeCustomerId, lines = [], discounts, discount, amount_paid = 1999 } : {
225
+ stripeCustomerId: string,
226
+ lines?: object[],
227
+ discounts?: (object | string)[],
228
+ discount?: object,
229
+ amount_paid?: number,
230
+ }) => ({
231
+ type: 'invoice.paid',
232
+ data: {
233
+ object: {
234
+ id: `in_test_${unique_suffix()}`,
235
+ customer: stripeCustomerId,
236
+ amount_paid,
237
+ lines: { object: 'list', data: lines, has_more: false },
238
+ discount: discount ?? null,
239
+ discounts: discounts ?? [],
240
+ },
241
+ },
242
+ })
243
+
244
+ /**
245
+ * Minimal charge.succeeded envelope. `customer` must be an embedded object — a bare string id makes
246
+ * the handler call stripe.customers.retrieve, an outbound call the fake API key cannot serve. The
247
+ * handler then matches the enduser by the embedded customer's email. `payment_intent: null` is the
248
+ * deterministic no-coupon case (the coupon lookup only runs for a payment_intent string).
249
+ */
250
+ export const charge_succeeded_event = ({ customer, amount = 1999, currency = 'usd', payment_intent = null } : {
251
+ customer: { id: string, email: string, name?: string },
252
+ amount?: number,
253
+ currency?: string,
254
+ payment_intent?: string | null,
255
+ }) => ({
256
+ type: 'charge.succeeded',
257
+ data: {
258
+ object: {
259
+ id: `ch_test_${unique_suffix()}`,
260
+ customer: { object: 'customer', ...customer },
261
+ amount,
262
+ currency,
263
+ payment_intent,
264
+ invoice: null,
265
+ metadata: {},
266
+ },
267
+ },
268
+ })
269
+
154
270
  /** Minimal customer.subscription.deleted envelope; the handler reads items.data[].plan.product. */
155
271
  export const subscription_deleted_event = ({ stripeCustomerId, stripeProductIds = [] } : {
156
272
  stripeCustomerId: string,
@@ -38,6 +38,8 @@ import {
38
38
  import { Session, APIQuery, EnduserSession } from "../sdk"
39
39
  import { enduser_observations_acknowledge_tests } from "./api_tests/enduser_observations_acknowledge.test"
40
40
  import { translations_tests } from "./api_tests/translations.test"
41
+ import { configurations_public_read_tests } from "./api_tests/configurations_public_read.test"
42
+ import { form_translations_tests } from "./api_tests/form_translations.test"
41
43
  import { embeddables_auth_token_tests } from "./api_tests/embeddables_auth_token.test"
42
44
  import { user_portal_settings_tests } from "./api_tests/user_portal_settings.test"
43
45
  import { integrations_redacted_tests } from "./api_tests/integrations_redacted.test"
@@ -56,6 +58,8 @@ import { purchase_made_trigger_tests } from "./api_tests/purchase_made_trigger.t
56
58
  import { appointment_rescheduled_trigger_tests } from "./api_tests/appointment_rescheduled_trigger.test"
57
59
  import { appointment_no_showed_trigger_tests } from "./api_tests/appointment_no_showed_trigger.test"
58
60
  import { group_event_attendee_status_triggers_tests } from "./api_tests/group_event_attendee_status_triggers.test"
61
+ import { group_event_attendee_self_update_tests } from "./api_tests/group_event_attendee_self_update.test"
62
+ import { calendar_event_create_and_bulk_triggers_tests } from "./api_tests/calendar_event_create_and_bulk_triggers.test"
59
63
  import { journey_error_branching_tests } from "./api_tests/journey_error_branching.test"
60
64
  import { journey_delete_cancels_actions_tests } from "./api_tests/journey_delete_cancels_actions.test"
61
65
  import { afteraction_day_of_month_delay_tests } from "./api_tests/afteraction_day_of_month_delay.test"
@@ -109,6 +113,7 @@ import { field_redaction_tests } from "./api_tests/field_redaction.test";
109
113
  import { data_sync_redaction_bypass_tests } from "./api_tests/security/F-0001-data-sync-redaction-bypass.test";
110
114
  import { enduser_write_restrictions_tests } from "./api_tests/security/F-0106-F-0110-enduser-write-restrictions.test";
111
115
  import { ai_conversations_rbac_tests } from "./api_tests/security/F-0005-ai-conversations-rbac.test";
116
+ import { develop_health_benefit_verification_rbac_tests } from "./api_tests/security/F-0006-develop-health-benefit-verification-rbac.test";
112
117
  import { cascade_role_rename_cross_tenant_tests } from "./api_tests/security/F-0053-cascade-role-rename-cross-tenant.test";
113
118
  import { self_admin_role_assignment_tests } from "./api_tests/security/F-0076-self-admin-role-assignment.test";
114
119
  import { invite_user_enumeration_tests } from "./api_tests/security/F-0007-invite-user-enumeration.test";
@@ -153,6 +158,7 @@ import { mdi_case_offerings_tests } from "./api_tests/mdi_case_offerings.test";
153
158
  import { beluga_manual_sync_tests } from "./api_tests/beluga_manual_sync.test";
154
159
  import { metriport_push_form_response_tests } from "./api_tests/metriport_push_form_response.test";
155
160
  import { beluga_trigger_refill_tests } from "./api_tests/beluga_trigger_refill.test";
161
+ import { mdi_trigger_refill_tests } from "./api_tests/mdi_trigger_refill.test";
156
162
  import { mdi_webhooks_tests } from "./api_tests/mdi_webhooks.test";
157
163
  import { stripe_webhook_triggers_tests } from "./api_tests/stripe_webhook_triggers.test";
158
164
  import { account_switcher_tests } from "./api_tests/account_switcher.test";
@@ -5752,6 +5758,8 @@ const automation_trigger_tests = async () => {
5752
5758
 
5753
5759
  await appointment_no_showed_trigger_tests({ sdk, sdkNonAdmin })
5754
5760
  await group_event_attendee_status_triggers_tests({ sdk, sdkNonAdmin })
5761
+ await group_event_attendee_self_update_tests({ sdk, sdkNonAdmin })
5762
+ await calendar_event_create_and_bulk_triggers_tests({ sdk, sdkNonAdmin })
5755
5763
  await assign_care_team_tests()
5756
5764
  await push_forms_to_portal_group_completion_tests({ sdk, sdkNonAdmin })
5757
5765
  await order_status_equals_tests()
@@ -6994,18 +7002,23 @@ export const self_serve_appointment_booking_tests = async () => {
6994
7002
  restrictedByState: false,
6995
7003
  }),
6996
7004
  { onResult: r => {
6997
- if (r.availabilityBlocks.length !== 4) return false // 2 providers with 1 hour availability for 30 minute meetings
6998
-
6999
- const user1block1_ET = r.availabilityBlocks.find(b => b.userId === sdk.userInfo.id)
7000
- const user2block1_PT = r.availabilityBlocks.find(b => b.userId === sdkNonAdmin.userInfo.id)
7001
-
7002
- if (!(user1block1_ET && user2block1_PT)) return false // should be slots for both users
7003
-
7004
- if (user2block1_PT.startTimeInMS - user1block1_ET.startTimeInMS !== 1000 * 60 * 60 * 3) {
7005
- return false // difference should be three hours, since same availability in different timezones
7006
- }
7007
-
7008
- return true
7005
+ // without a state restriction this endpoint returns availability for EVERY user in the business
7006
+ // with weeklyAvailabilities set, so scope to the two users this test provisions. An unrelated
7007
+ // user (e.g. one stranded by an interrupted suite — reset_db does not clear the users
7008
+ // collection) must not break this assertion.
7009
+ const adminBlocks = r.availabilityBlocks.filter(b => b.userId === sdk.userInfo.id)
7010
+ const nonAdminBlocks = r.availabilityBlocks.filter(b => b.userId === sdkNonAdmin.userInfo.id)
7011
+
7012
+ // 1 hour of availability each for 30 minute meetings, and the admin's duplicate
7013
+ // weeklyAvailabilities entry must not produce extra slots
7014
+ if (adminBlocks.length !== 2) return false
7015
+ if (nonAdminBlocks.length !== 2) return false
7016
+
7017
+ // the server shuffles users, so compare earliest-to-earliest rather than relying on ordering
7018
+ const earliest = (bs: typeof adminBlocks) => Math.min(...bs.map(b => b.startTimeInMS))
7019
+
7020
+ // difference should be three hours, since same availability in different timezones
7021
+ return earliest(nonAdminBlocks) - earliest(adminBlocks) === 1000 * 60 * 60 * 3
7009
7022
  }},
7010
7023
  )
7011
7024
 
@@ -15298,6 +15311,7 @@ const ip_address_form_tests = async () => {
15298
15311
  await beluga_manual_sync_tests({ sdk, sdkNonAdmin })
15299
15312
  await metriport_push_form_response_tests({ sdk, sdkNonAdmin })
15300
15313
  await beluga_trigger_refill_tests({ sdk, sdkNonAdmin })
15314
+ await mdi_trigger_refill_tests({ sdk, sdkNonAdmin })
15301
15315
  await beluga_pharmacy_mappings_tests({ sdk, sdkNonAdmin })
15302
15316
  await mdi_case_offerings_tests({ sdk, sdkNonAdmin })
15303
15317
  await enduser_write_restrictions_tests({ sdk, sdkNonAdmin })
@@ -15317,6 +15331,7 @@ const ip_address_form_tests = async () => {
15317
15331
  await enduser_login_rate_limits_tests({ sdk, sdkNonAdmin })
15318
15332
  await data_sync_redaction_bypass_tests({ sdk, sdkNonAdmin })
15319
15333
  await ai_conversations_rbac_tests({ sdk, sdkNonAdmin })
15334
+ await develop_health_benefit_verification_rbac_tests({ sdk, sdkNonAdmin })
15320
15335
  await cascade_role_rename_cross_tenant_tests({ sdk, sdkNonAdmin })
15321
15336
  await self_admin_role_assignment_tests({ sdk, sdkNonAdmin })
15322
15337
  await sanitize_user_html_xss_tests()
@@ -15393,6 +15408,8 @@ const ip_address_form_tests = async () => {
15393
15408
  await enduser_observations_acknowledge_tests({ sdk, sdkNonAdmin })
15394
15409
  await nutrition_tracking_tests({ sdk, sdkNonAdmin })
15395
15410
  await translations_tests({ sdk, sdkNonAdmin })
15411
+ await configurations_public_read_tests({ sdk, sdkNonAdmin })
15412
+ await form_translations_tests({ sdk, sdkNonAdmin })
15396
15413
  await create_user_notifications_trigger_tests({ sdk })
15397
15414
  await group_mms_active_tests()
15398
15415
  await auto_reply_tests()
Binary file