# Apply Stores and Channels ## Zen Electron’s multi-brand digital commerce requirements This document outlines the requirements for the digital commerce platforms of Zen Electron's two brands: Electronics High Tech and Zenith Living. | Category | Electronics High Tech | Zenith Living | | :--- | :--- | :--- | | **Brands & Structure** | Operates in Australia and New Zealand with separate online stores but shared product information | Operates solely in Australia with a single online store. Shares some products with Electronics High Tech. | | **Website Requirements** | Single domain: `electronicstech.com.au `Locale Identifiers: Australia: `/en-AU` New Zealand: `/en-NZ` | Single domain: `zenithliving.com.au `Locale: `en-AU` (implied) | | **Pricing** | Separate pricing structure Currencies: Australia: AUD New Zealand: NZD | Separate pricing structure Currency: AUD | | **Locales** | Supports localized EN and ZH for respective countries | Supports localized EN for Australia | | **Data Management & Isolation** | Customer data shared across AU and NZ but isolated from Zenith Living | Customer data isolated from Electronics High Tech | | **Category Structure** | Distinct category tree | Distinct category tree | | **Product Management** | Shared Products: Centrally managed for shared products between AU & NZ to ensure consistency Unique Products: Some products are unique to Electronics High Tech All products require tailored, premium-tone product descriptions | Shared Products: Centrally managed for shared products between brands Unique Products: Some products are unique to Zenith Living with premium descriptions All products require tailored, premium-tone product descriptions. | | **Shipping & Fulfillment** | Shipping Options: Click & Collect, Standard Shipping with regional free thresholds, Same Day Delivery (Australia only) | Shipping Options: Click & Collect, Standard Shipping with regional free thresholds, Same Day Delivery (Australia only) Exclusive: Premium Platinum Delivery | | **Customer Segmentation & Accounts** | Shared customer accounts across AU & NZ regions; isolated from Zenith Living | Customer accounts isolated from Electronics High Tech | | **Inventory Management** | Each website maintains its own inventory pool for Standard Shipping; retail stores also function as warehouses | Each website maintains inventory for Standard Shipping; uses same pool for Premium Platinum Delivery | | **Merchant Center Access** | Staff have exclusive access to manage own customer data Can manage product info and availability for Electronics High Tech | Staff have exclusive access to manage own customer data Can manage product info and availability for Zenith Living | | **Retail Store Locations** | Australia: 200 stores (WA: 21, SA: 12, NT: 2, VIC: 52, NSW: 60, QLD: 40, ACT: 5) New Zealand: 20 stores | Australia: 100 stores New Zealand: 0 stores | Let's now look at how we can solve Zen Electron's requirements, and how Stores and Channels can be implemented. ## Project settings The following requirements can be solved using [Project settings](/merchant-center/project-settings.md): | Category | Electronics High Tech | Zenith Living | | :--- | :--- | :--- | | **Brands & Structure** | Operates in Australia and New Zealand with separate online stores but shared product information | Operates solely in Australia with a single online store. Shares some products with Electronics High Tech. | | **Website Requirements** | Single domain: `electronicstech.com.au `Locale Identifiers: Australia: `en-AU` New Zealand: `en-NZ` | Single domain: `zenithliving.com.au `Locale: `en-AU` (implied) | | **Pricing** | Separate pricing structure Currencies: Australia: AUD New Zealand: NZD | Separate pricing structure Currency: AUD | | **Locales** | Supports localized EN and ZH for respective countries | Supports localized EN for Australia | We will add the following settings to the Project, via the [Project Settings](/merchant-center/project-settings.md#manage-internationalization): | Countries | Currencies | Languages | | --- | --- | --- | | AU, NZ | AUD, NZD | zh, en-AU, en-NZ | ## Categories The following requirements can be solved using [Categories](/api/projects/categories.md): | Category | Electronics High Tech | Zenith Living | | :--- | :--- | :--- | | **Category Structure** | Distinct category tree | Distinct category tree | Here are examples of what kind of Category tree each brand requires: **Electronics High Tech** - Computers & Tablets - Computers - Components - iPads, Surface & Tablets - Networking - Tech Services - Computer Accessories - Gaming **Zenith Living** - Computers - Shop by brand - Monitors - Software - Home internet & Wi-Fi - Printers & Ink - Hard drives & data storage - Office furniture - Cameras We can see some similarities in the taxonomy, but ultimately they are not the same and each brand is not willing to adopt the other brands taxonomy. To support the requirements, we will create two root-level Categories, one for each brand. The root-level Category for each brand will be the name of each respective brand, and then the Category tree will be created underneath it. The only limitation of this solution is that duplicate URL slugs per locale are illegal. Zen Electron can accept having slightly different URL slugs for duplicate names, whilst keeping the same category name, so we can continue with this option. For example, ‘computers’ is a duplicate and we will need to change one of its slugs to ‘‘computers’ or some other variation. The Categories will look like the example response below when querying Categories (we have only added `en-AU` localisation values for simplicity). When you are working with the Categories, you will be able to determine which root they belong to by looking at the top-level Category in the hierarchy; specifically, the parent of the brand's Category tree. When your application needs to understand this is best done by holding Category tree IDs. Category affiliation is determined by the root-level category. For efficient application logic, storing each branch of the Category with the IDs is recommended. This is because when querying for Products, the default response will return the Category ID without any hierarchy hints. Note that GraphQL will allow you to explicitly request detailed Category information including the ancestors. If we want to be able to retrieve the Categories belonging to a specific brand, we would add a Custom Field to the Category and use this to determine which Category belongs to which brand. Then we can query the Categories and pass this field in the query parameter to ensure that the response only returns Categories related to the brand that we want, which will give us the information needed to build a hierarchical Category menu for each brand. When assigning Products to Categories, we will know which Category belongs to which brand by the Category path or the custom type of the Category. ![Recognizing which brand a Category belongs to by reading the Category path.](https://docs.commercetools.com/learning-model-your-business-structureimages/stores-and-channels/add-categories.png) ## Product management We have the following requirements concerning the product catalog to solve: | Category | Electronics High Tech | Zenith Living | | :--- | :--- | :--- | | **Product Management** | Shared Products: Centrally managed for shared products between AU & NZ to ensure consistency Unique Products: Some products are unique to Electronics High Tech All products require tailored, premium-tone product descriptions | Shared Products: Centrally managed for shared products between brands Unique Products: Some products are unique to Zenith Living with premium descriptions All products require tailored, premium-tone product descriptions. | ### Product Types For all of the [Product Types](/api/projects/productTypes.md), the attributes that need to have localized values, we will set them to use [LocalizedString](/api/types.md#localizedstring) on the attribute. The LocalizedString will use ZH, en-AU and en-NZ. This will allow us to have localized Product data for each local and each website facing attribute. ### Product Tailoring To support having different Product descriptions for Zenith Living, we will need to use [Product Tailoring](/api/projects/product-tailoring.md). This will require us to create a Store for Zenith Living and then we can specifically tailor the Products that belong to Zenith Living. The BFF will use the [Product Projection in Store](/urn?urn=ctp%3Aapi%3Aendpoint%3A%2F%7BprojectKey%7D%2Fin-store%2Fkey%3D%7BstoreKey%7D%2Fproduct-projections%2F%7Bid%7D%3AGET) endpoint with projection query parameters to ensure that each brand is able to view the correct version of the Product data and only the expected locales. Product Search does not support querying on the tailored view, only the default values. ### Product Selections We'll use [Product Selections](/api/projects/product-selections.md) to customize product catalogs for each brand. First, we'll create a Product Selection containing all shared products. Then, we'll create individual Product Selections for each brand, containing their respective exclusive products. This will use Product Selection Type inclusion. Note: It’s important to give the Product Selections a good name, so that you know what they contain, just by reading the name. ![Three product selections created in Merchant Center.](https://docs.commercetools.com/learning-model-your-business-structureimages/stores-and-channels/product-selection.png) Next we will create a Store for each brand, with the following settings: - Assign the appropriate Product Selections to each brand - Assign the required languages for each store - Zenith Living: en-NZ - Electronics High Tech; ZH, en-AU, en-NZ - Countries - Set a Store key (this will be important for Store specific shipping rates) The Zen Electron Store should look similar to this: ```json { "id": "fbb33836-d683-46b2-98aa-fe00c241ba8d", "version": 1, "createdAt": "2024-10-07T05:14:23.851Z", "lastModifiedAt": "2024-10-07T05:14:23.851Z", "lastModifiedBy": { "isPlatformClient": true, "user": { "typeId": "user", "id": "c950c084-f5d0-4a42-8761-229d5c7a8598" } }, "createdBy": { "isPlatformClient": true, "user": { "typeId": "user", "id": "c950c084-f5d0-4a42-8761-229d5c7a8598" } }, "key": "zenith-living", "name": { "en-AU": "Zenith Living" }, "languages": ["en-AU"], "distributionChannels": [], "supplyChannels": [], "productSelections": [ { "productSelection": { "typeId": "product-selection", "id": "7665f708-aede-4718-bf22-9ca696752f9e" }, "active": true }, { "productSelection": { "typeId": "product-selection", "id": "0d3b40b0-e317-4f42-bcaa-8259adcb0100" }, "active": true } ], "countries": [ { "code": "AU" } ] } ``` Now we have all the settings needed to allow us to have a different catalog for each brand. Lets see how this would look like as a diagram: ```mermaid graph RL subgraph "commercetools Project" Project["Products Data

(Global product catalog)"] subgraph Stores StoreA["Zenith Living"] StoreB["Electronics & Tech"] end ProductSelectionA["Product Selection A

(Subset of products - Inclusion)"] ProductSelectionB["Product Selection B

(Subset of products - Inclusion)"] ProductSelectionC["Product Selection C

(Subset of products - Inclusion)"] end CustomerA["API Consumer: Zenith"] CustomerB["API Consumer: Electronics & Tech"] StoreA --> ProductSelectionA StoreA --> ProductSelectionC StoreB --> ProductSelectionB StoreB --> ProductSelectionC ProductSelectionA --> Project ProductSelectionB --> Project ProductSelectionC --> Project CustomerA --> StoreA CustomerB --> StoreB ``` ### Alternative Product Selection strategies The previous approach used inclusion-based Product Selections to define each brand's catalog. Alternatively, we can achieve the same result using exclusion-based Product Selections. Instead of defining what each brand sells, we define what each brand excludes. We create a Product Selection for each brand, populating it with the Products that the brand should not offer. These exclusion-based selections are then assigned to the corresponding brand's Store. For example, let's say we have a "shared products" selection and brand-specific products for "Zen Electron" and "Electronics High Tech". - **Zenith Living (Exclusion):** This Product Selection would contain all the products except those specific to Zen Electron and the shared products. - **Electronics High Tech (Exclusion):** This Product Selection would contain all products except those specific to Electronics High Tech and the shared products. This approach simplifies management when dealing with a large shared catalog and smaller brand-specific exclusions. Adding a new shared Product doesn't require updating every brand's selection. Only when a brand needs to exclude a specific Product would their selection need modification. ```mermaid graph RL subgraph "commercetools Project" Project["Products Data
(Global product catalog)"] subgraph Stores StoreA["Zenith Living"] StoreB["Electronics & Tech"] end ProductSelectionA["exclude-zenith-living-products
(Products *not* sold by Zenith Living)"] ProductSelectionB["exclude-electronics-tech-products
(Products *not* sold by Electronics & Tech)"] end CustomerA["API Consumer: Zenith"] CustomerB["API Consumer: Electronics & Tech"] StoreA --> ProductSelectionA StoreB --> ProductSelectionB ProductSelectionA --> Project ProductSelectionB --> Project CustomerA --> StoreA CustomerB --> StoreB ``` ### Comparison of inclusion and exclusion approaches | Feature | Inclusion (Individual) | Exclusion (IndividualExclusion) | | :--- | :--- | :--- | | Use case | Suitable when dealing with smaller, curated assortments or when the list of desired products is easier to manage. | Suitable for large catalogs where excluding a small subset of products is simpler than including a large number. | | Store availability | A new published Product that does not belong to any Selections, will not be available to any Stores. | A new published Product that does not belong to any Selections, will be available to any Stores. | Choosing the right approach depends on the specific needs and characteristics of the product catalog and the number of brands involved. Consider the anticipated growth of the product catalog and the frequency of adding new products when making this decision. For catalogs with a large shared component and frequent additions, the exclusion method may be more efficient. This approach still requires careful naming conventions for Product Selections. For example, prefixes like "exclude-" or suffixes like "-not-sold" indicate the selection's purpose (for example, "exclude-zen-electron-products" or "electronics-high-tech-not-sold"). This improves readability and reduces confusion. The same Store configuration demonstrated earlier applies, with the only difference being the logic behind the assigned Product Selections. ## Shipping The following requirements can be solved using [Shipping Methods](/api/projects/shippingMethods.md): | Category | Electronics High Tech | Zenith Living | | :--- | :--- | :--- | | **Shipping & Fulfillment** | Shipping Options: Click & Collect, Standard Shipping with regional free thresholds, Same Day Delivery (Australia only) | Shipping Options: Click & Collect, Standard Shipping with regional free thresholds, Same Day Delivery (Australia only) Exclusive: Premium Platinum Delivery | The brands share some Shipping Methods in common, whilst each has the right to set their own shipping Rates. To support this, we will use different Shipping Methods for each brand and use predicates to specify which brand they belong to. The predicate [Cart field identifier](/api/projects/predicates.md#cart-field-identifiers) that we need to use here is `store.key`. We can use this field by itself or with other predicate fields. Below is an example of what the predicate should look like: `"predicate": "store.key = \"zenith-living\""` For this to work correctly, the Cart must have the `store` value set. This will happen under any of the following circumstances: - You used [Create Cart in Store](/urn?urn=ctp%3Aapi%3Aendpoint%3A%2F%7BprojectKey%7D%2Fin-store%2Fkey%3D%7BstoreKey%7D%2Fcarts%3APOST) to create the Cart. - You set the `store` field in [CartDraft](/urn?urn=ctp%3Aapi%3Atype%3ACartDraft). Note that there is no update action to set the Store on an existing Cart. This means that you need to use one of the above options to set the Store on the Cart when the Cart is created. Below is an example of the whole Shipping Method with the `store.key` predicate. ```json { "id": "1d738bad-aa92-4387-ae32-a9c781afb951", "version": 6, "createdAt": "2024-10-07T05:30:21.173Z", "lastModifiedAt": "2024-10-07T05:39:03.265Z", "lastModifiedBy": { "isPlatformClient": true, "user": { "typeId": "user", "id": "c950c084-f5d0-4a42-8761-229d5c7a8598" } }, "createdBy": { "isPlatformClient": true, "user": { "typeId": "user", "id": "c950c084-f5d0-4a42-8761-229d5c7a8598" } }, "name": "Premium Platinum Delivery", "localizedName": { "en-AU": "Premium Platinum Delivery" }, "localizedDescription": { "en-AU": "Zen Electron" }, "taxCategory": { "typeId": "tax-category", "id": "52dca5ab-959c-4ad6-a653-d187bbc989d9" }, "zoneRates": [ { "zone": { "typeId": "zone", "id": "92584e63-60a0-4182-b0dc-e866d375c835" }, "shippingRates": [ { "price": { "type": "centPrecision", "currencyCode": "AUD", "centAmount": 990100, "fractionDigits": 2 }, "tiers": [] } ] } ], "active": true, "isDefault": true, "predicate": "store.key = \"zenith-living\"", "references": [] } ``` An additional challenge here is supporting “Same Day Delivery for metropolitan areas in Australia”. Metropolitan areas are densely populated urban centers, and they make up a subset of each state and territory in Australia. [Zones](/urn?urn=ctp%3Aapi%3Atype%3ALocation) will not support this case by themselves, as they only allow you to define country and state pairs and not anything more granular like we need to meet our requirement. These are the current postcodes that are classified as Metropolitan for delivery purposes (by Australia Post): - Postcode 1000 to 1935 - Postcode 2000 to 2079, 2085 to 2107, 2109 to 2156, 2158, 2160 to 2172, 2174 to 2229, 2232 to 2249, 2557 to 2559, 2564 to 2567, 2740 to 2744, 2747 to 2751, 2759 to 2764, 2766 to 2774, 2776, 2777, 2890 to 2897. - Postcode 3000 to 3062, 3064 to 3098, 3101 to 3138, 3140 to 3210, 3800, 3801. - Postcode 4000 to 4018, 4029 to 4068, 4072 to 4123, 4127 to 4129, 4131, 4132, 4151 to 4164, 4169 to 4182, 4205, 4206. - Postcode 5000 to 5113, 5115 to 5117, 5125 to 5130, 5158 to 5169, 5800 to 5999. - Postcode 8000 to 8999 - Postcode 9000 to 9275, 9999 We have a few solutions for this case: 1. Put all the address postcodes in the Shipping Predicate. For example, `shippingAddress.postalCode in ("1000","1001","1002","1003")` 2. Use a Custom Field on Carts that indicates if the address is a metro area or not, and use this Custom Field on the Shipping Predicate. Write logic to update this value on the Cart based on the address. This might be done by your BFF or API Extensions. ## Customers The following requirements can be solved using [Customers](/api/projects/customers.md): | Category | Electronics High Tech | Zenith Living | | :--- | :--- | :--- | | **Customer Segmentation & Accounts** | Shared customer accounts across AU & NZ regions; isolated from Zenith Living | Customer accounts isolated from Electronics High Tech | Since [Customer](/urn?urn=ctp%3Aapi%3Atype%3ACustomer) data isn't shared between brands, each new customer account must be associated with a specific Store. This allows a single customer to use the same email address to create separate accounts for each brand. When [creating a new Customer](/urn?urn=ctp%3Aapi%3Aendpoint%3A%2F%7BprojectKey%7D%2Fin-store%2Fkey%3D%7BstoreKey%7D%2Fcustomers%3APOST) we should also specify their preferred locale, this will allow your front-end application to show the Customer the expected experience. ## Pricing The following requirements can be solved using Prices scoped to Channels: | Category | Electronics High Tech | Zenith Living | | :--- | :--- | :--- | | **Pricing** | Separate pricing structure Currencies: Australia: AUD New Zealand: NZD | Separate pricing structure Currency: AUD | Each brand will have a dedicated Product Distribution Channel to manage their individual product pricing. All product Prices must be associated with the brand's corresponding channel within the [Price](/api/types.md#price). Without this Channel association, Prices cannot be filtered out by Store and so this Price will be available to all Stores. The required fields for our use case are `value`, `currencyCode` and `channel`. `country` and validity period are optional and depend on project requirements. For customer-specific pricing, use the `customerGroup` field (legacy approach, supports one group) or prefer `customerGroupAssignments` (supports multiple groups for complex segmentation). See the [migration guide](/tutorials/migration-guides/multiple-customer-groups.md) for transitioning from single to multiple Customer Groups. Example of the expected Price fields: ```json { "value": { "type": "centPrecision", "currencyCode": "AUD", "centAmount": 15800, "fractionDigits": 2 }, "channel": { "typeId": "channel", "id": "ac2e77df-ff8c-44eb-8048-f9789123767a" } } ``` Example with Customer Group targeting for customer-specific pricing: ```json { "value": { "type": "centPrecision", "currencyCode": "AUD", "centAmount": 13500, "fractionDigits": 2 }, "channel": { "typeId": "channel", "id": "ac2e77df-ff8c-44eb-8048-f9789123767a" }, "customerGroup": { "typeId": "customer-group", "id": "gold-members-id" } } ``` Now we need to update each Store and add the matching Product Distribution Channel. ## Inventory The following requirements can be solved using Supply Channels: | Category | Electronics High Tech | Zenith Living | | :--- | :--- | :--- | | **Inventory Management** | Each website maintains its own inventory pool for Standard Shipping; retail stores also function as warehouses | Each website maintains inventory for Standard Shipping; uses same pool for Premium Platinum Delivery | We will create Supply Channels for each inventory location and assign each of these Channels to a Store. When a Cart has a Store value set, then it can only use Supply Channels that belong to that Store, ensuring that the inventory is isolated per Store. Note: Inventory-to-shipping mapping strategies are outside the scope of this module. ## Merchant Center access The following requirements can be solved using [Team permissions](/merchant-center/administrators-team.md#assign-permissions-and-action-rights-to-a-team): | Category | Electronics High Tech | Zenith Living | | :--- | :--- | :--- | | **Merchant Center Access** | Staff have exclusive access to manage own customer data Can manage product info and availability for Electronics High Tech | Staff have exclusive access to manage own customer data Can manage product info and availability for Zenith Living | To create different access controls for each Brand in the Merchant Center, open the Merchant Center navigate to the **User Icon (top right) > Manage organizations & teams > Select your organization > Add team > give the team a name > Permissions**. From here select the relevant Project, expand the Customers permission, then select **Add condition**. Here we can define one or many Stores that the Team member can access, this will mean that they can only view and edit Customers belonging to those Stores. We will need to also do this for Orders and Cart discounts. View & Edit for Products, Product Selections and Categories will be enabled for both brand teams. ![Assigning Teams permissions to Stores.](https://docs.commercetools.com/learning-model-your-business-structureimages/stores-and-channels/team-permissions-a.png) ## Related pages - [Area overview page with navigation](/learning-model-your-business-structure.md) - [Previous page: Store-scoped resources and data fencing](/learning-model-your-business-structure/stores-and-channels/store-scoped-resources-and-data-fencing.md) - [Next page: Learning check](/learning-model-your-business-structure/stores-and-channels/learning-check.md)