@bytebrand/export-cars 1.26.31 → 1.26.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/configs/autouncle.config.d.ts +14 -0
  2. package/dist/configs/autouncle.config.js +36 -3
  3. package/dist/configs/autouncle.config.js.map +1 -1
  4. package/dist/configs/meinstadt.config.d.ts +15 -0
  5. package/dist/configs/meinstadt.config.js +42 -3
  6. package/dist/configs/meinstadt.config.js.map +1 -1
  7. package/dist/constants/autouncle.contants.d.ts +1 -3
  8. package/dist/constants/autouncle.contants.js +2 -4
  9. package/dist/constants/autouncle.contants.js.map +1 -1
  10. package/dist/exporters/autouncle.exporter.d.ts +3 -2
  11. package/dist/exporters/autouncle.exporter.js +45 -27
  12. package/dist/exporters/autouncle.exporter.js.map +1 -1
  13. package/dist/exporters/meinstadt.exporter.d.ts +3 -2
  14. package/dist/exporters/meinstadt.exporter.js +40 -23
  15. package/dist/exporters/meinstadt.exporter.js.map +1 -1
  16. package/dist/formats/autouncle-json.builder.js +1 -4
  17. package/dist/formats/autouncle-json.builder.js.map +1 -1
  18. package/dist/interfaces/export-item.interface.d.ts +8 -12
  19. package/dist/interfaces/openai-ads.interface.d.ts +2 -1
  20. package/dist/interfaces/openai-ads.interface.js +1 -0
  21. package/dist/interfaces/openai-ads.interface.js.map +1 -1
  22. package/dist/interfaces/params.interface.d.ts +6 -0
  23. package/dist/services/autouncle-feed-run.d.ts +66 -0
  24. package/dist/services/autouncle-feed-run.js +135 -0
  25. package/dist/services/autouncle-feed-run.js.map +1 -0
  26. package/dist/services/meinstadt-feed-run.d.ts +81 -0
  27. package/dist/services/meinstadt-feed-run.js +166 -0
  28. package/dist/services/meinstadt-feed-run.js.map +1 -0
  29. package/dist/services/openai-ads-feed-run.d.ts +1 -0
  30. package/dist/services/openai-ads-feed-run.js +4 -0
  31. package/dist/services/openai-ads-feed-run.js.map +1 -1
  32. package/dist/transformers/autouncle.transformer.d.ts +38 -22
  33. package/dist/transformers/autouncle.transformer.js +156 -144
  34. package/dist/transformers/autouncle.transformer.js.map +1 -1
  35. package/dist/transformers/meinstadt.transformer.d.ts +17 -13
  36. package/dist/transformers/meinstadt.transformer.js +88 -61
  37. package/dist/transformers/meinstadt.transformer.js.map +1 -1
  38. package/dist/transformers/openai-ads-finance.d.ts +2 -0
  39. package/dist/transformers/openai-ads-finance.js +39 -0
  40. package/dist/transformers/openai-ads-finance.js.map +1 -0
  41. package/dist/transformers/openai-ads.transformer.d.ts +9 -1
  42. package/dist/transformers/openai-ads.transformer.js +25 -5
  43. package/dist/transformers/openai-ads.transformer.js.map +1 -1
  44. package/dist/tsconfig.build.tsbuildinfo +1 -1
  45. package/docs/autouncle/AUDIT.md +97 -0
  46. package/docs/autouncle/PARTNER-QUESTIONS.md +53 -0
  47. package/docs/autouncle/RUNBOOK.md +64 -0
  48. package/docs/meinestadt/AUDIT.md +146 -0
  49. package/docs/meinestadt/PARTNER-QUESTIONS.md +54 -0
  50. package/docs/meinestadt/RUNBOOK.md +64 -0
  51. package/docs/openai-ads/AUDIT.md +2 -1
  52. package/docs/openai-ads/RUNBOOK.md +26 -2
  53. package/package.json +3 -2
  54. package/src/configs/autouncle.config.ts +51 -3
  55. package/src/configs/meinstadt.config.ts +57 -2
  56. package/src/constants/autouncle.contants.ts +2 -3
  57. package/src/exporters/autouncle.exporter.ts +59 -37
  58. package/src/exporters/meinstadt.exporter.ts +53 -32
  59. package/src/formats/autouncle-json.builder.ts +2 -4
  60. package/src/interfaces/export-item.interface.ts +15 -12
  61. package/src/interfaces/openai-ads.interface.ts +2 -0
  62. package/src/interfaces/params.interface.ts +10 -0
  63. package/src/services/autouncle-feed-run.ts +197 -0
  64. package/src/services/meinstadt-feed-run.ts +240 -0
  65. package/src/services/openai-ads-feed-run.ts +6 -0
  66. package/src/transformers/autouncle.transformer.ts +217 -175
  67. package/src/transformers/meinstadt.transformer.ts +114 -68
  68. package/src/transformers/openai-ads-finance.ts +60 -0
  69. package/src/transformers/openai-ads.transformer.ts +32 -6
  70. package/test/autouncle-feed-run.test.ts +256 -0
  71. package/test/autouncle-mapping.test.ts +225 -0
  72. package/test/fixtures/meinstadt.fixture.ts +87 -0
  73. package/test/fixtures/openai-ads-finance-titles.json +325 -0
  74. package/test/meinstadt-feed-run.test.ts +288 -0
  75. package/test/meinstadt-mapping.test.ts +215 -0
  76. package/test/openai-ads-finance.test.ts +109 -0
@@ -0,0 +1,97 @@
1
+ # AutoUncle inventory feed — audit (ADV-322 / ADV-323)
2
+
3
+ Status 13.10.2026, base `origin/deploy_develop` 3a025095, `@bytebrand/export-cars` 1.26.31.
4
+ Historical documents (Confluence "autouncle [paused]", "2.7.2 Export Plugins", ACF-3604/3650/3717/3725/6172/6190)
5
+ are evidence only. No current AutoUncle inventory schema is available; the public AutoUncle B2B
6
+ "automotive API" is a dealer/DMS **valuation** API and is not used as a schema here.
7
+
8
+ ## 1. Is the export live? — No
9
+
10
+ | Where | Event | State | Last run | Output |
11
+ |---|---|---|---|---|
12
+ | PROD Cronicle (`auto-de-prod-cronicle-01`, export-cars 1.26.18) | `3.0 Export to autouncle - part 1` `ek61rkico08` | **disabled**, no timing, nothing chains into it | one manual run 07.07.2026, aborted by user | `resources/autouncle-export/api_export_cars_autouncle.json` = 8 bytes (`{"ads":[`) |
13
+ | | `3.1 Export to autouncle - part 2` `ek61ro0yv09` | **disabled**; shell `cp` of the JSON to `htdocs/autouncle/export_cars_autouncle.json` | never ran | `htdocs/autouncle/` does not exist |
14
+ | DEV Cronicle (`auto-de-dev-cronicle-01`, 1.26.31) | `3.0 Export to Autouncle part1` `ek60tym7m0b` | enabled, no timing; chained from `2.1 Export facebook - part 2` | 10.09.2026, code 0 | 467 KB JSON of 10.09 (298 ads) |
15
+ | | `3.1 Export to Autouncle part2` `ek60u1gsd0c` | enabled, chains to `ek87441xa04` | 10.09.2026, **code 1** every run (`cp: … htdocs/autouncle/… No such file or directory`) | — the rest of the DEV chain (criteo, meinestadt, ooyyo, …) stops here |
16
+
17
+ PROD 3.0 params: `environment=production`, `String` = allow-list of 19 dealer `metaData.ownerId`s, `Number` =
18
+ `metaData.imagesCount:2`. DEV 3.0: no rules. Both events were last modified 2023 (DEV) / 01.04.2026 (PROD,
19
+ migration). Events, keys and scripts were read with `storage-cli list_get`, secrets redacted.
20
+
21
+ Hosting: `https://scheduler.auto.de/autouncle/export_cars_autouncle.json` (historic) — no answer (timeout);
22
+ `https://cronicle.auto.de/autouncle/…` and `http://cronicle.autode-dev.de/autouncle/…` — 404. Nothing is
23
+ hosted for AutoUncle today, on any environment.
24
+
25
+ ## 2. Code path
26
+
27
+ `ExportType.AUTOUNCLE = 'autouncle'` → `AutouncleConfig` + `AutouncleExporter` (`export.schema.ts`).
28
+
29
+ Before (unchanged since the monorepo import 3046a15d, CO2 class ADV-187 b46e7e83):
30
+ `emptyDir(resources/autouncle-export)` → for every car of the base export snapshot: shared `filterCar`
31
+ (imagesCount > 0 + Cronicle rules) → UPA dealer profile (skip if none) → `AutouncleTransformer` →
32
+ `AutouncleJsonBuilder` appends `{"ads":[…]}` directly to the published file.
33
+
34
+ After (this branch): `AutouncleFeedRun` (`services/autouncle-feed-run.ts`) — lock → source snapshot check
35
+ (shared ADV-335 `FeedGuard`) → per car: duplicate id, `filterCar`, car-level exclusions, dealer address (UPA,
36
+ cached per dealer), clean images (ADV-336 `getCleanImages` + `ImageUrlValidator` + `selectOpenAiImageFields`)
37
+ → pure `mapAutouncleItem` → temp file → count check → atomic rename (`.prev` kept) → report.
38
+ Dry-run is the default and writes `resources/autouncle-dryrun/`, which no Cronicle step reads.
39
+
40
+ ## 3. Field mapping (`ads[]`, keys/order/spellings unchanged)
41
+
42
+ | Key | Source | Before | Now | Open |
43
+ |---|---|---|---|---|
44
+ | back_link | CBD `_id` | `https://www.<domain>/search/vehicle/<_id>?source=classifieds&medium=cpc&campaign=autouncle` | same; stable across runs | UTM pattern = Confluence 2.7.2; others existed (ACF-3650/6172) → Q8 |
45
+ | make / model_name / model_variant | mainData.make / model / subModel | raw | same | `SONSTIGE` placeholder models are exported (data) → Q9 |
46
+ | fuel / body / gear_type | consumption.fuel / mainData.category / driveSuspension.gearbox | fixed maps; an unmapped or null selector made the key **disappear** from JSON | unmapped/null → `Other` / `other` | enum values → Q6 |
47
+ | doors | mainData.doors | string | same | |
48
+ | price | price.currentSalesPrice | `EUR <value>`; missing price → `"EUR "` and the car was still sent | same format; no valid positive price → car not sent (`no_valid_price`). Never a rate, never purchasePrice | currency format → Q6 |
49
+ | year | mainData.firstRegistration (Unix s) | null → `moment(0)` → **1970** (143 of 1044 DEV cars) | missing/0/null/invalid → `""`; outside 1900..next year → `""` | null vs "" → Q6 |
50
+ | status | same | `Neuwagen` only when firstRegistration === 0 | `Neuwagen` when no valid firstRegistration (same rule, null included) | 3 of 143 DEV cars are `condition=used` without registration → Q7 |
51
+ | mileage | mainData.mileage | `<n>km`, 0 → `1km` | same | |
52
+ | leasing / has_4wd | — | always `""` | same | → Q10 |
53
+ | color | exterior.exteriorColor, German label | same | same | |
54
+ | engine_size | engineData.cubicCapacity | `ccm` with no number when missing (189/1044) | `""` when missing | |
55
+ | power | engineData.powerKW | kW, no unit | same | unit → Q6 |
56
+ | seller_type / seller_name | — | `Dealer` / `auto.de` | same | marketplace vs dealer identity → Q3 |
57
+ | seller_id | metaData.ownerId | **dealer** id already (not a vehicle id) | same; contract test with two cars of one dealer | |
58
+ | seller_region / city / street_address / postcode | UPA businessAddress, else mainAddress; region from the postal code table | if the table did not know the place, street/city/zip were **blanked** (664/1044 DEV cars) | dealer's own values kept, region `Deutschland` | |
59
+ | description | offer.offerDescription | raw | same; 192/1002 empty on DEV, some contain literal `\\` line breaks (dealer data) | → Q10 |
60
+ | images | Imbo images | default profile: `/large` (format negotiated, can be WebP), padded with **3 noimage placeholders** below 17 photos (2820 placeholder URLs on DEV) | `[{url}]`, ADV-336 clean profile, explicit `large-cached.jpeg`, every URL checked (200 + image/jpeg|png); no valid 3/4 hero → car not sent | limits → ADV-323 gate |
61
+ | fuel_economy / co2_emission | consumption.consumptionCombined / environmentEmissions.co2 | raw | same | |
62
+ | co2_effiency_class / euro_emission_class / enviromental_sticker | environmentEmissions.* | raw selector keys (`selector_energyEfficiencyClass_d`) | same (no schema to translate to) | → Q6 |
63
+
64
+ Car-level exclusions (new, counted in the report): `duplicate_id`, `filtered`, `no_id`, `no_dealer`,
65
+ `reserved_or_sold` (offer.offerAvailability reserved/sold), `no_valid_price`, `no_valid_image`.
66
+
67
+ ## 4. Fixes on this branch
68
+
69
+ 1. Builder wrote the first car twice (`{"ads":[A,A,B…`) — fixed, test.
70
+ 2. Year 1970 for a null first registration (ACF-3717/3725 class of bug) — fixed, tests.
71
+ 3. Price must be a valid positive end-customer price, otherwise the car is not sent.
72
+ 4. Reserved / sold cars are not sent (absence = removal; partner semantics unknown → Q5).
73
+ 5. Dealer address no longer blanked when the postal code table misses the place.
74
+ 6. `engine_size` without a value is `""`, not `ccm`; unmapped selectors never drop a key.
75
+ 7. Images: clean profile + validator, no placeholders, no WebP risk, hero first (ADV-323).
76
+ 8. Fail-closed: no `emptyDir`; empty / partial / stale snapshot, empty result or a drop > 20 % against the
77
+ last good run → run blocked (exit 1, `3.1` copy does not run), last good file stays. Lock per state dir.
78
+ 9. Dry-run default; live only with `autouncleDryRun=false`.
79
+
80
+ ## 5. Evidence
81
+
82
+ - `npm test`: 138 ok (116 existing + 14 mapping + 8 run).
83
+ - Other feeds byte-identical: `origin/deploy_develop` and this branch compiled with the same `node_modules`;
84
+ every emitted `.js` outside the AutoUncle files is identical (only `params.interface.d.ts` gains three
85
+ optional fields). Covers Google, FB/Meta, Criteo, meinestadt, OOYYO, OpenAI and the rest.
86
+ - DEV dry-run 13.10.2026 (local, DEV snapshot 12.10 15:00 CEST, DEV `3.0` params): 2234 source cars →
87
+ 1002 ads, 370 dealers, excluded `filtered` 1190 / `no_valid_image` 42, images 7259/7259 ok, 0 placeholders,
88
+ 0 × 1970, 0 duplicate links. Old exporter on the same snapshot: 1045 ads (first duplicated), 143 × 1970,
89
+ 2820 placeholder URLs. 20-car sample in `autouncle-sample/` (worktree, not committed).
90
+
91
+ ## 6. Not done / not decided
92
+
93
+ - No partner schema, no test import, no activation, no Cronicle change, no version bump / publish.
94
+ - `minPublishedItems` default is 1 (PROD exports an allow-list of 19 dealers; real size unknown) — set per job
95
+ before activation.
96
+ - `3.1` copy target `htdocs/autouncle/` does not exist on either Cronicle; hosting/transport is a partner question (Q4).
97
+ - Portrait / small images: the validator checks status + MIME only, no pixel size — waits for partner limits.
@@ -0,0 +1,53 @@
1
+ # Open questions for AutoUncle (Mario Kunz-Pfauch, mario.pfauch@autouncle.com)
2
+
3
+ Draft for Marketing / Commercial. **Not sent.** Erik Lusuardi and Katarina Tökölyova were involved in the
4
+ January 2025 CPS proposal; their current ownership is not established. Historical documents (Confluence
5
+ "autouncle [paused]" 08.06.2023, "2.7.2 Export Plugins") are evidence only — we need the CURRENT inventory feed
6
+ specification in writing. The public AutoUncle B2B automotive API (dealer/DMS valuation) is not the inventory
7
+ ingestion contract and is not used as one.
8
+
9
+ ## Commercial / account
10
+
11
+ 1. **CPS vs CPC.** Is the January 2025 cost-per-sale proposal still valid, or is the model now cost-per-click
12
+ (or another)? Please confirm price, sale definition and eligibility, attribution/conversion window,
13
+ cancellations/returns, fees, credits, caps, term, billing and reporting.
14
+ 2. Who owns the account on your side today (business and technical contact)? Is the historic auto.de account
15
+ (dealer 1764) still active, or is a new contract / reactivation needed?
16
+ 3. **Seller identity.** We list as a marketplace: `seller_name` = "auto.de", `seller_id` = the individual
17
+ dealer's id, address = the dealer's address. Is that the model you expect, or must each dealer be a seller of
18
+ its own (name, imprint, contact)? Do you need dealer onboarding on your side?
19
+
20
+ ## Feed contract
21
+
22
+ 4. **Schema and transport.** Please send the current machine-readable inventory schema (field names, types,
23
+ mandatory/optional, enums, units). Is our historic `{"ads":[…]}` JSON with 31 keys still accepted —
24
+ including the spellings `co2_effiency_class` and `enviromental_sticker`? Do you pull a hosted URL (which
25
+ authentication, how often, from which IPs/user agent) or do we push (SFTP/API)?
26
+ 5. **Full vs delta, removals.** Is every file a full snapshot where a missing ad means "remove", or do you
27
+ need explicit delete/sold/reserved records? How fast do you process removals and price changes?
28
+ 6. **Formats.** Price as `EUR 15148` or a number + currency field? Unknown year: empty string, null, or omit
29
+ the key? Power in kW or PS? Mileage/engine size with or without the `km` / `ccm` suffix? Expected enum values
30
+ for fuel, body, gear type, CO₂ class (we can send A–G letters instead of internal keys), euro emission class
31
+ and environmental sticker?
32
+ 7. **New vs used.** How do you classify new cars, day registrations and demo cars? Is a car without a first
33
+ registration date "new"?
34
+ 8. **Unique key and links.** Which field is the unique ad key on your side — `back_link`, or should we add a
35
+ stable vehicle id? Which UTM parameters do you want on `back_link` (historic:
36
+ `source=classifieds&medium=cpc&campaign=autouncle`)? Do you need a click id passed through?
37
+ 9. How do you treat ads whose model is unknown (our placeholder "SONSTIGE")? Exclude or send?
38
+ 10. Optional fields: do you use `leasing`, `has_4wd`, `description` (German text), and do you support
39
+ structured financing offers (rate, duration, deposit, balloon, mileage, APR with legal disclosures) instead
40
+ of text?
41
+ 11. Feedback: do you provide an import report (accepted/rejected ads with reasons), and by which channel?
42
+ 12. **Test import.** Can you run a test import of our 20-ad sample (DEV data) before any production feed?
43
+
44
+ ## Images (ADV-323)
45
+
46
+ 13. Minimum / maximum number of images per ad, and is the order (main exterior 3/4 view first) kept?
47
+ 14. Accepted formats (JPEG / PNG / WebP), maximum file size, minimum/maximum pixel size, aspect ratios; do you
48
+ crop, resize or re-host?
49
+ 15. URL rules: maximum length, encoding, redirects allowed? How often do you re-fetch images, and how do you
50
+ handle a changed image under the same URL?
51
+ 16. Image rights, watermarks, dealer logos, marketing graphics or text overlays (e.g. rate badges): allowed or
52
+ not? (Our default is clean real photos only; overlays stay off without written approval.)
53
+ 17. Faces / licence plates: do you require blurring?
@@ -0,0 +1,64 @@
1
+ # AutoUncle feed — runbook (ADV-322 / ADV-323)
2
+
3
+ Not activated. Nothing below has been done on any Cronicle host.
4
+
5
+ ## Params (Cronicle text params of `3.0 Export to autouncle - part 1`)
6
+
7
+ | Param | Default | Meaning |
8
+ |---|---|---|
9
+ | `autouncleDryRun` | dry-run | only the exact text `false` makes a live run |
10
+ | `autouncleMaxImages` | no cut | cut the exported `images` list once AutoUncle gives a limit |
11
+ | `autouncleImageConcurrency` | 16 | parallel image URL checks |
12
+ | `feedGuardMinSourceItems` | 1000 | base snapshot size (shared by all feeds; DEV ~2,200) |
13
+ | `feedGuardMinPublishedItems` | 1 | only forbids an empty file — PROD exports a dealer allow-list, set a real minimum |
14
+ | `feedGuardMaxDropPercent` | 20 | max drop of source / ads against the last good run |
15
+ | `feedGuardMaxSourceAgeHours` | 26 | newest base export chunk must be younger |
16
+ | `feedGuardMaxStaleHours` | 48 | last good file reported STALE after this |
17
+ | `feedGuardAcceptDrop` | off | `true` for ONE run after a real stock drop |
18
+
19
+ `String` / `Number` rules stay as they are (PROD: dealer allow-list + `metaData.imagesCount:2`).
20
+
21
+ ## Files
22
+
23
+ | Mode | Output | State |
24
+ |---|---|---|
25
+ | dry-run | `resources/autouncle-dryrun/api_export_cars_autouncle.json` | `resources/autouncle-state/dryrun/` |
26
+ | live | `resources/autouncle-export/api_export_cars_autouncle.json` | `resources/autouncle-state/export/` |
27
+
28
+ State dir: `last-good.json`, `last-run-report.json` (every run, also blocked ones: guard code, exclusions,
29
+ dealer count, image stats), `run.lock`. A blocked run leaves `<json>.rejected`; the published JSON and
30
+ `<json>.prev` stay.
31
+
32
+ ## Exit codes and the Cronicle chain
33
+
34
+ Blocked or failed runs exit 1, so `3.1 … part 2` (copy to `htdocs/autouncle/`) does not run. DEV: `3.0` has no
35
+ `chain_error`, so a blocked run also stops the rest of the DEV chain — which today already stops at `3.1`
36
+ (copy target missing; all 18 runs in the DEV history failed there, latest 10.09.2026).
37
+
38
+ ## Before any activation (needs explicit OK)
39
+
40
+ 1. AutoUncle written schema + test import (PARTNER-QUESTIONS.md); adapt the mapping only inside
41
+ `transformers/autouncle.transformer.ts`.
42
+ 2. Decide hosting/transport: `htdocs/autouncle/` does not exist on DEV or PROD; the historic
43
+ `scheduler.auto.de` host does not answer.
44
+ 3. In dry-run mode `3.1` would copy whatever old file is in `autouncle-export/` (PROD: an 8-byte broken file) →
45
+ delete it or keep `3.1` disabled.
46
+ 4. Set `feedGuardMinPublishedItems` from a PROD dry-run.
47
+ 5. Version bump / publish / install on Cronicle — in turn with the meinestadt release.
48
+
49
+ ## Manual dry-run (as done 13.10.2026, locally)
50
+
51
+ DEV base snapshot `/opt/cronicle/resources/cars-export/cars.*.json` copied with mtimes (`scp -p`) to
52
+ `<run dir>/resources/cars-export/`; job input = DEV event `ek60tym7m0b` params (+ `feedGuardMaxSourceAgeHours`,
53
+ `feedGuardMinPublishedItems`), compiled branch code:
54
+
55
+ ```
56
+ node <compiled>/main.js < input.json # cwd = <run dir>; NODE_PATH must resolve `src/…` imports
57
+ ```
58
+
59
+ The job log echoes the input including the apikey — keep it local.
60
+
61
+ ## Rollback
62
+
63
+ Install the previous export-cars version on the Cronicle host. Dry-run output and state are separate from the
64
+ live path, so a dry-run never changes what `3.1` copies.
@@ -0,0 +1,146 @@
1
+ # meinestadt.de vehicle feed — audit (ADV-325 / ADV-326)
2
+
3
+ Status 13.10.2026, base `origin/deploy_develop` aa787932, `@bytebrand/export-cars` 1.26.31.
4
+ Historical documents are evidence only; nothing here is a confirmed current partner specification.
5
+
6
+ ## 1. Code and file contract (as built today)
7
+
8
+ | Item | Current code | Historical sources | Note |
9
+ |---|---|---|---|
10
+ | Export type | `ExportType.MEINSTADT = 'meinstadt'` | Confluence `Meinstadt` exporter | spelling without "e" everywhere in code |
11
+ | Files | `src/{exporters,transformers,formats,configs}/meinstadt.*.ts` | — | untouched since monorepo import (3046a15d, 12.06.2026) |
12
+ | Output dir | `resources/meinstadt-export/` | — | emptied at the start of every run |
13
+ | JSON file | `api_export_cars_meinstadt.json` | ACF-3985: `api_export_cars_meinestadt.json` | spellings differ; not normalized |
14
+ | ZIP file | `api_export_cars_meinstadt.zip` (entry = the JSON) | Confluence: `api_export_cars_meinstadt.zip` | the ZIP is built but the Cronicle upload step sends the **JSON**, not the ZIP |
15
+ | Envelope | bare JSON array `[ {...}, ... ]` | ACF-3985: `listings` array, "potentially optional" | no `{ "listings": [...] }` wrapper today |
16
+ | Sorting | base-export chunk order | — | not explicitly sorted |
17
+ | Fail-closed guard | none — a run with an empty source writes `[]` and the next step uploads it | — | see §4 |
18
+ | Live / dry-run | no notion of dry-run | — | every run writes the file the upload step reads |
19
+
20
+ ## 2. Cronicle (DEV, read-only check 13.10)
21
+
22
+ `auto-de-dev-cronicle-01`, export-cars 1.26.31 installed.
23
+
24
+ - `5.0 Export to Meintadt part1` (`ekiout5fl62`): plugin export-cars, `exportType=meinstadt`, `cpc=0.99`,
25
+ target `auto-de-dev-cronicle-01`, no timing, chained from `4.1 Export criteo - part 2`, chains to `5.1`.
26
+ - `5.1 Export menstadt part 2` (`ekipu4pcr01`): shell `ftp put` of
27
+ `/opt/cronicle/resources/meinstadt-export/api_export_cars_meinstadt.json` to an external FTP host
28
+ (185.15.211.90, user/password in plain text in the script), chains to `6.0 Export Ooyyo`.
29
+ Both events last modified 23.02.2023.
30
+ - No job history for either event, and `resources/meinstadt-export/` does not exist on this server:
31
+ the chain `2.0 fb → 2.1 → 3.0 autouncle → 3.1 → 4.0 criteo → …` stops at `3.1 Export to Autouncle part2`,
32
+ which fails every time (`cp: cannot create … /opt/cronicle/htdocs/autouncle/…`, last 10.09.2026).
33
+ So meinestadt has **never run** on the current DEV Cronicle.
34
+ - Risk: if the chain is repaired, DEV will push a DEV feed to the external FTP host without any guard.
35
+ - PROD state (schedule, last success, upload target) not checked yet — needs a separate read-only OK.
36
+
37
+ ## 3. Field matrix — 32 historical keys vs current code
38
+
39
+ Source: workbook `147qz9sT…`, tab `meinestadt` (gid 286164229), read 13.10.2026 via gviz.
40
+ DEV data: base-export snapshot 12.10.2026 15:00, 2,234 cars, 1,044 with `metaData.imagesCount > 0` (the
41
+ only ones any partner export sends).
42
+
43
+ | # | Key (as emitted) | Sheet type | Sheet source / note | Current code | DEV data | Finding |
44
+ |---|---|---|---|---|---|---|
45
+ | 1 | `id` | String | `metaData.vehicleId` | same | 1044/1044 string (6-char) | OK |
46
+ | 2 | `make` | String | `mainData.make` | same | OK | OK |
47
+ | 3 | `model` | String | `mainData.model` | same | placeholder `SONSTIGE` exists | sent as-is |
48
+ | 4 | `cpc` | Float | literal `0,08` | `config.cpc` param, default **`'0.08'`** (string) | DEV event `cpc=0.99` | **BUG (ADV-325)**: hidden historical default; emitted as string |
49
+ | 5 | `listingUrl` | String | `…/search/vehicle/<id>/?source=meineStadt&medium=cpc&campaign=listing` | `…/search/vehicle/<id>?source=classifieds&medium=listing&campaign=meinestadt` | — | UTM differs from sheet → ADV-327 (tracking, paused); unchanged |
50
+ | 6 | `title` | String | make & model & subModel | `${make} ${model} ${subModel}` | — | `SONSTIGE` / `undefined` possible; not changed now |
51
+ | 7 | `accidentFree` | Boolean | `mainData.accidentDamaged` | `accidentDamaged` **without inversion** | 697 `false`, 347 `null`, 0 `true` | **BUG (ADV-325)**: every known accident-free car is sent as `accidentFree:false` |
52
+ | 8 | `city` | String | "do not include this field" | not emitted | — | OK (comply) |
53
+ | 9 | `zip` | String | "do not include this field" | not emitted | — | OK |
54
+ | 10 | `latitude` | Float | "do not include this field" | not emitted | — | OK |
55
+ | 11 | `longitude` | Float | "do not include this field" | not emitted | — | OK |
56
+ | 12 | `mileage` | Number | `mainData.mileage` (km) | same | 3 null | OK |
57
+ | 13 | `initialRegistration` | **String** | `mainData.firstRegistration` (number) | raw value = **Unix seconds as number** | 888 number (2021-11 … 2026-09), 156 null (151 new cars) | type differs from sheet; a reader expecting ms sees 1970 → partner must confirm format |
58
+ | 14 | `offerCreationDate` | **String** | `metaData.creationDate` (number) | raw Unix seconds | 1044 number (2026-04-10 … 2026-10-09) | value is the CBD record creation date, the exporter never resets it; format as #13 |
59
+ | 15 | `numberOfDoors` | Number | `mainData.doors` | same | 12 null | OK |
60
+ | 16 | `numberOfSeats` | Number | `mainData.seats` | same | 5 null | OK |
61
+ | 17 | `numberOfPreviousOwners` | Number | `mainData.numberOfPreviousOwners` | same | 111 null | OK |
62
+ | 18 | `price` | Number | `price.currentSalesPrice` | same | 1044 number | OK — end-customer purchase price (EUR), not a rate, not `price.purchasePrice` (dealer cost) |
63
+ | 19 | `power` | Number | `engineData.powerPS`, "PS only" | `engineData.powerPS` | 1044; 6 cars where PS ≠ kW×1.36 (source data) | OK — PS, not kW |
64
+ | 20 | `co2Emissions` | Number | `environmentEmissions.co2` | same | OK | OK |
65
+ | 21 | `fuelConsumptionCombined` | Float | `consumption.consumptionCombined` | same | OK | OK |
66
+ | 22 | `images` | **String Array** | "put image url of main image … not the image id" | `carImages[0]` → a **single string** | — | **BUG (ADV-326)**: not an array; first entry of a placeholder-padded list |
67
+ | 23 | `equipment` | String Array / comma string (`"ABS, Klimaanlage, DAB"`) | `offer.offerDescription` | mobile.de description (markdown `**`, `\\`, dealer header/footer) | — | differs from sheet; partner question, not changed now |
68
+ | 24 | `color` | String (DE) | `exterior.exteriorColor` | `de[...]` | e.g. `Rot` | OK |
69
+ | 25 | `bodyType` | String (DE) | `mainData.category` | `de[...]` | 118 `selector_unknown` → **"Unbekannt"** | unknown sent as a value |
70
+ | 26 | `customerType/sellerType` | String | literal `Händler` | literal key `customerType/sellerType` = `Händler` | — | key name with slash is a mapping alternative in the sheet; code emits it literally → partner question |
71
+ | 27 | `emissionClass` | String (DE) | `environmentEmissions.emissionClass` | `de[...]` | 25 `selector_unknown` → "Unbekannt" | as #25 |
72
+ | 28 | `emissionSticker` | String (DE) | `environmentEmissions.emissionSticker` | `de[...]` | `Feinstaubplakette grün (4)` | OK |
73
+ | 29 | `fuelType` | String (DE) | `consumption.fuel` | `de[...]` | OK | OK |
74
+ | 30 | `transmissionType/transmission` | String (DE) | `driveSuspension.gearbox` | literal slash key | 1 unknown → "Unbekannt" | as #26 / #25 |
75
+ | 31 | `usageState` | String (DE) | Neu / Gebraucht | `de[condition]`, unknown condition → mileage ≥ 100 km = used | `Gebraucht` 654 / `Neu` 390 | OK |
76
+ | 32 | `imagesCount` | number | number of images | `carImages.length` | — | **BUG (ADV-326)**: counts the whole list incl. placeholders (see below) |
77
+
78
+ `MeinstadtJsonItem` also declares `powerPS`, `powerKS`, `transmissionType`, `sellerType` — never emitted.
79
+
80
+ ### Images today (DEV data, 1,044 cars)
81
+
82
+ `imageType = default`, `getImages(…, ignoreDefImages=false)`: the car's photos in shared sort order, padded
83
+ with the three `carimage/noimage/…` placeholders (404) whenever the car has fewer than 17 photos.
84
+
85
+ - 939 / 1,044 cars carry placeholders in the list; average list length 12.0.
86
+ - `images` = first entry only (a string), `imagesCount` = full list length → count ≠ 1 for all 1,044 cars.
87
+ - Clean profile (ADV-336 `getCleanImages`): average 7.0 real photos per car; 42 cars have no 3/4 exterior
88
+ hero and would be skipped (same 42 the OpenAI dry-run skips).
89
+
90
+ - Additional finding: the DEV Cronicle host has no `zip` binary (only `unzip`). `Zipper.zip` shells out to it,
91
+ so the legacy exporter would have failed at the ZIP step there (OOYYO/mobile/AS24/rubrikk use it too).
92
+
93
+ ## 4. Decisions in this branch (user OK 13.10.2026: "do everything, DEV only for now")
94
+
95
+ 1. `images` = array of all valid clean URLs (ADV-336 clean profile + `ImageUrlValidator`, same selection
96
+ rules as the OpenAI feed: valid 3/4 hero first, then the other valid views), `imagesCount` = its length.
97
+ Cars without a valid hero are skipped and counted. The Imbo gallery itself is not changed.
98
+ `meinstadtMaxImages` cuts the exported list only (default: no cut) — for a partner one-image rule.
99
+ The image list is produced behind one function (`resolveImages` dep → `toMeinstadtImages`): the planned
100
+ meinestadt marketing image designs will plug in there; `imagesCount` follows whatever list is exported.
101
+ 2. `accidentFree`: `accidentDamaged === false → true`, `true → false`, anything else → `null` (unknown).
102
+ 3. `cpc`: no default. Live mode (`meinstadtDryRun=false`) refuses to start without a positive decimal `cpc`.
103
+ Dry-run without one sends `cpc: null`.
104
+ 4. Dry-run is the default: `resources/meinstadt-dryrun/` + `resources/meinstadt-state/dryrun/`. Live keeps
105
+ the historical `resources/meinstadt-export/` that the Cronicle upload step reads.
106
+ 5. Shared `FeedGuard` (ADV-335): empty / partial / stale source, minimum counts and drops > 20 % against the
107
+ last good run block the run; temp file + atomic rename; last good JSON/ZIP stay in place; one run per
108
+ state dir (lock). A blocked/failed run exits 1 so the chained upload step does not run.
109
+ 6. ZIP built in-process (adm-zip 0.5.5, already in the tree via @bytebrand/zipper) from the published JSON,
110
+ one entry `api_export_cars_meinstadt.json`.
111
+ 7. Dates stay Unix seconds (historical output); 0 / negative / unparsable / missing → `null`, never 1970.
112
+ `offerCreationDate` is the CBD record creation date, passed through unchanged.
113
+ 8. `selector_unknown` and unmapped selectors → `null` instead of "Unbekannt".
114
+ 9. Not changed (partner questions): file names, envelope, slash keys, date format, `equipment`, `listingUrl`
115
+ (UTM → ADV-327), `title`.
116
+
117
+ ## 5. Results
118
+
119
+ Tests: `npm test` 96 → 116 checks green (`test/meinstadt-mapping.test.ts`, `test/meinstadt-feed-run.test.ts`).
120
+
121
+ Other feeds: `tsc` of `aa787932` vs the branch — of 138 compiled `.js` files only `meinstadt.config.js`,
122
+ `meinstadt.exporter.js`, `meinstadt.transformer.js` differ and `services/meinstadt-feed-run.js` is new; the other
123
+ 135 (all other exporters/transformers/builders, `images.service`, `feed-guard`, `car.service`, …) are
124
+ byte-identical, so Google, Meta (fb / facebookshop), Criteo, AutoUncle, OOYYO, Awin, Trovit, … output cannot change.
125
+ `images-regression.test.ts` (golden image URLs of all existing partner feeds) still passes.
126
+
127
+ Dry-run on the DEV snapshot (12.10.2026 15:00), real image checks against images.autode-dev.de, real UPA:
128
+
129
+ | | before (legacy code, computed) | after |
130
+ |---|---|---|
131
+ | source cars | 2,234 | 2,234 |
132
+ | items | 1,044 | 1,002 (42 without a valid 3/4 hero skipped) |
133
+ | `images` | 1 string, placeholders possible | array, 1–28 URLs, 7,259 / 7,259 URLs valid, 0 placeholders |
134
+ | `imagesCount` ≠ exported URLs | 1,044 | 0 |
135
+ | `accidentFree` | `false` 697, `null` 347 | `true` 676, `null` 326 |
136
+ | "Unbekannt" values | yes | none |
137
+ | runtime | — | ~5 min (image checks, concurrency 16) |
138
+
139
+ 20-car sample (ZIP, JSON, report, checklist, gallery preview): `meinestadt-sample/` in the worktree, not committed.
140
+
141
+ Not fixed, seen in the data: placeholder model `SONSTIGE` in `model`/`title`; `equipment` carries the auto.de
142
+ marketing header/footer of the mobile.de description ("über 180.000 Autos …") and mobile.de markup.
143
+
144
+ ## 6. Open partner questions
145
+
146
+ See `PARTNER-QUESTIONS.md`.
@@ -0,0 +1,54 @@
1
+ # Open questions for meinestadt.de (Oliver Blank, oliver.blank@meinestadt.de)
2
+
3
+ Draft for Marketing / Commercial. Not sent. Historical documents (ACF-3985 workbook, Confluence) are evidence
4
+ only — we need the CURRENT automotive specification in writing.
5
+
6
+ ## Account, contract, billing
7
+
8
+ 1. Who is the current owner of the automotive (vehicle listings) feed on your side — business and technical
9
+ contact?
10
+ 2. Is the partner account `AUTODE` still active, or does the feed need a new contract / reactivation?
11
+ 3. What is the current CPC tariff (amount, currency, net/gross, caps/budgets, cancellation terms)? Should the
12
+ feed carry the CPC per listing at all, and if yes, must it equal the contracted tariff?
13
+ 4. How are clicks reported and billed today (report format, frequency, recipient)? (Accounting will reconcile
14
+ the 2025 invoice correspondence separately.)
15
+
16
+ ## Feed format
17
+
18
+ 5. Please send the current automotive feed specification (JSON schema or field list with types, mandatory
19
+ fields and accepted enum values). Is our historical 32-field format still accepted?
20
+ 6. File and container: JSON inside a ZIP, plain JSON, or another format? Required file name
21
+ (`api_export_cars_meinestadt.json` vs `api_export_cars_meinstadt.json`)? Top-level array or
22
+ `{ "listings": [...] }`?
23
+ 7. Field names: are `transmissionType/transmission` and `customerType/sellerType` literal keys, or should we
24
+ send `transmissionType` / `customerType` (or `transmission` / `sellerType`)?
25
+ 8. Date format for `initialRegistration` and `offerCreationDate`: we send Unix seconds (number). Do you
26
+ expect seconds, milliseconds, ISO 8601 (`2025-06-25`) or `MM/YYYY`? What should a new car without first
27
+ registration contain (omit, null, empty)?
28
+ 9. `accidentFree`: we send `true` (no accident damage), `false` (accident damage) or `null` (unknown). Is `null`
29
+ accepted, or should the field be omitted when unknown?
30
+ 10. `equipment`: a list of features (`"ABS, Klimaanlage, DAB"` or an array) or free text? Maximum length?
31
+ 11. Enumerations: which German values do you accept for `color`, `bodyType`, `fuelType`,
32
+ `transmissionType`, `emissionClass`, `emissionSticker`, `usageState`, `customerType`?
33
+ 12. `power`: confirm PS (not kW). `price`: confirm gross EUR purchase price.
34
+ 13. Location: our historical mapping says not to send `city`, `zip`, `latitude`, `longitude`. Still correct?
35
+ 14. `listingUrl`: required tracking parameters (historical: `source=meineStadt&medium=cpc&campaign=listing`)?
36
+
37
+ ## Images
38
+
39
+ 15. Do you accept several images per listing (`images` array) or only one main image? Maximum count? Is the
40
+ order significant (first = main image)?
41
+ 16. Image requirements: formats (JPEG/PNG/WebP), minimum/maximum pixel size, aspect ratio, maximum file
42
+ size, maximum URL length?
43
+ 17. Do you download/cache images or hot-link them? Crawler user agent and IP ranges (for our CDN/WAF), retry
44
+ behaviour, how often images are re-fetched, and how removed/changed images are invalidated?
45
+ 18. Are text, logos, watermarks, price or financing-rate overlays allowed on images? Any image rights /
46
+ copyright statements required?
47
+
48
+ ## Delivery and operations
49
+
50
+ 19. Delivery path: FTP/SFTP upload (host, directory, credentials, IP allow-list), or should you pull from a
51
+ URL we host? The historical target was FTP host 185.15.211.90 — is it still valid?
52
+ 20. Update frequency and timing; full file each time or deltas? How do you remove listings that are no longer
53
+ in the file (sold / withdrawn), and how fast?
54
+ 21. Can we run a test import first and get the parsed item count and rejection report?
@@ -0,0 +1,64 @@
1
+ # meinestadt.de feed — runbook (ADV-325 / ADV-326)
2
+
3
+ ## Parameters (Cronicle event, export-cars plugin, `exportType=meinstadt`)
4
+
5
+ | Param | Default | Meaning |
6
+ |---|---|---|
7
+ | `meinstadtDryRun` | dry-run | only the exact text `false` makes a live run |
8
+ | `cpc` | none | tariff as a positive decimal with a dot (`0.99`). Required for live; no 0.08 / 0 fallback |
9
+ | `meinstadtMaxImages` | no cut | cut the exported `images` list (e.g. `1` if the partner accepts one main image) |
10
+ | `meinstadtImageConcurrency` | 16 | parallel image URL checks |
11
+ | `feedGuardMinSourceItems` | 1000 | DEV: ~2,200 source cars → keep or lower |
12
+ | `feedGuardMinPublishedItems` | 500 | DEV: ~1,000 items → keep or lower |
13
+ | `feedGuardMaxDropPercent` | 20 | max drop of source / items against the last good run |
14
+ | `feedGuardMaxSourceAgeHours` | 26 | newest base export chunk must be younger |
15
+ | `feedGuardMaxStaleHours` | 48 | last good file reported STALE after this |
16
+ | `feedGuardAcceptDrop` | off | `true` for ONE run after a real stock drop |
17
+
18
+ ## Files
19
+
20
+ | Mode | Output | State |
21
+ |---|---|---|
22
+ | dry-run | `resources/meinstadt-dryrun/api_export_cars_meinstadt.{json,zip}` | `resources/meinstadt-state/dryrun/` |
23
+ | live | `resources/meinstadt-export/api_export_cars_meinstadt.{json,zip}` | `resources/meinstadt-state/export/` |
24
+
25
+ State dir: `last-good.json` (counts of the last published run), `last-run-report.json` (every run, also
26
+ blocked ones: guard code, exclusions, accidentFree split, image stats), `run.lock`.
27
+ A blocked run leaves `<json>.rejected` for inspection; the published JSON/ZIP and `<json>.prev` stay.
28
+
29
+ ## Exit codes and the Cronicle chain
30
+
31
+ Blocked (guard) or failed runs and a live run without `cpc` exit 1, so the chained `5.1 … part 2` upload
32
+ does not run. Side effect in the DEV chain (`4.1 criteo → 5.0 meinstadt → 5.1 → 6.0 ooyyo → …`): the rest of
33
+ the chain stops too (no `chain_error` on 5.0). Today the DEV chain already stops earlier at
34
+ `3.1 Export to Autouncle part2` (fails since at least 25.08).
35
+
36
+ ## Before anything goes live (not done, needs explicit OK each)
37
+
38
+ 1. Partner answers (`PARTNER-QUESTIONS.md`), written acceptance of a test import; Commercial/Accounting OK.
39
+ 2. Cronicle `5.1 Export menstadt part 2` uploads the **JSON** from `resources/meinstadt-export/` by plain FTP to
40
+ 185.15.211.90 with credentials in the script. Before deploying this version where 5.1 is active:
41
+ - in dry-run mode 5.1 would upload whatever old file is still in `meinstadt-export/` → disable 5.1 or
42
+ remove the old file first;
43
+ - target host / protocol / JSON vs ZIP to be confirmed with the partner; credentials into a Cronicle secret.
44
+ 3. PROD Cronicle state (events, last success, existing files) not checked yet.
45
+
46
+ ## Manual DEV dry-run (as done 13.10.2026, locally)
47
+
48
+ Base export snapshot copied from the DEV Cronicle (`/opt/cronicle/resources/cars-export/cars.*.json`) to
49
+ `<run dir>/resources/cars-export/`, compiled branch code, job input = DEV event `ekiout5fl62` params:
50
+
51
+ ```
52
+ node <compiled>/main.js < input.json # cwd = <run dir>; NODE_PATH must resolve `src/…` imports
53
+ ```
54
+
55
+ ## Rollback
56
+
57
+ Install the previous export-cars version on the Cronicle host. Dry-run output and state dirs are separate
58
+ from the live path, so a dry-run never changes what the upload step reads.
59
+
60
+ ## Later: marketing images
61
+
62
+ The exported list comes from `resolveImages` in `MeinstadtExporter` (clean profile + validation →
63
+ `toMeinstadtImages`). A meinestadt marketing image profile replaces that function; `imagesCount` always
64
+ follows the exported list. Overlays/rate artwork only after written partner, Marketing and Legal OK (ADV-326).
@@ -182,6 +182,7 @@ Filters before mapping (all reasons counted in the run report):
182
182
  | Q11 | OAI-AdsBot / OAI-SearchBot reachability of `www.auto.de` VDP and `images.auto.de` behind Cloudflare (js_challenge rules from ADV-193). Test only, no CF change without approval. | Eng + approval | go-live |
183
183
  | Q12 | Image: ADV-336 clean profile is used (no overlay). Approval of plain photos + `ext_lr_main` question open in ADV-336 | ADV-336 | image_url |
184
184
  | Q13 | Is a "Stable" vs "Draft" schema selector visible in your docs/account? The public page shows none on 09.10. | Marketing | — |
185
+ | Q15 | Finance claims: closed 13.10 — no finance wording in the feed (`finance_claim` rule, RUNBOOK). Same dealer titles in Google/Meta feeds and on the CDP: separate ticket candidate. | — | done (1.26.33) |
185
186
  | Q14 | Make mapping at import (out of feed scope): DEV car titled "Jeep SWM G01a" — model SONSTIGE, variant names SWM (a different manufacturer), so the feed sends `brand=Jeep` for a non-Jeep car. Fix belongs to the import make/model mapping (sheet / CBD), not to the exporter. | Import owner | brand correctness |
186
187
 
187
188
  ---
@@ -230,7 +231,7 @@ fails the job itself (`exitError` + exit 1). The global fix is a separate task.
230
231
 
231
232
  Not done (next steps / human):
232
233
  - transport: hosted URL or SFTP, after Q10;
233
- - Cronicle event: DEV dry-run first;
234
+ - Cronicle event: DEV event `emv4ycd4g2y` exists (dry-run, manual runs; no schedule / failure e-mail on DEV by decision, closed 12.10); PROD event not created;
234
235
  - a real DEV/PROD dry-run plus the 20-car reconciliation;
235
236
  - delta PATCH: deferred, needs API access;
236
237
  - crawler access, per ADV-336: robots.txt has `Disallow: /search/` for OAI-AdsBot, plus the Cloudflare rules and the SPA shell without SSR on `/search*`.
@@ -34,10 +34,15 @@ State dir: `ledger.json` (items sent in_stock, for out_of_stock), `last-good.jso
34
34
  No transport is implemented yet. Nothing leaves the server until a hosted-URL location or an SFTP upload is set
35
35
  up for the **live** output only (separate, approved step).
36
36
 
37
- ## Cronicle event (to be created by an admin — not done)
37
+ ## Cronicle event
38
38
 
39
- - Plugin "Export cars", chained after "1.0 Base export" (same as the Google/Meta exports), daily.
39
+ **DEV (exists, closed 12.10):** event `openai_ads` = `emv4ycd4g2y`, dry-run with the default params, manual runs
40
+ only. By user decision it has no schedule and no failure e-mail — nothing to configure on DEV.
41
+
42
+ **PROD (not created):** to be set up by an admin once the go-live gate is passed:
43
+ - Plugin "Export cars", run daily after "1.0 Base export" (own timing about 1 h after it, or chained).
40
44
  Items expire at OpenAI after 2 weeks without refresh — never rely on manual uploads.
45
+ - Failure notification (e-mail) on — a blocked run is the only signal that the feed is not refreshed.
41
46
  - Concurrency 1 (the job also takes `run.lock`).
42
47
  - Params (all text; unset = default):
43
48
 
@@ -89,6 +94,25 @@ running after a failure as before.
89
94
  Only `openai_ads` is protected: the feed guard keeps the last good file, and the job exits with code 1
90
95
  (`exitError` + exit 1 in `openai-ads.exporter.ts`). It has no chain, so this stops nothing else.
91
96
 
97
+ ## No finance claims (`finance_claim`, since 1.26.33)
98
+
99
+ OpenAI's ad policy lists "Auto loans and leasing" outside the US as "Restricted to Select Approved Advertisers"
100
+ (category table in help.openai.com/en/articles/20001534-troubleshooting-common-onboarding-and-policy-issues;
101
+ openai.com/policies/ad-policies). The ADV-335/336 launch gate (13.10) therefore allows no finance wording in the feed.
102
+
103
+ - Source: only the dealer variant text (`mainData.subModel`), e.g. "GLB 200 4,49%Finz./Progressive",
104
+ "Tourneo Custom L1 0% Zins". The description is generated from structured fields and only repeats the title.
105
+ - `transformers/openai-ads-finance.ts`: before the title is normalised, rates (`x,xx %`, `0 %`) and finance words
106
+ (Finz./Finanzierung/finanzier*, Zins, Rate(n), Leasing, mtl./monatlich, `€/Monat`, Anzahlung, Schlussrate, Sollzins,
107
+ effektiver Jahreszins, Kredit, Darlehen, Santander) are cut. A percentage next to SoH/Akku/Batterie/elektrisch/
108
+ Schaden is kept ("99% SoH", "0% Schaden").
109
+ - Fail-closed: if a broader check still finds finance wording in title or description, the car is not sent
110
+ (`excluded.finance_claim`). Cleaned in_stock rows are counted as `cleaned.finance_terms_removed`.
111
+ - Rows re-sent from the ledger as out_of_stock are cleaned the same way (brand only if wording is left).
112
+ - DEV 12.10 (1.26.31): 64 of 1,002 titles had such wording (60 Mercedes "4,49%"/"5,99%"/"Finz.", 4 Ford "0% Zins").
113
+ - Out of scope, candidate for a separate ticket: the same dealer titles go into the Google/Meta feeds and are shown
114
+ on the CDP.
115
+
92
116
  ## Known source-data issues (not fixed by the feed)
93
117
 
94
118
  - Wrong make from the import mapping: e.g. DEV "Jeep SWM G01a" (model SONSTIGE, the variant names another
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytebrand/export-cars",
3
- "version": "1.26.31",
3
+ "version": "1.26.33",
4
4
  "description": "",
5
5
  "author": "bytebrand",
6
6
  "license": "UNLICENSED",
@@ -11,7 +11,7 @@
11
11
  "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
12
12
  "lint": "eslint -c .eslintrc.js --ext .ts src/",
13
13
  "start:dev": "nest start --watch",
14
- "test": "ts-node --transpile-only test/google-feed-envkv.test.ts && ts-node --transpile-only test/google-feed-item-row.test.ts && ts-node --transpile-only test/publication-gate.test.ts && ts-node --transpile-only test/images-regression.test.ts && ts-node --transpile-only test/clean-images.test.ts && ts-node --transpile-only test/image-url-validator.test.ts && ts-node --transpile-only test/openai-image-fields.test.ts && ts-node --transpile-only test/openai-ads-mapping.test.ts && ts-node --transpile-only test/openai-ads-feed-run.test.ts"
14
+ "test": "ts-node --transpile-only test/google-feed-envkv.test.ts && ts-node --transpile-only test/google-feed-item-row.test.ts && ts-node --transpile-only test/publication-gate.test.ts && ts-node --transpile-only test/images-regression.test.ts && ts-node --transpile-only test/clean-images.test.ts && ts-node --transpile-only test/image-url-validator.test.ts && ts-node --transpile-only test/openai-image-fields.test.ts && ts-node --transpile-only test/openai-ads-mapping.test.ts && ts-node --transpile-only test/openai-ads-feed-run.test.ts && ts-node --transpile-only test/openai-ads-finance.test.ts && ts-node --transpile-only test/meinstadt-mapping.test.ts && ts-node --transpile-only test/meinstadt-feed-run.test.ts && ts-node --transpile-only test/autouncle-mapping.test.ts && ts-node --transpile-only test/autouncle-feed-run.test.ts"
15
15
  },
16
16
  "dependencies": {
17
17
  "@bytebrand/autode-request": "^1.19.2",
@@ -29,6 +29,7 @@
29
29
  "@nestjs/cache-manager": "^2.1.0",
30
30
  "@nestjs/common": "^10.2.7",
31
31
  "@nestjs/core": "^10.2.7",
32
+ "adm-zip": "0.5.5",
32
33
  "axios": "^1.5.1",
33
34
  "bottleneck": "^2.19.5",
34
35
  "cache-manager": "^5.2.4",