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.
87 lines
6.5 KiB
Markdown
87 lines
6.5 KiB
Markdown
# aftersale Specification
|
|
|
|
## Purpose
|
|
Post-purchase after-sale handling: per-line refund-only and return-refund
|
|
applications, merchant processing, bilateral messages, and platform
|
|
arbitration. A completed refund updates the order's refund total and credits
|
|
the customer's available account with a ledger entry in one transaction.
|
|
## 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
|
|
|