# product-unit-pricing Specification

## Purpose

Selling one product in several units: configuring conversion units whose retail and wholesale prices derive from a per-unit formula, choosing a default selling unit, picking a unit per POS cart line, and pricing orders server-side while snapshotting the unit and conversion quantity actually sold.

## Requirements

### Requirement: Admin configures conversion units with auto-derived pricing
The product create and edit forms SHALL provide a "Giá bán theo đơn vị quy đổi" section where an admin can add and remove any number of conversion-unit rows for a product. Each row SHALL capture a unit (selected from the `units` table) and a conversion quantity, and SHALL display a computed, read-only retail price equal to the product's base retail price (`sale_price`) divided by the row's conversion quantity. No row SHALL accept a manually-entered price.

#### Scenario: Adding a conversion unit computes its price from the base price
- **WHEN** an admin sets the product's base retail price to 100000, adds a conversion-unit row with unit "Vỉ" and conversion quantity 5
- **THEN** the row displays a computed unit price of 20000

#### Scenario: Adding multiple conversion units computes each independently
- **WHEN** an admin adds a second conversion-unit row with unit "Viên" and conversion quantity 50, on the same product from the prior scenario
- **THEN** the "Vỉ" row still shows 20000 and the "Viên" row shows 2000

#### Scenario: Admin removes a conversion-unit row
- **WHEN** an admin removes a previously-added conversion-unit row before submitting the form
- **THEN** that row is not persisted for the product

#### Scenario: Product with no conversion units configured behaves as today
- **WHEN** an admin saves a product without adding any conversion-unit row
- **THEN** the product has no `product_units` records and is sellable only at its base unit, unchanged from current behavior

#### Scenario: Editing the base retail price re-derives conversion-unit prices
- **WHEN** an admin later edits an existing product's `sale_price` and saves, without touching its conversion-unit rows
- **THEN** each conversion unit's displayed and charged price is recomputed from the new `sale_price` divided by its stored conversion quantity, with no manual update required

### Requirement: Wholesale prices follow the same per-unit conversion formula
For each customer type present in `wholesale_prices`, every configured conversion unit's wholesale price SHALL be derived as that customer type's wholesale price divided by the unit's conversion quantity. The system SHALL NOT expose or store a separately-editable wholesale price per conversion unit.

#### Scenario: Wholesale price is derived per unit for a wholesale customer type
- **WHEN** a product's wholesale price for customer type "sỉ" is 80000, and it has a conversion-unit row "Vỉ" with conversion quantity 5
- **THEN** the price charged to a "sỉ" customer for one "Vỉ" is 16000

#### Scenario: Retail customers are unaffected by wholesale derivation
- **WHEN** a customer with no wholesale type (retail/"khách lẻ") buys a conversion unit
- **THEN** that unit's price is derived from `sale_price`, not from any `wholesale_prices` entry

### Requirement: One conversion unit may be marked the default selling unit
Each product's conversion-unit rows MAY have exactly one marked as the default selling unit (`is_default`). A product with no conversion unit marked default SHALL default to its base unit.

#### Scenario: Admin marks a conversion unit as default
- **WHEN** an admin marks the "Viên" conversion-unit row as default and saves
- **THEN** that product's default selling unit is "Viên"

#### Scenario: Marking a new default unmarks the previous one
- **WHEN** an admin marks a different conversion-unit row as default on a product that already had one marked
- **THEN** only the newly-marked row remains the default after saving

#### Scenario: No default marked falls back to the base unit
- **WHEN** a product has one or more conversion-unit rows but none marked as default
- **THEN** the product's default selling unit is its base unit

### Requirement: POS cart line offers a unit dropdown sourced from the product's configured units
When a product is added to the POS cart, its cart line SHALL show a unit dropdown listing the product's base unit plus every configured conversion unit, preselected to the product's default selling unit.

#### Scenario: Product with conversion units shows a populated dropdown
- **WHEN** a cashier adds a product that has conversion units configured to the cart
- **THEN** the cart line's unit dropdown lists the base unit and all configured conversion units, with the default selling unit preselected

#### Scenario: Product with no conversion units shows only the base unit
- **WHEN** a cashier adds a product with no `product_units` records to the cart
- **THEN** the cart line's unit dropdown shows only the base unit, matching current single-unit behavior

### Requirement: Changing a cart line's unit recalculates its price and subtotal
Selecting a different unit on an existing POS cart line SHALL immediately recompute that line's unit price and subtotal, using the currently-selected customer's type to determine retail vs. wholesale pricing, without requiring the item to be re-added.

#### Scenario: Switching unit recalculates price for a retail customer
- **WHEN** a cashier switches a cart line from the base unit to a configured "Vỉ" conversion unit, with no wholesale customer selected
- **THEN** the line's unit price updates to `sale_price / conversion_qty` for "Vỉ" and its subtotal updates to that price times the line's quantity

#### Scenario: Switching unit recalculates price for a wholesale customer
- **WHEN** a wholesale-type customer is selected on the order and a cashier switches a cart line to a configured conversion unit
- **THEN** the line's unit price updates using that customer's wholesale price divided by the unit's conversion quantity

### Requirement: Order creation prices each line from its selected unit, computed server-side
`OrderController` SHALL accept an optional unit selection per submitted cart item and SHALL compute the persisted price for that line entirely from the product, the resolved customer type, and the selected unit — never from any price value submitted by the client. A submitted unit that does not belong to the submitted product SHALL be rejected/ignored in favor of the base unit.

#### Scenario: Order line is priced from server-side product and unit data
- **WHEN** a cart submission includes an item with a `unit_id` for a configured conversion unit
- **THEN** the created order's line price is computed by the server from the product's current base price, the resolved customer type, and that unit's conversion quantity — regardless of any price the client displayed

#### Scenario: Unit not belonging to the product is not trusted
- **WHEN** a cart submission includes a `unit_id` that is not one of the submitted product's configured units
- **THEN** the server treats the line as sold at the base unit rather than applying an invalid conversion

#### Scenario: Omitted unit defaults to base-unit pricing
- **WHEN** a cart submission includes an item with no `unit_id`
- **THEN** the order line is priced at the product's base unit, matching current behavior

### Requirement: Order lines snapshot the unit and conversion quantity sold
Each persisted order line SHALL record the unit it was sold at (or none, for the base unit) and the conversion quantity used at the time of sale, independent of later edits to the product's conversion-unit configuration.

#### Scenario: Order line preserves its sold unit after configuration changes
- **WHEN** an order is created for a product sold as "Vỉ" with conversion quantity 5, and the product's "Vỉ" conversion quantity is later changed to 10
- **THEN** the previously-created order line still shows conversion quantity 5 and the price it was originally sold at

#### Scenario: Order line sold at the base unit records no conversion unit
- **WHEN** an order line is sold at the product's base unit
- **THEN** its stored unit reference is empty/null, consistent with existing orders created before this feature
