@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
@@ -0,0 +1,154 @@
1
+ require('source-map-support').install();
2
+
3
+ import {
4
+ assert_checks,
5
+ Check,
6
+ setup_isolated_tenant,
7
+ IsolatedSuite,
8
+ } from "../setup_isolated"
9
+
10
+ const host = process.env.API_URL || 'http://localhost:8080' as const
11
+
12
+ const RAND = () => Math.random().toString(36).slice(2, 10)
13
+
14
+ /**
15
+ * Schema round-trip for `Form.syncEnduserFieldsToEHR` and `Form.enduserFieldsSync`, the two fields
16
+ * behind "Sync Patient Fields to Healthie".
17
+ *
18
+ * The names are deliberately EHR-agnostic — each row's `externalId` is the question id in whichever
19
+ * EHR the form syncs to (`Form.externalId`) — even though only the Healthie push reads them today.
20
+ *
21
+ * What this file can and cannot cover: the actual `createFormAnswerGroup` round trip needs live
22
+ * Healthie credentials that no suite here has, and there is no Healthie mock or fixture layer in the
23
+ * repository, so the push itself is covered by the resolver's unit tests
24
+ * (api/modules/forms.test.ts) plus manual QA against Healthie staging. What IS covered here is
25
+ * everything the API owns: that the fields persist and read back unchanged, that validation rejects
26
+ * malformed rows, and — the one that guards a real decision in the form-editor modal — that an
27
+ * update with `replaceObjectFields` actually SHRINKS the list rather than merging into it, which is
28
+ * what makes deleting a mapping row persist.
29
+ */
30
+ export const enduser_fields_sync_checks = async (): Promise<Check[]> => {
31
+ const checks: Check[] = []
32
+ const record = (label: string, pass: boolean, detail: string) => checks.push({ label, pass, detail })
33
+
34
+ /** records a check that the given write is rejected (any 4xx), not silently accepted */
35
+ const record_rejected = async (label: string, write: () => Promise<any>) => {
36
+ try {
37
+ const accepted = await write()
38
+ record(label, false, `accepted, expected a validation error — got ${JSON.stringify(accepted?.enduserFieldsSync)}`)
39
+ } catch (err: any) {
40
+ const message = err?.message || String(err)
41
+ record(label, true, `rejected: ${message.slice(0, 200)}`)
42
+ }
43
+ }
44
+
45
+ let tenant: Awaited<ReturnType<typeof setup_isolated_tenant>> | undefined
46
+
47
+ try {
48
+ tenant = await setup_isolated_tenant({ name: `FieldSync ${RAND()}`, users: [{ isAdmin: true }], apiKeys: 0 })
49
+ const sdk = tenant.sdk
50
+
51
+ const TWO_ROWS = [
52
+ { field: 'dateOfBirth', externalId: '111111' },
53
+ { field: 'Referring Provider', externalId: '222222' },
54
+ ]
55
+
56
+ /* ===== 1. Create with both fields set, and read them back ===== */
57
+ const created = await sdk.api.forms.createOne({
58
+ title: `Field sync ${RAND()}`,
59
+ externalId: 'healthie-form-id',
60
+ syncEnduserFieldsToEHR: true,
61
+ enduserFieldsSync: TWO_ROWS,
62
+ })
63
+ const readBack = await sdk.api.forms.getOne(created.id)
64
+
65
+ record('1: create persists syncEnduserFieldsToEHR', readBack.syncEnduserFieldsToEHR === true,
66
+ `got ${JSON.stringify(readBack.syncEnduserFieldsToEHR)}`)
67
+ record('1: create persists enduserFieldsSync verbatim',
68
+ JSON.stringify(readBack.enduserFieldsSync) === JSON.stringify(TWO_ROWS),
69
+ `got ${JSON.stringify(readBack.enduserFieldsSync)} expected ${JSON.stringify(TWO_ROWS)}`)
70
+
71
+ /* ===== 2. replaceObjectFields must SHRINK the list, not merge into it =====
72
+ This is the guard for the form-editor modal's `replaceObjectFields` change. Without it,
73
+ deleting a mapping row in the UI would save and then silently come back on reload. */
74
+ const ONE_ROW = [{ field: 'insurance.memberId', externalId: '333333' }]
75
+ await sdk.api.forms.updateOne(created.id, { enduserFieldsSync: ONE_ROW }, { replaceObjectFields: true })
76
+ const shrunk = await sdk.api.forms.getOne(created.id)
77
+
78
+ record('2: replaceObjectFields shrinks a two-row list to one row',
79
+ JSON.stringify(shrunk.enduserFieldsSync) === JSON.stringify(ONE_ROW),
80
+ `got ${JSON.stringify(shrunk.enduserFieldsSync)} expected ${JSON.stringify(ONE_ROW)}`)
81
+
82
+ /* ===== 3. Malformed rows are rejected ===== */
83
+ await record_rejected('3: a row with a non-string field is rejected', () =>
84
+ sdk.api.forms.updateOne(created.id, { enduserFieldsSync: [{ field: 1, externalId: '1' }] } as any,
85
+ { replaceObjectFields: true }))
86
+
87
+ await record_rejected('3: a row missing externalId is rejected', () =>
88
+ sdk.api.forms.updateOne(created.id, { enduserFieldsSync: [{ field: 'fname' }] } as any,
89
+ { replaceObjectFields: true }))
90
+
91
+ await record_rejected('3: an externalId over 100 characters is rejected', () =>
92
+ sdk.api.forms.updateOne(created.id,
93
+ { enduserFieldsSync: [{ field: 'fname', externalId: 'x'.repeat(101) }] },
94
+ { replaceObjectFields: true }))
95
+
96
+ // a rejected write must not have partially applied - the one-row list from step 2 stands
97
+ const afterRejections = await sdk.api.forms.getOne(created.id)
98
+ record('3: rejected writes leave the stored list untouched',
99
+ JSON.stringify(afterRejections.enduserFieldsSync) === JSON.stringify(ONE_ROW),
100
+ `got ${JSON.stringify(afterRejections.enduserFieldsSync)} expected ${JSON.stringify(ONE_ROW)}`)
101
+
102
+ /* ===== 4. An empty list is accepted (clearing every row) ===== */
103
+ try {
104
+ await sdk.api.forms.updateOne(created.id, { enduserFieldsSync: [] }, { replaceObjectFields: true })
105
+ const cleared = await sdk.api.forms.getOne(created.id)
106
+ const rows = cleared.enduserFieldsSync
107
+ record('4: an empty list is accepted and clears the mapping',
108
+ Array.isArray(rows) && rows.length === 0,
109
+ `got ${JSON.stringify(rows)}`)
110
+ } catch (err: any) {
111
+ record('4: an empty list is accepted and clears the mapping', false,
112
+ `rejected: ${(err?.message || String(err)).slice(0, 200)}`)
113
+ }
114
+
115
+ /* ===== 5. Backward compatibility: a form that sets neither field reads back with neither ===== */
116
+ const plain = await sdk.api.forms.createOne({ title: `No field sync ${RAND()}` })
117
+ const plainReadBack = await sdk.api.forms.getOne(plain.id)
118
+
119
+ record('5: a form created without either field has no syncEnduserFieldsToEHR',
120
+ plainReadBack.syncEnduserFieldsToEHR === undefined,
121
+ `got ${JSON.stringify(plainReadBack.syncEnduserFieldsToEHR)}`)
122
+ record('5: a form created without either field has no enduserFieldsSync',
123
+ plainReadBack.enduserFieldsSync === undefined,
124
+ `got ${JSON.stringify(plainReadBack.enduserFieldsSync)}`)
125
+ } finally {
126
+ // Teardown BEFORE anything asserts — assert calls process.exit, which skips pending finallys and
127
+ // would strand the provisioned tenant permanently.
128
+ if (tenant) { try { await tenant.teardown() } catch (e) { console.error('teardown failed', e) } }
129
+ }
130
+
131
+ return checks
132
+ }
133
+
134
+ /** Asserting wrapper, for standalone runs and for sequential use in the suite. */
135
+ export const enduser_fields_sync_tests = async () => {
136
+ assert_checks('Enduser fields sync schema round-trip', await enduser_fields_sync_checks())
137
+ }
138
+
139
+ /** Enrolls this suite in the auto-discovered parallel pool — see run_discovered_isolated_suites. */
140
+ export const ISOLATED_SUITE: IsolatedSuite = { name: 'Enduser fields sync schema round-trip', run: enduser_fields_sync_checks }
141
+
142
+ // Allow running this test file independently
143
+ if (require.main === module) {
144
+ console.log(`🌐 Using API URL: ${host}`)
145
+ enduser_fields_sync_tests()
146
+ .then(() => {
147
+ console.log("✅ enduser fields sync test suite completed successfully")
148
+ process.exit(0)
149
+ })
150
+ .catch((error) => {
151
+ console.error("❌ enduser fields sync test suite failed:", error)
152
+ process.exit(1)
153
+ })
154
+ }
@@ -5,6 +5,7 @@ import {
5
5
  assert_checks,
6
6
  Check,
7
7
  setup_isolated_tenant,
8
+ IsolatedSuite,
8
9
  } from "../setup_isolated"
9
10
 
10
11
  const host = process.env.API_URL || 'http://localhost:8080' as const
@@ -343,6 +344,9 @@ export const enduser_observations_auto_review_tests = async () => {
343
344
  assert_checks('Vital auto-review on create', await enduser_observations_auto_review_checks())
344
345
  }
345
346
 
347
+ /** Enrolls this suite in the auto-discovered parallel pool — see run_discovered_isolated_suites. */
348
+ export const ISOLATED_SUITE: IsolatedSuite = { name: 'Vital auto-review on create', run: enduser_observations_auto_review_checks }
349
+
346
350
  // Allow running this test file independently
347
351
  if (require.main === module) {
348
352
  console.log(`🌐 Using API URL: ${host}`)
@@ -0,0 +1,393 @@
1
+ require('source-map-support').install();
2
+
3
+ import {
4
+ assert_checks,
5
+ Check,
6
+ setup_isolated_tenant,
7
+ IsolatedSuite,
8
+ } from "../setup_isolated"
9
+
10
+ const host = process.env.API_URL || 'http://localhost:8080' as const
11
+
12
+ const RAND = () => Math.random().toString(36).slice(2, 10)
13
+
14
+ const FORM_VITAL_READING_SOURCE = 'tellescope-form'
15
+
16
+ /**
17
+ * Coverage for the per-question opt-in that turns a numeric form answer into an
18
+ * `enduser_observations` vital reading on submission — `form_fields.options.vitalUnit`, consumed by
19
+ * `create_vital_readings_for_form_response` in api/modules/forms.ts.
20
+ *
21
+ * The point of the feature is that weight (and the other VITAL_UNITS) collected in a form charts
22
+ * beside device, portal and manually entered readings. The point of the opt-in being per FIELD is
23
+ * that nothing changes for an existing org or form until an admin configures a question — which is
24
+ * why the "no opt-in configured" check below is as load-bearing as the positive ones.
25
+ *
26
+ * Provisions its own throwaway tenant: readings are read back by enduser, and `handle_vital_create`
27
+ * reads business-wide vital_configurations, so a shared tenant would let concurrently running suites
28
+ * see each other's observations. Never calls `sdk.reset_db()`.
29
+ *
30
+ * Readings are created as an asynchronous side effect of the submission (fire-and-forget, so that a
31
+ * reading can never delay or fail a patient's submit), so every read polls — and every NEGATIVE
32
+ * check polls too, with a barrier that proves the submission's side effects have run.
33
+ */
34
+
35
+ const poll_for = async <T,>(get: () => Promise<T>, done: (v: T) => boolean, attempts=20): Promise<T> => {
36
+ let value = await get()
37
+ for (let i = 0; i < attempts && !done(value); i++) {
38
+ await new Promise(resolve => setTimeout(resolve, 250))
39
+ value = await get()
40
+ }
41
+ return value
42
+ }
43
+
44
+ export const form_vital_reading_checks = async (): Promise<Check[]> => {
45
+ const checks: Check[] = []
46
+ const record = (label: string, pass: boolean, detail: string) => checks.push({ label, pass, detail })
47
+
48
+ let tenant: Awaited<ReturnType<typeof setup_isolated_tenant>> | undefined
49
+
50
+ try {
51
+ tenant = await setup_isolated_tenant({
52
+ name: `FormVitals ${RAND()}`,
53
+ users: [{ isAdmin: true }],
54
+ apiKeys: 0,
55
+ })
56
+ const { sdk } = tenant
57
+
58
+ const observations_for = (enduserId: string) => (
59
+ sdk.api.enduser_observations.getSome({ filter: { enduserId } })
60
+ )
61
+
62
+ /* ===== A: a configured Number question creates a reading on submission ===== */
63
+
64
+ const form = await sdk.api.forms.createOne({ title: `Vitals intake ${RAND()}` })
65
+ const weightField = await sdk.api.form_fields.createOne({
66
+ formId: form.id,
67
+ title: 'Weight (lbs)',
68
+ type: 'number',
69
+ options: { vitalUnit: 'LB' },
70
+ previousFields: [{ type: 'root', info: {} }],
71
+ })
72
+ // a second question with NO vitalUnit, to prove the opt-in is per field and not per form
73
+ const plainField = await sdk.api.form_fields.createOne({
74
+ formId: form.id,
75
+ title: 'How many glasses of water?',
76
+ type: 'number',
77
+ previousFields: [{ type: 'after', info: { fieldId: weightField.id } }],
78
+ })
79
+
80
+ const enduser = await sdk.api.endusers.createOne({ fname: 'Form', lname: 'Vitals' })
81
+
82
+ const prepared = await sdk.api.form_responses.prepare_form_response({
83
+ formId: form.id, enduserId: enduser.id,
84
+ })
85
+ const submittedAt = new Date()
86
+ await sdk.api.form_responses.submit_form_response({
87
+ accessCode: prepared.accessCode,
88
+ responses: [
89
+ { fieldId: weightField.id, fieldTitle: weightField.title, answer: { type: 'number', value: 185 } },
90
+ { fieldId: plainField.id, fieldTitle: plainField.title, answer: { type: 'number', value: 8 } },
91
+ ],
92
+ })
93
+
94
+ const readings = await poll_for(() => observations_for(enduser.id), o => o.length >= 1)
95
+
96
+ record(
97
+ 'A1: exactly one reading is created — the opt-in is per field, not per form',
98
+ readings.length === 1,
99
+ `created ${readings.length}: ${JSON.stringify(readings.map(r => r.measurement))}`
100
+ + ` — the second Number question has no vitalUnit and must contribute nothing`,
101
+ )
102
+
103
+ const reading = readings[0]
104
+ record(
105
+ 'A2: the reading carries the answer value in the configured unit',
106
+ reading?.measurement?.value === 185 && reading?.measurement?.unit === 'LB',
107
+ `measurement=${JSON.stringify(reading?.measurement)} (want { value: 185, unit: 'LB' })`,
108
+ )
109
+ record(
110
+ 'A3: source marks it as form-collected',
111
+ reading?.source === FORM_VITAL_READING_SOURCE,
112
+ `source=${JSON.stringify(reading?.source)} (want ${JSON.stringify(FORM_VITAL_READING_SOURCE)})`
113
+ + ` — the webapp Vitals table renders an unlisted source as "Source: <source>"`,
114
+ )
115
+ record(
116
+ 'A4: it is a final vital-signs observation, like any other charted reading',
117
+ reading?.category === 'vital-signs' && reading?.status === 'final',
118
+ `category=${JSON.stringify(reading?.category)} status=${JSON.stringify(reading?.status)}`,
119
+ )
120
+ record(
121
+ 'A5: timestamp and recordedAt are the submission time',
122
+ (
123
+ !!reading?.timestamp && !!reading?.recordedAt
124
+ && new Date(reading.recordedAt).getTime() === new Date(reading.timestamp).getTime()
125
+ && Math.abs(new Date(reading.timestamp).getTime() - submittedAt.getTime()) < 1000 * 60 * 5
126
+ ),
127
+ `timestamp=${JSON.stringify(reading?.timestamp)} recordedAt=${JSON.stringify(reading?.recordedAt)}`
128
+ + ` (want both ~= the submission at ${submittedAt.toISOString()})`,
129
+ )
130
+ record(
131
+ 'A6: externalId links the reading back to <formResponseId>:<fieldId>',
132
+ reading?.externalId === `${prepared.response.id}:${weightField.id}`,
133
+ `externalId=${JSON.stringify(reading?.externalId)}`
134
+ + ` (want ${JSON.stringify(`${prepared.response.id}:${weightField.id}`)})`,
135
+ )
136
+ record(
137
+ 'A7: vital triggers are left enabled for an ordinary enduser',
138
+ reading?.dontTrigger !== true,
139
+ `dontTrigger=${JSON.stringify(reading?.dontTrigger)} (want not true, so Vital Update triggers fire)`,
140
+ )
141
+
142
+ /* ===== C: a form with no opted-in question creates nothing ===== */
143
+
144
+ const plainForm = await sdk.api.forms.createOne({ title: `No vitals ${RAND()}` })
145
+ const plainOnlyField = await sdk.api.form_fields.createOne({
146
+ formId: plainForm.id,
147
+ title: 'Current weight',
148
+ type: 'number',
149
+ previousFields: [{ type: 'root', info: {} }],
150
+ })
151
+ const plainEnduser = await sdk.api.endusers.createOne({ fname: 'NoOptIn', lname: 'Patient' })
152
+ const plainPrepared = await sdk.api.form_responses.prepare_form_response({
153
+ formId: plainForm.id, enduserId: plainEnduser.id,
154
+ })
155
+ await sdk.api.form_responses.submit_form_response({
156
+ accessCode: plainPrepared.accessCode,
157
+ responses: [
158
+ { fieldId: plainOnlyField.id, fieldTitle: plainOnlyField.title, answer: { type: 'number', value: 200 } },
159
+ ],
160
+ })
161
+ // barrier: the submission completed, so its side effects have had their chance
162
+ await poll_for(
163
+ () => sdk.api.form_responses.getOne(plainPrepared.response.id),
164
+ r => !!r.submittedAt,
165
+ )
166
+ const plainReadings = await poll_for(() => observations_for(plainEnduser.id), o => o.length > 0, 8)
167
+ record(
168
+ 'C1: a Number question with no vitalUnit creates no reading',
169
+ plainReadings.length === 0,
170
+ `created ${plainReadings.length}: ${JSON.stringify(plainReadings.map(r => r.measurement))}`
171
+ + ` — this is what keeps every existing org and form unchanged`,
172
+ )
173
+
174
+ /* ===== D: blank and non-numeric answers are skipped, and never fail the submission ===== */
175
+
176
+ const skipEnduser = await sdk.api.endusers.createOne({ fname: 'Skipped', lname: 'Answers' })
177
+ const skipPrepared = await sdk.api.form_responses.prepare_form_response({
178
+ formId: form.id, enduserId: skipEnduser.id,
179
+ })
180
+
181
+ let submitError: any = undefined
182
+ try {
183
+ await sdk.api.form_responses.submit_form_response({
184
+ accessCode: skipPrepared.accessCode,
185
+ responses: [
186
+ // a blank answer for the opted-in question
187
+ { fieldId: weightField.id, fieldTitle: weightField.title, answer: { type: 'number', value: '' as any } },
188
+ { fieldId: plainField.id, fieldTitle: plainField.title, answer: { type: 'number', value: 8 } },
189
+ ],
190
+ })
191
+ } catch (err) { submitError = err }
192
+
193
+ record(
194
+ 'D1: a blank answer does not fail the submission',
195
+ submitError === undefined,
196
+ `error=${submitError ? (submitError.message || JSON.stringify(submitError)) : 'none'}`
197
+ + ` — a patient must never see an error because of a vital reading`,
198
+ )
199
+ if (submitError === undefined) {
200
+ await poll_for(
201
+ () => sdk.api.form_responses.getOne(skipPrepared.response.id),
202
+ r => !!r.submittedAt,
203
+ )
204
+ const skipReadings = await poll_for(() => observations_for(skipEnduser.id), o => o.length > 0, 8)
205
+ record(
206
+ 'D2: a blank answer creates no reading',
207
+ skipReadings.length === 0,
208
+ `created ${skipReadings.length}: ${JSON.stringify(skipReadings.map(r => r.measurement))}`,
209
+ )
210
+ }
211
+
212
+ // A non-numeric answer never reaches the reading code over HTTP: the submit endpoint validates
213
+ // a 'number' answer with numberValidator ("Response must be a number") and rejects the whole
214
+ // submission. Pinned here because QA step 16 is written as if the field type allowed it.
215
+ // The parseFloat/NaN guard in create_vital_readings_for_form_response is therefore defence in
216
+ // depth for the non-HTTP path (submit_form_response_internal, whose responses are persisted
217
+ // incrementally rather than through this validator).
218
+ const badEnduser = await sdk.api.endusers.createOne({ fname: 'NonNumeric', lname: 'Answer' })
219
+ const badPrepared = await sdk.api.form_responses.prepare_form_response({
220
+ formId: form.id, enduserId: badEnduser.id,
221
+ })
222
+ let badError: any = undefined
223
+ try {
224
+ await sdk.api.form_responses.submit_form_response({
225
+ accessCode: badPrepared.accessCode,
226
+ responses: [
227
+ { fieldId: weightField.id, fieldTitle: weightField.title, answer: { type: 'number', value: 'one hundred' as any } },
228
+ ],
229
+ })
230
+ } catch (err) { badError = err }
231
+ const badReadings = await poll_for(() => observations_for(badEnduser.id), o => o.length > 0, 8)
232
+ record(
233
+ 'D3: a non-numeric answer on a Number question is rejected and creates no reading',
234
+ // assert the rejection too, so this check starts failing if numberValidator ever stops
235
+ // rejecting and the answer begins reaching create_vital_readings_for_form_response for real
236
+ !!badError && badReadings.length === 0,
237
+ `submit ${badError ? `rejected (${badError.message || JSON.stringify(badError)})` : 'ACCEPTED'};`
238
+ + ` ${badReadings.length} reading(s) — rejection happens in numberValidator, before this feature`,
239
+ )
240
+
241
+ /* ===== E: one unit per question — SBP and DBP are two questions ===== */
242
+
243
+ const bpForm = await sdk.api.forms.createOne({ title: `Blood pressure ${RAND()}` })
244
+ const sbpField = await sdk.api.form_fields.createOne({
245
+ formId: bpForm.id, title: 'Systolic', type: 'number',
246
+ options: { vitalUnit: 'SBP' },
247
+ previousFields: [{ type: 'root', info: {} }],
248
+ })
249
+ const dbpField = await sdk.api.form_fields.createOne({
250
+ formId: bpForm.id, title: 'Diastolic', type: 'number',
251
+ options: { vitalUnit: 'DBP' },
252
+ previousFields: [{ type: 'after', info: { fieldId: sbpField.id } }],
253
+ })
254
+ const bpEnduser = await sdk.api.endusers.createOne({ fname: 'Blood', lname: 'Pressure' })
255
+ const bpPrepared = await sdk.api.form_responses.prepare_form_response({
256
+ formId: bpForm.id, enduserId: bpEnduser.id,
257
+ })
258
+ await sdk.api.form_responses.submit_form_response({
259
+ accessCode: bpPrepared.accessCode,
260
+ responses: [
261
+ { fieldId: sbpField.id, fieldTitle: sbpField.title, answer: { type: 'number', value: 128 } },
262
+ { fieldId: dbpField.id, fieldTitle: dbpField.title, answer: { type: 'number', value: 82 } },
263
+ ],
264
+ })
265
+ const bpReadings = await poll_for(() => observations_for(bpEnduser.id), o => o.length >= 2)
266
+ const sbp = bpReadings.find(r => r.measurement?.unit === 'SBP')
267
+ const dbp = bpReadings.find(r => r.measurement?.unit === 'DBP')
268
+ record(
269
+ 'E1: two opted-in questions produce one reading each, in their own units',
270
+ bpReadings.length === 2 && sbp?.measurement?.value === 128 && dbp?.measurement?.value === 82,
271
+ `readings=${JSON.stringify(bpReadings.map(r => r.measurement))} (want SBP 128 and DBP 82)`,
272
+ )
273
+
274
+ /* ===== F: an enduser with vital triggers disabled still gets the reading ===== */
275
+
276
+ const quietEnduser = await sdk.api.endusers.createOne({
277
+ fname: 'Quiet', lname: 'Triggers', vitalTriggersDisabled: true,
278
+ })
279
+ const quietPrepared = await sdk.api.form_responses.prepare_form_response({
280
+ formId: form.id, enduserId: quietEnduser.id,
281
+ })
282
+ await sdk.api.form_responses.submit_form_response({
283
+ accessCode: quietPrepared.accessCode,
284
+ responses: [
285
+ { fieldId: weightField.id, fieldTitle: weightField.title, answer: { type: 'number', value: 210 } },
286
+ ],
287
+ })
288
+ const quietReadings = await poll_for(() => observations_for(quietEnduser.id), o => o.length >= 1)
289
+ record(
290
+ 'F1: the reading is still created and charted for a triggers-disabled enduser',
291
+ quietReadings.length === 1 && quietReadings[0]?.measurement?.value === 210,
292
+ `readings=${JSON.stringify(quietReadings.map(r => r.measurement))} (want one, 210 LB)`,
293
+ )
294
+ record(
295
+ 'F2: but it is marked dontTrigger, so no Vital Update automation fires',
296
+ quietReadings[0]?.dontTrigger === true,
297
+ `dontTrigger=${JSON.stringify(quietReadings[0]?.dontTrigger)} (want true, as smartmeter.ts does)`,
298
+ )
299
+
300
+ /* ===== G: a partial save creates nothing — final submission only ===== */
301
+
302
+ const partialEnduser = await sdk.api.endusers.createOne({ fname: 'Partial', lname: 'Save' })
303
+ const partialPrepared = await sdk.api.form_responses.prepare_form_response({
304
+ formId: form.id, enduserId: partialEnduser.id,
305
+ })
306
+ await sdk.api.form_responses.save_field_response({
307
+ accessCode: partialPrepared.accessCode,
308
+ response: { fieldId: weightField.id, fieldTitle: weightField.title, answer: { type: 'number', value: 175 } },
309
+ })
310
+ // barrier: the draft answer is persisted, so anything the save path triggers has had its chance
311
+ await poll_for(
312
+ () => sdk.api.form_responses.getOne(partialPrepared.response.id),
313
+ r => !!(r.responses || []).length,
314
+ )
315
+ const partialReadings = await poll_for(() => observations_for(partialEnduser.id), o => o.length > 0, 8)
316
+ record(
317
+ 'G1: a partial save creates no reading before the response is submitted',
318
+ partialReadings.length === 0,
319
+ `created ${partialReadings.length}: ${JSON.stringify(partialReadings.map(r => r.measurement))}`
320
+ + ` — readings are a final-submission side effect only`,
321
+ )
322
+
323
+
324
+ /* ===== H: clearing the select (vitalUnit '') is the documented off switch ===== */
325
+
326
+ // The editor writes '' rather than removing the key, so '' must read as off just like an absent
327
+ // vitalUnit. This is the path QA step 9 takes to turn a configured question back off.
328
+ const clearedForm = await sdk.api.forms.createOne({ title: `Cleared unit ${RAND()}` })
329
+ const clearedField = await sdk.api.form_fields.createOne({
330
+ formId: clearedForm.id, title: 'Weight (lbs)', type: 'number',
331
+ options: { vitalUnit: 'LB' },
332
+ previousFields: [{ type: 'root', info: {} }],
333
+ })
334
+ await sdk.api.form_fields.updateOne(clearedField.id, { options: { vitalUnit: '' } })
335
+ const storedCleared = await sdk.api.form_fields.getOne(clearedField.id)
336
+ record(
337
+ "H1: clearing the unit persists as '', it is not dropped back to the old value",
338
+ storedCleared.options?.vitalUnit === '',
339
+ `stored vitalUnit=${JSON.stringify(storedCleared.options?.vitalUnit)} (want '')`
340
+ + ` — stringValidatorOptionalEmptyOkay is what lets the empty round-trip`,
341
+ )
342
+
343
+ const clearedEnduser = await sdk.api.endusers.createOne({ fname: 'Cleared', lname: 'OptIn' })
344
+ const clearedPrepared = await sdk.api.form_responses.prepare_form_response({
345
+ formId: clearedForm.id, enduserId: clearedEnduser.id,
346
+ })
347
+ await sdk.api.form_responses.submit_form_response({
348
+ accessCode: clearedPrepared.accessCode,
349
+ responses: [
350
+ { fieldId: clearedField.id, fieldTitle: clearedField.title, answer: { type: 'number', value: 199 } },
351
+ ],
352
+ })
353
+ await poll_for(
354
+ () => sdk.api.form_responses.getOne(clearedPrepared.response.id),
355
+ r => !!r.submittedAt,
356
+ )
357
+ const clearedReadings = await poll_for(() => observations_for(clearedEnduser.id), o => o.length > 0, 8)
358
+ record(
359
+ 'H2: a question whose unit was cleared creates no reading',
360
+ clearedReadings.length === 0,
361
+ `created ${clearedReadings.length}: ${JSON.stringify(clearedReadings.map(r => r.measurement))}`
362
+ + ` — the field query treats '' as off, same as an absent vitalUnit`,
363
+ )
364
+
365
+ } finally {
366
+ // Teardown BEFORE anything asserts — assert calls process.exit, which would strand this tenant.
367
+ if (tenant) { try { await tenant.teardown() } catch (e) { console.error('teardown failed', e) } }
368
+ }
369
+
370
+ return checks
371
+ }
372
+
373
+ /** Asserting wrapper, for standalone runs and for sequential use in the suite. */
374
+ export const form_vital_reading_tests = async () => {
375
+ assert_checks('Vitals from form submissions', await form_vital_reading_checks())
376
+ }
377
+
378
+ /** Enrolls this suite in the auto-discovered parallel pool — see run_discovered_isolated_suites. */
379
+ export const ISOLATED_SUITE: IsolatedSuite = { name: 'Vitals from form submissions', run: form_vital_reading_checks }
380
+
381
+ // Allow running this test file independently
382
+ if (require.main === module) {
383
+ console.log(`🌐 Using API URL: ${host}`)
384
+ form_vital_reading_tests()
385
+ .then(() => {
386
+ console.log("✅ Vitals from form submissions test suite completed successfully")
387
+ process.exit(0)
388
+ })
389
+ .catch((error) => {
390
+ console.error("❌ Vitals from form submissions test suite failed:", error)
391
+ process.exit(1)
392
+ })
393
+ }