@tellescope/sdk 2.0.0-beta.1 → 2.0.0-beta.2

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 (293) hide show
  1. package/CLAUDE.md +24 -0
  2. package/env.example +6 -0
  3. package/lib/cjs/sdk.d.ts +373 -3
  4. package/lib/cjs/sdk.d.ts.map +1 -1
  5. package/lib/cjs/sdk.js.map +1 -1
  6. package/lib/cjs/tests/api_tests/booking_prevent_upcoming_duplicate.test.d.ts +22 -0
  7. package/lib/cjs/tests/api_tests/booking_prevent_upcoming_duplicate.test.d.ts.map +1 -0
  8. package/lib/cjs/tests/api_tests/booking_prevent_upcoming_duplicate.test.js +506 -0
  9. package/lib/cjs/tests/api_tests/booking_prevent_upcoming_duplicate.test.js.map +1 -0
  10. package/lib/cjs/tests/api_tests/enduser_fields_sync.test.d.ts +3 -1
  11. package/lib/cjs/tests/api_tests/enduser_fields_sync.test.d.ts.map +1 -1
  12. package/lib/cjs/tests/api_tests/enduser_fields_sync.test.js +3 -1
  13. package/lib/cjs/tests/api_tests/enduser_fields_sync.test.js.map +1 -1
  14. package/lib/cjs/tests/api_tests/enduser_observations_auto_review.test.d.ts +3 -1
  15. package/lib/cjs/tests/api_tests/enduser_observations_auto_review.test.d.ts.map +1 -1
  16. package/lib/cjs/tests/api_tests/enduser_observations_auto_review.test.js +3 -1
  17. package/lib/cjs/tests/api_tests/enduser_observations_auto_review.test.js.map +1 -1
  18. package/lib/cjs/tests/api_tests/form_vital_reading.test.d.ts +7 -0
  19. package/lib/cjs/tests/api_tests/form_vital_reading.test.d.ts.map +1 -0
  20. package/lib/cjs/tests/api_tests/form_vital_reading.test.js +461 -0
  21. package/lib/cjs/tests/api_tests/form_vital_reading.test.js.map +1 -0
  22. package/lib/cjs/tests/api_tests/journey_statistics_cache.test.d.ts +26 -0
  23. package/lib/cjs/tests/api_tests/journey_statistics_cache.test.d.ts.map +1 -0
  24. package/lib/cjs/tests/api_tests/journey_statistics_cache.test.js +232 -0
  25. package/lib/cjs/tests/api_tests/journey_statistics_cache.test.js.map +1 -0
  26. package/lib/cjs/tests/api_tests/journey_step_manual_count.test.d.ts +34 -0
  27. package/lib/cjs/tests/api_tests/journey_step_manual_count.test.d.ts.map +1 -0
  28. package/lib/cjs/tests/api_tests/journey_step_manual_count.test.js +243 -0
  29. package/lib/cjs/tests/api_tests/journey_step_manual_count.test.js.map +1 -0
  30. package/lib/cjs/tests/api_tests/load_threads_care_team.test.d.ts +3 -1
  31. package/lib/cjs/tests/api_tests/load_threads_care_team.test.d.ts.map +1 -1
  32. package/lib/cjs/tests/api_tests/load_threads_care_team.test.js +3 -1
  33. package/lib/cjs/tests/api_tests/load_threads_care_team.test.js.map +1 -1
  34. package/lib/cjs/tests/api_tests/organization_signup.test.d.ts +3 -1
  35. package/lib/cjs/tests/api_tests/organization_signup.test.d.ts.map +1 -1
  36. package/lib/cjs/tests/api_tests/organization_signup.test.js +3 -1
  37. package/lib/cjs/tests/api_tests/organization_signup.test.js.map +1 -1
  38. package/lib/cjs/tests/api_tests/phone_embed_leave_message.test.d.ts +7 -0
  39. package/lib/cjs/tests/api_tests/phone_embed_leave_message.test.d.ts.map +1 -0
  40. package/lib/cjs/tests/api_tests/phone_embed_leave_message.test.js +571 -0
  41. package/lib/cjs/tests/api_tests/phone_embed_leave_message.test.js.map +1 -0
  42. package/lib/cjs/tests/api_tests/programs.test.d.ts +3 -1
  43. package/lib/cjs/tests/api_tests/programs.test.d.ts.map +1 -1
  44. package/lib/cjs/tests/api_tests/programs.test.js +73 -17
  45. package/lib/cjs/tests/api_tests/programs.test.js.map +1 -1
  46. package/lib/cjs/tests/api_tests/public_form_case_insensitive_match.test.d.ts +7 -0
  47. package/lib/cjs/tests/api_tests/public_form_case_insensitive_match.test.d.ts.map +1 -0
  48. package/lib/cjs/tests/api_tests/public_form_case_insensitive_match.test.js +733 -0
  49. package/lib/cjs/tests/api_tests/public_form_case_insensitive_match.test.js.map +1 -0
  50. package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts +3 -1
  51. package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts.map +1 -1
  52. package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js +3 -1
  53. package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js.map +1 -1
  54. package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts +3 -1
  55. package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts.map +1 -1
  56. package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.js +3 -1
  57. package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.js.map +1 -1
  58. package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.d.ts +3 -1
  59. package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.d.ts.map +1 -1
  60. package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.js +3 -1
  61. package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.js.map +1 -1
  62. package/lib/cjs/tests/api_tests/security/form-logic-js-execution.test.d.ts +3 -1
  63. package/lib/cjs/tests/api_tests/security/form-logic-js-execution.test.d.ts.map +1 -1
  64. package/lib/cjs/tests/api_tests/security/form-logic-js-execution.test.js +3 -1
  65. package/lib/cjs/tests/api_tests/security/form-logic-js-execution.test.js.map +1 -1
  66. package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.d.ts.map +1 -1
  67. package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js +424 -39
  68. package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js.map +1 -1
  69. package/lib/cjs/tests/api_tests/security/register_business_id_injection.test.js +2 -2
  70. package/lib/cjs/tests/api_tests/security/register_business_id_injection.test.js.map +1 -1
  71. package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts +3 -1
  72. package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts.map +1 -1
  73. package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js +3 -1
  74. package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js.map +1 -1
  75. package/lib/cjs/tests/api_tests/session_for_join_link.test.d.ts +10 -0
  76. package/lib/cjs/tests/api_tests/session_for_join_link.test.d.ts.map +1 -0
  77. package/lib/cjs/tests/api_tests/session_for_join_link.test.js +147 -0
  78. package/lib/cjs/tests/api_tests/session_for_join_link.test.js.map +1 -0
  79. package/lib/cjs/tests/api_tests/set_fields_order_templates.test.d.ts.map +1 -1
  80. package/lib/cjs/tests/api_tests/set_fields_order_templates.test.js +191 -88
  81. package/lib/cjs/tests/api_tests/set_fields_order_templates.test.js.map +1 -1
  82. package/lib/cjs/tests/api_tests/stripe_checkout_items.test.d.ts +17 -0
  83. package/lib/cjs/tests/api_tests/stripe_checkout_items.test.d.ts.map +1 -0
  84. package/lib/cjs/tests/api_tests/stripe_checkout_items.test.js +219 -0
  85. package/lib/cjs/tests/api_tests/stripe_checkout_items.test.js.map +1 -0
  86. package/lib/cjs/tests/api_tests/stripe_multiple_customer_ids.test.d.ts +5 -0
  87. package/lib/cjs/tests/api_tests/stripe_multiple_customer_ids.test.d.ts.map +1 -0
  88. package/lib/cjs/tests/api_tests/stripe_multiple_customer_ids.test.js +396 -0
  89. package/lib/cjs/tests/api_tests/stripe_multiple_customer_ids.test.js.map +1 -0
  90. package/lib/cjs/tests/api_tests/tickets_bulk_assign_from_queue.test.d.ts +3 -1
  91. package/lib/cjs/tests/api_tests/tickets_bulk_assign_from_queue.test.d.ts.map +1 -1
  92. package/lib/cjs/tests/api_tests/tickets_bulk_assign_from_queue.test.js +3 -1
  93. package/lib/cjs/tests/api_tests/tickets_bulk_assign_from_queue.test.js.map +1 -1
  94. package/lib/cjs/tests/api_tests/totp_mfa.test.d.ts.map +1 -1
  95. package/lib/cjs/tests/api_tests/totp_mfa.test.js +37 -31
  96. package/lib/cjs/tests/api_tests/totp_mfa.test.js.map +1 -1
  97. package/lib/cjs/tests/api_tests/trigger_contains_matching.test.d.ts +3 -1
  98. package/lib/cjs/tests/api_tests/trigger_contains_matching.test.d.ts.map +1 -1
  99. package/lib/cjs/tests/api_tests/trigger_contains_matching.test.js +3 -1
  100. package/lib/cjs/tests/api_tests/trigger_contains_matching.test.js.map +1 -1
  101. package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.d.ts +3 -1
  102. package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.d.ts.map +1 -1
  103. package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.js +3 -1
  104. package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.js.map +1 -1
  105. package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.d.ts +3 -1
  106. package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.d.ts.map +1 -1
  107. package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.js +3 -1
  108. package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.js.map +1 -1
  109. package/lib/cjs/tests/api_tests/vitl_push.test.d.ts +7 -0
  110. package/lib/cjs/tests/api_tests/vitl_push.test.d.ts.map +1 -0
  111. package/lib/cjs/tests/api_tests/vitl_push.test.js +341 -0
  112. package/lib/cjs/tests/api_tests/vitl_push.test.js.map +1 -0
  113. package/lib/cjs/tests/setup_isolated.d.ts +48 -0
  114. package/lib/cjs/tests/setup_isolated.d.ts.map +1 -1
  115. package/lib/cjs/tests/setup_isolated.js +119 -1
  116. package/lib/cjs/tests/setup_isolated.js.map +1 -1
  117. package/lib/cjs/tests/tests.d.ts.map +1 -1
  118. package/lib/cjs/tests/tests.js +374 -292
  119. package/lib/cjs/tests/tests.js.map +1 -1
  120. package/lib/cjs/tests/unit_tests/isolated_suite_discovery.test.d.ts +2 -0
  121. package/lib/cjs/tests/unit_tests/isolated_suite_discovery.test.d.ts.map +1 -0
  122. package/lib/cjs/tests/unit_tests/isolated_suite_discovery.test.js +201 -0
  123. package/lib/cjs/tests/unit_tests/isolated_suite_discovery.test.js.map +1 -0
  124. package/lib/cjs/tests/unit_tests/programs_conditional_logic.test.d.ts +3 -0
  125. package/lib/cjs/tests/unit_tests/programs_conditional_logic.test.d.ts.map +1 -0
  126. package/lib/cjs/tests/unit_tests/programs_conditional_logic.test.js +120 -0
  127. package/lib/cjs/tests/unit_tests/programs_conditional_logic.test.js.map +1 -0
  128. package/lib/cjs/tests/webhooks_tests.js +44 -34
  129. package/lib/cjs/tests/webhooks_tests.js.map +1 -1
  130. package/lib/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  131. package/lib/esm/sdk.d.ts +373 -3
  132. package/lib/esm/sdk.d.ts.map +1 -1
  133. package/lib/esm/sdk.js.map +1 -1
  134. package/lib/esm/tests/api_tests/booking_prevent_upcoming_duplicate.test.d.ts +22 -0
  135. package/lib/esm/tests/api_tests/booking_prevent_upcoming_duplicate.test.d.ts.map +1 -0
  136. package/lib/esm/tests/api_tests/booking_prevent_upcoming_duplicate.test.js +501 -0
  137. package/lib/esm/tests/api_tests/booking_prevent_upcoming_duplicate.test.js.map +1 -0
  138. package/lib/esm/tests/api_tests/enduser_fields_sync.test.d.ts +3 -1
  139. package/lib/esm/tests/api_tests/enduser_fields_sync.test.d.ts.map +1 -1
  140. package/lib/esm/tests/api_tests/enduser_fields_sync.test.js +2 -0
  141. package/lib/esm/tests/api_tests/enduser_fields_sync.test.js.map +1 -1
  142. package/lib/esm/tests/api_tests/enduser_observations_auto_review.test.d.ts +3 -1
  143. package/lib/esm/tests/api_tests/enduser_observations_auto_review.test.d.ts.map +1 -1
  144. package/lib/esm/tests/api_tests/enduser_observations_auto_review.test.js +2 -0
  145. package/lib/esm/tests/api_tests/enduser_observations_auto_review.test.js.map +1 -1
  146. package/lib/esm/tests/api_tests/form_vital_reading.test.d.ts +7 -0
  147. package/lib/esm/tests/api_tests/form_vital_reading.test.d.ts.map +1 -0
  148. package/lib/esm/tests/api_tests/form_vital_reading.test.js +456 -0
  149. package/lib/esm/tests/api_tests/form_vital_reading.test.js.map +1 -0
  150. package/lib/esm/tests/api_tests/journey_statistics_cache.test.d.ts +26 -0
  151. package/lib/esm/tests/api_tests/journey_statistics_cache.test.d.ts.map +1 -0
  152. package/lib/esm/tests/api_tests/journey_statistics_cache.test.js +227 -0
  153. package/lib/esm/tests/api_tests/journey_statistics_cache.test.js.map +1 -0
  154. package/lib/esm/tests/api_tests/journey_step_manual_count.test.d.ts +34 -0
  155. package/lib/esm/tests/api_tests/journey_step_manual_count.test.d.ts.map +1 -0
  156. package/lib/esm/tests/api_tests/journey_step_manual_count.test.js +238 -0
  157. package/lib/esm/tests/api_tests/journey_step_manual_count.test.js.map +1 -0
  158. package/lib/esm/tests/api_tests/load_threads_care_team.test.d.ts +3 -1
  159. package/lib/esm/tests/api_tests/load_threads_care_team.test.d.ts.map +1 -1
  160. package/lib/esm/tests/api_tests/load_threads_care_team.test.js +2 -0
  161. package/lib/esm/tests/api_tests/load_threads_care_team.test.js.map +1 -1
  162. package/lib/esm/tests/api_tests/organization_signup.test.d.ts +3 -1
  163. package/lib/esm/tests/api_tests/organization_signup.test.d.ts.map +1 -1
  164. package/lib/esm/tests/api_tests/organization_signup.test.js +2 -0
  165. package/lib/esm/tests/api_tests/organization_signup.test.js.map +1 -1
  166. package/lib/esm/tests/api_tests/phone_embed_leave_message.test.d.ts +7 -0
  167. package/lib/esm/tests/api_tests/phone_embed_leave_message.test.d.ts.map +1 -0
  168. package/lib/esm/tests/api_tests/phone_embed_leave_message.test.js +566 -0
  169. package/lib/esm/tests/api_tests/phone_embed_leave_message.test.js.map +1 -0
  170. package/lib/esm/tests/api_tests/programs.test.d.ts +3 -1
  171. package/lib/esm/tests/api_tests/programs.test.d.ts.map +1 -1
  172. package/lib/esm/tests/api_tests/programs.test.js +72 -16
  173. package/lib/esm/tests/api_tests/programs.test.js.map +1 -1
  174. package/lib/esm/tests/api_tests/public_form_case_insensitive_match.test.d.ts +7 -0
  175. package/lib/esm/tests/api_tests/public_form_case_insensitive_match.test.d.ts.map +1 -0
  176. package/lib/esm/tests/api_tests/public_form_case_insensitive_match.test.js +728 -0
  177. package/lib/esm/tests/api_tests/public_form_case_insensitive_match.test.js.map +1 -0
  178. package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts +3 -1
  179. package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts.map +1 -1
  180. package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js +2 -0
  181. package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js.map +1 -1
  182. package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts +3 -1
  183. package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts.map +1 -1
  184. package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.js +2 -0
  185. package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.js.map +1 -1
  186. package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.d.ts +3 -1
  187. package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.d.ts.map +1 -1
  188. package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.js +2 -0
  189. package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.js.map +1 -1
  190. package/lib/esm/tests/api_tests/security/form-logic-js-execution.test.d.ts +3 -1
  191. package/lib/esm/tests/api_tests/security/form-logic-js-execution.test.d.ts.map +1 -1
  192. package/lib/esm/tests/api_tests/security/form-logic-js-execution.test.js +2 -0
  193. package/lib/esm/tests/api_tests/security/form-logic-js-execution.test.js.map +1 -1
  194. package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.d.ts.map +1 -1
  195. package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js +424 -39
  196. package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js.map +1 -1
  197. package/lib/esm/tests/api_tests/security/register_business_id_injection.test.js +2 -2
  198. package/lib/esm/tests/api_tests/security/register_business_id_injection.test.js.map +1 -1
  199. package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts +3 -1
  200. package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts.map +1 -1
  201. package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js +2 -0
  202. package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js.map +1 -1
  203. package/lib/esm/tests/api_tests/session_for_join_link.test.d.ts +10 -0
  204. package/lib/esm/tests/api_tests/session_for_join_link.test.d.ts.map +1 -0
  205. package/lib/esm/tests/api_tests/session_for_join_link.test.js +142 -0
  206. package/lib/esm/tests/api_tests/session_for_join_link.test.js.map +1 -0
  207. package/lib/esm/tests/api_tests/set_fields_order_templates.test.d.ts.map +1 -1
  208. package/lib/esm/tests/api_tests/set_fields_order_templates.test.js +191 -88
  209. package/lib/esm/tests/api_tests/set_fields_order_templates.test.js.map +1 -1
  210. package/lib/esm/tests/api_tests/stripe_checkout_items.test.d.ts +17 -0
  211. package/lib/esm/tests/api_tests/stripe_checkout_items.test.d.ts.map +1 -0
  212. package/lib/esm/tests/api_tests/stripe_checkout_items.test.js +214 -0
  213. package/lib/esm/tests/api_tests/stripe_checkout_items.test.js.map +1 -0
  214. package/lib/esm/tests/api_tests/stripe_multiple_customer_ids.test.d.ts +5 -0
  215. package/lib/esm/tests/api_tests/stripe_multiple_customer_ids.test.d.ts.map +1 -0
  216. package/lib/esm/tests/api_tests/stripe_multiple_customer_ids.test.js +388 -0
  217. package/lib/esm/tests/api_tests/stripe_multiple_customer_ids.test.js.map +1 -0
  218. package/lib/esm/tests/api_tests/tickets_bulk_assign_from_queue.test.d.ts +3 -1
  219. package/lib/esm/tests/api_tests/tickets_bulk_assign_from_queue.test.d.ts.map +1 -1
  220. package/lib/esm/tests/api_tests/tickets_bulk_assign_from_queue.test.js +2 -0
  221. package/lib/esm/tests/api_tests/tickets_bulk_assign_from_queue.test.js.map +1 -1
  222. package/lib/esm/tests/api_tests/totp_mfa.test.d.ts.map +1 -1
  223. package/lib/esm/tests/api_tests/totp_mfa.test.js +37 -31
  224. package/lib/esm/tests/api_tests/totp_mfa.test.js.map +1 -1
  225. package/lib/esm/tests/api_tests/trigger_contains_matching.test.d.ts +3 -1
  226. package/lib/esm/tests/api_tests/trigger_contains_matching.test.d.ts.map +1 -1
  227. package/lib/esm/tests/api_tests/trigger_contains_matching.test.js +2 -0
  228. package/lib/esm/tests/api_tests/trigger_contains_matching.test.js.map +1 -1
  229. package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.d.ts +3 -1
  230. package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.d.ts.map +1 -1
  231. package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.js +2 -0
  232. package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.js.map +1 -1
  233. package/lib/esm/tests/api_tests/vital_review_trigger_action.test.d.ts +3 -1
  234. package/lib/esm/tests/api_tests/vital_review_trigger_action.test.d.ts.map +1 -1
  235. package/lib/esm/tests/api_tests/vital_review_trigger_action.test.js +2 -0
  236. package/lib/esm/tests/api_tests/vital_review_trigger_action.test.js.map +1 -1
  237. package/lib/esm/tests/api_tests/vitl_push.test.d.ts +7 -0
  238. package/lib/esm/tests/api_tests/vitl_push.test.d.ts.map +1 -0
  239. package/lib/esm/tests/api_tests/vitl_push.test.js +333 -0
  240. package/lib/esm/tests/api_tests/vitl_push.test.js.map +1 -0
  241. package/lib/esm/tests/setup_isolated.d.ts +48 -0
  242. package/lib/esm/tests/setup_isolated.d.ts.map +1 -1
  243. package/lib/esm/tests/setup_isolated.js +114 -0
  244. package/lib/esm/tests/setup_isolated.js.map +1 -1
  245. package/lib/esm/tests/tests.d.ts.map +1 -1
  246. package/lib/esm/tests/tests.js +375 -293
  247. package/lib/esm/tests/tests.js.map +1 -1
  248. package/lib/esm/tests/unit_tests/isolated_suite_discovery.test.d.ts +2 -0
  249. package/lib/esm/tests/unit_tests/isolated_suite_discovery.test.d.ts.map +1 -0
  250. package/lib/esm/tests/unit_tests/isolated_suite_discovery.test.js +174 -0
  251. package/lib/esm/tests/unit_tests/isolated_suite_discovery.test.js.map +1 -0
  252. package/lib/esm/tests/unit_tests/programs_conditional_logic.test.d.ts +3 -0
  253. package/lib/esm/tests/unit_tests/programs_conditional_logic.test.d.ts.map +1 -0
  254. package/lib/esm/tests/unit_tests/programs_conditional_logic.test.js +117 -0
  255. package/lib/esm/tests/unit_tests/programs_conditional_logic.test.js.map +1 -0
  256. package/lib/esm/tests/webhooks_tests.js +44 -34
  257. package/lib/esm/tests/webhooks_tests.js.map +1 -1
  258. package/lib/tsconfig.tsbuildinfo +1 -1
  259. package/package.json +12 -11
  260. package/src/sdk.ts +3 -2
  261. package/src/tests/api_tests/booking_prevent_upcoming_duplicate.test.ts +541 -0
  262. package/src/tests/api_tests/enduser_fields_sync.test.ts +4 -0
  263. package/src/tests/api_tests/enduser_observations_auto_review.test.ts +4 -0
  264. package/src/tests/api_tests/form_vital_reading.test.ts +393 -0
  265. package/src/tests/api_tests/journey_statistics_cache.test.ts +237 -0
  266. package/src/tests/api_tests/journey_step_manual_count.test.ts +220 -0
  267. package/src/tests/api_tests/load_threads_care_team.test.ts +4 -0
  268. package/src/tests/api_tests/organization_signup.test.ts +4 -0
  269. package/src/tests/api_tests/phone_embed_leave_message.test.ts +541 -0
  270. package/src/tests/api_tests/programs.test.ts +351 -0
  271. package/src/tests/api_tests/public_form_case_insensitive_match.test.ts +632 -0
  272. package/src/tests/api_tests/security/add-to-journey-foreign-enduserid.test.ts +4 -0
  273. package/src/tests/api_tests/security/appointment-reschedule-ownership.test.ts +4 -0
  274. package/src/tests/api_tests/security/file-link-session-confinement.test.ts +4 -0
  275. package/src/tests/api_tests/security/form-logic-js-execution.test.ts +4 -0
  276. package/src/tests/api_tests/security/formsort_webhook_auth_field_lock.test.ts +225 -0
  277. package/src/tests/api_tests/security/register_business_id_injection.test.ts +2 -2
  278. package/src/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.ts +4 -0
  279. package/src/tests/api_tests/session_for_join_link.test.ts +99 -0
  280. package/src/tests/api_tests/set_fields_order_templates.test.ts +94 -9
  281. package/src/tests/api_tests/stripe_checkout_items.test.ts +186 -0
  282. package/src/tests/api_tests/tickets_bulk_assign_from_queue.test.ts +4 -0
  283. package/src/tests/api_tests/totp_mfa.test.ts +10 -5
  284. package/src/tests/api_tests/trigger_contains_matching.test.ts +4 -0
  285. package/src/tests/api_tests/user_logs_restore_deleted.test.ts +4 -0
  286. package/src/tests/api_tests/vital_review_trigger_action.test.ts +455 -0
  287. package/src/tests/api_tests/vitl_push.test.ts +323 -0
  288. package/src/tests/setup_isolated.ts +140 -0
  289. package/src/tests/tests.ts +124 -30
  290. package/src/tests/unit_tests/isolated_suite_discovery.test.ts +178 -0
  291. package/src/tests/unit_tests/programs_conditional_logic.test.ts +189 -0
  292. package/src/tests/webhooks_tests.ts +6 -1
  293. package/test_generated.pdf +0 -0
@@ -0,0 +1,541 @@
1
+ require('source-map-support').install();
2
+
3
+ import { EnduserSession, Session } from "../../sdk"
4
+ import {
5
+ assert_checks,
6
+ Check,
7
+ setup_isolated_tenant,
8
+ IsolatedSuite,
9
+ } from "../setup_isolated"
10
+
11
+ const host = process.env.API_URL || 'http://localhost:8080' as const
12
+
13
+ const RAND = () => Math.random().toString(36).slice(2, 10)
14
+
15
+ /**
16
+ * Coverage for the Phone Embed backend, `sms_messages.leave_message` (api/v1/api.ts) — the endpoint
17
+ * behind the "Leave a note, and we will text you back soon" widget.
18
+ *
19
+ * Two changes are under test:
20
+ *
21
+ * 1. Matching. Historically phone-only: a submission from a number that already existed was filed
22
+ * onto that contact no matter whose name was typed. Now, for orgs that have opted into
23
+ * `settings.endusers.matchEmailAndNames`, phone AND fname AND lname must all match, compared
24
+ * through `nameMatchValue` so `settings.endusers.caseInsensitiveMatching` applies exactly as it
25
+ * does to public form intake. Both settings OFF must behave exactly as before.
26
+ *
27
+ * 2. The "Incoming Message" automation trigger. Submissions used to fire only "Contact Created" and
28
+ * "Field Equals"; they now go through `handleIncomingCommunication` with channel 'SMS' and NO
29
+ * destination, so unfiltered and SMS-filtered triggers fire while destination-filtered and
30
+ * Email-filtered ones stay silent.
31
+ *
32
+ * Fixture warning, inherited from public_form_case_insensitive_match: `nameValidator` applies
33
+ * `first_letter_capitalized`, so a submitted "carol" is already stored as "Carol" and a
34
+ * "carol"/"Carol" fixture would pass with the setting OFF, proving nothing. The case-insensitive
35
+ * cases here differ AFTER the first letter ("MCDONALD" vs "mcdonald" -> "Mcdonald").
36
+ *
37
+ * `setup_isolated` rather than `setup_tests` because this flips ORGANIZATION settings, which on the
38
+ * shared fixture org would change behavior under every concurrently running suite, and because it
39
+ * asserts on "how many contacts carry this phone number", which needs a tenant nobody else writes to.
40
+ *
41
+ * RATE LIMIT: `leave_message` is throttled per IP at 10/min and 50/hour (`enduser-leave_message-ip`).
42
+ * Every submission in this suite comes from one IP, so the post count is kept to 8 deliberately — see
43
+ * POST_BUDGET below. Adding a submission risks 429ing the run rather than failing a check.
44
+ *
45
+ * Runs standalone or as part of the main suite:
46
+ * npm run test:modular -- api_tests/phone_embed_leave_message
47
+ */
48
+
49
+ /** Hard ceiling this suite stays under; `leave_message` allows 10 posts per minute per IP. */
50
+ const POST_BUDGET = 8
51
+
52
+ /** Polls until `check` returns truthy or the budget runs out. The endpoint responds 204 before its
53
+ * trigger work finishes (handleIncomingCommunication is deliberately not awaited), so every
54
+ * assertion about a trigger's effect has to wait for it. Returns the last value either way, so a
55
+ * timeout produces a failed check with real evidence rather than a thrown error. */
56
+ const pollFor = async <T>(
57
+ describe: string,
58
+ read: () => Promise<T>,
59
+ check: (value: T) => boolean,
60
+ { timeoutMs = 15_000, intervalMs = 500 } = {},
61
+ ): Promise<T> => {
62
+ const deadline = Date.now() + timeoutMs
63
+ let last = await read()
64
+ while (!check(last) && Date.now() < deadline) {
65
+ await new Promise(resolve => setTimeout(resolve, intervalMs))
66
+ last = await read()
67
+ }
68
+ if (!check(last)) console.log(` (timed out waiting for ${describe} after ${timeoutMs}ms)`)
69
+ return last
70
+ }
71
+
72
+ export const phone_embed_leave_message_checks = async (): Promise<Check[]> => {
73
+ const checks: Check[] = []
74
+ const record = (label: string, pass: boolean, detail: string) => checks.push({ label, pass, detail })
75
+
76
+ let tenant: Awaited<ReturnType<typeof setup_isolated_tenant>> | undefined
77
+
78
+ try {
79
+ tenant = await setup_isolated_tenant({
80
+ name: `PhoneEmbedLeaveMessage ${RAND()}`,
81
+ users: [{ isAdmin: true }],
82
+ apiKeys: 0,
83
+ })
84
+ const { sdk, businessId } = tenant
85
+
86
+ /* ---------------------------------------------------------------- helpers */
87
+
88
+ let posts = 0
89
+ /** One Phone Embed submission, exactly as the widget makes it (unauthenticated EnduserSession). */
90
+ const submit = async (fname: string, lname: string, phone: string, message: string) => {
91
+ posts += 1
92
+ if (posts > POST_BUDGET) {
93
+ throw new Error(`Suite exceeded its ${POST_BUDGET}-post budget; the per-IP throttle will 429`)
94
+ }
95
+ const enduserSDK = new EnduserSession({ host, businessId })
96
+ await enduserSDK.api.sms_messages.leave_message({ businessId, fname, lname, phone, message })
97
+ }
98
+
99
+ /** Every contact in the tenant carrying this phone number. The matcher's observable output: one
100
+ * contact means the submission matched, two means it inserted a second record. */
101
+ const contactsWithPhone = async (phone: string) => (
102
+ await sdk.api.endusers.getSome({ filter: { phone } })
103
+ )
104
+
105
+ /** Inbound notes filed onto one contact, newest last. */
106
+ const messagesFor = async (enduserId: string) => (
107
+ await sdk.api.sms_messages.getSome({ filter: { enduserId } })
108
+ )
109
+
110
+ /** Writes BOTH flags every time. Writing only one would be ambiguous about whether the update
111
+ * merges into `settings.endusers` or replaces it, and this suite needs them on simultaneously. */
112
+ const setSettings = async (matchEmailAndNames: boolean, caseInsensitiveMatching: boolean) => {
113
+ await sdk.api.organizations.updateOne(businessId, {
114
+ settings: { endusers: { matchEmailAndNames, caseInsensitiveMatching } },
115
+ })
116
+ const after = await sdk.api.organizations.getOne(businessId)
117
+ record(
118
+ `fixture: settings applied (matchEmailAndNames=${matchEmailAndNames}, caseInsensitive=${caseInsensitiveMatching})`,
119
+ after.settings?.endusers?.matchEmailAndNames === matchEmailAndNames
120
+ && after.settings?.endusers?.caseInsensitiveMatching === caseInsensitiveMatching,
121
+ `org now reports matchEmailAndNames=${after.settings?.endusers?.matchEmailAndNames}, `
122
+ + `caseInsensitiveMatching=${after.settings?.endusers?.caseInsensitiveMatching} `
123
+ + `(if these disagree every check in the section below is vacuous)`,
124
+ )
125
+ }
126
+
127
+ /* ------------------------------------------- triggers, created before any submission */
128
+
129
+ const TAG_ANY = `embed-any-${RAND()}`
130
+ const TAG_SMS = `embed-sms-${RAND()}`
131
+ const TAG_EMAIL = `embed-email-${RAND()}`
132
+ const TAG_DESTINATION = `embed-destination-${RAND()}`
133
+ const TAG_CREATED = `embed-created-${RAND()}`
134
+
135
+ const addTagsTrigger = async (title: string, event: any, tag: string) => (
136
+ await sdk.api.automation_triggers.createOne({
137
+ title: `${title} ${RAND()}`,
138
+ status: 'Active',
139
+ event,
140
+ action: { type: 'Add Tags', info: { tags: [tag] } },
141
+ })
142
+ )
143
+
144
+ await addTagsTrigger('Embed incoming, unfiltered', { type: 'Incoming Message', info: {} }, TAG_ANY)
145
+ await addTagsTrigger('Embed incoming, SMS only', { type: 'Incoming Message', info: { channels: ['SMS'] } }, TAG_SMS)
146
+ await addTagsTrigger('Embed incoming, Email only', { type: 'Incoming Message', info: { channels: ['Email'] } }, TAG_EMAIL)
147
+ await addTagsTrigger(
148
+ 'Embed incoming, destination filtered',
149
+ { type: 'Incoming Message', info: { destinations: ['+15005550123'] } },
150
+ TAG_DESTINATION,
151
+ )
152
+ await addTagsTrigger('Embed contact created', { type: 'Contact Created', info: {} }, TAG_CREATED)
153
+
154
+ // Executable guard for the journey-removal race. `handleIncomingCommunication` runs
155
+ // `handleRemoveFromJourneysForIncomingCommunication`, which strips a contact out of every journey
156
+ // marked `onIncomingEnduserCommunication: 'Remove'`. The handler passes it the enduser object it
157
+ // already has, and for a BRAND-NEW contact that object carries no journeys, so the removal no-ops
158
+ // and the contact stays in the journey "Contact Created" is concurrently adding them to.
159
+ //
160
+ // This is the only check that would fail if a future edit replaced `enduser: result` with a
161
+ // re-read of the contact — a comment alone cannot catch that. Both halves are asserted below,
162
+ // against the two submissions section 1 already makes, so this costs no extra posts:
163
+ // new contact -> removal no-ops, contact STAYS in the journey
164
+ // matched one -> removal really runs, contact is REMOVED (Twilio inbound-webhook parity)
165
+ const removeOnIncomingJourney = await sdk.api.journeys.createOne({
166
+ title: `Embed Remove On Incoming ${RAND()}`,
167
+ onIncomingEnduserCommunication: 'Remove',
168
+ })
169
+ await sdk.api.automation_triggers.createOne({
170
+ title: `Embed add to remove-on-incoming journey ${RAND()}`,
171
+ status: 'Active',
172
+ event: { type: 'Contact Created', info: {} },
173
+ action: { type: 'Add To Journey', info: { journeyId: removeOnIncomingJourney.id } },
174
+ })
175
+
176
+ const inRemoveJourney = async (enduserId: string) => (
177
+ Object.keys((await sdk.api.endusers.getOne(enduserId)).journeys ?? {})
178
+ .includes(removeOnIncomingJourney.id)
179
+ )
180
+
181
+ /* ======== 1. both settings OFF: unchanged legacy behavior, plus the new trigger ======== */
182
+
183
+ await setSettings(false, false)
184
+
185
+ const PHONE_LEGACY = '+15005550101'
186
+ {
187
+ /** Captured after the first submission so the second (a MATCH) can be shown to advance it. */
188
+ let stampBeforeMatch: Date | string | undefined
189
+
190
+ await submit('Alice', 'Smith', PHONE_LEGACY, 'off note one')
191
+
192
+ const afterFirst = await pollFor(
193
+ 'the first submission to create a contact',
194
+ () => contactsWithPhone(PHONE_LEGACY),
195
+ found => found.length >= 1,
196
+ )
197
+ record(
198
+ 'settings OFF: a submission from an unknown number creates a contact',
199
+ afterFirst.length === 1 && afterFirst[0].fname === 'Alice' && afterFirst[0].lname === 'Smith',
200
+ `contacts on ${PHONE_LEGACY}: ${afterFirst.length} `
201
+ + `(${afterFirst.map(e => `${e.fname} ${e.lname}`).join(', ') || 'none'}); expected exactly one Alice Smith`,
202
+ )
203
+
204
+ const alice = afterFirst[0]
205
+
206
+ if (alice) {
207
+ const notes = await messagesFor(alice.id)
208
+ record(
209
+ 'the note is filed as an inbound, log-only SMS (so the patient is never texted it back)',
210
+ notes.length === 1 && notes[0].inbound === true && notes[0].logOnly === true
211
+ && notes[0].message === 'off note one',
212
+ `messages=${notes.length}: ${JSON.stringify(notes.map(n => ({ m: n.message, inbound: n.inbound, logOnly: n.logOnly })))}`,
213
+ )
214
+
215
+ // The trigger work runs after the 204, so poll for the unfiltered tag before reading the rest.
216
+ const tagged = await pollFor(
217
+ 'the Incoming Message trigger to fire',
218
+ async () => (await sdk.api.endusers.getOne(alice.id)).tags ?? [],
219
+ tags => tags.includes(TAG_ANY),
220
+ )
221
+
222
+ record(
223
+ 'an unfiltered Incoming Message trigger FIRES for an embed submission (the headline ask)',
224
+ tagged.includes(TAG_ANY),
225
+ `tags on the contact: ${JSON.stringify(tagged)}; expected to contain ${TAG_ANY}`,
226
+ )
227
+ record(
228
+ 'an Incoming Message trigger filtered to channel SMS fires',
229
+ tagged.includes(TAG_SMS),
230
+ `tags on the contact: ${JSON.stringify(tagged)}; expected to contain ${TAG_SMS}`,
231
+ )
232
+ record(
233
+ 'an Incoming Message trigger filtered to channel Email does NOT fire',
234
+ !tagged.includes(TAG_EMAIL),
235
+ `tags on the contact: ${JSON.stringify(tagged)}; must NOT contain ${TAG_EMAIL}`,
236
+ )
237
+ record(
238
+ 'an Incoming Message trigger filtered to a DESTINATION stays silent (no Twilio number is involved)',
239
+ !tagged.includes(TAG_DESTINATION),
240
+ `tags on the contact: ${JSON.stringify(tagged)}; must NOT contain ${TAG_DESTINATION}`,
241
+ )
242
+ record(
243
+ 'the Contact Created trigger still fires for a contact the submission created (regression)',
244
+ tagged.includes(TAG_CREATED),
245
+ `tags on the contact: ${JSON.stringify(tagged)}; expected to contain ${TAG_CREATED}`,
246
+ )
247
+
248
+ const stamped = await sdk.api.endusers.getOne(alice.id)
249
+ record(
250
+ 'recentInboundSMSAt is stamped, so the contact sorts as recently active in the inbox',
251
+ !!stamped.recentInboundSMSAt,
252
+ `recentInboundSMSAt=${JSON.stringify(stamped.recentInboundSMSAt)}`,
253
+ )
254
+ stampBeforeMatch = stamped.recentInboundSMSAt
255
+
256
+ // half one of the race guard: the contact "Contact Created" just added must still be there
257
+ await pollFor(
258
+ 'the Contact Created trigger to add the new contact to the remove-on-incoming journey',
259
+ () => inRemoveJourney(alice.id),
260
+ present => present,
261
+ )
262
+ // settle, so a removal that fired slightly late would still be caught rather than missed
263
+ await new Promise(resolve => setTimeout(resolve, 2_000))
264
+ record(
265
+ 'a brand-new contact is NOT stripped from the journey Contact Created just added them to',
266
+ await inRemoveJourney(alice.id),
267
+ `contact ${alice.id} must still be in journey ${removeOnIncomingJourney.id}; `
268
+ + `journeys=${JSON.stringify(Object.keys((await sdk.api.endusers.getOne(alice.id)).journeys ?? {}))}`,
269
+ )
270
+ }
271
+
272
+ // The legacy carve-out: with the setting OFF a DIFFERENT name on the same number must still
273
+ // match, exactly as it has for years. This is the regression that proves the gate works.
274
+ await submit('Bob', 'Jones', PHONE_LEGACY, 'off note two')
275
+
276
+ // A direct read, not a poll: the handler commits the match-or-insert decision BEFORE it answers
277
+ // 204, so the contact count is already final here. Only the trigger and stamp writes lag.
278
+ const afterSecond = await contactsWithPhone(PHONE_LEGACY)
279
+ record(
280
+ 'settings OFF: a DIFFERENT name on the same number still matches (no second contact) — unchanged',
281
+ afterSecond.length === 1,
282
+ `contacts on ${PHONE_LEGACY}: ${afterSecond.length} `
283
+ + `(${afterSecond.map(e => `${e.fname} ${e.lname}`).join(', ')}); expected exactly one`,
284
+ )
285
+
286
+ if (alice) {
287
+ const notes = await pollFor(
288
+ 'the second note to land',
289
+ () => messagesFor(alice.id),
290
+ found => found.length >= 2,
291
+ )
292
+ record(
293
+ 'settings OFF: the second note is filed onto the matched contact',
294
+ notes.length === 2 && notes.some(n => n.message === 'off note two'),
295
+ `messages on the matched contact: ${JSON.stringify(notes.map(n => n.message))}`,
296
+ )
297
+
298
+ // Twilio-webhook parity is about EXISTING contacts too, so assert the stamp advances on a
299
+ // match and not only on the insert above. The write is fire-and-forget, hence the poll.
300
+ const restamped = await pollFor(
301
+ 'recentInboundSMSAt to advance on the matched contact',
302
+ async () => (await sdk.api.endusers.getOne(alice.id)).recentInboundSMSAt,
303
+ value => !!value && !!stampBeforeMatch && new Date(value) > new Date(stampBeforeMatch),
304
+ )
305
+ record(
306
+ 'recentInboundSMSAt advances on a MATCHED contact, not just a newly created one',
307
+ !!restamped && !!stampBeforeMatch && new Date(restamped) > new Date(stampBeforeMatch),
308
+ `before=${JSON.stringify(stampBeforeMatch)}, after=${JSON.stringify(restamped)}; after must be later`,
309
+ )
310
+
311
+ // half two of the race guard: for an EXISTING contact the removal must really run, which is
312
+ // the Twilio inbound-webhook semantics. If this stopped happening, the no-op above would be
313
+ // masking a removal that never works at all rather than one deferred for new contacts.
314
+ const removed = await pollFor(
315
+ 'the matched contact to be removed from the remove-on-incoming journey',
316
+ () => inRemoveJourney(alice.id),
317
+ present => !present,
318
+ )
319
+ record(
320
+ 'an incoming note from an EXISTING contact DOES remove them from a remove-on-incoming journey',
321
+ !removed,
322
+ `contact ${alice.id} must no longer be in journey ${removeOnIncomingJourney.id}; `
323
+ + `journeys=${JSON.stringify(Object.keys((await sdk.api.endusers.getOne(alice.id)).journeys ?? {}))}`,
324
+ )
325
+ }
326
+ }
327
+
328
+ /* ======== 2. matchEmailAndNames ON: phone + name must both match ======== */
329
+
330
+ await setSettings(true, false)
331
+
332
+ const PHONE_NAMED = '+15005550102'
333
+ {
334
+ await submit('Carol', 'Diaz', PHONE_NAMED, 'on note one')
335
+ const afterFirst = await pollFor(
336
+ 'the first named submission to create a contact',
337
+ () => contactsWithPhone(PHONE_NAMED),
338
+ found => found.length >= 1,
339
+ )
340
+ record(
341
+ 'setting ON: an unknown number still creates a contact',
342
+ afterFirst.length === 1,
343
+ `contacts on ${PHONE_NAMED}: ${afterFirst.length}; expected exactly one`,
344
+ )
345
+ const carol = afterFirst[0]
346
+
347
+ // "fires for every org, setting or not" — section 1 proved it with the setting OFF, so prove it
348
+ // again with name matching ON rather than assuming the trigger path is independent of the gate.
349
+ if (carol) {
350
+ const taggedOn = await pollFor(
351
+ 'the Incoming Message trigger to fire with matchEmailAndNames ON',
352
+ async () => (await sdk.api.endusers.getOne(carol.id)).tags ?? [],
353
+ tags => tags.includes(TAG_ANY),
354
+ )
355
+ record(
356
+ 'the Incoming Message trigger also fires with matchEmailAndNames ON (gate is matching-only)',
357
+ taggedOn.includes(TAG_ANY) && taggedOn.includes(TAG_SMS) && !taggedOn.includes(TAG_DESTINATION),
358
+ `tags: ${JSON.stringify(taggedOn)}; expected ${TAG_ANY} and ${TAG_SMS}, and NOT ${TAG_DESTINATION}`,
359
+ )
360
+ }
361
+
362
+ // same phone, SAME name -> must match the existing record
363
+ await submit('Carol', 'Diaz', PHONE_NAMED, 'on note two')
364
+ const afterSame = await contactsWithPhone(PHONE_NAMED)
365
+ record(
366
+ 'setting ON: the SAME name on the same number matches (no duplicate)',
367
+ afterSame.length === 1,
368
+ `contacts on ${PHONE_NAMED}: ${afterSame.length} `
369
+ + `(${afterSame.map(e => `${e.fname} ${e.lname}`).join(', ')}); expected exactly one`,
370
+ )
371
+
372
+ if (carol) {
373
+ const notes = await pollFor(
374
+ 'both notes to land on Carol',
375
+ () => messagesFor(carol.id),
376
+ found => found.length >= 2,
377
+ )
378
+ record(
379
+ 'setting ON: both notes from the same person land on the one contact',
380
+ notes.length === 2,
381
+ `messages on ${carol.id}: ${JSON.stringify(notes.map(n => n.message))}`,
382
+ )
383
+ }
384
+
385
+ // same phone, DIFFERENT name -> a second contact, which is the behavior change
386
+ await submit('Dave', 'Edwards', PHONE_NAMED, 'on note three')
387
+ const afterDifferent = await pollFor(
388
+ 'the differing-name submission to create a second contact',
389
+ () => contactsWithPhone(PHONE_NAMED),
390
+ found => found.length >= 2,
391
+ )
392
+ record(
393
+ 'setting ON: a DIFFERENT name on the same number creates a SECOND contact',
394
+ afterDifferent.length === 2,
395
+ `contacts on ${PHONE_NAMED}: ${afterDifferent.length} `
396
+ + `(${afterDifferent.map(e => `${e.fname} ${e.lname}`).join(', ')}); expected two`,
397
+ )
398
+
399
+ const dave = afterDifferent.find(e => e.fname === 'Dave' && e.lname === 'Edwards')
400
+ record(
401
+ 'setting ON: the differing-name note is filed onto the NEW contact, not the existing one',
402
+ !!dave && (await messagesFor(dave.id)).some(n => n.message === 'on note three'),
403
+ dave
404
+ ? `messages on Dave ${dave.id}: ${JSON.stringify((await messagesFor(dave.id)).map(n => n.message))}`
405
+ : `no Dave Edwards contact was created on ${PHONE_NAMED}`,
406
+ )
407
+
408
+ if (carol) {
409
+ const carolNotes = await messagesFor(carol.id)
410
+ record(
411
+ 'setting ON: the differing-name note did NOT leak onto the other contact',
412
+ !carolNotes.some(n => n.message === 'on note three'),
413
+ `messages on Carol ${carol.id}: ${JSON.stringify(carolNotes.map(n => n.message))}`,
414
+ )
415
+ }
416
+ }
417
+
418
+ /* ======== 3. a blank-name contact (what the Twilio webhook creates) is not a match ======== */
419
+
420
+ const PHONE_NAMELESS = '+15005550103'
421
+ {
422
+ // exactly the record shape the Twilio inbound-SMS webhook inserts: a phone and nothing else
423
+ const nameless = await sdk.api.endusers.createOne({ phone: PHONE_NAMELESS })
424
+ record(
425
+ 'fixture: the nameless contact really has no name',
426
+ !nameless.fname && !nameless.lname,
427
+ `fname=${JSON.stringify(nameless.fname)}, lname=${JSON.stringify(nameless.lname)} `
428
+ + `(if either is set, the check below is vacuous)`,
429
+ )
430
+
431
+ await submit('Fiona', 'Grant', PHONE_NAMELESS, 'blank name note')
432
+
433
+ const after = await pollFor(
434
+ 'the submission to create its own contact beside the nameless one',
435
+ () => contactsWithPhone(PHONE_NAMELESS),
436
+ found => found.length >= 2,
437
+ )
438
+ record(
439
+ 'setting ON: a blank-name contact on the same number is NOT matched; a second contact is created',
440
+ after.length === 2 && after.some(e => e.fname === 'Fiona' && e.lname === 'Grant'),
441
+ `contacts on ${PHONE_NAMELESS}: ${after.length} `
442
+ + `(${after.map(e => `${JSON.stringify(e.fname)} ${JSON.stringify(e.lname)}`).join(', ')}); `
443
+ + `expected two, one of them Fiona Grant`,
444
+ )
445
+
446
+ const namelessAfter = await sdk.api.endusers.getOne(nameless.id)
447
+ record(
448
+ 'setting ON: the nameless contact is left untouched — no name is written onto it',
449
+ !namelessAfter.fname && !namelessAfter.lname,
450
+ `the nameless contact now reads fname=${JSON.stringify(namelessAfter.fname)}, `
451
+ + `lname=${JSON.stringify(namelessAfter.lname)}; both must still be empty`,
452
+ )
453
+ record(
454
+ 'setting ON: the note did not land on the nameless contact',
455
+ !(await messagesFor(nameless.id)).some(n => n.message === 'blank name note'),
456
+ `messages on the nameless contact: `
457
+ + `${JSON.stringify((await messagesFor(nameless.id)).map(n => n.message))}`,
458
+ )
459
+ }
460
+
461
+ /* ======== 4/5. caseInsensitiveMatching governs the name comparison ======== */
462
+
463
+ const PHONE_CASE = '+15005550104'
464
+ {
465
+ // "MCDONALD" survives nameValidator verbatim — it only capitalizes the FIRST letter, so the
466
+ // difference here is after letter one and a submitted "mcdonald" becomes "Mcdonald".
467
+ const stored = await sdk.api.endusers.createOne({ fname: 'Ian', lname: 'MCDONALD', phone: PHONE_CASE })
468
+ record(
469
+ 'fixture: a non-first-letter capitalization variant is stored verbatim',
470
+ stored.lname === 'MCDONALD',
471
+ `stored lname=${JSON.stringify(stored.lname)} (if this is not MCDONALD both checks below are vacuous)`,
472
+ )
473
+
474
+ await setSettings(true, true)
475
+ await submit('Ian', 'mcdonald', PHONE_CASE, 'case note one')
476
+
477
+ const afterInsensitive = await contactsWithPhone(PHONE_CASE)
478
+ record(
479
+ 'caseInsensitiveMatching ON: "mcdonald" matches stored "MCDONALD" (no duplicate)',
480
+ afterInsensitive.length === 1,
481
+ `contacts on ${PHONE_CASE}: ${afterInsensitive.length} `
482
+ + `(${afterInsensitive.map(e => `${e.fname} ${e.lname}`).join(', ')}); expected exactly one`,
483
+ )
484
+ record(
485
+ 'caseInsensitiveMatching ON: the matched contact keeps its own capitalization',
486
+ (await sdk.api.endusers.getOne(stored.id)).lname === 'MCDONALD',
487
+ `stored lname is now ${JSON.stringify((await sdk.api.endusers.getOne(stored.id)).lname)}, must still be "MCDONALD"`,
488
+ )
489
+
490
+ await setSettings(true, false)
491
+ await submit('Ian', 'mcdonald', PHONE_CASE, 'case note two')
492
+
493
+ const afterSensitive = await pollFor(
494
+ 'the byte-exact submission to create a second contact',
495
+ () => contactsWithPhone(PHONE_CASE),
496
+ found => found.length >= 2,
497
+ )
498
+ record(
499
+ 'caseInsensitiveMatching OFF: "Mcdonald" no longer matches "MCDONALD"; a second contact is created',
500
+ afterSensitive.length === 2,
501
+ `contacts on ${PHONE_CASE}: ${afterSensitive.length} `
502
+ + `(${afterSensitive.map(e => `${e.fname} ${e.lname}`).join(', ')}); expected two`,
503
+ )
504
+ }
505
+
506
+ record(
507
+ `the suite stayed within its per-IP post budget`,
508
+ posts <= POST_BUDGET,
509
+ `made ${posts} submissions, budget is ${POST_BUDGET} (the endpoint allows 10/min per IP)`,
510
+ )
511
+
512
+ return checks
513
+ } finally {
514
+ await tenant?.teardown()
515
+ }
516
+ }
517
+
518
+ /** Asserting wrapper, for standalone runs. */
519
+ export const phone_embed_leave_message_tests = async () => {
520
+ assert_checks('Phone Embed leave_message', await phone_embed_leave_message_checks())
521
+ }
522
+
523
+ /** Enrolls this suite in the auto-discovered parallel pool — see run_discovered_isolated_suites. */
524
+ export const ISOLATED_SUITE: IsolatedSuite = {
525
+ name: 'Phone Embed leave_message',
526
+ run: phone_embed_leave_message_checks,
527
+ }
528
+
529
+ // Allow running this test file independently
530
+ if (require.main === module) {
531
+ console.log(`Using API URL: ${host}`)
532
+ phone_embed_leave_message_tests()
533
+ .then(() => {
534
+ console.log("Phone Embed leave_message test suite completed successfully")
535
+ process.exit(0)
536
+ })
537
+ .catch((error) => {
538
+ console.error("Phone Embed leave_message test suite failed:", error)
539
+ process.exit(1)
540
+ })
541
+ }