Transformer Contract¶
Reference for the pluggable transformation and storage pipeline introduced in CFX-46. This covers the interface every platform transformer must implement, the result value objects, and the unified Eloquent models.
Interface: TransformerContract¶
Namespace: App\Contracts\TransformerContract
Accepts a Collection<int, CustomerData|OrderData|ProductData> (the output of an integration's requestBulkExport()) and a SystemConnection. Persists each item to the unified Eloquent models. Returns a TransformationResult summarising successes and failures.
The method never throws — any exception raised while storing an individual item is captured and included in the result.
Abstract Base Class: AbstractTransformer¶
Namespace: App\Services\AbstractTransformer
Implements TransformerContract. Concrete transformers extend this class and implement store().
transform() is declared final. It iterates over the data collection, calls store() for each item, and catches any Throwable per item — meaning a failure on one record does not abort the rest of the batch. The result carries the full failure list so callers can log or retry specific items.
abstract protected function store(CustomerData|OrderData|ProductData $item, SystemConnection $connection): void;
This is the only method a concrete transformer must implement.
Result Value Objects¶
TransformationResult¶
Namespace: App\Data\TransformationResult
| Property | Type | Description |
|---|---|---|
succeeded |
int |
Number of items stored successfully |
failed |
int |
Number of items that raised an exception |
failures |
TransformationFailure[] |
One entry per failed item |
TransformationFailure¶
Namespace: App\Data\TransformationFailure
| Property | Type | Description |
|---|---|---|
item |
CustomerData\|OrderData\|ProductData |
The DTO that could not be stored |
reason |
Throwable |
The exception raised during store() |
Unified Eloquent Models¶
These models are the persistent representation of normalised platform data inside Conflux. All use HasUuids and SoftDeletes.
Customer¶
Namespace: App\Models\Customer
Table: customers
| Column | Type | Description |
|---|---|---|
id |
uuid |
Conflux-assigned primary key |
system_connection_id |
uuid FK |
The system connection that produced this record |
source_id |
string |
System-native customer ID (dedup key) |
source_custom_id |
?string |
Secondary system identifier (e.g. internal CRM ID) |
source_platform |
string |
Cast to SystemDriver enum |
email |
string |
— |
first_name |
?string |
— |
last_name |
?string |
— |
phone |
?string |
— |
gender |
?string |
Cast to Gender enum |
title |
?string |
— |
zip |
?string |
— |
date_of_birth |
?string |
ISO 8601 date string |
address_billing |
?json |
Serialised AddressData |
address_shipping |
?json |
Serialised AddressData |
tags |
?json |
Array of tag strings |
order_count |
?int |
Total number of orders on the source platform |
total_revenue |
?int |
Lifetime revenue in minor currency units |
external_data |
?json |
Platform-specific key/value pairs from customAttributes |
Unique constraint: (system_connection_id, source_id)
Relations: systemConnection() → BelongsTo<SystemConnection>, consents() → HasMany<CustomerConsent>, orders() → HasMany<Order>
CustomerConsent¶
Namespace: App\Models\CustomerConsent
Table: customer_consents
| Column | Type | Description |
|---|---|---|
id |
uuid |
Primary key |
customer_id |
uuid FK |
Owning customer |
channel |
string |
Cast to ConsentChannel enum (email, sms, general) |
subscribed |
bool |
Whether the customer is subscribed |
confirmed |
bool |
Whether the subscription is confirmed (double opt-in) |
subscribed_at |
?string |
ISO 8601 datetime string |
confirmed_at |
?string |
ISO 8601 datetime string |
unsubscribed_at |
?string |
ISO 8601 datetime string |
source |
?string |
Cast to ConsentSource enum |
ip_address |
?string |
— |
Unique constraint: (customer_id, channel) — one consent record per channel per customer.
Relations: customer() → BelongsTo<Customer>
Order¶
Namespace: App\Models\Order
Table: orders
| Column | Type | Description |
|---|---|---|
id |
uuid |
Conflux-assigned primary key |
system_connection_id |
uuid FK |
The system connection that produced this record |
customer_id |
?uuid FK |
Linked Customer (nullable) |
source_id |
string |
System-native order ID (dedup key) |
source_order_number |
?string |
Human-readable order number on the system |
source_platform |
string |
Cast to SystemDriver enum |
customer_source_id |
?string |
System-native ID of the associated customer |
status |
string |
System-native status string (not normalised) |
currency |
string |
ISO 4217 currency code |
total |
int |
Grand total in minor currency units |
total_tax |
?int |
Tax portion in minor currency units |
total_shipping |
?int |
Shipping portion in minor currency units |
address_billing |
?json |
Serialised AddressData |
address_shipping |
?json |
Serialised AddressData |
tags |
?json |
Array of tag strings |
date_paid |
?string |
ISO 8601 datetime string |
external_data |
?json |
System-specific key/value pairs from customAttributes |
Unique constraint: (system_connection_id, source_id)
Relations: systemConnection() → BelongsTo<SystemConnection>, customer() → BelongsTo<Customer>, lineItems() → HasMany<LineItem>
LineItem¶
Namespace: App\Models\LineItem
Table: line_items
| Column | Type | Description |
|---|---|---|
id |
uuid |
Primary key |
order_id |
uuid FK |
Owning order |
product_id |
?uuid FK |
Resolved Product — null if the product has not been synced |
source_id |
string |
System-native line item ID (dedup key within the order) |
product_source_id |
string |
System-native product ID used to resolve product_id |
name |
string |
Product name at time of order |
quantity |
int |
— |
unit_price |
int |
Price per unit in minor currency units |
total |
int |
Line total in minor currency units |
product_custom_id |
?string |
Secondary system product identifier |
sku |
?string |
— |
currency |
?string |
ISO 4217 currency code |
Unique constraint: (order_id, source_id)
Relations: order() → BelongsTo<Order>, product() → BelongsTo<Product>
product_idis resolved at import time by looking upProductwith(system_connection_id, product_source_id). If the product has not been synced yet,product_idremains null until the order is re-imported after the product sync.
Product¶
Namespace: App\Models\Product
Table: products
| Column | Type | Description |
|---|---|---|
id |
uuid |
Conflux-assigned primary key |
system_connection_id |
uuid FK |
The system connection that produced this record |
source_id |
string |
System-native product ID (dedup key) |
source_custom_id |
?string |
Secondary system identifier |
source_platform |
string |
Cast to SystemDriver enum |
name |
string |
— |
description |
?text |
— |
url |
?string |
Product page URL on the system |
image_url |
?string |
Primary product image URL |
status |
string |
Cast to ProductStatus enum |
sku |
?string |
— |
price |
?int |
Current price in minor currency units |
original_price |
?int |
Pre-discount price in minor currency units |
currency |
?string |
ISO 4217 currency code |
brand_name |
?string |
— |
target_group |
?string |
— |
categories |
?json |
Array of category strings |
tags |
?json |
Array of tag strings |
order_count |
?int |
Number of times this product has been ordered |
external_data |
?json |
System-specific key/value pairs from customAttributes |
Unique constraint: (system_connection_id, source_id)
Relations: systemConnection() → BelongsTo<SystemConnection>
Default store() Implementation¶
AbstractTransformer provides a concrete store() method that handles all three entity types via a match expression. It uses updateOrCreate keyed on (system_connection_id, source_id) so that re-running a bulk export updates existing records rather than creating duplicates.
Platform-specific transformers extend AbstractTransformer and override store() only when they need behaviour that differs from the default. Until that need arises, a minimal subclass with no overrides is sufficient.
TransformerRegistry¶
Namespace: App\Services\TransformerRegistry
A singleton that maps each SystemDriver to a concrete transformer class. It mirrors IntegrationRegistry and is the only mechanism by which Conflux locates the correct transformer at runtime.
public function register(SystemDriver $driver, string $transformer): void
public function resolve(SystemDriver $driver): AbstractTransformer
register()— associates aSystemDrivercase with aclass-string<AbstractTransformer>. Called once per platform inAppServiceProvider::boot().resolve()— returns an instance of the registered transformer, resolved via the service container (app($class)). ThrowsRuntimeExceptionif no transformer has been registered for the given driver.
The registry is bound as a singleton in AppServiceProvider::register():
PersistBulkDataJob receives the registry via constructor injection and calls resolve() at job execution time to obtain the transformer for the connection's system driver.
Registration is required
A transformer that is not registered in TransformerRegistry cannot be resolved. Any PersistBulkDataJob dispatched for its system driver will throw a RuntimeException at execution time. Every transformer implementation must be registered — see Implement a System Transformer.
PersistBulkDataJob¶
Namespace: App\Jobs\PersistBulkDataJob
Queue connection: rabbitmq
The job that bridges the bulk-fetch stage and the persistence stage. BulkSyncJob dispatches it after each successful requestBulkExport() call when the result is non-empty.
public function __construct(
public readonly Collection $data, // Collection<int, CustomerData|OrderData|ProductData>
public readonly SystemConnection $systemConnection,
) {}
On execution, it resolves the correct transformer from TransformerRegistry using the system driver of the associated SystemConnection, then calls transform(). If the connection has no associated system, the job exits silently.
The job runs on the rabbitmq connection, which places it on a different queue from BulkSyncJob (which runs on the default redis-backed queue). This separates the fetch stage from the persistence stage and allows them to scale independently.