Q-Putaway – Architecture

Stack

Core PHP (7.4–8.x, no framework), PDO/MySQL (InnoDB, utf8mb4), Bootstrap 5.3, jQuery 3.7, Chart.js 4, ZXing (camera decoding fallback), JsBarcode and qrcode-generator (labels). All front-end libraries are vendored under assets/vendor/ (MIT licences), so no CDN is needed and a strict Content-Security-Policy works. No Node.js, Redis, workers or WebSockets are required in production.

Directory layout (no public/ folder)

index.php              Front controller (all requests rewritten here by .htaccess)
.htaccess              Rewrite + blocks private folders
assets/                CSS, JS (app.js core, scanner.js, pages/*.js), vendor libs, icons
app/
  bootstrap.php        Constants, autoloader (PSR-4 QP\ → app/), config, error handler
  version.php          QP_VERSION / QP_SCHEMA_VERSION
  Core/                Config, DB, Router, Request, Response, Session, Csrf, Auth, View, Validator,
                       Audit, Crypto, Settings, Totp, Mailer, Csv, Xlsx, Grid, DocNumber, Idempotency,
                       Migrator, Installer, Markdown, HtmlSanitizer, Logger, ErrorHandler
  Controllers/         One controller per module (thin: input → service → JSON/view)
  Services/            Business logic (InventoryService is the only code that changes stock)
  Masters/definitions.php   Definition-driven master data (forms, grids, validation, CSV)
  Reports/definitions.php   Report catalogue
  routes/*.php         Route tables per module (permissions declared per route)
  navigation.php       Menu
  permissions.php      Permission catalogue (module × action)
views/                 PHP templates (layouts, components, module views, emails)
database/migrations/   Ordered SQL migrations (additive only)
database/seeds/        Default roles
config/                config.php (generated; private)
storage/               logs, uploads, exports, backups, temporary, cache (private)
cron/cron.php          Scheduler entry point
tests/                 unit, integration, security, concurrency, http
tools/                 dev router, icon generator, update signing tools
docs/                  Standalone documentation + help-articles/ (seeded into the Help Center)

Request flow

index.php → security headers → HTTPS redirect → session → (installer redirect if not installed) → Router matches the route → middleware (authentication, CSRF for POST, perm any-of, super, reauth) → controller → service → Response::ok() JSON or View page.

JSON responses: {"ok": true, "data": …, "message": "…"} or {"ok": false, "error": "…", "errors": {field: msg}} with HTTP 401/403/404/405/409/419/422/500. Production responses never include SQL errors or stack traces; errors are logged with a reference code.

Inventory model

  • stock_movements: append-only ledger. Each row moves qty (base units, > 0) from (from_location, from_status) to (to_location, to_status). A NULL source means inbound into the company and a NULL destination means outbound. Reservations are recorded as from = to rows (net zero).
  • stock_balances: one row per (location, product, batch, status) with qty_on_hand and qty_reserved, updated in the same transaction as the ledger row.
  • stock_reservations: batch-level allocations to order lines (ACTIVE → PICKED/RELEASED).
  • Statuses: AVAILABLE, QUARANTINE, DAMAGED, PICKED (staging, awaiting dispatch) and IN_TRANSIT (destination warehouse's TRANSIT location). Expired is derived from the batch expiry date.
  • Integrity: InnoDB transactions; SELECT … FOR UPDATE on source balances, orders, order lines, reservations, tasks and document headers; idempotency keys (X-Idempotency-Key) on every stock-changing endpoint; negative stock refused unless the allow_negative_stock setting is on (and even then only for counts and adjustments).
  • InventoryService::reconcile() recomputes balances and reservations from the ledger. The tests and the Reconcile page use it.

See docs/inventory-and-fefo.md for states and FEFO rules.

Security architecture

See docs/security.md. In summary: prepared statements everywhere; output escaping (e() / QP.esc); CSRF synchronizer tokens; server-side permission + warehouse scope on every route and query; strict CSP (no inline scripts); AES-256-GCM for secrets with keys in config/config.php; password_hash; selector/validator remember-me tokens; TOTP 2FA; audit log.

Front-end conventions

  • QP.api(method, path, data, opts): fetch wrapper that sends the CSRF header and idempotency keys, disables buttons, shows field errors and never reports success before the server confirms.
  • QP.grid: server-side paginated tables (search, filters, sorting, card layout on phones).
  • QP.poll: visibility-aware polling with no overlapping requests.
  • QP.Scanner: camera scanner (native BarcodeDetector → ZXing fallback). data-scan-input fields support keyboard-wedge scanners.
  • Page data is passed in <script type="application/json" id="page-data">. CSP forbids inline executable scripts.

PWA

/manifest.webmanifest and /service-worker.js are generated by PwaController (branding, version hash). Static assets are cached per version (cache-first). Pages and APIs are network-only and are never cached. Updates wait for the user (SKIP_WAITING message from the update banner).

Extending

  • New master: add a definition to app/Masters/definitions.php (plus a migration for the table) to get CRUD, CSV template, import and export.
  • New report: add an entry to app/Reports/definitions.php.
  • Schema change: add database/migrations/NNN_name.sql (additive). The updater and installer apply pending migrations.