Models & the owner trait
Purchase, PurchaseItem, Subscription, SubscriptionItem, PurchaseRefund and PurchaseNotification (under RoundlyConsulting\Purchases\Models) are standard Eloquent models with soft deletes and factories.
One owner’s purchases
Purchases::for($owner) needs no trait on the model — it scopes to exactly that owner’s morph type and key:
use RoundlyConsulting\Purchases\Facades\Purchases;
Purchases::for($user)->subscribedTo('pro'); // bool
Purchases::for($user)->activeSubscription('pro'); // ?Subscription (latest active)
Purchases::for($user)->purchases()->latest()->get(); // Builder<Purchase>
Purchases::for($user)->subscriptions()->count(); // Builder<Subscription>The HasPurchases trait
Add the trait to your owner model — typically User — for the purchases and subscriptions relations through the package’s owner morph. Its activeSubscription() and subscribedTo() helpers are sugar over Purchases::for($this), so they behave exactly like the facade:
use RoundlyConsulting\Purchases\Concerns\HasPurchases;
class User extends Authenticatable
{
use HasPurchases;
}
$user->purchases; // MorphMany<Purchase>
$user->subscriptions; // MorphMany<Subscription>
$user->activeSubscription(); // ?Subscription — the latest active one
$user->activeSubscription('pro'); // ?Subscription — by plan name
$user->subscribedTo('pro'); // bool — both delegate to Purchases::for($this)Subscription scopes & helpers
use RoundlyConsulting\Purchases\Models\Subscription;
Subscription::active()->expiring(7)->get(); // active and ending within 7 days
Subscription::trialing()->get();
Subscription::canceled()->get();
$subscription->isActive(); // entitled and not past ends_at
$subscription->onTrial();
$subscription->daysUntilRenewal(); // ?int
$subscription->isExpiring(7);
$subscription->items; // HasMany<SubscriptionItem>- active() — status Completed or InGracePeriod, and ends_at empty or in the future.
- trialing() — trial_ends_at in the future.
- expiring($days = 7) — ends_at within the next $days days.
- canceled() — status Canceled.
- daysUntilRenewal() — whole days until ends_at (never negative), or null without an end date.
Provider scopes & relations
Purchase, Subscription and PurchaseRefund share forProvider(), byProviderId() and byTransaction(); PurchaseRefund adds chargebacks() and PurchaseNotification adds pending():
use RoundlyConsulting\Purchases\Models\Purchase;
use RoundlyConsulting\Purchases\Models\PurchaseNotification;
use RoundlyConsulting\Purchases\Models\PurchaseRefund;
Purchase::forProvider('stripe')->byProviderId('pi_123')->first();
Purchase::byTransaction('2000000000000001')->first();
PurchaseRefund::forProvider('google')->chargebacks()->get();
PurchaseNotification::forProvider('apple')->pending()->get();
$purchase->owner; // MorphTo — your User (or null)
$purchase->items; // HasMany<PurchaseItem>
$purchase->refunds; // HasMany<PurchaseRefund>
$purchase->meta; // Collection — the raw store payload
$refund->purchase; // BelongsTo<Purchase>Swapping models
Every model is swappable via config('purchases.models.*'). Your class must be the package model it replaces or extend it — any other class throws an InvalidConfigurationException naming the key instead of silently falling back to the packaged model:
// app/Models/Purchase.php
namespace App\Models;
use RoundlyConsulting\Purchases\Models\Purchase as BasePurchase;
class Purchase extends BasePurchase
{
// your relations, accessors, casts …
}
// config/purchases.php
'models' => [
'purchase' => \App\Models\Purchase::class,
// …
],Schema
| Table | Columns |
|---|---|
purchases | owner (nullable morph), provider, provider_id, transaction_id, status, price + price_currency, meta, last_event_at — unique (provider, provider_id) |
purchase_items | purchase_id (cascade delete), provider_id, name, price + price_currency, quantity |
subscriptions | owner (nullable morph), provider, provider_id, transaction_id, name, status, price + price_currency, active_from, trial_ends_at, ends_at, meta, last_event_at — unique (provider, provider_id) |
subscription_items | subscription_id (cascade delete), provider_id, name, price + price_currency |
purchase_refunds | purchase_id (nullable), provider, provider_id, transaction_id, reason, chargeback, price + price_currency, refunded_at, meta, last_event_at — unique (provider, provider_id) |
purchase_notifications | provider, type, signature_verified, payload, processed_at |
Every table has timestamps and soft deletes. Table names come from each model’s getTable(), so a swapped model with its own $table is migrated under that name — configure it before you run the migrations.
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.