Appearance
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:
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.
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/.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)
| Direction | URL Pattern | Method | Format |
|---|---|---|---|
| Inbound | {farmakon_address}/Products.aspx?Offset=X&Count=Y | GET | ZIP -> XML |
| Outbound | {farmakon_address}/UploadPelates.aspx | POST | XML |
| Outbound | {farmakon_address}/UploadParag.aspx | POST | XML |
Europharmacy Endpoints (REST/JSON)
| Direction | Path | Method | Auth |
|---|---|---|---|
| Inbound | /api/Products?page=X&size=1000&searchMeds=true&searchUserItems=true | GET | Basic |
| Inbound | /api/Products/{barcode} | GET | Basic |
| Inbound | /api/Customers | GET | Basic |
| Inbound | /api/Customers?{params} | GET | Basic |
| Inbound | /api/MasterData/VatCaterogies | GET | Basic |
| Inbound | /api/Eshop/Status?modifiedLastDays=1&includeCreataDate=false&page=X&size=1000 | GET | Basic |
| Outbound | /api/Customers | POST | Basic |
| Outbound | /api/Eshop/{orderId} | POST | Basic |
IQVIA (SFTP Upload)
| Direction | Protocol | Format | Schedule |
|---|---|---|---|
| Outbound | SFTP | Pipe-delimited CSV | Daily 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 tableFarmakon 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 statusIQVIA 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}.csvArchitecture
File Map
Farmakon
| File | Purpose |
|---|---|
ecommercen/libraries/Adv_Farmakon.php | Core library -- product sync + order posting logic |
ecommercen/job/libraries/AdvFarmakonSyncProducts.php | JobCommand for product sync |
ecommercen/job/libraries/AdvFarmakonPostOrders.php | JobCommand for order posting |
ecommercen/helpers/farmakon_helper.php | HTTP functions: XML download, customer/order POST, VAT calculation |
ecommercen/erp/controllers/Adv_farmakon_erp.php | CLI-only controller (direct invocation, CLI-gated) |
application/config/farmakon.php | Payment/document/status code mappings |
application/controllers/Farmakon_erp.php | Application-level thin wrapper |
application/libraries/Farmakon.php | Application-level thin wrapper |
application/modules/job/libraries/FarmakonSyncProducts.php | Application-level job wrapper |
application/modules/job/libraries/FarmakonPostOrders.php | Application-level job wrapper |
Europharmacy
| File | Purpose |
|---|---|
src/Europharmacy/Europharmacy.php | PSR-4 REST client -- all API methods |
src/Europharmacy/Base.php | HTTP base -- Guzzle client, error handling, JSON validation |
src/Europharmacy/Config.php | Config VO -- baseUrl, username, password, API paths |
ecommercen/job/libraries/AdvSyncProductsEuropharmacy.php | JobCommand for product sync |
ecommercen/job/libraries/AdvPostOrdersEuropharmacy.php | JobCommand for order posting |
ecommercen/job/libraries/AdvSyncOrdersStatusEuropharmacy.php | JobCommand for status sync |
application/config/europharmacy.php | Connection credentials (baseUrl, username, password) |
application/modules/job/libraries/SyncProductsEuropharmacy.php | Application-level job wrapper |
application/modules/job/libraries/PostOrdersEuropharmacy.php | Application-level job wrapper |
application/modules/job/libraries/SyncOrdersStatusEuropharmacy.php | Application-level job wrapper |
patches/EuroPharmacyConfigToRegistry.php | Migration patcher for config-to-registry transition |
IQVIA
| File | Purpose |
|---|---|
ecommercen/iqvia/libraries/AdvIqviaUpload.php | JobCommand -- CSV generation + SFTP upload |
ecommercen/iqvia/libraries/AdvIqviaConfig.php | Config loader from CI config |
ecommercen/iqvia/models/AdvIqviaModel.php | Data model -- multi-table join query for sales data |
application/config/iqvia.php | Query options (statuses, date fields) + output format (delimiter) |
application/config/storage.php | SFTP connection config (iqvia storage type) |
application/modules/iqvia/libraries/IqviaUpload.php | Application-level wrapper |
application/modules/iqvia/libraries/IqviaConfig.php | Application-level wrapper |
application/modules/iqvia/models/Iqvia_model.php | Application-level wrapper |
docs/guides/Iqvia.md | Setup 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+jsonIQVIA 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 ID | Shop Status (payway=DELIVERY) | Shop Status (Other payways) |
|---|---|---|
| 1 | PENDING_ACCEPTED | PAID |
| 2 | PENDING | PENDING |
| 3 | SENT | PAID_SENT |
| 4 | PENDING | PENDING |
| 7 | RETURN | RETURN |
| 8 | INVOICED | INVOICED |
| default | CANCELED | CANCELED |
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 Status | ERP Status ID | Notes |
|---|---|---|
| PENDING (payway=BANK_TRANSFER) | 4 | Special case for bank transfer |
| PENDING | 2 | |
| CANCELED | 6 | |
| INVOICED | 8 | |
| Other (default) | 1 | Includes PENDING_ACCEPTED, PAID |
Europharmacy Payment Method Mapping
| Payment Way | ERP Method ID |
|---|---|
| DELIVERY | 1 |
| BANK_TRANSFER | 2 |
| PAYPAL | 3 |
| PAID_AT_STORE | 5 |
| Other | 4 (default) |
IQVIA CSV Columns
| Column | Source | Notes |
|---|---|---|
| source | (empty) | Reserved |
| site_identifier | IQVIA.ID registry | Client identifier |
| order_code | shop_order.order_serial | |
| line_item_id | Sequential per order | Assigned during processing |
| placed_at | Configurable date field | entry_datetime by default |
| barcode | shop_product_barcodes | First barcode for product |
| product_code | product_codes.id | |
| brand | shop_vendor_mui.name | |
| retail_price | shop_order_basket.original_price | |
| retail_payable_price | shop_order_basket.price | |
| vat_rate | shop_order_basket.product_vat | |
| quantity_sold | shop_order_basket.qty | |
| coupon_discount | (empty) | Reserved |
| promo_discount | (empty) | Reserved |
| zip_code | shop_order.pricing_postal | |
| order_type | C (receipt) or WP (wholesale) | Based on paymerch |
| delivery_type | C (courier) or P (pickup) | Based on store_id |
| added_to_basket_at | shop_order_basket.added_at | Fallback to placed_at |
| product_description | shop_product_mui.name | Pipes replaced with dashes |
| marketplace | First word of provider | E.g., "skroutz" from "skroutz_smart_cart" |
| spare1 | (empty) | Reserved |
| spare2 | (empty) | Reserved |
Key Database Tables
| Table | Used By | Purpose |
|---|---|---|
shop_product | All | Product master (price, stock, vat_id) |
product_codes | All | SKU-level data (stock, product_code) |
shop_product_barcodes | All | Barcode-to-product mapping |
shop_product_vats | Farmakon, Europharmacy | VAT rate definitions |
shop_order | All | Order header |
shop_order_basket | Farmakon, Europharmacy, IQVIA | Order line items |
shop_customer | Farmakon, Europharmacy | Customer data (erp_id for Europharmacy) |
new_products | Farmakon, Europharmacy | Staging table for unmatched ERP products |
Configuration
Registry Keys
| Group | Key | ERP | Purpose |
|---|---|---|---|
FARMAKON | IS_ENABLED | Farmakon | Master toggle (both IS_ENABLED and ADDRESS must be set) |
FARMAKON | ADDRESS | Farmakon | ERP server base URL (required for initialization) |
FARMAKON | EXCLUDED_PRODUCT_LIST | Farmakon | Product list ID to exclude from sync |
IQVIA | ENABLED | IQVIA | Master toggle |
IQVIA | ID | IQVIA | Site identifier for CSV and SFTP |
Config Files
| File | ERP | Contents |
|---|---|---|
application/config/farmakon.php | Farmakon | Folder paths, XML/ZIP filenames, payment/status code mappings |
application/config/europharmacy.php | Europharmacy | baseUrl, username, password |
application/config/iqvia.php | IQVIA | Query options (completedStatus=[], excludedStatus=[PENDING,CANCELED,RETURN], purchaseDateField=entry_datetime, completedDateField=entry_datetime), output delimiter (` |
application/config/storage.php | IQVIA | SFTP connection config (iqvia type) |
Environment Variables (.env)
| Variable | ERP | Purpose |
|---|---|---|
IQVIA_HOST | IQVIA | SFTP hostname |
IQVIA_USER | IQVIA | SFTP username |
IQVIA_PWD | IQVIA | SFTP password |
IQVIA_PORT | IQVIA | SFTP port (default 22) |
IQVIA_ROOT | IQVIA | SFTP root directory (default /Home/advisable) |
IQVIA_TIMEOUT | IQVIA | SFTP timeout in seconds (default 30) |
IQVIA_STORAGE_DISK | IQVIA | Storage 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
- Override library: Create
application/libraries/Farmakon.phpto customize product mapping, order payload, or barcode matching logic - Custom mappings: Override
application/config/farmakon.phpfor client-specific payment/status codes - Excluded products: Set
FARMAKON.EXCLUDED_PRODUCT_LISTto a product list ID to skip specific products - Schedule tuning: Adjust cron schedules in
application/config/jobs.php
Europharmacy
- Override job classes: Create client-level job wrappers in
application/modules/job/libraries/to customize order payload, status mapping, or product transformation - Connection config: Override
application/config/europharmacy.phpwith client-specific credentials - Fee mapping: Extend
getOrderFees()for custom fee types (currently supports delivery, transportation, gift packaging) - Payment method mapping: Override
getPaymentMethodId()for custom payment integrations
IQVIA
- Query customization: Override
application/config/iqvia.phpto change completed statuses, excluded statuses, or date field references - CLI commands: Run manual exports via
php cli.php job/index/IqviaUpload/PERIOD_YYYY-MM-DD_YYYY-MM-DD - Historical data: Use
PERIODparam for initial backfill when onboarding a new client
Business Rules
Product Matching (Shared)
- Barcode-based matching: Both Farmakon and Europharmacy match ERP products to shop products by barcode
- Single match: Product updated in-place (price, stock, code)
- Multiple matches: Farmakon logs a warning and skips; Europharmacy updates all matching products
- No match: Product inserted into
new_productsstaging table for admin review
Farmakon
- Excluded list: Products in the configured product list are skipped entirely
- Order eligibility: Only orders with status
PENDING_ACCEPTEDorPAIDare posted - VAT calculation: Orders are broken down by Greek VAT rates (0%, 6%, 13%, 24%) for SoftOne compliance
- Status transition: Successfully posted orders move to
SOFTONEstatus - CLI-only access: The ERP controller blocks non-CLI requests and logs the IP of blocked attempts
- Customer-first: Customer must be successfully posted before order posting proceeds
Europharmacy
- Customer ERP ID: Customers without an
erp_idare first created in the ERP via POST/api/Customers; the returned ID is stored inshop_customer.erp_id. Customer data includes name, address, phone, email, tax details, and country (resolved to Greek name viaCountriesCounties) - Price normalization: Prices are converted from VAT-inclusive to VAT-exclusive using the product's VAT rate
- 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
- 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
- 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
- Invoice date: When ERP status is INVOICED (8), the
invoised_date_timeis set from the ERP'sdateCompletedfield
IQVIA
- Enabled gate: Requires both
IQVIA.ENABLEDregistry flag andIQVIA.IDsite identifier - Empty data: If no orders match the date range, the job exits without uploading
- Order type: Receipt orders are marked
C(consumer); invoice orders are markedWP(wholesale/professional) - Delivery type: Orders with a store_id are
P(pickup); others areC(courier) - Marketplace attribution: The
providerfield's first word before underscore identifies the marketplace source - Line item ordering: Items are sorted by
added_attimestamp (orbasket_idfallback) and numbered sequentially per order
Related Flows
- SY-01 Cron Framework -- Job scheduler that executes all ERP jobs
- IN-01 Feed Generation -- Feed framework (separate from ERP sync)
- AD-03 Order Management -- Order admin where ERP sync status is visible
- IN-20 Order Webhooks -- ERP Ready hook triggered on order acceptance