@tellescope/sdk 2.0.0-beta.0 → 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 (303) hide show
  1. package/CLAUDE.md +24 -0
  2. package/env.example +6 -0
  3. package/lib/cjs/sdk.d.ts +376 -3
  4. package/lib/cjs/sdk.d.ts.map +1 -1
  5. package/lib/cjs/sdk.js +1 -0
  6. package/lib/cjs/sdk.js.map +1 -1
  7. package/lib/cjs/tests/api_tests/booking_prevent_upcoming_duplicate.test.d.ts +22 -0
  8. package/lib/cjs/tests/api_tests/booking_prevent_upcoming_duplicate.test.d.ts.map +1 -0
  9. package/lib/cjs/tests/api_tests/booking_prevent_upcoming_duplicate.test.js +506 -0
  10. package/lib/cjs/tests/api_tests/booking_prevent_upcoming_duplicate.test.js.map +1 -0
  11. package/lib/cjs/tests/api_tests/enduser_fields_sync.test.d.ts +3 -1
  12. package/lib/cjs/tests/api_tests/enduser_fields_sync.test.d.ts.map +1 -1
  13. package/lib/cjs/tests/api_tests/enduser_fields_sync.test.js +3 -1
  14. package/lib/cjs/tests/api_tests/enduser_fields_sync.test.js.map +1 -1
  15. package/lib/cjs/tests/api_tests/enduser_observations_auto_review.test.d.ts +3 -1
  16. package/lib/cjs/tests/api_tests/enduser_observations_auto_review.test.d.ts.map +1 -1
  17. package/lib/cjs/tests/api_tests/enduser_observations_auto_review.test.js +3 -1
  18. package/lib/cjs/tests/api_tests/enduser_observations_auto_review.test.js.map +1 -1
  19. package/lib/cjs/tests/api_tests/form_vital_reading.test.d.ts +7 -0
  20. package/lib/cjs/tests/api_tests/form_vital_reading.test.d.ts.map +1 -0
  21. package/lib/cjs/tests/api_tests/form_vital_reading.test.js +461 -0
  22. package/lib/cjs/tests/api_tests/form_vital_reading.test.js.map +1 -0
  23. package/lib/cjs/tests/api_tests/journey_statistics_cache.test.d.ts +26 -0
  24. package/lib/cjs/tests/api_tests/journey_statistics_cache.test.d.ts.map +1 -0
  25. package/lib/cjs/tests/api_tests/journey_statistics_cache.test.js +232 -0
  26. package/lib/cjs/tests/api_tests/journey_statistics_cache.test.js.map +1 -0
  27. package/lib/cjs/tests/api_tests/journey_step_manual_count.test.d.ts +34 -0
  28. package/lib/cjs/tests/api_tests/journey_step_manual_count.test.d.ts.map +1 -0
  29. package/lib/cjs/tests/api_tests/journey_step_manual_count.test.js +243 -0
  30. package/lib/cjs/tests/api_tests/journey_step_manual_count.test.js.map +1 -0
  31. package/lib/cjs/tests/api_tests/load_threads_care_team.test.d.ts +3 -1
  32. package/lib/cjs/tests/api_tests/load_threads_care_team.test.d.ts.map +1 -1
  33. package/lib/cjs/tests/api_tests/load_threads_care_team.test.js +3 -1
  34. package/lib/cjs/tests/api_tests/load_threads_care_team.test.js.map +1 -1
  35. package/lib/cjs/tests/api_tests/mdi_case_offerings.test.d.ts.map +1 -1
  36. package/lib/cjs/tests/api_tests/mdi_case_offerings.test.js +139 -9
  37. package/lib/cjs/tests/api_tests/mdi_case_offerings.test.js.map +1 -1
  38. package/lib/cjs/tests/api_tests/organization_signup.test.d.ts +3 -1
  39. package/lib/cjs/tests/api_tests/organization_signup.test.d.ts.map +1 -1
  40. package/lib/cjs/tests/api_tests/organization_signup.test.js +3 -1
  41. package/lib/cjs/tests/api_tests/organization_signup.test.js.map +1 -1
  42. package/lib/cjs/tests/api_tests/phone_embed_leave_message.test.d.ts +7 -0
  43. package/lib/cjs/tests/api_tests/phone_embed_leave_message.test.d.ts.map +1 -0
  44. package/lib/cjs/tests/api_tests/phone_embed_leave_message.test.js +571 -0
  45. package/lib/cjs/tests/api_tests/phone_embed_leave_message.test.js.map +1 -0
  46. package/lib/cjs/tests/api_tests/programs.test.d.ts +3 -1
  47. package/lib/cjs/tests/api_tests/programs.test.d.ts.map +1 -1
  48. package/lib/cjs/tests/api_tests/programs.test.js +73 -17
  49. package/lib/cjs/tests/api_tests/programs.test.js.map +1 -1
  50. package/lib/cjs/tests/api_tests/public_form_case_insensitive_match.test.d.ts +7 -0
  51. package/lib/cjs/tests/api_tests/public_form_case_insensitive_match.test.d.ts.map +1 -0
  52. package/lib/cjs/tests/api_tests/public_form_case_insensitive_match.test.js +733 -0
  53. package/lib/cjs/tests/api_tests/public_form_case_insensitive_match.test.js.map +1 -0
  54. package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts +3 -1
  55. package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts.map +1 -1
  56. package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js +58 -48
  57. package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js.map +1 -1
  58. package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts +3 -1
  59. package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts.map +1 -1
  60. package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.js +3 -1
  61. package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.js.map +1 -1
  62. package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.d.ts +3 -1
  63. package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.d.ts.map +1 -1
  64. package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.js +3 -1
  65. package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.js.map +1 -1
  66. package/lib/cjs/tests/api_tests/security/form-logic-js-execution.test.d.ts +3 -1
  67. package/lib/cjs/tests/api_tests/security/form-logic-js-execution.test.d.ts.map +1 -1
  68. package/lib/cjs/tests/api_tests/security/form-logic-js-execution.test.js +3 -1
  69. package/lib/cjs/tests/api_tests/security/form-logic-js-execution.test.js.map +1 -1
  70. package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.d.ts.map +1 -1
  71. package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js +424 -39
  72. package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js.map +1 -1
  73. package/lib/cjs/tests/api_tests/security/register_business_id_injection.test.js +2 -2
  74. package/lib/cjs/tests/api_tests/security/register_business_id_injection.test.js.map +1 -1
  75. package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts +3 -1
  76. package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts.map +1 -1
  77. package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js +3 -1
  78. package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js.map +1 -1
  79. package/lib/cjs/tests/api_tests/session_for_join_link.test.d.ts +10 -0
  80. package/lib/cjs/tests/api_tests/session_for_join_link.test.d.ts.map +1 -0
  81. package/lib/cjs/tests/api_tests/session_for_join_link.test.js +147 -0
  82. package/lib/cjs/tests/api_tests/session_for_join_link.test.js.map +1 -0
  83. package/lib/cjs/tests/api_tests/set_fields_order_templates.test.d.ts.map +1 -1
  84. package/lib/cjs/tests/api_tests/set_fields_order_templates.test.js +191 -88
  85. package/lib/cjs/tests/api_tests/set_fields_order_templates.test.js.map +1 -1
  86. package/lib/cjs/tests/api_tests/stripe_checkout_items.test.d.ts +17 -0
  87. package/lib/cjs/tests/api_tests/stripe_checkout_items.test.d.ts.map +1 -0
  88. package/lib/cjs/tests/api_tests/stripe_checkout_items.test.js +219 -0
  89. package/lib/cjs/tests/api_tests/stripe_checkout_items.test.js.map +1 -0
  90. package/lib/cjs/tests/api_tests/stripe_multiple_customer_ids.test.d.ts +5 -0
  91. package/lib/cjs/tests/api_tests/stripe_multiple_customer_ids.test.d.ts.map +1 -0
  92. package/lib/cjs/tests/api_tests/stripe_multiple_customer_ids.test.js +396 -0
  93. package/lib/cjs/tests/api_tests/stripe_multiple_customer_ids.test.js.map +1 -0
  94. package/lib/cjs/tests/api_tests/tickets_bulk_assign_from_queue.test.d.ts +3 -1
  95. package/lib/cjs/tests/api_tests/tickets_bulk_assign_from_queue.test.d.ts.map +1 -1
  96. package/lib/cjs/tests/api_tests/tickets_bulk_assign_from_queue.test.js +3 -1
  97. package/lib/cjs/tests/api_tests/tickets_bulk_assign_from_queue.test.js.map +1 -1
  98. package/lib/cjs/tests/api_tests/totp_mfa.test.d.ts.map +1 -1
  99. package/lib/cjs/tests/api_tests/totp_mfa.test.js +37 -31
  100. package/lib/cjs/tests/api_tests/totp_mfa.test.js.map +1 -1
  101. package/lib/cjs/tests/api_tests/trigger_contains_matching.test.d.ts +3 -1
  102. package/lib/cjs/tests/api_tests/trigger_contains_matching.test.d.ts.map +1 -1
  103. package/lib/cjs/tests/api_tests/trigger_contains_matching.test.js +3 -1
  104. package/lib/cjs/tests/api_tests/trigger_contains_matching.test.js.map +1 -1
  105. package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.d.ts +3 -1
  106. package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.d.ts.map +1 -1
  107. package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.js +3 -1
  108. package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.js.map +1 -1
  109. package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.d.ts +3 -1
  110. package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.d.ts.map +1 -1
  111. package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.js +3 -1
  112. package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.js.map +1 -1
  113. package/lib/cjs/tests/api_tests/vitl_push.test.d.ts +7 -0
  114. package/lib/cjs/tests/api_tests/vitl_push.test.d.ts.map +1 -0
  115. package/lib/cjs/tests/api_tests/vitl_push.test.js +341 -0
  116. package/lib/cjs/tests/api_tests/vitl_push.test.js.map +1 -0
  117. package/lib/cjs/tests/setup_isolated.d.ts +48 -0
  118. package/lib/cjs/tests/setup_isolated.d.ts.map +1 -1
  119. package/lib/cjs/tests/setup_isolated.js +119 -1
  120. package/lib/cjs/tests/setup_isolated.js.map +1 -1
  121. package/lib/cjs/tests/tests.d.ts.map +1 -1
  122. package/lib/cjs/tests/tests.js +378 -282
  123. package/lib/cjs/tests/tests.js.map +1 -1
  124. package/lib/cjs/tests/unit_tests/isolated_suite_discovery.test.d.ts +2 -0
  125. package/lib/cjs/tests/unit_tests/isolated_suite_discovery.test.d.ts.map +1 -0
  126. package/lib/cjs/tests/unit_tests/isolated_suite_discovery.test.js +201 -0
  127. package/lib/cjs/tests/unit_tests/isolated_suite_discovery.test.js.map +1 -0
  128. package/lib/cjs/tests/unit_tests/programs_conditional_logic.test.d.ts +3 -0
  129. package/lib/cjs/tests/unit_tests/programs_conditional_logic.test.d.ts.map +1 -0
  130. package/lib/cjs/tests/unit_tests/programs_conditional_logic.test.js +120 -0
  131. package/lib/cjs/tests/unit_tests/programs_conditional_logic.test.js.map +1 -0
  132. package/lib/cjs/tests/webhooks_tests.js +44 -34
  133. package/lib/cjs/tests/webhooks_tests.js.map +1 -1
  134. package/lib/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  135. package/lib/esm/sdk.d.ts +376 -3
  136. package/lib/esm/sdk.d.ts.map +1 -1
  137. package/lib/esm/sdk.js +1 -0
  138. package/lib/esm/sdk.js.map +1 -1
  139. package/lib/esm/tests/api_tests/booking_prevent_upcoming_duplicate.test.d.ts +22 -0
  140. package/lib/esm/tests/api_tests/booking_prevent_upcoming_duplicate.test.d.ts.map +1 -0
  141. package/lib/esm/tests/api_tests/booking_prevent_upcoming_duplicate.test.js +501 -0
  142. package/lib/esm/tests/api_tests/booking_prevent_upcoming_duplicate.test.js.map +1 -0
  143. package/lib/esm/tests/api_tests/enduser_fields_sync.test.d.ts +3 -1
  144. package/lib/esm/tests/api_tests/enduser_fields_sync.test.d.ts.map +1 -1
  145. package/lib/esm/tests/api_tests/enduser_fields_sync.test.js +2 -0
  146. package/lib/esm/tests/api_tests/enduser_fields_sync.test.js.map +1 -1
  147. package/lib/esm/tests/api_tests/enduser_observations_auto_review.test.d.ts +3 -1
  148. package/lib/esm/tests/api_tests/enduser_observations_auto_review.test.d.ts.map +1 -1
  149. package/lib/esm/tests/api_tests/enduser_observations_auto_review.test.js +2 -0
  150. package/lib/esm/tests/api_tests/enduser_observations_auto_review.test.js.map +1 -1
  151. package/lib/esm/tests/api_tests/form_vital_reading.test.d.ts +7 -0
  152. package/lib/esm/tests/api_tests/form_vital_reading.test.d.ts.map +1 -0
  153. package/lib/esm/tests/api_tests/form_vital_reading.test.js +456 -0
  154. package/lib/esm/tests/api_tests/form_vital_reading.test.js.map +1 -0
  155. package/lib/esm/tests/api_tests/journey_statistics_cache.test.d.ts +26 -0
  156. package/lib/esm/tests/api_tests/journey_statistics_cache.test.d.ts.map +1 -0
  157. package/lib/esm/tests/api_tests/journey_statistics_cache.test.js +227 -0
  158. package/lib/esm/tests/api_tests/journey_statistics_cache.test.js.map +1 -0
  159. package/lib/esm/tests/api_tests/journey_step_manual_count.test.d.ts +34 -0
  160. package/lib/esm/tests/api_tests/journey_step_manual_count.test.d.ts.map +1 -0
  161. package/lib/esm/tests/api_tests/journey_step_manual_count.test.js +238 -0
  162. package/lib/esm/tests/api_tests/journey_step_manual_count.test.js.map +1 -0
  163. package/lib/esm/tests/api_tests/load_threads_care_team.test.d.ts +3 -1
  164. package/lib/esm/tests/api_tests/load_threads_care_team.test.d.ts.map +1 -1
  165. package/lib/esm/tests/api_tests/load_threads_care_team.test.js +2 -0
  166. package/lib/esm/tests/api_tests/load_threads_care_team.test.js.map +1 -1
  167. package/lib/esm/tests/api_tests/mdi_case_offerings.test.d.ts.map +1 -1
  168. package/lib/esm/tests/api_tests/mdi_case_offerings.test.js +139 -9
  169. package/lib/esm/tests/api_tests/mdi_case_offerings.test.js.map +1 -1
  170. package/lib/esm/tests/api_tests/organization_signup.test.d.ts +3 -1
  171. package/lib/esm/tests/api_tests/organization_signup.test.d.ts.map +1 -1
  172. package/lib/esm/tests/api_tests/organization_signup.test.js +2 -0
  173. package/lib/esm/tests/api_tests/organization_signup.test.js.map +1 -1
  174. package/lib/esm/tests/api_tests/phone_embed_leave_message.test.d.ts +7 -0
  175. package/lib/esm/tests/api_tests/phone_embed_leave_message.test.d.ts.map +1 -0
  176. package/lib/esm/tests/api_tests/phone_embed_leave_message.test.js +566 -0
  177. package/lib/esm/tests/api_tests/phone_embed_leave_message.test.js.map +1 -0
  178. package/lib/esm/tests/api_tests/programs.test.d.ts +3 -1
  179. package/lib/esm/tests/api_tests/programs.test.d.ts.map +1 -1
  180. package/lib/esm/tests/api_tests/programs.test.js +72 -16
  181. package/lib/esm/tests/api_tests/programs.test.js.map +1 -1
  182. package/lib/esm/tests/api_tests/public_form_case_insensitive_match.test.d.ts +7 -0
  183. package/lib/esm/tests/api_tests/public_form_case_insensitive_match.test.d.ts.map +1 -0
  184. package/lib/esm/tests/api_tests/public_form_case_insensitive_match.test.js +728 -0
  185. package/lib/esm/tests/api_tests/public_form_case_insensitive_match.test.js.map +1 -0
  186. package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts +3 -1
  187. package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts.map +1 -1
  188. package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js +57 -47
  189. package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js.map +1 -1
  190. package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts +3 -1
  191. package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts.map +1 -1
  192. package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.js +2 -0
  193. package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.js.map +1 -1
  194. package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.d.ts +3 -1
  195. package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.d.ts.map +1 -1
  196. package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.js +2 -0
  197. package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.js.map +1 -1
  198. package/lib/esm/tests/api_tests/security/form-logic-js-execution.test.d.ts +3 -1
  199. package/lib/esm/tests/api_tests/security/form-logic-js-execution.test.d.ts.map +1 -1
  200. package/lib/esm/tests/api_tests/security/form-logic-js-execution.test.js +2 -0
  201. package/lib/esm/tests/api_tests/security/form-logic-js-execution.test.js.map +1 -1
  202. package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.d.ts.map +1 -1
  203. package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js +424 -39
  204. package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js.map +1 -1
  205. package/lib/esm/tests/api_tests/security/register_business_id_injection.test.js +2 -2
  206. package/lib/esm/tests/api_tests/security/register_business_id_injection.test.js.map +1 -1
  207. package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts +3 -1
  208. package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts.map +1 -1
  209. package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js +2 -0
  210. package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js.map +1 -1
  211. package/lib/esm/tests/api_tests/session_for_join_link.test.d.ts +10 -0
  212. package/lib/esm/tests/api_tests/session_for_join_link.test.d.ts.map +1 -0
  213. package/lib/esm/tests/api_tests/session_for_join_link.test.js +142 -0
  214. package/lib/esm/tests/api_tests/session_for_join_link.test.js.map +1 -0
  215. package/lib/esm/tests/api_tests/set_fields_order_templates.test.d.ts.map +1 -1
  216. package/lib/esm/tests/api_tests/set_fields_order_templates.test.js +191 -88
  217. package/lib/esm/tests/api_tests/set_fields_order_templates.test.js.map +1 -1
  218. package/lib/esm/tests/api_tests/stripe_checkout_items.test.d.ts +17 -0
  219. package/lib/esm/tests/api_tests/stripe_checkout_items.test.d.ts.map +1 -0
  220. package/lib/esm/tests/api_tests/stripe_checkout_items.test.js +214 -0
  221. package/lib/esm/tests/api_tests/stripe_checkout_items.test.js.map +1 -0
  222. package/lib/esm/tests/api_tests/stripe_multiple_customer_ids.test.d.ts +5 -0
  223. package/lib/esm/tests/api_tests/stripe_multiple_customer_ids.test.d.ts.map +1 -0
  224. package/lib/esm/tests/api_tests/stripe_multiple_customer_ids.test.js +388 -0
  225. package/lib/esm/tests/api_tests/stripe_multiple_customer_ids.test.js.map +1 -0
  226. package/lib/esm/tests/api_tests/tickets_bulk_assign_from_queue.test.d.ts +3 -1
  227. package/lib/esm/tests/api_tests/tickets_bulk_assign_from_queue.test.d.ts.map +1 -1
  228. package/lib/esm/tests/api_tests/tickets_bulk_assign_from_queue.test.js +2 -0
  229. package/lib/esm/tests/api_tests/tickets_bulk_assign_from_queue.test.js.map +1 -1
  230. package/lib/esm/tests/api_tests/totp_mfa.test.d.ts.map +1 -1
  231. package/lib/esm/tests/api_tests/totp_mfa.test.js +37 -31
  232. package/lib/esm/tests/api_tests/totp_mfa.test.js.map +1 -1
  233. package/lib/esm/tests/api_tests/trigger_contains_matching.test.d.ts +3 -1
  234. package/lib/esm/tests/api_tests/trigger_contains_matching.test.d.ts.map +1 -1
  235. package/lib/esm/tests/api_tests/trigger_contains_matching.test.js +2 -0
  236. package/lib/esm/tests/api_tests/trigger_contains_matching.test.js.map +1 -1
  237. package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.d.ts +3 -1
  238. package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.d.ts.map +1 -1
  239. package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.js +2 -0
  240. package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.js.map +1 -1
  241. package/lib/esm/tests/api_tests/vital_review_trigger_action.test.d.ts +3 -1
  242. package/lib/esm/tests/api_tests/vital_review_trigger_action.test.d.ts.map +1 -1
  243. package/lib/esm/tests/api_tests/vital_review_trigger_action.test.js +2 -0
  244. package/lib/esm/tests/api_tests/vital_review_trigger_action.test.js.map +1 -1
  245. package/lib/esm/tests/api_tests/vitl_push.test.d.ts +7 -0
  246. package/lib/esm/tests/api_tests/vitl_push.test.d.ts.map +1 -0
  247. package/lib/esm/tests/api_tests/vitl_push.test.js +333 -0
  248. package/lib/esm/tests/api_tests/vitl_push.test.js.map +1 -0
  249. package/lib/esm/tests/setup_isolated.d.ts +48 -0
  250. package/lib/esm/tests/setup_isolated.d.ts.map +1 -1
  251. package/lib/esm/tests/setup_isolated.js +114 -0
  252. package/lib/esm/tests/setup_isolated.js.map +1 -1
  253. package/lib/esm/tests/tests.d.ts.map +1 -1
  254. package/lib/esm/tests/tests.js +379 -283
  255. package/lib/esm/tests/tests.js.map +1 -1
  256. package/lib/esm/tests/unit_tests/isolated_suite_discovery.test.d.ts +2 -0
  257. package/lib/esm/tests/unit_tests/isolated_suite_discovery.test.d.ts.map +1 -0
  258. package/lib/esm/tests/unit_tests/isolated_suite_discovery.test.js +174 -0
  259. package/lib/esm/tests/unit_tests/isolated_suite_discovery.test.js.map +1 -0
  260. package/lib/esm/tests/unit_tests/programs_conditional_logic.test.d.ts +3 -0
  261. package/lib/esm/tests/unit_tests/programs_conditional_logic.test.d.ts.map +1 -0
  262. package/lib/esm/tests/unit_tests/programs_conditional_logic.test.js +117 -0
  263. package/lib/esm/tests/unit_tests/programs_conditional_logic.test.js.map +1 -0
  264. package/lib/esm/tests/webhooks_tests.js +44 -34
  265. package/lib/esm/tests/webhooks_tests.js.map +1 -1
  266. package/lib/tsconfig.tsbuildinfo +1 -1
  267. package/package.json +12 -11
  268. package/src/sdk.ts +9 -2
  269. package/src/tests/api_tests/booking_prevent_upcoming_duplicate.test.ts +541 -0
  270. package/src/tests/api_tests/enduser_fields_sync.test.ts +154 -0
  271. package/src/tests/api_tests/enduser_observations_auto_review.test.ts +4 -0
  272. package/src/tests/api_tests/form_vital_reading.test.ts +393 -0
  273. package/src/tests/api_tests/journey_statistics_cache.test.ts +237 -0
  274. package/src/tests/api_tests/journey_step_manual_count.test.ts +220 -0
  275. package/src/tests/api_tests/load_threads_care_team.test.ts +214 -0
  276. package/src/tests/api_tests/mdi_case_offerings.test.ts +138 -0
  277. package/src/tests/api_tests/organization_signup.test.ts +4 -0
  278. package/src/tests/api_tests/phone_embed_leave_message.test.ts +541 -0
  279. package/src/tests/api_tests/programs.test.ts +351 -0
  280. package/src/tests/api_tests/public_form_case_insensitive_match.test.ts +632 -0
  281. package/src/tests/api_tests/security/add-to-journey-foreign-enduserid.test.ts +37 -26
  282. package/src/tests/api_tests/security/appointment-reschedule-ownership.test.ts +436 -0
  283. package/src/tests/api_tests/security/file-link-session-confinement.test.ts +4 -0
  284. package/src/tests/api_tests/security/form-logic-js-execution.test.ts +4 -0
  285. package/src/tests/api_tests/security/formsort_webhook_auth_field_lock.test.ts +225 -0
  286. package/src/tests/api_tests/security/register_business_id_injection.test.ts +2 -2
  287. package/src/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.ts +4 -0
  288. package/src/tests/api_tests/session_for_join_link.test.ts +99 -0
  289. package/src/tests/api_tests/set_fields_order_templates.test.ts +94 -9
  290. package/src/tests/api_tests/stripe_checkout_items.test.ts +186 -0
  291. package/src/tests/api_tests/tickets_bulk_assign_from_queue.test.ts +4 -0
  292. package/src/tests/api_tests/totp_mfa.test.ts +10 -5
  293. package/src/tests/api_tests/trigger_contains_matching.test.ts +349 -0
  294. package/src/tests/api_tests/user_logs_restore_deleted.test.ts +419 -0
  295. package/src/tests/api_tests/vital_review_trigger_action.test.ts +455 -0
  296. package/src/tests/api_tests/vitl_push.test.ts +323 -0
  297. package/src/tests/setup_isolated.ts +140 -0
  298. package/src/tests/tests.ts +126 -20
  299. package/src/tests/unit_tests/isolated_suite_discovery.test.ts +178 -0
  300. package/src/tests/unit_tests/programs_conditional_logic.test.ts +189 -0
  301. package/src/tests/unit_tests/trigger_title_partials.test.ts +174 -0
  302. package/src/tests/webhooks_tests.ts +6 -1
  303. package/test_generated.pdf +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tellescope/sdk",
3
- "version": "2.0.0-beta.0",
3
+ "version": "2.0.0-beta.2",
4
4
  "description": "Code for interacting with the Tellescope API",
5
5
  "main": "./lib/cjs/sdk.js",
6
6
  "module": "./lib/esm/sdk.js",
@@ -35,17 +35,18 @@
35
35
  "node": ">=22.12.0"
36
36
  },
37
37
  "dependencies": {
38
- "@tellescope/constants": "2.0.0-beta.0",
39
- "@tellescope/schema": "2.0.0-beta.0",
40
- "@tellescope/testing": "2.0.0-beta.0",
41
- "@tellescope/types-client": "2.0.0-beta.0",
42
- "@tellescope/types-models": "2.0.0-beta.0",
43
- "@tellescope/types-utilities": "2.0.0-beta.0",
44
- "@tellescope/utilities": "2.0.0-beta.0",
45
- "@tellescope/validation": "2.0.0-beta.0",
46
- "axios": "0.33.0",
38
+ "@tellescope/constants": "2.0.0-beta.2",
39
+ "@tellescope/schema": "2.0.0-beta.2",
40
+ "@tellescope/testing": "2.0.0-beta.2",
41
+ "@tellescope/types-client": "2.0.0-beta.2",
42
+ "@tellescope/types-models": "2.0.0-beta.2",
43
+ "@tellescope/types-utilities": "2.0.0-beta.2",
44
+ "@tellescope/utilities": "2.0.0-beta.2",
45
+ "@tellescope/validation": "2.0.0-beta.2",
46
+ "axios": "0.34.0",
47
47
  "dotenv": "14.3.2",
48
48
  "form-data": "4.0.6",
49
+ "mongodb": "5.7.0",
49
50
  "source-map-support": "0.5.21"
50
51
  },
51
52
  "devDependencies": {
@@ -58,5 +59,5 @@
58
59
  "publishConfig": {
59
60
  "access": "public"
60
61
  },
61
- "gitHead": "95b2448bb219e7634e29ef4b7850f8f74d73dcbc"
62
+ "gitHead": "8f2fcf1866332fc118a58775092cc6464dabecc0"
62
63
  }
package/src/sdk.ts CHANGED
@@ -35,6 +35,7 @@ import {
35
35
  } from "@tellescope/types-client"
36
36
  import { CustomUpdateOptions, SortOption, UserIdentity, FileDetails, ReactNativeFile, SessionType } from "@tellescope/types-utilities"
37
37
  import { url_safe_path } from "@tellescope/utilities"
38
+ import type { Filter } from "mongodb"
38
39
 
39
40
  import { Session as SessionManager, SessionOptions } from "./session"
40
41
 
@@ -79,7 +80,7 @@ export interface LoadFunctionArguments <T> {
79
80
  search?: SearchOptions,
80
81
  ids?: string[],
81
82
  returnCount?: boolean,
82
- mdbFilter?: any,
83
+ mdbFilter?: Filter<T>,
83
84
  mdbSort?: any,
84
85
  projection?: { [K in keyof T]?: 1 },
85
86
  }
@@ -95,7 +96,7 @@ export interface APIQuery<
95
96
  {
96
97
  createOne: (t: CREATE & { sharedWithOrganizations?: string[][], _overrideUnique?: boolean }) => Promise<T>;
97
98
  createSome: (ts: CREATE[], o?: { _overrideUnique?: boolean }) => Promise<{ created: T[], errors: object[] }>;
98
- getOne: (argument: string | ReadFilter<T>, mdbFilter?: any) => Promise<T>;
99
+ getOne: (argument: string | ReadFilter<T>, mdbFilter?: Filter<T>) => Promise<T>;
99
100
  getSome: LoadFunction<T>;
100
101
  getByIds: ({ ids } : { ids: string[] }) => Promise<{ matches: T[] }>;
101
102
  updateOne: (id: string, updates: UPDATE, options?: CustomUpdateOptions, _overrideUnique?: boolean) => Promise<T>;
@@ -234,6 +235,11 @@ const loadDefaultQueries = (s: Session): { [K in keyof ClientModelForName] : API
234
235
  })
235
236
 
236
237
  type Queries = { [K in keyof ClientModelForName]: APIQuery<K> } & {
238
+ user_logs: {
239
+ restore_deleted: (args: extractFields<CustomActions['user_logs']['restore_deleted']['parameters']>) => (
240
+ Promise<extractFields<CustomActions['user_logs']['restore_deleted']['returns']>>
241
+ ),
242
+ },
237
243
  inbox_threads: {
238
244
  build_threads: (args: extractFields<CustomActions['inbox_threads']['build_threads']['parameters']>) => (
239
245
  Promise<extractFields<CustomActions['inbox_threads']['build_threads']['returns']>>
@@ -996,6 +1002,7 @@ export class Session extends SessionManager {
996
1002
  queries.api_keys.get_organization_api_keys = () => this._GET(`/v1${schema.api_keys.customActions.get_organization_api_keys.path}`)
997
1003
  queries.api_keys.delete_organization_api_key = ({ id }) => this._DELETE(`/v1/organization-api-key/${id}`)
998
1004
 
1005
+ queries.user_logs.restore_deleted = a => this._POST(`/v1${schema.user_logs.customActions.restore_deleted.path}`, a)
999
1006
  queries.enduser_orders.get_available_tests = a => this._GET(`/v1${schema.enduser_orders.customActions.get_available_tests.path}`, a)
1000
1007
  queries.enduser_orders.create_lab_order = a => this._POST(`/v1${schema.enduser_orders.customActions.create_lab_order.path}`, a)
1001
1008
  queries.enduser_orders.cancel_order = a => this._POST(`/v1${schema.enduser_orders.customActions.cancel_order.path}`, a)
@@ -0,0 +1,541 @@
1
+ require('source-map-support').install();
2
+
3
+ import { DateTime } from "luxon"
4
+ import { CalendarEvent } from "@tellescope/types-models"
5
+ import { find_conflicting_upcoming_event } from "@tellescope/utilities"
6
+ import { EnduserSession } from "../../sdk"
7
+ import {
8
+ assert_checks,
9
+ Check,
10
+ setup_isolated_tenant,
11
+ IsolatedSuite,
12
+ } from "../setup_isolated"
13
+
14
+ const host = process.env.API_URL || 'http://localhost:8080' as const
15
+
16
+ const RAND = () => Math.random().toString(36).slice(2, 10)
17
+
18
+ // dontsend- addresses are the provisioning convention for "never actually deliver"; no phone number
19
+ // is set on any enduser here, and every template disables confirmations and carries no reminders,
20
+ // so this suite cannot send a message to anyone.
21
+ const TEST_EMAIL = () => `dontsend-booking-dup-${RAND()}@tellescope.com`
22
+
23
+ const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000
24
+
25
+ /**
26
+ * Covers the per-template `preventBookingWhenUpcomingExists` setting: a patient cannot book a
27
+ * template while they already have an unresolved upcoming appointment of that same template.
28
+ *
29
+ * Provisions its own tenant because it needs host availability, two providers and templates in a
30
+ * known state — a shared tenant's weeklyAvailabilities and calendar are changed by other suites.
31
+ *
32
+ * Two layers:
33
+ * - unit checks of the pure predicate (find_conflicting_upcoming_event), which is the rule itself
34
+ * and is shared verbatim by the API and the webapp staff warning
35
+ * - end-to-end checks through book_appointment as an authenticated enduser, which is what the
36
+ * portal and the public booking page both call
37
+ *
38
+ * NOT covered here (manual QA): the AI voice-agent path and an EHR-synced event blocking a booking.
39
+ */
40
+ export const booking_prevent_upcoming_duplicate_checks = async (): Promise<Check[]> => {
41
+ const checks: Check[] = []
42
+ const record = (label: string, pass: boolean, detail: string) => checks.push({ label, pass, detail })
43
+
44
+ /* ===================== Unit checks: the pure predicate ===================== */
45
+
46
+ const TEMPLATE_ID = '507f1f77bcf86cd799439011'
47
+ const ENDUSER_ID = '507f1f77bcf86cd799439012'
48
+ const NOW = 1_700_000_000_000
49
+
50
+ const baseEvent = {
51
+ id: 'event-1',
52
+ startTimeInMS: NOW + ONE_DAY_IN_MS,
53
+ templateId: TEMPLATE_ID,
54
+ attendees: [{ id: 'some-user' }, { id: ENDUSER_ID }],
55
+ }
56
+ const find = (overrides: object = {}, args: object = {}) => find_conflicting_upcoming_event({
57
+ events: [{ ...baseEvent, ...overrides }],
58
+ enduserId: ENDUSER_ID,
59
+ templateId: TEMPLATE_ID,
60
+ nowInMS: NOW,
61
+ ...args,
62
+ })
63
+
64
+ record(
65
+ 'U1: a future unresolved event of the same template conflicts',
66
+ !!find(),
67
+ 'the base case the setting exists for',
68
+ )
69
+ record(
70
+ 'U2: the enduser need not be the FIRST attendee (group events)',
71
+ !!find({ attendees: [{ id: 'other-enduser' }, { id: 'some-user' }, { id: ENDUSER_ID }] }),
72
+ 'get_enduserId uses first-enduser-attendee; this predicate deliberately does not',
73
+ )
74
+ record(
75
+ 'U3: a PAST event never conflicts',
76
+ !find({ startTimeInMS: NOW - ONE_DAY_IN_MS }),
77
+ 'past appointments never block, resolved or not',
78
+ )
79
+ record(
80
+ 'U4: an event for a different template never conflicts',
81
+ !find({ templateId: 'a-different-template' }),
82
+ 'the restriction is per template',
83
+ )
84
+ record(
85
+ 'U5: an event this enduser does not attend never conflicts',
86
+ !find({ attendees: [{ id: 'someone-else' }] }),
87
+ "another patient's appointment must not block",
88
+ )
89
+ record(
90
+ 'U6: cancelledAt resolves the event',
91
+ !find({ cancelledAt: new Date(NOW) }),
92
+ 'cancelling frees up rebooking',
93
+ )
94
+ record(
95
+ 'U7: noShowedAt resolves the event',
96
+ !find({ noShowedAt: new Date(NOW) }),
97
+ 'a no-show frees up rebooking',
98
+ )
99
+ record(
100
+ 'U8: rescheduledAt resolves the event',
101
+ !find({ rescheduledAt: new Date(NOW) }),
102
+ 'a rescheduled-away event frees up rebooking',
103
+ )
104
+ record(
105
+ "U9: empty-string timestamps (the `Date | ''` shape) do NOT resolve the event",
106
+ !!find({ cancelledAt: '' as const, noShowedAt: '' as const, rescheduledAt: '' as const }),
107
+ "these fields are typed Date | '' — falsiness is what must be tested, not absence",
108
+ )
109
+ for (const status of ['Cancelled', 'No-showed', 'Rescheduled']) {
110
+ record(
111
+ `U10: an attendeeStatuses entry of '${status}' for THIS enduser resolves the event`,
112
+ !find({ attendeeStatuses: [{ id: ENDUSER_ID, status, at: new Date(NOW) }] }),
113
+ "note the exact spelling 'No-showed', which is not 'NoShow'",
114
+ )
115
+ }
116
+ record(
117
+ "U11: an attendeeStatuses entry of 'Confirmed' does NOT resolve the event",
118
+ !!find({ attendeeStatuses: [{ id: ENDUSER_ID, status: 'Confirmed', at: new Date(NOW) }] }),
119
+ 'only Cancelled / No-showed / Rescheduled free up rebooking',
120
+ )
121
+ record(
122
+ "U12: another attendee's 'Cancelled' status does NOT resolve it for this enduser",
123
+ !!find({ attendeeStatuses: [{ id: 'other-enduser', status: 'Cancelled', at: new Date(NOW) }] }),
124
+ 'a group event cancelled for someone else still blocks this patient',
125
+ )
126
+ record(
127
+ 'U13: excludeEventId (the event being rescheduled away) does not conflict',
128
+ !find({}, { excludeEventId: 'event-1' }),
129
+ 'a reschedule must not be blocked by the very event it replaces',
130
+ )
131
+ record(
132
+ 'U14: an event on hold (holdUntil) still conflicts',
133
+ !!find({ holdUntil: new Date(NOW + ONE_DAY_IN_MS) }),
134
+ 'accepted: an abandoned hold blocks rebooking until it expires',
135
+ )
136
+ record(
137
+ 'U15: a cancelledGroupAttendees entry for this enduser resolves the event',
138
+ !find({ cancelledGroupAttendees: [{ id: ENDUSER_ID }] }),
139
+ 'Healthie and the portal cancel a group attendee with ONLY this legacy mirror, leaving them in attendees',
140
+ )
141
+ record(
142
+ "U16: an explicit 'Confirmed' status outranks a cancelledGroupAttendees entry",
143
+ !!find({
144
+ attendeeStatuses: [{ id: ENDUSER_ID, status: 'Confirmed', at: new Date(NOW) }],
145
+ cancelledGroupAttendees: [{ id: ENDUSER_ID }],
146
+ }),
147
+ 'same precedence as enduser_ids_for_event_wide_completion: an explicit status is authoritative',
148
+ )
149
+ record(
150
+ "U17: another attendee's cancelledGroupAttendees entry does not resolve it for this enduser",
151
+ !!find({ cancelledGroupAttendees: [{ id: 'other-enduser' }] }),
152
+ 'a group event cancelled for someone else still blocks this patient',
153
+ )
154
+ const SAME_SLOT = { startTimeInMS: NOW + ONE_DAY_IN_MS, hostUserId: 'some-user' }
155
+ record(
156
+ 'U18: the slot being booked right now (same time AND same host) is not a duplicate of itself',
157
+ !find({ wasSelfScheduled: true }, { excludeSlot: SAME_SLOT }),
158
+ 'keeps a retry / an idempotent voice-agent replay from being refused as a duplicate',
159
+ )
160
+ record(
161
+ 'U19: the same time with a DIFFERENT host is still a duplicate',
162
+ !!find({ wasSelfScheduled: true }, { excludeSlot: { ...SAME_SLOT, hostUserId: 'a-different-host' } }),
163
+ 'excludeSlot must not become a bypass for a genuine second appointment at the same moment',
164
+ )
165
+ record(
166
+ 'U20: a NON-self-scheduled event at the same time and host is still a duplicate',
167
+ !!find({ wasSelfScheduled: false }, { excludeSlot: SAME_SLOT }),
168
+ 'the atomic upsert we defer to matches wasSelfScheduled: true, so it would never catch a '
169
+ + 'staff-created or EHR-synced event at that instant — excusing it here would let a second one through',
170
+ )
171
+
172
+ /* ===================== End-to-end checks ===================== */
173
+
174
+ let tenant: Awaited<ReturnType<typeof setup_isolated_tenant>> | undefined
175
+
176
+ try {
177
+ tenant = await setup_isolated_tenant({
178
+ name: `BookingDup ${RAND()}`,
179
+ users: [{ isAdmin: true }, { isAdmin: true }],
180
+ apiKeys: 0,
181
+ })
182
+ const { sdk } = tenant
183
+ const hostUserId = tenant.users[0].id
184
+ const otherHostUserId = tenant.users[1].id
185
+
186
+ const WEEKLY_AVAILABILITY = [1, 2, 3, 4, 5].map(dayOfWeekStartingSundayIndexedByZero => ({
187
+ dayOfWeekStartingSundayIndexedByZero,
188
+ startTimeInMinutes: 9 * 60,
189
+ endTimeInMinutes: 17 * 60,
190
+ intervalInMinutes: 60,
191
+ }))
192
+ await Promise.all([hostUserId, otherHostUserId].map(id => (
193
+ sdk.api.users.updateOne(id, {
194
+ timezone: 'America/New_York',
195
+ weeklyAvailabilities: WEEKLY_AVAILABILITY,
196
+ }, { replaceObjectFields: true })
197
+ )))
198
+
199
+ // no reminders, confirmations disabled — this suite must never send anything
200
+ const templateFields = {
201
+ durationInMinutes: 60,
202
+ confirmationEmailDisabled: true,
203
+ confirmationSMSDisabled: true,
204
+ reminders: [],
205
+ }
206
+ const [restrictedTemplate, unrestrictedTemplate] = await Promise.all([
207
+ sdk.api.calendar_event_templates.createOne({
208
+ title: 'Consult', ...templateFields, preventBookingWhenUpcomingExists: true,
209
+ }),
210
+ sdk.api.calendar_event_templates.createOne({
211
+ title: 'Follow-up', ...templateFields, // setting deliberately absent
212
+ }),
213
+ ])
214
+
215
+ const create_enduser_session = async (fname: string) => {
216
+ const enduser = await sdk.api.endusers.createOne({ fname, lname: 'Tester', email: TEST_EMAIL() })
217
+ const { authToken } = await sdk.api.endusers.generate_auth_token({ id: enduser.id })
218
+ const enduserSDK = new EnduserSession({ host, businessId: tenant!.businessId, authToken })
219
+ return { enduser, enduserSDK }
220
+ }
221
+
222
+ // A synthetic upcoming appointment, written straight through POST /calendar-event (which is
223
+ // deliberately NOT blocked server-side — EHR sync and automations write through it too).
224
+ // dontBlockAvailability + no user attendee keeps it from perturbing the host's open slots.
225
+ const give_upcoming_event = async (enduserId: string, templateId: string, overrides: Partial<CalendarEvent> = {}) => (
226
+ sdk.api.calendar_events.createOne({
227
+ title: 'Consult',
228
+ startTimeInMS: Date.now() + 3 * ONE_DAY_IN_MS,
229
+ durationInMinutes: 60,
230
+ templateId,
231
+ attendees: [{ type: 'enduser', id: enduserId }],
232
+ dontBlockAvailability: true,
233
+ reminders: [],
234
+ ...overrides,
235
+ } as any)
236
+ )
237
+
238
+ // Every booking attempt gets its own never-reused slot, so "Someone has already booked this
239
+ // time" can never be mistaken for the duplicate block we are actually testing. Both hosts share
240
+ // the 9-17 weekday availability set above: 8 hourly slots a day, walked forwards from a weekday
241
+ // next week (Tue->Fri stay inside the Mon-Fri availability).
242
+ const SLOTS_PER_DAY = 8
243
+ const nowET = DateTime.now().setZone('America/New_York')
244
+ let bookingStart = nowET.startOf('week').plus({ days: 1, weeks: 1 })
245
+ if (bookingStart <= nowET) bookingStart = bookingStart.plus({ weeks: 1 })
246
+
247
+ let slotCursor = 0
248
+ const next_slot = () => {
249
+ const i = slotCursor++
250
+ return (
251
+ bookingStart
252
+ .plus({ days: Math.floor(i / SLOTS_PER_DAY) })
253
+ .set({ hour: 9 + (i % SLOTS_PER_DAY), minute: 0, second: 0, millisecond: 0 })
254
+ .toJSDate()
255
+ )
256
+ }
257
+
258
+ const EXPECTED_REFUSAL = "This appointment can't be booked. Please contact us if you need help scheduling."
259
+
260
+ const attempt_booking = async (enduserSDK: EnduserSession, args: object) => {
261
+ try {
262
+ const { createdEvent } = await enduserSDK.api.calendar_events.book_appointment({
263
+ userId: hostUserId,
264
+ timezone: 'America/New_York',
265
+ ...args,
266
+ } as any)
267
+ return { booked: true as const, event: createdEvent, error: '' }
268
+ } catch (err: any) {
269
+ const message = typeof err === 'string' ? err : (err?.message ?? JSON.stringify(err))
270
+ return { booked: false as const, event: undefined, error: message }
271
+ }
272
+ }
273
+
274
+ /* --- Core: the block itself --- */
275
+
276
+ const { enduser: patA, enduserSDK: patASDK } = await create_enduser_session('PatA')
277
+
278
+ const firstBooking = await attempt_booking(patASDK, {
279
+ calendarEventTemplateId: restrictedTemplate.id,
280
+ startTime: next_slot(),
281
+ })
282
+ record(
283
+ 'E1: the first booking of a restricted template succeeds',
284
+ firstBooking.booked,
285
+ `error=${firstBooking.error}`,
286
+ )
287
+
288
+ const secondBooking = await attempt_booking(patASDK, {
289
+ calendarEventTemplateId: restrictedTemplate.id,
290
+ startTime: next_slot(),
291
+ })
292
+ record(
293
+ 'E2: a second booking of the same template is refused',
294
+ !secondBooking.booked,
295
+ `error=${secondBooking.error}`,
296
+ )
297
+ record(
298
+ 'E3: the refusal is the generic message, verbatim',
299
+ secondBooking.error === EXPECTED_REFUSAL,
300
+ `got ${JSON.stringify(secondBooking.error)}, expected ${JSON.stringify(EXPECTED_REFUSAL)}`,
301
+ )
302
+ record(
303
+ 'E4: the refusal leaks no template title, date or host',
304
+ !['Consult', 'Follow-up', String(hostUserId), String(patA.id)].some(s => secondBooking.error.includes(s)),
305
+ `the public booking page is unauthenticated, so any detail is an oracle — got ${JSON.stringify(secondBooking.error)}`,
306
+ )
307
+
308
+ const otherHostBooking = await attempt_booking(patASDK, {
309
+ calendarEventTemplateId: restrictedTemplate.id,
310
+ startTime: next_slot(),
311
+ userId: otherHostUserId,
312
+ })
313
+ record(
314
+ 'E5: the block is per TEMPLATE, not per provider — the other host is refused too',
315
+ !otherHostBooking.booked && otherHostBooking.error === EXPECTED_REFUSAL,
316
+ `booked=${otherHostBooking.booked} error=${otherHostBooking.error}`,
317
+ )
318
+
319
+ // scheduledBy is only a URL query parameter validated as a mongo id — if it exempted the
320
+ // booking, the block would be bypassable by anyone who could edit the link
321
+ const scheduledByBooking = await attempt_booking(patASDK, {
322
+ calendarEventTemplateId: restrictedTemplate.id,
323
+ startTime: next_slot(),
324
+ scheduledBy: hostUserId,
325
+ })
326
+ record(
327
+ 'E5b: a staff-sent link (scheduledBy) is NOT an exemption',
328
+ !scheduledByBooking.booked && scheduledByBooking.error === EXPECTED_REFUSAL,
329
+ `booked=${scheduledByBooking.booked} error=${scheduledByBooking.error}`,
330
+ )
331
+
332
+ const unrestrictedBooking = await attempt_booking(patASDK, {
333
+ calendarEventTemplateId: unrestrictedTemplate.id,
334
+ startTime: next_slot(),
335
+ })
336
+ record(
337
+ 'E6: a template WITHOUT the setting is unaffected (the default for every org)',
338
+ unrestrictedBooking.booked,
339
+ `error=${unrestrictedBooking.error}`,
340
+ )
341
+
342
+ /* --- An unrelated patient is never blocked --- */
343
+
344
+ const { enduserSDK: patBSDK } = await create_enduser_session('PatB')
345
+ const patBBooking = await attempt_booking(patBSDK, {
346
+ calendarEventTemplateId: restrictedTemplate.id,
347
+ startTime: next_slot(),
348
+ })
349
+ record(
350
+ "E7: another patient's appointment never blocks",
351
+ patBBooking.booked,
352
+ `error=${patBBooking.error}`,
353
+ )
354
+
355
+ /* --- Freeing up the slot: each case gets its own patient, so order cannot matter --- */
356
+
357
+ const freeing_case = async (
358
+ label: string,
359
+ overrides: (enduserId: string) => Partial<CalendarEvent>,
360
+ detail: string,
361
+ ) => {
362
+ const { enduser, enduserSDK } = await create_enduser_session('Freeing')
363
+ await give_upcoming_event(enduser.id, restrictedTemplate.id, overrides(enduser.id))
364
+ const result = await attempt_booking(enduserSDK, {
365
+ calendarEventTemplateId: restrictedTemplate.id,
366
+ startTime: next_slot(),
367
+ })
368
+ record(label, result.booked, `${detail} — error=${result.error}`)
369
+ }
370
+
371
+ await freeing_case('E8: a cancelled upcoming event frees up rebooking', () => ({ cancelledAt: new Date() }), 'cancelledAt set')
372
+ await freeing_case('E9: a no-showed upcoming event frees up rebooking', () => ({ noShowedAt: new Date() }), 'noShowedAt set')
373
+ await freeing_case('E10: a rescheduled-away upcoming event frees up rebooking', () => ({ rescheduledAt: new Date() }), 'rescheduledAt set')
374
+ await freeing_case(
375
+ "E11: an attendeeStatuses 'No-showed' entry frees up rebooking",
376
+ enduserId => ({ attendeeStatuses: [{ id: enduserId, status: 'No-showed', at: new Date() }] }),
377
+ "per-attendee status, spelled 'No-showed'",
378
+ )
379
+
380
+ /* --- A PAST unresolved event never blocks --- */
381
+
382
+ const { enduser: pastPat, enduserSDK: pastPatSDK } = await create_enduser_session('PastPat')
383
+ await give_upcoming_event(pastPat.id, restrictedTemplate.id, {
384
+ startTimeInMS: Date.now() - 3 * ONE_DAY_IN_MS, // nobody ever marked it complete
385
+ })
386
+ const pastBooking = await attempt_booking(pastPatSDK, {
387
+ calendarEventTemplateId: restrictedTemplate.id,
388
+ startTime: next_slot(),
389
+ userId: otherHostUserId,
390
+ })
391
+ record(
392
+ 'E12: a PAST unresolved appointment never blocks',
393
+ pastBooking.booked,
394
+ `error=${pastBooking.error}`,
395
+ )
396
+
397
+ /* --- Group event where the patient is not the first attendee --- */
398
+
399
+ const { enduser: groupPat, enduserSDK: groupPatSDK } = await create_enduser_session('GroupPat')
400
+ const { enduser: groupOther } = await create_enduser_session('GroupOther')
401
+ await give_upcoming_event(groupPat.id, restrictedTemplate.id, {
402
+ attendees: [
403
+ { type: 'enduser', id: groupOther.id },
404
+ { type: 'enduser', id: groupPat.id },
405
+ ],
406
+ enduserAttendeeLimit: 5,
407
+ })
408
+ const groupBooking = await attempt_booking(groupPatSDK, {
409
+ calendarEventTemplateId: restrictedTemplate.id,
410
+ startTime: next_slot(),
411
+ userId: otherHostUserId,
412
+ })
413
+ record(
414
+ 'E13: a group event blocks a patient who is NOT its first attendee',
415
+ !groupBooking.booked && groupBooking.error === EXPECTED_REFUSAL,
416
+ `booked=${groupBooking.booked} error=${groupBooking.error}`,
417
+ )
418
+
419
+ /* --- A retried identical booking is the same appointment, not a duplicate --- */
420
+
421
+ const { enduserSDK: retrySDK } = await create_enduser_session('RetryPat')
422
+ const retrySlot = next_slot()
423
+ const retryArgs = {
424
+ calendarEventTemplateId: restrictedTemplate.id,
425
+ startTime: retrySlot,
426
+ userId: otherHostUserId,
427
+ }
428
+ const retryFirst = await attempt_booking(retrySDK, retryArgs)
429
+ const retrySecond = await attempt_booking(retrySDK, retryArgs)
430
+ record(
431
+ 'E13b: re-submitting the SAME slot is not refused as a duplicate',
432
+ retryFirst.booked && retrySecond.error !== EXPECTED_REFUSAL,
433
+ `first=${retryFirst.booked} secondError=${retrySecond.error} — the duplicate block must not `
434
+ + 'swallow the idempotent-replay / slot-taken handling for the very booking being retried '
435
+ + '(the first booking is wasSelfScheduled, which is what excludeSlot is limited to)',
436
+ )
437
+
438
+ /* --- A group cancellation recorded only in cancelledGroupAttendees frees up rebooking --- */
439
+
440
+ const { enduser: groupCancelled, enduserSDK: groupCancelledSDK } = await create_enduser_session('GroupCancelled')
441
+ await give_upcoming_event(groupCancelled.id, restrictedTemplate.id, {
442
+ attendees: [{ type: 'enduser', id: groupCancelled.id }],
443
+ cancelledGroupAttendees: [{ at: new Date(), id: groupCancelled.id }],
444
+ enduserAttendeeLimit: 5,
445
+ })
446
+ const groupCancelledBooking = await attempt_booking(groupCancelledSDK, {
447
+ calendarEventTemplateId: restrictedTemplate.id,
448
+ startTime: next_slot(),
449
+ userId: otherHostUserId,
450
+ })
451
+ record(
452
+ 'E13c: a cancelledGroupAttendees-only cancellation frees up rebooking',
453
+ groupCancelledBooking.booked,
454
+ `Healthie-synced group cancels write only this mirror — error=${groupCancelledBooking.error}`,
455
+ )
456
+
457
+ /* --- Reschedule still works --- */
458
+
459
+ const { enduserSDK: reschedSDK } = await create_enduser_session('ReschedPat')
460
+ const reschedFirst = await attempt_booking(reschedSDK, {
461
+ calendarEventTemplateId: restrictedTemplate.id,
462
+ startTime: next_slot(),
463
+ userId: otherHostUserId,
464
+ })
465
+ const rescheduled = reschedFirst.booked
466
+ ? await attempt_booking(reschedSDK, {
467
+ calendarEventTemplateId: restrictedTemplate.id,
468
+ startTime: next_slot(),
469
+ userId: otherHostUserId,
470
+ rescheduledCalendarEventId: reschedFirst.event!.id,
471
+ })
472
+ : { booked: false as const, error: 'initial booking failed' }
473
+ record(
474
+ 'E14: rescheduling is not blocked by the event it replaces',
475
+ rescheduled.booked,
476
+ `initialBooked=${reschedFirst.booked} error=${rescheduled.error}`,
477
+ )
478
+
479
+ /* --- An unavailable slot cannot be used to probe for an upcoming appointment --- */
480
+
481
+ // The public booking page gives a session on an existing patient by email alone, so the refusal
482
+ // must not be reachable without first holding an open slot: an unavailable slot has to fail the
483
+ // same way whether or not the patient is already booked.
484
+ const { enduser: probedBooked, enduserSDK: probedBookedSDK } = await create_enduser_session('ProbedBooked')
485
+ const { enduserSDK: probedFreeSDK } = await create_enduser_session('ProbedFree')
486
+ await give_upcoming_event(probedBooked.id, restrictedTemplate.id)
487
+
488
+ // Sunday 10:00 ET, outside the hosts' Mon-Fri availability
489
+ const closedSlot = bookingStart.startOf('week').plus({ days: 6 }).set({ hour: 10, minute: 0, second: 0, millisecond: 0 }).toJSDate()
490
+ const probeArgs = { calendarEventTemplateId: restrictedTemplate.id, startTime: closedSlot }
491
+ const [probeBooked, probeFree] = await Promise.all([
492
+ attempt_booking(probedBookedSDK, probeArgs),
493
+ attempt_booking(probedFreeSDK, { ...probeArgs, userId: otherHostUserId }),
494
+ ])
495
+ const UNAVAILABLE = "This timeslot is no longer available"
496
+ record(
497
+ 'E15: an unavailable slot fails identically with or without an upcoming duplicate',
498
+ !probeBooked.booked && !probeFree.booked && probeBooked.error === UNAVAILABLE && probeFree.error === UNAVAILABLE,
499
+ `booked-patient error=${JSON.stringify(probeBooked.error)}, free-patient error=${JSON.stringify(probeFree.error)}`,
500
+ )
501
+
502
+ // the endpoint deletes a failed booking's placeholder without awaiting it, so give it a moment
503
+ let leftovers = await sdk.api.calendar_events.getSome({ filter: { startTimeInMS: closedSlot.getTime() } })
504
+ for (let i = 0; i < 10 && leftovers.length; i++) {
505
+ await new Promise(r => setTimeout(r, 200))
506
+ leftovers = await sdk.api.calendar_events.getSome({ filter: { startTimeInMS: closedSlot.getTime() } })
507
+ }
508
+ record(
509
+ 'E15b: the probes leave no placeholder event behind',
510
+ leftovers.length === 0,
511
+ `found ${leftovers.length} event(s) at the closed slot`,
512
+ )
513
+ } finally {
514
+ // Teardown BEFORE anything asserts — assert calls process.exit, which would strand this tenant.
515
+ if (tenant) { try { await tenant.teardown() } catch (e) { console.error('teardown failed', e) } }
516
+ }
517
+
518
+ return checks
519
+ }
520
+
521
+ /** Asserting wrapper, for standalone runs and for sequential use in the suite. */
522
+ export const booking_prevent_upcoming_duplicate_tests = async () => {
523
+ assert_checks('Prevent booking when an upcoming duplicate exists', await booking_prevent_upcoming_duplicate_checks())
524
+ }
525
+
526
+ /** Enrolls this suite in the auto-discovered parallel pool — see run_discovered_isolated_suites. */
527
+ export const ISOLATED_SUITE: IsolatedSuite = { name: 'Prevent booking when an upcoming duplicate exists', run: booking_prevent_upcoming_duplicate_checks }
528
+
529
+ // Allow running this test file independently
530
+ if (require.main === module) {
531
+ console.log(`🌐 Using API URL: ${host}`)
532
+ booking_prevent_upcoming_duplicate_tests()
533
+ .then(() => {
534
+ console.log("✅ Prevent-duplicate-booking test suite completed successfully")
535
+ process.exit(0)
536
+ })
537
+ .catch((error) => {
538
+ console.error("❌ Prevent-duplicate-booking test suite failed:", error)
539
+ process.exit(1)
540
+ })
541
+ }