Skip to content

ERP Integrations (Pharma)

Flow ID: IN-08 | Module(s): erp, job, iqvia, Europharmacy | Complexity: High Last Updated: 2026-04-04

Business Overview

The platform integrates with three pharmaceutical ERP systems used by Greek pharmacy e-shops. Each ERP serves a different segment of the pharmacy supply chain:

  1. Farmakon -- Traditional pharmacy wholesale ERP. XML-based catalog download (zipped) with barcode matching, plus XML order posting. Targets small-to-medium pharmacies using SoftOne-based warehouse management.

  2. Europharmacy -- Modern pharmacy cooperative ERP. RESTful JSON API with Basic auth, paginated product sync (1000 items/page), bidirectional order lifecycle management, and customer ERP ID mapping. Uses the PSR-4 client in src/Europharmacy/.

  3. IQVIA -- Global pharmaceutical data analytics company. One-way outbound reporting: daily CSV generation of completed orders uploaded via SFTP. Used for market analytics and regulatory reporting, not operational sync.

All three are implemented as JobCommand classes executed by the cron-based job scheduler.

API Reference

Farmakon Endpoints (XML/HTTP)

DirectionURL PatternMethodFormat
Inbound{farmakon_address}/Products.aspx?Offset=X&Count=YGETZIP -> XML
Outbound{farmakon_address}/UploadPelates.aspxPOSTXML
Outbound{farmakon_address}/UploadParag.aspxPOSTXML

Europharmacy Endpoints (REST/JSON)

DirectionPathMethodAuth
Inbound/api/Products?page=X&size=1000&searchMeds=true&searchUserItems=trueGETBasic
Inbound/api/Products/{barcode}GETBasic
Inbound/api/CustomersGETBasic
Inbound/api/Customers?{params}GETBasic
Inbound/api/MasterData/VatCaterogiesGETBasic
Inbound/api/Eshop/Status?modifiedLastDays=1&includeCreataDate=false&page=X&size=1000GETBasic
Outbound/api/CustomersPOSTBasic
Outbound/api/Eshop/{orderId}POSTBasic

IQVIA (SFTP Upload)

DirectionProtocolFormatSchedule
OutboundSFTPPipe-delimited CSVDaily 03:45 (default)

Code Flow

Farmakon Product Sync

Job: AdvFarmakonSyncProducts -- Schedule: */3 hours (client-configured)

AdvFarmakonSyncProducts->executeCommand()
  -> Farmakon->syncProducts()
    1. fetchFarmakonXml() -> farmakonGetXml()
       -> GET Products.aspx?Offset=0&Count=102400000
       -> Parse response headers for Total count (and Error header)
       -> Download ZIP body, append chunks with offset pagination
       -> Unzip to Products.xml (deletes old zip/xml first)
    2. farmakonXml2Array()
       -> Parse XML: BARCODES (barcode-to-product mapping) + APOTIKH (product data)
       -> Build product array with: AP_ID, price_with_vat, wholesale_price_with_vat,
          stock, vat, vatId, name, product_code, product_barcodes[]
       -> Auto-create missing VAT rates in shop_product_vats
    3. getProductNotToSync() -> product_list_model::getProductsByProductList(EXCLUDED_PRODUCT_LIST)
    4. For each product:
       -> Skip if product_code is in excluded product list
       -> product_model::getProductIdsByBarcodes(product_barcodes)
       -> MATCH 1: updateShopProduct() -- update price, stock, code, barcodes; remove from new_products
       -> MATCH >1: Log warning with barcode list and match count (ambiguous match, skip)
       -> MATCH 0: handleNewProduct() -- insert/update in new_products staging table

Farmakon Order Posting

Job: AdvFarmakonPostOrders -- Schedule: */5 minutes (client-configured)

AdvFarmakonPostOrders->executeCommand()
  -> Farmakon->postOrders(['PENDING_ACCEPTED', 'PAID'])
    1. getOrdersForFarmakon($status) -- fetch unposted orders
    2. For each order:
       a. Load customer data
       b. Resolve region names from CountriesCounties lookup
       c. farmakonPostCustomer($customer)
          -> POST XML to UploadPelates.aspx
          -> Parse response: STATUS=1 means success
       d. Map payment/document/status codes from config mappings
       e. calculateOrderVats($order, $cart)
          -> Group line items by Greek VAT rates (0%, 6%, 13%, 24%)
          -> Calculate per-rate net/vat/gross totals
       f. farmakonPostOrder($order, $orderCart)
          -> POST XML to UploadParag.aspx
          -> Parse response: STATUS=1 means success
       g. On success: update order status to 'SOFTONE'

Europharmacy Product Sync

Job: AdvSyncProductsEuropharmacy -- Schedule: daily at 23:00 (client-configured)

AdvSyncProductsEuropharmacy->executeCommand()
  1. ensureVatsInitialized()
     -> Fetch VATs from ERP: GET /api/MasterData/VatCaterogies
     -> Compare with shop_product_vats table
     -> Insert missing VAT rates
  2. getProducts()
     -> Europharmacy->getProducts()
        -> Paginated GET /api/Products (1000/page)
        -> Aggregate all pages until lastPage=true
  3. For each product:
     -> Skip if barcodeList is empty
     -> transformProductData(): map uniqId, productName, stock, price, wholePrice,
        eshopDiscount, vat_id, barcodeList
     -> getPriceWithoutVat(): use eshopPrice (or retailPrice fallback) / (1 + vat/100)
     -> getProductIdsByBarcode(barcodeList) -- match by barcode
     -> MATCH: Update shop_product (price, wholesale_price, discount, vat_id, date_changed)
               Update product_codes (stock, product_code)
               Sync barcodes: add new, remove obsolete
     -> NO MATCH: Check new_products staging table (by barcode + product_code)
                  If not exists: insertNewProductEuropharmacy()

Europharmacy Order Posting

Job: AdvPostOrdersEuropharmacy -- Schedule: */5 minutes (client-configured)

AdvPostOrdersEuropharmacy->executeCommand()
  1. Fetch orders with status PENDING_ACCEPTED or PAID
  2. For each order:
     a. Load customer; skip if no customer_id
     b. Check customer.erp_id:
        -> If empty: POST customer to ERP, store returned erp_id
     c. Build order payload:
        - Order metadata: status, dates, payment/shipping method
        - Billing details: name, address, tax info (invoice support)
        - Shipping details: address (store pickup: uses billing name/surname instead of shipping name/surname)
        - Items: product title, ERP ID (product_code), first barcode, qty, unit price (original_price), discount percentage (calculated as `((original - actual) / original) * 100`), total (subtotal)
        - Fees: delivery cost, transportation cost, gift packaging
        - Coupons: coupon code/amount, loyalty points
     d. POST /api/Eshop/{orderId}
     e. On success: update order status to 'SOFTONE'

Europharmacy Order Status Sync

Job: AdvSyncOrdersStatusEuropharmacy -- Schedule: */30 minutes (client-configured)

AdvSyncOrdersStatusEuropharmacy->executeCommand()
  1. getEuropharmacy()->getOrdersStatuses()
     -> Paginated GET /api/Eshop/Status?modifiedLastDays=1&includeCreataDate=false
        (1000/page, aggregates all pages until lastPage=true)
  2. processOrderStatuses(): for each status update:
     -> Load local order by orderId (order_model::getOrder)
     -> Skip if order not found locally
     -> Map ERP statusId to shop status via getOrderStatusID($statusId, $payway):
        1 -> PENDING_ACCEPTED (if payway=DELIVERY) or PAID (otherwise)
        2,4 -> PENDING
        3 -> SENT (if payway=DELIVERY) or PAID_SENT (otherwise)
        7 -> RETURN
        8 -> INVOICED (also sets invoised_date_time from dateCompleted, converted to Europe/Athens)
        default -> CANCELED
     -> Update order with new status

IQVIA Report Upload

Job: AdvIqviaUpload -- Schedule: daily at 03:45

AdvIqviaUpload->executeCommand()
  1. Check IQVIA.ENABLED registry flag and IQVIA.ID (site identifier)
  2. Determine date range from param:
     - PREVIOUS_DAY (default): yesterday
     - PREVIOUS_MONTH: full previous month
     - PREVIOUS_YEAR: full previous year
     - PERIOD_YYYY-MM-DD_YYYY-MM-DD: custom range
  3. dbData(dateStart, dateEnd):
     -> Query: shop_order JOIN shop_order_basket JOIN product_codes
               JOIN shop_product JOIN shop_product_mui JOIN shop_vendor_mui
     -> Filter by date range using configurable `completedDateField` (default: `entry_datetime`)
     -> If `completedStatus` is non-empty, include only those statuses (default: empty = no inclusion filter)
     -> Exclude statuses in `excludedStatus` list (default: PENDING, CANCELED, RETURN)
     -> Enrich with product barcodes
     -> Sort by added_at timestamp, group by order, assign line_item_id
  4. csvData(): Build pipe-delimited CSV with header:
     source|site_identifier|order_code|line_item_id|placed_at|barcode|
     product_code|brand|retail_price|retail_payable_price|vat_rate|
     quantity_sold|coupon_discount|promo_discount|zip_code|order_type|
     delivery_type|added_to_basket_at|product_description|marketplace|spare1|spare2
  5. uploadCsv(): storage('iqvia')->put(filename, csv)
     -> Uses League Flysystem SFTP adapter
     -> Filename format: {type}_sales_{site_id}_{date}.csv

Architecture

File Map

Farmakon

FilePurpose
ecommercen/libraries/Adv_Farmakon.phpCore library -- product sync + order posting logic
ecommercen/job/libraries/AdvFarmakonSyncProducts.phpJobCommand for product sync
ecommercen/job/libraries/AdvFarmakonPostOrders.phpJobCommand for order posting
ecommercen/helpers/farmakon_helper.phpHTTP functions: XML download, customer/order POST, VAT calculation
ecommercen/erp/controllers/Adv_farmakon_erp.phpCLI-only controller (direct invocation, CLI-gated)
application/config/farmakon.phpPayment/document/status code mappings
application/controllers/Farmakon_erp.phpApplication-level thin wrapper
application/libraries/Farmakon.phpApplication-level thin wrapper
application/modules/job/libraries/FarmakonSyncProducts.phpApplication-level job wrapper
application/modules/job/libraries/FarmakonPostOrders.phpApplication-level job wrapper

Europharmacy

FilePurpose
src/Europharmacy/Europharmacy.phpPSR-4 REST client -- all API methods
src/Europharmacy/Base.phpHTTP base -- Guzzle client, error handling, JSON validation
src/Europharmacy/Config.phpConfig VO -- baseUrl, username, password, API paths
ecommercen/job/libraries/AdvSyncProductsEuropharmacy.phpJobCommand for product sync
ecommercen/job/libraries/AdvPostOrdersEuropharmacy.phpJobCommand for order posting
ecommercen/job/libraries/AdvSyncOrdersStatusEuropharmacy.phpJobCommand for status sync
application/config/europharmacy.phpConnection credentials (baseUrl, username, password)
application/modules/job/libraries/SyncProductsEuropharmacy.phpApplication-level job wrapper
application/modules/job/libraries/PostOrdersEuropharmacy.phpApplication-level job wrapper
application/modules/job/libraries/SyncOrdersStatusEuropharmacy.phpApplication-level job wrapper
patches/EuroPharmacyConfigToRegistry.phpMigration patcher for config-to-registry transition

IQVIA

FilePurpose
ecommercen/iqvia/libraries/AdvIqviaUpload.phpJobCommand -- CSV generation + SFTP upload
ecommercen/iqvia/libraries/AdvIqviaConfig.phpConfig loader from CI config
ecommercen/iqvia/models/AdvIqviaModel.phpData model -- multi-table join query for sales data
application/config/iqvia.phpQuery options (statuses, date fields) + output format (delimiter)
application/config/storage.phpSFTP connection config (iqvia storage type)
application/modules/iqvia/libraries/IqviaUpload.phpApplication-level wrapper
application/modules/iqvia/libraries/IqviaConfig.phpApplication-level wrapper
application/modules/iqvia/models/Iqvia_model.phpApplication-level wrapper
docs/guides/Iqvia.mdSetup and usage guide

Europharmacy Client Architecture

Europharmacy extends Base
  Base:
    - GuzzleHttp\Client for HTTP requests
    - CodeIgniterLogger for error logging
    - SSL verification (cacert.pem in dev, system in prod)
    - JSON response validation
    - Debug call logging (optional)

  Config:
    - API paths defined as constants:
      customers -> /api/Customers
      products  -> /api/Products
      orders    -> /api/Eshop
      vats      -> /api/MasterData/VatCaterogies

  Auth: HTTP Basic (base64 username:password)
  Content-Type: application/json-patch+json

IQVIA Storage Architecture

IQVIA uses the League Flysystem storage abstraction (storage('iqvia')) with an SFTP adapter:

storage('iqvia')
  -> Flysystem Filesystem
    -> SftpAdapter (PhpseclibV3)
      -> Host, username, password from .env (IQVIA_HOST, IQVIA_USER, IQVIA_PWD)
      -> Port: IQVIA_PORT (default 22)
      -> Root: IQVIA_ROOT (default /Home/advisable)
      -> Timeout: IQVIA_TIMEOUT (default 30s)

Data Model

Farmakon XML Schema (Inbound)

xml
<APOTIKH>
  <AP_ID>...</AP_ID>
  <AP_CODE>...</AP_CODE>
  <AP_DESCRIPTION>...</AP_DESCRIPTION>
  <AP_TIMH_LIAN>...</AP_TIMH_LIAN>     <!-- retail price with VAT -->
  <AP_TIMH_XON>...</AP_TIMH_XON>       <!-- wholesale price with VAT -->
  <AP_YPOLOIPO>...</AP_YPOLOIPO>       <!-- stock -->
  <FP_POSOSTO>...</FP_POSOSTO>         <!-- VAT rate -->
</APOTIKH>
<BARCODES>
  <AB_AP_ID>...</AB_AP_ID>             <!-- product reference -->
  <AB_BARCODE>...</AB_BARCODE>
</BARCODES>

Farmakon Payment/Status Mappings

Defined in application/config/farmakon.php:

Payment codes (payway_codes): bank_transfer=1, delivery=2, eurobank=3, paid_at_store=4, paypal=5, apcopay=6, alpha=7, paybybank=8, piraeus=9, ethniki=10, proxypay=11, vivawallet=12, stripe=13, jcc=14, iris=15, ethniki_ee=16

Document codes (paymerch_codes): receipt=1, invoice=2

Status codes (status_codes): CANCELED=1, PAID=2, PAID_SENT=3, PENDING=4, PENDING_ACCEPTED=5, SENT=6, SOFTONE=7

Europharmacy Order Status Mapping

ERP Status IDShop Status (payway=DELIVERY)Shop Status (Other payways)
1PENDING_ACCEPTEDPAID
2PENDINGPENDING
3SENTPAID_SENT
4PENDINGPENDING
7RETURNRETURN
8INVOICEDINVOICED
defaultCANCELEDCANCELED

The DELIVERY payment way (cash on delivery) determines the status branch. For status 8 (INVOICED), the invoised_date_time field is also set from the ERP's dateCompleted (converted to Europe/Athens timezone).

Europharmacy Outbound Status Mapping (Shop -> ERP)

When posting orders to the ERP, the local shop status is mapped to an ERP status ID:

Shop StatusERP Status IDNotes
PENDING (payway=BANK_TRANSFER)4Special case for bank transfer
PENDING2
CANCELED6
INVOICED8
Other (default)1Includes PENDING_ACCEPTED, PAID

Europharmacy Payment Method Mapping

Payment WayERP Method ID
DELIVERY1
BANK_TRANSFER2
PAYPAL3
PAID_AT_STORE5
Other4 (default)

IQVIA CSV Columns

ColumnSourceNotes
source(empty)Reserved
site_identifierIQVIA.ID registryClient identifier
order_codeshop_order.order_serial
line_item_idSequential per orderAssigned during processing
placed_atConfigurable date fieldentry_datetime by default
barcodeshop_product_barcodesFirst barcode for product
product_codeproduct_codes.id
brandshop_vendor_mui.name
retail_priceshop_order_basket.original_price
retail_payable_priceshop_order_basket.price
vat_rateshop_order_basket.product_vat
quantity_soldshop_order_basket.qty
coupon_discount(empty)Reserved
promo_discount(empty)Reserved
zip_codeshop_order.pricing_postal
order_typeC (receipt) or WP (wholesale)Based on paymerch
delivery_typeC (courier) or P (pickup)Based on store_id
added_to_basket_atshop_order_basket.added_atFallback to placed_at
product_descriptionshop_product_mui.namePipes replaced with dashes
marketplaceFirst word of providerE.g., "skroutz" from "skroutz_smart_cart"
spare1(empty)Reserved
spare2(empty)Reserved

Key Database Tables

TableUsed ByPurpose
shop_productAllProduct master (price, stock, vat_id)
product_codesAllSKU-level data (stock, product_code)
shop_product_barcodesAllBarcode-to-product mapping
shop_product_vatsFarmakon, EuropharmacyVAT rate definitions
shop_orderAllOrder header
shop_order_basketFarmakon, Europharmacy, IQVIAOrder line items
shop_customerFarmakon, EuropharmacyCustomer data (erp_id for Europharmacy)
new_productsFarmakon, EuropharmacyStaging table for unmatched ERP products

Configuration

Registry Keys

GroupKeyERPPurpose
FARMAKONIS_ENABLEDFarmakonMaster toggle (both IS_ENABLED and ADDRESS must be set)
FARMAKONADDRESSFarmakonERP server base URL (required for initialization)
FARMAKONEXCLUDED_PRODUCT_LISTFarmakonProduct list ID to exclude from sync
IQVIAENABLEDIQVIAMaster toggle
IQVIAIDIQVIASite identifier for CSV and SFTP

Config Files

FileERPContents
application/config/farmakon.phpFarmakonFolder paths, XML/ZIP filenames, payment/status code mappings
application/config/europharmacy.phpEuropharmacybaseUrl, username, password
application/config/iqvia.phpIQVIAQuery options (completedStatus=[], excludedStatus=[PENDING,CANCELED,RETURN], purchaseDateField=entry_datetime, completedDateField=entry_datetime), output delimiter (`
application/config/storage.phpIQVIASFTP connection config (iqvia type)

Environment Variables (.env)

VariableERPPurpose
IQVIA_HOSTIQVIASFTP hostname
IQVIA_USERIQVIASFTP username
IQVIA_PWDIQVIASFTP password
IQVIA_PORTIQVIASFTP port (default 22)
IQVIA_ROOTIQVIASFTP root directory (default /Home/advisable)
IQVIA_TIMEOUTIQVIASFTP timeout in seconds (default 30)
IQVIA_STORAGE_DISKIQVIAStorage disk (default sftp)

Job Scheduler Configuration

Jobs are registered in application/config/jobs.php under the erp queue (disabled by default, enabled per client):

php
'erp' => [
    'enabled' => false,    // Enable per client
    'commands' => [
        ['command' => 'FarmakonPostOrders', 'schedule' => '*/5 * * * *'],
        ['command' => 'FarmakonSyncProducts', 'schedule' => '* */3 * * *'],
        ['command' => 'SyncProductsEuropharmacy', 'schedule' => '* 23 * * *'],
        ['command' => 'SyncOrdersStatusEuropharmacy', 'schedule' => '* */30 * * *'],
        ['command' => 'PostOrdersEuropharmacy', 'schedule' => '* */5 * * *'],
    ]
]

IQVIA runs in the core queue: ['command' => 'IqviaUpload', 'schedule' => '45 3 * * *']

Client Extension Points

Farmakon

  1. Override library: Create application/libraries/Farmakon.php to customize product mapping, order payload, or barcode matching logic
  2. Custom mappings: Override application/config/farmakon.php for client-specific payment/status codes
  3. Excluded products: Set FARMAKON.EXCLUDED_PRODUCT_LIST to a product list ID to skip specific products
  4. Schedule tuning: Adjust cron schedules in application/config/jobs.php

Europharmacy

  1. Override job classes: Create client-level job wrappers in application/modules/job/libraries/ to customize order payload, status mapping, or product transformation
  2. Connection config: Override application/config/europharmacy.php with client-specific credentials
  3. Fee mapping: Extend getOrderFees() for custom fee types (currently supports delivery, transportation, gift packaging)
  4. Payment method mapping: Override getPaymentMethodId() for custom payment integrations

IQVIA

  1. Query customization: Override application/config/iqvia.php to change completed statuses, excluded statuses, or date field references
  2. CLI commands: Run manual exports via php cli.php job/index/IqviaUpload/PERIOD_YYYY-MM-DD_YYYY-MM-DD
  3. Historical data: Use PERIOD param for initial backfill when onboarding a new client

Business Rules

Product Matching (Shared)

  1. Barcode-based matching: Both Farmakon and Europharmacy match ERP products to shop products by barcode
  2. Single match: Product updated in-place (price, stock, code)
  3. Multiple matches: Farmakon logs a warning and skips; Europharmacy updates all matching products
  4. No match: Product inserted into new_products staging table for admin review

Farmakon

  1. Excluded list: Products in the configured product list are skipped entirely
  2. Order eligibility: Only orders with status PENDING_ACCEPTED or PAID are posted
  3. VAT calculation: Orders are broken down by Greek VAT rates (0%, 6%, 13%, 24%) for SoftOne compliance
  4. Status transition: Successfully posted orders move to SOFTONE status
  5. CLI-only access: The ERP controller blocks non-CLI requests and logs the IP of blocked attempts
  6. Customer-first: Customer must be successfully posted before order posting proceeds

Europharmacy

  1. Customer ERP ID: Customers without an erp_id are first created in the ERP via POST /api/Customers; the returned ID is stored in shop_customer.erp_id. Customer data includes name, address, phone, email, tax details, and country (resolved to Greek name via CountriesCounties)
  2. Price normalization: Prices are converted from VAT-inclusive to VAT-exclusive using the product's VAT rate
  3. Order fees: Three fee types are supported, each with 24% VAT: delivery cost (fee_id=1, "ΕΞΟΔΑ ΑΝΤΙΚΑΤΑΒΟΛΗΣ"), transportation cost (fee_id=2, "ΕΞΟΔΑ ΑΠΟΣΤΟΛΗΣ"), gift packaging (fee_id=3, "ΕΞΟΔΑ ΣΥΣΚΕΥΑΣΙΑΣ ΔΩΡΟΥ"). Only fees with amount > 0 are sent
  4. Coupon support: Active coupons are sent as coupon line items (code=coupon_id, amount=coupon_value). Loyalty points are sent as a separate coupon with code='POINTS' and amount=points_reward
  5. Bidirectional status: The status sync job pulls ERP status changes and maps them back to shop statuses, respecting DELIVERY (cash on delivery) vs non-DELIVERY payment way differences
  6. Invoice date: When ERP status is INVOICED (8), the invoised_date_time is set from the ERP's dateCompleted field

IQVIA

  1. Enabled gate: Requires both IQVIA.ENABLED registry flag and IQVIA.ID site identifier
  2. Empty data: If no orders match the date range, the job exits without uploading
  3. Order type: Receipt orders are marked C (consumer); invoice orders are marked WP (wholesale/professional)
  4. Delivery type: Orders with a store_id are P (pickup); others are C (courier)
  5. Marketplace attribution: The provider field's first word before underscore identifies the marketplace source
  6. Line item ordering: Items are sorted by added_at timestamp (or basket_id fallback) and numbered sequentially per order