The annual flip. Once per year on July 1 (cron 0 5 1 7 *) or by manual trigger, the Bid Center converts an entire year of accumulated bid work into next year's NetSuite named price lists. The prepare_next_school_year Workflow enumerates 69 active customers, picks each customer's latest SY-version, applies the HPS MAP GPO allowance program where applicable, and builds 69 proposed NS named price list payloads. All 69 land in a single batch proposed_actions row with shared batch_id='jul1-2026'. On approval, NS_PUSH_QUEUE drains 69 writes (one per customer, gated by PushMutexDO). Partial failures loop back. Customer service sees the new named list on the next NS customer open. First live deploy is July 1, 2026.
AnnualRollWorkflow in src/annual_roll_workflow.ts (not prepare_next_school_year). It is bound as ANNUAL_ROLL_WORKFLOW in wrangler.jsonc. It is manually triggered; the cron 0 5 1 7 * is NOT present in wrangler.jsonc[triggers.crons]./api/bid/rollover/start, /api/ns/push/named-price-list, /api/ns/push-queue/drain do not exist. The wired endpoints are POST /api/workflows/annual-roll/start and POST /api/workflows/annual-roll/approve/<run_id>.step.waitForEvent('annual_roll:admin_approval', timeout: '30 days') — not a 69-line-item proposed_actions cohort with batch_id='jul1-2026'.https://gfs-pricing.pages.dev/#nsroll, the R616 variance flag (10% AND $5), and the R621 dynamic SY label render in Bid Center UI only — they are not yet wired into the workflow input. Workflow currently derives next-SY from customer_pricing_proposed via median of last 3 invoices, not from operator-curated NS Price Roll drafts.sy_readiness, ready_for_rollover, or equivalent in customers / bid_customers / migrations through 144.customer.pricelevel pointer). The code pushes bulk_replace on pricing_customer_prices via NS_PUSH_QUEUE to RESTlet #2948. The "named price list" record model in NS is aspirational.Bottom line: the rollover can run today, but only as a manual-trigger pricing-derivation workflow against last-12-months invoices — not as the bid-curated, NS-price-list cohort flow described below. Closing the gap requires either implementing the gaps or rewriting this doc to match what ships. See Gaps section at end.
jul1-2026)| kind | name | purpose |
|---|---|---|
| Cron | 0 5 1 7 * | Jul 1 05:00 UTC |
| Workflow class | src/annual_roll_workflow.ts | prepare_next_school_year |
| D1 table | cron_locks | prevents double-run |
| D1 table | bid_customers | 69 active cohort |
| D1 table | bid_quote_versions | is_latest=1 per customer |
| D1 table | bid_programs | HPS MAP GPO allowance |
| D1 table | proposed_actions | batch_id='jul1-2026' |
| D1 table | ns_pending_pushes | NS_PUSH_QUEUE |
| Durable Object | PushMutexDO | per-customer mutex |
| Endpoint | POST /api/bid/rollover/start | manual trigger |
| Endpoint | POST /api/ns/push/named-price-list | per-customer NS write |
| Endpoint | POST /api/proposed-actions/bulk-decide | cohort approve / subset / reject |
| NS RESTlet | customscript_gfs_platform_push_pricelist | writes NS PriceList |
| Event | bid.namedlist_pushed | per customer success |
| Event | bid.rollover_completed | cohort done |
Items below were identified by comparing this doc to the actual implementation in src/annual_roll_workflow.ts, src/index.ts, wrangler.jsonc, and migrations 117-144. Mike needs a decision per row: (A) ship the code to match the doc, (B) rewrite the doc to match reality, or (C) run the rollover with current scope and defer the rest.
| # | Gap | Doc claim | Reality (2026-05-27) | Severity | Suggested action |
|---|---|---|---|---|---|
| 1 | Cron trigger 0 5 1 7 * |
Cron fires Jul 1 05:00 UTC | Not in wrangler.jsonc[triggers.crons]. Workflow is manual-only. |
BLOCKING | Add the cron OR commit to "Mike-initiated only" and update the doc + runbook. If cron is desired, add cron handler in scheduled() that calls the annual-roll start endpoint with school_year='SY2728', cutover_date='2026-07-01'. |
| 2 | Workflow class name prepare_next_school_year |
Class instantiated as prepare_next_school_year |
Class is AnnualRollWorkflow (binding ANNUAL_ROLL_WORKFLOW). The skill file PREPARE_NEXT_SCHOOL_YEAR.md is an operator skill, not a Workflows class. |
HIGH (terminology) | Rename in this doc + wiki, OR rename the class. Choose one source of truth. |
| 3 | Endpoints /api/bid/rollover/start, /api/ns/push/named-price-list, /api/ns/push-queue/drain |
Listed in tables + runbook + diagram | None exist. Wired endpoints: POST /api/workflows/annual-roll/start, POST /api/workflows/annual-roll/approve/<run_id>. Push queue drain happens implicitly via NS_PUSH_QUEUE consumer; no admin force-drain endpoint. |
BLOCKING | Replace doc endpoints with the real ones. The runbook scenarios that reference POST /api/ns/push-queue/drain?batch_id=jul1-2026 will fail with 404 if Mike runs them under stress. |
| 4 | Batch HITL via proposed_actions with shared batch_id='jul1-2026', 69 line items, approve-all/subset/reject-row |
Diagram steps 8-11 + cohort UI panel + POST /api/proposed-actions/bulk-decide |
Workflow uses a single step.waitForEvent('annual_roll:admin_approval', timeout: '30 days') with one decision payload {decision: 'approved'|'rejected', notes}. There is no per-customer cohort UI, no 69-row staging, no approve-subset path. /api/proposed-actions/bulk-decide exists generally but is not what the annual roll uses. |
BLOCKING | Either (a) stage 69 proposed_actions rows in step 5 + replace the waitForEvent with a poll-on-cohort-status, building the partial-approve UI; OR (b) accept the current "all-or-nothing per run" gate and remove all subset-approval language from the doc + wiki + runbook scenario 4. |
| 5 | 69-customer cohort source: bid_customers WHERE status='active' + bid_quote_versions WHERE is_latest=1 |
Diagram steps 4-5 | Workflow enumerates SELECT DISTINCT customer FROM customer_pricing_proposed WHERE source='workflow:annual_roll' after writing rows via a 12-month invoice median (step 2 in the code). It does not read bid_quote_versions at all. The Bid Center curated state and the rollover input are decoupled. |
BLOCKING (architecture) | Decide: do we roll curated bids (what the doc says, what the new R612 NS Price Roll tab stages) or derived invoice medians (what the code does)? The two answers can disagree by hundreds of $/customer. Mike must pick before Jul 1. |
| 6 | NS named-price-list records + customer.pricelevel pointer + RESTlet customscript_gfs_platform_push_pricelist |
Diagram steps 13-15 + tables row "NS RESTlet" | Push payload is {target_table:'pricing_customer_prices', action:'bulk_replace', ns_record_type:'customer', push_uuid, ...} to RESTlet #2948 (the existing SuiteAPI RESTlet). No "named price list" custom record exists in NS today. customer.pricelevel is not touched by the workflow. |
BLOCKING (NS-side) | Confirm with Mike: does NS have a custom PriceList record type provisioned, and does #2948 or a new customscript_gfs_platform_push_pricelist handle it? If no — either build the NS RESTlet + custom record, or pivot the doc to describe "per-customer item-pricing bulk_replace" which is what actually ships. |
| 7 | R612 NS Price Roll 26-27 tab at gfs-pricing.pages.dev/#nsroll is the operator surface for staging |
Implied as the pre-rollover prep surface (not yet in diagram) | No #nsroll hash route in the gfs-pricing Pages project. Bid Center has /customers, /bids, /items, /price-matrix, /regional, /coverage, /compare per admin-dashboard hub links (lines 933-941), but nothing labeled "NS Price Roll" or "rollover staging." |
HIGH | Either ship the R612 tab before Jul 1 (operator must have a place to review 69 drafts) OR remove the reference. Without it there is no pre-flight surface; Mike is approving a workflow run blind. |
| 8 | R616 variance flag (10% AND $5) pre-push | Implied as the gate on outliers in NS Price Roll tab | Workflow does flag outliers in step 4, but the threshold is a single 15% drift via drift_threshold_pct param. No $5 absolute floor; no dual-threshold AND logic. Flags land in pricing_anomalies, not surfaced in any "NS Price Roll" tab. |
MEDIUM | Update drift_threshold_pct caller to pass 10 AND add AND ABS(cpp.derived_price - cp.price) > 5.0 to the WHERE clause in step 4 SQL. Surface the flagged rows in the NS Price Roll tab (depends on gap #7). |
| 9 | R621 dynamic SY label (SY2627 → SY2728 auto-bump on Jul 1) | Implied — doc currently still says SY2627 throughout |
Dashboard auto-detects the current SY (per Mike's reference). The workflow takes school_year as a param; nothing in the workflow code itself bumps version labels on each customer. The doc still hard-codes SY2627, batch_id='jul1-2026', "ACC SY2627 v2.5" — these are stale by Jul 1. |
MEDIUM | Doc-side: replace hard-coded SY2627 with SY<current>→SY<next> notation. Code-side: confirm versionHistory table receives a row per customer per rollover (not visible in current workflow steps 8-11; customer_pricing_history is global, not per-customer-version). |
| 10 | Customer readiness flags reset on Jul 1 (clear prior year, set new year = false until reviewed) | Step in Mike's verbatim description; not currently in the diagram | No such DB fields exist. Grepped sy_readiness, ready_for_rollover, readiness_flag, readiness_status across migrations 117-144 + src/index.ts — zero hits. The "verified for new SY" concept lives only in skill docs. |
BLOCKING | Add a migration: ALTER TABLE customers ADD COLUMN sy_readiness TEXT (or per-customer rows in customer_sy_readiness(customer_id, sy_version, status, verified_at, verified_by)). Add reset step at workflow start (clear) + close (verify). Without this, "readiness" is folklore. |
| 11 | R614 sync-verify + healthcheck post-rollover | Mentioned in Mike's flow step 7 | Step 10 in code does mirror-back verify via ns_touchpoint_log after a 30-min sleep — this is the closest match. There is no separate "sync-verify (R614)" or post-rollover healthcheck endpoint. /api/healthcheck and /api/ns/touchpoints exist (per pricing project memory) but are not invoked by the workflow. |
LOW | Either add an explicit step 12 calling /api/healthcheck + /api/ns/touchpoints?since=<rollover_start> and persisting results to annual_roll_runs, OR document that step 10 mirror-back IS the post-rollover verify. |
| 12 | Regional sheets rebuild via rebuild_regional_sheets skill |
Mike's flow step 3 | Skill file exists at ~/Desktop/GFS-NetSuite-Cloudflare/source-documents/Bids_and_Customer_Pricing/Prompts/REBUILD_REGIONAL_SHEETS.md and writes via scripts/post-session-sync.js --skill rebuild_regional_sheets. This is operator-driven, runs outside the platform Worker. It is not orchestrated by the annual-roll workflow. |
LOW (operator process) | Add a doc-only callout: "Step 0 (operator): run rebuild_regional_sheets skill ~T-7d before Jul 1 to refresh the regional file inputs the NS Price Roll tab reads from." Migration 144 regional_price_sheets exists so the table is ready; the skill→D1 sync path needs confirmation. |
| 13 | Stale hard-coded "69 customers" | Diagram + wiki KPI cards + ld+json + arrows all say "69" | Admin dashboard hub still says "69 customers · 44 bids · 127 items" (line 934). But bid_customers table size depends on status='active' at run time. Hard-coding 69 in the schema.org howto + diagram labels will be wrong by Jul 1 if any customer churn occurs. |
LOW (cosmetic) | Replace "69" with "all active bid_customers" or "~69" in the diagram, and let the actual cohort size land in events.bid.rollover_completed. |
Mike's call before Jul 1: Gap #5 (curated-bid vs. derived-invoice input) and Gap #6 (NS named-list vs. bulk_replace push) are architecturally load-bearing. Pick a direction this week so there is time to either build or rewrite.