diff --git a/.changes/unreleased/Added-20240813-121838.yaml b/.changes/unreleased/Added-20240813-121838.yaml new file mode 100644 index 00000000..820abd6a --- /dev/null +++ b/.changes/unreleased/Added-20240813-121838.yaml @@ -0,0 +1,4 @@ +kind: Added +body: Added resources for associate role and business units and extended project setting + options +time: 2024-08-13T12:18:38.233827008+02:00 diff --git a/Taskfile.yml b/Taskfile.yml index 85a539ee..0758c795 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -53,5 +53,5 @@ tasks: CTP_CLIENT_SECRET: x CTP_PROJECT_KEY: unittest CTP_SCOPES: manage_project:projectkey - CTP_API_URL: http://localhost:8989 - CTP_AUTH_URL: http://localhost:8989 + CTP_API_URL: http://localhost:3000 + CTP_AUTH_URL: http://localhost:3000 diff --git a/commercetools/resource_shipping_zone_rate.go b/commercetools/resource_shipping_zone_rate.go index a8b8914e..c8ffe324 100644 --- a/commercetools/resource_shipping_zone_rate.go +++ b/commercetools/resource_shipping_zone_rate.go @@ -410,7 +410,7 @@ func findShippingZoneRate(shippingMethod *platform.ShippingMethod, shippingZoneI for _, zoneRate := range shippingMethod.ZoneRates { if zoneRate.Zone.ID == shippingZoneID { for _, shippingRate := range zoneRate.ShippingRates { - if shippingRate.Price.(platform.CentPrecisionMoney).CurrencyCode == currencyCode { + if shippingRate.Price.CurrencyCode == currencyCode { return &shippingRate, nil } } @@ -435,36 +435,22 @@ func setShippingZoneRateState(d *schema.ResourceData, shippingMethod *platform.S tiers := flattenShippingZoneRateTiers(shippingRate) _ = d.Set("shipping_rate_price_tier", tiers) - if typedPrice, ok := shippingRate.Price.(platform.CentPrecisionMoney); ok { - price := map[string]any{ - "currency_code": typedPrice.CurrencyCode, - "cent_amount": typedPrice.CentAmount, - } - err = d.Set("price", []any{price}) - if err != nil { - return err - } - } else { - _ = d.Set("price", nil) - if err != nil { - return err - } + price := map[string]any{ + "currency_code": shippingRate.Price.CurrencyCode, + "cent_amount": shippingRate.Price.CentAmount, + } + err = d.Set("price", []any{price}) + if err != nil { + return err } - if typedFreeAbove, ok := (shippingRate.FreeAbove).(platform.CentPrecisionMoney); ok { - freeAbove := map[string]any{ - "currency_code": typedFreeAbove.CurrencyCode, - "cent_amount": typedFreeAbove.CentAmount, - } - err = d.Set("free_above", []any{freeAbove}) - if err != nil { - return err - } - } else { - _ = d.Set("free_above", nil) - if err != nil { - return err - } + freeAbove := map[string]any{ + "currency_code": shippingRate.FreeAbove.CurrencyCode, + "cent_amount": shippingRate.FreeAbove.CentAmount, + } + err = d.Set("free_above", []any{freeAbove}) + if err != nil { + return err } return nil } diff --git a/docs/resources/associate_role.md b/docs/resources/associate_role.md index 28e3694b..b1a83c37 100644 --- a/docs/resources/associate_role.md +++ b/docs/resources/associate_role.md @@ -22,12 +22,44 @@ resource "commercetools_associate_role" "regional_manager" { name = "Regional Manager - Europe" permissions = [ "AddChildUnits", - "UpdateBusinessUnitDetails", "UpdateAssociates", + "UpdateBusinessUnitDetails", + "UpdateParentUnit", + "ViewMyCarts", + "ViewOthersCarts", + "UpdateMyCarts", + "UpdateOthersCarts", "CreateMyCarts", + "CreateOthersCarts", "DeleteMyCarts", - "UpdateMyCarts", - "ViewMyCarts", + "DeleteOthersCarts", + "ViewMyOrders", + "ViewOthersOrders", + "UpdateMyOrders", + "UpdateOthersOrders", + "CreateMyOrdersFromMyCarts", + "CreateMyOrdersFromMyQuotes", + "CreateOrdersFromOthersCarts", + "CreateOrdersFromOthersQuotes", + "ViewMyQuotes", + "ViewOthersQuotes", + "AcceptMyQuotes", + "AcceptOthersQuotes", + "DeclineMyQuotes", + "DeclineOthersQuotes", + "RenegotiateMyQuotes", + "RenegotiateOthersQuotes", + "ReassignMyQuotes", + "ReassignOthersQuotes", + "ViewMyQuoteRequests", + "ViewOthersQuoteRequests", + "UpdateMyQuoteRequests", + "UpdateOthersQuoteRequests", + "CreateMyQuoteRequestsFromMyCarts", + "CreateQuoteRequestsFromOthersCarts", + "CreateApprovalRules", + "UpdateApprovalRules", + "UpdateApprovalFlows", ] } ``` @@ -37,15 +69,15 @@ resource "commercetools_associate_role" "regional_manager" { ### Required -- `key` (String) User-defined unique identifier of the AssociateRole. -- `permissions` (List of String) List of Permissions for the AssociateRole. +- `key` (String) User-defined unique identifier of the associate role. +- `permissions` (List of String) List of permissions for the associate role. See the [Associate Role API Documentation](https://docs.commercetools.com/api/projects/associate-roles#ctp:api:type:Permission) for more information. ### Optional -- `buyer_assignable` (Boolean) Whether the AssociateRole can be assigned to an Associate by a buyer. If false, the AssociateRole can only be assigned using the general endpoint. -- `name` (String) Name of the AssociateRole. +- `buyer_assignable` (Boolean) Whether the associate role can be assigned to an associate by a buyer. If false, the associate role can only be assigned using the general endpoint. Defaults to true. +- `name` (String) Name of the associate role. ### Read-Only -- `id` (String) Unique identifier of the AssociateRole. -- `version` (Number) Current version of the AssociateRole. +- `id` (String) Unique identifier of the associate role. +- `version` (Number) Current version of the associate role. diff --git a/docs/resources/business_unit_company.md b/docs/resources/business_unit_company.md new file mode 100644 index 00000000..99a1192f --- /dev/null +++ b/docs/resources/business_unit_company.md @@ -0,0 +1,137 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "commercetools_business_unit_company Resource - terraform-provider-commercetools" +subcategory: "" +description: |- + Business Unit type to represent the top level of a business. Contains specific fields and values that differentiate a Company from the generic BusinessUnit. + See also the [Business Unit API Documentation](https://docs.commercetools.com/api/projects/business-units +--- + +# commercetools_business_unit_company (Resource) + +Business Unit type to represent the top level of a business. Contains specific fields and values that differentiate a Company from the generic BusinessUnit. + +See also the [Business Unit API Documentation](https://docs.commercetools.com/api/projects/business-units + +## Example Usage + +```terraform +resource "commercetools_store" "my-store" { + key = "my-store" + name = { + en-US = "My store" + } + countries = ["NL", "BE"] + languages = ["en-GB"] +} + +resource "commercetools_business_unit_company" "my-company" { + key = "my-company" + name = "My company" + contact_email = "main@my-company.com" + + address { + key = "my-company-address-1" + country = "NL" + state = "Noord-Holland" + city = "Amsterdam" + street_name = "Keizersgracht" + street_number = "3" + additional_street_info = "4th floor" + postal_code = "1015 CJ" + } + + address { + key = "my-company-address-2" + country = "NL" + state = "Utrecht" + city = "Utrecht" + street_name = "Oudegracht" + street_number = "1" + postal_code = "3511 AA" + additional_street_info = "Main floor" + } + + store { + key = commercetools_store.my-store.key + } + + billing_address_keys = ["my-company-address-1"] + shipping_address_keys = ["my-company-address-1", "my-company-address-2"] + default_billing_address_key = "my-company-address-1" + default_shipping_address_key = "my-company-address-1" +} +``` + + +## Schema + +### Required + +- `key` (String) User-defined unique identifier for the Company. +- `name` (String) The name of the Company. + +### Optional + +- `address` (Block List) Addresses used by the Business Unit. (see [below for nested schema](#nestedblock--address)) +- `billing_address_keys` (Set of String) Indexes of entries in addresses to set as billing addresses. The billingAddressIds of the [Customer](https://docs.commercetools.com/api/projects/customers) will be replaced by these addresses. +- `contact_email` (String) The email address of the Company. +- `default_billing_address_key` (String) Index of the entry in addresses to set as the default billing address. +- `default_shipping_address_key` (String) Index of the entry in addresses to set as the default shipping address. +- `shipping_address_keys` (Set of String) Indexes of entries in addresses to set as shipping addresses. The shippingAddressIds of the [Customer](https://docs.commercetools.com/api/projects/customers) will be replaced by these addresses. +- `status` (String) The status of the Company. +- `store` (Block List) Sets the Stores the Business Unit is associated with. + +If the Business Unit has Stores defined, then all of its Carts, Orders, Quotes, or Quote Requests must belong to one of the Business Unit's Stores. + +If the Business Unit has no Stores, then all of its Carts, Orders, Quotes, or Quote Requests must not belong to any Store. (see [below for nested schema](#nestedblock--store)) + +### Read-Only + +- `id` (String) Unique identifier of the Company. +- `version` (Number) The current version of the Company. + + +### Nested Schema for `address` + +Required: + +- `country` (String) Name of the country +- `key` (String) User-defined identifier of the Address that must be unique when multiple addresses are referenced in BusinessUnits, Customers, and itemShippingAddresses (LineItem-specific addresses) of a Cart, Order, QuoteRequest, or Quote. + +Optional: + +- `additional_address_info` (String) Further information on the Address +- `additional_street_info` (String) Further information on the street address +- `apartment` (String) Name or number of the apartment +- `building` (String) Name or number of the building +- `city` (String) Name of the city +- `company` (String) Name of the company +- `department` (String) Name of the department +- `email` (String) Email address +- `external_id` (String) ID for the contact used in an external system +- `fax` (String) Fax number +- `first_name` (String) First name of the contact +- `last_name` (String) Last name of the contact +- `mobile` (String) Mobile phone number +- `phone` (String) Phone number +- `po_box` (String) Post office box number +- `postal_code` (String) Postal code +- `region` (String) Name of the region +- `salutation` (String) Salutation of the contact, for example Ms., Mr. +- `state` (String) Name of the state +- `street_name` (String) Name of the street +- `street_number` (String) Street number +- `title` (String) Title of the contact, for example Dr., Prof. + +Read-Only: + +- `id` (String) Unique identifier of the Address + + + +### Nested Schema for `store` + +Optional: + +- `key` (String) User-defined unique identifier of the Store diff --git a/docs/resources/business_unit_division.md b/docs/resources/business_unit_division.md new file mode 100644 index 00000000..2c2caa27 --- /dev/null +++ b/docs/resources/business_unit_division.md @@ -0,0 +1,162 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "commercetools_business_unit_division Resource - terraform-provider-commercetools" +subcategory: "" +description: |- + Business Unit type to represent the top level of a business. Contains specific fields and values that differentiate a Division from the generic BusinessUnit. + See also the [Business Unit API Documentation](https://docs.commercetools.com/api/projects/business-units +--- + +# commercetools_business_unit_division (Resource) + +Business Unit type to represent the top level of a business. Contains specific fields and values that differentiate a Division from the generic BusinessUnit. + +See also the [Business Unit API Documentation](https://docs.commercetools.com/api/projects/business-units + +## Example Usage + +```terraform +resource "commercetools_store" "my-store" { + key = "my-store" + name = { + en-US = "My store" + } + countries = ["NL", "BE"] + languages = ["en-GB"] +} + +resource "commercetools_business_unit_company" "my-company" { + key = "my-company" + name = "My company" + contact_email = "main@my-company.com" +} + +resource "commercetools_business_unit_division" "my-division" { + key = "my-division" + name = "My division" + contact_email = "my-division@my-company.com" + store_mode = "Explicit" + status = "Active" + associate_mode = "Explicit" + approval_rule_mode = "Explicit" + + parent_unit { + key = commercetools_business_unit_company.my-company.key + } + + store { + key = commercetools_store.my-store.key + } + + address { + key = "my-div-address-1" + country = "NL" + state = "Utrecht" + city = "Utrecht" + street_name = "Oudegracht" + street_number = "1" + postal_code = "3511 AA" + additional_street_info = "Main floor" + } + + address { + key = "my-div-address-2" + country = "NL" + state = "Zuid-Holland" + city = "Leiden" + street_name = "Breestraat" + street_number = "1" + postal_code = "2311 CH" + } + billing_address_keys = ["my-div-address-1"] + shipping_address_keys = ["my-div-address-1", "my-div-address-2"] + default_billing_address_key = "my-div-address-1" + default_shipping_address_key = "my-div-address-1" +} +``` + + +## Schema + +### Required + +- `key` (String) User-defined unique identifier for the Division. +- `name` (String) The name of the Division. + +### Optional + +- `address` (Block List) Addresses used by the Business Unit. (see [below for nested schema](#nestedblock--address)) +- `approval_rule_mode` (String) Determines whether the Business Unit can inherit Approval Rules from a parent. Defaults to `ExplicitAndFromParent`. +- `associate_mode` (String) Determines whether the Business Unit can inherit Associates from a parent. Defaults to `ExplicitAndFromParent`. +- `billing_address_keys` (List of String) List of the billing addresses used by the Division. +- `contact_email` (String) The email address of the Division. +- `default_billing_address_key` (String) Key of the default billing Address. +- `default_shipping_address_key` (String) Key of the default shipping Address. +- `parent_unit` (Block, Optional) Reference to a parent Business Unit by its key. (see [below for nested schema](#nestedblock--parent_unit)) +- `shipping_address_keys` (List of String) List of the shipping addresses used by the Division. +- `status` (String) Indicates whether the Business Unit can be edited and used in [Orders](https://docs.commercetools.com/api/projects/orders). Defaults to `Active`. +- `store` (Block List) Sets the Stores the Business Unit is associated with. + +If the Business Unit has Stores defined, then all of its Carts, Orders, Quotes, or Quote Requests must belong to one of the Business Unit's Stores. + +If the Business Unit has no Stores, then all of its Carts, Orders, Quotes, or Quote Requests must not belong to any Store. (see [below for nested schema](#nestedblock--store)) +- `store_mode` (String) Defines whether the Stores of the Business Unit are set directly on the Business Unit or are inherited from a parent. Defaults to `FromParent` + +### Read-Only + +- `id` (String) Unique identifier of the Division. +- `version` (Number) The current version of the Division. + + +### Nested Schema for `address` + +Required: + +- `country` (String) Name of the country +- `key` (String) User-defined identifier of the Address that must be unique when multiple addresses are referenced in BusinessUnits, Customers, and itemShippingAddresses (LineItem-specific addresses) of a Cart, Order, QuoteRequest, or Quote. + +Optional: + +- `additional_address_info` (String) Further information on the Address +- `additional_street_info` (String) Further information on the street address +- `apartment` (String) Name or number of the apartment +- `building` (String) Name or number of the building +- `city` (String) Name of the city +- `company` (String) Name of the company +- `department` (String) Name of the department +- `email` (String) Email address +- `external_id` (String) ID for the contact used in an external system +- `fax` (String) Fax number +- `first_name` (String) First name of the contact +- `last_name` (String) Last name of the contact +- `mobile` (String) Mobile phone number +- `phone` (String) Phone number +- `po_box` (String) Post office box number +- `postal_code` (String) Postal code +- `region` (String) Name of the region +- `salutation` (String) Salutation of the contact, for example Ms., Mr. +- `state` (String) Name of the state +- `street_name` (String) Name of the street +- `street_number` (String) Street number +- `title` (String) Title of the contact, for example Dr., Prof. + +Read-Only: + +- `id` (String) Unique identifier of the Address + + + +### Nested Schema for `parent_unit` + +Optional: + +- `id` (String) User-defined unique identifier of the Business Unit +- `key` (String) User-defined unique key of the Business Unit + + + +### Nested Schema for `store` + +Optional: + +- `key` (String) User-defined unique identifier of the Store diff --git a/docs/resources/category.md b/docs/resources/category.md index 33d1ed6b..b9a51305 100644 --- a/docs/resources/category.md +++ b/docs/resources/category.md @@ -107,7 +107,7 @@ Optional: Read-Only: -- `id` (String) The ID of this resource. +- `id` (String) ### Nested Schema for `assets.sources` diff --git a/docs/resources/channel.md b/docs/resources/channel.md index 507c529b..c6902ba2 100644 --- a/docs/resources/channel.md +++ b/docs/resources/channel.md @@ -84,7 +84,7 @@ Optional: Read-Only: -- `id` (String) The ID of this resource. +- `id` (String) diff --git a/docs/resources/project_settings.md b/docs/resources/project_settings.md index 6fe73cc0..1ea72d3e 100644 --- a/docs/resources/project_settings.md +++ b/docs/resources/project_settings.md @@ -49,6 +49,7 @@ resource "commercetools_project_settings" "my-project" { ### Optional +- `business_units` (Block List) Holds configuration specific to [Business Units](https://docs.commercetools.com/api/projects/business-units#ctp:api:type:BusinessUnit). (see [below for nested schema](#nestedblock--business_units)) - `carts` (Block List) [Carts Configuration](https://docs.commercetools.com/api/projects/project#carts-configuration) (see [below for nested schema](#nestedblock--carts)) - `countries` (List of String) A two-digit country code as per [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) - `currencies` (List of String) A three-digit currency code as per [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) @@ -68,6 +69,15 @@ resource "commercetools_project_settings" "my-project" { - `key` (String) The unique key of the project - `version` (Number) + +### Nested Schema for `business_units` + +Optional: + +- `my_business_unit_associate_role_key_on_creation` (String) Default Associate Role assigned to the Associate creating a Business Unit using the My Business Unit endpoint. Note that this field cannot be unset once assigned! +- `my_business_unit_status_on_creation` (String) Status of Business Units created using the My Business Unit endpoint. + + ### Nested Schema for `carts` diff --git a/docs/resources/shipping_zone_rate.md b/docs/resources/shipping_zone_rate.md index 1341b80f..3a9420c5 100644 --- a/docs/resources/shipping_zone_rate.md +++ b/docs/resources/shipping_zone_rate.md @@ -159,5 +159,5 @@ Required: Import is supported using the following syntax: ```shell -terraform import commercetools_shipping_zone_rate.my-shipping-zone-rate {shipping-method-id}@{shipping-zone-id}@{currency} +terraform import commercetools_shipping_zone_rate.my-shipping-zone-rate {my-shipping-method-id}@{my-shipping-zone-id}@{currency} ``` diff --git a/examples/resources/commercetools_associate_role/resource.tf b/examples/resources/commercetools_associate_role/resource.tf index 1fb59345..2cfb43bd 100644 --- a/examples/resources/commercetools_associate_role/resource.tf +++ b/examples/resources/commercetools_associate_role/resource.tf @@ -4,11 +4,43 @@ resource "commercetools_associate_role" "regional_manager" { name = "Regional Manager - Europe" permissions = [ "AddChildUnits", - "UpdateBusinessUnitDetails", "UpdateAssociates", + "UpdateBusinessUnitDetails", + "UpdateParentUnit", + "ViewMyCarts", + "ViewOthersCarts", + "UpdateMyCarts", + "UpdateOthersCarts", "CreateMyCarts", + "CreateOthersCarts", "DeleteMyCarts", - "UpdateMyCarts", - "ViewMyCarts", + "DeleteOthersCarts", + "ViewMyOrders", + "ViewOthersOrders", + "UpdateMyOrders", + "UpdateOthersOrders", + "CreateMyOrdersFromMyCarts", + "CreateMyOrdersFromMyQuotes", + "CreateOrdersFromOthersCarts", + "CreateOrdersFromOthersQuotes", + "ViewMyQuotes", + "ViewOthersQuotes", + "AcceptMyQuotes", + "AcceptOthersQuotes", + "DeclineMyQuotes", + "DeclineOthersQuotes", + "RenegotiateMyQuotes", + "RenegotiateOthersQuotes", + "ReassignMyQuotes", + "ReassignOthersQuotes", + "ViewMyQuoteRequests", + "ViewOthersQuoteRequests", + "UpdateMyQuoteRequests", + "UpdateOthersQuoteRequests", + "CreateMyQuoteRequestsFromMyCarts", + "CreateQuoteRequestsFromOthersCarts", + "CreateApprovalRules", + "UpdateApprovalRules", + "UpdateApprovalFlows", ] } diff --git a/examples/resources/commercetools_business_unit/resource.tf b/examples/resources/commercetools_business_unit/resource.tf deleted file mode 100644 index 3f52cf9b..00000000 --- a/examples/resources/commercetools_business_unit/resource.tf +++ /dev/null @@ -1,57 +0,0 @@ -resource "commercetools_business_unit_company" "acme_company" { - key = "acme-company" - name = "The ACME Company" - status = "Active" - contact_email = "acme@example.com" - - store { - key = "acme-usa" - type_id = "store" - } - - store { - key = "acme-germany" - type_id = "store" - } - - address { - key = "acme-business-unit-address" - title = "Acme Business Unit Address" - salutation = "Mr." - first_name = "John" - last_name = "Doe" - street_name = "Main Street" - street_number = "1" - additional_street_info = "Additional Street Info" - postal_code = "12345" - city = "Berlin" - region = "Berlin" - country = "DE" - company = "Acme" - department = "IT" - building = "Building" - apartment = "Apartment" - po_box = "P.O. Box" - phone = "123456789" - mobile = "987654321" - } - - default_shipping_address_id = "acme-business-unit-address" - default_billing_address_id = "acme-business-unit-address" -} - -resource "commercetools_business_unit_division" "acme-willie-coyote" { - key = "acme-willie-coyote" - name = "Willie Coyote - Traps for Roadrunners" - status = "Active" - contact_email = "acme-traps@example.com" - store_mode = "FromParent" - associate_mode = "ExplicitAndFromParent" - - // Only available for division business units as the Company - // business unit has no parent unit and must always be the Top Level Unit. - parent_unit { - key = commercetools_business_unit_company.acme-company.key - type_id = "company" - } -} diff --git a/examples/resources/commercetools_business_unit_company/resource.tf b/examples/resources/commercetools_business_unit_company/resource.tf new file mode 100644 index 00000000..6f13923a --- /dev/null +++ b/examples/resources/commercetools_business_unit_company/resource.tf @@ -0,0 +1,45 @@ +resource "commercetools_store" "my-store" { + key = "my-store" + name = { + en-US = "My store" + } + countries = ["NL", "BE"] + languages = ["en-GB"] +} + +resource "commercetools_business_unit_company" "my-company" { + key = "my-company" + name = "My company" + contact_email = "main@my-company.com" + + address { + key = "my-company-address-1" + country = "NL" + state = "Noord-Holland" + city = "Amsterdam" + street_name = "Keizersgracht" + street_number = "3" + additional_street_info = "4th floor" + postal_code = "1015 CJ" + } + + address { + key = "my-company-address-2" + country = "NL" + state = "Utrecht" + city = "Utrecht" + street_name = "Oudegracht" + street_number = "1" + postal_code = "3511 AA" + additional_street_info = "Main floor" + } + + store { + key = commercetools_store.my-store.key + } + + billing_address_keys = ["my-company-address-1"] + shipping_address_keys = ["my-company-address-1", "my-company-address-2"] + default_billing_address_key = "my-company-address-1" + default_shipping_address_key = "my-company-address-1" +} diff --git a/examples/resources/commercetools_business_unit_division/resource.tf b/examples/resources/commercetools_business_unit_division/resource.tf new file mode 100644 index 00000000..02923912 --- /dev/null +++ b/examples/resources/commercetools_business_unit_division/resource.tf @@ -0,0 +1,57 @@ +resource "commercetools_store" "my-store" { + key = "my-store" + name = { + en-US = "My store" + } + countries = ["NL", "BE"] + languages = ["en-GB"] +} + +resource "commercetools_business_unit_company" "my-company" { + key = "my-company" + name = "My company" + contact_email = "main@my-company.com" +} + +resource "commercetools_business_unit_division" "my-division" { + key = "my-division" + name = "My division" + contact_email = "my-division@my-company.com" + store_mode = "Explicit" + status = "Active" + associate_mode = "Explicit" + approval_rule_mode = "Explicit" + + parent_unit { + key = commercetools_business_unit_company.my-company.key + } + + store { + key = commercetools_store.my-store.key + } + + address { + key = "my-div-address-1" + country = "NL" + state = "Utrecht" + city = "Utrecht" + street_name = "Oudegracht" + street_number = "1" + postal_code = "3511 AA" + additional_street_info = "Main floor" + } + + address { + key = "my-div-address-2" + country = "NL" + state = "Zuid-Holland" + city = "Leiden" + street_name = "Breestraat" + street_number = "1" + postal_code = "2311 CH" + } + billing_address_keys = ["my-div-address-1"] + shipping_address_keys = ["my-div-address-1", "my-div-address-2"] + default_billing_address_key = "my-div-address-1" + default_shipping_address_key = "my-div-address-1" +} diff --git a/go.mod b/go.mod index a155a915..6305c62c 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,8 @@ module github.com/labd/terraform-provider-commercetools go 1.21 +//replace github.com/labd/commercetools-go-sdk v1.5.1 => ../commercetools-go-sdk + require ( github.com/elliotchance/orderedmap/v2 v2.2.0 github.com/elliotchance/pie/v2 v2.8.0 @@ -12,9 +14,10 @@ require ( github.com/hashicorp/terraform-plugin-go v0.23.0 github.com/hashicorp/terraform-plugin-mux v0.16.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.33.0 - github.com/labd/commercetools-go-sdk v1.5.1 + github.com/labd/commercetools-go-sdk v1.6.0 + github.com/mitchellh/mapstructure v1.5.0 github.com/stretchr/testify v1.9.0 - golang.org/x/oauth2 v0.20.0 + golang.org/x/oauth2 v0.21.0 golang.org/x/text v0.15.0 ) @@ -62,7 +65,6 @@ require ( github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/oklog/run v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/go.sum b/go.sum index faf9a823..f52b74a8 100644 --- a/go.sum +++ b/go.sum @@ -143,8 +143,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/labd/commercetools-go-sdk v1.5.1 h1:1JhKrfokvbTU+BqwfZLgMrUb+R5XEAfSscI4Yk8ic68= -github.com/labd/commercetools-go-sdk v1.5.1/go.mod h1:ta63PQfTBeuwxvUECiJFPZf87E4M0h/vfzeJY54YnsI= +github.com/labd/commercetools-go-sdk v1.6.0 h1:f+hXCSea6WSsANzPliUZes/qbIBBMyhZF1WhBcnLneM= +github.com/labd/commercetools-go-sdk v1.6.0/go.mod h1:3K76EpprufmZhqmcEZ+lPAKeK7tDUzEq81gT9pzrvyQ= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -230,8 +230,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= -golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 7e9b4d1e..f5e93baf 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -20,7 +20,8 @@ import ( datasourcetype "github.com/labd/terraform-provider-commercetools/internal/datasource/type" "github.com/labd/terraform-provider-commercetools/internal/resources/associate_role" "github.com/labd/terraform-provider-commercetools/internal/resources/attribute_group" - "github.com/labd/terraform-provider-commercetools/internal/resources/business_unit" + "github.com/labd/terraform-provider-commercetools/internal/resources/business_unit_company" + "github.com/labd/terraform-provider-commercetools/internal/resources/business_unit_division" "github.com/labd/terraform-provider-commercetools/internal/resources/product_selection" "github.com/labd/terraform-provider-commercetools/internal/resources/project" "github.com/labd/terraform-provider-commercetools/internal/resources/state" @@ -197,7 +198,7 @@ func (p *ctProvider) Resources(_ context.Context) []func() resource.Resource { attribute_group.NewResource, associate_role.NewResource, product_selection.NewResource, - business_unit.NewCompanyResource, - business_unit.NewDivisionResource, + business_unit_company.NewCompanyResource, + business_unit_division.NewDivisionResource, } } diff --git a/internal/resources/associate_role/model.go b/internal/resources/associate_role/model.go index d124b01b..4594953f 100644 --- a/internal/resources/associate_role/model.go +++ b/internal/resources/associate_role/model.go @@ -50,7 +50,7 @@ func (ar AssociateRole) updateActions(plan AssociateRole) platform.AssociateRole } // setName - if ar.Name != plan.Name { + if !ar.Name.Equal(plan.Name) { var newName *string if !plan.Name.IsNull() && !plan.Name.IsUnknown() { newName = utils.StringRef(plan.Name.ValueString()) @@ -63,7 +63,7 @@ func (ar AssociateRole) updateActions(plan AssociateRole) platform.AssociateRole } // setBuyerAssignable value - if ar.BuyerAssignable != plan.BuyerAssignable { + if !ar.BuyerAssignable.Equal(plan.BuyerAssignable) { result.Actions = append( result.Actions, platform.AssociateRoleChangeBuyerAssignableAction{ diff --git a/internal/resources/associate_role/resource.go b/internal/resources/associate_role/resource.go index 03d7890c..6d7029df 100644 --- a/internal/resources/associate_role/resource.go +++ b/internal/resources/associate_role/resource.go @@ -2,6 +2,9 @@ package associate_role import ( "context" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "regexp" "time" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" @@ -36,37 +39,91 @@ func NewResource() resource.Resource { // Schema implements resource.Resource. func (*associateRoleResource) Schema(_ context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ - Description: "Associate Roles provide a way to group granular Permissions and assign " + + MarkdownDescription: "Associate Roles provide a way to group granular Permissions and assign " + "them to Associates within a Business Unit.\n\n" + "See also the [Associate Role API Documentation](https://docs.commercetools.com/api/projects/associate-roles)", Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "Unique identifier of the AssociateRole.", - Computed: true, + MarkdownDescription: "Unique identifier of the associate role.", + Computed: true, }, "version": schema.Int64Attribute{ - Description: "Current version of the AssociateRole.", - Computed: true, + MarkdownDescription: "Current version of the associate role.", + Computed: true, }, "key": schema.StringAttribute{ - Description: "User-defined unique identifier of the AssociateRole.", - Required: true, + MarkdownDescription: "User-defined unique identifier of the associate role.", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(2, 256), + stringvalidator.RegexMatches( + regexp.MustCompile("^[A-Za-z0-9_-]+$"), + "Key must match pattern ^[A-Za-z0-9_-]+$", + ), + }, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "Name of the associate role.", + Optional: true, }, "buyer_assignable": schema.BoolAttribute{ - Description: "Whether the AssociateRole can be assigned to an Associate by a buyer. If false, " + - "the AssociateRole can only be assigned using the general endpoint.", + MarkdownDescription: "Whether the associate role can be assigned to an associate by a buyer. If false, " + + "the associate role can only be assigned using the general endpoint. Defaults to true.", Optional: true, - }, - "name": schema.StringAttribute{ - Description: "Name of the AssociateRole.", - Optional: true, + Computed: true, + Default: booldefault.StaticBool(true), }, "permissions": schema.ListAttribute{ Required: true, ElementType: types.StringType, - Description: "List of Permissions for the AssociateRole.", + MarkdownDescription: "List of permissions for the associate role. See the [Associate Role API " + + "Documentation](https://docs.commercetools.com/api/projects/associate-roles#ctp:api:type:Permission) " + + "for more information.", Validators: []validator.List{ - listvalidator.SizeAtLeast(1), + listvalidator.UniqueValues(), + listvalidator.ValueStringsAre( + stringvalidator.OneOf( + string(platform.PermissionAddChildUnits), + string(platform.PermissionUpdateAssociates), + string(platform.PermissionUpdateBusinessUnitDetails), + string(platform.PermissionUpdateParentUnit), + string(platform.PermissionViewMyCarts), + string(platform.PermissionViewOthersCarts), + string(platform.PermissionUpdateMyCarts), + string(platform.PermissionUpdateOthersCarts), + string(platform.PermissionCreateMyCarts), + string(platform.PermissionCreateOthersCarts), + string(platform.PermissionDeleteMyCarts), + string(platform.PermissionDeleteOthersCarts), + string(platform.PermissionViewMyOrders), + string(platform.PermissionViewOthersOrders), + string(platform.PermissionUpdateMyOrders), + string(platform.PermissionUpdateOthersOrders), + string(platform.PermissionCreateMyOrdersFromMyCarts), + string(platform.PermissionCreateMyOrdersFromMyQuotes), + string(platform.PermissionCreateOrdersFromOthersCarts), + string(platform.PermissionCreateOrdersFromOthersQuotes), + string(platform.PermissionViewMyQuotes), + string(platform.PermissionViewOthersQuotes), + string(platform.PermissionAcceptMyQuotes), + string(platform.PermissionAcceptOthersQuotes), + string(platform.PermissionDeclineMyQuotes), + string(platform.PermissionDeclineOthersQuotes), + string(platform.PermissionRenegotiateMyQuotes), + string(platform.PermissionRenegotiateOthersQuotes), + string(platform.PermissionReassignMyQuotes), + string(platform.PermissionReassignOthersQuotes), + string(platform.PermissionViewMyQuoteRequests), + string(platform.PermissionViewOthersQuoteRequests), + string(platform.PermissionUpdateMyQuoteRequests), + string(platform.PermissionUpdateOthersQuoteRequests), + string(platform.PermissionCreateMyQuoteRequestsFromMyCarts), + string(platform.PermissionCreateQuoteRequestsFromOthersCarts), + string(platform.PermissionCreateApprovalRules), + string(platform.PermissionUpdateApprovalRules), + string(platform.PermissionUpdateApprovalFlows), + ), + ), }, PlanModifiers: []planmodifier.List{ listplanmodifier.UseStateForUnknown(), diff --git a/internal/resources/business_unit/model.go b/internal/resources/business_unit/model.go deleted file mode 100644 index d7552d07..00000000 --- a/internal/resources/business_unit/model.go +++ /dev/null @@ -1,809 +0,0 @@ -package business_unit - -import ( - "reflect" - - "github.com/elliotchance/pie/v2" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/labd/commercetools-go-sdk/platform" - "github.com/labd/terraform-provider-commercetools/internal/utils" -) - -const ( - // Store modes for business units. - StoreModeExplicit = "Explicit" - StoreModeFromParent = "FromParent" - - // Statuses for business units. - BusinessUnitActive = "Active" - BusinessUnitInactive = "Inactive" - - // Unit types for business units. - CompanyType = "Company" - DivisionType = "Division" - - // Associate modes for business units. - ExplicitAssociateMode = "Explicit" - ExplicitAndFromParentAssociateMode = "ExplicitAndFromParent" - - // Associate role inheritance for business units. - AssociateRoleInheritanceEnabled = "Enabled" - AssociateRoleInheritanceDisabled = "Disabled" - - // Store type id. - StoreTypeID = "store" - AssociateRoleTypeID = "associate-role" - BusinessUnitTypeID = "business-unit" - CustomerTypeID = "customer" -) - -/* -Model types for the business unit resource. -*/ -type Company struct { - ID types.String `tfsdk:"id"` - Version types.Int64 `tfsdk:"version"` - Key types.String `tfsdk:"key"` - Name types.String `tfsdk:"name"` - Status types.String `tfsdk:"status"` - ContactEmail types.String `tfsdk:"contact_email"` - Stores []StoreKeyReference `tfsdk:"store"` - Addresses []Address `tfsdk:"address"` - ShippingAddressIDs []types.String `tfsdk:"shipping_address_ids"` - DefaultShippingAddressID types.String `tfsdk:"default_shipping_address_id"` - BillingAddressIDs []types.String `tfsdk:"billing_address_ids"` - DefaultBillingAddressID types.String `tfsdk:"default_billing_address_id"` - Associates []Associate `tfsdk:"associate"` -} - -func (c Company) draft() platform.CompanyDraft { - status := platform.BusinessUnitStatus(c.Status.ValueString()) - dsa := pie.Int(c.DefaultShippingAddressID.ValueString()) - dba := pie.Int(c.DefaultBillingAddressID.ValueString()) - - return platform.CompanyDraft{ - Key: c.Key.ValueString(), - Status: &status, - Stores: pie.Map(c.Stores, func(s StoreKeyReference) platform.StoreResourceIdentifier { - return platform.StoreResourceIdentifier{ - Key: s.Key.ValueStringPointer(), - ID: s.Key.ValueStringPointer(), - } - }), - Name: c.Name.ValueString(), - ContactEmail: c.ContactEmail.ValueStringPointer(), - Associates: pie.Map(c.Associates, func(a Associate) platform.AssociateDraft { - return a.draft() - }), - Addresses: pie.Map(c.Addresses, func(a Address) platform.BaseAddress { - return a.draft() - }), - ShippingAddresses: pie.Map(c.ShippingAddressIDs, func(id types.String) int { - return pie.Int(id.ValueString()) - }), - DefaultShippingAddress: &dsa, - BillingAddresses: pie.Map(c.BillingAddressIDs, func(id types.String) int { - return pie.Int(id.ValueString()) - }), - DefaultBillingAddress: &dba, - } -} - -func (c Company) updateActions(plan Company) platform.BusinessUnitUpdate { - result := platform.BusinessUnitUpdate{ - Version: int(c.Version.ValueInt64()), - Actions: []platform.BusinessUnitUpdateAction{}, - } - - if c.Name.ValueString() != plan.Name.ValueString() { - result.Actions = append(result.Actions, platform.BusinessUnitChangeNameAction{ - Name: plan.Name.ValueString(), - }) - } - - if c.ContactEmail.ValueString() != plan.ContactEmail.ValueString() { - result.Actions = append(result.Actions, platform.BusinessUnitSetContactEmailAction{ - ContactEmail: plan.ContactEmail.ValueStringPointer(), - }) - } - - if c.Status.ValueString() != plan.Status.ValueString() { - result.Actions = append(result.Actions, platform.BusinessUnitChangeStatusAction{ - Status: plan.Status.ValueString(), - }) - } - - if c.DefaultShippingAddressID.ValueString() != plan.DefaultShippingAddressID.ValueString() { - result.Actions = append(result.Actions, platform.BusinessUnitSetDefaultShippingAddressAction{ - AddressId: plan.DefaultShippingAddressID.ValueStringPointer(), - }) - } - - if c.DefaultBillingAddressID.ValueString() != plan.DefaultBillingAddressID.ValueString() { - result.Actions = append(result.Actions, platform.BusinessUnitSetDefaultBillingAddressAction{ - AddressId: plan.DefaultBillingAddressID.ValueStringPointer(), - }) - } - - if !reflect.DeepEqual(c.Stores, plan.Stores) { - // find stores to be added - for _, store := range plan.Stores { - if !pie.Contains(c.Stores, store) { - result.Actions = append(result.Actions, platform.BusinessUnitAddStoreAction{ - Store: platform.StoreResourceIdentifier{ - Key: store.Key.ValueStringPointer(), - }, - }) - } - } - - // find stores to be removed - for _, store := range c.Stores { - if !pie.Contains(plan.Stores, store) { - result.Actions = append(result.Actions, platform.BusinessUnitRemoveStoreAction{ - Store: platform.StoreResourceIdentifier{ - Key: store.Key.ValueStringPointer(), - }, - }) - } - } - } - - if !reflect.DeepEqual(c.Addresses, plan.Addresses) { - // find addresses to be added - for _, address := range plan.Addresses { - if !pie.Contains(c.Addresses, address) { - result.Actions = append(result.Actions, platform.BusinessUnitAddAddressAction{ - Address: address.draft(), - }) - } - } - - // find addresses to be removed - for _, address := range c.Addresses { - if !pie.Contains(plan.Addresses, address) { - result.Actions = append(result.Actions, platform.BusinessUnitRemoveAddressAction{ - AddressId: address.ID.ValueStringPointer(), - AddressKey: address.Key.ValueStringPointer(), - }) - } - } - } - - if !reflect.DeepEqual(c.ShippingAddressIDs, plan.ShippingAddressIDs) { - // find shipping addresses to be added - for _, id := range plan.ShippingAddressIDs { - if !pie.Contains(c.ShippingAddressIDs, id) { - result.Actions = append(result.Actions, platform.BusinessUnitAddShippingAddressIdAction{ - AddressId: id.ValueStringPointer(), - }) - } - } - - // find shipping addresses to be removed - for _, id := range c.ShippingAddressIDs { - if !pie.Contains(plan.ShippingAddressIDs, id) { - result.Actions = append(result.Actions, platform.BusinessUnitRemoveShippingAddressIdAction{ - AddressId: id.ValueStringPointer(), - }) - } - } - } - - if !reflect.DeepEqual(c.BillingAddressIDs, plan.BillingAddressIDs) { - // find billing addresses to be added - for _, id := range plan.BillingAddressIDs { - if !pie.Contains(c.BillingAddressIDs, id) { - result.Actions = append(result.Actions, platform.BusinessUnitAddBillingAddressIdAction{ - AddressId: id.ValueStringPointer(), - }) - } - } - - // find billing addresses to be removed - for _, id := range c.BillingAddressIDs { - if !pie.Contains(plan.BillingAddressIDs, id) { - result.Actions = append(result.Actions, platform.BusinessUnitRemoveBillingAddressIdAction{ - AddressId: id.ValueStringPointer(), - }) - } - } - } - - if !reflect.DeepEqual(c.Associates, plan.Associates) { - result.Actions = append(result.Actions, platform.BusinessUnitSetAssociatesAction{ - Associates: pie.Map(plan.Associates, func(a Associate) platform.AssociateDraft { - return a.draft() - }), - }) - } - - return result -} - -// NewCompanyFromNative creates a new Company from a platform.Company. -func NewCompanyFromNative(cc platform.BusinessUnit) Company { - c, ok := cc.(Company) - if !ok { - return Company{} - } - - company := Company{ - ID: types.StringPointerValue(utils.StringRef(c.ID)), - Version: types.Int64PointerValue(c.Version.ValueInt64Pointer()), - Key: types.StringPointerValue(utils.StringRef(c.Key)), - Name: types.StringPointerValue(utils.StringRef(c.Name)), - Status: types.StringPointerValue(utils.StringRef(c.Status)), - ContactEmail: types.StringPointerValue(c.ContactEmail.ValueStringPointer()), - DefaultShippingAddressID: types.StringPointerValue(c.DefaultShippingAddressID.ValueStringPointer()), - DefaultBillingAddressID: types.StringPointerValue(c.DefaultBillingAddressID.ValueStringPointer()), - Stores: make([]StoreKeyReference, len(c.Stores)), - Addresses: make([]Address, len(c.Addresses)), - ShippingAddressIDs: make([]types.String, len(c.ShippingAddressIDs)), - BillingAddressIDs: make([]types.String, len(c.BillingAddressIDs)), - Associates: make([]Associate, len(c.Associates)), - } - - for i, store := range c.Stores { - company.Stores[i] = StoreKeyReference{ - Key: types.StringValue(store.Key.ValueString()), - } - } - - copy(company.Addresses, c.Addresses) - - for i, id := range c.ShippingAddressIDs { - company.ShippingAddressIDs[i] = types.StringValue(id.ValueString()) - } - - for i, id := range c.BillingAddressIDs { - company.BillingAddressIDs[i] = types.StringValue(id.ValueString()) - } - - copy(company.Associates, c.Associates) - - return company -} - -type Division struct { - ID types.String `tfsdk:"id"` - Version types.Int64 `tfsdk:"version"` - Key types.String `tfsdk:"key"` - Status types.String `tfsdk:"status"` - ParentUnit BusinessUnitResourceIdentifier `tfsdk:"parent_unit"` - Stores []StoreKeyReference `tfsdk:"store"` - StoreMode types.String `tfsdk:"store_mode"` - Name types.String `tfsdk:"name"` - ContactEmail types.String `tfsdk:"contact_email"` - Addresses []Address `tfsdk:"address"` - ShippingAddressIDs []types.String `tfsdk:"shipping_address_ids"` - DefaultShippingAddressID types.String `tfsdk:"default_shipping_address_id"` - BillingAddressIDs []types.String `tfsdk:"billing_address_ids"` - DefaultBillingAddressID types.String `tfsdk:"default_billing_address_id"` - AssociateMode types.String `tfsdk:"associate_mode"` - Associates []Associate `tfsdk:"associate"` - InheritedAssociates []InheritedAssociate `tfsdk:"inherited_associates"` -} - -func (d Division) draft() platform.DivisionDraft { - mode := platform.BusinessUnitStoreMode(d.StoreMode.ValueString()) - associateMode := platform.BusinessUnitAssociateMode(d.AssociateMode.ValueString()) - status := platform.BusinessUnitStatus(d.Status.ValueString()) - dsa := pie.Int(d.DefaultShippingAddressID.ValueString()) - dba := pie.Int(d.DefaultBillingAddressID.ValueString()) - - return platform.DivisionDraft{ - Key: d.Key.ValueString(), - Status: &status, - ParentUnit: platform.BusinessUnitResourceIdentifier{ - ID: d.ParentUnit.ID.ValueStringPointer(), - Key: d.ParentUnit.Key.ValueStringPointer(), - }, - Stores: pie.Map(d.Stores, func(s StoreKeyReference) platform.StoreResourceIdentifier { - return platform.StoreResourceIdentifier{ - Key: s.Key.ValueStringPointer(), - ID: s.Key.ValueStringPointer(), - } - }), - StoreMode: &mode, - Name: d.Name.ValueString(), - ContactEmail: d.ContactEmail.ValueStringPointer(), - AssociateMode: &associateMode, - Associates: pie.Map(d.Associates, func(a Associate) platform.AssociateDraft { - return a.draft() - }), - Addresses: pie.Map(d.Addresses, func(a Address) platform.BaseAddress { - return a.draft() - }), - ShippingAddresses: pie.Map(d.ShippingAddressIDs, func(id types.String) int { - return pie.Int(id.ValueString()) - }), - DefaultShippingAddress: &dsa, - BillingAddresses: pie.Map(d.BillingAddressIDs, func(id types.String) int { - return pie.Int(id.ValueString()) - }), - DefaultBillingAddress: &dba, - } -} - -func (d Division) updateActions(plan Division) platform.BusinessUnitUpdate { - result := platform.BusinessUnitUpdate{ - Version: int(d.Version.ValueInt64()), - Actions: []platform.BusinessUnitUpdateAction{}, - } - - if d.Name.ValueString() != plan.Name.ValueString() { - result.Actions = append(result.Actions, platform.BusinessUnitChangeNameAction{ - Name: plan.Name.ValueString(), - }) - } - - if d.ContactEmail.ValueString() != plan.ContactEmail.ValueString() { - result.Actions = append(result.Actions, platform.BusinessUnitSetContactEmailAction{ - ContactEmail: plan.ContactEmail.ValueStringPointer(), - }) - } - - if d.Status.ValueString() != plan.Status.ValueString() { - result.Actions = append(result.Actions, platform.BusinessUnitChangeStatusAction{ - Status: plan.Status.ValueString(), - }) - } - - if d.DefaultShippingAddressID.ValueString() != plan.DefaultShippingAddressID.ValueString() { - result.Actions = append(result.Actions, platform.BusinessUnitSetDefaultShippingAddressAction{ - AddressId: plan.DefaultShippingAddressID.ValueStringPointer(), - }) - } - - if d.DefaultBillingAddressID.ValueString() != plan.DefaultBillingAddressID.ValueString() { - result.Actions = append(result.Actions, platform.BusinessUnitSetDefaultBillingAddressAction{ - AddressId: plan.DefaultBillingAddressID.ValueStringPointer(), - }) - } - - if d.AssociateMode.ValueString() != plan.AssociateMode.ValueString() { - result.Actions = append(result.Actions, platform.BusinessUnitChangeAssociateModeAction{ - AssociateMode: platform.BusinessUnitAssociateMode(plan.AssociateMode.ValueString()), - }) - } - - if d.StoreMode.ValueString() != plan.StoreMode.ValueString() { - result.Actions = append(result.Actions, platform.BusinessUnitSetStoreModeAction{ - StoreMode: platform.BusinessUnitStoreMode(plan.StoreMode.ValueString()), - }) - } - - if !reflect.DeepEqual(d.Stores, plan.Stores) { - // find stores to be added - for _, store := range plan.Stores { - if !pie.Contains(d.Stores, store) { - result.Actions = append(result.Actions, platform.BusinessUnitAddStoreAction{ - Store: platform.StoreResourceIdentifier{ - Key: store.Key.ValueStringPointer(), - }, - }) - } - } - - // find stores to be removed - for _, store := range d.Stores { - if !pie.Contains(plan.Stores, store) { - result.Actions = append(result.Actions, platform.BusinessUnitRemoveStoreAction{ - Store: platform.StoreResourceIdentifier{ - Key: store.Key.ValueStringPointer(), - }, - }) - } - } - - if !reflect.DeepEqual(d.BillingAddressIDs, plan.BillingAddressIDs) { - // find billing addresses to be added - for _, id := range plan.BillingAddressIDs { - if !pie.Contains(d.BillingAddressIDs, id) { - result.Actions = append(result.Actions, platform.BusinessUnitAddBillingAddressIdAction{ - AddressId: id.ValueStringPointer(), - }) - } - } - - // find billing addresses to be removed - for _, id := range d.BillingAddressIDs { - if !pie.Contains(plan.BillingAddressIDs, id) { - result.Actions = append(result.Actions, platform.BusinessUnitRemoveBillingAddressIdAction{ - AddressId: id.ValueStringPointer(), - }) - } - } - } - } - - if !reflect.DeepEqual(d.Associates, plan.Associates) { - result.Actions = append(result.Actions, platform.BusinessUnitSetAssociatesAction{ - Associates: pie.Map(plan.Associates, func(a Associate) platform.AssociateDraft { - return a.draft() - }), - }) - } - - if !reflect.DeepEqual(d.Addresses, plan.Addresses) { - // find addresses to be added - for _, address := range plan.Addresses { - if !pie.Contains(d.Addresses, address) { - result.Actions = append(result.Actions, platform.BusinessUnitAddAddressAction{ - Address: address.draft(), - }) - } - } - - // find addresses to be removed - for _, address := range d.Addresses { - if !pie.Contains(plan.Addresses, address) { - result.Actions = append(result.Actions, platform.BusinessUnitRemoveAddressAction{ - AddressId: address.ID.ValueStringPointer(), - AddressKey: address.Key.ValueStringPointer(), - }) - } - } - } - - if !reflect.DeepEqual(d.ShippingAddressIDs, plan.ShippingAddressIDs) { - // find shipping addresses to be added - for _, id := range plan.ShippingAddressIDs { - if !pie.Contains(d.ShippingAddressIDs, id) { - result.Actions = append(result.Actions, platform.BusinessUnitAddShippingAddressIdAction{ - AddressId: id.ValueStringPointer(), - }) - } - } - - // find shipping addresses to be removed - for _, id := range d.ShippingAddressIDs { - if !pie.Contains(plan.ShippingAddressIDs, id) { - result.Actions = append(result.Actions, platform.BusinessUnitRemoveShippingAddressIdAction{ - AddressId: id.ValueStringPointer(), - }) - } - } - } - - return result -} - -// NewDivisionFromNative creates a new Division from a platform.Company. -func NewDivisionFromNative(cc platform.BusinessUnit) Division { - c, ok := cc.(Division) - if !ok { - return Division{} - } - - parent := BusinessUnitResourceIdentifier{ - ID: types.StringValue(c.ParentUnit.ID.ValueString()), - Key: types.StringValue(c.ParentUnit.Key.ValueString()), - } - - company := Division{ - ID: types.StringPointerValue(utils.StringRef(c.ID)), - Version: types.Int64PointerValue(c.Version.ValueInt64Pointer()), - Key: types.StringPointerValue(utils.StringRef(c.Key)), - Status: types.StringPointerValue(utils.StringRef(c.Status)), - ParentUnit: parent, - StoreMode: types.StringValue(c.StoreMode.ValueString()), - Name: types.StringPointerValue(utils.StringRef(c.Name)), - ContactEmail: types.StringPointerValue(c.ContactEmail.ValueStringPointer()), - DefaultShippingAddressID: types.StringPointerValue(c.DefaultShippingAddressID.ValueStringPointer()), - DefaultBillingAddressID: types.StringPointerValue(c.DefaultBillingAddressID.ValueStringPointer()), - AssociateMode: types.StringPointerValue(utils.StringRef(c.AssociateMode)), - Stores: make([]StoreKeyReference, len(c.Stores)), - BillingAddressIDs: make([]types.String, len(c.BillingAddressIDs)), - Addresses: make([]Address, len(c.Addresses)), - ShippingAddressIDs: make([]types.String, len(c.ShippingAddressIDs)), - Associates: make([]Associate, len(c.Associates)), - InheritedAssociates: make([]InheritedAssociate, len(c.InheritedAssociates)), - } - - for i, store := range c.Stores { - company.Stores[i] = StoreKeyReference{ - Key: types.StringValue(store.Key.ValueString()), - } - } - - copy(company.Addresses, c.Addresses) - - for i, id := range c.ShippingAddressIDs { - company.ShippingAddressIDs[i] = types.StringValue(id.ValueString()) - } - - for i, id := range c.BillingAddressIDs { - company.BillingAddressIDs[i] = types.StringValue(id.ValueString()) - } - - copy(company.Associates, c.Associates) - - copy(company.InheritedAssociates, c.InheritedAssociates) - - return company -} - -/* - Support types for the business unit resource. -*/ - -type Address struct { - ID types.String `tfsdk:"id"` - Key types.String `tfsdk:"key"` - ExternalID types.String `tfsdk:"external_id"` - Country types.String `tfsdk:"country"` - Title types.String `tfsdk:"title"` - Salutation types.String `tfsdk:"salutation"` - FirstName types.String `tfsdk:"first_name"` - LastName types.String `tfsdk:"last_name"` - StreetName types.String `tfsdk:"street_name"` - StreetNumber types.String `tfsdk:"street_number"` - AdditionalStreetInfo types.String `tfsdk:"additional_street_info"` - PostalCode types.String `tfsdk:"postal_code"` - City types.String `tfsdk:"city"` - Region types.String `tfsdk:"region"` - State types.String `tfsdk:"state"` - Company types.String `tfsdk:"company"` - Department types.String `tfsdk:"department"` - Building types.String `tfsdk:"building"` - Apartment types.String `tfsdk:"apartment"` - POBox types.String `tfsdk:"po_box"` - Phone types.String `tfsdk:"phone"` - Mobile types.String `tfsdk:"mobile"` - Email types.String `tfsdk:"email"` - Fax types.String `tfsdk:"fax"` - AdditionalAddressInfo types.String `tfsdk:"additional_address_info"` -} - -func (a Address) draft() platform.BaseAddress { - return platform.BaseAddress{ - Key: a.Key.ValueStringPointer(), - ExternalId: a.ExternalID.ValueStringPointer(), - Country: a.Country.ValueString(), - Title: a.Title.ValueStringPointer(), - Salutation: a.Salutation.ValueStringPointer(), - FirstName: a.FirstName.ValueStringPointer(), - LastName: a.LastName.ValueStringPointer(), - StreetName: a.StreetName.ValueStringPointer(), - StreetNumber: a.StreetNumber.ValueStringPointer(), - AdditionalStreetInfo: a.AdditionalStreetInfo.ValueStringPointer(), - PostalCode: a.PostalCode.ValueStringPointer(), - City: a.City.ValueStringPointer(), - Region: a.Region.ValueStringPointer(), - State: a.State.ValueStringPointer(), - Company: a.Company.ValueStringPointer(), - Department: a.Department.ValueStringPointer(), - Building: a.Building.ValueStringPointer(), - Apartment: a.Apartment.ValueStringPointer(), - POBox: a.POBox.ValueStringPointer(), - Phone: a.Phone.ValueStringPointer(), - Mobile: a.Mobile.ValueStringPointer(), - Email: a.Email.ValueStringPointer(), - Fax: a.Fax.ValueStringPointer(), - AdditionalAddressInfo: a.AdditionalAddressInfo.ValueStringPointer(), - } -} - -// NewAddressFromNative creates a new Address from a platform.Address. -func NewAddressFromNative(a *platform.Address) Address { - return Address{ - ID: types.StringPointerValue(a.ID), - Key: types.StringPointerValue(a.Key), - ExternalID: types.StringPointerValue(a.ExternalId), - Country: types.StringValue(a.Country), - Title: types.StringPointerValue(a.Title), - Salutation: types.StringPointerValue(a.Salutation), - FirstName: types.StringPointerValue(a.FirstName), - LastName: types.StringPointerValue(a.LastName), - StreetName: types.StringPointerValue(a.StreetName), - StreetNumber: types.StringPointerValue(a.StreetNumber), - AdditionalStreetInfo: types.StringPointerValue(a.AdditionalStreetInfo), - PostalCode: types.StringPointerValue(a.PostalCode), - City: types.StringPointerValue(a.City), - Region: types.StringPointerValue(a.Region), - State: types.StringPointerValue(a.State), - Company: types.StringPointerValue(a.Company), - Department: types.StringPointerValue(a.Department), - Building: types.StringPointerValue(a.Building), - Apartment: types.StringPointerValue(a.Apartment), - POBox: types.StringPointerValue(a.POBox), - Phone: types.StringPointerValue(a.Phone), - Mobile: types.StringPointerValue(a.Mobile), - Email: types.StringPointerValue(a.Email), - Fax: types.StringPointerValue(a.Fax), - AdditionalAddressInfo: types.StringPointerValue(a.AdditionalAddressInfo), - } -} - -// Associate is a type to model the fields that all types of Associates have in common. -type Associate struct { - AssociateRoleAssignments []AssociateRoleAssignment `tfsdk:"associate_role_assignments"` - Customer CustomerReference `tfsdk:"customer"` -} - -func (a Associate) draft() platform.AssociateDraft { - return platform.AssociateDraft{ - AssociateRoleAssignments: pie.Map(a.AssociateRoleAssignments, func(ara AssociateRoleAssignment) platform.AssociateRoleAssignmentDraft { - return ara.draft() - }), - Customer: a.Customer.draft(), - } -} - -// NewAssociateFromNative creates a new Associate from a platform.Associate. -func NewAssociateFromNative(a *platform.Associate) Associate { - assoc := Associate{ - AssociateRoleAssignments: make([]AssociateRoleAssignment, len(a.AssociateRoleAssignments)), - Customer: NewCustomerReferenceFromNative(&a.Customer), - } - - for i, ara := range a.AssociateRoleAssignments { - assoc.AssociateRoleAssignments[i] = NewAssociateRoleAssignment(&ara) - } - - return assoc -} - -// AssociateRoleAssignment is a type to model the fields that all types of -// Associate Role Assignments have in common. -type AssociateRoleAssignment struct { - AssociateRole AssociateRoleKeyReference `tfsdk:"associate_role"` - Inheritance types.String `tfsdk:"inheritance"` -} - -func (ara AssociateRoleAssignment) draft() platform.AssociateRoleAssignmentDraft { - if ara.Inheritance.IsNull() || ara.Inheritance.IsUnknown() { - return platform.AssociateRoleAssignmentDraft{ - AssociateRole: ara.AssociateRole.draft(), - } - } - - inheritance := platform.AssociateRoleInheritanceMode(ara.Inheritance.ValueString()) - - return platform.AssociateRoleAssignmentDraft{ - AssociateRole: ara.AssociateRole.draft(), - Inheritance: &inheritance, - } -} - -// NewAssociateRoleAssignment creates a new AssociateRoleAssignment from a -// platform.AssociateRoleAssignment. -func NewAssociateRoleAssignment(ara *platform.AssociateRoleAssignment) AssociateRoleAssignment { - ar := AssociateRoleAssignment{} - - ar.AssociateRole = NewAssociateRoleKeyReferenceFromNative(&ara.AssociateRole) - ar.Inheritance = types.StringValue(string(ara.Inheritance)) - - return ar -} - -// AssociateRoleKeyReference is a type to model the fields that all types of -// Associate Role Key References have in common. -type AssociateRoleKeyReference struct { - Key types.String `tfsdk:"key"` - TypeID types.String `tfsdk:"type_id"` -} - -func (kr AssociateRoleKeyReference) draft() platform.AssociateRoleResourceIdentifier { - if !kr.Key.IsNull() || !kr.Key.IsUnknown() { - return platform.AssociateRoleResourceIdentifier{ - Key: kr.Key.ValueStringPointer(), - } - } - - return platform.AssociateRoleResourceIdentifier{} -} - -// NewAssociateRoleKeyReferenceFromNative creates a new AssociateRoleKeyReference -// from a platform.AssociateRoleKeyReference. -func NewAssociateRoleKeyReferenceFromNative(kr *platform.AssociateRoleKeyReference) AssociateRoleKeyReference { - return AssociateRoleKeyReference{ - Key: types.StringValue(kr.Key), - TypeID: types.StringValue(AssociateRoleTypeID), - } -} - -// BusinessUnitKeyReference is a type to model the fields that all types of -// Business Unit Key References have in common. -type BusinessUnitKeyReference struct { - Key types.String `tfsdk:"key"` - TypeID types.String `tfsdk:"type_id"` -} - -// NewBusinessUnitKeyReferenceFromNative creates a new BusinessUnitKeyReference -// from a platform.BusinessUnitKeyReference. -func NewBusinessUnitKeyReferenceFromNative(kr *platform.BusinessUnitKeyReference) BusinessUnitKeyReference { - return BusinessUnitKeyReference{ - Key: types.StringValue(kr.Key), - TypeID: types.StringValue(BusinessUnitTypeID), - } -} - -// BusinessUnitResourceIdentifier is a resource identifier for a business unit. -type BusinessUnitResourceIdentifier struct { - ID types.String `tfsdk:"id"` - Key types.String `tfsdk:"key"` -} - -// NewBusinessUnitResourceIdentifierFromNative creates a new BusinessUnitResourceIdentifier -// from a platform.BusinessUnitResourceIdentifier. -func NewBusinessUnitResourceIdentifierFromNative(kr *platform.BusinessUnitResourceIdentifier) BusinessUnitResourceIdentifier { - return BusinessUnitResourceIdentifier{ - ID: types.StringValue(*kr.ID), - Key: types.StringValue(*kr.Key), - } -} - -// CustomerReference is a type to model the fields that all types of -// Customer References have in common. -type CustomerReference struct { - ID types.String `tfsdk:"id"` - TypeID types.String `tfsdk:"type_id"` -} - -func (cr CustomerReference) draft() platform.CustomerResourceIdentifier { - if !cr.ID.IsNull() || !cr.ID.IsUnknown() { - return platform.CustomerResourceIdentifier{ - ID: cr.ID.ValueStringPointer(), - } - } - - return platform.CustomerResourceIdentifier{} -} - -// NewCustomerReferenceFromNative creates a new CustomerReference from a -// platform.CustomerReference. -func NewCustomerReferenceFromNative(kr *platform.CustomerReference) CustomerReference { - return CustomerReference{ - ID: types.StringValue(kr.ID), - TypeID: types.StringValue(CustomerTypeID), - } -} - -// InheritedAssociate is a type to model the fields that all types of Inherited Associates have in common. -type InheritedAssociate struct { - AssociateRoleAssignments []InheritedAssociateRoleAssignment `tfsdk:"associate_role_assignment"` - Customer CustomerReference `tfsdk:"customer"` -} - -// NewInheritedAssociateFromNative creates a new InheritedAssociate from a -// platform.InheritedAssociate. -func NewInheritedAssociateFromNative(ia *platform.InheritedAssociate) InheritedAssociate { - localIA := InheritedAssociate{ - AssociateRoleAssignments: make([]InheritedAssociateRoleAssignment, len(ia.AssociateRoleAssignments)), - Customer: NewCustomerReferenceFromNative(&ia.Customer), - } - - for i, ara := range ia.AssociateRoleAssignments { - localIA.AssociateRoleAssignments[i] = NewInheritedAssociateRoleAssignmentFromNative(&ara) - } - - return localIA -} - -// InheritedAssociateRoleAssignment is a type to model the fields that all types of -// Inherited Associate Role Assignments have in common. -type InheritedAssociateRoleAssignment struct { - AssociateRole AssociateRoleKeyReference `tfsdk:"associate_role"` - Source BusinessUnitKeyReference `tfsdk:"source"` -} - -// NewInheritedAssociateRoleAssignmentFromNative creates a new -// InheritedAssociateRoleAssignment from a platform.InheritedAssociateRoleAssignment. -func NewInheritedAssociateRoleAssignmentFromNative(ara *platform.InheritedAssociateRoleAssignment) InheritedAssociateRoleAssignment { - localARA := InheritedAssociateRoleAssignment{} - - localARA.AssociateRole = NewAssociateRoleKeyReferenceFromNative(&ara.AssociateRole) - localARA.Source = NewBusinessUnitKeyReferenceFromNative(&ara.Source) - - return localARA -} - -// StoreKeyReference is a type to model the fields that all types of -// Store Key References have in common. -type StoreKeyReference struct { - Key types.String `tfsdk:"key"` - TypeID types.String `tfsdk:"type_id"` -} diff --git a/internal/resources/business_unit/model_test.go b/internal/resources/business_unit/model_test.go deleted file mode 100644 index e6d0ac1f..00000000 --- a/internal/resources/business_unit/model_test.go +++ /dev/null @@ -1,648 +0,0 @@ -package business_unit - -import ( - "testing" - - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/labd/commercetools-go-sdk/platform" - "github.com/labd/terraform-provider-commercetools/internal/utils" - "github.com/stretchr/testify/assert" -) - -func TestBusinessUnit_Company_UpdateActions(t *testing.T) { - cases := []struct { - name string - state Company - plan Company - expected platform.BusinessUnitUpdate - }{ - { - "business unit update name", - Company{ - Name: types.StringValue("Example business unit"), - }, - Company{ - Name: types.StringValue("Updated business unit"), - }, - platform.BusinessUnitUpdate{ - Actions: []platform.BusinessUnitUpdateAction{ - platform.BusinessUnitChangeNameAction{ - Name: "Updated business unit", - }, - }, - }, - }, - { - "business unit update contact email", - Company{ - ContactEmail: types.StringValue("info@example.com"), - }, - Company{ - ContactEmail: types.StringValue("new@example.com"), - }, - platform.BusinessUnitUpdate{ - Actions: []platform.BusinessUnitUpdateAction{ - platform.BusinessUnitSetContactEmailAction{ - ContactEmail: types.StringValue("new@example.com").ValueStringPointer(), - }, - }, - }, - }, - { - "business unit update status", - Company{ - Status: types.StringValue("Active"), - }, - Company{ - Status: types.StringValue("Inactive"), - }, - platform.BusinessUnitUpdate{ - Actions: []platform.BusinessUnitUpdateAction{ - platform.BusinessUnitChangeStatusAction{ - Status: "Inactive", - }, - }, - }, - }, - { - "business unit update default shipping address", - Company{ - DefaultShippingAddressID: types.StringValue("some-random-id"), - }, - Company{ - DefaultShippingAddressID: types.StringValue("another-random-id"), - }, - platform.BusinessUnitUpdate{ - Actions: []platform.BusinessUnitUpdateAction{ - platform.BusinessUnitSetDefaultShippingAddressAction{ - AddressId: types.StringValue("another-random-id").ValueStringPointer(), - }, - }, - }, - }, - { - "business unit update default billing address", - Company{ - DefaultBillingAddressID: types.StringValue("some-random-id"), - }, - Company{ - DefaultBillingAddressID: types.StringValue("another-random-id"), - }, - platform.BusinessUnitUpdate{ - Actions: []platform.BusinessUnitUpdateAction{ - platform.BusinessUnitSetDefaultBillingAddressAction{ - AddressId: types.StringValue("another-random-id").ValueStringPointer(), - }, - }, - }, - }, - { - "business unit update stores", - Company{ - Stores: []StoreKeyReference{ - { - Key: types.StringValue("store-1"), - }, - { - Key: types.StringValue("store-2"), - }, - }, - }, - Company{ - Stores: []StoreKeyReference{ - { - Key: types.StringValue("store-1"), - }, - { - Key: types.StringValue("store-3"), - }, - }, - }, - platform.BusinessUnitUpdate{ - Actions: []platform.BusinessUnitUpdateAction{ - platform.BusinessUnitAddStoreAction{ - Store: platform.StoreResourceIdentifier{ - Key: types.StringValue("store-3").ValueStringPointer(), - ID: nil, - }, - }, - platform.BusinessUnitRemoveStoreAction{ - Store: platform.StoreResourceIdentifier{ - Key: types.StringValue("store-2").ValueStringPointer(), - ID: nil, - }, - }, - }, - }, - }, - { - "business unit add address", - Company{ - Addresses: []Address{}, - }, - Company{ - Addresses: []Address{ - { - Key: types.StringValue("new-york-office"), - Country: types.StringValue("US"), - Salutation: types.StringValue("Mr."), - FirstName: types.StringValue("John"), - LastName: types.StringValue("Doe"), - StreetName: types.StringValue("Main St."), - StreetNumber: types.StringValue("123"), - AdditionalStreetInfo: types.StringValue("Apt. 1"), - PostalCode: types.StringValue("12345"), - City: types.StringValue("New York"), - Region: types.StringValue("New York"), - State: types.StringValue("New York"), - Company: types.StringValue("Example Inc."), - Department: types.StringValue("Sales"), - Building: types.StringValue("1"), - Apartment: types.StringValue("1"), - POBox: types.StringValue("123"), - Phone: types.StringValue("1234567890"), - Mobile: types.StringValue("1234567890"), - Fax: types.StringValue("1234567890"), - }, - }, - }, - platform.BusinessUnitUpdate{ - Actions: []platform.BusinessUnitUpdateAction{ - platform.BusinessUnitAddAddressAction{ - Address: platform.BaseAddress{ - Key: utils.StringRef("new-york-office"), - Country: "US", - Salutation: utils.StringRef("Mr."), - FirstName: utils.StringRef("John"), - LastName: utils.StringRef("Doe"), - StreetName: utils.StringRef("Main St."), - StreetNumber: utils.StringRef("123"), - AdditionalStreetInfo: utils.StringRef("Apt. 1"), - PostalCode: utils.StringRef("12345"), - City: utils.StringRef("New York"), - Region: utils.StringRef("New York"), - State: utils.StringRef("New York"), - Company: utils.StringRef("Example Inc."), - Department: utils.StringRef("Sales"), - Building: utils.StringRef("1"), - Apartment: utils.StringRef("1"), - POBox: utils.StringRef("123"), - Phone: utils.StringRef("1234567890"), - Mobile: utils.StringRef("1234567890"), - Fax: utils.StringRef("1234567890"), - }, - }, - }, - }, - }, - { - "business unit remove address", - Company{ - Addresses: []Address{ - { - Key: types.StringValue("new-york-office"), - Country: types.StringValue("US"), - Salutation: types.StringValue("Mr."), - FirstName: types.StringValue("John"), - LastName: types.StringValue("Doe"), - StreetName: types.StringValue("Main St."), - StreetNumber: types.StringValue("123"), - AdditionalStreetInfo: types.StringValue("Apt. 1"), - PostalCode: types.StringValue("12345"), - City: types.StringValue("New York"), - Region: types.StringValue("New York"), - State: types.StringValue("New York"), - Company: types.StringValue("Example Inc."), - Department: types.StringValue("Sales"), - Building: types.StringValue("1"), - Apartment: types.StringValue("1"), - POBox: types.StringValue("123"), - Phone: types.StringValue("1234567890"), - Mobile: types.StringValue("1234567890"), - Fax: types.StringValue("1234567890"), - }, - }, - }, - Company{ - Addresses: []Address{}, - }, - platform.BusinessUnitUpdate{ - Actions: []platform.BusinessUnitUpdateAction{ - platform.BusinessUnitRemoveAddressAction{ - AddressKey: utils.StringRef("new-york-office"), - }, - }, - }, - }, - { - "business unit set associates", - Company{}, - Company{ - Associates: []Associate{ - { - AssociateRoleAssignments: []AssociateRoleAssignment{ - { - AssociateRole: AssociateRoleKeyReference{ - Key: types.StringValue("role-1"), - }, - }, - }, - Customer: CustomerReference{ - ID: types.StringValue("customer-1"), - }, - }, - }, - }, - platform.BusinessUnitUpdate{ - Actions: []platform.BusinessUnitUpdateAction{ - platform.BusinessUnitSetAssociatesAction{ - Associates: []platform.AssociateDraft{ - { - AssociateRoleAssignments: []platform.AssociateRoleAssignmentDraft{ - { - AssociateRole: platform.AssociateRoleResourceIdentifier{ - Key: utils.StringRef("role-1"), - }, - }, - }, - Customer: platform.CustomerResourceIdentifier{ - ID: utils.StringRef("customer-1"), - }, - }, - }, - }, - }, - }, - }, - { - "business unit add billing address id", - Company{ - BillingAddressIDs: []types.String{}, - }, - Company{ - BillingAddressIDs: []types.String{ - types.StringValue("new-york-office"), - }, - }, - platform.BusinessUnitUpdate{ - Actions: []platform.BusinessUnitUpdateAction{ - platform.BusinessUnitAddBillingAddressIdAction{ - AddressId: utils.StringRef("new-york-office"), - }, - }, - }, - }, - { - "business unit remove billing address id", - Company{ - BillingAddressIDs: []types.String{ - types.StringValue("new-york-office"), - }, - }, - Company{ - BillingAddressIDs: []types.String{}, - }, - platform.BusinessUnitUpdate{ - Actions: []platform.BusinessUnitUpdateAction{ - platform.BusinessUnitRemoveBillingAddressIdAction{ - AddressId: utils.StringRef("new-york-office"), - }, - }, - }, - }, - { - "business unit add shipping address id", - Company{ - ShippingAddressIDs: []types.String{}, - }, - Company{ - ShippingAddressIDs: []types.String{ - types.StringValue("new-york-office"), - }, - }, - platform.BusinessUnitUpdate{ - Actions: []platform.BusinessUnitUpdateAction{ - platform.BusinessUnitAddShippingAddressIdAction{ - AddressId: utils.StringRef("new-york-office"), - }, - }, - }, - }, - { - "business unit remove shipping address id", - Company{ - ShippingAddressIDs: []types.String{ - types.StringValue("new-york-office"), - }, - }, - Company{ - ShippingAddressIDs: []types.String{}, - }, - platform.BusinessUnitUpdate{ - Actions: []platform.BusinessUnitUpdateAction{ - platform.BusinessUnitRemoveShippingAddressIdAction{ - AddressId: utils.StringRef("new-york-office"), - }, - }, - }, - }, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - result := c.state.updateActions(c.plan) - assert.EqualValues(t, c.expected, result) - }) - } -} - -func TestBusinessUnit_Division_UpdateActions(t *testing.T) { - cases := []struct { - name string - state Division - plan Division - expected platform.BusinessUnitUpdate - }{ - { - "business unit update name", - Division{ - Name: types.StringValue("Example business unit"), - }, - Division{ - Name: types.StringValue("Updated business unit"), - }, - platform.BusinessUnitUpdate{ - Actions: []platform.BusinessUnitUpdateAction{ - platform.BusinessUnitChangeNameAction{ - Name: "Updated business unit", - }, - }, - }, - }, - { - "business unit update contact email", - Division{ - ContactEmail: types.StringValue("info@example.com"), - }, - Division{ - ContactEmail: types.StringValue("new@example.com"), - }, - platform.BusinessUnitUpdate{ - Actions: []platform.BusinessUnitUpdateAction{ - platform.BusinessUnitSetContactEmailAction{ - ContactEmail: types.StringValue("new@example.com").ValueStringPointer(), - }, - }, - }, - }, - { - "business unit update status", - Division{ - Status: types.StringValue("Active"), - }, - Division{ - Status: types.StringValue("Inactive"), - }, - platform.BusinessUnitUpdate{ - Actions: []platform.BusinessUnitUpdateAction{ - platform.BusinessUnitChangeStatusAction{ - Status: "Inactive", - }, - }, - }, - }, - { - "business unit update default shipping address", - Division{ - DefaultShippingAddressID: types.StringValue("some-random-id"), - }, - Division{ - DefaultShippingAddressID: types.StringValue("another-random-id"), - }, - platform.BusinessUnitUpdate{ - Actions: []platform.BusinessUnitUpdateAction{ - platform.BusinessUnitSetDefaultShippingAddressAction{ - AddressId: types.StringValue("another-random-id").ValueStringPointer(), - }, - }, - }, - }, - { - "business unit update default billing address", - Division{ - DefaultBillingAddressID: types.StringValue("some-random-id"), - }, - Division{ - DefaultBillingAddressID: types.StringValue("another-random-id"), - }, - platform.BusinessUnitUpdate{ - Actions: []platform.BusinessUnitUpdateAction{ - platform.BusinessUnitSetDefaultBillingAddressAction{ - AddressId: types.StringValue("another-random-id").ValueStringPointer(), - }, - }, - }, - }, - { - "business unit update associate mode", - Division{ - AssociateMode: types.StringValue("Explicit"), - }, - Division{ - AssociateMode: types.StringValue("ExplicitAndFromParent"), - }, - platform.BusinessUnitUpdate{ - Actions: []platform.BusinessUnitUpdateAction{ - platform.BusinessUnitChangeAssociateModeAction{ - AssociateMode: "ExplicitAndFromParent", - }, - }, - }, - }, - { - "business unit update stores", - Division{ - Stores: []StoreKeyReference{ - { - Key: types.StringValue("store-1"), - }, - { - Key: types.StringValue("store-2"), - }, - }, - }, - Division{ - Stores: []StoreKeyReference{ - { - Key: types.StringValue("store-1"), - }, - { - Key: types.StringValue("store-3"), - }, - }, - }, - platform.BusinessUnitUpdate{ - Actions: []platform.BusinessUnitUpdateAction{ - platform.BusinessUnitAddStoreAction{ - Store: platform.StoreResourceIdentifier{ - Key: types.StringValue("store-3").ValueStringPointer(), - ID: nil, - }, - }, - platform.BusinessUnitRemoveStoreAction{ - Store: platform.StoreResourceIdentifier{ - Key: types.StringValue("store-2").ValueStringPointer(), - ID: nil, - }, - }, - }, - }, - }, - { - "business unit add address", - Division{ - Addresses: []Address{}, - }, - Division{ - Addresses: []Address{ - { - Key: types.StringValue("new-york-office"), - Country: types.StringValue("US"), - Salutation: types.StringValue("Mr."), - FirstName: types.StringValue("John"), - LastName: types.StringValue("Doe"), - StreetName: types.StringValue("Main St."), - StreetNumber: types.StringValue("123"), - AdditionalStreetInfo: types.StringValue("Apt. 1"), - PostalCode: types.StringValue("12345"), - City: types.StringValue("New York"), - Region: types.StringValue("New York"), - State: types.StringValue("New York"), - Company: types.StringValue("Example Inc."), - Department: types.StringValue("Sales"), - Building: types.StringValue("1"), - Apartment: types.StringValue("1"), - POBox: types.StringValue("123"), - Phone: types.StringValue("1234567890"), - Mobile: types.StringValue("1234567890"), - Fax: types.StringValue("1234567890"), - }, - }, - }, - platform.BusinessUnitUpdate{ - Actions: []platform.BusinessUnitUpdateAction{ - platform.BusinessUnitAddAddressAction{ - Address: platform.BaseAddress{ - Key: utils.StringRef("new-york-office"), - Country: "US", - Salutation: utils.StringRef("Mr."), - FirstName: utils.StringRef("John"), - LastName: utils.StringRef("Doe"), - StreetName: utils.StringRef("Main St."), - StreetNumber: utils.StringRef("123"), - AdditionalStreetInfo: utils.StringRef("Apt. 1"), - PostalCode: utils.StringRef("12345"), - City: utils.StringRef("New York"), - Region: utils.StringRef("New York"), - State: utils.StringRef("New York"), - Company: utils.StringRef("Example Inc."), - Department: utils.StringRef("Sales"), - Building: utils.StringRef("1"), - Apartment: utils.StringRef("1"), - POBox: utils.StringRef("123"), - Phone: utils.StringRef("1234567890"), - Mobile: utils.StringRef("1234567890"), - Fax: utils.StringRef("1234567890"), - }, - }, - }, - }, - }, - { - "business unit remove address", - Division{ - Addresses: []Address{ - { - Key: types.StringValue("new-york-office"), - Country: types.StringValue("US"), - Salutation: types.StringValue("Mr."), - FirstName: types.StringValue("John"), - LastName: types.StringValue("Doe"), - StreetName: types.StringValue("Main St."), - StreetNumber: types.StringValue("123"), - AdditionalStreetInfo: types.StringValue("Apt. 1"), - PostalCode: types.StringValue("12345"), - City: types.StringValue("New York"), - Region: types.StringValue("New York"), - State: types.StringValue("New York"), - Company: types.StringValue("Example Inc."), - Department: types.StringValue("Sales"), - Building: types.StringValue("1"), - Apartment: types.StringValue("1"), - POBox: types.StringValue("123"), - Phone: types.StringValue("1234567890"), - Mobile: types.StringValue("1234567890"), - Fax: types.StringValue("1234567890"), - }, - }, - }, - Division{ - Addresses: []Address{}, - }, - platform.BusinessUnitUpdate{ - Actions: []platform.BusinessUnitUpdateAction{ - platform.BusinessUnitRemoveAddressAction{ - AddressKey: utils.StringRef("new-york-office"), - }, - }, - }, - }, - { - "business unit set associates", - Division{}, - Division{ - Associates: []Associate{ - { - AssociateRoleAssignments: []AssociateRoleAssignment{ - { - AssociateRole: AssociateRoleKeyReference{ - Key: types.StringValue("role-1"), - }, - }, - }, - Customer: CustomerReference{ - ID: types.StringValue("customer-1"), - }, - }, - }, - }, - platform.BusinessUnitUpdate{ - Actions: []platform.BusinessUnitUpdateAction{ - platform.BusinessUnitSetAssociatesAction{ - Associates: []platform.AssociateDraft{ - { - AssociateRoleAssignments: []platform.AssociateRoleAssignmentDraft{ - { - AssociateRole: platform.AssociateRoleResourceIdentifier{ - Key: utils.StringRef("role-1"), - }, - }, - }, - Customer: platform.CustomerResourceIdentifier{ - ID: utils.StringRef("customer-1"), - }, - }, - }, - }, - }, - }, - }, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - result := c.state.updateActions(c.plan) - assert.EqualValues(t, c.expected, result) - }) - } -} diff --git a/internal/resources/business_unit/resource.go b/internal/resources/business_unit/resource.go deleted file mode 100644 index 6f8c6dc0..00000000 --- a/internal/resources/business_unit/resource.go +++ /dev/null @@ -1,934 +0,0 @@ -package business_unit - -import ( - "context" - "errors" - "time" - - "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" - "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/listplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" - "github.com/labd/commercetools-go-sdk/platform" - "github.com/labd/terraform-provider-commercetools/internal/utils" -) - -var ( - _ resource.Resource = &companyResource{} - _ resource.ResourceWithConfigure = &companyResource{} - _ resource.ResourceWithImportState = &companyResource{} -) - -type companyResource struct { - client *platform.ByProjectKeyRequestBuilder -} - -// Schema implements resource.Resource. -func (b *companyResource) Schema(_ context.Context, req resource.SchemaRequest, res *resource.SchemaResponse) { - res.Schema = schema.Schema{ - Description: "Business Unit type to represent the top level of a business. Contains specific fields and values that differentiate a Company from the generic BusinessUnit.\n\n" + - "See also the [Business Unit API Documentation](https://docs.commercetools.com/api/projects/business-units", - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Unique identifier of the Company.", - Computed: true, - }, - "version": schema.Int64Attribute{ - Description: "The current version of the Company.", - Computed: true, - }, - "key": schema.StringAttribute{ - Description: "User-defined unique identifier for the Company.", - Required: true, - }, - "name": schema.StringAttribute{ - Description: "The name of the Company.", - Required: true, - }, - "status": schema.StringAttribute{ - Description: "The status of the Company.", - Required: true, - Validators: []validator.String{ - stringvalidator.OneOf(BusinessUnitActive, BusinessUnitInactive), - }, - }, - "contact_email": schema.StringAttribute{ - Description: "The email address of the Company.", - Optional: true, - }, - "shipping_address_ids": schema.ListAttribute{ - Description: "List of the shipping addresses used by the Company.", - Optional: true, - ElementType: types.StringType, - PlanModifiers: []planmodifier.List{ - listplanmodifier.UseStateForUnknown(), - }, - }, - "billing_address_ids": schema.ListAttribute{ - Description: "List of the billing addresses used by the Company.", - Optional: true, - ElementType: types.StringType, - PlanModifiers: []planmodifier.List{ - listplanmodifier.UseStateForUnknown(), - }, - }, - "default_shipping_address_id": schema.StringAttribute{ - Description: "ID of the default shipping Address.", - Optional: true, - }, - "default_billing_address_id": schema.StringAttribute{ - Description: "ID of the default billing Address.", - Optional: true, - }, - }, - Blocks: map[string]schema.Block{ - "associate": schema.ListNestedBlock{ - Description: "Associates that are part of the Business Unit in specific roles", - NestedObject: schema.NestedBlockObject{ - Blocks: map[string]schema.Block{ - "associate_role_assignments": schema.ListNestedBlock{ - Description: "Roles assigned to the Associate within a Business Unit.", - NestedObject: schema.NestedBlockObject{ - Attributes: map[string]schema.Attribute{ - "inheritance": schema.StringAttribute{ - Description: "Determines whether the AssociateRoleAssignment can be inherited by child Business Units", - Required: true, - Validators: []validator.String{ - stringvalidator.OneOf( - AssociateRoleInheritanceEnabled, - AssociateRoleInheritanceDisabled, - ), - }, - }, - }, - Blocks: map[string]schema.Block{ - "associate_role": schema.ListNestedBlock{ - Description: "Reference to an AssociateRole by its key.", - NestedObject: schema.NestedBlockObject{ - Attributes: map[string]schema.Attribute{ - "key": schema.StringAttribute{ - Description: "User-defined unique identifier of the Associate Role", - Required: true, - }, - "type_id": schema.StringAttribute{ - Description: "The type of the Associate Role", - Required: true, - Validators: []validator.String{ - stringvalidator.OneOf(AssociateRoleTypeID), - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - "address": schema.ListNestedBlock{ - Description: "Addresses used by the Business Unit.", - Validators: []validator.List{ - listvalidator.SizeAtLeast(1), - listvalidator.IsRequired(), - }, - NestedObject: schema.NestedBlockObject{ - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Unique identifier of the Address", - Computed: true, - }, - "key": schema.StringAttribute{ - Description: "User-defined unique identifier of the Address", - Optional: true, - }, - "external_id": schema.StringAttribute{ - Description: "ID for the contact used in an external system", - Optional: true, - }, - "country": schema.StringAttribute{ - Description: "Name of the country", - Required: true, - }, - "title": schema.StringAttribute{ - Description: "Title of the contact, for example Dr., Prof.", - Optional: true, - }, - "salutation": schema.StringAttribute{ - Description: "Salutation of the contact, for example Ms., Mr.", - Optional: true, - }, - "first_name": schema.StringAttribute{ - Description: "First name of the contact", - Optional: true, - }, - "last_name": schema.StringAttribute{ - Description: "Last name of the contact", - Optional: true, - }, - "street_name": schema.StringAttribute{ - Description: "Name of the street", - Optional: true, - }, - "street_number": schema.StringAttribute{ - Description: "Street number", - Optional: true, - }, - "additional_street_info": schema.StringAttribute{ - Description: "Further information on the street address", - Optional: true, - }, - "postal_code": schema.StringAttribute{ - Description: "Postal code", - Optional: true, - }, - "city": schema.StringAttribute{ - Description: "Name of the city", - Optional: true, - }, - "region": schema.StringAttribute{ - Description: "Name of the region", - Optional: true, - }, - "state": schema.StringAttribute{ - Description: "Name of the state", - Optional: true, - }, - "company": schema.StringAttribute{ - Description: "Name of the company", - Optional: true, - }, - "department": schema.StringAttribute{ - Description: "Name of the department", - Optional: true, - }, - "building": schema.StringAttribute{ - Description: "Name or number of the building", - Optional: true, - }, - "apartment": schema.StringAttribute{ - Description: "Name or number of the apartment", - Optional: true, - }, - "po_box": schema.StringAttribute{ - Description: "Post office box number", - Optional: true, - }, - "phone": schema.StringAttribute{ - Description: "Phone number", - Optional: true, - }, - "mobile": schema.StringAttribute{ - Description: "Mobile phone number", - Optional: true, - }, - "email": schema.StringAttribute{ - Description: "Email address", - Optional: true, - }, - "fax": schema.StringAttribute{ - Description: "Fax number", - Optional: true, - }, - "additional_address_info": schema.StringAttribute{ - Description: "Further information on the Address", - Optional: true, - }, - }, - }, - }, - "store": schema.ListNestedBlock{ - Description: "Stores that are part of the Business Unit.", - NestedObject: schema.NestedBlockObject{ - Attributes: map[string]schema.Attribute{ - "key": schema.StringAttribute{ - Description: "User-defined unique identifier of the Store", - Required: true, - }, - "type_id": schema.StringAttribute{ - Description: "The type of the Store", - Required: true, - Validators: []validator.String{ - stringvalidator.OneOf(StoreTypeID), - }, - }, - }, - }, - }, - }, - } -} - -// Metadata implements resource.Resource. -func (b *companyResource) Metadata(_ context.Context, req resource.MetadataRequest, res *resource.MetadataResponse) { - res.TypeName = req.ProviderTypeName + "_business_unit_company" -} - -// ImportState implements resource.ResourceWithImportState. -func (b *companyResource) ImportState(ctx context.Context, req resource.ImportStateRequest, res *resource.ImportStateResponse) { - resource.ImportStatePassthroughID(ctx, path.Root("id"), req, res) -} - -// Configure implements resource.ResourceWithConfigure. -func (b *companyResource) Configure(ctx context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { - if req.ProviderData == nil { - return - } - - data, ok := req.ProviderData.(*utils.ProviderData) - if !ok { - return - } - - b.client = data.Client -} - -// Create implements resource.Resource. -func (b *companyResource) Create(ctx context.Context, req resource.CreateRequest, res *resource.CreateResponse) { - var plan Company - diags := req.Plan.Get(ctx, &plan) - res.Diagnostics.Append(diags...) - if res.Diagnostics.HasError() { - return - } - - draft := plan.draft() - - var bu platform.BusinessUnit - err := retry.RetryContext(ctx, 20*time.Second, func() *retry.RetryError { - var err error - bu, err = b.client.BusinessUnits().Post(draft).Execute(ctx) - - return utils.ProcessRemoteError(err) - }) - if err != nil { - res.Diagnostics.AddError( - "Error creating business unit", - "Could not create business unit, unexpected error: "+err.Error(), - ) - return - } - - current := NewCompanyFromNative(bu) - - diags = res.State.Set(ctx, ¤t) - res.Diagnostics.Append(diags...) - if res.Diagnostics.HasError() { - return - } -} - -// Delete implements resource.Resource. -func (b *companyResource) Delete(ctx context.Context, req resource.DeleteRequest, res *resource.DeleteResponse) { - var state Company - - diags := req.State.Get(ctx, &state) - res.Diagnostics.Append(diags...) - - if res.Diagnostics.HasError() { - return - } - - err := retry.RetryContext( - ctx, - 5*time.Second, - func() *retry.RetryError { - _, err := b.client.BusinessUnits(). - WithId(state.ID.ValueString()). - Delete(). - Version(int(state.Version.ValueInt64())). - Execute(ctx) - - return utils.ProcessRemoteError(err) - }, - ) - if err != nil { - res.Diagnostics.AddError( - "Error deleting business unit", - "Could not delete business unit, unexpected error: "+err.Error(), - ) - return - } -} - -// Read implements resource.Resource. -func (b *companyResource) Read(ctx context.Context, req resource.ReadRequest, res *resource.ReadResponse) { - var state Company - diags := req.State.Get(ctx, &state) - res.Diagnostics.Append(diags...) - if res.Diagnostics.HasError() { - return - } - - company, err := b.client.BusinessUnits().WithId(state.ID.ValueString()).Get().Execute(ctx) - if err != nil { - if errors.Is(err, platform.ErrNotFound) { - res.State.RemoveResource(ctx) - return - } - - res.Diagnostics.AddError( - "Error reading business unit", - "Could not retrieve the business unit, unexpected error: "+err.Error(), - ) - return - } - - current := NewCompanyFromNative(company) - - diags = res.State.Set(ctx, current) - res.Diagnostics.Append(diags...) - if res.Diagnostics.HasError() { - return - } -} - -// Update implements resource.Resource. -func (b *companyResource) Update(ctx context.Context, req resource.UpdateRequest, res *resource.UpdateResponse) { - var plan Company - diags := req.Plan.Get(ctx, &plan) - res.Diagnostics.Append(diags...) - if res.Diagnostics.HasError() { - return - } - - var state Company - diags = req.State.Get(ctx, &state) - res.Diagnostics.Append(diags...) - if res.Diagnostics.HasError() { - return - } - - input := state.updateActions(plan) - var company *platform.BusinessUnit - - err := retry.RetryContext(ctx, 5*time.Second, func() *retry.RetryError { - var err error - company, err = b.client.BusinessUnits(). - WithId(state.ID.ValueString()). - Post(input). - Execute(ctx) - - return utils.ProcessRemoteError(err) - }) - if err != nil { - res.Diagnostics.AddError( - "Error updating business unit", - "Could not update business unit, unexpected error: "+err.Error(), - ) - return - } - - current := NewCompanyFromNative(company) - diags = res.State.Set(ctx, ¤t) - res.Diagnostics.Append(diags...) - if res.Diagnostics.HasError() { - return - } -} - -func NewCompanyResource() resource.Resource { - return &companyResource{} -} - -var ( - _ resource.Resource = &divisionResource{} - _ resource.ResourceWithConfigure = &divisionResource{} - _ resource.ResourceWithImportState = &divisionResource{} -) - -type divisionResource struct { - client *platform.ByProjectKeyRequestBuilder -} - -// Schema implements resource.Resource. -func (b *divisionResource) Schema(_ context.Context, req resource.SchemaRequest, res *resource.SchemaResponse) { - res.Schema = schema.Schema{ - Description: "Business Unit type to represent the top level of a business. Contains specific fields and values that differentiate a Company from the generic BusinessUnit.\n\n" + - "See also the [Business Unit API Documentation](https://docs.commercetools.com/api/projects/business-units", - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Unique identifier of the Company.", - Computed: true, - }, - "version": schema.Int64Attribute{ - Description: "The current version of the Company.", - Computed: true, - }, - "key": schema.StringAttribute{ - Description: "User-defined unique identifier for the Company.", - Required: true, - }, - "name": schema.StringAttribute{ - Description: "The name of the Company.", - Required: true, - }, - "status": schema.StringAttribute{ - Description: "The status of the Company.", - Required: true, - Validators: []validator.String{ - stringvalidator.OneOf(BusinessUnitActive, BusinessUnitInactive), - }, - }, - "associate_mode": schema.StringAttribute{ - Description: "The association mode of the Company.", - Required: true, - }, - "contact_email": schema.StringAttribute{ - Description: "The email address of the Company.", - Optional: true, - }, - "shipping_address_ids": schema.ListAttribute{ - Description: "List of the shipping addresses used by the Company.", - Optional: true, - ElementType: types.StringType, - PlanModifiers: []planmodifier.List{ - listplanmodifier.UseStateForUnknown(), - }, - }, - "billing_address_ids": schema.ListAttribute{ - Description: "List of the billing addresses used by the Company.", - Optional: true, - ElementType: types.StringType, - PlanModifiers: []planmodifier.List{ - listplanmodifier.UseStateForUnknown(), - }, - }, - "default_shipping_address_id": schema.StringAttribute{ - Description: "ID of the default shipping Address.", - Optional: true, - }, - "default_billing_address_id": schema.StringAttribute{ - Description: "ID of the default billing Address.", - Optional: true, - }, - }, - Blocks: map[string]schema.Block{ - "associate": schema.ListNestedBlock{ - Description: "Associates that are part of the Business Unit in specific roles", - NestedObject: schema.NestedBlockObject{ - Blocks: map[string]schema.Block{ - "associate_role_assignments": schema.ListNestedBlock{ - Description: "Roles assigned to the Associate within a Business Unit.", - NestedObject: schema.NestedBlockObject{ - Attributes: map[string]schema.Attribute{ - "inheritance": schema.StringAttribute{ - Description: "Determines whether the AssociateRoleAssignment can be inherited by child Business Units", - Required: true, - Validators: []validator.String{ - stringvalidator.OneOf( - AssociateRoleInheritanceEnabled, - AssociateRoleInheritanceDisabled, - ), - }, - }, - }, - Blocks: map[string]schema.Block{ - "associate_role": schema.ListNestedBlock{ - Description: "Reference to an AssociateRole by its key.", - NestedObject: schema.NestedBlockObject{ - Attributes: map[string]schema.Attribute{ - "key": schema.StringAttribute{ - Description: "User-defined unique identifier of the Associate Role", - Required: true, - }, - "type_id": schema.StringAttribute{ - Description: "The type of the Associate Role", - Required: true, - Validators: []validator.String{ - stringvalidator.OneOf(AssociateRoleTypeID), - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - "address": schema.ListNestedBlock{ - Description: "Addresses used by the Business Unit.", - Validators: []validator.List{ - listvalidator.SizeAtLeast(1), - listvalidator.IsRequired(), - }, - NestedObject: schema.NestedBlockObject{ - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Unique identifier of the Address", - Computed: true, - }, - "key": schema.StringAttribute{ - Description: "User-defined unique identifier of the Address", - Optional: true, - }, - "external_id": schema.StringAttribute{ - Description: "ID for the contact used in an external system", - Optional: true, - }, - "country": schema.StringAttribute{ - Description: "Name of the country", - Required: true, - }, - "title": schema.StringAttribute{ - Description: "Title of the contact, for example Dr., Prof.", - Optional: true, - }, - "salutation": schema.StringAttribute{ - Description: "Salutation of the contact, for example Ms., Mr.", - Optional: true, - }, - "first_name": schema.StringAttribute{ - Description: "First name of the contact", - Optional: true, - }, - "last_name": schema.StringAttribute{ - Description: "Last name of the contact", - Optional: true, - }, - "street_name": schema.StringAttribute{ - Description: "Name of the street", - Optional: true, - }, - "street_number": schema.StringAttribute{ - Description: "Street number", - Optional: true, - }, - "additional_street_info": schema.StringAttribute{ - Description: "Further information on the street address", - Optional: true, - }, - "postal_code": schema.StringAttribute{ - Description: "Postal code", - Optional: true, - }, - "city": schema.StringAttribute{ - Description: "Name of the city", - Optional: true, - }, - "region": schema.StringAttribute{ - Description: "Name of the region", - Optional: true, - }, - "state": schema.StringAttribute{ - Description: "Name of the state", - Optional: true, - }, - "company": schema.StringAttribute{ - Description: "Name of the company", - Optional: true, - }, - "department": schema.StringAttribute{ - Description: "Name of the department", - Optional: true, - }, - "building": schema.StringAttribute{ - Description: "Name or number of the building", - Optional: true, - }, - "apartment": schema.StringAttribute{ - Description: "Name or number of the apartment", - Optional: true, - }, - "po_box": schema.StringAttribute{ - Description: "Post office box number", - Optional: true, - }, - "phone": schema.StringAttribute{ - Description: "Phone number", - Optional: true, - }, - "mobile": schema.StringAttribute{ - Description: "Mobile phone number", - Optional: true, - }, - "email": schema.StringAttribute{ - Description: "Email address", - Optional: true, - }, - "fax": schema.StringAttribute{ - Description: "Fax number", - Optional: true, - }, - "additional_address_info": schema.StringAttribute{ - Description: "Further information on the Address", - Optional: true, - }, - }, - }, - }, - "parent_unit": schema.ListNestedBlock{ - Description: "Reference to a parent Business Unit by its key.", - Validators: []validator.List{ - listvalidator.SizeAtMost(1), - listvalidator.SizeAtLeast(1), - listvalidator.IsRequired(), - }, - NestedObject: schema.NestedBlockObject{ - Attributes: map[string]schema.Attribute{ - "key": schema.StringAttribute{ - Description: "User-defined unique identifier of the Business Unit", - Required: true, - }, - "type_id": schema.StringAttribute{ - Description: "The type of the Business Unit", - Required: true, - Validators: []validator.String{ - stringvalidator.OneOf(CompanyType, DivisionType), - }, - }, - }, - }, - }, - "stores": schema.ListNestedBlock{ - Description: "Stores that are part of the Business Unit.", - NestedObject: schema.NestedBlockObject{ - Attributes: map[string]schema.Attribute{ - "key": schema.StringAttribute{ - Description: "User-defined unique identifier of the Store", - Required: true, - }, - "type_id": schema.StringAttribute{ - Description: "The type of the Store", - Required: true, - Validators: []validator.String{ - stringvalidator.OneOf(StoreTypeID), - }, - }, - }, - }, - }, - "inherited_associates": schema.ListNestedBlock{ - Description: "Associates that are inherited from parent Business Unit", - NestedObject: schema.NestedBlockObject{ - Blocks: map[string]schema.Block{ - "inherited_associate_role_assignments": schema.ListNestedBlock{ - Description: "Inherited roles of the Associate within a Business Unit", - NestedObject: schema.NestedBlockObject{ - Blocks: map[string]schema.Block{ - "associate_role": schema.ListNestedBlock{ - Description: "Inherited role the Associate holds within a Business Unit", - NestedObject: schema.NestedBlockObject{ - Attributes: map[string]schema.Attribute{ - "key": schema.StringAttribute{ - Description: "User-defined unique identifier of the Associate Role", - Required: true, - }, - "type_id": schema.StringAttribute{ - Description: "The type of the Associate Role", - Required: true, - Validators: []validator.String{ - stringvalidator.OneOf(AssociateRoleTypeID), - }, - }, - }, - }, - }, - "source": schema.ListNestedBlock{ - Description: "", - NestedObject: schema.NestedBlockObject{ - Attributes: map[string]schema.Attribute{ - "key": schema.StringAttribute{ - Description: "User-defined unique identifier of the Business Unit", - Required: true, - }, - "type_id": schema.StringAttribute{ - Description: "The type of the Business Unit", - Required: true, - Validators: []validator.String{ - stringvalidator.OneOf(BusinessUnitTypeID), - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - } -} - -// Metadata implements resource.Resource. -func (b *divisionResource) Metadata(_ context.Context, req resource.MetadataRequest, res *resource.MetadataResponse) { - res.TypeName = req.ProviderTypeName + "_business_unit_division" -} - -// ImportState implements resource.ResourceWithImportState. -func (b *divisionResource) ImportState(ctx context.Context, req resource.ImportStateRequest, res *resource.ImportStateResponse) { - resource.ImportStatePassthroughID(ctx, path.Root("id"), req, res) -} - -// Configure implements resource.ResourceWithConfigure. -func (b *divisionResource) Configure(ctx context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { - if req.ProviderData == nil { - return - } - - data, ok := req.ProviderData.(*utils.ProviderData) - if !ok { - return - } - - b.client = data.Client -} - -// Create implements resource.Resource. -func (b *divisionResource) Create(ctx context.Context, req resource.CreateRequest, res *resource.CreateResponse) { - var plan Division - diags := req.Plan.Get(ctx, &plan) - res.Diagnostics.Append(diags...) - if res.Diagnostics.HasError() { - return - } - - draft := plan.draft() - - var bu platform.BusinessUnit - err := retry.RetryContext(ctx, 20*time.Second, func() *retry.RetryError { - var err error - bu, err = b.client.BusinessUnits().Post(draft).Execute(ctx) - - return utils.ProcessRemoteError(err) - }) - if err != nil { - res.Diagnostics.AddError( - "Error creating business unit", - "Could not create business unit, unexpected error: "+err.Error(), - ) - return - } - - current := NewDivisionFromNative(bu) - - diags = res.State.Set(ctx, ¤t) - res.Diagnostics.Append(diags...) - if res.Diagnostics.HasError() { - return - } -} - -// Delete implements resource.Resource. -func (b *divisionResource) Delete(ctx context.Context, req resource.DeleteRequest, res *resource.DeleteResponse) { - var state Division - - diags := req.State.Get(ctx, &state) - res.Diagnostics.Append(diags...) - - if res.Diagnostics.HasError() { - return - } - - err := retry.RetryContext( - ctx, - 5*time.Second, - func() *retry.RetryError { - _, err := b.client.BusinessUnits(). - WithId(state.ID.ValueString()). - Delete(). - Version(int(state.Version.ValueInt64())). - Execute(ctx) - - return utils.ProcessRemoteError(err) - }, - ) - if err != nil { - res.Diagnostics.AddError( - "Error deleting business unit", - "Could not delete business unit, unexpected error: "+err.Error(), - ) - return - } -} - -// Read implements resource.Resource. -func (b *divisionResource) Read(ctx context.Context, req resource.ReadRequest, res *resource.ReadResponse) { - var state Division - diags := req.State.Get(ctx, &state) - res.Diagnostics.Append(diags...) - if res.Diagnostics.HasError() { - return - } - - division, err := b.client.BusinessUnits().WithId(state.ID.ValueString()).Get().Execute(ctx) - if err != nil { - if errors.Is(err, platform.ErrNotFound) { - res.State.RemoveResource(ctx) - return - } - - res.Diagnostics.AddError( - "Error reading business unit", - "Could not retrieve the business unit, unexpected error: "+err.Error(), - ) - return - } - - current := NewDivisionFromNative(division) - - diags = res.State.Set(ctx, current) - res.Diagnostics.Append(diags...) - if res.Diagnostics.HasError() { - return - } -} - -// Update implements resource.Resource. -func (b *divisionResource) Update(ctx context.Context, req resource.UpdateRequest, res *resource.UpdateResponse) { - var plan Division - diags := req.Plan.Get(ctx, &plan) - res.Diagnostics.Append(diags...) - if res.Diagnostics.HasError() { - return - } - - var state Division - diags = req.State.Get(ctx, &state) - res.Diagnostics.Append(diags...) - if res.Diagnostics.HasError() { - return - } - - input := state.updateActions(plan) - var division *platform.BusinessUnit - - err := retry.RetryContext(ctx, 5*time.Second, func() *retry.RetryError { - var err error - division, err = b.client.BusinessUnits(). - WithId(state.ID.ValueString()). - Post(input). - Execute(ctx) - - return utils.ProcessRemoteError(err) - }) - if err != nil { - res.Diagnostics.AddError( - "Error updating business unit", - "Could not update business unit, unexpected error: "+err.Error(), - ) - return - } - - current := NewDivisionFromNative(division) - diags = res.State.Set(ctx, ¤t) - res.Diagnostics.Append(diags...) - if res.Diagnostics.HasError() { - return - } -} - -// NewDivisionResource creates a new resource for the Division type. -func NewDivisionResource() resource.Resource { - return &divisionResource{} -} diff --git a/internal/resources/business_unit_company/model.go b/internal/resources/business_unit_company/model.go new file mode 100644 index 00000000..6927d481 --- /dev/null +++ b/internal/resources/business_unit_company/model.go @@ -0,0 +1,316 @@ +package business_unit_company + +import ( + "fmt" + "github.com/elliotchance/pie/v2" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/labd/commercetools-go-sdk/platform" + "github.com/labd/terraform-provider-commercetools/internal/sharedtypes" + "github.com/labd/terraform-provider-commercetools/internal/utils" + "reflect" + "slices" + "sort" +) + +// Company is a type to model the fields that all types of Companies have in common. +type Company struct { + ID types.String `tfsdk:"id"` + Version types.Int64 `tfsdk:"version"` + Key types.String `tfsdk:"key"` + Status types.String `tfsdk:"status"` + Name types.String `tfsdk:"name"` + ContactEmail types.String `tfsdk:"contact_email"` + ShippingAddressKeys []types.String `tfsdk:"shipping_address_keys"` + DefaultShippingAddressKey types.String `tfsdk:"default_shipping_address_key"` + BillingAddressKeys []types.String `tfsdk:"billing_address_keys"` + DefaultBillingAddressKey types.String `tfsdk:"default_billing_address_key"` + Stores []sharedtypes.StoreKeyReference `tfsdk:"store"` + Addresses []sharedtypes.Address `tfsdk:"address"` +} + +func (c *Company) draft() (platform.CompanyDraft, error) { + status := platform.BusinessUnitStatus(c.Status.ValueString()) + storeMode := platform.BusinessUnitStoreModeExplicit + associateMode := platform.BusinessUnitAssociateModeExplicit + approvalRuleMode := platform.BusinessUnitApprovalRuleModeExplicit + + var addresses []platform.BaseAddress + for _, a := range c.Addresses { + addresses = append(addresses, a.Draft()) + } + + var stores []platform.StoreResourceIdentifier + for _, s := range c.Stores { + stores = append(stores, platform.StoreResourceIdentifier{ + Key: s.Key.ValueStringPointer(), + }) + } + + var shippingAddressIndexes []int + for _, key := range c.ShippingAddressKeys { + i := slices.IndexFunc(c.Addresses, func(a sharedtypes.Address) bool { + return a.Key.ValueString() == key.ValueString() + }) + + if i == -1 { + return platform.CompanyDraft{}, fmt.Errorf("shipping address key %s is not in addresses", key.ValueString()) + } + + shippingAddressIndexes = append(shippingAddressIndexes, i) + } + + var billingAddressIndexes []int + for _, key := range c.BillingAddressKeys { + i := slices.IndexFunc(c.Addresses, func(a sharedtypes.Address) bool { + return a.Key.ValueString() == key.ValueString() + }) + + if i == -1 { + return platform.CompanyDraft{}, fmt.Errorf("billing address key %s is not in addresses", key.ValueString()) + } + + billingAddressIndexes = append(billingAddressIndexes, i) + } + + var defaultBillingAddressIndex *int + if !c.DefaultBillingAddressKey.IsNull() { + i := slices.IndexFunc(c.Addresses, func(a sharedtypes.Address) bool { + return a.Key.ValueString() == c.DefaultBillingAddressKey.ValueString() + }) + + if i == -1 { + return platform.CompanyDraft{}, fmt.Errorf("default billing address key %s is not in addresses", c.DefaultBillingAddressKey.ValueString()) + } + + defaultBillingAddressIndex = &i + } + + var defaultShippingAddressIndex *int + if !c.DefaultShippingAddressKey.IsNull() { + i := slices.IndexFunc(c.Addresses, func(a sharedtypes.Address) bool { + return a.Key.ValueString() == c.DefaultShippingAddressKey.ValueString() + }) + + if i == -1 { + return platform.CompanyDraft{}, fmt.Errorf("default shipping address key %s is not in addresses", c.DefaultShippingAddressKey.ValueString()) + } + + defaultShippingAddressIndex = &i + } + + return platform.CompanyDraft{ + Key: c.Key.ValueString(), + Status: &status, + StoreMode: &storeMode, + AssociateMode: &associateMode, + ApprovalRuleMode: &approvalRuleMode, + Stores: stores, + Name: c.Name.ValueString(), + ContactEmail: c.ContactEmail.ValueStringPointer(), + Addresses: addresses, + ShippingAddresses: shippingAddressIndexes, + BillingAddresses: billingAddressIndexes, + DefaultShippingAddress: defaultShippingAddressIndex, + DefaultBillingAddress: defaultBillingAddressIndex, + }, nil +} + +func (c *Company) updateActions(plan Company) (platform.BusinessUnitUpdate, error) { + result := platform.BusinessUnitUpdate{ + Version: int(c.Version.ValueInt64()), + Actions: []platform.BusinessUnitUpdateAction{}, + } + + if !c.Name.Equal(plan.Name) { + result.Actions = append(result.Actions, platform.BusinessUnitChangeNameAction{ + Name: plan.Name.ValueString(), + }) + } + + if !c.ContactEmail.Equal(plan.ContactEmail) { + result.Actions = append(result.Actions, platform.BusinessUnitSetContactEmailAction{ + ContactEmail: plan.ContactEmail.ValueStringPointer(), + }) + } + + if !c.Status.Equal(plan.Status) { + result.Actions = append(result.Actions, platform.BusinessUnitChangeStatusAction{ + Status: plan.Status.ValueString(), + }) + } + + if !reflect.DeepEqual(c.Stores, plan.Stores) { + result.Actions = append(result.Actions, platform.BusinessUnitSetStoresAction{ + Stores: pie.Map(plan.Stores, func(s sharedtypes.StoreKeyReference) platform.StoreResourceIdentifier { + return platform.StoreResourceIdentifier{ + Key: s.Key.ValueStringPointer(), + } + }), + }) + } + + if !reflect.DeepEqual(c.Addresses, plan.Addresses) { + addressAddActions := sharedtypes.AddressesAddActions(c.Addresses, plan.Addresses) + for _, action := range addressAddActions { + result.Actions = append(result.Actions, action) + } + + addressChangeActions := sharedtypes.AddressesChangeActions(c.Addresses, plan.Addresses) + for _, action := range addressChangeActions { + result.Actions = append(result.Actions, action) + } + } + + if !c.DefaultShippingAddressKey.Equal(plan.DefaultShippingAddressKey) { + if !pie.Contains(plan.ShippingAddressKeys, plan.DefaultShippingAddressKey) { + return result, fmt.Errorf("default shipping address key %s is not in shipping address keys", plan.DefaultShippingAddressKey.ValueString()) + } + + result.Actions = append(result.Actions, platform.BusinessUnitSetDefaultShippingAddressAction{ + AddressKey: plan.DefaultShippingAddressKey.ValueStringPointer(), + }) + } + + if !c.DefaultBillingAddressKey.Equal(plan.DefaultBillingAddressKey) { + if !pie.Contains(plan.BillingAddressKeys, plan.DefaultBillingAddressKey) { + return result, fmt.Errorf("default shipping address key %s is not in shipping address keys", plan.DefaultBillingAddressKey.ValueString()) + } + + result.Actions = append(result.Actions, platform.BusinessUnitSetDefaultBillingAddressAction{ + AddressKey: plan.DefaultBillingAddressKey.ValueStringPointer(), + }) + } + + if !reflect.DeepEqual(c.ShippingAddressKeys, plan.ShippingAddressKeys) { + // find shipping addresses to be added + for _, i := range plan.ShippingAddressKeys { + if !pie.Contains(c.ShippingAddressKeys, i) { + result.Actions = append(result.Actions, platform.BusinessUnitAddShippingAddressIdAction{ + AddressKey: i.ValueStringPointer(), + }) + } + } + + // find shipping addresses to be removed + for _, i := range c.ShippingAddressKeys { + if !pie.Contains(plan.ShippingAddressKeys, i) { + result.Actions = append(result.Actions, platform.BusinessUnitRemoveShippingAddressIdAction{ + AddressKey: i.ValueStringPointer(), + }) + } + } + } + + if !reflect.DeepEqual(c.BillingAddressKeys, plan.BillingAddressKeys) { + // find billing addresses to be added + for _, i := range plan.BillingAddressKeys { + if !pie.Contains(c.BillingAddressKeys, i) { + result.Actions = append(result.Actions, platform.BusinessUnitAddBillingAddressIdAction{ + AddressKey: i.ValueStringPointer(), + }) + } + } + + // find billing addresses to be removed + for _, i := range c.BillingAddressKeys { + if !pie.Contains(plan.BillingAddressKeys, i) { + result.Actions = append(result.Actions, platform.BusinessUnitRemoveBillingAddressIdAction{ + AddressKey: i.ValueStringPointer(), + }) + } + } + } + + // We need to delete addresses only after we have removed keys + if !reflect.DeepEqual(c.Addresses, plan.Addresses) { + addressDeleteActions := sharedtypes.AddressesDeleteActions(c.Addresses, plan.Addresses) + for _, action := range addressDeleteActions { + result.Actions = append(result.Actions, action) + } + } + + return result, nil +} + +// NewCompanyFromNative creates a new Company from a platform.Company. +func NewCompanyFromNative(bu *platform.BusinessUnit) (Company, error) { + data, ok := (*bu).(map[string]interface{}) + if !ok { + return Company{}, fmt.Errorf("failed to convert business unit to map") + } + var c platform.Company + err := utils.DecodeStruct(data, &c) + if err != nil { + return Company{}, err + } + + var defaultShippingAddressKey *string + if c.DefaultShippingAddressId != nil { + i := slices.IndexFunc(c.Addresses, func(a platform.Address) bool { + return *a.ID == *c.DefaultShippingAddressId + }) + defaultShippingAddressKey = c.Addresses[i].Key + } + var defaultBillingAddressKey *string + if c.DefaultBillingAddressId != nil { + i := slices.IndexFunc(c.Addresses, func(a platform.Address) bool { + return *a.ID == *c.DefaultBillingAddressId + }) + defaultBillingAddressKey = c.Addresses[i].Key + } + + var shippingAddressKeys []types.String + for _, id := range c.ShippingAddressIds { + i := slices.IndexFunc(c.Addresses, func(a platform.Address) bool { + return *a.ID == id + }) + shippingAddressKeys = append(shippingAddressKeys, types.StringPointerValue(c.Addresses[i].Key)) + } + + var billingAddressKeys []types.String + for _, id := range c.BillingAddressIds { + i := slices.IndexFunc(c.Addresses, func(a platform.Address) bool { + return *a.ID == id + }) + billingAddressKeys = append(billingAddressKeys, types.StringPointerValue(c.Addresses[i].Key)) + } + + var stores []sharedtypes.StoreKeyReference + for _, s := range c.Stores { + stores = append(stores, sharedtypes.NewStoreKeyReferenceFromNative(&s)) + } + + var addresses []sharedtypes.Address + for _, a := range c.Addresses { + addresses = append(addresses, sharedtypes.NewAddressFromNative(&a)) + } + + company := Company{ + ID: types.StringValue(c.ID), + Version: types.Int64Value(int64(c.Version)), + Key: types.StringValue(c.Key), + Name: types.StringValue(c.Name), + Status: types.StringValue(string(c.Status)), + ContactEmail: types.StringPointerValue(c.ContactEmail), + DefaultShippingAddressKey: types.StringPointerValue(defaultShippingAddressKey), + DefaultBillingAddressKey: types.StringPointerValue(defaultBillingAddressKey), + Stores: stores, + Addresses: addresses, + ShippingAddressKeys: shippingAddressKeys, + BillingAddressKeys: billingAddressKeys, + } + + sort.Slice(company.Addresses, func(i, j int) bool { + return company.Addresses[i].Key.ValueString() < company.Addresses[j].Key.ValueString() + }) + + sort.Slice(company.ShippingAddressKeys, func(i, j int) bool { + return company.ShippingAddressKeys[i].ValueString() < company.ShippingAddressKeys[j].ValueString() + }) + + sort.Slice(company.BillingAddressKeys, func(i, j int) bool { + return company.BillingAddressKeys[i].ValueString() < company.BillingAddressKeys[j].ValueString() + }) + + return company, nil +} diff --git a/internal/resources/business_unit_company/model_test.go b/internal/resources/business_unit_company/model_test.go new file mode 100644 index 00000000..020cfa68 --- /dev/null +++ b/internal/resources/business_unit_company/model_test.go @@ -0,0 +1,480 @@ +package business_unit_company + +import ( + "github.com/labd/terraform-provider-commercetools/internal/sharedtypes" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/labd/commercetools-go-sdk/platform" + "github.com/labd/terraform-provider-commercetools/internal/utils" + "github.com/stretchr/testify/assert" +) + +func TestBusinessUnit_Company_Draft(t *testing.T) { + cases := []struct { + name string + company Company + expected platform.CompanyDraft + }{ + { + name: "Basic company draft", + company: Company{ + Key: types.StringValue("company-key"), + Status: types.StringValue("Active"), + Name: types.StringValue("Company Name"), + ContactEmail: types.StringValue("contact@example.com"), + Addresses: []sharedtypes.Address{ + { + Key: types.StringValue("address-1"), + Country: types.StringValue("US"), + City: types.StringValue("New York"), + }, + { + Key: types.StringValue("address-2"), + Country: types.StringValue("US"), + City: types.StringValue("Detroit"), + }, + }, + Stores: []sharedtypes.StoreKeyReference{ + {Key: types.StringValue("store-1")}, + }, + ShippingAddressKeys: []types.String{types.StringValue("address-1"), types.StringValue("address-2")}, + BillingAddressKeys: []types.String{types.StringValue("address-1"), types.StringValue("address-2")}, + DefaultBillingAddressKey: types.StringValue("address-2"), + DefaultShippingAddressKey: types.StringValue("address-2"), + }, + expected: platform.CompanyDraft{ + Key: "company-key", + Status: utils.Ref(platform.BusinessUnitStatusActive), + Name: "Company Name", + StoreMode: utils.Ref(platform.BusinessUnitStoreModeExplicit), + AssociateMode: utils.Ref(platform.BusinessUnitAssociateModeExplicit), + ApprovalRuleMode: utils.Ref(platform.BusinessUnitApprovalRuleModeExplicit), + ContactEmail: utils.Ref("contact@example.com"), + Addresses: []platform.BaseAddress{ + { + Key: utils.Ref("address-1"), + Country: "US", + City: utils.Ref("New York"), + }, + { + Key: utils.Ref("address-2"), + Country: "US", + City: utils.Ref("Detroit"), + }, + }, + Stores: []platform.StoreResourceIdentifier{ + { + Key: utils.Ref("store-1"), + }, + }, + DefaultShippingAddress: utils.Ref(1), + DefaultBillingAddress: utils.Ref(1), + ShippingAddresses: []int{0, 1}, + BillingAddresses: []int{0, 1}, + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + result, err := c.company.draft() + assert.NoError(t, err) + assert.Equal(t, c.expected, result) + }) + } +} + +func TestBusinessUnit_Company_UpdateActions(t *testing.T) { + cases := []struct { + name string + state Company + plan Company + expected platform.BusinessUnitUpdate + }{ + { + "business unit update name", + Company{ + Name: types.StringValue("Example business unit"), + }, + Company{ + Name: types.StringValue("Updated business unit"), + }, + platform.BusinessUnitUpdate{ + Actions: []platform.BusinessUnitUpdateAction{ + platform.BusinessUnitChangeNameAction{ + Name: "Updated business unit", + }, + }, + }, + }, + { + "business unit update contact email", + Company{ + ContactEmail: types.StringValue("info@example.com"), + }, + Company{ + ContactEmail: types.StringValue("new@example.com"), + }, + platform.BusinessUnitUpdate{ + Actions: []platform.BusinessUnitUpdateAction{ + platform.BusinessUnitSetContactEmailAction{ + ContactEmail: types.StringValue("new@example.com").ValueStringPointer(), + }, + }, + }, + }, + { + "business unit update status", + Company{ + Status: types.StringValue("Active"), + }, + Company{ + Status: types.StringValue("Inactive"), + }, + platform.BusinessUnitUpdate{ + Actions: []platform.BusinessUnitUpdateAction{ + platform.BusinessUnitChangeStatusAction{ + Status: "Inactive", + }, + }, + }, + }, + { + "business unit update default shipping address", + Company{ + ShippingAddressKeys: []types.String{types.StringValue("some-random-id"), types.StringValue("another-random-id")}, + DefaultShippingAddressKey: types.StringValue("some-random-id"), + }, + Company{ + ShippingAddressKeys: []types.String{types.StringValue("some-random-id"), types.StringValue("another-random-id")}, + DefaultShippingAddressKey: types.StringValue("another-random-id"), + }, + platform.BusinessUnitUpdate{ + Actions: []platform.BusinessUnitUpdateAction{ + platform.BusinessUnitSetDefaultShippingAddressAction{ + AddressKey: types.StringValue("another-random-id").ValueStringPointer(), + }, + }, + }, + }, + { + "business unit update default billing address", + Company{ + BillingAddressKeys: []types.String{types.StringValue("some-random-id"), types.StringValue("another-random-id")}, + DefaultBillingAddressKey: types.StringValue("some-random-id"), + }, + Company{ + BillingAddressKeys: []types.String{types.StringValue("some-random-id"), types.StringValue("another-random-id")}, + DefaultBillingAddressKey: types.StringValue("another-random-id"), + }, + platform.BusinessUnitUpdate{ + Actions: []platform.BusinessUnitUpdateAction{ + platform.BusinessUnitSetDefaultBillingAddressAction{ + AddressKey: types.StringValue("another-random-id").ValueStringPointer(), + }, + }, + }, + }, + { + "business unit update stores", + Company{ + Stores: []sharedtypes.StoreKeyReference{ + { + Key: types.StringValue("store-1"), + }, + { + Key: types.StringValue("store-2"), + }, + }, + }, + Company{ + Stores: []sharedtypes.StoreKeyReference{ + { + Key: types.StringValue("store-1"), + }, + { + Key: types.StringValue("store-3"), + }, + }, + }, + platform.BusinessUnitUpdate{ + Actions: []platform.BusinessUnitUpdateAction{ + platform.BusinessUnitSetStoresAction{ + Stores: []platform.StoreResourceIdentifier{ + { + Key: types.StringValue("store-1").ValueStringPointer(), + ID: nil, + }, + { + Key: types.StringValue("store-3").ValueStringPointer(), + ID: nil, + }, + }, + }, + }, + }, + }, + { + "business unit add address", + Company{ + Addresses: []sharedtypes.Address{}, + }, + Company{ + Addresses: []sharedtypes.Address{ + { + Key: types.StringValue("new-york-office"), + Country: types.StringValue("US"), + Salutation: types.StringValue("Mr."), + FirstName: types.StringValue("John"), + LastName: types.StringValue("Doe"), + StreetName: types.StringValue("Main St."), + StreetNumber: types.StringValue("123"), + AdditionalStreetInfo: types.StringValue("Apt. 1"), + PostalCode: types.StringValue("12345"), + City: types.StringValue("New York"), + Region: types.StringValue("New York"), + State: types.StringValue("New York"), + Company: types.StringValue("Example Inc."), + Department: types.StringValue("Sales"), + Building: types.StringValue("1"), + Apartment: types.StringValue("1"), + POBox: types.StringValue("123"), + Phone: types.StringValue("1234567890"), + Mobile: types.StringValue("1234567890"), + Fax: types.StringValue("1234567890"), + }, + }, + }, + platform.BusinessUnitUpdate{ + Actions: []platform.BusinessUnitUpdateAction{ + platform.BusinessUnitAddAddressAction{ + Address: platform.BaseAddress{ + Key: utils.StringRef("new-york-office"), + Country: "US", + Salutation: utils.StringRef("Mr."), + FirstName: utils.StringRef("John"), + LastName: utils.StringRef("Doe"), + StreetName: utils.StringRef("Main St."), + StreetNumber: utils.StringRef("123"), + AdditionalStreetInfo: utils.StringRef("Apt. 1"), + PostalCode: utils.StringRef("12345"), + City: utils.StringRef("New York"), + Region: utils.StringRef("New York"), + State: utils.StringRef("New York"), + Company: utils.StringRef("Example Inc."), + Department: utils.StringRef("Sales"), + Building: utils.StringRef("1"), + Apartment: utils.StringRef("1"), + POBox: utils.StringRef("123"), + Phone: utils.StringRef("1234567890"), + Mobile: utils.StringRef("1234567890"), + Fax: utils.StringRef("1234567890"), + }, + }, + }, + }, + }, + { + "business unit remove address", + Company{ + Addresses: []sharedtypes.Address{ + { + Key: types.StringValue("new-york-office"), + Country: types.StringValue("US"), + Salutation: types.StringValue("Mr."), + FirstName: types.StringValue("John"), + LastName: types.StringValue("Doe"), + StreetName: types.StringValue("Main St."), + StreetNumber: types.StringValue("123"), + AdditionalStreetInfo: types.StringValue("Apt. 1"), + PostalCode: types.StringValue("12345"), + City: types.StringValue("New York"), + Region: types.StringValue("New York"), + State: types.StringValue("New York"), + Company: types.StringValue("Example Inc."), + Department: types.StringValue("Sales"), + Building: types.StringValue("1"), + Apartment: types.StringValue("1"), + POBox: types.StringValue("123"), + Phone: types.StringValue("1234567890"), + Mobile: types.StringValue("1234567890"), + Fax: types.StringValue("1234567890"), + }, + }, + }, + Company{ + Addresses: []sharedtypes.Address{}, + }, + platform.BusinessUnitUpdate{ + Actions: []platform.BusinessUnitUpdateAction{ + platform.BusinessUnitRemoveAddressAction{ + AddressKey: utils.StringRef("new-york-office"), + }, + }, + }, + }, + { + "business unit add billing address id", + Company{ + BillingAddressKeys: []types.String{}, + }, + Company{ + BillingAddressKeys: []types.String{ + types.StringValue("new-york-office"), + }, + }, + platform.BusinessUnitUpdate{ + Actions: []platform.BusinessUnitUpdateAction{ + platform.BusinessUnitAddBillingAddressIdAction{ + AddressKey: utils.StringRef("new-york-office"), + }, + }, + }, + }, + { + "business unit remove billing address id", + Company{ + BillingAddressKeys: []types.String{ + types.StringValue("new-york-office"), + }, + }, + Company{ + BillingAddressKeys: []types.String{}, + }, + platform.BusinessUnitUpdate{ + Actions: []platform.BusinessUnitUpdateAction{ + platform.BusinessUnitRemoveBillingAddressIdAction{ + AddressKey: utils.StringRef("new-york-office"), + }, + }, + }, + }, + { + "business unit add shipping address id", + Company{ + ShippingAddressKeys: []types.String{}, + }, + Company{ + ShippingAddressKeys: []types.String{ + types.StringValue("new-york-office"), + }, + }, + platform.BusinessUnitUpdate{ + Actions: []platform.BusinessUnitUpdateAction{ + platform.BusinessUnitAddShippingAddressIdAction{ + AddressKey: utils.StringRef("new-york-office"), + }, + }, + }, + }, + { + "business unit remove shipping address id", + Company{ + ShippingAddressKeys: []types.String{ + types.StringValue("new-york-office"), + }, + }, + Company{ + ShippingAddressKeys: []types.String{}, + }, + platform.BusinessUnitUpdate{ + Actions: []platform.BusinessUnitUpdateAction{ + platform.BusinessUnitRemoveShippingAddressIdAction{ + AddressKey: utils.StringRef("new-york-office"), + }, + }, + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + result, err := c.state.updateActions(c.plan) + assert.NoError(t, err) + assert.EqualValues(t, c.expected, result) + }) + } +} + +func TestBusinessUnit_Company_NewCompanyFromNative(t *testing.T) { + cases := []struct { + name string + company map[string]interface{} + expected Company + }{ + { + name: "Basic company draft", + company: map[string]interface{}{ + "id": "company-id", + "key": "company-key", + "version": 1, + "status": platform.BusinessUnitStatusActive, + "name": "Company Name", + "contactEmail": utils.Ref("contact@example.com"), + "addresses": []map[string]interface{}{ + { + "id": utils.Ref("address-id-1"), + "key": utils.Ref("address-1"), + "country": "US", + "city": utils.Ref("New York"), + }, + { + "id": utils.Ref("address-id-2"), + "key": utils.Ref("address-2"), + "country": "US", + "city": utils.Ref("Detroit"), + }, + }, + "stores": []map[string]interface{}{ + {"key": "store-1"}, + }, + "shippingAddressIds": []string{"address-id-1", "address-id-2"}, + "billingAddressIds": []string{"address-id-1", "address-id-2"}, + "defaultBillingAddressId": utils.Ref("address-id-2"), + "defaultShippingAddressId": utils.Ref("address-id-2"), + }, + expected: Company{ + ID: types.StringValue("company-id"), + Key: types.StringValue("company-key"), + Version: types.Int64Value(1), + Status: types.StringValue("Active"), + Name: types.StringValue("Company Name"), + ContactEmail: types.StringValue("contact@example.com"), + Addresses: []sharedtypes.Address{ + { + ID: types.StringValue("address-id-1"), + Key: types.StringValue("address-1"), + Country: types.StringValue("US"), + City: types.StringValue("New York"), + }, + { + ID: types.StringValue("address-id-2"), + Key: types.StringValue("address-2"), + Country: types.StringValue("US"), + City: types.StringValue("Detroit"), + }, + }, + Stores: []sharedtypes.StoreKeyReference{ + {Key: types.StringValue("store-1")}, + }, + ShippingAddressKeys: []types.String{types.StringValue("address-1"), types.StringValue("address-2")}, + BillingAddressKeys: []types.String{types.StringValue("address-1"), types.StringValue("address-2")}, + DefaultBillingAddressKey: types.StringValue("address-2"), + DefaultShippingAddressKey: types.StringValue("address-2"), + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var data platform.BusinessUnit + err := utils.DecodeStruct(c.company, &data) + assert.NoError(t, err) + + result, err := NewCompanyFromNative(&data) + assert.NoError(t, err) + assert.Equal(t, c.expected, result) + }) + } +} diff --git a/internal/resources/business_unit_company/resource.go b/internal/resources/business_unit_company/resource.go new file mode 100644 index 00000000..4a725037 --- /dev/null +++ b/internal/resources/business_unit_company/resource.go @@ -0,0 +1,310 @@ +package business_unit_company + +import ( + "context" + "errors" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/labd/terraform-provider-commercetools/internal/sharedtypes" + "regexp" + "time" + + "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/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/labd/commercetools-go-sdk/platform" + "github.com/labd/terraform-provider-commercetools/internal/utils" +) + +var ( + _ resource.Resource = &companyResource{} + _ resource.ResourceWithConfigure = &companyResource{} + _ resource.ResourceWithImportState = &companyResource{} +) + +type companyResource struct { + client *platform.ByProjectKeyRequestBuilder +} + +func NewCompanyResource() resource.Resource { + return &companyResource{} +} + +// Schema implements resource.Resource. +func (b *companyResource) Schema(_ context.Context, req resource.SchemaRequest, res *resource.SchemaResponse) { + res.Schema = schema.Schema{ + MarkdownDescription: "Business Unit type to represent the top level of a business. Contains specific fields and values that differentiate a Company from the generic BusinessUnit.\n\n" + + "See also the [Business Unit API Documentation](https://docs.commercetools.com/api/projects/business-units", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "Unique identifier of the Company.", + Computed: true, + }, + "version": schema.Int64Attribute{ + MarkdownDescription: "The current version of the Company.", + Computed: true, + }, + "key": schema.StringAttribute{ + MarkdownDescription: "User-defined unique identifier for the Company.", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(2, 256), + stringvalidator.RegexMatches( + regexp.MustCompile("^[A-Za-z0-9_-]+$"), + "Key must match pattern ^[A-Za-z0-9_-]+$", + ), + }, + }, + "status": schema.StringAttribute{ + MarkdownDescription: "The status of the Company.", + Optional: true, + Computed: true, + Default: stringdefault.StaticString(string(platform.BusinessUnitStatusActive)), + Validators: []validator.String{ + stringvalidator.OneOf( + string(platform.BusinessUnitStatusActive), + string(platform.BusinessUnitStatusInactive), + ), + }, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "The name of the Company.", + Required: true, + }, + "contact_email": schema.StringAttribute{ + MarkdownDescription: "The email address of the Company.", + Optional: true, + }, + "shipping_address_keys": schema.SetAttribute{ + MarkdownDescription: "Indexes of entries in addresses to set as shipping addresses. The shippingAddressIds of the [Customer](https://docs.commercetools.com/api/projects/customers) will be replaced by these addresses.", + Optional: true, + ElementType: types.StringType, + }, + "default_shipping_address_key": schema.StringAttribute{ + MarkdownDescription: "Index of the entry in addresses to set as the default shipping address.", + Optional: true, + }, + "billing_address_keys": schema.SetAttribute{ + MarkdownDescription: "Indexes of entries in addresses to set as billing addresses. The billingAddressIds of the [Customer](https://docs.commercetools.com/api/projects/customers) will be replaced by these addresses.", + Optional: true, + ElementType: types.StringType, + }, + "default_billing_address_key": schema.StringAttribute{ + MarkdownDescription: "Index of the entry in addresses to set as the default billing address.", + Optional: true, + }, + }, + Blocks: map[string]schema.Block{ + "store": sharedtypes.StoreKeyReferenceBlockSchema, + "address": sharedtypes.AddressBlockSchema, + }, + } +} + +// Metadata implements resource.Resource. +func (b *companyResource) Metadata(_ context.Context, req resource.MetadataRequest, res *resource.MetadataResponse) { + res.TypeName = req.ProviderTypeName + "_business_unit_company" +} + +// ImportState implements resource.ResourceWithImportState. +func (b *companyResource) ImportState(ctx context.Context, req resource.ImportStateRequest, res *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, res) +} + +// Configure implements resource.ResourceWithConfigure. +func (b *companyResource) Configure(ctx context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + data, ok := req.ProviderData.(*utils.ProviderData) + if !ok { + return + } + + b.client = data.Client +} + +// Create implements resource.Resource. +func (b *companyResource) Create(ctx context.Context, req resource.CreateRequest, res *resource.CreateResponse) { + var plan Company + diags := req.Plan.Get(ctx, &plan) + res.Diagnostics.Append(diags...) + if res.Diagnostics.HasError() { + return + } + + draft, err := plan.draft() + if err != nil { + res.Diagnostics.AddError( + "Error creating business unit", + "Could not create business unit, unexpected error: "+err.Error(), + ) + return + } + + var bu *platform.BusinessUnit + err = retry.RetryContext(ctx, 20*time.Second, func() *retry.RetryError { + var err error + bu, err = b.client.BusinessUnits().Post(draft).Execute(ctx) + + return utils.ProcessRemoteError(err) + }) + if err != nil { + res.Diagnostics.AddError( + "Error creating business unit", + "Could not create business unit, unexpected error: "+err.Error(), + ) + return + } + + current, err := NewCompanyFromNative(bu) + if err != nil { + res.Diagnostics.AddError( + "Error mapping business unit", + "Could not create business unit, unexpected error: "+err.Error(), + ) + return + } + + diags = res.State.Set(ctx, ¤t) + res.Diagnostics.Append(diags...) + if res.Diagnostics.HasError() { + return + } +} + +// Read implements resource.Resource. +func (b *companyResource) Read(ctx context.Context, req resource.ReadRequest, res *resource.ReadResponse) { + var state Company + diags := req.State.Get(ctx, &state) + res.Diagnostics.Append(diags...) + if res.Diagnostics.HasError() { + return + } + + bu, err := b.client.BusinessUnits().WithId(state.ID.ValueString()).Get().Execute(ctx) + if err != nil { + if errors.Is(err, platform.ErrNotFound) { + res.State.RemoveResource(ctx) + return + } + + res.Diagnostics.AddError( + "Error reading business unit", + "Could not retrieve the business unit, unexpected error: "+err.Error(), + ) + return + } + + current, err := NewCompanyFromNative(bu) + if err != nil { + res.Diagnostics.AddError( + "Error mapping business unit", + "Could not create business unit, unexpected error: "+err.Error(), + ) + return + } + + diags = res.State.Set(ctx, current) + res.Diagnostics.Append(diags...) + if res.Diagnostics.HasError() { + return + } +} + +// Update implements resource.Resource. +func (b *companyResource) Update(ctx context.Context, req resource.UpdateRequest, res *resource.UpdateResponse) { + var plan Company + diags := req.Plan.Get(ctx, &plan) + res.Diagnostics.Append(diags...) + if res.Diagnostics.HasError() { + return + } + + var state Company + diags = req.State.Get(ctx, &state) + res.Diagnostics.Append(diags...) + if res.Diagnostics.HasError() { + return + } + + input, err := state.updateActions(plan) + if err != nil { + res.Diagnostics.AddError( + "Error updating business unit", + "Could not update business unit, unexpected error: "+err.Error(), + ) + return + } + + var bu *platform.BusinessUnit + + err = retry.RetryContext(ctx, 5*time.Second, func() *retry.RetryError { + var err error + bu, err = b.client.BusinessUnits(). + WithId(state.ID.ValueString()). + Post(input). + Execute(ctx) + + return utils.ProcessRemoteError(err) + }) + if err != nil { + res.Diagnostics.AddError( + "Error updating business unit", + "Could not update business unit, unexpected error: "+err.Error(), + ) + return + } + + current, err := NewCompanyFromNative(bu) + if err != nil { + res.Diagnostics.AddError( + "Error mapping business unit", + "Could not create business unit, unexpected error: "+err.Error(), + ) + return + } + + diags = res.State.Set(ctx, ¤t) + res.Diagnostics.Append(diags...) + if res.Diagnostics.HasError() { + return + } +} + +// Delete implements resource.Resource. +func (b *companyResource) Delete(ctx context.Context, req resource.DeleteRequest, res *resource.DeleteResponse) { + var state Company + + diags := req.State.Get(ctx, &state) + res.Diagnostics.Append(diags...) + + if res.Diagnostics.HasError() { + return + } + + err := retry.RetryContext( + ctx, + 5*time.Second, + func() *retry.RetryError { + _, err := b.client.BusinessUnits(). + WithId(state.ID.ValueString()). + Delete(). + Version(int(state.Version.ValueInt64())). + Execute(ctx) + + return utils.ProcessRemoteError(err) + }, + ) + if err != nil { + res.Diagnostics.AddError( + "Error deleting business unit", + "Could not delete business unit, unexpected error: "+err.Error(), + ) + return + } +} diff --git a/internal/resources/business_unit/resource_test.go b/internal/resources/business_unit_company/resource_test.go similarity index 82% rename from internal/resources/business_unit/resource_test.go rename to internal/resources/business_unit_company/resource_test.go index ad3b3ff2..a8273a1b 100644 --- a/internal/resources/business_unit/resource_test.go +++ b/internal/resources/business_unit_company/resource_test.go @@ -1,46 +1,27 @@ -package business_unit_test +package business_unit_company_test import ( "context" + "github.com/labd/terraform-provider-commercetools/internal/resources/business_unit_company" "testing" fwresource "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/labd/terraform-provider-commercetools/internal/acctest" - "github.com/labd/terraform-provider-commercetools/internal/resources/business_unit" "github.com/labd/terraform-provider-commercetools/internal/utils" ) -func TestCompanySchemaImplementation(t *testing.T) { - t.Parallel() - - ctx := context.Background() - schemaRequest := fwresource.SchemaRequest{} - schemaResponse := &fwresource.SchemaResponse{} - - business_unit.NewCompanyResource().Schema(ctx, schemaRequest, schemaResponse) - - if schemaResponse.Diagnostics.HasError() { - t.Fatalf("Schema method diagnostics: %+v", schemaResponse.Diagnostics) - } - - // schema validation - diagnostics := schemaResponse.Schema.ValidateImplementation(ctx) - - if diagnostics.HasError() { - t.Fatalf("Schema validation diagnostics: %+v", diagnostics) - } -} - -func TestDivisionSchemaImplementation(t *testing.T) { +func TestBusinessUnitCompanySchemaImplementation(t *testing.T) { t.Parallel() ctx := context.Background() schemaRequest := fwresource.SchemaRequest{} schemaResponse := &fwresource.SchemaResponse{} - business_unit.NewDivisionResource().Schema(ctx, schemaRequest, schemaResponse) + business_unit_company.NewCompanyResource().Schema(ctx, schemaRequest, schemaResponse) if schemaResponse.Diagnostics.HasError() { t.Fatalf("Schema method diagnostics: %+v", schemaResponse.Diagnostics) @@ -54,7 +35,7 @@ func TestDivisionSchemaImplementation(t *testing.T) { } } -func TestBusinessUnitResource_Company(t *testing.T) { +func TestBusinessUnitResource(t *testing.T) { r := "commercetools_business_unit_company.acme_company" resource.Test(t, resource.TestCase{ @@ -114,12 +95,10 @@ func businessUnitTFResourceDef(name, status, email string) string { store { key = "acme-usa" - type_id = "store" } store { key = "acme-germany" - type_id = "store" } address { diff --git a/internal/resources/business_unit_division/model.go b/internal/resources/business_unit_division/model.go new file mode 100644 index 00000000..d402fce3 --- /dev/null +++ b/internal/resources/business_unit_division/model.go @@ -0,0 +1,362 @@ +package business_unit_division + +import ( + "fmt" + "github.com/labd/terraform-provider-commercetools/internal/sharedtypes" + "github.com/labd/terraform-provider-commercetools/internal/utils" + "reflect" + "slices" + "sort" + + "github.com/elliotchance/pie/v2" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/labd/commercetools-go-sdk/platform" +) + +type Division struct { + ID types.String `tfsdk:"id"` + Version types.Int64 `tfsdk:"version"` + Key types.String `tfsdk:"key"` + Status types.String `tfsdk:"status"` + StoreMode types.String `tfsdk:"store_mode"` + ApprovalRuleMode types.String `tfsdk:"approval_rule_mode"` + Name types.String `tfsdk:"name"` + ContactEmail types.String `tfsdk:"contact_email"` + AssociateMode types.String `tfsdk:"associate_mode"` + ShippingAddressKeys []types.String `tfsdk:"shipping_address_keys"` + DefaultShippingAddressKey types.String `tfsdk:"default_shipping_address_key"` + BillingAddressKeys []types.String `tfsdk:"billing_address_keys"` + DefaultBillingAddressKey types.String `tfsdk:"default_billing_address_key"` + ParentUnit BusinessUnitResourceIdentifier `tfsdk:"parent_unit"` + Stores []sharedtypes.StoreKeyReference `tfsdk:"store"` + Addresses []sharedtypes.Address `tfsdk:"address"` +} + +func (d *Division) draft() (platform.DivisionDraft, error) { + mode := platform.BusinessUnitStoreMode(d.StoreMode.ValueString()) + associateMode := platform.BusinessUnitAssociateMode(d.AssociateMode.ValueString()) + status := platform.BusinessUnitStatus(d.Status.ValueString()) + approvalRuleMode := platform.BusinessUnitApprovalRuleMode(d.ApprovalRuleMode.ValueString()) + + var addresses []platform.BaseAddress + for _, a := range d.Addresses { + addresses = append(addresses, a.Draft()) + } + + var stores []platform.StoreResourceIdentifier + for _, s := range d.Stores { + stores = append(stores, platform.StoreResourceIdentifier{ + Key: s.Key.ValueStringPointer(), + }) + } + + var shippingAddressIndexes []int + for _, key := range d.ShippingAddressKeys { + i := slices.IndexFunc(d.Addresses, func(a sharedtypes.Address) bool { + return a.Key.ValueString() == key.ValueString() + }) + + if i == -1 { + return platform.DivisionDraft{}, fmt.Errorf("shipping address key %s is not in addresses", key.ValueString()) + } + + shippingAddressIndexes = append(shippingAddressIndexes, i) + } + + var billingAddressIndexes []int + for _, key := range d.BillingAddressKeys { + i := slices.IndexFunc(d.Addresses, func(a sharedtypes.Address) bool { + return a.Key.ValueString() == key.ValueString() + }) + + if i == -1 { + return platform.DivisionDraft{}, fmt.Errorf("billing address key %s is not in addresses", key.ValueString()) + } + + billingAddressIndexes = append(billingAddressIndexes, i) + } + + var defaultBillingAddressIndex *int + if !d.DefaultBillingAddressKey.IsNull() { + i := slices.IndexFunc(d.Addresses, func(a sharedtypes.Address) bool { + return a.Key.ValueString() == d.DefaultBillingAddressKey.ValueString() + }) + + if i == -1 { + return platform.DivisionDraft{}, fmt.Errorf("default billing address key %s is not in addresses", d.DefaultBillingAddressKey.ValueString()) + } + + defaultBillingAddressIndex = &i + } + + var defaultShippingAddressIndex *int + if !d.DefaultShippingAddressKey.IsNull() { + i := slices.IndexFunc(d.Addresses, func(a sharedtypes.Address) bool { + return a.Key.ValueString() == d.DefaultShippingAddressKey.ValueString() + }) + + if i == -1 { + return platform.DivisionDraft{}, fmt.Errorf("default shipping address key %s is not in addresses", d.DefaultShippingAddressKey.ValueString()) + } + + defaultShippingAddressIndex = &i + } + + return platform.DivisionDraft{ + Key: d.Key.ValueString(), + Status: &status, + StoreMode: &mode, + AssociateMode: &associateMode, + ApprovalRuleMode: &approvalRuleMode, + ParentUnit: platform.BusinessUnitResourceIdentifier{ + ID: d.ParentUnit.ID.ValueStringPointer(), + Key: d.ParentUnit.Key.ValueStringPointer(), + }, + Stores: stores, + Name: d.Name.ValueString(), + ContactEmail: d.ContactEmail.ValueStringPointer(), + Addresses: addresses, + ShippingAddresses: shippingAddressIndexes, + BillingAddresses: billingAddressIndexes, + DefaultShippingAddress: defaultShippingAddressIndex, + DefaultBillingAddress: defaultBillingAddressIndex, + }, nil +} + +func (d *Division) updateActions(plan Division) (platform.BusinessUnitUpdate, error) { + result := platform.BusinessUnitUpdate{ + Version: int(d.Version.ValueInt64()), + Actions: []platform.BusinessUnitUpdateAction{}, + } + + if !d.Name.Equal(plan.Name) { + result.Actions = append(result.Actions, platform.BusinessUnitChangeNameAction{ + Name: plan.Name.ValueString(), + }) + } + + if !d.ContactEmail.Equal(plan.ContactEmail) { + result.Actions = append(result.Actions, platform.BusinessUnitSetContactEmailAction{ + ContactEmail: plan.ContactEmail.ValueStringPointer(), + }) + } + + if !d.Status.Equal(plan.Status) { + result.Actions = append(result.Actions, platform.BusinessUnitChangeStatusAction{ + Status: plan.Status.ValueString(), + }) + } + + if !d.AssociateMode.Equal(plan.AssociateMode) { + result.Actions = append(result.Actions, platform.BusinessUnitChangeAssociateModeAction{ + AssociateMode: platform.BusinessUnitAssociateMode(plan.AssociateMode.ValueString()), + }) + } + + if !d.ApprovalRuleMode.Equal(plan.ApprovalRuleMode) { + result.Actions = append(result.Actions, platform.BusinessUnitChangeApprovalRuleModeAction{ + ApprovalRuleMode: platform.BusinessUnitApprovalRuleMode(plan.ApprovalRuleMode.ValueString()), + }) + } + + if !d.StoreMode.Equal(plan.StoreMode) { + result.Actions = append(result.Actions, platform.BusinessUnitSetStoreModeAction{ + StoreMode: platform.BusinessUnitStoreMode(plan.StoreMode.ValueString()), + Stores: []platform.StoreResourceIdentifier{}, + }) + } + + if !reflect.DeepEqual(d.Stores, plan.Stores) { + result.Actions = append(result.Actions, platform.BusinessUnitSetStoresAction{ + Stores: pie.Map(plan.Stores, func(s sharedtypes.StoreKeyReference) platform.StoreResourceIdentifier { + return platform.StoreResourceIdentifier{ + Key: s.Key.ValueStringPointer(), + } + }), + }) + } + + if !reflect.DeepEqual(d.Addresses, plan.Addresses) { + addressAddActions := sharedtypes.AddressesAddActions(d.Addresses, plan.Addresses) + for _, action := range addressAddActions { + result.Actions = append(result.Actions, action) + } + + addressChangeActions := sharedtypes.AddressesChangeActions(d.Addresses, plan.Addresses) + for _, action := range addressChangeActions { + result.Actions = append(result.Actions, action) + } + } + + if !d.DefaultShippingAddressKey.Equal(plan.DefaultShippingAddressKey) { + if plan.DefaultShippingAddressKey.IsNull() { + result.Actions = append(result.Actions, platform.BusinessUnitSetDefaultShippingAddressAction{}) + } else { + if !pie.Contains(plan.ShippingAddressKeys, plan.DefaultShippingAddressKey) { + return result, fmt.Errorf("default shipping address key %s is not in shipping address keys", plan.DefaultShippingAddressKey.ValueString()) + } + + result.Actions = append(result.Actions, platform.BusinessUnitSetDefaultShippingAddressAction{ + AddressKey: plan.DefaultShippingAddressKey.ValueStringPointer(), + }) + } + } + + if !d.DefaultBillingAddressKey.Equal(plan.DefaultBillingAddressKey) { + if plan.DefaultBillingAddressKey.IsNull() { + result.Actions = append(result.Actions, platform.BusinessUnitSetDefaultBillingAddressAction{}) + } else { + if !pie.Contains(plan.BillingAddressKeys, plan.DefaultBillingAddressKey) { + return result, fmt.Errorf("default billing address key %s is not in billing address keys", plan.DefaultBillingAddressKey.ValueString()) + } + + result.Actions = append(result.Actions, platform.BusinessUnitSetDefaultBillingAddressAction{ + AddressKey: plan.DefaultBillingAddressKey.ValueStringPointer(), + }) + } + } + + if !reflect.DeepEqual(d.ShippingAddressKeys, plan.ShippingAddressKeys) { + // find shipping addresses to be added + for _, i := range plan.ShippingAddressKeys { + if !pie.Contains(d.ShippingAddressKeys, i) { + result.Actions = append(result.Actions, platform.BusinessUnitAddShippingAddressIdAction{ + AddressKey: i.ValueStringPointer(), + }) + } + } + + // find shipping addresses to be removed + for _, i := range d.ShippingAddressKeys { + if !pie.Contains(plan.ShippingAddressKeys, i) { + result.Actions = append(result.Actions, platform.BusinessUnitRemoveShippingAddressIdAction{ + AddressKey: i.ValueStringPointer(), + }) + } + } + } + + if !reflect.DeepEqual(d.BillingAddressKeys, plan.BillingAddressKeys) { + // find billing addresses to be added + for _, i := range plan.BillingAddressKeys { + if !pie.Contains(d.BillingAddressKeys, i) { + result.Actions = append(result.Actions, platform.BusinessUnitAddBillingAddressIdAction{ + AddressKey: i.ValueStringPointer(), + }) + } + } + + // find billing addresses to be removed + for _, i := range d.BillingAddressKeys { + if !pie.Contains(plan.BillingAddressKeys, i) { + result.Actions = append(result.Actions, platform.BusinessUnitRemoveBillingAddressIdAction{ + AddressKey: i.ValueStringPointer(), + }) + } + } + } + + // We need to delete addresses only after we have removed keys + if !reflect.DeepEqual(d.Addresses, plan.Addresses) { + addressDeleteActions := sharedtypes.AddressesDeleteActions(d.Addresses, plan.Addresses) + for _, action := range addressDeleteActions { + result.Actions = append(result.Actions, action) + } + } + + return result, nil +} + +// NewDivisionFromNative creates a new Division from a platform.Division. +func NewDivisionFromNative(bu *platform.BusinessUnit) (Division, error) { + data := (*bu).(map[string]interface{}) + var d platform.Division + err := utils.DecodeStruct(data, &d) + if err != nil { + return Division{}, err + } + + parent := BusinessUnitResourceIdentifier{ + Key: types.StringValue(d.ParentUnit.Key), + } + + var defaultShippingAddressKey *string + if d.DefaultShippingAddressId != nil { + i := slices.IndexFunc(d.Addresses, func(a platform.Address) bool { + return *a.ID == *d.DefaultShippingAddressId + }) + defaultShippingAddressKey = d.Addresses[i].Key + } + var defaultBillingAddressKey *string + if d.DefaultBillingAddressId != nil { + i := slices.IndexFunc(d.Addresses, func(a platform.Address) bool { + return *a.ID == *d.DefaultBillingAddressId + }) + defaultBillingAddressKey = d.Addresses[i].Key + } + + var shippingAddressKeys []types.String + for _, id := range d.ShippingAddressIds { + i := slices.IndexFunc(d.Addresses, func(a platform.Address) bool { + return *a.ID == id + }) + shippingAddressKeys = append(shippingAddressKeys, types.StringPointerValue(d.Addresses[i].Key)) + } + + var billingAddressKeys []types.String + for _, id := range d.BillingAddressIds { + i := slices.IndexFunc(d.Addresses, func(a platform.Address) bool { + return *a.ID == id + }) + billingAddressKeys = append(billingAddressKeys, types.StringPointerValue(d.Addresses[i].Key)) + } + + var stores []sharedtypes.StoreKeyReference + for _, s := range d.Stores { + stores = append(stores, sharedtypes.NewStoreKeyReferenceFromNative(&s)) + } + + var addresses []sharedtypes.Address + for _, a := range d.Addresses { + addresses = append(addresses, sharedtypes.NewAddressFromNative(&a)) + } + + division := Division{ + ID: types.StringValue(d.ID), + Version: types.Int64Value(int64(d.Version)), + Key: types.StringValue(d.Key), + Status: types.StringValue(string(d.Status)), + ParentUnit: parent, + StoreMode: types.StringValue(string(d.StoreMode)), + ApprovalRuleMode: types.StringValue(string(d.ApprovalRuleMode)), + Name: types.StringValue(d.Name), + ContactEmail: types.StringPointerValue(d.ContactEmail), + DefaultShippingAddressKey: types.StringPointerValue(defaultShippingAddressKey), + DefaultBillingAddressKey: types.StringPointerValue(defaultBillingAddressKey), + AssociateMode: types.StringValue(string(d.AssociateMode)), + Stores: stores, + Addresses: addresses, + ShippingAddressKeys: shippingAddressKeys, + BillingAddressKeys: billingAddressKeys, + } + + sort.Slice(division.Addresses, func(i, j int) bool { + return division.Addresses[i].Key.ValueString() < division.Addresses[j].Key.ValueString() + }) + + sort.Slice(division.ShippingAddressKeys, func(i, j int) bool { + return division.ShippingAddressKeys[i].ValueString() < division.ShippingAddressKeys[j].ValueString() + }) + + sort.Slice(division.BillingAddressKeys, func(i, j int) bool { + return division.BillingAddressKeys[i].ValueString() < division.BillingAddressKeys[j].ValueString() + }) + + return division, nil +} + +// BusinessUnitResourceIdentifier is a resource identifier for a business unit. +type BusinessUnitResourceIdentifier struct { + ID types.String `tfsdk:"id"` + Key types.String `tfsdk:"key"` +} diff --git a/internal/resources/business_unit_division/model_test.go b/internal/resources/business_unit_division/model_test.go new file mode 100644 index 00000000..d76bd32a --- /dev/null +++ b/internal/resources/business_unit_division/model_test.go @@ -0,0 +1,446 @@ +package business_unit_division + +import ( + "github.com/labd/terraform-provider-commercetools/internal/sharedtypes" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/labd/commercetools-go-sdk/platform" + "github.com/labd/terraform-provider-commercetools/internal/utils" + "github.com/stretchr/testify/assert" +) + +func TestBusinessUnit_Division_Draft(t *testing.T) { + cases := []struct { + name string + division Division + expected platform.DivisionDraft + }{ + { + name: "Basic division draft", + division: Division{ + Key: types.StringValue("division-key"), + Status: types.StringValue("Active"), + Name: types.StringValue("division Name"), + ContactEmail: types.StringValue("contact@example.com"), + StoreMode: types.StringValue(string(platform.BusinessUnitStoreModeExplicit)), + AssociateMode: types.StringValue(string(platform.BusinessUnitAssociateModeExplicit)), + ApprovalRuleMode: types.StringValue(string(platform.BusinessUnitApprovalRuleModeExplicit)), + ParentUnit: BusinessUnitResourceIdentifier{ + Key: types.StringValue("parent-key"), + }, + Addresses: []sharedtypes.Address{ + { + Key: types.StringValue("address-1"), + Country: types.StringValue("US"), + City: types.StringValue("New York"), + }, + { + Key: types.StringValue("address-2"), + Country: types.StringValue("US"), + City: types.StringValue("Detroit"), + }, + }, + Stores: []sharedtypes.StoreKeyReference{ + {Key: types.StringValue("store-1")}, + }, + ShippingAddressKeys: []types.String{types.StringValue("address-1"), types.StringValue("address-2")}, + BillingAddressKeys: []types.String{types.StringValue("address-1"), types.StringValue("address-2")}, + DefaultBillingAddressKey: types.StringValue("address-2"), + DefaultShippingAddressKey: types.StringValue("address-2"), + }, + expected: platform.DivisionDraft{ + Key: "division-key", + Status: utils.Ref(platform.BusinessUnitStatusActive), + Name: "division Name", + StoreMode: utils.Ref(platform.BusinessUnitStoreModeExplicit), + AssociateMode: utils.Ref(platform.BusinessUnitAssociateModeExplicit), + ApprovalRuleMode: utils.Ref(platform.BusinessUnitApprovalRuleModeExplicit), + ContactEmail: utils.Ref("contact@example.com"), + ParentUnit: platform.BusinessUnitResourceIdentifier{ + Key: utils.Ref("parent-key"), + }, + Addresses: []platform.BaseAddress{ + { + Key: utils.Ref("address-1"), + Country: "US", + City: utils.Ref("New York"), + }, + { + Key: utils.Ref("address-2"), + Country: "US", + City: utils.Ref("Detroit"), + }, + }, + Stores: []platform.StoreResourceIdentifier{ + { + Key: utils.Ref("store-1"), + }, + }, + DefaultShippingAddress: utils.Ref(1), + DefaultBillingAddress: utils.Ref(1), + ShippingAddresses: []int{0, 1}, + BillingAddresses: []int{0, 1}, + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + result, err := c.division.draft() + assert.NoError(t, err) + assert.Equal(t, c.expected, result) + }) + } +} + +func TestBusinessUnit_Division_UpdateActions(t *testing.T) { + cases := []struct { + name string + state Division + plan Division + expected platform.BusinessUnitUpdate + }{ + { + "business unit update name", + Division{ + Name: types.StringValue("Example business unit"), + }, + Division{ + Name: types.StringValue("Updated business unit"), + }, + platform.BusinessUnitUpdate{ + Actions: []platform.BusinessUnitUpdateAction{ + platform.BusinessUnitChangeNameAction{ + Name: "Updated business unit", + }, + }, + }, + }, + { + "business unit update contact email", + Division{ + ContactEmail: types.StringValue("info@example.com"), + }, + Division{ + ContactEmail: types.StringValue("new@example.com"), + }, + platform.BusinessUnitUpdate{ + Actions: []platform.BusinessUnitUpdateAction{ + platform.BusinessUnitSetContactEmailAction{ + ContactEmail: types.StringValue("new@example.com").ValueStringPointer(), + }, + }, + }, + }, + { + "business unit update status", + Division{ + Status: types.StringValue("Active"), + }, + Division{ + Status: types.StringValue("Inactive"), + }, + platform.BusinessUnitUpdate{ + Actions: []platform.BusinessUnitUpdateAction{ + platform.BusinessUnitChangeStatusAction{ + Status: "Inactive", + }, + }, + }, + }, + { + "business unit update default shipping address", + Division{ + ShippingAddressKeys: []types.String{types.StringValue("some-random-id"), types.StringValue("another-random-id")}, + DefaultShippingAddressKey: types.StringValue("some-random-id"), + }, + Division{ + ShippingAddressKeys: []types.String{types.StringValue("some-random-id"), types.StringValue("another-random-id")}, + DefaultShippingAddressKey: types.StringValue("another-random-id"), + }, + platform.BusinessUnitUpdate{ + Actions: []platform.BusinessUnitUpdateAction{ + platform.BusinessUnitSetDefaultShippingAddressAction{ + AddressKey: types.StringValue("another-random-id").ValueStringPointer(), + }, + }, + }, + }, + { + "business unit update default billing address", + Division{ + BillingAddressKeys: []types.String{types.StringValue("some-random-id"), types.StringValue("another-random-id")}, + DefaultBillingAddressKey: types.StringValue("some-random-id"), + }, + Division{ + BillingAddressKeys: []types.String{types.StringValue("some-random-id"), types.StringValue("another-random-id")}, + DefaultBillingAddressKey: types.StringValue("another-random-id"), + }, + platform.BusinessUnitUpdate{ + Actions: []platform.BusinessUnitUpdateAction{ + platform.BusinessUnitSetDefaultBillingAddressAction{ + AddressKey: types.StringValue("another-random-id").ValueStringPointer(), + }, + }, + }, + }, + { + "business unit update associate mode", + Division{ + AssociateMode: types.StringValue("Explicit"), + }, + Division{ + AssociateMode: types.StringValue("ExplicitAndFromParent"), + }, + platform.BusinessUnitUpdate{ + Actions: []platform.BusinessUnitUpdateAction{ + platform.BusinessUnitChangeAssociateModeAction{ + AssociateMode: "ExplicitAndFromParent", + }, + }, + }, + }, + { + "business unit update stores", + Division{ + Stores: []sharedtypes.StoreKeyReference{ + { + Key: types.StringValue("store-1"), + }, + { + Key: types.StringValue("store-2"), + }, + }, + }, + Division{ + Stores: []sharedtypes.StoreKeyReference{ + { + Key: types.StringValue("store-1"), + }, + { + Key: types.StringValue("store-3"), + }, + }, + }, + platform.BusinessUnitUpdate{ + Actions: []platform.BusinessUnitUpdateAction{ + platform.BusinessUnitSetStoresAction{ + Stores: []platform.StoreResourceIdentifier{ + { + Key: types.StringValue("store-1").ValueStringPointer(), + ID: nil, + }, + { + + Key: types.StringValue("store-3").ValueStringPointer(), + ID: nil, + }, + }, + }, + }, + }, + }, + { + "business unit add address", + Division{ + Addresses: []sharedtypes.Address{}, + }, + Division{ + Addresses: []sharedtypes.Address{ + { + Key: types.StringValue("new-york-office"), + Country: types.StringValue("US"), + Salutation: types.StringValue("Mr."), + FirstName: types.StringValue("John"), + LastName: types.StringValue("Doe"), + StreetName: types.StringValue("Main St."), + StreetNumber: types.StringValue("123"), + AdditionalStreetInfo: types.StringValue("Apt. 1"), + PostalCode: types.StringValue("12345"), + City: types.StringValue("New York"), + Region: types.StringValue("New York"), + State: types.StringValue("New York"), + Company: types.StringValue("Example Inc."), + Department: types.StringValue("Sales"), + Building: types.StringValue("1"), + Apartment: types.StringValue("1"), + POBox: types.StringValue("123"), + Phone: types.StringValue("1234567890"), + Mobile: types.StringValue("1234567890"), + Fax: types.StringValue("1234567890"), + }, + }, + }, + platform.BusinessUnitUpdate{ + Actions: []platform.BusinessUnitUpdateAction{ + platform.BusinessUnitAddAddressAction{ + Address: platform.BaseAddress{ + Key: utils.StringRef("new-york-office"), + Country: "US", + Salutation: utils.StringRef("Mr."), + FirstName: utils.StringRef("John"), + LastName: utils.StringRef("Doe"), + StreetName: utils.StringRef("Main St."), + StreetNumber: utils.StringRef("123"), + AdditionalStreetInfo: utils.StringRef("Apt. 1"), + PostalCode: utils.StringRef("12345"), + City: utils.StringRef("New York"), + Region: utils.StringRef("New York"), + State: utils.StringRef("New York"), + Company: utils.StringRef("Example Inc."), + Department: utils.StringRef("Sales"), + Building: utils.StringRef("1"), + Apartment: utils.StringRef("1"), + POBox: utils.StringRef("123"), + Phone: utils.StringRef("1234567890"), + Mobile: utils.StringRef("1234567890"), + Fax: utils.StringRef("1234567890"), + }, + }, + }, + }, + }, + { + "business unit remove address", + Division{ + Addresses: []sharedtypes.Address{ + { + Key: types.StringValue("new-york-office"), + Country: types.StringValue("US"), + Salutation: types.StringValue("Mr."), + FirstName: types.StringValue("John"), + LastName: types.StringValue("Doe"), + StreetName: types.StringValue("Main St."), + StreetNumber: types.StringValue("123"), + AdditionalStreetInfo: types.StringValue("Apt. 1"), + PostalCode: types.StringValue("12345"), + City: types.StringValue("New York"), + Region: types.StringValue("New York"), + State: types.StringValue("New York"), + Company: types.StringValue("Example Inc."), + Department: types.StringValue("Sales"), + Building: types.StringValue("1"), + Apartment: types.StringValue("1"), + POBox: types.StringValue("123"), + Phone: types.StringValue("1234567890"), + Mobile: types.StringValue("1234567890"), + Fax: types.StringValue("1234567890"), + }, + }, + }, + Division{ + Addresses: []sharedtypes.Address{}, + }, + platform.BusinessUnitUpdate{ + Actions: []platform.BusinessUnitUpdateAction{ + platform.BusinessUnitRemoveAddressAction{ + AddressKey: utils.StringRef("new-york-office"), + }, + }, + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + result, err := c.state.updateActions(c.plan) + assert.NoError(t, err) + assert.EqualValues(t, c.expected, result) + }) + } +} + +func TestBusinessUnit_Division_NewDivisionFromNative(t *testing.T) { + cases := []struct { + name string + division map[string]interface{} + expected Division + }{ + { + name: "Basic division draft", + division: map[string]interface{}{ + "id": "division-id", + "key": "division-key", + "version": 1, + "status": platform.BusinessUnitStatusActive, + "approvalRuleMode": platform.BusinessUnitApprovalRuleModeExplicit, + "associateMode": platform.BusinessUnitAssociateModeExplicit, + "storeMode": platform.BusinessUnitStoreModeExplicit, + "name": "division Name", + "contactEmail": utils.Ref("contact@example.com"), + "addresses": []map[string]interface{}{ + { + "id": utils.Ref("address-id-1"), + "key": utils.Ref("address-1"), + "country": "US", + "city": utils.Ref("New York"), + }, + { + "id": utils.Ref("address-id-2"), + "key": utils.Ref("address-2"), + "country": "US", + "city": utils.Ref("Detroit"), + }, + }, + "stores": []map[string]interface{}{ + {"key": "store-1"}, + }, + "parentUnit": map[string]interface{}{ + "key": "parent-key", + }, + "shippingAddressIds": []string{"address-id-1", "address-id-2"}, + "billingAddressIds": []string{"address-id-1", "address-id-2"}, + "defaultBillingAddressId": utils.Ref("address-id-2"), + "defaultShippingAddressId": utils.Ref("address-id-2"), + }, + expected: Division{ + ID: types.StringValue("division-id"), + Key: types.StringValue("division-key"), + Version: types.Int64Value(1), + Status: types.StringValue(string(platform.BusinessUnitStatusActive)), + Name: types.StringValue("division Name"), + ContactEmail: types.StringValue("contact@example.com"), + StoreMode: types.StringValue(string(platform.BusinessUnitStoreModeExplicit)), + AssociateMode: types.StringValue(string(platform.BusinessUnitAssociateModeExplicit)), + ApprovalRuleMode: types.StringValue(string(platform.BusinessUnitApprovalRuleModeExplicit)), + ParentUnit: BusinessUnitResourceIdentifier{ + Key: types.StringValue("parent-key"), + }, + Addresses: []sharedtypes.Address{ + { + ID: types.StringValue("address-id-1"), + Key: types.StringValue("address-1"), + Country: types.StringValue("US"), + City: types.StringValue("New York"), + }, + { + ID: types.StringValue("address-id-2"), + Key: types.StringValue("address-2"), + Country: types.StringValue("US"), + City: types.StringValue("Detroit"), + }, + }, + Stores: []sharedtypes.StoreKeyReference{ + {Key: types.StringValue("store-1")}, + }, + ShippingAddressKeys: []types.String{types.StringValue("address-1"), types.StringValue("address-2")}, + BillingAddressKeys: []types.String{types.StringValue("address-1"), types.StringValue("address-2")}, + DefaultBillingAddressKey: types.StringValue("address-2"), + DefaultShippingAddressKey: types.StringValue("address-2"), + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var data platform.BusinessUnit + err := utils.DecodeStruct(c.division, &data) + assert.NoError(t, err) + + result, err := NewDivisionFromNative(&data) + assert.NoError(t, err) + assert.Equal(t, c.expected, result) + }) + } +} diff --git a/internal/resources/business_unit_division/resource.go b/internal/resources/business_unit_division/resource.go new file mode 100644 index 00000000..50e2a5d6 --- /dev/null +++ b/internal/resources/business_unit_division/resource.go @@ -0,0 +1,369 @@ +package business_unit_division + +import ( + "context" + "errors" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/labd/terraform-provider-commercetools/internal/sharedtypes" + "regexp" + "time" + + "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/listplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/labd/commercetools-go-sdk/platform" + "github.com/labd/terraform-provider-commercetools/internal/utils" +) + +var ( + _ resource.Resource = &divisionResource{} + _ resource.ResourceWithConfigure = &divisionResource{} + _ resource.ResourceWithImportState = &divisionResource{} +) + +type divisionResource struct { + client *platform.ByProjectKeyRequestBuilder +} + +// NewDivisionResource creates a new resource for the Division type. +func NewDivisionResource() resource.Resource { + return &divisionResource{} +} + +// Schema implements resource.Resource. +func (b *divisionResource) Schema(_ context.Context, req resource.SchemaRequest, res *resource.SchemaResponse) { + res.Schema = schema.Schema{ + MarkdownDescription: "Business Unit type to represent the top level of a business. Contains specific fields and values that differentiate a Division from the generic BusinessUnit.\n\n" + + "See also the [Business Unit API Documentation](https://docs.commercetools.com/api/projects/business-units", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "Unique identifier of the Division.", + Computed: true, + }, + "version": schema.Int64Attribute{ + MarkdownDescription: "The current version of the Division.", + Computed: true, + }, + "key": schema.StringAttribute{ + MarkdownDescription: "User-defined unique identifier for the Division.", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(2, 256), + stringvalidator.RegexMatches( + regexp.MustCompile("^[A-Za-z0-9_-]+$"), + "Key must match pattern ^[A-Za-z0-9_-]+$", + ), + }, + }, + "status": schema.StringAttribute{ + MarkdownDescription: "Indicates whether the Business Unit can be edited and used in [Orders](https://docs.commercetools.com/api/projects/orders). Defaults to `Active`.", + Optional: true, + Computed: true, + Default: stringdefault.StaticString(string(platform.BusinessUnitStatusActive)), + Validators: []validator.String{ + stringvalidator.OneOf( + string(platform.BusinessUnitStatusActive), + string(platform.BusinessUnitStatusInactive), + ), + }, + }, + "store_mode": schema.StringAttribute{ + MarkdownDescription: "Defines whether the Stores of the Business Unit are set directly on the Business Unit or are inherited from a parent. Defaults to `FromParent`", + Optional: true, + Computed: true, + Default: stringdefault.StaticString(string(platform.BusinessUnitStoreModeFromParent)), + Validators: []validator.String{ + stringvalidator.OneOf( + string(platform.BusinessUnitStoreModeFromParent), + string(platform.BusinessUnitStoreModeExplicit), + ), + }, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "The name of the Division.", + Required: true, + }, + "contact_email": schema.StringAttribute{ + MarkdownDescription: "The email address of the Division.", + Optional: true, + }, + "associate_mode": schema.StringAttribute{ + MarkdownDescription: "Determines whether the Business Unit can inherit Associates from a parent. Defaults to `ExplicitAndFromParent`.", + Optional: true, + Computed: true, + Default: stringdefault.StaticString(string(platform.BusinessUnitAssociateModeExplicitAndFromParent)), + Validators: []validator.String{ + stringvalidator.OneOf( + string(platform.BusinessUnitAssociateModeExplicitAndFromParent), + string(platform.BusinessUnitAssociateModeExplicit), + ), + }, + }, + "approval_rule_mode": schema.StringAttribute{ + MarkdownDescription: "Determines whether the Business Unit can inherit Approval Rules from a parent. Defaults to `ExplicitAndFromParent`.", + Optional: true, + Computed: true, + Default: stringdefault.StaticString(string(platform.BusinessUnitApprovalRuleModeExplicitAndFromParent)), + Validators: []validator.String{ + stringvalidator.OneOf( + string(platform.BusinessUnitApprovalRuleModeExplicit), + string(platform.BusinessUnitApprovalRuleModeExplicitAndFromParent), + ), + }, + }, + "shipping_address_keys": schema.ListAttribute{ + MarkdownDescription: "List of the shipping addresses used by the Division.", + Optional: true, + ElementType: types.StringType, + PlanModifiers: []planmodifier.List{ + listplanmodifier.UseStateForUnknown(), + }, + }, + "default_shipping_address_key": schema.StringAttribute{ + MarkdownDescription: "Key of the default shipping Address.", + Optional: true, + }, + "billing_address_keys": schema.ListAttribute{ + MarkdownDescription: "List of the billing addresses used by the Division.", + Optional: true, + ElementType: types.StringType, + PlanModifiers: []planmodifier.List{ + listplanmodifier.UseStateForUnknown(), + }, + }, + "default_billing_address_key": schema.StringAttribute{ + MarkdownDescription: "Key of the default billing Address.", + Optional: true, + }, + }, + Blocks: map[string]schema.Block{ + "store": sharedtypes.StoreKeyReferenceBlockSchema, + "address": sharedtypes.AddressBlockSchema, + "parent_unit": schema.SingleNestedBlock{ + MarkdownDescription: "Reference to a parent Business Unit by its key.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "User-defined unique identifier of the Business Unit", + Optional: true, + }, + "key": schema.StringAttribute{ + MarkdownDescription: "User-defined unique key of the Business Unit", + Optional: true, + }, + }, + }, + }, + } +} + +// Metadata implements resource.Resource. +func (b *divisionResource) Metadata(_ context.Context, req resource.MetadataRequest, res *resource.MetadataResponse) { + res.TypeName = req.ProviderTypeName + "_business_unit_division" +} + +// ImportState implements resource.ResourceWithImportState. +func (b *divisionResource) ImportState(ctx context.Context, req resource.ImportStateRequest, res *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, res) +} + +// Configure implements resource.ResourceWithConfigure. +func (b *divisionResource) Configure(ctx context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + data, ok := req.ProviderData.(*utils.ProviderData) + if !ok { + return + } + + b.client = data.Client +} + +// Create implements resource.Resource. +func (b *divisionResource) Create(ctx context.Context, req resource.CreateRequest, res *resource.CreateResponse) { + var plan Division + diags := req.Plan.Get(ctx, &plan) + res.Diagnostics.Append(diags...) + if res.Diagnostics.HasError() { + return + } + + draft, err := plan.draft() + if err != nil { + res.Diagnostics.AddError( + "Error creating business unit", + "Could not create business unit, unexpected error: "+err.Error(), + ) + return + + } + + var bu *platform.BusinessUnit + err = retry.RetryContext(ctx, 20*time.Second, func() *retry.RetryError { + var err error + bu, err = b.client.BusinessUnits().Post(draft).Execute(ctx) + + return utils.ProcessRemoteError(err) + }) + if err != nil { + res.Diagnostics.AddError( + "Error creating business unit", + "Could not create business unit, unexpected error: "+err.Error(), + ) + return + } + + current, err := NewDivisionFromNative(bu) + if err != nil { + res.Diagnostics.AddError( + "Error mapping business unit", + "Could not create business unit, unexpected error: "+err.Error(), + ) + return + } + + diags = res.State.Set(ctx, current) + res.Diagnostics.Append(diags...) + if res.Diagnostics.HasError() { + return + } +} + +// Delete implements resource.Resource. +func (b *divisionResource) Delete(ctx context.Context, req resource.DeleteRequest, res *resource.DeleteResponse) { + var state Division + + diags := req.State.Get(ctx, &state) + res.Diagnostics.Append(diags...) + + if res.Diagnostics.HasError() { + return + } + + err := retry.RetryContext( + ctx, + 5*time.Second, + func() *retry.RetryError { + _, err := b.client.BusinessUnits(). + WithId(state.ID.ValueString()). + Delete(). + Version(int(state.Version.ValueInt64())). + Execute(ctx) + + return utils.ProcessRemoteError(err) + }, + ) + if err != nil { + res.Diagnostics.AddError( + "Error deleting business unit", + "Could not delete business unit, unexpected error: "+err.Error(), + ) + return + } +} + +// Read implements resource.Resource. +func (b *divisionResource) Read(ctx context.Context, req resource.ReadRequest, res *resource.ReadResponse) { + var state Division + diags := req.State.Get(ctx, &state) + res.Diagnostics.Append(diags...) + if res.Diagnostics.HasError() { + return + } + + bu, err := b.client.BusinessUnits().WithId(state.ID.ValueString()).Get().Execute(ctx) + if err != nil { + if errors.Is(err, platform.ErrNotFound) { + res.State.RemoveResource(ctx) + return + } + + res.Diagnostics.AddError( + "Error reading business unit", + "Could not retrieve the business unit, unexpected error: "+err.Error(), + ) + return + } + + current, err := NewDivisionFromNative(bu) + if err != nil { + res.Diagnostics.AddError( + "Error mapping business unit", + "Could not create business unit, unexpected error: "+err.Error(), + ) + return + } + + diags = res.State.Set(ctx, current) + res.Diagnostics.Append(diags...) + if res.Diagnostics.HasError() { + return + } +} + +// Update implements resource.Resource. +func (b *divisionResource) Update(ctx context.Context, req resource.UpdateRequest, res *resource.UpdateResponse) { + var plan Division + diags := req.Plan.Get(ctx, &plan) + res.Diagnostics.Append(diags...) + if res.Diagnostics.HasError() { + return + } + + var state Division + diags = req.State.Get(ctx, &state) + res.Diagnostics.Append(diags...) + if res.Diagnostics.HasError() { + return + } + + input, err := state.updateActions(plan) + if err != nil { + res.Diagnostics.AddError( + "Error updating business unit", + "Could not update business unit, unexpected error: "+err.Error(), + ) + return + + } + var bu *platform.BusinessUnit + + err = retry.RetryContext(ctx, 5*time.Second, func() *retry.RetryError { + var err error + bu, err = b.client.BusinessUnits(). + WithId(state.ID.ValueString()). + Post(input). + Execute(ctx) + + return utils.ProcessRemoteError(err) + }) + if err != nil { + res.Diagnostics.AddError( + "Error updating business unit", + "Could not update business unit, unexpected error: "+err.Error(), + ) + return + } + + current, err := NewDivisionFromNative(bu) + if err != nil { + res.Diagnostics.AddError( + "Error mapping business unit", + "Could not create business unit, unexpected error: "+err.Error(), + ) + return + } + + diags = res.State.Set(ctx, ¤t) + res.Diagnostics.Append(diags...) + if res.Diagnostics.HasError() { + return + } +} diff --git a/internal/resources/business_unit_division/resource_test.go b/internal/resources/business_unit_division/resource_test.go new file mode 100644 index 00000000..907a871a --- /dev/null +++ b/internal/resources/business_unit_division/resource_test.go @@ -0,0 +1,132 @@ +package business_unit_division_test + +import ( + "context" + "github.com/labd/terraform-provider-commercetools/internal/resources/business_unit_division" + "testing" + + fwresource "github.com/hashicorp/terraform-plugin-framework/resource" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + + "github.com/labd/terraform-provider-commercetools/internal/acctest" + "github.com/labd/terraform-provider-commercetools/internal/utils" +) + +func TestDivisionSchemaImplementation(t *testing.T) { + t.Parallel() + + ctx := context.Background() + schemaRequest := fwresource.SchemaRequest{} + schemaResponse := &fwresource.SchemaResponse{} + + business_unit_division.NewDivisionResource().Schema(ctx, schemaRequest, schemaResponse) + + if schemaResponse.Diagnostics.HasError() { + t.Fatalf("Schema method diagnostics: %+v", schemaResponse.Diagnostics) + } + + // schema validation + diagnostics := schemaResponse.Schema.ValidateImplementation(ctx) + + if diagnostics.HasError() { + t.Fatalf("Schema validation diagnostics: %+v", diagnostics) + } +} + +func TestBusinessUnitResource_Division(t *testing.T) { + r := "commercetools_business_unit_division.acme_division" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.TestAccPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testBusinessUnitDestroy, + Steps: []resource.TestStep{ + { + Config: businessUnitTFResourceDef("", "", ""), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(r, "key", "acme-division"), + resource.TestCheckResourceAttr(r, "name", "Acme Company Business Unit"), + resource.TestCheckResourceAttr(r, "status", "Active"), + resource.TestCheckResourceAttr(r, "stores.#", "2"), + resource.TestCheckResourceAttr(r, "stores.0.key", "acme-usa"), + resource.TestCheckResourceAttr(r, "stores.1.key", "acme-germany"), + resource.TestCheckResourceAttr(r, "addresses.#", "1"), + ), + }, + { + Config: businessUnitTFResourceDef("Acme Business Unit - Updated", "Inactive", ""), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(r, "key", "acme-division"), + resource.TestCheckResourceAttr(r, "status", "Inactive"), + resource.TestCheckResourceAttr(r, "stores.#", "2"), + resource.TestCheckResourceAttr(r, "stores.0.key", "acme-usa"), + resource.TestCheckResourceAttr(r, "stores.1.key", "acme-germany"), + resource.TestCheckResourceAttr(r, "addresses.#", "1"), + ), + }, + }, + }) +} + +func testBusinessUnitDestroy(_ *terraform.State) error { + return nil +} + +func businessUnitTFResourceDef(name, status, email string) string { + if status == "" { + status = "Active" + } + + if email == "" { + email = "acme@example.com" + } + + if name == "" { + name = "Acme Company Business Unit" + } + + return utils.HCLTemplate(`resource "commercetools_business_unit_division" "acme_division" { + key = "acme-division" + name = {{ .name }} + status = {{ .status }} + contact_email = {{ .email}} + + store { + key = "acme-usa" + } + + store { + key = "acme-germany" + } + + address { + key = "acme-business-unit-address" + title = "Acme Business Unit Address" + salutation = "Mr." + first_name = "John" + last_name = "Doe" + street_name = "Main Street" + street_number = "1" + additional_street_info = "Additional Street Info" + postal_code = "12345" + city = "Berlin" + region = "Berlin" + country = "DE" + company = "Acme" + department = "IT" + building = "Building" + apartment = "Apartment" + po_box = "P.O. Box" + phone = "123456789" + mobile = "987654321" + } + default_shipping_address_id = "acme-business-unit-address" + default_billing_address_id = "acme-business-unit-address" +}`, map[string]any{ + "name": name, + "status": status, + "email": email, + }) +} diff --git a/internal/resources/product_selection/resource.go b/internal/resources/product_selection/resource.go index 82fa7599..df51478a 100644 --- a/internal/resources/product_selection/resource.go +++ b/internal/resources/product_selection/resource.go @@ -2,6 +2,7 @@ package product_selection import ( "context" + "regexp" "time" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" @@ -51,6 +52,13 @@ func (*productSelectionResource) Schema(_ context.Context, req resource.SchemaRe "key": schema.StringAttribute{ Description: "User-defined unique identifier of the ProductSelection.", Optional: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(2, 256), + stringvalidator.RegexMatches( + regexp.MustCompile("^[A-Za-z0-9_-]+$"), + "Key must match pattern ^[A-Za-z0-9_-]+$", + ), + }, }, "mode": schema.StringAttribute{ Description: "Specifies in which way the Products are assigned to the ProductSelection." + diff --git a/internal/resources/product_selection/resource_test.go b/internal/resources/product_selection/resource_test.go index 60c550c2..204f442c 100644 --- a/internal/resources/product_selection/resource_test.go +++ b/internal/resources/product_selection/resource_test.go @@ -12,7 +12,7 @@ import ( "github.com/labd/terraform-provider-commercetools/internal/utils" ) -func TestProductSelctionResource_Create(t *testing.T) { +func TestProductSelectionResource_Create(t *testing.T) { rn := "commercetools_product_selection.test_product_selection" id := "test_product_selection" diff --git a/internal/resources/project/model.go b/internal/resources/project/model.go index 9b299ca1..75d9fa8f 100644 --- a/internal/resources/project/model.go +++ b/internal/resources/project/model.go @@ -1,6 +1,7 @@ package project import ( + "fmt" "reflect" "github.com/elliotchance/pie/v2" @@ -37,6 +38,8 @@ type Project struct { ShippingRateInputType types.String `tfsdk:"shipping_rate_input_type"` ShippingRateCartClassificationValue []models.CustomFieldLocalizedEnumValue `tfsdk:"shipping_rate_cart_classification_value"` + + BusinessUnits []BusinessUnits `tfsdk:"business_units"` } func NewProjectFromNative(n *platform.Project) Project { @@ -66,6 +69,7 @@ func NewProjectFromNative(n *platform.Project) Project { }, }, ExternalOAuth: []ExternalOAuth{}, + BusinessUnits: []BusinessUnits{}, } // always set it to an empty list to avoid the wrong comparison in the update actions part @@ -112,6 +116,18 @@ func NewProjectFromNative(n *platform.Project) Project { } } + if n.BusinessUnits != nil { + var businessUnits = BusinessUnits{ + MyBusinessUnitStatusOnCreation: types.StringValue(string(n.BusinessUnits.MyBusinessUnitStatusOnCreation)), + } + + if n.BusinessUnits.MyBusinessUnitAssociateRoleOnCreation != nil { + businessUnits.MyBusinessUnitAssociateRoleKeyOnCreation = types.StringValue(n.BusinessUnits.MyBusinessUnitAssociateRoleOnCreation.Key) + } + + res.BusinessUnits = []BusinessUnits{businessUnits} + } + return res } @@ -132,22 +148,22 @@ func (p *Project) setStateData(o Project) { p.Messages[0].DeleteDaysAfterCreation = o.Messages[0].DeleteDaysAfterCreation } } - // If the state has no data for messages (0 items) and the configuration is - // the default we match the state + // If the state has no data for messages (0 items) and the configuration is the default we match the state if len(p.Messages) > 0 && p.Messages[0].isDefault() && (len(o.Messages) == 0 || o.Messages[0].isDefault()) { p.Messages = o.Messages } + + if len(o.BusinessUnits) == 0 { + p.BusinessUnits = nil + } } -func (p *Project) updateActions(plan Project) platform.ProjectUpdate { +func (p *Project) updateActions(plan Project) (platform.ProjectUpdate, error) { result := platform.ProjectUpdate{ Version: int(p.Version.ValueInt64()), Actions: []platform.ProjectUpdateAction{}, } - // changeMyBusinessUnitStatusOnCreation - // TODO - // changeCartsConfiguration if !reflect.DeepEqual(p.Carts, plan.Carts) { if len(plan.Carts) == 0 { @@ -259,9 +275,6 @@ func (p *Project) updateActions(plan Project) platform.ProjectUpdate { ) } - // changeShoppingListsConfiguration - // TODO - // setExternalOAuth if !reflect.DeepEqual(p.ExternalOAuth, plan.ExternalOAuth) { var value *platform.ExternalOAuth @@ -301,7 +314,60 @@ func (p *Project) updateActions(plan Project) platform.ProjectUpdate { ) } - return result + // changeBusinessUnitConfiguration + if !reflect.DeepEqual(p.BusinessUnits, plan.BusinessUnits) { + // If the existing business unit configuration is nil, but the plan has a configuration we apply the configuration + if len(p.BusinessUnits) == 0 && len(plan.BusinessUnits) != 0 { + result.Actions = append(result.Actions, + platform.ProjectChangeBusinessUnitStatusOnCreationAction{ + Status: platform.BusinessUnitConfigurationStatus(plan.BusinessUnits[0].MyBusinessUnitStatusOnCreation.ValueString()), + }, + ) + + //TODO: should set associate role to nil, but that is not currently supported in the SDK + if plan.BusinessUnits[0].MyBusinessUnitAssociateRoleKeyOnCreation.ValueStringPointer() != nil { + result.Actions = append(result.Actions, + platform.ProjectSetBusinessUnitAssociateRoleOnCreationAction{ + AssociateRole: platform.AssociateRoleResourceIdentifier{ + Key: plan.BusinessUnits[0].MyBusinessUnitAssociateRoleKeyOnCreation.ValueStringPointer(), + }, + }, + ) + } + } else if len(p.BusinessUnits) != 0 && len(plan.BusinessUnits) == 0 { + // If the existing business unit configuration is not nil, but the plan has no configuration we remove the configuration + result.Actions = append(result.Actions, + platform.ProjectChangeBusinessUnitStatusOnCreationAction{ + Status: platform.BusinessUnitConfigurationStatusInactive, + }, + ) + //TODO: should set associate role to nil, but that is not currently supported in the SDK + } else { + if !p.BusinessUnits[0].MyBusinessUnitStatusOnCreation.Equal(plan.BusinessUnits[0].MyBusinessUnitStatusOnCreation) { + result.Actions = append(result.Actions, + platform.ProjectChangeBusinessUnitStatusOnCreationAction{ + Status: platform.BusinessUnitConfigurationStatus(plan.BusinessUnits[0].MyBusinessUnitStatusOnCreation.ValueString()), + }, + ) + } + if !p.BusinessUnits[0].MyBusinessUnitAssociateRoleKeyOnCreation.Equal(plan.BusinessUnits[0].MyBusinessUnitAssociateRoleKeyOnCreation) { + if plan.BusinessUnits[0].MyBusinessUnitAssociateRoleKeyOnCreation.IsNull() { + return result, fmt.Errorf("AssociateRoleKeyReference cannot be set to nil after it has been assigned") + } + + result.Actions = append(result.Actions, + platform.ProjectSetBusinessUnitAssociateRoleOnCreationAction{ + AssociateRole: platform.AssociateRoleResourceIdentifier{ + Key: plan.BusinessUnits[0].MyBusinessUnitAssociateRoleKeyOnCreation.ValueStringPointer(), + }, + }, + ) + } + } + + } + + return result, nil } type Messages struct { @@ -353,3 +419,37 @@ func (c Carts) toNative() platform.CartsConfiguration { CountryTaxRateFallbackEnabled: utils.BoolRef(c.CountryTaxRateFallbackEnabled.ValueBool()), } } + +type BusinessUnits struct { + MyBusinessUnitStatusOnCreation types.String `tfsdk:"my_business_unit_status_on_creation"` + MyBusinessUnitAssociateRoleKeyOnCreation types.String `tfsdk:"my_business_unit_associate_role_key_on_creation"` +} + +func (b BusinessUnits) toNative() platform.BusinessUnitDraft { + draft := platform.BusinessUnitConfiguration{ + MyBusinessUnitStatusOnCreation: platform.BusinessUnitConfigurationStatus(b.MyBusinessUnitStatusOnCreation.ValueString()), + } + + if !b.MyBusinessUnitAssociateRoleKeyOnCreation.IsNull() { + draft.MyBusinessUnitAssociateRoleOnCreation = &platform.AssociateRoleKeyReference{ + Key: b.MyBusinessUnitAssociateRoleKeyOnCreation.ValueString(), + } + } + + return draft +} + +func (b BusinessUnits) isDefault() bool { + return b.MyBusinessUnitStatusOnCreation.ValueString() == string(platform.BusinessUnitStatusActive) && + b.MyBusinessUnitAssociateRoleKeyOnCreation.IsNull() +} + +type AssociateRoleKeyReference struct { + Key types.String `tfsdk:"key"` +} + +func (r AssociateRoleKeyReference) toNative() *platform.AssociateRoleKeyReference { + return &platform.AssociateRoleKeyReference{ + Key: r.Key.ValueString(), + } +} diff --git a/internal/resources/project/model_test.go b/internal/resources/project/model_test.go index 1532e8d0..d42b1b3d 100644 --- a/internal/resources/project/model_test.go +++ b/internal/resources/project/model_test.go @@ -49,6 +49,7 @@ func TestNewProjectFromNative(t *testing.T) { }, }, ShippingRateCartClassificationValue: []models.CustomFieldLocalizedEnumValue{}, + BusinessUnits: []BusinessUnits{}, }, }, } @@ -175,10 +176,40 @@ func TestUpdateActions(t *testing.T) { }, }, }, + { + name: "Create with business unit settings", + state: Project{ + Version: types.Int64Value(1), + BusinessUnits: []BusinessUnits{}, + }, + plan: Project{ + Version: types.Int64Value(1), + BusinessUnits: []BusinessUnits{ + { + MyBusinessUnitStatusOnCreation: types.StringValue(string(platform.BusinessUnitConfigurationStatusActive)), + MyBusinessUnitAssociateRoleKeyOnCreation: types.StringValue("my-associate-role"), + }, + }, + }, + action: platform.ProjectUpdate{ + Version: 1, + Actions: []platform.ProjectUpdateAction{ + platform.ProjectChangeBusinessUnitStatusOnCreationAction{ + Status: platform.BusinessUnitConfigurationStatusActive, + }, + platform.ProjectSetBusinessUnitAssociateRoleOnCreationAction{ + AssociateRole: platform.AssociateRoleResourceIdentifier{ + Key: utils.StringRef("my-associate-role"), + }, + }, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := tt.state.updateActions(tt.plan) + result, err := tt.state.updateActions(tt.plan) + assert.NoError(t, err) assert.Equal(t, tt.action, result) }) } @@ -246,6 +277,25 @@ func TestSetStateData(t *testing.T) { }, Carts: nil, }, + }, { + name: "business unit in plan", + state: Project{ + BusinessUnits: []BusinessUnits{ + { + MyBusinessUnitStatusOnCreation: types.StringValue(string(platform.BusinessUnitConfigurationStatusInactive)), + }, + }, + Carts: []Carts{ + {}, + }, + }, + plan: Project{ + BusinessUnits: []BusinessUnits{}, + }, + expected: Project{ + BusinessUnits: nil, + Carts: nil, + }, }, } for _, tt := range tests { diff --git a/internal/resources/project/resource.go b/internal/resources/project/resource.go index 9c5ca44f..937696e9 100644 --- a/internal/resources/project/resource.go +++ b/internal/resources/project/resource.go @@ -2,6 +2,7 @@ package project import ( "context" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" "time" "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" @@ -27,28 +28,28 @@ import ( // Ensure the implementation satisfies the expected interfaces. var ( - _ resource.Resource = &ProjectResource{} - _ resource.ResourceWithConfigure = &ProjectResource{} - _ resource.ResourceWithImportState = &ProjectResource{} + _ resource.Resource = &projectResource{} + _ resource.ResourceWithConfigure = &projectResource{} + _ resource.ResourceWithImportState = &projectResource{} ) -// NewOrderResource is a helper function to simplify the provider implementation. +// NewResource is a helper function to simplify the provider implementation. func NewResource() resource.Resource { - return &ProjectResource{} + return &projectResource{} } -// orderResource is the resource implementation. -type ProjectResource struct { +// projectResource is the resource implementation. +type projectResource struct { client *platform.ByProjectKeyRequestBuilder } // Metadata returns the data source type name. -func (r *ProjectResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { +func (r *projectResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_project_settings" } // Schema defines the schema for the data source. -func (r *ProjectResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { +func (r *projectResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ MarkdownDescription: "The project endpoint provides a limited set of information about settings and configuration of " + "the project. Updating the settings is eventually consistent, it may take up to a minute before " + @@ -58,32 +59,32 @@ func (r *ProjectResource) Schema(_ context.Context, _ resource.SchemaRequest, re Attributes: map[string]schema.Attribute{ // The ID is only here to make testing framework happy. "id": schema.StringAttribute{ - Description: "The unique key of the project", - Computed: true, + MarkdownDescription: "The unique key of the project", + Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), }, }, "key": schema.StringAttribute{ - Description: "The unique key of the project", - Computed: true, + MarkdownDescription: "The unique key of the project", + Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), }, }, "name": schema.StringAttribute{ - Description: "The name of the project", - Optional: true, - Computed: true, + MarkdownDescription: "The name of the project", + Optional: true, + Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), }, }, "currencies": schema.ListAttribute{ - ElementType: types.StringType, - Description: "A three-digit currency code as per [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217)", - Optional: true, - Computed: true, + ElementType: types.StringType, + MarkdownDescription: "A three-digit currency code as per [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217)", + Optional: true, + Computed: true, PlanModifiers: []planmodifier.List{ listplanmodifier.UseStateForUnknown(), }, @@ -92,10 +93,10 @@ func (r *ProjectResource) Schema(_ context.Context, _ resource.SchemaRequest, re }, }, "countries": schema.ListAttribute{ - ElementType: types.StringType, - Description: "A two-digit country code as per [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)", - Optional: true, - Computed: true, + ElementType: types.StringType, + MarkdownDescription: "A two-digit country code as per [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)", + Optional: true, + Computed: true, PlanModifiers: []planmodifier.List{ listplanmodifier.UseStateForUnknown(), }, @@ -113,23 +114,23 @@ func (r *ProjectResource) Schema(_ context.Context, _ resource.SchemaRequest, re }, }, "enable_search_index_products": schema.BoolAttribute{ - Description: "Enable the Search Indexing of products", - Optional: true, - Computed: true, + MarkdownDescription: "Enable the Search Indexing of products", + Optional: true, + Computed: true, PlanModifiers: []planmodifier.Bool{ boolplanmodifier.UseStateForUnknown(), }, }, "enable_search_index_orders": schema.BoolAttribute{ - Description: "Enable the Search Indexing of orders", - Optional: true, - Computed: true, + MarkdownDescription: "Enable the Search Indexing of orders", + Optional: true, + Computed: true, PlanModifiers: []planmodifier.Bool{ boolplanmodifier.UseStateForUnknown(), }, }, "shipping_rate_input_type": schema.StringAttribute{ - Description: "Three ways to dynamically select a ShippingRatePriceTier exist. The CartValue type uses " + + MarkdownDescription: "Three ways to dynamically select a ShippingRatePriceTier exist. The CartValue type uses " + "the sum of all line item prices, whereas CartClassification and CartScore use the " + "shippingRateInput field on the cart to select a tier", Optional: true, @@ -155,13 +156,13 @@ func (r *ProjectResource) Schema(_ context.Context, _ resource.SchemaRequest, re NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ "country_tax_rate_fallback_enabled": schema.BoolAttribute{ - Description: "Indicates if country - no state tax rate fallback should be used when a " + + MarkdownDescription: "Indicates if country - no state tax rate fallback should be used when a " + "shipping address state is not explicitly covered in the rates lists of all tax " + "categories of a cart line items", Optional: true, }, "delete_days_after_last_modification": schema.Int64Attribute{ - Description: "Number - Optional The default value for the " + + MarkdownDescription: "Number - Optional The default value for the " + "deleteDaysAfterLastModification parameter of the CartDraft. Initially set to 90 for " + "projects created after December 2019.", Optional: true, @@ -173,19 +174,19 @@ func (r *ProjectResource) Schema(_ context.Context, _ resource.SchemaRequest, re }, }, "messages": schema.ListNestedBlock{ - Description: "The change notifications subscribed to", + MarkdownDescription: "The change notifications subscribed to", NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ "enabled": schema.BoolAttribute{ - Description: "When true the creation of messages on the Messages Query HTTP API is enabled", - Optional: true, + MarkdownDescription: "When true the creation of messages on the Messages Query HTTP API is enabled", + Optional: true, PlanModifiers: []planmodifier.Bool{ boolplanmodifier.UseStateForUnknown(), }, }, "delete_days_after_creation": schema.Int64Attribute{ - Description: "Specifies the number of days each Message should be available via the Messages Query API", - Optional: true, + MarkdownDescription: "Specifies the number of days each Message should be available via the Messages Query API", + Optional: true, PlanModifiers: []planmodifier.Int64{ int64planmodifier.UseStateForUnknown(), }, @@ -213,8 +214,8 @@ func (r *ProjectResource) Schema(_ context.Context, _ resource.SchemaRequest, re }, }, "authorization_header": schema.StringAttribute{ - Description: "Partially hidden on retrieval", - Optional: true, + MarkdownDescription: "Partially hidden on retrieval", + Optional: true, Validators: []validator.String{ stringvalidator.AlsoRequires( path.MatchRelative().AtParent().AtName("url"), @@ -228,11 +229,12 @@ func (r *ProjectResource) Schema(_ context.Context, _ resource.SchemaRequest, re }, }, "shipping_rate_cart_classification_value": schema.ListNestedBlock{ - Description: "If shipping_rate_input_type is set to CartClassification these values are used to create " + + MarkdownDescription: "If shipping_rate_input_type is set to CartClassification these values are used to create " + "tiers\n. Only a key defined inside the values array can be used to create a tier, or to set a value " + "for the shippingRateInput on the cart. The keys are checked for uniqueness and the request is " + "rejected if keys are not unique", Validators: []validator.List{ + listvalidator.SizeAtMost(1), customvalidator.RequireValueValidator( "CartClassification", path.MatchRoot("shipping_rate_input_type"), @@ -250,12 +252,40 @@ func (r *ProjectResource) Schema(_ context.Context, _ resource.SchemaRequest, re }, }, }, + "business_units": schema.ListNestedBlock{ + MarkdownDescription: "Holds configuration specific to [Business Units](https://docs.commercetools.com/api/projects/business-units#ctp:api:type:BusinessUnit).", + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "my_business_unit_status_on_creation": schema.StringAttribute{ + MarkdownDescription: "Status of Business Units created using the My Business Unit endpoint.", + Computed: true, + Optional: true, + Default: stringdefault.StaticString(string(platform.BusinessUnitConfigurationStatusInactive)), + Validators: []validator.String{ + stringvalidator.OneOf( + string(platform.BusinessUnitConfigurationStatusActive), + string(platform.BusinessUnitConfigurationStatusInactive), + ), + }, + }, + "my_business_unit_associate_role_key_on_creation": schema.StringAttribute{ + MarkdownDescription: "Default Associate Role assigned to the Associate creating a " + + "Business Unit using the My Business Unit endpoint. Note that this field cannot be " + + "unset once assigned!", + Optional: true, + }, + }, + }, + }, }, } } // Configure adds the provider configured client to the data source. -func (r *ProjectResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { +func (r *projectResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { if req.ProviderData == nil { return } @@ -263,7 +293,7 @@ func (r *ProjectResource) Configure(_ context.Context, req resource.ConfigureReq r.client = data.Client } -func (p *ProjectResource) UpgradeState(ctx context.Context) map[int64]resource.StateUpgrader { +func (r *projectResource) UpgradeState(ctx context.Context) map[int64]resource.StateUpgrader { return map[int64]resource.StateUpgrader{ 0: { StateUpgrader: upgradeStateV0, @@ -272,7 +302,7 @@ func (p *ProjectResource) UpgradeState(ctx context.Context) map[int64]resource.S } // Create creates the resource and sets the initial Terraform state. -func (r *ProjectResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { +func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // Retrieve values from plan var plan Project diags := req.Plan.Get(ctx, &plan) @@ -291,7 +321,12 @@ func (r *ProjectResource) Create(ctx context.Context, req resource.CreateRequest } current := NewProjectFromNative(project) - input := current.updateActions(plan) + input, err := current.updateActions(plan) + if err != nil { + resp.Diagnostics.AddError("Error updating project", err.Error()) + return + } + var res *platform.Project err = sdk_resource.RetryContext(ctx, 5*time.Second, func() *sdk_resource.RetryError { var err error @@ -315,7 +350,7 @@ func (r *ProjectResource) Create(ctx context.Context, req resource.CreateRequest } // Read refreshes the Terraform state with the latest data. -func (r *ProjectResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { +func (r *projectResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // Get current state var state Project diags := req.State.Get(ctx, &state) @@ -344,7 +379,7 @@ func (r *ProjectResource) Read(ctx context.Context, req resource.ReadRequest, re } // Update updates the resource and sets the updated Terraform state on success. -func (r *ProjectResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { +func (r *projectResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // Retrieve values from plan var plan Project diags := req.Plan.Get(ctx, &plan) @@ -361,10 +396,14 @@ func (r *ProjectResource) Update(ctx context.Context, req resource.UpdateRequest return } - input := state.updateActions(plan) + input, err := state.updateActions(plan) + if err != nil { + resp.Diagnostics.AddError("Error updating project", err.Error()) + return + } var res *platform.Project - err := sdk_resource.RetryContext(ctx, 5*time.Second, func() *sdk_resource.RetryError { + err = sdk_resource.RetryContext(ctx, 5*time.Second, func() *sdk_resource.RetryError { var err error res, err = r.client.Post(input).Execute(ctx) return utils.ProcessRemoteError(err) @@ -384,7 +423,7 @@ func (r *ProjectResource) Update(ctx context.Context, req resource.UpdateRequest } // Delete deletes the resource and removes the Terraform state on success. -func (r *ProjectResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { +func (r *projectResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // Retrieve values from state var state Project diags := req.State.Get(ctx, &state) @@ -395,7 +434,7 @@ func (r *ProjectResource) Delete(ctx context.Context, req resource.DeleteRequest } -func (r *ProjectResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { +func (r *projectResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { // Retrieve import ID and save to id attribute resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) } diff --git a/internal/sharedtypes/address.go b/internal/sharedtypes/address.go new file mode 100644 index 00000000..ce85ed76 --- /dev/null +++ b/internal/sharedtypes/address.go @@ -0,0 +1,301 @@ +package sharedtypes + +import ( + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/labd/commercetools-go-sdk/platform" + "slices" +) + +var ( + AddressBlockSchema = schema.ListNestedBlock{ + MarkdownDescription: "Addresses used by the Business Unit.", + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "Unique identifier of the Address", + Computed: true, + }, + "key": schema.StringAttribute{ + MarkdownDescription: "User-defined identifier of the Address that must be unique when multiple " + + "addresses are referenced in BusinessUnits, Customers, and itemShippingAddresses " + + "(LineItem-specific addresses) of a Cart, Order, QuoteRequest, or Quote.", + Required: true, + }, + "external_id": schema.StringAttribute{ + MarkdownDescription: "ID for the contact used in an external system", + Optional: true, + }, + "country": schema.StringAttribute{ + MarkdownDescription: "Name of the country", + Required: true, + }, + "title": schema.StringAttribute{ + MarkdownDescription: "Title of the contact, for example Dr., Prof.", + Optional: true, + }, + "salutation": schema.StringAttribute{ + MarkdownDescription: "Salutation of the contact, for example Ms., Mr.", + Optional: true, + }, + "first_name": schema.StringAttribute{ + MarkdownDescription: "First name of the contact", + Optional: true, + }, + "last_name": schema.StringAttribute{ + MarkdownDescription: "Last name of the contact", + Optional: true, + }, + "street_name": schema.StringAttribute{ + MarkdownDescription: "Name of the street", + Optional: true, + }, + "street_number": schema.StringAttribute{ + MarkdownDescription: "Street number", + Optional: true, + }, + "additional_street_info": schema.StringAttribute{ + MarkdownDescription: "Further information on the street address", + Optional: true, + }, + "postal_code": schema.StringAttribute{ + MarkdownDescription: "Postal code", + Optional: true, + }, + "city": schema.StringAttribute{ + MarkdownDescription: "Name of the city", + Optional: true, + }, + "region": schema.StringAttribute{ + MarkdownDescription: "Name of the region", + Optional: true, + }, + "state": schema.StringAttribute{ + MarkdownDescription: "Name of the state", + Optional: true, + }, + "company": schema.StringAttribute{ + MarkdownDescription: "Name of the company", + Optional: true, + }, + "department": schema.StringAttribute{ + MarkdownDescription: "Name of the department", + Optional: true, + }, + "building": schema.StringAttribute{ + MarkdownDescription: "Name or number of the building", + Optional: true, + }, + "apartment": schema.StringAttribute{ + MarkdownDescription: "Name or number of the apartment", + Optional: true, + }, + "po_box": schema.StringAttribute{ + MarkdownDescription: "Post office box number", + Optional: true, + }, + "phone": schema.StringAttribute{ + MarkdownDescription: "Phone number", + Optional: true, + }, + "mobile": schema.StringAttribute{ + MarkdownDescription: "Mobile phone number", + Optional: true, + }, + "email": schema.StringAttribute{ + MarkdownDescription: "Email address", + Optional: true, + }, + "fax": schema.StringAttribute{ + MarkdownDescription: "Fax number", + Optional: true, + }, + "additional_address_info": schema.StringAttribute{ + MarkdownDescription: "Further information on the Address", + Optional: true, + }, + }, + }, + } +) + +/* + Support types for the business unit resource. +*/ + +type Address struct { + ID types.String `tfsdk:"id"` + Key types.String `tfsdk:"key"` + ExternalID types.String `tfsdk:"external_id"` + Country types.String `tfsdk:"country"` + Title types.String `tfsdk:"title"` + Salutation types.String `tfsdk:"salutation"` + FirstName types.String `tfsdk:"first_name"` + LastName types.String `tfsdk:"last_name"` + StreetName types.String `tfsdk:"street_name"` + StreetNumber types.String `tfsdk:"street_number"` + AdditionalStreetInfo types.String `tfsdk:"additional_street_info"` + PostalCode types.String `tfsdk:"postal_code"` + City types.String `tfsdk:"city"` + Region types.String `tfsdk:"region"` + State types.String `tfsdk:"state"` + Company types.String `tfsdk:"company"` + Department types.String `tfsdk:"department"` + Building types.String `tfsdk:"building"` + Apartment types.String `tfsdk:"apartment"` + POBox types.String `tfsdk:"po_box"` + Phone types.String `tfsdk:"phone"` + Mobile types.String `tfsdk:"mobile"` + Email types.String `tfsdk:"email"` + Fax types.String `tfsdk:"fax"` + AdditionalAddressInfo types.String `tfsdk:"additional_address_info"` +} + +func (a Address) Equal(other Address) bool { + return a.Key.Equal(other.Key) && + a.ExternalID.Equal(other.ExternalID) && + a.Country.Equal(other.Country) && + a.Title.Equal(other.Title) && + a.Salutation.Equal(other.Salutation) && + a.FirstName.Equal(other.FirstName) && + a.LastName.Equal(other.LastName) && + a.StreetName.Equal(other.StreetName) && + a.StreetNumber.Equal(other.StreetNumber) && + a.AdditionalStreetInfo.Equal(other.AdditionalStreetInfo) && + a.PostalCode.Equal(other.PostalCode) && + a.City.Equal(other.City) && + a.Region.Equal(other.Region) && + a.State.Equal(other.State) && + a.Company.Equal(other.Company) && + a.Department.Equal(other.Department) && + a.Building.Equal(other.Building) && + a.Apartment.Equal(other.Apartment) && + a.POBox.Equal(other.POBox) && + a.Phone.Equal(other.Phone) && + a.Mobile.Equal(other.Mobile) && + a.Email.Equal(other.Email) && + a.Fax.Equal(other.Fax) && + a.AdditionalAddressInfo.Equal(other.AdditionalAddressInfo) +} + +func (a Address) Draft() platform.BaseAddress { + return platform.BaseAddress{ + Key: a.Key.ValueStringPointer(), + ExternalId: a.ExternalID.ValueStringPointer(), + Country: a.Country.ValueString(), + Title: a.Title.ValueStringPointer(), + Salutation: a.Salutation.ValueStringPointer(), + FirstName: a.FirstName.ValueStringPointer(), + LastName: a.LastName.ValueStringPointer(), + StreetName: a.StreetName.ValueStringPointer(), + StreetNumber: a.StreetNumber.ValueStringPointer(), + AdditionalStreetInfo: a.AdditionalStreetInfo.ValueStringPointer(), + PostalCode: a.PostalCode.ValueStringPointer(), + City: a.City.ValueStringPointer(), + Region: a.Region.ValueStringPointer(), + State: a.State.ValueStringPointer(), + Company: a.Company.ValueStringPointer(), + Department: a.Department.ValueStringPointer(), + Building: a.Building.ValueStringPointer(), + Apartment: a.Apartment.ValueStringPointer(), + POBox: a.POBox.ValueStringPointer(), + Phone: a.Phone.ValueStringPointer(), + Mobile: a.Mobile.ValueStringPointer(), + Email: a.Email.ValueStringPointer(), + Fax: a.Fax.ValueStringPointer(), + AdditionalAddressInfo: a.AdditionalAddressInfo.ValueStringPointer(), + } +} + +// NewAddressFromNative creates a new Address from a platform.Address. +func NewAddressFromNative(a *platform.Address) Address { + return Address{ + ID: types.StringPointerValue(a.ID), + Key: types.StringPointerValue(a.Key), + ExternalID: types.StringPointerValue(a.ExternalId), + Country: types.StringValue(a.Country), + Title: types.StringPointerValue(a.Title), + Salutation: types.StringPointerValue(a.Salutation), + FirstName: types.StringPointerValue(a.FirstName), + LastName: types.StringPointerValue(a.LastName), + StreetName: types.StringPointerValue(a.StreetName), + StreetNumber: types.StringPointerValue(a.StreetNumber), + AdditionalStreetInfo: types.StringPointerValue(a.AdditionalStreetInfo), + PostalCode: types.StringPointerValue(a.PostalCode), + City: types.StringPointerValue(a.City), + Region: types.StringPointerValue(a.Region), + State: types.StringPointerValue(a.State), + Company: types.StringPointerValue(a.Company), + Department: types.StringPointerValue(a.Department), + Building: types.StringPointerValue(a.Building), + Apartment: types.StringPointerValue(a.Apartment), + POBox: types.StringPointerValue(a.POBox), + Phone: types.StringPointerValue(a.Phone), + Mobile: types.StringPointerValue(a.Mobile), + Email: types.StringPointerValue(a.Email), + Fax: types.StringPointerValue(a.Fax), + AdditionalAddressInfo: types.StringPointerValue(a.AdditionalAddressInfo), + } +} + +type DeleteAddressAction interface { + platform.BusinessUnitRemoveAddressAction +} + +type AddAddressAction interface { + platform.BusinessUnitAddAddressAction +} + +type ChangeAddressAction interface { + platform.BusinessUnitChangeAddressAction +} + +func AddressesAddActions[A AddAddressAction](currentAddresses []Address, plannedAddresses []Address) []any { + var actions []any + + for _, pa := range plannedAddresses { + if !slices.ContainsFunc(currentAddresses, func(ca Address) bool { return pa.Key.Equal(ca.Key) }) { + actions = append(actions, A{ + Address: pa.Draft(), + }) + } + } + + return actions +} + +func AddressesDeleteActions[D DeleteAddressAction](currentAddresses []Address, plannedAddresses []Address) []any { + var actions []any + + for _, ca := range currentAddresses { + if !slices.ContainsFunc(plannedAddresses, func(pa Address) bool { return ca.Key.Equal(pa.Key) }) { + actions = append(actions, D{ + AddressKey: ca.Key.ValueStringPointer(), + }) + } + } + + return actions +} + +func AddressesChangeActions[C ChangeAddressAction](currentAddresses []Address, plannedAddresses []Address) []any { + var actions []any + + for _, ca := range currentAddresses { + pai := slices.IndexFunc(plannedAddresses, func(pa Address) bool { return ca.Key.Equal(pa.Key) }) + if pai == -1 { + continue + } + + pa := plannedAddresses[pai] + + if !ca.Equal(pa) { + actions = append(actions, C{ + AddressKey: pa.Key.ValueStringPointer(), + Address: pa.Draft(), + }) + } + } + + return actions +} diff --git a/internal/sharedtypes/store_key_reference.go b/internal/sharedtypes/store_key_reference.go new file mode 100644 index 00000000..8eba99c3 --- /dev/null +++ b/internal/sharedtypes/store_key_reference.go @@ -0,0 +1,35 @@ +package sharedtypes + +import ( + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/labd/commercetools-go-sdk/platform" +) + +var ( + StoreKeyReferenceBlockSchema = schema.ListNestedBlock{ + MarkdownDescription: "Sets the Stores the Business Unit is associated with. \n\nIf the Business Unit has Stores defined, " + + "then all of its Carts, Orders, Quotes, or Quote Requests must belong to one of the Business Unit's " + + "Stores.\n\nIf the Business Unit has no Stores, then all of its Carts, Orders, Quotes, or Quote Requests " + + "must not belong to any Store.", + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "key": schema.StringAttribute{ + MarkdownDescription: "User-defined unique identifier of the Store", + Optional: true, + }, + }, + }, + } +) + +// StoreKeyReference is a type to model the fields that all types of StoreKeyReference have in common. +type StoreKeyReference struct { + Key types.String `tfsdk:"key"` +} + +func NewStoreKeyReferenceFromNative(n *platform.StoreKeyReference) StoreKeyReference { + return StoreKeyReference{ + Key: types.StringValue(n.Key), + } +} diff --git a/internal/utils/decode.go b/internal/utils/decode.go new file mode 100644 index 00000000..db403cac --- /dev/null +++ b/internal/utils/decode.go @@ -0,0 +1,63 @@ +package utils + +import ( + "github.com/mitchellh/mapstructure" + "reflect" + "time" +) + +func toTimeHookFunc() mapstructure.DecodeHookFunc { + return func( + f reflect.Type, + t reflect.Type, + data interface{}) (interface{}, error) { + if t != reflect.TypeOf(time.Time{}) { + return data, nil + } + + switch f.Kind() { + case reflect.String: + return time.Parse(time.RFC3339, data.(string)) + case reflect.Float64: + return time.Unix(0, int64(data.(float64))*int64(time.Millisecond)), nil + case reflect.Int64: + return time.Unix(0, data.(int64)*int64(time.Millisecond)), nil + default: + return data, nil + } + // Convert it by parsing + } +} + +func DecodeStruct(input interface{}, result interface{}) error { + meta := &mapstructure.Metadata{} + decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + Metadata: meta, + DecodeHook: mapstructure.ComposeDecodeHookFunc( + toTimeHookFunc()), + Result: result, + }) + if err != nil { + return err + } + + if err := decoder.Decode(input); err != nil { + return err + } + + if val, ok := result.(Decoder); ok { + if raw, ok := input.(map[string]interface{}); ok { + unused := make(map[string]interface{}) + for _, key := range meta.Unused { + unused[key] = raw[key] + } + val.DecodeStruct(unused) + } + } + + return err +} + +type Decoder interface { + DecodeStruct(map[string]interface{}) error +} diff --git a/internal/utils/fields.go b/internal/utils/fields.go index 236772b1..9f73dd97 100644 --- a/internal/utils/fields.go +++ b/internal/utils/fields.go @@ -81,3 +81,7 @@ func BoolRef(value any) *bool { result := value.(bool) return &result } + +func Ref[T comparable](value T) *T { + return &value +}