Build an external integration
Billing owns PaymentGateway; Infrastructure provides the fake Adapter. The Adapter translates a vendor exception to a Domain Exception, while the UseCase reuses its idempotency key for a retry.
Before you begin
Run composer run reference:test:integration and read Contracts and Adapters.
Keep the provider boundary outward
examples/reference-app/app/Pulsar/Services/Client/Modules/Orders/UseCases/ChargeOrder.php — Tenant-scoped charge, key reuse, and vendor-error translation.
php
<?php
namespace App\Pulsar\Services\Client\Modules\Orders\UseCases;
use App\Pulsar\Domain\Billing\Contracts\PaymentGateway;
use App\Pulsar\Domain\Orders\Contracts\TenantContext;
use App\Pulsar\Domain\Orders\Models\Order;
use Illuminate\Support\Facades\DB;
final class ChargeOrder
{
public function __construct(private readonly PaymentGateway $gateway, private readonly TenantContext $tenant) {}
// #region charge-order-integration
public function execute(int $orderId, string $tenantId, string $idempotencyKey): string
{
return DB::transaction(function () use ($orderId, $tenantId, $idempotencyKey): string {
$this->tenant->establish($tenantId);
$order = Order::query()->where('tenant_id', $tenantId)->lockForUpdate()->findOrFail($orderId);
$existing = DB::table('payments')->where('idempotency_key', $idempotencyKey)->value('gateway_reference');
if ($existing !== null) {
return $existing;
}
$reference = $this->gateway->charge($order->amount_cents, $idempotencyKey);
DB::table('payments')->insert(['order_id' => $order->id, 'tenant_id' => $tenantId, 'gateway_reference' => $reference, 'idempotency_key' => $idempotencyKey, 'created_at' => now(), 'updated_at' => now()]);
return $reference;
});
}
// #endregion
}Verify and troubleshoot
The test proves the fake provider failure becomes PaymentDeclined. Do not let provider exceptions escape a Contract boundary or imply that a fake adapter proves a real provider account.