Un retailer avea un ROAS de 4,2 pe Google Shopping. POAS, pe aceeași campanie, în aceeași săptămână: 0,7. Adică 38% din cifra de afaceri se ducea pe undeva pe drum, în niște locuri pe care ROAS-ul pur și simplu nu le vede.

Era brandul nostru. Nu atribuirea. Nu există niciun bug în Shopping Ads. Nici sezonul. Era formula pe care o foloseam noi, aceeași pe care o folosește jumătate de piață când pune cuvântul „profit" într-un dashboard fără să verifice ce înseamnă fiecare componentă. După câteva săptămâni de uitat la cifre, ne-am prins că greșim în patru locuri în același timp. Mai rău: toate cele patru greșeli bat în aceeași direcție. Și răspunsul era deja în stack-ul nostru (Funnel.io, GA4, Magento, Innoship, Klaviyo), doar că legam datele prost. Aici e cum am reconstruit-o, pornind de la presupunerea că de ce POAS contează mai mult decât ROAS e o discuție pe care ai purtat-o deja.

Prima minciună: shipping-ul nu este distribuit corect pe canale. Costul total de transport pe zi îl împărțeam pe canale, folosind ponderile din funnel_daily.ga4_transactions, un tabel filtrat doar pe paid. Rezultatul: tot shipping-ul aterizează pe canalele paid, inclusiv partea care aparținea comenzilor direct, organic sau email. În portofoliul nostru, paid-ul ieșea supraestimat cu aproximativ 17% pentru costul de shipping. E suficient cât să transforme o campanie profitabilă într-una marginală pe dashboard.

A doua: TVA-ul nu iese din venituri când calculezi profitul. ROAS se calculează pe cifra de afaceri cu TVA inclus (așa raportează Google, Meta, Funnel). Profitul brut, raportat la venitul net, fără TVA. Dacă pui ROAS și profit în același dashboard fără să aliniezi bazele, compari două lucruri care arată la fel, dar măsoară altceva. Diferența este exact cota de TVA: 19% sau 21%, în funcție de perioadă.

A treia: costul curent al produsului folosit în comparații istorice. Dacă un SKU costă 40 RON în ianuarie și 55 RON azi, un YoY bazat pe prețul de achiziție de azi îți spune că marja a scăzut. De fapt, comenzile din ianuarie aveau o marjă reală bazată pe costul de atunci. Fără un snapshot istoric al costurilor (SCD-2 pe cost de produs), orice comparație peste timp minte sistematic.

A patra: funnel_daily.ga4_transactions e paid-only. Conectorul Funnel filtrează pe paid_organic='Paid' când scrie tabelul, deci direct, organic, email, referral nu apar cu ponderi proprii. Când folosești ga4_transactions ca denominator pentru distribuție, paid-ul devine denominator unic și primește 100% din ponderi. Shipping-ul (sau orice alt cost distribuit astfel) ajunge artificial tot pe paid. Atribuirea GA4 pe comenzile paid e bună (peste 88% din comenzile Magento au atribuire GA4), dar asta nu salvează distribuția, pentru că tabelul folosit e restricționat prin construcție la paid.

La final, ai formulele pe care le folosim pentru POAS canonic și variantele lui, pragurile concrete de decizie săptămânală pe care le rulăm pe cele 4 branduri (Otter, Tezyo, Gryxx, Aldo), plus un checklist de 15 întrebări cu care îți poți audita propriul stack în 2 ore. Nu e o introducere în POAS. Presupun că știi de ce contează. E playbook-ul operațional: ce citești din ce sistem, cum legi datele între ele, ce formulă scrii, ce decizie iei luni dimineața când te uiți la dashboard.

Un retailer avea un ROAS de 4,2 pe Google Shopping. POAS, pe aceeași campanie, în aceeași săptămână: 0,7. Adică 38% din cifra de afaceri se ducea pe undeva pe drum, în niște locuri pe care ROAS-ul pur și simplu nu le vede.

Era brandul nostru. Nu atribuirea. Nu există niciun bug în Shopping Ads. Nici sezonul. Era formula pe care o foloseam noi, aceeași pe care o folosește jumătate de piață când pune cuvântul „profit" într-un dashboard fără să verifice ce înseamnă fiecare componentă. După câteva săptămâni de uitat la cifre, ne-am prins că greșim în patru locuri în același timp. Mai rău: toate cele patru greșeli bat în aceeași direcție. Și răspunsul era deja în stack-ul nostru (Funnel.io, GA4, Magento, Innoship, Klaviyo), doar că legam datele prost. Aici e cum am reconstruit-o, pornind de la presupunerea că de ce POAS contează mai mult decât ROAS e o discuție pe care ai purtat-o deja.

Prima minciună: shipping-ul nu este distribuit corect pe canale. Costul total de transport pe zi îl împărțeam pe canale, folosind ponderile din funnel_daily.ga4_transactions, un tabel filtrat doar pe paid. Rezultatul: tot shipping-ul aterizează pe canalele paid, inclusiv partea care aparținea comenzilor direct, organic sau email. În portofoliul nostru, paid-ul ieșea supraestimat cu aproximativ 17% pentru costul de shipping. E suficient cât să transforme o campanie profitabilă într-una marginală pe dashboard.

A doua: TVA-ul nu iese din venituri când calculezi profitul. ROAS se calculează pe cifra de afaceri cu TVA inclus (așa raportează Google, Meta, Funnel). Profitul brut, raportat la venitul net, fără TVA. Dacă pui ROAS și profit în același dashboard fără să aliniezi bazele, compari două lucruri care arată la fel, dar măsoară altceva. Diferența este exact cota de TVA: 19% sau 21%, în funcție de perioadă.

A treia: costul curent al produsului folosit în comparații istorice. Dacă un SKU costă 40 RON în ianuarie și 55 RON azi, un YoY bazat pe prețul de achiziție de azi îți spune că marja a scăzut. De fapt, comenzile din ianuarie aveau o marjă reală bazată pe costul de atunci. Fără un snapshot istoric al costurilor (SCD-2 pe cost de produs), orice comparație peste timp minte sistematic.

A patra: funnel_daily.ga4_transactions e paid-only. Conectorul Funnel filtrează pe paid_organic='Paid' când scrie tabelul, deci direct, organic, email, referral nu apar cu ponderi proprii. Când folosești ga4_transactions ca denominator pentru distribuție, paid-ul devine denominator unic și primește 100% din ponderi. Shipping-ul (sau orice alt cost distribuit astfel) ajunge artificial tot pe paid. Atribuirea GA4 pe comenzile paid e bună (peste 88% din comenzile Magento au atribuire GA4), dar asta nu salvează distribuția, pentru că tabelul folosit e restricționat prin construcție la paid.

La final, ai formulele pe care le folosim pentru POAS canonic și variantele lui, pragurile concrete de decizie săptămânală pe care le rulăm pe cele 4 branduri (Otter, Tezyo, Gryxx, Aldo), plus un checklist de 15 întrebări cu care îți poți audita propriul stack în 2 ore. Nu e o introducere în POAS. Presupun că știi de ce contează. E playbook-ul operațional: ce citești din ce sistem, cum legi datele între ele, ce formulă scrii, ce decizie iei luni dimineața când te uiți la dashboard.

Două minute pe dashboard-ul propriu-zis. Date dummy, fără audio, fără captions: doar UI-ul în mișcare. Vei vedea POAS pe canal cu color coding, drilldown pe campanie, movers săptămânali și health indicator pentru sync. Apoi intrăm în pași.

Pasul 1: Alege-ți sursele de adevăr

POAS-ul real începe de la 5 surse de date care nu se suprapun: ad spend, venit, cost marfă, cost livrare, retenție. Dacă îți lipsește una, formula ta minte într-o direcție previzibilă. Întotdeauna în aceeași direcție, rareori în favoarea ta.

Înainte de orice formulă, stabilim de unde provine fiecare număr. Un channel, o sursă, fără suprapunere. Pe stack-ul nostru de 4 branduri, sursele sunt:

  • Funnel.io (via BigQuery): agregator și normalizator de ad spend, impresii și click-uri din toate platformele de ads pe care le rulăm: Google Ads, Meta, TikTok, 2Performant, Glami, RTB House, CJ Affiliate, Bing, Pinterest, Criteo. E singura platformă care se integrează cu opțiunile locale (Glami, 2Performant). Fără Funnel.io, ar trebui să scriem conectori custom pentru ele. Toate datele ajung zilnic în BigQuery, agregate per campanie și channel, iar din același warehouse facem join cu venitul din GA4, fără să mai exportăm CSV-uri între platforme.

  • Google Analytics 4: venituri, tranzacții, sesiuni per channel, campanie, SKU. E sursa care leagă un click paid de o comandă finalizată la nivel de eveniment, când atribuirea funcționează.

  • Magento (multi-store, per brand): cost de achiziție, preț de vânzare, stoc, plus taxele care nu apar nicăieri altundeva: cost de transport facturat clientului, taxa de ramburs, TVA per rând din comandă. Magento știe și istoricul per client (email, număr de comenzi, frecvență), deci repeat rate-ul se poate calcula și fără Klaviyo.

  • Innoship (cont unic, multi-brand): un SaaS de logistică, nu un curier. Se integrează cu curierii contractați (FAN Courier, Sameday, Cargus, în cazul nostru) și rutează fiecare colet în funcție de zonă, preț și SLA. Din Innoship luăm estimarea de cost per AWB plus carrierul ales și metadata pe comandă. Costul real nu vine de aici. Vin din anexele XLSX ale facturilor lunare de la curieri, cu un decalaj de 1–6 săptămâni. Construim acum pipeline-ul shipping-actuals care pollează un folder Drive, parsează anexele per curier (parser determinist plus fallback LLM) și suprascrie estimarea Innoship în tabelul shipping_cost_awb pe măsură ce apar costurile reale. Designul folosește o coloană source cu valori innoship_estimate sau invoice_real, plus un upsert guard care nu permite ca un real să fie downgrade-at înapoi la estimate. Consumatorii aval văd un singur număr per AWB, iar flag-ul source le spune pe ce date se bazează.

  • Klaviyo (chei separate per brand): comenzi, clienți unici, AOV (average order value, valoarea medie a unei comenzi), plus segmentare pe cohorte comportamentale (clienți noi, returning, risc de churn). Repeat-rate-ul se calculează și în Magento, dar Klaviyo îl oferă pe cohorte deja segmentate, cu atribuire pe flow-uri de email. Util când vrei să răspunzi la întrebarea „A crescut POAS-ul pentru că am scalat Meta sau pentru că email-ul a făcut cohorta să cumpere mai devreme?".

Dacă n-ai Funnel, poți intra direct în BigQuery prin transferul nativ din Google Ads. Supermetrics sau Dataslayer fac același lucru pentru Meta și TikTok și te lasă în același warehouse. Tool-ul nu contează, regula contează: o singură sursă de ad spend per channel; restul se aliniază pe zi, pe channel, pe campanie. Două surse de spend pe același channel înseamnă două totaluri care nu vor coincide niciodată și o săptămână pierdută pe an reconciliind.

Aceeași regulă, mai dură pentru atribuirea conversiilor. Fiecare platformă măsoară cu propriul model. Google Ads: data-driven, cu view-through; vede doar click-urile de la Google. Meta: 7-day click plus 1-day view, vede doar click-urile Meta și modelează agresiv pentru iOS. TikTok: fereastră similară cu Meta. Afiliații (2Performant, CJ): last-click în rețeaua lor. Toate sunt încurajate să-și supraestimeze propriul aport. Dacă suni la Google și Meta pentru aceeași perioadă, fiecare îți va raporta 100 de conversii. Asta nu înseamnă 200 de comenzi reale. Înseamnă overlap masiv: același cumpărător, văzut și pe Meta, și pe Google, creditat de două ori. Sumarea rapoartelor native produce overcount sistematic. Noi am ales GA4 ca observator unic: vede toate sursele cross-canal (paid, organic, direct, email), aplică un singur model de atribuire calibrat pe traficul propriu și deduplicează la nivel de transactionId. Pierdem precizie pe in-platform optimization (Meta vede mai bine view-through-urile sale), dar câștigăm consistența cross-platform. E singura bază cu care POAS-ul e comparabil între canale.

Klaviyo are o capcană pe care o verificăm la fiecare deploy: „unique customers" pe perioade agregate cross-brand nu pot fi sumați direct, pentru că același client apare în două branduri și e numărat de două ori. Pe widget-ul cross-brand afișăm 3 estimări: MAX (lower bound, presupunând că sunt aceiași clienți în toate brandurile), SUM (upper bound, presupunând că sunt disjuncți) și midpoint (estimarea funcțională pe care o folosim în luarea deciziilor). Pe widget-urile per brand nu apare. Acolo numerele sunt exacte.

❝

„O singură sursă pentru fiecare tip de adevăr. Dacă ad spend-ul vine din 2 locuri (Funnel și raportul Google Ads direct), o să diverge, o să pierzi timp reconciliind. Iar când diverge tare, o să crezi că dashboard-ul e spart, când, de fapt, au drift-uit bazele."

În practică, toate cele 5 surse se sincronizează de 2 ori pe zi. În sidebar avem un health indicator live, un punct colorat cu popover per sursă, care arată cât de proaspete sunt datele și când a avut loc ultima extracție. Verde, galben, roșu, în funcție de freshness. E singura garanție că un raport deschis luni dimineața nu e bazat pe datele de vinerea trecută.

Întrebarea pe care o primim mereu: de ce intern în loc de Triple Whale, Northbeam sau Polar Analytics? Trei motive. (1) Platforme locale. Niciun SaaS de attribution nu integrează Glami sau 2Performant. Pentru un DTC RO, asta înseamnă de la 5% până la 15% din ad spend complet invizibil. (2) Black-box vs white-box. Când un SaaS îți zice POAS 1.4, nu poți verifica formula. Nu vezi cum tratează shipping-ul, ce ratio de cost folosește la fallback și cum splitează TVA. La un audit intern sau într-o discuție cu CFO-ul, „așa zice tool-ul" nu e un răspuns. (3) Ownership pe date. Datele tale stau în warehouse-ul tău, nu pe servere terțe. Important pentru GDPR și pentru orice migrație viitoare. Costul construit intern: aproximativ 3 luni de inginer senior pentru bază, plus iterație continuă. Costul SaaS: 15 până la 30k EUR pe an plus integrare custom pentru piețele locale. Pentru un portofoliu cu o cifră între 8 și 9, calculul e clar.

Pasul 2: TVA time-aware

ROAS-ul se raportează cu TVA, așa cum face toată industria. Profitul și marjele se calculează fără TVA, deoarece TVA-ul nu îți aparține. Iar dacă treci printr-o schimbare de cotă, divizorul trebuie să fie time-aware. Altfel, YoY-ul tău nu mai măsoară business-ul, măsoară cota.

Prima decizie pe care o stabilim înainte de orice formulă: ce bază folosim pentru ce metrică. ROAS-ul rămâne calculat pe venitul cu TVA inclus, așa cum raportează Google Ads, Meta și TikTok. Așa livrează GA4 venitul per tranzacție, așa se uită toate benchmark-urile din piață. Dacă schimbi convenția asta doar la tine, nu mai poți compara ROAS-ul tău cu nimic. Nici cu săptămâna trecută în propriul dashboard Google Ads, nici cu cifrele pe care le discuți cu agenția.

Profitul se calculează pe baza veniturilor nete, fără TVA. Asta nu e o preferință, e contabilitate. TVA-ul colectat nu e al tău; e al statului, îl plătești lunar la ANAF. Orice „profit brut", „marjă brută" sau „CPS%" (cost per sale ca procent din venit), care include TVA în numitor, umflă artificial baza și îți dă marje optimiste de 16% (la cota de 19%) sau de 17% (la cota de 21%). Pe zero profit real, asta e diferența dintre „mergem bine" și „sângerăm".

Pe 1 august 2025, TVA-ul standard în România a crescut de la 19% la 21%. Pentru noi, pe cele 4 branduri, asta a însemnat că orice interogare care scoate TVA-ul din venit nu mai poate folosi un singur divizor. Pentru date dinainte de august 2025, divizorul e 1,19. Pentru date după, e 1.21. Dacă aplici 1,21 pe tot istoricul, tot ce e înainte de august 2025 apare artificial mai mic cu 1,65%. Iar YoY-ul pe luni de comparație (martie 2026 vs. martie 2025, de exemplu) compară două convenții diferite. Trendul apărut este al divizorului, nu al business-ului.

Soluția este un CASE WHEN în SQL pe date_day-ul rândului, nu pe data curentă sau pe data de rulare a raportului. Divizorul se alege pe baza zilei raportate, deci un YoY pe aprilie 2026 vs. aprilie 2025 folosește 1.21 pentru partea din 2026 și 1.19 pentru partea din 2025. Exact ce vrea contabilitatea și ce așteaptă orice persoană care se uită la dashboard. Aceeași logică se aplică oriunde calculezi profit, marjă brută, CPS% sau POAS, oriunde TVA-ul trebuie să dispară din numărător sau din numitor.

❝

„ROAS-ul cu TVA. Profitul fără TVA. Divizorul time-aware pentru orice comparație istorică care implică o schimbare a cotei. Altfel, compari mere din 2024 cu pere din 2025."

În practică, avem un singur loc în warehouse unde se aplică regula, expus ca revenue_ex_vat din revenue_incl_vat:

revenue_ex_vat = revenue_incl_vat /
  CASE
    WHEN date_day < '2025-08-01' THEN 1.19
    ELSE 1.21
  END

ROAS-ul rămâne calculat pe revenue_incl_vat, deci divizorul nu intervine acolo. Dar orice widget de „profit real", orice rând de marjă pe brand, orice comparație YoY cu context monetar trec prin această expresie. Sunt 6 rânduri de SQL care te scapă de o săptămână de explicații despre contabilitate la fiecare închidere de lună.

Pasul 3: Distribuie shipping cost pe ordine reale

Dacă împarți costul de shipping pe canale după funnel_daily.ga4_transactions, paid-ul absoarbe 100% din costul de shipping. Inclusiv partea care aparținea comenzilor directe, organice, email. Pe portofoliul nostru, măsurat pe 30 de zile, asta însemna o supraestimare de aproximativ 17% pentru canalele de paid.

Shipping-ul (cost de transport) ne vine ca factură lunară de la fiecare curier cu care lucrăm: FAN Courier, Sameday, Cargus, toți accesați prin Innoship. Azi folosim estimarea Innoship per AWB. Pe măsură ce construim pipeline-ul de invoice parsing, costurile reale din anexele facturilor vor suprascrie estimarea pentru același AWB. Indiferent de sursă, rezultatul e același: un total pe zi și pe brand, care trebuie spart pe canale de marketing (paid, organic, direct, email) ca să putem calcula POAS pe canal. Prima abordare, cea care pare evidentă, e să ponderi distribuția pe tranzacțiile GA4 paid: dacă Google Ads aduce 40% din tranzacțiile GA4 într-o zi, atunci 40% din costul de livrare al zilei se atribuie Google Ads. Simplu, rapid, greșit.

De ce e greșit: funnel_daily.ga4_transactions e paid-only din construcție. Conectorul Funnel filtrează la ingest pe paid_organic = 'Paid'. Direct, organic, email, referral nu apar ca rânduri în tabel. Când îl folosești ca denominator pentru distribuție, paid-ul rămâne singurul cu ponderi. Absoarbe 100% din shipping-ul zilei, indiferent de câte comenzi au venit pe canalele neincluse.

Măsurat pe 30 de zile pentru un brand, Magento a înregistrat 26 008 comenzi finalizate (excludem canceled, retur_complet, nelivrată, statusurile cu revenue zero). Dintre ele, 21 420 au atribuire „paid via GA4", iar 4 588 cad în bucket-ul „Direct", fără atribuire. Tabelul funnel_daily.ga4_transactions conține doar comenzile atribuite paid (21 163 sesiuni, aproape identice cu 21 420 comenzi, deci atribuirea e curată). Problema e că distribuția shipping-ului prin funnel_daily nu are rând pentru cele 4 588 comenzi Direct, așa că tot shipping-ul lor pleacă pe paid. 4 588 / 26 008 = ~17% supraestimare pentru canalele paid. Suficient ca să transforme o campanie marginal profitabilă într-una pierdătoare în raport. Sau invers, după corectare.

Noua distribuție pleacă din Magento, nu din GA4. Magento știe fiecare comandă plasată; e sistemul de checkout; n-are cum să o piardă. Fiecărei comenzi Magento îi atașăm canalul de marketing prin join pe ID-ul tranzacției când avem semnal GA4. Job-ul care face asta e sync-magento-fees. Ponderile pentru spartul shipping-ului vin acum din numărul real de comenzi Magento per canal, nu din tranzacțiile GA4. Dacă într-o zi avem 200 de comenzi Magento, iar 40 dintre ele au GA4 source „Google Ads", ponderea Google Ads pentru shipping-ul zilei e 40 / 200, nu raportul tranzacțiilor GA4.

❝

„Shipping-ul se distribuie pe baza comenzilor reale, nu pe baza tranzacțiilor din GA4. Restul merge într-un bucket izolat, nu în canalele de paid."

Restul, cele 4.588 de comenzi Magento care nu au atribuire în GA4, nu sunt aruncate. Le păstrăm într-un bucket separat în funnel_shipping_daily, numit „Direct", cu shipping proporțional cu numărul lor. Bucket-ul ăsta nu se afișează pe dashboard. Scopul dashboard-ului e performanța canalelor paid, iar un bucket care înseamnă „nu știm de unde au venit" n-are ce să caute acolo; ar invita interpretări false. Dar rămâne în DB și asta nu e un accident. Poluția canalelor paid cu shipping din comenzi neatribuite e exact greșeala pe care o făcea formula veche. Izolarea Direct-ului într-un rând separat menține canalele paid corecte în sine. Lasă și o ușă deschisă: când îmbunătățim atribuirea (server-side tracking, enhanced conversions, consent mode updates), bucketul Direct scade, iar canalele primesc retroactiv shipping-ul care le-a aparținut mereu.

Concret, pentru fiecare zi multiplicată cu canalul, distributorul verifică în ordine. Întâi: există comenzi Magento cu atribuire GA4 pe canalul ăsta? Da: ponderă shipping-ul pe numărul lor. Apoi: nu există comenzi atribuite pe canal, dar sync-magento-fees a rulat cu succes în ziua respectivă? Atunci, shipping-ul rămâne în bucket-ul „Direct". Ultimul pas: sync-ul n-a rulat sau a eșuat? Fallback pe tranzacțiile GA4, cu un flag de warning scris în audit log care se aprinde galben în health indicatorul din sidebar. Prioritatea este corectitudinea comenzilor Magento, nu completitudinea în GA4. Fallback-ul există strict pentru a preveni dispariția shipping-ului din dashboard într-o zi în care sync-ul e stricat. Nu e un mod de operare normal.

Diferența între cele două formule, pe același brand, pe aceeași lună: paid-ul arăta cifrele corecte și mai multe campanii care păreau marginale s-au dovedit solide. Am schimbat doar tabelul folosit ca sursă pentru ponderi.

Câteva detalii de calibrare specifice pieței RO pe care le folosim ca prag de sănătate la audit. În portofoliul nostru, 65% din comenzi sunt COD (cash on delivery, ramburs), față de sub 5% în piețele occidentale. Înseamnă o taxă fixă de 5 RON per comandă, plătită băncii, și un risc mai mare de anulare la livrare. Costul de transport plătit la curier e fix, indiferent de zonă sau metodă de plată: zero pentru clientul cu coș peste 299 RON (suportat de noi), suportat de client pentru coșuri sub 299 RON (intră ca venit de shipping în Magento). Niciuna dintre constantele astea nu apare în vreun template internațional pentru calculatorul POAS. Dacă pleci de la formule importate din articole americane, vei subestima costul real per comandă cu 5 până la 15%.

Pasul 4: Cost istoric SCD-2

Costul curent al produselor este folosit pentru comparații istorice în majoritatea dashboard-urilor de profitabilitate. Rezultatul: marje false după orice modificare a prețului de achiziție. Soluția e să păstrezi istoricul costurilor într-un tabel separat, time-aware, și să pui JOIN-ul de widget pe costul valabil la data raportată.

Un exemplu concret. În martie 2024, costul nostru de achiziție pentru un SKU era de 50 de lei. Azi, aprilie 2026, același SKU costă 65 de lei (inflație la materii prime, plus o renegociere cu furnizorul care a mers în direcția greșită). Dacă widgetul YoY folosește current_cost din tabelul de produse Magento, marja brută din martie 2024 apare cu 15 lei mai mică pe bucată decât în realitate. Te uiți la raport și ai impresia că marjele se îmbunătățesc treptat. De fapt, comparația e coruptă, iar marja din 2024 era deja mai sănătoasă decât pare azi.

Soluția pe care o rulăm este un tabel SCD-2 (Slowly Changing Dimension Type 2, un pattern standard din data warehousing) numit product_costs_history. Fiecare schimbare de cost produce un rând nou cu valid_from setat pe ziua modificării și valid_to setat pe ziua anterioară următoarei modificări (sau NULL pentru costul curent, cel activ acum). Orice JOIN din widget-ul YoY, din breakdown-ul pe channel sau din raportul de movers pornește de la date_day-ul rândului raportat și extrage costul valabil la acel moment. Nu costul de azi.

Două fallback-uri în cascadă, pentru că nu orice SKU are un cost propriu. (1) Dacă SKU-ul copil (varianta concretă: mărime, culoare) n-are cost setat, încercăm costul parentului (parent_sku, produsul configurable din Magento). Majoritatea cataloagelor Magento au costul setat doar pe parent, nu pe fiecare variantă, iar asta acoperă marea majoritate a comenzilor. (2) Dacă nici copilul, nici parentul n-au cost, cădem pe un ratio sales-weighted aplicat pe preț. Ratioul nostru e 0,55, calibrat pe baza comenzilor reale ale brandurilor: 55% din preț merge în cost, iar 45% rămâne marjă brută. Vechea valoare folosită în dashboard era 0.45 flat, neverificată niciodată împotriva comenzilor reale, ceea ce împingea toate marjele în sus artificial cu 10 puncte procentuale pe SKU-urile fără cost.

❝

„Costul de azi nu e acela din martie 2024. Orice comparație istorică care nu respectă asta îți oferă marje false."

Structura, pe scurt. Tabelul product_costs_history are patru coloane relevante: sku, cost, valid_from, valid_to. La fiecare insert nou (schimbare de cost), update-ul cel mai recent pentru același SKU (cel cu valid_to IS NULL) primește valid_to = new.valid_from - 1 day, apoi se inserează un rând nou cu valid_to IS NULL. La query, clauza de JOIN e: WHERE valid_from <= date_day AND (valid_to >= date_day OR valid_to IS NULL). Un SKU poate avea 5 rânduri în product_costs_history, iar widget-ul trage exact unul pentru fiecare zi raportată. E un pattern ortodox în data warehousing, doar că rar e implementat corect în shop-uri DTC mid-market, unde current_cost din tabelul de produse rămâne sursa unică și toată lumea trăiește cu marje false până când cineva face cifrele pe hârtie.

O întrebare pe care o primești inevitabil când schimbi o formulă (TVA, ratio cost, distribuție shipping): ce facem cu cei 2 ani de istoric care arată cifre vechi? Refacem trecutul sau lăsăm fix-ul de pe data X încoace? Regula noastră: fixul codului = data 0, backfill-ul rulează in-place pentru toate datele istorice afectate, iar comparațiile YoY care traversează data fix-ului primesc un flag „metric drift" în UI. Adică: martie 2024 ridică 1,19 ca divizor TVA, chiar dacă raportul a fost generat în 2026, dar widget-ul de YoY arată un tooltip mic: „divizor recalibrat 22 aprilie 2026". Onestitatea istoriei contează mai mult decât consistența vizuală. Dacă cititorul vede o cifră schimbată față de săptămâna trecută, vrem să știe exact de ce.

Pasul 5: POAS canonic + variantele

POAS-ul vine în trei arome. Formula headline e (Profit Brut − Cost transport) / Ad Spend. Dar widget-urile de seasonality și analiza per SKU folosesc formule diferite, cu break-even-uri diferite. Etichetele diferă pentru că semantica diferă, iar o confuzie de etichete pe un dashboard cu 4 branduri e o decizie greșită garantată.

Înainte să scriem formulele, o precizare de scope. În POAS-ul nostru intră strict costurile direct atribuibile unei comenzi: cost de achiziție (COGS), cost de transport, cost de ads. Nu intră costurile indirecte (salarii, chirie, software, marketing non-paid) și nici costurile directe care nu se pot atribui cronologic pe zi (ramburs bancar lunar, taxă Stripe, comisioane platforme). Alea sunt pentru P&L-ul lunar sau semestrial, nu pentru decizii on the go. Dashboard-ul e un instrument operațional; te ajută să decizi luni dimineața ce tai și ce scalezi, iar decizia aia are nevoie doar de costurile care se mișcă odată cu fiecare comandă sau cu fiecare leu de ad spend. Marjele „contabile complete" stau în raportul lunar al contabilității, unde le poți lega corect de perioada de referință.

Cele 3 variante pe care le rulăm în producție, fiecare cu contextul ei:

Variantă

Formulă

Break-even

Când o folosești

POAS canonic (headline)

(Profit Brut − Cost transport) / Ad Spend

1.0

Widget-uri de channel, campanie, decizie săptămânală

Net POAS

Profit Net / Ad Spend

0.0

Widget-uri seasonality, LTV (compari cu 0)

SKU POAS ex-shipping

Profit Brut / Ad Spend alocat

1.0

Widget-uri per SKU (shipping nu se atribuie per linie)

Trei formule, trei contexte. Canonicul e ce raportăm la board și ce citim luni dimineața ca să decidem unde băgăm sau tăiem buget: profit brut minus cost transport, împărțit la ad spend, break-even 1.0. Net POAS-ul e pentru contexte unde semnul contează mai mult decât distanța de la 1, în special seasonality și LTV, unde profitul net pe o săptămână poate fi pozitiv sau negativ, iar ce vrei să vezi e pragul de rentabilitate, nu amplitudinea. Numeric diferă de canonic, deci l-am etichetat deliberat diferit la nivel de funcție (computePoasNet vs. computePoasStandard) ca să nu se amestece accidental într-un widget nou.

La nivel de SKU, costul de transport nu se atribuie pe linie de produs. Shipping-ul e la nivelul comenzii, iar o comandă cu 3 SKU-uri diferite nu se împarte în 3 fără o regulă arbitrară. Soluția e să excludem shipping-ul din formula SKU și să afișăm contribution_poas_ex_shipping, cu o etichetă distinctă în UI („POAS ex-shipping"), ca să semnaleze limpede că nu e aceeași formulă ca headline-ul. Break-even-ul rămâne 1.0, dar numitorul e ad spend-ul alocat pe SKU prin ponderea contribuției în revenue, nu ad spend-ul total al campaniei.

Disciplina de nume e critică. Dacă 3 formule cu rezultate diferite se numesc toate „POAS" în UI, cineva o să compare apples-to-oranges și o să ia o decizie greșită (un headline de 1.1 lângă un seasonality de 0.3 nu e aceeași metrică, cu prag diferit). Etichetele noastre sunt deliberate: POAS pentru canonic, Net POAS pentru seasonality și LTV, POAS ex-shipping pentru SKU. Orice widget nou alege una dintre cele trei. Nu inventează a patra.

❝

„Nume diferite pentru semantică diferită. Un singur ‚POAS' în UI pentru 3 formule e o capcană care te face să compari greșit."

Teoretic, am putea forța toate widget-urile într-o singură formulă. Practic ar cere rescriere de logică live pe componente stabile, recalibrare a pragurilor pentru seasonality (unde break-even-ul 0 e natural și calibrat peste 18 luni), plus atribuire de shipping la nivel de linie de SKU (o problemă de modelare, nu de cod: shipping-ul unei comenzi cu 3 SKU-uri nu se împarte la 3 fără o regulă arbitrară, pe greutate, valoare sau volum, iar orice alegere produce artefacte care se reflectă în decizie). Am preferat să lăsăm cele 3 formule separate, cu etichete clare, în locul unei unificări false.

Pasul 6: Reguli de decizie săptămânale

POAS-ul e o cifră. Decizia este un set de reguli aplicate săptămânal. Folosim praguri diferite per canal, un Health Score 40/30/30, plus 3 tipuri de acțiuni (tai, monitorizezi, scalezi).

Odată ce formulele sunt curate, întrebarea nu mai e „care e POAS-ul real?" ci „ce fac cu el luni dimineața?". Protocolul nostru pornește de la o observație simplă: nu există un prag universal pentru POAS. Fiecare canal are o fiziologie proprie (cât pierde în atribuție, ce AOV aduce, ce intenție captează), iar asta trebuie să se reflecte în prag.

Pragurile noastre de bază, calculate pe 14 zile rolling pentru fiecare campanie activă:

  • Google Shopping: tai sub 0,8, monitorizezi între 0,8 și 1,2, scalezi peste 1,5

  • Google Search (brand): tai sub 1.5 (brandul trebuie să aducă profit, nu doar să recupereze click-uri care veneau oricum), scalezi peste 2.5 cu volum

  • Google Search (non-brand): tai sub 0.9, scalezi peste 1.4

  • Meta: tai sub 0.7 (atribuirea pierde mai mult, deci break-even real e sub 1.0), scalezi peste 1.3

  • TikTok: tai sub 0.6 (atribuirea pierde și mai mult, dar e upper funnel), scalezi peste 1.2 cu cohortă de remarketing

Pragurile noastre sunt ancora, nu lege. Le calibrezi pe categoria ta, pe ciclul de cumpărare și pe procentul de atribuție directă din GA4 pe care îl ai în realitate.

Cum îți dai seama de pragul tău, nu al nostru? Aritmetica e directă. POAS la 1.0 înseamnă „ads acoperă exact costurile directe atribuite per comandă", dar nu acoperă overhead-ul (salarii, chirie, software, marketing, labor non-paid). Pentru pragul real de break-even net, formula este POAS_break_even = 1 + (overhead_pct × ROAS_ex_VAT). Pentru un brand cu 8% overhead pe venit ex-VAT și ROAS_ex_VAT mediu de 4, real break-even POAS = 1 + 0.08 × 4 = 1,32. O campanie cu POAS 1.1 arată „break-even" pe dashboard, dar pe P&L pierde 22% pentru fiecare leu cheltuit. Asta e diferența dintre un dashboard operațional și un raport contabil. De aceea, pragurile noastre de tăiere/scalare nu sunt setate naiv la 1.0. Calibrarea finală o facem împotriva LTV-ului per cohortă, nu împotriva POAS-ului single-purchase (vezi paragraful despre LTV mai jos).

De ce nu un prag unic? Trei motive. (1) Attribution leak diferit per canal. Meta și TikTok pierd mai mult în cookie blocking și iOS ITP decât Google, pentru că primesc mai mult trafic mobile-first din browsere cu setări de confidențialitate agresive. Pragul trebuie să compenseze: un Meta la 1.0, aparent, e la 0.7, în realitate. (2) Shipping mix diferit. Google Shopping aduce comenzi cu AOV mai mic, ceea ce împinge raportul shipping/venit mai sus și trage POAS-ul natural mai jos. Un Shopping la 1.0 e adesea mai sănătos decât un Search la 1.0. (3) Intenție diferită. Brand search e captare de trafic deja cald. La break-even 1.0 plătești pentru click-uri care veneau oricum organic. Pragul trebuie să fie mult mai ridicat decât în prospecting-ul rece.

Pe lângă POAS, fiecare campanie primește un Health Score (1 până la 100), ponderat astfel: 40% POAS, 30% CPS% (cost per sale, ca procent din venitul net), 30% volum de cheltuieli. POAS-ul singur nu e suficient. O campanie cu POAS 2.0 și 500 de lei spend pe săptămână e irelevantă la scala unui portofoliu de 4 branduri. Health Score-ul te forțează să ignori campaniile cu performanță bună pe un volum neglijabil (sau să le scalezi dacă vrei să conteze) și să pui presiune pe campaniile mari cu profitabilitate mediocră, acolo unde 1 punct procentual de CPS% înseamnă mii de lei pe lună.

Luni dimineața, treci prin lista de campanii active cu 3 filtre, în ordine:

  1. Tai: POAS sub pragul canalului pe 14 zile consecutive și volum semnificativ (peste 1000 de lei/săpt.). Nu tai pe volatilitate de 3 zile, asta e zgomot, nu semnal. Tăierea înseamnă pauză, nu ștergere: păstrezi istoricul pentru calibrare viitoare.

  2. Monitorizezi: POAS în banda intermediară (între „tai" și „scalezi"), sau sub prag, dar pe volum mic. Nu tai, dar verifici week-over-week dacă tendința e descendentă. Două săptămâni consecutive de scădere intră în bucket-ul „tai" la revizuirea următoare.

  3. Scalezi: POAS peste pragul de scalare ȘI volum ascendent pe 2 săptămâni ȘI CPS% stabil (nu urcă). Scalezi cu 15-20% din buget, nu cu 100%. O săritură mare de buget modifică mix-ul de targeting în platformă și POAS-ul observat devine instabil. Câștigul aparent pe 3 zile post-scalare este aproape întotdeauna un artefact de atribuire.

❝

„Praguri diferite per canal, Health Score care penalizează volumul neglijabil, plus un check pe Klaviyo înainte de orice scalare serioasă."

Klaviyo e semnalul de alarmă pentru pull-forward, fenomenul în care clienți existenți cumpără mai devreme decât ar cumpăra natural (grăbiți de o promoție sau de un mesaj de urgență), ceea ce umflă POAS-ul pe fereastra curentă, dar golește cohorta pentru săptămânile următoare. Scenariu tipic: POAS paid urcă frumos, vrei să scalezi. Klaviyo arată însă un churn crescut și o scădere a numărului de clienți noi în aceeași perioadă. Nu e o creștere reală, e o bază de clienți existenți care consumă inventarul de cerere mai repede. Peste 4-6 săptămâni, POAS-ul cade sub 1,0, pentru că ai vândut deja la toată lumea care urma să cumpere. Regulă: înainte de orice scalare semnificativă, verifici cohortele Klaviyo pentru clienți noi vs. clienți reveniți. Dacă ratio-ul clienților noi scade în timp ce POAS paid crește, nu scalezi. Monitorizezi două săptămâni.

Cealaltă față a aceleiași monede este LTV. POAS pe single-purchase subestimează valoarea reală a unui client achiziționat, deoarece nu include cumpărările ulterioare. Adjustarea aproximativă pe care o folosim pentru campanii de prospecting: POAS_LTV = POAS_single × (1 + repeat_rate × repeat_AOV / first_AOV). Pe portofoliul nostru, repeat_rate la 90 de zile este între 25 și 35%, în funcție de brand, cu repeat_AOV la aproximativ 90% din first_AOV (cumpărători returning aleg conservator). Pentru o campanie cu repeat_rate 0.30 și acel raport AOV, multiplicatorul e aproximativ 1.27. Un POAS single-purchase de 0.8 devine 1.02 LTV-adjusted, peste break-even-ul direct. De aia pragul nostru de tăiere pe Google Shopping e 0.8, nu 1.0: dacă produsul are repeat dovedit, un single-purchase POAS sub 1.0 nu e neapărat o pierdere pe orizontul de 90 de zile. Klaviyo e sursa cea mai curată pentru repeat_rate per cohortă. Magento îl confirmă cross-check.

Concret, luni dimineața avem un export automat care listează toate campaniile active cu POAS pe 14 zile, POAS pe 28 de zile (pentru tendință), CPS%, Health Score, plus semnalul Klaviyo (verde, galben sau roșu) pe cohortă. Human review: 15 până la 20 de minute, decizii luate și comunicate echipei de ads până la prânz. Review-ul se face pe un view comun, cu o singură coloană de „acțiune propusă" pe fiecare rând și un singur rând de confirmare sau override. Fără un review săptămânal consistent, pragurile sunt un ornament pe dashboard.

Pasul 7: Audit checklist

15 întrebări împărțite pe date, formule, decizii. Dacă răspunzi „da" la toate, POAS-ul tău reflectă realitatea. Sub 10, ai goluri mari.

Testul final: auditează-ți stack-ul pe care îl ai azi. Nu trebuie să construiești tot ce am descris, trebuie doar să știi unde sunt golurile. Răspunde cinstit, notează-ți scorul.

Categoria A: Date

  1. Ai costul real al transportului per comandă (nu estimat)?

  2. Ai costul de achiziție per SKU (nu doar prețul de vânzare)?

  3. Păstrezi istoricul costurilor (nu doar costul curent)?

  4. Știi ce procent din comenzi au atribuție GA4 vs. sunt „Direct"?

  5. Ai sync automat de 2× pe zi cu monitorizarea de freshness?

Categoria B: Formule

  1. TVA-ul e time-aware (divizor diferit pre/post schimbarea cotei)?

  2. Shipping-ul se distribuie pe baza comenzilor Magento reale, nu pe baza tranzacțiilor din GA4?

  3. Ai fallback parent SKU → ratio sales-weighted când lipsesc costurile?

  4. Ai POAS canonic separat de variante (seasonality, LTV, SKU)?

  5. ROAS-ul folosește venitul cu TVA; profitul folosește venitul fără TVA?

Categoria C: Decizii

  1. Ai praguri POAS diferite per canal (Google Shopping, Search, Meta, TikTok)?

  2. Ai un Health Score care combină POAS + CPS% + volum?

  3. Ai reguli formale de tăiere și scalare pentru ferestrele de 14 zile?

  4. Verifici cohortele Klaviyo înainte de scalări mari (pull-forward check)?

  5. Cineva review-uiește dashboard-ul luni dimineața, nu ad hoc?

Cum citești scorul:

  • 14-15 da: POAS-ul tău reflectă profitul real. Păstrează ritmul.

  • 10-13 da: are goluri, dar deciziile săptămânale sunt, în mare parte, corecte. Prioritizează golurile din Categoria B (formule), deoarece acestea introduc un bias sistemic.

  • Sub 10 da: POAS-ul tău e nedemn de încredere. Fiecare decizie bazată pe el are riscul de a fi greșită. Prioritizează categoriile A și B înainte de a rafina categoria C.

❝

„15 întrebări, scor sub 10 = POAS nedemn de încredere. Categoriile A și B au prioritate. Fără date și formule corecte, regulile de decizie sunt placebo."

Ordinea priorităților nu e accidentală. Categoria A (date) e fundația. Fără shipping real, fără cost de achiziție, fără istoric, orice formulă construită deasupra minte cu amplitudine mare. Categoria B (formule) este nivelul la care bias-ul sistemic intră în cifre: TVA aplicat greșit, shipping distribuit pe un eșantion înclinat, costul de azi folosit pentru comenzi din 2024. Greșelile de aici nu produc zgomot, ci direcție. Toate campaniile arată mai proaste sau mai bune decât sunt, în aceeași direcție, tot timpul. Categoria C (decizii) este stratul la care oamenii cred că stă diferența. Dar pragurile și Health Score-ul, aplicate peste date și formule stricate, sunt ritual, nu management.

Un scor de 13 cu golurile în Categoria A e mai periculos decât un scor de 11 cu golurile în C. În primul caz, executivii iau decizii rapide pe baza unor cifre care par corecte: UI-ul e stabil, dashboard-ul se încarcă, numerele par plauzibile. Singura persoană care ar fi observat discrepanța e cea care a scris query-ul original și nu s-a mai uitat la el de 2 ani. În al doilea caz, executivii știu că nu au un proces stabil și tratează cifrele cu scepticismul potrivit. Calibrarea încrederii contează mai mult decât scorul absolut. Un dashboard pe care nu ai încredere 100% e mai util decât unul în care ai încredere oarbă.

Dacă ai ajuns aici și ai cel puțin 3 întrebări la care ai răspuns „nu", ai material de lucru pentru 2-3 luni. Nu trebuie să rescrii totul simultan. Începe cu cea mai dureroasă, probabil costul istoric sau distribuția de shipping, în experiența noastră.

Dashboard-ul nostru a trecut prin 4 iterații majore în ultimele 3 săptămâni. Fiecare a făcut POAS-ul un pic mai realist. Și deciziile, un pic mai puțin ghicite. Probabil ai deja câteva idei despre propriul tău stack, iar una dintre ele te pune pe gânduri mai mult decât celelalte.

Pentru cei care evaluează make-vs-buy strict pe cifre: stack-ul ăsta rulează la aproximativ 80-150 EUR/lună pentru infra (Vercel Pro pentru hosting și cron-uri, Postgres managed pe Neon/Supabase, BigQuery pay-per-query pentru join-urile cu Funnel.io), plus Funnel.io aproximativ 1400 EUR/lună (plan Marketing Essentials, 13 connectori, inclusiv pentru platforme locale ca 2Performant și Glami, pe care nicio alternativă nu le suportă). Total aproximativ 1500 EUR/lună, față de un SaaS de attribution all-in (Triple Whale, Northbeam, Polar) de 2000-3000 EUR/lună fără connectorii locali, deci ar trebui să mai plătești Funnel.io în paralel oricum. Calculul te scoate cu aproximativ 12-18k EUR/an înainte, plus data ownership și extensibilitate ca bonus.

O a treia opțiune pe care AI-ul a făcut-o realistă în 2025-2026: vibe-code-uiești connectorii direct la API-urile platformelor. Nu există platformă de ads fără API public: Google Ads, Meta Marketing, TikTok Business, 2Performant, Glami, RTB House, Klaviyo; toate au endpoint-uri documentate. Cu un coding assistant, un connector ajunge într-o sesiune de 1-2 ore. Estimativ: aproximativ 26 de ore inițial pentru 13 connectori, plus aproximativ 5-10 ore pe lună pentru mentenanță când API-urile se schimbă. Cost infra adițional zero. Funnel.io iese din bill complet. Pierzi SLA-ul lor (când un endpoint se rupe la 3 dimineața, debugging-ul e treaba ta), dar economisești încă aproximativ 17k EUR/an. La scala noastră curentă n-am făcut switch-ul. Funnel.io e asigurarea ieftină pentru un sync stabil. Dacă reîncepem stack-ul de la zero azi, ăsta e calculul pe care l-am refăcut.

Dacă ceva din checklist ți-a scos la suprafață o întrebare care te-a ținut agățat, răspunde la newsletter cu ea. Răspund personal la fiecare. Cel mai probabil alții au aceeași problemă și merită un follow-up dedicat, scris pentru întreaga listă.

Dacă ești curios de partea teoretică, de ce POAS contează mai mult decât ROAS, am scris despre asta separat. E un argument mai lung, cu mai puține formule și mai multe decizii, pentru momentul în care vrei să convingi pe cineva că merită efortul de a reconstrui stack-ul.

Iar dacă vrei să discutăm cum arată peisajul ăsta concret pentru brandul tău (ce date ai, ce îți lipsește, ce ai corecta primul), scrie-mi pe LinkedIn. Conversațiile cu oameni care operează branduri DTC reale sunt cea mai bună sursă de inputuri pe care am avut-o pentru iterațiile dashboard-ului. Dacă ne poți spune cum arată stack-ul tău, schimbăm note și mergem mai departe.