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:


Revision #2
Created 4 July 2026 13:05:50 by Stephen Reynolds Jr
Updated 4 July 2026 13:47:28 by Stephen Reynolds Jr