Skip to content

Developer notes

Technical reference for developers who customize Conrad Marketplace Connector (Mirakl).

Item Value
Add-on id kv_conrad
Function prefix fn_kv_conrad_
Language variables kv_conrad.*
CSS scope .kv-conrad
PHP namespace Tygh\Addons\KvConrad
Preset marketplace (Mirakl engine copied from the PcComponentes connector; operator file Operators\Conrad)

Source of every operation: the official Mirakl Marketplace Seller OpenAPI file, https://developer.mirakl.com/_bundle/content/product/mmp/rest/seller/openapi3.json?download (reference pages under https://developer.mirakl.com/content/product/mmp/rest/seller/openapi3). The seller API key goes in the plain Authorization header; the host is the Mirakl address of the seller’s own Conrad instance. Client\Api has one method per operation.

Id Method and path Client\Api method Used by
A01 GET /api/account account(), ping() Test connection, market currency check
H11 GET /api/hierarchies hierarchies() Conrad category tree (category mapping)
PM11 GET /api/products/attributes?hierarchy= productAttributes() Attributes page: the product attributes Conrad defines for a category (read only, shown so you can complete your feature data)
AF01 GET /api/additional_fields?entities=OFFER offerFields() Offer fields that can be mapped (lead time, shipping class, warehouse …)
OF61 GET /api/offers/states offerStates() Code of the condition “New”
OF01 POST /api/offers/imports (multipart file, import_mode=NORMAL) importOffers() Create / update offers; stored before the call
OF24 POST /api/offers postOffers() JSON alternative, selectable in the operator file (offer_import = json)
OF02 GET /api/offers/imports/{import} offerImport() State of an offer import
OF03 GET /api/offers/imports/{import}/error_report offerImportErrors() Reason per rejected line
OF04 GET /api/offers/imports?start_date= offerImports() Finds an import whose POST answer was lost
STO01-STO03 POST /api/offers/stock/imports, GET .../{id}/status, GET .../{id}/error_report importStock(), stockImport(), stockImportErrors() Stock
PRI01-PRI03 POST /api/offers/pricing/imports, GET /api/offers/pricing/imports?import_id=, GET .../{id}/error_report importPrices(), priceImport(), priceImportErrors() Price
OR11 GET /api/orders orders() New orders (order_state_codes=WAITING_ACCEPTANCE), changed orders, order lines for refunds
OR21 PUT /api/orders/{order_id}/accept acceptOrder() Accept or refuse each line, decided by stock
OR23 PUT /api/orders/{order_id}/tracking setTracking() Carrier, tracking number, link
OR24 PUT /api/orders/{order_id}/ship (Content-Length: 0) shipOrder() Validate the shipment
OR28 PUT /api/orders/refund refundLines() Credit note for shipped lines
OR33 PUT /api/orders/{order_id}/cancel-order-lines cancelLines() Cancel lines the seller cannot deliver
SH21 GET /api/shipping/carriers carriers() Carrier codes
RE01 GET /api/reasons reasons() Cancellation (CANCELATION) and refund (REFUND) reasons
RT11 / RT21 GET /api/returns, PUT /api/returns/accept returns(), acceptReturns() Return requests

tests/openapi/mirakl-subset.json holds the schemas of these operations (built by build-subset.mjs); every request the unit tests and the e2e send is validated against it.

Class Role
Client\BaseClient HTTP (cURL, TLS verification, timeouts), JSON, error mapping, masked logging, scope() (company + mode, used in cache keys)
Client\Api The Mirakl Seller API calls, API key in the Authorization header, multipart upload for the import files, API URL check
Drivers\Mirakl\Driver The Mirakl engine: implements Hub\DriverInterface (offers, stock, price, orders, shipping, refunds, returns) without naming an operator
Drivers\Mirakl\Mapper Pure translation hub arrays <-> Mirakl fields (import files, error reports, orders, refund rows, returns); unit tested with the OpenAPI examples
Drivers\Mirakl\OperatorConfig Reads the operator file; OPERATOR names it
Drivers\Mirakl\Operators\Conrad Everything specific to Conrad: markets and currencies (EUR, CHF), product id type, import style, “new” labels, limits, the sources of the facts
Hub\Orders Order import and acceptLines() (the stock decision of OR21)
Hub\ProductSync, Hub\ProductMap Sync runs; rows are claimed (listed P, batch pending) before a driver call; removed mappings become tombstones (listed D)
Hub\Shipments OR23/OR24 push (only when every unit is shipped), cancel, refund (per-order lock, state written before the call)
Accounts, Secret Credentials (test and live) sealed with AES-256-GCM; one account per store owner or per vendor. For Mirakl the account “id” is the API URL and the “secret” the API key
Config, Access Settings, owner mode, who may see and manage what
Queue, Jobs, JobException Job queue: back-off 5 min / 15 min / 1 h / 3 h / 12 h, per-job isolation, overlap lock
Log Event log with masking of secrets and buyer data, filters, CSV, retention

Behaviour that matters when you change the code

Section titled “Behaviour that matters when you change the code”
  • Offer import: Driver::createListings() writes the batch (empty import id, SKUs, line count, send time) before the POST. A 5xx, a timeout or an answer without an id keeps the batch; batchResult() finds the import with OF04 (oldest import since the send time with the same line count that no other batch owns) and polls OF02; after 30 minutes without a match the lines fail with “did not confirm” and the hub sends them again. A 4xx drops the batch and raises.
  • Hub claim: ProductSync::createListings() marks the rows P / pending before calling the driver; rows that stay pending for an hour are due again (offer imports are keyed by the offer SKU).
  • Acceptance: Driver::acceptNew() asks Orders::acceptLines() per line; it answers every line, then Orders::reserve() writes the accepted lines as an order map row without order. Orders::reserved() (rows without order of the last 14 days) is subtracted by acceptLines() and by Listings::state() before the absolute stock import.
  • Exactly once: Orders::handle() claims a new order with placing_at set (INSERT IGNORE), or an existing row with UPDATE ... WHERE placing_at = <old>; a claim younger than 15 minutes is left alone, an older one or a missing row first looks for the CS-Cart order with Orders::findPlaced() (notes key (package <id>), top orders only) and adopts it. installed_at (options, company 0, written at install) is the floor of the first import. The adoption window starts one day before the CHANNEL order’s creation date (never at installed_at, which a reinstall rewrites).
  • Shipment step: Driver::pushShipment() reads OR11 first; OR23 + OR24 only in SHIPPING; already shipped states return; CANCELED / REFUSED throw 410 and Shipments::push() marks the row shipment_pushed = 'X'; other states throw 409 and the row waits 30 minutes (retry_at). Rows that cannot go out yet get retry_at = now and the selection is ordered by retry_at, map_id, so the 50-row window rotates.
  • Lines: Mapper::order() drops lines in REFUSED / CANCELED state.
  • Status follow: StatusMap::keep() (forward-only rank, final statuses C D F I, cs_status = last status the connector set); Returns::isFullReturn() decides whether an accepted return is a “returned” order.
  • Multi-Vendor parent orders: Orders::leafOrders() resolves a parent to its child orders; Shipments::push(), fullyShipped(), the status follow, returns, refund and cancel checks use the children.
  • Buyer e-mails: Orders::place() sets Orders::$silent around fn_place_order(); the get_notification_rules hook turns the customer notification off while it is set (option notify_buyer).
  • Tombstones: ProductMap::delete() and fn_kv_conrad_delete_product_post() never drop a live row; ProductSync::apply() deletes it after the stock import confirmed quantity 0. A row removed while its offer is being created keeps its batch: the withdrawal waits for the create result, a create result never sets it back to listed (AND listed = 'P'), a created offer is withdrawn, an offer that was never created goes at once.
  • Dates: protocol timestamps use gmdate (// kv-allow:gmdate); documents and numbers use the shop time zone (date()).
Table Purpose
?:kv_conrad_accounts Credentials per company (secrets sealed), last connection check
?:kv_conrad_log Log
?:kv_conrad_jobs Queue
?:kv_conrad_product_map, ?:kv_conrad_order_map, ?:kv_conrad_category_map, ?:kv_conrad_attr_map Product, order, category and offer field mapping (the order map carries refund_state, placing_at, hub_status, cs_status)
?:kv_conrad_returns, ?:kv_conrad_options, ?:kv_conrad_cache, ?:kv_conrad_batches Returns, rules, reference data and batch cache, import batches

See init.php (product save and update_product_amount mark the mapping dirty, product delete withdraws the offer, get_notification_rules keeps the buyer e-mail off during an import) and the template hooks in design/backend/templates/addons/kv_conrad/hooks/ (add-on settings page).

No test calls a real Mirakl endpoint. The base URL can be overridden for tests in config.local.php (development only); the e2e scripts refuse to run unless it points at 127.0.0.1:

$config['kv_conrad_base_url'] = 'http://127.0.0.1:11931';
  • node tests/openapi/build-subset.mjs <openapi3.json> builds tests/openapi/mirakl-subset.json from the official Mirakl Seller OpenAPI file.
  • php tests/unit.php, php tests/unit_mirakl.php and php tests/unit_hub.php run the unit tests; the second records every request and node tests/openapi/check.mjs validates them against the OpenAPI file; the third covers the order, shipment, return and listing rules of the hub with a database fixture.
  • node tests/mock-server/server.js 11931 starts a local Mirakl stand-in that validates every request against the same file; node tests/e2e/e2e.mjs, node tests/e2e/e2e-owner.mjs (Multi-Vendor owner mode, an order of two vendors) and node tests/e2e/e2e-vendor.mjs run the HTTP end-to-end tests against it.

Admin templates live in design/backend/templates/addons/kv_conrad/. Do not edit them in a store; they are replaced on update.