# Order Export & Tracking Import Documentation

Installation steps and a complete reference for every profile setting, mapping option, the automatic tracking import and the unified log, with concrete examples — the same level of detail our support uses to help customers.

## Installation

### Requirements

Magento 2.4.x — tested on 2.4.9, compatible with previous 2.4.* releases. PHP 8.1–8.5. Requires `codingrow/module-core` (installed automatically as a Composer dependency) for the shared Codingrow admin menu and license validation.

### Setup steps

1. Add the credentials you'll receive by email to `auth.json` in your Magento project root:
   ```json
   { "http-basic": { "repo.codingrow.com": { "username": "...", "password": "..." } } }
   ```
2. `composer config repositories.codingrow composer https://repo.codingrow.com`
3. `composer require codingrow/module-oeti`
4. `bin/magento module:enable Codingrow_Oeti`
5. `bin/magento setup:upgrade && bin/magento setup:di:compile && bin/magento cache:flush`
6. Paste your license key into **Admin → Stores → Configuration → Codingrow → Order Export & Tracking Import → License → License Key**, save, then `bin/magento cache:flush`.
7. Under the same section's **General** group, set **Enable module** to "Yes" — the module ships enabled at the Magento level but functionally off, so it never exports anything until you explicitly turn it on.
8. Create your first profile under **Codingrow → Order Export & Tracking Import → Export Profiles** — see [Profiles](#profiles) below.

The whole profile admin, mapping builder, log and every notification email are available in 7 languages, auto-selected per admin user locale.

### Uninstallation

`composer remove codingrow/module-oeti` then `bin/magento setup:upgrade && bin/magento setup:di:compile && bin/magento cache:flush`. Profiles and the export/tracking log (`codingrow_oeti_profile`, `codingrow_oeti_log`, `codingrow_oeti_tracking_log`) are never deleted automatically — use [Export profile](#import-export-profile) first if you want to keep a copy of your profile configuration.

## User guide

### Profiles

Everything in the module is organized around **profiles**, listed under **Codingrow → Order Export & Tracking Import → Export Profiles**. Each profile is completely independent: its own name, its own enabled/disabled switch, its own trigger status(es), its own destination (channel + format), its own field mapping and its own row in the export log — you can run any number of profiles side by side (one per supplier, one per carrier, one for an internal ERP), and disabling or deleting one never affects the others.

A profile that is disabled (**Profile enabled** = "No") never exports anything, whether triggered automatically, from the manual action, or from the CLI command — this is independent from the module-wide **Enable module** switch under System Config, which acts as a single master off-switch for every profile at once.

### Channels & formats

Every profile picks one **channel** (where the payload is delivered) and one **format** (how it's serialized) — the two choices are independent, so any format works with any channel.

| Channel | Typical use | Key settings |
|---|---|---|
| **REST API** | Sends the payload as an HTTP request to a webservice | Destination URL, HTTP method (GET/POST/PUT/PATCH), authentication (None, HTTP Basic, Bearer token), minimum seconds between requests (rate limiting, 1 second by default) |
| **FTP / SFTP** | Uploads the payload as a file to a remote server | Host, port (21 by default), username, password, remote path, optional FTPS (explicit TLS), passive mode |
| **Local file** | Writes the payload to a file under this store's own `pub/media/`, for an external system to pick up over HTTP | Subfolder — the file lands under `pub/media/codingrow_oeti/<subfolder>/` (default subfolder: "default") |

Formats: **JSON**, **XML** or **CSV**, chosen independently of the channel above — the same field mapping (see below) drives all three, the format only changes how it's serialized.

### Field mapping

The Mapping tab is a table of rows, each connecting a **target** (the field name in the output — a dot means one level of nesting, e.g. `customer.email`) to a **source path** chosen from a dropdown of every available order/customer/item field. Empty rows are ignored on save.

Rows are read top to bottom in the same shape as the output itself: an optional **Header** template first, then the order-level mapping rows, then any **Repeat blocks** (see below), and an optional **Footer** template last.

### Transformations

Each mapping row can apply one transformation to the source value before it's written to the target field:

| Transform | What it does |
|---|---|
| **None** | Direct copy of the source path's value (falls back to "Default value" when the source is empty). |
| **Static value** | Ignores the source entirely, always writes the fixed text in "Value". |
| **Strip HTML tags** | Removes HTML markup from the source value — useful for a product name or description that carries residual formatting. |
| **Text template** | Free text with placeholders, e.g. `{order.shipping_address.firstname} {order.shipping_address.lastname}`. A function is also available inside a placeholder: `striphtml{path}` (same as the Strip HTML tags transform, usable inline) and `substr{path,N}` (drops the first N characters, e.g. `substr{order.some_code,3}`). |
| **Search and replace** | Looks for the "Search" text inside the source value (case-insensitive) and replaces it with "Value" exactly as given. |
| **Number** | Serializes the value as a real JSON number instead of a quoted string — use it when the destination validates the field's TYPE strictly (a quantity sent as the database-raw string `"1.0000"` gets rejected by some APIs, while `1.0` as a real number is accepted). This is **not** the default for numeric-looking fields on purpose: turning it on everywhere would silently strip leading zeros from things like a postcode or an article code, so it's an opt-in choice per row, not an automatic behaviour. |

The same `{path}` / `striphtml{}` / `substr{}` syntax used in Text template rows is also available in Repeat block rows and in the profile's optional Header/Footer.

### Repeat blocks

A repeat block builds a nested array in the output — the most common case is one entry per order item. Each block has a **Block output key** (where the array is written in the output) and a **source collection** (what it repeats over, e.g. the order's items), plus its own set of mapping rows underneath, evaluated once per element with "current" scoped to that element — so a row's source path inside a repeat block refers to fields of the current item, not the order as a whole.

**Example.** A block with output key `items`, source collection "Order items", and two rows (`sku` ← current item SKU, `qty` ← current item quantity) produces:

```json
{
  "items": [
    { "sku": "43241", "qty": 2 }
  ]
}
```

A profile can define any number of independent repeat blocks — for example one for the item list and a separate one for a list of applied discounts.

### Trigger conditions

The General tab has a nested AND/OR condition builder — the same tree-style interface as Magento's own Cart Price Rules (an "Add" link on every group to add a condition or a nested sub-group, a remove icon on any node) — that decides **both** which order rows a profile actually exports **and** whether the profile exports the order at all. Each condition compares a product attribute (e.g. `sku`, a custom "supplier code" attribute, `price`...) against a value, with an operator: equal to/not equal to, contains/does not contain, starts with/ends with, greater than/less than (or equal to), is empty/is not empty.

Conditions inside a group combine with ALL (AND) or ANY (OR), and groups can nest inside groups — for example `sku contains "ABC" OR (supplier_code = "Supplier2" AND price > 10)`. No conditions configured = no filter, every row is exported (same as the old "Only supplier items" = "No" before version 1.3.0).

**Typical use with several dropshipping suppliers**: one profile per supplier, each with its own condition (e.g. Profile A: `supplier_code = "Supplier1"`, Profile B: `supplier_code = "Supplier2"`) — an order containing items from both suppliers correctly triggers both profiles in parallel, each one exporting only the rows that belong to it. If no row matches a given profile's conditions, that profile simply does not export that order (see the "Skipped" log status below) — not an error.

Every attribute used in a condition automatically becomes available as a mappable field in the payload (`items.attributes.<code>`, see Field mapping above). When the conditions exclude rows from an order, the [live preview](#preview) shows an explicit warning naming how many rows were left out and why — so an empty or shorter-than-expected payload is never a silent surprise.

### Live preview

The Preview button on the profile edit page builds a temporary, never-saved profile from whatever is currently in the form — including changes you haven't saved yet — and runs it against a real order you pick, through the exact same code path used for a genuine export. The result is the literal JSON/XML/CSV payload that order would produce, so you can correct the mapping before ever activating the profile, instead of finding out from a failed real export.

### Trigger & deduplication

The **Trigger status** multiselect on the General tab lists which order status(es) start an automatic export for that profile (Ctrl/Cmd-click to select more than one) — checked only on the automatic, event-driven trigger; the manual grid action and the CLI command ignore it on purpose, since triggering by hand is itself the deliberate choice to export regardless of status. The manual "Export via Codingrow Order Export" action on the Sales > Orders grid runs every enabled profile against the selected orders in one click — each profile's own Trigger conditions decide what it actually sends, exactly like the automatic trigger.

Every attempt is written to that profile's log (see below), and the log is also what prevents duplicates: the same order is never exported twice automatically by the same profile, whatever the order does afterwards (further edits, status changes back and forth). If a genuine resend is needed — after fixing a destination-side issue, for example — use [Force resend](#log), which explicitly bypasses this protection.

### Tracking import

A per-profile **Tracking Import** tab (off by default) turns on a periodic poll that asks your supplier which items have shipped and, for those items, creates a native Magento shipment with its tracking number — independently per profile, reusing the profile's own [Trigger conditions](#trigger-conditions) to know which items of the order belong to it.

- **Reference field** — which already-mapped field holds the supplier's order reference. It is captured automatically on every export attempt (successful *or* failed), so tracking still works even when the order already existed on the supplier's side (for example, entered by hand before the automatic export). No extra send-side configuration.
- **Item matching attribute** — which product attribute identifies an item for the supplier, usually the same one used in the Trigger conditions (e.g. a "supplier code"). Suppliers report their own items by their own code, not the Magento SKU, so the module has to be told which attribute to compare.
- **Verification endpoint** — URL, authentication and frequency (in minutes), plus the mapping of the supplier's response: where to read the reference, the tracking number, the tracking URL (if any), the carrier and the list of shipped items.

**Partial shipments.** If an order has items from several suppliers (or one supplier ships in more than one wave), each check creates a shipment only for the items actually reported as shipped at that moment, never beyond what is still left to ship — so an order legitimately ending up with several shipments over time is the intended behaviour, not a bug.

**Customer email.** When the supplier provides a clickable tracking link (a URL, not just a number), the customer receives an email with a **"Track your package"** button instead of the plain native Magento email (which would show only the number, with no clickable link for a carrier Magento doesn't recognise natively). When the supplier provides no URL, the standard native email is used, unchanged.

No manual action is needed in normal operation: the check runs on its own in the background at the configured frequency (Magento cron must be running, as for any scheduled task).

### Log & Force resend

From version 1.4.0 the log is no longer split per profile (a Log tab inside each profile). A single **Log** page (button on the Export Profiles grid) lists **orders** — not individual attempts — one per row, paginated, with the latest export status and the latest tracking status side by side: a single glance at an order even when several profiles/suppliers are involved.

Clicking **View full history** opens that order's complete history, split into two sections — **Export log** and **Tracking log** — each listing every attempt/check from every profile involved, with an expandable "Details" row (exact request payload sent, response body received) and, on the export side, a per-row Force resend button.

On the export side an attempt is Success, Error or **Skipped**. **Skipped** is not an error: no order item matched that profile's Trigger conditions, so nothing was sent — useful with several profiles/suppliers to see at a glance why a given profile did not export a given order. On the tracking side, a failed check usually just means the supplier hasn't reported anything shipped for those items yet; it is retried automatically on the next poll, with no manual action needed.

A failed or skipped row's **Force resend** (export side only) re-runs the export for that exact order through that exact profile right now, ignoring the deduplication protection described above — a confirmation dialog spells this out before it runs, since it's a deliberate override. It is available both from the main Log page and from the order detail. Tracking has no manual "force check" equivalent: the next scheduled poll simply tries again.

### Failure notification emails

Two General-tab settings control failure notifications, independent of the log (which always records every attempt regardless of these settings): **Notification recipients** (one or more email addresses, comma-separated — leave empty to send nothing) and **Send email for** (which events trigger a notification).

The email body includes the actual error returned by the destination, not just a generic HTTP status line: when the response body is parseable JSON with a `message` field, that message is appended verbatim (e.g. "Destination returned HTTP 422 - Minimum product quantity is 1.0"); otherwise the raw response body is included, truncated to 500 characters — so the real cause is visible straight from the inbox, without opening the log.

### Import / Export profile

The **Export profile** button on a profile's edit page downloads its entire configuration — general settings, destination, mapping rows and repeat blocks — as a single JSON file, with destination **credentials deliberately excluded** so the file is safe to store, share with support, or commit alongside a deployment. The **Import profile** button on the Export Profiles grid re-creates a profile from such a file — the typical use is moving a profile from a staging/demo store to production, or keeping a backup of a mapping you're happy with before experimenting further. Credentials always have to be re-entered by hand after an import, since they were never in the file to begin with.

### License

One license per domain, entered under **Admin → Stores → Configuration → Codingrow → Order Export & Tracking Import → License → License Key**. Includes all updates for that domain. Need to move to a different domain (staging → production, or a site migration)? Your first domain change is free and instant from your account page — no waiting, no ticket needed. From the second change onward, a change is allowed once a year. You can also request a refund within 30 days of purchase, on your own, from the same account page.
