Technical Documentation — How it works, flows & requirements
The Skroutz Invoice Uploader is a WordPress/WooCommerce plugin module that automatically or manually uploads Oxygen (Pelatologio) invoices to the Skroutz Marketplace API after an order is placed through Skroutz.
The module bridges two external APIs:
| API | Purpose | Used for |
|---|---|---|
| Oxygen / Pelatologio API | Greek accounting platform | Retrieve the invoice PDF for a WooCommerce order |
| Skroutz Marketplace API | Greek e-commerce marketplace | Upload the invoice PDF so the buyer can download it |
There are two distinct upload flows depending on how the upload is triggered:
| Flow | Trigger | Method |
|---|---|---|
| A — Auto | Oxygen invoice auto-created on order status change | Added to a database queue, processed by WP-Cron |
| B — Manual | Admin clicks "Upload Invoice to Skroutz" on order edit page | Immediate upload, records result in queue as manually_uploaded |
| Option key | Where to set | Required value |
|---|---|---|
oxygen_skroutz_uploader_enabled | Plugin options | yes — enables the entire module |
skroutz_api_token | Skroutz Uploader → Settings | Bearer token from Skroutz Merchant API |
skroutz_iview_token | Skroutz Uploader → Settings | Token for iView PDF access (Oxygen MyData) |
skroutz_auto_upload | Skroutz Uploader → Settings | 1 — enables auto-queue after invoice creation |
skroutz_max_retries | Skroutz Uploader → Settings | Number of retry attempts (default: 3, range: 1–10) |
skroutz_cleanup_days | Skroutz Uploader → Settings | Days before deleting completed items (default: 365, range: 1–365) |
OxygenApi class and fires oxygen_invoice_created action| Meta key | Set by | Contains |
|---|---|---|
_oxygen_invoice | Oxygen plugin after invoice creation | Invoice data array including id and iview_url |
_skroutz_order_code | Skroutz webhook handler on order creation | Skroutz marketplace order code (e.g. 260818-9607017) |
_skroutz_invoice_uploaded | Set on successful upload (auto or manual) | MySQL datetime of successful upload |
_oxygen_invoice) must exist on the WooCommerce order before the queue worker tries to upload it. If the invoice hasn't been created yet the upload will fail and go to retry.
This flow is triggered when an Oxygen invoice is automatically created because the order's WooCommerce status changed to the configured trigger status (e.g. processing).
/oxygen-skroutz/v1/webhook_skroutz_order_code storedon-holdcreate_invoice() fires on status change hook_oxygen_invoice set on orderdo_action('oxygen_invoice_created', $order_id)wp_skroutz_upload_queue with status pendingoxygen_skroutz_cron_hook fires hourly (or manual trigger)| Hook | File | Purpose |
|---|---|---|
woocommerce_order_status_{status} | class-oxygenorder.php | Triggers create_invoice() when order reaches configured status |
oxygen_invoice_created | class-oxygenorder.php (fires) class-skroutz-uploader.php (listens) | Custom action fired after invoice successfully saved to order meta |
oxygen_skroutz_cron_hook | class-skroutz-uploader.php | WP-Cron event that runs the queue worker |
This flow is triggered when an admin clicks "Upload Invoice to Skroutz" in the order edit page meta box. It runs synchronously — no delay, no cron needed.
_oxygen_invoice meta must exist)_skroutz_order_code meta)OxygenApi::get_invoice_pdf()manually_uploaded_skroutz_invoice_uploaded meta saved on the order with a datetime stampmanually_uploaded, the cron worker (which only picks up pending and retry rows) will never attempt to upload this order again automatically.
URL pattern: admin-post.php?action=skroutz_upload_invoice&order_id={ID}
Function: skroutz_handle_upload_invoice()
The queue is a custom database table wp_skroutz_upload_queue that holds one row per order. Each row tracks both the WooCommerce order ID and the Skroutz order code for easy cross-referencing.
| Status | Meaning | Next action |
|---|---|---|
| pending | Newly added, waiting to be picked up by the worker | Worker picks it up on next run |
| processing | Currently being uploaded (in-flight) | Moves to completed, retry, or failed |
| completed | Successfully uploaded to Skroutz by the cron worker | No further action; can be cleared from dashboard |
| manually_uploaded | Successfully uploaded by an admin via the order edit page | No further action; cron worker skips this row permanently |
| retry | Failed but under the retry limit | Worker picks it up again on next run |
| failed | Failed and exhausted all retry attempts | Requires manual "Reset Failed" action from dashboard |
order_id column has a UNIQUE KEY. An order can only appear once in the queue. If it is already queued, skroutz_queue_enqueue() returns false silently (idempotent).
When an order is enqueued, skroutz_queue_enqueue() immediately resolves and stores the Skroutz order code:
_skroutz_order_code meta from the WC order$order->get_order_number()skroutz_order_code columnThis means the dashboard always shows both IDs side-by-side for easier debugging.
| Function | Purpose |
|---|---|
skroutz_queue_enqueue($order_id) | Add order to queue; resolves and stores Skroutz order code; idempotent |
skroutz_queue_run_worker($batch) | Process up to N pending/retry items |
skroutz_queue_get_stats() | Count items per status for the dashboard stat cards |
skroutz_queue_get_recent($limit) | Fetch the most recent rows for the activity table |
skroutz_queue_clear_completed() | Delete all rows with status completed |
skroutz_queue_clear_all() | Truncate the entire queue table (all statuses) |
skroutz_queue_reset_failed() | Reset all failed rows back to pending (attempts = 0) |
skroutz_queue_update_row($id, $status, $msg, $attempts) | Update a single row's status, message, and attempt count |
The queue is processed automatically by a WP-Cron scheduled event named oxygen_skroutz_cron_hook, which runs hourly.
| Property | Value |
|---|---|
| Event name | oxygen_skroutz_cron_hook |
| Interval | Hourly |
| Batch size | 20 items per run |
| Worker function | skroutz_queue_run_worker(20) |
| Item selection | Status IN ('pending', 'retry'), ordered by created_at ASC (oldest first) |
pending or retryprocessing, read skroutz_order_code from the row, call skroutz_do_upload_invoice()completedretry if under max, or failed if exhaustedpending and retry only. Orders uploaded manually are permanently safe from re-upload.
The worker can also be triggered immediately from the dashboard via "Process Queue Now" (Quick Actions panel), without waiting for the next hourly cron.
Use "Reschedule Cron" from the Quick Actions panel. This unschedules any existing event and registers a new hourly schedule starting immediately.
Both flows (auto queue and manual) ultimately use the same upload steps. The queue worker calls skroutz_do_upload_invoice(int $order_id, string $api_token); the manual handler performs equivalent steps inline.
skroutz_do_upload_invoice()| # | Step | Details |
|---|---|---|
| 1 | Load WC order | Validate order exists via wc_get_order() |
| 2 | Read invoice meta | Get _oxygen_invoice — must contain id and iview_url |
| 3 | Resolve Skroutz order code | Read _skroutz_order_code meta; fall back to WC order number |
| 4 | Build order reference | Combines both IDs: WC#1433 / Skroutz#260818-9607017 — used in all log messages |
| 5 | Call Oxygen API | OxygenApi::get_invoice_pdf(invoice_id) — downloads raw PDF bytes |
| 6 | Validate PDF | Check response is not empty and starts with %PDF- |
| 7 | Write temp file | wp_tempnam() + file_put_contents() — needed for cURL multipart |
| 8 | Upload to Skroutz | cURL multipart POST to https://api.sandbox.skroutz.dev/merchants/ecommerce/orders/{code}/invoices |
| 9 | Clean up | Delete temp file with @unlink() |
| 10 | Parse response | HTTP 2xx → success; otherwise parse JSON errors and return message string |
| 11 | Save timestamp | On success: _skroutz_invoice_uploaded meta set on order |
| Return | Meaning |
|---|---|
true | Upload succeeded |
string | Error message (e.g. HTTP 404: Order not found) |
POST https://api.sandbox.skroutz.dev/merchants/ecommerce/orders/{skroutz_order_code}/invoices
Headers:
Authorization: Bearer {api_token}
Accept: application/vnd.skroutz+json; version=3.0
Body (multipart/form-data):
invoice_file: [PDF binary]
{
"errors": [
{
"code": "order_error",
"messages": ["Order not found"]
}
]
}
The plugin parses this JSON and stores the human-readable messages (e.g. HTTP 404: Order not found) in the queue's message column and in the log file.
| Scenario | Result |
|---|---|
| Upload succeeds (cron) | Status → completed |
| Upload succeeds (manual) | Status → manually_uploaded |
| Upload fails, attempts < max_retries | Status → retry, picked up next cron run |
| Upload fails, attempts ≥ max_retries | Status → failed, no more automatic retries |
When items reach failed status they will not be retried automatically. The cron worker only queries for pending and retry rows.
To re-queue failed items:
attempts = 0Accessible at WP Admin → Skroutz Invoice Uploader. Two tabs: Dashboard and Settings.
Shows live counts from the queue table (7 cards total):
| Card | Color | Description |
|---|---|---|
| Total Items | ■ Blue | All rows in the queue |
| Pending | ■ Orange | Waiting for next cron run |
| Processing | ■ Cyan | Currently being uploaded (in-flight) |
| Completed | ■ Green | Successfully uploaded by cron |
| Manual Upload | ■ Purple | Successfully uploaded by admin manually |
| Failed | ■ Red | Exhausted all retry attempts |
| Retry | ■ Yellow | Failed temporarily, will retry next run |
| Button | Visibility | Action |
|---|---|---|
| Process Queue Now | Always | Runs the worker immediately (batch of 20) |
| Reschedule Cron | Always | Unschedules + re-registers the hourly cron event |
| Test API Connection | Always | Pings the Skroutz API to verify the token works |
| Reset Failed (N) (red) | Only when failed > 0 | Resets all failed rows to pending with attempts = 0 |
| Clear Queue (N) (dark red) | Only when queue has items | Truncates the entire queue table — irreversible |
Shows the 20 most recently updated queue rows with columns:
updated_at timestamp| Section | Fields |
|---|---|
| API Configuration | Skroutz API Token (password), iView Token (password) |
| Upload Settings | Enable Automatic Upload (checkbox), Maximum Retries (1–10) |
| Maintenance | Cleanup After Days (1–365) |
| Actions | Save Settings button, Test API Connection button, Download Log File button |
Serves the most recently modified wc-logs/skroutz-uploader-*.log file as a direct browser download. If no log file exists, an error notice is shown.
On every WooCommerce order edit page, a "Skroutz Invoice Uploader" meta box shows:
_skroutz_invoice_uploaded meta exists)All logging goes through WooCommerce's logger (wc_get_logger()) with source tag skroutz-uploader.
| Location | Value |
|---|---|
| WC Admin path | WooCommerce → Status → Logs → skroutz-uploader |
| Physical path | wp-content/uploads/wc-logs/skroutz-uploader-YYYY-MM-DD-{hash}.log |
| Download | Skroutz Uploader → Settings → Download Log File button |
Since the recent update, all log messages that reference an order use the combined format:
WC#{woocommerce_order_id} / Skroutz#{skroutz_order_code}
Example: WC#1433 / Skroutz#260818-9607017
This makes it easy to search for either ID in the log file and immediately see the corresponding other ID.
| Level | Message pattern | When |
|---|---|---|
| INFO | Cron event fired — launching queue worker | Cron hook triggered |
| INFO | Cron event complete — processed: N, errors: N, total: N | After cron run finishes |
| INFO | Queue worker started — found N item(s) (batch_size=20, max_retries=N) | Worker start |
| DEBUG | Queue worker: nothing to do, exiting | Queue is empty |
| INFO | Queue worker finished — processed: N, errors: N, total: N | Worker end |
| DEBUG | Queue item #N — WC#N / Skroutz#XXX, attempt N/N: starting | Before each upload attempt |
| INFO | Queue item #N — WC#N / Skroutz#XXX: completed on attempt N | Successful upload by cron |
| WARNING | Queue item #N — WC#N / Skroutz#XXX: will retry (attempt N/N) — {error} | Failed but will retry |
| ERROR | Queue item #N — WC#N / Skroutz#XXX: permanently failed after N attempt(s) — {error} | Max retries reached |
| DEBUG | WC#N / Skroutz#XXX: downloading PDF from Oxygen API (invoice_id=N) | Before PDF download |
| DEBUG | WC#N / Skroutz#XXX: PDF downloaded OK (N bytes) | After PDF downloaded |
| DEBUG | WC#N / Skroutz#XXX: uploading to Skroutz | Before Skroutz cURL call |
| DEBUG | WC#N / Skroutz#XXX: Skroutz API responded HTTP NNN | After cURL response |
| INFO | WC#N / Skroutz#XXX: invoice uploaded successfully to Skroutz | Upload OK (cron) |
| INFO | WC#N: invoice uploaded manually to Skroutz (Skroutz#XXX) | Upload OK (manual) |
| ERROR | WC#N / Skroutz#XXX: upload failed — HTTP NNN: message | API error response |
| INFO | Order #N enqueued for Skroutz upload after invoice creation | Auto-enqueue hook |
| WARNING | Order #N could not be enqueued (already in queue or DB error) | Duplicate enqueue attempt |
| ERROR | Queue worker: API token not configured — aborting | Token missing at worker start |
| INFO | Reset N failed queue item(s) back to pending via manual action | Reset Failed button clicked |
| INFO | Entire upload queue cleared via manual action | Clear Queue button clicked |
wp_skroutz_upload_queue| Column | Type | Default | Description |
|---|---|---|---|
id | bigint(20) AUTO_INCREMENT | — | Row identifier (primary key) |
order_id | bigint(20) | — | WooCommerce order ID — one row per order (unique) |
skroutz_order_code | varchar(100) | '' | Skroutz marketplace order code, resolved at enqueue time |
status | varchar(20) | 'pending' | pending / processing / completed / manually_uploaded / retry / failed |
attempts | int(11) | 0 | Number of upload attempts made so far |
message | text | NULL | Last result message (success text or API error) |
created_at | datetime | — | When the row was first inserted |
updated_at | datetime | — | Last status change timestamp |
| Index | Columns | Purpose |
|---|---|---|
| PRIMARY KEY | id | Row identity |
| UNIQUE KEY | order_id | Prevent duplicate queue entries per order |
| KEY | status | Fast filtering by status in worker query |
The table is created/upgraded automatically on every WordPress init request via dbDelta(). It is safe to call repeatedly — it only modifies the schema if it has changed. The skroutz_order_code column was added in August 2026 and will be added to existing installs automatically on the next page load.
| Function | Purpose |
|---|---|
oxygen_add_new_feature_menu() | Register admin menu page for the uploader |
skroutz_page_url($tab) | Build admin page URL with a tab parameter |
skroutz_get_option($key, $default) | Read a skroutz_-prefixed option from the database |
skroutz_auto_enqueue_after_invoice_created($order_id) | Hook: enqueue order after invoice is created |
skroutz_order_meta_box_html($post_or_order) | Render the meta box on the order edit page |
skroutz_handle_process_queue() | Admin-post: manually trigger queue worker |
skroutz_handle_reschedule_cron() | Admin-post: reschedule the hourly cron event |
skroutz_handle_test_api() | Admin-post: test Skroutz API token |
skroutz_handle_clear_completed() | Admin-post: delete all completed queue rows |
skroutz_handle_clear_all_queue() | Admin-post: truncate the entire queue table |
skroutz_handle_reset_failed() | Admin-post: reset failed rows back to pending |
skroutz_handle_download_log() | Admin-post: serve most recent log file as download |
skroutz_handle_upload_invoice() | Admin-post: manual immediate upload from order edit page |
skroutz_handle_save_settings() | Admin-post: save Settings form values |
oxygen_skroutz_uploader_page() | Render the full admin page (tabs, notices, content) |
skroutz_render_dashboard_tab() | Render dashboard tab: stat cards + quick actions + activity table |
skroutz_render_settings_tab() | Render settings tab: form fields + action buttons |
skroutz_uploader_log($level, $message) | Write to WC logger with source skroutz-uploader |
skroutz_queue_table() | Return the full queue table name |
skroutz_queue_maybe_create_table() | Create or upgrade queue table via dbDelta() |
skroutz_queue_enqueue($order_id) | Add order to queue; resolves Skroutz code; idempotent |
skroutz_queue_get_stats() | Return counts per status plus total |
skroutz_queue_get_recent($limit) | Return most recently updated rows (newest first) |
skroutz_queue_clear_all() | Truncate entire queue table |
skroutz_queue_clear_completed() | Delete rows with status = completed |
skroutz_queue_reset_failed() | Reset failed rows to pending (attempts = 0) |
skroutz_queue_update_row($id, $status, $msg, $attempts) | Update a single queue row |
skroutz_do_upload_invoice($order_id, $api_token) | Execute the upload: download PDF, POST to Skroutz, parse response |
skroutz_queue_run_worker($batch_size) | Process a batch of pending/retry items |
| Hook / Action | Handler | Purpose |
|---|---|---|
admin_menu | oxygen_add_new_feature_menu | Register admin menu page |
oxygen_invoice_created | skroutz_auto_enqueue_after_invoice_created | Auto-enqueue after Oxygen invoice creation |
init | skroutz_queue_maybe_create_table | Create/upgrade DB table on every request |
oxygen_skroutz_cron_hook | inline closure | Run queue worker hourly via WP-Cron |
admin_post_skroutz_process_queue | skroutz_handle_process_queue | Process queue manually |
admin_post_skroutz_reschedule_cron | skroutz_handle_reschedule_cron | Reschedule cron event |
admin_post_skroutz_test_api | skroutz_handle_test_api | Test API token |
admin_post_skroutz_clear_completed | skroutz_handle_clear_completed | Delete completed queue rows |
admin_post_skroutz_clear_all_queue | skroutz_handle_clear_all_queue | Truncate entire queue |
admin_post_skroutz_reset_failed | skroutz_handle_reset_failed | Reset failed → pending |
admin_post_skroutz_download_log | skroutz_handle_download_log | Download log file |
admin_post_skroutz_save_settings | skroutz_handle_save_settings | Save settings form |
admin_post_skroutz_upload_invoice | skroutz_handle_upload_invoice | Manual immediate upload from order edit |
rest_api_init | inline closure | Register /oxygen-skroutz/v1/webhook REST endpoint |
Skroutz Invoice Uploader — oxygen-woocommerce-plugin | Documentation regenerated August 2026