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.760readySend back
Article — NativeContentbox228070700readySend back
Static designContentbox6120awaiting_feeEnter fee
VideoWebify1cost_entryAwaiting specialist
Preview subtotal (linii ready)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 (preview din liniile ready) snapshot_fee_profit: 500 snapshot_client_amount: 2.460 state: derivat: „cost_entry" cât există o linie fără cost
Clientul nu vede niciodată split-ul 350 + 90 — doar 440/bucată. Snapshot-ul de preview agregă exclusiv liniile ready, dar factura lunii rămâne all-or-nothing pe perioadă: „Static design" și „Video" trebuie completate, trimise înapoi, șterse ca draft sau mutate explicit în luna următoare. Rollover-ul nu este automat. Nicio rotunjire intermediară: engine-ul păstrează precizia completă și rotunjește o singură dată când persistă snapshot-urile și sumele liniilor. AR copiază valorile persistate; numai TVA-ul se rotunjește la Issue.
4

1 septembrie + la sosirea facturilor furnizorilor

O factură client, mai multe facturi furnizor

Finance modulul Billing — urmează

AR (Accounts Receivable): după rezolvarea sau mutarea liniilor incomplete, toate liniile rămase ajung ready; abia atunci perioada intră integral în factura clientului cu suma combinată (2.460 + TVA), iar toate liniile devin invoiced împreună. 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 toate service_cost_line ale perioadei · la Issue-ul facturii client
state: 'ready' → 'invoiced' (Locked)
issue este refuzat dacă o singură linie nu este ready; apoi toate se blochează împreună. Corecții doar prin reopen explicit / storno.
client_invoice
2.460
toate liniile, emise împreună
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: „cost_entry" dacă orice linie nu are cost; „awaiting_fee" doar când toate au cost, dar cel puțin una nu are fee
liniile în cost_entry / awaiting_fee sunt EXCLUSE din agregate — doar cele invoiceable (ready + invoiced) intră în preview. Perioada devine facturabilă doar all-or-nothing, când toate liniile sunt ready.
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 subtotalul de preview. Fiecare linie își are propriul lifecycle, starea perioadei se derivă din linii, iar Issue devine disponibil numai când toate liniile sunt ready; atunci toate devin invoiced împreună. 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
Preview subtotal (ready; final după Issue) 0
client preview / invoice
0
preview ready; final doar all-or-nothing
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. Facturarea rămâne all-or-nothing pe perioadă: cât timp există o linie incompletă, finance/ops trebuie să o completeze, să o trimită înapoi, să o șteargă sau să o mute explicit în luna următoare.

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
  • linia este exclusă din agregate cât fee-ul este NULL