Agency Flow · Prezentare de flux · Referință istorică

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
Referință istorică, conceptuală. Această pagină păstrează lifecycle-ul A1 ca material de referință. Ruta veche /t/{tenantId}/digital-assets/{digitalAssetId}/services/fixed-fee/new, FixedFeeServiceCreator și generarea perioadelor/cost lines la save nu mai reprezintă UI-ul curent. Setup-ul actual folosește selectorul /t/{tenantId}/digital-assets/{digitalAssetId}/services/new, apoi ruta generică /t/{tenantId}/digital-assets/{digitalAssetId}/services/new/{billingType}, cu forme dedicate pentru toate cele 10 billing types; setup-ul nu generează perioade sau cost lines. Pentru implementarea curentă, consultă planul de implementare și fluxul generic de setup.
1

O singură dată · la vânzarea serviciului

Setup: drawerul de serviciu

Account manager

În fluxul A1 istoric, se alegeau assetul (clientul se deriva din el, nu se alegea separat), departamentul (grupul se deriva din el) și billing type-ul Fixed fee (A1). Singurul parametru de preț era suma lunară fixă. Numele serviciului nu se scria — se compunea.

Tracer bullet-ul istoric folosea ruta contextuală /t/{tenantId}/digital-assets/{digitalAssetId}/services/fixed-fee/new: un formular minim A1, fără supplier, care persista prin FixedFeeServiceCreator. După redirect, Digital Asset afișa un panou minim Services cu departamentul, tipul, suma fixă și planning-ul. Rândurile erau ordonate după statusul efectiv: întâi active, apoi pe pauză, apoi închise; departamentul și data creării rămâneau departajarea în interiorul fiecărui grup. Fiecare rând ducea la pagina read-only /t/{tenantId}/services/{serviceId}, unde se verificau identitatea serviciului, pricing-ul A1, orele planificate și faptul că serviciul labor apărea ca No supplier. Aceeași pagină păstra pricing-ul de setup ca default și îl putea edita separat prin Edit defaults (fixed_amount și planning_hours). Excepțiile lunare stăteau pe rândul perioadei: o lună ready, dar nefacturată, putea fi ajustată dintr-un drawer de period cu sumă manuală și motiv obligatoriu. Programarea finalului folosește Schedule end / Change end date și formularul Last active day, care poate seta, schimba sau șterge end_date ca ultima zi activă, inclusiv; nu există status persistat closed. O dată viitoare afișează Scheduled to end, serviciul rămâne activ până la sfârșitul acelei zile și devine Closed în ziua următoare. Ultima lună nefacturată se recalculează după zilele eligibile, perioadele ulterioare se elimină, iar orice conflict cu date facturate sau cu input uman blochează salvarea. După închiderea efectivă, setup-ul, pricing-ul și perioadele rămân istoric read-only. Pauza temporară folosește Pause service / Resume service și salvează în service_pause date calendaristice exacte: paused_from și paused_until, ambele inclusive. Formularele folosesc Paused from, Last paused day și prima zi activă Resume on. Pauza oprește simultan livrarea și billing-ul. O lună complet suspendată primește service_billing_pause și nicio perioadă; o pauză parțială păstrează perioada cu baza auditabilă billable_days / calendar_days. „Resume on 20 August” înseamnă că ultima zi suspendată salvată este 19 August. Reluarea în chiar prima zi programată anulează pauza prin traseul auditat de corecție. O pauză planificată cu final, dar fără actor de reluare, poate fi încă schimbată; o pauză anulată sau deja reluată rămâne audit imuabil. Toate Dashboard-ul păstrează un tabel de audit cu intervalele scheduled/current/completed/cancelled, Resume on, actorii și motivele. Perioadele arată intervalul calendaristic, baza billable_days / calendar_days și ecuația din snapshot_pricing_fixed_amount. Suma fixă folosită este persistată pe perioadă, deci schimbarea default-ului nu rescrie auditul; rândurile legacy facturate fără baza sau inputul original afișează Historical calculation. Toate scrierile derivate din calendar folosesc același coordinator: setup, editarea pricing-ului cu recalcularea lunilor deschise, pause/resume/cancel, service end, generator/reconciler și recompute/reopen/close/override de perioadă. Operația exterioară deschide o singură tranzacție proprie și refuză să adopte o tranzacție DBAL externă deja activă. Blochează pesimist rândul Digital Asset comun și reîncarcă o singură dată Service-ul și rândul financiar direct înainte de citirea stării mutabile. Operațiile imbricate pe același coordinator refolosesc lock-ul și refresh-ul și lasă flush/commit limitei exterioare. Astfel, un writer cu o perioadă stale nu poate suprascrie o reconciliere de calendar deja comisă, iar batch-urile își păstrează un singur flush. Query-urile canonice reîncarcă rândurile existente fără să piardă rândurile încă programate în UnitOfWork. Metodele engine-ului care primesc coverage și metodele mutante ale reconcilerului sunt inner-only și verifică runtime că Digital Asset-ul Service-ului este blocat în scope. Sub MariaDB REPEATABLE READ, batch-ul capturează candidații înaintea tranzacției, deduplică și sortează Digital Asset-urile după UUID, apoi le blochează pe toate înainte de callback, refresh sau orice consistent read; altfel primul SELECT ar fixa un snapshot care poate ascunde un pause/pricing commit ulterior. Fiecare limită exterioară îngheață setul declarat de Digital Asset-uri imediat după lock-uri și înainte de primul refresh, read sau callback. Operațiile imbricate pot refolosi numai asset-uri din setul înghețat; un asset omis este respins înainte de orice lock, refresh sau work suplimentar care ar putea inversa ordinea lock-urilor. Batch-ul este outermost și îngheață setul complet sortat și deduplicat înainte de work. Un batch gol îngheață un set gol și respinge asset work imbricat; setul se elimină la ieșirea din limita exterioară, inclusiv după excepții. Comenzile per-Service precolectează perechea de ID Service/Digital Asset, deschid tranzacția, obțin o referință Digital Asset fără SELECT, o blochează și abia apoi încarcă Service-ul fresh. Emiterea facturii și fluxurile financiare F1 vor adopta același protocol când vor fi implementate. Schimbările de calendar reconciliază doar lunile nefacturate și nu șterg niciodată în tăcere pontaje, inputuri, linii sau override-uri. Reconcilierea post-migrare verifică înainte de orice mutație UnitOfWork: coverage zero nu poate șterge un override manual sau o linie deliverable user-owned, iar coverage zero/parțial nu poate exclude pontaje datate. La save, FixedFeeServiceCreator pornea generatorul pentru lunile eligibile, așa că un A1 cu invoice_start_date în luna curentă se năștea deja cu perioada ready. Din aceeași pagină se putea reporni manual generatorul idempotent doar după ce luna curentă ajungea la invoice_start_date; înainte de acea lună acțiunea nu apărea, iar POST-ul direct era respins fără rând lunar sau skip. Se vedeau rândurile lunare cu state, suma clientului, labour și net profit snapshot. Drawerul complet cu toate tipurile rămânea pasul următor.

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 effective state: active — derivat, nu persistat
CHK_SERVICE_SUPPLIER_BY_TYPE ✓ — A1 e labor, supplier-ul trebuie să fie NULL. UNIQ_SERVICE_OPEN_ASSET_DEPARTMENT_SUPPLIER_KEY ✓ — un singur serviciu deschis DMK–DMK pe acest asset.
INSERT service_pricing_fixed_fee
service_id: ↑ același id fixed_amount: 3500.00 planning_hours: 24.00
tabel de pricing 1:1 — aplicația îl persistă împreună cu service într-o singură tranzacție.
Strict ca setup, A1 = două rânduri: service și service_pricing_fixed_fee. În implementarea tracer bullet, același use-case pornește imediat generatorul pentru lunile eligibile; dacă există deja o lună de facturat, apare și service_billing_period. service_cost_line rămâne zero la A1.
2

1 august · automat

Luna se deschide: engine-ul creează perioada

Engine

Un generator lunar calculează mai întâi acoperirea calendaristică exactă. Creează service_billing_period dacă rămâne cel puțin o zi facturabilă; numai când o pauză elimină toate zilele eligibile creează service_billing_pause. Intervalele de pauză anulate sunt ignorate. Entry point-ul operațional este app:billing:generate-periods, cu --tenant=<uuid> pentru rulare pe un tenant și --month=YYYY-MM pentru generare până la luna respectivă. Comanda este idempotentă: rerun-ul nu dublează perioadele și nu emite facturi. Acțiunea manuală de pe Service detail urmează aceeași regulă de eligibilitate și apare numai din luna invoice_start_date. După migrarea calendarului exact, app:billing:reconcile-calendar completează doar lunile A1 deja reprezentate; nu generează istoric nou și nu recalculează perioade invoiced. Migrarea rulează prima, reconcilierea a doua, apoi generatorul normal. Pentru A1 nu există nimic de introdus și suma nu depinde de ore, dar componenta fixă se proratează cu billable_days / calendar_days într-o lună parțială, 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: ctx.coverage.prorate(3.500), supplierCost: 0, labourCost: ctx.labour.cost,
  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 calendar_days / billable_days: 31 / 31 snapshot_pricing_fixed_amount: 3500.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 sunt modificate automat. Dacă august trebuie facturat la 4.000, dar default-ul serviciului rămâne 3.500, finance/ops ajustează doar perioada august din drawerul rândului. Periodul păstrează suma manuală + motivul, snapshot_client_amount devine 4.000, iar lunile următoare continuă să pornească din fixed_amount.
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 calendar_days / billable_days: 31 / 31 snapshot_pricing_fixed_amount: 3500.00 — inputul înghețat al ecuației snapshot_client_amount: = fixed_amount × billable_days / calendar_days (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 automat lunile cu ready_at setat (A1 e ready din naștere). Aplicarea default-ului curent unei luni ready, nefacturate, cere reopen + recompute explicit și autorizat. Separat, finance/ops poate ajusta doar suma acelei perioade: engine-ul păstrează suma manuală ca input, recalculează snapshot-ul și pune A1 înapoi ready. Pentru o lună deja invoiced, corecția merge prin storno/adjustment.
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
editarea default-ului nu rescrie automat o perioadă ready
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). Editarea default-ului de setup afectează perioada ready numai după acțiunea explicită reopen + recompute. 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 pentru servicii deschise (open_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.