Technical Documentation
ParkSwift POS is a mobile-first parking ticketing system for Android handheld terminals with built-in thermal printers. This page documents the stack, architecture, and internal conventions for whoever maintains or extends it next.
Overview
An attendant opens the app on an Android PDA, creates a ticket at vehicle entry (plate number optional, vehicle type required), and the receipt prints automatically via the device's built-in print service. At exit, the attendant scans or looks up the ticket, the fee is calculated, and a second receipt prints. Tickets are stored locally on-device first and synced to a central SQLite database, so a terminal keeps working through a dropped connection.
Two hidden, unlinked pages sit alongside the attendant flow: an Executive Dashboard for revenue and activity reporting, and an Admin Maintenance page for correcting or removing bad records. Both are reachable only by URL — see Routes Reference.
Tech Stack
parkswift.db, no separate DB server
.js files, IndexedDB via Dexie-style localStorage queue for offline tickets
alert()/confirm()
Project Structure
Views are grouped by controller, and each hidden/internal page
(Report, Admin, and this
Home/Documentation / Home/Manual pair)
sets Layout = null and owns its full HTML document —
they intentionally never load _Layout.cshtml, so the
attendant nav and activation.js device-activation
prompt never fire on them.
Architecture
The whole app is one ASP.NET Core MVC project — no separate API
layer. Controllers call straight into TicketStore,
which wraps Dapper queries against a single SQLite file. There is
one shared TicketStore instance (registered as a
singleton in Program.cs), so every controller sees
the same data without an extra service layer in between.
Why offline-first
Attendant terminals are handheld Android devices that may lose
signal in a parking structure. offline-tickets.js
writes every ticket to localStorage immediately at
creation — the server round-trip is not on the critical path for
printing a ticket. A background sync (manual "Sync" action or
periodic retry) later pushes queued tickets to
TicketController's sync endpoint, which upserts them
into SQLite.
Identity model
Two identifiers matter more than the human-facing ticket number:
DeviceId— a GUID generated once per device and kept in that device'slocalStorage. Identifies which terminal created a ticket, independent of anything a human names it.ClientTicketId— a GUID generated on-device at ticket creation. This is the real sync identity: stable, globally unique, and what sync upserts key on — so a retried sync request or a second sync after exit updates the same row instead of duplicating it.
TicketNumber (YY-MMdd-####) stays purely
a human-facing display/print value. It is not relied on
for uniqueness — two devices can legitimately print the same
ticket number on the same day without conflict, since each has a
distinct ClientTicketId underneath.
Process Flow
Entry → Exit → Receipt
Offline Sync
Reporting & Admin
Data Model
Tickets table
| Column | Type | Notes |
|---|---|---|
Id | INTEGER PK | Server-side surrogate key |
ClientTicketId | TEXT, UNIQUE | Sync identity — GUID from the originating device |
DeviceId | TEXT | Which terminal created the ticket |
TicketNumber | TEXT | Display/print value only, not unique |
PlateNumber | TEXT, nullable | Optional at entry |
VehicleType | TEXT, default All | All / Ebike / Motorcycle / Car / Truck |
EntryTime | TEXT (ISO 8601) | Required |
ExitTime | TEXT (ISO 8601), nullable | Null = still parked |
Fee | REAL, nullable | Set at exit |
SyncedAt | TEXT (ISO 8601) | Server-side timestamp of last import/update |
Devices table
| Column | Type | Notes |
|---|---|---|
DeviceId | TEXT PK | Matches Tickets.DeviceId |
Name | TEXT | Admin-assigned friendly label, e.g. "Gate 1 - North Entrance" |
UpdatedAt | TEXT (ISO 8601) | Last rename timestamp |
Devices simply shows its
truncated GUID everywhere in the UI until someone names it via
the "Name Device" control on either hidden page.
Routes Reference
| Route | Purpose | Visibility |
|---|---|---|
/Ticket/Entry | Create ticket, print receipt | Attendant nav |
/Ticket/Exit | Scan/lookup, close ticket, print receipt | Attendant nav |
/Ticket/Summary | Daily Report (attendant-facing totals) | Attendant nav |
/system-report-x9k2 | Executive Dashboard — KPIs, chart, device rename | Hidden, unlinked |
/system-admin-t4v8 | Admin Maintenance — edit/delete tickets, bulk delete, delete-by-device | Hidden, unlinked |
/Home/Documentation | This page | Unlinked |
/Home/Manual | User manual | Unlinked |
/system-report-x9k2 nor /system-admin-t4v8
has a password, IP restriction, or auth check — anyone with the
exact URL gets straight in. If either URL ever leaks, swap the
route segment in the relevant controller for a new unguessable
value, or add real authentication.
Deployment (IIS)
- Hosting model:
InProcess— avoids the extra reverse-proxy hop to Kestrel under IIS. - Forwarded headers: wired up in
Program.csso IIS/ARR's forwarded scheme/host are respected — required forUseHttpsRedirection()and correct URL generation behind an HTTPS binding. - Globalization:
InvariantGlobalizationisfalseso the ₱ symbol formats correctly — confirm ICU data is present on the target Windows Server (present by default on full Server, unlike minimal/Nano containers). - Publish:
dotnet publish -c Release -o ./publish, then point an IIS site (ASP.NET Core Module / Hosting Bundle installed) at the publish folder.web.configis auto-generated. - HTTPS: bind the IIS site to a real or internally-trusted certificate — self-signed dev certs are local-testing only.
- Data persistence:
App_Data/parkswift.dbis created at runtime, not part of the publish output — a full folder wipe/mirror-style deploy (Web Deploy sync,robocopy /MIR) can delete it. PointConnectionStrings:Sqliteoutside the deploy folder inappsettings.Production.jsonfor a safer setup.
Known Limitations
- No auth. No attendant login or shift binding — every action is anonymous, and neither hidden page is access-controlled beyond URL secrecy.
- No CSRF/antiforgery tokens on POST actions yet — add before this runs over an untrusted/public network.
- Vehicle-tiered hourly rate — Ebike/Motorcycle ₱30, Car/Truck/All ₱50; 1~2 hours flat at base rate, then +base rate per additional hour, no day-cap tier. No configurable rate-rule engine yet.
- Print confirmation is neutral. The browser doesn't report whether the attendant actually printed or cancelled the dialog — "sent to printer" is not a guarantee of a physical print.
- Sequential integer ticket IDs are exposed in admin URLs/payloads — not yet the no-exposed-IDs routing convention used elsewhere in the suite.
Roadmap
- Attendant login + shift binding, for per-attendant traceability.
- Antiforgery tokens on all POST actions.
- Real rate-rule engine (flat / hourly / day-cap).
- Confirm the built-in printer's Android print-service driver handles 58mm paper width/margins by default (some OEM services default to A4/Letter).
- Tighten admin/report routing away from sequential integer IDs.