NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Purchases for Laravel

Unified result contract

Every provider maps its native payload onto RoundlyConsulting\Purchases\Contracts\ProviderResult, so host code is written once and works across Apple, Google and Stripe:

MethodReturnsDescription
provider()stringOrigin provider: apple, google or stripe.
type()ResultTypePurchase, Subscription, Refund, Notification or Unknown.
providerId()stringThe id the record is keyed on when persisted.
transactionId()?stringThe underlying transaction id.
status()StatusNormalized status across providers.
name()?stringProduct or subscription name.
productId()?stringProduct / SKU id.
price()?MoneyAmount paid, when the store exposes it.
activeFrom()?CarbonInterfaceSubscription start.
trialEndsAt()?CarbonInterfaceTrial end.
endsAt()?CarbonInterfaceSubscription end or expiry.
items()list<ResultItem>Line items.
refundReason()?stringRefund reason, for refund results.
isChargeback()boolA dispute rather than a voluntary refund.
occurredAt()?CarbonInterfaceWhen the provider says it happened — a Stripe event’s created, an Apple notification’s signedDate, a Google notification’s eventTimeMillis, or when a store API reported the state; null when unknown.
raw()arrayThe raw decoded store payload.

Reading a result

use RoundlyConsulting\Purchases\Enum\ResultType;
use RoundlyConsulting\Purchases\Facades\Purchases;

$result = Purchases::result('apple', $request);

$result->provider();            // 'apple'
$result->type();                // ResultType::Subscription
$result->status();              // Status::Completed
$result->status()->isActive();  // true for Completed and InGracePeriod
$result->providerId();          // the id the record is keyed on
$result->price();               // RoundlyConsulting\Money\Money|null
$result->endsAt();              // ?CarbonInterface
$result->raw();                 // array — the decoded store payload

if ($result->type() === ResultType::Refund && $result->isChargeback()) {
    // a dispute, not a voluntary refund
}

GenericResult

GenericResult is the concrete, readonly implementation every built-in provider returns — and the one to return from a custom provider. Only provider, type, providerId and status are required; everything else defaults to null, an empty array or false:

use RoundlyConsulting\Money\Money;
use RoundlyConsulting\Purchases\DataTransferObjects\ResultItem;
use RoundlyConsulting\Purchases\Enum\ResultType;
use RoundlyConsulting\Purchases\Enum\Status;
use RoundlyConsulting\Purchases\Results\GenericResult;

$result = new GenericResult(
    provider: 'stripe',
    type: ResultType::Subscription,
    providerId: 'sub_123',
    status: Status::Completed,
    transactionId: 'sub_123',
    name: 'pro',
    productId: 'price_pro',
    price: Money::ofMinor(1999, 'USD'),
    activeFrom: now(),
    trialEndsAt: null,
    endsAt: now()->addMonth(),
    items: [new ResultItem('Pro plan', 'price_pro', Money::ofMinor(1999, 'USD'))],
    raw: $payload,
    refundReason: null,
    chargeback: false,
);

Line items are ResultItem DTOs: ResultItem(string $name, ?string $providerId = null, ?Money $price = null, int $quantity = 1). Status and ResultType values are covered in Enums & exceptions.

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 crypto

By 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.