Add project documentation (CLAUDE.md)
Architecture, schema map, rebuild order, and testing notes for the database-first pricing engine. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ca625c0c2b
commit
14269f64fc
161
CLAUDE.md
Normal file
161
CLAUDE.md
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This is a **database-first pricing engine** that computes product pricing recommendations. Business logic lives entirely in SQL stored procedures and functions. There is no application server — consumers call the database directly. Both SQL Server (`.ms.sql`) and PostgreSQL (`.pg.sql`) implementations exist in parallel.
|
||||||
|
|
||||||
|
## Testing a Pricing Scenario
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- PostgreSQL (primary)
|
||||||
|
SELECT *, ui_json->'data'
|
||||||
|
FROM pricequote.single_price_call(
|
||||||
|
'GRIF0001', -- bill-to customer code
|
||||||
|
'GRIF0001', -- ship-to customer code
|
||||||
|
'XNS0T1G3G18B096', -- part number
|
||||||
|
'v1:B..PLT..', -- product data segment
|
||||||
|
9600 -- volume (pieces)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- SQL Server
|
||||||
|
EXEC pricing.single_price_call
|
||||||
|
@bill = 'GRIF0001',
|
||||||
|
@ship = 'GRIF0001',
|
||||||
|
@part = 'XNS0T1G3G18B096',
|
||||||
|
@v1ds = 'v1:B..PLT..',
|
||||||
|
@vol = 9600;
|
||||||
|
```
|
||||||
|
|
||||||
|
Test `guidance_logic` in isolation (SQL Server):
|
||||||
|
```sql
|
||||||
|
SELECT g.*
|
||||||
|
FROM (SELECT
|
||||||
|
TRY_CAST(.33275 AS NUMERIC(20,5)) AS tprice,
|
||||||
|
TRY_CAST(.758 AS NUMERIC(20,5)) AS last_price_norm,
|
||||||
|
TRY_CAST(null AS NUMERIC(20,5)) AS listprice_eff,
|
||||||
|
TRY_CAST('2025-06-01' AS DATE) AS last_date
|
||||||
|
) q
|
||||||
|
CROSS APPLY pricing.guidance_logic(q.tprice, q.last_price_norm, q.listprice_eff, q.last_date, 0.05, 1.0, 1.0) g;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Rebuild (PostgreSQL)
|
||||||
|
|
||||||
|
Run in order after source data changes:
|
||||||
|
```sql
|
||||||
|
REFRESH MATERIALIZED VIEW pricequote.lastpricedetail; -- ~37s
|
||||||
|
CALL pricequote.rebuild_pricelist(); -- ~32s
|
||||||
|
CALL pricequote.refresh_target_prices_base(); -- ~45s
|
||||||
|
REFRESH MATERIALIZED VIEW rlarp.cost_v1ds;
|
||||||
|
REFRESH MATERIALIZED VIEW rlarp.cost_v0ds;
|
||||||
|
```
|
||||||
|
|
||||||
|
Batch pricing for the full matrix (seeds `pricequote.queue` from `rlarp.osm_stack`, then merges results back):
|
||||||
|
```sql
|
||||||
|
CALL pricequote.process_queue();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### `single_price_call` — the main entry point
|
||||||
|
Located in `procs/single_price_call.pg.sql` (and `.ms.sql`). Accepts `(bill, ship, part, v1ds, vol)` and returns a fully enriched pricing row including `guidance_price`, `expl` (JSONB explanation), and `ui_json` (structured panels for UI rendering).
|
||||||
|
|
||||||
|
**Processing steps:**
|
||||||
|
1. Resolve `chan` / `tier` / `cust` from bill/ship codes via `rlarp.cust`
|
||||||
|
2. Look up historical prices from `pricequote.lastpricedetail` (materialized view over `rlarp.osm_stack`)
|
||||||
|
3. Look up target price from `pricequote.target_prices_base` (keyed by stlc, ds, chan, tier, volume range)
|
||||||
|
4. Resolve cost standards for cross-segment normalization (`rlarp.cost_v1ds`, `rlarp.cost_v0ds`)
|
||||||
|
5. Normalize last price when the historical sale was a different data segment (`last_isdiff`)
|
||||||
|
6. Look up list price from `pricequote.pricelist_ranged` via `CMS.CUSLG.IPRCBHC` price-level mapping
|
||||||
|
7. Apply `pricequote.guidance_logic` to produce final recommended price
|
||||||
|
8. Build `expl` and `ui_json` JSON
|
||||||
|
|
||||||
|
### `process_queue` — batch version
|
||||||
|
`procs/matrix_guidance.pg.sql`. Seeds `pricequote.queue` from `rlarp.osm_stack`, runs the same 12-step logic as `single_price_call` via set-based UPDATEs, then merges `expl`/`ui` back into `rlarp.osm_stack.pricing`.
|
||||||
|
|
||||||
|
### Key helper functions
|
||||||
|
- `pricequote.pick_last_price_from_hist(hist JSONB, v1ds TEXT)` — selects the best historical price from the packed `part_stats` JSONB using source precedence: `dss` → `mrs` → fallback (18-month age threshold). Source codes: `dss`=direct sale, `dsq`=direct quote, `mrs`=most-recent-similar sale, `mrq`=most-recent-similar quote.
|
||||||
|
- `pricequote.guidance_logic(target, last_norm, list_eff, last_date, ...)` — picks base price (target → last → list), prevents price drops below last, caps at list price.
|
||||||
|
- `pricequote.build_pricing_path_base(options JSONB)` — recursive CTE that expands a JSON array of pricing options (Anchor, Color Tier, Packaging, Channel, Volume, etc.) into a full combinatorial pricing matrix.
|
||||||
|
|
||||||
|
### Target price computation
|
||||||
|
`pricequote.core_target` holds anchor prices + options JSON per product line. `pricequote.refresh_target_prices_base()` calls `build_pricing_path_base()` to expand those into every `(stlc, ds, chan, tier, vol_range)` combination and stores results in `pricequote.target_prices_base`.
|
||||||
|
|
||||||
|
### Product data segments (`v1ds` / `v0ds`)
|
||||||
|
Format: `v1:B..PLT..` encodes color tier (position 4), branding (position 6), packaging (position 8-10), etc. The `v0ds` simplification reduces to just color tier + branding suffix. Customized parts have a `v1ds` that differs from their item master `v1ds`.
|
||||||
|
|
||||||
|
### Channel/tier resolution
|
||||||
|
- Bill-to class `DIS` + ship-to class `DIS` → channel `WHS` (wholesale); tier/plevel from bill-to distributor
|
||||||
|
- Bill-to class `DIS` + ship-to class non-`DIS` → channel `DRP` (drop-ship); tier/plevel from ship-to
|
||||||
|
- Bill-to class `DIR` → channel `DIR`; tier/plevel from bill-to
|
||||||
|
|
||||||
|
## Database Schemas
|
||||||
|
|
||||||
|
| Schema | Database | Purpose |
|
||||||
|
|--------|----------|---------|
|
||||||
|
| `pricequote.*` | PostgreSQL | All pricing functions, tables, materialized views |
|
||||||
|
| `pricing.*` | SQL Server | Parallel SQL Server implementation |
|
||||||
|
| `rlarp.*` | Both | Source data: `osm_stack` (order/quote history), `cust`, `cost_v1ds`, `cost_v0ds` |
|
||||||
|
| `CMS.CUSLG.*` | Both | ERP item master (`itemm`), price level assignments (`IPRCBHC`) |
|
||||||
|
|
||||||
|
## File Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
procs/ -- stored procedures and functions (core logic)
|
||||||
|
tables/ -- table/view DDL
|
||||||
|
builder/ -- build_pricing_path_base and related option-cost helpers
|
||||||
|
rebuild/ -- refresh scripts for materialized views and derived tables
|
||||||
|
quote_review/ -- reporting views over osm_stack
|
||||||
|
archive/ -- legacy TypeScript and older SQL approaches (reference only)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quote Review (Power BI dashboard source)
|
||||||
|
|
||||||
|
`rlarp.quote_review` (PostgreSQL view, DDL at `quote_review/quote_review.pg.sql`) is the source for the Power BI quote dashboard — a per-quote-line list with a related-sales scatter plot. Full lineage crosses platforms:
|
||||||
|
|
||||||
|
```
|
||||||
|
SQL Server view fanalysis.rlarp.live_quotes DDL: SQL repo quotes/live_quotes.sql (ALTER VIEW)
|
||||||
|
↓ postgres_fdw (server usmidsql01)
|
||||||
|
Postgres foreign table pricequote.live_quotes DDL: /opt/sync/usmidsql01/live_quotes/ddl.sql
|
||||||
|
↓
|
||||||
|
Postgres view rlarp.quote_review DDL: quote_review/quote_review.pg.sql
|
||||||
|
↓
|
||||||
|
Power BI dashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
- `rlarp.live_quotes` is a **SQL Server** view (in the SQL repo), surfaced to Postgres only as the FDW table `pricequote.live_quotes` (note: `pricequote` schema, not `rlarp`). It filters to quotes touched in the last 60 days, status IN (3,8,9,11,4).
|
||||||
|
- `quote_review.pg.sql` filters `pricequote.live_quotes` to `qstat LIKE '%Submitted%'`, then layers guidance logic, customer tier, price limits/midrange, target price, part group.
|
||||||
|
- **Name collision:** SQL Server objects also named `rlarp.quote_review` exist in the SQL repo (`quotes/quote_reviews/quote_review.ms.sql` is an `ALTER PROC`; `sql/mssql/pricing/reporting/quote_review.sql` is an `ALTER VIEW`) — these are NOT the dashboard source.
|
||||||
|
- CASCADE dependent of `osm_stack`; auto-rebuilt by salesmatrix's `tables/pg/rebuild_osm_stack_dependents.sh`.
|
||||||
|
|
||||||
|
## Open Issue: Last Price Is Net of Discounts
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
Last price is currently computed as `sales_usd / qty` where `sales_usd = fb_val_loc * r_rate`. `fb_val_loc` is the **net value after discounts**, so last price reflects a discounted unit price rather than a base/list price. This causes price_api to anchor guidance off a lower-than-intended price for customers who receive line discounts.
|
||||||
|
|
||||||
|
### Data Available to Fix It
|
||||||
|
In `rlarp.osm` (both PG and MS SQL), `fb_val_loc_dis` holds the discount amount and `fb_val_loc` holds the net value. Gross/base value = `fb_val_loc + fb_val_loc_dis`. However, **`fb_val_loc_dis` is not carried into `osm_stack`** on either database — it is dropped during `osm_stack_refresh()`.
|
||||||
|
|
||||||
|
### Required Changes
|
||||||
|
To fix this properly, changes are needed in both databases (MS SQL feeds `rebuild_lastprice`; PostgreSQL feeds `pricequote.lastpricedetail`):
|
||||||
|
|
||||||
|
**PostgreSQL (`/opt/salesmatrix`):**
|
||||||
|
1. `tables/pg/osm_stack.pg.sql` — add `gross_usd NUMERIC` and `gross_local NUMERIC` columns
|
||||||
|
2. `procs/pg/osm_stack_refresh.pg.sql` — populate: `gross_local = fb_val_loc + fb_val_loc_dis`, `gross_usd = gross_local * r_rate`
|
||||||
|
3. `tables/pg/lastpricedetail.pg.sql` (`pricequote.lastpricedetail`) — change price calc from `sales_usd/qty` to `gross_usd/qty`
|
||||||
|
|
||||||
|
**MS SQL (`/opt/salesmatrix`):**
|
||||||
|
1. `tables/ms/osm_stack.ms.sql` — add `gross_usd` and `gross_local` columns
|
||||||
|
2. `procs/ms/osm_stack_refresh.sql` — populate same as PG
|
||||||
|
3. `rebuild/rebuild_lastprice.ms.sql` — change price calc to use `gross_usd/qty`
|
||||||
|
|
||||||
|
**Note on PG DROP CASCADE**: Changing `osm_stack` DDL cascades to `osm_stack_pretty`, `osm_powerbi`, `price_pool`, `quote_review`, `pricequote.lastpricedetail` — all must be rebuilt after any DDL change. Adding a column avoids the drop; use `ALTER TABLE rlarp.osm_stack ADD COLUMN`.
|
||||||
|
|
||||||
|
## Deployment Order
|
||||||
|
|
||||||
|
When making schema changes, deploy in dependency order:
|
||||||
|
1. `tables/` DDL
|
||||||
|
2. `builder/` functions
|
||||||
|
3. `procs/guidance_logic`, `procs/lastprice_logic`, `procs/approval_logic`
|
||||||
|
4. `procs/single_price_call`, `procs/matrix_guidance`
|
||||||
Loading…
Reference in New Issue
Block a user