Skip to content

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 returned VerificationResult.
  • requestBulkExport() — returns a Collection<int, CustomerData|OrderData|ProductData> containing every fetched record. Throws IntegrationRequestException (or a subclass) on any integration-layer failure. Built on top of streamBulkExport().
  • streamBulkExport() — invokes $onBatch once per fetched page, without accumulating the full result set in memory. Throws IntegrationRequestException (or a subclass).
  • probeBulkExport() — returns the total page count and page size without fetching any records, so the job layer can fan out parallel fetchBulkExportPageRange() calls. Throws IntegrationRequestException (or a subclass).
  • fetchBulkExportPageRange() — fetches a specific [startPage, endPage] range, invoking $onBatch once per page. Throws IntegrationRequestException (or a subclass).
  • handleWebhook() — processes an inbound webhook payload for the given connection. Throws IntegrationRequestException (or a subclass) on any integration-layer failure.
  • transformerClass() — returns the TransformerContract implementation 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)

abstract protected function performVerification(SystemConnection $connection): VerificationResult;

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 a SystemDriver case with a class-string<IntegrationContract>. By convention, every integration is registered directly in App\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)). Throws RuntimeException if no integration has been registered for the given driver.

The registry itself is bound as a singleton in AppServiceProvider::register():

$this->app->singleton(IntegrationRegistry::class);

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