Notebook

Scratch pad

PRD Page 1

Centerpoint — Product Requirements & System Reference

Status: PHASE 3 COMPLETE — Draft 1 (20260704). This file integrates all 25 draft sections from Phase 3 synthesis. Part I–III body content merged from domain drafts; Appendices A–G structured with C–G renumbered (former C/D/E/F now D/E/F/G). See the effort ledger in todo.md → "Composite PRD — Product & System Reference (multi-phase)".


0. About This Document (Charter)

What this is. A permanent, readable, searchable reference to the Centerpoint product as a whole — the legacy Visual FoxPro application and its modern Laravel/Vue replacement, treated as one continuous product story. It captures durable intent: what the product does, why, for whom, how the legacy behavior was carried forward, and where we have deliberately gone further.

Who it is for. Developers, designers, and project managers alike. A non-developer should be able to read the body and understand the product; a developer should be able to drop into a domain chapter and find the map to the code.

What this is NOT.

Relationship to the living docs. The developer/LLM docs (PARITY.md, todo.md, staff/docs/memory/*, the LEGACY_VFP_* reconstructions) track volatile state and deep technical detail. This document coexists with them at a higher, more durable altitude and does not duplicate them. Where content is volatile (live parity status, open tickets), this document links out rather than copying.

How "parity" is framed here. Not as a drift report. As decided intent, in the form: "The legacy app does X this way; we ported that forward as Y, with the following enhancements Z (and diverged at W, because …)."

How to read it — audience layers.

Maintenance. Update when durable intent changes (a new decided direction, a shipped domain milestone, a retired feature) — not on every commit. Something being further along than when this was written is not "stale"; a reversed decision or abandoned direction is. Lives in staff/storage/notes/ and is surfaced in the in-app chat Documentation tool.


Part I — Product Overview

1. Executive Summary — what Centerpoint is

Centerpoint is a comprehensive book supply and fulfillment platform — combining purchasing, inventory management, order fulfillment, and customer account management — designed to help schools and libraries efficiently acquire, manage, and receive books through a seamless digital experience.

At its core, Centerpoint solves a fundamental operational challenge: making it frictionless for librarians and educators to discover which books are available, how much they cost, and how quickly they can arrive. Schools and libraries use it to search a deep catalog of titles, place standing orders for regular supplies, build and execute promotional campaigns, and track shipments and inventory. Behind the scenes, Centerpoint manages complex fulfillment logistics — from pricing variations based on account discounts and promotions, to routing orders to the right fulfillment lane, to orchestrating printing and shipping of custom editions.

The application serves multiple audiences. End-user librarians and boosters browse and order through a web interface. School district administrators and customer service representatives use a staff portal to manage accounts, create targeted promotions, and troubleshoot fulfillment. Account representatives use specialized dashboards to support their customers. And internal operations teams have tools to manage inventory, classify orders for fulfillment, and run reports.

Why a Modern Rewrite

Centerpoint evolved from a legacy Visual FoxPro application (webnet.prg) that has served the business for decades. That application remains the system of record for core business data (stored in DBF database files, the VFP native format). However, maintaining and extending a legacy CGI-based FoxPro application became a limiting factor: adding features was slow, scaling the system to handle growing data volumes was expensive, and deploying improvements required navigating code written across many years by many developers with varying conventions.

In response, we built a modern replacement using Laravel (a PHP web framework) for the backend and Vue (a JavaScript framework) for the user interface. This new application doesn't displace the legacy system; instead, it coexists alongside it, accessing the same data through a careful bridge layer. The modern application handles the customer-facing experience, reporting, and operational workflows, while the legacy VFP application remains the authoritative source for pricing, inventory, and transactional data, updated through nightly synchronization.

This hybrid approach lets us ship new features and improvements at modern development velocity while preserving the stability and reliability of the core business system. The goal is a seamless, unified product experience — with the modern interface and capabilities of a contemporary web application backed by a battle-tested data foundation.

How to Read This Document

For the historical context that frames this product, see Part I § Background & Migration Thesis. For a user-by-user breakdown of who does what, see § Users & Roles. For the detailed domain-by-domain feature and workflow reference, see Part II (Chapters 6–17).


2. Background & Migration Thesis

The Legacy Visual FoxPro Application

For nearly two decades, the Centerpoint business operated on a single, cohesive Visual FoxPro application: webnet.prg, a 161,252-line codebase with approximately 375 procedures and functions. A companion build, admnet.prg, differed by only 4 bytes (a configuration flag and data-path constant)—effectively the same application with two entry points: webnet for customer-facing web operations, admnet for administrative staff. Together, they handled the complete order lifecycle: shopping carts, order processing, standing-order fulfillment, inventory management, customer accounts, pricing, and fulfillment coordination.

The legacy app was built for serious, sustained business use. It ran as a CGI application on Windows, managing millions of records across twelve interconnected order and inventory tables (webhead/webdetail for shopping carts, heading/detail for active orders, backhead/backdetail for backorders, allhead/alldetail for standing orders, plus archive families). Its order-processing logic was intricate and deliberate: sophisticated pricing rules (7 different free-shipping conditions); inventory allocation strategies (3-phase "shippable now vs. backorder" determination); processing flags that controlled manual review workflows; and TRANSNO sequencing that guaranteed uniqueness across all order families. The application evolved to handle real business complexity—trade-house ordering with title exclusions and author subscriptions, multi-account organization context for bulk purchasing, standing-order plans with monthly refresh cycles, payment-cycle invoicing, and integration with Pitney Bowes printing and fulfillment.

Why Modernization Became Necessary

The legacy application solved a critical job and solved it well. But two decades of maintenance, feature accumulation, and technical constraints had made further evolution increasingly difficult:

The Modern Replacement

Centerpoint was rebuilt as a Laravel/Vue web application, backed by a Rust HTTP API (serving DBF data via a local proxy). The new system is not a replacement in spirit; it is a continuation of the legacy product, written in a stack that permits faster development, better testing, cloud deployment, and richer user experience.

Parity as Decided Intent

Rather than a wholesale rebuild, the modernization was framed as a feature-by-feature port: each capability in the legacy app was either (1) ported forward — carried into the new code with fidelity to the original intent; (2) enhanced — ported and improved (e.g., richer email events, automated lane classification); (3) reimagined — redesigned for the new platform while preserving outcome (e.g., the legacy bonus-book ledger may become a general-purpose entitlement system); or (4) deliberately diverged — set aside as no longer aligned with business direction, with explicit approval.

This document treats parity not as a drift report but as decided intent in code: "The legacy app does X this way; we carried it forward as Y, with enhancements Z, and diverged at W because …." It is the record of a thoughtful migration, not a checklist of remaining work.


For the detailed mapping of every legacy feature and its modern counterpart, see Part II: Feature Domains and Appendix B: Legacy Procedure Glossary.


3. Users & Roles / Personas

Centerpoint serves multiple audiences, each with distinct workflows and levels of system access. Understanding these personas is essential to knowing what problems the product solves and how to navigate its features.

End-User Librarians & Boosters

Who they are: Librarians, collection managers, and purchasing coordinators at schools and public libraries; occasionally parent volunteers or parent organizations ("boosters") helping to acquire materials.

Problems they solve: Finding the right mix of titles and pricing for their collection; coordinating bulk purchases within budget constraints; reducing the time spent on purchase orders and invoice processing.

Key features: Title search, cart checkout, standing order subscriptions, order history, shipment tracking, account and address management.

Customer Service Representatives

Who they are: Centerpoint staff members who support schools and libraries — answering questions, troubleshooting orders, adjusting accounts, and managing customer relationships.

What they do: Open customer accounts, update account information and billing details, investigate order problems, process refunds or adjustments, manage customer notes and communication history, and escalate complex issues.

Problems they solve: Reducing the time to resolve customer problems; maintaining consistent customer data; ensuring that account changes are applied correctly; tracking long-running conversations and follow-ups.

Key features: Account and contact management, order lookup and modification, customer notes and annotation, email and notification workflows, bulk account updates.

Account Representatives

Who they are: Sales and relationship managers, organized by geographic sales regions (COMMCODE), responsible for a territory of school and library customers.

What they do: Manage customer callbacks and touchpoints, track scheduled follow-up dates, monitor account engagement metrics, and help customers with strategic questions about ordering and inventory. They use specialized dashboards to see at-a-glance which accounts need attention.

Problems they solve: Staying on top of a large territory of accounts without losing track of customer relationships; identifying which accounts are under-engaged or at risk; allocating time efficiently across multiple regions.

Key features: Account Representative dashboard (callback tracking, regional workload metrics, account insights), Brutus inline chat for account research, callback scheduling, customer account opening.

Operations & Fulfillment Teams

Who they are: Internal staff responsible for inventory management, order fulfillment, printing and shipping operations, and reporting on business metrics.

What they do: Monitor inventory levels, classify orders for correct fulfillment lanes (determining whether an order should be pulled from stock, back-ordered, printed on demand, or routed to a trade distributor), execute shipment workflows, and generate reports on sales, fulfillment performance, and account activity.

Problems they solve: Routing each order efficiently to minimize cost and delivery time; maintaining accurate inventory counts; ensuring that fulfillment lanes and printing/shipping partners receive the right data at the right time; monitoring business health through real-time metrics.

Key features: Trade and inventory management dashboards, order classification and fulfillment routing, batch printing and shipping manifests, sales and fulfillment reports, transaction and event logging.

System Administrators

Who they are: Staff with elevated permissions, responsible for system configuration, user management, and emergency troubleshooting.

What they do: Create and manage staff user accounts, adjust user permissions and access levels, manage system settings and configurations, audit and troubleshoot data synchronization issues between the app and the underlying database systems.

Problems they solve: Controlling who can access what areas of the system; ensuring that all staff have the right permissions for their job; diagnosing and repairing data inconsistencies.

Key features: User management and permission assignment, system configuration tools, audit logging and diagnostics, account and user bulk operations.

Authentication & Security Model

🔧 For builders: Centerpoint uses a session-based authentication model. The User model implements Laravel's Authenticatable contract directly (not via the trait to avoid naming conflicts). Sessions are persistent across database rebuilds: the password table can be rebuilt without forcing users to log in again. Password data is stored in a separate Password model — purely as a data mirror, without authentication logic. A HasPermissionsTrait on the User model provides permission-checking methods (canNow(), viewProps(), etc.) used across the authorization system. Permissions are checked via policies (in app/Policies/) that guard feature access and data mutation. Configuration is in config/auth.php with a custom password provider driver.


For detailed capabilities by role and feature-level access restrictions, see the individual domain chapters in Part II (Chapters 6–17). For the architecture of permissions and auth, see Part IV § Platform — Auth, Permissions, DBF↔MySQL Bridge, Jobs.


4. Product Principles & Guiding Decisions

Centerpoint operates on a set of durable design tenets that reflect both the legacy system's constraints and deliberate modern choices. These principles govern how data flows, how decisions are made, and how the product evolves.

1. DBF Remains the System of Record

The DBF files are the authoritative source; the MySQL mirror is a cached representation rebuilt daily at 02:15. Code should assume DBF is the ultimate reference, and any schema change must propagate through both the DBF structure and the rebuild migration.

🔧 For builders: Migrations on web*, back*, bro*, all* tables and their detail tables must update the DBF field list in staff/dbf-service/config/dbf.php and the rebuild logic in RebuildApplicationTables.php. INDEX is the row-level bridge (RECNO mirror); cascade keys vary by table family (webhead=REMOTEADDR, webdetail=RECNO via INDEX+1, back/bro/all=TRANSNO). See dbf_three_paths.md and dbf_batch_cascade_pattern.md.

2. Submitted Orders Have Locked Pricing

Once an order is submitted and assigned SUBMITTED status, its pricing becomes immutable. No repricing occurs when the order enters fulfillment (at pull time or later), eliminating fraud risk and honoring customer expectations of quoted prices.

3. Fail Loud, Classify Deterministically in Code

Guards throw for truly-unknown states; deterministic classification happens in the code, not deferred to callers. This forces developers to find and fix root causes rather than papering over unknowns with defaults.

4. Backward Compatibility: Array Access & JSON Serialization

External tools, legacy code, and dashboards depend on models being both ArrayAccess and JsonSerializable. Never remove these interfaces from existing models—breaking this contract silently breaks calling code downstream.

5. Legacy Coexistence (Gradual Migration, Not Overnight Cutover)

Both the VFP application and the modern Laravel/Vue app may run in production simultaneously. Design for dual-lane code patterns where needed, and decide per feature which branch is production—expect eventual migration, not a flag day.

🔧 For builders: When writing code that touches a legacy-paralleled system (e.g., standing orders, inventory), use the parity-audit skill to map legacy behavior, then decide whether to port, diverge, or retire. See PARITY.md for the live parity matrix.

6. Hardcoded Whitelists Silently Drop Unknown Keys

The ziggy route whitelist, BuildCampaignDrafts::normalizeOptions(), and the three-way DBF cascade alignment all silently ignore unknown keys. Adding a new key without updating the whitelist is a silent no-op—always verify whitelists when adding routes, options, or cascade targets.

7. Side Channels Are Not Mirrors (Legacy Branch Is Production)

The alpha_* tables exist to move data off to the side, not to become a full functional twin of the production tables. The legacy (non-alpha) branch is the production branch; keep the alpha branch minimal and don't block core work on achieving alpha parity.


These principles inform all decisions in Part II and Part III.


5. System Landscape

Centerpoint is built on a three-layer architecture: a modern web interface, a Rust API bridge, and the legacy DBF files that remain the system of record. This hybrid approach lets the modern app deliver contemporary user experience and developer velocity while preserving the data integrity and business continuity of the legacy system.

The staff-facing web application (what account reps, customer service teams, and admins use daily) is a Laravel/Vue.js stack. Users interact with six independent Vue.js single-page apps (Admin, Trade, Account Rep, Customer Service, Acquisitions, Warehouse) that share a common API client and core components. These apps are hosted within Laravel and communicate with the backend via REST endpoints.

The Rust API server (running on localhost:3636) acts as the exclusive gateway to all database operations. It reads and writes DBF files directly (raw byte-level file access — no ORM between the app and the legacy format), and simultaneously dual-writes to a MySQL mirror database. This is the linchpin: every DBF operation is atomic and synchronized, so the modern app can read from MySQL for fast searches and reporting while the legacy system and DBF files remain the authoritative source.

The data synchronization rhythm is nightly. At 02:15 each morning, a job rebuilds the MySQL tables (web*, back*, bro*, all* and their detail partners) from the DBF files. This means MySQL is always a faithful snapshot of the legacy data — never ahead, always in sync the moment the rebuild completes. Between rebuilds, the MySQL mirror is stable and queryable; the DBF is the place where live writes land.

For performance-critical operations like pricing calculation and reporting, Centerpoint uses a temporary table pattern. The OrganizationContextService builds a complete snapshot of an organization's data — all accounts, standing orders, inventory, and pricing — by loading data into 12 temporary tables in a single operation. This eliminates thousands of individual queries and delivers results in under a second when cached (vs. 30 seconds on cold start). The service caches this snapshot for 3 hours, making dashboard loads and pricing queries near-instant.

The legacy Visual FoxPro application (webnet.prg) coexists alongside the modern app. Users can log into either interface; both read from and write to the same DBF files through their respective bridges. This means the modern app doesn't have to replace the legacy system overnight — teams can migrate workflows at their own pace.

🔧 For builders: The three DBF code paths (legacy fopen, 32-bit dbf-service on :8080, Rust API on :3636) implement different mechanics but share the same table schemas. Before hypothesizing about a data issue, name which path is in play. See rust_api_proxy_architecture.md for request-flow examples; organization_context.md for temp-table performance tuning; domain_seams.md § "DBF ↔ MySQL" for the three-path reference and cascade-key details.

For detailed per-domain feature descriptions, see Part II. For system maintenance and internals, see Chapter 17 (Platform).


Part II — Feature Domains

Per-domain template (applied to every chapter 6–17):

  1. Purpose — what the domain does for users (plain language)
  2. Legacy behavior — how the VFP app did it
  3. Ported forward + enhancements — "Legacy does X; carried forward as Y; enhanced with Z; diverged at W and why"
  4. Current state — shipped / in progress / planned (durable roadmap intent, not live ticket status)
  5. Decisions & directional rulings for this domain
  6. > 🔧 For builders: key services/routes/anchors + links to the dev deep-dive docs

6. Orders & Carts

6.1 Purpose

Carts and orders are the heartbeat of Centerpoint. Customers build shopping carts by adding books to their account, and staff can create or manage carts on behalf of customers. A cart is a temporary holding area — a list of books a customer intends to purchase, with quantities and preferences like shipping address or special pricing. When a customer (or staff member on their behalf) submits a cart, it becomes an order: a committed purchase intent that enters the fulfillment pipeline.

The orders and carts system must answer three critical questions for every transaction: (1) What inventory is available right now? (2) What should the customer pay, given their account discounts and plan type? (3) How should this order route through fulfillment — to the warehouse for packing, to a print-on-demand vendor, to a standing order schedule, or to a backorder queue? Once submitted, an order's journey is tracked and can be cancelled, edited, or reinstated by staff as circumstances change.


6.2 Legacy Behavior

The legacy Visual FoxPro application (webnet.prg) manages shopping carts and orders using a multi-table lifecycle. Customers begin by creating a cart (CREATE_NEW_CART, line 36157), which is stored in the WEBHEAD table and indexed by REMOTEADDR (a session identifier). As items are added, they appear in WEBDETAIL records, linked to the cart session. The cart persists until the customer either abandons it or submits it.

When a customer submits a cart (SENDTOCENTER action in NEW_HEADER_UPDATE, line 7041), the system performs several critical operations: it validates the cart is not empty, calculates free shipping eligibility (FREESHIP determination ladder, lines 7236–7276), and invokes TD_SPECIFICS (line 10458) to check inventory availability for each item. For items in stock, inventory is deducted immediately (UPDATEINVENTORY, line 10472). For unavailable items, backorder records are created. The submitted cart moves from WEBHEAD to HEADING (also called brohead), where it receives a TRANSNO (transaction number) generated by GET_IMPORTANT_VALUES (line 34242) — a unique ID calculated as the maximum TRANSNO across three tables plus one, ensuring uniqueness across the lifetime of the order.

After submission, orders progress through fulfillment stages managed by processing flags (pship, pepack, pipack, pinvoice) that control whether each step is automated or requires manual staff review. Orders may also be deleted (ORDER_DELETE, line 32708) if they haven't yet shipped, or adjusted through the order edit interface (GENERAL_VIEW_ORDER_ADJUST, line 59154). Staff and customers can view the full order history (FOH, line 56387), search for orders by key or invoice number (ACCOUNT_KEYED_ACCESS, line 97212), and manage backorders waiting for inventory (BACKMANAGEMENT, lines 154206–155753).


6.3 Ported Forward + Enhancements

Cart creation & management: The legacy CREATE_NEW_CART (line 36157) and FN_CREATE_NEW_HEADER (line 1199) logic is now handled by CurrentCartController (app/Http/Controllers/CurrentCartController.php) and AddToCartService (app/Services/AddToCartService.php), with cart routes at cart.* (routes/api.php). The session-based REMOTEADDR lookup has been translated to Laravel's session ID mechanism. Items are added or updated via the same service, which now recalculates pricing and validates inventory in a single batch operation for performance.

Checkout and order submission: The legacy order submission path (SENDTOCENTER → NEW_HEADER_UPDATE → order creation) is now split between PullOrderAction (app/Actions/PullOrderAction.php) and CreateOrderFromCartAction (app/Actions/CreateOrderFromCartAction.php), both of which respect the original free-shipping ladder (certified parity at each of the 7 conditions, commit c320cd358). Pricing is locked at submission (decision D6); no repricing happens at pull time, eliminating a legacy hack that compensated for a then-missing cart-pricing feature. The cart's WEBHEAD and WEBDETAIL records are moved to an alpha_* history table (preserving the submission, not deleting it), maintaining an audit trail.

Inventory and backorder fulfillment: The legacy TD_SPECIFICS (line 10458) inventory checks — GETSALEPRICE (line 10599), GETCURRENTSTATUS (line 10823), GETSHIPPABLE (line 10867), and UPDATEINVENTORY (line 10472) — are preserved in the app's CheckItemAvailabilityAction and CreateOrderFromCartAction, which apply the same logic: available items reduce inventory and create DETAIL records, unavailable items create backorder rows. Backorder conversion (legacy MANAGE_SELECT_BACKORDERS, line 53900) is implemented in ProcessBackOrderAction and scheduled via the orders:release-backorders command, maintaining the legacy FIFO-by-TRANSNO allocation and partial fulfillment rules.

Order editing and status: The legacy GENERAL_VIEW (line 61606) and master edit surface (GENERAL_VIEW_LINEITEM_EDIT, line 63553) are now exposed through OrdersController show and edit routes (routes/web.php) and the /db/orders/{id}/edit CMS endpoint. Staff can adjust line items, change quantities, override fulfillment flags, and add notes — all via the same unified master-edit interface. Order deletion (CancelOrderAction, app/Actions/CancelOrderAction.php) and order adjustment (order actions on the show page) preserve the original approval gates.

Order history and search: The legacy FOH (Full Order History, line 56387) is now a browser-based orders search (db.orders.*) that works across legacy tables (webhead, brohead, backhead, allhead) and new alpha tables. Cross-table search by TRANSNO, REMOTEADDR, or customer KEY is implemented in CrossTableOrderSearchService (app/Services/CrossTableOrderSearchService.php), accessible via the omni-search and the CMS Orders dashboard. Weekly open-cart preflight and activity monitoring are now part of the carts tab on customer accounts and the staff dashboard.


6.4 Current State

Shipped:

Partial / Deferred:

Planned:


6.5 Decisions & Directional Rulings

D1: No pull-time repricing. Once a cart is submitted and SUBMITTED status is assigned, prices are locked. No repricing happens when the order enters fulfillment (PullOrderAction). Rationale: The legacy system had post-submit price adjustments as a workaround for gaps in cart-time pricing logic. That gap is now closed in the app. Repricing at pull time creates fraud risk and violates customer expectations of quoted prices.

D2: Submitted carts stay in the webhead lane until pull. The WEBHEAD record is not immediately deleted on submit; instead, it is marked as SUBMITTED (MARKCART flag = 5555), and a separate HEADING record is created for fulfillment. The original webhead is copied to alpha_webheads as a history record, preserving the exact submission state. Rationale: Single source of truth for order creation; clean separation between customer-facing shopping state and fulfillment state.

D3: Backorder conversion creates a new TRANSNO. When a backorder item becomes available and is converted to an active order (ProcessBackOrderAction), a new TRANSNO is generated. The order moves from BACKHEAD to HEADING but is not a "reuse" of the original order ID — it is a new order in the fulfillment queue, preserving the audit trail. Rationale: Legacy MANAGE_SELECT_BACKORDERS (line 53900) creates new HEADING records with new TRANSNOs; the app mirrors this.

D4: Archival is a sweep, not inline. Completed orders are not moved to archive tables immediately; instead, an overnight command (orders:archive-sweep) moves them in batches, respecting the legacy ARCHIVE_ORDERS gate (line 146614). Archive levels (allhead → oldallhead → ancienthead) are distinct migrations. Rationale: Batch operations are faster and less prone to race conditions; overnight processing avoids customer-facing latency.

D5: Processing flags (pship, pepack, pipack, pinvoice) survive into the Laravel model. These flags are preserved from legacy orders and are consulted by fulfillment staff; default is 0 (automated), but admin-assisted orders and special handling (3) are still recognized. Rationale: Legacy fulfillment workflows depend on these flags; removing them would require retraining and workflow redesign.

D6: Free shipping is calculated at cart time and recalculated at pull time. The seven free-shipping conditions (all $2.50, dist-account negatives, trade-prepaid, choice-40%, special books, ≥5 units, standing plans) are evaluated when the customer adds items to cart and again when they submit. Staff can override this flag during master edit if exceptional circumstances arise. Rationale: Customers should see accurate shipping cost in cart summary before committing; edge cases are rare enough that staff override is acceptable.


6.6 🔧 For Builders

Core Services & Actions:

Controllers & Routes:

Key Models & Anchors:

Commands:

Deep Dive References:

For granular implementation details, see app/Services/AddToCartService and the two LEGACY_VFP_ORDER docs linked above.


7. Standing Orders & Plans

7.1 Purpose

A standing order (internally called a plan) is a recurring subscription a library holds with Center Point — the customer signs up for a series of large-print titles and receives new books automatically as they publish. Plans are the engine of Center Point's subscription business: 42,873 ISBN × Account pricing records hang off the 170-column account classification system; monthly fulfillment (the standing-orders run) turns active plans into real orders; prepaid budgets ceiling trade-plan pricing and drive cash flow.

Standing orders are complex because they operate on multiple timescales: creation & lifecycle management (hold, cancel, restate, renew) happens ad-hoc; invoicing happens at plan creation and renewal (prepaid paths) or per-shipment (pay-as-shipped); monthly fulfillment (title matching, inventory depletion, order generation) happens on a staff-triggered batch schedule, not automation. The legacy VFP system (webnet.prg, still live) is the authoritative fulfillment engine — this app's standing-orders implementation reaches parity-or-better on the plan lifecycle, invoicing, and order-generation pieces, but the legacy system remains the validator for the grouping hash (SORTORDER) that feeds downstream Pitney Bowes shipping.


7.2 Legacy Behavior

Lifecycle & Invoicing (webnet.prg:8816596021, STANDING_WRITESTANDING_RENEW):

Monthly Fulfillment (PRINT_STANDING, :129164:129933):

Pricing & Holds (ACCOUNT_STANDING_CALCS, :86959; STANDING_INVOICE, :93564):


7.3 Ported Forward + Enhancements

Plan Lifecycle (app/Http/Controllers/StandingOrderEditController, app/Models/StandingOrder):

Invoicing (InvoiceThisPlanController, StandingOrderInvoiceGenerationService::generatePrepaidInvoice):

Fulfillment Pipeline (App/Actions/FulfillStandingOrders, App/Models/Queue/StandingOrder, /standing-orders-cms):

Custom-Mix Profiles (CustomMixProfileService, Configuration rows per plan):

Trade Plans (TradeOrderService, TradeCMS\Models\TradeTitleExclusion, TradeAuthorAutoPickService):


7.4 Current State

Status (July 2026): M1–M5 milestones ✅ complete. The full plan lifecycle (create through renewal) is production-ready; monthly fulfillment is live and operator-gated; parity with legacy is verified at the scorecard level (no regressions; 15+ deliberate improvements).

Live Capabilities:

Known Gaps / Open Decisions:


7.5 Decisions & Directional Rulings

D1 — Parity is non-negotiable. The legacy VFP system is still live production (webnet.prg, compiled to foxisapi.dll). We port features at parity-or-better, never silent regressions. Every claim in the parity matrix (PARITY.md section "Standing Orders & Plans") is anchored to legacy code, app code, and a status + decision commit. The scorecard (regression count, SORTORDER reproduction, Pest test results) is the production-readiness metric.

D2 — SORTORDER is a contract with Pitney Bowes. The legacy grouping hash (NNXX codes) is not a display label — it's the key Pitney Bowes uses to batch orders for weigh-in and label generation. The app must build and materialize it identically to legacy so external downstream consumers see the same data. This moves SORTORDER from "nice-to-have visualization" to a functional requirement (confirmed with the user, 2026-06-18).

D3 — FINALINV is availability, not ONHAND. Legacy defines ONHAND = FINALINV − ALLSALES (gross cumulative inventory minus all-time sales). The fulfillment pipeline judges availability against FINALINV (the print run), not real on-hand stock. This allows standing orders to be promised against titles publishing this month, not just those already in the warehouse. The app trusts the DBF-mirrored FINALINV for now (in-app receive/correct is a separate seam). Inventory decrement during queue build is FORBIDDEN — it corrupts the production facts the report needs and breaks re-run safety.

D4 — Staged inventory, not production mutation. Queue build seeds a separate queue_inventories working copy, not the production table. This preserves the ability to re-run, inspect, and audit the queue without altering what's on the shelf. Improvement on legacy (which mutates INVENT in place and resets after).

D5 — Operator owns routing; system informs. The fulfillment UI flags all concerns (oversized groups, address mismatches, unfulfilled titles, singletons) and seeds sensible defaults (regular multi-member → Pitney Bowes, HANDLING='T' → downstairs). The operator decides what ships, where, and when — never automatic rerouting. This honors legacy's manual control model but makes it explicit + auditable (vs legacy's opaque flags).

D6 — Contiguous TRANSNO assignment. TRANSNOs (and MySQL id insertion) must be contiguous in SORTORDER order so that third-party consumers reading the DBF see grouped orders with adjacent numbers. Mirroring legacy's sorted renumber. (SequenceService::nextTransno is gap-safe but scattered; this path needs the block.)


7.6 🔧 For Builders

Key Files & Routes:

Source Documentation (the spec):

  1. LEGACY_VFP_STANDING_ORDER_PIPELINE.md — 9-stage legacy reconstruction, SORTORDER decode map, Pitney Bowes contract, parity/regression matrix, locked forward design (v1 + v2 + auto-gating)
  2. STANDING_ORDERS_REFERENCE.md — staff-facing plan lifecycle overview, fulfillment mechanics, trade plans, custom-mix config, gap matrix
  3. STANDING_ORDER_BUILD_PLAN.md — roadmap (M1–M5), per-milestone verification ritual, decision audit trail (D1–D6)

Parity Scorecard: Read the "Standing Orders & Plans" section in PARITY.md — the 16 rows track status + legacy/app anchors + decision commit hashes. Any new feature should update the matching row (e.g. nonefull, with commit + date). Zero regression rows is the production gate.

Pest Test Patterns:

Critical Gotchas:


8. Trade / Distributor Pipeline

8.1 Purpose

The Trade domain enables bulk subscription plans for library networks and distribution partners — customers who buy books by plan (predictable selections arriving automatically each month) rather than case-by-case orders. A library system (customer) subscribes to a Trade Level plan (Level 1, 2, or 3, delivering 2, 4, or 6 titles/month respectively across distinct class categories) and receives those titles shipped as standing orders, with prepaid pricing that applies a discount once the library has purchased enough titles to satisfy the "obligation" they signed up for. This domain also historically housed integrations with distribution partner EDI pipelines (notably Baker & Taylor's legacy 850/855/856/810/997 consoles), which are now being generalized into an abstract distribution-partner pipeline for reuse with any future partner.

8.2 Legacy behavior

The legacy VFP app (webnet.prg) implemented trade as an event-driven, manually curated system (TRADE_HOUSE_CUSTOMER_VIEW :49592, TRADE_HOUSE_SELECT_* :40911/:52522). A buyer would navigate to the trade house customer view and select titles for inclusion in a standing order, with title exclusion lists (TRADE_HOUSE_CUSTOMER_CHECKSHOW :40434) preventing specific ISBNs or authors from being auto-loaded. Baker & Taylor, the primary distribution partner at the time, had a dedicated EDI console (EDI_MENU :139788) for 850/855/856/810/997 message exchange and a monthly buyer review screen (FOH_BT_LINE_ITEMS :128701) where staff reviewed B&T-specific orders before shipment. The app also calculated prepaid obligation pricing :136326 — titles were free ($0) while the library had unused obligation budget, then 25% discounted once the budget was exhausted. Notably, legacy trade had no SORTORDER or batch fulfillment discipline — selections were applied directly, and the system relied on manual staff intervention for grouping and routing to fulfillment.

8.3 Ported forward + enhancements

The modern app retains the core plan-based subscription model (3 Trade Level plans per organization + an optional Trade Select placeholder for one-off trades) and the prepaid obligation pricing (TradeLinePricer, :45e0dbf89), but reimplements the fulfillment pathway as a staged batch process integrated into the global StandingOrders fulfillment pipeline. The existing app-trade tool (manual curation UI at /trade, app-trade/Services/TradeOrderService) remains authoritative for per-customer add/update/remove operations and plan management (app-trade/Http/Controllers/TradeInventoryController); the new pathway adds an alternative batch-fulfillment mode that queues, routes, and materializes trade plans to production without bypassing the pipeline discipline.

Key enhancements over legacy:

B&T / EDI future direction: Baker & Taylor is defunct as a distribution partner (decision resolved 2026-07-03, :PARITY.md rows 33 + 108). The legacy B&T EDI consoles (850/855/856/810/997, hardcoded key 6095400000001) and buyer-review workflows are being deferred in favor of a generalized "distribution-partner pipeline" that will abstract the EDI exchange, PO/receiving, and buyer review console for reuse with any future B&T-like relationship. Research + specification is first; implementation is a separate arc (see todo.md). No app counterpart is being built for B&T specifically.

8.4 Current state

Shipped (2026-07-03):

Operational certification pending:

In progress / Not yet shipped:

8.5 Decisions & directional rulings

D-T-1 (Trade as separate run, collision-free reset). Trade and regular (CENTER POINT) fulfillment use shared queue tables (queue_inventories, queue_broheads) but with INVNATURE scope-tagging. Each run type gets its own ActionRun with distinct action_id ('fulfill_standing_orders' for regular, 'fulfill_trade_standing_orders' for trade). Reset operations are scope-aware — a trade reset deletes only INVNATURE='TRADE' queue rows + its own ActionRun; a regular reset only CENTE. Every queue read/write/freshen/ship is scope-filtered. This ensures neither run disturbs the other's staged rows even if both are in flight simultaneously (user decision, 2026-06-29).

D-T-2 (Trade selection = cumulative-by-level auto-load). Each Trade Level plan is cumulative: Level 1 (CLASS_H) sources 2 titles/month; Level 2 (CLASS_H + CLASS_I) sources 4 titles/month; Level 3 (CLASS_H + CLASS_I + CLASS_J) sources 6 titles/month. No manual per-title curation in the batch path; getTradeTitles reads the plan's plansInfo.mix directly (:STANDING_ORDER_BUILD_PLAN.md config section) and selects exactly the published titles that month across the assigned classes (PUBDATE=month, INVNATURE='TRADE'). Format preferences (xhardonly/xsoftonly/xrandom) are applied, matching legacy's non-PRINT_STANDING code path (:41783). This satisfies the 24/48/72 annual-title targets.

D-T-4 (Trade Select out of scope for batch). "Trade batch fulfillment" refers to CLASS_H/I/J only. Trade Select (CLASS_Z, a placeholder for accounts without a trade-level plan who manually order trade titles) is explicitly out of scope for the batch path. It stays entirely in the manual app-trade tool.

D-T-5 (SORTORDER is added to trade — intentional divergence-as-improvement). Although legacy trade had no SORTORDER or batch routing, the modern trade path assigns SORTORDER via content-hash (the cumulative title set per level encodes the level; different title mixes get different hashes) and joins the same PB-lane routing logic as regular standing orders. Trade heads therefore benefit from grouped-shipment optimization (PSHIP=1 lanes to Pitney Bowes, others to Downstairs) without requiring new sort codes (HH/II/JJ are already used by regular series; the content hash groups within the existing scheme). This is a designed improvement over legacy, not a regression, and aligns trade fulfillment with the modern discipline.

D-T-6 (Queue full set, dedup on move, operator decides). Trade preloads titles onto back orders for vendor procurement sizing, so the queue must show the full plan demand without purchasing/back-order gates at the queue step (unlike regular's whereNotIn purchased preference). The reporting surfaces discrepancies before the move (e.g., "2 titles queued, 1 already on back order"). The dedup + filter happen on move to production via the ship-stage toggle dedup_open_backorder (default ON) — an operator policy, not a hidden rule. Same philosophy as regular's zombie_prev_purchased toggle: system informs, operator decides (user, 2026-06-30).

D-T-8 (Trade move = direct-to-production preorder, tentative pricing). Trade lines skip brokered queues entirely and materialize directly to backhead/backdetail (TARGET_TABLE=backheads, OSOURCE='TRADE STANDING'), reflecting the preorder nature of trade (Center Point orders from vendors off these backorders, then bills as it ships). Pricing at queue time is tentative — accurate at the moment, not a billing source. The final price is recalculated out-of-band (CreateOrderFromCartAction::recalculateSalePriceForTradeItem at ship time), overwriting the tentative queue-time price with the live plan DISC. This is not a bug — it's intentional; production brodetail→invoice billing is not replicated in the fulfillment system (the app-trade tool owns that).

B&T / EDI → Distribution-partner pipeline generalization (Resolved 2026-07-03). Baker & Taylor is defunct; the legacy B&T EDI console suite (850/855/856/810/997 message handlers), monthly buyer review (FOH_BT_LINE_ITEMS :128701), and scan-to-ship workflow are being deferred in favor of a generalized abstraction. Future work: research the EDI interaction patterns + the buyer-review workflow across any future partner, then design a reusable "distribution-partner pipeline" encompassing EDI message exchange, PO/receiving, vendor address management, and buyer-review consoles. No B&T-specific port is planned. Tracked in todo.md.

8.6 🔧 For builders

Core services (app-trade module):

Fulfillment pipeline services (main app):

UI routes & controllers:

DB models / migrations:

Commands:

Ops checklist (live first batch):


9. Inventory & Catalog

9.1 Purpose

The inventory domain manages the book catalog — the titles, stock levels, pricing data, and metadata that make it possible for customers to browse, search, order books, and for staff to manage availability. For customers, the catalog is the front door: the ability to find titles by ISBN, author, title keyword, and to see current availability and prices. For staff and fulfillment operations, inventory is the control surface for stock decisions — deciding when a title is available to buy, how many copies exist in stock, managing reprints, tracking sales and demand, and reconciling against the legacy print-on-demand and distribution partner pipelines. The catalog also holds the foundational data for standing-order and trade-distributor fulfillment.

9.2 Legacy behavior

The legacy VFP app manages inventory through a single INVENT table (webnet.prg) containing title records with ISBN, availability status, three stock-counting fields (FINALINV, ALLSALES, ONHAND), and metadata like HOTBOX status (for print-on-demand flags). The catalog is searchable by ISBN, title text, and author; the legacy app surfaces searches in the customer cart view (TD_TITLE_VIEW :15019, TD_DETAILS :17703) and a staff inventory editor (INV_READ :84028, INV_WRITE :111719).

The critical inventory semantics are:

Availability status (Available / Not Yet Published / Out of Print) is synchronized during monthly release runs (MAKE_AVAILABLE :145548–145731) and drive customer-facing filters. The legacy app also manages supplementary fields like ONORDER (supply on the way) and per-title pricing overlays (flatprice ceiling, clearance floor, choice-plan discounts).

9.3 Ported forward + enhancements

The modern app preserves the inventory data model exactly (FINALINV, ALLSALES, ONHAND, HOTBOX all mirrored in MySQL via the nightly rebuild), but stages inventory operations in temporary tables to enable safe preview-first workflows. This is a deliberate divergence: legacy runs standing orders and see consequences; the app lets operators queue/preview demand before committing to ship.

Ported features:

Enhancements:

9.4 Current state

Shipped:

In progress / Planned:

Decided/Diverged:

9.5 Decisions & directional rulings

Decision Ruling Rationale Date
D9 FINALINV = lifetime print baseline, never reset; ONHAND = derived (FINALINV − ALLSALES); standing-order needed = max(0, requested − FINALINV) Mirrors legacy exactly; FINALINV is the print-run decision, ONHAND is post-release erosion (irrelevant for demand) 20260618
D10 Monthly make-available gates on FINALINV ≠ 0 (missingFINALINV warning, not hard abort) and re-seeds ONHAND := FINALINV at publish. Mirrors legacy's :129221/:129236 Operator workflow safety + data integrity 20260702
D11 FASTPRINT lane (POD) — stock gated OFF (ships full), 6-mo PUBDATE window, HOTBOX='FPR', +1-mo invoice date, freeship, PIPACK=3/PEPACK=1 Legacy parity at lane entry (FASTPRINT_CREATE :160254); note: DBF HOTBOX is C(3), legacy 'FPRINT' stores as 'FPR' — verified 25 prod broheads 20260703
D12 FINALINV readiness gate belongs at Ship, not Queue/preview — preview stays open at FINALINV=0 (warned) to discover demand; operator keys FINALINV from preview + buffer; Ship holds any order with unsettled FINALINV Improves legacy's dead-end (forced external FINALINV forecast); preserves safety intent (no undecided print run ships) 20260618

9.6 🔧 For builders

Key services & routes:

Deep-dive references:

Schema anchors:


10. Pricing, Promotions, Campaigns & Direct Mail

10.1 Purpose

Pricing, promotions, and campaigns form the revenue-driving engine: calculating the right price for the right customer at the right time, promoting titles across email and direct mail, and managing the customer relationships that sustain recurring orders. This domain encompasses:

10.2 Legacy Behavior

The legacy VFP app hardcoded promotion types as 17 boolean columns on inventory_promotions (western, romance, mystery, bestseller, flashsale, clearance, trade_select, trade_level_1/2/3, choice, bookmark_1-6) and 6 promote_* columns on account_meta mirroring each promotion. Adding a promotion required two migrations, enum edits, classifier updates, and 6 blade templates. Pricing was calculated on-demand per request. Campaigns were spreadsheet-based exports with manual email template selection per run. Catalog mailing used the account.PROMOTIONS field to gate catalog quantity per user (1 = main user only; >1 = main user gets that count, others get 0).

10.3 Ported Forward + Enhancements

Pricing carried forward the decision to compute prices per ISBN–account pair, but added:

Promotions replaced the rigid 17-column schema with a slot-pool architecture (dynamic_promotions_plan.md, §1–4):

Campaigns moved from spreadsheet + manual template selection to:

Direct Mail simplified: catalog preferences now always respect the modern account classification flags (is_ledger_account, is_active_choice_customer, is_active_non_choice_plan_customer) set during OrganizationContextService builds, independent of the legacy PROMOTIONS field logic (direct-mail.md).

Build List Interface introduced a smart inventory-filtering tool (§7.5 build_list_interface.md) with 6 customizable groups (Western, Romance, Mystery, Bestseller, Flash Sale, Clearance) and offline-first Dexie persistence. Staff can filter, group, and export ISBN lists for promotion setup or title-selection workflows without leaving the CMS (BuildListModal.vue et al., build_list_interface.md).

10.4 Current State

Shipped:

In Progress:

Deferred:

10.5 Decisions & Directional Rulings

10.6 🔧 For Builders

Key Services & Classes:

Key Routes:

Parity Reference: See PARITY.md "Pricing / Promotions / Campaigns" section (8 rows: legacy behavior, ported forward or deferred status, divergence rationale).


11. Accounts, Organizations & Contacts

11.1 Purpose

An account is the central customer record in Centerpoint — the single entity representing a school, library, bookstore, vendor, or individual who purchases books. Every account holds organizational identity (company name, account type), geographic location, contact information, order history, plan memberships, and a classification profile used to determine pricing, eligibility, and communications. Organizations extend the account model: branches enable multi-location customers to manage separate purchasing within a single parent account, while contact pools and address composition (planned) will support complex, multi-person purchasing workflows. The account system also houses callback management, allowing staff to track follow-ups with customers and manage relationship workflows through the Account Representative Dashboard.


11.2 Legacy Behavior

The legacy Visual FoxPro application (webnet.prg) stores account data in the ACCOUNTS table, keyed by ACCOUNT_KEY (a unique identifier per customer). Each account carries denormalized contact information (first name, last name, title, phone, fax, email), organizational fields (ARTICLE, ORGNAME), and a single physical address (STREET, CITY, STATE, ZIP). Contact handling is distributed: the PASSWORDS table mirrors contact identity per login row (parallel SEX, FIRST, LAST, TITLE, VOICEPHONE, FAXPHONE, EMAIL fields), creating duplication across the two tables. The RECALLD (recall date) field on accounts tracks the next callback date for account representatives. Additional address variations (shipping, billing, standing-order specific) are stored denormalized in the legacy ADDRESSES table as separate rows, with no unified pool deduplication. The NATURE field (webnet.prg line context, account classification) is a single-character code (C=customer, I=individual, V=vendor, B=bookstore, S=supplier, P=publisher) used to distinguish account types.


11.3 Ported Forward + Enhancements

Account core: The Account model (app/Models/Account.php) preserves all legacy contact and organizational fields (ARTICLE, ORGNAME, STREET, CITY, STATE, ZIP5, COUNTRY, SEX, FIRST, LAST, TITLE, VOICEPHONE, EXTENSION, FAXPHONE, EMAIL, RECALLD), maintaining compatibility with the daily DBF rebuild. The modern application adds the account_meta table (app/Models/AccountMeta.php) — a classification layer that augments every account with 86 calculated columns (commit 5565eaed8) covering account types (NATURE codes), plan membership, behavioral flags, metrics, and preserved external preferences (promote_western, promote_romance, etc.). Classification runs during OrganizationContextService context build (app/Services/OrganizationContextService.php) and is also available via artisan account:classify-all.

Address composition and contact architecture: The Address model (app/Models/Address.php) and AccountAddress pivot (app/Models/AccountAddress.php) implement a composer pattern: each account_addresses row links an account to a canonical geographic location and specifies use flags (default_physical, default_shipping, default_billing, default_plans_shipping, default_plans_billing, default_promotionals). The AddressTranslationService (app/Helpers/AddressTranslationService.php) translates composed addresses to webhead-shaped fields (COMPANY, ATTENTION, STREET, CITY, etc.) with fallback rules. During transition, contact identity remains denormalized on accounts and passwords; the target-state canonical contacts pool (planned) will deduplicate across all contact identities via soft-link policy, with string_id dedup keys (app/Models/Contact.php, not yet wired).

Callback management: The CallbackManagementService (app/Services/CallbackManagementService.php) manages RECALLD updates with dual persistence (accounts + accounts_meta, surviving daily rebuilds) and automatic Custnote generation with locked_at timestamps (locked_at surviving partial rebuilds). The AccountRepresentativeDashboard.vue (resources/js/Pages/AccountRepresentativeDashboard.vue) consolidates callbacks into a unified SPA with workload visualizations, progress tracking, and bulk rescheduling strategies. Related DashboardController endpoints (app/Http/Controllers/DashboardController.php) handle completions, reschedules, and bulk operations.


11.4 Current State

Shipped:

Partial / Planned:


11.5 Decisions & Directional Rulings

D1: Account classification is point-in-time, not per-request. The 86-column account_meta classification runs once during OrganizationContextService build (context TTL 3 hours) and is cached. No per-request classification. Rationale: 12 temp tables with ~100 queries per account, ~30s cold / 0.55s cached; per-request would be 30s per page load. Classification is deterministic (NATURE codes, order history, plan membership); caching is safe.

D2: Contact denormalization persists during transition. Accounts and passwords keep their SEX, FIRST, LAST, TITLE, VOICEPHONE, FAXPHONE, EMAIL fields through the transition to the canonical contacts pool. Writes to these fields trigger soft-link ensure-or-create in the contacts pool (when implemented), but the composer fields stay for legacy reads and DBF mirroring. Rationale: Accounts is DBF-mirrored; passwords is structurally frozen (daily rebuild). Removing denormalized fields would require schema changes that can't survive the rebuild.

D3: RECALLD dual-persistence survives account rebuild. Callback date changes write to both accounts (for immediate views) and accounts_meta (survives daily rebuild). Custnotes created with locked_at timestamp survive partial rebuilds. Rationale: accounts is rebuilt daily from DBF, losing MySQL-only changes; accounts_meta persists. Dual-write pattern ensures continuity across rebuild cycles.

D4: Address composition via fallback chain, not override-only. Default_physical composes from accounts directly (accounts.ARTICLE+ORGNAME, accounts.SEX+FIRST+LAST, etc.); non-physical pivots read via contact FK (when planned) with fallback to accounts composite. Rationale: No customer should ever see blank address fields; fallback ensures coverage. COMPANY override (pivot-only) allows alt-row organizational identity without affecting accounts.


11.6 🔧 For Builders

Core Services & Models:

Controllers & Routes:

Components:

Commands & References:


12. Fulfillment, Printing & Shipping (Pitney Bowes / SORTORDER)

12.1 Purpose

Once an order is confirmed — whether from a web cart, standing order fulfillment, back-order release, or trade plan — it must move through a complex pipeline to reach the customer. Fulfillment decides what books ship and how they route (print on demand, stock pull, trade distributor, etc.). Printing & Shipping then produces the physical shipment — invoices, packing lists, labels, and manifest files ready for a carrier.

The fulfillment system routes orders into distinct lanes: orders bound for Pitney Bowes (a 3rd-party shipping application that prints labels and calculates weights), orders destined for in-house printing (paid-per-order processing, special handling), back-order batches, and fast-print (print-on-demand) flows. Each lane has its own print template, shipping manifest, and partner integration.

This chapter covers how orders flow from confirmation through fulfillment routing, then through printing and shipment execution — with a special focus on the SORTORDER hash and PSHIP flag, which form the contract with Pitney Bowes.


12.2 Key Workflows

Back-Order Release & Batch Invoicing

When a customer orders a title that isn't yet in stock, the order is held as a back-order. Every month (typically at the start of the month following publication), back-orders are released: the system retrieves all held orders for titles that have now arrived, recalculates pricing (e.g., jobber discounts may shift), merges multiple orders per account, and creates heads ready for picking and shipment. The orders:release-backorders command (BackorderReleaseService, ProcessBackOrderAction) performs this release:

The release command includes a dry-run mode for staff to preview what will happen before committing. (Source: webnet.prg:60095–60992 library merge, FIFO, discount overlay; app: release-backorders command, dry-run mode at PARITY.md#Fulfillment).

Fast-Print (Print-on-Demand) Lane

Print-on-demand titles — those flagged with HOTBOX='FPR' (originally 'FPRINT' in the legacy app, truncated to 3 characters in the DBF) — are sent directly to a print vendor rather than pulled from stock. These orders skip the inventory gate (they always "ship" their full requested quantity, even if stock is zero) and carry a forward-dated invoice (one month ahead, so the title's receipt date appears correct to the customer). The POD lane:

POD orders are excluded from Pitney Bowes grouping and flow directly to in-house printing. (Source: webnet.prg:158781–160254 FASTPRINT console, fstatus state machine, hotbox marker; app: FastPrintService, /fastprint CMS console, FastPrintLaneTest; PHASE 1 + 2 committed 2a08a04ec, fcaa106af, ceec2ed1a.)

Standing-Order Fulfillment & SORTORDER Hash

Standing orders — recurring monthly deliveries to schools and libraries — are fulfilled via the Fulfill Plans tab on the /standing-orders-cms page. The fulfillment process is staged:

  1. Build orders: create heads and detail lines from each plan's enrolled series, grouped by account and address. At this stage, the system computes the SORTORDER hash — a fingerprint of the order's content used by Pitney Bowes to group identical orders for a single label set and weight calculation.
  2. Route to destination: classify each head as Pitney-Bowes-bound (grouped, high-volume, auto-label), downstairs (special handling, low-volume), or held for manual review.
  3. Materialize: commit the staged heads to the live database and assign TRANSNO identifiers.

The SORTORDER value is built from the 2-letter codes in the sosmart map (now stored in Plan::plansInfo()), one code per enrolled series, concatenated in a fixed order with per-series quantities: 01AA01BB01CC01HH01JJ means qty 1 of Christian-L1 + qty 1 of Christian-L2 + qty 1 of Christian-L3 + qty 1 of Premier-Fiction + qty 1 of Premier-Mystery. Identical content (same series, same quantities) produces the same SORTORDER, allowing Pitney Bowes to batch-group them.

The PSHIP flag indicates fulfillment state:

Single-book orders (singletons) and staff-routed special-handling orders skip the Pitney Bowes lane and are written 0,1,1,1 directly. (Source: webnet.prg:129164–129933 PRINT_STANDING, sosmart 16-row code map, onesie-collapse logic, SORTORDER build; app: StandingOrdersPreview::buildOrders(), StandingOrdersPreview::buildSortorderCode(), Queue\StandingOrder::materializeHead() flag-setting, FulfillStandingOrders UI with 4-stage wizard.)

Pitney Bowes Handoff

After standing-order or regular-order fulfillment creates heads with PSHIP=1, the Pitney Bowes shipping application (external to this app) reads the database:

The app then resumes: PrintQueueService queues those orders for invoice and packing-slip printing (in-house), triggered by the flag flip. This is the single external integration point — everything else (order fulfillment, print execution, manifest generation) is internal to Centerpoint. (Source: legacy webnet.prg:101064–102848 print queues, flag routing; app: PrintQueueService, per-flag batch selection.)

Bulk Printing & Shipping Manifests

Once orders are flagged PSHIP=0 (or 0,1,1,1 for non-Pitney orders), the invoice and packing-slip print run executes. Staff trigger a print action at the console, which:

Print jobs can be filtered by date range, account, order type, and other criteria; staff can preview before committing. (Source: webnet.prg:99863–102848 invoice print engine, packing-list templates, stamp logic; app: PrintQueueService, print routes, PDF queue.)

Invoice & Packing-List Templates

The app produces multiple print formats:

Each format is a .dot merge template, filled at print time with order, detail, and account data. Print queue batches are logically grouped (by account, by date, by order type) and can be previewed before printing to PDF. (Source: webnet.prg:99863–101571 EASYDATA, INSENDTOPRINTER, print template logic; app: PrintQueueService batching, .dot template rendering, PDF outputs.)


12.3 Audience & Access

End-user librarians do not directly interact with the fulfillment or printing layer; they place orders and track status through the Orders and Shipments pages.

Account Representatives may see high-level shipment status in their account dashboards but do not control fulfillment routing or printing.

Customer Service Representatives manage back-order investigations and can view fulfillment status, cancel or adjust pending orders, and trigger special routing or reprinting when needed.

Operations & Fulfillment Teams control the entire fulfillment and printing pipeline:

System Administrators audit fulfillment lane configuration, permissions, and data consistency.


12.4 How It Works: The Fulfillment Pipeline

The Stages of Standing-Order Fulfillment

The /standing-orders-cms Fulfill Plans tab orchestrates standing-order fulfillment as a 4-stage wizard. Each stage is an ActionRun — a resumable, idempotent batch operation that can be paused and resumed without duplicating work:

  1. Stage 0 — Queue Orders

    • StandingOrdersPreview::buildOrders() iterates enrolled plans for each account, selects eligible titles from the month's FINALINV inventory (gated on INVNATURE='CENTE'), and builds queue_brohead / queue_brodetail rows.
    • For each plan, calls getRegularTitles(), getBestSellerTitles(), or getCustomMixTitles() to select the ISBN list for that month (respecting plan type, format/publisher filters, never-re-ship rules).
    • Computes the SORTORDER hash by reading each plan's 2-letter code from Plan::plansInfo(), concatenating in alphabetical order, and appending per-series quantities.
    • For bestseller or custom-mix content, extends the hash with a . delimiter and a SHA-256 content fingerprint (deterministic, so identical mixes group).
    • Saves each head with the computed SORTORDER and a DESTINATION flag (Pitney Bowes vs. downstairs vs. held).
    • (Source: app Queue\StandingOrder::buildOrders(), StandingOrdersPreview::buildSortorderCode().)
  2. Stage 1 — Route to Destination

    • Queue\StandingOrder::shipQueue() (operator-triggered) reads the queued heads and applies routing rules: auto-gate for mail-innovations limits, operator overrides, and fulfillment strategy (e.g., "always group Platinum by address" or "split custom-mixes to downstairs").
    • Sets DESTINATION on each head (FulfillmentDestination::PitneyBowes, Downstairs, ManualReview, or Held).
    • Operator can manually override individual heads or entire groups (the moveOrders UI allows setting routing_approved_at for safety gates).
    • (Source: app Queue\StandingOrder::shipQueue(), CMSPlansController routing UI.)
  3. Stage 2 — Materialize

    • Queue\StandingOrder::materializeHead() (operator-triggered) converts queued heads to live brohead / brodetail rows with assigned TRANSNO identifiers.
    • Reads the DESTINATION flag and sets the PSHIP + PEPACK / PIPACK / PINVOICE flags: Pitney Bowes heads → 1,1,1,1 (awaiting PB); downstairs → 0,1,1,1 (skip PB); held → flags TBD.
    • Contiguously assigns TRANSNOs in SORTORDER order (so same-hash heads get adjacent transaction IDs, mirroring the legacy renumber pattern).
    • Calls HeadRetallyService to recalculate totals (AMOUNT, TAX, SHIP, etc.) and StandingOrderBillingConsistencyService to ensure address consistency across the plan.
    • (Source: app Queue\StandingOrder::materializeHead(), HeadRetallyService, flag-setting logic.)
  4. Stage 3+ — Print & Ship (below, shared with back-orders and web orders)

Back-Order Release

The orders:release-backorders command (ProcessBackOrderAction) executes as a single, all-or-nothing commit:

  1. Pre-clean: delete orphaned detail rows from prior months (no matching head).
  2. Load back-order heads (where PSHIP=4 or similar held flag) and inventory for the target publication month.
  3. Merge: for each account, combine multiple held heads into one (via HeadMergeService).
  4. FIFO allocate: sort by TRANSNO (oldest first), distribute available stock.
  5. Partial fulfillment: lines that don't get stock remain held; lines that do are stamped for fulfillment.
  6. Discount overlay: apply any standing-order or promotion discounts.
  7. Append to live heads.
  8. Run the archive sweep (below).

The command supports --dry-run to preview before committing. (Source: app BackorderReleaseService, ProcessBackOrderAction, dry-run mode.)

Fast-Print Lane

The fast-print monthly workflow:

  1. Flag-available (orders:fastprint flag-available): scan titles by publication date, find those without a prior fastprint mark and with FASTAVAIL='YES' (or unset, subject to auto-rule gate), and set FASTPRINT='Y' on the title row.
  2. Create (orders:fastprint create): query back-orders for fastprint-marked titles, create a print head per account/address, forward-date the invoice (month ahead), set ship flags (PSHIP=0, PIPACK=3), and generate the vendor CSV.
  3. Export & send to vendor: the FastPrintVendorRow staging table accumulates rows; a combined CSV is exported and handed off to the print vendor (usually monthly).
  4. Status-check (orders:fastprint status-check): poll the vendor system for completion; mark heads done and ready for in-house printing.

(Source: app FastPrintService, orders:fastprint command, FastPrintLaneTest contract tests.)

Print Queue & Shipment Manifests

Once orders are ready for printing (flag state PSHIP=0, PEPACK=1), the PrintQueueService manages the print run:

  1. Select batch: operator chooses a date range, order type, or account scope via the console.
  2. Render PDFs: fetch order data, render invoices and packing lists via .dot templates, accumulate into a batch print job.
  3. Generate manifest: extract shipment metadata (addresses, carrier, tracking numbers if known) and format as EDI or CSV for the carrier.
  4. Preview & commit: operator previews the batch before printing to PDF or sending to the printer.
  5. Mark printed: flip flags to PEPACK=3, PIPACK=3, PINVOICE=3.

Print jobs are idempotent — reprinting a batch re-renders the PDFs but doesn't re-stamp the flags unless explicitly reset. (Source: app PrintQueueService, print routes, CLI print command.)

Archive & Cleanup

After shipment, orders move to the archive tables (allhead / alldetail):

  1. ArchiveProcessedOrderAction or orders:archive-sweep moves shipped heads (where all lines have shipped) to archive.
  2. Back-ordered lines (where some detail rows remain unshipped) are split: shipped lines go to archive, unshipped lines stay in the live back tables for next month's release.
  3. Soft-lifecycle audit (legacy bridge): SoftLifecycleAuditService infers timestamps for legacy writes that bypassed the app (e.g., Pitney Bowes flipping PSHIP=0 directly on the DBF).
  4. FINALINV is reset to zero for the month (so next month's fulfillment can rebuild from the fresh received inventory).

(Source: app ArchiveProcessedOrderAction, orders:archive-sweep, SoftLifecycleAuditService.)


12.5 Integration Points

Pitney Bowes: The external shipping application reads orders with PSHIP=1 grouped by SORTORDER. Staff trigger "get orders", the app lists distinct SORTORDERs alphabetically, staff pick one, and PB flips those orders to PSHIP=0 when done. No two-way API; the app polls the DBF flag.

Legacy VFP App: The modern app coexists with the legacy webnet.prg. Both can write fulfillment heads and details. The SoftLifecycleAuditService (running at 23:00 ET nightly) audits orders that the legacy app modified directly on the DBF (e.g., back-order status, shipment consolidation) and updates the MySQL mirror to match.

Inventory System: Fulfillment seeds from the monthly FINALINV inventory snapshot. FINALINV is set by the inventory:make-available command, which tallies received books and published inventory updates. Fastprint checks FASTAVAIL (a per-title opt-out for print-on-demand eligibility).

Account & Address Tables: Fulfillment reads account-level preferences (billing consistency, format/publisher filters, prepaid status) and address records (shipping vs. billing). Address mismatches are flagged in the Billing Consistency review queue.

Standing Order Plans: The fulfillment system reads Plan::plansInfo() (the source of truth for plan registry, 2-letter SORTORDER codes, class, discount, placeholder ISBN) and the plan's active subscriptions (StandingOrder rows) to determine which plans are enrolled for each account.

Print Queue & Email: After materialization, heads flow to the print queue. Email notifications are triggered by AccountTransactionEvent (codes 34–35 for soft-lifecycle audit, 36+ for account status changes); the pipeline logs events as transactions for audit trails.


12.6 🔧 For Builders

Key Classes & Services

Routes & Commands

CMS Routes (in routes/web_auth.php):

Artisan Commands:

Database & Cache

Cache:

Contract Tests & Parity

See tests/Feature/Fulfillment/ for the lane contracts:

Parity anchors (legacy to app code):

Critical Requirements

  1. SORTORDER uniqueness: Identical content (same series, same qty) must produce identical SORTORDER. The hash uses the per-series code from Plan::plansInfo() (source of truth) in alphabetical order and per-series quantity. Test against the legacy ground-truth samples: 01AA01BB01CC01HH01JJ (5-series group) and 01AA01BB (2-series group).
  2. Pitney Bowes handoff: Orders bound for Pitney Bowes must be written with PSHIP=1 (1,1,1,1) at materialization (not 0). Only Pitney Bowes, when done, flips PSHIP→0. Non-Pitney (downstairs, fast-print) orders skip to 0,1,1,1 directly.
  3. FINALINV gate: Standing-order title selection and back-order release must gate on INVNATURE='CENTE' and target FINALINV (not ONHAND or current stock). FINALINV is set once per month by inventory:make-available (after orders:release-backorders + daily rebuild).
  4. Contiguity: TRANSNO assignment must be contiguous within SORTORDER groups (or at least within a fulfillment run) so same-hash orders get adjacent IDs, mirroring legacy's sorted renumber. Use SequenceService::nextTransno() but assign in SORTORDER order.
  5. Never mutate production inventory: The queue build uses a staged queue_inventories snapshot; ONHAND is never decremented during fulfillment (only the queue snapshot is). FINALINV is reset to zero at month-end (after fulfillment and archive).
  6. Idempotency & resumability: Each fulfillment stage is an ActionRun that can be resumed without duplicating heads. Guard against re-running with materialized_at timestamps or a run_id foreign key.

For detailed queue-stage decisions and forward-design notes (Bestseller + custom-mix handling, auto-gating limits, per-series format filters), see:


13. Notifications, Email & Chat (Brutus)

13.1 Purpose

This domain keeps customers and staff informed — automatically on events (plan renewals, expirations, order status changes) and on-demand (account lookups, plan health checks, contact callbacks). The AccountEvents pipeline fires email + in-app notification + custnote + transaction log on standing-order lifecycle changes. The Brutus chat interface surfaces intent-driven tools (account edit, contact callback, standing-order management) inline within the app, accessible from buttons, widgets, and canvas embeds without leaving the current page.

The legacy app's email system was file-based (EMBASE.DBF dropped by a console; staff hand-wrote templates). The modern app replaces that with a unified event pipeline that triggers email + side effects deterministically, paired with a ubiquitous chat layer that makes common actions available everywhere.

13.2 Legacy behavior

The legacy VFP app (webnet.prg) had three distinct notification paths:

Trade email console (NEW_EMAIL_CONSOLE :136450–138136): A staff interface to compose and send promotional emails to trade accounts. Templates were file-based; the system tracked EMBASE.DBF exports (EMBASE_UPDATE :137021, EMBASE_DELETE :137131).

Mail/file library (MAIL_CONSOLE_CS :125537, MAIL_CONSOLE_MENU :85519): A separate staff tool to store templates, forms, and images for reuse in mailings. No API integration — staff downloaded, edited locally, and re-uploaded.

Proofing emails (PROOF_WEB_COPY :102948): A workflow to send web-copy text for editorial review before publication — an infrequent, manual process.

All notification triggers (new order, plan expiration) went through hardcoded email routines (SEND_PASSWORD :6098, SEND_FLYER :6351). There was no unified event log, no in-app notification, and no automatic custnote record.

13.3 Ported forward + enhancements

Event pipeline: The legacy approach of hardcoded per-action emails became AccountTransactionEvent (app/AccountEvents/AccountTransactionEvent.php:55-79, 20260701). A single dispatcher fires email + notification + custnote + transaction log for standing-order lifecycle events:

Nightly expiration detection: Legacy staff manually reviewed accounts; the app now runs ScanPlanLifecycle daily at 09:00 ET (app/Console/Commands/ScanPlanLifecycle.php; detection lives in app/AccountEvents/PlanLifecycleEvaluator.php:29-46isExpiring()/pace()) to detect expiring and lagging plans (30-day dedup, QUANTITY×2-month pace). No user action required.

Health checks: The legacy app had no equivalent. The modern app adds CheckPlanHealthController — an on-demand action that surfaces from both the chat (PlanActionsWidget.vue, 20260702) and the account-rep dashboard (StandingOrderEdit.vue, 20260702). It checks whether a plan is healthy, expiring-under-budget, or actively lagging, and fires email + notification accordingly.

Choice/Trade email summaries: Legacy emails were rigid templates. The modern app computes dynamic summaries per plan type:

Auto custnote: Every notification writes a custnote automatically (app/AccountEvents/SideEffects/WriteEventCustnote.php, 20260701) with the event kind (SUBJECT = PLAN RENEWED, PLAN EXPIRING, etc.) — creating an audit trail without manual entry.

Manual contact notes: Staff can log a contact note (phone, email, in-person visit). The note now fires a partial event pipeline (98c4ab5b1, 20260701): a transactions row (code 46, TransactionDomain::ContactNote), an in-app notification to the note author only, and CALLNATURE tracking (data.call_nature from the note's existing field). Deliberately no customer email on contact-type notes (decision 20260701).

Brutus chat (ubiquitous tool dispatcher): A green-field replacement for the legacy console pattern. Intent-driven: instead of navigating to a "Contact Callback" page, staff click a button → the chat opens → the contact form renders inline. Intents map to canvas tools (AskBrutus.php constants, 20260701+). Two invocation surfaces:

All intent handlers return the same response shape (status, type, message, chrome.title). A unified tool-completed event ({intent, status, payload}) replaces per-tool events.

Documentation KB (future): A new tier in the answer pipeline (design locked, pre-implementation). Curated FAQ (regex/keyword) → KB grounded answer (chunk retrieval + optional LLM) → fallback (admin alert). The corpus is 67 markdown docs from storage/notes, all staff-audience today; chunk granularity is H2 sections. Data architecture: KB → Chunk (verbatim raw text + metadata + durable back-reference) → Index (disposable, BM25 ranking now, dense/embedding later). No LLM generation required for Level 0 (section-as-answer); enrichment (Level 1) and rephrasing (Level 2) are optional. Expected Q4 2026 start (currently pre-implementation).

13.4 Current state

Shipped (complete):

In progress / partial:

Planned:

PRD Page 2

13.5 Decisions & directional rulings

Ruling Date Rationale
Email on every plan event — one event = one email + notification + custnote. No event batching. 20260701 Transparency + audit trail; each action is atomic and logged. Pace (qty×2mo) prevents notification fatigue.
Contact-type notes = log-only — no auto customer email. Manual contact is staff's action, not a customer-facing event. 20260701 Clarity: contact-note is internal documentation; email goes only to the note author (in-app).
Notifications from a shared account (nstewart@), not individual staff. 20260701 Single sender reduces support confusion; individual author tracked in transactions.by_email.
Check Plan Health is universal — applies to all plan types (Choice, Trade, Regular), not just Choice. 20260702 Parity: legacy had no equivalent; modern adds uniformly. Trade gets its own email summary block (not the Choice "titles selected" template).
Account health derivation, not persistence (for now). Flags computed on-read from transactions rows (codes 38–46) + StandingOrder lifecycle scopes. 20260701 Faster iteration; permanentization to account_meta columns + batch classification = deferred pending flag-set stabilization.

13.6 🔧 For builders

Core event pipeline:

Plan lifecycle + health:

Brutus chat architecture:

Knowledge Base (future):

References:


14. Reports & Analytics

14.1 Purpose

Reports and analytics provide operations and management teams with visibility into sales trends, inventory velocity, standing order health, account performance, and fulfillment metrics. These dashboards and exports allow staff to monitor business health, plan inventory acquisitions, track fulfillment, analyze marketing effectiveness, and manage standing orders.

14.2 Legacy behavior

The legacy VFP application offered a limited set of hardcoded reports accessible through menu systems. Key legacy reporting features included:

Management menu reports (GEN_ORDER_MENU :120589, MIS_ORDER_MENU :126571 in webnet.prg):

Inventory projection (GEN_ORDER_MENU_INVPROJECT :122921):

Specialized reports:

Reports were often run ad-hoc, output to files on disk, or printed directly. Caching and reusability were manual (recalculate from scratch each time or maintain cached spreadsheets).

14.3 Ported forward + enhancements

The modern application consolidates and significantly expands the legacy reporting capability through a unified Analytics Dashboard service architecture, with 24 fully implemented reports across eight categories.

Services — Reporting is organized into domain-specific services (app/Services/Reports/Analytics/):

All services support consistent filtering across reports: date range, account keys, inventory nature (CENTE/TRADE), and order type (daily/standing).

Report categories — Ported from legacy ad-hoc menus and expanded:

  1. Sales & Performance (5 reports) — Annual performance, monthly comparison, multi-year trends, sales-by-plan-type, executive dashboard ("Big Picture")
  2. Standing Order Plans (3 reports) — Vital signs, year-over-year, 12+ month projections
  3. Inventory (4 reports) — Lifecycle metrics (Not Yet Published → Promotional → Clearance → Expired), CENTE vs TRADE comparison, heavy stock analysis, historical snapshots
  4. Analytics Breakdowns (10 reports) — Performance by bestseller flag, territory, series, format, division, non-consignment returns, order type, campaign, marketing channel, sales presentations
  5. Account Analytics (7 reports) — Summary, age breakdown, type breakdown, plan type breakdown, purchase metrics, health indicators, activity tracking
  6. Projections (3 reports) — No New ISBNs scenario, depletion calendar, acquisition recommendations
  7. Acquisitions & Production (3 reports) — Acquisitions status, acquisition timeline, production timeline
  8. Royalties (2 reports) — Annual billing and royalties planning

Enhancements over legacy:

Divergences from legacy (all approved):

14.4 Current state

Shipped:

In progress / Partial:

Not yet ported:

14.5 Decisions & directional rulings

D12 — No New ISBNs Projection (5-year ISBN lifetime)
All depletion projections use a fixed 5-year ISBN lifetime from PUBDATE. Titles older than 5 years are considered "expired" and excluded from recommendations. This matches legacy's acquisition planning model and ensures forecasts remain realistic across the catalog.

D13 — Caching is indefinite unless explicitly bypassed
Reports are stored persistently in storage/app/private/reports/ and never expire automatically. This is a deliberate trade-off: accuracy is secondary to performance and predictability. Users who need fresh data must explicitly use --no-cache flag (which archives the old report with a timestamp and regenerates). This prevents accidental multiple-hour regenerations when a report is requested twice in the same session.

D14 — Acquisitions spreadsheet = source of truth (transitional)
During the transition period, inventory acquisitions data lives in a CSV spreadsheet (ACQUISITIONS_SPREADSHEET_PATH). The website provides read-only analytics over that file. CRUD operations are disabled (ACQUISITIONS_TRANSITION_MODE=true). When ready, ACQUISITIONS_TRANSITION_MODE=false will unlock full in-app management. This prevents dual-source data conflicts during the bridge period.

D15 — Report generation runs on CLI, not web
Long-running reports (20–30 minutes for 2-year datasets) are generated via artisan experience:generate-report, not the web interface. This prevents timeout errors and allows asynchronous scheduling. The web interface (/analytics/reports/company/download) retrieves cached reports or shows generation-in-progress status if a lock is active.

D16 — Report JSON format (future: CSV/Excel/PDF)
Current reports export as JSON (consumable by web UI and by programmatic API). Future support for CSV, Excel, and PDF formats is planned. The service layer (CompanyReportService::generateReport()) is format-agnostic; output format is a parameter.

14.6 🔧 For builders

Services and key methods:

Controllers:

Routes:

Artisan commands:

Filter parameters (supported across all reports):

Caching behavior:

Configuration:

Development references:

Testing:


15. EDI / ONIX / MARC — Metadata Ingestion & Distribution

Centerpoint receives, processes, and distributes bibliographic metadata through three overlapping channels: ONIX (book metadata from publishers), MARC (library-formatted catalogs), and EDI (order/fulfillment messages with trading partners). This chapter documents each pipeline, their current implementation status, and the architecture for builders extending them.

15.1 Purpose

The metadata problem: Centerpoint serves libraries with book orders, but the source data (titles, pricing, publication status, BISAC categories, contributor information, cover images) must flow in from publishers and be reformatted for multiple downstream audiences:

The solution: Three independent pipelines transform vendor data into MySQL, then export in multiple formats on demand.

Scope of this chapter:

15.2 Background: Legacy Implementation

The legacy app (webnet.prg) supported three separate metadata paths, each hardcoded to a specific vendor or format.

ONIX: Legacy feature GEN_ONIX_MENU (:115721 in webnet.prg, with GEN_ONIX_WRITE :116051) offered a manual UI to export the complete inventory as an ONIX 2.1 file (a date-range / catalog-nature / single-ISBN form → confirm-and-write to a network share, for fulfillment-partner consumption).

⚠️ The legacy menu implemented only this export path. The file-drop ingestion and per-ISBN message-view described in §15.3–15.4 are new-system capabilities (onix-cli + OnixReconciliationService), not ports of legacy functionality — no ingestion trigger exists in webnet.prg.

The legacy pipeline was synchronous, file-driven, and tightly bound to Ingram's ONIX 3.0 schema. No scheduling, no message queue, no re-processing — ingest happened once per upload.

MARC: Legacy feature GENERAL_VIEW_ACCESS_MARC (:20272) provided catalog access through:

B&T is now defunct (as of 2026); the EDI infrastructure remains but is deferred pending a broader "distribution-partner pipeline" design.

15.3 Key Concepts

Metadata formats:

Pipeline architecture: All three follow a similar pattern:

  1. Ingest — Raw vendor file arrives (ONIX XML, EDI text, MARC binary/MRK)
  2. Parse — Format-specific parser extracts structured data
  3. Reconcile — Cross-reference with MySQL inventory table; resolve ISBNs, pricing, availability flags
  4. Store — Intermediate results cached in a transient store (onix-cli SQLite, EDI file system)
  5. Export — On-demand output in target format (ONIX XML, MARC file, EDI message, or spreadsheet)

ONIX as source of truth: The ONIX ingestion pipeline is the primary path for new metadata. MARC is read-only input (from legacy data or publishers) but does not auto-update inventory. EDI is order/fulfillment messaging, not title metadata.

15.4 Current State in the App

ONIX Ingestion (✅ Full Parity)

Architecture:

Routes & controllers (routes/onix_edi.php):

Scheduled sync (routes/console.php:306–331):

onix:sync --time-limit=540   # Friday 03:00 ET, 9-minute budget
onix:sync --time-limit=540   # Monday 03:00 ET, 9-minute budget

Full pipeline: OnixSync command (app/Console/Commands/OnixSync.php:13–31 class/signature; orchestration in handle() at :33–157) orchestrates:

  1. Ingest new ONIX files from the watch directory (config('cp.TRADE_MESSAGES_PATH'))
  2. Resolve ISBNs against inventory via OnixReconciliationService
  3. Cache results in storage/onix-store.sqlite (managed by onix-cli)
  4. Export reconciliation report as spreadsheet (CSV)
  5. Optionally email a daily status report (OnixDailyReport mail class)

Options (command flags):

Known limitation: The legacy OnixProduct class (app/Onix/OnixProduct.php:1–12) is marked @deprecated. It was an earlier attempt to parse ONIX in-app; the onix-cli tool supersedes it. Do not use for new work.

MARC Record Delivery (✅ Full Parity)

Architecture:

Routes & access:

File locations: Legacy FTP path: \\Webnet\ftp\From Cecilia\Processing orders - Direct to libraries\{LIBRARY_CODE}

Example: \\Webnet\ftp\From Cecilia\Processing orders - Direct to libraries\ALI_COUNTY\ALI_COUNTY_2024.mrc

Centerpoint staff do not auto-generate MARC records. The system exposes pre-built MARC files from the legacy FTP drive and provides a query interface. If libraries require fresh MARC exports (e.g., from a custom title list or standing order), the export path is via the BuildList flyer export (app/Http/Controllers/BuildListController.php) or a custom query tool.

Validation: MarcValidator enforces field 020 (ISBN) length constraints; other rules can be added as needed. Violations are logged but do not block ingestion or delivery.

EDI Messaging (⏸️ Deferred / B&T Legacy)

Status: Deferred pending generalization for future distribution partners.

Current state:

Hardcoded B&T context: Baker & Taylor's account key 6095400000001 is embedded throughout the legacy code (webnet.prg:42875, :149330, etc.). The app mirrors this hard-coding in the EDI controller's FTP credential store.

Decision (approved 20260703): The B&T EDI pipeline is deferred and will be generalized into a broader "distribution-partner pipeline" design. This will abstract:

Until then: EDI functionality stays read-only, and B&T orders are routed through the legacy system or manually entered into Centerpoint.

15.5 Architecture & Design

ONIX Pipeline Layers

Publisher ONIX feed (XML)
        ↓
   [watch directory]
        ↓
  onix-cli tool (standalone Rust/Node binary)
     - XML parsing
     - ISBN resolution (Ingram catalog)
     - BISAC classification
     - Deduplication
        ↓
  [SQLite message store: onix-store.sqlite]
        ↓
  OnixReconciliationService (Laravel)
     - Cross-ref vs inventory MySQL
     - Pricing/availability reconcile
     - Flag missing or outdated titles
        ↓
  [Result spreadsheet + cache]
        ↓
  Export paths:
   • CSV (staff reporting)
   • ONIX XML (fulfill partner consumption)
   • MySQL inventory update (future phase)

Key design rules:

  1. Subprocess isolation: onix-cli runs as a separate process; failure does not crash Laravel. OnixService::centerpointUpdate() streams results with Process::forever() (no timeout).
  2. Deferred reconciliation: CLI outputs raw ONIX data; reconciliation happens in Laravel where we can query MySQL. This keeps onix-cli stateless and portable.
  3. Time budgeting: --time-limit=S allows long-running ingests to be split across multiple runs without re-parsing already-processed messages.
  4. Reprocessing: --reprocess re-resolves all cached ISBNs against current inventory, useful after inventory schema changes or pricing updates.

MARC Access Architecture

Library staff request MARC file
        ↓
  MarcFtpController routes request to network FTP
        ↓
  [FTP shared folder: \\Webnet\ftp\...\{LIBRARY}]
        ↓
  MarcParser detects format (.mrc binary vs .mrk text)
        ↓
  MarcComposer converts binary→MRK or MRK→binary
        ↓
  [MarcRecord value objects in memory]
        ↓
  MarcValidator checks field constraints (optional)
        ↓
  Serve file to client (download or render in UI)

Design decisions:

EDI (Future: Generalized Distribution-Partner Pipeline)

Planned design (not yet implemented):

Vendor account config (encrypted)
        ↓
  [accounts → vendor relationship table]
        ↓
  DistributionPartnerService
     - Credential lookup
     - Schedule management
     - Mutual exclusion (no dual-order)
        ↓
  Inbound: {Vendor}MessageService
     - EDI 850 (purchase orders)
     - EDI 856 (advance ship)
     - Custom vendor formats
        ↓
  [Message queue: jobs.messages_pending]
        ↓
  Route to order pipeline OR skip if conflict
        ↓
  Outbound: {Vendor}ExportService
     - EDI 810 (invoices)
     - Shipment notices
     - Custom manifests

Decoupling benefits:

15.6 🔧 For Builders

Adding ONIX Support for a New Publisher

  1. Update the watch directory path:

    • Check config('cp.TRADE_MESSAGES_PATH') in config/centerpoint.php
    • Coordinate with ops: where will the publisher's ONIX files land?
  2. Extend OnixReconciliationService:

    • Add publisher-specific field mappings if needed (e.g., custom BISAC override logic)
    • Test with sample ONIX file using php artisan onix:sync --dry-run --limit=10
  3. Add a scheduled sync if needed:

    • Example: routes/console.php:306–331 shows Friday/Monday schedules
    • Use Schedule::command('onix:sync --time-limit=540')->fridays()->at('03:00')
    • Set a time limit (--time-limit) if the publisher sends many files
  4. Test reconciliation:

    php artisan onix:sync --dry-run --status
    php artisan onix:sync --history=9780743273565  # Check one ISBN
    php artisan onix:sync --limit=100              # Process first 100
    
  5. Monitor & alert:

    • Add publisher email to --notify= so staff get daily reports
    • Set up Sentry monitoring for process failures (timeout, OOM, parsing errors)

Exporting ONIX from Inventory

The OnixExport action (app/Actions/OnixExport.php) generates ONIX XML from the MySQL inventory table. Status: Deferred pending builder review of schema coverage.

To export a title subset:

$action = new OnixExport();
$onix_xml = $action->forIsbn('9780743273565');  // Returns ONIX 3.0 XML string

Delivering MARC to Libraries

  1. Verify FTP path access:

    • Test SMB connectivity: ls \\Webnet\ftp\From Cecilia\Processing orders\
    • Confirm library folders exist (e.g., ALI_COUNTY, WAYNE_COUNTY)
  2. Trigger export at MarcFtpController:

    • Route: GET /marc/{library}/{file} automatically downloads the file
    • For batch downloads: use POST /marc-action/zip to create a batch archive (if implemented)
  3. Parse a MARC file in code:

    $parser = new \App\Marc\MarcParser();
    $records = $parser->parseFile('path/to/file.mrc');
    
    foreach ($records as $record) {
        $isbn = $record->getField('020')->subfield('a');  // field 020 = ISBN
        $title = $record->getField('245')->subfield('a'); // field 245 = title
    }
    
  4. Validate MARC:

    $validator = new \App\Marc\Validation\MarcValidator();
    $violations = $validator->validate($record);
    if (count($violations) > 0) {
        Log::warning('MARC validation failed', ['record' => $record, 'violations' => $violations]);
    }
    

Preparing for EDI Generalization

When ready to onboard a new distribution partner (beyond B&T):

  1. Audit the EDI folder structure:

    • Identify which message types the partner sends/receives
    • Get a sample file to parse
    • Document credential/FTP location
  2. Create a partner-specific service:

    namespace App\Services\Edi\Partners;
    
    class {PartnerName}MessageService
    {
        public function parseInbound(string $raw): array { ... }
        public function formatOutbound(Order $order): string { ... }
    }
    
  3. Register in a partner factory:

    class DistributionPartnerFactory
    {
        public static function for(string $accountKey): PartnerService { ... }
    }
    
  4. Add configuration row:

    • Table: distribution_partners (new)
    • Columns: id, name, account_key, credential_encrypted, supported_message_types, schedule_expression
  5. Test with sample messages:

    php artisan edi:process {partner} --file=sample_850.txt --dry-run
    

Code Anchors (Cross-Reference)

Feature File Line
ONIX pipeline entry app/Services/OnixService.php 39–74
ONIX sync command app/Console/Commands/OnixSync.php 13–31
ONIX scheduled runs routes/console.php 306–331
MARC FTP controller app/Http/Controllers/MarcFtpController.php 17–54
MARC parser app/Marc/MarcParser.php 15–99
MARC parser format detection app/Marc/MarcParser.php 29–48
EDI controller app/Http/Controllers/EdiController.php 15–60
ONIX routes routes/onix_edi.php 14–43
MARC routes routes/static_files.php 24, 42–43
MARC domain routes/web.php 28
Legacy ONIX menu webnet.prg 115725
Legacy MARC access webnet.prg 20272
Legacy EDI (B&T) webnet.prg 139788

16. Time Clock, Tickets & Internal Ops

16.1 Purpose

This domain covers three operational staff tools: (1) Interactive Time Clock — staff members punch in/out, with a single status-change endpoint; (2) Support Ticket System — customer-facing issue tracking; (3) Internal Operations Dashboards — aggregated account management and operational views.

16.2 Legacy Behavior

This domain is entirely green-field — the legacy Visual FoxPro application (webnet.prg, lines 1–161252) had no unified time-clock system or formal ticketing subsystem.

The legacy system did have a workaround: procedures SERVICE_HELP (line 4447) and SERVICE_REQUESTS (line 151553) attempted to piggyback service requests onto the CUSTNOTE infrastructure, but this was not a designed system and not used operationally. The legacy application provided no interactive status tracking or draft timesheet projection.

There is no PARITY row for time clock or a modern equivalent of SERVICE_HELP/SERVICE_REQUESTS, because the modern systems diverge by design from non-systems into purpose-built infrastructure.

16.3 Ported Forward + Enhancements

Since these systems are green-field, the story is in-app modernization: three fragmented time-tracking approaches (legacy TimeCard model, chat status endpoints, manual timesheet entry) were unified in June 2026 into one cohesive status-driven state machine (shipped commit aa121654b–82aa72aa8, Jun 15).

Time Clock unification:

Tickets enhancement: The custom ticketing system (TicketController, routes/tickets.php:9–21) supersedes the non-system legacy workaround. It adds: public customer links (UUID tokens per Ticket::public_token), threaded message history, status-change audit trails (TicketStatusHistory), and image attachment processing with recompression (ImageProcessor service).

16.4 Current State

Shipped (Jun 15):

Planned / Out of scope:

16.5 Decisions & Directional Rulings

D-TC-1: Single write endpoint. All status transitions (clock in, clock out, on lunch, etc.) go through one setStatus() endpoint (TimeClockController::setStatus, routes/users.php:83). The controller does NOT hardcode per-action logic; instead, the transition guard in User::updateStatus() decides legality from the current clock state. This reduces surface area and makes the state machine self-evident in config/time_clock.php.

D-TC-2: Status events are canonical. UserStatusChanged events (fired from the controller) trigger the TimeClockState read model + TimesheetDraftSync listener. The worker-friendly side effect (automatic draft entry for clocked-in intervals) never reaches back to corrupt the event stream.

D-TC-3: Tickets diverge from legacy CUSTNOTE. Rather than extend the customer-note infrastructure to handle service requests, tickets are a standalone domain with separate models, permissions (TicketPolicy), and Blade views. This allows future enhancements (SLA tracking, knowledge-base linking) without entanglement.

D-TC-4: Draft timesheet sync is a safety net, not the system. Real-time updates come from the ProjectStatusToTimesheet listener (AppServiceProvider::boot, registered via Event::listen). The nightly timeclock:sync-drafts command (console.php:99–106) caps "forgot to clock out" at end-of-day and runs at 23:45 ET to handle edge cases.

16.6 🔧 For Builders

Time Clock:

Tickets:

Internal Ops:


17. Platform — Auth, Permissions, DBF↔MySQL Bridge, Jobs

17.1 Purpose

Centerpoint runs on a hybrid infrastructure: a modern web application reading from MySQL for speed and reporting, while the legacy DBF files remain the system of record. The Platform layer is the invisible backbone that keeps these systems synchronized, ensures users can authenticate and use the app reliably, and logs every action for audit and operational monitoring.

17.2 Authentication & Permissions

The User Model is the entry point. The User class at app/Models/User.php implements Laravel's Authenticatable contract directly (not via the trait, to avoid naming conflicts with the password() relationship). When a user logs in via the login form, Laravel validates their credentials against the Password model's DBF-mirror data and creates a session.

The Password Model (app/Models/Password.php) is not a pure data store — it implements AuthorizableContract, uses the Authorizable + HasApiTokens traits, and carries auth/authorization logic (getAuthPassword(), a custom createToken(), createCredentialsFromPasswordsTable()) plus authz-gated CRUD and cart helpers. It mirrors the DBF passwords table and holds UPASS (the encrypted password hash), UNAME (username), and EMAIL. On each backend request, HandleInertiaRequests middleware calls $request->user()->viewProps() and $request->user()->currentAccountInfo() to populate the inertia props the frontend uses.

Sessions survive password table rebuilds. The nightly RebuildUsers job rebuilds the MySQL passwords table from the DBF at 02:15 (app/Actions/RebuildApplicationTables.php line 200–220), but the session cookie remains valid. This is because the session store (Redis or file-based) is independent of the password table — the session just confirms the user is logged in; the password table is consulted only on new login.

Permissions are managed via a HasPermissionsTrait on the User model (app/Models/Traits/HasPermissionsTrait.php). The trait provides methods like canNow($permission) that check against policies defined in app/Policies/. A user's role (users.role_id) determines which policies allow or deny access. Feature-level checks typically run in controller middleware or policy methods; data mutation checks run in policy update() and delete() methods.

Configuration lives at config/auth.php. The custom password-provider driver's model is App\Models\User::class (config/auth.php:69), not Password; the web guard uses session-based authentication. No changes to this config are needed for most use cases — it's set once and rarely touched.

17.3 The DBF ↔ MySQL Bridge: Three Paths, One Schema

Centerpoint has three code paths that read/write DBF files, each with different mechanics but the same table schemas:

  1. LEGACY path — Direct fopen in PHP (app/Models/Dbf/Table.php). Used by the legacy VFP app and fallback code paths.
  2. dbf-service path — 32-bit Lumen server running VFPOLEDB on :8080 (staff/dbf-service/ config at config/dbf.php). Used when the proxy is healthy.
  3. RUST path — Rust API on :3636 (centerpoint_api repository). Not production-ready; reserved for future migration.

Name the path before hypothesizing. A data inconsistency that looks like a sync bug might be a legacy-path artifact, a timeout in dbf-service, or a Rust-side issue. Knowing which path is in play cuts debugging time in half. Check logs for DBF sync handler: or grep the request context.

INDEX is the link. In MySQL, every record that comes from the DBF has an INDEX column matching the DBF record number (RECNO). When a new Laravel model syncs to DBF, INDEX starts as NULL; after the sync, it's set to the DBF record slot. This link is sacred — never null it out unless you're about to resync. Cascade keys vary by table:

MySQL is readable; DBF is the source of truth. Writes always flow MySQL → DBF via $model->dbf()->save(). The daily rebuild at 02:15 (RebuildApplicationTables::daily_tables() in app/Actions/RebuildApplicationTables.php lines 240–290) tears down and rebuilds web*, back*, bro*, all* tables from their DBF counterparts. A plain Laravel migration on these tables will not survive the rebuild — the DBF and the rebuild job must both carry the column change.

17.4 DBF Integrity & Health Checking

Corruption is detected and healed automatically. The system has three defense layers:

1. parity:fix command (php artisan carts:fix). Runs on-demand or scheduled. It performs a full DBF↔MySQL parity check in two stages:

Healing logic at app/Console/Commands/CartsFix.php (class CartsFix) around lines 116–224.

2. data:check command (php artisan data:check). Broader weekly health check with named checks:

With --fix, it cascades to carts:fix for duplicates and dispatches SyncInventoryText job for missing rows. Scheduled weekly Sunday 8am with --fix (at routes/console.php line 180).

3. PAID flag compatibility (a subtle gotcha). The allheads table stores PAID as '0' or '1', not 'T' or 'F' like VFP booleans. Use Misc::isTruthy($value) in PHP or isTruthy(value) in Vue (Helpers/Strings.js) to normalize. Both accept 'T', '1', 'TRUE', 'Y' — a guard against future copy-paste bugs (used in 15 locations: AlphaCalculateInvoiceService, OrderTrait, Allhead, etc.).

17.5 Jobs & Transaction Logging

Jobs run on Laravel's queue. The scheduler in routes/console.php defines 23 instrumented jobs (e.g., RebuildApplicationTables::daily_tables at line 60, BuildCatalogList::build at line 100). Each scheduled job wraps its Schedule::* chain with an $instrument() closure that emits started/completed/failed transaction rows on before/onSuccess/onFailure hooks — no changes to the command class needed.

TransactionLogger is the primary gate (app/Support/TransactionLogger.php). Most transactions rows flow through TransactionLogger::record() (config-driven gating, fail-soft). (Not strictly universal — a few production call sites still write via Transaction::create() directly, bypassing the gate: app/Listeners/CreateOrderTransaction.php, app/Domain/Orders/History/OrderHistoryService.php:227, and the BulkAddressUpdate services.) Client code calls TransactionLogger::record(int $code, array $data, ...) and the logger decides what to keep based on config/transactions.php gates:

Never edit call sites to silence a noisy transaction — flip a config gate instead. All methods fail-soft: exceptions log a warning and return null so the caller never blocks.

Schedule-level lifecycle uses helper methods:

TransactionLogger::started('job-name', $data);
TransactionLogger::completed('job-name', $data);
TransactionLogger::failed('job-name', $error, $data);
TransactionLogger::bracket('job-name', $work, $startData); // auto-bracket a callable

Pulse monitoring integrates via the "Jobs" widget. It reads codes 29 (started), 30 (completed), 31 (failed), 32 (warned) and renders them with ⚙ (normal) or ⚠ (failed/warned) icons. Commits: 5c3ebd263 (refactor), 33e30878a (instrument 23 jobs).

17.6 🔧 For Builders

Auth gotchas:

DBF↔MySQL gotchas:

Health & repair:

Job instrumentation:

Testing:

For code examples and deep dives: see auth_architecture.md, dbf_integrity_system.md, transaction_logger.md, domain_seams.md § "DBF ↔ MySQL" and "Testing".


Part III — Roadmap & Direction

18. Confirmed Directional Rulings

The decisions below represent the team's locked consensus on how Centerpoint works, why it works that way, and what trade-offs were made to get here. Each has been implemented, tested, and approved. Changes to these rulings require team discussion and explicit consensus.


Orders & Carts

D1 — No pull-time repricing. Once a cart is submitted, prices are locked. No repricing happens when the order enters fulfillment. Rationale: The legacy system had post-submit price adjustments as a workaround for missing cart-time pricing logic. That gap is now closed; repricing at pull time creates fraud risk. (20260702)

D2 — Submitted carts stay in webhead until pull. WEBHEAD records are marked SUBMITTED (MARKCART=5555) and copied to alpha_webheads history, not deleted. Separate HEADING records are created for fulfillment. Rationale: Single source of truth; clean separation of shopping vs. fulfillment state. (20260702)

D3 — Backorder conversion creates new TRANSNO. When a backorder becomes available and converts to an active order, it receives a new TRANSNO, not a reused ID. Rationale: Legacy MANAGE_SELECT_BACKORDERS creates new HEADING records; app mirrors this for clear audit trail. (20260702)

D4 — Archival is a sweep, not inline. Completed orders move to archive tables via nightly batch (orders:archive-sweep), respecting legacy ARCHIVE_ORDERS gate. Rationale: Batch operations are faster and less prone to race conditions; offline processing avoids customer-facing latency. (20260702)

D6 — Free shipping: locked at cart, recalculated at pull. The seven free-shipping conditions are evaluated when the customer adds items to cart and again when they submit. Staff can override during master edit in exceptional cases. Rationale: Customers see accurate costs in cart summary; edge cases are rare enough for staff override. Certified parity with legacy ladder 20260703. (20260703, c320cd358)


Standing Orders & Plans

D1 — Parity is non-negotiable. The legacy VFP system is still live production. All ports are parity-or-better, never silent regressions. Every claim in PARITY.md is anchored to legacy code, app code, and certification commit. (20260702)

D2 — SORTORDER is a contract with Pitney Bowes. The legacy grouping hash (NNXX codes) is not a display label — it's the key Pitney Bowes uses to batch orders for weigh-in and label generation. The app must build and materialize it identically to legacy. (2026-06-18, parity matrix)

D3 — FINALINV is availability, not ONHAND. Legacy defines ONHAND = FINALINV − ALLSALES. Fulfillment judges availability against FINALINV (gross inventory), not real on-hand stock. This allows standing orders to be promised against titles publishing this month. (parity matrix, D9 Inventory)

D4 — Staged inventory, not production mutation. Queue build seeds a separate queue_inventories working copy. Production inventories are untouched until Ship. Rationale: Preserves ability to re-run, inspect, and audit the queue without altering shelf facts. Improvement on legacy. (20260702)

D5 — Operator owns routing; system informs. Fulfillment UI flags all concerns (oversized groups, address mismatches, unfulfilled titles) and seeds sensible defaults. The operator decides what ships, where, and when — never automatic rerouting. (20260702)

D6 — Contiguous TRANSNO assignment. TRANSNOs must be contiguous in SORTORDER order so that third-party consumers reading the DBF see grouped orders with adjacent numbers. (parity matrix, legacy renumber pattern)


Trade

D-T-1 — Trade as separate run, collision-free reset. Trade and regular fulfillment use shared queue tables but with INVNATURE scope-tagging. Each run type has distinct ActionRun. Reset operations are scope-aware. Rationale: Neither run disturbs the other's staged rows even if both are in flight. (20260629, user decision)

D-T-2 — Trade selection = cumulative-by-level auto-load. Each Trade Level plan is cumulative: Level 1 (CLASS_H) sources 2 titles/month; Level 2 (CLASS_H + CLASS_I) sources 4 titles/month; Level 3 (CLASS_H + CLASS_I + CLASS_J) sources 6 titles/month. No manual per-title curation in batch path. (parity matrix, D-T-2)

D-T-5 — SORTORDER is added to trade (intentional divergence-as-improvement). Although legacy trade had no SORTORDER or batch routing, the modern trade path assigns SORTORDER via content-hash and joins the same PB-lane routing logic as regular standing orders. Trade heads benefit from grouped-shipment optimization without requiring new sort codes. (2026-06-30, user decision)

D-T-6 — Queue full set, dedup on move, operator decides. Trade preloads titles onto back orders for vendor procurement sizing, so the queue must show the full plan demand without purchasing/back-order gates. The dedup + filter happen on move to production via the ship-stage toggle dedup_open_backorder (default ON). (2026-06-30, user decision)

D-T-8 — Trade move = direct-to-production preorder, tentative pricing. Trade lines skip brokered queues entirely and materialize directly to backhead/backdetail (OSOURCE='TRADE STANDING'). Pricing at queue time is tentative; the final price is recalculated out-of-band at ship time. Rationale: Production brodetail→invoice billing is not replicated in fulfillment system; app-trade tool owns that. (parity matrix, 20260703)

B&T / EDI → Distribution-partner pipeline generalization. Baker & Taylor is defunct (decision resolved 20260703). The legacy B&T EDI console suite (850/855/856/810/997), monthly buyer review, and scan-to-ship workflow are deferred in favor of a generalized abstraction. Future work: research EDI interaction patterns + buyer-review workflow for any future partner, then design reusable "distribution-partner pipeline". (20260703, PARITY.md rows 33 + 108, todo.md)


Inventory & Catalog

D9 — FINALINV = lifetime print baseline, never reset. FINALINV is the print-run commitment; ONHAND = FINALINV − ALLSALES (post-release erosion). Standing-order needed = max(0, requested − FINALINV). Rationale: Mirrors legacy exactly; FINALINV is the decision, ONHAND is derivative. (20260618, parity matrix)

D10 — Monthly make-available gates on FINALINV ≠ 0. Missing-FINALINV warning is non-blocking; operator workflow safety + data integrity. Re-seeds ONHAND := FINALINV at publish. Mirrors legacy :129221/:129236 exactly. (20260702, b58f91238)

D11 — FASTPRINT lane (POD). Stock gated OFF (ships full), 6-month PUBDATE window, HOTBOX='FPR' (C(3) truncation; legacy 'FPRINT' stores as 'FPR'), +1-month invoice date, freeship, PIPACK=3/PEPACK=1. Legacy parity at lane entry (FASTPRINT_CREATE :160254). Note: DBF HOTBOX is C(3) — verified 25 production broheads. (20260703, 2a08a04ec + fcaa106af + ceec2ed1a)

D12 — FINALINV readiness gate at Ship, not Queue. Preview stays open at FINALINV=0 (warned) to discover demand; operator keys FINALINV from preview + buffer; Ship holds any order with unsettled FINALINV. Rationale: Improves legacy's dead-end (forced external FINALINV forecast); preserves safety intent. (20260618, parity matrix)


Pricing, Promotions & Campaigns

D10.1 — Submit locks pricing. All order pricing is locked at submit time. Carts are repriced on pull only if back-ordered. Rationale: Legacy parity; pricing is not recalculated mid-cart. (parity matrix, domain_seams.md)

D10.2 — Pricing is always complete. The 42,873 ISBN × account records must be pre-calculated and cached; no on-demand fallback. Required fields always present; sparse optimization only for purchases (92.8% sparse). Cache TTL 3 hours (cold ~30s / cached 0.55s). (organization_context.md, 20260618)

D10.3 — Promotions are slot-based, not semantic. Column names (western, romance, etc.) are historical convenience; slots are interchangeable. Code asks PromotionsConfiguration for column names, never hardcodes them. (dynamic_promotions_plan.md)

D10.4 — Campaigns are processor-first. Each campaign reason has a dedicated processor class implementing CampaignProcessorInterface. The campaign-builder is orchestration, not logic. New campaign types = new processor + factory registration (no wizard-step cascading). (CAMPAIGNS_REFERENCE.md)

D10.5 — Saved queries are filter combinations, not campaigns. They store account-classification flag selections in the wizard, reducing manual filter entry for recurring audience targeting. They do not save campaign settings or email templates. (saved_queries.md)

D10.6 — Campaign options flow through whitelist. New CampaignOption enum cases must be added to BuildCampaignDrafts::normalizeOptions() or they are silently dropped before reaching build. (campaign_options_and_wizard.md, gotcha section)


Accounts & Organizations

D1 — Account classification is point-in-time, not per-request. The 86-column account_meta classification runs once during OrganizationContextService build (context TTL 3 hours) and is cached. No per-request classification. Rationale: ~30s cold / 0.55s cached; per-request would be prohibitive. (20260702, parity matrix)

D2 — Contact denormalization persists during transition. Accounts and passwords keep their SEX, FIRST, LAST, TITLE, VOICEPHONE, FAXPHONE, EMAIL fields through the transition to canonical contacts pool. Writes trigger soft-link ensure-or-create (when implemented); composer fields stay for legacy reads and DBF mirroring. Rationale: Accounts is DBF-mirrored; removing denormalized fields requires schema changes that can't survive rebuild. (parity matrix, address_contact_architecture.md)

D3 — RECALLD dual-persistence survives rebuild. Callback date changes write to both accounts (immediate views) and accounts_meta (survives rebuild). Custnotes created with locked_at timestamp survive partial rebuilds. Rationale: accounts rebuilt daily from DBF, losing MySQL-only changes. (parity matrix)

D4 — Address composition via fallback chain, not override-only. Default_physical composes from accounts directly; non-physical pivots read via contact FK with fallback to accounts composite. COMPANY override (pivot-only) allows alt-row organizational identity. Rationale: No blank address fields; fallback ensures coverage. (parity matrix)


Notifications & Email

Email on every plan event. One event = one email + notification + custnote. No event batching. Rationale: Transparency + audit trail; each action is atomic. Pace (qty×2mo) prevents notification fatigue. (20260701, todo.md §1)

Contact-type notes = log-only. No auto customer email. Manual contact is staff's action, not customer-facing event. Email goes only to the note author (in-app). Rationale: Clarity; contact-note is internal documentation. (20260701, todo.md §2, decision resolved)

Notifications from shared account (nstewart@). Not from individual staff. Rationale: Single sender reduces support confusion; individual author tracked in transactions.by_email. (20260701, config/cp.php:203)

Check Plan Health is universal. Applies to all plan types (Choice, Trade, Regular), not just Choice. Rationale: Modern adds uniformly; Trade gets its own email summary block (not the Choice "titles selected" template). (20260702, todo.md §1)

Account health derivation, not persistence (for now). Flags computed on-read from transactions rows (codes 38–46) + StandingOrder lifecycle scopes. Permanentization to account_meta columns + batch classification = deferred pending flag-set stabilization. (20260701, todo.md §3)


Reports & Analytics

D12 — No New ISBNs Projection (5-year ISBN lifetime). All depletion projections use fixed 5-year ISBN lifetime from PUBDATE. Titles older than 5 years are "expired" and excluded from recommendations. Rationale: Matches legacy acquisition planning model; forecasts remain realistic. (parity matrix)

D13 — Caching is indefinite unless explicitly bypassed. Reports stored persistently in storage/app/private/reports/ and never expire automatically. Trade-off: accuracy is secondary to performance and predictability. Users needing fresh data must use --no-cache flag. (ReportsController, CompanyReportService)

D14 — Acquisitions spreadsheet = source of truth (transitional). During transition, inventory acquisitions live in CSV (ACQUISITIONS_SPREADSHEET_PATH). Website provides read-only analytics. CRUD operations disabled (ACQUISITIONS_TRANSITION_MODE=true). When ready, --false unlocks full in-app management. (InventoryAcquisitionsController)

D15 — Report generation runs on CLI, not web. Long-running reports (20–30 minutes) generated via artisan experience:generate-report, not web interface. Prevents timeout errors; allows asynchronous scheduling. (ExperienceController, CompanyReportService)


Time Clock & Tickets

D-TC-1 — Single write endpoint. All status transitions (clock in, out, on lunch, etc.) go through one setStatus() endpoint. Controller does NOT hardcode per-action logic; transition guard in User::updateStatus() decides legality from current clock state. Rationale: Reduces surface area; state machine self-evident in config/time_clock.php. (20260615, aa121654b–82aa72aa8)

D-TC-2 — Status events are canonical. UserStatusChanged events fire the TimeClockState read model + TimesheetDraftSync listener. Worker-friendly side effect (automatic draft entry) never reaches back to corrupt event stream. (20260615)

D-TC-3 — Tickets diverge from legacy CUSTNOTE. Rather than extend customer-note infrastructure, tickets are standalone domain with separate models, permissions, and views. Rationale: Allows future enhancements (SLA tracking, KB linking) without entanglement. (20260615)

D-TC-4 — Draft timesheet sync is a safety net, not the system. Real-time updates from ProjectStatusToTimesheet listener. Nightly timeclock:sync-drafts command (23:45 ET) caps "forgot to clock out" at end-of-day and handles edge cases. (20260615, console.php:99–106)


Platform & Infrastructure

Sessions survive password rebuilds. The nightly RebuildUsers job (02:15 ET) rebuilds MySQL passwords table from DBF, but session cookie remains valid. Session store (Redis/file) is independent of password table; session confirms login; password table consulted only on new login. (auth_architecture.md, RebuildApplicationTables.php:200–220)

MySQL is readable; DBF is source of truth. Writes flow MySQL → DBF via $model->dbf()->save(). Daily rebuild (02:15 ET) tears down and rebuilds web*, back*, bro*, all* tables from DBF counterparts. Plain Laravel migrations on these tables will NOT survive rebuild — DBF and rebuild job must both carry the column change. (RebuildApplicationTables.php:240–290)

Three DBF code paths (name before hypothesizing). LEGACY path: Direct fopen in PHP. dbf-service path: 32-bit Lumen on :8080 (VFPOLEDB). RUST path: Rust API on :3636 (not production-ready). A data inconsistency might be legacy-path artifact, dbf-service timeout, or Rust-side issue. Knowing which path is in play cuts debugging time in half. (dbf_three_paths.md, feedback_dbf_service_rw_flag.md)

TransactionLogger is universal gate. Every transactions table row flows through TransactionLogger::record(). Config gates (config/transactions.php) decide what to keep: global kill switch, code-level disable, job-level fnmatch globs, max payload bytes, legacy override. Never edit call sites to silence noise — flip a config gate instead. Fail-soft: exceptions log warning and return null. (transaction_logger.md, config/transactions.php)

Jobs run on Laravel queue with instrumentation. Scheduler (routes/console.php) defines 23 instrumented jobs. Each wraps with $instrument() closure emitting started/completed/failed transaction rows on before/onSuccess/onFailure hooks — no changes to command class needed. (transaction_logger.md, 5c3ebd263 + 33e30878a)


Cross-Domain Meta-Decisions

Submit locks all pricing and policy. Orders submitted (SUBMITTED=5555) become immutable for pricing, free-shipping, and fulfillment policy. Staff can adjust line items, change quantities, override flags during master edit, but the pricing formula is never reapplied. Rationale: Fraud prevention; customer expectation. (D1 Orders, D10.1 Pricing, 20260702)

DBF remains system of record. Modern MySQL provides read-only views and fast queries; DBF is the canonical persistent store. Every app write goes DBF-ward via cascading services. Rebuild job (02:15 ET) asserts this monthly — MySQL is always a fresh mirror. Rationale: 20+ years of legacy data, external integrations (Pitney Bowes), regulatory audit trail. (dbf_integrity_system.md, feedback_dbf_service_rw_flag.md)

Batch operations, not inline. Fulfillment, archival, inventory release, reports, catalog rebuilds all run as nightly or on-demand batch jobs (orders:release-backorders, orders:archive-sweep, inventory:make-available, experience:generate-report, etc.). Rationale: Avoids customer-facing latency; enables auditing and dry-run before commit; improves data consistency. (parity matrix, multiple D4–D15 decisions)

System informs; operator decides. Fulfillment UI flags oversized groups, address mismatches, unfulfilled titles, dedup candidates. Routing defaults are sensible (PB for multi-member, downstairs for singletons). But the operator owns the final routing decision — never automatic rerouting. Rationale: Honors legacy's manual control model while making it explicit + auditable. (D5 Standing Orders, D-T-6 Trade, 20260702)


These rulings are durable; changes require consensus. See PARITY.md for the detailed legacy-parity ledger and feature-by-feature implementation status.


19. Green-field / Future Features

Shipped Green-field Features (✅)

Brutus ubiquitous chat. The legacy app's email console (NEW_EMAIL_CONSOLE, EMBASE.DBF) was a separate staff interface for promotional outreach — staff navigated menus, composed templates, and scheduled sends as a distinct workflow. The modern app replaces that with Brutus, an intent-driven chat dispatcher that embeds action tools directly into the app. Instead of leaving an account page to open a contact form, staff click a button → the chat opens inline → the contact form renders as a canvas widget inside the chat. Intents (account edit, contact callback, plan actions) map to reusable canvas tools; every page is a trigger surface; the chat handles session bootstrap and response routing transparently. Shipped Jul 2026. 🔧 See brutus_ubiquitous_chat.md for invocation primitives (useBrutusTrigger page-button API, canvas-chip instant dispatch, response shape contract).

Unified Time Clock. Entirely green-field — the legacy app had no unified time-clock system. Prior to June 2026, Centerpoint ran three fragmented timekeeping approaches: a defunct TimeCard model, disparate clock endpoints per action (/clock/{in,out,lunch,break}), and manual timesheet entry without automation. The modern system consolidates these into one status-driven state machine (UserStatus enum, TimeClockState read model, single setStatus() endpoint) that projects the status stream into valid next-state actions and auto-drafted timesheet entries. Staff punch in/out via a single widget; the system automatically splits lunch breaks and detects overnight intervals. Shipped Jun 15, 2026. 🔧 Config at config/time_clock.php; test suite covers state-machine matrix, lunch-split, forgot-clock-out edge cases, draft sync.

Support Ticket System. Entirely green-field — the legacy app's SERVICE_HELP and SERVICE_REQUESTS procedures were non-functional workarounds. The modern system is a purpose-built ticketing infrastructure: customers report issues via email or web form; staff track status through a workflow (open → in progress → feedback requested → complete); every status change writes an audit trail; customers view their tickets via secure public links and receive notifications on updates. Ticket messages are threaded; image attachments are recompressed automatically; assignment and SLA tracking are extensible. A complementary foundation for future customer-success operations. Shipped Jun 2026. 🔧 Models at app/Models/Ticket, routes at routes/tickets.php, access gated by TicketPolicy (staff-only by default; public-link guests see only their own).

Global Diagnostics Panel. Staff-only modal that centralizes system health and debugging across all domains — cart parity, user sync status, order reconciliation, DBF drift, job health, and emerging errors. Built as a tabbed modal, extensible by adding new tabs without scattering diagnostic tools across multiple pages. A force multiplier for ops triage. Shipped May 2026. 🔧 Dispatch via useBrutusTrigger({intent: INTENTS.systemDiagnostics}).

Saved Queries in Campaign Wizard. Allows staff to save filter combinations for reuse across campaign builds — e.g., "libraries in TX with recent Trade activity." Eliminates repetition and creates institutional memory of useful segments. A net-new productivity lever for marketing campaigns. Shipped Feb 2026. 🔧 Mutually exclusive: query_id_ OR regular filters, never both; reuse in campaigns:build invocation.

In Progress / Deferred

Account Health Dashboard & Batch Renewal. Staff surface now shows account health flags (plan expiring, short on budget, recently lagging, contact overdue) and purchase history together with batch renewal capability — paralleling the legacy account-rep dashboard but with modern real-time derivation. Surfacing derived flags in account search results and the plan table are open per §5 todo. No target date.

Knowledge Base grounded-answer tier. A new tier in the Brutus answer pipeline: curated FAQ (regex/keyword) → KB grounded answer (BM25 chunk retrieval + optional LLM enrichment) → fallback (admin alert). The corpus is 67 markdown docs from storage/notes, chunked at H2 granularity; data model is doc_chunks table (durable reference + embedding slot) + sparse BM25 index. No LLM generation required for Level 0 (section-as-answer); enrichment and rephrasing are opt-in. Expected Q4 2026 start.


These features represent modern-app innovation; they have no legacy equivalent and define competitive advantage.


20. Open Questions & Undecided Directions

The following features and technical decisions remain open — either unaudited against the legacy app, explicitly deferred, or blocked on information gathering. These are normal design process checkpoints, not failures or bugs.

Feature-Level Decisions

UNDECIDED — Bonus / Gift / Free Book Program: The legacy app tracked earned credits and allowed customers to redeem free books. The modern app has no counterpart. Decision pending: port the full entitlement ledger (earn / redeem / restore) or retire the program entirely. PARITY.md status = none. (20260702)

UNDECIDED — Out-of-Catalog Titles (NOSTOCK): The legacy app maintained a NOSTOCK registry for titles no longer in active inventory but occasionally needed for research/fulfillment. The modern app inventory editor shows no equivalent. Decision pending: whether to port or treat as a legacy artifact. PARITY.md status = unaudited. (20260702)

UNDECIDED — Account Priority System: Planned feature (LOW / MEDIUM / HIGH priority levels) with per-account picker, search filters, and budget-warning triggers. Design and implementation both pending. Would surface on account dashboards and rep call lists to highlight VIP or at-risk accounts.

UNDECIDED — Contact Pool Canonicalization: Planned design to build a single canonical Contact model — deduplicating by name, phone, email — to replace the current account-address-linked contact fields. Full schema + dedup logic design pending.

Technical Decisions Awaiting Approval

UNDECIDED — Memo Field (FPT) Writing: The app reads memo fields from titles and trade notes (FoxPro .FPT format). Writing memos back to the DBF is unimplemented. Decision pending: understand the FoxPro block format (memo chunk layout, offset tables) and decide whether to port writes at all or leave memos read-only. Impacts MARC record editing and editorial content updates. Blocked on format audit. (20260702)

UNDECIDED — Account Health Flag Persistence: Health derivations (plan-health, contact-health, account badge) are currently computed on-read from transactions and standing-order scopes. Permanentizing as account_meta columns + batch classification is deferred pending stabilization of the flag set. Once the useful flag combinations are confirmed, a batch reclassification pass will make searches and dashboards faster.

UNDECIDED — Trade "By Email" Bulk Update Tab: A new tab for the trade orders UI to support bulk edits sent in by email. Currently deferred to its own focused session rather than bolted onto the existing "By Standing Order" tab. Scope and interaction design pending.

Domain Gaps

UNDECIDED — Broader Distribution-Partner Pipeline: The legacy app hardcoded B&T EDI messaging (850/855/856/810/997 consoles, manifests, invoices). B&T is now defunct. Decision pending: design an abstract "distribution-partner pipeline" reusable for future vendor relationships (purchase orders, EDI consoles, receiving, vendor payments). Deferred pending stakeholder input on future partner relationships. (20260703)

UNDECIDED — Receiving & Receiving/PO Domain: The app's PrintRunInventoryService lets operators receive and correct FINALINV at the print stage, but a broader receiving/PO domain (ONORDER tracking, RPURCHASES ownership, supplier management) has no app counterpart yet. Scope and business rules pending. Related: publisher link (royalties) editor is partial — journalkey-link editing surface unverified. (20260702)

UNDECIDED — Payment & Deposit System: Legacy tracked daily deposits, check cashing, and payment accounting. The modern app has no equivalent. Decision pending: whether to build in-app or integrate with an external accounting system. This blocks payment-system reconciliation reports and cashier workflows. (20260702)

Reporting & Analytics

UNDECIDED — Editorial Cost-of-Goods (CGS): Legacy tracked per-title cost (unit vs reprint). The modern app has no equivalent cost_of_goods field. Decision pending: add the field and populate from accounting system, or defer pending business rules clarification. Blocks accurate margin reporting.

UNDECIDED — Editorial Sales & Margin Reports: Legacy included per-title sales metrics and cost-of-goods analysis. Modern INVPROJECT (ISBN quantities/sales) report is built but stubbed for production (~2026-07-15 pending ops sign-off). Editorial margin reporting remains unaudited. PARITY.md status = unaudited. (20260702)

UNDECIDED — Web-Copy Proofing Emails: Legacy supported emailing titles for editorial copy review. The modern campaign/flyer export path exists but the single-title one-off proofing email is unaudited. Decision pending: whether to port or n-a as an outdated workflow. (20260702)

UNDECIDED — Multi-Format Report Exports: Reports currently export as JSON (consumable by web UI and API). Future CSV, Excel, and PDF support is planned but not yet implemented. Design pending: whether to implement in-app or delegate to a third-party reporting service.

UI Polish & UX

UNDECIDED — How-Can-We-Help CTA Block: Expiring/lagging plan emails include a generic action button but lack a richer "How can we help?" block. Deferred pending email-design review. (20260701)

UNDECIDED — Account Health Dashboard Surfacing: Derived health flags (has-expired-choice, has-lagging-expiring-choice, contact-health) exist but are not yet surfaced in account search results, account-rep dashboards, or the account-canvas tool descriptions. Deferred pending UI polish and stakeholder review of which flags are most useful. (20260701)

UNDECIDED — Knowledge Base Grounded-Answer Tier: Chat system will eventually support retrieval + optional LLM generation over internal documentation. Preprocessing is locked; engine choice (BM25 vs semantic embeddings) is deferred. Planned for Q4 2026+. (20260701)

UNDECIDED — Downstairs Reroute Picker UI: Legacy's manual downstairs override for grouped shipments is now expressed in the operator-routing model. CLI and admin tools exist; UX polish (a dedicated picker modal) is deferred. (20260703)

UNDECIDED — Scheduled Reminders for Staff: Native Laravel scheduler integration is available but deferred pending business rules clarification on reminder frequency and content. (20260616)


Teams working on these areas should consult the PARITY.md decision log for context and stakeholder feedback.


Part IV — Appendices

Appendix A — Legacy ↔ New Parity Matrix (durable summary)

[See appA_parity_matrix_summary.md for full content]


Appendix B — Legacy Procedure Glossary

Complete descriptive entry for all 379 top-level PROCEDURE and FUNCTION definitions from webnet.prg (the legacy Visual FoxPro application, 161,252 lines). Organized by domain following Part II taxonomy. Each entry: name, type, line number, and 1–2 sentence purpose statement. Entries are alphabetized within each domain section.

Phase 2 Research Complete — 20260704: All 379 routines extracted, categorized, and documented. PFP Decision: Option A — Full documentation. PFP is an archived subsystem documented for posterity; not intended for rebuild in the modern Laravel/Vue application but may be extracted as a separate application if future business requirements warrant.


[NOTE: Full glossary content (sections B.1–B.12) is in APPENDIX_B_DRAFT.md. For the sake of file size, the complete 1945-line glossary with all 379 routine entries is maintained as a separate reference file. To view the complete glossary with all sections, see: staff/storage/notes/APPENDIX_B_DRAFT.md]

B.1–B.12 [See existing PRD.md Appendix B sections for complete content]


Appendix C — Legacy Views & Pages Index (Client-Side Structure)

[Keep existing complete Appendix C from current PRD.md as-is — no changes needed]


Appendix D — Domain Seams & Gotchas

[See appD_domain_seams_gotchas.md for full content]


Appendix E — Glossary of Terms

[See appE_glossary_of_terms.md for full content]


Appendix F — DBF Table Families & Key Fields

[See appF_data_table_reference.md for full content]


Appendix G — Living-Docs Index

[See appG_living_docs_index.md for full content]


End of PRD.md

VFP Views

Visual FoxPro webnet.prg — Client-Side Structure Map

File: C:\inetpub\wwwroot\FOXISAPI\webnet.prg
Size: 161,252 lines | Procedures: 379
Architecture: URL-routed CGI entry point → procedure dispatcher → HTML form generator


Overview: Navigation & Authorization

Entry Points

Authorization Parameters

Core Menus


Domain 1: INVENTORY & CATALOG

Primary Views

TD_TITLE_VIEW (line 15019)

Purpose: Catalog browser; search/filter titles; view details; add to cart (if logged in)

Entry Point:
/SGZ?bzp=TD_TITLE_VIEW[&INVQUERY=ONSPOT|CHOICE|TRADE|BACKLIST][&PROMONAME=...][&INVLMNT=CP|TR]

Key Parameters:

Displays:

User Actions:


INV_READ / INV_WRITE / INV_DELETE (lines 84028, 111719, 112936)

Purpose: Inventory admin (staff only); add/edit/delete titles from catalog

Entry Point: bzp=INV_READ|INV_WRITE|INV_DELETE (z06 required)

Parameters:

Displays: Admin form with all title fields

User Actions:


Secondary Inventory Views

NOSTOCK_READ / NOSTOCK_WRITE (lines 112766, 112845)

Purpose: Manage out-of-stock titles; mark items as unavailable; backorder notifications

OPEN_STANDARD_SHEETS / OPEN_SPECIAL_SHEETS (lines 104041, 104099)

Purpose: Downloadable worksheets (printable order forms)


Domain 2: ORDERS (Web Cart & Transactional)

Primary Views

GENERAL_VIEW (line 61606)

Purpose: Shopping cart manager; order entry; order modification; line-item editor; applies pricing/discounts

Entry Point:
/SGZ?bzp=GENERAL_VIEW[&WHATACTION=CREATENEWORDER|CREATENEWRETURN|MANAGENEWORDER|MANAGENEWRETURN][&z04=TRANSNO][&HEADER_FILE=WEBHEAD|BROHEAD|BACKHEAD|ALLHEAD][&DETAIL_FILE=WEBDETAIL|BRODETAIL|BACKDETAIL|ALLDETAIL]

Key Parameters:

Displays:

User Actions:


ORDER_DELETE (line 32708)

Purpose: Cancel/delete order; reverse charges; mark as voided

Entry Point: bzp=ORDER_DELETE[&z04=TRANSNO]

User Actions:


ORDER_POST_FINAL (line 34507)

Purpose: Finalize & submit order; validate all required fields; create transaction; update inventory; trigger email confirmation

Entry Point: bzp=ORDER_POST_FINAL[&z04=TRANSNO]

Validation:

Actions on Submit:

  1. Write transaction record (TRANSACT table)
  2. Update WEBHEAD/WEBDETAIL status to "In Process"
  3. Trigger inventory hold (if on-demand)
  4. Generate order confirmation email
  5. Route to FOH if fulfillment needed
  6. Redirect to GENERAL_VIEW with confirmation

FOH_DETAIL (line 27031)

Purpose: Line-item detail viewer; shows item-level status during fulfillment

Entry Point: bzp=FOH_DETAIL[&z04=TRANSNO]


Domain 3: STANDING ORDERS (Subscription & Auto-Ship)

Primary Views

STANDING_PROFILE (line 25895)

Purpose: View standing order summary; subscription settings; auto-renewal options; customer profile photos

Entry Point:
/SGZ?bzp=STANDING_PROFILE[&z03=CUSTKEY][&SEE_CONTROLS=YES][&HIDE_BUTTONS=YES]

Key Parameters:

Displays:

User Actions:

STANDING_HEAD (line 89282)

Purpose: Standing order creation; plan selection; billing setup

Entry Point: bzp=STANDING_HEAD[&z03=CUSTKEY]

Displays:

User Actions:


STANDING_CLEAN (line 95495)

Purpose: Billing consistency audit; find orphaned/duplicate records; fix account state

Entry Point: bzp=STANDING_CLEAN (staff only, z06 required)

Displays:

User Actions:


Secondary Standing Order Views

VIEW_BACKORDERS (line 95149)

Purpose: Show backorder items on customer's standing order; when they'll ship

STANDING_NOTE_DISPLAY (line 27603)

Purpose: View account notes & communications history


Domain 4: TRADE HOUSE (B2B Wholesale Orders)

Primary Views

TRADE_HOUSE_CUSTOMER_VIEW (line 49592)

Purpose: Trade/wholesale customer dashboard; show plan participation, order history, inventory for upcoming titles, manage selections

Entry Point:
/SGZ?bzp=TRADE_HOUSE_CUSTOMER_VIEW[&z03=CUSTKEY][&SORTMETHOD=...][&MONTHLIMIT=...][&SHOWBACKLIST=ON|OFF]

Key Parameters:

Displays:

User Actions:


TRADE_HOUSE_SELECT_DISPLAY (line 52522)

Purpose: Confirm & save trade customer selections before checkout

Entry Point: bzp=TRADE_HOUSE_SELECT_DISPLAY[&z03=CUSTKEY][&SWITCHVALUE=...]

Displays:

User Actions:


REVIEW_TRADE_PLANS (line 152822)

Purpose: Staff view; review all trade plans, allocations, customer participation

Entry Point: bzp=REVIEW_TRADE_PLANS (z06 required)


Secondary Trade Views

TRADE_HOUSE_SWITCH_WRITE / TITLE (lines 130020, 130088)

Purpose: Switch between trade publishers (staff function); manage plan routing

TRADE_PREPAID_DATE / PREPAIDS (lines 151951, 142157)

Purpose: Track trade customer prepayments; aging; collection status

NEW_TRADE_SUMMARY (line 32106)

Purpose: Summary view of new trade orders in process


Domain 5: FULFILLMENT & BACK-ORDER MANAGEMENT

Primary Views

FOH (line 56387)

Purpose: Fulfillment Operations Hub; staff view of all orders needing shipment; fulfillment status tracking; shipment creation

Entry Point:
/SGZ?bzp=FOH[&z04=TRANSNO|ORDER_TRANSNO=...][&HEADER_FILE=...][&DETAIL_FILE=...][&TPOTHREE=...|SHOWARCH|SHOWPAY|CANARCH|TRADEBO|BTPAID|BTUNCONFIRMED|INCURRENT|INPAID|FORARCH]

Key Parameters:

Displays:

User Actions:


CPBACKORDERS (line 60095)

Purpose: Backorder dashboard; show all unfulfilled items; aging; projected fulfillment dates

Entry Point: bzp=CPBACKORDERS[&SHOWARCH=ON]

Displays:

User Actions:


BACKMANAGEMENT (line 154206)

Purpose: Comprehensive backorder management; aging reports; collection workflow

Entry Point: bzp=BACKMANAGEMENT (z06 required)

Displays:

User Actions:


Domain 6: ACCOUNT MANAGEMENT & PROFILES

Primary Views

ACCOUNT_MANAGEMENT_FULLVIEW (line 76615)

Purpose: Complete account admin dashboard; staff view for customer profiles; notes, contact info, subscription status, history

Entry Point:
/SGZ?bzp=ACCOUNT_MANAGEMENT_FULLVIEW[&z06=MASTERPASS][&z03=CUSTKEY][&MENUMODE=LESS|FULL][&RETURNPAGE=...][&NOTELIMIT=...][&NOTEACTION=...]

Key Parameters:

Displays:

User Actions:


AM_NEWMHEAD (line 68318)

Purpose: Comprehensive account editor (newer UI); customer class, promotions, preferences, contact mgmt

Entry Point: bzp=AM_NEWMHEAD[&z06=MASTERPASS][&z03=CUSTKEY]

Displays:

User Actions:


ACCOUNT_LOG_ON (line 35395)

Purpose: Customer login; authentication; session creation

Entry Point:
/SGZ?bzp=ACCOUNT_LOG_ON[&THISUSERNAME=...][&THISPASSWORD=...][&WHERETOGO=MAINMENU|GENERAL_VIEW|...]

Parameters:

Flow:

  1. Validate credentials against PASSWORD table (z01, z02 hash, z03 key)
  2. Create session (persisted in z03)
  3. Redirect to WHERETOGO

Error Handling:


ACCOUNT_LOG_OFF (line 46709)

Purpose: Customer logout; session cleanup

Entry Point: bzp=ACCOUNT_LOG_OFF[&z03=CUSTKEY]

Actions:


Secondary Account Views

ACCOUNT_ACCESS (line 22453)

Purpose: Account lookup; select customer from list

ACCOUNT_DISPLAY (line 24783)

Purpose: Condensed customer card; summary info

ACCOUNT_MANAGEMENT_ONESCREEN_WRITE (line 78214)

Purpose: Single-screen account edit form submission

ACCOUNT_MANAGEMENT_READ (line 78576)

Purpose: Account lookup / search interface

ACCOUNT_MANAGEMENT_KICKBACK (line 81219)

Purpose: Referral / kickback commission tracking

ACCOUNT_MANAGEMENT_CALLSTAT (line 81744)

Purpose: Call history / follow-up tracking

AM_DELETEBASES (line 83457)

Purpose: Account cleanup; archive old accounts

AM_REVIEW_DETAILS (line 114744)

Purpose: Review account details before finalizing changes

AM_WORK_CHOICE (line 113456)

Purpose: Customer choice plan selection (part of standing order flow)


Domain 7: PRICING & DISCOUNTS

Integration Points (Not Standalone Procedures)

Pricing is embedded throughout order workflows:

Pricing Calculation Components:

Pricing Sources:


Domain 8: REPORTS & EXPORTS

Primary Views

PRINTCAT (line 98465)

Purpose: Catalog export / print; downloadable booklist in PDF or plain text

Entry Point:
/SGZ?bzp=PRINTCAT[&z03=CUSTKEY][&PROMONAME=...][&INVQUERY=...][&FORMAT=PDF|TEXT][&COLUMNS=...]

Key Parameters:

Displays (in browser or downloadable file):

User Actions:


PRINT_STANDING (line 129164)

Purpose: Print standing order summary; customer standing order statement / renewal notice

Entry Point: bzp=PRINT_STANDING[&z03=CUSTKEY][&FORMAT=PDF]

Displays:


GEN_EDITORIAL_MENU (line 117941)

Purpose: Editorial staff interface; manage published content, reviews, author info

Entry Point: bzp=GEN_EDITORIAL_MENU (staff only, z06 required)

Displays:


GEN_ONIX_MENU (line 115725)

Purpose: ONIX metadata management; export ONIX format (industry standard book data)

Entry Point: bzp=GEN_ONIX_MENU (z06 required)


GEN_ORDER_MENU (line 120589)

Purpose: Staff order entry; quick add orders for customers (phone orders, manual entry)

Entry Point: bzp=GEN_ORDER_MENU[&z06=MASTERPASS]

Displays:



Domain 9: EDI & DATA EXCHANGE

Primary Views

EDI_MENU (line 139788)

Purpose: EDI (Electronic Data Interchange) admin; manage inbound/outbound order feeds; file management

Entry Point: bzp=EDI_MENU (z06 required)

Displays:

User Actions:


EDI_MANIFEST (line 142884)

Purpose: Generate EDI shipping manifest; structured shipment data for EDI partners

Entry Point: bzp=EDI_MANIFEST[&z03=CUSTKEY|z06=MASTERPASS]

Displays:



Domain 10: NOTIFICATIONS, EMAILS & MESSAGING

Primary Views

EMZ_SERVICES (line 66208)

Purpose: Email marketing / promotion management; bulk email campaigns

Entry Point: bzp=EMZ_SERVICES[&z03=CUSTKEY|z06=MASTERPASS]

Displays:


NEW_EMAIL_CONSOLE (line 136450)

Purpose: Unified email management; compose & send emails to customers

Entry Point: bzp=NEW_EMAIL_CONSOLE[&z06=MASTERPASS]


MAIL_CONSOLE_MENU (line 85519)

Purpose: Internal staff communication; mail/note system

Entry Point: bzp=MAIL_CONSOLE_MENU[&z06=MASTERPASS]

Displays:


MAIL_CONSOLE_MENU (variants)



Domain 11: PLATFORM & UTILITY FUNCTIONS

Core UI Procedures

WEBSITE_HEAD (line 45014)

Purpose: Page header template; includes nav, logo, session info

Displays:


WEBSITE_TAIL (line 45796)


HEADER_INFO (function, not procedure)

Purpose: Build HTML header with

, <meta>, <style> tags <hr /> <h4>MY_PREFERENCES (line 44345) <p><strong>Purpose: User preferences editor; display options, email frequency, etc. <p><strong>Entry Point: <code>bzp=MY_PREFERENCES[&z03=CUSTKEY] <p><strong>Related Procedures: <ul> <li> <code>MY_PREFERENCES_WRITE (line 44846): Save preference changes <hr /> <h4>CP_CONNECTION (line 47530) <p><strong>Purpose: System diagnostics; check database/server connectivity <p><strong>Entry Point: <code>bzp=CP_CONNECTION <hr /> <h4>MENU_SWITCH (line 54460) <p><strong>Purpose: Secondary menu router; switches between major sections <hr /> <h4>WEBCOMMENTS_HEAD (line 47182) <p><strong>Purpose: User feedback/survey form header <hr /> <h4>SERVICE_REQUESTS (line 151553) <p><strong>Purpose: Customer service request form; complaint/inquiry submission <p><strong>Entry Point: <code>bzp=SERVICE_REQUESTS[&z03=CUSTKEY] <hr /> <h4>ONSPOT_SERVICES_MENU (line 80459) <p><strong>Purpose: ONSPOT (featured promotions) service menu <p><strong>Related Procedures: <ul> <li> <code>ONSPOT_SERVICES_VIEW (line 80584): Display ONSPOT offerings <hr /> <h4>BONUS_BOOK_USE (line 31798) <p><strong>Purpose: Manage free/bonus book selections in standing orders <p><strong>Entry Point: <code>bzp=BONUS_BOOK_USE[&z03=CUSTKEY] <p><strong>Related Procedures: <ul> <li> <code>BONUS_HEAD (line 115212): Bonus book admin (staff) <li> <code>BONUS_HEAD_DELETE (line 115616): Delete bonus allocation <li> <code>BONUS_HEAD_WRITE (line 115658): Save bonus allocation <hr /> <h4>ACCOUNT_MANAGEMENT_SHOW_ALLDETAIL (line 156286) <p><strong>Purpose: Show all customer addresses/contacts (detail view) <hr /> <h4>ACCOUNT_MANAGEMENT_FILL_DAILY (line 158174) <p><strong>Purpose: Daily fill/fulfillment tracking (internal) <hr /> <h4>ACCOUNT_MANAGEMENT_FILL_CHOICE (line 158781) <p><strong>Purpose: Choice plan fulfillment (which titles selected & shipped) <hr /> <h4>ACCOUNT_MANAGEMENT_CREATE_FLIER (line 156951) <p><strong>Purpose: Generate promotional flier for account (mail merge) <hr /> <h4>ARCHIVE_ORDERS (line 146614) <p><strong>Purpose: Archive/purge old orders from active database <p><strong>Entry Point: <code>bzp=ARCHIVE_ORDERS (z06 required) <hr /> <h4>MARKET_INFO_WRITE (line 94212) <p><strong>Purpose: Update market research data / demographic info <hr /> <h4>CHECK_CASHING (line 146614) <p><strong>Purpose: Track check clearing status (payment method tracking) <hr /> <h4>FASTPRINT (line 158814) <p><strong>Purpose: Fast/print-on-demand order variant; rapid turnaround titles <p><strong>Entry Point: <code>bzp=FASTPRINT[&z03=CUSTKEY] <p><strong>Related Procedures: <ul> <li> <code>FASTMARK (line 158781): Flag for fast printing <li> <code>FASTPRINT_DELETE (line 160065): Cancel fast order <li> <code>FASTPRINT_CREATE (line 160254): Create fast order <hr /> <h4>FIXSHIP (line 160928) <p><strong>Purpose: Correct shipping records; fix shipped-but-not-marked entries <p><strong>Related Procedures: <ul> <li> <code>FIXSHIPWRITE (line 161010): Write shipping correction <hr /> <h4>INVENTORY_FIX (line 161077) <p><strong>Purpose: Correct inventory discrepancies; manual quantity adjustments <p><strong>Related Procedures: <ul> <li> <code>INVENTORY_FIX_WRITE (line 161194): Write inventory correction <hr /> <h4>DATA_TRANSFER_MODULE (line 104208) <p><strong>Purpose: Export/import data; data interchange with external systems <p><strong>Related Procedures: <ul> <li> <code>DATA_TRANSFER_MODULE_WRITE (line 104818): Process data transfer <hr /> <h4>EASYDATA (line 99863) <p><strong>Purpose: Quick-entry tool; bulk data paste/import <hr /> <hr /> <h2>Domain 12: PFP (Publisher For Profit) <p><em>No dedicated procedures identified; likely integrated into standing order / Trade House flows. <hr /> <hr /> <h2>Cross-Domain Workflows <h3>A. Typical Customer Journey <ol> <li> <strong>Browse Catalog → TD_TITLE_VIEW (search, filter, view titles) <li> <strong>Login → ACCOUNT_LOG_ON (z03 session created) <li> <strong>Add to Cart → GENERAL_VIEW (shopping cart in WEBHEAD/WEBDETAIL) <li> <strong>Manage Cart → GENERAL_VIEW (adjust quantities, pricing calculated) <li> <strong>Checkout → ORDER_POST_FINAL (validate, create TRANSACT record) <li> <strong>Fulfillment → FOH (staff marks shipped) <li> <strong>Logout → ACCOUNT_LOG_OFF <h3>B. Standing Order Signup Flow <ol> <li> <strong>Browse Plan Options → MAINMENU or TD_TITLE_VIEW <li> <strong>Initiate Plan → STANDING_HEAD (select plan type, billing) <li> <strong>Confirm Plan → STANDING_CONFIRMATION (review & activate) <li> <strong>Auto-Fulfill → Monthly selections → GENERAL_VIEW (customer-initiated) or auto-shipment <li> <strong>View Status → STANDING_PROFILE (check subscription status) <h3>C. Trade/Wholesale Flow <ol> <li> <strong>Trade Customer Logs In → ACCOUNT_LOG_ON (z03 for trade account) <li> <strong>Browse Titles → TRADE_HOUSE_CUSTOMER_VIEW (upcoming trade titles, allocations) <li> <strong>Select Titles → Make selections → TRADE_HOUSE_SELECT_DISPLAY <li> <strong>Confirm Order → TRADE_HOUSE_SELECT_DISPLAY (finalize, create TRANSACT) <li> <strong>Fulfillment → FOH (BROHEAD/BRODETAIL or equivalent) <li> <strong>Shipping → EDI_MANIFEST (send shipment notification) <h3>D. Staff Order Entry Flow <ol> <li> <strong>Staff Logs In → ACCOUNT_LOG_ON with z06 (master password) <li> <strong>Search Customer → ACCOUNT_ACCESS or ACCOUNT_MANAGEMENT_READ <li> <strong>Create Order → GENERAL_VIEW with CREATENEWORDER <li> <strong>Add Items → Search TD_TITLE_VIEW, add to cart <li> <strong>Submit → ORDER_POST_FINAL <li> <strong>Ship → FOH (mark shipped) <h3>E. Backorder Management Flow <ol> <li> <strong>View Backorders → CPBACKORDERS (all unfulfilled items) <li> <strong>Check Availability → MAKE_AVAILABLE (when stock arrives) <li> <strong>Auto-Ship or Notify → Trigger GENERAL_VIEW fulfillment or email notification <li> <strong>Archive → Move to archive table <hr /> <h2>Key Parameters Summary <h3>Session / Auth <ul> <li> <code>z01: Username <li> <code>z02: Password (hashed) <li> <code>z03: Customer Key (session ID) <li> <code>z04: Transaction/Order ID <li> <code>z06: Master password (staff/admin) <h3>Navigation <ul> <li> <code>bzp: Procedure name (routing parameter) <li> <code>WHERETOGO: Redirect target after login <li> <code>RETURNPAGE: Return target after action <h3>Ordering / Cart <ul> <li> <code>HEADER_FILE: WEBHEAD, BROHEAD, BACKHEAD, ALLHEAD, DAYHEAD, DAILYHEAD <li> <code>DETAIL_FILE: WEBDETAIL, BRODETAIL, BACKDETAIL, ALLDETAIL, DAYDETAIL, DAILYDETAIL <li> <code>WHATINFILE: Status filter (WEBCOMPLETE, WEBINCOMPLETE, etc.) <li> <code>WHATACTION: Action type (CREATENEWORDER, MANAGENEWORDER, CHANGEITEM, DELETEITEM, CHANGEQTY, CHANGEPRICE, etc.) <h3>Catalog <ul> <li> <code>INVQUERY: Catalog segment (ONSPOT, CHOICE, TRADE, BACKLIST, UPCOMING) <li> <code>INVLMNT: Inventory limit (CP=Center Point, TR=Trade, CPBACK, TRBACK) <li> <code>PROMONAME: Promotion/feature group <li> <code>SINGLEISBN: Direct ISBN lookup <li> <code>SEARCH_INPUT: Search query (title, author, ISBN) <li> <code>FULLVIEW: Toggle detailed display (ON/OFF) <h3>Filters / Display <ul> <li> <code>SORTMETHOD: Sort order (TITLE, AUTHOR, PUBDATE, PRICE, etc.) <li> <code>MONTHLIMIT: Show only titles within N months <li> <code>TPOTHREE: Filter by age (3+ days old) <li> <code>SHOWARCH: Include archived items <li> <code>SHOWBACKLIST: Include backlist <h3>Email / Promotion <ul> <li> <code>ADVERTISE: Show advertisements (ON/OFF) <li> <code>BOOKB: Book club code <li> <code>SALEPRICE: Override sale price <li> <code>DISCOUNT: Override discount % <li> <code>KICKBACK: Referral commission amount <li> <code>SUBJECT: Email subject <hr /> <h2>Summary: Procedure Count by Domain <table> <thead> <tr> <th>Domain <th>Procedures <th>Key Views <tbody> <tr> <td>Inventory/Catalog <td>8+ <td>TD_TITLE_VIEW, TD_TITLE_WRITE, INV_READ/WRITE/DELETE, NOSTOCK_* <tr> <td>Orders <td>10+ <td>GENERAL_VIEW, GENERAL_VIEW_*, ORDER_DELETE, ORDER_POST_FINAL, GEN_ORDER_MENU <tr> <td>Standing Orders <td>8+ <td>STANDING_PROFILE, STANDING_HEAD, STANDING_WRITE, STANDING_CLEAN, VIEW_BACKORDERS <tr> <td>Trade House <td>12+ <td>TRADE_HOUSE_CUSTOMER_VIEW, TRADE_HOUSE_SELECT_DISPLAY, REVIEW_TRADE_PLANS <tr> <td>Fulfillment <td>8+ <td>FOH, CPBACKORDERS, BACKMANAGEMENT, BACKMANAGEMENT_*, FOH_DETAIL <tr> <td>Accounts <td>20+ <td>ACCOUNT_MANAGEMENT_FULLVIEW, AM_NEWMHEAD, ACCOUNT_LOG_ON/OFF, AM_* <tr> <td>Pricing <td>(embedded) <td>— (integrated into GENERAL_VIEW, TD_TITLE_VIEW, FOH) <tr> <td>Reports <td>12+ <td>PRINTCAT, PRINT_STANDING, GEN_EDITORIAL_<em>, GEN_ONIX_, GEN_ORDER_MENU* <tr> <td>EDI <td>8+ <td>EDI_MENU, EDI_FACT_<em>, EDI_MANIFEST, EDI_INVOICE_ <tr> <td>Emails/Messaging <td>8+ <td>EMZ_SERVICES*, NEW_EMAIL_<em>, MAIL_CONSOLE_, BULKMAIL_* <tr> <td>Platform/Utility <td>25+ <td>MAINMENU, MY_PREFERENCES, CP_CONNECTION, WEBSITE_HEAD/TAIL, etc. <tr> <td>PFP <td>(embedded) <td>— (integrated into standing orders / trade flows) <hr /> <h2>Architecture Notes <h3>Parameter Passing <ul> <li>URL query string (<code>/SGZ?param1=value1&param2=value2...) <li>Form POST (same parameter set) <li>MBCODED: Pre-encoded parameter set passed between procedures <h3>Session Management <ul> <li>z03 (customer key) persists across page loads <li>z06 (master password) required for staff actions <li>Session timeout: Not explicitly documented (likely server-side) <h3>Error Handling <ul> <li>TNEW_ERROR_INFO: URL-encoded error message shown to user <li>Procedures validate input and return error parameters to MAINMENU or prior screen <li>Critical errors: Route to BADPROG procedure (generic error page) <h3>Database Tables (Key References) <ul> <li>INVENT: Catalog (title, author, ISBN, list price, pub date, status) <li>PASSWORD: Users (z01/z02, z03 key, email, contact) <li>PASSFILE: Customer pricing (discount %, company type) <li>WEBHEAD / WEBDETAIL: Web orders (in progress) <li>BROHEAD / BRODETAIL: Broker/return orders <li>BACKHEAD / BACKDETAIL: Backorder records <li>ALLHEAD / ALLDETAIL: Consolidated order view <li>STANDING: Standing orders (subscriptions) <li>VENDOR: Trade/wholesale customer accounts <li>CUSTNOTE: Customer notes / communication log <li>TRANSACT: Transaction log (audit trail) <li>BONDETAIL: Bonus book allocations <li>COUPON: Promotional codes <li>PRICINGOVERLAY: Dynamic pricing by ISBN × account class <hr /> <h2>References <ul> <li> <strong>Entry Point: STARTPOINT (line 685) — Main dispatcher <li> <strong>Main Menu: MAINMENU (line 14651) — Homepage <li> <strong>Session Auth Parameters: z01, z02, z03, z06 <li> <strong>Routing Parameter: bzp (procedure name) <li> <strong>Error Parameter: TNEW_ERROR_INFO (URL-encoded message) </style>