Bid Center — Path 3 · ★ July 1 rollover

cron OR manual → enumerate 69 customers → build named price list payloads → batch HITL → NS_PUSH_QUEUE drain → NS records updated

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.

workflow contract batch HITL (ADR-031) first live deploy Jul 1 2026

Pipeline — ★ annual flip

idle
Bid Center path 3 - July 1 rollover - cron 0 5 1 7 * fires (or manual) - cron lock acquired - prepare_next_school_year workflow starts - enumerate 69 customers - per-customer latest SY-version - apply programs sidecar - build NS named price list payload - stage batch proposed_action with 69 rows - Mike batch HITL approve - NS_PUSH_QUEUE drains - PushMutexDO per customer - POST /api/ns/push/named-price-list per customer - NS PriceList written - on success mark done + event - on fail requeue or surface in partial-failure UI - cohort complete event - customer service sees new list - next SO reads new list - customer_health recomputes - cron lock released - customer + SY-version threaded throughout LANE A / Trigger + workflow start LANE B / Per-customer payload build (loop x 69) LANE C / ★ Batch HITL gate (single proposed_action, 69 line items) LANE D / NS_PUSH_QUEUE drain (PushMutexDO per customer) LANE E / Closeout (events, customer health, cron lock release) WHAT THIS DOES: On July 1 at 05:00 UTC, the cron fires. Mike can also trigger manually for dry runs or off-cycle rollovers. D1 TABLE: cron_locks (about to be acquired) TECHNICAL DETAILS: TRIGGER FIRES CRON schedule: 0 5 1 7 * (Jul 1 05:00 UTC) defined: wrangler.toml [triggers.crons] MANUAL POST /api/bid/rollover/start?dry_run=false GUARDS rate_limit, X-Edit-Token for manual mode STATUS: REAL (contract) - first live Jul 1 2026 ★ trigger: cron 0 5 1 7 * OR manual Jul 1 anchor · first live 2026 manual via /api/bid/rollover/start FINANCE-KEY · annual flip i WHAT THIS DOES: Acquire a cron_locks row to prevent double-run (e.g. cron + manual at the same time). Released at the end of the path. D1 TABLE: cron_locks (INSERT, cron_name='bid_rollover') TECHNICAL DETAILS: ACQUIRE CRON LOCK LOCK INSERT INTO cron_locks (cron_name, locked_at, expires_at) VALUES ('bid_rollover', now(), now()+'6 hours') COLLISION if already locked: abort with already-running STATUS: REAL acquire cron lock cron_name='bid_rollover' 6h TTL · collision check SECURITY i WHAT THIS DOES: prepare_next_school_year Workflow instance starts. Cloudflare Workflows class (src/annual_roll_workflow.ts) handles step durability. D1 TABLE: workflow_run_log (INSERT) TECHNICAL DETAILS: WORKFLOW STARTS CLASS src/annual_roll_workflow.ts WORKFLOW prepare_next_school_year (workflow_type) STEP DURABILITY CF Workflows native checkpointing STATUS: REAL (shell) - first live Jul 1 2026 prepare_next_school_year starts src/annual_roll_workflow.ts CF Workflows class BACKEND · workflow i WHAT THIS DOES: SELECT all active customers. 69 active today. Filter excludes archived. D1 TABLE: bid_customers (READ WHERE status='active') TECHNICAL DETAILS: ENUMERATE 69 ACTIVE CUSTOMERS QUERY SELECT customer_id FROM bid_customers WHERE status='active' ORDER BY customer_id COUNT 69 STATUS: REAL enumerate 69 active customers bid_customers WHERE status='active' cohort = 69 DATABASE i WHAT THIS DOES: For each customer, look up the latest SY-version (where is_latest=1). This is the customer's "current quote" - what gets pushed to NS. D1 TABLE: bid_quote_versions (READ) TECHNICAL DETAILS: PICK LATEST SY-VERSION QUERY SELECT sy_version FROM bid_quote_versions WHERE customer_id=? AND is_latest=1 INVARIANT exactly one is_latest=1 per customer STATUS: REAL pick latest SY-version per customer bid_quote_versions WHERE is_latest=1 trace key: customer + SY-version DATABASE i WHAT THIS DOES: For each customer, apply the HPS MAP GPO allowance program (SY2526 / SY2627). Programs adjust per-item pricing for compliance reasons. D1 TABLE: bid_programs (READ) TECHNICAL DETAILS: APPLY PROGRAMS SIDECAR PROGRAMS HPS MAP GPO allowance SY2526, SY2627 LOOKUP bid_programs WHERE customer_id=? AND sy_version=? EFFECT per-item adjustment applied to bid_price_snapshots STATUS: REAL apply HPS MAP GPO programs SY2526 / SY2627 allowance per-item price adjust MESSAGEBUS · compliance i WHAT THIS DOES: Build the NS named-price-list payload for this customer: header (customer, named_list_name, effective_date), lines (sku, price, UOM). Store in scratch until staged. D1 TABLE: scratch in workflow state TECHNICAL DETAILS: BUILD NS NAMED PRICE LIST PAYLOAD SHAPE { customer_id, named_list_name: '<customer> <sy_version>', effective_date: '2026-07-01', source_sy_version: <sy_version>, lines: [{sku, price, uom, brand, allowance_applied}] } SIZE ~127 commercial items x 8 brands minus customer overrides STATUS: REAL build NS named-price-list payload customer + SY-version + lines + allowance named_list_name = '<customer> <sy_version>' BACKEND i WHAT THIS DOES: Steps 5-7 repeat for each of 69 customers. CF Workflows checkpoints between iterations so a Worker restart resumes mid-loop. D1 TABLE: workflow_step_log (per iteration) TECHNICAL DETAILS: LOOP x 69 STEP DURABILITY each iteration is a workflow step on restart resume at last completed customer STATUS: REAL loop x 69 (CF Workflows step-durable) resume mid-loop on Worker restart BACKEND i WHAT THIS DOES: All 69 payloads are staged as a single batch with shared batch_id='jul1-2026'. Each customer is a line item; UI allows approve-all, approve-subset, reject per customer. D1 TABLE: proposed_actions (INSERT 69 rows with shared batch_id) TECHNICAL DETAILS: STAGE BATCH PROPOSED_ACTION INSERTS for each customer: INSERT INTO proposed_actions kind='ns_named_price_list', batch_id='jul1-2026', customer_id, sy_version, payload (JSON), status='pending', risk_level=5 INVARIANT (ADR-031) no NS write without an approval row UI cohort card with 69 expandable line items STATUS: REAL (contract) ★ stage batch_id='jul1-2026' with 69 line items proposed_actions cohort · per-customer rows under one batch_id ADR-031: no NS write without approval · risk_level=5 FINANCE-KEY · THE batch HITL gate i WHAT THIS DOES: Admin dashboard surfaces the cohort. Mike sees a single banner ("69 named price lists ready to write back to NS - approve?") with expandable per-customer line items. D1 TABLE: proposed_actions (READ for dashboard) TECHNICAL DETAILS: NOTIFY MIKE SURFACES admin-dashboard.html cohort panel optional: email digest COPY "69 named price lists ready to write back to NS - approve?" STATUS: REAL (contract) - cohort UI thread Agent N owns notify Mike (cohort banner) "69 ready to write back - approve?" CLOUD · UI: Agent N i WHAT THIS DOES: Mike opens the cohort panel. Can: approve all 69, approve a subset (e.g. 65 + send 4 back for review), or reject individual customers. X-Edit-Token required. D1 TABLE: proposed_actions (UPDATE status per row) TECHNICAL DETAILS: MIKE APPROVES COHORT ACTIONS approve-all -> status='approved' on all 69 approve-subset -> status='approved' on selected, 'rejected' or 'pending_review' on rest reject-row -> status='rejected' GUARD X-Edit-Token + audit trail STATUS: REAL ★ Mike approves cohort (all / subset / reject) X-Edit-Token + per-row decision FINANCE-KEY · HITL decision i WHAT THIS DOES: For each approved row, INSERT into ns_pending_pushes. NS_PUSH_QUEUE drainer picks up rows in FIFO order. D1 TABLE: ns_pending_pushes (INSERT one row per approved customer) TECHNICAL DETAILS: ENQUEUE NS_PUSH_QUEUE INSERT ns_pending_pushes record_type='named_price_list', customer_id, payload (JSON), proposed_action_id, batch_id, status='queued' STATUS: REAL enqueue NS_PUSH_QUEUE (one row per approved customer) ns_pending_pushes FIFO MESSAGEBUS · durable queue i WHAT THIS DOES: PushMutexDO is a Durable Object keyed by customer_id. Drainer must acquire the mutex before writing - prevents two concurrent writes to the same NS customer. DURABLE OBJECT: PushMutexDO (one instance per customer_id) TECHNICAL DETAILS: PushMutexDO NAMESPACE bind='PUSH_MUTEX' KEY customer_id INTERFACE POST /acquire?customer=X&ttl=300 POST /release?customer=X STATUS: REAL acquire PushMutexDO per customer prevents concurrent NS writes for one customer SECURITY · mutex i WHAT THIS DOES: POST /api/ns/push/named-price-list per customer with the payload. Server-side double-checks the approved proposed_action exists. The NS RESTlet writes the PriceList record. ENDPOINT: POST /api/ns/push/named-price-list?preview=false&confirm=true TECHNICAL DETAILS: POST NS PUSH HEADERS X-Edit-Token Content-Type: application/json BODY customer_id, sy_version, named_list_name, lines[] NS RESTlet customscript_gfs_platform_push_pricelist STATUS: REAL (contract) POST /api/ns/push/named-price-list preview=false · confirm=true · X-Edit-Token BACKEND · per customer i WHAT THIS DOES: NS RESTlet writes the custom PriceList record and updates customer.pricelevel pointer. NS RECORDS: custom PriceList record + customer.pricelevel TECHNICAL DETAILS: NS RESTlet WRITES PriceList RECORDS custom named price list (one per customer per SY) customer.pricelevel = id of new named list STATUS: REAL NS RESTlet writes PriceList custom record + customer.pricelevel BACKEND · NS write-back i WHAT THIS DOES: On 2xx success: mark done. On non-2xx: backoff retry up to 3x; on terminal failure, surface in the cohort partial-failure UI so Mike can re-approve or fix. D1 TABLES: proposed_actions (UPDATE status), ns_pending_pushes (UPDATE attempts) TECHNICAL DETAILS: SUCCESS / FAIL BRANCH SUCCESS (2xx) proposed_actions.status='done' emit events.bid.namedlist_pushed (customer_id, sy_version) FAIL (non-2xx) ns_pending_pushes.attempts++ backoff: 1m, 5m, 30m terminal -> status='partial_failure', surface in cohort UI STATUS: REAL 2xx done · non-2xx requeue / partial-failure backoff 1m / 5m / 30m · terminal -> partial-failure UI SECURITY · escalation i WHAT THIS DOES: When all 69 are either done or partial_failure, fire cohort complete event. D1 TABLE: events TECHNICAL DETAILS: COHORT COMPLETE EVENT EVENT bid.rollover_completed (batch_id, total, succeeded, failed) SUBSCRIBERS analytics, customer_health (price stability) STATUS: REAL events.bid.rollover_completed cohort-level event MESSAGEBUS i WHAT THIS DOES: Customer service opens the customer record in NS the next morning and sees the new named price list attached. Next SO entered for the customer reads the new pricing. NS RECORDS READ: customer.pricelevel + PriceList TECHNICAL DETAILS: CS SEES NEW NAMED LIST INTERACTION CS opens NS customer record sees customer.pricelevel = new named list any new SO entered uses new prices STATUS: REAL CS sees new named list in NS customer.pricelevel reads new list BACKEND · loop closed i WHAT THIS DOES: The next Sales Order entered for that customer reads the new named price list - new SY-version prices automatically flow downstream into the SO / Item Fulfillment / Invoice chain. NS RECORDS WRITE: SalesOrd (reads new pricelevel) TECHNICAL DETAILS: NEXT SO READS NEW LIST INTERACTION SO entered -> reads customer.pricelevel -> new named list prices STATUS: REAL next SO reads new pricing flows into SO / IF / Invoice chain BACKEND i WHAT THIS DOES: customer_health watcher consumes the rollover event and re-runs the price-stability signal for each affected customer. D1 TABLE: v_customer_health (refresh) TECHNICAL DETAILS: CUSTOMER_HEALTH RECOMPUTE SIGNAL price stability per customer SUBSCRIBES bid.rollover_completed STATUS: REAL customer_health recomputes price stability signal CLOUD i WHAT THIS DOES: cron_locks row released so the next year's run (or a manual re-trigger) can proceed. D1 TABLE: cron_locks (DELETE) TECHNICAL DETAILS: RELEASE CRON LOCK ACTION DELETE FROM cron_locks WHERE cron_name='bid_rollover' STATUS: REAL release cron lock cron_name='bid_rollover' SECURITY i WHAT THIS DOES: Customer + SY-version is the trace thread across the entire path - from bid_quote_versions through proposed_actions, ns_pending_pushes, and into the NS PriceList record. EXAMPLE: ACC Distributors / SY2627-v2.5 -> named_list_name 'ACC SY2627 v2.5' in NS TECHNICAL DETAILS: TRACE THREAD KEY customer_id + sy_version CARRIED THROUGH bid_quote_versions -> proposed_actions -> ns_pending_pushes -> NS PriceList.name STATUS: REAL customer + SY-version threading e.g. ACC Distributors / SY2627-v2.5 FINANCE-KEY · trace thread i Jul 1 fires 69 customers customer + SY-version cohort of 69 PROPOSED approve mutex acquired partial-failure loop cohort done
Glossary
Database / D1
Backend (Worker, NS RESTlet)
Cloud (dashboard, customer_health)
Messagebus (ns_pending_pushes, events)
Mutex / cron lock / partial-failure escalation
★ NS write-back gate
batch_id: cohort identifier (e.g. jul1-2026)
PushMutexDO: per-customer Durable Object mutex
partial-failure loop: failures re-surface as a smaller cohort for re-approval

Tables, endpoints, code paths

kindnamepurpose
Cron0 5 1 7 *Jul 1 05:00 UTC
Workflow classsrc/annual_roll_workflow.tsprepare_next_school_year
D1 tablecron_locksprevents double-run
D1 tablebid_customers69 active cohort
D1 tablebid_quote_versionsis_latest=1 per customer
D1 tablebid_programsHPS MAP GPO allowance
D1 tableproposed_actionsbatch_id='jul1-2026'
D1 tablens_pending_pushesNS_PUSH_QUEUE
Durable ObjectPushMutexDOper-customer mutex
EndpointPOST /api/bid/rollover/startmanual trigger
EndpointPOST /api/ns/push/named-price-listper-customer NS write
EndpointPOST /api/proposed-actions/bulk-decidecohort approve / subset / reject
NS RESTletcustomscript_gfs_platform_push_pricelistwrites NS PriceList
Eventbid.namedlist_pushedper customer success
Eventbid.rollover_completedcohort done