Commit Graph

13 Commits

Author SHA1 Message Date
Fabian @ Blax Software 53832f49ce fix(stripe): cents everywhere, idempotent refunds, admin refunds, webhook ledger sync
- Webhook handlers stored euros in cent columns (charge.succeeded,
  charge.refunded, refund.created, payment_intent.succeeded, the success
  redirect, dispute/invoice notes). All amounts are cents now; purchases get
  their own line amount as amount_paid.
- One Stripe refund arrives as refund.created AND charge.refunded, in either
  order. New Order::applyStripeRefund() / applyStripeRefundedTotal() raise the
  order's Stripe refund total (meta.stripe_refunded, meta.stripe_refunds) to
  what Stripe confirmed, so nothing is counted twice. refund.created now finds
  Checkout orders by payment_intent.
- New ShopService::refundOrder($order, $cents, $reason) refunds through the
  Stripe API (idempotency key per order/amount/state) and records it, plus
  refundableAmount(). Bind \Stripe\StripeClient to swap the client.
- charge.succeeded remembers meta.stripe_charge_id; with
  shop.ledger.sync_on_webhook (default off) charge/refund/dispute webhooks
  upsert the balance transactions into stripe_transactions.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 10:50:25 +02:00
Fabian @ Blax Software 08e459959e style: pint the files touched by the OrderPaid / shop.models.order fix
Formatting only (laravel preset via pint.json): import ordering, FQCN -> use,
`new X()` -> `new X`, `!` spacing, phpdoc trims, braces on one-line continue.
No behaviour change; suite re-run green (1485 tests).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-24 08:24:59 +02:00
Fabian @ Blax Software 0d9823df04 fix(orders): dispatch OrderPaid once; honour shop.models.order on webhook + service paths
OrderPaid was defined but never dispatched. Order::booted() now fires it from
the `updated` hook exactly when paid_at goes null -> value (recordPayment()
reaching amount_total, the Stripe checkout webhook, or a direct update). Later
saves of a paid order, partial payments and overpayments do not re-fire it, and
a host subclass configured via shop.models.order gets it with its own instance.

StripeWebhookController, ShopService and Cart::checkout used the base
Blax\Shop\Models\Order statically (createFromCart, find, where, scopes,
aggregates), so a host app's configured Order subclass — with its own boot
hooks — was bypassed on the real webhook path. They now resolve
config('shop.models.order') through a small orderModel() helper. Return types
are unchanged (subclass instances satisfy Order); Order::formatMoney stays a
pure static.

Tests: OrderPaidEventTest (once-only on recordPayment, partial payments,
direct paid_at update, host subclass) and StripeWebhookOrderModelTest
(handleCheckoutSessionCompleted builds the configured subclass and its boot
hook runs; webhook/ShopService/Cart finders return it). Suite: 1485 tests green.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-24 08:22:55 +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 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 7aeffd27a9 RI optimizations 2025-12-29 11:11:27 +01:00
Fabian @ Blax Software 6e9c9043ae I order, A handy methods 2025-12-29 10:26:51 +01:00
Fabian @ Blax Software cabae43950 A facades, U readme 2025-12-09 10:59:46 +01:00