# Sales Return & Refund Bundle v39

v39 closes the first return/refund business loop across Sales, Inventory, Payments and Cash.

## Return document

A return has:
- immutable public ULID
- original sale reference
- draft → posted lifecycle
- return lines referencing original immutable sale lines
- derived proportional return amount from original line net amount
- cumulative returned-quantity guard
- optional Approval Engine request
- refund status independent from return posting status

## Inventory disposition

Each return line declares one disposition:

- `restock`: positive `sale_reversal` movement into an explicit warehouse
- `damaged`: no sellable stock increase in v39
- `waste`: no sellable stock increase in v39
- `no_stock`: financial/customer return only

`restock` requires warehouse_id.

Inventory posting is idempotent through the existing ledger source tuple:
`sales_return + return_public_id + return_line_public_id`.

Weighted-average valuation uses the warehouse's current WAC when no original sale-issue
cost snapshot is available. A later sale-stock orchestration bundle should persist the
original issue cost so returns can use exact historical issue valuation.

## Refund allocations

A return can allocate its value to one or more captured original payments.

Rules:
- payment must belong to original sale
- allocated pending + completed refunds cannot exceed original payment
- total allocations cannot exceed return total

Cash:
- completed synchronously
- reuses Cash `PostCashRefundToShiftAction`
- therefore requires the sale register to have an open shift
- cashbox and expected cash decrease atomically

Card / external:
- creates `payments.refunds` in pending state
- later provider/manual resolution marks completed or failed
- provider integrations can replace the manual resolution boundary without changing return data

## Sale payment status

`SalePaymentStatusUpdater` now understands completed payment refunds:

- paid
- partially_refunded
- refunded

Pending card/external refunds do not reduce captured value until resolved successfully.

## Approval Engine

Optional active policy:

- action_code: `sales.return.post`
- conditions.max_return_amount_without_approval

Above the configured threshold, posting requires an approved request:

- subject_type = `sales.return`
- subject_public_id = return ULID
- payload.return_total_amount = exact return total

## APIs

- POST `/api/v1/sales/{salePublicId}/returns`
- GET `/api/v1/sales/{salePublicId}/returns`
- GET `/api/v1/sales/returns/{returnPublicId}`
- POST `/api/v1/sales/returns/{returnPublicId}/post`
- POST `/api/v1/payments/refunds/{refundPublicId}/resolve`

## Important accounting boundary

v39 does not invent a provider refund success for card/external payments.
Those refunds remain pending until explicitly resolved.

This preserves UNKNOWN-resolution discipline and avoids pretending an external provider
has returned money when no provider integration exists yet.
