Mapping Examples
The erp-toolkit-mapping-examples repository is an open source reference project that demonstrates how to build and test ERP Toolkit mapping configurations using a test-driven development (TDD) approach.
tip
Clone this repo and use it as a starting point for your own mapping configurations. The TDD workflow lets you iterate on mappings locally before deploying to production.
Overview​
The repo contains sample mapping configurations, sample ERP payloads, and integration tests that validate mappings against the mapping simulation API without persisting any data.
| Component | Description |
|---|---|
samples/mapping.*.json | Mapping configurations for different event types |
samples/payload.*.json | Sample ERP payloads that simulate real inbound events |
tests/sample-mappings.test.ts | Integration tests that validate mapping output |
Getting Started​
Prerequisites​
- Node.js (v18+)
- An epilot API token with access to the ERP Integration API
Setup​
# Clone the repo
git clone https://github.com/epilot-dev/erp-toolkit-mapping-examples.git
cd erp-toolkit-mapping-examples
# Install dependencies
npm install
# Configure your API token
cp .env.example .env
# Edit .env and add your EPILOT_API_TOKEN
Run Tests​
# Run all tests once
npm test
# Run tests in watch mode (re-runs on file changes)
npm run test:watch
TDD Workflow​
The repo demonstrates a test-driven approach to mapping development:
- Define your sample payload -- Create a JSON file in
samples/that represents the ERP data you expect to receive - Write the mapping configuration -- Create a mapping JSON file that transforms the payload into epilot entities
- Write a test -- Define the expected entity output and assert against the simulation API response
- Iterate -- Run tests in watch mode, adjust mappings until all tests pass
- Deploy -- Copy the validated mapping configuration into your integration's use case
Each test calls the simulateMappingV2 endpoint from the @epilot/erp-integration-client SDK, which applies the mapping to the payload and returns the transformed entities without persisting anything.
Using AI Coding Agents​
AI coding agents like Claude Code can significantly speed up the mapping development process. When given access to both the epilot entity schema documentation and your ERP's data model documentation, an AI agent can:
- Draft mapping configurations from sample payloads and target entity schemas
- Write JSONata expressions for complex transformations, enum lookups, and conditional logic
- Generate test cases with expected output based on the sample payloads
- Debug failing tests by analyzing the simulation API response and adjusting mappings
For best results, provide the agent with your ERP's API documentation or sample payloads alongside the epilot entity schemas and the examples in this repo.
Example Mappings​
CustomerChanged​
Transforms a customer event into 3 entities: contact, account, and billing_account.
Key patterns demonstrated:
- Multi-value attributes with
_tags(e.g., Primary email, Mobile phone, Billing Address) - Conditional entity creation (
accountonly created whencustomerType = 'business') - SEPA payment data mapping
- Bidirectional entity relations (contact ↔ account)
- Defensive
$exists()checks in JSONata expressions
Sample payload
{
"customerId": "CUST-2024-001",
"customerType": "business",
"firstName": "Max",
"lastName": "Mustermann",
"companyName": "Acme Corporation",
"email": "max.mustermann@acme.de",
"phone": "+49 89 12345678",
"address": {
"street": "Hauptstraße",
"houseNumber": "42",
"zip": "80331",
"city": "München",
"country": "DE"
},
"defaultPayment": {
"iban": "DE89370400440532013000",
"bic": "COBADEFFXXX",
"accountHolder": "Acme Corporation"
}
}
Mapping configuration (excerpt)
{
"entities": [
{
"entity_schema": "contact",
"unique_ids": ["external_id"],
"fields": [
{ "attribute": "external_id", "field": "customerId" },
{ "attribute": "first_name", "field": "firstName" },
{ "attribute": "last_name", "field": "lastName" },
{
"attribute": "email",
"_type": "email",
"_tags": ["Primary"],
"field": "email"
},
{
"attribute": "phone",
"_type": "phone",
"_tags": ["Primary"],
"field": "phone"
}
]
},
{
"entity_schema": "account",
"unique_ids": ["customer_number"],
"condition": "customerType = 'business'",
"fields": [
{ "attribute": "customer_number", "field": "customerId" },
{ "attribute": "name", "field": "companyName" }
]
}
]
}
OrderChanged​
Transforms an order event into 2 entities: contact and order.
Key patterns demonstrated:
- Accessing nested source fields (e.g.,
customer.customerId,customer.email) - Array transformation for order line items with
_tagsper item - Currency amount formatting with
$formatNumber(multiplying by 100 for cents) - Computed fields (e.g.,
item_countvia$count(items))
ContractChanged​
The most complex example. Transforms a power contract event into 4+ entities plus meter readings: contract, meter, meter_counter, and meter readings.
Key patterns demonstrated:
- Array iteration -- Entity-level
jsonataExpressionto iterate over ameter.countersarray, creating onemeter_counterentity per counter register (e.g., HT peak, NT off-peak) - Enum/value mapping -- JSONata lookup table to map ERP meter types (
SMART,IMS,TP) to epilot values (smart-meter,intelligent-measuring-system,two-phase-meter) with a fallback default - Meter readings -- Dedicated
meter_readingsmapping section (separate fromentities), referencing meter and meter counter by unique IDs - Multi-entity relations -- Contract relates to both customer and meter; meter relates back to customer and contract
Array iteration example
Creating one meter_counter entity per item in the meter.counters array:
{
"entity_schema": "meter_counter",
"jsonataExpression": "meter.counters",
"unique_ids": ["external_id"],
"fields": [
{ "attribute": "external_id", "field": "counterId" },
{ "attribute": "obis_number", "field": "obisCode" },
{ "attribute": "direction", "field": "direction" }
]
}
Enum mapping example
Translating ERP meter type codes to epilot values using a JSONata lookup:
{
"attribute": "type",
"jsonataExpression": "$lookup({'SMART':'smart-meter','IMS':'intelligent-measuring-system','TP':'two-phase-meter','MME':'modern-measuring-equipment'}, meter.meterType) ~> $default('analog')"
}
Meter readings mapping example
Meter readings use a separate top-level meter_readings section:
{
"meter_readings": [
{
"jsonataExpression": "meter.readings",
"meter": {
"unique_ids": ["external_id"]
},
"fields": [
{ "attribute": "external_id", "field": "$$.meter.meterId" },
{ "attribute": "reading_date", "field": "readingDate" },
{ "attribute": "value", "field": "value" },
{ "attribute": "reason", "field": "reason" }
]
}
]
}
Test Structure​
Tests use Vitest and follow a consistent pattern:
import { simulateMappingV2 } from './helpers';
describe('CustomerChanged', () => {
it('should map to contact entity', async () => {
const mapping = require('../samples/mapping.CustomerChanged.json');
const payload = require('../samples/payload.customer.json');
const response = await simulateMappingV2(mapping, payload);
const contact = response.entity_updates.find(
(e) => e.entity_schema === 'contact'
);
expect(contact).toMatchObject({
entity_schema: 'contact',
fields: expect.objectContaining({
first_name: 'Max',
last_name: 'Mustermann',
}),
});
});
});
Each test:
- Loads a mapping configuration and sample payload from
samples/ - Calls
simulateMappingV2()to dry-run the mapping against the simulation API - Finds the relevant entity in the response
- Asserts the expected output with
toMatchObject()
CI Pipeline​
The repo includes a GitHub Actions workflow (.github/workflows/test.yml) that runs all tests on every push and pull request. The EPILOT_API_TOKEN is stored as a GitHub Actions secret.
Key Mapping Concepts​
| Concept | Description | Example |
|---|---|---|
| Field mapping | Map a source field to an entity attribute | { "attribute": "first_name", "field": "firstName" } |
| JSONata expression | Compute a value using JSONata | { "attribute": "full_name", "jsonataExpression": "firstName & ' ' & lastName" } |
| Unique identifiers | Match existing entities for upsert | "unique_ids": ["customer_number"] |
| Conditional creation | Only create entity when condition is met | "condition": "customerType = 'business'" |
| Multi-value attributes | Arrays with tags for email, phone, address | "_type": "email", "_tags": ["Primary"] |
| Array iteration | Create multiple entities from an array | "jsonataExpression": "meter.counters" on entity level |
| Enum mapping | Translate ERP codes to epilot values | $lookup({...}, sourceField) in JSONata |
| Entity relations | Link entities using $relation | "$relation": [{ "entity_schema": "contact", ... }] |
| Meter readings | Dedicated mapping section for readings | Top-level "meter_readings" array in config |
Next Steps​
- Fork the repo and add your own mappings
- Read the Mapping Configuration guide for the full mapping syntax reference
- Use the mapping simulation endpoint to test mappings interactively
- See the Inbound Examples for additional mapping patterns