Files
vmall/openspec/changes/archive/2026-09-23-add-aftersale-refunds/specs/aftersale/spec.md
T

6.1 KiB

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