Add PriceList functionality with RetailPrice and AgencyMarginPercent
This commit is contained in:
@@ -50,6 +50,36 @@ default — the same negotiation-boundary pattern already used for discount limi
|
||||
(400, via a null resolution) if the requested price falls outside the band, or if no price can be
|
||||
resolved at all.
|
||||
|
||||
### Retail price and agency margin (informational, not enforced on sales)
|
||||
|
||||
Two more numbers can be tracked per `(PriceListItem)` beyond the wholesale `Price`/`MinPrice` band,
|
||||
covering the rest of the chain from distributor down to the end consumer:
|
||||
|
||||
- **`RetailPrice` (decimal?)** — the MRP the store is expected to charge the end consumer. Not
|
||||
enforced anywhere (a store can charge whatever it wants at the till) — it exists so the
|
||||
wholesale `Price` can be validated against it (a store must always have a positive margin) and so
|
||||
the sales rep can see it while negotiating with a store.
|
||||
- **`AgencyMarginPercent` (decimal?)** — the percentage of `Price` the regional agency operating
|
||||
that territory retains as commission for running local distribution (sales force, vehicles,
|
||||
warehousing). Purely informational/reporting — not deducted from anything automatically; it feeds
|
||||
agency settlement/payout reporting, the same `RegionalAgency` role that raises Agency Claims
|
||||
([18_returns_claims.md](./18_returns_claims.md)).
|
||||
|
||||
Neither field participates in `ResolveUnitPriceAsync`'s pricing logic or the `[MinPrice, Price]`
|
||||
negotiation band — they ride along in the same `PriceListItem` row and `PriceResolution` response
|
||||
purely for display and reporting.
|
||||
|
||||
- `PriceListItem.RetailPrice (decimal?)` — must be `>= Price` when set (409 if violated); `null` =
|
||||
not tracked for this item/list.
|
||||
- `PriceListItem.AgencyMarginPercent (decimal?)` — must be between 0 and 100 when set (409 if
|
||||
violated); `null` = not tracked for this item/list.
|
||||
- `PUT /api/price-lists/{id}/items/{sku}` accepts the extended body
|
||||
`{ "price": ..., "minPrice": ..., "retailPrice": ..., "agencyMarginPercent": ... }` — both new
|
||||
fields are optional and independent of each other and of `minPrice`.
|
||||
- `PriceResolution` (returned by `ResolvePriceAsync`/`ResolveByStoreAsync`) carries both fields
|
||||
through so the frontend can display them without a second lookup; both are `null` on the
|
||||
`Item.SalePrice` fallback path, since neither is tracked outside an actual `PriceListItem`.
|
||||
|
||||
---
|
||||
|
||||
## Data Model Changes
|
||||
@@ -75,6 +105,10 @@ PriceListItem
|
||||
│ the parent PriceList is active and applicable to the order's region
|
||||
├── MinPrice (decimal?) — nullable floor for negotiated pricing; null = Price is fixed.
|
||||
│ Must be <= Price when set.
|
||||
├── RetailPrice (decimal?) — nullable MRP the store charges the end consumer; informational,
|
||||
│ not enforced on sales. Must be >= Price when set.
|
||||
├── AgencyMarginPercent (decimal?) — nullable, 0-100, the regional agency's commission % of Price;
|
||||
│ informational only, feeds agency settlement reporting.
|
||||
├── IsActive, CreatedAt — from Template
|
||||
```
|
||||
Unique constraint on `(PriceListId, ItemId)` — one price per item per price list.
|
||||
@@ -138,10 +172,11 @@ One migration needed:
|
||||
|
||||
### PUT `/api/price-lists/{id}/items/{sku}`
|
||||
```json
|
||||
{ "price": 125.50, "minPrice": 115.00 }
|
||||
{ "price": 125.50, "minPrice": 115.00, "retailPrice": 150.00, "agencyMarginPercent": 15 }
|
||||
```
|
||||
Resolves `sku` to `ItemId`, upserts the `PriceListItem` row. `minPrice` is optional — omit or send
|
||||
`null` for a fixed, non-negotiable price. Rejected with 409 if `minPrice > price`.
|
||||
Resolves `sku` to `ItemId`, upserts the `PriceListItem` row. `minPrice`, `retailPrice`, and
|
||||
`agencyMarginPercent` are all optional — omit or send `null` for none. Rejected with 409 if
|
||||
`minPrice > price`, `retailPrice < price`, or `agencyMarginPercent` is outside `[0, 100]`.
|
||||
|
||||
### POST `/api/suppliers`
|
||||
```json
|
||||
@@ -178,6 +213,14 @@ Resolves `sku` to `ItemId`, upserts the `PriceListItem` row. `minPrice` is optio
|
||||
`[MinPrice ?? Price, Price]` for the item's resolved `PriceListItem` (region-scoped, then
|
||||
global). The `Item.SalePrice` fallback has no `MinPrice` — always fixed, no negotiation. An
|
||||
out-of-band price, or no resolvable price at all, rejects the whole order (400).
|
||||
8. `PriceListItem.RetailPrice`, when set, must be `>= Price` — enforced on every write via
|
||||
`PUT /api/price-lists/{id}/items/{sku}` (409 if violated); a store must never be expected to
|
||||
sell below what it paid.
|
||||
9. `PriceListItem.AgencyMarginPercent`, when set, must be within `[0, 100]` — enforced the same
|
||||
way (409 if violated).
|
||||
10. Neither `RetailPrice` nor `AgencyMarginPercent` participates in order pricing/validation
|
||||
(rule 7) — both are informational only, carried through `PriceResolution` for display and
|
||||
reporting, and are `null` on the `Item.SalePrice` fallback path.
|
||||
|
||||
---
|
||||
|
||||
@@ -311,9 +354,14 @@ POST /api/price-lists
|
||||
→ PriceList A (RegionId = North)
|
||||
|
||||
PUT /api/price-lists/{A.Id}/items/COKE-330
|
||||
{ "price": 90, "minPrice": 80 }
|
||||
{ "price": 90, "minPrice": 80, "retailPrice": 120, "agencyMarginPercent": 15 }
|
||||
```
|
||||
This publishes a band for `COKE-330` in the North region: ceiling 90, floor 80.
|
||||
This publishes a band for `COKE-330` in the North region: ceiling 90, floor 80. It also records that
|
||||
the store is expected to retail it at 120 (so the store's own margin is 90–120, or up to 40 if the
|
||||
rep negotiated down to 80), and that the North region's agency retains 15% of whatever wholesale
|
||||
price was actually charged (e.g. 15% of 90 = 13.50 if sold at the ceiling) as their distribution
|
||||
commission — informational only, used for agency settlement reporting, not deducted automatically
|
||||
from anything.
|
||||
|
||||
**Order at the default (no negotiation):**
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user