Implements, verifies, and archives the three remaining Wave 2 changes from openspec/MIGRATION-PLAN.md. - add-wallet-settlement (P3): demo recharge, guarded withdrawal freeze and one-time admin review, paginated own fund entries, idempotent per-shop weekly/monthly settlement statements with commission rate and one-time payout confirmation. - add-merchant-onboarding (P5): personal/enterprise applications with one live application per user, guarded review with mandatory rejection reason, and transactional shop + owner provisioning returning one-time credentials; mall onboarding/status pages and an admin review console. - add-membership-messaging (P7): platform member levels, append-only growth accrual on order completion with guarded one-way leveling, order/shipment/ refund system messages with unread/read state and soft deletion, plus the mall header unread badge. Backend: migrations 0019-0023, new wallet, settlement, merchant_onboarding, membership and messaging modules, event hooks in order/fulfillment/aftersale, and integration suites for each. Shared contract extended and all three frontends updated; code indexes, domain docs, backend guidelines and the migration tracker synced. Verification: cargo test -p vmall-api green twice consecutively; mall, admin and shop-admin builds pass; browser smoke on every new surface; openspec validate --all --strict green (33 passed). The three changes share the @vmall/shared contract, the mall mock adapter and per-app locale/nav files, so they are committed together to keep every commit buildable.
6.7 KiB
shipping Specification
Purpose
Merchant-owned freight templates (by piece or by weight, with region rules and a default per shop) plus the platform shipping-company dictionary, used by server-side checkout pricing of per-shop shipping fees.
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<Dto>.
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