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