Skroutz Invoice Uploader

Technical Documentation — How it works, flows & requirements

Plugin: oxygen-woocommerce-plugin  |  File: inc/class-skroutz-uploader.php

Generated: August 2026

Table of Contents

  1. System Overview
  2. Requirements & Configuration
  3. Flow A — Auto Invoice Creation (Queue)
  4. Flow B — Manual Upload (Immediate)
  5. Queue System
  6. WP-Cron Worker
  7. Core Upload Logic
  8. Retry & Failure Handling
  9. Dashboard & Quick Actions
  10. Log Files
  11. Database Table
  12. Function Reference
  13. Hook Reference

1. System Overview

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:

APIPurposeUsed for
Oxygen / Pelatologio APIGreek accounting platformRetrieve the invoice PDF for a WooCommerce order
Skroutz Marketplace APIGreek e-commerce marketplaceUpload the invoice PDF so the buyer can download it

There are two distinct upload flows depending on how the upload is triggered:

FlowTriggerMethod
A — AutoOxygen invoice auto-created on order status changeAdded to a database queue, processed by WP-Cron
B — ManualAdmin clicks "Upload Invoice to Skroutz" on order edit pageImmediate upload, records result in queue as manually_uploaded

2. Requirements & Configuration

Plugin Settings to Enable

Option keyWhere to setRequired value
oxygen_skroutz_uploader_enabledPlugin optionsyes — enables the entire module
skroutz_api_tokenSkroutz Uploader → SettingsBearer token from Skroutz Merchant API
skroutz_iview_tokenSkroutz Uploader → SettingsToken for iView PDF access (Oxygen MyData)
skroutz_auto_uploadSkroutz Uploader → Settings1 — enables auto-queue after invoice creation
skroutz_max_retriesSkroutz Uploader → SettingsNumber of retry attempts (default: 3, range: 1–10)
skroutz_cleanup_daysSkroutz Uploader → SettingsDays before deleting completed items (default: 365, range: 1–365)

External Dependencies

Order Meta Required on WooCommerce Order

Meta keySet byContains
_oxygen_invoiceOxygen plugin after invoice creationInvoice data array including id and iview_url
_skroutz_order_codeSkroutz webhook handler on order creationSkroutz marketplace order code (e.g. 260818-9607017)
_skroutz_invoice_uploadedSet on successful upload (auto or manual)MySQL datetime of successful upload
⚠ Important The Oxygen invoice (_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.

3. Flow A — Auto Invoice Creation (Queue)

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).

Step-by-step sequence

1Skroutz Webhook
POST to /oxygen-skroutz/v1/webhook
2WC Order Created
Customer data, line items, fees, meta saved; _skroutz_order_code stored
3Status Changed
Order set to on-hold
4Oxygen Invoice Created
create_invoice() fires on status change hook
5Meta Saved
_oxygen_invoice set on order
6Hook Fires
do_action('oxygen_invoice_created', $order_id)
7Enqueue
Order + Skroutz order code added to wp_skroutz_upload_queue with status pending
8Cron Runs
oxygen_skroutz_cron_hook fires hourly (or manual trigger)
9Worker Processes
Downloads PDF from Oxygen API, uploads to Skroutz API

Key hooks involved

HookFilePurpose
woocommerce_order_status_{status}class-oxygenorder.phpTriggers create_invoice() when order reaches configured status
oxygen_invoice_createdclass-oxygenorder.php (fires)
class-skroutz-uploader.php (listens)
Custom action fired after invoice successfully saved to order meta
oxygen_skroutz_cron_hookclass-skroutz-uploader.phpWP-Cron event that runs the queue worker
✓ Why the queue? Uploading to Skroutz is an HTTP request that can fail due to network timeouts, API rate limits, or temporary errors. The queue allows automatic retries without any manual intervention.

4. Flow B — Manual Upload (Immediate)

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.

Prerequisites before clicking the button

  1. The order must have an Oxygen invoice already created (_oxygen_invoice meta must exist)
  2. The Skroutz API token must be configured in Settings
  3. The order should have a Skroutz order code (_skroutz_order_code meta)

Step-by-step sequence

1Button Clicked
"Upload Invoice to Skroutz" in order meta box
2Nonce + Auth Check
Security validation, capability check
3Validate
Order exists, invoice meta exists, API token set
4Download PDF
OxygenApi::get_invoice_pdf()
5Upload to Skroutz
Multipart POST with PDF to Skroutz API
6Record Result
Order note added, queue row set to manually_uploaded

What happens after a successful manual upload

ℹ Cron protection Because the manual upload sets the queue status to manually_uploaded, the cron worker (which only picks up pending and retry rows) will never attempt to upload this order again automatically.

Admin post handler

URL pattern: admin-post.php?action=skroutz_upload_invoice&order_id={ID}

Function: skroutz_handle_upload_invoice()

5. Queue System

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.

Queue item lifecycle

StatusMeaningNext action
pendingNewly added, waiting to be picked up by the workerWorker picks it up on next run
processingCurrently being uploaded (in-flight)Moves to completed, retry, or failed
completedSuccessfully uploaded to Skroutz by the cron workerNo further action; can be cleared from dashboard
manually_uploadedSuccessfully uploaded by an admin via the order edit pageNo further action; cron worker skips this row permanently
retryFailed but under the retry limitWorker picks it up again on next run
failedFailed and exhausted all retry attemptsRequires manual "Reset Failed" action from dashboard
⚠ Uniqueness constraint The 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).

Skroutz order code resolution

When an order is enqueued, skroutz_queue_enqueue() immediately resolves and stores the Skroutz order code:

  1. Read _skroutz_order_code meta from the WC order
  2. If empty, fall back to $order->get_order_number()
  3. Store the resolved code in the skroutz_order_code column

This means the dashboard always shows both IDs side-by-side for easier debugging.

Key queue functions

FunctionPurpose
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

6. WP-Cron Worker

The queue is processed automatically by a WP-Cron scheduled event named oxygen_skroutz_cron_hook, which runs hourly.

Cron schedule

PropertyValue
Event nameoxygen_skroutz_cron_hook
IntervalHourly
Batch size20 items per run
Worker functionskroutz_queue_run_worker(20)
Item selectionStatus IN ('pending', 'retry'), ordered by created_at ASC (oldest first)

What the cron worker does per run

  1. Check Skroutz API token is configured — abort if missing
  2. Fetch up to 20 rows with status pending or retry
  3. For each row: mark as processing, read skroutz_order_code from the row, call skroutz_do_upload_invoice()
  4. On success: mark as completed
  5. On failure: increment attempts; mark as retry if under max, or failed if exhausted
  6. Log a summary line when finished
ℹ manually_uploaded rows are never touched by the worker The worker's SQL query explicitly filters for pending and retry only. Orders uploaded manually are permanently safe from re-upload.

Manual trigger

The worker can also be triggered immediately from the dashboard via "Process Queue Now" (Quick Actions panel), without waiting for the next hourly cron.

If cron stops working

Use "Reschedule Cron" from the Quick Actions panel. This unschedules any existing event and registers a new hourly schedule starting immediately.

7. Core Upload Logic

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.

Upload steps inside skroutz_do_upload_invoice()

#StepDetails
1Load WC orderValidate order exists via wc_get_order()
2Read invoice metaGet _oxygen_invoice — must contain id and iview_url
3Resolve Skroutz order codeRead _skroutz_order_code meta; fall back to WC order number
4Build order referenceCombines both IDs: WC#1433 / Skroutz#260818-9607017 — used in all log messages
5Call Oxygen APIOxygenApi::get_invoice_pdf(invoice_id) — downloads raw PDF bytes
6Validate PDFCheck response is not empty and starts with %PDF-
7Write temp filewp_tempnam() + file_put_contents() — needed for cURL multipart
8Upload to SkroutzcURL multipart POST to https://api.sandbox.skroutz.dev/merchants/ecommerce/orders/{code}/invoices
9Clean upDelete temp file with @unlink()
10Parse responseHTTP 2xx → success; otherwise parse JSON errors and return message string
11Save timestampOn success: _skroutz_invoice_uploaded meta set on order

Return value

ReturnMeaning
trueUpload succeeded
stringError message (e.g. HTTP 404: Order not found)

Skroutz API endpoint

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]

Error response format from Skroutz API

{
  "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.

8. Retry & Failure Handling

Automatic retry flow

ScenarioResult
Upload succeeds (cron)Status → completed
Upload succeeds (manual)Status → manually_uploaded
Upload fails, attempts < max_retriesStatus → retry, picked up next cron run
Upload fails, attempts ≥ max_retriesStatus → failed, no more automatic retries

Manual recovery for failed items

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:

  1. Go to Skroutz Invoice Uploader → Dashboard
  2. Click the red "Reset Failed (N)" button (only visible when failed count > 0)
  3. Confirm the dialog
  4. All failed rows are reset to pending with attempts = 0
  5. Click "Process Queue Now" or wait for the next cron run
⚠ Common failure reasons

9. Dashboard & Quick Actions

Accessible at WP Admin → Skroutz Invoice Uploader. Two tabs: Dashboard and Settings.

Stat cards

Shows live counts from the queue table (7 cards total):

CardColorDescription
Total Items■ BlueAll rows in the queue
Pending■ OrangeWaiting for next cron run
Processing■ CyanCurrently being uploaded (in-flight)
Completed■ GreenSuccessfully uploaded by cron
Manual Upload■ PurpleSuccessfully uploaded by admin manually
Failed■ RedExhausted all retry attempts
Retry■ YellowFailed temporarily, will retry next run

Quick Actions (Dashboard tab)

ButtonVisibilityAction
Process Queue NowAlwaysRuns the worker immediately (batch of 20)
Reschedule CronAlwaysUnschedules + re-registers the hourly cron event
Test API ConnectionAlwaysPings the Skroutz API to verify the token works
Reset Failed (N) (red)Only when failed > 0Resets all failed rows to pending with attempts = 0
Clear Queue (N) (dark red)Only when queue has itemsTruncates the entire queue table — irreversible

Recent Queue Activity table

Shows the 20 most recently updated queue rows with columns:

Settings tab

SectionFields
API ConfigurationSkroutz API Token (password), iView Token (password)
Upload SettingsEnable Automatic Upload (checkbox), Maximum Retries (1–10)
MaintenanceCleanup After Days (1–365)
ActionsSave Settings button, Test API Connection button, Download Log File button

Download Log File button (Settings tab)

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.

Order edit page meta box

On every WooCommerce order edit page, a "Skroutz Invoice Uploader" meta box shows:

10. Log Files

All logging goes through WooCommerce's logger (wc_get_logger()) with source tag skroutz-uploader.

LocationValue
WC Admin pathWooCommerce → Status → Logs → skroutz-uploader
Physical pathwp-content/uploads/wc-logs/skroutz-uploader-YYYY-MM-DD-{hash}.log
DownloadSkroutz Uploader → Settings → Download Log File button

Log identifier format

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.

Log message reference

LevelMessage patternWhen
INFOCron event fired — launching queue workerCron hook triggered
INFOCron event complete — processed: N, errors: N, total: NAfter cron run finishes
INFOQueue worker started — found N item(s) (batch_size=20, max_retries=N)Worker start
DEBUGQueue worker: nothing to do, exitingQueue is empty
INFOQueue worker finished — processed: N, errors: N, total: NWorker end
DEBUGQueue item #N — WC#N / Skroutz#XXX, attempt N/N: startingBefore each upload attempt
INFOQueue item #N — WC#N / Skroutz#XXX: completed on attempt NSuccessful upload by cron
WARNINGQueue item #N — WC#N / Skroutz#XXX: will retry (attempt N/N) — {error}Failed but will retry
ERRORQueue item #N — WC#N / Skroutz#XXX: permanently failed after N attempt(s) — {error}Max retries reached
DEBUGWC#N / Skroutz#XXX: downloading PDF from Oxygen API (invoice_id=N)Before PDF download
DEBUGWC#N / Skroutz#XXX: PDF downloaded OK (N bytes)After PDF downloaded
DEBUGWC#N / Skroutz#XXX: uploading to SkroutzBefore Skroutz cURL call
DEBUGWC#N / Skroutz#XXX: Skroutz API responded HTTP NNNAfter cURL response
INFOWC#N / Skroutz#XXX: invoice uploaded successfully to SkroutzUpload OK (cron)
INFOWC#N: invoice uploaded manually to Skroutz (Skroutz#XXX)Upload OK (manual)
ERRORWC#N / Skroutz#XXX: upload failed — HTTP NNN: messageAPI error response
INFOOrder #N enqueued for Skroutz upload after invoice creationAuto-enqueue hook
WARNINGOrder #N could not be enqueued (already in queue or DB error)Duplicate enqueue attempt
ERRORQueue worker: API token not configured — abortingToken missing at worker start
INFOReset N failed queue item(s) back to pending via manual actionReset Failed button clicked
INFOEntire upload queue cleared via manual actionClear Queue button clicked

11. Database Table

Table: wp_skroutz_upload_queue

ColumnTypeDefaultDescription
idbigint(20) AUTO_INCREMENTRow identifier (primary key)
order_idbigint(20)WooCommerce order ID — one row per order (unique)
skroutz_order_codevarchar(100)''Skroutz marketplace order code, resolved at enqueue time
statusvarchar(20)'pending'pending / processing / completed / manually_uploaded / retry / failed
attemptsint(11)0Number of upload attempts made so far
messagetextNULLLast result message (success text or API error)
created_atdatetimeWhen the row was first inserted
updated_atdatetimeLast status change timestamp

Indexes

IndexColumnsPurpose
PRIMARY KEYidRow identity
UNIQUE KEYorder_idPrevent duplicate queue entries per order
KEYstatusFast 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.

12. Function Reference

FunctionPurpose
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

13. Hook Reference

Hook / ActionHandlerPurpose
admin_menuoxygen_add_new_feature_menuRegister admin menu page
oxygen_invoice_createdskroutz_auto_enqueue_after_invoice_createdAuto-enqueue after Oxygen invoice creation
initskroutz_queue_maybe_create_tableCreate/upgrade DB table on every request
oxygen_skroutz_cron_hookinline closureRun queue worker hourly via WP-Cron
admin_post_skroutz_process_queueskroutz_handle_process_queueProcess queue manually
admin_post_skroutz_reschedule_cronskroutz_handle_reschedule_cronReschedule cron event
admin_post_skroutz_test_apiskroutz_handle_test_apiTest API token
admin_post_skroutz_clear_completedskroutz_handle_clear_completedDelete completed queue rows
admin_post_skroutz_clear_all_queueskroutz_handle_clear_all_queueTruncate entire queue
admin_post_skroutz_reset_failedskroutz_handle_reset_failedReset failed → pending
admin_post_skroutz_download_logskroutz_handle_download_logDownload log file
admin_post_skroutz_save_settingsskroutz_handle_save_settingsSave settings form
admin_post_skroutz_upload_invoiceskroutz_handle_upload_invoiceManual immediate upload from order edit
rest_api_initinline closureRegister /oxygen-skroutz/v1/webhook REST endpoint

Skroutz Invoice Uploader — oxygen-woocommerce-plugin  |  Documentation regenerated August 2026