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:
| Method | Returns | Description |
|---|---|---|
provider() | string | Origin provider: apple, google or stripe. |
type() | ResultType | Purchase, Subscription, Refund, Notification or Unknown. |
providerId() | string | The id the record is keyed on when persisted. |
transactionId() | ?string | The underlying transaction id. |
status() | Status | Normalized status across providers. |
name() | ?string | Product or subscription name. |
productId() | ?string | Product / SKU id. |
price() | ?Money | Amount paid, when the store exposes it. |
activeFrom() | ?CarbonInterface | Subscription start. |
trialEndsAt() | ?CarbonInterface | Trial end. |
endsAt() | ?CarbonInterface | Subscription end or expiry. |
items() | list<ResultItem> | Line items. |
refundReason() | ?string | Refund reason, for refund results. |
isChargeback() | bool | A dispute rather than a voluntary refund. |
occurredAt() | ?CarbonInterface | When 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() | array | The 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 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.