@tellescope/sdk 1.256.2 → 1.256.4

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 (67) hide show
  1. package/lib/cjs/tests/api_tests/metriport_push_form_response.test.d.ts +6 -0
  2. package/lib/cjs/tests/api_tests/metriport_push_form_response.test.d.ts.map +1 -0
  3. package/lib/cjs/tests/api_tests/metriport_push_form_response.test.js +298 -0
  4. package/lib/cjs/tests/api_tests/metriport_push_form_response.test.js.map +1 -0
  5. package/lib/cjs/tests/api_tests/phone_calls_move_enduser.test.d.ts +6 -0
  6. package/lib/cjs/tests/api_tests/phone_calls_move_enduser.test.d.ts.map +1 -0
  7. package/lib/cjs/tests/api_tests/phone_calls_move_enduser.test.js +274 -0
  8. package/lib/cjs/tests/api_tests/phone_calls_move_enduser.test.js.map +1 -0
  9. package/lib/cjs/tests/api_tests/stripe_webhook_triggers.test.d.ts +18 -0
  10. package/lib/cjs/tests/api_tests/stripe_webhook_triggers.test.d.ts.map +1 -0
  11. package/lib/cjs/tests/api_tests/stripe_webhook_triggers.test.js +964 -0
  12. package/lib/cjs/tests/api_tests/stripe_webhook_triggers.test.js.map +1 -0
  13. package/lib/cjs/tests/api_tests/welle_partner_token.test.d.ts +6 -0
  14. package/lib/cjs/tests/api_tests/welle_partner_token.test.d.ts.map +1 -0
  15. package/lib/cjs/tests/api_tests/welle_partner_token.test.js +313 -0
  16. package/lib/cjs/tests/api_tests/welle_partner_token.test.js.map +1 -0
  17. package/lib/cjs/tests/stripe_webhook_helpers.d.ts +204 -0
  18. package/lib/cjs/tests/stripe_webhook_helpers.d.ts.map +1 -0
  19. package/lib/cjs/tests/stripe_webhook_helpers.js +286 -0
  20. package/lib/cjs/tests/stripe_webhook_helpers.js.map +1 -0
  21. package/lib/cjs/tests/tests.d.ts.map +1 -1
  22. package/lib/cjs/tests/tests.js +192 -180
  23. package/lib/cjs/tests/tests.js.map +1 -1
  24. package/lib/cjs/tests/unit_tests/translation.test.d.ts +3 -0
  25. package/lib/cjs/tests/unit_tests/translation.test.d.ts.map +1 -0
  26. package/lib/cjs/tests/unit_tests/translation.test.js.map +1 -0
  27. package/lib/esm/enduser.d.ts +1 -0
  28. package/lib/esm/enduser.d.ts.map +1 -1
  29. package/lib/esm/sdk.d.ts +3 -2
  30. package/lib/esm/sdk.d.ts.map +1 -1
  31. package/lib/esm/session.d.ts +1 -0
  32. package/lib/esm/session.d.ts.map +1 -1
  33. package/lib/esm/tests/api_tests/metriport_push_form_response.test.d.ts +6 -0
  34. package/lib/esm/tests/api_tests/metriport_push_form_response.test.d.ts.map +1 -0
  35. package/lib/esm/tests/api_tests/metriport_push_form_response.test.js +294 -0
  36. package/lib/esm/tests/api_tests/metriport_push_form_response.test.js.map +1 -0
  37. package/lib/esm/tests/api_tests/phone_calls_move_enduser.test.d.ts +6 -0
  38. package/lib/esm/tests/api_tests/phone_calls_move_enduser.test.d.ts.map +1 -0
  39. package/lib/esm/tests/api_tests/phone_calls_move_enduser.test.js +270 -0
  40. package/lib/esm/tests/api_tests/phone_calls_move_enduser.test.js.map +1 -0
  41. package/lib/esm/tests/api_tests/stripe_webhook_triggers.test.d.ts +18 -0
  42. package/lib/esm/tests/api_tests/stripe_webhook_triggers.test.d.ts.map +1 -0
  43. package/lib/esm/tests/api_tests/stripe_webhook_triggers.test.js +960 -0
  44. package/lib/esm/tests/api_tests/stripe_webhook_triggers.test.js.map +1 -0
  45. package/lib/esm/tests/api_tests/welle_partner_token.test.d.ts +6 -0
  46. package/lib/esm/tests/api_tests/welle_partner_token.test.d.ts.map +1 -0
  47. package/lib/esm/tests/api_tests/welle_partner_token.test.js +309 -0
  48. package/lib/esm/tests/api_tests/welle_partner_token.test.js.map +1 -0
  49. package/lib/esm/tests/stripe_webhook_helpers.d.ts +204 -0
  50. package/lib/esm/tests/stripe_webhook_helpers.d.ts.map +1 -0
  51. package/lib/esm/tests/stripe_webhook_helpers.js +267 -0
  52. package/lib/esm/tests/stripe_webhook_helpers.js.map +1 -0
  53. package/lib/esm/tests/tests.d.ts.map +1 -1
  54. package/lib/esm/tests/tests.js +192 -180
  55. package/lib/esm/tests/tests.js.map +1 -1
  56. package/lib/esm/tests/unit_tests/translation.test.d.ts +3 -0
  57. package/lib/esm/tests/unit_tests/translation.test.d.ts.map +1 -0
  58. package/lib/esm/tests/unit_tests/translation.test.js +52 -0
  59. package/lib/esm/tests/unit_tests/translation.test.js.map +1 -0
  60. package/lib/tsconfig.tsbuildinfo +1 -1
  61. package/package.json +10 -10
  62. package/src/tests/api_tests/metriport_push_form_response.test.ts +226 -0
  63. package/src/tests/api_tests/stripe_webhook_triggers.test.ts +676 -0
  64. package/src/tests/api_tests/welle_partner_token.test.ts +249 -0
  65. package/src/tests/stripe_webhook_helpers.ts +240 -0
  66. package/src/tests/tests.ts +6 -0
  67. package/test_generated.pdf +0 -0
@@ -0,0 +1,249 @@
1
+ require('source-map-support').install();
2
+
3
+ import { createHmac } from "crypto"
4
+
5
+ import { Session, EnduserSession } from "../../sdk"
6
+ import {
7
+ assert,
8
+ async_test,
9
+ log_header,
10
+ wait,
11
+ } from "@tellescope/testing"
12
+ import { HEALTHIE_TITLE, WELLE_TITLE } from "@tellescope/constants"
13
+ import { authenticate_enduser_via_token, setup_tests } from "../setup"
14
+
15
+ const host = process.env.API_URL || 'http://localhost:8080' as const
16
+
17
+ const SIGNING_KEY = 'welle_test_signing_key_do_not_use_in_prod'
18
+ const HEALTHIE_PATIENT_ID = '999888777'
19
+
20
+ // decoding needs no dependency: these tests assert on CLAIMS, and verify the signature separately with crypto
21
+ const decode_jwt_header = (token: string) => (
22
+ JSON.parse(Buffer.from(token.split('.')[0], 'base64url').toString())
23
+ )
24
+ const decode_jwt_payload = (token: string) => (
25
+ JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString()) as {
26
+ patientId: string,
27
+ iat: number,
28
+ exp: number,
29
+ }
30
+ )
31
+ const sign_hs256 = (signingInput: string, key: string) => (
32
+ createHmac('sha256', key).update(signingInput).digest('base64url')
33
+ )
34
+
35
+ // reset_db does NOT clear integrations, so a crashed prior run can leave a Welle record behind.
36
+ // The partner-token branch does findOne({ title: WELLE_TITLE }), so a stray record would make the
37
+ // "disconnected" assertion pass spuriously — clear them all rather than only the one we created.
38
+ const delete_all_welle_integrations = async (sdk: Session) => {
39
+ const existing = await sdk.api.integrations.getSome({})
40
+ for (const integration of existing.filter(i => i.title === WELLE_TITLE)) {
41
+ await sdk.api.integrations.deleteOne(integration.id).catch(console.error)
42
+ }
43
+ }
44
+
45
+ export const welle_partner_token_tests = async ({ sdk, sdkNonAdmin } : { sdk: Session, sdkNonAdmin: Session }) => {
46
+ log_header("Welle Partner Token Tests")
47
+
48
+ const suffix = Math.random().toString(36).slice(2, 10)
49
+ const linkedEmail = `welle.linked.${suffix}@tellescope.com`
50
+ const unlinkedEmail = `welle.unlinked.${suffix}@tellescope.com`
51
+
52
+ let linkedId = ''
53
+ let unlinkedId = ''
54
+
55
+ try {
56
+ await delete_all_welle_integrations(sdk)
57
+
58
+ await sdk.api.integrations.createOne({
59
+ title: WELLE_TITLE,
60
+ authentication: {
61
+ type: 'apiKey',
62
+ info: {
63
+ access_token: SIGNING_KEY,
64
+ refresh_token: 'unused',
65
+ scope: '',
66
+ token_type: 'Bearer',
67
+ expiry_date: 0,
68
+ },
69
+ },
70
+ })
71
+
72
+ // an enduser linked to Healthie — eligible for a partner token
73
+ const linked = await sdk.api.endusers.createOne({ email: linkedEmail, fname: 'Welle', lname: 'Linked' })
74
+ linkedId = linked.id
75
+ await sdk.api.endusers.updateOne(
76
+ linkedId,
77
+ { references: [{ type: HEALTHIE_TITLE, id: HEALTHIE_PATIENT_ID }] } as any,
78
+ { replaceObjectFields: true },
79
+ )
80
+
81
+ // an enduser with no Healthie reference
82
+ const unlinked = await sdk.api.endusers.createOne({ email: unlinkedEmail, fname: 'Welle', lname: 'Unlinked' })
83
+ unlinkedId = unlinked.id
84
+
85
+ const linkedSDK = new EnduserSession({ host, businessId: sdk.userInfo.businessId })
86
+ await authenticate_enduser_via_token(sdk, linkedSDK, { email: linkedEmail })
87
+
88
+ const unlinkedSDK = new EnduserSession({ host, businessId: sdk.userInfo.businessId })
89
+ await authenticate_enduser_via_token(sdk, unlinkedSDK, { email: unlinkedEmail })
90
+
91
+ // 1) happy path — claims are correct
92
+ let issuedToken = ''
93
+ await async_test(
94
+ `enduser can mint a Welle partner token with the expected claims`,
95
+ () => linkedSDK.api.integrations.proxy_read({ integration: WELLE_TITLE, type: 'partner-redirect-token' }),
96
+ { onResult: (r: any) => {
97
+ issuedToken = r?.data?.token
98
+ if (!issuedToken) return false
99
+
100
+ const header = decode_jwt_header(issuedToken)
101
+ const payload = decode_jwt_payload(issuedToken)
102
+ const expectedExp = Math.floor(Date.now() / 1000) + 3600
103
+
104
+ return (
105
+ header.alg === 'HS256'
106
+ && payload.patientId === HEALTHIE_PATIENT_ID
107
+ && Math.abs(payload.exp - expectedExp) < 60
108
+ )
109
+ }},
110
+ )
111
+
112
+ // 2) the signature is real — verifies against the stored signing key, and only that key
113
+ const [header, payload, signature] = issuedToken.split('.')
114
+ assert(
115
+ sign_hs256(`${header}.${payload}`, SIGNING_KEY) === signature,
116
+ "token signature does not verify against the stored Welle signing key",
117
+ "token is signed with the organization's Welle signing key",
118
+ )
119
+ assert(
120
+ sign_hs256(`${header}.${payload}`, `${SIGNING_KEY}_wrong`) !== signature,
121
+ "token signature verified against the WRONG key",
122
+ "token signature does not verify against a different key",
123
+ )
124
+
125
+ // 3) the subject is non-forgeable — the branch ignores the id param and uses the session's enduser
126
+ await async_test(
127
+ `enduser cannot mint a token for another enduser by passing id`,
128
+ () => linkedSDK.api.integrations.proxy_read({
129
+ integration: WELLE_TITLE,
130
+ type: 'partner-redirect-token',
131
+ id: unlinkedId,
132
+ }),
133
+ { onResult: (r: any) => (
134
+ !!r?.data?.token && decode_jwt_payload(r.data.token).patientId === HEALTHIE_PATIENT_ID
135
+ )},
136
+ )
137
+
138
+ // 4) an enduser with no Healthie reference gets no token
139
+ await async_test(
140
+ `enduser without a Healthie reference cannot mint a token`,
141
+ () => unlinkedSDK.api.integrations.proxy_read({ integration: WELLE_TITLE, type: 'partner-redirect-token' }),
142
+ { shouldError: true, onError: (e: any) => (e?.message || '').includes('enduser has no Healthie reference') },
143
+ )
144
+
145
+ // 5) staff sessions are not served by this branch (it is enduser-gated) — falls through to an empty result
146
+ await async_test(
147
+ `admin user session is not issued a Welle partner token`,
148
+ () => sdk.api.integrations.proxy_read({ integration: WELLE_TITLE, type: 'partner-redirect-token' }),
149
+ { onResult: (r: any) => !r?.data?.token },
150
+ )
151
+ await async_test(
152
+ `non-admin user session is not issued a Welle partner token`,
153
+ () => sdkNonAdmin.api.integrations.proxy_read({ integration: WELLE_TITLE, type: 'partner-redirect-token' }),
154
+ { onResult: (r: any) => !r?.data?.token },
155
+ )
156
+
157
+ // 6) no other Welle proxy_read type is reachable by an enduser
158
+ await async_test(
159
+ `enduser cannot read other Welle proxy types`,
160
+ () => linkedSDK.api.integrations.proxy_read({ integration: WELLE_TITLE, type: 'anything-else' }),
161
+ { shouldError: true, onError: (e: any) => (
162
+ (e?.message || '').includes('enduser session type not valid for this integration type')
163
+ )},
164
+ )
165
+
166
+ // 7) with the integration disconnected, no token can be minted
167
+ await delete_all_welle_integrations(sdk)
168
+ await async_test(
169
+ `no token is issued once the Welle integration is disconnected`,
170
+ () => linkedSDK.api.integrations.proxy_read({ integration: WELLE_TITLE, type: 'partner-redirect-token' }),
171
+ { shouldError: true, onError: (e: any) => (e?.message || '').includes('Welle integration not found') },
172
+ )
173
+
174
+ // 8) the real connect path — add_api_key_integration, not integrations.createOne.
175
+ // Welle has no credential-validation branch (an opaque HMAC key has nothing to authenticate
176
+ // against), so unlike Canvas/Candid/MDI this endpoint makes no upstream call and is testable.
177
+ await async_test(
178
+ `add_api_key_integration connects Welle`,
179
+ () => sdk.api.integrations.add_api_key_integration({ integration: WELLE_TITLE, API_KEY: SIGNING_KEY }),
180
+ { onResult: (r: any) => r?.integration?.title === WELLE_TITLE },
181
+ )
182
+ await async_test(
183
+ `add_api_key_integration sets hasConnectedWelle`,
184
+ () => sdk.api.organizations.getOne(sdk.userInfo.businessId),
185
+ { onResult: (o: any) => o?.hasConnectedWelle === true },
186
+ )
187
+
188
+ // the key stored by add_api_key_integration must land in the field the proxy_read branch reads
189
+ // (authentication.info.access_token) — minting a verifiable token proves the two paths agree
190
+ await async_test(
191
+ `token minted from the add_api_key_integration credential verifies`,
192
+ () => linkedSDK.api.integrations.proxy_read({ integration: WELLE_TITLE, type: 'partner-redirect-token' }),
193
+ { onResult: (r: any) => {
194
+ const token = r?.data?.token
195
+ if (!token) return false
196
+ const [h, p, s] = token.split('.')
197
+ return sign_hs256(`${h}.${p}`, SIGNING_KEY) === s
198
+ && decode_jwt_payload(token).patientId === HEALTHIE_PATIENT_ID
199
+ }},
200
+ )
201
+
202
+ // 9) the real disconnect path — remove_api_key_integration
203
+ await async_test(
204
+ `remove_api_key_integration disconnects Welle`,
205
+ () => sdk.api.integrations.remove_api_key_integration({ integration: WELLE_TITLE }),
206
+ { onResult: () => true },
207
+ )
208
+ // the org flag update is fire-and-forget on this handler (res.end() returns before the .then chain)
209
+ await wait(undefined, 250)
210
+ await async_test(
211
+ `remove_api_key_integration clears hasConnectedWelle`,
212
+ () => sdk.api.organizations.getOne(sdk.userInfo.businessId),
213
+ { onResult: (o: any) => !o?.hasConnectedWelle },
214
+ )
215
+ await async_test(
216
+ `no token is issued after remove_api_key_integration`,
217
+ () => linkedSDK.api.integrations.proxy_read({ integration: WELLE_TITLE, type: 'partner-redirect-token' }),
218
+ { shouldError: true, onError: (e: any) => (e?.message || '').includes('Welle integration not found') },
219
+ )
220
+ } finally {
221
+ await delete_all_welle_integrations(sdk).catch(console.error)
222
+ await sdk.api.organizations.updateOne(sdk.userInfo.businessId, { hasConnectedWelle: false }).catch(console.error)
223
+ for (const id of [linkedId, unlinkedId]) {
224
+ if (!id) continue
225
+ await sdk.api.endusers.deleteOne(id).catch(console.error)
226
+ }
227
+ }
228
+ }
229
+
230
+ // Allow running this test file independently
231
+ if (require.main === module) {
232
+ const sdk = new Session({ host })
233
+ const sdkNonAdmin = new Session({ host })
234
+
235
+ const runTests = async () => {
236
+ await setup_tests(sdk, sdkNonAdmin)
237
+ await welle_partner_token_tests({ sdk, sdkNonAdmin })
238
+ }
239
+
240
+ runTests()
241
+ .then(() => {
242
+ console.log("✅ Welle partner token test suite completed successfully")
243
+ process.exit(0)
244
+ })
245
+ .catch((error) => {
246
+ console.error("❌ Welle partner token test suite failed:", error)
247
+ process.exit(1)
248
+ })
249
+ }
@@ -0,0 +1,240 @@
1
+ import crypto from "crypto"
2
+
3
+ import { Session } from "../sdk"
4
+
5
+ const host = process.env.API_URL || 'http://localhost:8080'
6
+
7
+ // Marks the throwaway integrations created by tests so a run can clear residue left by an earlier
8
+ // run that exited early. The test harness calls process.exit(1) on the first failed assertion, which
9
+ // skips finally blocks, so cleanup cannot be relied on.
10
+ export const TEST_STRIPE_ENVIRONMENT = 'tellescope-test-stripe-webhooks'
11
+
12
+ // title must be exactly STRIPE_TITLE for the webhook handler to find the integration
13
+ const STRIPE_TITLE = 'Stripe'
14
+
15
+ let uniqueCounter = 0
16
+ const unique_suffix = () => `${Date.now()}_${uniqueCounter++}`
17
+
18
+ /**
19
+ * Stripe signs `${timestamp}.${rawBody}` with HMAC-SHA256 and sends it hex-encoded in a
20
+ * `t=<unixSeconds>,v1=<hex>` header. Reproducing it here lets tests exercise the real webhook
21
+ * route — signature verification included — without Stripe or network access.
22
+ *
23
+ * The default timestamp is always in tolerance (Stripe allows 300s and only rejects old ones);
24
+ * pass an old timestamp explicitly to test rejection.
25
+ */
26
+ export const sign_stripe_payload = (
27
+ rawBody: string,
28
+ secret: string,
29
+ timestamp = Math.floor(Date.now() / 1000),
30
+ ) => (
31
+ `t=${timestamp},v1=${crypto.createHmac('sha256', secret).update(`${timestamp}.${rawBody}`, 'utf8').digest('hex')}`
32
+ )
33
+
34
+ /**
35
+ * POSTs an event to the custom Stripe webhook route.
36
+ *
37
+ * Serializes the body here and signs those exact bytes, because the route uses
38
+ * express.raw and the signature covers the raw body verbatim — handing a pre-serialized
39
+ * string to fetch is what keeps the two in sync.
40
+ *
41
+ * Pass `signature` to override the computed one (for rejection tests).
42
+ */
43
+ export const post_stripe_webhook = async ({ stripePublicKey, event, secret, signature } : {
44
+ stripePublicKey: string,
45
+ event: object,
46
+ secret: string,
47
+ signature?: string,
48
+ }) => {
49
+ const rawBody = JSON.stringify(event)
50
+
51
+ const res = await fetch(`${host}/v1/webhooks/stripe/custom/${stripePublicKey}`, {
52
+ method: 'POST',
53
+ headers: {
54
+ // express.raw({ type: 'application/json' }) leaves req.body unset without this
55
+ 'Content-Type': 'application/json',
56
+ 'stripe-signature': signature ?? sign_stripe_payload(rawBody, secret),
57
+ },
58
+ body: rawBody,
59
+ })
60
+
61
+ return { status: res.status, data: await res.text() }
62
+ }
63
+
64
+ /**
65
+ * Creates a throwaway custom Stripe integration and returns the values needed to sign webhooks for
66
+ * it, plus a cleanup function to call in a finally block.
67
+ *
68
+ * The access token is never used: neither invoice.payment_failed nor customer.subscription.deleted
69
+ * calls the Stripe API, and constructing a Stripe client doesn't validate the key or hit the network.
70
+ */
71
+ export const setup_stripe_test_integration = async (sdk: Session) => {
72
+ // The handler looks up the integration across all businesses, so this must not collide with
73
+ // another tenant's real public key or with a concurrent run.
74
+ const stripePublicKey = `pk_test_tellescope_${unique_suffix()}`
75
+ const webhooksSecret = `whsec_test_${crypto.randomBytes(16).toString('hex')}`
76
+
77
+ await cleanup_stripe_test_integrations(sdk)
78
+
79
+ const integration = await sdk.api.integrations.createOne({
80
+ title: STRIPE_TITLE,
81
+ environment: TEST_STRIPE_ENVIRONMENT,
82
+ authentication: {
83
+ type: 'apiKey',
84
+ info: {
85
+ // all of these are required by integrationAuthenticationsValidator, and only
86
+ // external_id (the public key in the webhook URL) is actually read by the handler
87
+ access_token: 'sk_test_unused_by_this_path',
88
+ refresh_token: 'unused',
89
+ scope: '',
90
+ expiry_date: Date.now(),
91
+ token_type: 'Bearer',
92
+ external_id: stripePublicKey,
93
+ },
94
+ },
95
+ // never readable afterwards (redactions: ['all']), so the caller must keep the returned value
96
+ webhooksSecret,
97
+ })
98
+
99
+ return {
100
+ stripePublicKey,
101
+ webhooksSecret,
102
+ integrationId: integration.id,
103
+ cleanup: () => sdk.api.integrations.deleteOne(integration.id),
104
+ }
105
+ }
106
+
107
+ /** Deletes any test Stripe integrations left behind by a run that exited before its cleanup. */
108
+ export const cleanup_stripe_test_integrations = async (sdk: Session) => {
109
+ const existing = await sdk.api.integrations.getSome({ filter: { title: STRIPE_TITLE } })
110
+ .catch(() => [])
111
+
112
+ await Promise.all(
113
+ existing
114
+ .filter(i => i.environment === TEST_STRIPE_ENVIRONMENT)
115
+ .map(i => sdk.api.integrations.deleteOne(i.id).catch(console.error))
116
+ )
117
+ }
118
+
119
+ /* ------------------------------- event fixtures ------------------------------- */
120
+
121
+ // An invoice line item references its product through price.product on every modern line item, and
122
+ // through plan.product only on subscription/proration lines. Either may be a bare id or an expanded
123
+ // object, so each shape gets a builder.
124
+ export const line_with_price_product = (stripeProductId: string) => ({
125
+ price: { product: stripeProductId },
126
+ })
127
+ export const line_with_expanded_price_product = (stripeProductId: string) => ({
128
+ price: { product: { id: stripeProductId, object: 'product' } },
129
+ })
130
+ export const line_with_plan_product = (stripeProductId: string) => ({
131
+ price: null,
132
+ plan: { product: stripeProductId },
133
+ })
134
+
135
+ /**
136
+ * Minimal invoice.payment_failed envelope. constructEvent only requires valid JSON — it does not
137
+ * validate id/object/api_version — so this carries just the fields the handler reads.
138
+ */
139
+ export const invoice_payment_failed_event = ({ stripeCustomerId, lines = [], has_more = false } : {
140
+ stripeCustomerId: string,
141
+ lines?: object[],
142
+ has_more?: boolean,
143
+ }) => ({
144
+ type: 'invoice.payment_failed',
145
+ data: {
146
+ object: {
147
+ id: `in_test_${unique_suffix()}`,
148
+ customer: stripeCustomerId,
149
+ lines: { object: 'list', data: lines, has_more },
150
+ },
151
+ },
152
+ })
153
+
154
+ /** Minimal customer.subscription.deleted envelope; the handler reads items.data[].plan.product. */
155
+ export const subscription_deleted_event = ({ stripeCustomerId, stripeProductIds = [] } : {
156
+ stripeCustomerId: string,
157
+ stripeProductIds?: string[],
158
+ }) => ({
159
+ type: 'customer.subscription.deleted',
160
+ data: {
161
+ object: {
162
+ id: `sub_test_${unique_suffix()}`,
163
+ customer: stripeCustomerId,
164
+ items: {
165
+ object: 'list',
166
+ data: stripeProductIds.map(product => ({ plan: { product } })),
167
+ },
168
+ },
169
+ },
170
+ })
171
+
172
+ /**
173
+ * A subscription item references its product through price.product on every modern item, and through
174
+ * plan.product on legacy ones. Either may be a bare id or an expanded object.
175
+ */
176
+ export const item_with_price_product = (stripeProductId: string) => ({
177
+ price: { product: stripeProductId },
178
+ })
179
+ export const item_with_expanded_price_product = (stripeProductId: string) => ({
180
+ price: { product: { id: stripeProductId, object: 'product' } },
181
+ })
182
+ export const item_with_plan_product = (stripeProductId: string) => ({
183
+ price: null,
184
+ plan: { product: stripeProductId },
185
+ })
186
+
187
+ /** A pause_collection value, as Stripe carries it on a paused-collection subscription. */
188
+ export const PAUSE_COLLECTION = { behavior: 'void', resumes_at: null }
189
+
190
+ /**
191
+ * customer.subscription.updated envelope. `previousAttributes` is the whole previous_attributes bag,
192
+ * so a case can express "pause_collection key absent" (pass {}) distinctly from
193
+ * "pause_collection was null" (pass { pause_collection: null }) — that distinction is the entire
194
+ * gate the handler uses to avoid running on every renewal.
195
+ */
196
+ export const subscription_updated_event = ({
197
+ stripeCustomerId, items = [], previousAttributes = {}, pauseCollection = null, status = 'active',
198
+ } : {
199
+ stripeCustomerId: string,
200
+ items?: object[],
201
+ previousAttributes?: object,
202
+ pauseCollection?: object | null,
203
+ status?: string,
204
+ }) => ({
205
+ type: 'customer.subscription.updated',
206
+ data: {
207
+ object: {
208
+ id: `sub_test_${unique_suffix()}`,
209
+ customer: stripeCustomerId,
210
+ status,
211
+ pause_collection: pauseCollection,
212
+ items: { object: 'list', data: items, has_more: false },
213
+ },
214
+ previous_attributes: previousAttributes,
215
+ },
216
+ })
217
+
218
+ /**
219
+ * The dedicated customer.subscription.paused / .resumed events, which Stripe emits only for
220
+ * status='paused' transitions. The handler fires unconditionally on these, so no previous_attributes.
221
+ */
222
+ const subscription_pause_event = (type: string) => ({ stripeCustomerId, items = [], status } : {
223
+ stripeCustomerId: string,
224
+ items?: object[],
225
+ status?: string,
226
+ }) => ({
227
+ type,
228
+ data: {
229
+ object: {
230
+ id: `sub_test_${unique_suffix()}`,
231
+ customer: stripeCustomerId,
232
+ status: status ?? (type === 'customer.subscription.paused' ? 'paused' : 'active'),
233
+ pause_collection: null,
234
+ items: { object: 'list', data: items, has_more: false },
235
+ },
236
+ },
237
+ })
238
+
239
+ export const subscription_paused_event = subscription_pause_event('customer.subscription.paused')
240
+ export const subscription_resumed_event = subscription_pause_event('customer.subscription.resumed')
@@ -41,6 +41,7 @@ import { translations_tests } from "./api_tests/translations.test"
41
41
  import { embeddables_auth_token_tests } from "./api_tests/embeddables_auth_token.test"
42
42
  import { user_portal_settings_tests } from "./api_tests/user_portal_settings.test"
43
43
  import { integrations_redacted_tests } from "./api_tests/integrations_redacted.test"
44
+ import { welle_partner_token_tests } from "./api_tests/welle_partner_token.test"
44
45
  import { healthie_multi_integration_tests } from "./api_tests/healthie_multi_integration.test"
45
46
  import { get_some_projection_tests } from "./api_tests/get_some_projection.test"
46
47
  import { file_download_unicode_names_tests } from "./api_tests/file_download_unicode_names.test"
@@ -150,8 +151,10 @@ import { openloop_webhooks_tests } from "./api_tests/openloop_webhooks.test";
150
151
  import { beluga_pharmacy_mappings_tests } from "./api_tests/beluga_pharmacy_mappings.test";
151
152
  import { mdi_case_offerings_tests } from "./api_tests/mdi_case_offerings.test";
152
153
  import { beluga_manual_sync_tests } from "./api_tests/beluga_manual_sync.test";
154
+ import { metriport_push_form_response_tests } from "./api_tests/metriport_push_form_response.test";
153
155
  import { beluga_trigger_refill_tests } from "./api_tests/beluga_trigger_refill.test";
154
156
  import { mdi_webhooks_tests } from "./api_tests/mdi_webhooks.test";
157
+ import { stripe_webhook_triggers_tests } from "./api_tests/stripe_webhook_triggers.test";
155
158
  import { account_switcher_tests } from "./api_tests/account_switcher.test";
156
159
  import { totp_mfa_tests } from "./api_tests/totp_mfa.test";
157
160
  import { set_fields_order_templates_tests } from "./api_tests/set_fields_order_templates.test";
@@ -15293,6 +15296,7 @@ const ip_address_form_tests = async () => {
15293
15296
  await automation_trigger_tests()
15294
15297
  await resource_access_tags_tests({ sdk, sdkNonAdmin })
15295
15298
  await beluga_manual_sync_tests({ sdk, sdkNonAdmin })
15299
+ await metriport_push_form_response_tests({ sdk, sdkNonAdmin })
15296
15300
  await beluga_trigger_refill_tests({ sdk, sdkNonAdmin })
15297
15301
  await beluga_pharmacy_mappings_tests({ sdk, sdkNonAdmin })
15298
15302
  await mdi_case_offerings_tests({ sdk, sdkNonAdmin })
@@ -15338,7 +15342,9 @@ const ip_address_form_tests = async () => {
15338
15342
  await phone_tree_enduser_condition_tests({ sdk, sdkNonAdmin })
15339
15343
  await openloop_webhooks_tests({ sdk, sdkNonAdmin })
15340
15344
  await mdi_webhooks_tests({ sdk, sdkNonAdmin })
15345
+ await stripe_webhook_triggers_tests({ sdk, sdkNonAdmin })
15341
15346
  await integrations_redacted_tests({ sdk, sdkNonAdmin })
15347
+ await welle_partner_token_tests({ sdk, sdkNonAdmin })
15342
15348
  await healthie_multi_integration_tests({ sdk, sdkNonAdmin })
15343
15349
  await mdb_sort_tests({ sdk, sdkNonAdmin })
15344
15350
  await search_tests()
Binary file