Commit Graph

3 Commits

Author SHA1 Message Date
Fabian @ Blax Software 644c84def3 feat(ledger): shop.paypal.required makes a missing or failing PayPal import throw
Without credentials the PayPal import was a silent no-op, so an app could
lose its PayPal fees from revenue without anyone noticing. With
shop.paypal.required on, a missing account, missing migration or a failed
account import throws (after every account was tried), which the host's
scheduler failure handling can alert on. Default off.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 12:48:05 +02:00
Fabian @ Blax Software 945d7e70aa fix(ledger): count bounced direct debits and Stripe account fees
payment_failure_refund (a SEPA/ACH payment that bounced, with its failure
fee) and Stripe's account-level fees (stripe_fee, stripe_fx_fee, tax_fee)
were not revenue types, so a bounced payment still counted as income and
Billing/Tax fees never lowered net. Both are revenue types now. Account
fees arrive from Stripe as a negative amount; the ledger stores them as
fee with amount 0 so they do not show up as refunds. The daily full
import rewrites existing rows.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 10:51:09 +02:00
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