Commit Graph

98 Commits

Author SHA1 Message Date
Fabian @ Blax Software f10d5c2f04 feat(tax): configurable TaxService applied across every checkout path
Blax\Shop\Services\TaxService::rates($exempt, $rates=null) is one source of
truth that turns "which rate(s)" (config shop.tax.rates or an explicit list)
and "is this customer exempt" into the array Stripe expects. It fails loud
(TaxRateNotConfiguredException) when shop.tax.require is set and no rate is
configured for a non-exempt charge, instead of silently billing 0% VAT.

New config('shop.tax') block (SHOP_TAX_RATES / SHOP_TAX_REQUIRE). Unit tested.

Consumers (learn-atc #1509) delegate getApplicableTaxRates() here so no path
can apply the rate twice (Stripe rejects duplicate tax rates) or drop VAT to 0%.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-21 08:23:56 +02:00
Fabian @ Blax Software b154d8fea2 feat(seats): assignable license seats — buy N, assign/reassign/reclaim per user
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>
2026-07-29 10:07:58 +02:00
Fabian @ Blax Software 8594af4236 fix(subscriptions): grant every product in a multi-item (bundle) subscription
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.
2026-06-09 19:29:14 +02:00
Fabian @ Blax Software db827bbb8c feat(enums): add SERVICE product type; fix flaky multi-product checkout test
- 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.
2026-06-03 09:49:12 +02:00
Fabian @ Blax Software d4ae9339ac feat(subscriptions): Cashier-backed subscription lifecycle with product link + events
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.
2026-06-02 11:40:15 +02:00
Fabian @ Blax Software 96b9a19287 feat(fulfillment): add model-agnostic PurchaseCompleted event
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.
2026-06-02 11:19:21 +02:00
Fabian @ Blax Software 30dc3755d2 test: pin clock in loan-vocabulary test; docs: bump suite counts to 1400/3755
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.
2026-06-02 09:48:26 +02:00
Fabian @ Blax Software d4008caec0 feat(checkout): support subscription/recurring carts in Stripe Checkout
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.
2026-06-02 09:34:32 +02:00
Fabian @ Blax Software 4712133eac IAM "max usage" feature 2026-05-19 14:01:52 +02:00
Fabian @ Blax Software 6d22e130ed A cart availability + test 2026-05-19 11:17:35 +02:00
Fabian @ Blax Software 0cfbdf221d BF edgecases, R structure, A tests 2026-05-18 13:05:38 +02:00
Fabian @ Blax Software 1ed301b759 R loan purchase to physical loan, A physical loan stuff 2026-05-17 15:20:58 +02:00
Fabian @ Blax Software da6d89f668 BFI commands, A test 2026-05-17 14:09:03 +02:00
Fabian @ Blax Software 2bc64c6369 BF stocks, A test 2026-05-17 13:57:20 +02:00
Fabian @ Blax Software 99fd71f4ae BF commands, A command tests, I traits
- 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.
2026-05-17 13:25:34 +02:00
Fabian @ Blax Software f55c7e11df A events, IA commands
- 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.
2026-05-17 11:24:43 +02:00
Fabian @ Blax Software 8eb1802ef8 feat: promote IsLoanableProduct to MayBeLoanableProduct on Product
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.
2026-05-16 12:17:38 +02:00
Fabian @ Blax Software afdcd8bc75 feat: Enhance traits with strict types and improve method signatures
- 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.
2026-05-15 20:26:24 +02:00
Fabian @ Blax Software fe41475c84 I loanable product type, tiered pricing, lifecycle events, host helpers 2026-05-15 10:27:59 +02:00
Fabian @ Blax Software 440be3a36f I order notes, BF checkout currency 2026-01-25 10:40:17 +01:00
Fabian @ Blax Software 1a8f111110 I order 2026-01-05 12:29:55 +01:00
Fabian @ Blax Software a66fd7ccb8 BFI cart / orders 2026-01-05 10:30:21 +01:00
Fabian @ Blax Software cbb4b84948 BF cart/pool/booking 2026-01-05 09:07:09 +01:00
Fabian @ Blax Software c0ae7c4927 BF pool cart bug, R structure 2025-12-30 10:56:03 +01:00
Fabian @ Blax Software 2ea8273c29 BF pool cart bug, R structure 2025-12-30 10:55:06 +01:00
Fabian @ Blax Software 136b7ade63 A prompts, I docs/readme, BF orders, R tests locations 2025-12-30 09:29:43 +01:00
Fabian @ Blax Software 6e9c9043ae I order, A handy methods 2025-12-29 10:26:51 +01:00
Fabian @ Blax Software 9c1fcd6cfd AM orders 2025-12-29 09:59:02 +01:00
Fabian @ Blax Software 1486424229 BF checkout session test 2025-12-29 08:26:09 +01:00
Fabian @ Blax Software 2f3c0dc61f IA stock attributes 2025-12-28 12:05:05 +01:00
Fabian @ Blax Software 7cd11728b1 BFRI cart 2025-12-28 11:12:58 +01:00
Fabian @ Blax Software 6beecf597c BF has_more, I hasprices->fromPrice 2025-12-28 10:48:22 +01:00
Fabian @ Blax Software 37b3e6bdc0 A cart->calendarAvailability, product->has_more 2025-12-28 10:29:23 +01:00
a6a2f5842 fc3ae3e756 A pool availabilities 2025-12-26 16:45:30 +01:00
a6a2f5842 5ac0229555 A stock methods calendarAvailability & dayAvailability 2025-12-26 16:10:40 +01:00
a6a2f5842 38e841a986 I test 2025-12-26 08:46:14 +01:00
a6a2f5842 70adf0b0c6 BFI cart availability 2025-12-26 08:42:59 +01:00
a6a2f5842 9e7812c70e BFI cart single item 2025-12-24 19:40:10 +01:00
Fabian @ Blax Software 816e8661e2 BF cart items 2025-12-20 15:08:08 +01:00
Fabian @ Blax Software 1398fd0c27 BF cart items 2025-12-20 12:43:28 +01:00
Fabian @ Blax Software 0e6b420297 BFI pool cart 2025-12-20 12:19:34 +01:00
Fabian @ Blax Software 20e6538626 BFI pool cart 2025-12-20 11:22:04 +01:00
Fabian @ Blax Software 145c629786 BFI cart stock, A exceptions 2025-12-19 14:26:57 +01:00
Fabian @ Blax Software 317b28af8a BFIM pool cart checkout, stocks/productpurchase 2025-12-19 13:32:00 +01:00
Fabian @ Blax Software d13ac99725 BF pool cart 2025-12-19 12:47:55 +01:00
Fabian @ Blax Software dbc297122e BF pool cart 2025-12-19 12:25:59 +01:00
Fabian @ Blax Software f20637770f I pool cart support, tests 2025-12-19 10:57:26 +01:00
Fabian @ Blax Software 06b45cc9b3 I cart set dates, A test 2025-12-19 10:08:24 +01:00
Fabian @ Blax Software f6c60f3d79 BFI cart conversion product purchase, R cart dates 2025-12-19 09:53:44 +01:00
Fabian @ Blax Software c5b78071e7 IA booking checking & simplifications, A test/trait 2025-12-18 16:54:33 +01:00