Agency Flow · Prezentare de flux

Platform · % of spend — singurul hibrid din sistem

Media platform (Google Ads) cu rebate + fee. Excepția asumată de la regula labor-XOR-supplier: financiar de furnizor plus tasks și pontaje. Profitul se descompune în două bucket-uri cu destin diferit: rebate-ul (profit direct, intangibil) și fee-ul clientului (labour-recovery — din el se plătește echipa).

Account manager — setup Engine — automat Specialist — spend + pontaje Finance — facturi
1

O singură dată · la vânzarea serviciului

Setup: două procente per serviciu

Account manager

platform_pct (10%) — rebate-ul pe care platforma îl acordă agenției; client_pct (3%) — fee-ul facturat clientului peste spend. Ambele sunt per serviciu, nu globale — alt client, alt procent negociat.

Drawer · New service
Aurora Cosmetics — Website · auroracosmetics.ro
DMK
grup derivat: DMK
Platform — % of spend (rebate + fee)
Google Ads
10 % — rebate → profit direct
3 % — fee → labour-recovery
nume compus → Aurora Cosmetics — DMK — DMK — Google Ads
În baza de date · la Save
INSERT service + service_pricing_platform
billing_type: 'platform_pct' supplier_id: → Google Ads platform_pct: 10.000 client_pct: 3.000
2

Lunar · după închiderea platformei

Specialistul introduce spend-ul — cascada pornește

Specialist

Un singur input: amount_spend (10.000). Din el, strategia derivă tot: rebate 1.000, cost net către platformă 9.000, fee client 300, factură 10.300. Important: spend-ul nu e un cost (§1.12.1) — e cifra-antet pass-through; costul real e spend − rebate.

Formular · Enter spend — August 2024
10.000 RON — din platformă
10.300 RON = spend × 1,03
Calculul · PlatformPctStrategy
ƒ
PlatformPctStrategy::calculate(ctx) → BillingBreakdown {
  clientInvoice: 10.300, supplierCost: 9.000,  // spend − rebate
  directProfit: 1.000,  // rebate — spread, nu cash
  labourRecovery: 300, labourCost: ctx.labour  // hibridul
}
În baza de date · la salvare
UPDATE service_billing_period
input_amount_spend: 10000.00 snapshot_client_amount: 10300.00 snapshot_supplier_cost: 9000.00 snapshot_direct_profit: 1000.00 snapshot_labour_recovery: 300.00 state: 'cost_entry' → 'ready'
INSERT service_cost_line · materializată
line_type: 'platform_spend' supplier_id: → Google Ads supplier_cost_amount: 9000.00 — netul, nu spend-ul
platforma facturează netul (9.000) — de asta linia de cost poartă 9.000, nu 10.000. Rebate-ul nu e mișcare de cash: nicio factură, niciun receivable — doar spread.
GENERATED calculate de DB
snapshot_gross_profit: 1300.00 snapshot_recovery_net: urmează labour-ul…
3

Pe parcursul lunii · în paralel

Partea de labor: echipa care ține platforma

Echipa task & time_entry — pasul următor

Aici Platform își arată natura dublă: serviciul are tasks, iar orele echipei se adună în labour_cost (ca la A1) — 250 RON în august. DB-ul calculează singur întrebarea de management: a acoperit fee-ul de 3% munca echipei?

În baza de date · după pontaje
UPDATE service_billing_period
snapshot_labour_cost: 250.00
GENERATED calculate de DB
snapshot_recovery_net: +50.00 = 300 − 250 snapshot_net_profit: 1050.00 = 10.300 − 9.000 − 250
recovery_net pozitiv = client_pct dimensionat bine; negativ = echipa costă mai mult decât fee-ul — semnal de renegociere, fără ca rebate-ul să fie atins.
De ce două bucket-uri? Dacă ai aduna totul, un labour mare ar „mânca" din rebate și n-ai ști niciodată dacă problema e fee-ul prea mic sau altceva. Separat, rebate-ul rămâne curat (1.000, direct_profit), iar fee-ul se judecă singur contra orelor (recovery_net) — exact §1.12.3 din spec.
4

1 septembrie + factura Google Ads

Facturile: clientul vede 10.000 + 3%, atât

Finance modulul Billing — urmează

AR (Accounts Receivable): factura clientului arată spend-ul ca o singură cifră (niciodată split-ul 9.000 + 1.000) plus linia de fee 3%. AP (Accounts Payable): Google Ads facturează netul de 9.000, legat de linia de cost. La Issue, perioada devine invoiced și labour-ul îngheață.

Client invoice · DR-2024-0151
linieamountTVA 21%total
Media spend — Google Ads · aug10.000,002.100,0012.100,00
Management fee 3%300,0063,00363,00
total factură12.463,00
client_invoice
10.300
spend + 3% fee
supplier_cost
9.000
AP · netul platformei
direct_profit
1.000
rebate — neatins de labour
recovery_net
+50
300 fee − 250 labour
net_profit
1.050
= 1.000 + 50
DB

service_billing_period — ce se scrie și ce se actualizează

Platform se naște cost_entry; la introducerea spend-ului derivă cascada (rebate / net / recovery), apoi — hibrid — labour-ul se mai actualizează din pontaje până la invoiced.

service_billing_period · ciclul de scriere al rândului lunar
INSERT service_billing_period · la generarea perioadei
state: 'cost_entry' input_amount_spend: NULL — se așteaptă input_supplier_cost: NULL — nu aparține Platform input_supplier_hours: NULL — nu aparține Platform snapshot_*: NULL — toate ready_at: NULL invoiced_at: NULL
rândul lunar se naște gol în cost_entry — doar așteaptă spend-ul de la specialist; nimic derivat încă.
UPDATE service_billing_period · specialistul introduce spend-ul — cascada
input_amount_spend: 10000.00 — scris de specialist snapshot_client_amount: 10300.00 = spend + client_pct×spend snapshot_supplier_cost: 9000.00 = spend − rebate · NETUL snapshot_direct_profit: 1000.00 = platform_pct×spend · rebate snapshot_labour_recovery: 300.00 = client_pct×spend snapshot_fee_profit: 0.00 state: 'cost_entry' → 'ready' ready_at: setat
engine-ul materializează linia platform_spend cu supplier_cost_amount = NETUL (9.000), NU spend-ul; rebate-ul nu e mișcare de cash.
UPDATE service_billing_period · pontaje — hibrid, cât invoiced_at IS NULL
snapshot_labour_cost: 250.00 — din pontaje snapshot_labour_hours: Σ ore snapshot_recovery_net: +50.00 = labour_recovery − labour · generat de DB snapshot_net_profit: 1050.00 = gross − labour · generat de DB
singurul tip supplier cu labour; rebate-ul (direct_profit) rămâne neatins de ore.
UPDATE service_billing_period · reeditare spend înainte de facturare — reset
input_amount_spend: valoare nouă state: 'ready' → 'cost_entry' ready_at: NULL
modificarea spend-ului rupe confirmarea: perioada revine la cost_entry, cascada se recalculează la reconfirmare.
UPDATE service_billing_period · la emitere
state: 'ready' → 'invoiced' invoiced_at: setat — îngheață labour_cost + labour_hours
după invoiced_at hibridul se închide: labour-ul nu se mai actualizează din pontaje.
DB

service_cost_line — linia materializată de engine

La Platform, engine-ul materializează linia la introducerea spend-ului. Cheia: supplier_cost_amount = NETUL (spend − rebate), nu spend-ul — platforma facturează netul; rebate-ul NU e mișcare de cash.

service_cost_line · linia lunii (materializată la input)
INSERT service_cost_line · la introducerea spend-ului (input_amount_spend 10.000 → materializare)
billing_period_id: → perioada supplier_id: → service.supplier_id (platforma) line_type: 'platform_spend' description: auto quantity: 1 unit_cost: = NETUL (9.000) unit_fee: NULL — markup-ul client nu e un fee per-unitate supplier_cost_amount: 9.000 — NETUL (spend − rebate), ancora facturii platformei fee_amount: 1.300 (= markup total = rebate 1.000 + recovery 300) client_amount: 10.300 (= 9.000 + 1.300) state: 'ready' — oglindește perioada position: 1
supplier_cost_amount = qty × unit_cost = 1 × 9.000; client_amount = cost + fee = 9.000 + 1.300.
UPDATE service_cost_line · la emitere
state: 'ready' → 'invoiced' (Locked)
supplier_cost_amount = NETUL (9.000), NU spend-ul (10.000) — platforma îți facturează netul; rebate-ul (1.000) NU e linie de cash, e spread. Pe linie, fee_amount (1.300) e markup-ul total; în snapshot-uri el se descompune în direct_profit (rebate 1.000) + labour_recovery (300). Editarea spend-ului înainte de facturare → UPDATE cu reset (starea liniei revine cost_entry).

Simulare · cascada hibridă, pas cu pas

interactiv · doar vizual

Singurul tip care combină financiarul de furnizor cu orele echipei. Fixezi cele două procente la setup, apoi introduci spend-ul lunar și labour_cost-ul din pontaje. Profitul se rupe în două bucket-uri: rebate (direct_profit, intangibil) și fee client (labour_recovery, din care se plătește echipa). recovery_net spune dacă fee-ul a acoperit orele — fără să atingă rebate-ul. Nimic nu se salvează — e o schiță.

Specialist — spend platformă Echipă — pontaje (labour) Finance — emite factura
Setup · procente per serviciu rol: Account manager
platform_pct = rebate-ul platformei → direct_profit; client_pct = fee-ul facturat clientului peste spend → labour_recovery.
Input lunar · spend + pontaje rol: Specialist & Echipă
Spend-ul nu e un cost — e cifra-antet pass-through; costul real e spend − rebate. Labour-ul se actualizează până la invoiced (hibridul).
Linia de cost materializată perioada: cost_entry
line_typeqtysupplier_cost client_amountstate
platform_spend 1 9.000 10.300 cost_entry
client_invoice
10.300
spend + fee client_pct
supplier_cost
9.000
netul — spend − rebate
direct_profit
1.000
rebate — neatins de labour
labour_recovery
300
fee client — plătește echipa
recovery_net
+50
labour_recovery − labour
net_profit
1.050
gross − labour
Ce ilustrează: rebate-ul NU e mișcare de cash — platforma facturează netul (9.000), clientul plătește gross-ul; diferența e spread, fără receivable. Clientul vede spend-ul (10.000) + linia de fee client_pct (300) ca o singură cifră — niciodată split-ul 9.000 + 1.000. Un recovery_net negativ = client_pct subdimensionat față de orele echipei; rebate-ul (direct_profit) rămâne neatins de ore.

Platform, pe scurt: toate mecanismele deodată

Ultimul flux le folosește pe toate — de asta e și testul suprem al schemei:

Din familia supplier

  • supplier obligatoriu pe serviciu (Google Ads)
  • input lunar (input_amount_spend)
  • linie de cost materializată → ancora AP (9.000)

Din familia labor

  • tasks + time entries → labour_cost real
  • labour îngheață abia la invoiced_at
  • singura excepție de la labor-XOR-supplier

Doar aici

  • bucket-uri separate: direct_profit vs labour_recovery
  • recovery_net — generat de DB, KPI de renegociere
  • rebate = spread, nu cash — fără entitate de receivable