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