diff --git a/.changelog/44750.txt b/.changelog/44750.txt new file mode 100644 index 000000000000..b9c8d4de9f1a --- /dev/null +++ b/.changelog/44750.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_connect_routing_profile: Add `media_concurrencies.cross_channel_behavior` argument +``` + +```release-note:enhancement +data-source/aws_connect_routing_profile: Add `media_concurrencies.cross_channel_behavior` attribute +``` diff --git a/internal/service/connect/connect_test.go b/internal/service/connect/connect_test.go index 4f9a955fd5bb..1bd9d0800121 100644 --- a/internal/service/connect/connect_test.go +++ b/internal/service/connect/connect_test.go @@ -128,6 +128,7 @@ func TestAccConnect_serial(t *testing.T) { acctest.CtDisappears: testAccRoutingProfile_disappears, "tags": testAccRoutingProfile_updateTags, "concurrency": testAccRoutingProfile_updateConcurrency, + "crossChannelBehavior": testAccRoutingProfile_crossChannelBehavior, "defaultOutboundQueue": testAccRoutingProfile_updateDefaultOutboundQueue, "queues": testAccRoutingProfile_updateQueues, "createQueueBatchAssociations": testAccRoutingProfile_createQueueConfigsBatchedAssociateDisassociate, diff --git a/internal/service/connect/routing_profile.go b/internal/service/connect/routing_profile.go index b888578dbff2..6a2401c83e01 100644 --- a/internal/service/connect/routing_profile.go +++ b/internal/service/connect/routing_profile.go @@ -78,6 +78,21 @@ func resourceRoutingProfile() *schema.Resource { Required: true, ValidateFunc: validation.IntBetween(1, 10), }, + "cross_channel_behavior": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "behavior_type": { + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: enum.Validate[awstypes.BehaviorType](), + }, + }, + }, + }, }, }, }, @@ -201,7 +216,7 @@ func resourceRoutingProfileRead(ctx context.Context, d *schema.ResourceData, met d.Set("default_outbound_queue_id", routingProfile.DefaultOutboundQueueId) d.Set(names.AttrDescription, routingProfile.Description) d.Set(names.AttrInstanceID, instanceID) - if err := d.Set("media_concurrencies", flattenMediaConcurrencies(routingProfile.MediaConcurrencies)); err != nil { + if err := d.Set("media_concurrencies", flattenMediaConcurrencies(routingProfile.MediaConcurrencies, d.Get("media_concurrencies").(*schema.Set).List())); err != nil { return sdkdiag.AppendErrorf(diags, "setting media_concurrencies: %s", err) } d.Set(names.AttrName, routingProfile.Name) @@ -477,21 +492,68 @@ func expandMediaConcurrencies(tfList []any) []awstypes.MediaConcurrency { Channel: awstypes.Channel(tfMap["channel"].(string)), Concurrency: aws.Int32(int32(tfMap["concurrency"].(int))), } + + if v, ok := tfMap["cross_channel_behavior"].([]any); ok && len(v) > 0 { + apiObject.CrossChannelBehavior = expandCrossChannelBehavior(v) + } + apiObjects = append(apiObjects, apiObject) } return apiObjects } -func flattenMediaConcurrencies(apiObjects []awstypes.MediaConcurrency) []any { +func expandCrossChannelBehavior(tfList []any) *awstypes.CrossChannelBehavior { + if len(tfList) == 0 { + return nil + } + + tfMap := tfList[0].(map[string]any) + + return &awstypes.CrossChannelBehavior{ + BehaviorType: awstypes.BehaviorType(tfMap["behavior_type"].(string)), + } +} + +func flattenCrossChannelBehavior(apiObject *awstypes.CrossChannelBehavior) []map[string]any { + if apiObject == nil { + return nil + } + + return []map[string]any{ + { + "behavior_type": string(apiObject.BehaviorType), + }, + } +} + +func flattenMediaConcurrencies(apiObjects []awstypes.MediaConcurrency, configList ...[]any) []any { tfList := []any{} + configuredBehaviors := make(map[string]bool) + if len(configList) > 0 && configList[0] != nil { + for _, configRaw := range configList[0] { + configMap := configRaw.(map[string]any) + channel := configMap["channel"].(string) + if crossChannelBehaviorList, ok := configMap["cross_channel_behavior"].([]any); ok && len(crossChannelBehaviorList) > 0 { + configuredBehaviors[channel] = true + } + } + } + for _, apiObject := range apiObjects { tfMap := map[string]any{ "channel": apiObject.Channel, "concurrency": aws.ToInt32(apiObject.Concurrency), } + channel := string(apiObject.Channel) + if apiObject.CrossChannelBehavior != nil { + if len(configList) == 0 || configuredBehaviors[channel] { + tfMap["cross_channel_behavior"] = flattenCrossChannelBehavior(apiObject.CrossChannelBehavior) + } + } + tfList = append(tfList, tfMap) } diff --git a/internal/service/connect/routing_profile_data_source.go b/internal/service/connect/routing_profile_data_source.go index e80799e5d7d8..1359f171c8cb 100644 --- a/internal/service/connect/routing_profile_data_source.go +++ b/internal/service/connect/routing_profile_data_source.go @@ -59,6 +59,18 @@ func dataSourceRoutingProfile() *schema.Resource { Type: schema.TypeInt, Computed: true, }, + "cross_channel_behavior": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "behavior_type": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, }, }, }, diff --git a/internal/service/connect/routing_profile_test.go b/internal/service/connect/routing_profile_test.go index c1e2aee9a6f4..cb20b539e61b 100644 --- a/internal/service/connect/routing_profile_test.go +++ b/internal/service/connect/routing_profile_test.go @@ -335,6 +335,80 @@ func testAccRoutingProfile_updateQueues(t *testing.T) { }) } +func testAccRoutingProfile_crossChannelBehavior(t *testing.T) { + ctx := acctest.Context(t) + var v awstypes.RoutingProfile + rName := sdkacctest.RandomWithPrefix("resource-test-terraform") + rName2 := sdkacctest.RandomWithPrefix("resource-test-terraform") + rName3 := sdkacctest.RandomWithPrefix("resource-test-terraform") + resourceName := "aws_connect_routing_profile.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.ConnectServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckRoutingProfileDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccRoutingProfileConfig_crossChannelBehaviorDefault(rName, rName2, rName3), + Check: resource.ComposeTestCheckFunc( + testAccCheckRoutingProfileExists(ctx, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "media_concurrencies.#", "2"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "media_concurrencies.*", map[string]string{ + "channel": string(awstypes.ChannelVoice), + "concurrency": "1", + "cross_channel_behavior.#": "0", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "media_concurrencies.*", map[string]string{ + "channel": string(awstypes.ChannelChat), + "concurrency": "2", + "cross_channel_behavior.#": "0", + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccRoutingProfileConfig_crossChannelBehaviorCurrentChannelOnly(rName, rName2, rName3), + Check: resource.ComposeTestCheckFunc( + testAccCheckRoutingProfileExists(ctx, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "media_concurrencies.#", "2"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "media_concurrencies.*", map[string]string{ + "channel": string(awstypes.ChannelVoice), + "concurrency": "1", + "cross_channel_behavior.0.behavior_type": string(awstypes.BehaviorTypeRouteCurrentChannelOnly), + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "media_concurrencies.*", map[string]string{ + "channel": string(awstypes.ChannelChat), + "concurrency": "3", + "cross_channel_behavior.0.behavior_type": string(awstypes.BehaviorTypeRouteCurrentChannelOnly), + }), + ), + }, + { + Config: testAccRoutingProfileConfig_crossChannelBehaviorMixed(rName, rName2, rName3), + Check: resource.ComposeTestCheckFunc( + testAccCheckRoutingProfileExists(ctx, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "media_concurrencies.#", "2"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "media_concurrencies.*", map[string]string{ + "channel": string(awstypes.ChannelVoice), + "concurrency": "1", + "cross_channel_behavior.0.behavior_type": string(awstypes.BehaviorTypeRouteAnyChannel), + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "media_concurrencies.*", map[string]string{ + "channel": string(awstypes.ChannelChat), + "concurrency": "3", + "cross_channel_behavior.0.behavior_type": string(awstypes.BehaviorTypeRouteCurrentChannelOnly), + }), + ), + }, + }, + }) +} + func testAccRoutingProfile_updateTags(t *testing.T) { ctx := acctest.Context(t) var v awstypes.RoutingProfile @@ -997,3 +1071,98 @@ resource "aws_connect_routing_profile" "test" { } `, rName3)) } + +func testAccRoutingProfileConfig_crossChannelBehaviorDefault(rName, rName2, rName3 string) string { + return acctest.ConfigCompose( + testAccRoutingProfileConfig_base(rName, rName2), + fmt.Sprintf(` +resource "aws_connect_routing_profile" "test" { + instance_id = aws_connect_instance.test.id + name = %[1]q + default_outbound_queue_id = aws_connect_queue.default_outbound_queue.queue_id + description = "Test cross-channel behavior - default" + + media_concurrencies { + channel = "VOICE" + concurrency = 1 + # behaviour uses AWS server-side default + } + + media_concurrencies { + channel = "CHAT" + concurrency = 2 + # behaviour uses AWS server-side default + } + + tags = { + "Name" = "Test Routing Profile Cross-Channel Behavior", + } +} +`, rName3)) +} + +func testAccRoutingProfileConfig_crossChannelBehaviorCurrentChannelOnly(rName, rName2, rName3 string) string { + return acctest.ConfigCompose( + testAccRoutingProfileConfig_base(rName, rName2), + fmt.Sprintf(` +resource "aws_connect_routing_profile" "test" { + instance_id = aws_connect_instance.test.id + name = %[1]q + default_outbound_queue_id = aws_connect_queue.default_outbound_queue.queue_id + description = "Test cross-channel behavior - current channel only" + + media_concurrencies { + channel = "VOICE" + concurrency = 1 + cross_channel_behavior { + behavior_type = "ROUTE_CURRENT_CHANNEL_ONLY" + } + } + + media_concurrencies { + channel = "CHAT" + concurrency = 3 + cross_channel_behavior { + behavior_type = "ROUTE_CURRENT_CHANNEL_ONLY" + } + } + + tags = { + "Name" = "Test Routing Profile Cross-Channel Behavior", + } +} +`, rName3)) +} + +func testAccRoutingProfileConfig_crossChannelBehaviorMixed(rName, rName2, rName3 string) string { + return acctest.ConfigCompose( + testAccRoutingProfileConfig_base(rName, rName2), + fmt.Sprintf(` +resource "aws_connect_routing_profile" "test" { + instance_id = aws_connect_instance.test.id + name = %[1]q + default_outbound_queue_id = aws_connect_queue.default_outbound_queue.queue_id + description = "Test cross-channel behavior - mixed" + + media_concurrencies { + channel = "VOICE" + concurrency = 1 + cross_channel_behavior { + behavior_type = "ROUTE_ANY_CHANNEL" + } + } + + media_concurrencies { + channel = "CHAT" + concurrency = 3 + cross_channel_behavior { + behavior_type = "ROUTE_CURRENT_CHANNEL_ONLY" + } + } + + tags = { + "Name" = "Test Routing Profile Cross-Channel Behavior", + } +} +`, rName3)) +} diff --git a/website/docs/d/connect_routing_profile.html.markdown b/website/docs/d/connect_routing_profile.html.markdown index 005882b55f99..67401aab407c 100644 --- a/website/docs/d/connect_routing_profile.html.markdown +++ b/website/docs/d/connect_routing_profile.html.markdown @@ -57,6 +57,11 @@ A `media_concurrencies` block supports the following attributes: * `channel` - Channels that agents can handle in the Contact Control Panel (CCP). Valid values are `VOICE`, `CHAT`, `TASK`. * `concurrency` - Number of contacts an agent can have on a channel simultaneously. Valid Range for `VOICE`: Minimum value of 1. Maximum value of 1. Valid Range for `CHAT`: Minimum value of 1. Maximum value of 10. Valid Range for `TASK`: Minimum value of 1. Maximum value of 10. +* `cross_channel_behavior` - Configuration block for cross-channel behavior. Documented below. + +A `cross_channel_behavior` block supports the following attributes: + +* `behavior_type` - Cross-channel behavior for routing contacts across multiple channels. Valid values are `ROUTE_CURRENT_CHANNEL_ONLY`, `ROUTE_ANY_CHANNEL`. A `queue_configs` block supports the following attributes: diff --git a/website/docs/r/connect_routing_profile.html.markdown b/website/docs/r/connect_routing_profile.html.markdown index 833c7fb69537..cd070a5b78c5 100644 --- a/website/docs/r/connect_routing_profile.html.markdown +++ b/website/docs/r/connect_routing_profile.html.markdown @@ -23,6 +23,17 @@ resource "aws_connect_routing_profile" "example" { media_concurrencies { channel = "VOICE" concurrency = 1 + cross_channel_behavior { + behavior_type = "ROUTE_ANY_CHANNEL" + } + } + + media_concurrencies { + channel = "CHAT" + concurrency = 3 + cross_channel_behavior { + behavior_type = "ROUTE_CURRENT_CHANNEL_ONLY" + } } queue_configs { @@ -56,6 +67,11 @@ A `media_concurrencies` block supports the following arguments: * `channel` - (Required) Specifies the channels that agents can handle in the Contact Control Panel (CCP). Valid values are `VOICE`, `CHAT`, `TASK`. * `concurrency` - (Required) Specifies the number of contacts an agent can have on a channel simultaneously. Valid Range for `VOICE`: Minimum value of 1. Maximum value of 1. Valid Range for `CHAT`: Minimum value of 1. Maximum value of 10. Valid Range for `TASK`: Minimum value of 1. Maximum value of 10. +* `cross_channel_behavior` - (Optional) Configuration block for cross-channel behavior. If not specified, AWS will use the service default behavior. Documented below. + +A `cross_channel_behavior` block supports the following arguments: + +* `behavior_type` - (Required) Specifies the cross-channel behavior for routing contacts across multiple channels. `ROUTE_ANY_CHANNEL` allows agents to receive contacts from any channel regardless of what they are currently handling. `ROUTE_CURRENT_CHANNEL_ONLY` restricts agents to receive contacts only from the channel they are currently handling. Valid values are `ROUTE_CURRENT_CHANNEL_ONLY`, `ROUTE_ANY_CHANNEL`. A `queue_configs` block supports the following arguments: