@tellescope/sdk 2.0.0-beta.1 → 2.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +24 -0
- package/env.example +6 -0
- package/lib/cjs/sdk.d.ts +373 -3
- package/lib/cjs/sdk.d.ts.map +1 -1
- package/lib/cjs/sdk.js.map +1 -1
- package/lib/cjs/tests/api_tests/booking_prevent_upcoming_duplicate.test.d.ts +22 -0
- package/lib/cjs/tests/api_tests/booking_prevent_upcoming_duplicate.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/booking_prevent_upcoming_duplicate.test.js +506 -0
- package/lib/cjs/tests/api_tests/booking_prevent_upcoming_duplicate.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/enduser_fields_sync.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/enduser_fields_sync.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/enduser_fields_sync.test.js +3 -1
- package/lib/cjs/tests/api_tests/enduser_fields_sync.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/enduser_observations_auto_review.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/enduser_observations_auto_review.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/enduser_observations_auto_review.test.js +3 -1
- package/lib/cjs/tests/api_tests/enduser_observations_auto_review.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/form_vital_reading.test.d.ts +7 -0
- package/lib/cjs/tests/api_tests/form_vital_reading.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/form_vital_reading.test.js +461 -0
- package/lib/cjs/tests/api_tests/form_vital_reading.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/journey_statistics_cache.test.d.ts +26 -0
- package/lib/cjs/tests/api_tests/journey_statistics_cache.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/journey_statistics_cache.test.js +232 -0
- package/lib/cjs/tests/api_tests/journey_statistics_cache.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/journey_step_manual_count.test.d.ts +34 -0
- package/lib/cjs/tests/api_tests/journey_step_manual_count.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/journey_step_manual_count.test.js +243 -0
- package/lib/cjs/tests/api_tests/journey_step_manual_count.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/load_threads_care_team.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/load_threads_care_team.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/load_threads_care_team.test.js +3 -1
- package/lib/cjs/tests/api_tests/load_threads_care_team.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/organization_signup.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/organization_signup.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/organization_signup.test.js +3 -1
- package/lib/cjs/tests/api_tests/organization_signup.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/phone_embed_leave_message.test.d.ts +7 -0
- package/lib/cjs/tests/api_tests/phone_embed_leave_message.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/phone_embed_leave_message.test.js +571 -0
- package/lib/cjs/tests/api_tests/phone_embed_leave_message.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/programs.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/programs.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/programs.test.js +73 -17
- package/lib/cjs/tests/api_tests/programs.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/public_form_case_insensitive_match.test.d.ts +7 -0
- package/lib/cjs/tests/api_tests/public_form_case_insensitive_match.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/public_form_case_insensitive_match.test.js +733 -0
- package/lib/cjs/tests/api_tests/public_form_case_insensitive_match.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js +3 -1
- package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.js +3 -1
- package/lib/cjs/tests/api_tests/security/appointment-reschedule-ownership.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.js +3 -1
- package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/form-logic-js-execution.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/security/form-logic-js-execution.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/security/form-logic-js-execution.test.js +3 -1
- package/lib/cjs/tests/api_tests/security/form-logic-js-execution.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js +424 -39
- package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/register_business_id_injection.test.js +2 -2
- package/lib/cjs/tests/api_tests/security/register_business_id_injection.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js +3 -1
- package/lib/cjs/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/session_for_join_link.test.d.ts +10 -0
- package/lib/cjs/tests/api_tests/session_for_join_link.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/session_for_join_link.test.js +147 -0
- package/lib/cjs/tests/api_tests/session_for_join_link.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/set_fields_order_templates.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/set_fields_order_templates.test.js +191 -88
- package/lib/cjs/tests/api_tests/set_fields_order_templates.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/stripe_checkout_items.test.d.ts +17 -0
- package/lib/cjs/tests/api_tests/stripe_checkout_items.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/stripe_checkout_items.test.js +219 -0
- package/lib/cjs/tests/api_tests/stripe_checkout_items.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/stripe_multiple_customer_ids.test.d.ts +5 -0
- package/lib/cjs/tests/api_tests/stripe_multiple_customer_ids.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/stripe_multiple_customer_ids.test.js +396 -0
- package/lib/cjs/tests/api_tests/stripe_multiple_customer_ids.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/tickets_bulk_assign_from_queue.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/tickets_bulk_assign_from_queue.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/tickets_bulk_assign_from_queue.test.js +3 -1
- package/lib/cjs/tests/api_tests/tickets_bulk_assign_from_queue.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/totp_mfa.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/totp_mfa.test.js +37 -31
- package/lib/cjs/tests/api_tests/totp_mfa.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/trigger_contains_matching.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/trigger_contains_matching.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/trigger_contains_matching.test.js +3 -1
- package/lib/cjs/tests/api_tests/trigger_contains_matching.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.js +3 -1
- package/lib/cjs/tests/api_tests/user_logs_restore_deleted.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.d.ts +3 -1
- package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.js +3 -1
- package/lib/cjs/tests/api_tests/vital_review_trigger_action.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/vitl_push.test.d.ts +7 -0
- package/lib/cjs/tests/api_tests/vitl_push.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/vitl_push.test.js +341 -0
- package/lib/cjs/tests/api_tests/vitl_push.test.js.map +1 -0
- package/lib/cjs/tests/setup_isolated.d.ts +48 -0
- package/lib/cjs/tests/setup_isolated.d.ts.map +1 -1
- package/lib/cjs/tests/setup_isolated.js +119 -1
- package/lib/cjs/tests/setup_isolated.js.map +1 -1
- package/lib/cjs/tests/tests.d.ts.map +1 -1
- package/lib/cjs/tests/tests.js +374 -292
- package/lib/cjs/tests/tests.js.map +1 -1
- package/lib/cjs/tests/unit_tests/isolated_suite_discovery.test.d.ts +2 -0
- package/lib/cjs/tests/unit_tests/isolated_suite_discovery.test.d.ts.map +1 -0
- package/lib/cjs/tests/unit_tests/isolated_suite_discovery.test.js +201 -0
- package/lib/cjs/tests/unit_tests/isolated_suite_discovery.test.js.map +1 -0
- package/lib/cjs/tests/unit_tests/programs_conditional_logic.test.d.ts +3 -0
- package/lib/cjs/tests/unit_tests/programs_conditional_logic.test.d.ts.map +1 -0
- package/lib/cjs/tests/unit_tests/programs_conditional_logic.test.js +120 -0
- package/lib/cjs/tests/unit_tests/programs_conditional_logic.test.js.map +1 -0
- package/lib/cjs/tests/webhooks_tests.js +44 -34
- package/lib/cjs/tests/webhooks_tests.js.map +1 -1
- package/lib/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/lib/esm/sdk.d.ts +373 -3
- package/lib/esm/sdk.d.ts.map +1 -1
- package/lib/esm/sdk.js.map +1 -1
- package/lib/esm/tests/api_tests/booking_prevent_upcoming_duplicate.test.d.ts +22 -0
- package/lib/esm/tests/api_tests/booking_prevent_upcoming_duplicate.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/booking_prevent_upcoming_duplicate.test.js +501 -0
- package/lib/esm/tests/api_tests/booking_prevent_upcoming_duplicate.test.js.map +1 -0
- package/lib/esm/tests/api_tests/enduser_fields_sync.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/enduser_fields_sync.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/enduser_fields_sync.test.js +2 -0
- package/lib/esm/tests/api_tests/enduser_fields_sync.test.js.map +1 -1
- package/lib/esm/tests/api_tests/enduser_observations_auto_review.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/enduser_observations_auto_review.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/enduser_observations_auto_review.test.js +2 -0
- package/lib/esm/tests/api_tests/enduser_observations_auto_review.test.js.map +1 -1
- package/lib/esm/tests/api_tests/form_vital_reading.test.d.ts +7 -0
- package/lib/esm/tests/api_tests/form_vital_reading.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/form_vital_reading.test.js +456 -0
- package/lib/esm/tests/api_tests/form_vital_reading.test.js.map +1 -0
- package/lib/esm/tests/api_tests/journey_statistics_cache.test.d.ts +26 -0
- package/lib/esm/tests/api_tests/journey_statistics_cache.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/journey_statistics_cache.test.js +227 -0
- package/lib/esm/tests/api_tests/journey_statistics_cache.test.js.map +1 -0
- package/lib/esm/tests/api_tests/journey_step_manual_count.test.d.ts +34 -0
- package/lib/esm/tests/api_tests/journey_step_manual_count.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/journey_step_manual_count.test.js +238 -0
- package/lib/esm/tests/api_tests/journey_step_manual_count.test.js.map +1 -0
- package/lib/esm/tests/api_tests/load_threads_care_team.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/load_threads_care_team.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/load_threads_care_team.test.js +2 -0
- package/lib/esm/tests/api_tests/load_threads_care_team.test.js.map +1 -1
- package/lib/esm/tests/api_tests/organization_signup.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/organization_signup.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/organization_signup.test.js +2 -0
- package/lib/esm/tests/api_tests/organization_signup.test.js.map +1 -1
- package/lib/esm/tests/api_tests/phone_embed_leave_message.test.d.ts +7 -0
- package/lib/esm/tests/api_tests/phone_embed_leave_message.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/phone_embed_leave_message.test.js +566 -0
- package/lib/esm/tests/api_tests/phone_embed_leave_message.test.js.map +1 -0
- package/lib/esm/tests/api_tests/programs.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/programs.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/programs.test.js +72 -16
- package/lib/esm/tests/api_tests/programs.test.js.map +1 -1
- package/lib/esm/tests/api_tests/public_form_case_insensitive_match.test.d.ts +7 -0
- package/lib/esm/tests/api_tests/public_form_case_insensitive_match.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/public_form_case_insensitive_match.test.js +728 -0
- package/lib/esm/tests/api_tests/public_form_case_insensitive_match.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js +2 -0
- package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.js +2 -0
- package/lib/esm/tests/api_tests/security/appointment-reschedule-ownership.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.js +2 -0
- package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/form-logic-js-execution.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/security/form-logic-js-execution.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/security/form-logic-js-execution.test.js +2 -0
- package/lib/esm/tests/api_tests/security/form-logic-js-execution.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js +424 -39
- package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/register_business_id_injection.test.js +2 -2
- package/lib/esm/tests/api_tests/security/register_business_id_injection.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js +2 -0
- package/lib/esm/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.js.map +1 -1
- package/lib/esm/tests/api_tests/session_for_join_link.test.d.ts +10 -0
- package/lib/esm/tests/api_tests/session_for_join_link.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/session_for_join_link.test.js +142 -0
- package/lib/esm/tests/api_tests/session_for_join_link.test.js.map +1 -0
- package/lib/esm/tests/api_tests/set_fields_order_templates.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/set_fields_order_templates.test.js +191 -88
- package/lib/esm/tests/api_tests/set_fields_order_templates.test.js.map +1 -1
- package/lib/esm/tests/api_tests/stripe_checkout_items.test.d.ts +17 -0
- package/lib/esm/tests/api_tests/stripe_checkout_items.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/stripe_checkout_items.test.js +214 -0
- package/lib/esm/tests/api_tests/stripe_checkout_items.test.js.map +1 -0
- package/lib/esm/tests/api_tests/stripe_multiple_customer_ids.test.d.ts +5 -0
- package/lib/esm/tests/api_tests/stripe_multiple_customer_ids.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/stripe_multiple_customer_ids.test.js +388 -0
- package/lib/esm/tests/api_tests/stripe_multiple_customer_ids.test.js.map +1 -0
- package/lib/esm/tests/api_tests/tickets_bulk_assign_from_queue.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/tickets_bulk_assign_from_queue.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/tickets_bulk_assign_from_queue.test.js +2 -0
- package/lib/esm/tests/api_tests/tickets_bulk_assign_from_queue.test.js.map +1 -1
- package/lib/esm/tests/api_tests/totp_mfa.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/totp_mfa.test.js +37 -31
- package/lib/esm/tests/api_tests/totp_mfa.test.js.map +1 -1
- package/lib/esm/tests/api_tests/trigger_contains_matching.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/trigger_contains_matching.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/trigger_contains_matching.test.js +2 -0
- package/lib/esm/tests/api_tests/trigger_contains_matching.test.js.map +1 -1
- package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.js +2 -0
- package/lib/esm/tests/api_tests/user_logs_restore_deleted.test.js.map +1 -1
- package/lib/esm/tests/api_tests/vital_review_trigger_action.test.d.ts +3 -1
- package/lib/esm/tests/api_tests/vital_review_trigger_action.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/vital_review_trigger_action.test.js +2 -0
- package/lib/esm/tests/api_tests/vital_review_trigger_action.test.js.map +1 -1
- package/lib/esm/tests/api_tests/vitl_push.test.d.ts +7 -0
- package/lib/esm/tests/api_tests/vitl_push.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/vitl_push.test.js +333 -0
- package/lib/esm/tests/api_tests/vitl_push.test.js.map +1 -0
- package/lib/esm/tests/setup_isolated.d.ts +48 -0
- package/lib/esm/tests/setup_isolated.d.ts.map +1 -1
- package/lib/esm/tests/setup_isolated.js +114 -0
- package/lib/esm/tests/setup_isolated.js.map +1 -1
- package/lib/esm/tests/tests.d.ts.map +1 -1
- package/lib/esm/tests/tests.js +375 -293
- package/lib/esm/tests/tests.js.map +1 -1
- package/lib/esm/tests/unit_tests/isolated_suite_discovery.test.d.ts +2 -0
- package/lib/esm/tests/unit_tests/isolated_suite_discovery.test.d.ts.map +1 -0
- package/lib/esm/tests/unit_tests/isolated_suite_discovery.test.js +174 -0
- package/lib/esm/tests/unit_tests/isolated_suite_discovery.test.js.map +1 -0
- package/lib/esm/tests/unit_tests/programs_conditional_logic.test.d.ts +3 -0
- package/lib/esm/tests/unit_tests/programs_conditional_logic.test.d.ts.map +1 -0
- package/lib/esm/tests/unit_tests/programs_conditional_logic.test.js +117 -0
- package/lib/esm/tests/unit_tests/programs_conditional_logic.test.js.map +1 -0
- package/lib/esm/tests/webhooks_tests.js +44 -34
- package/lib/esm/tests/webhooks_tests.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +12 -11
- package/src/sdk.ts +3 -2
- package/src/tests/api_tests/booking_prevent_upcoming_duplicate.test.ts +541 -0
- package/src/tests/api_tests/enduser_fields_sync.test.ts +4 -0
- package/src/tests/api_tests/enduser_observations_auto_review.test.ts +4 -0
- package/src/tests/api_tests/form_vital_reading.test.ts +393 -0
- package/src/tests/api_tests/journey_statistics_cache.test.ts +237 -0
- package/src/tests/api_tests/journey_step_manual_count.test.ts +220 -0
- package/src/tests/api_tests/load_threads_care_team.test.ts +4 -0
- package/src/tests/api_tests/organization_signup.test.ts +4 -0
- package/src/tests/api_tests/phone_embed_leave_message.test.ts +541 -0
- package/src/tests/api_tests/programs.test.ts +351 -0
- package/src/tests/api_tests/public_form_case_insensitive_match.test.ts +632 -0
- package/src/tests/api_tests/security/add-to-journey-foreign-enduserid.test.ts +4 -0
- package/src/tests/api_tests/security/appointment-reschedule-ownership.test.ts +4 -0
- package/src/tests/api_tests/security/file-link-session-confinement.test.ts +4 -0
- package/src/tests/api_tests/security/form-logic-js-execution.test.ts +4 -0
- package/src/tests/api_tests/security/formsort_webhook_auth_field_lock.test.ts +225 -0
- package/src/tests/api_tests/security/register_business_id_injection.test.ts +2 -2
- package/src/tests/api_tests/security/rich-text-answer-sanitized-on-write.test.ts +4 -0
- package/src/tests/api_tests/session_for_join_link.test.ts +99 -0
- package/src/tests/api_tests/set_fields_order_templates.test.ts +94 -9
- package/src/tests/api_tests/stripe_checkout_items.test.ts +186 -0
- package/src/tests/api_tests/tickets_bulk_assign_from_queue.test.ts +4 -0
- package/src/tests/api_tests/totp_mfa.test.ts +10 -5
- package/src/tests/api_tests/trigger_contains_matching.test.ts +4 -0
- package/src/tests/api_tests/user_logs_restore_deleted.test.ts +4 -0
- package/src/tests/api_tests/vital_review_trigger_action.test.ts +455 -0
- package/src/tests/api_tests/vitl_push.test.ts +323 -0
- package/src/tests/setup_isolated.ts +140 -0
- package/src/tests/tests.ts +124 -30
- package/src/tests/unit_tests/isolated_suite_discovery.test.ts +178 -0
- package/src/tests/unit_tests/programs_conditional_logic.test.ts +189 -0
- package/src/tests/webhooks_tests.ts +6 -1
- 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.
|
|
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.
|
|
39
|
-
"@tellescope/schema": "2.0.0-beta.
|
|
40
|
-
"@tellescope/testing": "2.0.0-beta.
|
|
41
|
-
"@tellescope/types-client": "2.0.0-beta.
|
|
42
|
-
"@tellescope/types-models": "2.0.0-beta.
|
|
43
|
-
"@tellescope/types-utilities": "2.0.0-beta.
|
|
44
|
-
"@tellescope/utilities": "2.0.0-beta.
|
|
45
|
-
"@tellescope/validation": "2.0.0-beta.
|
|
46
|
-
"axios": "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": "
|
|
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?:
|
|
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?:
|
|
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>;
|
|
@@ -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
|
+
}
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
assert_checks,
|
|
5
5
|
Check,
|
|
6
6
|
setup_isolated_tenant,
|
|
7
|
+
IsolatedSuite,
|
|
7
8
|
} from "../setup_isolated"
|
|
8
9
|
|
|
9
10
|
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
@@ -135,6 +136,9 @@ export const enduser_fields_sync_tests = async () => {
|
|
|
135
136
|
assert_checks('Enduser fields sync schema round-trip', await enduser_fields_sync_checks())
|
|
136
137
|
}
|
|
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
|
+
|
|
138
142
|
// Allow running this test file independently
|
|
139
143
|
if (require.main === module) {
|
|
140
144
|
console.log(`🌐 Using API URL: ${host}`)
|
|
@@ -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}`)
|