feat: three nuxt frontends, demo seed, rounding + money-exponent + rate-cast fixes, archived specs
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
# Tasks: cart-checkout-orders
|
||||
|
||||
## 1. Schema
|
||||
- [x] Migration: orders, order_items; order status enum; order_no sequence
|
||||
|
||||
## 2. Cart (Redis)
|
||||
- [x] GET /api/cart, POST /api/cart/items, PUT/DELETE /api/cart/items/{sku_id}
|
||||
- [x] Cart read joins SKU/product snapshots; rejects inactive/unpublished SKUs
|
||||
|
||||
## 3. Checkout & orders
|
||||
- [x] POST /api/orders/checkout (one order per shop, tx: stock decrement + order insert, cart clear)
|
||||
- [x] GET /api/orders (mine, paged), GET /api/orders/{id}
|
||||
- [x] POST /api/orders/{id}/pay (mock) and /cancel with state rules
|
||||
- [x] GET /api/shop/orders for merchants; platform admin list
|
||||
|
||||
## 4. Tests
|
||||
- [x] cargo test: multi-shop checkout splits orders, insufficient stock → 409, cancel rules, stock restored on cancel
|
||||
@@ -0,0 +1,22 @@
|
||||
# Tasks: catalog-i18n-currency
|
||||
|
||||
## 1. Schema
|
||||
- [x] Migration: currencies, categories, products, skus; product status enum
|
||||
- [x] Seed currencies (USD base, CNY, EUR, JPY) + demo categories (bilingual)
|
||||
|
||||
## 2. Currency APIs
|
||||
- [x] GET /api/currencies (public, enabled only)
|
||||
- [x] GET /api/currencies/convert?amount_minor&from&to
|
||||
|
||||
## 3. Public catalog APIs
|
||||
- [x] GET /api/products (paged; filters: category_id, q, shop_id; only published)
|
||||
- [x] GET /api/products/{id_or_slug} (published only, with SKUs)
|
||||
- [x] GET /api/categories
|
||||
|
||||
## 4. Shop-admin catalog APIs
|
||||
- [x] GET/POST /api/shop/products, PUT /api/shop/products/{id}
|
||||
- [x] POST /api/shop/products/{id}/publish | /unpublish
|
||||
- [x] POST /api/shop/products/{id}/skus (upsert by sku_code)
|
||||
|
||||
## 5. Tests
|
||||
- [x] cargo test: publish lifecycle visibility, i18n fields round-trip, currency conversion math, shop isolation
|
||||
+7
-7
@@ -7,14 +7,14 @@
|
||||
- [x] sqlx migrate on boot; 0001_init migration
|
||||
|
||||
## 2. Identity schema
|
||||
- [ ] Migration: users, shops tables; role enum
|
||||
- [ ] Seed platform admin (admin@vmall.local / admin1234)
|
||||
- [x] Migration: users, shops tables; role enum
|
||||
- [x] Seed platform admin (admin@vmall.local / admin1234)
|
||||
|
||||
## 3. Auth API
|
||||
- [ ] POST /api/auth/register (customer role)
|
||||
- [ ] POST /api/auth/login → JWT + user
|
||||
- [ ] GET /api/auth/me (Bearer)
|
||||
- [ ] Auth extractor + require_role guard
|
||||
- [x] POST /api/auth/register (customer role)
|
||||
- [x] POST /api/auth/login → JWT + user
|
||||
- [x] GET /api/auth/me (Bearer)
|
||||
- [x] Auth extractor + require_role guard
|
||||
|
||||
## 4. Tests
|
||||
- [ ] cargo test: register/login/me happy path, wrong password, role guard denies customer on admin route
|
||||
- [x] cargo test: register/login/me happy path, wrong password, role guard denies customer on admin route
|
||||
@@ -0,0 +1,21 @@
|
||||
# Task: build apps/admin (platform console) — READ openspec/changes/frontend-apps/agent-conventions.md FIRST and follow it exactly. Your app = @vmall/admin, port 3002. Allowed roles: platform_admin.
|
||||
|
||||
Replace the placeholder pages. app.vue (sidebar layout) exists — keep it.
|
||||
|
||||
## Pages to implement (apps/admin/pages/)
|
||||
|
||||
1. `login.vue` — per conventions; only platform_admin may proceed. (Seed account: admin@vmall.local / admin1234 — display this hint on the login page in a muted line for the dev MVP.)
|
||||
2. `index.vue` (auth) — dashboard: stat cards (total users, shops, orders, enabled currencies) from `$api.admin.listUsers()`, `$api.admin.listShops()`, `$api.admin.listOrders()`, `$api.admin.listCurrencies()`.
|
||||
3. `users.vue` (auth) — `$api.admin.listUsers(page)` table: email, display_name, role badge, assigned shop (resolve shop_id → shop name via `$api.admin.listShops()`, localized), created. Role editor per row: role select (platform_admin/shop_owner/shop_staff/customer) + shop select shown ONLY for shop roles + save button → `$api.admin.setUserRole(id, role, shopId|null)`; on validation error (e.g. shop role without shop → 400) show message. Paging.
|
||||
4. `shops.vue` (auth) — `$api.admin.listShops()` table: localized name, slug, status badge, created; actions suspend/activate → `$api.admin.setShopStatus`. Create-shop card: name EN, name ZH, slug → `$api.admin.createShop({en, zh}, slug)`; refetch; show conflict error (duplicate slug → 409).
|
||||
5. `orders.vue` (auth) — `$api.admin.listOrders(page)`: table (order_no, shop (resolve id→name), user_id short, total formatted in order.currency, status badge, created). Paging.
|
||||
6. `currencies.vue` (auth) — `$api.admin.listCurrencies()` table: code, localized name, symbol, exponent, rate_to_base, base badge, enabled badge. Per-row rate editor (number input + save → `$api.admin.setRate(code, rate)`). New-currency card: code (3 letters), name EN + ZH, symbol, exponent (0–6), rate_to_base, enabled checkbox → `$api.admin.upsertCurrency({...})` with CurrencyUpsertBody (rate_to_base is a STRING). Toggle enabled via upsert with the row's other fields preserved.
|
||||
|
||||
## Middleware
|
||||
middleware/auth.ts per conventions; apply to everything except /login.
|
||||
|
||||
## Verify
|
||||
`pnpm --filter @vmall/admin build` MUST pass. Fix type errors properly (no `any`, no @ts-ignore).
|
||||
|
||||
## Report back
|
||||
Pages built, deviations, contract gaps found.
|
||||
@@ -0,0 +1,27 @@
|
||||
# VMall frontend conventions (READ FIRST — applies to every app agent)
|
||||
|
||||
Repo: /Users/chengdzhang/github/jamyun/vmall (pnpm workspace). The Rust API is COMPLETE and tested; do not touch apps/api, packages/shared, openspec, or any app other than yours.
|
||||
|
||||
## Your app
|
||||
- Nuxt 3 + pinia + @nuxtjs/i18n. Deps installed. Verify with `pnpm --filter @vmall/<your-app> build` at the end (MUST pass). Do not run dev servers, do not run other apps' builds, do not run cargo.
|
||||
- The API may not be running while you work — code against the contract, prove with `nuxt build` (type safety + compile). NEVER mock the API.
|
||||
|
||||
## Contract (packages/shared/src)
|
||||
- `useNuxtApp().$api` is a typed `ApiClient` (see packages/shared/src/api.ts for every method + request body types). Provided by plugins/api.ts (already wired, reads baseUrl from runtimeConfig public.apiBase, token from localStorage `vmall.token`).
|
||||
- Types in packages/shared/src/types.ts (User, Product, Sku, Cart, Order, Shipment, Invoice, Currency, Paged, etc.). Money = integer minor units + ISO currency code.
|
||||
- `t(localizedText, locale)` picks the display string from a JSONB {"en","zh"} map. `formatMoney(amountMinor, currency, exponent, locale)` formats minor units.
|
||||
- All UI strings via $t with keys from @vmall/shared/locales (en + zh exist). If you need a key that doesn't exist, add it to YOUR app's locales-extra.ts (enExtra/zhExtra, same nested shape) — NEVER edit packages/shared.
|
||||
- Shared stylesheet `@vmall/shared/ui.css` is loaded: use its classes (.card, .btn .primary .sm .danger, .table, .badge .green/.blue/.orange/.red, .field, .grid.products, .product-card, .page-title, .page-head, .form-narrow, .muted, .row, .between, .mt, .mb, .error-text). Add app-scoped CSS only in a <style> block when needed.
|
||||
|
||||
## Session & guards
|
||||
- stores/session.ts: `useSessionStore()` — hydrate() on mounted (app.vue already does), setAuth({token,user}), logout(), getters.isLoggedIn, state.user (role, display_name).
|
||||
- Route guard convention: create `middleware/auth.ts` with defineNuxtRouteMiddleware that (client-side) reads localStorage `vmall.token` + `vmall.user` (JSON) and redirects to /login when absent or when user.role is not in the app's allowed roles. Apply with `definePageMeta({ middleware: "auth" })` on every protected page. The /login page itself is public.
|
||||
- Login page pattern: form → `$api.login(email, password)` → on success check `user.role` against the app's allowed roles (wrong role → show error, do not setAuth) → `session.setAuth(...)` → navigateTo("/"). Show API error message on failure.
|
||||
|
||||
## Behavior rules
|
||||
- All pages render in BOTH en and zh — switcher already in app.vue; never hardcode user-facing strings.
|
||||
- All list pages handle empty state ($t('common.empty')) and API errors (show err.message in .error-text).
|
||||
- After every mutation, re-fetch the affected list/detail from the API (no local-only state faking).
|
||||
- Dates: `new Date(x).toLocaleString(locale === 'zh' ? 'zh-CN' : 'en-US')`.
|
||||
- Money inputs: enter major units (e.g. 12.99), convert to minor via exponent (SKU currencies are exp-2 except JPY exp-0; SKU editor may restrict currency to USD/CNY/EUR which are all exp-2).
|
||||
- Status badges: map statuses to .badge colors (green=positive terminal, blue=active/in-progress, orange=pending, red=cancelled/rejected).
|
||||
@@ -0,0 +1,24 @@
|
||||
# Task: build apps/mall (customer storefront) — READ openspec/changes/frontend-apps/agent-conventions.md FIRST and follow it exactly. Your app = @vmall/mall, port 3000. Allowed roles: customer.
|
||||
|
||||
Replace the placeholder pages with the real storefront. app.vue (topnav with locale + currency switchers) already exists — keep it; you may refine but not regress it. Currency selection comes from `usePrefs().currency` (cookie) and is fed by the switcher in app.vue.
|
||||
|
||||
## Pages to implement (apps/mall/pages/)
|
||||
|
||||
1. `index.vue` — catalog home. Category filter (select, from `$api.listCategories()`), search input (q), paging (prev/next). Product grid (.grid.products): card with first image (or placeholder div), localized name via `t(name, locale)`, price = lowest active-SKU price, displayed in the SELECTED currency.
|
||||
Price display helper (make `composables/usePrice.ts`): given amount_minor + sku currency, if it equals selected currency use as-is, else `$api.convert(...)`; cache results in a reactive Map keyed `amount:from:to` to avoid duplicate calls; format with `formatMoney(converted, selected, exponentOfSelected, locale)`. Load currency list once (store it in the composable via useState) for exponents.
|
||||
2. `products/[id].vue` — detail. `$api.getProduct(route.params.id)`: images (main + thumbs), localized name/description, SKU picker (radio list: sku_code + attributes JSON rendered as key: value pairs + stock), qty input (1..stock), converted price, add-to-cart button. Not logged in → redirect /login (preserve intent not required). Success → navigate to /cart.
|
||||
3. `cart.vue` (auth) — `$api.getCart()`: line items (localized name, sku_code, unit price converted, qty editor with update on change `$api.updateCartItem`, remove `$api.removeCartItem`), line totals, subtotal (sum of converted lines), checkout button → /checkout. Empty state.
|
||||
4. `checkout.vue` (auth) — shipping address form (recipient/phone/country/region/city/line1/postal_code, all validated non-empty except region/postal optional-but-shown), order preview (lines + subtotal in selected currency), place order → `$api.checkout(address, currency)` → success page state: show created order numbers + link to /orders.
|
||||
5. `orders/index.vue` (auth) — `$api.listMyOrders(page)`: table (order_no, created, item count, total formatted in order.currency, status badge), link to detail. Paging.
|
||||
6. `orders/[id].vue` (auth) — `$api.getOrder(id)`: items table, address card, status badge, total. Actions by status: pending_payment → Pay (`$api.payOrder`) and Cancel (`$api.cancelOrder`). Shipments section: from `$api.listMyShipments()` filtered by order_id — carrier/tracking/status, confirm-delivery button when shipped (`$api.confirmDelivered`). Invoice section: request form (kind select personal/company, title, tax_no shown when company) → `$api.requestInvoice`; show existing invoice for this order from `$api.listMyInvoices()`.
|
||||
7. `invoices.vue` (auth) — `$api.listMyInvoices()`: table (invoice_no or —, order_no, title, kind, amount formatted, status badge, issued_at).
|
||||
8. `login.vue`, `register.vue` — .form-narrow cards. Register: `$api.register(email, password, displayName)` then setAuth and go home. Link between the two.
|
||||
|
||||
## Middleware
|
||||
middleware/auth.ts per conventions; apply to cart, checkout, orders/*, invoices.
|
||||
|
||||
## Verify
|
||||
`pnpm --filter @vmall/mall build` MUST pass. Fix all type errors properly (no `any`, no @ts-ignore).
|
||||
|
||||
## Report back
|
||||
Pages built, any deviations, anything missing from the API client contract (e.g. a pay endpoint) — list precisely.
|
||||
@@ -0,0 +1,25 @@
|
||||
# Task: build apps/shop-admin (merchant console) — READ openspec/changes/frontend-apps/agent-conventions.md FIRST and follow it exactly. Your app = @vmall/shop-admin, port 3001. Allowed roles: shop_owner, shop_staff.
|
||||
|
||||
Replace the placeholder pages. app.vue (sidebar layout) exists — keep it.
|
||||
|
||||
## Pages to implement (apps/shop-admin/pages/)
|
||||
|
||||
1. `login.vue` — per conventions; only shop_owner/shop_staff may proceed.
|
||||
2. `index.vue` (auth) — dashboard: stat cards (total products, published count, orders by status counts, pending invoice requests) computed from `$api.shop.listMyProducts({ per_page: 100 })`, `$api.shop.listOrders({ per_page: 100 })`, `$api.shop.listInvoices()`. Also show shop profile from `$api.shop.getMyShop()` (localized name, slug, status).
|
||||
3. `products/index.vue` (auth) — table: localized name, slug, SKU count, status badge, created; status filter select (all/draft/published/unpublished); actions: edit link, publish (`$api.shop.publish`) / unpublish (`$api.shop.unpublish`) buttons with immediate refetch; "New product" button → products/new. Paging if total > per_page.
|
||||
4. `products/new.vue` + `products/[id].vue` (auth) — product form (shared component `components/ProductForm.vue`): slug, name EN + ZH, description EN + ZH (textareas), category select (`$api.listCategories()`, optional), images textarea (one URL per line → string[]). Save: create → `$api.shop.createProduct`, edit → `$api.shop.updateProduct`; body type ProductUpsertBody.
|
||||
On the EDIT page additionally: SKU manager — table of existing SKUs (sku_code, price formatted, currency, stock, active) + form to add/update a SKU (`$api.shop.upsertSku(productId, body)` with SkuUpsertBody): sku_code, price in MAJOR units converted to minor ×100 (currencies USD/CNY/EUR all exponent 2; offer a select of those three), stock integer, active checkbox. After upsert, refetch the product via `$api.shop.getProduct(id)` (exists in the client).
|
||||
Validation: slug required (lowercase alnum + dash), name EN required; show API errors (e.g. publish without priced SKU → 400) via .error-text.
|
||||
5. `orders/index.vue` (auth) — `$api.shop.listOrders({ status, page })`: status filter, table (order_no, created, items count, total in order.currency formatted, status badge), row → orders/[id].
|
||||
6. `orders/[id].vue` (auth) — order items, address, status. Create-shipment card (when status paid or fulfilling): carrier + tracking_no inputs + one qty input per order item (default = unshipped remainder = item.qty − sum of that item's qty across this order's shipments from `$api.shop.listShipments()` filtered by order_id); submit → `$api.shop.createShipment(orderId, carrier, trackingNo, items)` with only qty>0 lines. Shipments of this order: table with status + "Mark shipped" button when pending (`$api.shop.markShipped`). Refetch after every action.
|
||||
7. `shipments.vue` (auth) — `$api.shop.listShipments()`: full table (shipment_no, order_no, carrier, tracking_no, status badge, created) + mark-shipped action.
|
||||
8. `invoices.vue` (auth) — `$api.shop.listInvoices()`: table (invoice_no or —, order_no, title, kind, tax_no, amount formatted, status badge) + "Issue" button when requested (`$api.shop.issueInvoice`).
|
||||
|
||||
## Middleware
|
||||
middleware/auth.ts per conventions; apply to everything except /login.
|
||||
|
||||
## Verify
|
||||
`pnpm --filter @vmall/shop-admin build` MUST pass. Fix type errors properly (no `any`, no @ts-ignore).
|
||||
|
||||
## Report back
|
||||
Pages built, deviations, contract gaps found.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Tasks: frontend-apps
|
||||
|
||||
## 1. Shared package
|
||||
- [x] @vmall/shared: types, API client, locales (en/zh), ui.css
|
||||
|
||||
## 2. Scaffolds
|
||||
- [x] Three Nuxt apps: config, i18n, pinia session store, api plugin, layouts
|
||||
|
||||
## 3. mall
|
||||
- [x] Home: product grid + search + category filter + paging
|
||||
- [x] Product detail with SKU picker, localized content, converted price
|
||||
- [x] Cart page, checkout page (address form), orders list/detail, shipments, invoices, login/register
|
||||
|
||||
## 4. shop-admin
|
||||
- [x] Login guard, dashboard, products list/new/edit (i18n fields, SKUs), publish/unpublish
|
||||
- [x] Orders, shipment creation + mark shipped, invoices list + issue
|
||||
|
||||
## 5. admin
|
||||
- [x] Login guard, users (role assignment), shops (create/suspend), orders, currencies (rate edit)
|
||||
|
||||
## 6. Verification
|
||||
- [x] All three apps build; browser smoke: register→shop create→product publish→purchase→ship→invoice in en + zh
|
||||
@@ -0,0 +1,17 @@
|
||||
# Tasks: fulfillment-invoices
|
||||
|
||||
## 1. Schema
|
||||
- [x] Migration: shipments, shipment_items, invoices; status enums; shipment_no/invoice_no sequences
|
||||
|
||||
## 2. Shipments
|
||||
- [x] POST /api/shop/orders/{id}/shipments (qty validation vs unshipped remainder)
|
||||
- [x] POST /api/shop/shipments/{id}/ship; order status propagation (fulfilling/shipped)
|
||||
- [x] GET /api/shipments (customer), POST /api/shipments/{id}/confirm-delivered → order completed when done
|
||||
- [x] GET /api/shop/shipments
|
||||
|
||||
## 3. Invoices
|
||||
- [x] POST /api/orders/{id}/invoice (one open invoice per order; amount = order total)
|
||||
- [x] GET /api/invoices (customer), GET /api/shop/invoices, POST /api/shop/invoices/{id}/issue
|
||||
|
||||
## 4. Tests
|
||||
- [x] cargo test: partial shipment qty math, order status propagation, duplicate invoice rejected, issue flow
|
||||
@@ -1,17 +0,0 @@
|
||||
# Tasks: cart-checkout-orders
|
||||
|
||||
## 1. Schema
|
||||
- [ ] Migration: orders, order_items; order status enum; order_no sequence
|
||||
|
||||
## 2. Cart (Redis)
|
||||
- [ ] GET /api/cart, POST /api/cart/items, PUT/DELETE /api/cart/items/{sku_id}
|
||||
- [ ] Cart read joins SKU/product snapshots; rejects inactive/unpublished SKUs
|
||||
|
||||
## 3. Checkout & orders
|
||||
- [ ] POST /api/orders/checkout (one order per shop, tx: stock decrement + order insert, cart clear)
|
||||
- [ ] GET /api/orders (mine, paged), GET /api/orders/{id}
|
||||
- [ ] POST /api/orders/{id}/pay (mock) and /cancel with state rules
|
||||
- [ ] GET /api/shop/orders for merchants; platform admin list
|
||||
|
||||
## 4. Tests
|
||||
- [ ] cargo test: multi-shop checkout splits orders, insufficient stock → 409, cancel rules, stock restored on cancel
|
||||
@@ -1,22 +0,0 @@
|
||||
# Tasks: catalog-i18n-currency
|
||||
|
||||
## 1. Schema
|
||||
- [ ] Migration: currencies, categories, products, skus; product status enum
|
||||
- [ ] Seed currencies (USD base, CNY, EUR, JPY) + demo categories (bilingual)
|
||||
|
||||
## 2. Currency APIs
|
||||
- [ ] GET /api/currencies (public, enabled only)
|
||||
- [ ] GET /api/currencies/convert?amount_minor&from&to
|
||||
|
||||
## 3. Public catalog APIs
|
||||
- [ ] GET /api/products (paged; filters: category_id, q, shop_id; only published)
|
||||
- [ ] GET /api/products/{id_or_slug} (published only, with SKUs)
|
||||
- [ ] GET /api/categories
|
||||
|
||||
## 4. Shop-admin catalog APIs
|
||||
- [ ] GET/POST /api/shop/products, PUT /api/shop/products/{id}
|
||||
- [ ] POST /api/shop/products/{id}/publish | /unpublish
|
||||
- [ ] POST /api/shop/products/{id}/skus (upsert by sku_code)
|
||||
|
||||
## 5. Tests
|
||||
- [ ] cargo test: publish lifecycle visibility, i18n fields round-trip, currency conversion math, shop isolation
|
||||
@@ -1,22 +0,0 @@
|
||||
# Tasks: frontend-apps
|
||||
|
||||
## 1. Shared package
|
||||
- [x] @vmall/shared: types, API client, locales (en/zh), ui.css
|
||||
|
||||
## 2. Scaffolds
|
||||
- [x] Three Nuxt apps: config, i18n, pinia session store, api plugin, layouts
|
||||
|
||||
## 3. mall
|
||||
- [ ] Home: product grid + search + category filter + paging
|
||||
- [ ] Product detail with SKU picker, localized content, converted price
|
||||
- [ ] Cart page, checkout page (address form), orders list/detail, shipments, invoices, login/register
|
||||
|
||||
## 4. shop-admin
|
||||
- [ ] Login guard, dashboard, products list/new/edit (i18n fields, SKUs), publish/unpublish
|
||||
- [ ] Orders, shipment creation + mark shipped, invoices list + issue
|
||||
|
||||
## 5. admin
|
||||
- [ ] Login guard, users (role assignment), shops (create/suspend), orders, currencies (rate edit)
|
||||
|
||||
## 6. Verification
|
||||
- [ ] All three apps build; browser smoke: register→shop create→product publish→purchase→ship→invoice in en + zh
|
||||
@@ -1,17 +0,0 @@
|
||||
# Tasks: fulfillment-invoices
|
||||
|
||||
## 1. Schema
|
||||
- [ ] Migration: shipments, shipment_items, invoices; status enums; shipment_no/invoice_no sequences
|
||||
|
||||
## 2. Shipments
|
||||
- [ ] POST /api/shop/orders/{id}/shipments (qty validation vs unshipped remainder)
|
||||
- [ ] POST /api/shop/shipments/{id}/ship; order status propagation (fulfilling/shipped)
|
||||
- [ ] GET /api/shipments (customer), POST /api/shipments/{id}/confirm-delivered → order completed when done
|
||||
- [ ] GET /api/shop/shipments
|
||||
|
||||
## 3. Invoices
|
||||
- [ ] POST /api/orders/{id}/invoice (one open invoice per order; amount = order total)
|
||||
- [ ] GET /api/invoices (customer), GET /api/shop/invoices, POST /api/shop/invoices/{id}/issue
|
||||
|
||||
## 4. Tests
|
||||
- [ ] cargo test: partial shipment qty math, order status propagation, duplicate invoice rejected, issue flow
|
||||
@@ -0,0 +1,34 @@
|
||||
# auth Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change foundation-auth. Update Purpose after archive.
|
||||
## Requirements
|
||||
### Requirement: Customer registration
|
||||
The API SHALL provide `POST /api/auth/register` accepting email, password, display_name. New users are created with role `customer`. Duplicate emails MUST be rejected with 409.
|
||||
|
||||
#### Scenario: successful registration
|
||||
- **WHEN** a client posts a unique email with password ≥ 8 chars
|
||||
- **THEN** the API returns 201 with `{ token, user }` and the user can call `/api/auth/me` with the token
|
||||
|
||||
#### Scenario: duplicate email
|
||||
- **WHEN** the email already exists
|
||||
- **THEN** the API returns 409 with code `CONFLICT`
|
||||
|
||||
### Requirement: Login
|
||||
The API SHALL provide `POST /api/auth/login` issuing a signed JWT (24h TTL) containing user id and role.
|
||||
|
||||
#### Scenario: valid credentials
|
||||
- **WHEN** email + correct password are posted
|
||||
- **THEN** the API returns `{ token, user }`
|
||||
|
||||
#### Scenario: invalid credentials
|
||||
- **WHEN** the password is wrong or email unknown
|
||||
- **THEN** the API returns 401 with code `UNAUTHORIZED` and no token
|
||||
|
||||
### Requirement: Current user
|
||||
`GET /api/auth/me` SHALL return the authenticated user profile.
|
||||
|
||||
#### Scenario: missing token
|
||||
- **WHEN** no Bearer token is supplied
|
||||
- **THEN** the API returns 401
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# cart Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change cart-checkout-orders. Update Purpose after archive.
|
||||
## Requirements
|
||||
### Requirement: Server-side cart
|
||||
Authenticated shoppers SHALL have a Redis-backed cart keyed by user id, containing sku_id + qty entries.
|
||||
|
||||
#### Scenario: add and update
|
||||
- **WHEN** a shopper POSTs sku + qty, then PUTs a new qty
|
||||
- **THEN** GET /api/cart reflects the latest qty with current price/name snapshot
|
||||
|
||||
#### Scenario: unpurchasable SKU rejected
|
||||
- **WHEN** adding a SKU that is inactive or whose product is not published
|
||||
- **THEN** the API returns 400
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# catalog Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change catalog-i18n-currency. Update Purpose after archive.
|
||||
## Requirements
|
||||
### Requirement: Localized product content
|
||||
Product and category names/descriptions SHALL be stored as JSONB maps keyed by locale (`en`, `zh`). The API MUST return the full map; clients pick the display locale.
|
||||
|
||||
#### Scenario: bilingual round-trip
|
||||
- **WHEN** a shop owner creates a product with name `{"en": "Mug", "zh": "马克杯"}`
|
||||
- **THEN** both public detail and shop-admin GET return the identical map
|
||||
|
||||
### Requirement: Publish lifecycle
|
||||
Products SHALL have status `draft | published | unpublished`. Only `published` products appear in public list/detail.
|
||||
|
||||
#### Scenario: publish then unpublish
|
||||
- **WHEN** a product is published
|
||||
- **THEN** it appears in `GET /api/products`
|
||||
- **WHEN** it is unpublished
|
||||
- **THEN** public detail returns 404 and it disappears from listings
|
||||
|
||||
#### Scenario: publish requires sellable SKU
|
||||
- **WHEN** publishing a product with no active SKU having price > 0
|
||||
- **THEN** the API returns 400
|
||||
|
||||
### Requirement: Shop isolation
|
||||
Shop-role users SHALL only see and mutate their own shop's products under `/api/shop/products`.
|
||||
|
||||
#### Scenario: cross-shop access denied
|
||||
- **WHEN** shop owner A requests `/api/shop/products/{id}` of shop B
|
||||
- **THEN** the API returns 404
|
||||
|
||||
### Requirement: SKU pricing
|
||||
Each SKU SHALL carry `price_minor` (integer minor units) and an ISO `currency` code; stock is a non-negative integer.
|
||||
|
||||
#### Scenario: negative stock rejected
|
||||
- **WHEN** upserting a SKU with stock < 0
|
||||
- **THEN** the API returns 400
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# currency Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change catalog-i18n-currency. Update Purpose after archive.
|
||||
## Requirements
|
||||
### Requirement: Currency registry
|
||||
The system SHALL maintain a currencies table: ISO code, localized name, symbol, exponent (minor units), enabled flag, and `rate_to_base` (NUMERIC). Exactly one currency is the base.
|
||||
|
||||
#### Scenario: seeded currencies
|
||||
- **WHEN** migrations finish
|
||||
- **THEN** USD exists as base with rate 1, and CNY/EUR/JPY exist with positive rates
|
||||
|
||||
### Requirement: Amount conversion
|
||||
`GET /api/currencies/convert` SHALL convert integer minor units between enabled currencies via base rates, rounding half-up to the target exponent.
|
||||
|
||||
#### Scenario: USD to JPY
|
||||
- **WHEN** converting 1000 minor USD (=$10.00) to JPY with rate 150
|
||||
- **THEN** the result is 1500 minor JPY (¥1500), an integer
|
||||
|
||||
#### Scenario: disabled currency rejected
|
||||
- **WHEN** converting to a disabled or unknown currency
|
||||
- **THEN** the API returns 400
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# frontend-admin Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change frontend-apps. Update Purpose after archive.
|
||||
## Requirements
|
||||
### Requirement: Platform user and shop management
|
||||
Platform admins SHALL assign user roles (with shop scope), create shops, and suspend/activate shops. Suspended shops' products MUST NOT be purchasable (enforced by API, reflected in UI).
|
||||
|
||||
#### Scenario: assign shop owner
|
||||
- **WHEN** an admin assigns role shop_owner with a shop to a user
|
||||
- **THEN** that user can log into shop-admin and manage that shop
|
||||
|
||||
### Requirement: Currency management
|
||||
Platform admins SHALL view currencies and update exchange rates; new rates affect subsequent conversions.
|
||||
|
||||
#### Scenario: rate update
|
||||
- **WHEN** an admin updates CNY rate_to_base
|
||||
- **THEN** the mall conversion endpoint returns amounts computed with the new rate
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# frontend-mall Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change frontend-apps. Update Purpose after archive.
|
||||
## Requirements
|
||||
### Requirement: Localized storefront
|
||||
The mall SHALL render UI strings and catalog content in en or zh from one switcher, defaulting to en.
|
||||
|
||||
#### Scenario: switch to Chinese
|
||||
- **WHEN** a shopper switches locale to zh
|
||||
- **THEN** navigation, buttons and product names render in Chinese without reload errors
|
||||
|
||||
### Requirement: Multi-currency display
|
||||
The mall SHALL offer a currency switcher (enabled currencies from the API) converting SKU prices for display; checkout uses the selected currency.
|
||||
|
||||
#### Scenario: switch currency
|
||||
- **WHEN** a shopper switches from USD to JPY on a product priced $10.00
|
||||
- **THEN** the displayed price reflects the API conversion rate (integer minor units)
|
||||
|
||||
### Requirement: Shopping flow
|
||||
A shopper SHALL be able to browse, view detail, add to cart, checkout with a shipping address, pay (mock), track shipments, confirm delivery, and request an invoice — all against the live API.
|
||||
|
||||
#### Scenario: end-to-end purchase
|
||||
- **WHEN** a registered shopper completes checkout on a non-empty cart
|
||||
- **THEN** orders appear under Orders and the cart is empty
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# frontend-shop-admin Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change frontend-apps. Update Purpose after archive.
|
||||
## Requirements
|
||||
### Requirement: Merchant product management
|
||||
Shop users SHALL manage only their own shop's products: create/edit bilingual content, manage SKUs, publish/unpublish with immediate effect on the storefront.
|
||||
|
||||
#### Scenario: publish visible in mall
|
||||
- **WHEN** a merchant publishes a product in shop-admin
|
||||
- **THEN** it appears in the mall product list for the matching locale
|
||||
|
||||
### Requirement: Merchant fulfillment
|
||||
Shop users SHALL see incoming orders, create shipments, mark them shipped, and issue requested invoices.
|
||||
|
||||
#### Scenario: ship an order
|
||||
- **WHEN** a merchant creates a shipment for a paid order and marks it shipped
|
||||
- **THEN** the shopper sees the shipment with tracking info
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# invoice Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change fulfillment-invoices. Update Purpose after archive.
|
||||
## Requirements
|
||||
### Requirement: Invoice request
|
||||
Customers SHALL request at most one open invoice (发票) per order, with title, kind (`personal | company`), and tax_no required for company invoices. Amount equals the order total in the order currency.
|
||||
|
||||
#### Scenario: company invoice requires tax number
|
||||
- **WHEN** requesting a company invoice without tax_no
|
||||
- **THEN** the API returns 400
|
||||
|
||||
#### Scenario: duplicate rejected
|
||||
- **WHEN** an order already has a requested or issued invoice
|
||||
- **THEN** a second request returns 409
|
||||
|
||||
### Requirement: Invoice issuance
|
||||
Merchants SHALL issue requested invoices of their own shop's orders; issuing sets invoice_no, issued_at and status `issued`.
|
||||
|
||||
#### Scenario: issue flow
|
||||
- **WHEN** the shop issues a requested invoice
|
||||
- **THEN** the customer sees status `issued` with an invoice number
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# order Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change cart-checkout-orders. Update Purpose after archive.
|
||||
## Requirements
|
||||
### Requirement: Checkout splits by shop
|
||||
`POST /api/orders/checkout` SHALL create one order per distinct shop in the cart, in a single database transaction: stock decrement, order + item insert with price snapshots, cart clear. All amounts use the cart's SKU currencies converted into the buyer-chosen display currency at checkout time.
|
||||
|
||||
#### Scenario: two shops → two orders
|
||||
- **WHEN** the cart contains SKUs from shops A and B
|
||||
- **THEN** two orders are created, each with only its shop's items, and the cart is empty
|
||||
|
||||
#### Scenario: insufficient stock
|
||||
- **WHEN** any line's qty exceeds SKU stock
|
||||
- **THEN** the whole checkout returns 409 and no order is created and stock is unchanged
|
||||
|
||||
### Requirement: Order lifecycle
|
||||
Status transitions SHALL be: pending_payment → paid → fulfilling → shipped → completed; cancellable only from pending_payment, which MUST restore stock.
|
||||
|
||||
#### Scenario: cancel restores stock
|
||||
- **WHEN** a customer cancels a pending_payment order
|
||||
- **THEN** stock of each SKU increases by the ordered qty and status is cancelled
|
||||
|
||||
#### Scenario: illegal transition rejected
|
||||
- **WHEN** cancelling a paid order via the customer endpoint
|
||||
- **THEN** the API returns 409
|
||||
|
||||
### Requirement: Order ownership
|
||||
Customers SHALL see only their own orders; shop roles only their shop's orders; platform_admin sees all.
|
||||
|
||||
#### Scenario: cross-customer read denied
|
||||
- **WHEN** customer X requests customer Y's order id
|
||||
- **THEN** the API returns 404
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# rbac Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change foundation-auth. Update Purpose after archive.
|
||||
## Requirements
|
||||
### Requirement: Role model
|
||||
The system SHALL support roles `platform_admin`, `shop_owner`, `shop_staff`, `customer`. Shop roles MUST carry a `shop_id` scope.
|
||||
|
||||
#### Scenario: seeded platform admin
|
||||
- **WHEN** migrations run on a fresh database
|
||||
- **THEN** a `platform_admin` account exists and can log in
|
||||
|
||||
### Requirement: Role enforcement
|
||||
Protected routes SHALL declare required roles; the API MUST reject requests with insufficient role using 403.
|
||||
|
||||
#### Scenario: customer hits admin route
|
||||
- **WHEN** a `customer` token calls an `/api/admin/*` route
|
||||
- **THEN** the API returns 403 with code `FORBIDDEN`
|
||||
|
||||
#### Scenario: shop scope isolation
|
||||
- **WHEN** a `shop_owner` of shop A accesses `/api/shop/*` resources of shop B
|
||||
- **THEN** the API returns 403 or 404, never the data
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# shipment Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change fulfillment-invoices. Update Purpose after archive.
|
||||
## Requirements
|
||||
### Requirement: Shipment creation
|
||||
Merchants SHALL create shipments (发货单) for their own orders in `paid` or `fulfilling` status, specifying carrier, tracking_no and per-item quantities.
|
||||
|
||||
#### Scenario: partial shipment
|
||||
- **WHEN** an order has 3 units of item X and a shipment covers 2
|
||||
- **THEN** a later shipment may cover the remaining 1; requesting more than the remainder returns 400
|
||||
|
||||
#### Scenario: status propagation
|
||||
- **WHEN** the first shipment is created for a paid order
|
||||
- **THEN** the order becomes `fulfilling`
|
||||
- **WHEN** all ordered quantities are covered by shipped shipments
|
||||
- **THEN** the order becomes `shipped`
|
||||
|
||||
### Requirement: Delivery confirmation
|
||||
Customers SHALL confirm delivery of their own shipments; when every shipment of an order is delivered the order becomes `completed`.
|
||||
|
||||
#### Scenario: confirm delivered
|
||||
- **WHEN** the customer confirms the only shipment of a shipped order
|
||||
- **THEN** shipment becomes `delivered` and the order `completed`
|
||||
|
||||
Reference in New Issue
Block a user