Custom providers
Providers plug in through RoundlyConsulting\Purchases\Providers\Provider:
| Method | Returns | Purpose |
|---|---|---|
id() | string | The id used in config, the facade and the webhook URL. |
notification(Request $request) | mixed | Decode an inbound store notification into a native payload object. |
callback(Request $request) | mixed | Verify a client-side callback or verification request. |
result(Request $request) | ProviderResult | Verify and decode into the unified result — what the manager calls. |
Extend BaseProvider to get id() for free — the kebab-cased class basename — plus stubs for the other methods that throw until you override them. Implement VerifiesConnectivity to take part in purchases:verify:
namespace App\Purchases;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use RoundlyConsulting\Money\Money;
use RoundlyConsulting\Purchases\Contracts\ProviderResult;
use RoundlyConsulting\Purchases\Contracts\VerifiesConnectivity;
use RoundlyConsulting\Purchases\DataTransferObjects\ConnectivityResult;
use RoundlyConsulting\Purchases\Enum\ResultType;
use RoundlyConsulting\Purchases\Enum\Status;
use RoundlyConsulting\Purchases\Exceptions\VerificationException;
use RoundlyConsulting\Purchases\Providers\BaseProvider;
use RoundlyConsulting\Purchases\Results\GenericResult;
final class AcmePay extends BaseProvider implements VerifiesConnectivity
{
// id() defaults to the kebab-cased class basename: "acme-pay"
public function result(Request $request): ProviderResult
{
$expected = hash_hmac('sha256', $request->getContent(), (string) config('services.acme_pay.secret'));
if (! hash_equals($expected, (string) $request->header('Acme-Signature'))) {
throw VerificationException::because('Invalid AcmePay signature.');
}
$payload = $request->json()->all();
return new GenericResult(
provider: $this->id(),
type: ResultType::Purchase,
providerId: $payload['order_id'],
status: Status::Completed,
transactionId: $payload['transaction_id'] ?? null,
price: Money::ofMinor($payload['amount'], $payload['currency']),
raw: $payload,
);
}
public function verifyConnectivity(): ConnectivityResult
{
$response = Http::withToken((string) config('services.acme_pay.key'))
->get('https://api.acme-pay.test/v1/ping'); // a cheap authenticated call
return $response->successful()
? ConnectivityResult::ok('AcmePay credentials are valid.')
: ConnectivityResult::failed('AcmePay rejected the API key.');
}
}Register the class in config/purchases.php. Providers are resolved through the container, so constructor dependencies are injected:
// config/purchases.php
'providers' => [
\RoundlyConsulting\Purchases\Providers\Apple\Apple::class,
\RoundlyConsulting\Purchases\Providers\Google\Google::class,
\RoundlyConsulting\Purchases\Providers\Stripe\Stripe::class,
\App\Purchases\AcmePay::class,
],The provider is then available as Purchases::handle('acme-pay', $request), through the webhook route at /purchases/webhooks/acme-pay, and in purchases:providers and purchases:verify.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.