shop.stripe.line_item_price_name (env SHOP_STRIPE_LINE_ITEM_PRICE_NAME,
default off) appends the price's name to the line item ('Flat Art – A4'),
so a product sold in several named prices shows the buyer which one they
pay for. Off by default: existing shops see no change.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- lang/de and lang/pl for cart.php and auth.php (informal register), every
key of lang/en translated; README documents the `shop::` namespace and
the lang/vendor/shop/<locale>/ override path.
- StripeWebhookController::handleCheckoutSessionCompleted now persists the
session's customer_details as Order.billing_address and shipping_details
as Order.shipping_address (falls back to billing when Stripe sent no
shipping block). Snapshot shape: name, email, phone, line1, line2,
postal_code, city, state, country, uid (first tax id). Only empty columns
are filled; a session without either block leaves the order untouched.
Optional host-added customer_email/customer_name columns are filled when
present and empty. Accepts Stripe objects, stdClass and arrays.
- Controller formatted with pint (composer lint).
Tests: tests/Unit/Stripe/StripeWebhookAddressTest.php (5),
tests/Feature/ShopServiceProvider/TranslationsTest.php (4). Full suite
green (1477, 7 skipped).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Adds the primitives a host webhook needs to keep the ledger current in
real time, so the importer is a backstop rather than the only path:
- ledgerRowFromBalanceTransaction(): the canonical row builder (the
import command now delegates to it), with refund→charge buyer fallback.
- recordBalanceTransaction(): idempotent upsert on the txn_ id that never
downgrades a known customer_id/email to null (so a webhook's resolved
buyer isn't clobbered by a later importer pass that couldn't resolve it).
- syncLedgerForCharge(): retrieves a charge + its refunds and upserts their
balance transactions, user-independent, never throws (logs + returns 0).
Facade @methods + 3 LedgerMetricsTest cases (upsert idempotency, no-downgrade,
non-charge-id guard). Full suite 1461 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
customerLedgerTotals() sums the user-independent Stripe ledger for one
buyer by ANY of their customer id(s) OR email(s) — so subscription
payments (which product_purchases stores as amount=0) and deleted- or
guest-customer charges are still attributed. ledgerAmountByCustomer()
returns [customer_id => amount] for per-user rollups in one query.
Both are additive and filter on ledgerRevenueTypes(); empty identity
returns zero rather than matching everything. Covered by two new
LedgerMetricsTest cases (full suite 1458 green).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirror every Stripe balance transaction (signed gross/fee/net) into a new
stripe_transactions table decoupled from any local user, so revenue net of
fees and refunds is a plain SUM(net) and a deleted customer never removes
their money history.
- StripeTransaction model (config-resolved, HasUuids) + guarded, publishable
migration; config tables.stripe_transactions + models.stripe_transaction +
ledger.revenue_types.
- Shop::revenueLedgerByDay / ledgerTotals / ledgerEarliest (+ facade @method).
- shop:import-stripe-ledger: pages ALL balance transactions globally (not
per-customer), upserts on txn_ id, --dry-run.
- LedgerMetricsTest (4). Full suite green (1456).
Additive only; existing consumers unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add ProductPrice.license_percent + license_min_amount and
ProductPrice::licenseFeePerPeriod() = max(percent% of unit_amount, minimum).
The shop amortizes it over the licence-term bracket (<=quarter / 6mo / 12mo),
so a monthly subscriber billed 3x in a quarter costs ONE quarterly fee — the
faithful reading of a per-unit, quarterly-accounted content licence.
subscriptionMetrics() now returns license_cost and net_mrr nets it out.
Additive: a null rule contributes 0. Covered by MrrMetricsTest; full suite green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add ProductPrice.cost_amount (integer cents) — the cost to deliver one unit at
that price: manufacturing, shipping, a content license or royalty; per billing
period for recurring prices. Shop now reports margin natively: subscriptionMetrics()
returns `cogs` + `net_mrr`, and Shop::netMrr() gives MRR after cost of goods.
Additive: cost_amount defaults to 0, so existing consumers are unchanged.
Covered by MrrMetricsTest; full suite green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add Shop::mrr(), activeSubscribers(), mrrByProduct(), subscriptionMetrics()
and subscriberStatuses() to ShopService. Counts active/past_due/trialing
subscribers (not `ends_at IS NULL`, which silently drops still-billing
cancel-at-period-end subs and under-reports MRR), normalizes every interval
to a monthly run-rate (year/12, quarter/3, week & day by the average
days-per-month constant so they match Stripe), and returns `unpriced_items`
so a stale or incomplete price mirror is surfaced, never a silent undercount.
Additive only: no existing method changed. Covered by MrrMetricsTest.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A seat-based product (meta.seat_based) sold at quantity N provisions N
assignable LicenseSeat rows instead of granting the buyer. Assigning a seat
runs the product's existing ProductAction grants against the seat holder via
an explicit `grantee` (resolution: grantee ?? purchaser ?? subscription->user),
so a seat confers exactly the product's access with zero duplicated grant logic.
- LicenseSeat model: assign / reassign / reclaim / retire / refreshGrant
- SeatService: idempotent provisioning for one-time purchases and subscriptions
(sizes the pool to quantity; renewal extends expiry + re-grants held seats)
- SeatAssigned / SeatReassigned / SeatRevoked events
- Product::isSeatBased(); ProductPurchase + Subscription provision on
completion / lifecycle hooks
- license_seats migration + factory; config `shop.seats` block
- 15 new tests (purchase, subscription, renewal, retire, delegation, gating)
Backward compatible: with no `grantee` the buyer grant is unchanged, and seat
behavior is gated behind the per-product meta flag + config `shop.seats.enabled`.
Full package suite green: 1425 tests / 3812 assertions / 0 failures.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
callProductActions() now resolves and runs actions for ALL subscription line items (new resolveProducts()), not just items()->first(), so combined/configurator bundle subscriptions fulfill every product instead of silently granting only the first. resolveProduct() is kept for single-product callers; product_id is still cached to the first resolved product. Adds a multi-item bundle regression test to SubscriptionLifecycleTest.
- 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.
Cart::checkoutSession() now inspects each line's price type and selects the
session mode automatically: `subscription` when the cart carries any recurring
price, `payment` otherwise. Recurring lines reuse a synced Stripe Price
(stripe_price_id) when present and fall back to dynamic price_data with a
`recurring` block otherwise; quarterly cadence maps to a 3-month interval since
Stripe has no native quarter. The cart id is propagated via subscription_data
metadata for webhook mapping.
Mixing recurring and one-time prices in one cart throws the new
MixedCheckoutModeException, since a Stripe Checkout session is single-mode.
The recurring resolver tolerates both the package's enum-cast price model and a
host model storing type/interval as plain strings, so it keeps working when
shop.models.product_price is overridden.
- Implement CommandReinstallTest to verify the behavior of the shop:reinstall command, including force and fresh flags, and confirmation prompts.
- Create CommandReleaseExpiredStocksTest to test the shop:release-expired-stocks command, ensuring it correctly releases expired stock claims based on configuration.
- Add CommandStatsTest to validate the shop:stats command, checking counts and revenue calculations for products, purchases, carts, and orders.
- Introduce LoanShopCommandsTest to cover loanable products in stock management, ensuring accurate reporting of assigned, used, and available stock.
- Implement LoanStockEventsTest to verify that stock events are dispatched correctly during loan operations, including checkouts and returns.
- Add NextAvailableAtTest to ensure the nextAvailableAt method behaves correctly for loanable products, considering loans and claims.
- Introduced events for stock management including StockBecameLow, StockClaimed, StockClaimExpired, StockDecreased, StockDepleted, StockIncreased, StockReleased, StockReplenished, StockFullyAvailable, and StockFullyAvailable.
- Added events for Stripe payment processing: StripePaymentFailed, StripePaymentSucceeded, StripePriceSynced, StripeProductSynced, StripeRefundProcessed, and StripeWebhookReceived.
- Created tests for command availability, listing, stocks, and event dispatching to ensure proper functionality and integration.
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.
- Added strict types declaration to multiple traits for better type safety.
- Updated method signatures in traits to use nullable types where applicable.
- Improved documentation for traits, including host-model contracts and method descriptions.
- Added new tests to ensure correct behavior of loan checkout and stock management.
- Fixed regression in order number generation to ensure proper string formatting.
- Ensured that currency codes sent to Stripe are consistently lowercased.