diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d97901c --- /dev/null +++ b/CLAUDE.md @@ -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`