Skip to content

fix(meta-ads): currency-offset-aware budget conversions (JPY and other zero-decimal currencies)#372

Merged
hyoshi merged 1 commit into
mainfrom
fix/meta-currency-offset
Jul 8, 2026
Merged

fix(meta-ads): currency-offset-aware budget conversions (JPY and other zero-decimal currencies)#372
hyoshi merged 1 commit into
mainfrom
fix/meta-currency-offset

Conversation

@hyoshi

@hyoshi hyoshi commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

Units audit follow-up to #366/#367. Meta expresses budgets/bids in the currency's minor units with a per-currency offset — 100 for USD-like currencies, 1 for zero-decimal currencies (JPY, KRW, TWD, VND, IDR, HUF, ISK, CLP, COP, CRC, PYG) per the official Marketing API currencies table. Three places hardcoded the offset-100 assumption, silently scaling zero-decimal-currency money 100x:

Site Direction JPY failure mode (before)
meta_ads/mappers.py _cents_to_amount (÷100) read budgets reported 1/100 (exported-but-uncalled API; MCP read path is raw passthrough and was already correct)
adapters/meta_ads/mappers.py (×10,000) read daily_budget_micros 1/100 — inconsistent with the correctly-converted spend micros in the same model
adapters/meta_ads/adapter.py (÷10,000) write ¥5,000/day intent transmitted as ¥500,000/day to the live API (adapter is documented-latent; no OSS code wires it yet)

Fix

  • core/providers/models.py: ZERO_DECIMAL_CURRENCIES + META_OFFSET_100_CURRENCIES whitelists (both transcribed from Meta's table; CRC's offset-1 membership double-checked) and minor_units_per_unit(currency). Unrecognized codes raise ValueError — security review showed a typo like "JPN" falling back to offset 100 would 100x-overstate the write; fail-fast also converts future Meta-table drift into a loud error instead of silent mis-scaling.
  • meta_ads/mappers.py: _minor_units_to_amount(value, currency); map_campaign / map_ad_set gain a required keyword-only currency (breaking for the exported-but-unused API — a default would reintroduce the bug).
  • adapters/meta_ads/: both directions currency-aware (JPY ×/÷1e6, USD ×/÷1e4, exact integer arithmetic); write path rejects negative micros; MetaAdsAdapter.__init__ now requires the account currency (readable from GET /act_<id>?fields=currency).
  • Docs: stale "in cents" docstrings corrected; _mureo-strategy SKILL.md guardrail bullets now state Meta budget caps compare in Meta's native minor units (= currency units for JPY-like currencies; cents for USD-like).

spend conversions (Meta returns spend in major units) verified correct and untouched. Google Ads micros paths unaffected.

Test plan

  • 17 new tests (tests/test_currency_offset.py): both directions × JPY/USD, whitelist parity with Meta docs, disjoint-sets, typo "JPN" / unknown "XXX" fail fast (helper + write path), negative rejection, required/blank currency on the adapter
  • Legacy mapper/adapter tests updated to explicit currency="USD" (semantics preserved)
  • ruff / black / mypy (CI flags) clean; full suite green except known local plugin-env noise

Reviews: security-reviewer (HIGH unknown-code fallback → fail-fast fix → APPROVE, whitelists verified disjoint) + python-reviewer (arithmetic verified both directions, round-trip exact → CONFIRM) + /code-review checklist.

https://claude.ai/code/session_0195WBK6YopTveq9RAQE2xMz

…r zero-decimal currencies)

Meta expresses budgets/bids in the currency's minor units with a
per-currency offset (100 for USD-like; 1 for zero-decimal: JPY, KRW,
TWD, ...). Blanket /100, *10_000 and //10_000 conversions silently
scaled zero-decimal-currency money 100x in both directions.

- core/providers/models.py: ZERO_DECIMAL_CURRENCIES +
  META_OFFSET_100_CURRENCIES whitelists (transcribed from the official
  Meta currencies table) and minor_units_per_unit(), which fail-fasts
  on unrecognized codes — a wrong guess (typo 'JPN') would otherwise
  send a 100x-too-large budget to the live API.
- meta_ads/mappers.py: _cents_to_amount -> _minor_units_to_amount;
  map_campaign / map_ad_set take a required currency kwarg (exported
  API; the MCP read path is raw minor-unit passthrough and unchanged).
- adapters/meta_ads: read (minor units -> micros) and write (micros ->
  minor units, negative-rejecting) conversions are currency-aware;
  MetaAdsAdapter requires the account currency at construction.
- Stale 'in cents' docstrings corrected; _mureo-strategy SKILL.md now
  states guardrail caps compare Meta budgets in Meta's native minor
  units (identical to currency units for JPY).

Found during a units audit of #366/#367; the previously wired MCP
paths were already correct — the affected mappers/adapter were latent.

Claude-Session: https://claude.ai/code/session_0195WBK6YopTveq9RAQE2xMz
@hyoshi
hyoshi merged commit 352caca into main Jul 8, 2026
9 checks passed
@hyoshi
hyoshi deleted the fix/meta-currency-offset branch July 8, 2026 21:44
@hyoshi hyoshi mentioned this pull request Jul 9, 2026
hyoshi added a commit that referenced this pull request Jul 9, 2026
Google Ads read-only demographic/audience criteria + image assets (#369),
budget type get/set (#371); Meta Ads ad-set end_time + lifetime_budget
(#368); Search Console cross-client isolation / site_url tenant allow-list
(#375, security); About Installed-packages consistency (#365); Meta budget
zero-decimal currency fix (#372).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant