Commit Graph

102 Commits

Author SHA1 Message Date
Fabian @ Blax Software 423da476f5 feat(ledger): mirror PayPal accounts into the money ledger
PayPal checkouts that run through Stripe settle into our PayPal account.
Stripe's ledger row books the gross and Stripe's fee, but PayPal takes its
own fee inside the PayPal account, so net revenue was overstated.

- Ledger rows carry `provider` (stripe, paypal) and `account`; existing
  rows default to stripe.
- shop:import-paypal-ledger reads every account in shop.paypal.accounts
  through the Transaction Search API (read-only) and upserts one row per
  PayPal transaction. Stripe-originated payments and refunds add a
  `paypal_fee` row (amount 0, net -fee); payments PayPal swept on to
  Stripe are booked as `paypal_fee_passthrough`, because Stripe's fee
  already holds PayPal's. Direct PayPal sales count in full; holds,
  sweeps and withdrawals are non-revenue.
- `paypal_fee` joins the default revenue types.
- Stripe rows keep their payment intent in meta so PayPal rows find the
  buyer.
- Shop::ledgerTotalsByAccount() splits the totals per account.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 10:46:46 +02:00
Fabian @ Blax Software 8aa727fb5e fix(ledger): count PayPal payments at their charge amount
PayPal pays into the merchant's PayPal balance, so Stripe's balance
transaction for it has amount 0 and only the fee. The ledger copied that
0, so every PayPal buyer showed no revenue. When a payment or refund
balance transaction is 0 and its charge/refund says otherwise, record the
charge's gross (net = gross - fee) and keep Stripe's own figures in meta.

syncLedgerForCharge also accepts py_ charge ids now, so PayPal payments
reach the ledger from the webhook instead of only the daily import.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 10:52:32 +02:00
Fabian @ Blax Software 4df7e1373a fix(stripe): name Checkout lines after the product, options only when there are some
- A product with a single price showed "Shinsou Standee – Default" on
  Checkout; the price name is now only added when the product sells in
  more than one price.
- A line added by price id was named after the price ("A4 – A4"); it is
  named after the price's product now ("Flat Art – A4").
- The test for this lived inside the fake Stripe Session class at the end
  of the file and never ran; it is a real test again.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 13:02:03 +02:00
Fabian @ Blax Software 961ed5af4c feat(stripe): optionally name the chosen price on Checkout line items
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>
2026-09-25 10:28:34 +02:00
Fabian @ Blax Software b4125c1ee1 feat(i18n,stripe): de/pl translations + order address snapshots from Checkout Session
- 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>
2026-09-21 13:39:40 +02:00
Fabian @ Blax Software 1849f55558 feat(ledger): real-time balance-transaction upsert for webhook sync
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>
2026-09-15 09:36:40 +02:00
Fabian @ Blax Software f4624b2903 feat(ledger): per-customer revenue attribution primitives
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>
2026-09-15 08:39:40 +02:00
Fabian @ Blax Software 86e6469d1f feat(ledger): user-independent Stripe BalanceTransaction ledger + daily net stats
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>
2026-09-03 10:01:07 +02:00
Fabian @ Blax Software 47e7300e98 feat(cost): royalty / minimum-licence-fee margin (Aircademy-style)
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>
2026-08-28 16:03:04 +02:00
Fabian @ Blax Software 9e3e4b2270 feat(cost): per-price cost basis (COGS) + net-MRR margin
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>
2026-08-28 15:12:42 +02:00
Fabian @ Blax Software f6f8916bed feat(metrics): subscription MRR API on Shop facade
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>
2026-08-28 13:38:42 +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 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 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 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