- ProductType::SERVICE for intangible/served products (subscriptions, licences,
consulting) — no stock, behaves like SIMPLE for cart purposes. Lets hosts
whose catalogue includes services adopt the package without a value clash.
- it_creates_separate_line_items_for_multiple_products pinned its two prices to
one_time; without a type the factory randomised it and could mix recurring +
one-time, tripping MixedCheckoutModeException intermittently.
Adds the package's missing subscription lifecycle so any host app gets
duration-aware, product-linked subscriptions without re-implementing billing:
- Models: Subscription (extends Laravel\Cashier\Subscription) + SubscriptionItem,
in the package's UUID convention, resolved through shop.models/shop.tables.
Subscription gains product()/resolveProduct(), callProductActions() (runs the
product's ProductActions with the subscription + an access-expiry override),
and recordStarted()/recordRenewed()/recordCanceled() lifecycle hooks.
- Events: SubscriptionStarted / SubscriptionRenewed / SubscriptionCanceled,
carrying the Cashier subscription so host subclasses work too.
- Migration: UUID subscriptions / subscription_items tables (hasTable-guarded,
config table names, nullable product_id + current_period_* columns).
- ShopServiceProvider points Cashier at these models by default; opt out via
shop.subscriptions.register_cashier_models for apps that subclass Cashier.
Additive and backward-compatible (registration is config-gated, tables are
guarded). Adds SubscriptionLifecycleTest; full suite 1409 green. Docs + README.
Introduce a first-class PurchaseCompleted lifecycle event so host apps can run
fulfillment (grant access, send receipts, provision licences) without coupling
to the ProductAction table or to a concrete purchasable model. It fires when a
purchase is created already-COMPLETED and when one transitions into COMPLETED,
and is transition-guarded so it does not re-fire on unrelated saves of an
already-completed purchase.
Also generalise the built-in ProductAction fulfillment in ProductPurchase:
the actionable product is now resolved via config('shop.models.product') /
'...product_price' (instead of a hard instanceof the bundled Product), and
callActions() is only invoked when the resolved product exposes it — so apps
overriding the models, or using IsSimplePurchasable host models, complete
cleanly. Existing behaviour for the bundled Product is unchanged.
Adds 4 EventsWiredUpTest cases; full suite 1404 green. Docs + README updated.
PurchaseResourceTest::it_translates_e_commerce_columns_into_loan_vocabulary
relied on the real wall clock falling inside the fixture's loan window
(2026-05-14..2026-05-28); once that window passed, the loan resolved to
`overdue` and the test failed. Freeze the clock inside the window like its
sibling tests already do.
Update the README test/assertion badges and the Testing section to the current
suite size (1400 tests, 3755 assertions) after the subscription-checkout tests.
Renames the host-attached IsLoanableProduct trait to MayBeLoanableProduct
and mixes it directly into Product, matching the existing MayBePoolProduct
shape. Hosts opt in with a single DEFAULT_TYPE constant — no more manual
`use ...Trait` ceremony to forget:
class Book extends Product
{
public const DEFAULT_TYPE = ProductType::LOANABLE;
}
The boot hook reads DEFAULT_TYPE on the concrete class and only applies
the LOANABLE creating-defaults (type, status, is_visible, manage_stock)
when it matches; type-specific helpers (checkOutTo, total_quantity,
available_quantity) early-out via isLoanable() so they're harmless on
non-loanable products. checkOutTo now throws NotLoanableProductException
when called on the wrong type, mirroring NotPoolProductException.
Also fixes total_quantity for the loan lifecycle: previously summed every
INCREASE entry in product_stocks, which inflated the displayed total
after each loan cycle because returns fire increaseStock(). Now reports
physical inventory as availableStock + activeLoans.
README gains a Testing section that surfaces the current phpunit summary
line, plus passing and assertion-count badges linking to it.