diff --git a/openspec/MIGRATION-PLAN.md b/openspec/MIGRATION-PLAN.md new file mode 100644 index 0000000..d98949b --- /dev/null +++ b/openspec/MIGRATION-PLAN.md @@ -0,0 +1,58 @@ +# Tigshop → VMall Migration Plan + +> 临时跟踪文档:每实现并验证一个 change(tasks 全勾、`openspec archive` 完成、`openspec validate --all --strict` 全绿)后,更新本文档状态表。**8 个 change 全部归档后删除本文件。** + +来源:对 `/Users/chengdzhang/github/jamyun/tigshop`(Admin + Uniapp + PHP 后端)的只读盘点。迁移的是领域模型与信息架构,不是代码。 + +## 明确排除(不迁移) + +- 微信小程序专属能力(wx.login / requestPayment / 订阅消息 / 分享 / 企业客服) +- 供应商(vendor)三层体系 —— 架构级决策,非功能迁移 +- 分销(tigshop 自身半成品)、完整拖拽装修器、电子卡密/虚拟商品、直播、打印、内置 IM + +## 依赖图 + +``` +P6 content-admin-ui ──┐(无依赖,热身) +P1 product-reviews ───┤(无依赖,可并行) +P5 onboarding ────────┤(无依赖,可并行) +P7 membership ────────┤(无依赖,可并行) + │ +P0 aftersale ─────────┤ + │ │ + ├─→ P3 wallet-settlement(结算需扣减退款,硬依赖 P0 的 aftersales 表) + │ │ + └────────┴─→ P4 mobile-h5(售后/钱包页面消费 P0/P3 API;核心链路只依赖现有 API,可与 P3 并行起步) +P2 freight ───────────┘(改 checkout/order totals,与 P0 同触 order 模块,建议与 P0 串行) +``` + +## 推荐实施波次 + +| 波次 | Changes | 说明 | +|---|---|---| +| Wave 1 | P6 → P0 → P2 → P1 | P6 热身(纯前端);P0/P2 都动 order 模块故串行;P1 任意位置插入 | +| Wave 2 | P3、P5、P7 | P3 必须在 P0 归档后启动;P5/P7 随时可插 | +| Wave 3 | P4 | 移动 H5 收尾,消费前面所有 API;P0/P3 未完成时对应页面降级 Coming soon | + +## 状态跟踪 + +| # | Change | 依赖 | 状态 | 归档日期 | +|---|---|---|---|---| +| P0 | `add-aftersale-refunds` | — | proposed | — | +| P1 | `add-product-reviews` | — | proposed | — | +| P2 | `add-freight-templates` | —(与 P0 串行) | proposed | — | +| P3 | `add-wallet-settlement` | P0 | proposed | — | +| P4 | `add-mobile-h5` | P0、P3(软) | proposed | — | +| P5 | `add-merchant-onboarding` | — | proposed | — | +| P6 | `add-content-admin-ui` | — | proposed | — | +| P7 | `add-membership-messaging` | — | proposed | — | + +状态取值:`proposed` → `implementing` → `verified`(tasks 全勾 + 测试/构建/smoke 通过)→ `archived`。 + +## 更新规则 + +1. 开始实现某 change:状态改 `implementing`。 +2. 实现并验证完成(含 `cargo test -p vmall-api` 连跑两次全绿、受影响前端 build、浏览器 smoke):状态改 `verified`。 +3. `openspec archive --yes` 且 `openspec validate --all --strict` 全绿:状态改 `archived`,填归档日期。 +4. 实施中发现新的跨 change 依赖:先更新本文件依赖图,再继续。 +5. 全部 `archived` 后:删除本文件。 diff --git a/openspec/changes/add-aftersale-refunds/proposal.md b/openspec/changes/add-aftersale-refunds/proposal.md new file mode 100644 index 0000000..48b4dcd --- /dev/null +++ b/openspec/changes/add-aftersale-refunds/proposal.md @@ -0,0 +1,29 @@ +## Why + +The order lifecycle currently stops after confirmation of delivery. Without a per-line after-sale and refund path, a paid order has no marketplace-safe recovery when goods or fulfillment fail. This is a baseline commerce gap, not an optional enhancement. + +## What Changes + +- Add customer-owned refund-only and return-refund applications for paid or shipped order items, with localized reasons, integer minor-unit amounts, and evidence URLs. +- Add guarded merchant decisions, buyer return tracking, bilateral messages, one-time rejected appeals, and optional platform arbitration. +- Credit completed refunds to `customer_accounts` through the existing append-only ledger and aggregate refunded order totals without a real payment gateway. +- Expose the flow in Mall, shop-admin, and platform-admin while preserving a deterministic Mall mock adapter and `LIVE_PICKS` wiring. + +## Capabilities + +### New Capabilities +- `aftersale`: Per-order-item applications, state transitions, messages, arbitration, and ledger-backed refund completion. + +### Modified Capabilities +- `order`: After-sale eligibility windows, per-line refundable amounts, and authoritative refund totals. +- `frontend-mall`: Customer after-sale application, tracking, messages, and history surfaces. +- `frontend-shop-admin`: Shop-scoped after-sale processing workspace. +- `frontend-admin`: Platform read-only monitoring and dispute arbitration. + +## Non-goals + +Real payment-channel refunds, exchanges, reshipments, shipping insurance, and IM negotiation are excluded; messages replace live negotiation. + +## Impact + +Adds one after-sale migration, shared API contracts, Rust module and integration coverage, customer and console pages, fixed-adapter parity, and account-ledger/order-total integration. \ No newline at end of file diff --git a/openspec/changes/add-aftersale-refunds/specs/aftersale/spec.md b/openspec/changes/add-aftersale-refunds/specs/aftersale/spec.md new file mode 100644 index 0000000..84da813 --- /dev/null +++ b/openspec/changes/add-aftersale-refunds/specs/aftersale/spec.md @@ -0,0 +1,79 @@ +## ADDED Requirements + +### Requirement: Per-line aftersale application +An authenticated customer SHALL apply for after-sale against one owned order item from an order that is paid or shipped and within the configured after-sale window. The application SHALL choose exactly `refund_only` or `return_refund`, include a localized reason, an integer minor-unit refund amount greater than zero and no greater than the line's remaining refundable amount, and zero or more evidence image URLs. The API SHALL reject unavailable, already fully refunded, out-of-window, or cross-customer items. + +#### Scenario: customer requests a partial refund +- **WHEN** a customer submits a `refund_only` application for a paid order item with an amount within the remaining refundable minor-unit balance +- **THEN** one `pending` aftersale is created with the customer, order, shop, and item ownership captured + +#### Scenario: amount cannot exceed the line balance +- **WHEN** the requested amount is greater than the order item's paid amount less previously completed refunds +- **THEN** the API returns 409 and creates no application + +#### Scenario: evidence URLs are retained +- **WHEN** a customer submits evidence image URLs with an application +- **THEN** the detail response returns the same validated URL list without storing binary image data + +### Requirement: Guarded aftersale state machine +Aftersales SHALL use guarded status transitions: `pending` to `approved` or `rejected`; for `refund_only`, `approved` SHALL transition to `refunded` through the ledger-backed completion path; `return_refund` SHALL proceed from `approved` to `buyer_shipping` to `merchant_confirmed` to `refunded`. A customer MAY cancel any non-terminal pending state before refund completion. A rejected application MAY be reopened to `pending` at most once. Every transition SHALL condition its update on the expected previous status and return 409 for stale or illegal actions. + +#### Scenario: merchant approves a return +- **WHEN** the owning shop approves a pending `return_refund` application +- **THEN** its status becomes `approved` and the customer can submit return tracking + +#### Scenario: merchant confirms returned goods +- **WHEN** the owning shop confirms receipt for a `buyer_shipping` application and completes the refund action +- **THEN** the service records the `merchant_confirmed` step and reaches `refunded` only through the guarded refund-completion transaction + +#### Scenario: buyer cancels before completion +- **WHEN** the customer cancels a still-pending aftersale before refund completion +- **THEN** the status becomes `cancelled` and a later merchant transition returns 409 + +#### Scenario: rejected appeal is limited +- **WHEN** a customer reopens a rejected application for the first time +- **THEN** it returns to `pending`; a second reopen attempt returns 409 + +### Requirement: Unique active aftersale per order item +The persistence layer SHALL permit at most one active aftersale for an order item at a time. Active statuses SHALL include every non-terminal application state, while `refunded`, `rejected`, and `cancelled` records remain historical. A reopened rejection SHALL reuse its record and count as the one active application. + +#### Scenario: duplicate active application +- **WHEN** two requests concurrently apply for aftersale on the same order item +- **THEN** at most one succeeds and the other returns 409 without a second active row + +#### Scenario: historical record does not block a new request +- **WHEN** a prior application is rejected or cancelled and no other active application exists +- **THEN** the customer can create a new application subject to the remaining refundable amount and window + +### Requirement: Bilateral aftersale messages +Customers and the owning shop's authorized users SHALL append messages to an aftersale message log as buyer or merchant, with localized JSONB content, optional evidence URLs, and immutable author/timestamp metadata. Readers SHALL be limited to the customer, the owning shop under `own_shop`, and platform administrators. + +#### Scenario: buyer adds a message +- **WHEN** the customer posts a message on their aftersale +- **THEN** the message is appended and appears in chronological detail history + +#### Scenario: unrelated shop cannot read messages +- **WHEN** a shop user requests an aftersale belonging to another shop +- **THEN** the API returns 404 or 403 and reveals no message content + +### Requirement: Ledger-backed refund completion +A refund completion SHALL run in one transaction with a guarded status update, a guarded increment of the order's integer `refund_total_minor`, and one append-only available-balance credit in `customer_accounts` using the order currency and aftersale reference. The `refund_completed` hook SHALL be emitted after commit. Retrying a completed action SHALL not create another ledger entry or increase the order total twice. + +#### Scenario: completed refund credits the customer +- **WHEN** a valid aftersale reaches `refunded` +- **THEN** the customer's account balance and immutable ledger entry increase by exactly the requested minor-unit amount, and the order refund total increases by the same amount + +#### Scenario: concurrent completion is idempotent +- **WHEN** two workers attempt to complete the same merchant-confirmed aftersale +- **THEN** one guarded transition performs the credit and the other returns the already-completed result without a duplicate entry + +### Requirement: Optional platform arbitration +A platform administrator SHALL view aftersale applications across shops and MAY resolve an escalated application with a terminal refund or rejection. Arbitration SHALL enforce the same ownership-independent guarded transitions, amount limits, order-total and ledger invariants, and immutable audit/message record as merchant processing. + +#### Scenario: platform grants a disputed refund +- **WHEN** a platform administrator resolves an eligible dispute in the customer's favor +- **THEN** the application reaches `refunded` through the same ledger-backed completion path + +#### Scenario: platform rejects a dispute +- **WHEN** a platform administrator rejects an escalated application +- **THEN** it reaches terminal `rejected` and cannot be refunded without the one permitted customer reopen diff --git a/openspec/changes/add-aftersale-refunds/specs/frontend-admin/spec.md b/openspec/changes/add-aftersale-refunds/specs/frontend-admin/spec.md new file mode 100644 index 0000000..e2dce4d --- /dev/null +++ b/openspec/changes/add-aftersale-refunds/specs/frontend-admin/spec.md @@ -0,0 +1,16 @@ +## ADDED Requirements + +### Requirement: Platform aftersale monitoring and arbitration +The platform admin console SHALL provide a read-only cross-shop aftersale list and detail view with customer, shop, order-item, amount, evidence, status, message history, and refund-total context. A platform administrator SHALL be able to resolve an escalated dispute with a terminal refund or rejection through the guarded service path, without directly editing balances or bypassing shop ownership checks. + +#### Scenario: admin inspects a cross-shop application +- **WHEN** a platform administrator opens the aftersale workspace +- **THEN** applications from all shops are listed with filters and authoritative status and refund amounts + +#### Scenario: admin grants terminal refund +- **WHEN** a platform administrator resolves an eligible dispute in favor of the customer +- **THEN** the aftersale reaches refunded, the customer ledger is credited once, and the order refund total refreshes from the API + +#### Scenario: admin rejects terminal dispute +- **WHEN** a platform administrator rejects an escalated dispute +- **THEN** the aftersale reaches rejected and the console shows that only the permitted one-time customer reopen can resume it diff --git a/openspec/changes/add-aftersale-refunds/specs/frontend-mall/spec.md b/openspec/changes/add-aftersale-refunds/specs/frontend-mall/spec.md new file mode 100644 index 0000000..fe80aff --- /dev/null +++ b/openspec/changes/add-aftersale-refunds/specs/frontend-mall/spec.md @@ -0,0 +1,24 @@ +## ADDED Requirements + +### Requirement: Mall aftersale customer flow +The Mall SHALL use the shared selected API adapter to expose an after-sale action for eligible order items, an aftersale list and detail view, chronological buyer/merchant messages, cancellation and one-time rejected appeal, and return-refund shipping tracking. The form SHALL show the localized reason/type, integer minor-unit amount and remaining limit, evidence URL inputs, current status, and authoritative refund result. Anonymous actions SHALL redirect to sign-in with the current route as return destination. + +#### Scenario: apply from an eligible order line +- **WHEN** a signed-in shopper opens a paid or shipped order inside its after-sale window and submits a valid item application +- **THEN** the Mall creates the aftersale through the shared client and shows its pending status without fixture-only state + +#### Scenario: return shipping is recorded +- **WHEN** an approved return-refund shopper submits carrier and tracking data +- **THEN** the detail view shows the buyer-shipping status and the persisted tracking information after reload + +#### Scenario: shopper cancels an application +- **WHEN** a shopper cancels a non-terminal pending aftersale +- **THEN** the API state and list/detail views show cancelled and merchant actions are no longer offered + +#### Scenario: shopper messages the merchant +- **WHEN** a shopper sends a localized message with optional evidence URLs +- **THEN** it appears in chronological detail history and remains after a reload + +#### Scenario: fixed adapter remains usable +- **WHEN** the aftersale domain is configured for fixed data +- **THEN** Mall list, detail, messages, cancellation, tracking, and refund-history flows behave deterministically through the same shared methods diff --git a/openspec/changes/add-aftersale-refunds/specs/frontend-shop-admin/spec.md b/openspec/changes/add-aftersale-refunds/specs/frontend-shop-admin/spec.md new file mode 100644 index 0000000..a60d8db --- /dev/null +++ b/openspec/changes/add-aftersale-refunds/specs/frontend-shop-admin/spec.md @@ -0,0 +1,20 @@ +## ADDED Requirements + +### Requirement: Shop-scoped aftersale workspace +Shop-admin SHALL provide an aftersale list and detail workspace filtered to the authenticated shop's `own_shop` resources. Authorized shop users SHALL inspect order-item evidence and messages, approve or reject pending applications, and confirm returned goods with the guarded refund action. The UI SHALL show status transitions, remaining amount, ledger-backed refund result, and stale-action errors through the shared API contract. + +#### Scenario: merchant approves a request +- **WHEN** a shop user opens a pending application for an item belonging to their shop and approves it +- **THEN** the status advances according to the selected aftersale type and the customer can see the persisted result + +#### Scenario: merchant confirms return and refunds +- **WHEN** a shop user confirms receipt of a buyer-shipped return +- **THEN** the service records merchant confirmation, credits the customer's account once, and displays the refunded status and order total + +#### Scenario: shop scope is enforced +- **WHEN** a shop user requests or mutates an aftersale for another shop +- **THEN** the API denies the operation and the workspace exposes no cross-shop data + +#### Scenario: merchant messages buyer +- **WHEN** an authorized shop user appends a localized message +- **THEN** the message appears in the same chronological aftersale thread visible to the buyer diff --git a/openspec/changes/add-aftersale-refunds/specs/order/spec.md b/openspec/changes/add-aftersale-refunds/specs/order/spec.md new file mode 100644 index 0000000..dd5d970 --- /dev/null +++ b/openspec/changes/add-aftersale-refunds/specs/order/spec.md @@ -0,0 +1,27 @@ +## ADDED Requirements + +### Requirement: Order aftersale eligibility and refund summary +Order and order-item responses SHALL expose the configured after-sale deadline after confirmation of delivery, whether each line has an active aftersale, its remaining refundable integer minor-unit amount, and the order's authoritative `refund_total_minor`. The API SHALL allow applications for paid or shipped orders while the configured window is open, and SHALL reject new applications after the deadline. The refund summary SHALL be sourced from completed `aftersales` records and updated only by a guarded refund-completion transaction. + +#### Scenario: confirmed order remains eligible during the window +- **WHEN** a customer views a completed order before the configured N-day after-sale window expires +- **THEN** the order detail includes an after-sale deadline and each refundable line exposes its remaining amount and application action + +#### Scenario: expired window hides the action +- **WHEN** the configured after-sale deadline has passed +- **THEN** the order and item responses mark after-sale unavailable and an application returns 409 + +#### Scenario: order refund total is authoritative +- **WHEN** an aftersale completes a refund +- **THEN** the order response's `refund_total_minor` equals the sum of completed aftersale amounts and is not client-calculated + +### Requirement: Order item ownership and state safety +After-sale eligibility SHALL resolve the order item through the authenticated customer's order ownership (or the owning shop's `own_shop` scope for merchant actions), and every refund total update SHALL use a conditional status/amount guard so concurrent completions cannot refund more than the paid order amount. + +#### Scenario: cross-customer item is hidden +- **WHEN** a customer submits another customer's order item id +- **THEN** the API returns 404 without disclosing order or refund data + +#### Scenario: concurrent refunds stay within paid amount +- **WHEN** concurrent aftersales would make completed refunds exceed the line or order paid amount +- **THEN** the guarded update rejects the excess completion and stored refund totals remain within the paid amount diff --git a/openspec/changes/add-aftersale-refunds/tasks.md b/openspec/changes/add-aftersale-refunds/tasks.md new file mode 100644 index 0000000..f3c6516 --- /dev/null +++ b/openspec/changes/add-aftersale-refunds/tasks.md @@ -0,0 +1,31 @@ +## 1. Persistence and shared contract + +- [ ] 1.1 Add migration `0017_aftersales.sql` with `aftersales` and append-only `aftersale_messages` tables, order/customer/shop/order-item foreign keys, `refund_only`/`return_refund` type checks, status checks, i64 minor-unit amounts, evidence URL arrays, localized JSONB reason/content fields, timestamps, ownership indexes, and a partial unique index allowing only one active aftersale per `order_item_id`. +- [ ] 1.2 Add shared aftersale enums, DTOs, paginated summaries, state/action types, bilingual reason/message fields, and API client methods for customer, shop-admin, and platform-admin flows. Keep all money as integer minor units and expose `refund_total_minor`/remaining refundable amounts from the contract. + +## 2. Rust aftersale module and behavioral proof + +- [ ] 2.1 Implement `apps/api/src/modules/aftersale/` repository, service, DTO, handlers, routes, and module registration for customer applications/list/detail/cancel/reopen/messages/return tracking, shop processing, and platform arbitration. +- [ ] 2.2 Enforce paid-or-shipped order-item eligibility, configured post-confirmation window, amount not exceeding the guarded remaining refundable amount, evidence URL validation, one active aftersale per line, customer ownership, `own_shop` scope for shop roles, and platform-admin arbitration. +- [ ] 2.3 Implement guarded state transitions: `pending` to `approved` or `rejected`; return-refund `approved` to `buyer_shipping` to `merchant_confirmed` to `refunded`; cancellation from any non-terminal pending state; one rejected-to-`pending` reopen appeal. Reject stale transitions with 409. +- [ ] 2.4 Complete refunds transactionally: conditionally update the aftersale and order refund total, append one `customer_accounts` ledger credit, emit the hookable `refund_completed` event, and make retries idempotent so no duplicate credit is possible. +- [ ] 2.5 Add focused API integration tests under `apps/api/tests/` reusing `tests/common/mod.rs` for eligibility/window boundaries, amount guards, uniqueness, ownership/RBAC, every transition and illegal transition, one-time reopen, messages, ledger/order totals, concurrent completion, and idempotent retry. + +## 3. Mall customer surfaces and adapter parity + +- [ ] 3.1 Add the aftersale methods and deterministic mutable fixtures to `apps/mall/mock/api.ts`, preserving the shared contract for applications, messages, cancellation, reopen, return tracking, and refund totals. +- [ ] 3.2 Add the aftersale domain and exact method picks to Mall `LIVE_PICKS`; keep fixed fallback behavior and localized en/zh labels in `@vmall/shared`. +- [ ] 3.3 Add Mall order-detail eligible-item entry, aftersale list/detail, reason/amount/evidence form, bilateral message thread, cancel/reopen actions, and return-shipping tracking form, with sign-in redirect and reload-safe state. + +## 4. Merchant and platform console surfaces + +- [ ] 4.1 Add shop-admin aftersale workspace with shop-scoped list/detail filters, item and evidence display, bilingual messages, approve/reject actions, return-receipt-and-refund action, and guarded error states. +- [ ] 4.2 Add platform-admin read-only aftersale list/detail and dispute arbitration actions for terminal refund or rejection, with role-protected routes and visible ledger/order-total outcomes. +- [ ] 4.3 Route all three frontends through `@vmall/shared` contracts and `@vmall/ui` primitives; do not duplicate API types or bypass `own_shop` authorization. + +## 5. Verification + +- [ ] 5.1 Run the aftersale integration tests in `apps/api/tests/` with the shared `tests/common/mod.rs` fixtures and exercise customer, merchant, and platform paths. +- [ ] 5.2 Build affected frontends: `pnpm --filter @vmall/mall build`, `pnpm --filter @vmall/shop-admin build`, and `pnpm --filter @vmall/admin build`. +- [ ] 5.3 Browser-smoke the Mall application/detail/message/cancel/return-tracking/refund-history flow, shop-admin processing, and platform arbitration against the local stack, including fixed-adapter fallback. +- [ ] 5.4 Run `openspec change validate add-aftersale-refunds --strict` and `openspec validate --all --strict`. \ No newline at end of file diff --git a/openspec/changes/add-content-admin-ui/proposal.md b/openspec/changes/add-content-admin-ui/proposal.md new file mode 100644 index 0000000..60bcfad --- /dev/null +++ b/openspec/changes/add-content-admin-ui/proposal.md @@ -0,0 +1,34 @@ +## Why + +The home-content APIs (`GET /api/admin/content`, `PUT /api/admin/content/{kind}` for banners, promos, quick links and floor adverts) and the brand registry APIs (`GET /api/brands`, `PUT /api/admin/brands`) already exist and already feed the mall, but the platform console has no pages for them: content and brands can only be changed with hand-built API calls. A shop profile is likewise writable only through the platform-admin endpoint `PUT /api/admin/shops/{id}/profile`, so a merchant cannot maintain their own store identity, contact and service copy at all. + +## What Changes + +- Add a content management page to `apps/admin` with one editor per home-content kind (banners, promos, quick links, floor adverts): ordered rows with add, remove and reorder, per-entry fields and active flags, form validation mirroring the API rules, a storefront preview, and per-kind whole-list replacement saves with the exact semantics of the existing `PUT /api/admin/content/{kind}`. +- Add a brand management page to `apps/admin` editing the whole ordered brand list (slug, bilingual name, active flag) and saving it with the exact semantics of the existing `PUT /api/admin/brands`, with validation, preview and save feedback. +- Add a shop profile editing page to `apps/shop-admin` where a merchant maintains their own logo, banner, company, region, and bilingual address, notice and after-sale copy through the shared API adapter. +- Add one minimal merchant-scoped endpoint, `PUT /api/shop/profile`, plus its `@vmall/shared` contract method, because no merchant-writable profile route exists today; profile scores stay platform-set. + +## Capabilities + +### New Capabilities + +None. + +### Modified Capabilities + +- `frontend-admin`: platform-console pages for storefront home content and the brand registry, with per-row form validation, storefront preview, and explicit whole-list save feedback. +- `frontend-shop-admin`: merchant self-editing of the shop profile through the shop-scoped endpoint, with bilingual validation and read-only platform-set scores. +- `store-directory`: shop owners may write their own shop profile through `PUT /api/shop/profile` under the `own_shop` scope; the platform-admin write and the public reads keep their existing behavior. + +## Non-goals + +Drag-and-drop storefront decoration, draft/publish states for content entries, multi-page templates, and full shop storefront decoration (the tigshop store-fitting scope) are excluded. Media upload and hosting are excluded (entries keep carrying image URLs), as are a platform-admin UI for shop profiles (the existing admin API stays as-is), mall rendering changes for content and brands, brand-to-product assignment UI, and per-entry scheduling or analytics. + +## Impact + +- `apps/admin`: new content and brands pages, nav entries, and bilingual strings in the existing admin locale sources. +- `apps/shop-admin`: new shop profile page, nav entry, and bilingual strings in the existing shop-admin locale sources. +- `apps/api`: one new shop-scoped route `PUT /api/shop/profile` reusing the existing profile upsert and bilingual validation (no migration), with integration coverage in `apps/api/tests/`. +- `@vmall/shared`: one `shop.updateMyProfile` method; the existing content, brand and store-directory contract methods are consumed unchanged. +- No database migration and no change to the public storefront contract. diff --git a/openspec/changes/add-content-admin-ui/specs/frontend-admin/spec.md b/openspec/changes/add-content-admin-ui/specs/frontend-admin/spec.md new file mode 100644 index 0000000..18afe5f --- /dev/null +++ b/openspec/changes/add-content-admin-ui/specs/frontend-admin/spec.md @@ -0,0 +1,31 @@ +## ADDED Requirements + +### Requirement: Storefront content management +The platform console SHALL manage the four home-content kinds — banners, promos, quick links and floor adverts — on a content page reachable from the console navigation, loading every entry including inactive ones through the shared API adapter. Each kind SHALL be edited as its ordered list of rows with add, remove and reorder controls (no drag and drop), the per-entry fields the API accepts, an active flag per entry, and a preview of how the storefront renders each entry. Saving a kind SHALL submit the whole edited list in its displayed order as one replacement with the semantics of `PUT /api/admin/content/{kind}`, so positions follow the submitted order and the other three kinds are untouched. Rows SHALL be validated before any request with the rules the API enforces — non-empty image and destination URLs, non-empty glyph, and non-empty `en` and `zh` quick-link labels in `{ en, zh }` JSONB text — with inline field errors on failing rows. Saves SHALL give clear success feedback; a rejected save SHALL keep the edited rows and show a visible failure message. + +#### Scenario: save replaces one kind only +- **WHEN** an admin reorders banners, saves that kind, and the mall reloads its home content +- **THEN** banners appear in the new order while promos, quick links and floor adverts are unchanged + +#### Scenario: invalid row is blocked before submit +- **WHEN** an admin leaves a banner image URL empty and saves +- **THEN** the row shows an inline field error and no replacement request is sent + +#### Scenario: rejected save keeps the editor state +- **WHEN** the API rejects a replacement +- **THEN** the page shows a failure message and keeps the edited rows for correction + +### Requirement: Brand management +Platform admins SHALL manage the ordered brand registry on a brands page reachable from the console navigation, through the shared API adapter. The page SHALL edit the whole list — slug, non-empty `{ en, zh }` names and an active flag per row — with add, remove and reorder controls and a preview of each brand as the public list serves it. Saving SHALL replace the whole list in its displayed order so positions follow the submitted order. Slugs SHALL be validated against the ascii `a-z`, `0-9`, `-` pattern and duplicate slugs and incomplete bilingual names SHALL be rejected with inline errors before any request is sent. Saves SHALL give clear success feedback; a rejected save SHALL keep the edited rows and show a visible failure message. + +#### Scenario: replace round-trips to the public list +- **WHEN** an admin reorders brands, renames one, and saves +- **THEN** the public brand list returns the same entries in the submitted order + +#### Scenario: duplicate slug is blocked before submit +- **WHEN** an admin saves with two rows carrying the same slug +- **THEN** the duplicate rows show inline errors and no replacement request is sent + +#### Scenario: save feedback distinguishes success and failure +- **WHEN** a save succeeds or the API refuses the list +- **THEN** the page shows a success confirmation or keeps the edited rows with a failure message, respectively diff --git a/openspec/changes/add-content-admin-ui/specs/frontend-shop-admin/spec.md b/openspec/changes/add-content-admin-ui/specs/frontend-shop-admin/spec.md new file mode 100644 index 0000000..b364268 --- /dev/null +++ b/openspec/changes/add-content-admin-ui/specs/frontend-shop-admin/spec.md @@ -0,0 +1,20 @@ +## ADDED Requirements + +### Requirement: Merchant shop profile editing +Shop users SHALL edit their own shop's profile from a shop-admin page reachable from the shop-scoped navigation, through the shared API adapter's shop-scoped profile update. The form SHALL prefill from the shop's current profile and cover the merchant-writable fields only: logo and banner URLs, company, region, and `{ en, zh }` bilingual address, notice and after-sale copy. Bilingual fields SHALL be validated with non-empty `en` and `zh` text before any request is sent, every write SHALL target the caller's own shop, and profile scores SHALL NOT be editable — they remain platform-set and saving never changes them. Saves SHALL give clear success feedback; a refused save SHALL keep the form values and show a visible failure message. + +#### Scenario: merchant updates their own profile +- **WHEN** a shop owner edits their notice, saves, and the mall store page reloads +- **THEN** only that shop shows the new notice and no other shop's profile changes + +#### Scenario: incomplete bilingual text is blocked before submit +- **WHEN** a shop owner leaves the `zh` after-sale text empty and saves +- **THEN** the field shows an inline error and no update request is sent + +#### Scenario: scores are not editable +- **WHEN** the profile page loads and is saved +- **THEN** no score fields are offered and the platform-set scores remain unchanged + +#### Scenario: refused save keeps the form +- **WHEN** the API refuses an update +- **THEN** the page keeps the entered values and shows a failure message diff --git a/openspec/changes/add-content-admin-ui/specs/store-directory/spec.md b/openspec/changes/add-content-admin-ui/specs/store-directory/spec.md new file mode 100644 index 0000000..1ae0573 --- /dev/null +++ b/openspec/changes/add-content-admin-ui/specs/store-directory/spec.md @@ -0,0 +1,20 @@ +## ADDED Requirements + +### Requirement: Merchant self-service shop profile +A shop owner SHALL set their own shop's profile with `PUT /api/shop/profile`, which upserts the profile row of the shop the caller owns and returns the composed shop profile. The write SHALL require an authenticated user with a shop under the `own_shop` scope and SHALL accept logo and banner URLs, company, region, and bilingual `address`, `notice` and `after_sale` text with the same `{ en, zh }` validation as the platform-admin profile write. Profile scores remain platform-set: merchant-supplied `score_rating`, `score_agreement`, `score_service` or `score_speed` values SHALL NOT be stored. A user without a shop SHALL be refused, and the platform-admin write and public reads SHALL keep their existing behavior. + +#### Scenario: merchant upsert round-trips +- **WHEN** a shop owner sets their profile and the storefront reads the shop by slug +- **THEN** the public read returns those values for that shop + +#### Scenario: incomplete bilingual text is refused +- **WHEN** a shop owner submits a notice with only `en` text +- **THEN** the request is refused and the stored profile is unchanged + +#### Scenario: merchant cannot set scores +- **WHEN** a shop owner submits score values with their profile +- **THEN** the stored scores are unchanged + +#### Scenario: a user without a shop is refused +- **WHEN** a signed-in customer sends the merchant profile write +- **THEN** the API refuses the write diff --git a/openspec/changes/add-content-admin-ui/tasks.md b/openspec/changes/add-content-admin-ui/tasks.md new file mode 100644 index 0000000..c395384 --- /dev/null +++ b/openspec/changes/add-content-admin-ui/tasks.md @@ -0,0 +1,28 @@ +## 1. Admin content management page + +- [ ] 1.1 Add `apps/admin/pages/content.vue` loading all four kinds through `admin.getContent()` from `@vmall/shared` (inactive entries included) and add a content entry to the `apps/admin` nav beside existing platform operations. +- [ ] 1.2 Implement one editor section per kind — banners, promos, quick links, floor adverts — with the exact per-entry fields the API accepts: image and destination URL for banners and promos, `{ en, zh }` label, destination URL and inline SVG glyph for quick links, and image for floor adverts, plus an active flag on every row. +- [ ] 1.3 Support ordered row editing per kind (add, remove, move up/down — no drag and drop) and pre-validate each row with the API rules (non-empty image and destination URLs, non-empty glyph, non-empty `en` and `zh` quick-link labels), showing inline field errors before any request is sent. +- [ ] 1.4 Save one kind at a time by submitting the whole displayed list through `admin.replaceContent(kind, items)` so the API replaces that kind atomically and reindexes positions, leaving the other three kinds untouched; render a per-entry storefront preview (image, destination, glyph/label) and clear success feedback on save. +- [ ] 1.5 On a rejected save keep the edited rows and show a visible failure message, and add the page's bilingual strings through the existing admin locale sources. + +## 2. Admin brand management page + +- [ ] 2.1 Add `apps/admin/pages/brands.vue` loading the registry through `admin.getBrands()` and add a brands entry to the `apps/admin` nav beside existing platform operations. +- [ ] 2.2 Edit the whole ordered list with add, remove and move up/down rows carrying slug, non-empty `{ en, zh }` names and an active flag; pre-validate slugs against the API pattern (ascii `a-z`, `0-9`, `-`) and reject duplicate slugs and incomplete bilingual names with inline errors before any request is sent. +- [ ] 2.3 Save the full list through `admin.replaceBrands(items)` so positions follow the displayed order, and render a preview row per brand as the public list serves it (slug and bilingual name) with clear success feedback on save. +- [ ] 2.4 On a rejected save keep the edited rows and show a visible failure message, and add the page's bilingual strings through the existing admin locale sources. + +## 3. Shop-admin profile editing + +- [ ] 3.1 Add the minimal merchant-scoped endpoint `PUT /api/shop/profile` in `apps/api/src/modules/shop/`: an authenticated write scoped with `own_shop` over the caller's shop, reusing the existing profile upsert and `{ en, zh }` bilingual validation, accepting logo, banner, company, region, address, notice and after_sale, and never storing `score_rating`, `score_agreement`, `score_service` or `score_speed`; the service returns `ApiResult`. +- [ ] 3.2 Add a `shop.updateMyProfile` method to the `@vmall/shared` contract (own-profile input without score fields, returning `ShopProfile`) with live-client wiring; all frontend calls go through this shared contract only. +- [ ] 3.3 Add `apps/shop-admin/pages/shop-profile.vue` editing the caller's own shop profile — logo and banner URLs, company, region and bilingual address, notice and after-sale copy — prefilled from `shop.getMyShop()` and the composed `getShop(slug)` read, with non-empty `en` and `zh` validation on bilingual fields and no score fields in the form. +- [ ] 3.4 Save through `shop.updateMyProfile`, keep the form values and show a visible failure message when the write is refused (success feedback otherwise), and add the shop-profile nav entry to `apps/shop-admin` with bilingual strings through the existing shop-admin locale sources. + +## 4. Verification + +- [ ] 4.1 Add integration coverage in `apps/api/tests/` reusing the `tests/common/mod.rs` fixtures for the new endpoint: a merchant upsert round-trips to the public shop read, incomplete bilingual text is refused with the stored profile unchanged, submitted score values leave the stored scores unchanged, a user without a shop is refused, and another shop's profile is untouched; run the focused `cargo test -p vmall-api --test shops`. +- [ ] 4.2 Build all three frontends because the shared contract changes: `pnpm --filter @vmall/admin build`, `pnpm --filter @vmall/shop-admin build`, and `pnpm --filter @vmall/mall build`. +- [ ] 4.3 Run the API and browser-smoke the admin content page (edit, reorder, per-kind save reflected on the mall home with the other kinds untouched), the admin brands page (edit and save reflected in the public brand list), and the shop-admin profile page (edit, save, and the mall store page showing the new copy for that shop only). +- [ ] 4.4 Check every OpenSpec task and run `openspec change validate add-content-admin-ui --strict` plus `openspec validate --all --strict`. diff --git a/openspec/changes/add-freight-templates/proposal.md b/openspec/changes/add-freight-templates/proposal.md new file mode 100644 index 0000000..75af04e --- /dev/null +++ b/openspec/changes/add-freight-templates/proposal.md @@ -0,0 +1,31 @@ +## Why + +Checkout currently sums item prices only, so orders carry no delivery cost and their totals are not what a buyer would really pay. Merchants have no way to charge different shipping for distant regions, heavier parcels, or small orders, and fulfillment cannot record which carrier moved a parcel. Adding freight templates and server-side shipping calculation makes order money truthful end to end. + +## What Changes + +- Add merchant-scoped freight templates: name, default-template flag, always-free toggle, pricing method `by_piece` or `by_weight`, first-unit and additional-unit fees in i64 minor units, integer first/additional unit sizes (pieces or grams), and an optional free-shipping threshold in i64 minor units. +- Add per-template region rules that override the template's default fees for a set of destination regions, using a simplified region code table / text matching explicitly scoped in the spec (not a full national region tree). +- Associate each product with a freight template, resolving fees product template first and falling back to the shop's default template. +- Calculate shipping on the server at checkout: group the shop's items by resolved template, merge quantities or integer weights per template group, apply always-free and threshold free shipping first, let matching region rules override the default first/additional fees, and write each shop order's fee into the order total. Client-supplied amounts are never trusted. +- Snapshot the pricing method used on order items and persist each order's `shipping_fee_minor`. +- Add a platform shipping-company dictionary and let merchants pick the company when shipping an order. +- Surface fees in the merchant console (freight template management, company selection) and in Mall checkout (per-shop delivery fee and total), behind the `@vmall/shared` contract with a fixed-adapter fallback. + +## Capabilities + +### New Capabilities +- `shipping`: Merchant freight templates with region rules and free-shipping policy, default-template resolution, server-side per-shop shipping fee calculation, and a shipping-company dictionary with merchant selection at fulfillment. + +### Modified Capabilities +- `order`: Checkout computes per-shop shipping fees from the shipping capability and persists shipping-fee and item pricing-method snapshots on the order. +- `frontend-shop-admin`: Shop users manage freight templates and region rules and choose a shipping company when shipping an order. +- `frontend-mall`: Checkout shows each shop order's delivery fee and the combined shipping total. + +## Non-goals + +A full national region tree and region database (a simplified region code table / text matching is used and its scope is documented in the spec), multi-warehouse dispatch, carrier tracking APIs, electronic waybills, volumetric weight, per-SKU shipping overrides beyond the product template link, and cross-shop free-shipping thresholds are excluded. + +## Impact + +Adds one Postgres migration (freight templates, region rules, shipping companies, and order/order-item columns), a Rust freight module with a pure calculation service wired into checkout, shared types and API methods in `@vmall/shared`, a Mall mock-adapter implementation and live-domain pick for the new shipping surface, shop-admin freight-template and fulfillment pages, and Mall checkout fee display. All money stays in i64 minor units and weights in integer grams; no floating-point amounts are introduced. diff --git a/openspec/changes/add-freight-templates/specs/frontend-mall/spec.md b/openspec/changes/add-freight-templates/specs/frontend-mall/spec.md new file mode 100644 index 0000000..932cffe --- /dev/null +++ b/openspec/changes/add-freight-templates/specs/frontend-mall/spec.md @@ -0,0 +1,16 @@ +## ADDED Requirements + +### Requirement: Checkout shipping fee display +Checkout SHALL show each shop group's delivery fee and the combined shipping total computed by the server, alongside the merchandise and order totals in the selected currency. Changing the selected shipping address SHALL refresh the fee breakdown before submission, and payment and order detail SHALL show the persisted `shipping_fee` from the order payload. Fees SHALL come only from the shared `@vmall/shared` contract; the fixed-data adapter SHALL implement the same quote surface as a fallback so checkout works without the backend. + +#### Scenario: per-shop fees at checkout +- **WHEN** a shopper's checkout spans two shops with different templates +- **THEN** each shop group shows its own delivery fee and the summary shows their sum + +#### Scenario: address change updates fees +- **WHEN** the shopper selects an address whose region rule raises shipping +- **THEN** the displayed per-shop fees and total update to the server's new calculation + +#### Scenario: fixed adapter quotes fees +- **WHEN** the shipping surface is configured to fixed data +- **THEN** checkout renders deterministic per-shop delivery fees through the same shared client methods diff --git a/openspec/changes/add-freight-templates/specs/frontend-shop-admin/spec.md b/openspec/changes/add-freight-templates/specs/frontend-shop-admin/spec.md new file mode 100644 index 0000000..d9e54d5 --- /dev/null +++ b/openspec/changes/add-freight-templates/specs/frontend-shop-admin/spec.md @@ -0,0 +1,19 @@ +## ADDED Requirements + +### Requirement: Merchant freight template management +Shop-admin SHALL provide a freight template management page for the signed-in shop user's own shop only, listing the shop's templates with their default flag, always-free toggle, pricing method, minor-unit fees, integer unit sizes, free-shipping threshold, and region rules. Create, edit, default-toggle, region-rule editing, and delete SHALL go through the shared `@vmall/shared` contract, and making a template default SHALL reflect the server's single-default outcome. The page SHALL use `@vmall/ui` primitives and shared tokens. + +#### Scenario: manage a template and its region rules +- **WHEN** a shop user creates a by-piece template, adds a region rule with higher fees, and saves +- **THEN** reloading the page shows the template and rule from the backend + +#### Scenario: default template toggle +- **WHEN** a shop user marks a second template as default +- **THEN** the page shows exactly that template as default after the server confirms + +### Requirement: Merchant shipping company selection +When a shop user ships an order from the fulfillment view, shop-admin SHALL offer the shipping companies from the shared dictionary and require a selection before marking the order shipped. The chosen company SHALL reach the backend through the shared contract and appear on the order after shipping. + +#### Scenario: ship with a selected company +- **WHEN** a shop user selects a shipping company and confirms shipment +- **THEN** the order shows the shipped state and the selected company diff --git a/openspec/changes/add-freight-templates/specs/order/spec.md b/openspec/changes/add-freight-templates/specs/order/spec.md new file mode 100644 index 0000000..0adf1aa --- /dev/null +++ b/openspec/changes/add-freight-templates/specs/order/spec.md @@ -0,0 +1,19 @@ +## ADDED Requirements + +### Requirement: Checkout computes per-shop shipping fees +The checkout request SHALL carry the destination address, and in the same transaction that creates the shop orders the server SHALL compute each shop order's shipping fee from the shipping capability's resolution and calculation rules and add it to that order's total. Per-shop fees and their sum SHALL be returned in the checkout response. Client-supplied shipping fees or totals SHALL NOT be persisted. + +#### Scenario: two shops pay two fees +- **WHEN** a checkout covers two shops whose templates both charge shipping to the destination +- **THEN** each created order carries its own computed shipping fee and the response reports both plus the sum + +#### Scenario: address change recomputes +- **WHEN** the buyer submits the same cart to a destination whose region rule raises the fee +- **THEN** the created orders' shipping fees reflect the new destination rather than any earlier quote + +### Requirement: Order shipping and pricing snapshots +Each order SHALL persist its shipping fee in i64 minor units alongside its other amounts, and each order item SHALL snapshot the freight pricing method and template applied at checkout. Order detail, payment, and history views SHALL read these snapshots so later template edits cannot change historical orders. + +#### Scenario: detail shows the persisted fee +- **WHEN** a customer or shop user opens an order created before a template's fees changed +- **THEN** the order detail shows the shipping fee and item pricing methods snapshotted at checkout diff --git a/openspec/changes/add-freight-templates/specs/shipping/spec.md b/openspec/changes/add-freight-templates/specs/shipping/spec.md new file mode 100644 index 0000000..410d561 --- /dev/null +++ b/openspec/changes/add-freight-templates/specs/shipping/spec.md @@ -0,0 +1,80 @@ +## ADDED Requirements + +### Requirement: Merchant freight template management +Shop users SHALL manage freight templates for their own shop only, scoped through `own_shop`, and platform administrators SHALL manage none implicitly. A template SHALL have a name, an optional always-free toggle, a pricing method of `by_piece` or `by_weight`, first-unit and additional-unit fees in i64 minor units of the shop currency, integer first/additional unit sizes (pieces for `by_piece`, grams for `by_weight`), and an optional free-shipping threshold in i64 minor units. At most one template per shop SHALL be the default, enforced by the database and preserved under concurrent default changes with guarded updates. Amounts SHALL be i64 minor units and weights integer grams; floating-point money is forbidden. Services SHALL return `ApiResult`. + +#### Scenario: create a template for the shop +- **WHEN** a shop user creates a by-weight template with a 500g first unit and minor-unit fees +- **THEN** the template is stored with integer fees and grams and is visible only to that shop's users + +#### Scenario: cross-shop template access denied +- **WHEN** a shop user reads or edits another shop's freight template +- **THEN** the request is rejected as not found or forbidden + +#### Scenario: single default preserved +- **WHEN** a shop user makes a second template the default while another default exists +- **THEN** exactly one template of that shop remains flagged as default + +### Requirement: Freight region rules +Each freight template SHALL optionally carry region rules that override its default fees for matching destination regions. A rule SHALL name a set of destination regions and its own first-unit and additional-unit fees and unit sizes in the same integer conventions. Region matching SHALL use a simplified region code table and text matching against the destination address's region fields; the spec scope of this simplification is exactly that table and matching, with no hierarchical region tree, adjacency, or postal-code logic. The rule set SHALL be edited with its template through the same merchant-scoped API. + +#### Scenario: rule overrides default fees +- **WHEN** a template has a rule for a remote region and an order ships there +- **THEN** the rule's first-unit and additional-unit fees are used instead of the template's defaults + +#### Scenario: no matching rule uses defaults +- **WHEN** an order ships to a region no rule names +- **THEN** the template's default fees apply + +### Requirement: Default freight template resolution +A product SHALL optionally reference one freight template of its own shop. When calculating shipping, each order item SHALL resolve its template from the product's template first, then from the shop's default template, and a shop order whose items resolve no template at all SHALL carry zero shipping fee. Product edits that point at a template of another shop SHALL be rejected. + +#### Scenario: product template wins +- **WHEN** an order item's product references a freight template and the shop also has a default template +- **THEN** the product's template resolves for that item + +#### Scenario: fallback to shop default +- **WHEN** an order item's product references no template and the shop has a default template +- **THEN** the shop's default template resolves for that item + +#### Scenario: nothing resolves +- **WHEN** neither the products nor the shop resolve a template for a shop order +- **THEN** the shop order's shipping fee is zero + +### Requirement: Server-side shipping fee calculation +Checkout SHALL compute shipping entirely on the server and SHALL ignore or reject any client-supplied fee or total. For each shop order the server SHALL group items by resolved freight template and merge quantities (`by_piece`) or integer weights (`by_weight`) within each group, charging the first-unit fee once plus the additional-unit fee per extra unit, with part of an additional unit charging a full additional unit. Free shipping SHALL be decided before fees: an always-free template yields zero for its group, and a template whose free-shipping threshold is met by that group's merchandise subtotal also yields zero. A matching region rule SHALL override the group's default first/additional fees and unit sizes. The shop order's shipping fee SHALL be the sum of its groups' fees, and the order total SHALL include it. + +#### Scenario: merged pieces across items +- **WHEN** two items of one shop resolve to the same by-piece template with quantities 2 and 3 +- **THEN** the fee is one first-unit fee plus four additional-unit fees, not five separate shipments + +#### Scenario: partial additional unit rounds up +- **WHEN** a by-weight group's merged weight exceeds the first unit by part of an additional unit +- **THEN** one full additional-unit fee is charged for that part + +#### Scenario: free-shipping threshold wins +- **WHEN** a template group's merchandise subtotal meets the template's free-shipping threshold +- **THEN** that group contributes zero shipping fee regardless of region rules + +#### Scenario: mixed templates in one shop order +- **WHEN** one shop order contains items resolving to two different templates +- **THEN** each group is computed independently and the shop order's fee is their sum + +#### Scenario: region override beats default in a mixed order +- **WHEN** one group's destination matches a region rule and another group's does not +- **THEN** only the matching group uses the rule's fees and the other group uses default fees + +#### Scenario: client amounts are not trusted +- **WHEN** a checkout request carries client-computed fees or totals different from the server calculation +- **THEN** the server-computed amounts are persisted and returned + +### Requirement: Shipping company dictionary and selection +The platform SHALL keep a dictionary of shipping companies with a code and bilingual `{en, zh}` name, and checkout and order surfaces SHALL never invent companies outside the dictionary. A merchant SHALL select one dictionary company when shipping an order, and the shipment SHALL record the selected company. The dictionary SHALL be readable through the shared contract so the merchant console lists real companies. + +#### Scenario: ship with a dictionary company +- **WHEN** a shop user marks an order shipped and selects a shipping company +- **THEN** the shipment records that company and order surfaces show it + +#### Scenario: unknown company rejected +- **WHEN** a ship request names a company absent from the dictionary +- **THEN** the request is rejected and the order is not marked shipped diff --git a/openspec/changes/add-freight-templates/tasks.md b/openspec/changes/add-freight-templates/tasks.md new file mode 100644 index 0000000..1139e7b --- /dev/null +++ b/openspec/changes/add-freight-templates/tasks.md @@ -0,0 +1,29 @@ +## 1. Migration and shared contract + +- [ ] 1.1 Add migration `0017_freight_templates.sql`: `freight_templates` (shop FK, name, default flag with a partial unique index per shop, always-free toggle, `by_piece`/`by_weight` pricing method, first/additional unit fees as i64 minor units, integer first/additional unit sizes in pieces or grams, optional free-shipping threshold in i64 minor units), `freight_region_rules` (template FK, region code set, overriding fees and unit sizes), `shipping_companies` (code, `{en, zh}` JSONB name), `orders.shipping_fee_minor`, `order_items` pricing-method and template-id snapshot columns, and `shipments.shipping_company_code`, with shop/template indexes and cascading deletes. +- [ ] 1.2 Add shared freight template, region rule, shipping company, and checkout shipping-quote types plus `@vmall/shared` API methods for merchant template CRUD, region-rule editing, company listing, and checkout fee quoting; all money in i64 minor units and weights in integer grams, no floats. +- [ ] 1.3 Implement `apps/api/src/modules/freight/` repository, service, DTO, handlers, and module registration with shop routes scoped through `own_shop` and services returning `ApiResult`; add a product-to-template link on the product edit contract, rejecting templates of another shop. + +## 2. Calculation service and tests + +- [ ] 2.1 Implement the pure shipping calculation service: per-shop grouping by resolved template (product template first, shop default second, zero fee fallback), quantity or integer-weight merging within a group, first-unit fee plus per-additional-unit fees with part units rounding up, always-free and free-shipping-threshold checks before fees, and region-rule fee overrides for matching destinations. +- [ ] 2.2 Wire checkout to the calculation service in the order-creation transaction: require the destination address, compute each shop order's fee server-side ignoring client amounts, add it to the order total, and persist `shipping_fee_minor` plus per-item pricing-method and template snapshots. +- [ ] 2.3 Add focused calculation tests at the boundaries: free-shipping threshold met and unmet, region rule overriding default fees, mixed templates in one shop order, partial additional-unit rounding, by-weight versus by-piece merging, and the no-template zero-fee fallback. + +## 3. Shop-admin pages + +- [ ] 3.1 Build the shop-admin freight template management page (list, create/edit, default toggle, always-free and threshold fields, pricing method and unit sizes, region-rule editor, delete) reading and writing through `@vmall/shared`, styled with `@vmall/ui` primitives and shared tokens, with a navigation entry beside existing shop operations. +- [ ] 3.2 Extend the shop-admin fulfillment flow with a shipping-company selector populated from the shared dictionary, required before marking an order shipped, showing the recorded company afterwards. + +## 4. Mall checkout and mock + +- [ ] 4.1 Implement the shared shipping-quote and company client methods in `apps/mall/mock/api.ts` with deterministic fixture templates (per-shop by-piece and by-weight cases plus one region override), and register the shipping surface in Mall API selection with exact shared-client method picks, enabling it in the default live configuration alongside `LIVE_PICKS`. +- [ ] 4.2 Update Mall checkout to select the shipping address before quoting, render each shop group's server-computed delivery fee and the combined shipping total in the selected currency, and show the persisted `shipping_fee` on payment and order detail. +- [ ] 4.3 Add bilingual en/zh checkout shipping strings through the existing Mall locale source without per-page hard-coded copy. + +## 5. Verification and tracker cleanup + +- [ ] 5.1 Add isolated API integration tests in `apps/api/tests/`, reusing the `tests/common/mod.rs` fixtures, covering freight template CRUD with `own_shop` isolation and single-default enforcement, checkout fee persistence and totals for multi-shop carts, region-rule and free-shipping-threshold edges, snapshot stability after template edits, and shipping-company selection and rejection of unknown companies. +- [ ] 5.2 Build the affected frontends because the shared contract changes: `pnpm --filter @vmall/shop-admin build`, `pnpm --filter @vmall/mall build`, and `pnpm --filter @vmall/admin build`. +- [ ] 5.3 Browser-smoke the running stack: create a template with a region rule in shop-admin, run a two-shop Mall checkout showing per-shop fees and totals, change the address to see fees update, pay, and ship with a selected company. +- [ ] 5.4 Run `openspec change validate add-freight-templates --strict` and `openspec validate --all --strict`, and check every OpenSpec task. diff --git a/openspec/changes/add-membership-messaging/proposal.md b/openspec/changes/add-membership-messaging/proposal.md new file mode 100644 index 0000000..6ad3d0e --- /dev/null +++ b/openspec/changes/add-membership-messaging/proposal.md @@ -0,0 +1,30 @@ +## Why + +Members have no growth ladder and no reach-out channel: a customer's spending never converts into status, and order events (payment, dispatch, refund) are invisible unless the shopper re-opens the buyer center. tigshop's level page is itself a half-finished stub, so vmall defines its own member-level and growth-value design instead of porting that page, and pairs it with a system in-site message inbox so transaction events actually reach the customer. + +## What Changes + +- Add platform-managed member levels with bilingual name, icon, growth-value threshold, and bilingual benefits description. +- Accrue growth value from an order's realized paid amount when the customer confirms receipt, recorded in an append-only growth ledger in the same style as the points ledger. +- Automatically upgrade a customer's level inside the accrual transaction, writing `users.level` with a guarded update from the highest threshold the customer's growth qualifies for. +- Emit system in-site messages on order payment, shipment dispatch, and refund completion events. +- Give customers a message inbox with unread/read state, mark-all-read, soft deletion, and an unread count endpoint for the header badge. +- Surface all of it through `@vmall/shared`: a Mall member-center level page, a message center, and a top-bar unread badge (with fixed-adapter fallback and live picks for the new domains), plus a member-level management page in the admin console. + +## Capabilities + +### New Capabilities +- `membership`: Member level catalog, growth-value accrual ledger, automatic upgrade-only leveling, and level benefits display. +- `messaging`: Order and after-sale event-driven system messages, per-user unread/read state with soft deletion, and unread counting. + +### Modified Capabilities +- `frontend-mall`: Member-center level page, message center, and top-bar unread badge use the selected API adapter instead of absent or local state. +- `frontend-admin`: Member level management beside the existing platform operations. + +## Non-goals + +Paid level purchases, level-exclusive pricing, SMS/email/WeChat-template push delivery, real-time WebSocket push (polling or fetch-on-entry is enough), and rich-text message bodies are excluded. + +## Impact + +Adds one Postgres migration, Rust `membership` and `messaging` modules with order/fulfillment event hooks, shared types and API methods, `membership` and `messaging` Mall domains with fixed adapters and live picks, Mall pages `user/membership` and `user/messages` plus the shell badge, and an admin `member-levels` page. \ No newline at end of file diff --git a/openspec/changes/add-membership-messaging/specs/frontend-admin/spec.md b/openspec/changes/add-membership-messaging/specs/frontend-admin/spec.md new file mode 100644 index 0000000..10ae3a8 --- /dev/null +++ b/openspec/changes/add-membership-messaging/specs/frontend-admin/spec.md @@ -0,0 +1,20 @@ +## ADDED Requirements + +### Requirement: Member level management +Platform admins SHALL manage member levels from a dedicated admin console page through the shared API contract: levels listed in growth-threshold order, and create, edit, and delete actions over name, icon, growth threshold, and benefits with both locales editable. Deleting a level in use SHALL surface the API rejection instead of silently succeeding, and member-level management SHALL be reachable from the authenticated console navigation beside existing platform operations. + +#### Scenario: manage a member level +- **WHEN** a platform admin creates a level with bilingual name and benefits, an icon, and a growth threshold +- **THEN** it appears in threshold order and is available for automatic leveling + +#### Scenario: edit a growth threshold +- **WHEN** a platform admin changes a level's growth threshold to a unique value +- **THEN** the level persists with the new threshold and the bilingual content unchanged + +#### Scenario: deleting a level in use fails visibly +- **WHEN** a platform admin deletes a level that customers hold +- **THEN** the console surfaces the rejection and the level and its members remain unchanged + +#### Scenario: level management appears in admin navigation +- **WHEN** an authenticated platform admin opens the admin console +- **THEN** member-level management is reachable from the console nav \ No newline at end of file diff --git a/openspec/changes/add-membership-messaging/specs/frontend-mall/spec.md b/openspec/changes/add-membership-messaging/specs/frontend-mall/spec.md new file mode 100644 index 0000000..5b2b6f3 --- /dev/null +++ b/openspec/changes/add-membership-messaging/specs/frontend-mall/spec.md @@ -0,0 +1,46 @@ +## ADDED Requirements + +### Requirement: Member center level page +The buyer center SHALL render a membership level page from the selected API adapter: the current level's name, icon, and benefits, the total growth value, progress toward the next level's threshold, and the customer's growth history from the growth ledger. The page SHALL NOT derive level state from fixtures or local state, and it SHALL behave deterministically when the `membership` domain is configured to fixed data. + +#### Scenario: level page reflects backend state +- **WHEN** a signed-in shopper whose growth qualifies for a level opens the member-center level page +- **THEN** the current level, benefits, growth total, and remaining growth to the next level render from the adapter without per-entry requests + +#### Scenario: growth history lists ledger entries +- **WHEN** a shopper opens the level page +- **THEN** recent growth ledger entries with delta, reason, and time render paginated from the adapter + +#### Scenario: fixed adapter remains functional +- **WHEN** the `membership` domain is configured to fixed data +- **THEN** the level page renders deterministic fixed level and growth data through the same shared client methods + +### Requirement: Message center +The mall SHALL render a user message center from the selected API adapter with a paginated message list and an unread-only filter. Opening or explicitly marking a message read, marking all read, and deleting a message SHALL persist through the API and update the visible list and counts without local-only mutation. Each message SHALL render its title and body in the active locale from the shared bilingual contract. Listing and refresh SHALL happen on entry and on demand; no push transport is required. + +#### Scenario: unread filter and marking +- **WHEN** a shopper filters the message center to unread and marks one message read +- **THEN** the message persists as read and leaves the unread-only view with the unread count reduced + +#### Scenario: mark all read +- **WHEN** a shopper uses mark-all-read in the message center +- **THEN** every unread message becomes read and the unread-only view empties + +#### Scenario: delete a message +- **WHEN** a shopper deletes a message +- **THEN** it disappears from the list and the unread count through the API state + +#### Scenario: fixed adapter remains functional +- **WHEN** the `messaging` domain is configured to fixed data +- **THEN** message listing, read marking, mark-all-read, and deletion behave deterministically through the same shared client methods + +### Requirement: Top-bar unread badge +The mall shell SHALL show the authenticated customer's unread message count as a badge on its message entry, read from the shared unread-count contract. The badge SHALL refresh on page entry and after read, mark-all-read, and delete actions, and SHALL be absent for anonymous shoppers. + +#### Scenario: badge reflects unread count +- **WHEN** a signed-in shopper with three unread messages loads any mall page +- **THEN** the message entry badge shows three + +#### Scenario: badge clears after mark-all-read +- **WHEN** a shopper marks all messages read and returns to the shell +- **THEN** the badge shows no unread count \ No newline at end of file diff --git a/openspec/changes/add-membership-messaging/specs/membership/spec.md b/openspec/changes/add-membership-messaging/specs/membership/spec.md new file mode 100644 index 0000000..21f8107 --- /dev/null +++ b/openspec/changes/add-membership-messaging/specs/membership/spec.md @@ -0,0 +1,61 @@ +## ADDED Requirements + +### Requirement: Member level catalog +The platform SHALL manage member levels through admin-only APIs. Each level SHALL carry a bilingual name (`{en, zh}`), an icon key, an integer growth-value threshold, and a bilingual benefits description (`{en, zh}`). Growth thresholds SHALL be unique across levels, and the effective ordering of levels SHALL follow the threshold. Deleting a level that any customer currently holds SHALL be rejected instead of reassigning or orphaning members. + +#### Scenario: create a level +- **WHEN** a platform admin creates a level with name, icon, growth threshold, and benefits in both locales +- **THEN** the level is listed with its bilingual content and threshold ordering + +#### Scenario: duplicate threshold is rejected +- **WHEN** a platform admin creates or edits a level to reuse another level's growth threshold +- **THEN** the request is rejected and the existing levels are unchanged + +#### Scenario: deleting a level in use is rejected +- **WHEN** a platform admin deletes a level that at least one customer holds +- **THEN** the request is rejected and no customer's level changes + +### Requirement: Growth value accrual ledger +When a customer confirms receipt and an order reaches completed, the customer SHALL earn growth value equal to that order's realized paid amount converted to the base currency and truncated to whole units through integer minor-unit arithmetic using the base currency exponent, with no floating-point computation. Accrual SHALL append exactly one entry per order to a growth ledger that, like the points ledger, is append-only and records the delta, running growth total, reason, and order reference. Retried or repeated completion events SHALL NOT create a second entry or a second accrual. + +#### Scenario: confirm receipt accrues growth +- **WHEN** a customer confirms receipt of an order whose realized paid amount converts to 120 whole base-currency units +- **THEN** the growth ledger gains one entry with delta 120 referencing that order and the customer's growth total rises by 120 + +#### Scenario: repeated completion is idempotent +- **WHEN** the completion handling for the same order runs again +- **THEN** no second ledger entry exists and the growth total is unchanged + +#### Scenario: ledger entries are immutable +- **WHEN** any code path handles growth after an entry was written +- **THEN** the entry is only ever appended to, never updated or deleted + +### Requirement: Automatic level upgrade +Inside the growth accrual transaction, the customer's level SHALL be re-derived as the level with the highest growth threshold less than or equal to the customer's growth total and written to `users.level` with a guarded update. Leveling SHALL be one-way: a customer is only ever moved to a level with a strictly higher threshold than the current one, and never demoted automatically. A growth total exactly at a threshold qualifies for that level, and when several thresholds are passed the highest qualifying level wins. A customer below every threshold SHALL hold no level. + +#### Scenario: growth at the threshold upgrades +- **WHEN** an accrual brings a customer's growth total exactly to a level's threshold +- **THEN** the customer's level becomes that level in the same transaction as the ledger entry + +#### Scenario: jumping past intermediate levels +- **WHEN** an accrual passes two levels' thresholds at once +- **THEN** the customer holds the highest qualifying level, not the intermediate one + +#### Scenario: no level below every threshold +- **WHEN** a customer's growth total is below the lowest defined threshold +- **THEN** the customer holds no level and a later qualifying accrual assigns one + +### Requirement: Level benefits display +An authenticated customer SHALL read their own membership status: current level with name, icon, and benefits, total growth value, the next level's threshold and remaining growth to reach it, and a paginated view of their own growth ledger. The status SHALL re-derive the displayed level against current thresholds, and one customer SHALL never read another customer's growth history. + +#### Scenario: progress to the next level +- **WHEN** a customer between two thresholds opens their membership status +- **THEN** the response shows the current level's benefits and the exact growth remaining to the next level + +#### Scenario: top level has no next level +- **WHEN** a customer at or above the highest threshold reads their membership status +- **THEN** the response contains no next level and no remaining growth target + +#### Scenario: growth history is own-only +- **WHEN** a customer requests their growth ledger +- **THEN** only entries of the authenticated customer are returned, newest first and paginated \ No newline at end of file diff --git a/openspec/changes/add-membership-messaging/specs/messaging/spec.md b/openspec/changes/add-membership-messaging/specs/messaging/spec.md new file mode 100644 index 0000000..f3a1d72 --- /dev/null +++ b/openspec/changes/add-membership-messaging/specs/messaging/spec.md @@ -0,0 +1,50 @@ +## ADDED Requirements + +### Requirement: System event messages +An order payment success, a shipment dispatch, and a refund completion (including after-sale refunds) SHALL each create one system message for the customer who owns the order. Each message SHALL record its kind, a bilingual title and body (`{en, zh}`) naming the affected order, and the order (and refund) reference it was triggered by. Emission SHALL be idempotent per customer, kind, and reference, so re-running an event handler never writes a duplicate message. + +#### Scenario: paying an order notifies the customer +- **WHEN** a customer's order payment succeeds +- **THEN** an `order_paid` message referencing that order appears in the customer's inbox + +#### Scenario: dispatch notifies the customer +- **WHEN** a shop marks a shipment of the customer's order as shipped +- **THEN** an `order_shipped` message referencing that order appears in the customer's inbox + +#### Scenario: refund completion notifies the customer +- **WHEN** a refund for the customer's order completes +- **THEN** a `refund_completed` message referencing that order and refund appears in the customer's inbox + +#### Scenario: re-run event handler does not duplicate +- **WHEN** the same event handler for the same order, kind, and reference runs twice +- **THEN** exactly one message exists for that customer, kind, and reference + +### Requirement: User message state machine +A message SHALL belong to exactly one customer and move through `unread` and `read` states plus a soft deletion. Messages are created `unread`; marking one read or all read SHALL use a guarded update that touches only `unread` rows, so repeated marking never rewrites already-read rows. Deletion SHALL set a soft-delete marker rather than removing the row, SHALL be idempotent, and SHALL make the message invisible to listing and counting. Customers SHALL only ever read, mark, or delete their own messages. + +#### Scenario: mark one message read +- **WHEN** a customer marks their unread message read twice +- **THEN** the message is read and only the first call changed its state + +#### Scenario: mark all read touches only unread +- **WHEN** a customer with three unread and two read messages marks all read +- **THEN** exactly the three unread rows become read and the two read rows are untouched + +#### Scenario: deleted messages disappear +- **WHEN** a customer deletes a message +- **THEN** it is absent from their list and unread count while its row remains for audit + +#### Scenario: foreign messages are unreachable +- **WHEN** a customer addresses another customer's message for read or delete +- **THEN** the request fails without changing that message + +### Requirement: Unread count endpoint +An authenticated customer SHALL read their unread message count through a dedicated endpoint that excludes soft-deleted messages. The count SHALL reflect read, mark-all-read, and delete actions immediately and SHALL only ever count the requesting customer's messages. + +#### Scenario: count drops after mark-all-read +- **WHEN** a customer with four unread messages marks all read +- **THEN** the unread count endpoint returns zero + +#### Scenario: deleting an unread message drops the count +- **WHEN** a customer deletes one of three unread messages +- **THEN** the unread count endpoint returns two \ No newline at end of file diff --git a/openspec/changes/add-membership-messaging/tasks.md b/openspec/changes/add-membership-messaging/tasks.md new file mode 100644 index 0000000..6b436d7 --- /dev/null +++ b/openspec/changes/add-membership-messaging/tasks.md @@ -0,0 +1,34 @@ +## 1. Persistence and shared contract + +- [ ] 1.1 Add migration `0016_membership_messaging.sql`: `member_levels` (bilingual `name` JSONB `{en, zh}`, `icon`, unique integer `growth_threshold`, bilingual `benefits` JSONB `{en, zh}`), `growth_logs` (append-only `user_id`, `delta`, running `growth_total`, `reason`, `reference_type`/`reference_id` order reference with a partial unique index per user and reference), `messages` (`user_id`, `kind`, bilingual `title`/`body` JSONB `{en, zh}`, `reference_type`/`reference_id`, `status` `unread`/`read`, `deleted_at` soft delete, partial unique index per user/kind/reference), a `users.level` column referencing `member_levels`, and listing/counting indexes. +- [ ] 1.2 Add shared types (`MemberLevel`, `MemberLevelInput`, `MembershipStatus`, `GrowthLogEntry`, `Message`, `MessageListQuery`) and `@vmall/shared` methods `getMembership`, `listGrowthLogs`, `listMessages`, `markMessageRead`, `markAllMessagesRead`, `deleteMessage`, `getUnreadCount`, and `admin.listMemberLevels`, `admin.createMemberLevel`, `admin.updateMemberLevel`, `admin.deleteMemberLevel` with all amounts and growth values as integers and i18n content as `{en, zh}` JSONB. +- [ ] 1.3 Implement `apps/api/src/modules/membership/` repository, service, DTO, handlers, and module registration: admin-role-gated member-level CRUD routes and customer-scoped membership status and growth-ledger routes, services returning `ApiResult`. +- [ ] 1.4 Implement `apps/api/src/modules/messaging/` repository, service, DTO, handlers, and module registration: customer-scoped message list (paginated, optional unread-only filter), mark-read, mark-all-read, soft delete, and unread-count routes returning `ApiResult`. + +## 2. Services and behavioral tests + +- [ ] 2.1 Implement growth accrual on order completion (customer confirms receipt): convert the order's realized paid amount to the base currency with integer minor-unit arithmetic (truncated whole units, no floating point), append exactly one ledger entry per order, and update `users.level` in the same transaction with a guarded `UPDATE ... WHERE` that only moves the customer to a strictly higher-threshold level. +- [ ] 2.2 Wire idempotent message emission into the order, fulfillment, and refund (after-sale) transitions: order payment success emits `order_paid`, shipment dispatch emits `order_shipped`, and refund completion emits `refund_completed`, each a guarded insert keyed by user, kind, and reference with bilingual `{en, zh}` title and body naming the order. +- [ ] 2.3 Add behavioral coverage in `apps/api/tests/membership.rs` and `apps/api/tests/messaging.rs` (fixtures from `tests/common/mod.rs`) for upgrade boundaries: growth exactly at a threshold upgrades, growth below every threshold holds no level, one accrual jumping two thresholds lands on the highest qualifying level, and repeated completion events accrue and upgrade exactly once. +- [ ] 2.4 Extend the behavioral coverage for event triggers and read semantics: each of the three events creates exactly one correctly referenced message, re-run handlers do not duplicate, mark-read touches only `unread` rows (idempotent single and all-read), soft delete is idempotent and excluded from lists and unread counts, and cross-user message access fails. + +## 3. Mall member and message surfaces + +- [ ] 3.1 Implement the `getMembership`, `listGrowthLogs`, `listMessages`, `markMessageRead`, `markAllMessagesRead`, `deleteMessage`, and `getUnreadCount` methods in `apps/mall/mock/api.ts` with deterministic per-session fixture state and the same idempotent read/delete semantics. +- [ ] 3.2 Add the `membership` and `messaging` domains with their exact shared-client method picks to the Mall API selection and enable them in the default live runtime configuration. +- [ ] 3.3 Add or adjust bilingual level, growth, message, badge, and failure strings through the existing Mall locale source without per-page hard-coded copy. +- [ ] 3.4 Build `apps/mall/pages/user/membership.vue`: current level name/icon/benefits, growth total, progress to the next threshold, and paginated growth history from the shared contract, with fixed-adapter parity and no fixture imports. +- [ ] 3.5 Build `apps/mall/pages/user/messages.vue`: paginated message list with unread-only filter, open/mark-read, mark-all-read, and delete actions that persist through the API and refresh list and counts. +- [ ] 3.6 Add the top-bar unread badge on the shell message entry: unread count on page entry and after read/mark-all/delete actions, absent for anonymous shoppers, linking to the message center. + +## 4. Admin member-level management + +- [ ] 4.1 Build `apps/admin/pages/member-levels.vue`: levels in threshold order with create/edit/delete forms over bilingual name, icon, growth threshold, and benefits through the shared `admin` level methods, surfacing the delete rejection for levels in use. +- [ ] 4.2 Register the member-levels entry in the authenticated admin console navigation beside existing platform operations. + +## 5. Verification + +- [ ] 5.1 Run the `apps/api/tests/` integration suites with the `tests/common/mod.rs` fixtures — the new `membership.rs` and `messaging.rs` plus the affected `orders.rs`, `order_service.rs`, and `points.rs` suites — proving upgrade boundaries, event triggers, and read semantics against the shared test database. +- [ ] 5.2 Browser-smoke the running API, Mall, and admin console: confirm receipt upgrades the level and appends the growth entry, the three system events land one message each, message-center read/mark-all/delete update lists and counts, the top-bar badge tracks unread count, and admin level CRUD with in-use delete rejection works end to end. +- [ ] 5.3 Build the affected frontends because the shared API contract changes: `pnpm --filter @vmall/mall build`, `pnpm --filter @vmall/admin build`, and `pnpm --filter @vmall/shop-admin build`. +- [ ] 5.4 Run `openspec change validate add-membership-messaging --strict` and `openspec validate --all --strict` and fix findings until both pass. \ No newline at end of file diff --git a/openspec/changes/add-merchant-onboarding/.openspec.yaml b/openspec/changes/add-merchant-onboarding/.openspec.yaml new file mode 100644 index 0000000..265da3d --- /dev/null +++ b/openspec/changes/add-merchant-onboarding/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-23 diff --git a/openspec/changes/add-merchant-onboarding/proposal.md b/openspec/changes/add-merchant-onboarding/proposal.md new file mode 100644 index 0000000..a2e1f49 --- /dev/null +++ b/openspec/changes/add-merchant-onboarding/proposal.md @@ -0,0 +1,28 @@ +## Why + +A shop can currently exist only when a platform admin creates it directly and assigns an owner by hand. There is no merchant-facing entry: prospective sellers cannot apply, nothing records their entity and qualification data, and admins have no review queue. As the 2B entry of the B2B2C mall, joining as a merchant must become an application-and-review flow instead of a manual admin back-office step. + +## What Changes + +- Add a public merchant onboarding application ("商家入驻") with two form kinds — personal (个人) and enterprise (企业) — covering entity information, operating categories, contact details, and qualification materials submitted as URL fields only. +- Require registration or sign-in before submission: the form can be filled anonymously, but only an authenticated user can submit, and a user holding a `pending` or `approved` application is deduplicated and rejected. +- Add a review state machine `pending -> approved | rejected`: platform admins review applications in a queue, rejection requires a reason, and approval creates the shop and a dedicated `shop_owner` account in the same transaction, issuing initial credentials displayed exactly once. +- Add a mall application status page so an applicant can track their own review state and rejection reason, with re-apply after rejection. +- Land the mall top-bar "商家入驻" entry on the onboarding page instead of the stores directory. + +## Capabilities + +### New Capabilities +- `merchant-onboarding`: Application submission with per-user deduplication, the review state machine, transactional approval provisioning with one-time credentials, and applicant/admin visibility. + +### Modified Capabilities +- `frontend-mall`: Multi-step merchant onboarding form and application status page behind the top-bar "商家入驻" entry. +- `frontend-admin`: Merchant application list and review console with approve/reject actions. + +## Non-goals + +File upload storage, qualification OCR and real-name identity verification, onboarding fees, public notice (公示期) periods, supplier (non-shop) onboarding, and multiple active applications per user are excluded. + +## Impact + +Adds one Postgres migration (`merchant_applications`), a Rust merchant-onboarding module with customer and admin routes, shared types and API methods, a `merchant-onboarding` live-domain pick with fixed-adapter parity, Mall onboarding and status pages, and an admin review console. Approval reuses the existing identity and shop provisioning paths inside one transaction. diff --git a/openspec/changes/add-merchant-onboarding/specs/frontend-admin/spec.md b/openspec/changes/add-merchant-onboarding/specs/frontend-admin/spec.md new file mode 100644 index 0000000..302bd3b --- /dev/null +++ b/openspec/changes/add-merchant-onboarding/specs/frontend-admin/spec.md @@ -0,0 +1,20 @@ +## ADDED Requirements + +### Requirement: Merchant application review console +Platform admins SHALL review merchant onboarding applications in a dedicated console entry beside existing platform operations: a status-filtered paginated list and a detail view showing entity kind, entity information, operating categories, contact details, and qualification URL fields. Approving SHALL provision the shop and `shop_owner` account and display the one-time initial credentials exactly once, with copy stating the password cannot be retrieved again. Rejecting SHALL require a reason. Reviewed applications SHALL leave the pending queue immediately. + +#### Scenario: review an enterprise application +- **WHEN** a platform admin opens a pending enterprise application +- **THEN** the company entity data, operating categories, contact details, and qualification URLs are visible for review + +#### Scenario: approve shows one-time credentials +- **WHEN** a platform admin approves an application +- **THEN** the created shop owner's initial credentials are shown once and the application moves to the approved list + +#### Scenario: reject requires a reason +- **WHEN** a platform admin attempts to reject without entering a reason +- **THEN** the action is blocked until a non-empty reason is provided + +#### Scenario: entry beside platform operations +- **WHEN** an authenticated platform admin opens the admin console +- **THEN** merchant application review is reachable from the console navigation diff --git a/openspec/changes/add-merchant-onboarding/specs/frontend-mall/spec.md b/openspec/changes/add-merchant-onboarding/specs/frontend-mall/spec.md new file mode 100644 index 0000000..f729e6c --- /dev/null +++ b/openspec/changes/add-merchant-onboarding/specs/frontend-mall/spec.md @@ -0,0 +1,31 @@ +## ADDED Requirements + +### Requirement: Merchant onboarding multi-step form +The mall SHALL expose a merchant onboarding page ("商家入驻") reachable from the top-bar entry, with a multi-step form covering entity kind (personal 个人 / enterprise 企业), kind-specific entity information, operating categories from the published category tree, contact details, and qualification materials as URL input fields with no file-upload controls. The form SHALL be fillable while signed out, but submission SHALL require registration or sign-in and return the applicant to the completed form to submit through the shared selected API adapter. A duplicate-application conflict SHALL be surfaced inline. All copy SHALL come from the mall locale source in en and zh. + +#### Scenario: anonymous fill then sign-in +- **WHEN** a signed-out visitor completes the form and submits +- **THEN** they are sent to register or sign in and, once signed in, returned to the completed form to submit + +#### Scenario: enterprise kind shows company fields +- **WHEN** the applicant selects the enterprise entity kind +- **THEN** the company-specific entity and qualification fields replace the personal ones + +#### Scenario: duplicate application surfaced +- **WHEN** a signed-in user holding a pending or approved application submits the form +- **THEN** the mall shows the conflict instead of silently creating a second application + +#### Scenario: fixed adapter remains functional +- **WHEN** the merchant-onboarding domain is configured to fixed data +- **THEN** the form and status flows behave deterministically through the same shared client methods + +### Requirement: Application status page +The mall SHALL show a signed-in applicant their latest merchant application state — status, submitted entity kind, timestamps, and the rejection reason when rejected — linked from the onboarding page, with a re-apply action after rejection. Anonymous visitors SHALL be sent to sign in first. + +#### Scenario: applicant tracks review +- **WHEN** a signed-in applicant opens the status page while the application is `pending` +- **THEN** the pending state and submission summary are shown + +#### Scenario: rejection explains reason +- **WHEN** a rejected applicant opens the status page +- **THEN** the rejection reason is displayed with an action to apply again diff --git a/openspec/changes/add-merchant-onboarding/specs/merchant-onboarding/spec.md b/openspec/changes/add-merchant-onboarding/specs/merchant-onboarding/spec.md new file mode 100644 index 0000000..830966f --- /dev/null +++ b/openspec/changes/add-merchant-onboarding/specs/merchant-onboarding/spec.md @@ -0,0 +1,68 @@ +## ADDED Requirements + +### Requirement: Merchant application submission +A prospective seller SHALL submit one merchant onboarding application as either a personal (个人) or an enterprise (企业) entity. Each kind SHALL require its own entity information, one or more operating categories referenced from the published category tree, contact details, and qualification materials submitted as URL fields. The API MUST reject submissions with missing kind-specific fields, unknown categories, or malformed qualification URLs. The form MAY be filled anonymously, but submission SHALL require an authenticated user. + +#### Scenario: enterprise submission accepted +- **WHEN** an authenticated user submits an enterprise application with entity information, operating categories, contact details, and qualification URLs +- **THEN** the application is stored with status `pending` and returned to the applicant + +#### Scenario: personal submission missing identity document +- **WHEN** a user submits a personal application without the required identity document URL +- **THEN** the API returns a validation error and stores no application row + +#### Scenario: anonymous submit requires sign-in +- **WHEN** a signed-out visitor submits the completed form +- **THEN** no application is stored and the visitor must register or sign in before submitting + +### Requirement: One active application per user +Submission SHALL be deduplicated per user: a user holding a `pending` or `approved` application MUST NOT create another one, enforced by service validation backed by a database partial unique index so concurrent submissions cannot both succeed. A user whose application was `rejected` MAY apply again. + +#### Scenario: duplicate pending submission +- **WHEN** a user with a `pending` application submits again +- **THEN** the API rejects the request with a conflict and exactly one application row exists + +#### Scenario: re-apply after rejection +- **WHEN** a user whose application was `rejected` submits a new application +- **THEN** a new `pending` application row is created + +### Requirement: Review state machine +An application SHALL move only from `pending` to `approved` or `rejected`, executed as guarded updates matching the `pending` state so concurrent or repeated reviews of the same application fail with a conflict instead of overwriting each other. Rejecting SHALL require a non-empty reason recorded on the row, and `approved` and `rejected` are terminal states that MUST NOT transition again. + +#### Scenario: double review conflict +- **WHEN** two platform admins review the same `pending` application concurrently +- **THEN** exactly one transition succeeds and the other receives a conflict + +#### Scenario: rejection records a reason +- **WHEN** a platform admin rejects a `pending` application with a reason +- **THEN** the application becomes `rejected` with the reason stored on the row + +#### Scenario: terminal state is immutable +- **WHEN** a platform admin reviews an already `approved` or `rejected` application +- **THEN** the API returns a conflict and the stored state is unchanged + +### Requirement: Transactional approval provisioning +Approving an application SHALL, within a single database transaction, create the shop, create a dedicated `shop_owner` account scoped to that shop, and mark the application `approved` referencing the created shop. The generated initial password SHALL be returned exactly once in the approve response and MUST NOT be retrievable through any later read. If any step fails, the status change and all provisioning SHALL roll back so the application remains `pending`. + +#### Scenario: approval provisions shop and account +- **WHEN** a platform admin approves a `pending` application +- **THEN** one transaction results in an `approved` application, an active shop, and a working `shop_owner` login scoped to that shop + +#### Scenario: credentials shown once +- **WHEN** the approver reads the application again after approval +- **THEN** the initial password is absent from every subsequent response + +#### Scenario: provisioning failure rolls back +- **WHEN** shop or account creation fails during approval +- **THEN** no shop or account persists and the application remains `pending` + +### Requirement: Application visibility +An applicant SHALL read only their own application history, including status, submitted data, timestamps, and rejection reason. Platform admins SHALL list all applications with status filtering and pagination and read any application detail. Other users' applications MUST NOT be readable through customer routes. + +#### Scenario: applicant checks status +- **WHEN** a signed-in applicant requests their application status +- **THEN** their own application state and rejection reason are returned + +#### Scenario: other applicant is hidden +- **WHEN** one authenticated user requests another user's application through customer routes +- **THEN** the API returns no data about the other user's application diff --git a/openspec/changes/add-merchant-onboarding/tasks.md b/openspec/changes/add-merchant-onboarding/tasks.md new file mode 100644 index 0000000..600bc64 --- /dev/null +++ b/openspec/changes/add-merchant-onboarding/tasks.md @@ -0,0 +1,33 @@ +## 1. Persistence and shared contract + +- [ ] 1.1 Add migration `0017_merchant_applications.sql` (take the next free number if a sibling change claims it): `merchant_applications` with applicant `user_id` FK, `entity_type` enum (`personal`, `enterprise`), kind-specific entity and contact columns, `category_ids`, qualification URL columns (identity document, business license, extra materials as a JSONB URL array), `status` enum (`pending`, `approved`, `rejected`), `rejection_reason`, `reviewed_by`/`reviewed_at`, `created_shop_id` FK, and a partial unique index on `user_id` `WHERE status IN ('pending', 'approved')`. +- [ ] 1.2 Add shared discriminated application types (submit payloads for both entity kinds, application summary/detail with status and rejection reason, one-time approval credentials) and the `submitMerchantApplication`, `getMyMerchantApplications`, `adminListMerchantApplications`, `adminGetMerchantApplication`, `adminApproveMerchantApplication`, and `adminRejectMerchantApplication` methods to `@vmall/shared`. + +## 2. Backend service and behavioral tests + +- [ ] 2.1 Implement `apps/api/src/modules/merchant_onboarding/` repository, service, DTO, handlers, and module registration: authenticated customer routes for submit and self status, and `platform_admin`-guarded admin routes for list, detail, approve, and reject. Services return `ApiResult`. +- [ ] 2.2 Enforce submission validation (kind-specific required fields, published category references, qualification URL shape) and duplicate rejection for users holding a `pending` or `approved` application, with the partial unique index as the concurrency backstop. +- [ ] 2.3 Implement guarded status transitions (`UPDATE ... WHERE status = 'pending'`) with mandatory rejection reason and a 409 conflict for already-reviewed applications. +- [ ] 2.4 Implement approval as a single transaction: create the shop, create a dedicated `shop_owner` account scoped to it, flip the application to `approved` with `created_shop_id`, and return the generated initial password exactly once; any failure rolls back the status flip and all provisioning. +- [ ] 2.5 Add isolated API integration coverage in `apps/api/tests/merchant_applications.rs` (reuse the `tests/common/mod.rs` fixtures) for duplicate submission, double-review conflicts, terminal-state immutability, rejection-reason enforcement, and provisioning rollback. + +## 3. Mall onboarding form + +- [ ] 3.1 Implement the six merchant-onboarding client methods in `apps/mall/mock/api.ts` with deterministic per-session fixture state mirroring dedupe conflicts, review status, and approval/rejection outcomes. +- [ ] 3.2 Add the `merchant-onboarding` domain and exact shared-client method picks to Mall API selection (`apps/mall/plugins/api.ts` LIVE_PICKS) and enable it in the default live runtime configuration. +- [ ] 3.3 Build the multi-step onboarding page (`/merchant/join`): entity-kind step (personal 个人 / enterprise 企业) switching kind-specific fields, entity information, operating categories from the published category tree, contact details, and qualification URL input fields with no upload controls; allow anonymous filling but gate submission behind registration/sign-in with return to the completed form. +- [ ] 3.4 Build the application status page (`/merchant/status`) showing the applicant's latest application state, submitted entity kind, timestamps, and rejection reason, with a re-apply action after rejection. +- [ ] 3.5 Land the top-bar "商家入驻" (`sellerJoin`) entry and the footer "Become a Seller" link on the onboarding page instead of the stores directory, and add bilingual onboarding, form, and status strings through the existing Mall locale source without per-page hard-coded copy. + +## 4. Admin review console + +- [ ] 4.1 Add a platform-admin merchant applications page in `apps/admin` with a status-filtered paginated table and a detail view of entity kind, entity information, operating categories, contact details, and qualification URL fields through the shared contract. +- [ ] 4.2 Add approve/reject actions: rejection requires a non-empty reason, approve shows a one-time initial-credentials dialog with explicit copy that the password cannot be retrieved again, and reviewed rows leave the pending queue immediately. +- [ ] 4.3 Register the merchant applications entry in admin navigation beside existing platform operations and add bilingual console strings through the shared locale source. + +## 5. Verification and tracker cleanup + +- [ ] 5.1 Add end-to-end API integration coverage in `apps/api/tests/merchant_applications.rs` (reuse the `tests/common/mod.rs` fixtures) for submit -> approve provisioning (the created `shop_owner` can log in and manage the linked shop) and submit -> reject -> re-apply, then run the focused merchant onboarding integration tests. +- [ ] 5.2 Run the API plus Mall and browser-smoke: anonymous fill -> sign-in gate -> submit, duplicate submission conflict, mall status page after approve and after reject, admin queue filtering and detail, approve one-time credentials display, and reject-reason enforcement. +- [ ] 5.3 Build all three frontends because the shared API contract changes: `pnpm --filter @vmall/mall build`, `pnpm --filter @vmall/shop-admin build`, and `pnpm --filter @vmall/admin build`. +- [ ] 5.4 Update the README mock boundary for the merchant-onboarding adapter fallback, record any remaining fixture-driven onboarding surfaces in `docs/TBD-marketing.md`, check every OpenSpec task, and run `openspec change validate add-merchant-onboarding --strict` plus `openspec validate --all --strict`. diff --git a/openspec/changes/add-mobile-h5/proposal.md b/openspec/changes/add-mobile-h5/proposal.md new file mode 100644 index 0000000..05f83e5 --- /dev/null +++ b/openspec/changes/add-mobile-h5/proposal.md @@ -0,0 +1,27 @@ +## Why + +The mall top bar's "Mobile" (手机端) entry is a dead link to `/` and no mobile client exists at all: the entire mobile journey — browse, buy, track orders, after-sale, coupons, wallet — is missing. tigshop's uniapp client cannot be adopted because its `wx.*` mini-program capabilities have no portable equivalent, so only its information architecture is worth migrating, rebuilt as a first-party web client. + +## What Changes + +- Add `apps/mobile`, a standalone Nuxt3 H5 app served on port 3003 that reuses `@vmall/shared` contracts and `theme.css`. No backend capability changes: every surface consumes existing APIs through the shared client; any API gap is recorded as a dependency, not implemented here. +- Ship the migrated information architecture: home content floors, category browse, search, product detail, cart, checkout, order list/detail, after-sale entry (reusing the `aftersale` API from `add-aftersale-refunds`), coupon center, wallet (reusing the `wallet` API from `add-wallet-settlement`), "My" center, sign-in and register. +- Wrap the app in a custom bottom tab bar (Home / Category / Cart / My) with safe-area adaptation and mobile interaction conventions (pull-to-refresh, sign-in redirect that returns to the origin). +- Keep the session contract identical to the mall: same `vmall.token`/`vmall.user` storage, Bearer header, and `me()` validation semantics. +- Point the mall top-bar "Mobile" entry at the H5 app instead of the dead `/` link. + +## Capabilities + +### New Capabilities +- `frontend-mobile`: Standalone Nuxt3 H5 storefront — page inventory and navigation structure, pure consumption of `@vmall/shared` (no app-local API wrapper), mobile shell/interaction conventions (tab bar, safe area, pull-to-refresh), and a token contract identical to the mall's. + +### Modified Capabilities +None. Backend capabilities are reused as-is (after-sale and wallet surfaces consume `aftersale` and `wallet` from `add-aftersale-refunds` and `add-wallet-settlement`), and `frontend-mall` behavior changes only in the single top-bar link target. + +## Non-goals + +Mini-program and native APP builds, WeChat login/payment/sharing/subscription messages, IM customer service, PWA offline mode, push notifications, and a responsive retrofit of `apps/mall` are all excluded. Mobile-optimizing `admin` or `shop-admin` is excluded. Dedicated address-book management, reviews/ratings, and any new backend endpoint are excluded. + +## Impact + +Adds one new pnpm workspace app (`apps/mobile`, Nuxt3, port 3003) consuming `@vmall/shared` and `theme.css`, plus a one-line link fix in the mall top bar. Deliberately no migrations, no Rust modules, and no shared-contract changes; after-sale and wallet pages depend on `add-aftersale-refunds` and `add-wallet-settlement` being archived and degrade to localized "Coming soon" placeholders until then. diff --git a/openspec/changes/add-mobile-h5/specs/frontend-mobile/spec.md b/openspec/changes/add-mobile-h5/specs/frontend-mobile/spec.md new file mode 100644 index 0000000..a13b5ce --- /dev/null +++ b/openspec/changes/add-mobile-h5/specs/frontend-mobile/spec.md @@ -0,0 +1,85 @@ +## ADDED Requirements + +### Requirement: Mobile page inventory and navigation structure +The `apps/mobile` H5 app SHALL provide the migrated tigshop information architecture as Nuxt routes: home `/`, category browse `/category`, search `/search` with results `/search/result`, product detail `/product/:id`, cart `/cart`, checkout `/checkout`, order list `/orders`, order detail `/orders/:id`, coupon center `/coupon`, wallet `/wallet`, "My" center `/user`, and sign-in `/login` with register `/register`. Every listed route SHALL be reachable through navigation from the tab bar, the "My" center, or in-flow links, and no route SHALL depend on mini-program-only capabilities. + +#### Scenario: walk the browse-to-buy path +- **WHEN** a mobile shopper opens `/`, drills into `/category`, opens `/search/result`, selects a product, adds it to the cart, checks out, and then opens `/orders/:id` +- **THEN** each route renders its documented surface from the shared API client and the created order is visible in the order list and detail + +#### Scenario: user-center entries resolve +- **WHEN** a shopper opens `/user` and taps the coupon, wallet, order, or after-sale entry +- **THEN** each entry navigates to its documented route and no entry points at an unbuilt placeholder route + +### Requirement: Custom tab bar and safe-area shell +The app SHALL render a custom fixed bottom tab bar with exactly four destinations — Home, Category, Cart, My (首页/分类/购物车/我的) mapped to `/`, `/category`, `/cart`, `/user` — with the active tab indicated by the shared brand token. Tab bar and page chrome SHALL pad with `env(safe-area-inset-bottom)` and `env(safe-area-inset-top)` so content is never hidden behind the home indicator or status bar. Routes outside the four tab routes SHALL replace the tab bar with a top back-bar instead. + +#### Scenario: tab bar switches root sections +- **WHEN** a shopper taps a tab bar item +- **THEN** the matching root route renders with that tab shown active and the tab bar stays fixed above the safe area + +#### Scenario: content clears the home indicator +- **WHEN** a shopper opens the cart on a device with a non-zero bottom safe-area inset +- **THEN** the last cart line and the tab bar remain fully visible above the inset + +### Requirement: Shared-contract-only data access +The app SHALL obtain every piece of backend data through the `@vmall/shared` API client and its existing adapter selection (live backend or fixed-data fallback) and SHALL NOT define its own fetch wrapper, endpoint paths, or response types. Styling SHALL consume `@vmall/shared/theme.css` tokens rather than a private global stylesheet. Money SHALL be handled as integer minor units with shared formatting helpers and bilingual content SHALL render the active locale from `{en, zh}` localized text. + +#### Scenario: no private API layer +- **WHEN** a developer inspects `apps/mobile` +- **THEN** all backend calls go through `@vmall/shared` client methods and no page defines its own HTTP request to an API path + +#### Scenario: prices render from minor units +- **WHEN** any surface renders a price +- **THEN** the amount derives from integer minor units through shared formatting with no floating-point money arithmetic + +### Requirement: Mobile interaction conventions +Home, search results, and order list SHALL support pull-to-refresh reloading through the shared client. Actions requiring authentication SHALL redirect a signed-out shopper to `/login` and return them to the interrupted route after sign-in. In-flight mutations (add to cart, place order, claim coupon) SHALL be guarded against duplicate submission. + +#### Scenario: pull to refresh home +- **WHEN** a shopper pulls down on the home page +- **THEN** home content floors reload from the shared client and the refreshed state renders + +#### Scenario: anonymous add returns to the product +- **WHEN** a signed-out shopper adds a product to the cart from `/product/:id` and then signs in +- **THEN** they land back on that product route + +### Requirement: Mall-identical session and token contract +The app SHALL persist the session exactly as the mall does — `AuthTokens.token` in `localStorage["vmall.token"]` and the user in `localStorage["vmall.user"]` — and SHALL send it as a Bearer header via the shared client's `getToken`. A restored token SHALL be validated against the auth API on load and a rejected token SHALL clear the stored session and route the shopper to `/login`, matching mall semantics so one sign-in serves both apps on the same origin storage. + +#### Scenario: token validated on load +- **WHEN** a shopper with a stored token reopens the app +- **THEN** the session is validated through the shared auth API and the "My" center renders the authenticated user + +#### Scenario: rejected token clears storage +- **WHEN** a restored token is rejected by the auth API +- **THEN** `vmall.token` and `vmall.user` are cleared and the shopper is routed to `/login` + +### Requirement: Transaction and account surfaces on existing APIs +Cart, checkout, order list and order detail SHALL read and write backend state exclusively through shared client cart, checkout, order and address methods, so carts and orders survive reloads and match the mall's data. Checkout SHALL offer the customer's saved addresses (defaulting to the default address) with an inline manual address form when none exist, and SHALL present eligible owned coupons per shop order. Order detail SHALL expose the appropriate status actions (pay, cancel, confirm delivery) through the shared client's guarded status transitions. Coupon center SHALL list claimable shop coupons, claim them, and show the customer's owned coupons through the shared client. + +#### Scenario: cart survives reload +- **WHEN** a signed-in shopper adds an item and reloads `/cart` +- **THEN** the line is still present from backend state + +#### Scenario: claim from the coupon center +- **WHEN** a signed-in shopper claims a coupon with remaining stock from `/coupon` +- **THEN** it appears in the owned-coupon list without any fixture import + +### Requirement: Dependency-gated after-sale and wallet surfaces +The after-sale entry SHALL expose the customer's after-sale requests through the `aftersale` shared-client API from `add-aftersale-refunds`, and the wallet page SHALL render the customer's balances and ledger entries through the `wallet` shared-client API from `add-wallet-settlement`. While either change is not yet archived, the corresponding surface SHALL render a localized "Coming soon" placeholder instead of broken or fixture-backed behavior, and SHALL be enabled without further design change once its dependency lands. + +#### Scenario: wallet before its dependency lands +- **WHEN** `add-wallet-settlement` is not yet archived and a shopper opens `/wallet` +- **THEN** a localized "Coming soon" placeholder renders instead of an error + +#### Scenario: after-sale after its dependency lands +- **WHEN** `add-aftersale-refunds` is archived and a shopper opens the after-sale entry from `/orders/:id` +- **THEN** after-sale data renders from the `aftersale` shared-client API + +### Requirement: Reachable mobile entry from the mall shell +The mall top bar's "Mobile" (手机端) entry SHALL resolve to the H5 app's home page instead of the current dead `/` link, so the desktop storefront is the discoverable entry point for the mobile client. + +#### Scenario: top-bar mobile link opens the H5 app +- **WHEN** a desktop shopper clicks the mall top bar's Mobile entry +- **THEN** the `apps/mobile` home page opens instead of reloading the mall home diff --git a/openspec/changes/add-mobile-h5/tasks.md b/openspec/changes/add-mobile-h5/tasks.md new file mode 100644 index 0000000..8a058ec --- /dev/null +++ b/openspec/changes/add-mobile-h5/tasks.md @@ -0,0 +1,36 @@ +## 1. Scaffold and workspace wiring + +- [ ] 1.1 Scaffold `apps/mobile` as a Nuxt3 app `@vmall/mobile` picked up by the existing `apps/*` pnpm workspace glob, with `dev`/`build`/`preview` scripts, dev server port 3003 (mall 3000, shop-admin 3001, admin 3002), and en/zh i18n matching the mall's locale source. +- [ ] 1.2 Import `@vmall/shared/theme.css` and style exclusively with shared tokens and Tailwind utilities (brand `#ca151e`, `#f5f5f5` surfaces); no private global stylesheet. +- [ ] 1.3 Wire `@vmall/shared` `createApi` as the only data path — mall-consistent `runtimeConfig.public.apiBase`, `getToken` reading `localStorage["vmall.token"]`, and the same per-domain live/fixed adapter selection as the mall (reuse the existing fixed adapter instead of authoring new API glue). No app-local fetch wrapper, endpoint paths, or response types. +- [ ] 1.4 Implement the session store on the mall token contract: `AuthTokens.token` in `localStorage["vmall.token"]`, user in `localStorage["vmall.user"]`, `me()` validation on load, and clearing plus `/login` redirect when the token is rejected. + +## 2. Shell, tab bar, home, category, search + +- [ ] 2.1 Build the custom fixed bottom tab bar with exactly four tabs — Home `/`, Category `/category`, Cart `/cart`, My `/user` (首页/分类/购物车/我的) — active-tab styling on shared brand tokens, `env(safe-area-inset-bottom)` padding plus matching content padding, `env(safe-area-inset-top)` on page chrome, and a top back-bar replacing the tab bar on non-tab routes. +- [ ] 2.2 Build home `/` rendering `getHomeContent()` banners, promotions, quick links and bilingual product floors in stacked mobile layout, with pull-to-refresh reload and integer-minor-unit prices through shared format helpers. +- [ ] 2.3 Build category `/category`: two-panel browse (top-level rail plus children) from `listCategories()`, drilling into `/search/result` scoped to the selected category subtree. +- [ ] 2.4 Build search `/search` and `/search/result`: keyword input, category/brand/sort filters, paginated product grid from `listProducts()`, empty state, pull-to-refresh. +- [ ] 2.5 Point the mall top bar "Mobile"/"手机端" entry (currently a dead `NuxtLink to="/"`) at the `apps/mobile` home URL; leave the rest of the desktop shell unchanged. + +## 3. Product detail, cart, checkout + +- [ ] 3.1 Build `/product/:id`: image gallery, bilingual name/subtitle, attribute and SKU selection with stock-aware quantity, integer-minor-unit prices, shop card, and add-to-cart via `addCartItem` with in-flight duplicate-tap guarding and anonymous → `/login` redirect returning to the product route. +- [ ] 3.2 Build `/cart`: shop-grouped live cart via `getCart`/`updateCartItem`/`removeCartItem`, minor-unit totals, persistence across reload. +- [ ] 3.3 Build `/checkout`: saved-address selection defaulting to the default address with inline manual address form when none exist, per-shop owned coupon selection via `listMyCoupons`, submission via `checkout(...)` (money as integer minor units throughout), payment through the shared pay contract, and success routing to the new `/orders/:id`. + +## 4. User center, orders, after-sale entry, coupons, wallet + +- [ ] 4.1 Build `/user` "My": `getAccountSummary` balances/points, and an entry grid to orders, coupons, wallet, and after-sale, plus sign-in/register links when signed out. +- [ ] 4.2 Build `/orders` (paginated `listMyOrders`, pull-to-refresh) and `/orders/:id` (`getOrder`) with status-appropriate actions through the shared client's guarded transitions: `payOrder`, `cancelOrder`, `confirmDelivered`. +- [ ] 4.3 Build the after-sale entry from `/orders/:id` and `/user` backed by the `aftersale` shared-client API from `add-aftersale-refunds` (list + request detail). Dependency: if `add-aftersale-refunds` is not yet archived when this task is implemented, render a localized "Coming soon" placeholder and keep this task unchecked with a blocking note — no fixtures or invented endpoints. +- [ ] 4.4 Build `/coupon` coupon center: claimable shop coupon templates and claiming via `listShopCouponTemplates`/`claimCoupon`, owned coupons via `listMyCoupons`, anonymous claim redirecting to `/login` and returning to `/coupon`. +- [ ] 4.5 Build `/wallet` on the `wallet` shared-client API from `add-wallet-settlement` (balances and ledger entries). Dependency: if `add-wallet-settlement` is not yet archived when this task is implemented, render a localized "Coming soon" placeholder and keep this task unchecked with a blocking note. +- [ ] 4.6 Build `/login` and `/register` on the shared auth client with mall-identical semantics: minimum 8-character registration password validated before the API call, distinct invalid-credentials vs already-registered copy, and return-to-origin routing. + +## 5. Verification and OpenSpec cleanup + +- [ ] 5.1 Add focused integration coverage in `apps/api/tests/`, reusing the `tests/common/mod.rs` fixtures, for the existing endpoints the H5 flows consume (home content, cart → checkout → order, coupon claim, order listing/transition guards) so the mobile contract stays green; no new backend behavior is introduced by this change. +- [ ] 5.2 Build the affected frontends: `pnpm --filter @vmall/mobile build` and `pnpm --filter @vmall/mall build` (top-bar link change). +- [ ] 5.3 Run the API plus `apps/mobile` and browser-smoke at a mobile viewport (e.g. 390×844): tab bar navigation and safe-area padding, home floors, category → search → product → add to cart → checkout → order detail, coupon claim, pull-to-refresh, and anonymous sign-in return routing. +- [ ] 5.4 Check every task in this file, then run `openspec change validate add-mobile-h5 --strict` and `openspec validate --all --strict` and fix anything until both pass. diff --git a/openspec/changes/add-product-reviews/proposal.md b/openspec/changes/add-product-reviews/proposal.md new file mode 100644 index 0000000..1cd588b --- /dev/null +++ b/openspec/changes/add-product-reviews/proposal.md @@ -0,0 +1,30 @@ +## Why + +Product detail still renders display-only comment fixtures and deliberately offers no reviews tab because no review model exists. Reviews are mall trust infrastructure: shoppers decide from real buyer feedback on real purchases and merchants answer it publicly, so the placeholder must be replaced by reviews backed by completed order lines instead of invented reviewers. + +## What Changes + +- Add buyer reviews for product lines of completed (received) orders: a 1-5 star rating, text, and optional image URLs, at most one review per order line enforced by a unique index plus creation preconditions. +- Let the owning merchant reply to a review exactly once and let platform admins hide (soft delete) or delete reviews as moderation. +- Add product review listing with pagination over visible reviews only and a SQL-aggregated rating summary (average, count, and star distribution). +- Replace the mall product-detail display-only comment placeholder with the real review area and add a buyer-center "pending review" entry with a review submission form. +- Add a shop-admin review list/reply page and an admin review moderation list over the shared contract. +- Keep equivalent fixed-adapter review behavior as the rollback implementation. + +## Capabilities + +### New Capabilities +- `reviews`: One review per completed order line, a single merchant reply per review, platform hide/delete moderation, visible-only public listing, and SQL rating aggregation. + +### Modified Capabilities +- `frontend-mall`: Product detail renders the real review area with summary and paginated visible reviews, and the buyer center gains pending-review discovery and review submission. +- `frontend-shop-admin`: Merchants list their own shop's reviews and reply once per review. +- `frontend-admin`: Platform admins moderate reviews with hide and delete actions. + +## Non-goals + +Follow-up reviews (追评), review ranking or sorting algorithms, photo-reward points (left to P7), anonymous reviews, video reviews, review editing or withdrawal, and merchant review analytics are excluded. + +## Impact + +Adds one Postgres migration (`product_reviews`), a Rust review module with customer, `own_shop`-scoped merchant, and admin routes, shared types and API methods, a Mall `reviews` domain pick with fixed-adapter parity, and Mall product-detail plus buyer-center updates alongside new shop-admin and admin console pages. diff --git a/openspec/changes/add-product-reviews/specs/frontend-admin/spec.md b/openspec/changes/add-product-reviews/specs/frontend-admin/spec.md new file mode 100644 index 0000000..af53741 --- /dev/null +++ b/openspec/changes/add-product-reviews/specs/frontend-admin/spec.md @@ -0,0 +1,16 @@ +## ADDED Requirements + +### Requirement: Platform review moderation +Platform admins SHALL review a paginated moderation list of all reviews with their product, customer, and shop context and SHALL hide or delete reviews through the shared API contract. Admin SHALL expose review moderation navigation beside existing platform operations. + +#### Scenario: hide a review from the moderation list +- **WHEN** a platform admin hides a review +- **THEN** the list shows it as hidden and the review leaves the mall storefront and rating summary + +#### Scenario: delete a review +- **WHEN** a platform admin deletes a review +- **THEN** the row is removed and absent from both admin and storefront listings + +#### Scenario: moderation appears in admin navigation +- **WHEN** an authenticated platform admin opens the admin console +- **THEN** a review moderation entry is reachable from the console nav diff --git a/openspec/changes/add-product-reviews/specs/frontend-mall/spec.md b/openspec/changes/add-product-reviews/specs/frontend-mall/spec.md new file mode 100644 index 0000000..d2f4e9d --- /dev/null +++ b/openspec/changes/add-product-reviews/specs/frontend-mall/spec.md @@ -0,0 +1,27 @@ +## ADDED Requirements + +### Requirement: Live product review area +The mall SHALL render product reviews from the shared selected API adapter instead of display-only comment fixtures. Product detail SHALL show the rating summary (average, count, star distribution) and a paginated list of visible reviews with images and merchant replies, with totals from the API and no invented reviewers or ratings. + +#### Scenario: detail page shows real reviews +- **WHEN** a shopper opens a product whose reviews were posted through completed orders +- **THEN** the review area shows the aggregated summary and those reviews with their merchant replies + +#### Scenario: detail page without reviews +- **WHEN** a shopper opens a product with no visible reviews +- **THEN** the review area shows an empty state and a zeroed summary instead of fixture comments + +### Requirement: Buyer-center pending review and submission +The buyer center SHALL expose a "pending review" entry counting completed order lines awaiting review and a submission form posting rating, text, and optional image URLs through the shared API contract. Submission SHALL require the customer's own unreviewed completed order line, and the pending list SHALL refresh after a successful submission. + +#### Scenario: submit a review from the buyer center +- **WHEN** a shopper submits a review for a pending order line and it succeeds +- **THEN** the pending list and count drop that line and the review appears on the product detail page + +#### Scenario: anonymous submission requires sign-in +- **WHEN** a signed-out shopper opens the pending review entry or submission form +- **THEN** the mall sends the shopper to sign in with the current URL as the return destination + +#### Scenario: fixed adapter remains functional +- **WHEN** the reviews domain is configured to fixed data +- **THEN** the review area, pending-review entry, and submission flows behave deterministically through the same shared client methods diff --git a/openspec/changes/add-product-reviews/specs/frontend-shop-admin/spec.md b/openspec/changes/add-product-reviews/specs/frontend-shop-admin/spec.md new file mode 100644 index 0000000..da1ad83 --- /dev/null +++ b/openspec/changes/add-product-reviews/specs/frontend-shop-admin/spec.md @@ -0,0 +1,12 @@ +## ADDED Requirements + +### Requirement: Merchant review management +Shop users SHALL list only their own shop's reviews in shop-admin through the shared API contract, with pagination and visible reply state, and SHALL submit at most one reply per review. Shop-admin SHALL expose a review management entry beside existing shop operations. + +#### Scenario: reply to a review +- **WHEN** a merchant opens an unreplied review of their shop and submits a reply +- **THEN** the reply is stored once and the review row shows it as replied + +#### Scenario: already replied review offers no second reply +- **WHEN** a merchant opens a review that already carries their shop's reply +- **THEN** no reply submission is offered and other shops' reviews are unreachable diff --git a/openspec/changes/add-product-reviews/specs/reviews/spec.md b/openspec/changes/add-product-reviews/specs/reviews/spec.md new file mode 100644 index 0000000..578d2c6 --- /dev/null +++ b/openspec/changes/add-product-reviews/specs/reviews/spec.md @@ -0,0 +1,67 @@ +## ADDED Requirements + +### Requirement: One review per completed order line +A signed-in customer SHALL create a review only for a product line of their own completed (received) order that has not been reviewed yet. A review SHALL carry a 1-5 star rating, text content, and optional image URLs, and the rating and content snapshot SHALL never change after creation. The database SHALL enforce at most one review per order line through a unique index on the order line, and creation SHALL validate the order-line precondition before insert. + +#### Scenario: review a completed order line +- **WHEN** a customer reviews a product line of their own completed order +- **THEN** the review is created with the submitted rating, text, and image URLs and the line can no longer be reviewed + +#### Scenario: second review of the same line is rejected +- **WHEN** a customer submits a second review for an already reviewed order line +- **THEN** the request is rejected and exactly one review row exists for that line + +#### Scenario: unreviewable line is rejected +- **WHEN** a customer reviews a line from another customer's order or from an order that is not completed +- **THEN** the request is rejected without creating a review + +### Requirement: Bilingual review content shape +Review content and merchant replies SHALL be stored as `{en, zh}` localized JSONB content. A submission MAY populate only the shopper's or merchant's active locale, and every display SHALL fall back to the other locale when the active locale is empty. + +#### Scenario: single-locale submission renders everywhere +- **WHEN** a shopper submits review text in only one locale +- **THEN** both mall locales display the review through the non-empty locale fallback + +### Requirement: Single merchant reply per review +Only a shop user of the review's own shop SHALL reply to a review, at most once. The reply SHALL be written with a guarded update that succeeds only while no reply exists, and a second or cross-shop reply attempt SHALL be rejected. + +#### Scenario: first reply succeeds +- **WHEN** a merchant of the reviewed product's shop replies to a review +- **THEN** the reply is stored with its audit timestamp and appears with the review + +#### Scenario: second reply is rejected +- **WHEN** the same merchant submits another reply to a review that already has one +- **THEN** the request is rejected and the existing reply is unchanged + +### Requirement: Platform moderation hides or deletes reviews +Platform admins SHALL hide or delete any review. Hiding SHALL be a soft delete recorded through a guarded status transition that validates the prior visible state, and deletion SHALL remove the row. Hidden and deleted reviews SHALL be absent from storefront listings and rating summaries, while admin listings SHALL still show hidden reviews with their state. + +#### Scenario: hidden review leaves the storefront +- **WHEN** a platform admin hides a visible review +- **THEN** it disappears from the product's public review list and no longer contributes to the rating summary + +#### Scenario: hide transition validates prior state +- **WHEN** a platform admin hides a review that is already hidden +- **THEN** the guarded transition changes nothing and reports the conflict + +### Requirement: Visible-only paginated review listing +A product's review list SHALL be publicly readable and paginated, containing only visible reviews with the reviewer's display name, rating, content, image URLs, creation time, and any merchant reply. Pagination totals SHALL count only visible reviews, and a customer's pending-review listing SHALL show only their own completed order lines without a review. + +#### Scenario: totals count only visible reviews +- **WHEN** a product has visible and hidden reviews and the public list is requested +- **THEN** only visible reviews are returned and `total` excludes the hidden ones + +#### Scenario: pending-review listing shrinks after submission +- **WHEN** a customer reviews one of their pending order lines +- **THEN** that line disappears from the pending-review listing + +### Requirement: SQL rating summary aggregation +The product rating summary SHALL be computed with SQL aggregation over visible reviews only and SHALL report the review count, average rating, and per-star (1-5) distribution. A product without visible reviews SHALL report a zero count, zero average, and an empty distribution. + +#### Scenario: summary reflects only visible reviews +- **WHEN** a product's summary is requested after one of its reviews is hidden +- **THEN** the count, average, and star distribution exclude the hidden review + +#### Scenario: product without reviews +- **WHEN** the summary is requested for a product with no visible reviews +- **THEN** it reports a zero count and zero average diff --git a/openspec/changes/add-product-reviews/tasks.md b/openspec/changes/add-product-reviews/tasks.md new file mode 100644 index 0000000..3880d70 --- /dev/null +++ b/openspec/changes/add-product-reviews/tasks.md @@ -0,0 +1,30 @@ +## 1. Persistence and shared contract + +- [ ] 1.1 Add migration `0017_product_reviews.sql` (0016 is taken by membership/messaging) creating `product_reviews` with an `order_item_id` unique index, cascading order-item/order/product/shop/customer foreign keys, a `rating` 1-5 check, bilingual `content` and `reply` JSONB `{en, zh}` columns, optional `image_urls`, a visibility `status` column, reply audit columns, and product/status and shop listing indexes. +- [ ] 1.2 Add shared `Review`, `ReviewInput`, `ReviewSummary`, `ReviewableOrderItem`, and review query types plus `listProductReviews`, `getProductReviewSummary`, `listReviewableItems`, `createReview`, `listShopReviews`, `replyReview`, `listModerationReviews`, `hideReview`, and `deleteReview` methods to `@vmall/shared`. +- [ ] 1.3 Implement `apps/api/src/modules/review/` repository, service (returning `ApiResult`), DTO, handlers, and module registration with customer routes, merchant routes declaring roles and scoping shop resources through `own_shop`, and admin routes declaring the platform role. +- [ ] 1.4 Implement completed-order-line precondition checks, duplicate-review rejection backed by the unique index, a single guarded merchant reply (`UPDATE ... WHERE reply IS NULL`), guarded visibility transitions (`UPDATE ... WHERE status = ...`), visible-only public filtering, and SQL-aggregated rating summaries (average, count, per-star counts) over visible reviews. + +## 2. Backend behavioral proof + +- [ ] 2.1 Add API integration coverage in `apps/api/tests/` reusing `tests/common/mod.rs` fixtures for creation preconditions and unique-index enforcement, customer ownership and role checks, `own_shop` scoping on merchant routes, one-reply enforcement, guarded hide/delete transitions, hidden-review filtering from public lists and summaries, and rating aggregation with hidden rows excluded. +- [ ] 2.2 Run the focused review integration tests twice to prove pagination totals stay correct against the shared non-truncated test database. + +## 3. Mall review surfaces and adapter + +- [ ] 3.1 Implement the nine review client methods in `apps/mall/mock/api.ts` with per-session mutable fixture state and the same one-review-per-line, one-reply, and visible-only behavior as the live backend. +- [ ] 3.2 Add the `reviews` domain and exact shared-client method picks to `LIVE_PICKS` in `apps/mall/plugins/api.ts` and enable it in `DEFAULT_LIVE_DOMAINS`. +- [ ] 3.3 Add or adjust bilingual review loading, submission, reply, and failure strings through the existing Mall locale source without per-page hard-coded copy. +- [ ] 3.4 Replace the product-detail display-only comment placeholder and the "no reviews tab" comment in `apps/mall/pages/goods/[id].vue` with the real review tab: rating summary (average, count, star distribution), paginated visible reviews with images and merchant replies, and pagination totals from the API. +- [ ] 3.5 Add the buyer-center "pending review" entry and submission form: list completed order lines awaiting review with counts, submit rating, text, and optional image URLs once per line, and refresh the pending list after submission. + +## 4. Merchant and platform consoles + +- [ ] 4.1 Add a shop-admin review list/reply page scoped to the merchant's own shop with pagination and one-reply submission through the shared contract. +- [ ] 4.2 Add the admin review moderation list with hide (soft delete) and delete actions plus console navigation. + +## 5. Verification and tracker cleanup + +- [ ] 5.1 Seed a deterministic completed order with unreviewed lines, run the API plus Mall, and browser-smoke review submission from the buyer center, product-detail summary and paginated list refresh, merchant reply in shop-admin, admin hide removing the review from the storefront and its summary, and the fixed-adapter review flow. +- [ ] 5.2 Run the review integration tests in `apps/api/tests/` (reusing `tests/common/mod.rs` fixtures) and build all three frontends because the shared contract changes: `pnpm --filter @vmall/mall build`, `pnpm --filter @vmall/shop-admin build`, and `pnpm --filter @vmall/admin build`. +- [ ] 5.3 Mark Reviews implemented in `docs/TBD-marketing.md` and update the README mock boundary, check every OpenSpec task, and run `openspec change validate add-product-reviews --strict` plus `openspec validate --all --strict`. diff --git a/openspec/changes/add-wallet-settlement/proposal.md b/openspec/changes/add-wallet-settlement/proposal.md new file mode 100644 index 0000000..3b6e739 --- /dev/null +++ b/openspec/changes/add-wallet-settlement/proposal.md @@ -0,0 +1,30 @@ +## Why + +Customer accounts and the append-only entry ledger already exist (`0010_customer_accounts.sql`, the `customer-accounts` capability), but nothing in the product can reach them: buyers have no wallet surface, withdrawals have no lifecycle, and money moving between buyers, merchants, and the platform leaves no statement. Platform-mediated merchant settlement — confirmed-received orders minus refunds minus a platform commission — is the core B2B2C promise and is currently invisible. This change gives the existing ledger its entry points: a buyer wallet with simulated recharge and withdrawal review, and per-period merchant settlement statements whose confirmed payout is recorded in the ledger. + +## What Changes + +- Add a buyer wallet: available/frozen balance summary, paginated fund entries mapped to the existing ledger, simulated (demo) recharge that directly credits the available account with a ledger entry, and withdrawal applications that move the requested amount from available to frozen through guarded updates. +- Add platform-admin withdrawal review: approve deducts the frozen amount, reject returns it to available balance, each application reviewable exactly once through guarded status transitions. +- Add merchant settlement: manually generated per-period (week/month) reconciliation statements per shop, snapshotted from confirmed-received orders minus completed refunds and a platform-level commission rate, unique per shop and period. +- Add settlement payout confirmation: a platform admin confirms exactly once via a guarded `pending -> confirmed` transition, which records the payout in the ledger of the shop owner's account. +- Surface everything through `@vmall/shared`: a mall wallet page, admin withdrawal review / commission configuration / statement confirmation, and shop-admin statement list/detail plus shop-account withdrawal. + +## Capabilities + +### New Capabilities +- `wallet`: Demo recharge, guarded withdrawal freeze and one-time review lifecycle, and paginated fund entries over the existing customer-account ledger. +- `settlement`: Idempotent per-shop periodic settlement statements with immutable amount snapshots, a platform-level commission rate, and a `pending -> confirmed` payout state machine. + +### Modified Capabilities +- `frontend-mall`: A buyer-center wallet page (balance, fund entries, demo recharge, withdrawal request) driven by the shared wallet contract with fixed-adapter fallback. +- `frontend-admin`: Withdrawal review queue, platform commission-rate configuration, and settlement statement generation and confirmation. +- `frontend-shop-admin`: Shop-scoped settlement statement list/detail with manual generation, and shop-account withdrawal requests. + +## Non-goals + +Real payment or withdrawal channels (bank cards, WeChat/Alipay payouts), WeChat profit-sharing (分账) APIs, invoice/tax linkage with settlement, automatic settlement batch timers (statements are generated manually), multi-currency wallet balances beyond the account's currency, withdrawal fee tiers, and per-shop commission rate overrides are excluded. + +## Impact + +Adds migrations for wallet recharge/withdrawal records, settlement statements, and the platform commission setting; new `wallet` and `settlement` Rust modules built on the existing `customer-accounts` primitives; `@vmall/shared` wallet and settlement contracts; a `wallet` mall domain with mock-adapter parity and `LIVE_PICKS` wiring; and wallet, review, and settlement pages across the mall, admin, and shop-admin frontends. Settlement generation consumes the refund rows introduced by `add-aftersale-refunds`. diff --git a/openspec/changes/add-wallet-settlement/specs/frontend-admin/spec.md b/openspec/changes/add-wallet-settlement/specs/frontend-admin/spec.md new file mode 100644 index 0000000..b521a0f --- /dev/null +++ b/openspec/changes/add-wallet-settlement/specs/frontend-admin/spec.md @@ -0,0 +1,27 @@ +## ADDED Requirements + +### Requirement: Withdrawal review and commission configuration +The platform console SHALL list withdrawal applications and approve or reject each pending application through the shared API contract, surfacing review outcomes and conflicts (409 on a repeated review) without silent failure. It SHALL expose the platform commission rate as an integer basis-point setting that admins can read and update. + +#### Scenario: reject returns funds +- **WHEN** an admin rejects a pending withdrawal application +- **THEN** the console shows the rejected status and the buyer's wallet reflects the amount back in available balance + +#### Scenario: approve deducts frozen funds +- **WHEN** an admin approves a pending withdrawal application +- **THEN** the console shows the approved status and the frozen balance decreases by the requested amount + +#### Scenario: set commission rate +- **WHEN** an admin updates the commission rate +- **THEN** settlement statements generated afterwards snapshot the new rate + +### Requirement: Settlement statement confirmation +The platform console SHALL list settlement statements across shops with their amount snapshots and statuses, allow manual generation for a shop and closed period, and confirm payout exactly once per statement through the shared API contract, surfacing a 409 on repeated confirmation. + +#### Scenario: confirm payout +- **WHEN** an admin confirms a pending statement +- **THEN** the console shows the statement confirmed and the shop owner's ledger records the payout + +#### Scenario: manual generation is idempotent +- **WHEN** an admin generates a statement for a shop and period that already has one +- **THEN** the existing statement is shown and no duplicate is created diff --git a/openspec/changes/add-wallet-settlement/specs/frontend-mall/spec.md b/openspec/changes/add-wallet-settlement/specs/frontend-mall/spec.md new file mode 100644 index 0000000..6fd07b7 --- /dev/null +++ b/openspec/changes/add-wallet-settlement/specs/frontend-mall/spec.md @@ -0,0 +1,20 @@ +## ADDED Requirements + +### Requirement: Buyer wallet surface +The mall SHALL render a buyer-center wallet page driven by the selected API adapter through `@vmall/shared`: available and frozen balance with currency, paginated fund entries, a clearly labeled demo recharge form, and a withdrawal request form. Balances and entries SHALL reflect backend state after each action rather than local-only state. The wallet SHALL be a mall API domain with fixed-adapter fallback methods and `LIVE_PICKS` wiring following the established per-domain adapter pattern. + +#### Scenario: wallet page loads live state +- **WHEN** a signed-in buyer opens the wallet page +- **THEN** balances and the first page of fund entries render from the shared wallet contract + +#### Scenario: demo recharge updates balance +- **WHEN** the buyer submits a demo recharge +- **THEN** the visible available balance reflects the credit without a reload and the form is visibly marked simulated + +#### Scenario: withdrawal freezes visibly +- **WHEN** the buyer submits a withdrawal request +- **THEN** the visible summary shows available decreased and frozen increased by the requested amount, and the request appears in the withdrawal list as pending + +#### Scenario: fixed adapter remains functional +- **WHEN** the wallet domain is configured to fixed data +- **THEN** the wallet page behaves deterministically through the same shared client methods diff --git a/openspec/changes/add-wallet-settlement/specs/frontend-shop-admin/spec.md b/openspec/changes/add-wallet-settlement/specs/frontend-shop-admin/spec.md new file mode 100644 index 0000000..535f01f --- /dev/null +++ b/openspec/changes/add-wallet-settlement/specs/frontend-shop-admin/spec.md @@ -0,0 +1,23 @@ +## ADDED Requirements + +### Requirement: Merchant settlement statements +Shop-admin SHALL list and open the signed-in shop's settlement statements — period, amount snapshot, and status — through the shared API contract, and allow manual generation for a closed period of the own shop. Shop-scoped pages SHALL never expose another shop's statements or their order/refund breakdown. + +#### Scenario: statements list and detail +- **WHEN** a merchant opens the settlement page +- **THEN** only their own shop's statements are listed and each opens into its snapshot breakdown of orders, refunds, commission, and payable amount + +#### Scenario: generation is idempotent in the UI +- **WHEN** the merchant generates a statement for a period that already has one +- **THEN** the existing statement appears without duplication + +### Requirement: Shop-account withdrawal +The shop owner SHALL view the shop account summary (available and frozen balance of the shop owner's account) and apply to withdraw from it through the shared API contract, seeing the request as pending until the platform reviews it. + +#### Scenario: shop-account withdrawal freezes funds +- **WHEN** the shop owner submits a withdrawal request from the shop-account page +- **THEN** the summary reflects the frozen amount and the request lists as pending + +#### Scenario: reviewed request reflects outcome +- **WHEN** the platform rejects the shop owner's pending withdrawal +- **THEN** the shop-account summary shows the amount returned to available balance and the request shows as rejected diff --git a/openspec/changes/add-wallet-settlement/specs/settlement/spec.md b/openspec/changes/add-wallet-settlement/specs/settlement/spec.md new file mode 100644 index 0000000..ad21ad1 --- /dev/null +++ b/openspec/changes/add-wallet-settlement/specs/settlement/spec.md @@ -0,0 +1,56 @@ +## ADDED Requirements + +### Requirement: Idempotent periodic statement generation +A settlement statement SHALL be generated manually for one shop, one period kind (week or month), and one closed period. For the same shop, period kind, and period start at most one statement SHALL exist, enforced by the database. A repeated generation request SHALL return the existing statement unchanged instead of recomputing or duplicating it. No scheduled batch job SHALL generate statements. + +#### Scenario: repeat generation is idempotent +- **WHEN** a statement already exists for a shop and period and generation is requested again +- **THEN** the existing statement is returned unchanged and no second row is created + +#### Scenario: generation is manual only +- **WHEN** no generation request is made for a closed period +- **THEN** no statement exists for that period + +### Requirement: Immutable amount snapshot +A generated statement SHALL snapshot the contributing order count, the gross total of confirmed-received orders in the period, the deduction total of completed refunds against those orders (`aftersales` rows with status `refunded`, per-order totals from `add-aftersale-refunds`), the platform commission rate at generation time, the commission amount, and the payable amount where payable = gross − refunds − commission. All amounts SHALL be integer minor units computed with integer arithmetic, and the commission rate SHALL be an integer basis-point value. The snapshot SHALL NOT change after generation even when later orders, refunds, or commission-rate changes occur. + +#### Scenario: refunds reduce the payable amount +- **WHEN** a confirmed-received order in the period has a completed refund of 2000 minor units +- **THEN** the statement's refund deduction includes those 2000 minor units and the payable amount is reduced accordingly + +#### Scenario: commission arithmetic is integral +- **WHEN** the commission rate is 500 basis points and gross minus refunds is 10000 minor units +- **THEN** the commission snapshot is 500 minor units and the payable snapshot is 9500 minor units with no floating-point arithmetic + +#### Scenario: snapshot survives rate changes +- **WHEN** the platform commission rate changes after a statement was generated +- **THEN** the generated statement keeps its snapshotted rate and amounts + +### Requirement: Platform commission rate configuration +The commission rate SHALL be a single platform-level setting stored as integer basis points and readable and editable by platform admins. A rate change SHALL affect only statements generated after the change. + +#### Scenario: new rate applies to new statements +- **WHEN** an admin sets the commission rate and a statement is generated afterwards +- **THEN** that statement snapshots the new rate + +### Requirement: One-time payout confirmation +A statement SHALL transition exactly once from `pending` to `confirmed` through a guarded status transition requiring the `pending` status, triggered by a platform admin confirming the payout; a repeated confirmation SHALL return 409. Confirmation SHALL record the confirming admin and timestamp and SHALL credit the payable amount to the shop owner's available account with exactly one ledger entry referencing the statement. + +#### Scenario: confirm pays out once +- **WHEN** an admin confirms a pending statement +- **THEN** the statement becomes confirmed and the shop owner's available balance increases by the payable amount with one ledger entry referencing the statement + +#### Scenario: double confirmation conflicts +- **WHEN** an admin confirms an already confirmed statement +- **THEN** the API returns 409 and no second ledger entry is written + +### Requirement: Shop-scoped statement visibility +Statements and their breakdown SHALL be scoped to one shop. Through the shop's own-shop scope, shop users SHALL read and generate statements only for their own shop and SHALL never observe another shop's statements, contributing orders, or refund lines; platform admins SHALL observe every shop. + +#### Scenario: merchant sees own shop only +- **WHEN** a shop user lists settlement statements +- **THEN** only statements of their own shop appear + +#### Scenario: detail breakdown is shop-scoped +- **WHEN** a shop user opens a statement detail +- **THEN** the contributing orders and refunded amounts are visible for that shop only diff --git a/openspec/changes/add-wallet-settlement/specs/wallet/spec.md b/openspec/changes/add-wallet-settlement/specs/wallet/spec.md new file mode 100644 index 0000000..6f5cee7 --- /dev/null +++ b/openspec/changes/add-wallet-settlement/specs/wallet/spec.md @@ -0,0 +1,49 @@ +## ADDED Requirements + +### Requirement: Demo wallet recharge +An authenticated user SHALL recharge their wallet through a simulated demo flow that records a wallet recharge row and credits the available monetary account with one signed ledger entry in one transaction, without invoking any external payment channel. The API payload and the wallet UI SHALL clearly label the recharge as simulated/demo. Amounts SHALL be positive integer minor units in the account's currency, and monetary arithmetic SHALL NOT use floating point. + +#### Scenario: demo recharge credits balance +- **WHEN** a signed-in buyer submits a demo recharge of 5000 minor units +- **THEN** the available balance increases by exactly 5000 minor units and exactly one ledger entry with reason `wallet_recharge` records the resulting balance + +#### Scenario: flow is labeled as demo +- **WHEN** a buyer opens the recharge flow +- **THEN** it is visibly marked as a simulated recharge and no external payment provider is contacted + +### Requirement: Guarded withdrawal freeze +An authenticated user SHALL apply to withdraw a positive amount in their account currency. The application SHALL atomically move the amount from available to frozen balance through guarded conditional updates that succeed only when available balance covers the amount, and each balance change SHALL be paired with a ledger entry in the same transaction. When available balance is insufficient the request SHALL fail without changing any balance, and concurrent applications SHALL never overdraw or produce a negative balance. + +#### Scenario: withdrawal freezes funds +- **WHEN** a buyer applies to withdraw 1000 minor units +- **THEN** available balance decreases and frozen balance increases by exactly 1000 minor units and two ledger entries record the move + +#### Scenario: concurrent applications cannot overdraw +- **WHEN** two withdrawal applications together exceed the available balance +- **THEN** at most one application succeeds and all balances remain non-negative + +### Requirement: One-time withdrawal review +Platform admins SHALL list pending withdrawal applications and approve or reject each exactly once through a guarded status transition that requires the `pending` status and returns 409 on a repeated review. Approve SHALL deduct the frozen amount with a ledger entry recording that the funds left the platform. Reject SHALL return the frozen amount to available balance with a ledger entry. Every review SHALL record the reviewing admin, timestamp, and optional note. + +#### Scenario: reject returns funds +- **WHEN** an admin rejects a pending withdrawal application +- **THEN** the frozen amount returns to available balance and the application status becomes rejected + +#### Scenario: approve consumes frozen funds +- **WHEN** an admin approves a pending withdrawal application +- **THEN** frozen balance decreases by the requested amount and the application status becomes approved + +#### Scenario: repeated review conflicts +- **WHEN** an admin reviews an application that was already reviewed +- **THEN** the API returns 409 and no balance or ledger row changes + +### Requirement: Paginated fund entries +An authenticated user SHALL page through their own monetary account entries newest first. Each entry SHALL carry the signed delta minor, resulting balance minor, reason, optional business reference, and timestamp, mapped from the existing append-only ledger rows. Entries SHALL never expose or mutate another user's ledger. + +#### Scenario: entries page maps the ledger +- **WHEN** the buyer requests a page of fund entries +- **THEN** only their own available/frozen ledger rows appear with signed deltas and resulting balances + +#### Scenario: entries are user-isolated +- **WHEN** one user requests fund entries +- **THEN** no entry belonging to another user is ever returned diff --git a/openspec/changes/add-wallet-settlement/tasks.md b/openspec/changes/add-wallet-settlement/tasks.md new file mode 100644 index 0000000..0282cf6 --- /dev/null +++ b/openspec/changes/add-wallet-settlement/tasks.md @@ -0,0 +1,38 @@ +## 1. Persistence and shared contract + +- [ ] 1.1 Add migration `0018_wallet.sql` adapting tigshop's `user_recharge_order` and `user_withdraw_apply`: `wallet_recharges` (user, currency, amount_minor BIGINT, status, timestamps) and `wallet_withdrawals` (user, currency, amount_minor BIGINT, account_details JSONB, status pending/approved/rejected with CHECK, reviewing admin, reviewed_at, review_note, timestamps) with foreign keys, non-negative amount CHECKs, and customer-facing indexes. (Migration numbers 0016/0017 are taken by parallel changes.) +- [ ] 1.2 Add migration `0019_settlement.sql` adapting tigshop's `vendor_settlement_order`: `settlement_statements` (shop, period_kind week/month, period_start, period_end, order_count, gross_minor, refund_minor, commission_rate_bps, commission_minor, payable_minor, status pending/confirmed with CHECK, generated_by, confirmed_by, confirmed_at, timestamps) with a unique index on (shop_id, period_kind, period_start), plus a platform-level settings row for `settlement.commission_rate_bps`. +- [ ] 1.3 Add `@vmall/shared` wallet types and customer-client methods: `getWallet` (available/frozen minor with currency), `listWalletEntries` (paged signed ledger entries), `rechargeWallet` (demo), `applyWithdrawal`, and `listMyWithdrawals`. +- [ ] 1.4 Add `@vmall/shared` settlement and review methods: admin client `listWithdrawalApplications`, `reviewWithdrawal`, `getCommissionRate`, `setCommissionRate`, `listSettlementStatements`, `getSettlementStatement`, `generateSettlementStatement`, `confirmSettlementStatement`; shop client `listShopSettlementStatements`, `getShopSettlementStatement`, `generateShopSettlementStatement`. All money fields are i64 minor units; no floating-point amounts cross the contract. + +## 2. Wallet backend + +- [ ] 2.1 Implement `apps/api/src/modules/wallet/` (repo, service returning `ApiResult`, DTOs, handlers, module registration) with user-scoped `/wallet` routes and `/admin/wallet/withdrawals` review routes declaring the platform-admin role. +- [ ] 2.2 Implement demo recharge: one transaction records a `wallet_recharges` row and credits the available account through the existing `customer-accounts` primitives with a `wallet_recharge` ledger entry; the DTO carries an explicit demo marker. +- [ ] 2.3 Implement the withdrawal lifecycle: application freezes funds via guarded `UPDATE ... WHERE balance_minor >= $amount` moving available to frozen with paired ledger entries; admin approve deducts frozen balance with a ledger entry; admin reject returns frozen balance to available with a ledger entry; each review is guarded by `UPDATE ... WHERE status = 'pending'`, returns 409 on repeat, and records reviewer, timestamp, and note. +- [ ] 2.4 Implement paginated fund-entry listing mapped to `customer_account_entries` for the caller's monetary accounts (newest first; signed delta, resulting balance, reason, reference) with ownership filtering. +- [ ] 2.5 Add `apps/api/tests/wallet.rs` covering concurrent withdrawal overdraw, insufficient-balance no-op, double-review 409, reject unfreeze, approve frozen deduction, recharge ledger pairing, and entry pagination isolation, reusing the `tests/common/mod.rs` fixtures. + +## 3. Settlement backend + +- [ ] 3.1 Implement `apps/api/src/modules/settlement/` (repo, service returning `ApiResult`, DTOs, handlers, module registration) with `/shop/settlement/*` routes scoped through `AuthUser::own_shop` and `/admin/settlement/*` routes declaring the platform-admin role. +- [ ] 3.2 Implement idempotent statement generation for a shop and closed week/month period: snapshot order count and gross totals of confirmed-received orders in the period, deduct completed refunds (`aftersales` rows with status `refunded`, per-order totals maintained by `add-aftersale-refunds` — if developed in parallel with P0, merge the P0 `aftersales` migration first), apply the platform commission rate in integer basis points with integer minor-unit arithmetic (payable = gross − refunds − commission), and return the existing statement untouched on repeat generation. +- [ ] 3.3 Implement statement listing and detail with per-order and refund breakdown, with merchant routes restricted to the own shop and platform-admin routes seeing every shop. +- [ ] 3.4 Implement `pending -> confirmed` payout confirmation: guarded `UPDATE ... WHERE status = 'pending'` (409 on repeat) crediting the payable amount to the shop owner's available account with exactly one ledger entry referencing the statement. +- [ ] 3.5 Implement platform commission-rate configuration (integer basis points), applied only to statements generated after a change; generated statements keep their snapshot. +- [ ] 3.6 Add `apps/api/tests/settlement.rs` covering generation idempotency and period uniqueness, refund deduction, commission snapshot immutability after a rate change, double-confirmation 409 with exactly one payout ledger entry, and cross-shop isolation, reusing the `tests/common/mod.rs` fixtures. + +## 4. Frontend surfaces + +- [ ] 4.1 Add the mall buyer-center wallet page (available/frozen balance, paginated fund entries, clearly labeled demo recharge, withdrawal request and history) through `@vmall/shared`, with bilingual strings in the existing Mall locale source. +- [ ] 4.2 Add the `wallet` domain to the Mall fixed-data adapter (`apps/mall/mock/api.ts`) with matching behavior, and wire `wallet` into the Mall API selection `LIVE_PICKS` and default live domains (`apps/mall/plugins/api.ts`). +- [ ] 4.3 Add admin pages for withdrawal review (approve/reject with outcome and 409 feedback), commission-rate configuration, and settlement statement list/detail with manual generation and one-time payout confirmation. +- [ ] 4.4 Add shop-admin pages for own-shop settlement statement list/detail with manual generation and shop-account summary plus withdrawal request/history. +- [ ] 4.5 Keep all three frontends on the `@vmall/shared` contract only (no page-level fetch of wallet/settlement endpoints) and render page chrome with `@vmall/ui` primitives. + +## 5. Verification and tracker cleanup + +- [ ] 5.1 Run the wallet and settlement integration tests in `apps/api/tests/wallet.rs` and `apps/api/tests/settlement.rs` (reusing the `tests/common/mod.rs` fixtures), then `cargo test -p vmall-api` to prove the suite stays green against the shared test database. +- [ ] 5.2 Build all three frontends because the shared API contract changes: `pnpm --filter @vmall/mall build`, `pnpm --filter @vmall/admin build`, and `pnpm --filter @vmall/shop-admin build`. +- [ ] 5.3 Browser-smoke the API plus frontends: demo recharge updates the wallet; withdrawal freeze, admin reject unfreeze, and admin approve deduction round trip; wallet entries pagination; commission-rate configuration; idempotent statement generation with refund deduction; one-time payout confirmation writing one ledger entry; and shop-admin own-shop statement isolation. +- [ ] 5.4 Check every OpenSpec task, then run `openspec change validate add-wallet-settlement --strict` and `openspec validate --all --strict`.