Project Overview
The Integrated Ticketing System V2 is a complete reconstruction of a legacy ticket-sales and accountability application I originally developed in 2019 for museum ticket counters associated with Astan Quds Razavi.
The first version was intentionally small and operationally focused. Its purpose was not to become a large enterprise platform. It existed to solve a concrete problem at the ticket counter: paper tickets were being sold and manually counted, while accounting needed a reliable way to determine who had issued which tickets, how many tickets had been sold, and whether the money collected matched the recorded sales.
The central idea of the original project was simple:
Minimal interaction at the counter; maximum accountability behind the counter.
That principle remains the foundation of V2.
However, the technical implementation has been completely redesigned.
The 2026 rebuild is not a migration of old PHP files into a newer framework and it is not a visual redesign of the original application. The entire system was re-engineered around explicit domain rules, financial traceability, operator accountability, controlled printing, reconciliation, auditability, and long-term maintainability.
Why the Original System Needed to Be Rebuilt
The 2019 version successfully solved the immediate operational problem, but it reflected the constraints and development style of its time.
The original database was very small and consisted mainly of users, ticket prices, and ticket sales. Sales stored the operator name, quantity, price, and a text-based Jalali timestamp.
The legacy implementation also contained several architectural and security limitations that are common in small early-stage internal applications:
- direct PHP/MySQL queries;
- MyISAM tables without relational foreign keys;
- hard-coded database credentials;
- mutation endpoints based on GET requests;
- no CSRF protection;
- weak authentication logic;
- inconsistent password handling;
- no robust session guard;
- no transaction-safe insert flow;
- no idempotency protection;
- no structured shift lifecycle;
- no explicit print lifecycle;
- no reprint audit model;
- no reconciliation workflow;
- no append-only audit trail;
- no proper separation between operator UI and administrative functions;
- no strong financial snapshot strategy.
It was important, however, not to confuse technical weaknesses in the old code with the physical operating environment.
The ticket workstations were controlled internal devices in restricted ticket booths. Public visitors did not have access to the workstation, staff access was physically controlled, and cashier operations were monitored in the real environment.
For that reason, the V2 redesign did not attempt to replace operational controls with unnecessary complexity. Instead, it preserved the original operational simplicity while rebuilding the software controls correctly.
Architecture
Modular Monolith
V2 uses a Modular Monolith architecture.
A microservice architecture would have added operational complexity without solving a real problem for this project. The ticketing domain has strongly related transactional workflows — sales, shifts, printing, reconciliation, and auditing — that benefit from strong consistency and a shared transactional database boundary.
The modular monolith therefore provides:
- clear domain boundaries;
- transactional consistency;
- simple deployment;
- easier maintenance;
- easier testing;
- no distributed transaction complexity;
- no unnecessary messaging infrastructure;
- a clean path for future extraction if a real scaling requirement ever appears.
The project is currently implemented with:
- PHP 8.2
- Laravel 12
- server-rendered Blade interfaces;
- relational database design;
- SQLite for the local development environment;
- a production-ready database configuration path for MySQL/MariaDB.
Core Domain Model
The V2 domain is centered around the following entities:
- User
- Location
- Booth
- Device
- Tariff
- Shift
- Sale
- PrintJob
- Reconciliation
- AuditEvent
The system was deliberately designed so that every financial transaction can be traced back to the full operating context.
Every sale belongs to:
- exactly one operator;
- exactly one shift;
- exactly one booth;
- exactly one device;
- exactly one tariff.
This gives the system an explicit accountability chain rather than relying on loosely stored names or client-submitted values.
Authentication and Roles
The application uses username/password authentication with active-user enforcement.
Four primary roles are supported:
- Operator
- Supervisor
- Accountant
- Admin
Each role has a different responsibility.
Operator
The operator works primarily at the ticket terminal.
An operator can:
- log in;
- open a shift;
- issue tickets;
- print tickets;
- request a reprint when necessary;
- close a shift;
- submit the declared end-of-shift amount.
The operator interface is intentionally minimal.
Supervisor
A supervisor has broader operational authority and can review selected workflows.
Accountant
The accountant primarily uses the Back Office for:
- reports;
- reconciliation review;
- discrepancy analysis;
- sales summaries;
- audit inspection.
Admin
The administrator has access to configuration management for:
- users;
- tariffs;
- locations;
- booths;
- devices;
- system-readiness diagnostics.
Trusted Device Model
The ticket terminal is not treated as a generic public browser.
Each workstation can be registered as a trusted Device, identified by a unique UUID and linked to a specific Booth.
The trusted-device layer verifies that:
- the device exists;
- the device is active;
- its booth is active;
- its location is active.
The operator therefore does not manually choose an arbitrary booth or device for every transaction.
The workstation context is part of the system configuration and becomes part of the accountability chain for sales and shifts.
Shift Management
Ticket sales require an active shift.
The shift lifecycle is:
OPEN → CLOSED → reconciliation process
A shift contains:
- operator;
- booth;
- device;
- opening timestamp;
- closing timestamp;
- status.
The application prevents inconsistent operating states such as:
- one operator having multiple open shifts;
- one device being used by multiple simultaneous open shifts.
Opening the same shift context repeatedly is handled idempotently rather than creating duplicate shifts.
Closing a shift does not silently complete accounting reconciliation. These are intentionally separate business concepts.
Explicit Tariff Model
One of the important changes from the legacy version is the removal of the old “discount checkbox” concept.
In the original operational discussion, a discount control created an accountability problem: an operator could potentially claim that the discount had been selected accidentally.
V2 therefore models pricing as explicit Tariffs.
Examples can include:
- Iranian standard;
- Iranian discounted;
- foreign visitor standard;
- future organization-specific tariff categories.
A discounted ticket is not a boolean flag.
It is a distinct tariff with its own:
- code;
- name;
- visitor type;
- unit price;
- discounted status;
- active status;
- validity period;
- display order.
This makes the business decision explicit and auditable.
Financial Snapshot Design
A key financial rule in V2 is that historical sales must never change when a tariff changes later.
For this reason, every sale stores its own price snapshot:
quantityunit_pricetotal_amount
The current tariff price is read on the server at the moment of sale.
The client cannot decide the authoritative price.
The server calculates:
total amount = quantity × unit price
All monetary values are stored as integer base units rather than floating-point numbers.
This prevents floating-point rounding problems in financial calculations.
Atomic Sales
The normal cashier workflow creates a completed Sale inside a controlled transactional operation.
The application validates:
- active user;
- allowed role;
- active shift;
- matching workstation;
- active tariff;
- tariff validity;
- quantity limits.
The sale records snapshots of:
- user;
- shift;
- booth;
- device;
- tariff;
- unit price;
- total price;
- timestamp.
The system never trusts hidden browser fields for the authoritative operator, booth, device, or price.
Idempotency and Duplicate Protection
A sale contains a unique request_id.
This provides idempotency protection.
If the same browser request is submitted twice — for example because of a double click, network retry, or accidental resubmission — the system can return the original sale instead of creating another financial transaction.
The final data-integrity audit also verifies that duplicate request IDs and duplicate public sale IDs do not exist.
Public Sale Identifier
Each sale also receives a public UUID.
The public identifier is separate from the internal numeric database ID.
This provides a safer external reference for receipt pages and operational communication while retaining efficient internal relational IDs.
Printing Architecture
Printing is deliberately separated from financial sale creation.
This is a major difference from simplistic ticket systems where “print” and “sale” are treated as the same event.
A sale can exist independently from the state of its printer job.
The print lifecycle is:
QUEUED → PRINTED
or:
QUEUED → FAILED
Each PrintJob contains:
- sale;
- device;
- type;
- status;
- attempt number;
- requested user;
- requested timestamp;
- print timestamp;
- failure message;
- optional reason.
Original Print and Reprint
There are two explicit print types:
- ORIGINAL
- REPRINT
The original print uses attempt number 1.
Every reprint receives a later attempt number.
For example:
- attempt 1 — ORIGINAL
- attempt 2 — REPRINT
- attempt 3 — REPRINT
A unique (sale_id, attempt) constraint prevents duplicate attempt numbers.
Most importantly:
A failed print never creates another Sale.
The financial transaction and the printer retry are separate concepts.
Mandatory Reprint Reason
A reprint requires a reason.
This means a ticket cannot simply be printed repeatedly without leaving an audit trail.
The reason becomes part of the PrintJob and is available later for inspection.
This improves accountability without making the original cashier sale workflow unnecessarily complicated.
Printer Abstraction
At the current code-complete stage, the application provides a browser-based print view and explicit print-result handling.
The implementation intentionally does not pretend that a web browser can reliably prove that paper physically exited a printer.
Browser events such as afterprint cannot reliably distinguish:
- a real print;
- a cancelled print dialog;
- a driver failure;
- a hardware problem.
For that reason, hardware-specific silent-print integration remains a deployment concern.
The domain model is already structured so that the browser printing layer can later be replaced by a local printer bridge or hardware adapter without changing the Sale model.
Reconciliation
Shift reconciliation is one of the most important financial-control additions in V2.
After a shift is closed, the system calculates the expected financial values directly from completed sales.
The server calculates:
- transaction count;
- ticket count;
- expected amount.
The operator only enters:
- declared amount;
- optional note.
The operator does not submit the authoritative expected amount or difference.
The system calculates:
difference = declared amount − expected amount
This prevents client-side manipulation of reconciliation totals.
Reconciliation Status
A reconciliation can move through statuses including:
- DRAFT
- SUBMITTED
- ACCEPTED
- REVIEW_REQUIRED
When the difference is zero, the reconciliation can be automatically accepted.
When the difference is non-zero, the record becomes:
REVIEW_REQUIRED
The originating operator cannot approve their own discrepancy.
Only authorized higher-level roles such as:
- Supervisor;
- Accountant;
- Admin
can accept a discrepancy.
A review note is mandatory and the system records:
- reviewer;
- review timestamp;
- explanation.
This preserves the difference instead of hiding or rewriting it.
Audit Trail
V2 includes a structured AuditEvent model.
Important actions create append-only application audit events.
Examples include:
- user login;
- user logout;
- shift opening;
- shift closing;
- sale creation;
- sale completion;
- print queued;
- print success;
- print failure;
- reprint requested;
- reprint success;
- reconciliation submitted;
- reconciliation accepted;
- reconciliation requiring review;
- user configuration;
- tariff configuration;
- location configuration;
- booth configuration;
- device configuration.
Audit records can contain:
- actor;
- shift;
- booth;
- device;
- event type;
- subject type;
- subject ID;
- JSON metadata;
- timestamp.
Administrative password values are never written into audit metadata.
Back Office
V2 separates the cashier workflow from reporting and accounting.
Back Office access does not require the user to be physically bound to the trusted cashier workstation.
Authorized roles can access a dedicated reporting interface.
The Back Office includes:
Dashboard
The dashboard provides operational KPIs such as:
- transaction count;
- ticket count;
- revenue;
- shift count;
- number of reconciliations requiring review.
It also displays:
- recent sales;
- outstanding reconciliation discrepancies.
Sales Reporting
The sales report supports filters for:
- date range;
- operator;
- booth;
- tariff;
- sale/request identifier.
For the current filtered dataset, the system calculates:
- transaction count;
- total ticket quantity;
- total revenue.
The same filtered result can be exported to CSV.
Shift Reporting
Shift reports can be filtered by:
- date;
- status;
- operator;
- booth.
The report exposes:
- operator;
- location;
- booth;
- device;
- opened time;
- closed time;
- shift status;
- reconciliation status.
Reconciliation Reporting
The accounting view supports reconciliation filtering by:
- date;
- status;
- discrepancy-only mode.
The report exposes:
- shift;
- operator;
- booth;
- transaction count;
- ticket count;
- expected amount;
- declared amount;
- difference;
- status;
- reviewer;
- notes.
Audit Reporting
The Audit Trail can be filtered by:
- date;
- event type;
- actor.
Audit metadata remains visible for forensic and operational inspection.
Administrative Configuration
The Admin area manages configuration without physically deleting financially referenced entities.
This is intentional.
Deleting a tariff, device, booth, or user that appears in historical sales would damage the accountability chain.
The administrative model therefore favors:
- activation;
- deactivation;
- controlled updates.
instead of destructive deletion.
User Administration
Administrators can:
- create users;
- change names;
- change usernames;
- assign roles;
- reset passwords;
- activate users;
- deactivate users.
An administrator cannot deactivate their own account or remove their own Admin role through the normal administration workflow.
New administrative passwords must meet a minimum length requirement.
Tariff Administration
Administrators can create and update tariffs.
Changing a tariff affects future sales only.
Existing sales retain their original financial snapshots.
This means a price change tomorrow cannot alter the financial meaning of a ticket sold yesterday.
Location, Booth, and Device Administration
The infrastructure administration area supports:
- Locations;
- Booths;
- Devices.
Each device has a unique UUID.
If an administrator creates a device without manually entering a UUID, the application can generate one automatically.
Locations, booths, and devices can be disabled without destroying historical references.
Security Improvements
V2 replaces the weak authentication and request model of the legacy application with standard framework security controls.
Implemented controls include:
- Laravel password hashing;
- authenticated sessions;
- active-user enforcement;
- CSRF protection;
- authorization policies and middleware;
- role restrictions;
- trusted device verification;
- session regeneration on login;
- logout session invalidation;
- login rate limiting;
- server-side request validation;
- protected financial calculations;
- security response headers.
Login Throttling
Authentication is rate-limited using a username + IP key.
Repeated failed login attempts result in a temporary lockout.
Successful authentication clears the limiter.
This provides basic brute-force protection while remaining appropriate for an internal ticket-counter application.
HTTP Security Headers
Responses include security headers such as:
X-Content-Type-Options: nosniffX-Frame-Options: DENYReferrer-Policy: no-referrer- restricted
Permissions-Policy Content-Security-Policy
The application currently allows the inline CSS/JavaScript required by the server-rendered kiosk interface, while still restricting other browser capabilities.
Session Design
A typical public website should aggressively expire sessions.
A ticket counter is different.
The original operational requirement was that employees should not repeatedly log in during continuous counter operation.
V2 therefore preserves the long-session concept, but implements it through a proper authenticated session model rather than insecure unlimited legacy behavior.
The session is combined with:
- individual user identity;
- trusted workstation identity;
- active shift state;
- controlled logout;
- server-side authorization.
This preserves usability without giving up accountability.
Testing Strategy
Testing was built into the reconstruction phase by phase.
The project includes automated tests covering:
- domain enums;
- schema;
- relationships;
- financial snapshots;
- authentication;
- authorization;
- trusted devices;
- shift lifecycle;
- sale creation;
- sale guards;
- idempotency;
- HTTP sales flow;
- printing;
- print failure;
- reprinting;
- reconciliation;
- discrepancy review;
- Back Office permissions;
- reports;
- CSV export;
- audit filters;
- security headers;
- login throttling;
- administrative configuration;
- system readiness.
The final regression suite completed with:
53 Tests Passed
201 Assertions Passed
0 Test Failures
Final Acceptance Audit
In addition to the Laravel test suite, I created a separate final acceptance audit.
The audit independently validates the application and the development database.
It checks:
- completion of all implementation phases;
- PHP syntax;
- Laravel framework boot;
- migration status;
- route registration;
- config compilation;
- route compilation;
- Blade view compilation;
- complete automated test suite;
- database tables;
- required columns;
- named routes;
- APP_KEY;
- encrypted sessions;
- trusted ticket device;
- seed accounts;
- sale arithmetic;
- duplicate public IDs;
- duplicate idempotency request IDs;
- orphan financial records;
- duplicate open shifts;
- shift lifecycle timestamps;
- duplicate print attempts;
- print references;
- successful-print timestamps;
- failed-print error messages;
- original/reprint attempt rules;
- reprint reasons;
- completed-sale print jobs;
- reconciliation arithmetic;
- discrepancy-review rules;
- reconciliation snapshots;
- audit event identity.
The final result was:
Failures: 0
Warnings: 0
FINAL ACCEPTANCE: PASSED
CODEBASE STATUS: COMPLETE
Development Phases
The reconstruction was divided into eight controlled phases.
Phase 1 — Domain and Database Design
Defined:
- domain entities;
- database relationships;
- financial rules;
- lifecycle states;
- architecture decisions;
- security observations.
Phase 2 — Framework Foundation
Implemented:
- Laravel application foundation;
- enums;
- models;
- migrations;
- factories;
- policies;
- seed data;
- baseline tests.
Phase 3 — Operator, Device, and Shift
Implemented:
- username/password authentication;
- trusted device context;
- shift opening;
- shift closing;
- login/logout auditing;
- minimal kiosk interface.
Phase 4 — Ticket Sales Core
Implemented:
- tariff selection;
- quantity;
- server-calculated totals;
- financial snapshots;
- atomic sales;
- idempotency;
- sale audit events.
Phase 5 — Printing
Implemented:
- original PrintJob;
- print lifecycle;
- failed-print handling;
- reprint workflow;
- required reprint reason;
- print audit events.
Phase 6 — Reconciliation
Implemented:
- server-derived expected totals;
- operator declared amount;
- signed differences;
- automatic exact-match acceptance;
- review-required discrepancies;
- reviewer identity;
- reconciliation audit.
Phase 7 — Back Office and Reports
Implemented:
- management dashboard;
- sales reporting;
- shift reporting;
- reconciliation reporting;
- audit reporting;
- filters;
- CSV export;
- independent Back Office authorization.
Phase 8 — Hardening and Administration
Implemented:
- user administration;
- tariff administration;
- location administration;
- booth administration;
- device administration;
- administrative auditing;
- login throttling;
- security headers;
- system-readiness diagnostics;
- production configuration template.
Final Status
The V2 codebase has completed its planned development scope.
The software is now internally consistent according to:
- automated regression testing;
- framework compilation checks;
- database-integrity validation;
- domain-invariant validation;
- migration validation;
- route validation.
Current project status
CODEBASE: COMPLETE
The remaining work is not core software reconstruction.
The remaining work is environment-specific production acceptance:
- connect and validate the actual thermal ticket printer;
- configure the final production database;
- establish automated database backups;
- deploy behind HTTPS;
- configure production secrets and credentials;
- replace development passwords;
- perform final operator User Acceptance Testing at the real ticket counter.
Engineering Principles Behind the Rebuild
Several principles guided the project.
1. Operational simplicity over feature inflation
The cashier interface must remain simple.
The system should not force an operator to understand accounting or software architecture in order to sell a ticket.
2. Server authority over client authority
The browser may request an action, but it does not decide:
- authoritative price;
- total;
- operator identity;
- device identity;
- booth identity;
- expected reconciliation amount;
- reconciliation difference.
Those values are controlled by the server.
3. Historical financial data is immutable in meaning
A future tariff change must never alter an old sale.
A reprint must never create another sale.
Administrative cleanup must never delete the reference chain of a financial transaction.
4. Accountability should be explicit
A transaction should answer:
- Who performed it?
- During which shift?
- At which booth?
- On which device?
- Which tariff was selected?
- What price was captured?
- When was it sold?
- Was it printed?
- Was it reprinted?
- Why was it reprinted?
- Was the shift reconciled?
- Was there a discrepancy?
- Who reviewed that discrepancy?
5. Physical controls and software controls complement each other
The application does not assume that software alone defines the security environment.
The real ticket counter also has:
- restricted physical access;
- assigned staff;
- controlled workstations;
- organizational supervision.
V2 strengthens the software controls while respecting this operational context.
6. Architecture should match the actual problem
The project does not use microservices, Kafka, Kubernetes, event sourcing, or other infrastructure simply because they are fashionable.
For this domain, a well-structured transactional modular monolith provides the stronger engineering trade-off.
Outcome
This project is a good example of the difference between maintaining old code and actually reconstructing a system.
The functionality may sound straightforward — sell a ticket and record the transaction — but a reliable ticketing system becomes much more interesting when accountability, printing, failed prints, repeated requests, shift boundaries, price history, financial reconciliation, administrative changes, reporting, and auditing are treated as first-class domain concepts.
The result is a system that preserves the simplicity of the original ticket-counter workflow while providing a much stronger technical foundation for traceability, maintainability, security, testing, and future deployment.