@bytebrand/export-cars 1.26.30 → 1.26.32
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.
- package/README.md +12 -4
- package/dist/configs/autouncle.config.d.ts +14 -0
- package/dist/configs/autouncle.config.js +36 -3
- package/dist/configs/autouncle.config.js.map +1 -1
- package/dist/configs/meinstadt.config.d.ts +15 -0
- package/dist/configs/meinstadt.config.js +42 -3
- package/dist/configs/meinstadt.config.js.map +1 -1
- package/dist/constants/autouncle.contants.d.ts +1 -3
- package/dist/constants/autouncle.contants.js +2 -4
- package/dist/constants/autouncle.contants.js.map +1 -1
- package/dist/exporters/autouncle.exporter.d.ts +3 -2
- package/dist/exporters/autouncle.exporter.js +45 -27
- package/dist/exporters/autouncle.exporter.js.map +1 -1
- package/dist/exporters/meinstadt.exporter.d.ts +3 -2
- package/dist/exporters/meinstadt.exporter.js +40 -23
- package/dist/exporters/meinstadt.exporter.js.map +1 -1
- package/dist/formats/autouncle-json.builder.js +1 -4
- package/dist/formats/autouncle-json.builder.js.map +1 -1
- package/dist/interfaces/export-item.interface.d.ts +8 -12
- package/dist/interfaces/params.interface.d.ts +6 -0
- package/dist/services/autouncle-feed-run.d.ts +66 -0
- package/dist/services/autouncle-feed-run.js +135 -0
- package/dist/services/autouncle-feed-run.js.map +1 -0
- package/dist/services/images.service.d.ts +4 -0
- package/dist/services/images.service.js +9 -3
- package/dist/services/images.service.js.map +1 -1
- package/dist/services/meinstadt-feed-run.d.ts +81 -0
- package/dist/services/meinstadt-feed-run.js +166 -0
- package/dist/services/meinstadt-feed-run.js.map +1 -0
- package/dist/transformers/autouncle.transformer.d.ts +38 -22
- package/dist/transformers/autouncle.transformer.js +156 -144
- package/dist/transformers/autouncle.transformer.js.map +1 -1
- package/dist/transformers/meinstadt.transformer.d.ts +17 -13
- package/dist/transformers/meinstadt.transformer.js +88 -61
- package/dist/transformers/meinstadt.transformer.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/autouncle/AUDIT.md +97 -0
- package/docs/autouncle/PARTNER-QUESTIONS.md +53 -0
- package/docs/autouncle/RUNBOOK.md +64 -0
- package/docs/meinestadt/AUDIT.md +146 -0
- package/docs/meinestadt/PARTNER-QUESTIONS.md +54 -0
- package/docs/meinestadt/RUNBOOK.md +64 -0
- package/docs/openai-ads/AUDIT.md +1 -0
- package/docs/openai-ads/RUNBOOK.md +19 -0
- package/package.json +3 -2
- package/src/configs/autouncle.config.ts +51 -3
- package/src/configs/meinstadt.config.ts +57 -2
- package/src/constants/autouncle.contants.ts +2 -3
- package/src/exporters/autouncle.exporter.ts +59 -37
- package/src/exporters/meinstadt.exporter.ts +53 -32
- package/src/formats/autouncle-json.builder.ts +2 -4
- package/src/interfaces/export-item.interface.ts +15 -12
- package/src/interfaces/params.interface.ts +10 -0
- package/src/services/autouncle-feed-run.ts +197 -0
- package/src/services/images.service.ts +15 -4
- package/src/services/meinstadt-feed-run.ts +240 -0
- package/src/transformers/autouncle.transformer.ts +217 -175
- package/src/transformers/meinstadt.transformer.ts +114 -68
- package/test/autouncle-feed-run.test.ts +256 -0
- package/test/autouncle-mapping.test.ts +225 -0
- package/test/clean-images.test.ts +26 -0
- package/test/fixtures/meinstadt.fixture.ts +87 -0
- package/test/meinstadt-feed-run.test.ts +288 -0
- package/test/meinstadt-mapping.test.ts +215 -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).
|
package/docs/openai-ads/AUDIT.md
CHANGED
|
@@ -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
|
+
| 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 |
|
|
185
186
|
|
|
186
187
|
---
|
|
187
188
|
|
|
@@ -77,6 +77,25 @@ up for the **live** output only (separate, approved step).
|
|
|
77
77
|
| `count_drop` | > N % fewer cars than the last good run | confirm the drop is real (CBD online count, dealer offboarding), then ONE run with `feedGuardAcceptDrop=true` |
|
|
78
78
|
| `locked` | another run holds `run.lock` (< 6 h) | wait; a lock older than 6 h is taken over automatically |
|
|
79
79
|
|
|
80
|
+
## Known risk: other export-cars jobs report failures as success (accepted, 12.10)
|
|
81
|
+
|
|
82
|
+
Every export-cars run except `openai_ads` ends with `{"complete":1,"code":0}`, even when the exporter threw:
|
|
83
|
+
nest-commander's default `serviceErrorHandler` only prints the error and `main.ts` then reports success. Cronicle
|
|
84
|
+
therefore treats a failed part 1 as successful and starts the chained part 2, which can upload an empty or
|
|
85
|
+
partial file (Google feed incident 24.09: a 169-byte feed was uploaded and fetched by Google). Decision
|
|
86
|
+
(user, 12.10): keep it — no global fix, no `chain_error` handlers, no Cronicle config change; the chains keep
|
|
87
|
+
running after a failure as before.
|
|
88
|
+
|
|
89
|
+
Only `openai_ads` is protected: the feed guard keeps the last good file, and the job exits with code 1
|
|
90
|
+
(`exitError` + exit 1 in `openai-ads.exporter.ts`). It has no chain, so this stops nothing else.
|
|
91
|
+
|
|
92
|
+
## Known source-data issues (not fixed by the feed)
|
|
93
|
+
|
|
94
|
+
- Wrong make from the import mapping: e.g. DEV "Jeep SWM G01a" (model SONSTIGE, the variant names another
|
|
95
|
+
manufacturer). The feed sends the stored make as `brand`; correct it in the import make/model mapping (AUDIT Q14).
|
|
96
|
+
- Handled in the feed since 1.26.30: placeholder model `SONSTIGE` is left out of title/description; UTF-8 read as
|
|
97
|
+
Latin-1/cp1252 upstream (`ü` → `ü`) is repaired.
|
|
98
|
+
|
|
80
99
|
## Rollback
|
|
81
100
|
|
|
82
101
|
- Bad content published: `cp <output>.prev <output>` (the previous good file), then disable the Cronicle event.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bytebrand/export-cars",
|
|
3
|
-
"version": "1.26.
|
|
3
|
+
"version": "1.26.32",
|
|
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/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",
|
|
@@ -1,12 +1,60 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { toNumber } from 'lodash';
|
|
2
|
+
|
|
3
|
+
import { ExportType, FileType, ImageType } from '../enums';
|
|
2
4
|
import { Params } from '../interfaces/params.interface';
|
|
5
|
+
import { FeedGuardOptions } from '../services/feed-guard';
|
|
3
6
|
import { Config } from './config';
|
|
4
7
|
|
|
8
|
+
const text = (value: unknown): string => (typeof value === 'string' ? value.trim() : '');
|
|
9
|
+
|
|
10
|
+
const positiveNumber = (value: unknown, fallback: number): number => {
|
|
11
|
+
const parsed = toNumber(text(value));
|
|
12
|
+
return text(value) && Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Guard defaults. The PROD job only exports a dealer allow-list, so the published count is far below
|
|
16
|
+
// the base snapshot: minPublishedItems only forbids an empty file — set a real minimum per job.
|
|
17
|
+
export const AUTOUNCLE_DEFAULTS = {
|
|
18
|
+
imageConcurrency: 16,
|
|
19
|
+
minSourceItems: 1000,
|
|
20
|
+
minPublishedItems: 1,
|
|
21
|
+
maxDropPercent: 20,
|
|
22
|
+
maxSourceAgeHours: 26,
|
|
23
|
+
maxStaleHours: 48,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* AutoUncle JSON feed (ADV-322/323): `{"ads":[...]}` with clean ADV-336 images.
|
|
28
|
+
* Dry-run is the default: the file goes to resources/autouncle-dryrun, which no Cronicle step reads.
|
|
29
|
+
* Only autouncleDryRun=false writes resources/autouncle-export (the path `3.1 … part 2` copies from).
|
|
30
|
+
* Each mode has its own guard state.
|
|
31
|
+
*/
|
|
5
32
|
export class AutouncleConfig extends Config {
|
|
33
|
+
dryRun: boolean;
|
|
34
|
+
imageConcurrency: number;
|
|
35
|
+
// 0 = no limit (no partner limit known)
|
|
36
|
+
maxImages: number;
|
|
37
|
+
stateDir: string;
|
|
38
|
+
feedGuard: FeedGuardOptions;
|
|
39
|
+
|
|
6
40
|
constructor(params: Params) {
|
|
7
41
|
super(params);
|
|
8
|
-
this.
|
|
9
|
-
this.
|
|
42
|
+
this.dryRun = text(params.autouncleDryRun).toLowerCase() !== 'false';
|
|
43
|
+
const mode = this.dryRun ? 'dryrun' : 'export';
|
|
44
|
+
this.pathesConfig.localConvertedDirectory = `resources/${ExportType.AUTOUNCLE}-${mode}`;
|
|
45
|
+
this.stateDir = `resources/${ExportType.AUTOUNCLE}-state/${mode}`;
|
|
46
|
+
this.imageType = ImageType.clean;
|
|
47
|
+
this.imageConcurrency = positiveNumber(params.autouncleImageConcurrency, AUTOUNCLE_DEFAULTS.imageConcurrency);
|
|
48
|
+
this.maxImages = Math.floor(positiveNumber(params.autouncleMaxImages, 0));
|
|
49
|
+
this.feedGuard = {
|
|
50
|
+
stateDir: this.stateDir,
|
|
51
|
+
minSourceItems: positiveNumber(params.feedGuardMinSourceItems, AUTOUNCLE_DEFAULTS.minSourceItems),
|
|
52
|
+
minPublishedItems: positiveNumber(params.feedGuardMinPublishedItems, AUTOUNCLE_DEFAULTS.minPublishedItems),
|
|
53
|
+
maxDropPercent: positiveNumber(params.feedGuardMaxDropPercent, AUTOUNCLE_DEFAULTS.maxDropPercent),
|
|
54
|
+
maxSourceAgeHours: positiveNumber(params.feedGuardMaxSourceAgeHours, AUTOUNCLE_DEFAULTS.maxSourceAgeHours),
|
|
55
|
+
maxStaleHours: positiveNumber(params.feedGuardMaxStaleHours, AUTOUNCLE_DEFAULTS.maxStaleHours),
|
|
56
|
+
acceptDrop: text(params.feedGuardAcceptDrop).toLowerCase() === 'true',
|
|
57
|
+
};
|
|
10
58
|
this.outputFiles = [
|
|
11
59
|
`${this.pathesConfig.localConvertedDirectory}/api_export_cars_${ExportType.AUTOUNCLE}.${FileType.JSON}`,
|
|
12
60
|
];
|