Agency Flow · Prezentare de flux

F1 · Per deliverable — două roluri, o linie pe livrabil

Cel mai bogat flux din sistem: serviciul nu are supplier — fiecare linie de livrabil și-l alege pe al ei. Specialistul introduce costurile, Finance/Ops pune fee-ul, iar fiecare linie își parcurge propriul lifecycle. Singurul tip care folosește starea awaiting_fee.

Account manager — setup Engine — automat Specialist — supplier, qty, cost Finance / Ops — fee + send back
1

O singură dată · la vânzarea serviciului

Setup: nici parametri, nici supplier

Account manager

Ca la E, nu există tabel de pricing. În plus, câmpul Supplier din drawer dispare: F1 e singurul tip supplier fără furnizor la nivel de serviciu — furnizorii trăiesc pe linii, și un singur serviciu F1 poate traversa oricâți. Numele compus rămâne fără supplier, ca la tipurile labor.

În baza de date · la Save
INSERT service · doar părintele
billing_type: 'per_deliverable' supplier_id: NULL — CHECK-ul o cere digital_asset_id: → madebysociety.pl department_id: → Linkbuilding (SEO)
nume compus → Made by society PL — SEO — Linkbuilding (fără supplier, deliberat).
2

1 august + pe parcursul lunii

Specialistul construiește liniile

Specialist

Perioada se naște cost_entry, goală. Pe măsură ce livrabilele se conturează, specialistul adaugă linii: descriere, supplier (per linie!), cantitate, cost/unitate. Coloana de fee nici nu-i apare în UI — e teritoriul finance. Cu „Submit cost", linia pleacă spre finance.

Formular · Add deliverable line rol: Specialist
Article — Outreach
Outreach Media
4
350 RON
În baza de date · per linie adăugată
INSERT service_cost_line · introdusă de OM, nu de engine
line_type: 'deliverable' description: 'Article — Outreach' supplier_id: → Outreach Media quantity: 4 unit_cost: 350.00 unit_fee: NULL — nu e treaba specialistului state: 'cost_entry' → 'awaiting_fee' la submit
Aici regula de ownership se inversează: la C–E liniile erau materializate de engine și interzise omului; la F1 sunt singura intrare umană, iar starea per linie e mașina de stări reală. Starea perioadei devine un simplu rezumat derivat din linii.
3

Pe măsură ce liniile sosesc · rol: Finance/Ops

Finance pune fee-ul — sau trimite înapoi

Finance / Ops

Finance vede liniile în awaiting_fee și completează fee/unit — „Enter fee" → linia devine ready. Dacă ceva nu e în regulă, „Send back" o întoarce la specialist. Iar dacă specialistul reeditează costul unei linii deja ready, linia se resetează automat la cost_entry — fee-ul trebuie reconfirmat.

Deliverables · August 2024 instantaneu la mijlocul lunii
deliverablesupplierqtycost/ufee/uclientstateacțiune
Article — OutreachOutreach Media4350901.760invoicedLocked
Article — NativeContentbox228070700readySend back
Static designContentbox6120awaiting_feeEnter fee
VideoWebify1cost_entryAwaiting specialist
Invoiceable (ready + invoiced)2.460
În baza de date · la Enter fee
UPDATE service_cost_line · „Article — Native"
unit_fee: 70.00 — scris de finance fee_amount: 140.00 client_amount: 700.00 = 2 × (280 + 70) state: 'awaiting_fee' → 'ready'
editare pe roluri, la nivel de câmp: specialistul deține cost-ul, finance deține fee-ul și reopen-ul (spec §1.11a).
UPDATE service_billing_period · recalculat din linii
snapshot_supplier_cost: 1.960 (liniile ready+invoiced) snapshot_fee_profit: 500 snapshot_client_amount: 2.460 state: derivat: „awaiting_fee" cât există linii în urmă
Clientul nu vede niciodată split-ul 350 + 90 — doar 440/bucată. Iar în factura lunii intră exclusiv liniile ready + invoiced: „Static design" și „Video" rămân pe luna viitoare dacă nu se închid la timp. Nicio rotunjire intermediară — se rotunjește o singură dată, la facturare.
4

1 septembrie + la sosirea facturilor furnizorilor

O factură client, mai multe facturi furnizor

Finance modulul Billing — urmează

AR (Accounts Receivable): liniile invoiceable intră în factura clientului cu suma combinată (2.460 + TVA). AP (Accounts Payable): fiecare linie se leagă de factura propriului supplier — Outreach Media își revendică linia de 1.400, Contentbox pe a lui de 560. Un serviciu, doi (sau zece) furnizori, reconciliere rând cu rând.

În baza de date · legăturile AP
INSERT supplier_invoice_line · factura Outreach Media
amount: 1400.00 (4 × 350) service_cost_line_id: → „Article — Outreach"
INSERT supplier_invoice_line · factura Contentbox
amount: 560.00 (2 × 280) service_cost_line_id: → „Article — Native"
UPDATE service_cost_line · la Issue-ul facturii client
state: 'ready' → 'invoiced' (Locked)
o linie invoiced nu se mai resetează prin editare — doar reopen explicit / storno.
client_invoice
2.460
Σ linii ready+invoiced
supplier_cost
1.960
1.400 + 560, doi furnizori
fee_profit
500
360 + 140 — marjă curată
margin
20,3%
doar ratio de afișare
DB

service_billing_period — ce se scrie și ce se actualizează

La F1 rândul lunar NU primește inputuri și NU se editează direct — snapshot-urile se AGREGĂ din linii (service_cost_line), iar starea perioadei e DERIVATĂ din stările liniilor.

service_billing_period · ciclul de scriere al rândului lunar (derivat din linii)
INSERT service_billing_period · la generare, gol
state: 'cost_entry' input_supplier_cost: NULL — F1 nu folosește inputuri de perioadă input_supplier_hours: NULL — datele stau pe linii input_amount_spend: NULL — datele stau pe linii snapshot_client_amount: NULL — încă nimic de agregat snapshot_supplier_cost: NULL snapshot_fee_profit: NULL snapshot_labour_cost: NULL snapshot_labour_hours: NULL ready_at: NULL invoiced_at: NULL
rândul se naște gol — nici inputuri, nici snapshot-uri; totul se construiește ulterior din linii.
UPDATE service_billing_period · la fiecare tranziție de linie, recalculat din linii
snapshot_client_amount: 2.460 = Σ client_amount al liniilor ready+invoiced snapshot_supplier_cost: 1.960 = Σ supplier_cost_amount (ready+invoiced) snapshot_fee_profit: 500 = Σ fee_amount (ready+invoiced) snapshot_labour_cost: 0 — F1 n-are muncă internă snapshot_labour_hours: 0 state: derivat: „awaiting_fee" cât există linii în urmă
liniile în cost_entry / awaiting_fee (fee NULL) sunt EXCLUSE din agregate — doar cele invoiceable (ready + invoiced) intră.
UPDATE service_billing_period · când toate liniile ajung ready
ready_at: setat când starea derivată = ready
perioada devine ready abia când nicio linie nu mai e în urmă (cost_entry / awaiting_fee).
UPDATE service_billing_period · la emitere
state: → 'invoiced' invoiced_at: setat
liniile de cost trec și ele în 'invoiced' (Locked); corecțiile ulterioare doar prin reopen / storno.
DB

service_cost_line — introdusă de oameni, una per livrabil

La F1 liniile NU sunt materializate de engine — le introduce specialistul (cost) și le completează Finance (fee). Sunt N linii/lună, fiecare cu supplierul ei, fiecare cu propriul lifecycle. Aici e singura intrare umană în service_cost_line.

service_cost_line · ciclul unei linii de livrabil
INSERT service_cost_line · specialistul adaugă o linie de livrabil
billing_period_id: → perioada supplier_id: → supplierul liniei (obligatoriu, per linie!) line_type: 'deliverable' description: text liber (ex. 'Article — Outreach') quantity: ex. 4 unit_cost: ex. 350 unit_fee: NULL — nu-i accesibil specialistului supplier_cost_amount: 1.400 (= qty × unit_cost) fee_amount: NULL client_amount: NULL state: 'cost_entry' → 'awaiting_fee' la „Submit cost" position: ex. 1
rând nou scris de OM — specialistul deține cost-ul, supplierul e per linie, fee-ul e teritoriul finance.
UPDATE service_cost_line · Finance pune fee-ul („Enter fee")
unit_fee: ex. 90 — scris de finance fee_amount: 360 (= qty × unit_fee) client_amount: 1.760 (= supplier_cost_amount + fee_amount) state: 'awaiting_fee' → 'ready'
fee-ul e completat la nivel de câmp de finance; linia devine facturabilă abia acum.
UPDATE service_cost_line · Send back / reeditare cost pe linie ready
state: → 'cost_entry' — fee-ul se reconfirmă (linia re-parcurge awaiting_fee la re-submit)
editarea costului unei linii ready o resetează automat — fee-ul trebuie reconfirmat.
UPDATE service_cost_line · la emiterea facturii
state: 'ready' → 'invoiced' (Locked)
o linie invoiced nu se mai resetează prin editare — doar reopen explicit / storno.
N linii/lună, furnizori diferiți → AP către mai mulți furnizori dintr-un singur serviciu. DOAR liniile ready + invoiced intră în agregatele perioadei (cele cu unit_fee NULL sunt excluse), iar starea perioadei se derivă din stările liniilor.

Simulare · construiește liniile tu

interactiv · doar vizual

Exact cum funcționează F1 în realitate: specialistul adaugă linii de livrabil (cost, fără fee) și le trimite spre finance; Finance/Ops pune fee-ul → linia devine ready și intră în factură. Fiecare linie își are propriul lifecycle, starea perioadei se derivă din linii, iar totalurile numără doar liniile ready + invoiced. Nimic nu se salvează — e o schiță.

Specialist — adaugă & submit cost Finance/Ops — fee, send back, issue
Add deliverable line rol: Specialist
fee/unit nu apare aici — e teritoriul finance
Deliverables starea perioadei: cost_entry
deliverablesupplierqtycost/u fee/uclientstateacțiune
Invoiceable (ready + invoiced) 0
client_invoice
0
Σ linii ready + invoiced
supplier_cost
0
Σ qty × cost/u
fee_profit
0
Σ qty × fee/u — marjă curată
margin
doar ratio de afișare
Ce ilustrează: ownership pe roluri (specialistul nu vede fee-ul, finance nu editează costul), starea per linie ca mașină de stări reală și snapshot-urile care agregă exclusiv liniile facturabile — o linie rămasă în cost_entry sau awaiting_fee nu intră în totaluri până nu ajunge ready.

De ce F1 justifică singur designul

Trei decizii de schemă există în primul rând pentru fluxul ăsta:

Supplier per linie

  • service.supplier_id NULL la F1 — impus de CHECK
  • fiecare service_cost_line.supplier_id obligatoriu
  • un serviciu → oricâți furnizori, AP atribuit corect

Stare per linie

  • singurul tip care folosește awaiting_fee
  • editarea costului resetează linia la cost_entry
  • starea perioadei = rezumat derivat, recalculat de engine

Fee nullable

  • unit_fee NULL = „finance n-a decis încă" — NULL de lifecycle
  • devine obligatoriu la ready — regulă de engine
  • fee-ul intră cu 0 în agregate cât e NULL