price_api/CLAUDE.md
Paul Trowbridge 14269f64fc 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>
2026-06-18 10:15:05 -04:00

162 lines
9.0 KiB
Markdown

# 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`