Scan Diagnostics — Dataprocessor
Integration middleware that keeps the WooCommerce webshop and Visma.net (ERP) in sync.
About this page
This is the living technical documentation for the Dataprocessor. It is served directly from the production server, so it is always available and always reflects the deployed system — you do not need to store or track this file yourselves. It is written in English to be useful to any future developer who works on the system.
Purpose
Scan Diagnostics sells laboratory and diagnostics products through a WooCommerce webshop, and runs accounting, inventory and order fulfilment in Visma.net. These two systems must agree without anyone re-typing data by hand.
The Dataprocessor is the automated bridge between them. It runs unattended once per day and moves data in two one-directional flows. There is no user interface — it is a headless background service.
Data flows
1 · Products Visma → WooCommerce
Visma is the source of truth for the product catalogue. Each run pulls new or recently edited inventory items from Visma, filters them down to the items that belong in the shop, translates them into WooCommerce products (including placing them in the correct category, e.g. Mikrobiologi, Prøveforbehandling), and pushes them to WooCommerce. Existing items are updated in place rather than duplicated.
2 · Orders WooCommerce → Visma
WooCommerce is the source of truth for sales. Each run pulls newly placed orders from WooCommerce, maps them to Visma sales orders (resolving or creating the customer and contact in Visma), and posts them to Visma for fulfilment and accounting.
Architecture
Both flows share the same shape: a scheduled command invokes a service that orchestrates the run, calling integration clients (the external APIs) and mappers (which translate between the two data models).
| Trigger | Command | Service |
|---|---|---|
| Daily 00:00 | process:products | ProductsService |
| Daily 00:00 | process:orders | OrdersService |
Code layout (app/):
- Console/Commands — thin entry points (
process:products,process:orders). - Services — orchestrators that own the run loop (fetch → filter → map → send → log).
- Integration — HTTP clients for Visma and WooCommerce, behind interfaces so a real or mock implementation can be swapped via the service container.
- Mapping — value objects for each API payload plus the two translators that convert between the Visma and WooCommerce data models.
- Support/SyncWindow — the shared "since when" cutoff (see below).
The local database holds almost no business state — only a failure log (pipeline_errors), a SKU
lookup table (visma_products), and standard framework tables. The systems of record are Visma and WooCommerce.
Scheduling & execution
Both syncs are registered in routes/console.php with Laravel's scheduler and run
once daily at 00:00. They are scheduled as commands, which means each run executes
synchronously — there is no background queue and no worker process to maintain.
Deliberate: run once, no retries
A failed run is not automatically retried. Error handling is explicit throughout the services and clients, so the system fails loudly and visibly rather than silently repeating work. This is an intentional design decision, not an omission.
Products and orders are independent and are allowed to run concurrently.
Sync window
Rather than comparing entire datasets, each run pulls "everything changed in the last 24 hours". The cutoff
is defined once, centrally, in App\Support\SyncWindow and shared by both flows. Because the
scheduler fires at midnight, the cutoff is the previous midnight — so consecutive daily runs
tile perfectly, with no gap and no overlap.
The same instant is formatted differently for each API (ISO-8601 with a T for WooCommerce,
space-separated for Visma), but it is always the same moment in time.
Error handling & monitoring
- Persisted log. Failures — whether a whole-system error or a single bad product/order —
are written to the
pipeline_errorstable with an HTTP status, a name, and a message. - Application log. The same failures are written to the daily log file
(
storage/logs/laravel.log); see the Operations runbook for how to read it on the server. - Transport resilience. Only narrow, transient network calls (such as the Visma auth token request) retry at the request level. The sync run itself does not retry — see above.
Planned — not yet active
Automatic email alerts to stakeholders are intended but not implemented yet. Today,
failures are only logged and stored in pipeline_errors — no email is sent. Until the email
notifier ships, a failed run is visible by reading the log or the pipeline_errors table.
Tech stack
| Language / framework | PHP 8.3+, Laravel 13 |
| Execution | Laravel scheduler (cron-driven), synchronous commands |
| Database | SQLite (failure log + SKU lookup; minimal state) |
| External systems | Visma.net REST API (OAuth2 client-credentials), WooCommerce REST API (basic auth) |
| Notifications | None active yet — failures logged + stored in pipeline_errors (email planned) |
| Hosting | Laravel Forge |
Configuration
All credentials are supplied through environment variables on the server. The following must be present:
# Visma
VISMA_API_BASE_URL=
VISMA_CLIENT_ID=
VISMA_CLIENT_SECRET=
VISMA_TENANT_ID=
# WooCommerce / WordPress
WORDPRESS_API_BASE_URL=
WORDPRESS_CONSUMER_KEY=
WORDPRESS_CONSUMER_SECRET=
Mail credentials are not required yet — email notifications are planned but not
implemented, and MAIL_MAILER is currently log.
Note
A missing credential is a fatal error at boot — the integration clients require these values, so they must all be set in the server environment before deploying.
Deployment
The application is deployed via Laravel Forge. A normal deploy runs composer install
and rebuilds the optimized autoloader. Because the syncs run as scheduled commands, the only background process
Forge needs is the Scheduler:
# Forge → Server → Scheduler · every minute · user: forge
php /home/forge/<site>/current/artisan schedule:run
That single cron entry runs every minute, checks what is due, and fires the daily syncs at 00:00. No queue worker or daemon is required.
Timezone
"Midnight" is interpreted in the application timezone (UTC). If the
business day should follow Norwegian/Danish local time, set the app timezone accordingly — otherwise the 00:00
cutoff is in UTC.
Operations runbook
Useful commands when operating or debugging the system on the server:
# See what is scheduled and when it next runs
php artisan schedule:list
# Run a sync immediately (synchronous, bypasses the scheduler)
php artisan process:products
php artisan process:orders
# Trigger a scheduled task on demand (interactive picker)
php artisan schedule:test
# Inspect recorded failures
php artisan tinker --execute="App\Models\PipelineError::latest('created_at')->take(20)->get()->each(fn(\$e) => print_r(\$e->toArray()));"
Reading the production logs (Laravel Forge)
The sync writes an entry to the application log on every run. To read it on the production server through Forge:
- Open the server production-01.
- Open the site laravel-dataprocessor-jgirogcl.on-forge.com.
- Click the search field in the upper-right corner.
- Type terminal.
- Click Launch terminal.
- Change into the log directory:
cd storage/logs/ - Show the current log:
cat laravel.log - List every log file, including older rotated/compressed ones:
ls
Logs rotate daily, so older days are gzip-compressed (e.g. laravel.log.1.gz).
Read a compressed one without unpacking it with zcat laravel.log.1.gz.
Known limitations & future work
Orders are not yet idempotent
The orders flow has no check for "does this WooCommerce order already exist in Visma?". To avoid creating duplicate sales orders, the sync window is kept at an exact 24 hours with no overlap. The consequence: with no retries and no overlap, if a nightly run fails or is missed, that day's orders are not re-sent automatically.
The recommended next step is to make orders idempotent — e.g. a local table mapping the WooCommerce order to its Visma counterpart, or looking the order up in Visma by a stored reference before creating it. Once orders can be safely re-sent, a small overlap can be reintroduced to make the system resilient to a missed run, the same way the products flow already tolerates re-processing through SKU updates.