# Conversion correction ledger

Open `index.html` in a current browser. It is self-contained and works by double-clicking a local file, offline, with no installation or account. Load the synthetic sample, select **Review ledger**, inspect held jobs and save the review. The ZIP contains this exact tool, source and fixtures.

Nothing is uploaded. There are no analytics, remote scripts, fonts or requests. External documentation links require internet access. Input stays in browser memory until reset/close; downloaded JSON contains the supplied data. Use job IDs instead of names, email addresses or phone numbers.

## Synthetic example

Eight invented jobs start at **USD 8,400.00**. Resolved jobs project **USD 4,100.00**, four remaining conversions. Two held jobs retain **USD 1,000.00** of original value outside that final total. Six corrections: four RESTATE, two RETRACT. One exact retry is suppressed. Independently specified expected outcomes: `fixtures/golden.json`.

This is a proposed-batch reviewer. Supplied `imported` and `approved` statuses are operator assertions, not verified provider receipts. Local totals are projections, not live Google or accounting balances.

## Input schema

JSON has `asOf`, `originals` and `events` arrays. Browser review uses the visible Review as-of field; loading JSON with `asOf` fills it. The sample has a fixed teaching cutoff. The core API uses JSON `asOf`.

Original fields:

| Field | Meaning |
| --- | --- |
| `id` | Stable local original import/job ID |
| `orderId` | Original Order ID, preferred |
| `gclid` | Original GCLID, only when original had no Order ID |
| `conversionName` | Exact conversion action name |
| `conversionTime` | Original conversion timestamp |
| `recordedAt` | Timestamp your receipt says original was recorded/imported |
| `value` | Original imported nonnegative absolute amount |
| `currency` | USD, EUR, GBP, CAD, AUD or NZD |
| `status` | Must be `imported` |
| `actionType` | Optional, defaults to `UPLOAD_CLICKS`; also recognizes `SALESFORCE` and `WEBPAGE` |

Events require `eventId`, `originalId`, original `orderId` (or `gclid`), exact `conversionName`, original `conversionTime`, `adjustmentTime`, `type`, `currency`, `status`, and `reason`. RESTATE `value` is the **new absolute total**, not a delta. RETRACT value can be blank or zero. Event status must be `approved`. Unknown/pending/missing status holds the job.

The local ID joins the original; the full advertising identity must also match. WBRAID is outside scope and held, even if an Order ID exists. GBRAID and WEBPAGE require an Order ID. Unsupported action types are held. GCLID path produces review JSON only.

Dates use `YYYY-MM-DDTHH:mm:ssZ` or `YYYY-MM-DDTHH:mm:ss±HH:mm`. Invalid calendars, missing timezones and fractional seconds are rejected. Output timestamps normalize to UTC as `YYYY-MM-DD HH:mm:ss+0000`.

Amounts accept strings or finite numbers, at most two decimals. Prefer strings to preserve intended precision. Zero is valid; missing is unknown. Negative values, exponent strings, nonfinite values, extra decimals and individual amounts above 10 billion currency units are held. Choose accounting rounding at the source. Integer cents and BigInt sums preserve exact totals. Currencies are never combined; currency changes are held for a separate FX review.

For CSV, load two files using the JSON field names as headers. See `fixtures/originals.csv` and `fixtures/events.csv`. UTF-8 BOM, CRLF/LF, quoted commas, newlines and escaped quotes work. Malformed quotes, duplicate headers and wrong row widths are rejected. These are **our input schemas**, not Google import files. Limits: 10,000 originals, 20,000 events; each loaded file under 12 MB. Large tables may render slowly on mobile.

## Audit history, retries and holds

Preserve original receipts and append business decisions to your source history. This tool does not enforce or persist an append-only database. Save its full JSON review after every run.

- Identical original retries count once. Conflicting original IDs or duplicate provider identities hold the original.
- Identical event retries count once even with different local event IDs. Same provider identity/adjustment instant with different value, type or status is a conflict. A changed reason alone does not change the adjustment.
- Reusing an event ID for another adjustment holds both originals.
- Actual UTC time determines event order. Positive restatements replace prior values. RETRACT or zero RESTATE ends the adjustment path; later events hold the proposed batch.
- Any bad/unapproved event holds **all proposed events for that original**. Its known original amount is held separately. Missing original amounts/unsupported currencies cannot be summed; rows and reasons remain visible.
- Unknown-original events appear in the issues list and produce no rows. Unchanged valid originals retain their original count, including valid zero-value originals.

Resolve a hold explicitly: save the earlier JSON, preserve superseded proposals and explanations in source history, then prepare a resolved batch containing approved events. Appending a valid event after an invalid one does not erase the hold. The tool cannot infer which conflicting decision the office meant.

Idempotency is local to the input batch, not across actual Google uploads. Downloading a repeated draft does not establish whether it was sent. Keep native acceptance/failure/retry receipts separately. Supply relevant earlier correction history when reviewing later changes so a prior terminal outcome stays visible. The tool does not create new conversions to recover retractions.

## Local timing policy

For originals with proposed events, hold if recorded less than 24 hours before the cutoff or if conversion is at least 54 days old. Using conversion time for the latter is a conservative local policy, not a claim about Google's backend clock. Events must follow conversion and precede cutoff; conversion/import times must be sensible.

Current English Help differs: preparation says 54 days generally and mentions 7-day autobidding readability; overview gives 55 days/7-day specifically for Hotel Ads. Confirm actual route/action eligibility in Google. The tool makes no bidding-effect claim.

## Draft output and native template check

The review JSON includes supplied input, proposed rows and issues. Browser and sample CSVs use six documented Order-ID fields:

```text
Order ID
Conversion Name
Adjustment Time
Adjustment Type
Adjusted Value
Adjusted Value Currency
```

These are **drafts**, not certified import-ready files. Obtain the current template through Google Ads Goals > Conversions > Uploads > plus > View templates > Conversions adjustments. Compare all required columns and formats, retain native columns, and check each processing result. No authenticated template was downloaded and no provider upload performed for this tool.

GCLID rows stay in review JSON and are omitted from CSVs. Retraction value/currency are blank. Restatements contain final absolute amount and original currency. New conversions belong in a separate upload. The tool has no account, manager-permission or eligibility checks.

## Reproduce

Node.js 20 or later; core/build use only built-ins. From extracted folder:

```sh
node --test core.test.mjs
node build.mjs
```

`build.mjs` embeds exact `core.js` and sample JSON into `index.template.html`, writes standalone `index.html`, sample review and both CSVs. Tests cover absolute refunds, chronology, terminal/conflict/duplicate rules, identity joins, malformed inputs, exact cents and independent golden totals.

Optional browser tests use the site's existing Playwright and installed Chromium. From the site repository root:

```sh
node --test web/public/downloads/conversion-adjustments/browser.test.mjs
```

The extracted bundle does not include Playwright; core tests need no dependencies. Browser tests exercise actual local-file downloads, JSON/CSV loading, stale draft clearing, safe hostile text, 390px containment, no external requests and exact large-total display. Set `CONVERSION_SHOT_DIR` to an existing directory for a mobile screenshot.

To rebuild and byte-check a deterministic complete ZIP from site root, using Python 3 standard library:

```sh
python3 web/public/downloads/conversion-adjustments/package.py
```

## Primary sources and licensing

Checked October 24, 2026:

- [Google Ads Help: How to adjust your conversions](https://support.google.com/google-ads/answer/7686280?hl=en): manual fields, timestamp formats and repeated adjustments.
- [Google Ads Help: About conversion adjustments](https://support.google.com/google-ads/answer/7686447?hl=en): value replacement, removal, terminal outcomes and identity paths.
- [Google Ads API: Import conversion adjustments](https://developers.google.com/google-ads/api/docs/conversions/upload-adjustments): action types, identifiers, ownership and action-change restrictions.

Original tool and fixtures: MIT, included `LICENSE.txt`. No Google sample code copied. Documentation is linked for requirements. No client data, performance result, Google endorsement or native validation is claimed.
