feat(freight): shop freight templates, server-side checkout fees, company dictionary (add-freight-templates)
This commit is contained in:
@@ -263,3 +263,18 @@ The Mall SHALL use the shared selected API adapter to expose an after-sale actio
|
||||
- **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
|
||||
|
||||
### 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
|
||||
|
||||
|
||||
@@ -102,3 +102,21 @@ Shop-admin SHALL provide an aftersale list and detail workspace filtered to the
|
||||
- **WHEN** an authorized shop user appends a localized message
|
||||
- **THEN** the message appears in the same chronological aftersale thread visible to the buyer
|
||||
|
||||
### 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
|
||||
|
||||
|
||||
@@ -90,3 +90,21 @@ After-sale eligibility SHALL resolve the order item through the authenticated cu
|
||||
- **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
|
||||
|
||||
### 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
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
# shipping Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change add-freight-templates. Update Purpose after archive.
|
||||
## 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
|
||||
|
||||
Reference in New Issue
Block a user