--- name: shoplazza-customers description: >- Use when the user wants to manage customers (buyers) on a shoplazza store through the CLI — create a customer profile, find a customer by email or phone, get customer details or the total customer count, update names / tags / email or SMS marketing subscription, or manage a customer's shipping addresses including the default checkout address. Triggers include 客户 / 买家 / 顾客 / 客户资料 / 新建客户 / 按邮箱查客户 / 按手机号查客户 / 给客户打标签 / 退订营销 / 收货地址 / 默认地址 / customer profile / new customer / find customer by email or phone / customer count / tag a customer / unsubscribe marketing / shipping address / default address. Finding the customer RECORD by email (按邮箱找客户资料) belongs HERE (customers +search); that customer's ORDER HISTORY (该客户下过的订单 / orders placed by this email) belongs to shoplazza-orders. --- # shoplazza CLI — customers module **CRITICAL — before anything else, use Read on [`../shoplazza-common/SKILL.md`](../shoplazza-common/SKILL.md).** It owns every cross-cutting mechanic: the three access tiers, the output envelope (`.data`), `--dry-run`, `--jq` (incl. "don't pass `-r`"), `--fields`, `schema`, `api rest`, auth / profiles, and the safety protocol. This file covers only the customers domain and never repeats them. ## Overview The `customers` module manages customer (buyer) profiles and their shipping addresses. All three access tiers apply (tier-selection rule is in shoplazza-common — prefer `+shortcut`). Shortcuts are **`+create` and `+search` only**. There is **no `+count`** — counting is the `count` spec leaf — and no `+update` / `+delete`; updates are the `update` leaf, and this module has **no customer-delete command at all** (only addresses have `delete`). ## Command map Intent → command, highest-fit tier first. The authoritative flags/params live in `shoplazza customers --help` and `shoplazza schema customers.`, not this table. | User intent | Command | |-------------|---------| | Create a customer | `customers +create (--email \| --phone

) [--first-name --last-name --tags --no-marketing]` | | Find customer by email / phone | `customers +search --email ` / `customers +search --phone

` | | Total customer count | `customers count` (leaf — there is NO `+count`) | | Get one customer by ID | `customers get --params '{"customer_id":""}'` | | List / filter customers (cursor paging, ids, contact) | `customers list --params '{…}'` | | Update name / tags / marketing subscription | `customers update --params '{"customer_id":""}' --data '{"customer":{…}}'` | | List a customer's addresses | `customers addresses list --params '{"customer_id":""}'` | | Add an address | `customers addresses create --params '{"customer_id":""}' --data '{"address":{…}}'` | | Get / update / delete one address | `customers addresses {get,update,delete} --params '{"customer_id":"","address_id":""}'` | | Set the default shipping address | `customers addresses set-default --params '{"customer_id":"","address_id":""}'` (no `--data`) | ## Acting on a request "新建客户" / "add a new customer" / "给客户打标签" / "unsubscribe them" is an **action**, not a question: 1. Match intent to a command via the trigger table. 2. Check the **required, no-default values** against the user's own words. Missing or ambiguous → **ask with `AskUserQuestion`, never fabricate** — above all the contact identifier: **never invent an email address or phone number.** 3. **Never ask about anything with a CLI default** (marketing defaults to subscribe; names and tags are optional). Let the default win. 4. All required present → execute. Writes here are non-destructive single-record updates; `--dry-run` first only if the user asks (see shoplazza-common → Safety protocol). ### Trigger phrase → command | User says | Command | How to extract values | |---|---|---| | 新建客户 / 建个客户档案 / add a new customer | `+create` | exactly ONE of `--email` / `--phone`; a full name like "Jane Doe" splits into `--first-name Jane --last-name Doe` | | 不要订阅营销 / 不订阅邮件 / don't subscribe to marketing | `+create … --no-marketing` | default is subscribe — the flag MUST be set on explicit opt-out wording | | 按邮箱查客户 / find the customer by email | `+search --email ` | the ask is the customer record, not their orders | | 按手机号查客户 / look up by phone | `+search --phone

` | do not invent `--since`/`--until` time bounds | | 一共多少客户 / 注册客户数 / how many customers | `count` | leaf, no flags; extract with `--jq '.data.count'` | | 给客户 X 打标签 / tag customer X | `update` | tags is a WHOLE-ARRAY replace — read-merge-write (see Gotchas) | | 退订邮件营销 / unsubscribe email marketing | `update` with `{"customer":{"accepts_marketing":false}}` | email only — do NOT touch `accepts_sms_marketing` | | 退订短信营销 / unsubscribe SMS marketing | `update` with `{"customer":{"accepts_sms_marketing":false}}` | SMS only — do NOT touch `accepts_marketing` | | 加一个收货地址 / add a shipping address | `addresses create` | body requires `country` + `country_code` | | 把地址设为默认 / set the default address | `addresses set-default` | both IDs are path params via `--params`; no body | ### Required-vs-ask matrix Only **no-default** values are askable. | Command | Must ASK if unspecified | Infer if possible | Default silently | |---|---|---|---| | `+create` | the contact identifier — **exactly one** of `--email` / `--phone` (if you ask, offer both and explain only ONE is needed) | `--first-name`/`--last-name` from a stated name; `--no-marketing` from opt-out wording; `--tags` if the user names tags | marketing (default subscribe), `--tags` (omit), names (optional — never ask) | | `+search` | which customer — an email or phone to search for, when the user wants a specific person but gave neither | filter flag from what they gave (email vs phone) | `--page-limit`, `--since`, `--until` (all omit) | | `update` | `customer_id`; the new value if wording gave none | which field from wording (标签→`tags`, 邮件营销→`accepts_marketing`, 短信→`accepts_sms_marketing`) | every field the user did not mention — never send it | | `addresses create` | `customer_id`; `country` + `country_code` (required body fields) | other address fields from wording | `default` and all optional fields (omit) | | `addresses set-default` | `customer_id` + `address_id` | — | — (no body at all) | ### Never-ask list Optional or defaulted — never a question: `--first-name` / `--last-name` / `--tags` (`+create`, optional) · marketing subscription (`+create` defaults to subscribe; set `--no-marketing` only on explicit opt-out wording, never ask) · `--page-limit` / `--since` / `--until` (`+search`; omit — never invent time bounds) · optional address fields incl. `default`. ## Boundaries Reads like customers, actually belongs elsewhere (and vice versa): | Sounds like customers | Actually belongs to | Command | |---|---|---| | 该客户下过的订单 / order history for jane@example.com | `shoplazza-orders` | an orders search filtered by that customer | | 按邮箱找到这个客户 / customer profile for an email | **this module** (not orders) | `customers +search --email ` | Direction decides: **资料 (profile/record) → customers; 订单 (orders placed) → orders.** Resolving a customer ID here as an intermediate step for an orders query is fine — the final answer must come from the owning domain. ## Permissions · Scope Authorization is by **domain** (the unit of `--domain`), not per command. Authorization flow is in shoplazza-common → Authentication. | Operation | Needs | Grant | |---|---|---| | read (`get` / `list` / `count` / `+search` / `addresses get\|list`) | `customers` read scope | `auth login --domain customers` | | write (`+create` / `create` / `update` / `addresses create\|update\|delete\|set-default`) | `customers` write scope | `auth login --domain customers` | Look up exact scope literals with `shoplazza auth scopes`; `--domain customers` expands into them. ## Gotchas Domain-specific pitfalls only (generic ones — `.data` prefix, `--jq` without `-r` — are in shoplazza-common). | Symptom | Cause | Fix | |---------|-------|-----| | `customers +count` → unknown command | Shortcuts are `+create`/`+search` only (unlike products/orders, customers has no `+count`) | `customers count` (leaf), extract with `--jq '.data.count'` — never list-and-count manually | | `+create` rejected: "exactly one of --email or --phone is required" | Strict XOR — both given, or neither | Pass exactly one; if the user gave neither, ASK — never fabricate contact info | | Spec-leaf `create` fails without `contact_type` | The leaf body requires `contact_type`; `+create` fills it automatically (`email` / `phone` to match the flag) | Prefer `+create`; on the leaf, set `"contact_type"` to match the identifier you send | | Customer's old tags vanished after tagging | `update` `tags` REPLACES the whole array | Read-merge-write: `get` current tags, append, then `update` with the merged array | | Unsubscribed more than the user asked | Email (`accepts_marketing`) and SMS (`accepts_sms_marketing`) are separate booleans | Change only the one named; leave the other out of the body | | Tried to change email / phone via `update` | The update body only accepts `first_name` / `last_name` / `accepts_marketing` / `accepts_sms_marketing` / `tags` | Not expressible via `update` — check `schema customers.update` | | Leaf `list` ignores a `phone` param | `list` documents `email` and `contact` query params, not `phone` | Prefer `+search --phone` (it sends `contact`); on the leaf use `"contact"` (matches the customer's primary contact — email or phone) | | `unknown flag: --fields` on `customers +search` | The customers list endpoint documents no `fields` param, so the flag was removed rather than left as a silent no-op | Project with `--jq` | | Wanted to delete a customer | No customer-delete command exists in this module (only `addresses delete`) | Not expressible at tiers 1–2; confirm an endpoint exists before reaching for `api rest` | | Set default via `addresses update` with `"default":true` | A dedicated endpoint owns this | `addresses set-default --params '{"customer_id":…,"address_id":…}'` — path params only, no `--data` | | `+search --since/--until` returns "wrong" customers | They map to `created_at_min`/`created_at_max` — bounds on the customer's CREATION time | Only pass them when the user asks for a signup-time window | ## Recipes ```bash # 1. New customer with email, opted out of marketing emails customers +create --email jane.doe@example.com --first-name Jane --last-name Doe --no-marketing # 2. Find a customer by phone (project with --jq; +search has no --fields) customers +search --phone 13800138000 --jq '.data.customers[] | {id, name, email, phone}' # 3. Total registered customers customers count --jq '.data.count' # 4. Tag customer 424242 as VIP without clobbering existing tags (read-merge-write) customers get --params '{"customer_id":"424242"}' --jq '.data.customer.tags' # → e.g. ["wholesale"]; append and write the MERGED array back: customers update --params '{"customer_id":"424242"}' --data '{"customer":{"tags":["wholesale","VIP"]}}' # 5. Unsubscribe email marketing only (SMS untouched) customers update --params '{"customer_id":"887766"}' --data '{"customer":{"accepts_marketing":false}}' # 6. Set the default shipping address customers addresses set-default --params '{"customer_id":"335577","address_id":"ADDR99"}' # 7. List a customer's address book customers addresses list --params '{"customer_id":"335577"}' --jq '.data.addresses[] | {id, name, address1, default}' ``` ## References - Per-command flags: `shoplazza customers --help` - Spec-leaf params / body / response: `shoplazza schema customers.` (addresses: `schema customers.addresses.`) - Cross-cutting mechanics (auth / tiers / output envelope / `--dry-run` / `--jq` / safety): `../shoplazza-common/SKILL.md` - Shortcut source of truth: `shortcuts/customers/*.go`