One currency, one timezone (normalise multi-account reporting first)

· has watched the rupee rewrite last quarter’s CAC🌍 with an FX table and one rule, not a lecture

Published September 6, 2026

The 60-second version

  • Summing USD and INR in one column rewrites history every time the exchange rate moves against you.
  • Convert, then truncate — never the reverse. Shift to the reporting timezone before taking the date, or Tuesdays split in two.
  • Normalise once in shared views: every report reads the views, raw stays immutable, and the FX method gets footnoted.
bottom line: one Tuesday, one rupee, one view

Your US ad account spends dollars. Your India account spends rupees. Your Meta account reports in Pacific Time and your Google account in IST — while your warehouse stores everything in UTC. Sum that raw data into one report and you get a number that is precisely wrong: exchange-rate drift baked into trends, and day boundaries that move revenue between dates depending on who is asking.

Every multi-account, multi-currency reporting setup needs a normalisation layer: one currency, one timezone, applied the same way by every query. This post builds it — the exchange-rate table, the timezone conversion, and the SQL view everything else reads from. two corruptors, one cure

Normalise once, read everywhere

The Normalisation Layer

Data Journey
Stage 1As the APIs give it
Raw platform data

Spend in account currency, timestamps in account timezone. Correct per-account, incomparable across accounts.

$1,200 + ₹98,000 + €410
Stage 2One place, one logic
Normalisation view

A single view converts every row to reporting currency and reporting timezone. All reports read from here, never from raw.

₹1,02,400 + ₹98,000 + ₹37,200
Stage 3By construction
Every report agrees

Blended CAC, MER, pacing and ad-hoc questions share one definition of 'spend on Tuesday in INR'. Disputes become data questions, not logic disputes.

One Tuesday, one number

A reporting timezone is the single timezone all reporting is expressed in — usually the business's home timezone. Reporting currency is the same idea for money. The layer itself is boring infrastructure: a rates table, a view, a convention. Its absence is the cause of a remarkable share of "the dashboard is wrong" tickets.

Normalise at query time, not at load time. Storing converted values in raw tables destroys the original and freezes an exchange rate into history. Keep raw spend and raw timestamps immutable; convert in a view so the rate methodology can improve without reloading anything.


Problem 1: mixed currencies

Summing USD and INR spend in one column is obviously wrong, and everyone does a version of it anyway — usually by converting once in a spreadsheet with "the current rate" applied to all of history. That rewrites the past every time the rate moves: last quarter's CAC changes because this week's rupee did.

The rates table

You need one row per currency per day. Sources in ascending order of rigour: your payment processor's settlement rates (best — it is what you actually paid), a daily ECB/RBI reference feed, or a manual monthly average documented as such. What matters is not the source's perfection but that it is daily, versioned, and shared:

Settlement rates

Your payment processor's own rates. Best — it is what you actually paid, spread included, and finance already books it.

ECB or RBI reference feed

A daily public reference rate. Rigorous and free; differs slightly from settlement, but consistently and transparently.

Manual monthly average

A documented average, refreshed monthly. Acceptable only if labelled as such on every report that uses it.

Daily FX Rates Table Plus the Normalised Spend View

Document the rate source on the report, not in a wiki. A footnote — "Spend converted at daily settlement rates" vs "at monthly average rates" — changes how finance reads every number on the page. Monthly-average conversion can shift a volatile month's CAC by 3–5% versus daily rates; that is larger than some optimisations you will debate all quarter.

LIVE · DRAG ITWhat does the FX method change?
1,200
83
87

Same spend, rewritten history

Spend at old rate
₹99.6k
Spend at new rate
₹1L
History rewritten by
4.8%
RATE DRIFT, NOT PERFORMANCE

One current rate applied to all history moves last quarter's CAC by more than some optimisations you will debate all quarter. Use daily rates.

live mathsestimates are fine — this is a what-if sandbox

Problem 2: mixed day boundaries

Ad accounts report in their own timezone; warehouses store UTC; your business thinks in IST. An order at 11:30 PM IST on Tuesday is Wednesday in UTC. A Meta account on Pacific Time ends its "Tuesday" 13.5 hours after IST does. Compare daily spend (account timezone) against daily revenue (UTC date) and every single day is misaligned by the overlap gap.

The 9 PM order that moved days

UTC vs IST vs PT
Order placedTue 11:30 PM ISTCustomer in Mumbai checks out
Warehouse (UTC)Tue 6:00 PM UTCDATE(timestamp) = Tuesday. Correct.
Meta account (PT)Tue 6:00 AM PTSame instant, different 'day' in ad reporting
Naive daily joinRevenue Tue vs spend TueTwo different Tuesdays — off by the timezone gap at both edges
No data is missing and no query is wrong. The two sides simply cut the day at different meridians. The fix is converting both sides to the reporting timezone before truncating to date — never truncating first.

The rule: convert, then truncate — never the reverse

-- WRONG: truncates in UTC, then labels it a business day.
DATE(order_created_at)  -- a UTC day, not your Tuesday

-- RIGHT: shift to reporting timezone, then take the date.
DATE(order_created_at, 'Asia/Kolkata')  -- your Tuesday

Apply the same to spend timestamps from each account's native timezone into the reporting timezone, and daily joins finally compare like with like. Keep the account timezone per account in a tiny dimension table — accounts do get recreated in different timezones, and hardcoding America/Los_Angeles in forty queries is how the bug returns.

An order lands at 2:00 AM IST on Wednesday. DATE(utc_timestamp) files it under which day?


The reporting mart

Three Layers, Each Owned Once

Reporting Hierarchy
Tier 1
Raw (immutable)

Exactly what the APIs delivered: native currency, native timestamps, UTC warehouse times. Append-only; never edited.

raw_ad_spend + raw_orders + fx_rates
Tier 2
Normalised view

Currency and timezone conversion in one reviewed place. The only logic downstream is allowed to assume.

v_normalised_spend + v_normalised_orders
Tier 3
Reports (thin)

MER, CAC, pacing — all plain aggregations over the views. A report bug is now a filter bug, never a conversion bug.

GROUP BY reporting day, SUM(spend_inr)

Build the layer in an afternoon one afternoon, in order →

  • Declare reporting currency and timezone — INR and Asia/Kolkata for most readers here. A business decision, written down, not a technical default.
  • Backfill the FX table — daily rows per currency from settlement data or a reference feed. One table, shared by every query from now on.
  • Build the two normalisation views — spend and orders, converted but otherwise untouched. Review the SQL once, carefully: everything downstream inherits it.
  • Point reports at the views and footnote the method — migrate one report, reconcile against the old logic for two weeks, then migrate the rest.

Frequently Asked Questions

Should revenue be converted at the order-date rate or today's rate?

Performance reporting

📊 Order-date rate

What were the economics when the decision was made? CAC, ROAS and channel efficiency all convert at the rate of the day the money moved.

  • CAC and ROAS
  • Channel efficiency
  • Period comparisons
Cash snapshots only

💰 Current rate

What is it worth right now? Only for cash-position views — never mixed into performance trends.

  • Cash position today
  • Treasury views
  • Never in the same report as order-date numbers

Mixing the two in one report is the most common FX bug after summing raw currencies. pick one per report, footnote it

What about crypto-like intraday FX swings?

Daily granularity is enough for marketing reporting — your attribution windows are days wide and your spend decisions are daily. Sub-daily FX precision matters for treasury, not for CAC. Do not build hourly FX infrastructure for a dashboard read once a morning.

How does this interact with data freshness?

Independently — and that is the point. Freshness tells you whether Tuesday's data has arrived; normalisation tells you what Tuesday means. Debug them separately: first confirm both sides are complete for the window, then confirm both sides cut the window identically.


Summary & Next Steps

Multi-account reporting has exactly two silent corruptors — mixed currencies and mixed day boundaries — and one cure: a declared reporting currency and timezone, a daily FX table, and views that every report shares.

Takeaways for your next report

  • Mixed currencies and mixed day boundaries are the two silent corruptors — one declared reporting currency and timezone cures both.
  • Convert, then truncate: shift every timestamp to the reporting timezone before taking the date, or days split at the wrong meridian.
  • Keep raw immutable, convert in shared views from a daily versioned FX table, and footnote the rate method on the report.
  • Use order-date rates for performance and current rates for cash snapshots — never both in one report.
stick this on your Monday report
Free tool

Blended CAC Calculator

Total spend across every channel, divided by total new customers — the acquisition cost number that reconciles with what you actually spent.

Chinmay Raibagkar

Chinmay Raibagkar

About author →

Founder of DataLens AI. He helps non-technical teams read their ad and database numbers with confidence — which number to trust, what to do next, and what to ignore.