diff --git a/docs/resources/phone_call_handling_business_hours.md b/docs/resources/phone_call_handling_business_hours.md new file mode 100644 index 0000000..2f23cc5 --- /dev/null +++ b/docs/resources/phone_call_handling_business_hours.md @@ -0,0 +1,331 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "zoom_phone_call_handling_business_hours Resource - zoom" +subcategory: "" +description: |- + Call handling settings allow you to control how your system routes calls during business hours. + For more information, read our Call Handling API guide https://developers.zoom.us/docs/zoom-phone/call-handling/ or Zoom support article Customizing call handling settings https://support.zoom.us/hc/en-us/articles/360059966372-Customizing-call-handling-settings. + API Permissions + The following API permissions are required in order to use this resource. + This resource requires the phone:read:call_handling_setting:admin, phone:write:call_handling_setting:admin, phone:update:call_handling_setting:admin, phone:delete:call_handling_setting:admin. +--- + +# zoom_phone_call_handling_business_hours (Resource) + +Call handling settings allow you to control how your system routes calls during business hours. +For more information, read our [Call Handling API guide](https://developers.zoom.us/docs/zoom-phone/call-handling/) or Zoom support article [Customizing call handling settings](https://support.zoom.us/hc/en-us/articles/360059966372-Customizing-call-handling-settings). + +## API Permissions +The following API permissions are required in order to use this resource. +This resource requires the `phone:read:call_handling_setting:admin`, `phone:write:call_handling_setting:admin`, `phone:update:call_handling_setting:admin`, `phone:delete:call_handling_setting:admin`. + +## Example Usage + +```terraform +# Define only one resource for each extension id. +resource "zoom_phone_call_handling_business_hours" "example" { + extension_id = "wGJDBcnJQC6tV86BbtlXXX" + + custom_hours = { + type = 2 # Custom hours + allow_members_to_reset = true + settings = [ + { + weekday = 1 # Sunday + type = 0 # Disabled + }, + { + weekday = 2 # Monday + type = 2 # Customized hours + from = "09:00" + to = "22:00" + }, + { + weekday = 3 # Saturday + type = 2 # Customized hours + from = "09:00" + to = "23:59" + }, + { + weekday = 4 # Saturday + type = 1 # 24hours + }, + { + weekday = 5 # Saturday + type = 1 # 24hours + }, + { + weekday = 3 # Saturday + type = 2 # Customized hours + from = "00:00" + to = "22:00" + }, + { + weekday = 7 # Saturday + type = 0 # Disabled + }, + ] + } + + call_handling = { + call_not_answer_action = 7 + forward_to_extension_id = "XXX" + busy_on_another_call_action = 21 + busy_forward_to_extension_id = "XXX" + allow_callers_check_voicemail = true + allow_members_to_reset = false + audio_while_connecting_id = "XXX" + call_distribution = { + handle_multiple_calls = true + ring_duration = 30 + ring_mode = "simultaneous" + skip_offline_device_phone_number = true + } + busy_require_press_1_before_connecting = true + un_answered_require_press_1_before_connecting = true + overflow_play_callee_voicemail_greeting = true + play_callee_voicemail_greeting = true + busy_play_callee_voicemail_greeting = true + phone_number = "+1234567890" + phone_number_description = "XXX" + busy_phone_number = "+1234567890" + busy_phone_number_description = "" + connect_to_operator = true + greeting_prompt_id = "0" # default + max_call_in_queue = 20 + max_wait_time = 30 + music_on_hold_id = "0" # default + operator_extension_id = "XXX" + receive_call = true + ring_mode = "simultaneous" + voicemail_greeting_id = "" + wrap_up_time = 60 + } + + # some extension can use call forwarding such as user + call_forwarding = { + require_press_1_before_connecting = true + enable_zoom_mobile_apps = true # Zoom Mobile Apps + enable_zoom_desktop_apps = true # Zoom Desktop Apps + enable_zoom_phone_appliance_apps = true # Zoom Phone Appliance Apps + settings = [ + { + "description" = "external person", + "enable" = false, + "phone_number" = "+1234567890" + }, + ] + } + + lifecycle { + ignore_changes = [ + # zoom api doesn't return some fields, so please ignore them + call_handling.receive_call, + ] + } +} +``` + + +## Schema + +### Required + +- `call_handling` (Attributes) The call handling settings. + - NOTE: some fields doesn't return from zoom api, so please ignore_changes for these fields. (see [below for nested schema](#nestedatt--call_handling)) +- `custom_hours` (Attributes) The custom hours settings. (see [below for nested schema](#nestedatt--custom_hours)) +- `extension_id` (String) Extension ID. + +### Optional + +- `call_forwarding` (Attributes) The call forwarding settings. (see [below for nested schema](#nestedatt--call_forwarding)) + + +### Nested Schema for `call_handling` + +Optional: + +- `allow_callers_check_voicemail` (Boolean) Whether to allow the callers to check voicemails over a phone. It's required only when the call_not_answer_action setting is set to 1 (Forward to a voicemail). +- `allow_members_to_reset` (Boolean) This field allows queue members to set their own business hours. This field allows queue members' business Hours to override the default hours of the call queue. + - Only required for Call Queue custom_hours sub-setting. +- `audio_while_connecting_id` (String) The audio while connecting the prompt ID. This option can select the audio played for the inbound callers when they are waiting to be routed to the next available call queue member. + - Options: empty char - default and 0 - disable + - This is only required for the Call Queue call_handling sub-setting. +- `busy_forward_to_extension_id` (String) The forwarding extension ID that's required only when busy_on_another_call_action setting is set to: + - 2 - Forward to the user. + - 4 - Forward to the common area. + - 6 - Forward to the auto receptionist. + - 7 - Forward to a call queue. + - 8 - Forward to a shared line group. + - 9 - forward to an external contact. +- `busy_on_another_call_action` (Number) The action to take when the user is busy on another call: + - 1 — Forward to a voicemail. + - 2 — Forward to the user. + - 4 — Forward to the common area. + - 6 — Forward to the auto receptionist. + - 7 — Forward to a call queue. + - 8 — Forward to a shared line group. + - 9 — Forward to an external contact. + - 10 - Forward to a phone number. + - 12 — Play a message, then disconnect. + - 21 — Call waiting. + - 22 — Play a busy signal. +- `busy_phone_number` (String) The extension's phone number or forward to an external number in [E.164](https://en.wikipedia.org/wiki/E.164) format format. It sets when `busy_on_another_call_action` action is set to `10` - Forward to an external number. +- `busy_phone_number_description` (String) This field forwards to an external number description (optional). It sets when `busy_on_another_call_action` action is set to `10` - Forward to an external number. +- `busy_play_callee_voicemail_greeting` (Boolean) Whether to play callee's voicemail greeting when the caller reaches the end of the forwarding sequence. It displays when busy_on_another_call_action action is set to + - 2 - Forward to the user + - 4 - Forward to the common area + - 6 - Forward to the auto receptionist + - 7 - Forward to a call queue + - 8 - Forward to a shared line group + - 9 - Forward to an external contact + - 10 - Forward to an external number. +- `busy_require_press1_before_connecting` (Boolean) When one is busy on another call, the receiver needs to press 1 before connecting the call for it to be forwarded to an external contact or a number. This option ensures that forwarded calls won't reach the voicemail box for the external contact or a number. +- `call_distribution` (Attributes) This option distributes incoming calls. + - If Sequential or Rotating is selected, calls will ring for a specific time before trying the next available queue member. + - This is only required for the call_handling sub-setting. (see [below for nested schema](#nestedatt--call_handling--call_distribution)) +- `call_not_answer_action` (Number) The action to take when a call is not answered: + - 1 — Forward to a voicemail. + - 2 — Forward to the user. + - 4 — Forward to the common area. + - 6 — Forward to the auto receptionist. + - 7 — Forward to a call queue. + - 8 — Forward to a shared line group. + - 9 — Forward to an external contact. + - 10 - Forward to a phone number. + - 11 — Disconnect. + - 12 — Play a message, then disconnect. + - 13 - Forward to a message. + - 14 - Forward to an interactive voice response (IVR). +- `connect_to_operator` (Boolean) Whether to allow callers to reach an operator. It's required only when the `call_not_answer_action` or `busy_on_another_call_action` is set to 1 (Forward to a voicemail). +- `forward_to_extension_id` (String) The forwarding extension ID that's required only when call_not_answer_action setting is set to: + - 2 - Forward to the user. + - 4 - Forward to the common area. + - 6 - Forward to the auto receptionist. + - 7 - Forward to a call queue. + - 8 - Forward to a shared line group. + - 9 - forward to an external contact. +- `greeting_prompt_id` (String) The greeting audio prompt ID. + - Options: empty char - default and 0 - disable + - This is only required for the Call Queue or Auto Receptionist call_handling sub-setting. +- `max_call_in_queue` (Number) The maximum number of calls in queue. Specify the maximum number of callers to place in the queue. When this number is exceeded, callers will be routed based on the overflow option. Up to 60. + - It's required for the Call Queue call_handling sub-setting. +- `max_wait_time` (Number) The maximum wait time, in seconds. + - for simultaneous ring mode or the ring duration for each device for sequential ring mode: 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60. + - Specify how long a caller will wait in the queue. Once the wait time is exceeded, the caller will be rerouted based on the overflow option for Call Queue: 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 120, 180, 240, 300, 600, 900, 1200, 1500, 1800. + - This is only required for the call_handling sub-setting. +- `music_on_hold_id` (String) The music on hold prompt ID. This field is an option to choose music for inbound callers when they're placed on hold by a call queue member. + - Options: empty char - default and 0 - disable + - Only required for the Call Queue call_handling sub-setting. +- `operator_extension_id` (String) The extension ID of the operator to whom the call is being forwarded. It's required only when `call_not_answer_action` is set to `1` (Forward to a voicemail) and `connect_to_operator` is set to true. +- `overflow_play_callee_voicemail_greeting` (Boolean) Whether to play the callee's voicemail greeting when the caller reaches the end of the forwarding sequence. It displays when call_not_answer_action is set to: + - 2 - Forward to the user + - 4 - Forward to the common area + - 6 - Forward to the auto receptionist + - 7 - Forward to a call queue + - 8 - Forward to a shared line group + - 9 - Forward to an external contact + - 10 - Forward to an external number. +- `phone_number` (String) The extension's phone number or forward to an external number in [E.164](https://en.wikipedia.org/wiki/E.164) format format. It's required when `call_not_answer_action` action is set to `10` - Forward to an external number. +- `phone_number_description` (String) (Optional) This field forwards to an external number description. Add this field when `call_not_answer_action` is set to `10` - Forward to an external number. +- `play_callee_voicemail_greeting` (Boolean) Whether to play callee's voicemail greeting when the caller reaches the end of forwarding sequence. It displays when `busy_on_another_call_action` action or `call_not_answer_action` is set to `1` - Forward to a voicemail. +- `receive_call` (Boolean) This field receives calls while on a call. When enabled, call queue members can receive new incoming calls notification even on the call. + - It's required for the Call Queue call handling sub-setting. +- `ring_mode` (String) The call handling ring mode: + - simultaneous + - sequential. For user business hours, ring_mode needs to be set with max_wait_time. +- `unanswered_require_press1_before_connecting` (Boolean) When a call is unanswered, press 1 before connecting the call to forward to an external contact or a number. This option ensures that forwarded calls won't reach the voicemail box for the external contact or a number. +- `voicemail_greeting_id` (String) The voicemail greeting prompt ID. It's required when `call_not_answer_action` or `busy_on_another_call_action` is set to `1` (Forward to a voicemail). Required only for `call_handling` subsettings of `Call Queue`, `Auto Receptionist` or `User`. +- `wrap_up_time` (Number) The wrap up time in seconds. Specify the duration before the next queue call is routed to a member in call queue: + - This is only required for the call_handling sub-setting. + - Allowed: 10┃15┃20┃25┃30┃35┃40┃45┃50┃55┃60┃120┃180┃240┃300 + + +### Nested Schema for `call_handling.call_distribution` + +Optional: + +- `handle_multiple_calls` (Boolean) The maximum number of calls that can be handled simultaneously is less than half of the total amount of available call queue members. Note that the first incoming call may not be answered first. + - Required except for simultaneous ring mode. +- `ring_duration` (Number) The ringing duration for each member: + - Required except for simultaneous ring mode. + - Allowed: 10┃15┃20┃25┃30┃35┃40┃45┃50┃55┃60 +- `ring_mode` (String) The call distribution ring mode: + - Allowed: simultaneous┃sequential┃rotating┃longest_idle +- `skip_offline_device_phone_number` (Boolean) Devices with Zoom app or client not launched and mobile phone with screen locked will be skipped. Phone numbers added to user's call handling settings will be skipped. + - Required except for simultaneous ring mode. + + + + +### Nested Schema for `custom_hours` + +Required: + +- `type` (Number) The type of custom hours: + - 1 — 24 hours, 7 days a week. + - 2 — Custom hours. + +Optional: + +- `allow_members_to_reset` (Boolean) This field allows queue members to set their own business hours. This field allows queue members' business hours to override the default hours of the call queue. + - Only required for Call Queue custom_hours sub-setting. +- `settings` (Attributes Set) The custom hours settings. It's only required for the custom_hours sub-setting. (see [below for nested schema](#nestedatt--custom_hours--settings)) + + +### Nested Schema for `custom_hours.settings` + +Required: + +- `type` (Number) The type of custom hours: + - 0 — Disabled. + - 1 — 24 hours. + - 2 — Customized hours. +- `weekday` (Number) The day of the week: + - 1 — Sunday + - 2 — Monday + - 3 — Tuesday + - 4 — Wednesday + - 5 — Thursday + - 6 — Friday + - 7 — Saturday + +Optional: + +- `from` (String) The custom hours start time HH:mm format. +- `to` (String) The custom hours end time in HH:mm format. + + + + +### Nested Schema for `call_forwarding` + +Optional: + +- `enable_zoom_desktop_apps` (Boolean) Whether to enable Zoom Desktop Apps call forwarding +- `enable_zoom_mobile_apps` (Boolean) Whether to enable Zoom Mobile Apps call forwarding +- `enable_zoom_phone_appliance_apps` (Boolean) Whether to enable Zoom Phone Appliance Apps call forwarding +- `require_press_1_before_connecting` (Boolean) When a call is forwarded to a personal phone number, whether the user must press "1" before the call connects. Enable this option to ensure missed calls do not reach to your personal voicemail. It's required for the `call_forwarding` sub-setting. Press 1 is always enabled and is required for callQueue type extension calls. +- `settings` (Attributes Set) The call forwarding settings. It's only required for the `call_forwarding` sub-setting. (see [below for nested schema](#nestedatt--call_forwarding--settings)) + + +### Nested Schema for `call_forwarding.settings` + +Optional: + +- `description` (String) The external phone number's description. +- `enable` (Boolean) Whether to receive a call. +- `phone_number` (String) The external phone number in [E.164](https://en.wikipedia.org/wiki/E.164) format format. + +Read-Only: + +- `id` (String) The call forwarding's ID. + +## Import + +Import is supported using the following syntax: + +```shell +# ${extension_id} +terraform import zoom_phone_call_handling_business_hours.example t6wyhAZRQXXX_Rv3jj3XXX +``` diff --git a/docs/resources/phone_call_handling_closed_hours.md b/docs/resources/phone_call_handling_closed_hours.md new file mode 100644 index 0000000..384068e --- /dev/null +++ b/docs/resources/phone_call_handling_closed_hours.md @@ -0,0 +1,199 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "zoom_phone_call_handling_closed_hours Resource - zoom" +subcategory: "" +description: |- + Call handling settings allow you to control how your system routes calls during closed hours. + For more information, read our Call Handling API guide https://developers.zoom.us/docs/zoom-phone/call-handling/ or Zoom support article Customizing call handling settings https://support.zoom.us/hc/en-us/articles/360059966372-Customizing-call-handling-settings. + NOTE + This resource is depends on zoom_phone_call_handling_business_hours. Please set business hours type = 2 (Custom hours). + API Permissions + The following API permissions are required in order to use this resource. + This resource requires the phone:read:call_handling_setting:admin, phone:write:call_handling_setting:admin, phone:update:call_handling_setting:admin, phone:delete:call_handling_setting:admin. +--- + +# zoom_phone_call_handling_closed_hours (Resource) + +Call handling settings allow you to control how your system routes calls during closed hours. +For more information, read our [Call Handling API guide](https://developers.zoom.us/docs/zoom-phone/call-handling/) or Zoom support article [Customizing call handling settings](https://support.zoom.us/hc/en-us/articles/360059966372-Customizing-call-handling-settings). + +## NOTE +This resource is depends on `zoom_phone_call_handling_business_hours`. Please set business hours type = 2 (Custom hours). + +## API Permissions +The following API permissions are required in order to use this resource. +This resource requires the `phone:read:call_handling_setting:admin`, `phone:write:call_handling_setting:admin`, `phone:update:call_handling_setting:admin`, `phone:delete:call_handling_setting:admin`. + +## Example Usage + +```terraform +# Define only one resource for each extension id. +resource "zoom_phone_call_handling_closed_hours" "example" { + extension_id = "wGJDBcnJQC6tV86BbtlXXX" + + call_handling = { + call_not_answer_action = 1 + busy_on_another_call_action = 21 + connect_to_operator = false + allow_callers_check_voicemail = false + voicemail_greeting_id = "" # default + ring_mode = "simultaneous" + max_wait_time = 30 + } + + # some extension can use call forwarding such as user + call_forwarding = { + require_press_1_before_connecting = true + enable_zoom_mobile_apps = true # Zoom Mobile Apps + enable_zoom_desktop_apps = true # Zoom Desktop Apps + enable_zoom_phone_appliance_apps = true # Zoom Phone Appliance Apps + settings = [ + { + "description" : "external person", + "enable" : true, + "phone_number" : "+1234567890" + }, + ] + } + + # closed hours can handle after setting business_hours.custom_hours.type = 2 + depends_on = [ + zoom_phone_call_handling_business_hours.example + ] + + lifecycle { + ignore_changes = [ + # zoom api doesn't return some fields, so please ignore them + call_handling.receive_call, + ] + } +} +``` + + +## Schema + +### Required + +- `call_handling` (Attributes) The call handling settings. + - NOTE: some fields doesn't return from zoom api, so please ignore_changes for these fields. (see [below for nested schema](#nestedatt--call_handling)) +- `extension_id` (String) Extension ID. + +### Optional + +- `call_forwarding` (Attributes) The call forwarding settings. (see [below for nested schema](#nestedatt--call_forwarding)) + + +### Nested Schema for `call_handling` + +Optional: + +- `allow_callers_check_voicemail` (Boolean) Whether to allow the callers to check voicemails over a phone. It's required only when the call_not_answer_action setting is set to 1 (Forward to a voicemail). +- `busy_forward_to_extension_id` (String) The forwarding extension ID that's required only when busy_on_another_call_action setting is set to: + - 2 - Forward to the user. + - 4 - Forward to the common area. + - 6 - Forward to the auto receptionist. + - 7 - Forward to a call queue. + - 8 - Forward to a shared line group. + - 9 - forward to an external contact. +- `busy_on_another_call_action` (Number) The action to take when the user is busy on another call: + - 1 — Forward to a voicemail. + - 2 — Forward to the user. + - 4 — Forward to the common area. + - 6 — Forward to the auto receptionist. + - 7 — Forward to a call queue. + - 8 — Forward to a shared line group. + - 9 — Forward to an external contact. + - 10 - Forward to a phone number. + - 12 — Play a message, then disconnect. + - 21 — Call waiting. + - 22 — Play a busy signal. +- `busy_phone_number` (String) The extension's phone number or forward to an external number in [E.164](https://en.wikipedia.org/wiki/E.164) format format. It sets when `busy_on_another_call_action` action is set to `10` - Forward to an external number. +- `busy_phone_number_description` (String) This field forwards to an external number description (optional). It sets when `busy_on_another_call_action` action is set to `10` - Forward to an external number. +- `busy_play_callee_voicemail_greeting` (Boolean) Whether to play callee's voicemail greeting when the caller reaches the end of the forwarding sequence. It displays when busy_on_another_call_action action is set to + - 2 - Forward to the user + - 4 - Forward to the common area + - 6 - Forward to the auto receptionist + - 7 - Forward to a call queue + - 8 - Forward to a shared line group + - 9 - Forward to an external contact + - 10 - Forward to an external number. +- `busy_require_press1_before_connecting` (Boolean) When one is busy on another call, the receiver needs to press 1 before connecting the call for it to be forwarded to an external contact or a number. This option ensures that forwarded calls won't reach the voicemail box for the external contact or a number. +- `call_not_answer_action` (Number) The action to take when a call is not answered: + - 1 — Forward to a voicemail. + - 2 — Forward to the user. + - 4 — Forward to the common area. + - 6 — Forward to the auto receptionist. + - 7 — Forward to a call queue. + - 8 — Forward to a shared line group. + - 9 — Forward to an external contact. + - 10 - Forward to a phone number. + - 11 — Disconnect. + - 12 — Play a message, then disconnect. + - 13 - Forward to a message. + - 14 - Forward to an interactive voice response (IVR). +- `connect_to_operator` (Boolean) Whether to allow callers to reach an operator. It's required only when the `call_not_answer_action` or `busy_on_another_call_action` is set to 1 (Forward to a voicemail). +- `forward_to_extension_id` (String) The forwarding extension ID that's required only when call_not_answer_action setting is set to: + - 2 - Forward to the user. + - 4 - Forward to the common area. + - 6 - Forward to the auto receptionist. + - 7 - Forward to a call queue. + - 8 - Forward to a shared line group. + - 9 - forward to an external contact. +- `greeting_prompt_id` (String) The greeting audio prompt ID. + - Options: empty char - default and 0 - disable + - This is only required for the Call Queue or Auto Receptionist call_handling sub-setting. +- `max_wait_time` (Number) The maximum wait time, in seconds. + - for simultaneous ring mode or the ring duration for each device for sequential ring mode: 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60. + - Specify how long a caller will wait in the queue. Once the wait time is exceeded, the caller will be rerouted based on the overflow option for Call Queue: 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 120, 180, 240, 300, 600, 900, 1200, 1500, 1800. + - This is only required for the call_handling sub-setting. +- `operator_extension_id` (String) The extension ID of the operator to whom the call is being forwarded. It's required only when `call_not_answer_action` is set to `1` (Forward to a voicemail) and `connect_to_operator` is set to true. +- `overflow_play_callee_voicemail_greeting` (Boolean) Whether to play the callee's voicemail greeting when the caller reaches the end of the forwarding sequence. It displays when call_not_answer_action is set to: + - 2 - Forward to the user + - 4 - Forward to the common area + - 6 - Forward to the auto receptionist + - 7 - Forward to a call queue + - 8 - Forward to a shared line group + - 9 - Forward to an external contact + - 10 - Forward to an external number. +- `phone_number` (String) The extension's phone number or forward to an external number in [E.164](https://en.wikipedia.org/wiki/E.164) format format. It's required when `call_not_answer_action` action is set to `10` - Forward to an external number. +- `phone_number_description` (String) (Optional) This field forwards to an external number description. Add this field when `call_not_answer_action` is set to `10` - Forward to an external number. +- `play_callee_voicemail_greeting` (Boolean) Whether to play callee's voicemail greeting when the caller reaches the end of forwarding sequence. It displays when `busy_on_another_call_action` action or `call_not_answer_action` is set to `1` - Forward to a voicemail. +- `ring_mode` (String) The call handling ring mode: + - simultaneous + - sequential. For user closed hours, ring_mode needs to be set with max_wait_time. +- `unanswered_require_press1_before_connecting` (Boolean) When a call is unanswered, press 1 before connecting the call to forward to an external contact or a number. This option ensures that forwarded calls won't reach the voicemail box for the external contact or a number. + + + +### Nested Schema for `call_forwarding` + +Optional: + +- `enable_zoom_desktop_apps` (Boolean) Whether to enable Zoom Desktop Apps call forwarding +- `enable_zoom_mobile_apps` (Boolean) Whether to enable Zoom Mobile Apps call forwarding +- `enable_zoom_phone_appliance_apps` (Boolean) Whether to enable Zoom Phone Appliance Apps call forwarding +- `require_press_1_before_connecting` (Boolean) When a call is forwarded to a personal phone number, whether the user must press "1" before the call connects. Enable this option to ensure missed calls do not reach to your personal voicemail. It's required for the `call_forwarding` sub-setting. Press 1 is always enabled and is required for callQueue type extension calls. +- `settings` (Attributes Set) The call forwarding settings. It's only required for the `call_forwarding` sub-setting. (see [below for nested schema](#nestedatt--call_forwarding--settings)) + + +### Nested Schema for `call_forwarding.settings` + +Optional: + +- `description` (String) The external phone number's description. +- `enable` (Boolean) Whether to receive a call. +- `phone_number` (String) The external phone number in [E.164](https://en.wikipedia.org/wiki/E.164) format format. + +Read-Only: + +- `id` (String) The call forwarding's ID. + +## Import + +Import is supported using the following syntax: + +```shell +# ${extension_id} +terraform import zoom_phone_call_handling_closed_hours.example t6wyhAZRQXXX_Rv3jj3XXX +``` diff --git a/docs/resources/phone_call_handling_holiday_hours.md b/docs/resources/phone_call_handling_holiday_hours.md new file mode 100644 index 0000000..75ab2d4 --- /dev/null +++ b/docs/resources/phone_call_handling_holiday_hours.md @@ -0,0 +1,173 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "zoom_phone_call_handling_holiday_hours Resource - zoom" +subcategory: "" +description: |- + Call handling settings allow you to control how your system routes calls during holiday hours. + For more information, read our Call Handling API guide https://developers.zoom.us/docs/zoom-phone/call-handling/ or Zoom support article Customizing call handling settings https://support.zoom.us/hc/en-us/articles/360059966372-Customizing-call-handling-settings. + API Permissions + The following API permissions are required in order to use this resource. + This resource requires the phone:read:call_handling_setting:admin, phone:write:call_handling_setting:admin, phone:update:call_handling_setting:admin, phone:delete:call_handling_setting:admin. +--- + +# zoom_phone_call_handling_holiday_hours (Resource) + +Call handling settings allow you to control how your system routes calls during holiday hours. +For more information, read our [Call Handling API guide](https://developers.zoom.us/docs/zoom-phone/call-handling/) or Zoom support article [Customizing call handling settings](https://support.zoom.us/hc/en-us/articles/360059966372-Customizing-call-handling-settings). + +## API Permissions +The following API permissions are required in order to use this resource. +This resource requires the `phone:read:call_handling_setting:admin`, `phone:write:call_handling_setting:admin`, `phone:update:call_handling_setting:admin`, `phone:delete:call_handling_setting:admin`. + +## Example Usage + +```terraform +# Define multiple resources for each extension id. +resource "zoom_phone_call_handling_holiday_hours" "example" { + extension_id = "wGJDBcnJQC6tV86BbtlXXX" + + holiday = { + name = "temporary" + from = "2024-08-24T09:00:00Z" + to = "2024-08-26T17:00:00+09:00" + } + + call_handling = { + call_not_answer_action = 1 + connect_to_operator = false + allow_callers_check_voicemail = false + } + + # some extension can use call forwarding such as user + call_forwarding = { + require_press_1_before_connecting = true + enable_zoom_mobile_apps = true # Zoom Mobile Apps + enable_zoom_desktop_apps = true # Zoom Desktop Apps + enable_zoom_phone_appliance_apps = true # Zoom Phone Appliance Apps + settings = [ + { + "description" : "external person", + "enable" : true, + "phone_number" : "+1234567890" + }, + ] + } + + lifecycle { + ignore_changes = [ + # zoom api doesn't return some fields, so please ignore them + call_handling.receive_call, + ] + } +} +``` + + +## Schema + +### Required + +- `call_handling` (Attributes) The call handling settings. + - NOTE: some fields doesn't return from zoom api, so please ignore_changes for these fields. (see [below for nested schema](#nestedatt--call_handling)) +- `extension_id` (String) Extension ID. +- `holiday` (Attributes) Holiday settings. (see [below for nested schema](#nestedatt--holiday)) + +### Optional + +- `call_forwarding` (Attributes) The call forwarding settings. (see [below for nested schema](#nestedatt--call_forwarding)) + +### Read-Only + +- `holiday_id` (String) The holiday's ID. It's required for the `holiday` sub-setting. + + +### Nested Schema for `call_handling` + +Optional: + +- `allow_callers_check_voicemail` (Boolean) Whether to allow the callers to check voicemails over a phone. It's required only when the call_not_answer_action setting is set to 1 (Forward to a voicemail). +- `call_not_answer_action` (Number) The action to take when a call is not answered: + - 1 — Forward to a voicemail. + - 2 — Forward to the user. + - 4 — Forward to the common area. + - 6 — Forward to the auto receptionist. + - 7 — Forward to a call queue. + - 8 — Forward to a shared line group. + - 9 — Forward to an external contact. + - 10 - Forward to a phone number. + - 11 — Disconnect. + - 12 — Play a message, then disconnect. + - 13 - Forward to a message. + - 14 - Forward to an interactive voice response (IVR). +- `connect_to_operator` (Boolean) Whether to allow callers to reach an operator. It's required only when the `call_not_answer_action` or `busy_on_another_call_action` is set to 1 (Forward to a voicemail). +- `forward_to_extension_id` (String) The forwarding extension ID that's required only when call_not_answer_action setting is set to: + - 2 - Forward to the user. + - 4 - Forward to the common area. + - 6 - Forward to the auto receptionist. + - 7 - Forward to a call queue. + - 8 - Forward to a shared line group. + - 9 - forward to an external contact. +- `max_wait_time` (Number) The maximum wait time, in seconds. + - for simultaneous ring mode or the ring duration for each device for sequential ring mode: 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60. + - Specify how long a caller will wait in the queue. Once the wait time is exceeded, the caller will be rerouted based on the overflow option for Call Queue: 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 120, 180, 240, 300, 600, 900, 1200, 1500, 1800. + - This is only required for the call_handling sub-setting. +- `operator_extension_id` (String) The extension ID of the operator to whom the call is being forwarded. It's required only when `call_not_answer_action` is set to `1` (Forward to a voicemail) and `connect_to_operator` is set to true. +- `overflow_play_callee_voicemail_greeting` (Boolean) Whether to play the callee's voicemail greeting when the caller reaches the end of the forwarding sequence. It displays when call_not_answer_action is set to: + - 2 - Forward to the user + - 4 - Forward to the common area + - 6 - Forward to the auto receptionist + - 7 - Forward to a call queue + - 8 - Forward to a shared line group + - 9 - Forward to an external contact + - 10 - Forward to an external number. +- `phone_number` (String) The extension's phone number or forward to an external number in [E.164](https://en.wikipedia.org/wiki/E.164) format format. It's required when `call_not_answer_action` action is set to `10` - Forward to an external number. +- `phone_number_description` (String) (Optional) This field forwards to an external number description. Add this field when `call_not_answer_action` is set to `10` - Forward to an external number. +- `play_callee_voicemail_greeting` (Boolean) Whether to play callee's voicemail greeting when the caller reaches the end of forwarding sequence. It displays when `busy_on_another_call_action` action or `call_not_answer_action` is set to `1` - Forward to a voicemail. +- `ring_mode` (String) The call handling ring mode: + - simultaneous + - sequential. For user holiday hours, ring_mode needs to be set with max_wait_time. +- `unanswered_require_press1_before_connecting` (Boolean) When a call is unanswered, press 1 before connecting the call to forward to an external contact or a number. This option ensures that forwarded calls won't reach the voicemail box for the external contact or a number. + + + +### Nested Schema for `holiday` + +Required: + +- `from` (String) The holiday's start date and time in `yyyy-MM-dd'T'HH:mm:ss'Z'` format. It's required for the `holiday` sub-setting. +- `name` (String) The name of the holiday. It's required for the `holiday` sub-setting. +- `to` (String) The holiday's end date and time in `yyyy-MM-dd'T'HH:mm:ss'Z'` format. It's required for the `holiday` sub-setting. + + + +### Nested Schema for `call_forwarding` + +Optional: + +- `enable_zoom_desktop_apps` (Boolean) Whether to enable Zoom Desktop Apps call forwarding +- `enable_zoom_mobile_apps` (Boolean) Whether to enable Zoom Mobile Apps call forwarding +- `enable_zoom_phone_appliance_apps` (Boolean) Whether to enable Zoom Phone Appliance Apps call forwarding +- `require_press_1_before_connecting` (Boolean) When a call is forwarded to a personal phone number, whether the user must press "1" before the call connects. Enable this option to ensure missed calls do not reach to your personal voicemail. It's required for the `call_forwarding` sub-setting. Press 1 is always enabled and is required for callQueue type extension calls. +- `settings` (Attributes Set) The call forwarding settings. It's only required for the `call_forwarding` sub-setting. (see [below for nested schema](#nestedatt--call_forwarding--settings)) + + +### Nested Schema for `call_forwarding.settings` + +Optional: + +- `description` (String) The external phone number's description. +- `enable` (Boolean) Whether to receive a call. +- `phone_number` (String) The external phone number in [E.164](https://en.wikipedia.org/wiki/E.164) format format. + +Read-Only: + +- `id` (String) The call forwarding's ID. + +## Import + +Import is supported using the following syntax: + +```shell +# ${extension_id/holiday_id} +terraform import zoom_phone_call_handling_holiday_hours.example t6wyhAZRQXXX_Rv3jj3XXX/gkqphABALXXX_Al0g13XXX +``` diff --git a/examples/resources/zoom_phone_call_handling_business_hours/import.sh b/examples/resources/zoom_phone_call_handling_business_hours/import.sh new file mode 100644 index 0000000..bd0e3d4 --- /dev/null +++ b/examples/resources/zoom_phone_call_handling_business_hours/import.sh @@ -0,0 +1,2 @@ +# ${extension_id} +terraform import zoom_phone_call_handling_business_hours.example t6wyhAZRQXXX_Rv3jj3XXX diff --git a/examples/resources/zoom_phone_call_handling_business_hours/provider.tf b/examples/resources/zoom_phone_call_handling_business_hours/provider.tf new file mode 100644 index 0000000..23b61ca --- /dev/null +++ b/examples/resources/zoom_phone_call_handling_business_hours/provider.tf @@ -0,0 +1,22 @@ +terraform { + required_providers { + zoom = { + source = "folio-sec/zoom" + version = "~> 0.0.0" + } + } +} + +provider "zoom" { + account_id = var.zoom_account_id + client_id = var.zoom_client_id + client_secret = var.zoom_client_secret +} + +variable "zoom_account_id" {} + +variable "zoom_client_id" {} + +variable "zoom_client_secret" { + sensitive = true +} diff --git a/examples/resources/zoom_phone_call_handling_business_hours/resource.tf b/examples/resources/zoom_phone_call_handling_business_hours/resource.tf new file mode 100644 index 0000000..6834135 --- /dev/null +++ b/examples/resources/zoom_phone_call_handling_business_hours/resource.tf @@ -0,0 +1,102 @@ +# Define only one resource for each extension id. +resource "zoom_phone_call_handling_business_hours" "example" { + extension_id = "wGJDBcnJQC6tV86BbtlXXX" + + custom_hours = { + type = 2 # Custom hours + allow_members_to_reset = true + settings = [ + { + weekday = 1 # Sunday + type = 0 # Disabled + }, + { + weekday = 2 # Monday + type = 2 # Customized hours + from = "09:00" + to = "22:00" + }, + { + weekday = 3 # Saturday + type = 2 # Customized hours + from = "09:00" + to = "23:59" + }, + { + weekday = 4 # Saturday + type = 1 # 24hours + }, + { + weekday = 5 # Saturday + type = 1 # 24hours + }, + { + weekday = 3 # Saturday + type = 2 # Customized hours + from = "00:00" + to = "22:00" + }, + { + weekday = 7 # Saturday + type = 0 # Disabled + }, + ] + } + + call_handling = { + call_not_answer_action = 7 + forward_to_extension_id = "XXX" + busy_on_another_call_action = 21 + busy_forward_to_extension_id = "XXX" + allow_callers_check_voicemail = true + allow_members_to_reset = false + audio_while_connecting_id = "XXX" + call_distribution = { + handle_multiple_calls = true + ring_duration = 30 + ring_mode = "simultaneous" + skip_offline_device_phone_number = true + } + busy_require_press_1_before_connecting = true + un_answered_require_press_1_before_connecting = true + overflow_play_callee_voicemail_greeting = true + play_callee_voicemail_greeting = true + busy_play_callee_voicemail_greeting = true + phone_number = "+1234567890" + phone_number_description = "XXX" + busy_phone_number = "+1234567890" + busy_phone_number_description = "" + connect_to_operator = true + greeting_prompt_id = "0" # default + max_call_in_queue = 20 + max_wait_time = 30 + music_on_hold_id = "0" # default + operator_extension_id = "XXX" + receive_call = true + ring_mode = "simultaneous" + voicemail_greeting_id = "" + wrap_up_time = 60 + } + + # some extension can use call forwarding such as user + call_forwarding = { + require_press_1_before_connecting = true + enable_zoom_mobile_apps = true # Zoom Mobile Apps + enable_zoom_desktop_apps = true # Zoom Desktop Apps + enable_zoom_phone_appliance_apps = true # Zoom Phone Appliance Apps + settings = [ + { + "description" = "external person", + "enable" = false, + "phone_number" = "+1234567890" + }, + ] + } + + lifecycle { + ignore_changes = [ + # zoom api doesn't return some fields, so please ignore them + call_handling.receive_call, + ] + } +} diff --git a/examples/resources/zoom_phone_call_handling_closed_hours/import.sh b/examples/resources/zoom_phone_call_handling_closed_hours/import.sh new file mode 100644 index 0000000..8acc2bb --- /dev/null +++ b/examples/resources/zoom_phone_call_handling_closed_hours/import.sh @@ -0,0 +1,2 @@ +# ${extension_id} +terraform import zoom_phone_call_handling_closed_hours.example t6wyhAZRQXXX_Rv3jj3XXX diff --git a/examples/resources/zoom_phone_call_handling_closed_hours/provider.tf b/examples/resources/zoom_phone_call_handling_closed_hours/provider.tf new file mode 100644 index 0000000..23b61ca --- /dev/null +++ b/examples/resources/zoom_phone_call_handling_closed_hours/provider.tf @@ -0,0 +1,22 @@ +terraform { + required_providers { + zoom = { + source = "folio-sec/zoom" + version = "~> 0.0.0" + } + } +} + +provider "zoom" { + account_id = var.zoom_account_id + client_id = var.zoom_client_id + client_secret = var.zoom_client_secret +} + +variable "zoom_account_id" {} + +variable "zoom_client_id" {} + +variable "zoom_client_secret" { + sensitive = true +} diff --git a/examples/resources/zoom_phone_call_handling_closed_hours/resource.tf b/examples/resources/zoom_phone_call_handling_closed_hours/resource.tf new file mode 100644 index 0000000..7e57254 --- /dev/null +++ b/examples/resources/zoom_phone_call_handling_closed_hours/resource.tf @@ -0,0 +1,41 @@ +# Define only one resource for each extension id. +resource "zoom_phone_call_handling_closed_hours" "example" { + extension_id = "wGJDBcnJQC6tV86BbtlXXX" + + call_handling = { + call_not_answer_action = 1 + busy_on_another_call_action = 21 + connect_to_operator = false + allow_callers_check_voicemail = false + voicemail_greeting_id = "" # default + ring_mode = "simultaneous" + max_wait_time = 30 + } + + # some extension can use call forwarding such as user + call_forwarding = { + require_press_1_before_connecting = true + enable_zoom_mobile_apps = true # Zoom Mobile Apps + enable_zoom_desktop_apps = true # Zoom Desktop Apps + enable_zoom_phone_appliance_apps = true # Zoom Phone Appliance Apps + settings = [ + { + "description" : "external person", + "enable" : true, + "phone_number" : "+1234567890" + }, + ] + } + + # closed hours can handle after setting business_hours.custom_hours.type = 2 + depends_on = [ + zoom_phone_call_handling_business_hours.example + ] + + lifecycle { + ignore_changes = [ + # zoom api doesn't return some fields, so please ignore them + call_handling.receive_call, + ] + } +} diff --git a/examples/resources/zoom_phone_call_handling_holiday_hours/import.sh b/examples/resources/zoom_phone_call_handling_holiday_hours/import.sh new file mode 100644 index 0000000..fd44bea --- /dev/null +++ b/examples/resources/zoom_phone_call_handling_holiday_hours/import.sh @@ -0,0 +1,2 @@ +# ${extension_id/holiday_id} +terraform import zoom_phone_call_handling_holiday_hours.example t6wyhAZRQXXX_Rv3jj3XXX/gkqphABALXXX_Al0g13XXX diff --git a/examples/resources/zoom_phone_call_handling_holiday_hours/provider.tf b/examples/resources/zoom_phone_call_handling_holiday_hours/provider.tf new file mode 100644 index 0000000..23b61ca --- /dev/null +++ b/examples/resources/zoom_phone_call_handling_holiday_hours/provider.tf @@ -0,0 +1,22 @@ +terraform { + required_providers { + zoom = { + source = "folio-sec/zoom" + version = "~> 0.0.0" + } + } +} + +provider "zoom" { + account_id = var.zoom_account_id + client_id = var.zoom_client_id + client_secret = var.zoom_client_secret +} + +variable "zoom_account_id" {} + +variable "zoom_client_id" {} + +variable "zoom_client_secret" { + sensitive = true +} diff --git a/examples/resources/zoom_phone_call_handling_holiday_hours/resource.tf b/examples/resources/zoom_phone_call_handling_holiday_hours/resource.tf new file mode 100644 index 0000000..4b1cae2 --- /dev/null +++ b/examples/resources/zoom_phone_call_handling_holiday_hours/resource.tf @@ -0,0 +1,38 @@ +# Define multiple resources for each extension id. +resource "zoom_phone_call_handling_holiday_hours" "example" { + extension_id = "wGJDBcnJQC6tV86BbtlXXX" + + holiday = { + name = "temporary" + from = "2024-08-24T09:00:00Z" + to = "2024-08-26T17:00:00+09:00" + } + + call_handling = { + call_not_answer_action = 1 + connect_to_operator = false + allow_callers_check_voicemail = false + } + + # some extension can use call forwarding such as user + call_forwarding = { + require_press_1_before_connecting = true + enable_zoom_mobile_apps = true # Zoom Mobile Apps + enable_zoom_desktop_apps = true # Zoom Desktop Apps + enable_zoom_phone_appliance_apps = true # Zoom Phone Appliance Apps + settings = [ + { + "description" : "external person", + "enable" : true, + "phone_number" : "+1234567890" + }, + ] + } + + lifecycle { + ignore_changes = [ + # zoom api doesn't return some fields, so please ignore them + call_handling.receive_call, + ] + } +} diff --git a/generated/api/zoomphone/oas_json_gen.go b/generated/api/zoomphone/oas_json_gen.go index 9683570..442e620 100644 --- a/generated/api/zoomphone/oas_json_gen.go +++ b/generated/api/zoomphone/oas_json_gen.go @@ -125721,11 +125721,18 @@ func (s *PatchCallHandlingSettingsCallForwardingSettings) encodeFields(e *jx.Enc s.RequirePress1BeforeConnecting.Encode(e) } } + { + if s.HolidayID.Set { + e.FieldStart("holiday_id") + s.HolidayID.Encode(e) + } + } } -var jsonFieldsNameOfPatchCallHandlingSettingsCallForwardingSettings = [2]string{ +var jsonFieldsNameOfPatchCallHandlingSettingsCallForwardingSettings = [3]string{ 0: "call_forwarding_settings", 1: "require_press_1_before_connecting", + 2: "holiday_id", } // Decode decodes PatchCallHandlingSettingsCallForwardingSettings from json. @@ -125763,6 +125770,16 @@ func (s *PatchCallHandlingSettingsCallForwardingSettings) Decode(d *jx.Decoder) }(); err != nil { return errors.Wrap(err, "decode field \"require_press_1_before_connecting\"") } + case "holiday_id": + if err := func() error { + s.HolidayID.Reset() + if err := s.HolidayID.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"holiday_id\"") + } default: return d.Skip() } @@ -126232,9 +126249,15 @@ func (s *PatchCallHandlingSettingsCallHandlingSettings) encodeFields(e *jx.Encod s.WrapUpTime.Encode(e) } } + { + if s.HolidayID.Set { + e.FieldStart("holiday_id") + s.HolidayID.Encode(e) + } + } } -var jsonFieldsNameOfPatchCallHandlingSettingsCallHandlingSettings = [27]string{ +var jsonFieldsNameOfPatchCallHandlingSettingsCallHandlingSettings = [28]string{ 0: "allow_callers_check_voicemail", 1: "allow_members_to_reset", 2: "audio_while_connecting_id", @@ -126262,6 +126285,7 @@ var jsonFieldsNameOfPatchCallHandlingSettingsCallHandlingSettings = [27]string{ 24: "ring_mode", 25: "voicemail_greeting_id", 26: "wrap_up_time", + 27: "holiday_id", } // Decode decodes PatchCallHandlingSettingsCallHandlingSettings from json. @@ -126542,6 +126566,16 @@ func (s *PatchCallHandlingSettingsCallHandlingSettings) Decode(d *jx.Decoder) er }(); err != nil { return errors.Wrap(err, "decode field \"wrap_up_time\"") } + case "holiday_id": + if err := func() error { + s.HolidayID.Reset() + if err := s.HolidayID.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"holiday_id\"") + } default: return d.Skip() } diff --git a/generated/api/zoomphone/oas_schemas_gen.go b/generated/api/zoomphone/oas_schemas_gen.go index 06fb572..ddf7493 100644 --- a/generated/api/zoomphone/oas_schemas_gen.go +++ b/generated/api/zoomphone/oas_schemas_gen.go @@ -82249,6 +82249,8 @@ type PatchCallHandlingSettingsCallForwardingSettings struct { // voicemail. It's required for the `call_forwarding` sub-setting. // Press 1 is always enabled and is required for `callQueue` type extension calls. RequirePress1BeforeConnecting OptBool `json:"require_press_1_before_connecting"` + // The ID of the holiday. + HolidayID OptString `json:"holiday_id"` } // GetCallForwardingSettings returns the value of CallForwardingSettings. @@ -82261,6 +82263,11 @@ func (s *PatchCallHandlingSettingsCallForwardingSettings) GetRequirePress1Before return s.RequirePress1BeforeConnecting } +// GetHolidayID returns the value of HolidayID. +func (s *PatchCallHandlingSettingsCallForwardingSettings) GetHolidayID() OptString { + return s.HolidayID +} + // SetCallForwardingSettings sets the value of CallForwardingSettings. func (s *PatchCallHandlingSettingsCallForwardingSettings) SetCallForwardingSettings(val []PatchCallHandlingSettingsCallForwardingSettingsCallForwardingSettingsItem) { s.CallForwardingSettings = val @@ -82271,6 +82278,11 @@ func (s *PatchCallHandlingSettingsCallForwardingSettings) SetRequirePress1Before s.RequirePress1BeforeConnecting = val } +// SetHolidayID sets the value of HolidayID. +func (s *PatchCallHandlingSettingsCallForwardingSettings) SetHolidayID(val OptString) { + s.HolidayID = val +} + type PatchCallHandlingSettingsCallForwardingSettingsCallForwardingSettingsItem struct { // The external phone number's description. Description OptString `json:"description"` @@ -82563,6 +82575,8 @@ type PatchCallHandlingSettingsCallHandlingSettings struct { // * `300` // This is only required for the `call_handling` sub-setting. WrapUpTime OptInt `json:"wrap_up_time"` + // The ID of the holiday. + HolidayID OptString `json:"holiday_id"` } // GetAllowCallersCheckVoicemail returns the value of AllowCallersCheckVoicemail. @@ -82700,6 +82714,11 @@ func (s *PatchCallHandlingSettingsCallHandlingSettings) GetWrapUpTime() OptInt { return s.WrapUpTime } +// GetHolidayID returns the value of HolidayID. +func (s *PatchCallHandlingSettingsCallHandlingSettings) GetHolidayID() OptString { + return s.HolidayID +} + // SetAllowCallersCheckVoicemail sets the value of AllowCallersCheckVoicemail. func (s *PatchCallHandlingSettingsCallHandlingSettings) SetAllowCallersCheckVoicemail(val OptBool) { s.AllowCallersCheckVoicemail = val @@ -82835,6 +82854,11 @@ func (s *PatchCallHandlingSettingsCallHandlingSettings) SetWrapUpTime(val OptInt s.WrapUpTime = val } +// SetHolidayID sets the value of HolidayID. +func (s *PatchCallHandlingSettingsCallHandlingSettings) SetHolidayID(val OptString) { + s.HolidayID = val +} + // This option distributes incoming calls. // If `Sequential` or `Rotating` is selected, calls will ring for a specific time before trying the // next available queue member. diff --git a/internal/provider/httpclient/logging.go b/internal/provider/httpclient/logging.go index 8a64aa4..a7c4798 100644 --- a/internal/provider/httpclient/logging.go +++ b/internal/provider/httpclient/logging.go @@ -20,6 +20,26 @@ func NewLoggingRoundTripper(ctx context.Context, rt http.RoundTripper) http.Roun } func (t LoggingRoundTripper) RoundTrip(req *http.Request) (resp *http.Response, err error) { + if req != nil && req.Method != "GET" { + buf, _ := io.ReadAll(req.Body) + loggingBody := io.NopCloser(bytes.NewBuffer(buf)) + rawBody := io.NopCloser(bytes.NewBuffer(buf)) + req.Body = rawBody + bodyBytes, err := io.ReadAll(loggingBody) + if err != nil { + return nil, fmt.Errorf("http failed to read response body: %w", err) + } + bodyString := string(bodyBytes) + tflog.Debug(t.ctx, + fmt.Sprintf("http request"), + map[string]interface{}{ + "method": req.Method, + "request_uri": req.URL.RequestURI(), + "body": bodyString, + }, + ) + } + resp, err = t.rt.RoundTrip(req) if err != nil { tflog.Info( diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 818d3d9..1687bae 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -14,6 +14,7 @@ import ( "github.com/folio-sec/terraform-provider-zoom/internal/services/phone/autoreceptionist" "github.com/folio-sec/terraform-provider-zoom/internal/services/phone/autoreceptionistivr" "github.com/folio-sec/terraform-provider-zoom/internal/services/phone/blockedlist" + "github.com/folio-sec/terraform-provider-zoom/internal/services/phone/callhandling" "github.com/folio-sec/terraform-provider-zoom/internal/services/phone/callqueue" "github.com/folio-sec/terraform-provider-zoom/internal/services/phone/callqueuemember" "github.com/folio-sec/terraform-provider-zoom/internal/services/phone/callqueuephonenumber" @@ -209,6 +210,9 @@ func (p *zoomProvider) Resources(_ context.Context) []func() resource.Resource { autoreceptionist.NewPhoneAutoReceptionistResource, autoreceptionistivr.NewPhoneAutoReceptionistIvrResource, blockedlist.NewPhoneBlockedListResource, + callhandling.NewPhoneCallHandlingBusinessHoursResource, + callhandling.NewPhoneCallHandlingClosedHoursResource, + callhandling.NewPhoneCallHandlingHolidayHoursResource, callqueue.NewPhoneCallQueueResource, callqueuemember.NewPhoneCallQueueMembersResource, callqueuephonenumber.NewPhoneCallQueuePhoneNumbersResource, diff --git a/internal/services/phone/callhandling/call_handling_business_hours_resource.go b/internal/services/phone/callhandling/call_handling_business_hours_resource.go new file mode 100644 index 0000000..3298204 --- /dev/null +++ b/internal/services/phone/callhandling/call_handling_business_hours_resource.go @@ -0,0 +1,902 @@ +package callhandling + +import ( + "context" + "fmt" + "regexp" + "strings" + + "github.com/folio-sec/terraform-provider-zoom/internal/provider/shared" + "github.com/hashicorp/terraform-plugin-framework-validators/int32validator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/samber/lo" +) + +var ( + _ resource.Resource = &tfBusinessHoursResource{} + _ resource.ResourceWithConfigure = &tfBusinessHoursResource{} + _ resource.ResourceWithImportState = &tfBusinessHoursResource{} +) + +func NewPhoneCallHandlingBusinessHoursResource() resource.Resource { + return &tfBusinessHoursResource{} +} + +type tfBusinessHoursResource struct { + crud *crud +} + +func (r *tfBusinessHoursResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + data, ok := req.ProviderData.(*shared.ProviderData) + if !ok { + resp.Diagnostics.AddError( + "Unexpected ProviderData Source Configure Type", + fmt.Sprintf("Expected *provider.ProviderData, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + r.crud = newCrud(data.PhoneClient) +} + +func (r *tfBusinessHoursResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_phone_call_handling_business_hours" +} + +func (r *tfBusinessHoursResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: `Call handling settings allow you to control how your system routes calls during business hours. +For more information, read our [Call Handling API guide](https://developers.zoom.us/docs/zoom-phone/call-handling/) or Zoom support article [Customizing call handling settings](https://support.zoom.us/hc/en-us/articles/360059966372-Customizing-call-handling-settings). + +## API Permissions +The following API permissions are required in order to use this resource. +This resource requires the ` + strings.Join([]string{ + "`phone:read:call_handling_setting:admin`", + "`phone:write:call_handling_setting:admin`", + "`phone:update:call_handling_setting:admin`", + "`phone:delete:call_handling_setting:admin`", + }, ", ") + ".", + Attributes: map[string]schema.Attribute{ + "extension_id": schema.StringAttribute{ + Required: true, + MarkdownDescription: "Extension ID.", + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "custom_hours": schema.SingleNestedAttribute{ + Required: true, + MarkdownDescription: "The custom hours settings.", + Attributes: map[string]schema.Attribute{ + "type": schema.Int32Attribute{ + Required: true, + MarkdownDescription: `The type of custom hours: + - 1 — 24 hours, 7 days a week. + - 2 — Custom hours.`, + Validators: []validator.Int32{ + int32validator.OneOf(1, 2), + }, + }, + "allow_members_to_reset": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: `This field allows queue members to set their own business hours. This field allows queue members' business hours to override the default hours of the call queue. + - Only required for Call Queue custom_hours sub-setting.`, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), + }, + }, + "settings": schema.SetNestedAttribute{ + Optional: true, + MarkdownDescription: "The custom hours settings. It's only required for the custom_hours sub-setting.", + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "weekday": schema.Int32Attribute{ + Required: true, + MarkdownDescription: `The day of the week: + - 1 — Sunday + - 2 — Monday + - 3 — Tuesday + - 4 — Wednesday + - 5 — Thursday + - 6 — Friday + - 7 — Saturday`, + Validators: []validator.Int32{ + int32validator.OneOf(1, 2, 3, 4, 5, 6, 7), + }, + }, + "type": schema.Int32Attribute{ + Required: true, + MarkdownDescription: `The type of custom hours: + - 0 — Disabled. + - 1 — 24 hours. + - 2 — Customized hours.`, + Validators: []validator.Int32{ + int32validator.OneOf(0, 1, 2), + }, + }, + "from": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The custom hours start time HH:mm format.", + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(`\d{2}:\d{2}`), "value must be HH:MM format"), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "to": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The custom hours end time in HH:mm format.", + Validators: []validator.String{ + stringvalidator.RegexMatches(regexp.MustCompile(`\d{2}:\d{2}`), "value must be HH:MM format"), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + }, + }, + }, + }, + "call_handling": schema.SingleNestedAttribute{ + Required: true, + MarkdownDescription: `The call handling settings. + - NOTE: some fields doesn't return from zoom api, so please ignore_changes for these fields. +`, + Attributes: map[string]schema.Attribute{ + "call_not_answer_action": schema.Int32Attribute{ + Optional: true, + MarkdownDescription: `The action to take when a call is not answered: + - 1 — Forward to a voicemail. + - 2 — Forward to the user. + - 4 — Forward to the common area. + - 6 — Forward to the auto receptionist. + - 7 — Forward to a call queue. + - 8 — Forward to a shared line group. + - 9 — Forward to an external contact. + - 10 - Forward to a phone number. + - 11 — Disconnect. + - 12 — Play a message, then disconnect. + - 13 - Forward to a message. + - 14 - Forward to an interactive voice response (IVR).`, + Validators: []validator.Int32{ + int32validator.OneOf(1, 2, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14), + }, + }, + "forward_to_extension_id": schema.StringAttribute{ + Optional: true, + MarkdownDescription: `The forwarding extension ID that's required only when call_not_answer_action setting is set to: + - 2 - Forward to the user. + - 4 - Forward to the common area. + - 6 - Forward to the auto receptionist. + - 7 - Forward to a call queue. + - 8 - Forward to a shared line group. + - 9 - forward to an external contact.`, + }, + "busy_on_another_call_action": schema.Int32Attribute{ + Optional: true, + MarkdownDescription: `The action to take when the user is busy on another call: + - 1 — Forward to a voicemail. + - 2 — Forward to the user. + - 4 — Forward to the common area. + - 6 — Forward to the auto receptionist. + - 7 — Forward to a call queue. + - 8 — Forward to a shared line group. + - 9 — Forward to an external contact. + - 10 - Forward to a phone number. + - 12 — Play a message, then disconnect. + - 21 — Call waiting. + - 22 — Play a busy signal.`, + Validators: []validator.Int32{ + int32validator.OneOf(1, 2, 4, 6, 7, 8, 9, 10, 12, 21, 22), + }, + }, + "busy_forward_to_extension_id": schema.StringAttribute{ + Optional: true, + MarkdownDescription: `The forwarding extension ID that's required only when busy_on_another_call_action setting is set to: + - 2 - Forward to the user. + - 4 - Forward to the common area. + - 6 - Forward to the auto receptionist. + - 7 - Forward to a call queue. + - 8 - Forward to a shared line group. + - 9 - forward to an external contact.`, + }, + "allow_callers_check_voicemail": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "Whether to allow the callers to check voicemails over a phone. It's required only when the call_not_answer_action setting is set to 1 (Forward to a voicemail).", + }, + "allow_members_to_reset": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: `This field allows queue members to set their own business hours. This field allows queue members' business Hours to override the default hours of the call queue. + - Only required for Call Queue custom_hours sub-setting.`, + }, + "audio_while_connecting_id": schema.StringAttribute{ + Optional: true, + MarkdownDescription: `The audio while connecting the prompt ID. This option can select the audio played for the inbound callers when they are waiting to be routed to the next available call queue member. + - Options: empty char - default and 0 - disable + - This is only required for the Call Queue call_handling sub-setting. +`, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "call_distribution": schema.SingleNestedAttribute{ + Optional: true, + MarkdownDescription: `This option distributes incoming calls. + - If Sequential or Rotating is selected, calls will ring for a specific time before trying the next available queue member. + - This is only required for the call_handling sub-setting.`, + Attributes: map[string]schema.Attribute{ + "handle_multiple_calls": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: `The maximum number of calls that can be handled simultaneously is less than half of the total amount of available call queue members. Note that the first incoming call may not be answered first. + - Required except for simultaneous ring mode.`, + }, + "ring_duration": schema.Int32Attribute{ + Optional: true, + MarkdownDescription: `The ringing duration for each member: + - Required except for simultaneous ring mode. + - Allowed: 10┃15┃20┃25┃30┃35┃40┃45┃50┃55┃60`, + Validators: []validator.Int32{ + int32validator.OneOf(10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60), + }, + }, + "ring_mode": schema.StringAttribute{ + Optional: true, + MarkdownDescription: `The call distribution ring mode: + - Allowed: simultaneous┃sequential┃rotating┃longest_idle`, + }, + "skip_offline_device_phone_number": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: `Devices with Zoom app or client not launched and mobile phone with screen locked will be skipped. Phone numbers added to user's call handling settings will be skipped. + - Required except for simultaneous ring mode.`, + }, + }, + }, + "busy_require_press1_before_connecting": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "When one is busy on another call, the receiver needs to press 1 before connecting the call for it to be forwarded to an external contact or a number. This option ensures that forwarded calls won't reach the voicemail box for the external contact or a number.", + }, + "unanswered_require_press1_before_connecting": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "When a call is unanswered, press 1 before connecting the call to forward to an external contact or a number. This option ensures that forwarded calls won't reach the voicemail box for the external contact or a number.", + }, + "overflow_play_callee_voicemail_greeting": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: `Whether to play the callee's voicemail greeting when the caller reaches the end of the forwarding sequence. It displays when call_not_answer_action is set to: + - 2 - Forward to the user + - 4 - Forward to the common area + - 6 - Forward to the auto receptionist + - 7 - Forward to a call queue + - 8 - Forward to a shared line group + - 9 - Forward to an external contact + - 10 - Forward to an external number.`, + }, + "play_callee_voicemail_greeting": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "Whether to play callee's voicemail greeting when the caller reaches the end of forwarding sequence. It displays when `busy_on_another_call_action` action or `call_not_answer_action` is set to `1` - Forward to a voicemail.", + }, + "busy_play_callee_voicemail_greeting": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: `Whether to play callee's voicemail greeting when the caller reaches the end of the forwarding sequence. It displays when busy_on_another_call_action action is set to + - 2 - Forward to the user + - 4 - Forward to the common area + - 6 - Forward to the auto receptionist + - 7 - Forward to a call queue + - 8 - Forward to a shared line group + - 9 - Forward to an external contact + - 10 - Forward to an external number.`, + }, + "phone_number": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The extension's phone number or forward to an external number in [E.164](https://en.wikipedia.org/wiki/E.164) format format. It's required when `call_not_answer_action` action is set to `10` - Forward to an external number.", + }, + "phone_number_description": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "(Optional) This field forwards to an external number description. Add this field when `call_not_answer_action` is set to `10` - Forward to an external number.", + }, + "busy_phone_number": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The extension's phone number or forward to an external number in [E.164](https://en.wikipedia.org/wiki/E.164) format format. It sets when `busy_on_another_call_action` action is set to `10` - Forward to an external number.", + }, + "busy_phone_number_description": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "This field forwards to an external number description (optional). It sets when `busy_on_another_call_action` action is set to `10` - Forward to an external number.", + }, + "connect_to_operator": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "Whether to allow callers to reach an operator. It's required only when the `call_not_answer_action` or `busy_on_another_call_action` is set to 1 (Forward to a voicemail).", + }, + "greeting_prompt_id": schema.StringAttribute{ + Optional: true, + MarkdownDescription: `The greeting audio prompt ID. + - Options: empty char - default and 0 - disable + - This is only required for the Call Queue or Auto Receptionist call_handling sub-setting.`, + }, + "max_call_in_queue": schema.Int32Attribute{ + Optional: true, + MarkdownDescription: `The maximum number of calls in queue. Specify the maximum number of callers to place in the queue. When this number is exceeded, callers will be routed based on the overflow option. Up to 60. + - It's required for the Call Queue call_handling sub-setting.`, + Validators: []validator.Int32{ + int32validator.AtMost(60), + }, + }, + "max_wait_time": schema.Int32Attribute{ + Optional: true, + MarkdownDescription: `The maximum wait time, in seconds. + - for simultaneous ring mode or the ring duration for each device for sequential ring mode: 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60. + - Specify how long a caller will wait in the queue. Once the wait time is exceeded, the caller will be rerouted based on the overflow option for Call Queue: 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 120, 180, 240, 300, 600, 900, 1200, 1500, 1800. + - This is only required for the call_handling sub-setting. +`, + Validators: []validator.Int32{ + int32validator.OneOf(10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 120, 180, 240, 300, 600, 900, 1200, 1500, 1800), + }, + }, + "music_on_hold_id": schema.StringAttribute{ + Optional: true, + MarkdownDescription: `The music on hold prompt ID. This field is an option to choose music for inbound callers when they're placed on hold by a call queue member. + - Options: empty char - default and 0 - disable + - Only required for the Call Queue call_handling sub-setting.`, + }, + "operator_extension_id": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The extension ID of the operator to whom the call is being forwarded. It's required only when `call_not_answer_action` is set to `1` (Forward to a voicemail) and `connect_to_operator` is set to true.", + }, + "receive_call": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: `This field receives calls while on a call. When enabled, call queue members can receive new incoming calls notification even on the call. + - It's required for the Call Queue call handling sub-setting.`, + }, + "ring_mode": schema.StringAttribute{ + Optional: true, + MarkdownDescription: `The call handling ring mode: + - simultaneous + - sequential. For user business hours, ring_mode needs to be set with max_wait_time.`, + Validators: []validator.String{ + stringvalidator.OneOf("simultaneous", "sequential"), + }, + }, + "voicemail_greeting_id": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The voicemail greeting prompt ID. It's required when `call_not_answer_action` or `busy_on_another_call_action` is set to `1` (Forward to a voicemail). Required only for `call_handling` subsettings of `Call Queue`, `Auto Receptionist` or `User`.", + }, + "wrap_up_time": schema.Int32Attribute{ + Optional: true, + MarkdownDescription: `The wrap up time in seconds. Specify the duration before the next queue call is routed to a member in call queue: + - This is only required for the call_handling sub-setting. + - Allowed: 10┃15┃20┃25┃30┃35┃40┃45┃50┃55┃60┃120┃180┃240┃300`, + Validators: []validator.Int32{ + int32validator.OneOf(10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 120, 180, 240, 300), + }, + }, + }, + }, + "call_forwarding": schema.SingleNestedAttribute{ + Optional: true, + MarkdownDescription: "The call forwarding settings.", + Attributes: map[string]schema.Attribute{ + "require_press_1_before_connecting": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "When a call is forwarded to a personal phone number, whether the user must press \"1\" before the call connects. Enable this option to ensure missed calls do not reach to your personal voicemail. It's required for the `call_forwarding` sub-setting. Press 1 is always enabled and is required for callQueue type extension calls.", + }, + "enable_zoom_mobile_apps": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "Whether to enable Zoom Mobile Apps call forwarding", + }, + "enable_zoom_desktop_apps": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "Whether to enable Zoom Desktop Apps call forwarding", + }, + "enable_zoom_phone_appliance_apps": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "Whether to enable Zoom Phone Appliance Apps call forwarding", + }, + "settings": schema.SetNestedAttribute{ + Optional: true, + MarkdownDescription: "The call forwarding settings. It's only required for the `call_forwarding` sub-setting.", + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The call forwarding's ID.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "description": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The external phone number's description.", + }, + "enable": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "Whether to receive a call.", + }, + "phone_number": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The external phone number in [E.164](https://en.wikipedia.org/wiki/E.164) format format.", + }, + }, + }, + }, + }, + }, + }, + } +} + +type businessHoursResourceModel struct { + ExtensionID types.String `tfsdk:"extension_id"` + CustomHours *businessHoursResourceModelCustomHours `tfsdk:"custom_hours"` + CallHandling *businessHoursResourceModelCallHandling `tfsdk:"call_handling"` + CallForwarding *businessHoursResourceModelCallForwarding `tfsdk:"call_forwarding"` +} + +type businessHoursResourceModelCustomHours struct { + Type types.Int32 `tfsdk:"type"` + AllowMembersToReset types.Bool `tfsdk:"allow_members_to_reset"` + Settings []*businessHoursResourceModelCustomHoursSettings `tfsdk:"settings"` +} + +type businessHoursResourceModelCustomHoursSettings struct { + Weekday types.Int32 `tfsdk:"weekday"` + Type types.Int32 `tfsdk:"type"` + From types.String `tfsdk:"from"` + To types.String `tfsdk:"to"` +} + +type businessHoursResourceModelCallHandling struct { + CallNotAnswerAction types.Int32 `tfsdk:"call_not_answer_action"` + ForwardToExtensionID types.String `tfsdk:"forward_to_extension_id"` + BusyOnAnotherCallAction types.Int32 `tfsdk:"busy_on_another_call_action"` + BusyForwardToExtensionID types.String `tfsdk:"busy_forward_to_extension_id"` + AllowCallersCheckVoicemail types.Bool `tfsdk:"allow_callers_check_voicemail"` + AllowMembersToReset types.Bool `tfsdk:"allow_members_to_reset"` + AudioWhileConnectingID types.String `tfsdk:"audio_while_connecting_id"` + CallDistribution *businessHoursResourceModelCallHandlingCallDistribution `tfsdk:"call_distribution"` + BusyRequirePress1BeforeConnecting types.Bool `tfsdk:"busy_require_press1_before_connecting"` + UnAnsweredRequirePress1BeforeConnecting types.Bool `tfsdk:"unanswered_require_press1_before_connecting"` + OverflowPlayCalleeVoicemailGreeting types.Bool `tfsdk:"overflow_play_callee_voicemail_greeting"` + PlayCalleeVoicemailGreeting types.Bool `tfsdk:"play_callee_voicemail_greeting"` + BusyPlayCalleeVoicemailGreeting types.Bool `tfsdk:"busy_play_callee_voicemail_greeting"` + PhoneNumber types.String `tfsdk:"phone_number"` + PhoneNumberDescription types.String `tfsdk:"phone_number_description"` + BusyPhoneNumber types.String `tfsdk:"busy_phone_number"` + BusyPhoneNumberDescription types.String `tfsdk:"busy_phone_number_description"` + ConnectToOperator types.Bool `tfsdk:"connect_to_operator"` + GreetingPromptID types.String `tfsdk:"greeting_prompt_id"` + MaxCallInQueue types.Int32 `tfsdk:"max_call_in_queue"` + MaxWaitTime types.Int32 `tfsdk:"max_wait_time"` + MusicOnHoldID types.String `tfsdk:"music_on_hold_id"` + OperatorExtensionID types.String `tfsdk:"operator_extension_id"` + ReceiveCall types.Bool `tfsdk:"receive_call"` + RingMode types.String `tfsdk:"ring_mode"` + VoiceMailGreetingID types.String `tfsdk:"voicemail_greeting_id"` + WrapUpTime types.Int32 `tfsdk:"wrap_up_time"` +} + +type businessHoursResourceModelCallHandlingCallDistribution struct { + HandleMultipleCalls types.Bool `tfsdk:"handle_multiple_calls"` + RingDuration types.Int32 `tfsdk:"ring_duration"` + RingMode types.String `tfsdk:"ring_mode"` + SkipOfflineDevicePhoneNumber types.Bool `tfsdk:"skip_offline_device_phone_number"` +} + +type businessHoursResourceModelCallForwarding struct { + RequirePress1BeforeConnecting types.Bool `tfsdk:"require_press_1_before_connecting"` + EnableZoomMobileApps types.Bool `tfsdk:"enable_zoom_mobile_apps"` + EnableZoomDesktopApps types.Bool `tfsdk:"enable_zoom_desktop_apps"` + EnableZoomPhoneApplianceApps types.Bool `tfsdk:"enable_zoom_phone_appliance_apps"` + Settings []*businessHoursResourceModelCallForwardingSetting `tfsdk:"settings"` +} + +type businessHoursResourceModelCallForwardingSetting struct { + ID types.String `tfsdk:"id"` + Description types.String `tfsdk:"description"` + Enable types.Bool `tfsdk:"enable"` + PhoneNumber types.String `tfsdk:"phone_number"` +} + +func (r *tfBusinessHoursResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state businessHoursResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + output, err := r.read(ctx, &state) + if err != nil { + resp.Diagnostics.AddError("Error reading phone call handling", err.Error()) + return + } + + diags = resp.State.Set(ctx, &output) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *tfBusinessHoursResource) read(ctx context.Context, plan *businessHoursResourceModel) (*businessHoursResourceModel, error) { + dto, err := r.crud.readBusinessHours(ctx, plan.ExtensionID) + if err != nil { + return nil, fmt.Errorf("error read: %v", err) + } + if dto == nil { + return nil, nil // already deleted + } + + customHoursSetting := lo.Ternary(plan.CustomHours != nil && plan.CustomHours.Settings != nil, make([]*businessHoursResourceModelCustomHoursSettings, 0), nil) + for _, item := range dto.customHours.settings { + customHoursSetting = append(customHoursSetting, &businessHoursResourceModelCustomHoursSettings{ + Weekday: item.weekday, + Type: item.typ, + // NOTE: Zoom API returns "00:00" or pre-registered time for from/to when type is not 2. + // when changing type from 2 to 1, generally we set from/to with null, so just ignore them. + From: lo.Ternary(item.typ.ValueInt32() == 2, item.from, types.StringNull()), + To: lo.Ternary(item.typ.ValueInt32() == 2, item.to, types.StringNull()), + }) + } + customHours := &businessHoursResourceModelCustomHours{ + Type: dto.customHours.typ, + AllowMembersToReset: dto.customHours.allowMembersToReset, + Settings: customHoursSetting, + } + callHandling := &businessHoursResourceModelCallHandling{ + CallNotAnswerAction: dto.callHandling.callNotAnswerAction, + ForwardToExtensionID: dto.callHandling.forwardToExtensionID, + BusyOnAnotherCallAction: dto.callHandling.busyOnAnotherCallAction, + BusyForwardToExtensionID: dto.callHandling.busyForwardToExtensionID, + AllowCallersCheckVoicemail: dto.callHandling.allowCallersCheckVoicemail, + AllowMembersToReset: dto.callHandling.allowMembersToReset, + AudioWhileConnectingID: dto.callHandling.audioWhileConnectingID, + CallDistribution: lo.TernaryF(plan.CallHandling != nil && plan.CallHandling.CallDistribution != nil && dto.callHandling.callDistribution != nil, func() *businessHoursResourceModelCallHandlingCallDistribution { + return &businessHoursResourceModelCallHandlingCallDistribution{ + HandleMultipleCalls: dto.callHandling.callDistribution.handleMultipleCalls, + RingDuration: dto.callHandling.callDistribution.ringDuration, + RingMode: dto.callHandling.callDistribution.ringMode, + SkipOfflineDevicePhoneNumber: dto.callHandling.callDistribution.skipOfflineDevicePhoneNumber, + } + }, func() *businessHoursResourceModelCallHandlingCallDistribution { + return nil + }), + BusyRequirePress1BeforeConnecting: dto.callHandling.busyRequirePress1BeforeConnecting, + UnAnsweredRequirePress1BeforeConnecting: dto.callHandling.unAnsweredRequirePress1BeforeConnecting, + OverflowPlayCalleeVoicemailGreeting: dto.callHandling.overflowPlayCalleeVoicemailGreeting, + PlayCalleeVoicemailGreeting: dto.callHandling.playCalleeVoicemailGreeting, + BusyPlayCalleeVoicemailGreeting: dto.callHandling.busyPlayCalleeVoicemailGreeting, + PhoneNumber: dto.callHandling.phoneNumber, + PhoneNumberDescription: dto.callHandling.phoneNumberDescription, + BusyPhoneNumber: dto.callHandling.busyPhoneNumber, + BusyPhoneNumberDescription: dto.callHandling.busyPhoneNumberDescription, + ConnectToOperator: dto.callHandling.connectToOperator, + GreetingPromptID: dto.callHandling.greetingPromptID, + MaxCallInQueue: dto.callHandling.maxCallInQueue, + MaxWaitTime: dto.callHandling.maxWaitTime, + MusicOnHoldID: dto.callHandling.musicOnHoldID, + OperatorExtensionID: dto.callHandling.operatorExtensionID, + ReceiveCall: dto.callHandling.receiveCall, + RingMode: dto.callHandling.ringMode, + VoiceMailGreetingID: dto.callHandling.voiceMailGreetingID, + WrapUpTime: dto.callHandling.wrapUpTime, + } + callForwarding := lo.TernaryF(plan.CallForwarding != nil && dto.callForwarding != nil, func() *businessHoursResourceModelCallForwarding { + return &businessHoursResourceModelCallForwarding{ + RequirePress1BeforeConnecting: dto.callForwarding.requirePress1BeforeConnecting, + EnableZoomMobileApps: dto.callForwarding.enableZoomMobileApps, + EnableZoomDesktopApps: dto.callForwarding.enableZoomDesktopApps, + EnableZoomPhoneApplianceApps: dto.callForwarding.enableZoomPhoneApplianceApps, + Settings: lo.Map(dto.callForwarding.settings, func(item *readDtoCallForwardingSetting, index int) *businessHoursResourceModelCallForwardingSetting { + return &businessHoursResourceModelCallForwardingSetting{ + ID: item.id, + Description: item.description, + Enable: item.enable, + PhoneNumber: item.phoneNumber, + } + }), + } + }, func() *businessHoursResourceModelCallForwarding { + return nil + }) + return &businessHoursResourceModel{ + ExtensionID: dto.extensionID, + CustomHours: customHours, + CallHandling: callHandling, + CallForwarding: callForwarding, + }, nil +} + +func (r *tfBusinessHoursResource) sync(ctx context.Context, plan *businessHoursResourceModel, onDelete bool) error { + // business_hours sync handling like followings + // 1. custom_hours contains one setting and cannot delete it -> do PATCH + // 2. call_handling contains one setting and cannot delete it -> do PATCH + // 3. call_forwarding may contain one setting and can delete it -> do CREATE/PATCH/DELETE + + asis, err := r.read(ctx, plan) + if err != nil { + return err + } + + // 1. PATCH custom_hours + switch plan.CustomHours.Type.ValueInt32() { + case 1: + if plan.CustomHours.Settings != nil { + return fmt.Errorf("custom_hours type 1 cannot have settings") + } + case 2: + if plan.CustomHours.Settings == nil || len(plan.CustomHours.Settings) != 7 { + return fmt.Errorf("custom_hours type 2 must have Sunday-Saturday settings") + } + } + for _, setting := range plan.CustomHours.Settings { + if setting.Type.ValueInt32() == 2 { + if setting.From.ValueString() == "" || setting.To.ValueString() == "" { + return fmt.Errorf("custom_hours.setting from/to must contains value when type is 2 on weekday=%d", setting.Weekday.ValueInt32()) + } + } else { + if setting.From.ValueString() != "" || setting.To.ValueString() != "" { + return fmt.Errorf("custom_hours.setting from/to must not contains value when type is not 2 on weekday=%d", setting.Weekday.ValueInt32()) + } + } + } + patchCustomHours := &patchCustomHoursDto{ + extensionID: plan.ExtensionID, + settingType: settingTypeBusinessHours, + typ: plan.CustomHours.Type, + allowMembersToReset: plan.CustomHours.AllowMembersToReset, + settings: lo.Map(plan.CustomHours.Settings, func(item *businessHoursResourceModelCustomHoursSettings, index int) *patchCustomHoursDtoSetting { + return &patchCustomHoursDtoSetting{ + weekday: item.Weekday, + typ: item.Type, + from: item.From, + to: item.To, + } + }), + } + if err = r.crud.patchCustomHours(ctx, patchCustomHours); err != nil { + return err + } + + // 2. PATCH call_handling + patchCallHandling := &patchCallHandlingDto{ + extensionID: plan.ExtensionID, + settingType: settingTypeBusinessHours, + settings: &patchCallHandlingDtoSettings{ + callNotAnswerAction: plan.CallHandling.CallNotAnswerAction, + forwardToExtensionID: plan.CallHandling.ForwardToExtensionID, + busyOnAnotherCallAction: plan.CallHandling.BusyOnAnotherCallAction, + busyForwardToExtensionID: plan.CallHandling.BusyForwardToExtensionID, + allowCallersCheckVoicemail: plan.CallHandling.AllowCallersCheckVoicemail, + allowMembersToReset: plan.CallHandling.AllowMembersToReset, + audioWhileConnectingID: plan.CallHandling.AudioWhileConnectingID, + callDistribution: lo.TernaryF(plan.CallHandling.CallDistribution != nil, func() *patchCallHandlingDtoSettingsDistribution { + return &patchCallHandlingDtoSettingsDistribution{ + handleMultipleCalls: plan.CallHandling.CallDistribution.HandleMultipleCalls, + ringDuration: plan.CallHandling.CallDistribution.RingDuration, + ringMode: plan.CallHandling.CallDistribution.RingMode, + skipOfflineDevicePhoneNumber: plan.CallHandling.CallDistribution.SkipOfflineDevicePhoneNumber, + } + }, func() *patchCallHandlingDtoSettingsDistribution { + return nil + }), + busyRequirePress1BeforeConnecting: plan.CallHandling.BusyRequirePress1BeforeConnecting, + unAnsweredRequirePress1BeforeConnecting: plan.CallHandling.UnAnsweredRequirePress1BeforeConnecting, + overflowPlayCalleeVoicemailGreeting: plan.CallHandling.OverflowPlayCalleeVoicemailGreeting, + playCalleeVoicemailGreeting: plan.CallHandling.PlayCalleeVoicemailGreeting, + busyPlayCalleeVoicemailGreeting: plan.CallHandling.BusyPlayCalleeVoicemailGreeting, + phoneNumber: plan.CallHandling.PhoneNumber, + phoneNumberDescription: plan.CallHandling.PhoneNumberDescription, + busyPhoneNumber: plan.CallHandling.BusyPhoneNumber, + busyPhoneNumberDescription: plan.CallHandling.BusyPhoneNumberDescription, + connectToOperator: plan.CallHandling.ConnectToOperator, + greetingPromptID: plan.CallHandling.GreetingPromptID, + maxCallInQueue: plan.CallHandling.MaxCallInQueue, + maxWaitTime: plan.CallHandling.MaxWaitTime, + musicOnHoldID: plan.CallHandling.MusicOnHoldID, + operatorExtensionID: plan.CallHandling.OperatorExtensionID, + receiveCall: plan.CallHandling.ReceiveCall, + ringMode: plan.CallHandling.RingMode, + voiceMailGreetingID: plan.CallHandling.VoiceMailGreetingID, + wrapUpTime: plan.CallHandling.WrapUpTime, + }, + } + if err = r.crud.patchCallHandling(ctx, patchCallHandling); err != nil { + return err + } + + // 3. CREATE/PATCH/DELETE call_forwarding + // 3-1: PATCH existed call forwarding + // 3-2: create new call forwarding + // 3-3: delete unused call forwarding + + // 3-1: PATCH existed call forwarding + if plan.CallForwarding != nil { + var settings []*patchCallForwardingDtoSetting + if plan.CallForwarding.Settings != nil { + settings = lo.FilterMap(plan.CallForwarding.Settings, func(item *businessHoursResourceModelCallForwardingSetting, index int) (*patchCallForwardingDtoSetting, bool) { + return &patchCallForwardingDtoSetting{ + id: item.ID, + description: item.Description, + enable: item.Enable, + phoneNumber: item.PhoneNumber, + }, item.ID.ValueString() != "" // patch only id is existed + }) + } + patchCallForwarding := &patchCallForwardingDto{ + extensionID: plan.ExtensionID, + settingType: settingTypeBusinessHours, + requirePress1BeforeConnecting: plan.CallForwarding.RequirePress1BeforeConnecting, + enableZoomMobileApps: plan.CallForwarding.EnableZoomMobileApps, + enableZoomDesktopApps: plan.CallForwarding.EnableZoomDesktopApps, + enableZoomPhoneApplianceApps: plan.CallForwarding.EnableZoomPhoneApplianceApps, + settings: settings, + } + if err = r.crud.patchCallForwarding(ctx, patchCallForwarding, onDelete); err != nil { + return err + } + } else if asis.CallForwarding != nil { + patchCallForwarding := &patchCallForwardingDto{ + extensionID: plan.ExtensionID, + settingType: settingTypeBusinessHours, + requirePress1BeforeConnecting: types.BoolValue(false), + enableZoomMobileApps: types.BoolValue(true), + enableZoomDesktopApps: types.BoolValue(true), + enableZoomPhoneApplianceApps: types.BoolValue(true), + settings: []*patchCallForwardingDtoSetting{}, + } + if err = r.crud.patchCallForwarding(ctx, patchCallForwarding, onDelete); err != nil { + return err + } + } + + // 3-2: create new call forwarding + if plan.CallForwarding != nil { + newCallForwardings := lo.Filter(plan.CallForwarding.Settings, func(item *businessHoursResourceModelCallForwardingSetting, index int) bool { + return item.ID.ValueString() == "" + }) + for _, newCallForwarding := range newCallForwardings { + createAllForwardingDto := &createCallForwardingDto{ + extensionID: plan.ExtensionID, + settingType: settingTypeBusinessHours, + description: newCallForwarding.Description, + phoneNumber: newCallForwarding.PhoneNumber, + } + created, err := r.crud.createCallForwarding(ctx, createAllForwardingDto) + if err != nil { + return err + } + newCallForwarding.ID = created.callForwardingID + } + } + + // 3-3: delete unused call forwarding + if asis.CallForwarding != nil { + deleteCallForwardings := lo.Filter(asis.CallForwarding.Settings, func(item *businessHoursResourceModelCallForwardingSetting, index int) bool { + if plan.CallForwarding == nil { + return true + } + for _, planCallForwarding := range plan.CallForwarding.Settings { + if item.ID == planCallForwarding.ID { + return false + } + } + return true + }) + for _, setting := range deleteCallForwardings { + deleteCallForwarding := &deleteCallForwardingDto{ + extensionID: asis.ExtensionID, + settingType: settingTypeBusinessHours, + callForwardingID: setting.ID, + } + if err = r.crud.deleteCallForwarding(ctx, deleteCallForwarding); err != nil { + return nil + } + } + } + + return nil +} + +func (r *tfBusinessHoursResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan businessHoursResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if err := r.sync(ctx, &plan, false); err != nil { + resp.Diagnostics.AddError( + "Error creating phone call handling", + err.Error(), + ) + return + } + + // XXX zoom api doesn't return some fields after create/update, so just set plan with existed plan values + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *tfBusinessHoursResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan businessHoursResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + resp.Diagnostics.AddError( + "Error updating phone call handling", + "Error updating phone call handling", + ) + return + } + + if err := r.sync(ctx, &plan, false); err != nil { + resp.Diagnostics.AddError( + "Error updating phone call handling", + fmt.Sprintf( + "Could not update phone call handling %s, unexpected error: %s", + plan.ExtensionID.ValueString(), + err, + ), + ) + return + } + + // XXX zoom api doesn't return some fields after create/update, so just set plan with existed plan values + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *tfBusinessHoursResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state businessHoursResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + defaultModel := businessHoursResourceModel{ + ExtensionID: state.ExtensionID, + CustomHours: &businessHoursResourceModelCustomHours{ + Type: types.Int32Value(1), // 1=24hours + AllowMembersToReset: types.BoolValue(false), + }, + CallHandling: &businessHoursResourceModelCallHandling{}, + CallForwarding: &businessHoursResourceModelCallForwarding{}, + } + err := r.sync(ctx, &defaultModel, true) + if err != nil { + resp.Diagnostics.AddError( + "Error deleting phone call handling", + fmt.Sprintf( + "Could not delete phone call handling %s, unexpected error: %s", + state.ExtensionID.ValueString(), + err, + ), + ) + return + } + + tflog.Info(ctx, "deleted phone call handling", map[string]interface{}{ + "extension_id": state.ExtensionID.ValueString(), + }) +} + +func (r *tfBusinessHoursResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("extension_id"), req, resp) +} diff --git a/internal/services/phone/callhandling/call_handling_closed_hours_resource.go b/internal/services/phone/callhandling/call_handling_closed_hours_resource.go new file mode 100644 index 0000000..6ed6472 --- /dev/null +++ b/internal/services/phone/callhandling/call_handling_closed_hours_resource.go @@ -0,0 +1,640 @@ +package callhandling + +import ( + "context" + "fmt" + "strings" + + "github.com/folio-sec/terraform-provider-zoom/internal/provider/shared" + "github.com/hashicorp/terraform-plugin-framework-validators/int32validator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/samber/lo" +) + +var ( + _ resource.Resource = &tfClosedHoursResource{} + _ resource.ResourceWithConfigure = &tfClosedHoursResource{} + _ resource.ResourceWithImportState = &tfClosedHoursResource{} +) + +func NewPhoneCallHandlingClosedHoursResource() resource.Resource { + return &tfClosedHoursResource{} +} + +type tfClosedHoursResource struct { + crud *crud +} + +func (r *tfClosedHoursResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + data, ok := req.ProviderData.(*shared.ProviderData) + if !ok { + resp.Diagnostics.AddError( + "Unexpected ProviderData Source Configure Type", + fmt.Sprintf("Expected *provider.ProviderData, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + r.crud = newCrud(data.PhoneClient) +} + +func (r *tfClosedHoursResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_phone_call_handling_closed_hours" +} + +func (r *tfClosedHoursResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: `Call handling settings allow you to control how your system routes calls during closed hours. +For more information, read our [Call Handling API guide](https://developers.zoom.us/docs/zoom-phone/call-handling/) or Zoom support article [Customizing call handling settings](https://support.zoom.us/hc/en-us/articles/360059966372-Customizing-call-handling-settings). + +## NOTE +This resource is depends on ` + "`zoom_phone_call_handling_business_hours`" + `. Please set business hours type = 2 (Custom hours). + +## API Permissions +The following API permissions are required in order to use this resource. +This resource requires the ` + strings.Join([]string{ + "`phone:read:call_handling_setting:admin`", + "`phone:write:call_handling_setting:admin`", + "`phone:update:call_handling_setting:admin`", + "`phone:delete:call_handling_setting:admin`", + }, ", ") + ".", + Attributes: map[string]schema.Attribute{ + "extension_id": schema.StringAttribute{ + Required: true, + MarkdownDescription: "Extension ID.", + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "call_handling": schema.SingleNestedAttribute{ + Required: true, + MarkdownDescription: `The call handling settings. + - NOTE: some fields doesn't return from zoom api, so please ignore_changes for these fields. +`, + Attributes: map[string]schema.Attribute{ + "call_not_answer_action": schema.Int32Attribute{ + Optional: true, + MarkdownDescription: `The action to take when a call is not answered: + - 1 — Forward to a voicemail. + - 2 — Forward to the user. + - 4 — Forward to the common area. + - 6 — Forward to the auto receptionist. + - 7 — Forward to a call queue. + - 8 — Forward to a shared line group. + - 9 — Forward to an external contact. + - 10 - Forward to a phone number. + - 11 — Disconnect. + - 12 — Play a message, then disconnect. + - 13 - Forward to a message. + - 14 - Forward to an interactive voice response (IVR).`, + Validators: []validator.Int32{ + int32validator.OneOf(1, 2, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14), + }, + }, + "forward_to_extension_id": schema.StringAttribute{ + Optional: true, + MarkdownDescription: `The forwarding extension ID that's required only when call_not_answer_action setting is set to: + - 2 - Forward to the user. + - 4 - Forward to the common area. + - 6 - Forward to the auto receptionist. + - 7 - Forward to a call queue. + - 8 - Forward to a shared line group. + - 9 - forward to an external contact.`, + }, + "busy_on_another_call_action": schema.Int32Attribute{ + Optional: true, + MarkdownDescription: `The action to take when the user is busy on another call: + - 1 — Forward to a voicemail. + - 2 — Forward to the user. + - 4 — Forward to the common area. + - 6 — Forward to the auto receptionist. + - 7 — Forward to a call queue. + - 8 — Forward to a shared line group. + - 9 — Forward to an external contact. + - 10 - Forward to a phone number. + - 12 — Play a message, then disconnect. + - 21 — Call waiting. + - 22 — Play a busy signal.`, + Validators: []validator.Int32{ + int32validator.OneOf(1, 2, 4, 6, 7, 8, 9, 10, 12, 21, 22), + }, + }, + "busy_forward_to_extension_id": schema.StringAttribute{ + Optional: true, + MarkdownDescription: `The forwarding extension ID that's required only when busy_on_another_call_action setting is set to: + - 2 - Forward to the user. + - 4 - Forward to the common area. + - 6 - Forward to the auto receptionist. + - 7 - Forward to a call queue. + - 8 - Forward to a shared line group. + - 9 - forward to an external contact.`, + }, + "allow_callers_check_voicemail": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "Whether to allow the callers to check voicemails over a phone. It's required only when the call_not_answer_action setting is set to 1 (Forward to a voicemail).", + }, + "busy_require_press1_before_connecting": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "When one is busy on another call, the receiver needs to press 1 before connecting the call for it to be forwarded to an external contact or a number. This option ensures that forwarded calls won't reach the voicemail box for the external contact or a number.", + }, + "unanswered_require_press1_before_connecting": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "When a call is unanswered, press 1 before connecting the call to forward to an external contact or a number. This option ensures that forwarded calls won't reach the voicemail box for the external contact or a number.", + }, + "overflow_play_callee_voicemail_greeting": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: `Whether to play the callee's voicemail greeting when the caller reaches the end of the forwarding sequence. It displays when call_not_answer_action is set to: + - 2 - Forward to the user + - 4 - Forward to the common area + - 6 - Forward to the auto receptionist + - 7 - Forward to a call queue + - 8 - Forward to a shared line group + - 9 - Forward to an external contact + - 10 - Forward to an external number.`, + }, + "play_callee_voicemail_greeting": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "Whether to play callee's voicemail greeting when the caller reaches the end of forwarding sequence. It displays when `busy_on_another_call_action` action or `call_not_answer_action` is set to `1` - Forward to a voicemail.", + }, + "busy_play_callee_voicemail_greeting": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: `Whether to play callee's voicemail greeting when the caller reaches the end of the forwarding sequence. It displays when busy_on_another_call_action action is set to + - 2 - Forward to the user + - 4 - Forward to the common area + - 6 - Forward to the auto receptionist + - 7 - Forward to a call queue + - 8 - Forward to a shared line group + - 9 - Forward to an external contact + - 10 - Forward to an external number.`, + }, + "phone_number": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The extension's phone number or forward to an external number in [E.164](https://en.wikipedia.org/wiki/E.164) format format. It's required when `call_not_answer_action` action is set to `10` - Forward to an external number.", + }, + "phone_number_description": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "(Optional) This field forwards to an external number description. Add this field when `call_not_answer_action` is set to `10` - Forward to an external number.", + }, + "busy_phone_number": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The extension's phone number or forward to an external number in [E.164](https://en.wikipedia.org/wiki/E.164) format format. It sets when `busy_on_another_call_action` action is set to `10` - Forward to an external number.", + }, + "busy_phone_number_description": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "This field forwards to an external number description (optional). It sets when `busy_on_another_call_action` action is set to `10` - Forward to an external number.", + }, + "connect_to_operator": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "Whether to allow callers to reach an operator. It's required only when the `call_not_answer_action` or `busy_on_another_call_action` is set to 1 (Forward to a voicemail).", + }, + "greeting_prompt_id": schema.StringAttribute{ + Optional: true, + MarkdownDescription: `The greeting audio prompt ID. + - Options: empty char - default and 0 - disable + - This is only required for the Call Queue or Auto Receptionist call_handling sub-setting.`, + }, + "max_wait_time": schema.Int32Attribute{ + Optional: true, + MarkdownDescription: `The maximum wait time, in seconds. + - for simultaneous ring mode or the ring duration for each device for sequential ring mode: 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60. + - Specify how long a caller will wait in the queue. Once the wait time is exceeded, the caller will be rerouted based on the overflow option for Call Queue: 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 120, 180, 240, 300, 600, 900, 1200, 1500, 1800. + - This is only required for the call_handling sub-setting. +`, + Validators: []validator.Int32{ + int32validator.OneOf(10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 120, 180, 240, 300, 600, 900, 1200, 1500, 1800), + }, + }, + "operator_extension_id": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The extension ID of the operator to whom the call is being forwarded. It's required only when `call_not_answer_action` is set to `1` (Forward to a voicemail) and `connect_to_operator` is set to true.", + }, + "ring_mode": schema.StringAttribute{ + Optional: true, + MarkdownDescription: `The call handling ring mode: + - simultaneous + - sequential. For user closed hours, ring_mode needs to be set with max_wait_time.`, + Validators: []validator.String{ + stringvalidator.OneOf("simultaneous", "sequential"), + }, + }, + }, + }, + "call_forwarding": schema.SingleNestedAttribute{ + Optional: true, + MarkdownDescription: "The call forwarding settings.", + Attributes: map[string]schema.Attribute{ + "require_press_1_before_connecting": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "When a call is forwarded to a personal phone number, whether the user must press \"1\" before the call connects. Enable this option to ensure missed calls do not reach to your personal voicemail. It's required for the `call_forwarding` sub-setting. Press 1 is always enabled and is required for callQueue type extension calls.", + }, + "enable_zoom_mobile_apps": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "Whether to enable Zoom Mobile Apps call forwarding", + }, + "enable_zoom_desktop_apps": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "Whether to enable Zoom Desktop Apps call forwarding", + }, + "enable_zoom_phone_appliance_apps": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "Whether to enable Zoom Phone Appliance Apps call forwarding", + }, + "settings": schema.SetNestedAttribute{ + Optional: true, + MarkdownDescription: "The call forwarding settings. It's only required for the `call_forwarding` sub-setting.", + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The call forwarding's ID.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "description": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The external phone number's description.", + }, + "enable": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "Whether to receive a call.", + }, + "phone_number": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The external phone number in [E.164](https://en.wikipedia.org/wiki/E.164) format format.", + }, + }, + }, + }, + }, + }, + }, + } +} + +type closedHoursResourceModel struct { + ExtensionID types.String `tfsdk:"extension_id"` + CallHandling *closedHoursResourceModelCallHandling `tfsdk:"call_handling"` + CallForwarding *closedHoursResourceModelCallForwarding `tfsdk:"call_forwarding"` +} + +type closedHoursResourceModelCallHandling struct { + CallNotAnswerAction types.Int32 `tfsdk:"call_not_answer_action"` + ForwardToExtensionID types.String `tfsdk:"forward_to_extension_id"` + BusyOnAnotherCallAction types.Int32 `tfsdk:"busy_on_another_call_action"` + BusyForwardToExtensionID types.String `tfsdk:"busy_forward_to_extension_id"` + AllowCallersCheckVoicemail types.Bool `tfsdk:"allow_callers_check_voicemail"` + BusyRequirePress1BeforeConnecting types.Bool `tfsdk:"busy_require_press1_before_connecting"` + UnAnsweredRequirePress1BeforeConnecting types.Bool `tfsdk:"unanswered_require_press1_before_connecting"` + OverflowPlayCalleeVoicemailGreeting types.Bool `tfsdk:"overflow_play_callee_voicemail_greeting"` + PlayCalleeVoicemailGreeting types.Bool `tfsdk:"play_callee_voicemail_greeting"` + BusyPlayCalleeVoicemailGreeting types.Bool `tfsdk:"busy_play_callee_voicemail_greeting"` + PhoneNumber types.String `tfsdk:"phone_number"` + PhoneNumberDescription types.String `tfsdk:"phone_number_description"` + BusyPhoneNumber types.String `tfsdk:"busy_phone_number"` + BusyPhoneNumberDescription types.String `tfsdk:"busy_phone_number_description"` + ConnectToOperator types.Bool `tfsdk:"connect_to_operator"` + GreetingPromptID types.String `tfsdk:"greeting_prompt_id"` + MaxWaitTime types.Int32 `tfsdk:"max_wait_time"` + OperatorExtensionID types.String `tfsdk:"operator_extension_id"` + RingMode types.String `tfsdk:"ring_mode"` +} + +type closedHoursResourceModelCallForwarding struct { + RequirePress1BeforeConnecting types.Bool `tfsdk:"require_press_1_before_connecting"` + EnableZoomMobileApps types.Bool `tfsdk:"enable_zoom_mobile_apps"` + EnableZoomDesktopApps types.Bool `tfsdk:"enable_zoom_desktop_apps"` + EnableZoomPhoneApplianceApps types.Bool `tfsdk:"enable_zoom_phone_appliance_apps"` + Settings []*closedHoursResourceModelCallForwardingSetting `tfsdk:"settings"` +} + +type closedHoursResourceModelCallForwardingSetting struct { + ID types.String `tfsdk:"id"` + Description types.String `tfsdk:"description"` + Enable types.Bool `tfsdk:"enable"` + PhoneNumber types.String `tfsdk:"phone_number"` +} + +func (r *tfClosedHoursResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state closedHoursResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + output, err := r.read(ctx, &state) + if err != nil { + resp.Diagnostics.AddError("Error reading phone call handling", err.Error()) + return + } + + diags = resp.State.Set(ctx, &output) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *tfClosedHoursResource) read(ctx context.Context, plan *closedHoursResourceModel) (*closedHoursResourceModel, error) { + dto, err := r.crud.readClosedHours(ctx, plan.ExtensionID) + if err != nil { + return nil, fmt.Errorf("error read: %v", err) + } + if dto == nil { + return nil, nil // already deleted + } + + callHandling := &closedHoursResourceModelCallHandling{ + CallNotAnswerAction: dto.callHandling.callNotAnswerAction, + ForwardToExtensionID: dto.callHandling.forwardToExtensionID, + BusyOnAnotherCallAction: dto.callHandling.busyOnAnotherCallAction, + BusyForwardToExtensionID: dto.callHandling.busyForwardToExtensionID, + AllowCallersCheckVoicemail: dto.callHandling.allowCallersCheckVoicemail, + BusyRequirePress1BeforeConnecting: dto.callHandling.busyRequirePress1BeforeConnecting, + UnAnsweredRequirePress1BeforeConnecting: dto.callHandling.unAnsweredRequirePress1BeforeConnecting, + OverflowPlayCalleeVoicemailGreeting: dto.callHandling.overflowPlayCalleeVoicemailGreeting, + PlayCalleeVoicemailGreeting: dto.callHandling.playCalleeVoicemailGreeting, + BusyPlayCalleeVoicemailGreeting: dto.callHandling.busyPlayCalleeVoicemailGreeting, + PhoneNumber: dto.callHandling.phoneNumber, + PhoneNumberDescription: dto.callHandling.phoneNumberDescription, + BusyPhoneNumber: dto.callHandling.busyPhoneNumber, + BusyPhoneNumberDescription: dto.callHandling.busyPhoneNumberDescription, + ConnectToOperator: dto.callHandling.connectToOperator, + GreetingPromptID: dto.callHandling.greetingPromptID, + MaxWaitTime: dto.callHandling.maxWaitTime, + OperatorExtensionID: dto.callHandling.operatorExtensionID, + RingMode: dto.callHandling.ringMode, + } + callForwarding := lo.TernaryF(plan.CallForwarding != nil && dto.callForwarding != nil, func() *closedHoursResourceModelCallForwarding { + return &closedHoursResourceModelCallForwarding{ + RequirePress1BeforeConnecting: dto.callForwarding.requirePress1BeforeConnecting, + EnableZoomMobileApps: dto.callForwarding.enableZoomMobileApps, + EnableZoomDesktopApps: dto.callForwarding.enableZoomDesktopApps, + EnableZoomPhoneApplianceApps: dto.callForwarding.enableZoomPhoneApplianceApps, + Settings: lo.Map(dto.callForwarding.settings, func(item *readDtoCallForwardingSetting, index int) *closedHoursResourceModelCallForwardingSetting { + return &closedHoursResourceModelCallForwardingSetting{ + ID: item.id, + Description: item.description, + Enable: item.enable, + PhoneNumber: item.phoneNumber, + } + }), + } + }, func() *closedHoursResourceModelCallForwarding { + return nil + }) + return &closedHoursResourceModel{ + ExtensionID: dto.extensionID, + CallHandling: callHandling, + CallForwarding: callForwarding, + }, nil +} + +func (r *tfClosedHoursResource) sync(ctx context.Context, plan *closedHoursResourceModel, onDelete bool) error { + // closed_hours sync handling like followings + // 1. call_handling contains one setting and cannot delete it -> do PATCH + // 2. call_forwarding may contain one setting and can delete it -> do CREATE/PATCH/DELETE + + asis, err := r.read(ctx, plan) + if err != nil { + return err + } + + // 0. Validate + businessHours, err := r.crud.readBusinessHours(ctx, plan.ExtensionID) + if err != nil { + return err + } + if businessHours == nil { + if onDelete { + return nil + } + return fmt.Errorf("business_hours not found") + } + if businessHours.customHours.typ.ValueInt32() != 2 { + // closed_hours can set after setting business_hours.custom_hours.type = 2(custom_hours) + return fmt.Errorf("closed_hours can set after setting business_hours.custom_hours.type = 2, please add depends_on setting with business_hours") + } + + // 1. PATCH call_handling + patchCallHandling := &patchCallHandlingDto{ + extensionID: plan.ExtensionID, + settingType: settingTypeClosedHours, + settings: &patchCallHandlingDtoSettings{ + callNotAnswerAction: plan.CallHandling.CallNotAnswerAction, + forwardToExtensionID: plan.CallHandling.ForwardToExtensionID, + busyOnAnotherCallAction: plan.CallHandling.BusyOnAnotherCallAction, + busyForwardToExtensionID: plan.CallHandling.BusyForwardToExtensionID, + allowCallersCheckVoicemail: plan.CallHandling.AllowCallersCheckVoicemail, + busyRequirePress1BeforeConnecting: plan.CallHandling.BusyRequirePress1BeforeConnecting, + unAnsweredRequirePress1BeforeConnecting: plan.CallHandling.UnAnsweredRequirePress1BeforeConnecting, + overflowPlayCalleeVoicemailGreeting: plan.CallHandling.OverflowPlayCalleeVoicemailGreeting, + playCalleeVoicemailGreeting: plan.CallHandling.PlayCalleeVoicemailGreeting, + busyPlayCalleeVoicemailGreeting: plan.CallHandling.BusyPlayCalleeVoicemailGreeting, + phoneNumber: plan.CallHandling.PhoneNumber, + phoneNumberDescription: plan.CallHandling.PhoneNumberDescription, + busyPhoneNumber: plan.CallHandling.BusyPhoneNumber, + busyPhoneNumberDescription: plan.CallHandling.BusyPhoneNumberDescription, + connectToOperator: plan.CallHandling.ConnectToOperator, + greetingPromptID: plan.CallHandling.GreetingPromptID, + maxWaitTime: plan.CallHandling.MaxWaitTime, + operatorExtensionID: plan.CallHandling.OperatorExtensionID, + ringMode: plan.CallHandling.RingMode, + }, + } + if err = r.crud.patchCallHandling(ctx, patchCallHandling); err != nil { + return err + } + + // 2. CREATE/PATCH/DELETE call_forwarding + // 2-1: PATCH existed call forwarding + // 2-2: create new call forwarding + // 2-3: delete unused call forwarding + + // 2-1: PATCH existed call forwarding + if plan.CallForwarding != nil { + var settings []*patchCallForwardingDtoSetting + if plan.CallForwarding.Settings != nil { + settings = lo.FilterMap(plan.CallForwarding.Settings, func(item *closedHoursResourceModelCallForwardingSetting, index int) (*patchCallForwardingDtoSetting, bool) { + return &patchCallForwardingDtoSetting{ + id: item.ID, + description: item.Description, + enable: item.Enable, + phoneNumber: item.PhoneNumber, + }, item.ID.ValueString() != "" // patch only id is existed + }) + } + patchCallForwarding := &patchCallForwardingDto{ + extensionID: plan.ExtensionID, + settingType: settingTypeClosedHours, + requirePress1BeforeConnecting: plan.CallForwarding.RequirePress1BeforeConnecting, + enableZoomMobileApps: plan.CallForwarding.EnableZoomMobileApps, + enableZoomDesktopApps: plan.CallForwarding.EnableZoomDesktopApps, + enableZoomPhoneApplianceApps: plan.CallForwarding.EnableZoomPhoneApplianceApps, + settings: settings, + } + if err = r.crud.patchCallForwarding(ctx, patchCallForwarding, onDelete); err != nil { + return err + } + } else if asis.CallForwarding != nil { + patchCallForwarding := &patchCallForwardingDto{ + extensionID: plan.ExtensionID, + settingType: settingTypeClosedHours, + requirePress1BeforeConnecting: types.BoolValue(false), + enableZoomMobileApps: types.BoolValue(true), + enableZoomDesktopApps: types.BoolValue(true), + enableZoomPhoneApplianceApps: types.BoolValue(true), + settings: []*patchCallForwardingDtoSetting{}, + } + if err = r.crud.patchCallForwarding(ctx, patchCallForwarding, onDelete); err != nil { + return err + } + } + + // 2-2: create new call forwarding + if plan.CallForwarding != nil { + newCallForwardings := lo.Filter(plan.CallForwarding.Settings, func(item *closedHoursResourceModelCallForwardingSetting, index int) bool { + return item.ID.ValueString() == "" + }) + for _, newCallForwarding := range newCallForwardings { + createAllForwardingDto := &createCallForwardingDto{ + extensionID: plan.ExtensionID, + settingType: settingTypeClosedHours, + description: newCallForwarding.Description, + phoneNumber: newCallForwarding.PhoneNumber, + } + created, err := r.crud.createCallForwarding(ctx, createAllForwardingDto) + if err != nil { + return err + } + newCallForwarding.ID = created.callForwardingID + } + } + + // 2-3: delete unused call forwarding + if asis.CallForwarding != nil { + deleteCallForwardings := lo.Filter(asis.CallForwarding.Settings, func(item *closedHoursResourceModelCallForwardingSetting, index int) bool { + if plan.CallForwarding == nil { + return true + } + for _, planCallForwarding := range plan.CallForwarding.Settings { + if item.ID == planCallForwarding.ID { + return false + } + } + return true + }) + for _, setting := range deleteCallForwardings { + deleteCallForwarding := &deleteCallForwardingDto{ + extensionID: asis.ExtensionID, + settingType: settingTypeClosedHours, + callForwardingID: setting.ID, + } + if err = r.crud.deleteCallForwarding(ctx, deleteCallForwarding); err != nil { + return nil + } + } + } + + return nil +} + +func (r *tfClosedHoursResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan closedHoursResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if err := r.sync(ctx, &plan, false); err != nil { + resp.Diagnostics.AddError( + "Error creating phone call handling", + err.Error(), + ) + return + } + + // XXX zoom api doesn't return some fields after create/update, so just set plan with existed plan values + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *tfClosedHoursResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan closedHoursResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + resp.Diagnostics.AddError( + "Error updating phone call handling", + "Error updating phone call handling", + ) + return + } + + if err := r.sync(ctx, &plan, false); err != nil { + resp.Diagnostics.AddError( + "Error updating phone call handling", + fmt.Sprintf( + "Could not update phone call handling %s, unexpected error: %s", + plan.ExtensionID.ValueString(), + err, + ), + ) + return + } + + // XXX zoom api doesn't return some fields after create/update, so just set plan with existed plan values + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *tfClosedHoursResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state closedHoursResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + defaultModel := closedHoursResourceModel{ + ExtensionID: state.ExtensionID, + CallHandling: &closedHoursResourceModelCallHandling{}, + CallForwarding: &closedHoursResourceModelCallForwarding{}, + } + err := r.sync(ctx, &defaultModel, true) + if err != nil { + resp.Diagnostics.AddError( + "Error deleting phone call handling", + fmt.Sprintf( + "Could not delete phone call handling %s, unexpected error: %s", + state.ExtensionID.ValueString(), + err, + ), + ) + return + } + + tflog.Info(ctx, "deleted phone call handling", map[string]interface{}{ + "extension_id": state.ExtensionID.ValueString(), + }) +} + +func (r *tfClosedHoursResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("extension_id"), req, resp) +} diff --git a/internal/services/phone/callhandling/call_handling_crud.go b/internal/services/phone/callhandling/call_handling_crud.go new file mode 100644 index 0000000..5df9f34 --- /dev/null +++ b/internal/services/phone/callhandling/call_handling_crud.go @@ -0,0 +1,758 @@ +package callhandling + +import ( + "context" + "errors" + "fmt" + + "github.com/folio-sec/terraform-provider-zoom/generated/api/zoomphone" + "github.com/folio-sec/terraform-provider-zoom/internal/util" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/samber/lo" +) + +func newCrud(client *zoomphone.Client) *crud { + return &crud{ + client: client, + } +} + +type crud struct { + client *zoomphone.Client +} + +// Zoom phone provides default call forwarding ids for zoom phone mobile apps, zoom phone desktop apps and zoom phone appliance client +// They become different call_forwarding_id, so collect them using description. +const callForwardingDescriptionZoomMobileApps = "Zoom Mobile Apps" +const callForwardingDescriptionZoomDesktopApps = "Zoom Desktop Apps" +const callForwardingDescriptionZoomPhoneApplianceApps = "Zoom Phone Appliance Apps" + +func (c *crud) readBusinessHours(ctx context.Context, extensionID types.String) (*readDtoBusinessHours, error) { + detail, err := c.client.GetCallHandling(ctx, zoomphone.GetCallHandlingParams{ + ExtensionId: extensionID.ValueString(), + }) + if err != nil { + var status *zoomphone.ErrorResponseStatusCode + if errors.As(err, &status) { + if status.StatusCode == 400 && status.Response.Code.Value == 300 { + return nil, nil // already deleted + } + } + return nil, fmt.Errorf("unable to read phone call handling: %v", err) + } + + // BusinessHours should contain upper to one custom_hours + retCustomHour, _ := lo.Find(detail.BusinessHours, func(item zoomphone.GetCallHandlingOKBusinessHoursItem) bool { + return item.SubSettingType.Value == "custom_hours" + }) + customHours := &readDtoBusinessHoursCustomHours{ + typ: util.FromOptInt(retCustomHour.Settings.Value.Type), + allowMembersToReset: util.FromOptBool(retCustomHour.Settings.Value.AllowMembersToReset), + settings: lo.Map(retCustomHour.Settings.Value.CustomHoursSettings, func(item zoomphone.GetCallHandlingOKBusinessHoursItemSettingsCustomHoursSettingsItem, index int) *readDtoBusinessHoursCustomHoursSetting { + return &readDtoBusinessHoursCustomHoursSetting{ + weekday: util.FromOptInt(item.Weekday), + typ: util.FromOptInt(item.Type), + from: util.FromOptString(item.From), + to: util.FromOptString(item.To), + } + }), + } + + // BusinessHours should contain upper to one call_handling. + retCallHandling, _ := lo.Find(detail.BusinessHours, func(item zoomphone.GetCallHandlingOKBusinessHoursItem) bool { + return item.SubSettingType.Value == "call_handling" + }) + forwardToExtensionID := types.StringNull() + if len(retCallHandling.Settings.Value.CallForwardingSettings) > 0 { + forwardToExtensionID = util.FromOptString(retCallHandling.Settings.Value.CallForwardingSettings[0].ID) + } + callDistribution := &readDtoBusinessHoursCallHandlingCallDistribution{ + handleMultipleCalls: util.FromOptBool(retCallHandling.Settings.Value.CallDistribution.Value.HandleMultipleCalls), + ringDuration: util.FromOptInt(retCallHandling.Settings.Value.CallDistribution.Value.RingDuration), + ringMode: util.FromOptString(retCallHandling.Settings.Value.CallDistribution.Value.RingMode), + skipOfflineDevicePhoneNumber: util.FromOptBool(retCallHandling.Settings.Value.CallDistribution.Value.SkipOfflineDevicePhoneNumber), + } + tflog.Info(ctx, "readBusinessHours", map[string]interface{}{ + "retCallHandling": retCallHandling, + "settings": retCallHandling.Settings, + "receiveCall": retCallHandling.Settings.Value.ReceiveCall.Value, + }) + callHandling := &readDtoBusinessHoursCallHandling{ + callNotAnswerAction: util.FromOptInt(retCallHandling.Settings.Value.CallNotAnswerAction), + forwardToExtensionID: forwardToExtensionID, + busyOnAnotherCallAction: util.FromOptInt(retCallHandling.Settings.Value.BusyRouting.Value.Action), + busyForwardToExtensionID: util.FromOptString(retCallHandling.Settings.Value.BusyRouting.Value.ForwardTo.Value.ExtensionID), + allowCallersCheckVoicemail: util.FromOptBool(retCallHandling.Settings.Value.AllowCallersCheckVoicemail), + allowMembersToReset: util.FromOptBool(retCallHandling.Settings.Value.AllowMembersToReset), + audioWhileConnectingID: util.FromOptString(retCallHandling.Settings.Value.AudioWhileConnecting.Value.ID), + callDistribution: callDistribution, + busyRequirePress1BeforeConnecting: util.FromOptBool(retCallHandling.Settings.Value.BusyRouting.Value.RequirePress1BeforeConnecting), + unAnsweredRequirePress1BeforeConnecting: util.FromOptBool(retCallHandling.Settings.Value.Routing.Value.RequirePress1BeforeConnecting), + overflowPlayCalleeVoicemailGreeting: util.FromOptBool(retCallHandling.Settings.Value.Routing.Value.OverflowPlayCalleeVoicemailGreeting), + playCalleeVoicemailGreeting: util.FromOptBool(retCallHandling.Settings.Value.Routing.Value.OverflowPlayCalleeVoicemailGreeting), + busyPlayCalleeVoicemailGreeting: util.FromOptBool(retCallHandling.Settings.Value.BusyRouting.Value.PlayCalleeVoicemailGreeting), + phoneNumber: util.FromOptString(retCallHandling.Settings.Value.Routing.Value.ForwardTo.Value.PhoneNumber), + phoneNumberDescription: util.FromOptString(retCallHandling.Settings.Value.Routing.Value.ForwardTo.Value.Description), + busyPhoneNumber: util.FromOptString(retCallHandling.Settings.Value.BusyRouting.Value.ForwardTo.Value.PhoneNumber), + busyPhoneNumberDescription: util.FromOptString(retCallHandling.Settings.Value.BusyRouting.Value.ForwardTo.Value.Description), + connectToOperator: util.FromOptBool(retCallHandling.Settings.Value.ConnectToOperator), + greetingPromptID: util.FromOptString(retCallHandling.Settings.Value.GreetingPrompt.Value.ID), + maxCallInQueue: util.FromOptInt(retCallHandling.Settings.Value.MaxCallInQueue), + maxWaitTime: util.FromOptInt(retCallHandling.Settings.Value.MaxWaitTime), + musicOnHoldID: util.FromOptString(retCallHandling.Settings.Value.MusicOnHold.Value.ID), + operatorExtensionID: util.FromOptString(retCallHandling.Settings.Value.Routing.Value.Operator.Value.ExtensionID), + receiveCall: util.FromOptBool(retCallHandling.Settings.Value.ReceiveCall), + ringMode: util.FromOptString(retCallHandling.Settings.Value.RingMode), + voiceMailGreetingID: util.FromOptString(retCallHandling.Settings.Value.GreetingPrompt.Value.ID), + wrapUpTime: util.FromOptInt(retCallHandling.Settings.Value.WrapUpTime), + } + + // BusinessHours should contain upper to one call_forwarding. + var callForwarding *readDtoHolidayHoursCallForwarding + retCallForwarding, ok := lo.Find(detail.BusinessHours, func(item zoomphone.GetCallHandlingOKBusinessHoursItem) bool { + return item.SubSettingType.Value == "call_forwarding" + }) + if ok { + enableZoomMobileApps, enableZoomDesktopApps, enableZoomPhoneApplianceApps := types.BoolNull(), types.BoolNull(), types.BoolNull() + zoomPhoneMobileAppsItem, okMobile := lo.Find(retCallForwarding.Settings.Value.CallForwardingSettings, func(item zoomphone.GetCallHandlingOKBusinessHoursItemSettingsCallForwardingSettingsItem) bool { + return item.Description.Value == callForwardingDescriptionZoomMobileApps + }) + if okMobile { + enableZoomMobileApps = util.FromOptBool(zoomPhoneMobileAppsItem.Enable) + } + zoomPhoneDesktopAppsItem, okDesktop := lo.Find(retCallForwarding.Settings.Value.CallForwardingSettings, func(item zoomphone.GetCallHandlingOKBusinessHoursItemSettingsCallForwardingSettingsItem) bool { + return item.Description.Value == callForwardingDescriptionZoomDesktopApps + }) + if okDesktop { + enableZoomDesktopApps = util.FromOptBool(zoomPhoneDesktopAppsItem.Enable) + } + zoomPhoneApplianceAppsItem, okAppliance := lo.Find(retCallForwarding.Settings.Value.CallForwardingSettings, func(item zoomphone.GetCallHandlingOKBusinessHoursItemSettingsCallForwardingSettingsItem) bool { + return item.Description.Value == callForwardingDescriptionZoomPhoneApplianceApps + }) + if okAppliance { + enableZoomPhoneApplianceApps = util.FromOptBool(zoomPhoneApplianceAppsItem.Enable) + } + callForwarding = &readDtoHolidayHoursCallForwarding{ + requirePress1BeforeConnecting: util.FromOptBool(retCallForwarding.Settings.Value.RequirePress1BeforeConnecting), + enableZoomMobileApps: enableZoomMobileApps, + enableZoomDesktopApps: enableZoomDesktopApps, + enableZoomPhoneApplianceApps: enableZoomPhoneApplianceApps, + settings: lo.FilterMap(retCallForwarding.Settings.Value.CallForwardingSettings, func(item zoomphone.GetCallHandlingOKBusinessHoursItemSettingsCallForwardingSettingsItem, index int) (*readDtoCallForwardingSetting, bool) { + description := item.Description.Value + // Zoom provided call_forwarding setting should be ignored (they are managed by enableZoomMobileApps and so on) + isManaged := (description != callForwardingDescriptionZoomMobileApps && + description != callForwardingDescriptionZoomDesktopApps && + description != callForwardingDescriptionZoomPhoneApplianceApps) && item.PhoneNumber.IsSet() + return &readDtoCallForwardingSetting{ + id: util.FromOptString(item.ID), + description: util.FromOptString(item.Description), + enable: util.FromOptBool(item.Enable), + phoneNumber: util.FromOptString(item.PhoneNumber), + externalContact: &readDtoCallForwardingSettingsExternalContact{ + externalContactID: util.FromOptString(item.ExternalContact.Value.ExternalContactID), + }, + }, isManaged + }), + } + } + + return &readDtoBusinessHours{ + extensionID: extensionID, + customHours: customHours, + callHandling: callHandling, + callForwarding: callForwarding, + }, nil +} + +func (c *crud) readClosedHours(ctx context.Context, extensionID types.String) (*readDtoClosedHours, error) { + detail, err := c.client.GetCallHandling(ctx, zoomphone.GetCallHandlingParams{ + ExtensionId: extensionID.ValueString(), + }) + if err != nil { + var status *zoomphone.ErrorResponseStatusCode + if errors.As(err, &status) { + if status.StatusCode == 400 && status.Response.Code.Value == 300 { + return nil, nil // already deleted + } + } + return nil, fmt.Errorf("unable to read phone call handling: %v", err) + } + + // ClosedHours should contain upper to one call_handling. + retCallHandling, _ := lo.Find(detail.ClosedHours, func(item zoomphone.GetCallHandlingOKClosedHoursItem) bool { + return item.SubSettingType.Value == "call_handling" + }) + forwardToExtensionID := types.StringNull() + if len(retCallHandling.Settings.Value.CallForwardingSettings) > 0 { + forwardToExtensionID = util.FromOptString(retCallHandling.Settings.Value.CallForwardingSettings[0].ID) + } + callHandling := &readDtoClosedHoursCallHandling{ + callNotAnswerAction: util.FromOptInt(retCallHandling.Settings.Value.CallNotAnswerAction), + forwardToExtensionID: forwardToExtensionID, + busyOnAnotherCallAction: util.FromOptInt(retCallHandling.Settings.Value.BusyRouting.Value.Action), + busyForwardToExtensionID: util.FromOptString(retCallHandling.Settings.Value.BusyRouting.Value.ForwardTo.Value.ExtensionID), + allowCallersCheckVoicemail: util.FromOptBool(retCallHandling.Settings.Value.AllowCallersCheckVoicemail), + busyRequirePress1BeforeConnecting: util.FromOptBool(retCallHandling.Settings.Value.BusyRouting.Value.RequirePress1BeforeConnecting), + unAnsweredRequirePress1BeforeConnecting: util.FromOptBool(retCallHandling.Settings.Value.Routing.Value.RequirePress1BeforeConnecting), + overflowPlayCalleeVoicemailGreeting: util.FromOptBool(retCallHandling.Settings.Value.Routing.Value.OverflowPlayCalleeVoicemailGreeting), + playCalleeVoicemailGreeting: util.FromOptBool(retCallHandling.Settings.Value.Routing.Value.OverflowPlayCalleeVoicemailGreeting), + busyPlayCalleeVoicemailGreeting: util.FromOptBool(retCallHandling.Settings.Value.BusyRouting.Value.PlayCalleeVoicemailGreeting), + phoneNumber: util.FromOptString(retCallHandling.Settings.Value.Routing.Value.ForwardTo.Value.PhoneNumber), + phoneNumberDescription: util.FromOptString(retCallHandling.Settings.Value.Routing.Value.ForwardTo.Value.Description), + busyPhoneNumber: util.FromOptString(retCallHandling.Settings.Value.BusyRouting.Value.ForwardTo.Value.PhoneNumber), + busyPhoneNumberDescription: util.FromOptString(retCallHandling.Settings.Value.BusyRouting.Value.ForwardTo.Value.Description), + connectToOperator: util.FromOptBool(retCallHandling.Settings.Value.ConnectToOperator), + maxWaitTime: util.FromOptInt(retCallHandling.Settings.Value.MaxWaitTime), + operatorExtensionID: util.FromOptString(retCallHandling.Settings.Value.Routing.Value.Operator.Value.ExtensionID), + ringMode: util.FromOptString(retCallHandling.Settings.Value.RingMode), + } + + // ClosedHours should contain upper to one call_forwarding. + var callForwarding *readDtoHolidayHoursCallForwarding + retCallForwarding, ok := lo.Find(detail.ClosedHours, func(item zoomphone.GetCallHandlingOKClosedHoursItem) bool { + return item.SubSettingType.Value == "call_forwarding" + }) + if ok { + enableZoomMobileApps, enableZoomDesktopApps, enableZoomPhoneApplianceApps := types.BoolNull(), types.BoolNull(), types.BoolNull() + zoomPhoneMobileAppsItem, okMobile := lo.Find(retCallForwarding.Settings.Value.CallForwardingSettings, func(item zoomphone.GetCallHandlingOKClosedHoursItemSettingsCallForwardingSettingsItem) bool { + return item.Description.Value == callForwardingDescriptionZoomMobileApps + }) + if okMobile { + enableZoomMobileApps = util.FromOptBool(zoomPhoneMobileAppsItem.Enable) + } + zoomPhoneDesktopAppsItem, okDesktop := lo.Find(retCallForwarding.Settings.Value.CallForwardingSettings, func(item zoomphone.GetCallHandlingOKClosedHoursItemSettingsCallForwardingSettingsItem) bool { + return item.Description.Value == callForwardingDescriptionZoomDesktopApps + }) + if okDesktop { + enableZoomDesktopApps = util.FromOptBool(zoomPhoneDesktopAppsItem.Enable) + } + zoomPhoneApplianceAppsItem, okAppliance := lo.Find(retCallForwarding.Settings.Value.CallForwardingSettings, func(item zoomphone.GetCallHandlingOKClosedHoursItemSettingsCallForwardingSettingsItem) bool { + return item.Description.Value == callForwardingDescriptionZoomPhoneApplianceApps + }) + if okAppliance { + enableZoomPhoneApplianceApps = util.FromOptBool(zoomPhoneApplianceAppsItem.Enable) + } + callForwarding = &readDtoHolidayHoursCallForwarding{ + requirePress1BeforeConnecting: util.FromOptBool(retCallForwarding.Settings.Value.RequirePress1BeforeConnecting), + enableZoomMobileApps: enableZoomMobileApps, + enableZoomDesktopApps: enableZoomDesktopApps, + enableZoomPhoneApplianceApps: enableZoomPhoneApplianceApps, + settings: lo.FilterMap(retCallForwarding.Settings.Value.CallForwardingSettings, func(item zoomphone.GetCallHandlingOKClosedHoursItemSettingsCallForwardingSettingsItem, index int) (*readDtoCallForwardingSetting, bool) { + description := item.Description.Value + // Zoom provided call_forwarding setting should be ignored (they are managed by enableZoomMobileApps and so on) + isManaged := (description != callForwardingDescriptionZoomMobileApps && + description != callForwardingDescriptionZoomDesktopApps && + description != callForwardingDescriptionZoomPhoneApplianceApps) && item.PhoneNumber.IsSet() + return &readDtoCallForwardingSetting{ + id: util.FromOptString(item.ID), + description: util.FromOptString(item.Description), + enable: util.FromOptBool(item.Enable), + phoneNumber: util.FromOptString(item.PhoneNumber), + externalContact: &readDtoCallForwardingSettingsExternalContact{ + externalContactID: util.FromOptString(item.ExternalContact.Value.ExternalContactID), + }, + }, isManaged + }), + } + } + return &readDtoClosedHours{ + extensionID: extensionID, + callHandling: callHandling, + callForwarding: callForwarding, + }, nil +} + +func (c *crud) readHolidayHours(ctx context.Context, extensionID, holidayID types.String) (*readDtoHolidayHours, error) { + detail, err := c.client.GetCallHandling(ctx, zoomphone.GetCallHandlingParams{ + ExtensionId: extensionID.ValueString(), + }) + if err != nil { + var status *zoomphone.ErrorResponseStatusCode + if errors.As(err, &status) { + if status.StatusCode == 400 && status.Response.Code.Value == 300 { + return nil, nil // already deleted + } + } + return nil, fmt.Errorf("unable to read phone call handling: %v", err) + } + + // holiday may contain multiple settings, so filtered by holiday id + target, ok := lo.Find(detail.HolidayHours, func(item zoomphone.GetCallHandlingOKHolidayHoursItem) bool { + return item.HolidayID.Value == holidayID.ValueString() + }) + if !ok { + return nil, nil // not existed + } + + // target should contain upper to one holiday setting + retHoliday, _ := lo.Find(target.Details, func(item zoomphone.GetCallHandlingOKHolidayHoursItemDetailsItem) bool { + return item.SubSettingType.Value == "holiday" + }) + holiday := &readDtoHolidayHoursHoliday{ + name: util.FromOptString(retHoliday.Settings.Value.Name), + from: util.FromOptDateTime(retHoliday.Settings.Value.From), + to: util.FromOptDateTime(retHoliday.Settings.Value.To), + } + + // target should contain upper to one call_handling setting + retCallHandling, _ := lo.Find(target.Details, func(item zoomphone.GetCallHandlingOKHolidayHoursItemDetailsItem) bool { + return item.SubSettingType.Value == "call_handling" + }) + forwardToExtensionID := types.StringNull() + if len(retCallHandling.Settings.Value.CallForwardingSettings) > 0 { + forwardToExtensionID = util.FromOptString(retCallHandling.Settings.Value.CallForwardingSettings[0].ID) + } + callHandling := &readDtoHolidayHoursCallHandling{ + callNotAnswerAction: util.FromOptInt(retCallHandling.Settings.Value.CallNotAnswerAction), + forwardToExtensionID: forwardToExtensionID, + allowCallersCheckVoicemail: util.FromOptBool(retCallHandling.Settings.Value.AllowCallersCheckVoicemail), + unAnsweredRequirePress1BeforeConnecting: util.FromOptBool(retCallHandling.Settings.Value.Routing.Value.RequirePress1BeforeConnecting), + overflowPlayCalleeVoicemailGreeting: util.FromOptBool(retCallHandling.Settings.Value.Routing.Value.OverflowPlayCalleeVoicemailGreeting), + playCalleeVoicemailGreeting: util.FromOptBool(retCallHandling.Settings.Value.Routing.Value.OverflowPlayCalleeVoicemailGreeting), + phoneNumber: util.FromOptString(retCallHandling.Settings.Value.Routing.Value.ForwardTo.Value.PhoneNumber), + phoneNumberDescription: util.FromOptString(retCallHandling.Settings.Value.Routing.Value.ForwardTo.Value.Description), + connectToOperator: util.FromOptBool(retCallHandling.Settings.Value.ConnectToOperator), + maxWaitTime: util.FromOptInt(retCallHandling.Settings.Value.MaxWaitTime), + operatorExtensionID: util.FromOptString(retCallHandling.Settings.Value.Routing.Value.Operator.Value.ExtensionID), + ringMode: util.FromOptString(retCallHandling.Settings.Value.RingMode), + } + + // target should contain upper to one call_forwarding setting + var callForwarding *readDtoHolidayHoursCallForwarding + retCallForwarding, ok := lo.Find(target.Details, func(item zoomphone.GetCallHandlingOKHolidayHoursItemDetailsItem) bool { + return item.SubSettingType.Value == "call_forwarding" + }) + if ok { + enableZoomMobileApps, enableZoomDesktopApps, enableZoomPhoneApplianceApps := types.BoolNull(), types.BoolNull(), types.BoolNull() + zoomPhoneMobileAppsItem, okMobile := lo.Find(retCallForwarding.Settings.Value.CallForwardingSettings, func(item zoomphone.GetCallHandlingOKHolidayHoursItemDetailsItemSettingsCallForwardingSettingsItem) bool { + return item.Description.Value == callForwardingDescriptionZoomMobileApps + }) + if okMobile { + enableZoomMobileApps = util.FromOptBool(zoomPhoneMobileAppsItem.Enable) + } + zoomPhoneDesktopAppsItem, okDesktop := lo.Find(retCallForwarding.Settings.Value.CallForwardingSettings, func(item zoomphone.GetCallHandlingOKHolidayHoursItemDetailsItemSettingsCallForwardingSettingsItem) bool { + return item.Description.Value == callForwardingDescriptionZoomDesktopApps + }) + if okDesktop { + enableZoomDesktopApps = util.FromOptBool(zoomPhoneDesktopAppsItem.Enable) + } + zoomPhoneApplianceAppsItem, okAppliance := lo.Find(retCallForwarding.Settings.Value.CallForwardingSettings, func(item zoomphone.GetCallHandlingOKHolidayHoursItemDetailsItemSettingsCallForwardingSettingsItem) bool { + return item.Description.Value == callForwardingDescriptionZoomPhoneApplianceApps + }) + if okAppliance { + enableZoomPhoneApplianceApps = util.FromOptBool(zoomPhoneApplianceAppsItem.Enable) + } + callForwarding = &readDtoHolidayHoursCallForwarding{ + requirePress1BeforeConnecting: util.FromOptBool(retCallForwarding.Settings.Value.RequirePress1BeforeConnecting), + enableZoomMobileApps: enableZoomMobileApps, + enableZoomDesktopApps: enableZoomDesktopApps, + enableZoomPhoneApplianceApps: enableZoomPhoneApplianceApps, + settings: lo.FilterMap(retCallForwarding.Settings.Value.CallForwardingSettings, func(item zoomphone.GetCallHandlingOKHolidayHoursItemDetailsItemSettingsCallForwardingSettingsItem, index int) (*readDtoCallForwardingSetting, bool) { + description := item.Description.Value + // Zoom provided call_forwarding setting should be ignored (they are managed by enableZoomMobileApps and so on) + isManaged := (description != callForwardingDescriptionZoomMobileApps && + description != callForwardingDescriptionZoomDesktopApps && + description != callForwardingDescriptionZoomPhoneApplianceApps) && item.PhoneNumber.IsSet() + return &readDtoCallForwardingSetting{ + id: util.FromOptString(item.ID), + description: util.FromOptString(item.Description), + enable: util.FromOptBool(item.Enable), + phoneNumber: util.FromOptString(item.PhoneNumber), + externalContact: &readDtoCallForwardingSettingsExternalContact{ + externalContactID: util.FromOptString(item.ExternalContact.Value.ExternalContactID), + }, + }, isManaged + }), + } + } + + return &readDtoHolidayHours{ + extensionID: extensionID, + holiday: holiday, + callHandling: callHandling, + callForwarding: callForwarding, + }, nil +} + +func (c *crud) createHoliday(ctx context.Context, dto *createHolidayDto) (*createdHolidayDto, error) { + res, err := c.client.AddCallHandling(ctx, zoomphone.OptAddCallHandlingReq{ + Value: zoomphone.AddCallHandlingReq{ + Type: zoomphone.PostCallHandlingSettingsHolidayAddCallHandlingReq, + PostCallHandlingSettingsHoliday: zoomphone.PostCallHandlingSettingsHoliday{ + Settings: zoomphone.OptPostCallHandlingSettingsHolidaySettings{ + Value: zoomphone.PostCallHandlingSettingsHolidaySettings{ + Name: util.ToPhoneOptString(dto.name), + From: util.ToPhoneOptDateTime(dto.from), + To: util.ToPhoneOptDateTime(dto.to), + }, + Set: true, + }, + SubSettingType: zoomphone.NewOptString("holiday"), + }, + }, + Set: true, + }, zoomphone.AddCallHandlingParams{ + ExtensionId: dto.extensionID.ValueString(), + SettingType: string(dto.settingType), + }) + if err != nil { + return nil, fmt.Errorf("error creating phone call handling on holiday: %v", err) + } + + return &createdHolidayDto{ + holidayID: util.FromOptString(res.AddCallHandlingCreated1.HolidayID), + }, nil +} + +func (c *crud) createCallForwarding(ctx context.Context, dto *createCallForwardingDto) (*createdCallForwardingDto, error) { + res, err := c.client.AddCallHandling(ctx, zoomphone.OptAddCallHandlingReq{ + Value: zoomphone.AddCallHandlingReq{ + Type: zoomphone.PostCallHandlingSettingsCallForwardingAddCallHandlingReq, + PostCallHandlingSettingsCallForwarding: zoomphone.PostCallHandlingSettingsCallForwarding{ + Settings: zoomphone.OptPostCallHandlingSettingsCallForwardingSettings{ + Value: zoomphone.PostCallHandlingSettingsCallForwardingSettings{ + HolidayID: util.ToPhoneOptString(dto.holidayID), + Description: util.ToPhoneOptString(dto.description), + PhoneNumber: util.ToPhoneOptString(dto.phoneNumber), + }, + Set: true, + }, + SubSettingType: zoomphone.NewOptString("call_forwarding"), + }, + }, + Set: true, + }, zoomphone.AddCallHandlingParams{ + ExtensionId: dto.extensionID.ValueString(), + SettingType: string(dto.settingType), + }) + if err != nil { + return nil, fmt.Errorf("error creating phone call handling on call forwarding: %v", err) + } + + return &createdCallForwardingDto{ + callForwardingID: util.FromOptString(res.AddCallHandlingCreated0.CallForwardingID), + }, nil +} + +func (c *crud) patchCustomHours(ctx context.Context, dto *patchCustomHoursDto) error { + err := c.client.UpdateCallHandling(ctx, zoomphone.OptUpdateCallHandlingReq{ + Value: zoomphone.UpdateCallHandlingReq{ + Type: zoomphone.PatchCallHandlingSettingsCustomHoursUpdateCallHandlingReq, + PatchCallHandlingSettingsCustomHours: zoomphone.PatchCallHandlingSettingsCustomHours{ + Settings: zoomphone.OptPatchCallHandlingSettingsCustomHoursSettings{ + Value: zoomphone.PatchCallHandlingSettingsCustomHoursSettings{ + AllowMembersToReset: util.ToPhoneOptBool(dto.allowMembersToReset), + CustomHoursSettings: lo.Map(dto.settings, func(item *patchCustomHoursDtoSetting, index int) zoomphone.PatchCallHandlingSettingsCustomHoursSettingsCustomHoursSettingsItem { + return zoomphone.PatchCallHandlingSettingsCustomHoursSettingsCustomHoursSettingsItem{ + From: util.ToPhoneOptString(item.from), + To: util.ToPhoneOptString(item.to), + Type: util.ToPhoneOptInt(item.typ), + Weekday: util.ToPhoneOptInt(item.weekday), + } + }), + Type: util.ToPhoneOptInt(dto.typ), + }, + Set: true, + }, + SubSettingType: zoomphone.NewOptString("custom_hours"), + }, + }, + Set: true, + }, zoomphone.UpdateCallHandlingParams{ + ExtensionId: dto.extensionID.ValueString(), + SettingType: string(dto.settingType), + }) + if err != nil { + return fmt.Errorf("error patching phone call handling on custom hour: %v", err) + } + + return nil +} + +func (c *crud) patchCallHandling(ctx context.Context, dto *patchCallHandlingDto) error { + err := c.client.UpdateCallHandling(ctx, zoomphone.OptUpdateCallHandlingReq{ + Value: zoomphone.UpdateCallHandlingReq{ + Type: zoomphone.PatchCallHandlingSettingsCallHandlingUpdateCallHandlingReq, + PatchCallHandlingSettingsCallHandling: zoomphone.PatchCallHandlingSettingsCallHandling{ + Settings: zoomphone.OptPatchCallHandlingSettingsCallHandlingSettings{ + Value: zoomphone.PatchCallHandlingSettingsCallHandlingSettings{ + HolidayID: util.ToPhoneOptString(dto.settings.holidayID), + AllowCallersCheckVoicemail: util.ToPhoneOptBool(dto.settings.allowCallersCheckVoicemail), + AllowMembersToReset: util.ToPhoneOptBool(dto.settings.allowMembersToReset), + AudioWhileConnectingID: util.ToPhoneOptString(dto.settings.audioWhileConnectingID), + CallDistribution: lo.TernaryF(dto.settings.callDistribution != nil, func() zoomphone.OptPatchCallHandlingSettingsCallHandlingSettingsCallDistribution { + return zoomphone.OptPatchCallHandlingSettingsCallHandlingSettingsCallDistribution{ + Value: zoomphone.PatchCallHandlingSettingsCallHandlingSettingsCallDistribution{ + HandleMultipleCalls: util.ToPhoneOptBool(dto.settings.callDistribution.handleMultipleCalls), + RingDuration: util.ToPhoneOptInt(dto.settings.callDistribution.ringDuration), + RingMode: util.ToPhoneOptString(dto.settings.callDistribution.ringMode), + SkipOfflineDevicePhoneNumber: util.ToPhoneOptBool(dto.settings.callDistribution.skipOfflineDevicePhoneNumber), + }, + Set: true, + } + }, func() zoomphone.OptPatchCallHandlingSettingsCallHandlingSettingsCallDistribution { + return zoomphone.OptPatchCallHandlingSettingsCallHandlingSettingsCallDistribution{} + }), + CallNotAnswerAction: util.ToPhoneOptInt(dto.settings.callNotAnswerAction), + BusyOnAnotherCallAction: util.ToPhoneOptInt(dto.settings.busyOnAnotherCallAction), + BusyRequirePress1BeforeConnecting: util.ToPhoneOptBool(dto.settings.busyRequirePress1BeforeConnecting), + UnAnsweredRequirePress1BeforeConnecting: util.ToPhoneOptBool(dto.settings.unAnsweredRequirePress1BeforeConnecting), + OverflowPlayCalleeVoicemailGreeting: util.ToPhoneOptBool(dto.settings.overflowPlayCalleeVoicemailGreeting), + PlayCalleeVoicemailGreeting: util.ToPhoneOptBool(dto.settings.playCalleeVoicemailGreeting), + BusyPlayCalleeVoicemailGreeting: util.ToPhoneOptBool(dto.settings.busyPlayCalleeVoicemailGreeting), + PhoneNumber: util.ToPhoneOptString(dto.settings.phoneNumber), + Description: util.ToPhoneOptString(dto.settings.phoneNumberDescription), + BusyPhoneNumber: util.ToPhoneOptString(dto.settings.busyPhoneNumber), + BusyDescription: util.ToPhoneOptString(dto.settings.busyPhoneNumberDescription), + ConnectToOperator: util.ToPhoneOptBool(dto.settings.connectToOperator), + ForwardToExtensionID: util.ToPhoneOptString(dto.settings.forwardToExtensionID), + BusyForwardToExtensionID: util.ToPhoneOptString(dto.settings.busyForwardToExtensionID), + GreetingPromptID: util.ToPhoneOptString(dto.settings.greetingPromptID), + MaxCallInQueue: util.ToPhoneOptInt(dto.settings.maxCallInQueue), + MaxWaitTime: util.ToPhoneOptInt(dto.settings.maxWaitTime), + MusicOnHoldID: util.ToPhoneOptString(dto.settings.musicOnHoldID), + OperatorExtensionID: util.ToPhoneOptString(dto.settings.operatorExtensionID), + ReceiveCall: util.ToPhoneOptBool(dto.settings.receiveCall), + RingMode: util.ToPhoneOptString(dto.settings.ringMode), + VoicemailGreetingID: util.ToPhoneOptString(dto.settings.voiceMailGreetingID), + WrapUpTime: util.ToPhoneOptInt(dto.settings.wrapUpTime), + }, + Set: true, + }, + SubSettingType: zoomphone.NewOptString("call_handling"), + }, + }, + Set: true, + }, zoomphone.UpdateCallHandlingParams{ + ExtensionId: dto.extensionID.ValueString(), + SettingType: string(dto.settingType), + }) + if err != nil { + return fmt.Errorf("error patching phone call handling on call handling: %v", err) + } + + return nil +} + +func (c *crud) patchHoliday(ctx context.Context, dto *patchHolidayDto) error { + err := c.client.UpdateCallHandling(ctx, zoomphone.OptUpdateCallHandlingReq{ + Value: zoomphone.UpdateCallHandlingReq{ + Type: zoomphone.PatchCallHandlingSettingsHolidayUpdateCallHandlingReq, + PatchCallHandlingSettingsHoliday: zoomphone.PatchCallHandlingSettingsHoliday{ + Settings: zoomphone.OptPatchCallHandlingSettingsHolidaySettings{ + Value: zoomphone.PatchCallHandlingSettingsHolidaySettings{ + HolidayID: util.ToPhoneOptString(dto.holidayID), + Name: util.ToPhoneOptString(dto.name), + From: util.ToPhoneOptDateTime(dto.from), + To: util.ToPhoneOptDateTime(dto.to), + }, + Set: true, + }, + SubSettingType: zoomphone.NewOptString("holiday"), + }, + }, + Set: true, + }, zoomphone.UpdateCallHandlingParams{ + ExtensionId: dto.extensionID.ValueString(), + SettingType: string(dto.settingType), + }) + if err != nil { + return fmt.Errorf("error patching phone call handling custom hour: %v", err) + } + + return nil +} + +func (c *crud) patchCallForwarding(ctx context.Context, dto *patchCallForwardingDto, onDelete bool) error { + // to patch call forwarding, collect zoom predefined call forwarding settings + // such as Zoom Mobile Apps, Zoom Desktop Apps, Zoom Phone Appliance Apps + detail, err := c.client.GetCallHandling(ctx, zoomphone.GetCallHandlingParams{ + ExtensionId: dto.extensionID.ValueString(), + }) + if err != nil { + return err + } + var callForwardingSettings []zoomphone.PatchCallHandlingSettingsCallForwardingSettingsCallForwardingSettingsItem + switch dto.settingType { + case settingTypeBusinessHours: + callForwarding, ok := lo.Find(detail.BusinessHours, func(item zoomphone.GetCallHandlingOKBusinessHoursItem) bool { + return item.SubSettingType.Value == "call_forwarding" + }) + if !ok { + if onDelete { + return nil + } + return fmt.Errorf("call_fowarding not found on business hours") + } + for _, setting := range callForwarding.Settings.Value.CallForwardingSettings { + desc := setting.Description.Value + if desc == callForwardingDescriptionZoomMobileApps { + callForwardingSettings = append(callForwardingSettings, zoomphone.PatchCallHandlingSettingsCallForwardingSettingsCallForwardingSettingsItem{ + ID: setting.ID, + Enable: util.ToPhoneOptBool(dto.enableZoomMobileApps), + }) + } else if desc == callForwardingDescriptionZoomDesktopApps { + callForwardingSettings = append(callForwardingSettings, zoomphone.PatchCallHandlingSettingsCallForwardingSettingsCallForwardingSettingsItem{ + ID: setting.ID, + Enable: util.ToPhoneOptBool(dto.enableZoomDesktopApps), + }) + } else if desc == callForwardingDescriptionZoomPhoneApplianceApps { + callForwardingSettings = append(callForwardingSettings, zoomphone.PatchCallHandlingSettingsCallForwardingSettingsCallForwardingSettingsItem{ + ID: setting.ID, + Enable: util.ToPhoneOptBool(dto.enableZoomPhoneApplianceApps), + }) + } + } + break + case settingTypeClosedHours: + callForwarding, ok := lo.Find(detail.ClosedHours, func(item zoomphone.GetCallHandlingOKClosedHoursItem) bool { + return item.SubSettingType.Value == "call_forwarding" + }) + if !ok { + if onDelete { + return nil + } + return fmt.Errorf("call_fowarding not found on closed hours") + } + for _, setting := range callForwarding.Settings.Value.CallForwardingSettings { + desc := setting.Description.Value + if desc == callForwardingDescriptionZoomMobileApps { + callForwardingSettings = append(callForwardingSettings, zoomphone.PatchCallHandlingSettingsCallForwardingSettingsCallForwardingSettingsItem{ + ID: setting.ID, + Enable: util.ToPhoneOptBool(dto.enableZoomMobileApps), + }) + } else if desc == callForwardingDescriptionZoomDesktopApps { + callForwardingSettings = append(callForwardingSettings, zoomphone.PatchCallHandlingSettingsCallForwardingSettingsCallForwardingSettingsItem{ + ID: setting.ID, + Enable: util.ToPhoneOptBool(dto.enableZoomDesktopApps), + }) + } else if desc == callForwardingDescriptionZoomPhoneApplianceApps { + callForwardingSettings = append(callForwardingSettings, zoomphone.PatchCallHandlingSettingsCallForwardingSettingsCallForwardingSettingsItem{ + ID: setting.ID, + Enable: util.ToPhoneOptBool(dto.enableZoomPhoneApplianceApps), + }) + } + } + break + case settingTypeHolidayHours: + holiday, ok := lo.Find(detail.HolidayHours, func(item zoomphone.GetCallHandlingOKHolidayHoursItem) bool { + return item.HolidayID.Value == dto.holidayID.ValueString() + }) + if !ok { + if onDelete { + return nil + } + return fmt.Errorf("holiday setting not found on holiday hours") + } + callForwarding, ok := lo.Find(holiday.Details, func(item zoomphone.GetCallHandlingOKHolidayHoursItemDetailsItem) bool { + return item.SubSettingType.Value == "call_forwarding" + }) + if !ok { + if onDelete { + return nil + } + return fmt.Errorf("call_fowarding not found on holiday hours") + } + for _, setting := range callForwarding.Settings.Value.CallForwardingSettings { + desc := setting.Description.Value + if desc == callForwardingDescriptionZoomMobileApps { + callForwardingSettings = append(callForwardingSettings, zoomphone.PatchCallHandlingSettingsCallForwardingSettingsCallForwardingSettingsItem{ + ID: setting.ID, + Enable: util.ToPhoneOptBool(dto.enableZoomMobileApps), + }) + } else if desc == callForwardingDescriptionZoomDesktopApps { + callForwardingSettings = append(callForwardingSettings, zoomphone.PatchCallHandlingSettingsCallForwardingSettingsCallForwardingSettingsItem{ + ID: setting.ID, + Enable: util.ToPhoneOptBool(dto.enableZoomDesktopApps), + }) + } else if desc == callForwardingDescriptionZoomPhoneApplianceApps { + callForwardingSettings = append(callForwardingSettings, zoomphone.PatchCallHandlingSettingsCallForwardingSettingsCallForwardingSettingsItem{ + ID: setting.ID, + Enable: util.ToPhoneOptBool(dto.enableZoomPhoneApplianceApps), + }) + } + } + break + default: + return fmt.Errorf("unknown setting type, provider implementation error: %s", dto.settingType) + } + + for _, item := range dto.settings { + callForwardingSettings = append(callForwardingSettings, zoomphone.PatchCallHandlingSettingsCallForwardingSettingsCallForwardingSettingsItem{ + Description: util.ToPhoneOptString(item.description), + Enable: util.ToPhoneOptBool(item.enable), + ID: util.ToPhoneOptString(item.id), + PhoneNumber: util.ToPhoneOptString(item.phoneNumber), + ExternalContact: zoomphone.OptPatchCallHandlingSettingsCallForwardingSettingsCallForwardingSettingsItemExternalContact{ + Value: zoomphone.PatchCallHandlingSettingsCallForwardingSettingsCallForwardingSettingsItemExternalContact{ + ExternalContactID: util.ToPhoneOptString(item.externalContactID), + }, + Set: true, + }, + }) + } + err = c.client.UpdateCallHandling(ctx, zoomphone.OptUpdateCallHandlingReq{ + Value: zoomphone.UpdateCallHandlingReq{ + Type: zoomphone.PatchCallHandlingSettingsCallForwardingUpdateCallHandlingReq, + PatchCallHandlingSettingsCallForwarding: zoomphone.PatchCallHandlingSettingsCallForwarding{ + Settings: zoomphone.OptPatchCallHandlingSettingsCallForwardingSettings{ + Value: zoomphone.PatchCallHandlingSettingsCallForwardingSettings{ + HolidayID: util.ToPhoneOptString(dto.holidayID), + RequirePress1BeforeConnecting: util.ToPhoneOptBool(dto.requirePress1BeforeConnecting), + CallForwardingSettings: callForwardingSettings, + }, + Set: true, + }, + SubSettingType: zoomphone.NewOptString("call_forwarding"), + }, + }, + Set: true, + }, zoomphone.UpdateCallHandlingParams{ + ExtensionId: dto.extensionID.ValueString(), + SettingType: string(dto.settingType), + }) + if err != nil { + return fmt.Errorf("error patching phone call handling on call forwarding: %v", err) + } + + return nil +} + +func (c *crud) deleteCallForwarding(ctx context.Context, dto *deleteCallForwardingDto) error { + err := c.client.DeleteCallHandling(ctx, zoomphone.DeleteCallHandlingParams{ + ExtensionId: dto.extensionID.ValueString(), + SettingType: string(dto.settingType), + CallForwardingID: util.ToPhoneOptString(dto.callForwardingID), + }) + if err != nil { + var status *zoomphone.ErrorResponseStatusCode + if errors.As(err, &status) { + if status.StatusCode == 404 && status.Response.Code.Value == 404 { + return nil + } + } + return fmt.Errorf("error deleting phone call handling on call forwarding: %v", err) + } + + return nil +} + +func (c *crud) deleteHoliday(ctx context.Context, dto *deleteHolidayDto) error { + err := c.client.DeleteCallHandling(ctx, zoomphone.DeleteCallHandlingParams{ + ExtensionId: dto.extensionID.ValueString(), + SettingType: string(dto.settingType), + HolidayID: util.ToPhoneOptString(dto.holidayID), + }) + if err != nil { + var status *zoomphone.ErrorResponseStatusCode + if errors.As(err, &status) { + if status.StatusCode == 404 && status.Response.Code.Value == 404 { + return nil + } + } + return fmt.Errorf("error deleting phone call handling on holiday: %v", err) + } + + return nil +} diff --git a/internal/services/phone/callhandling/call_handling_dto.go b/internal/services/phone/callhandling/call_handling_dto.go new file mode 100644 index 0000000..51ed6f1 --- /dev/null +++ b/internal/services/phone/callhandling/call_handling_dto.go @@ -0,0 +1,271 @@ +package callhandling + +import ( + "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type readDtoBusinessHours struct { + extensionID types.String + customHours *readDtoBusinessHoursCustomHours + callHandling *readDtoBusinessHoursCallHandling + callForwarding *readDtoHolidayHoursCallForwarding +} + +type readDtoBusinessHoursCustomHours struct { + typ types.Int32 + allowMembersToReset types.Bool + settings []*readDtoBusinessHoursCustomHoursSetting +} + +type readDtoBusinessHoursCustomHoursSetting struct { + weekday types.Int32 + typ types.Int32 + from types.String + to types.String +} + +type readDtoBusinessHoursCallHandling struct { + callNotAnswerAction types.Int32 + forwardToExtensionID types.String + busyOnAnotherCallAction types.Int32 + busyForwardToExtensionID types.String + allowCallersCheckVoicemail types.Bool + allowMembersToReset types.Bool + audioWhileConnectingID types.String + callDistribution *readDtoBusinessHoursCallHandlingCallDistribution + busyRequirePress1BeforeConnecting types.Bool + unAnsweredRequirePress1BeforeConnecting types.Bool + overflowPlayCalleeVoicemailGreeting types.Bool + playCalleeVoicemailGreeting types.Bool + busyPlayCalleeVoicemailGreeting types.Bool + phoneNumber types.String + phoneNumberDescription types.String + busyPhoneNumber types.String + busyPhoneNumberDescription types.String + connectToOperator types.Bool + greetingPromptID types.String + maxCallInQueue types.Int32 + maxWaitTime types.Int32 + musicOnHoldID types.String + operatorExtensionID types.String + receiveCall types.Bool + ringMode types.String + voiceMailGreetingID types.String + wrapUpTime types.Int32 +} + +type readDtoBusinessHoursCallHandlingCallDistribution struct { + handleMultipleCalls types.Bool + ringDuration types.Int32 + ringMode types.String + skipOfflineDevicePhoneNumber types.Bool +} + +type readDtoClosedHours struct { + extensionID types.String + callHandling *readDtoClosedHoursCallHandling + callForwarding *readDtoHolidayHoursCallForwarding +} + +type readDtoClosedHoursCallHandling struct { + callNotAnswerAction types.Int32 + forwardToExtensionID types.String + busyOnAnotherCallAction types.Int32 + busyForwardToExtensionID types.String + allowCallersCheckVoicemail types.Bool + busyRequirePress1BeforeConnecting types.Bool + unAnsweredRequirePress1BeforeConnecting types.Bool + overflowPlayCalleeVoicemailGreeting types.Bool + playCalleeVoicemailGreeting types.Bool + busyPlayCalleeVoicemailGreeting types.Bool + phoneNumber types.String + phoneNumberDescription types.String + busyPhoneNumber types.String + busyPhoneNumberDescription types.String + connectToOperator types.Bool + greetingPromptID types.String + maxWaitTime types.Int32 + operatorExtensionID types.String + ringMode types.String +} + +type readDtoHolidayHours struct { + extensionID types.String + holidayID types.String + holiday *readDtoHolidayHoursHoliday + callHandling *readDtoHolidayHoursCallHandling + callForwarding *readDtoHolidayHoursCallForwarding +} + +type readDtoHolidayHoursHoliday struct { + name types.String + from timetypes.RFC3339 + to timetypes.RFC3339 +} + +type readDtoHolidayHoursCallHandling struct { + callNotAnswerAction types.Int32 + forwardToExtensionID types.String + allowCallersCheckVoicemail types.Bool + unAnsweredRequirePress1BeforeConnecting types.Bool + overflowPlayCalleeVoicemailGreeting types.Bool + playCalleeVoicemailGreeting types.Bool + phoneNumber types.String + phoneNumberDescription types.String + connectToOperator types.Bool + maxWaitTime types.Int32 + operatorExtensionID types.String + ringMode types.String +} + +type readDtoHolidayHoursCallForwarding struct { + requirePress1BeforeConnecting types.Bool + enableZoomMobileApps types.Bool + enableZoomDesktopApps types.Bool + enableZoomPhoneApplianceApps types.Bool + settings []*readDtoCallForwardingSetting +} + +type readDtoCallForwardingSetting struct { + id types.String + description types.String + enable types.Bool + phoneNumber types.String + externalContact *readDtoCallForwardingSettingsExternalContact +} + +type readDtoCallForwardingSettingsExternalContact struct { + externalContactID types.String +} + +type createHolidayDto struct { + extensionID types.String + settingType settingType + name types.String + from timetypes.RFC3339 + to timetypes.RFC3339 +} + +type createdHolidayDto struct { + holidayID types.String +} + +type createCallForwardingDto struct { + extensionID types.String + settingType settingType + holidayID types.String + description types.String + phoneNumber types.String +} + +type createdCallForwardingDto struct { + callForwardingID types.String +} + +type settingType string + +const ( + settingTypeBusinessHours settingType = "business_hours" + settingTypeClosedHours settingType = "closed_hours" + settingTypeHolidayHours settingType = "holiday_hours" +) + +type patchCustomHoursDto struct { + extensionID types.String + settingType settingType + typ types.Int32 + allowMembersToReset types.Bool + settings []*patchCustomHoursDtoSetting +} + +type patchCustomHoursDtoSetting struct { + weekday types.Int32 + typ types.Int32 + from types.String + to types.String +} + +type patchCallHandlingDto struct { + extensionID types.String + settingType settingType + settings *patchCallHandlingDtoSettings +} + +type patchCallHandlingDtoSettings struct { + holidayID types.String + callNotAnswerAction types.Int32 + forwardToExtensionID types.String + busyOnAnotherCallAction types.Int32 + busyForwardToExtensionID types.String + allowCallersCheckVoicemail types.Bool + allowMembersToReset types.Bool + audioWhileConnectingID types.String + callDistribution *patchCallHandlingDtoSettingsDistribution + busyRequirePress1BeforeConnecting types.Bool + unAnsweredRequirePress1BeforeConnecting types.Bool + overflowPlayCalleeVoicemailGreeting types.Bool + playCalleeVoicemailGreeting types.Bool + busyPlayCalleeVoicemailGreeting types.Bool + phoneNumber types.String + phoneNumberDescription types.String + busyPhoneNumber types.String + busyPhoneNumberDescription types.String + connectToOperator types.Bool + greetingPromptID types.String + maxCallInQueue types.Int32 + maxWaitTime types.Int32 + musicOnHoldID types.String + operatorExtensionID types.String + receiveCall types.Bool + ringMode types.String + voiceMailGreetingID types.String + wrapUpTime types.Int32 +} + +type patchCallHandlingDtoSettingsDistribution struct { + handleMultipleCalls types.Bool + ringDuration types.Int32 + ringMode types.String + skipOfflineDevicePhoneNumber types.Bool +} + +type patchHolidayDto struct { + extensionID types.String + settingType settingType + holidayID types.String + name types.String + from timetypes.RFC3339 + to timetypes.RFC3339 +} + +type patchCallForwardingDto struct { + extensionID types.String + holidayID types.String + settingType settingType + requirePress1BeforeConnecting types.Bool + enableZoomMobileApps types.Bool + enableZoomDesktopApps types.Bool + enableZoomPhoneApplianceApps types.Bool + settings []*patchCallForwardingDtoSetting +} + +type patchCallForwardingDtoSetting struct { + id types.String + description types.String + enable types.Bool + phoneNumber types.String + externalContactID types.String +} + +type deleteCallForwardingDto struct { + extensionID types.String + settingType settingType + callForwardingID types.String +} + +type deleteHolidayDto struct { + extensionID types.String + settingType settingType + holidayID types.String +} diff --git a/internal/services/phone/callhandling/call_handling_holiday_hours_resource.go b/internal/services/phone/callhandling/call_handling_holiday_hours_resource.go new file mode 100644 index 0000000..80488f3 --- /dev/null +++ b/internal/services/phone/callhandling/call_handling_holiday_hours_resource.go @@ -0,0 +1,639 @@ +package callhandling + +import ( + "context" + "fmt" + "strings" + + "github.com/folio-sec/terraform-provider-zoom/internal/provider/shared" + "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes" + "github.com/hashicorp/terraform-plugin-framework-validators/int32validator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/samber/lo" +) + +var ( + _ resource.Resource = &tfHolidayHoursResource{} + _ resource.ResourceWithConfigure = &tfHolidayHoursResource{} + _ resource.ResourceWithImportState = &tfHolidayHoursResource{} +) + +func NewPhoneCallHandlingHolidayHoursResource() resource.Resource { + return &tfHolidayHoursResource{} +} + +type tfHolidayHoursResource struct { + crud *crud +} + +func (r *tfHolidayHoursResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + data, ok := req.ProviderData.(*shared.ProviderData) + if !ok { + resp.Diagnostics.AddError( + "Unexpected ProviderData Source Configure Type", + fmt.Sprintf("Expected *provider.ProviderData, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + r.crud = newCrud(data.PhoneClient) +} + +func (r *tfHolidayHoursResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_phone_call_handling_holiday_hours" +} + +func (r *tfHolidayHoursResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: `Call handling settings allow you to control how your system routes calls during holiday hours. +For more information, read our [Call Handling API guide](https://developers.zoom.us/docs/zoom-phone/call-handling/) or Zoom support article [Customizing call handling settings](https://support.zoom.us/hc/en-us/articles/360059966372-Customizing-call-handling-settings). + +## API Permissions +The following API permissions are required in order to use this resource. +This resource requires the ` + strings.Join([]string{ + "`phone:read:call_handling_setting:admin`", + "`phone:write:call_handling_setting:admin`", + "`phone:update:call_handling_setting:admin`", + "`phone:delete:call_handling_setting:admin`", + }, ", ") + ".", + Attributes: map[string]schema.Attribute{ + "extension_id": schema.StringAttribute{ + Required: true, + MarkdownDescription: "Extension ID.", + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "holiday_id": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The holiday's ID. It's required for the `holiday` sub-setting.", + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "holiday": schema.SingleNestedAttribute{ + Required: true, + MarkdownDescription: "Holiday settings.", + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Required: true, + MarkdownDescription: "The name of the holiday. It's required for the `holiday` sub-setting.", + }, + "from": schema.StringAttribute{ + Required: true, + CustomType: timetypes.RFC3339Type{}, + MarkdownDescription: "The holiday's start date and time in `yyyy-MM-dd'T'HH:mm:ss'Z'` format. It's required for the `holiday` sub-setting.", + }, + "to": schema.StringAttribute{ + Required: true, + CustomType: timetypes.RFC3339Type{}, + MarkdownDescription: "The holiday's end date and time in `yyyy-MM-dd'T'HH:mm:ss'Z'` format. It's required for the `holiday` sub-setting.", + }, + }, + }, + "call_handling": schema.SingleNestedAttribute{ + Required: true, + MarkdownDescription: `The call handling settings. + - NOTE: some fields doesn't return from zoom api, so please ignore_changes for these fields. +`, + Attributes: map[string]schema.Attribute{ + "call_not_answer_action": schema.Int32Attribute{ + Optional: true, + MarkdownDescription: `The action to take when a call is not answered: + - 1 — Forward to a voicemail. + - 2 — Forward to the user. + - 4 — Forward to the common area. + - 6 — Forward to the auto receptionist. + - 7 — Forward to a call queue. + - 8 — Forward to a shared line group. + - 9 — Forward to an external contact. + - 10 - Forward to a phone number. + - 11 — Disconnect. + - 12 — Play a message, then disconnect. + - 13 - Forward to a message. + - 14 - Forward to an interactive voice response (IVR).`, + Validators: []validator.Int32{ + int32validator.OneOf(1, 2, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14), + }, + }, + "forward_to_extension_id": schema.StringAttribute{ + Optional: true, + MarkdownDescription: `The forwarding extension ID that's required only when call_not_answer_action setting is set to: + - 2 - Forward to the user. + - 4 - Forward to the common area. + - 6 - Forward to the auto receptionist. + - 7 - Forward to a call queue. + - 8 - Forward to a shared line group. + - 9 - forward to an external contact.`, + }, + "allow_callers_check_voicemail": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "Whether to allow the callers to check voicemails over a phone. It's required only when the call_not_answer_action setting is set to 1 (Forward to a voicemail).", + }, + "unanswered_require_press1_before_connecting": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "When a call is unanswered, press 1 before connecting the call to forward to an external contact or a number. This option ensures that forwarded calls won't reach the voicemail box for the external contact or a number.", + }, + "overflow_play_callee_voicemail_greeting": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: `Whether to play the callee's voicemail greeting when the caller reaches the end of the forwarding sequence. It displays when call_not_answer_action is set to: + - 2 - Forward to the user + - 4 - Forward to the common area + - 6 - Forward to the auto receptionist + - 7 - Forward to a call queue + - 8 - Forward to a shared line group + - 9 - Forward to an external contact + - 10 - Forward to an external number.`, + }, + "play_callee_voicemail_greeting": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "Whether to play callee's voicemail greeting when the caller reaches the end of forwarding sequence. It displays when `busy_on_another_call_action` action or `call_not_answer_action` is set to `1` - Forward to a voicemail.", + }, + "phone_number": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The extension's phone number or forward to an external number in [E.164](https://en.wikipedia.org/wiki/E.164) format format. It's required when `call_not_answer_action` action is set to `10` - Forward to an external number.", + }, + "phone_number_description": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "(Optional) This field forwards to an external number description. Add this field when `call_not_answer_action` is set to `10` - Forward to an external number.", + }, + "connect_to_operator": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "Whether to allow callers to reach an operator. It's required only when the `call_not_answer_action` or `busy_on_another_call_action` is set to 1 (Forward to a voicemail).", + }, + "max_wait_time": schema.Int32Attribute{ + Optional: true, + MarkdownDescription: `The maximum wait time, in seconds. + - for simultaneous ring mode or the ring duration for each device for sequential ring mode: 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60. + - Specify how long a caller will wait in the queue. Once the wait time is exceeded, the caller will be rerouted based on the overflow option for Call Queue: 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 120, 180, 240, 300, 600, 900, 1200, 1500, 1800. + - This is only required for the call_handling sub-setting. +`, + Validators: []validator.Int32{ + int32validator.OneOf(10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 120, 180, 240, 300, 600, 900, 1200, 1500, 1800), + }, + }, + "operator_extension_id": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The extension ID of the operator to whom the call is being forwarded. It's required only when `call_not_answer_action` is set to `1` (Forward to a voicemail) and `connect_to_operator` is set to true.", + }, + "ring_mode": schema.StringAttribute{ + Optional: true, + MarkdownDescription: `The call handling ring mode: + - simultaneous + - sequential. For user holiday hours, ring_mode needs to be set with max_wait_time.`, + Validators: []validator.String{ + stringvalidator.OneOf("simultaneous", "sequential"), + }, + }, + }, + }, + "call_forwarding": schema.SingleNestedAttribute{ + Optional: true, + MarkdownDescription: "The call forwarding settings.", + Attributes: map[string]schema.Attribute{ + "require_press_1_before_connecting": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "When a call is forwarded to a personal phone number, whether the user must press \"1\" before the call connects. Enable this option to ensure missed calls do not reach to your personal voicemail. It's required for the `call_forwarding` sub-setting. Press 1 is always enabled and is required for callQueue type extension calls.", + }, + "enable_zoom_mobile_apps": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "Whether to enable Zoom Mobile Apps call forwarding", + }, + "enable_zoom_desktop_apps": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "Whether to enable Zoom Desktop Apps call forwarding", + }, + "enable_zoom_phone_appliance_apps": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "Whether to enable Zoom Phone Appliance Apps call forwarding", + }, + "settings": schema.SetNestedAttribute{ + Optional: true, + MarkdownDescription: "The call forwarding settings. It's only required for the `call_forwarding` sub-setting.", + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The call forwarding's ID.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "description": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The external phone number's description.", + }, + "enable": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "Whether to receive a call.", + }, + "phone_number": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The external phone number in [E.164](https://en.wikipedia.org/wiki/E.164) format format.", + }, + }, + }, + }, + }, + }, + }, + } +} + +type holidayHoursResourceModel struct { + ExtensionID types.String `tfsdk:"extension_id"` + HolidayID types.String `tfsdk:"holiday_id"` + Holiday *holidayHoursResourceModelHoliday `tfsdk:"holiday"` + CallHandling *holidayHoursResourceModelCallHandling `tfsdk:"call_handling"` + CallForwarding *holidayHoursResourceModelCallForwarding `tfsdk:"call_forwarding"` +} + +type holidayHoursResourceModelHoliday struct { + Name types.String `tfsdk:"name"` + From timetypes.RFC3339 `tfsdk:"from"` + To timetypes.RFC3339 `tfsdk:"to"` +} + +type holidayHoursResourceModelCallHandling struct { + CallNotAnswerAction types.Int32 `tfsdk:"call_not_answer_action"` + ForwardToExtensionID types.String `tfsdk:"forward_to_extension_id"` + AllowCallersCheckVoicemail types.Bool `tfsdk:"allow_callers_check_voicemail"` + UnAnsweredRequirePress1BeforeConnecting types.Bool `tfsdk:"unanswered_require_press1_before_connecting"` + OverflowPlayCalleeVoicemailGreeting types.Bool `tfsdk:"overflow_play_callee_voicemail_greeting"` + PlayCalleeVoicemailGreeting types.Bool `tfsdk:"play_callee_voicemail_greeting"` + PhoneNumber types.String `tfsdk:"phone_number"` + PhoneNumberDescription types.String `tfsdk:"phone_number_description"` + ConnectToOperator types.Bool `tfsdk:"connect_to_operator"` + MaxWaitTime types.Int32 `tfsdk:"max_wait_time"` + OperatorExtensionID types.String `tfsdk:"operator_extension_id"` + RingMode types.String `tfsdk:"ring_mode"` +} + +type holidayHoursResourceModelCallForwarding struct { + RequirePress1BeforeConnecting types.Bool `tfsdk:"require_press_1_before_connecting"` + EnableZoomMobileApps types.Bool `tfsdk:"enable_zoom_mobile_apps"` + EnableZoomDesktopApps types.Bool `tfsdk:"enable_zoom_desktop_apps"` + EnableZoomPhoneApplianceApps types.Bool `tfsdk:"enable_zoom_phone_appliance_apps"` + Settings []*holidayHoursResourceModelCallForwardingSetting `tfsdk:"settings"` +} + +type holidayHoursResourceModelCallForwardingSetting struct { + ID types.String `tfsdk:"id"` + Description types.String `tfsdk:"description"` + Enable types.Bool `tfsdk:"enable"` + PhoneNumber types.String `tfsdk:"phone_number"` +} + +func (r *tfHolidayHoursResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state holidayHoursResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + output, err := r.read(ctx, &state) + if err != nil { + resp.Diagnostics.AddError("Error reading phone call handling", err.Error()) + return + } + + diags = resp.State.Set(ctx, &output) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *tfHolidayHoursResource) read(ctx context.Context, plan *holidayHoursResourceModel) (*holidayHoursResourceModel, error) { + if plan.HolidayID.ValueString() == "" { + return nil, nil + } + dto, err := r.crud.readHolidayHours(ctx, plan.ExtensionID, plan.HolidayID) + if err != nil { + return nil, fmt.Errorf("error read: %v", err) + } + if dto == nil { + return nil, nil // already deleted + } + + holiday := &holidayHoursResourceModelHoliday{ + Name: dto.holiday.name, + From: dto.holiday.from, + To: dto.holiday.to, + } + callHandling := &holidayHoursResourceModelCallHandling{ + CallNotAnswerAction: dto.callHandling.callNotAnswerAction, + ForwardToExtensionID: dto.callHandling.forwardToExtensionID, + AllowCallersCheckVoicemail: dto.callHandling.allowCallersCheckVoicemail, + UnAnsweredRequirePress1BeforeConnecting: dto.callHandling.unAnsweredRequirePress1BeforeConnecting, + OverflowPlayCalleeVoicemailGreeting: dto.callHandling.overflowPlayCalleeVoicemailGreeting, + PlayCalleeVoicemailGreeting: dto.callHandling.playCalleeVoicemailGreeting, + PhoneNumber: dto.callHandling.phoneNumber, + PhoneNumberDescription: dto.callHandling.phoneNumberDescription, + ConnectToOperator: dto.callHandling.connectToOperator, + MaxWaitTime: dto.callHandling.maxWaitTime, + OperatorExtensionID: dto.callHandling.operatorExtensionID, + RingMode: dto.callHandling.ringMode, + } + callForwarding := lo.TernaryF(dto.callForwarding != nil, func() *holidayHoursResourceModelCallForwarding { + return &holidayHoursResourceModelCallForwarding{ + RequirePress1BeforeConnecting: dto.callForwarding.requirePress1BeforeConnecting, + EnableZoomMobileApps: dto.callForwarding.enableZoomMobileApps, + EnableZoomDesktopApps: dto.callForwarding.enableZoomDesktopApps, + EnableZoomPhoneApplianceApps: dto.callForwarding.enableZoomPhoneApplianceApps, + Settings: lo.Map(dto.callForwarding.settings, func(item *readDtoCallForwardingSetting, index int) *holidayHoursResourceModelCallForwardingSetting { + return &holidayHoursResourceModelCallForwardingSetting{ + ID: item.id, + Description: item.description, + Enable: item.enable, + PhoneNumber: item.phoneNumber, + } + }), + } + }, func() *holidayHoursResourceModelCallForwarding { + return nil + }) + return &holidayHoursResourceModel{ + ExtensionID: dto.extensionID, + HolidayID: plan.HolidayID, + Holiday: holiday, + CallHandling: callHandling, + CallForwarding: callForwarding, + }, nil +} + +func (r *tfHolidayHoursResource) sync(ctx context.Context, plan *holidayHoursResourceModel, onDelete bool) error { + // holiday_hours sync handling like followings + // 1. holiday -> do CREATE or PATCH + // 2. call_handling contains one setting and cannot delete it -> do PATCH + // 3. call_forwarding may contain one setting and can delete it -> do CREATE/PATCH/DELETE + + asis, err := r.read(ctx, plan) + if err != nil { + return err + } + + // 1. CREATE or PATCH holiday + if plan.HolidayID.ValueString() == "" { + createHoliday := &createHolidayDto{ + extensionID: plan.ExtensionID, + settingType: settingTypeHolidayHours, + name: plan.Holiday.Name, + from: plan.Holiday.From, + to: plan.Holiday.To, + } + created, err := r.crud.createHoliday(ctx, createHoliday) + if err != nil { + return err + } + plan.HolidayID = created.holidayID + } else { + patchHoliday := &patchHolidayDto{ + extensionID: plan.ExtensionID, + settingType: settingTypeHolidayHours, + holidayID: plan.HolidayID, + name: plan.Holiday.Name, + from: plan.Holiday.From, + to: plan.Holiday.To, + } + if err = r.crud.patchHoliday(ctx, patchHoliday); err != nil { + return err + } + } + + // 2. PATCH call_handling + patchCallHandling := &patchCallHandlingDto{ + extensionID: plan.ExtensionID, + settingType: settingTypeHolidayHours, + settings: &patchCallHandlingDtoSettings{ + holidayID: plan.HolidayID, + callNotAnswerAction: plan.CallHandling.CallNotAnswerAction, + forwardToExtensionID: plan.CallHandling.ForwardToExtensionID, + allowCallersCheckVoicemail: plan.CallHandling.AllowCallersCheckVoicemail, + unAnsweredRequirePress1BeforeConnecting: plan.CallHandling.UnAnsweredRequirePress1BeforeConnecting, + overflowPlayCalleeVoicemailGreeting: plan.CallHandling.OverflowPlayCalleeVoicemailGreeting, + playCalleeVoicemailGreeting: plan.CallHandling.PlayCalleeVoicemailGreeting, + phoneNumber: plan.CallHandling.PhoneNumber, + phoneNumberDescription: plan.CallHandling.PhoneNumberDescription, + connectToOperator: plan.CallHandling.ConnectToOperator, + maxWaitTime: plan.CallHandling.MaxWaitTime, + operatorExtensionID: plan.CallHandling.OperatorExtensionID, + ringMode: plan.CallHandling.RingMode, + }, + } + if err = r.crud.patchCallHandling(ctx, patchCallHandling); err != nil { + return err + } + + // 3. CREATE/PATCH/DELETE call_forwarding + // 3-1: PATCH existed call forwarding + // 3-2: create new call forwarding + // 3-3: delete unused call forwarding + + // 3-1: PATCH existed call forwarding + if plan.CallForwarding != nil { + var settings []*patchCallForwardingDtoSetting + if plan.CallForwarding.Settings != nil { + settings = lo.FilterMap(plan.CallForwarding.Settings, func(item *holidayHoursResourceModelCallForwardingSetting, index int) (*patchCallForwardingDtoSetting, bool) { + return &patchCallForwardingDtoSetting{ + id: item.ID, + description: item.Description, + enable: item.Enable, + phoneNumber: item.PhoneNumber, + }, item.ID.ValueString() != "" // patch only id is existed + }) + } + patchCallForwarding := &patchCallForwardingDto{ + extensionID: plan.ExtensionID, + holidayID: plan.HolidayID, + settingType: settingTypeHolidayHours, + requirePress1BeforeConnecting: plan.CallForwarding.RequirePress1BeforeConnecting, + enableZoomMobileApps: plan.CallForwarding.EnableZoomMobileApps, + enableZoomDesktopApps: plan.CallForwarding.EnableZoomDesktopApps, + enableZoomPhoneApplianceApps: plan.CallForwarding.EnableZoomPhoneApplianceApps, + settings: settings, + } + if err = r.crud.patchCallForwarding(ctx, patchCallForwarding, onDelete); err != nil { + return err + } + } else if asis.CallForwarding != nil { + patchCallForwarding := &patchCallForwardingDto{ + extensionID: plan.ExtensionID, + holidayID: plan.HolidayID, + settingType: settingTypeHolidayHours, + requirePress1BeforeConnecting: types.BoolValue(false), + enableZoomMobileApps: types.BoolValue(true), + enableZoomDesktopApps: types.BoolValue(true), + enableZoomPhoneApplianceApps: types.BoolValue(true), + settings: []*patchCallForwardingDtoSetting{}, + } + if err = r.crud.patchCallForwarding(ctx, patchCallForwarding, onDelete); err != nil { + return err + } + } + + // 3-2: create new call forwarding + if plan.CallForwarding != nil { + newCallForwardings := lo.Filter(plan.CallForwarding.Settings, func(item *holidayHoursResourceModelCallForwardingSetting, index int) bool { + return item.ID.ValueString() == "" + }) + for _, newCallForwarding := range newCallForwardings { + createAllForwardingDto := &createCallForwardingDto{ + extensionID: plan.ExtensionID, + holidayID: plan.HolidayID, + settingType: settingTypeHolidayHours, + description: newCallForwarding.Description, + phoneNumber: newCallForwarding.PhoneNumber, + } + created, err := r.crud.createCallForwarding(ctx, createAllForwardingDto) + if err != nil { + return err + } + newCallForwarding.ID = created.callForwardingID + } + } + + // 3-3: delete unused call forwarding + if asis != nil && asis.CallForwarding != nil { + deleteCallForwardings := lo.Filter(asis.CallForwarding.Settings, func(item *holidayHoursResourceModelCallForwardingSetting, index int) bool { + if plan.CallForwarding == nil { + return true + } + for _, planCallForwarding := range plan.CallForwarding.Settings { + if item.ID == planCallForwarding.ID { + return false + } + } + return true + }) + for _, setting := range deleteCallForwardings { + deleteCallForwarding := &deleteCallForwardingDto{ + extensionID: asis.ExtensionID, + settingType: settingTypeHolidayHours, + callForwardingID: setting.ID, + } + if err = r.crud.deleteCallForwarding(ctx, deleteCallForwarding); err != nil { + return nil + } + } + } + + return nil +} + +func (r *tfHolidayHoursResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan holidayHoursResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if err := r.sync(ctx, &plan, false); err != nil { + resp.Diagnostics.AddError( + "Error creating phone call handling", + err.Error(), + ) + return + } + + // XXX zoom api doesn't return some fields after create/update, so just set plan with existed plan values + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *tfHolidayHoursResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan holidayHoursResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + resp.Diagnostics.AddError( + "Error updating phone call handling", + "Error updating phone call handling", + ) + return + } + + if err := r.sync(ctx, &plan, false); err != nil { + resp.Diagnostics.AddError( + "Error updating phone call handling", + fmt.Sprintf( + "Could not update phone call handling %s, unexpected error: %s", + plan.ExtensionID.ValueString(), + err, + ), + ) + return + } + + // XXX zoom api doesn't return some fields after create/update, so just set plan with existed plan values + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *tfHolidayHoursResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state holidayHoursResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + deleteHoliday := &deleteHolidayDto{ + extensionID: state.ExtensionID, + settingType: settingTypeHolidayHours, + holidayID: state.HolidayID, + } + err := r.crud.deleteHoliday(ctx, deleteHoliday) + if err != nil { + resp.Diagnostics.AddError( + "Error deleting phone call handling", + fmt.Sprintf( + "Could not delete phone call handling %s, unexpected error: %s", + state.ExtensionID.ValueString(), + err, + ), + ) + return + } + + tflog.Info(ctx, "deleted phone call handling", map[string]interface{}{ + "extension_id": state.ExtensionID.ValueString(), + }) +} + +func (r *tfHolidayHoursResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + // id = ${extension_id/holiday_id} + ids := strings.Split(req.ID, "/") + if len(ids) != 2 { + resp.Diagnostics.AddError("Invalid import ID", "Import ID must be in the format `extension_id/holiday_id`.") + return + } + + state, err := r.read(ctx, &holidayHoursResourceModel{ + ExtensionID: types.StringValue(ids[0]), + HolidayID: types.StringValue(ids[1]), + }) + if err != nil { + resp.Diagnostics.AddError("Import failed", err.Error()) + return + } + + diags := resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} diff --git a/internal/services/phone/callqueuephonenumber/call_queue_phone_numbers_resource.go b/internal/services/phone/callqueuephonenumber/call_queue_phone_numbers_resource.go index 3ec07b9..b378e50 100644 --- a/internal/services/phone/callqueuephonenumber/call_queue_phone_numbers_resource.go +++ b/internal/services/phone/callqueuephonenumber/call_queue_phone_numbers_resource.go @@ -284,6 +284,14 @@ func (r *tfResource) Delete(ctx context.Context, req resource.DeleteRequest, res return } + asis, err := r.crud.read(ctx, state.CallQueueID) + if err != nil { + resp.Diagnostics.AddError("Error deleting phone call queue phone numbers", err.Error()) + } + if asis == nil { + return + } + if err := r.crud.unassignAll(ctx, state.CallQueueID); err != nil { resp.Diagnostics.AddError( "Error deleting phone call queue phone numbers", diff --git a/internal/util/typesphone.go b/internal/util/typesphone.go index cfa60c5..3dab3ee 100644 --- a/internal/util/typesphone.go +++ b/internal/util/typesphone.go @@ -1,9 +1,8 @@ package util import ( - "time" - "github.com/folio-sec/terraform-provider-zoom/generated/api/zoomphone" + "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -35,10 +34,10 @@ func ToPhoneOptInt(o types.Int32) zoomphone.OptInt { return zoomphone.NewOptInt(int(o.ValueInt32())) } -func ToPhoneOptDateTime(o types.String) zoomphone.OptDateTime { +func ToPhoneOptDateTime(o timetypes.RFC3339) zoomphone.OptDateTime { if o.IsNull() || o.IsUnknown() { return zoomphone.OptDateTime{} } - value, _ := time.Parse(o.ValueString(), "2006-01-02 15:04:05.999999999 -0700 MST") - return zoomphone.NewOptDateTime(value) + value, _ := o.ValueRFC3339Time() + return zoomphone.NewOptDateTime(value.UTC()) } diff --git a/internal/util/typesuser.go b/internal/util/typesuser.go index c7a2008..415b06f 100644 --- a/internal/util/typesuser.go +++ b/internal/util/typesuser.go @@ -40,5 +40,5 @@ func ToUserOptDateTime(o timetypes.RFC3339) zoomuser.OptDateTime { } value, _ := o.ValueRFC3339Time() - return zoomuser.NewOptDateTime(value) + return zoomuser.NewOptDateTime(value.UTC()) } diff --git a/scripts/patchSpec.js b/scripts/patchSpec.js index 45c9422..7086bb4 100755 --- a/scripts/patchSpec.js +++ b/scripts/patchSpec.js @@ -325,6 +325,19 @@ function phonePatch(spec) { delete spec.paths['/phone/call_queues/{callQueueId}/phone_numbers']['post']['responses']['204'] } + // PATCH /phone/extension/{extensionId}/call_handling/settings/{settingType} PatchCallHandlingSettingsCustomHours should accept holidayId property for holiday_hours + if (spec.components.schemas['PatchCallHandlingSettingsCallHandling']) { + spec.components.schemas['PatchCallHandlingSettingsCallHandling']['properties']['settings']['properties']['holiday_id'] = { + "type": "string", "description": "The ID of the holiday.", + } + } + // PATCH /phone/extension/{extensionId}/call_handling/settings/{settingType} PatchCallHandlingSettingsCallForwarding should accept holidayId property for holiday_hours + if (spec.components.schemas['PatchCallHandlingSettingsCallForwarding']) { + spec.components.schemas['PatchCallHandlingSettingsCallForwarding']['properties']['settings']['properties']['holiday_id'] = { + "type": "string", "description": "The ID of the holiday.", + } + } + // GET PATCH /phone/users/{userId} should have calling_plans[].name property if (spec.paths['/phone/users/{userId}']) { spec.paths['/phone/users/{userId}']['get']['responses']['200']['content']['application/json']['schema']['properties']['calling_plans']['items']['properties']['name'] = { diff --git a/spec/ZoomPhoneAPISpec.json b/spec/ZoomPhoneAPISpec.json index 5657620..64cc304 100644 --- a/spec/ZoomPhoneAPISpec.json +++ b/spec/ZoomPhoneAPISpec.json @@ -50591,6 +50591,10 @@ "type": "boolean", "description": "When a call is forwarded to a personal phone number, whether the user must press "1" before the call connects. Enable this option to ensure missed calls do not reach to your personal voicemail. It's required for the `call_forwarding` sub-setting.\n\nPress 1 is always enabled and is required for `callQueue` type extension calls.", "example": true + }, + "holiday_id": { + "type": "string", + "description": "The ID of the holiday." } } }, @@ -50856,6 +50860,10 @@ "type": "integer", "description": "The wrap up time in seconds. \nSpecify the duration before the next queue call is routed to a member in call queue:\n* `0` \n* `10` \n* `15` \n* `20` \n* `25` \n* `30` \n* `35` \n* `40` \n* `45` \n* `50` \n* `55` \n* `60` \n* `120` \n* `180` \n* `240` \n* `300`\n\nThis is only required for the `call_handling` sub-setting.", "example": 25 + }, + "holiday_id": { + "type": "string", + "description": "The ID of the holiday." } } },