# Finance — Earnings + Wallet/Withdrawals + VAT — BUSINESS RULES

Rules the **demo** encodes (canonical), each tagged with our status. Refs:
demo = `/private/tmp/Navagoo_MI_dev/navagoo-app/src/lib/finance.ts` (+ `.test.ts`,
`vat.test.ts`); ours = repo cwd.

## VAT

1. **VAT strip.** For a VAT/taxable shop, `base = inclusive / (1 + vatPct/100)`;
   `vat = inclusive − base`. Non-VAT shop → vat 0, base = inclusive.
   Demo `noVat`/`vatBreakdown` (`finance.ts:31-71`; `vat.test.ts:9-31`).
   Ours `Earnings::calculateShopVat` (`base/(1+r)*r`, `Earnings.php:547-573`),
   gated on `is_taxable` (`Shop.php:135-136`). **DONE.**
2. **VAT on Navagoo fees is added ON TOP, not embedded.** `vatAmount =
   feeAmount × vatPct/100` per fee row (demo `finance.ts:106,137,164,185`).
   Ours `calculateVatNavagoo = (navagooFees + serviceFee) × rate`
   (`Earnings.php:610-618`). **DONE (matches for the marketing+service slice).**
3. **VAT rate source.** Demo: `config.vatPct` (15 in fixtures). Ours: global
   `Settings::findOne(1)->taxes` (`Earnings.php:561`). **DONE** (single global
   rate; not per-shop).

## Marketing fee

4. **Charged ONLY to navagoo-sourced customers.** `shop_owned` → no marketing fee
   (demo `finance.ts:248`; example D `finance.test.ts:237-256`).
   Ours charges `platform_commission` to **every** earning regardless of source.
   **MISSING.**
5. **Basis = booking value excluding VAT** (`noVat(bookingValue)`,
   `finance.ts:133`). Ours uses **collected-excl-VAT** (`net_collected_excl_vat`,
   `Earnings.php:598`) → diverges when not fully collected online. **PARTIAL.**
6. **Minimum marketing fee floor** = `config.minMarketingFee` (default 5);
   `amount = max(basis×rate%, min)` (`finance.ts:135`; example B2 `→ 5`,
   `finance.test.ts:195`). Ours has no floor. **MISSING.**
7. **Grace-window waiver.** Inside the shop's grace window the marketing fee is
   waived to 0 (`finance.ts:136,293-295`; example E `finance.test.ts:258-277`).
   Ours has no grace window. **MISSING.**
8. **Marketing-fee rate** is `shop.marketingFeeRatePct`. Ours
   `shop->platform_commission` (`Shop.php:62`, 0–100 validated, `:215`). **DONE**
   (per-shop rate exists).

## Payment-processing (gateway) fee

9. **Created only when money collected online** (`amountCollected>0`);
   `amount = collected×rate% + fixed`; VAT on top; non-refundable; never reverses
   (`finance.ts:95-118`; examples A `14.30`, C proc `3.57` survives cancel). Ours
   has **no processing-fee concept**. **MISSING.**

## Refunds & reversals

10. **Refund zone by cancellation timing:** `≥ cancelFullHours → full`;
    `≥ cancelPartialHours → partial`; else `none` (`finance.ts:318-323`).
    Ours: no zone logic; `refund_value` is a stored number. **MISSING.**
11. **Customer-refund amount:** shop-cancel → full refund; deposit →
    non-refundable (0); customer-cancel → by zone
    (`partial → ×partialRefundPct/100`) (`finance.ts:326-338`; example C refund 0).
    Ours: none of these branches — refund is pre-entered. **MISSING.**
12. **Marketing-fee reversal** on customer cancellation: negative offsetting
    ledger row for `full → 100%` / `partial → partialRefundPct/100`; only
    `marketing_fee` reverses; processing fee never does
    (`finance.ts:205-222,269-282`; example C reversal −6.13). Ours: no reversal
    rows. **MISSING.**
13. **No-show CEO rule:** an uncollected (pay-on-visit) no-show incurs **no**
    marketing fee; a *completed* pay-on-visit booking still does (carried forward)
    (`finance.ts:256-260`; examples B `len 0`, B2 `owes 5.75`). Ours: fee derives
    from collected/booking_method, not from this terminal-state rule. **MISSING.**

## Settlement, eligibility & withdrawal

14. **Net payout per booking** =
    `(collected − refund) + tips − marketingFees − processingFees − feeVat`,
    over UNPAID non-pending fee rows (`finance.ts:358-383`). Ours
    `net_collectible = final_collected + tip − navagoo_fees − vat_navagoo`
    (`ShopEarning.php:268-293`) — same shape **minus the processing-fee term**.
    **PARTIAL.**
15. **Booking-method payout branches:** Mobile = full formula; Walk-in-Shop drops
    `final_collected` (cash taken by salon, Navagoo nets tip − fees − VAT);
    Walk-in-Specialist excluded entirely. Ours `ShopEarning.php:280-293` +
    controller filter `EarningsController.php:72-78`. **DONE (ours-specific; no
    demo equivalent — demo has no walk-in-method split).**
16. **Settlement eligibility:** booking terminal (completed | no_show | cancelled)
    AND `daysBetween(now, transactionDate) ≥ settlementHoldDays`
    (`finance.ts:472-478`). Ours: `daysElapsed ≥ minimum_elapsed_period_days`
    (default 7) measured from earning `created_at`
    (`EarningsController.php:167,174-182`). **PARTIAL** (hold from earning row,
    not booking transaction date; not re-deriving terminality).
17. **Minimum withdrawal:** withdrawal allowed only when eligible net `≥
    minWithdrawalAmount` (default 5000) (`finance.ts:518`). Ours: same gate,
    `minimum_withdrawal_amount` default 5000 (`EarningsController.php:166,197`;
    `AgentsWalletController.php:262,351-360`). **DONE.**
18. **Carry-forward / negative balance:** when outstanding fees exceed collectable
    earnings the shop **owes Navagoo**; `shopCarriedBalance = max(0,
    −runningBalance)` (`finance.ts:588-631`). Per-shop credit limit
    `effectiveCarryThreshold` (`finance.ts:569-571`). Ours: no negative-balance /
    carry-forward model. **MISSING.**
19. **Don't double-settle:** rows already in a transfer/invoice are excluded;
    paid fees drop out of the payout but still count in P&L
    (`finance.ts:424-430,541-559`). Ours: enforced operationally by
    `withdrawal_id IS NULL` filter + stamping `withdrawal_id`/`transfer_request_id`
    on settle (`AgentsWalletController.php:256,529-549`). **PARTIAL** (works, but no
    paid-vs-incurred distinction).

## Pricing / display helpers

20. **Service discount on catalogue price:** `fixed` subtracts (floor 0);
    `percent` clamps 0–100 (`finance.ts:43-52`; `vat.test.ts:33-46`). Display-only.
    Outside the earnings/wallet surface; not evaluated here. **N/A.**
21. **Subscription price by period** (`12mo`/`6mo`/monthly) + `pricePerSession`
    (`finance.ts:648-656`). Subscription billing, not earnings/wallet. **N/A.**

## Permissions / shop-scoping (ours — strengths)

22. **Shop-scoping:** earnings/withdrawals filtered by current shop
    (`EarningsController.php:50-53`, `ShopEarningSearch`); withdrawal `findModel`
    + bank-account lookup shop-scoped to prevent IDOR/cross-shop deletion
    (`AgentsWalletController.php:631-675`). The demo (single-tenant prototype)
    has no equivalent. **DONE (ours-only).**
