--- title: Product Attribute Mapping tags: [integration, attributes, line-items, cart] --- This guide explains how to enrich SimplyClub transactions with **per-line product attributes** — mapping each cart line's `extended_attributes` (tags, collections, metafields) onto the Simply line-item fields (`Department`, `CategoryCode`, `Vendor`, …) that drive product-level promotions and reporting. It is the companion to the **Frontend Embedding Guide** (which covers `setCart`) and the **Webhook Integration Guide** (which covers the order line-item shape). This page focuses only on the attribute → Simply-field layer. > **Opt-in, off by default.** Mapping does nothing until you explicitly enable it for a POI (`active: true`). With it off, your `extended_attributes` are accepted and ignored — zero behavior change. --- ## 1. What it does Each cart line item can carry an optional `extended_attributes` bag. When mapping is **active** for a POI, the SimplyClub backend reads that bag, resolves a value for each configured field, and copies it onto the Simply transaction line item (`TranItem`) under the Simply field name. The result: a single product can tell SimplyClub its department, category, vendor, and classification codes — so club rules and promotions can target "all items in Department 12" or "anything from Vendor X" without SimplyClub having a product catalog. ``` cart line.extended_attributes ──► (per-POI mapping rules) ──► TranItem Simply fields { tags, collections, active === true Department, CategoryCode, metafields } Vendor, SubDept1…3, PLU…1…3 ``` The same mapping is applied in **three** contexts, so a line is enriched identically wherever it enters: - Drop-in **session** create / update (live cart / benefit calculation) - Shopify **`orders/paid`** webhook (transaction close) - **Refunds** --- ## 2. The `extended_attributes` contract Your storefront / cart extension is responsible for putting `extended_attributes` on each line item it sends (via `setCart`, and forwarded into the order webhook). The shape: ```ts extended_attributes?: { tags?: { id: string; value: string }[]; collections?: { id: string; value: string }[]; metafields?: { id: string; value: string | number | boolean }[]; } ``` - **`id`** is the machine key SimplyClub maps and stores (e.g. a tag ID, a collection ID, or a metafield identifier like `custom.material_composition`). - **`value`** is the human-readable label — useful for your own debugging; mapping itself keys off `id` (tags/collections) or matches by `id` (metafields). > This is the contract **this app** expects on line objects — it is not a verbatim Shopify Admin API payload. Any storefront that emits this shape works (see [§6](#6-channel-support)). For the surrounding line-item fields (`id`, `sku`, `price`, `quantity`, …) see the line-item shape in the **Webhook Integration Guide** and the `setCart` command in the **Technical Documentation** — this page does not repeat them. --- ## 3. The mappable Simply fields Nine Simply line-item fields can be filled. Each maps from one source: | Mapping key | Simply field | Typical use | |---|---|---| | `department` | `Department` | Top-level merchandise department | | `category` | `CategoryCode` | Product category | | `subDept1` | `SubDept1` | Sub-department level 1 | | `subDept2` | `SubDept2` | Sub-department level 2 | | `subDept3` | `SubDept3` | Sub-department level 3 | | `vendor` | `Vendor` | Supplier / brand | | `pluClassification1` | `PLUClassificationCode1` | Classification code 1 | | `pluClassification2` | `PLUClassificationCode2` | Classification code 2 | | `pluClassification3` | `PLUClassificationCode3` | Classification code 3 | A field is only written when its rule resolves to a value — **unresolved fields are omitted**, never set to an empty string. --- ## 4. How a value is resolved Each of the nine fields has a rule: ```ts { source: 'tags' | 'collections' | 'metafield' | 'none', key?: string } ``` | `source` | How the value is resolved | |---|---| | `tags` | Joins the `id` of **every** tag on the line with `, ` (e.g. `t1, t2`). Resolves to nothing if the line has no tags. | | `collections` | Joins the `id` of **every** collection on the line with `, `. Entries missing an `id` are skipped (and logged). | | `metafield` | Requires a `key`. Finds the metafield whose `id === key` and uses its `value` (coerced to a string). | | `none` | Field is not mapped. (Default for every field.) | > **Tags & collections are bulk by nature.** Because they join *all* entries on the line, point a single Simply field (e.g. `Department`) at `tags` only when a line carries exactly the tag(s) you want there. For one-value-per-field precision, prefer `metafield`. ### Example A line item like: ```json { "id": 44550337626248, "sku": "1004", "extended_attributes": { "tags": [{ "id": "12", "value": "Womenswear" }], "metafields": [ { "id": "custom.vendor_code", "value": "ACME" }, { "id": "custom.category", "value": "337" } ] } } ``` with this mapping: ```json { "department": { "source": "tags" }, "category": { "source": "metafield", "key": "custom.category" }, "vendor": { "source": "metafield", "key": "custom.vendor_code" } } ``` produces these fields on the Simply line item: ```json { "Department": "12", "CategoryCode": "337", "Vendor": "ACME" } ``` --- ## 5. Configuring a POI Mapping is stored per POI under `settings.shopifySimplyAttributeMapping`: ```ts { active: boolean, // master switch — defaults to false mapping: { /* the 9 rules */ }, discoveredMetafieldKeys: string[] // see §5.2 } ``` ### 5.1 Management API These endpoints are vendor-authenticated (Dashboard) and POI-scoped. ``` GET /api/v1/poi/:poiId/attribute-mapping PUT /api/v1/poi/:poiId/attribute-mapping GET /api/v1/poi/:poiId/attribute-mapping/metafield-keys ``` **GET** returns the current configuration (with defaults filled in for any unset rule): ```json { "storeId": "65f...", "active": true, "mapping": { "department": { "source": "tags" }, "category": { "source": "metafield", "key": "custom.category" }, "subDept1": { "source": "none" }, "subDept2": { "source": "none" }, "subDept3": { "source": "none" }, "vendor": { "source": "metafield", "key": "custom.vendor_code" }, "pluClassification1": { "source": "none" }, "pluClassification2": { "source": "none" }, "pluClassification3": { "source": "none" } }, "discoveredMetafieldKeys": ["custom.category", "custom.vendor_code"] } ``` **PUT** persists `{ active, mapping }`. The body is validated: `active` must be a boolean, and any rule with `source: "metafield"` **must** include a non-empty `key`. Unknown / omitted rules default to `{ source: "none" }`. The response uses the standard envelope: ```json { "success": true, "data": { "...same shape as GET..." }, "error": null } ``` ```bash curl -X PUT https://dropins.simplyclub.co.il/api/v1/poi/65f.../attribute-mapping \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "active": true, "mapping": { "department": { "source": "tags" }, "category": { "source": "metafield", "key": "custom.category" }, "vendor": { "source": "metafield", "key": "custom.vendor_code" } } }' ``` ### 5.2 Metafield-key discovery You don't have to know your metafield IDs up front. As carts and orders flow through, the backend records every metafield `id` it sees into `discoveredMetafieldKeys` (best-effort, non-blocking). The Dashboard surfaces these as a picklist when you choose `source: metafield`, so the keys your storefront actually emits become selectable. `GET …/attribute-mapping/metafield-keys` returns just that list. --- ## 6. Channel support Despite the `shopify…` naming on the setting and internal types, **the mapping engine has no Shopify-specific logic**. It works for any storefront that emits the [`extended_attributes` contract](#2-the-extended_attributes-contract) through the drop-in session / cart endpoints — custom HTML, WooCommerce, or anything else. Shopify is simply the one **webhook** intake wired today. The drop-in session path (used by every channel) already applies the mapping. > **Webhook caveat.** Shopify's `orders/paid` webhook frequently **drops** `extended_attributes` from its line items. To avoid losing your mapping at transaction close, the backend re-attaches the attributes from the saved cart **session** (matching lines by `variant_id` / `sku`) before applying the mapping. So attributes set on the cart survive into the paid-order transaction — as long as the session carried them. --- ## 7. Checklist before going live - [ ] Storefront emits `extended_attributes` (`tags` / `collections` / `metafields`, each `{ id, value }`) on every relevant line in `setCart`. - [ ] You've chosen a `source` for each Simply field you need; everything else stays `none`. - [ ] Every `metafield` rule has a non-empty `key` that matches the metafield `id` your storefront emits. - [ ] `active` is set to `true` for the POI (PUT `…/attribute-mapping`). - [ ] You ran a test cart and confirmed the expected Simply fields appear (GET `…/attribute-mapping/metafield-keys` shows your keys were seen). - [ ] For Shopify: confirmed the session carries the attributes so the webhook merge can restore them at `orders/paid`. --- ## 8. Troubleshooting | Symptom | Likely cause | |---|---| | Fields never appear on the transaction | Mapping is not `active` for the POI, or no rule points at a populated source. | | A `metafield` field is always empty | The rule's `key` doesn't match any metafield `id` on the line, or the storefront isn't sending that metafield. Check `discoveredMetafieldKeys`. | | `Department` has several comma-joined values | `source: tags`/`collections` joins **all** entries on the line. Use `metafield` for a single value. | | Attributes present on the cart but missing at `orders/paid` | The Shopify webhook dropped them and the session didn't carry them (no `variant_id`/`sku` match for the merge). Verify the session cart included `extended_attributes`. | | PUT rejected with a validation error | `active` must be boolean; every `metafield` rule needs a non-empty `key`. |