Delivered remotely for a business based in Lisbon, Portugal. Names withheld by agreement.
Project Overview
Industry: Regulatory compliance and enforcement — an industry oversight body administering a formal violation reporting, investigation and penalty programme.
Type of solution: A web platform combining unauthenticated public intake and payment journeys, an authenticated administrative case management back office, a cloud-backed document workspace, and machine-to-machine payment reconciliation — with infrastructure defined as code and a continuous deployment pipeline.
Business context: When one party in a regulated industry believes another has breached a safety obligation, a formal report is filed with the oversight body. What follows is quasi-judicial: the body acknowledges receipt, notifies the party named in the report that an investigation has opened, gathers evidence, corresponds with both sides, schedules the case for review, issues a recommendation, and — depending on the outcome — collects a penalty, records completed training, or closes the file. Every step generates a document, and every document has to be findable years later.
General users: Administrators, case-handling staff, limited-access users with a personal document workspace, and members of the public who submit reports or make payments without ever holding an account.
General purpose: To turn a paper-and-email administrative process into a system of record — one where every case has a structured file, every formal notice has a proven delivery attempt, every payment reconciles itself against the case it belongs to, and the whole register can be filtered, saved and exported.
The Business Challenge
The case file was wherever someone put it. Reports arrived by email. Evidence arrived as attachments. Correspondence was typed individually. Each case ended up as a folder someone had organised according to personal habit, which meant reconstructing a case history was an act of archaeology.
Formal notices carry legal weight, and nobody could prove they were sent. A notice that opens an investigation starts a clock. "It should have gone out" is not an acceptable answer when the question is asked months later in a review.
Correspondence was retyped every time. The same handful of letters were produced repeatedly, by hand, with the same details keyed in again — slow, and inconsistent in exactly the places where consistency matters.
The rules changed part-way through the archive. The statutory obligations that can be cited depend on when the incident happened. Any intake form that ignores this collects citations that are wrong on their face.
Money moved outside the system. Submission fees and penalties were collected separately from the case they belonged to, with reconciliation done by hand and different categories of payment needing to reach different accounts. Payment processing costs also came out of the assessed amount, so the organisation received less than the figure on the notice.
Non-experts had to complete an expert form. The intake form covers the parties, the site, the work being performed, the damage, the emergency response and the impact on service. Presented as one page it is intimidating; presented without validation it produces unusable records.
A decade of history existed only as emails and scans. Migrating it was not optional — without it the new system would have no memory, and questions like "has this company been named before?" would be unanswerable. Some of the source documents were image-only, with no extractable text at all.
Our Approach
Make the case file a first-class object. When a case is created, the platform provisions its folder structure automatically — in the database tree and in cloud object storage — so correspondence, evidence, research and generated letters have a home before anyone thinks about filing. Nothing depends on individual discipline.
Treat notice delivery as data, not as a side effect. Every formal notice is a tracked record with attempt count, timestamps, delivered flag and last error. A console command retries anything explicitly recorded as unsent, and the administrative interface exposes the delivery log. The question "was this sent?" has an answer.
Generate correspondence from templates, but leave room for judgement. Letter templates are grouped by category and populated from live case data through a merge-field map. The generated letter is presented for editing before it is committed, then rendered to PDF and filed automatically into that case's correspondence folder. Standardised, not rigid.
Guide the intake, then validate twice. The public form is a ten-step journey, each step validated server-side before the user advances, with the complete record re-validated in full immediately before it is written. The provision set offered adapts to the incident date, so the citation is correct by construction rather than corrected afterwards.
Store the money problem correctly. Payment events are verified for signature, persisted raw and acknowledged immediately; a scheduled process then reconciles them against cases. A payment can never be lost to an application error mid-request, and an operator can reprocess an event from a dashboard rather than editing records by hand. A shared surcharge service derives the amount charged from the amount owed, so the organisation nets the assessed figure.
Migrate the archive properly, and make the migration re-runnable. The import pipeline extracts text natively, falls back to page-level OCR when a document is image-only, detects which of two historical layouts it is reading, extracts by label, normalises dates and yes/no answers, and upserts on the original case number so it can be run again without creating duplicates. A separate validate-only command parses a document and diffs the result against the schema before anything is written.
Define the environment in code. The server, managed database, object storage, web and PHP runtime, caching, intrusion prevention, unattended security patching and monitoring agent are all provisioned by playbook, with secrets held in an encrypted vault. Deployment is a release switch driven from the CI pipeline.
The Solution
Guided public intake. A ten-step report submission covering the parties, the site, the work performed, the damage, the emergency response and the service impact — with per-step validation, evidence upload, date-aware provision selection and a confirmation step returning a case reference. An embeddable variant of the same form can be hosted inside another site.
Case register. Server-side searched and paginated listing of every case, with detail, edit and status management.
Advanced filtering, saved views and export. Filter by submission year, review year, incident year, status, violation classification, reporting organisation, named organisation and repeat-appearance lookups. Filters can be saved per user and reapplied, and results exported.
Case actions with audit trail. Dated follow-up items with notes, attached documents, assigned status and owner, each change recorded with previous and new values, the acting user and a timestamp. A scheduled reminder emails owners when an action has gone stale.
Per-case document workspace. A folder and file tree backed by cloud object storage, provisioned automatically per case, supporting upload, move, rename, soft delete, breadcrumb navigation and ZIP upload with server-side extraction, per-entry validation and structure preservation. The same tree implementation serves personal user workspaces through polymorphic attachment, with sharing to named users and per-user access records.
Generated formal notices. Acknowledgement and investigation notices rendered to PDF from templates, emailed to the correct party, and filed automatically into the case's correspondence folder.
Delivery tracking and retry. Every notice attempt is recorded with its outcome, with a retry command for failures and a delivery log in the administrative interface.
Template-driven letters. Categorised letter templates with merge fields, generated per case, editable before saving, rendered to PDF and filed into the case's letter folder.
Integrated payments. Card payment at submission, plus standalone payment journeys for penalties and for other fees — each with its own merchant configuration, its own validation limits and its own embeddable variant — alongside non-card paths for organisations that pay by other means.
Automatic reconciliation. Signature-verified payment webhooks persisted on receipt and reconciled against cases by a scheduled process, with an operations dashboard for inspection, search by transaction and reprocessing.
Historical archive migration. Console tooling that reconstructs structured case records from emailed forms and scanned PDFs, including OCR for image-only documents, and imports legacy folder structures into the cloud document store attached to the correct case.
Administration. User management across three roles with soft deletion, template and template-category management, authenticated log inspection, and error monitoring with readable production stack traces.
Key Features
Automatic per-case document workspace Every case is provisioned with its standard folder structure in both the database tree and cloud object storage at the moment it is created, so filing is structural rather than a matter of habit.
Notice delivery tracking with retry Formal notices are tracked entities with attempt counts, timestamps, delivered status and error capture, plus a scheduled retry path — so the delivery of a legally significant document can be evidenced rather than assumed.
Merge-field correspondence templates Categorised templates populated from live case data, editable before commit, rendered to PDF and filed automatically into the correct case folder.
Date-aware rule selection at intake The set of statutory provisions offered by the form depends on when the incident occurred, so citations are correct at the point of entry rather than corrected later.
Ten-step guided submission with double validation Each step is validated server-side before the user advances, and the complete record is re-validated in full immediately before persistence.
Store-then-process payment reconciliation Payment events are signature-verified and persisted on receipt, then reconciled by a scheduled process, with an operations dashboard for search and reprocessing.
Processing-fee surcharge handling The amount charged is derived from the amount owed, so the organisation receives the assessed figure rather than the figure net of processing costs.
Archive migration with OCR fallback Historical cases are reconstructed from emailed forms and scanned documents using native text extraction with page-level OCR fallback, layout detection across two historical form generations, and idempotent upserts so the import can be re-run safely.
Saved filters and export over the case register A multi-dimensional filter set — including repeat-appearance lookups — with per-user saved views and export for reporting outside the platform.
Structured audit trail on case actions Previous and new values stored as structured data with actor and timestamp, so the handling of a case can be reviewed after the fact.
Technical Architecture
Public tier. Unauthenticated intake and payment journeys rendered server-side, including embeddable variants designed to be hosted inside another site, with a hosted card element handling card data so payment credentials never reach the application.
Administrative tier. An authenticated back office over the same codebase: case register with server-side tables, case detail and editing, action management, document workspace, correspondence generation, payment registers, delivery log, operations dashboards and user administration.
Service layer. Business logic is extracted into dedicated services — notification orchestration, acknowledgement-notice generation, investigation-notice generation, cloud folder provisioning, archive ingestion, three parallel payment services over a shared surcharge calculation, and a shared date-formatting helper.
Document layer. A polymorphic folder/file tree persisted relationally and mirrored into cloud object storage, attachable to either a user or a case, with scoped queries keeping the two apart, ZIP ingestion with per-entry validation, and time-limited signed URLs for retrieval so files never transit the application tier.
Payment layer. Separate merchant configurations per payment category, each with its own keys, webhook secret and validation limits; a webhook receiver that verifies signatures and persists events; and a scheduled reconciliation process with an operations dashboard.
Batch layer. Console commands for archive import, import validation, path repair, account file merges, notice retries, action reminders and webhook processing — scheduled where recurring, invoked manually where one-off.
Data layer. A managed relational database reached through a cloud authentication proxy, with a migration history reflecting continuous development across several years.
Infrastructure layer. Playbook-provisioned application server, managed database instance and object storage buckets with uniform access control; web server and runtime configured with security headers, banner suppression, intrusion prevention and unattended security patching; monitoring agent installed; secrets held in an encrypted vault; release-based deployment driven from CI.
Flow: Public intake and payment pages → Laravel application → Service layer (notices, documents, payments) → Managed relational database + cloud object storage → Scheduled reconciliation and retry commands → Generated PDFs filed into the case workspace → Tracked email delivery to the parties
Technology Stack
| Category | Technology |
|---|---|
| Backend language | PHP |
| Backend framework | Laravel |
| Database | MySQL (managed instance, accessed through a cloud auth proxy) |
| Templating & UI | Blade with Bootstrap, Sass and Vite |
| Data tables | Server-side DataTables |
| Document generation | DomPDF |
| Object storage | Google Cloud Storage via the official client and Flysystem adapter |
| Payments | Stripe with hosted card elements, multiple merchant configurations, signature-verified webhooks |
| Archive migration | PDF text extraction with Tesseract OCR fallback and image processing |
| Scheduling | Laravel console scheduling with overlap protection and output logging |
| Monitoring | Sentry SDK against a self-hosted, Sentry-compatible error monitor, with hidden source maps uploaded at deploy time |
| Browser security reporting | Report-only Content Security Policy with a reporting endpoint |
| Log inspection | Authenticated in-app log viewer |
| Infrastructure as code | Ansible with a Google Cloud collection; encrypted vault for secrets |
| Cloud services | Compute instance, managed SQL instance, object storage with uniform bucket-level access, cloud operations agent |
| Web tier | Apache with PHP-FPM over a Unix socket, security headers, banner suppression |
| Host hardening | Intrusion prevention, unattended security upgrades, swap configuration |
| CI/CD | CircleCI building and deploying through Capistrano release directories |
| Legacy mobile back end | Laravel with OAuth2 token authentication, Webpack asset build, Vue and Bootstrap |
| Testing | PHPUnit, with code style tooling |
Technical Challenges & Solutions
| Challenge | Our Approach |
|---|---|
| A decade of case history existing only as emailed forms and scanned documents, some image-only | An import pipeline doing native PDF text extraction with page-level OCR fallback, detecting which of two historical layouts it is reading, extracting by label, normalising dates and boolean answers, and upserting on the original case number so the import is idempotent and re-runnable — with a separate validate-only command that diffs parsed fields against the schema before anything is written. |
| Formal notices whose delivery has legal consequences | Notice delivery modelled as its own entity with attempt counts, timestamps, delivered flag and captured errors, a scheduled retry command for anything recorded as unsent, and a delivery log in the administrative interface. |
| Case documents scattered across ad-hoc folders | A polymorphic folder tree persisted relationally and mirrored into cloud object storage, provisioned automatically per case with a standard structure, supporting ZIP ingestion with per-entry validation and retrieval through time-limited signed URLs. |
| The same letters retyped by hand for every case | Categorised templates with a merge-field map populated from live case data, presented for editing before commit, rendered to PDF and filed automatically into the correct case folder. |
| Obligations that changed on a known date | The intake form selects the applicable provision set from the incident date and records which regime applies, so citations are correct at entry rather than corrected downstream. |
| Payments that must never be lost, across categories that settle differently | Signature-verified webhooks persisted raw on receipt and acknowledged immediately, with a scheduled reconciliation process, an operations dashboard for search and reprocessing, and separate merchant configurations and validation limits per payment category. |
| Processing costs eroding assessed amounts | A shared surcharge service that derives the amount charged from the amount owed, applied consistently across every payment journey. |
| A long, expert-level form completed by non-experts | A ten-step guided journey with server-side validation at each step and a full re-validation of the complete record immediately before persistence, plus an embeddable variant for hosting inside another site. |
| Document rendering deadlocking a constrained deployment | All assets embedded directly in the rendered document and remote fetching disabled, so PDF generation never issues a request back to the application that is generating it. |
Security & Reliability
Verified payment events. Every incoming payment webhook is signature-verified before it is trusted, persisted raw, and only then interpreted — so a malformed or forged event is rejected at the boundary and a genuine one survives an application error.
Documents served by short-lived signed URLs. Files are never public objects and never stream through the application tier; retrieval issues a time-limited URL directly against object storage.
Hardened web tier. Strict transport security, frame controls, content-type sniffing protection, a restrictive permissions policy, referrer policy, HttpOnly and Secure cookie flags, and suppression of server identification headers — all defined in the provisioning template rather than configured by hand.
Host hardening as code. Intrusion prevention and unattended security upgrades are part of the playbook, so a rebuilt server is hardened identically.
Secrets in an encrypted vault. Infrastructure secrets are held encrypted in the provisioning repository, and the application's runtime environment file is linked in at deploy time rather than shipped with the release.
Audit trail. Case action changes are recorded with previous and new values, the acting user and a timestamp; notice delivery attempts and payment events are retained as records in their own right.
Soft deletion. Users, folders, files, actions and templates are soft-deleted, so removal is recoverable and history remains intact.
Error monitoring with readable traces. Application and browser errors report to a self-hosted, Sentry-compatible monitor, with source maps uploaded at deploy time and withheld from browsers, plus a report-only content security policy feeding the same service.
Reprocessable operations. Failed notices and unprocessed payment events can be retried from a command or a dashboard, rather than requiring database intervention.
Scalability & Performance
Files never occupy application workers. Documents live in object storage and are delivered by signed URL, so upload volume and file size do not consume request capacity.
Server-side filtering and pagination. The case register and its filter set operate at the database level, keeping result sets bounded as the archive grows — which, with a decade of migrated history, it already has.
Reconciliation off the request path. Payment interpretation runs on a schedule with overlap protection and its own output log, so processing volume never affects the responsiveness of the endpoint receiving events.
Batch work confined to console commands. Archive import, OCR, validation and repair run outside the web tier entirely.
Independent data tier. A managed database instance reached through a cloud auth proxy scales separately from the application server.
Release-based deployment. Shared environment and upload directories with symlinked releases make adding or replacing an application host a mechanical operation rather than a migration.
Assets built and fingerprinted. Front-end assets are bundled at build time with source maps kept out of the browser payload.
Business Outcomes
- Every case has a structured file, provisioned automatically with a consistent folder structure in cloud storage from the moment it is created.
- Notice delivery can be evidenced, with attempts, timestamps, errors and retries recorded against each formal notice.
- Correspondence is standardised without being rigid, generated from templates populated with live case data and editable before it is committed and filed.
- Citations are correct at the point of entry, because the intake form selects the applicable rule set from the incident date.
- Payments reconcile themselves against cases, with verified events, a reprocessing dashboard and assessed amounts received in full.
- A decade of history is searchable, migrated out of emailed forms and scanned documents into structured records — so questions about prior appearances can be answered from the system.
- Case handling is reviewable, with an audit trail of action changes and a full document record retained per case.
- The register can be interrogated and reported on, through multi-dimensional filters, saved per-user views and export.
- The environment is reproducible, defined as code with encrypted secrets and deployed through a release pipeline.
Why it worked
Administrative and regulatory processes are deceptively hard to build for. The workflow looks like a simple sequence of statuses until you notice that each transition produces a document, several of those documents have legal weight, the rules governing them changed part-way through the archive, money moves at two different points for different reasons, and none of it can be lost — including the decade of history that exists only as scanned paper.
Our team builds for that reality. We modelled notice delivery as a tracked entity because "it should have gone out" is not an answer a regulator can give. We provisioned the case file automatically, in the database and in object storage, because filing structure left to individual habit is filing structure that decays. We made payment reconciliation a store-then-process pipeline because a payment lost to a mid-request error is a problem that surfaces weeks later in a reconciliation. And we treated the historical archive as a first-class engineering problem — text extraction, OCR fallback, layout detection, idempotent upserts and a validate-only mode — because a system of record with no memory is not a system of record.
Our teams work across Laravel and modern PHP, document-heavy workflow systems, cloud object storage, payment integration and reconciliation, data migration from unstructured sources, and infrastructure as code — with the judgement to know which parts of an administrative process must be modelled exactly and which can be simplified without anyone noticing.
Final Summary
An industry oversight body ran a formal investigation and enforcement process the way most such bodies have always run one: reports by email, evidence as attachments, letters typed individually, folders organised by whoever created them, and payments collected somewhere else entirely. It worked, in the sense that the work got done. It did not work in the sense that anyone could prove a notice had been sent, find a case file quickly, or answer whether a company had been named before.
Our team built the platform that replaced it. Public intake became a guided, validated journey that selects the applicable rule set from the incident date and can be embedded in another site. Every case is provisioned with its own document workspace in cloud object storage. Formal notices are generated as PDFs, emailed to the correct party, filed automatically into the case folder, and tracked as delivery records with attempts, errors and a retry path. Correspondence comes from categorised merge-field templates that staff can edit before committing. Payments run through separate merchant configurations by category, with verified webhooks persisted on receipt and reconciled by a scheduled process against the case they belong to, and a surcharge calculation that ensures assessed amounts arrive in full.
Behind that sits the part nobody demos: a migration pipeline that reconstructed a decade of case history from emailed forms and scanned documents, using native text extraction with OCR fallback for image-only pages, layout detection across two generations of form, and idempotent upserts so it could be run again safely. And beneath that, an environment defined entirely as code — server, managed database, object storage, hardened web tier, monitoring and encrypted secrets — deployed through a release pipeline. The result is a system of record that remembers, evidences and reconciles, in a process where all three of those things eventually get asked about.