Integration Contract¶
Reference for the integration communication contract. This covers the interface every platform integration must implement, the request/response DTOs, and the exceptions. The contract covers connection verification (CFX-53), outbound bulk exports (CFX-45), and inbound webhook processing (CFX-48).
Interface: IntegrationContract¶
Namespace: App\Contracts\IntegrationContract
public function verify(SystemConnection $connection): VerificationResult;
public function requestBulkExport(SystemConnection $connection, ExportRequest $request): Collection;
/** @param Closure(Collection<int, CustomerData|OrderData|ProductData>): void $onBatch */
public function streamBulkExport(SystemConnection $connection, ExportRequest $request, Closure $onBatch): void;
public function probeBulkExport(SystemConnection $connection, ExportRequest $request): BulkExportProbe;
/** @param Closure(Collection<int, CustomerData|OrderData|ProductData>): void $onBatch */
public function fetchBulkExportPageRange(SystemConnection $connection, ExportRequest $request, int $startPage, int $endPage, int $perPage, Closure $onBatch): void;
/** @param array<string, mixed> $payload */
public function handleWebhook(SystemConnection $connection, array $payload): void;
/** @return class-string<TransformerContract> */
public function transformerClass(): string;
verify()— authenticates the connection's credentials against the live system and resolves the external user/account identifier. Never throws; failures are reported through the returnedVerificationResult.requestBulkExport()— returns aCollection<int, CustomerData|OrderData|ProductData>containing every fetched record. ThrowsIntegrationRequestException(or a subclass) on any integration-layer failure. Built on top ofstreamBulkExport().streamBulkExport()— invokes$onBatchonce per fetched page, without accumulating the full result set in memory. ThrowsIntegrationRequestException(or a subclass).probeBulkExport()— returns the total page count and page size without fetching any records, so the job layer can fan out parallelfetchBulkExportPageRange()calls. ThrowsIntegrationRequestException(or a subclass).fetchBulkExportPageRange()— fetches a specific[startPage, endPage]range, invoking$onBatchonce per page. ThrowsIntegrationRequestException(or a subclass).handleWebhook()— processes an inbound webhook payload for the given connection. ThrowsIntegrationRequestException(or a subclass) on any integration-layer failure.transformerClass()— returns theTransformerContractimplementation responsible for persisting this integration's fetched data.
Abstract Base Class: AbstractIntegration¶
Namespace: App\Services\AbstractIntegration
Implements IntegrationContract. Concrete integrations extend this class and implement the abstract/overridable methods below.
verify(), requestBulkExport(), streamBulkExport(), probeBulkExport(), fetchBulkExportPageRange(), handleWebhook(), and transformerClass() are all declared final on AbstractIntegration — concrete integrations never override them directly. The bulk-export and webhook methods wrap their integration-provided counterparts in the same error-handling strategy: any Throwable that is not already an IntegrationRequestException is caught and re-thrown as InvalidIntegrationResponseException. verify() uses a different strategy — see below.
performVerification() (abstract, required)¶
Every concrete integration must implement this method. It performs the integration-specific credential check against the live system and resolves the external user/account identifier consumed by the Connection Flow. Return VerificationResult::success($externalUserId) on success. On failure, either return VerificationResult::failure($message) explicitly, or simply throw — verify() catches any Throwable and converts it into a failed VerificationResult, so an integration can rely on $response->throw() as its failure path without any local try/catch.
fetchBulkExportBatches() (abstract, required)¶
/** @param Closure(Collection<int, CustomerData|OrderData|ProductData>): void $onBatch */
abstract protected function fetchBulkExportBatches(SystemConnection $connection, ExportRequest $request, Closure $onBatch): void;
Every concrete integration must implement this method. It fetches records from the external system in pages, calling $onBatch once per non-empty page — page size and pagination strategy are entirely up to the integration. Called by streamBulkExport()/requestBulkExport().
provideTransformerClass() (abstract, required)¶
/** @return class-string<TransformerContract> */
abstract protected function provideTransformerClass(): string;
Every concrete integration must implement this method, returning the AbstractTransformer subclass responsible for persisting the DTOs this integration produces. Backs the public, sealed transformerClass().
performProbeBulkExport() (virtual, optional)¶
protected function performProbeBulkExport(SystemConnection $connection, ExportRequest $request): BulkExportProbe;
Override to report the total page count and page size without fetching any records, enabling BulkSyncJob to fan out parallel fetchBulkExportPageRange() jobs. The default returns new BulkExportProbe(totalPages: 1, perPage: 0), so performFetchBulkExportPageRange() falls back to a single fetchBulkExportBatches() call.
performFetchBulkExportPageRange() (virtual, optional)¶
/** @param Closure(Collection<int, CustomerData|OrderData|ProductData>): void $onBatch */
protected function performFetchBulkExportPageRange(SystemConnection $connection, ExportRequest $request, int $startPage, int $endPage, int $perPage, Closure $onBatch): void;
Override alongside performProbeBulkExport() to fetch a specific page range, calling $onBatch once per page. The default ignores the range and delegates to fetchBulkExportBatches(), which is correct for integrations whose probe always returns a single page.
processWebhook() (virtual, optional)¶
/** @param array<string, mixed> $payload */
protected function processWebhook(SystemConnection $connection, array $payload): void;
Override this method to handle inbound webhooks. The default implementation throws BadMethodCallException. Only integrations that receive webhooks need to implement it.
Verification DTO: VerificationResult¶
Namespace: App\Data\VerificationResult
Plain readonly class — not a Spatie\LaravelData\Data object, so it is never serialised directly and has no SnakeCaseMapper. Construct it via the named constructors rather than new:
public static function success(string $externalUserId): self;
public static function failure(?string $message = null): self;
| Property | Type | Required | Description |
|---|---|---|---|
successful |
bool |
Yes | Whether the credentials verified against the live system |
externalUserId |
?string |
No | External account/user identifier resolved during verification; stored on SystemConnection.external_user_id on success |
message |
?string |
No | Failure reason. Not currently read by any caller — RegisterSystemConnectionController responds with a fixed 422 message regardless of this value |
Request DTO: ExportRequest¶
Namespace: App\Data\ExportRequest
| Property | Type | Required | Description |
|---|---|---|---|
capability |
SystemCapability |
Yes | The data type to export |
systemConnectionId |
string |
Yes | UUID of the SystemConnection model |
modifiedAfter |
?string |
No | ISO 8601 timestamp; when set, the integration should return only records modified after this point. null means fetch all records (full sync). See Delta Sync. |
SystemCapability enum¶
| Case | Value | Description |
|---|---|---|
Products |
products |
Product catalogue data |
Orders |
orders |
Order data |
Customers |
customers |
Customer/contact data |
Response DTO: BulkExportProbe¶
Namespace: App\Data\BulkExportProbe
Plain class (not a Spatie\LaravelData\Data object), returned by probeBulkExport().
| Property | Type | Description |
|---|---|---|
totalPages |
int |
Total number of pages available for this export |
perPage |
int |
Page size used by the integration's pagination |
Response DTOs¶
All DTOs use Spatie\LaravelData\Data and the SnakeCaseMapper — property names are camelCase in PHP and snake_case when serialised.
Monetary values (price, originalPrice, unitPrice, total, totalTax, totalShipping, totalSpent) are always integers in minor currency units (e.g. cents). A price of €12.99 is stored as 1299.
Datetime fields (createdAt, updatedAt, subscribedAt, etc.) are strings — integrations are responsible for formatting them consistently (ISO 8601 recommended).
CustomerData¶
Namespace: App\Data\CustomerData
| Property | Type | Required | Description |
|---|---|---|---|
sourceId |
string |
Yes | Platform-native customer ID (dedup key) |
sourceSystem |
SystemDriver |
Yes | The originating system |
email |
string |
Yes | Customer email address |
createdAt |
string |
Yes | Record creation timestamp |
updatedAt |
string |
Yes | Record last-updated timestamp |
tags |
string[] |
Yes (default []) |
Arbitrary labels |
customAttributes |
Collection<int, CustomAttributeData> |
Yes (default empty) | Platform-specific key/value metadata |
id |
?string |
No | Conflux-assigned UUID (null until persisted) |
sourceCustomId |
?string |
No | Secondary platform ID (e.g. external CRM reference) |
firstName |
?string |
No | — |
lastName |
?string |
No | — |
phone |
?string |
No | — |
gender |
?Gender |
No | See Gender enum below |
title |
?string |
No | Salutation or academic title |
zip |
?string |
No | Postal code (shorthand field) |
dateOfBirth |
?string |
No | — |
newsletterConsent |
?ConsentData |
No | See ConsentData below |
addressBilling |
?AddressData |
No | See AddressData below |
addressShipping |
?AddressData |
No | See AddressData below |
orderCount |
?int |
No | Lifetime order count |
totalSpent |
?int |
No | Lifetime spend in minor currency units |
ProductData¶
Namespace: App\Data\ProductData
| Property | Type | Required | Description |
|---|---|---|---|
sourceId |
string |
Yes | Platform-native product ID (dedup key) |
sourceSystem |
SystemDriver |
Yes | The originating system |
name |
string |
Yes | Product name |
status |
ProductStatus |
Yes | See ProductStatus enum below |
createdAt |
string |
Yes | — |
updatedAt |
string |
Yes | — |
categories |
string[] |
Yes (default []) |
Category names |
tags |
string[] |
Yes (default []) |
Arbitrary labels |
customAttributes |
Collection<int, CustomAttributeData> |
Yes (default empty) | Platform-specific metadata |
id |
?string |
No | Conflux-assigned UUID |
sourceCustomId |
?string |
No | Secondary platform ID |
sku |
?string |
No | Stock-keeping unit |
description |
?string |
No | — |
url |
?string |
No | Public product URL |
imageUrl |
?string |
No | Primary image URL |
price |
?int |
No | Current price in minor currency units |
originalPrice |
?int |
No | Pre-discount price in minor currency units |
currency |
?string |
No | ISO 4217 currency code |
brandName |
?string |
No | — |
targetGroup |
?string |
No | Platform-defined audience segment |
orderCount |
?int |
No | Number of times ordered |
OrderData¶
Namespace: App\Data\OrderData
| Property | Type | Required | Description |
|---|---|---|---|
sourceId |
string |
Yes | Platform-native order ID (dedup key) |
sourceSystem |
SystemDriver |
Yes | The originating system |
status |
string |
Yes | Platform-native status string |
currency |
string |
Yes | ISO 4217 currency code |
total |
int |
Yes | Order grand total in minor currency units |
createdAt |
string |
Yes | — |
updatedAt |
string |
Yes | — |
lineItems |
Collection<int, LineItemData> |
Yes (default empty) | See LineItemData below |
tags |
string[] |
Yes (default []) |
Arbitrary labels |
customAttributes |
Collection<int, CustomAttributeData> |
Yes (default empty) | Platform-specific metadata |
id |
?string |
No | Conflux-assigned UUID |
sourceOrderNumber |
?string |
No | Human-readable order number (distinct from sourceId) |
customerId |
?string |
No | Conflux UUID of the linked customer |
customerSourceId |
?string |
No | Platform-native ID of the linked customer |
totalTax |
?int |
No | Tax amount in minor currency units |
totalShipping |
?int |
No | Shipping cost in minor currency units |
addressBilling |
?AddressData |
No | — |
addressShipping |
?AddressData |
No | — |
datePaid |
?string |
No | Payment timestamp |
Sub-DTOs¶
ConsentData¶
Namespace: App\Data\ConsentData
| Property | Type | Required | Description |
|---|---|---|---|
subscribed |
bool |
Yes | Whether the contact has opted in |
confirmed |
bool |
Yes | Whether the opt-in has been confirmed (e.g. double opt-in) |
channel |
ConsentChannel |
Yes | The consent channel |
subscribedAt |
?string |
No | Timestamp of initial subscription |
confirmedAt |
?string |
No | Timestamp of confirmation |
unsubscribedAt |
?string |
No | Timestamp of opt-out |
source |
?ConsentSource |
No | How consent was collected |
ipAddress |
?string |
No | IP address at time of consent |
AddressData¶
Namespace: App\Data\AddressData
All fields are optional (?string).
| Property | Description |
|---|---|
firstName |
— |
lastName |
— |
company |
— |
address1 |
Primary street line |
address2 |
Secondary street line (apartment, suite, etc.) |
city |
— |
region |
State, province, or region |
postalCode |
— |
country |
ISO 3166-1 alpha-2 recommended |
phone |
— |
email |
— |
vatId |
VAT identification number |
CustomAttributeData¶
Namespace: App\Data\CustomAttributeData
Represents a single platform-specific attribute. Serialises as an object in a JSON array: [{"key": "shop_id", "value": "7"}, ...].
| Property | Type | Required | Description |
|---|---|---|---|
key |
string |
Yes | Attribute name — namespace to avoid collisions (e.g. woocommerce_loyalty_points) |
value |
string\|int\|bool\|null |
Yes | Attribute value |
LineItemData¶
Namespace: App\Data\LineItemData
| Property | Type | Required | Description |
|---|---|---|---|
sourceId |
string |
Yes | Platform-native line item ID |
productSourceId |
string |
Yes | Platform-native ID of the linked product |
name |
string |
Yes | Product name at time of purchase |
quantity |
int |
Yes | Units ordered |
unitPrice |
int |
Yes | Price per unit in minor currency units |
total |
int |
Yes | Line total in minor currency units (quantity × unitPrice after discounts) |
productId |
?string |
No | Conflux UUID of the linked product |
productCustomId |
?string |
No | Secondary platform ID of the linked product |
sku |
?string |
No | Stock-keeping unit |
currency |
?string |
No | ISO 4217 currency code |
Enums¶
SystemDriver¶
| Case | Value |
|---|---|
WooCommerce |
woocommerce |
PrestaShop |
prestashop |
RapidMail |
rapidmail |
UserPlatform |
user-platform |
Gender¶
| Case | Value |
|---|---|
Male |
male |
Female |
female |
Other |
other |
Unknown |
unknown |
ProductStatus¶
| Case | Value |
|---|---|
Active |
active |
Inactive |
inactive |
Deleted |
deleted |
ConsentChannel¶
| Case | Value |
|---|---|
Sms |
sms |
Email |
email |
General |
general |
ConsentSource¶
| Case | Value |
|---|---|
Api |
api |
Import |
import |
Admin |
admin |
User |
user |
Integration |
integration |
Offline |
offline |
IntegrationRegistry¶
Namespace: App\Services\IntegrationRegistry
A singleton that maps each SystemDriver to a concrete integration class. It is the only mechanism by which Conflux locates the correct integration at runtime — no integration class is referenced directly outside of AppServiceProvider.
public function register(SystemDriver $driver, string $integration): void
public function resolve(SystemDriver $driver): IntegrationContract
register()— associates aSystemDrivercase with aclass-string<IntegrationContract>. By convention, every integration is registered directly inApp\Providers\AppServiceProvider::boot()— there is no per-integration service provider.resolve()— returns an instance of the registered integration, resolved via the service container (app($class)). ThrowsRuntimeExceptionif no integration has been registered for the given driver.
The registry itself is bound as a singleton in AppServiceProvider::register():
Because it is a singleton, all register() calls in AppServiceProvider::boot() accumulate in the same instance throughout the application lifecycle.
Registration is required
An integration that is not registered in IntegrationRegistry cannot be resolved. Any BulkSyncJob or ProcessWebhookJob dispatched for its system driver will throw a RuntimeException at job execution time. Every integration implementation must register itself — see Implement a Platform Integration.
Registered Integrations¶
SystemDriver |
Integration class | Notes |
|---|---|---|
WooCommerce |
App\Integrations\WooCommerce\WooCommerceIntegration |
performVerification() calls the conflux-connector WordPress plugin's connectivity-check endpoint. fetchBulkExportBatches() paginates GET /wp-json/wc/v3/customers via HTTP Basic Auth (credentials.consumer_key/consumer_secret); only the Customers capability is implemented. |
PrestaShop |
App\Integrations\PrestaShop\PrestaShopIntegration |
performVerification() calls the PrestaShop Webservice API (GET {base_url}/api/shops?output_format=JSON) with HTTP Basic Auth (credentials.api_key as username, empty password) and resolves the shop ID as the external user identifier. fetchBulkExportBatches() dispatches on capability via the same Basic Auth scheme and PrestaShop's native limit=<offset>,<count> pagination syntax: Customers paginates GET {base_url}/api/customers, Products paginates GET {base_url}/api/products?display=full and resolves category names and a default currency via one-time lookups, and Orders paginates GET {base_url}/api/orders?display=full, resolves order state names/currencies/country names via one-time lookups, and loads only the addresses referenced by each fetched page. modifiedAfter delta filtering happens client-side for all three capabilities, since the API does not support filtering or sorting by date_upd on any of them. All three capabilities are implemented. performProbeBulkExport()/performFetchBulkExportPageRange() are also overridden: since the Webservice API exposes no total-count header, the probe derives the page count from a single unpaginated id listing, enabling the same parallel FetchBulkPageRangeJob fan-out WooCommerceIntegration uses — see the PrestaShop Integration reference. |
Both are registered in App\Providers\AppServiceProvider::boot(). RapidMail and UserPlatform are internal consuming products, not sync integrations, and have no IntegrationRegistry entry.
Exceptions¶
| Class | Extends | When thrown |
|---|---|---|
App\Exceptions\IntegrationRequestException |
RuntimeException |
Base class for all integration-layer errors; integrations throw this (or a subclass) for expected failure conditions |
App\Exceptions\InvalidIntegrationResponseException |
IntegrationRequestException |
Thrown by AbstractIntegration when fetchBulkExportBatches(), performProbeBulkExport(), performFetchBulkExportPageRange(), or processWebhook() raises any Throwable that is not already an IntegrationRequestException |
See also¶
- Webhook Receiver — inbound webhook endpoint that dispatches
ProcessWebhookJoband routes payloads tohandleWebhook() - Integration Contract design — reasoning behind the design decisions for both directions