# Products Pricing Specification

## Purpose

The `products-pricing` capability exposes a single read-only endpoint that computes and returns the effective unit sale price for one product, given an optional customer identity and an optional selling unit. It is the pricing authority called synchronously by the POS terminal on every cart-line event.

## Requirements

### Requirement: Authentication Required

The system SHALL reject any request from an unauthenticated session with a `302` redirect to `auth/login` before processing any parameters. (Implemented in `routes/web.php:47-48`, admin group `['web','admin']`)

#### Scenario: Unauthenticated request is redirected

- **GIVEN** a caller that has no authenticated admin session
- **WHEN** the caller sends `GET /products/get-price` with any query parameters
- **THEN** the system responds with HTTP `302` redirecting to `auth/login` and performs no pricing computation

---

### Requirement: product_id Is Mandatory

The system SHALL validate that `product_id` is present and truthy before performing any lookup, and SHALL return HTTP `422` with the body `"product_id is required"` when it is absent or falsy. (Implemented in `app/Http/Controllers/ProductController.php:277-280`)

#### Scenario: Missing product_id returns 422

- **GIVEN** an authenticated admin session
- **WHEN** the caller sends `GET /products/get-price` with no `product_id` parameter
- **THEN** the system returns HTTP `422` with `Content-Type: application/json` and body `"product_id is required"`

#### Scenario: Falsy product_id returns 422

- **GIVEN** an authenticated admin session
- **WHEN** the caller sends `GET /products/get-price?product_id=0`
- **THEN** the system returns HTTP `422` with body `"product_id is required"`

---

### Requirement: Unknown Product Returns 404

The system SHALL return HTTP `404` with the body `"Product not found"` when `product_id` is truthy but does not correspond to any existing product, without leaking any framework exception internals. (Implemented in `app/Http/Controllers/ProductController.php:282-286`)

#### Scenario: Non-existent product_id returns 404

- **GIVEN** an authenticated admin session and no product exists with id `999999`
- **WHEN** the caller sends `GET /products/get-price?product_id=999999`
- **THEN** the system returns HTTP `404` with `Content-Type: application/json` and body `"Product not found"`

---

### Requirement: Customer Resolution by Id

The system SHALL resolve the optional customer from the `id` query parameter using a non-throwing lookup; an unknown `id` SHALL silently yield no customer (degrading to retail pricing) rather than returning an error. (Implemented in `app/Http/Controllers/ProductController.php:289-290`)

#### Scenario: Known customer id applies that customer's pricing tier

- **GIVEN** an authenticated admin session, an existing product, and a customer with id `7` and type `si_1`
- **WHEN** the caller sends `GET /products/get-price?product_id=<id>&id=7`
- **THEN** the system returns HTTP `200` with the wholesale `si_1` price for that product

#### Scenario: Unknown customer id degrades to retail pricing

- **GIVEN** an authenticated admin session, an existing product, and no customer with id `999999`
- **WHEN** the caller sends `GET /products/get-price?product_id=<id>&id=999999`
- **THEN** the system returns HTTP `200` with the product's `sale_price` (retail)

---

### Requirement: Customer Resolution by Phone

The system SHALL resolve the optional customer from the `phone` query parameter using a first-match lookup; an unknown phone SHALL silently yield no customer rather than returning an error. (Implemented in `app/Http/Controllers/ProductController.php:291-292`)

#### Scenario: Known phone applies that customer's pricing tier

- **GIVEN** an authenticated admin session, an existing product, and a customer with phone `0909123456` and type `si_2`
- **WHEN** the caller sends `GET /products/get-price?product_id=<id>&phone=0909123456`
- **THEN** the system returns HTTP `200` with the wholesale `si_2` price for that product

#### Scenario: Unknown phone degrades to retail pricing

- **GIVEN** an authenticated admin session and an existing product
- **WHEN** the caller sends `GET /products/get-price?product_id=<id>&phone=0000000000` where no customer has that phone
- **THEN** the system returns HTTP `200` with the product's `sale_price`

---

### Requirement: Phone Overrides Id When Both Are Present

When both `id` and `phone` are provided and each resolves a customer, the system SHALL apply the customer resolved by `phone` and ignore the customer resolved by `id`. (Implemented in `app/Http/Controllers/ProductController.php:289-292`)

#### Scenario: phone customer wins over id customer

- **GIVEN** an authenticated admin session, an existing product, customer A with id `7` and type `si_1`, and customer B with phone `0909123456` and type `si_2`
- **WHEN** the caller sends `GET /products/get-price?product_id=<id>&id=7&phone=0909123456`
- **THEN** the system returns HTTP `200` with the `si_2` wholesale price (customer B's tier, not customer A's)

---

### Requirement: Wholesale Tier Pricing

The system SHALL return the wholesale tier price `wholesale_prices[type]` as the base price when the resolved customer's `type` is truthy and that key exists in the product's `wholesale_prices`; in all other cases the system SHALL use `sale_price` as the base price. (Implemented in `app/Models/Product.php:96`)

#### Scenario: Wholesale customer with configured tier gets tier price

- **GIVEN** a product with `sale_price = 20000` and `wholesale_prices.si_1 = 18000`, and a resolved customer with `type = si_1`
- **WHEN** the pricing computation runs
- **THEN** the base price is `18000`

#### Scenario: No customer falls back to sale_price

- **GIVEN** a product with `sale_price = 20000` and no customer resolved
- **WHEN** the pricing computation runs
- **THEN** the base price is `20000`

#### Scenario: Retail-tier customer falls back to sale_price

- **GIVEN** a product with `sale_price = 20000` and a resolved customer with `type = khach_le`
- **WHEN** the pricing computation runs
- **THEN** the base price is `20000` (no `khach_le` key in `wholesale_prices`)

#### Scenario: Wholesale customer with no configured tier price falls back to sale_price

- **GIVEN** a product with `sale_price = 20000` and no entry for `si_1` in `wholesale_prices`, and a resolved customer with `type = si_1`
- **WHEN** the pricing computation runs
- **THEN** the base price is `20000`

---

### Requirement: Unit Conversion by Division

When `unit_id` resolves to a `ProductUnit` whose `conversion_qty` is greater than zero, the system SHALL return `basePrice / conversion_qty` as the effective price; otherwise the system SHALL return the base price unchanged. (Implemented in `app/Models/Product.php:98-105`)

#### Scenario: Valid unit_id with positive conversion_qty divides base price

- **GIVEN** a product with `sale_price = 24000` and a `ProductUnit` with `unit_id = 5` and `conversion_qty = 12`
- **WHEN** the caller sends `GET /products/get-price?product_id=<id>&unit_id=5`
- **THEN** the system returns HTTP `200` with body `2000`

#### Scenario: Absent unit_id returns base price undivided

- **GIVEN** a product with `sale_price = 24000`
- **WHEN** the caller sends `GET /products/get-price?product_id=<id>` with no `unit_id`
- **THEN** the system returns HTTP `200` with body `24000`

#### Scenario: Unknown unit_id returns base price undivided

- **GIVEN** a product with `sale_price = 24000` and no `ProductUnit` matching `unit_id = 999`
- **WHEN** the caller sends `GET /products/get-price?product_id=<id>&unit_id=999`
- **THEN** the system returns HTTP `200` with body `24000`

#### Scenario: unit_id with conversion_qty of zero returns base price undivided

- **GIVEN** a product with `sale_price = 24000` and a `ProductUnit` with `unit_id = 5` and `conversion_qty = 0`
- **WHEN** the caller sends `GET /products/get-price?product_id=<id>&unit_id=5`
- **THEN** the system returns HTTP `200` with body `24000`

---

### Requirement: Success Response Is a Bare JSON Number

On a successful price computation the system SHALL respond with HTTP `200`, `Content-Type: application/json`, and a body that is a bare JSON numeric value representing the effective unit price. (Implemented in `app/Http/Controllers/ProductController.php:297`)

#### Scenario: Integer price is returned as a JSON number

- **GIVEN** a product with `sale_price = 20000` and no customer or unit supplied
- **WHEN** the caller sends `GET /products/get-price?product_id=<id>`
- **THEN** the system returns HTTP `200` with body `20000` and `Content-Type: application/json`

#### Scenario: Non-integer division result is returned as-is

- **GIVEN** a product with `sale_price = 20000` and a `ProductUnit` with `conversion_qty = 12`
- **WHEN** the caller sends `GET /products/get-price?product_id=<id>&unit_id=<unit>`
- **THEN** the system returns HTTP `200` with body `1666.6666666667` (the raw division result, no rounding applied)

---

### Requirement: Error Responses Use Distinct Status Codes per Failure Kind

The system SHALL return a distinct HTTP status code and a bare JSON string body for each defined failure kind: `422` with `"product_id is required"` for a missing or falsy `product_id`, and `404` with `"Product not found"` for an unresolvable product. The system SHALL NOT expose raw framework exception messages in any error response body. (Implemented in `app/Http/Controllers/ProductController.php:277-286`)

#### Scenario: Missing product_id produces 422 not 404

- **GIVEN** an authenticated admin session
- **WHEN** the caller sends `GET /products/get-price` with no `product_id`
- **THEN** the system returns `422` with body `"product_id is required"`, not `404` and not `400`

#### Scenario: Non-existent product produces 404 not 400

- **GIVEN** an authenticated admin session and no product exists with id `999999`
- **WHEN** the caller sends `GET /products/get-price?product_id=999999`
- **THEN** the system returns `404` with body `"Product not found"`, not `400`, and the body contains no framework stack-trace or exception class name
