Commit Graph

40 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 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 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 7d35c224f3 fix: product_action_runs.action_id must be uuidMorphs (ProductAction is HasUuids)
morphs('action') creates a bigint action_id, but ProductAction uses HasUuids — so ProductAction::callForProduct's run-logging dies with 'Incorrect integer value: <uuid>' on the first dispatch. uuidMorphs('action') matches the uuid key. Fresh-install fix; existing installs need an in-place widen.
2026-06-10 11:33:09 +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 4712133eac IAM "max usage" feature 2026-05-19 14:01:52 +02:00
Fabian @ Blax Software 0cfbdf221d BF edgecases, R structure, A tests 2026-05-18 13:05:38 +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 1a8f111110 I order 2026-01-05 12:29:55 +01:00
Fabian @ Blax Software cbb4b84948 BF cart/pool/booking 2026-01-05 09:07:09 +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 0e6b420297 BFI pool cart 2025-12-20 12:19:34 +01:00
Fabian @ Blax Software 317b28af8a BFIM pool cart checkout, stocks/productpurchase 2025-12-19 13:32:00 +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 e687836024 BF index too long 2025-12-18 12:55:11 +01:00
Fabian @ Blax Software 23a85762a5 BF index too long 2025-12-18 12:39:59 +01:00
Fabian @ Blax Software d13945a731 IMU cart + test 2025-12-18 12:21:29 +01:00
Fabian @ Blax Software 1b2559b824 RI cartitems, A exceptions, tests 2025-12-17 12:26:26 +01:00
Fabian @ Blax Software 0e776b6d84 BFM customer uuid morph, RD example product test 2025-12-17 08:29:20 +01:00
Fabian @ Blax Software 67917e6a31 A booking/pool procedures & tests, IA tests, exceptions 2025-12-15 11:32:31 +01:00
Fabian @ Blax Software c711afb570 R reserve to claim 2025-12-04 11:06:09 +01:00
a6a2f5842 c5004158eb A types, enums & statuses, U product models, I shopping trait, A booking support, R tests 2025-12-03 13:59:01 +01:00
a6a2f5842 a61620e607 BF stub migration for SQL 2025-12-02 09:15:53 +01:00
a6a2f5842 a4fedcdb58 A tests, I product action 2025-11-29 20:09:19 +01:00
a6a2f5842 c6c159a4ff I tests & structure 2025-11-29 12:05:02 +01:00
a6a2f5842 ffc8716c22 A stripe & BFI cart 2025-11-28 10:24:07 +01:00
a6a2f5842 929e87bc28 AI payment process & payment provider files 2025-11-26 11:09:52 +01:00
a6a2f5842 c1f531e659 BFI tests 2025-11-24 07:00:07 +01:00
a6a2f5842 ab1e2468ca BFI tests fixing, prices, purchases, cart 2025-11-23 15:07:12 +01:00
a6a2f5842 fda536deea BFIM bugfixed failed tests 2025-11-22 18:09:45 +01:00
a6a2f5842 2c31b6ea98 IM product purchase process 2025-11-22 15:13:30 +01:00
a6a2f5842 afe7359ea4 R product columns 2025-11-22 09:55:58 +01:00
a6a2f5842 b65706e029 MBF product prices migration 2025-11-21 16:30:50 +01:00
a6a2f5842 af4f6b9fa6 MBF product categories migration 2025-11-21 16:03:24 +01:00
a6a2f5842 b76ae2f867 MI product name, short and description 2025-11-21 15:55:15 +01:00
a6a2f5842 2244bc4cf1 BF default json 2025-11-21 15:13:52 +01:00
a6a2f5842 d610cc5717 init 2025-11-21 11:49:41 +01:00