Enums & exceptions
Status is the normalized status stored on purchases and subscriptions:
| Case | Value | Keeps access |
|---|---|---|
New | new | No |
Pending | pending | No |
Processing | processing | No |
Completed | completed | Yes |
Failed | failed | No |
Canceled | canceled | No |
InGracePeriod | in_grace | Yes |
OnHold | on_hold | No |
Refunded | refunded | No |
Status::isActive() is true only for Completed and InGracePeriod — the states that keep an entitlement. It drives Subscription::isActive() and the active() scope.
ResultType
| Case | Value | Meaning |
|---|---|---|
Purchase | purchase | A one-off purchase — recorded as a Purchase. |
Subscription | subscription | A subscription — recorded as a Subscription. |
Refund | refund | A refund or chargeback — recorded as a PurchaseRefund. |
Notification | notification | A store notification without a transaction. |
Unknown | unknown | An unrecognised payload. |
Select options & validation
Both enums use the Helpers trait from enums-for-laravel, so admin filters and forms need no bespoke arrays or language files:
use RoundlyConsulting\Purchases\Enum\Status;
Status::options(); // list of {value, label, name} option DTOs for select inputs
Status::toOptions(); // ['completed' => 'Completed', 'in_grace' => 'In Grace', …]
Status::labels(); // human strings incl. "In Grace" / "On Hold" (no lang files)
Status::validationRule(); // "in:new,pending,processing,completed,failed,canceled,in_grace,on_hold,refunded"
$status = Status::tryFromLabel('On Hold'); // Status::OnHold
$status?->isActive(); // false — an account hold keeps no accessExceptions
| Exception | Thrown when |
|---|---|
VerificationException | A signature or credential check fails — including an Apple certificate outside its validity window — or a client sends bad input to a provider’s callback(): a malformed id, or one the store rejects with a 4xx (getPrevious() keeps the store’s RequestException). The webhook route answers 400. |
InvalidConfigurationException | A config value is unusable — certificate_clock_skew outside 0–3600, a duration that isn’t a whole number, a URL or queue name that isn’t a string, a provider that doesn’t implement Provider. Deliberately not a VerificationException — a misconfigured host is not a forged notification. |
UnknownProviderException | Resolving a provider id that isn’t registered. |
EventInProgressException | Under audit.deduplicate, a redelivery of an event another delivery is still processing. Nothing was recorded; the webhook route answers 409 so the store retries. |
BillingException | A Purchases::stripe() call refused before Stripe is asked — no customer, an unsaved owner, a non-Stripe subscription, a malformed id or key, a trial end behaviour without a trial, a billing mode the pinned API version predates, or a second subscription under subscriptions.one_per_owner. |
StripeException | Any Stripe failure, typed: StripeCardException, StripeInvalidRequestException, StripeAuthenticationException, StripeRateLimitException, StripeIdempotencyException, StripeApiException, StripeConnectionException. Each carries httpStatus, stripeCode, requestId and idempotencyKey; secrets in the message are redacted. |
UnsupportedCapabilityException | Purchases::stripe() without a registered Stripe provider — only Stripe can bill. |
InvalidProviderNotificationException | A notification payload can’t be decoded — or replay() refuses a notification (unsaved or soft-deleted, a provider notification that failed verification, or no longer rebuildable). |
All extend RoundlyConsulting\Purchases\Exceptions\Exception, which adds a because(string $message, int $code = 0) named constructor:
use RoundlyConsulting\Purchases\Exceptions\UnknownProviderException;
use RoundlyConsulting\Purchases\Exceptions\VerificationException;
use RoundlyConsulting\Purchases\Facades\Purchases;
try {
$result = Purchases::result('stripe', $request);
} catch (VerificationException $e) {
abort(400, $e->getMessage());
} catch (UnknownProviderException) {
abort(404);
}Config typos caught by the package toolkit are the exception: an unparseable on/off switch (push authentication aside), a key_type outside bigint, uuid and ulid, and a models.* class that isn’t the packaged model or a subclass of it throw RoundlyConsulting\PackageToolkit\Exceptions\InvalidConfigurationException, which doesn’t extend the package’s base exception. Like the package’s own InvalidConfigurationException, it names the key.
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.