Agency Flow · Prezentare de flux

A1 · Fixed fee — de la fixed_amount la factură

Cel mai simplu billing type, urmărit cap-coadă pe un serviciu real: ce completează omul, ce rânduri se inserează, ce calculează engine-ul, ce calculează baza de date singură — și în ce moment îngheață fiecare cifră.

Account manager — setup Engine — automat Echipa — pontaje Finance — facturare
1

O singură dată · la vânzarea serviciului

Setup: drawerul de serviciu

Account manager

Se aleg assetul (clientul se derivă din el, nu se alege separat), departamentul (grupul se derivă din el) și billing type-ul Fixed fee (A1). Singurul parametru de preț: suma lunară fixă. Numele serviciului nu se scrie — se compune.

Drawer · New service
Made by society RO — Website · madebysociety.ro
client derivat: Made by society RO → compania legală a clientului
DMK
grup derivat: DMK
Fixed fee (A1)
3.500 RON
24 h / mo — estimare, nu cap
01.08.2024
01.08.2024
nume compus → Made by society RO — DMK — DMK
În baza de date · la Save
INSERT service
billing_type: 'fixed_fee' digital_asset_id: → madebysociety.ro department_id: → DMK supplier_id: NULL start / invoice_start: 2024-08-01 status: 'active'
CHK_SERVICE_SUPPLIER_BY_TYPE ✓ — A1 e labor, supplier-ul trebuie să fie NULL. UNIQ_SERVICE_COMPOSED_NAME ✓ — un singur serviciu DMK–DMK pe acest asset.
INSERT service_pricing_fixed_fee
service_id: ↑ același id fixed_amount: 3500.00 planning_hours: 24.00
tabelul-copil CTI — Doctrine le scrie pe amândouă într-o singură tranzacție, ca o singură entitate.
Atât. Setupul A1 = două rânduri. Nu se atinge nici service_billing_period, nici service_cost_line — ele aparțin lunilor, nu serviciului.
2

1 august · automat

Luna se deschide: engine-ul creează perioada

Engine

Un generator lunar creează câte un rând service_billing_period pentru fiecare serviciu activ. Pentru A1 nu există nimic de introdus și suma nu depinde de ore, așa că rândul se naște direct ready: strategia rulează la creare și sumele facturabile îngheață pe loc. (A1 e singurul cu privilegiul ăsta — la A2/B factura depinde de orele lunii, deci perioada lor se naște cost_entry.)

Calculul · FixedFeeStrategy
ƒ
// strategia e pură: pricing în → breakdown afară
FixedFeeStrategy::calculate(ctx) → BillingBreakdown {
  clientInvoice: 3.500, supplierCost: 0, labourCost: ctx.labour,
  directProfit: 0, feeProfit: 0, labourRecovery: 0
}
În baza de date
INSERT service_billing_period
period: 2024-08-01 state: 'ready' ready_at: 2024-08-01 06:00 snapshot_client_amount: 3500.00 snapshot_supplier_cost: 0.00 snapshot_labour_cost: 0.00 input_supplier_cost / _hours / _spend: NULL
coloanele input_* rămân NULL — la A1 nu participă (whitelist per tip, regulă de engine).
GENERATED calculate de DB, automat
snapshot_gross_profit: 3500.00 snapshot_net_profit: 3500.00 snapshot_recovery_net: 0.00
nimeni nu le scrie vreodată — MariaDB le derivă din primitive la fiecare UPDATE.
ready_at e setat → cele 3.500 nu mai pot fi modificate de nimeni. Dacă account managerul schimbă mâine fixed_amount la 4.000, se recalculează doar lunile viitoare — augustul rămâne 3.500.
3

Pe parcursul lui august

Echipa lucrează: pontajele curg

Echipa task & time_entry — pasul următor de implementare

Factura A1 e fixă, dar profitul nu e — depinde de cât timp consumă echipa. Orele se loghează pe task-urile serviciului; costul intern e per angajat, din payroll (cost/h efectiv la data pontajului). După fiecare pontaj, engine-ul actualizează snapshot_labour_cost, iar DB-ul recalculează singur netul.

Time entries · august
angajattaskorecost/h interncost
Ana — Senior DMKCampanie always-on15,01001.500
Radu — DMK SpecialistRapoarte + optimizări8,080640
total august (planificat: 24 h)23,02.140
În baza de date · după fiecare pontaj
UPDATE service_billing_period · 2024-08
snapshot_labour_cost: 0 → 1.500 → 2.140
GENERATED DB-ul urmează automat
snapshot_net_profit: 3.500 → 2.000 → 1.360
netul nu poate rămâne în urmă — e aceeași tranzacție, prin construcție.
De ce e voie? ready_at îngheață doar facturabilele; labour-ul curge liber până la invoiced_at — pontajele sosesc mereu cu întârziere, iar factura clientului nu depinde de ele la A1.
4

1 septembrie · batch lunar

Facturarea: generate → review → issue

Finance modulul Billing — urmează

Fluxul ghidat de facturare (clienți → assets → servicii → generate) adună toate perioadele ready din august ale clientului și produce o factură per client, cu câte o linie per serviciu. Linia noastră vine direct din snapshot — nimic nu se recalculează.

Client invoice · draft DR-2024-0132
linieqtypreț unitarTVA 21%total
Made by society RO — DMK — DMK · aug 202413.500,00735,004.235,00
total factură (1 serviciu la acest client)4.235,00
În baza de date · la Issue
INSERT client_invoice + client_invoice_line
description: numele compus unit_price: 3500.00 vat_rate: 21% service_id / billing_period_id: → legătura spre sursă
tabelele de Billing vin în pasul următor — perioada e deja construită ca să fie sursa liniei.
UPDATE service_billing_period · 2024-08
state: 'ready' → 'invoiced' invoiced_at: 2024-09-01 10:14
invoiced_at setat → și labour-ul îngheață. Un pontaj întârziat pe august e respins — se corectează doar explicit, prin storno.
5

Oricând după aceea

Raportarea: doar citiri, zero calcule

Engine

Profitabilitatea pe serviciu / client / agenție e un SUM peste rândurile de perioadă — cifrele au fost înghețate la momentul potrivit, deci raportul e stabil istoric și nu re-rulează nicio strategie.

client_invoice
3.500
înghețat la ready
labour_cost
2.140
înghețat la invoiced
net_profit
1.360
calculat de DB
margin
38,9%
doar ratio de afișare
DB

service_billing_period — ce se scrie și ce se actualizează

A1 se naște direct ready — suma facturabilă e constantă din setup. Singurul lucru care se mai mișcă lunar e labour-ul (din pontaje), până la invoiced.

service_billing_period · ciclul de scriere al rândului lunar
INSERT service_billing_period · la generare (naștere)
state: 'ready' ready_at: setat acum input_supplier_cost / input_supplier_hours / input_amount_spend: NULL — nu aparțin A1 snapshot_client_amount: = fixed_amount (ex. 3.500) snapshot_supplier_cost: 0 — labor pur snapshot_labour_cost: 0 snapshot_labour_hours: 0 snapshot_direct_profit: 0 snapshot_fee_profit: 0 snapshot_labour_recovery: 0 snapshot_gross_profit: 3.500 · generat de DB snapshot_net_profit: 3.500 · generat de DB snapshot_recovery_net: 0 · generat de DB
primitivele se scriu la creare, iar gross / net / recovery_net sunt coloane generate — DB-ul le derivă singur, nimeni nu le scrie.
UPDATE service_billing_period · pe măsură ce sosesc pontaje (cât invoiced_at IS NULL)
snapshot_labour_cost: ← Σ ore × cost/h snapshot_labour_hours: ← Σ ore snapshot_net_profit: recalculat de DB = client − labour
client_amount NU se schimbă (fix); doar labour_cost / labour_hours / net se mișcă.
UPDATE service_billing_period · la emiterea facturii
state: 'ready' → 'invoiced' invoiced_at: setat — îngheață labour_cost + labour_hours
după invoiced, pontajele pe luna asta se resping; editarea lui fixed_amount în setup NU atinge lunile cu ready_at setat (A1 e ready din naștere) — se aplică lunilor viitoare.
DB

service_cost_line — niciuna la acest tip

A1/A2/B sunt labor pur (supplier_id NULL, impus de CHECK) — nu există cost de furnizor, deci zero linii în service_cost_line. Rândul lunar din service_billing_period e singura evidență: factura clientului + profitabilitatea din pontaje.

Contrast: la C–Platform engine-ul materializează o linie de cost (ancora AP (Accounts Payable) a facturii furnizorului); la F1 liniile sunt introduse de oameni, una per livrabil. Aici nu e cazul — nu ai către cine să emiți o factură de furnizor.

Simulare · factură fixă, profit variabil

interactiv · doar vizual

Esența A1: clientul plătește suma fixă indiferent de ore, dar profitul depinde de cât lucrează echipa. Echipa adaugă pontaje (oră × cost/h) care ating doar labour_cost și net_profit; Finance emite factura → invoiced și îngheață labour-ul. Schimbă orele și vezi cum un serviciu profitabil la puține ore devine neprofitabil dacă orele cresc. Nimic nu se salvează — e o schiță.

Specialist / echipă — pontaje Finance — emitere factură
Setup · pricing A1
suma fixă intră în client_invoice — nu depinde de ore
Add pontaj rol: Echipă
pontajele ating doar labour_cost / net_profit
Time entries starea perioadei: ready
angajatorecost/h cost linieacțiune
total labour 0
client_invoice
0
suma fixă, constantă
labour_cost
0
Σ ore × cost/h
net_profit
0
client − labour
net_margin
doar ratio de afișare
Ce ilustrează: clientul plătește suma fixă indiferent de ore — pontajele ating doar labour_cost și net_profit, până la invoiced_at (înghețare în două faze: ready îngheață factura, invoiced îngheață labour-ul). Un serviciu profitabil la puține ore devine neprofitabil dacă orele cresc — de-aia se pontează chiar dacă factura e fixă.

Recapitulare: cine scrie ce

Fiecare coloană din schemă are exact un stăpân — de aici vine predictibilitatea fluxului.

Omul

  • setup-ul serviciului: service + service_pricing_fixed_fee
  • pontajele (task / time_entry)
  • acțiunea de facturare (generate → issue)

Engine-ul

  • creează perioada lunară, direct ready
  • rulează FixedFeeStrategy → scrie cele 6 primitive
  • actualizează labour_cost la fiecare pontaj
  • mișcă stările + setează ready_at / invoiced_at

Baza de date

  • gross / net / recovery_net — coloane generate
  • numele compus unic (supplier_key)
  • CHECK-uri: supplier NULL la A1, period = ziua 1, sume ≥ 0
Ce n-a folosit A1 deloc: coloanele input_* (rămân NULL) și service_cost_line (zero rânduri) — pentru că A1 n-are furnizor și n-are nimic de introdus lunar. Exact în golurile astea intră celelalte tipuri: E își pune costul în input_supplier_cost, F2 orele, Platform spend-ul, iar F1 își aduce liniile de deliverable. Aceleași tabele, alt actor pe aceleași locuri — ăsta e tot strategy pattern-ul.