@bytebrand/export-cars 1.26.42 → 1.26.44
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/dist/configs/autouncle.config.js +0 -22
- package/dist/configs/autouncle.config.js.map +1 -1
- package/dist/exporters/autouncle.exporter.js +0 -10
- package/dist/exporters/autouncle.exporter.js.map +1 -1
- package/dist/interfaces/export-item.interface.d.ts +2 -2
- package/dist/services/autouncle-feed-run.d.ts +0 -6
- package/dist/services/autouncle-feed-run.js +1 -11
- package/dist/services/autouncle-feed-run.js.map +1 -1
- package/dist/transformers/autouncle.transformer.d.ts +0 -3
- package/dist/transformers/autouncle.transformer.js +4 -7
- package/dist/transformers/autouncle.transformer.js.map +1 -1
- package/dist/transformers/meinstadt.transformer.d.ts +2 -0
- package/dist/transformers/meinstadt.transformer.js +13 -5
- package/dist/transformers/meinstadt.transformer.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/autouncle/AUDIT.md +103 -103
- package/docs/autouncle/RUNBOOK.md +0 -2
- package/docs/meinestadt/AUDIT.md +20 -4
- package/docs/meinestadt/PARTNER-QUESTIONS.md +1 -1
- package/package.json +1 -1
- package/src/configs/autouncle.config.ts +1 -26
- package/src/exporters/autouncle.exporter.ts +0 -12
- package/src/interfaces/export-item.interface.ts +3 -2
- package/src/services/autouncle-feed-run.ts +2 -19
- package/src/transformers/autouncle.transformer.ts +6 -13
- package/src/transformers/meinstadt.transformer.ts +21 -4
- package/test/autouncle-feed-run.test.ts +2 -25
- package/test/autouncle-mapping.test.ts +2 -21
- package/test/meinstadt-mapping.test.ts +35 -1
package/docs/autouncle/AUDIT.md
CHANGED
|
@@ -1,103 +1,103 @@
|
|
|
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` | stable across runs; since 1.26.41 (ADV-324) `?source=autouncle&medium=listing&campaign=listing&utm_source=autouncle&utm_medium=listing&utm_campaign=listing` | pending AutoUncle approval → 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 legacy fields; a BEV sent its kWh (CBD mirrors it into consumptionCombined) as fuel economy | since 1.26.41: the Google feed's EnVKV picks as plain numbers (WLTP first, legacy only as fallback; confirmed plug-in weighted); BEV: fuel `""`, CO₂ `0`;
|
|
62
|
-
| co2_effiency_class | environmentEmissions.wltpEnergyEfficiencyClass (ADV-187) | raw | same
|
|
63
|
-
| euro_emission_class / enviromental_sticker | environmentEmissions.* | raw selector keys | same (no schema to translate to) | → Q6 |
|
|
64
|
-
|
|
65
|
-
Car-level exclusions (new, counted in the report): `duplicate_id`, `filtered`, `no_id`, `no_dealer`,
|
|
66
|
-
`reserved_or_sold` (offer.offerAvailability reserved/sold), `no_valid_price`, `no_valid_image`.
|
|
67
|
-
|
|
68
|
-
## 4. Fixes on this branch
|
|
69
|
-
|
|
70
|
-
1. Builder wrote the first car twice (`{"ads":[A,A,B…`) — fixed, test.
|
|
71
|
-
2. Year 1970 for a null first registration (ACF-3717/3725 class of bug) — fixed, tests.
|
|
72
|
-
3. Price must be a valid positive end-customer price, otherwise the car is not sent.
|
|
73
|
-
4. Reserved / sold cars are not sent (absence = removal; partner semantics unknown → Q5).
|
|
74
|
-
5. Dealer address no longer blanked when the postal code table misses the place.
|
|
75
|
-
6. `engine_size` without a value is `""`, not `ccm`; unmapped selectors never drop a key.
|
|
76
|
-
7. Images: clean profile + validator, no placeholders, no WebP risk, hero first (ADV-323).
|
|
77
|
-
8. Fail-closed: no `emptyDir`; empty / partial / stale snapshot, empty result or a drop > 20 % against the
|
|
78
|
-
last good run → run blocked (exit 1, `3.1` copy does not run), last good file stays. Lock per state dir.
|
|
79
|
-
9. Dry-run default; live only with `autouncleDryRun=false`.
|
|
80
|
-
10. (1.26.41, 10.11) EnVKV:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
11. (1.26.41, 10.11) `back_link` parameters per ADV-324 (partner in `source`, identical utm pair).
|
|
85
|
-
|
|
86
|
-
## 5. Evidence
|
|
87
|
-
|
|
88
|
-
- `npm test`: 138 ok (116 existing + 14 mapping + 8 run).
|
|
89
|
-
- Other feeds byte-identical: `origin/deploy_develop` and this branch compiled with the same `node_modules`;
|
|
90
|
-
every emitted `.js` outside the AutoUncle files is identical (only `params.interface.d.ts` gains three
|
|
91
|
-
optional fields). Covers Google, FB/Meta, Criteo, meinestadt, OOYYO, OpenAI and the rest.
|
|
92
|
-
- DEV dry-run 13.10.2026 (local, DEV snapshot 12.10 15:00 CEST, DEV `3.0` params): 2234 source cars →
|
|
93
|
-
1002 ads, 370 dealers, excluded `filtered` 1190 / `no_valid_image` 42, images 7259/7259 ok, 0 placeholders,
|
|
94
|
-
0 × 1970, 0 duplicate links. Old exporter on the same snapshot: 1045 ads (first duplicated), 143 × 1970,
|
|
95
|
-
2820 placeholder URLs. 20-car sample in `autouncle-sample/` (worktree, not committed).
|
|
96
|
-
|
|
97
|
-
## 6. Not done / not decided
|
|
98
|
-
|
|
99
|
-
- No partner schema, no test import, no activation, no Cronicle change, no version bump / publish.
|
|
100
|
-
- `minPublishedItems` default is 1 (PROD exports an allow-list of 19 dealers; real size unknown) — set per job
|
|
101
|
-
before activation.
|
|
102
|
-
- `3.1` copy target `htdocs/autouncle/` does not exist on either Cronicle; hosting/transport is a partner question (Q4).
|
|
103
|
-
- Portrait / small images: the validator checks status + MIME only, no pixel size — waits for partner limits.
|
|
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` | stable across runs; since 1.26.41 (ADV-324) `?source=autouncle&medium=listing&campaign=listing&utm_source=autouncle&utm_medium=listing&utm_campaign=listing` | pending AutoUncle approval → 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 legacy fields; a BEV sent its kWh (CBD mirrors it into consumptionCombined) as fuel economy | since 1.26.41: the Google feed's EnVKV picks as plain numbers (WLTP first, legacy only as fallback; confirmed plug-in weighted); BEV: fuel `""`, CO₂ `0`; no filter, the stored values as in the Google / Meta feeds | units → Q6 |
|
|
62
|
+
| co2_effiency_class | environmentEmissions.wltpEnergyEfficiencyClass (ADV-187) | raw | same | → Q6 |
|
|
63
|
+
| euro_emission_class / enviromental_sticker | environmentEmissions.* | raw selector keys | same (no schema to translate to) | → Q6 |
|
|
64
|
+
|
|
65
|
+
Car-level exclusions (new, counted in the report): `duplicate_id`, `filtered`, `no_id`, `no_dealer`,
|
|
66
|
+
`reserved_or_sold` (offer.offerAvailability reserved/sold), `no_valid_price`, `no_valid_image`.
|
|
67
|
+
|
|
68
|
+
## 4. Fixes on this branch
|
|
69
|
+
|
|
70
|
+
1. Builder wrote the first car twice (`{"ads":[A,A,B…`) — fixed, test.
|
|
71
|
+
2. Year 1970 for a null first registration (ACF-3717/3725 class of bug) — fixed, tests.
|
|
72
|
+
3. Price must be a valid positive end-customer price, otherwise the car is not sent.
|
|
73
|
+
4. Reserved / sold cars are not sent (absence = removal; partner semantics unknown → Q5).
|
|
74
|
+
5. Dealer address no longer blanked when the postal code table misses the place.
|
|
75
|
+
6. `engine_size` without a value is `""`, not `ccm`; unmapped selectors never drop a key.
|
|
76
|
+
7. Images: clean profile + validator, no placeholders, no WebP risk, hero first (ADV-323).
|
|
77
|
+
8. Fail-closed: no `emptyDir`; empty / partial / stale snapshot, empty result or a drop > 20 % against the
|
|
78
|
+
last good run → run blocked (exit 1, `3.1` copy does not run), last good file stays. Lock per state dir.
|
|
79
|
+
9. Dry-run default; live only with `autouncleDryRun=false`.
|
|
80
|
+
10. (1.26.41, 10.11) EnVKV: the Google feed's picks (WLTP first). DEV dry-run 10.11: 1731 ads, 128 BEV with fuel ""
|
|
81
|
+
and CO₂ 0, the WLTP picks changed 338 fuel / 403 CO₂ values against the legacy fields.
|
|
82
|
+
(1.26.43, 11.11) The EnVKV C8 publication gate of 1.26.41/42 was removed (user decision): like the Google and Meta
|
|
83
|
+
jobs, the feed sends the stored values without parameters. No `publicationGate*` params on this job.
|
|
84
|
+
11. (1.26.41, 10.11) `back_link` parameters per ADV-324 (partner in `source`, identical utm pair).
|
|
85
|
+
|
|
86
|
+
## 5. Evidence
|
|
87
|
+
|
|
88
|
+
- `npm test`: 138 ok (116 existing + 14 mapping + 8 run).
|
|
89
|
+
- Other feeds byte-identical: `origin/deploy_develop` and this branch compiled with the same `node_modules`;
|
|
90
|
+
every emitted `.js` outside the AutoUncle files is identical (only `params.interface.d.ts` gains three
|
|
91
|
+
optional fields). Covers Google, FB/Meta, Criteo, meinestadt, OOYYO, OpenAI and the rest.
|
|
92
|
+
- DEV dry-run 13.10.2026 (local, DEV snapshot 12.10 15:00 CEST, DEV `3.0` params): 2234 source cars →
|
|
93
|
+
1002 ads, 370 dealers, excluded `filtered` 1190 / `no_valid_image` 42, images 7259/7259 ok, 0 placeholders,
|
|
94
|
+
0 × 1970, 0 duplicate links. Old exporter on the same snapshot: 1045 ads (first duplicated), 143 × 1970,
|
|
95
|
+
2820 placeholder URLs. 20-car sample in `autouncle-sample/` (worktree, not committed).
|
|
96
|
+
|
|
97
|
+
## 6. Not done / not decided
|
|
98
|
+
|
|
99
|
+
- No partner schema, no test import, no activation, no Cronicle change, no version bump / publish.
|
|
100
|
+
- `minPublishedItems` default is 1 (PROD exports an allow-list of 19 dealers; real size unknown) — set per job
|
|
101
|
+
before activation.
|
|
102
|
+
- `3.1` copy target `htdocs/autouncle/` does not exist on either Cronicle; hosting/transport is a partner question (Q4).
|
|
103
|
+
- Portrait / small images: the validator checks status + MIME only, no pixel size — waits for partner limits.
|
|
@@ -15,8 +15,6 @@ Not activated. Nothing below has been done on any Cronicle host.
|
|
|
15
15
|
| `feedGuardMaxSourceAgeHours` | 26 | newest base export chunk must be younger |
|
|
16
16
|
| `feedGuardMaxStaleHours` | 48 | last good file reported STALE after this |
|
|
17
17
|
| `feedGuardAcceptDrop` | off | `true` for ONE run after a real stock drop |
|
|
18
|
-
| `publicationGate` | off | `audit` counts estimated EnVKV values, `enforce` sends them empty. **A live run needs `enforce`** (config error otherwise). enforce fails the run if the enrichment log cannot be read |
|
|
19
|
-
| `publicationGateBqProject` / `…Dataset` / `…Table` | — | CBD enrichment log table, same values as the Google job; required for audit / enforce |
|
|
20
18
|
|
|
21
19
|
`String` / `Number` rules stay as they are (PROD: dealer allow-list + `metaData.imagesCount:2`).
|
|
22
20
|
|
package/docs/meinestadt/AUDIT.md
CHANGED
|
@@ -46,8 +46,8 @@ only ones any partner export sends).
|
|
|
46
46
|
| 2 | `make` | String | `mainData.make` | same | OK | OK |
|
|
47
47
|
| 3 | `model` | String | `mainData.model` | same | placeholder `SONSTIGE` exists | sent as-is |
|
|
48
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` | — |
|
|
50
|
-
| 6 | `title` | String | make & model & subModel | `${make} ${model} ${subModel}` | — |
|
|
49
|
+
| 5 | `listingUrl` | String | `…/search/vehicle/<id>/?source=meineStadt&medium=cpc&campaign=listing` | `…/search/vehicle/<id>?source=classifieds&medium=listing&campaign=meinestadt` | — | **1.26.44**: `?source=meinestadt&medium=listing&campaign=listing` + identical `utm_*` (ADV-327, see §7) |
|
|
50
|
+
| 6 | `title` | String | make & model & subModel | `${make} ${model} ${subModel}` | — | **1.26.44**: missing parts left out (no `undefined`); `SONSTIGE` still sent as-is |
|
|
51
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
52
|
| 8 | `city` | String | "do not include this field" | not emitted | — | OK (comply) |
|
|
53
53
|
| 9 | `zip` | String | "do not include this field" | not emitted | — | OK |
|
|
@@ -61,8 +61,8 @@ only ones any partner export sends).
|
|
|
61
61
|
| 17 | `numberOfPreviousOwners` | Number | `mainData.numberOfPreviousOwners` | same | 111 null | OK |
|
|
62
62
|
| 18 | `price` | Number | `price.currentSalesPrice` | same | 1044 number | OK — end-customer purchase price (EUR), not a rate, not `price.purchasePrice` (dealer cost) |
|
|
63
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 |
|
|
65
|
-
| 21 | `fuelConsumptionCombined` | Float | `consumption.consumptionCombined` | same | OK |
|
|
64
|
+
| 20 | `co2Emissions` | Number | `environmentEmissions.co2` | same | OK | **1.26.44**: Google feed EnVKV pick (WLTP first, legacy fallback; BEV 0), null when none |
|
|
65
|
+
| 21 | `fuelConsumptionCombined` | Float | `consumption.consumptionCombined` | same | OK | **1.26.44**: Google feed EnVKV pick; BEV null (CBD mirrors kWh into `consumptionCombined`) |
|
|
66
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
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
68
|
| 24 | `color` | String (DE) | `exterior.exteriorColor` | `de[...]` | e.g. `Rot` | OK |
|
|
@@ -144,3 +144,19 @@ marketing header/footer of the mobile.de description ("über 180.000 Autos …")
|
|
|
144
144
|
## 6. Open partner questions
|
|
145
145
|
|
|
146
146
|
See `PARTNER-QUESTIONS.md`.
|
|
147
|
+
|
|
148
|
+
## 7. Round 2 — export-cars 1.26.44 (12.11.2026, DEV only)
|
|
149
|
+
|
|
150
|
+
Same follow-ups as the AutoUncle feed got in 1.26.41–1.26.43 (ADV-322/324), no parameters added:
|
|
151
|
+
|
|
152
|
+
- `co2Emissions` / `fuelConsumptionCombined`: the Google feed's EnVKV picks (`envkvCo2Value`,
|
|
153
|
+
`envkvFuelConsumptionValue`): WLTP first, legacy only as fallback, confirmed plug-in weighted; a BEV sends
|
|
154
|
+
no fuel consumption (`null`, CBD mirrors its kWh into `consumptionCombined`) and CO2 `0`. Nothing usable → `null`.
|
|
155
|
+
Stored values are sent like in the Google / Meta feeds (no publication gate).
|
|
156
|
+
- `listingUrl` (ADV-327): `?source=meinestadt&medium=listing&campaign=listing&utm_source=meinestadt&utm_medium=listing&utm_campaign=listing`.
|
|
157
|
+
Partner in `source` so the reporting views, the CRM source and GA4 all see meinestadt; no paid medium (CPC is a
|
|
158
|
+
contract attribute). web-funnel-tracking-health 1.5.5 books it (and the legacy `source=classifieds&campaign=meinestadt`
|
|
159
|
+
and the sheet's `source=meineStadt&medium=cpc`) as `CLASSIFIEDS_MEINESTADT`, family Classifieds. Pending meinestadt's OK.
|
|
160
|
+
- `title`: make / model / subModel without missing parts (was `"… undefined"` when subModel is missing).
|
|
161
|
+
- Unchanged: keys, order, file names, envelope, dry-run default, images (ADV-326 tests incl. 8 raw / 1 valid → `imagesCount` 1).
|
|
162
|
+
|
|
@@ -32,7 +32,7 @@ only — we need the CURRENT automotive specification in writing.
|
|
|
32
32
|
`transmissionType`, `emissionClass`, `emissionSticker`, `usageState`, `customerType`?
|
|
33
33
|
12. `power`: confirm PS (not kW). `price`: confirm gross EUR purchase price.
|
|
34
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`)?
|
|
35
|
+
14. `listingUrl`: required tracking parameters (historical: `source=meineStadt&medium=cpc&campaign=listing`)? DEV feed since 1.26.44 sends `source=meinestadt&medium=listing&campaign=listing` + the same `utm_*` values — OK for you?
|
|
36
36
|
|
|
37
37
|
## Images
|
|
38
38
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { toNumber } from 'lodash';
|
|
2
2
|
|
|
3
3
|
import { ExportType, FileType, ImageType } from '../enums';
|
|
4
|
-
import { Params
|
|
4
|
+
import { Params } from '../interfaces/params.interface';
|
|
5
5
|
import { FeedGuardOptions } from '../services/feed-guard';
|
|
6
6
|
import { Config } from './config';
|
|
7
7
|
|
|
@@ -55,33 +55,8 @@ export class AutouncleConfig extends Config {
|
|
|
55
55
|
maxStaleHours: positiveNumber(params.feedGuardMaxStaleHours, AUTOUNCLE_DEFAULTS.maxStaleHours),
|
|
56
56
|
acceptDrop: text(params.feedGuardAcceptDrop).toLowerCase() === 'true',
|
|
57
57
|
};
|
|
58
|
-
this.publicationGate = parsePublicationGate(params);
|
|
59
|
-
// ADV-322 "do not guess emissions": a live file never goes out with unchecked EnVKV estimates.
|
|
60
|
-
if (!this.dryRun && this.publicationGate?.mode !== 'enforce') {
|
|
61
|
-
throw new Error('a live AutoUncle run (autouncleDryRun=false) needs publicationGate=enforce');
|
|
62
|
-
}
|
|
63
58
|
this.outputFiles = [
|
|
64
59
|
`${this.pathesConfig.localConvertedDirectory}/api_export_cars_${ExportType.AUTOUNCLE}.${FileType.JSON}`,
|
|
65
60
|
];
|
|
66
61
|
}
|
|
67
62
|
}
|
|
68
|
-
|
|
69
|
-
// Same params and rules as the Google job (GoogleConfig): off by default; audit / enforce need the
|
|
70
|
-
// enrichment log table (publicationGateBqProject / Dataset / Table).
|
|
71
|
-
function parsePublicationGate(params: Params): PublicationGateConfig | undefined {
|
|
72
|
-
const mode = text(params.publicationGate).toLowerCase();
|
|
73
|
-
if (!mode || mode === 'off') return undefined;
|
|
74
|
-
if (mode !== 'audit' && mode !== 'enforce') {
|
|
75
|
-
throw new Error(`publicationGate must be off, audit or enforce, got "${mode}"`);
|
|
76
|
-
}
|
|
77
|
-
const gate = {
|
|
78
|
-
mode: mode as PublicationGateConfig['mode'],
|
|
79
|
-
project: text(params.publicationGateBqProject),
|
|
80
|
-
dataset: text(params.publicationGateBqDataset),
|
|
81
|
-
table: text(params.publicationGateBqTable),
|
|
82
|
-
};
|
|
83
|
-
if (!gate.project || !gate.dataset || !gate.table) {
|
|
84
|
-
throw new Error(`publicationGate=${mode} needs publicationGateBqProject, publicationGateBqDataset and publicationGateBqTable`);
|
|
85
|
-
}
|
|
86
|
-
return gate;
|
|
87
|
-
}
|
|
@@ -9,7 +9,6 @@ import { resolveOpenAiImageFields } from '../transformers/openai-image-fields';
|
|
|
9
9
|
import { CarService, RulesService, LoaderService, ConfigService, ImagesService } from '../services';
|
|
10
10
|
import { ImageUrlValidator } from '../services/image-url-validator.service';
|
|
11
11
|
import { FeedGuard } from '../services/feed-guard';
|
|
12
|
-
import { PublicationGate, loadPublicationGate } from '../services/publication-gate';
|
|
13
12
|
import { AutouncleFeedRun, AutouncleSourceCar } from '../services/autouncle-feed-run';
|
|
14
13
|
import { AutouncleConfig } from '../configs/autouncle.config';
|
|
15
14
|
import { Exporter } from '../interfaces/exporter.interface';
|
|
@@ -37,16 +36,6 @@ export class AutouncleExporter implements Exporter {
|
|
|
37
36
|
const transformer = await this.moduleRef.create(AutouncleTransformer);
|
|
38
37
|
const addresses = new Map<string, Promise<AutouncleAddress | null>>();
|
|
39
38
|
|
|
40
|
-
// EnVKV C8 (ADV-322): estimated values are withheld (enforce) or only counted (audit). enforce fails
|
|
41
|
-
// the job when the enrichment log cannot be read, so the last published file stays.
|
|
42
|
-
let gate: PublicationGate;
|
|
43
|
-
try {
|
|
44
|
-
gate = await loadPublicationGate(config.publicationGate);
|
|
45
|
-
} catch (error) {
|
|
46
|
-
LoggerService.exitError(`autouncle feed not published: ${error?.message ?? error}`);
|
|
47
|
-
process.exit(1);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
39
|
const run = new AutouncleFeedRun(
|
|
51
40
|
{
|
|
52
41
|
forEachChunk: fn => this.loaderService.process((chunk: FsChunkInterface) => fn(chunk.data as AutouncleSourceCar[])),
|
|
@@ -66,7 +55,6 @@ export class AutouncleExporter implements Exporter {
|
|
|
66
55
|
imageStats: () => validator.stats,
|
|
67
56
|
translate: key => transformer.translate(key),
|
|
68
57
|
guard: new FeedGuard(config.feedGuard),
|
|
69
|
-
gate,
|
|
70
58
|
},
|
|
71
59
|
{
|
|
72
60
|
runId: config.runId,
|
|
@@ -88,8 +88,9 @@ export interface MeinstadtJsonItem {
|
|
|
88
88
|
price: number;
|
|
89
89
|
// PS
|
|
90
90
|
power: number;
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
// EnVKV picks (as Google), null when nothing usable
|
|
92
|
+
co2Emissions: number | null;
|
|
93
|
+
fuelConsumptionCombined: number | null;
|
|
93
94
|
equipment: string;
|
|
94
95
|
color: string;
|
|
95
96
|
bodyType: string;
|
|
@@ -4,11 +4,8 @@ import { ImboImageResponseInterface } from '@bytebrand/chunkable';
|
|
|
4
4
|
import { DataSchemes } from '@bytebrand/car-schema';
|
|
5
5
|
|
|
6
6
|
import { AutouncleJsonBuilder } from '../formats/autouncle-json.builder';
|
|
7
|
-
import {
|
|
8
|
-
AUTOUNCLE_ENVKV_ATTRIBUTES, AutouncleAddress, AutouncleExclusion, getAutouncleExclusion, mapAutouncleItem,
|
|
9
|
-
} from '../transformers/autouncle.transformer';
|
|
7
|
+
import { AutouncleAddress, AutouncleExclusion, getAutouncleExclusion, mapAutouncleItem } from '../transformers/autouncle.transformer';
|
|
10
8
|
import { FeedGuard, FeedGuardError, LastGood, inspectSourceSnapshot } from './feed-guard';
|
|
11
|
-
import { PublicationGate } from './publication-gate';
|
|
12
9
|
|
|
13
10
|
export interface AutouncleSourceCar {
|
|
14
11
|
car: DataSchemes.Car;
|
|
@@ -29,8 +26,6 @@ export interface AutouncleRunDeps {
|
|
|
29
26
|
imageStats: () => Record<string, number>;
|
|
30
27
|
translate: (key: string) => string;
|
|
31
28
|
guard: FeedGuard;
|
|
32
|
-
// EnVKV C8 publication gate (CBD enrichment log); absent = off
|
|
33
|
-
gate?: Pick<PublicationGate, 'mode' | 'estimatedAttributes'>;
|
|
34
29
|
}
|
|
35
30
|
|
|
36
31
|
export interface AutouncleRunOptions {
|
|
@@ -59,8 +54,6 @@ export interface AutouncleRunReport {
|
|
|
59
54
|
excluded: Record<string, number>;
|
|
60
55
|
imagesPerItem: { min: number | null; max: number | null; total: number };
|
|
61
56
|
images: Record<string, number>;
|
|
62
|
-
// published items with an estimated EnVKV value per attribute; with gate enforce those were sent empty
|
|
63
|
-
envkv: { gate: 'off' | 'audit' | 'enforce'; estimated: Record<string, number> };
|
|
64
57
|
lastGood: LastGood | null;
|
|
65
58
|
lastGoodAgeHours: number | null;
|
|
66
59
|
stale: boolean;
|
|
@@ -100,7 +93,6 @@ export class AutouncleFeedRun {
|
|
|
100
93
|
excluded: {},
|
|
101
94
|
imagesPerItem: { min: null, max: null, total: 0 },
|
|
102
95
|
images: {},
|
|
103
|
-
envkv: { gate: this.deps.gate?.mode ?? 'off', estimated: {} },
|
|
104
96
|
lastGood,
|
|
105
97
|
lastGoodAgeHours: guard.staleHours(lastGood, startedAt),
|
|
106
98
|
stale: guard.isStale(lastGood, startedAt),
|
|
@@ -149,20 +141,11 @@ export class AutouncleFeedRun {
|
|
|
149
141
|
continue;
|
|
150
142
|
}
|
|
151
143
|
const images = await this.deps.resolveImages(source);
|
|
152
|
-
const
|
|
153
|
-
.filter(attribute => AUTOUNCLE_ENVKV_ATTRIBUTES.includes(attribute));
|
|
154
|
-
const mapped = mapAutouncleItem(source.car, {
|
|
155
|
-
domain: this.options.domain,
|
|
156
|
-
address,
|
|
157
|
-
images,
|
|
158
|
-
translate: this.deps.translate,
|
|
159
|
-
withheldEnvkv: this.deps.gate?.mode === 'enforce' ? estimated : [],
|
|
160
|
-
});
|
|
144
|
+
const mapped = mapAutouncleItem(source.car, { domain: this.options.domain, address, images, translate: this.deps.translate });
|
|
161
145
|
if (mapped.ok === false) {
|
|
162
146
|
increment(report.excluded, mapped.reason);
|
|
163
147
|
continue;
|
|
164
148
|
}
|
|
165
|
-
estimated.forEach(attribute => increment(report.envkv.estimated, attribute));
|
|
166
149
|
await builder.addItem(mapped.item, first);
|
|
167
150
|
first = false;
|
|
168
151
|
dealers.add(ownerId);
|
|
@@ -11,7 +11,7 @@ import { DEFAULT_REGION, MIN_MILEAGES } from '../constants/autouncle.contants';
|
|
|
11
11
|
import { AutouncleJsonItem } from '../interfaces';
|
|
12
12
|
import { OpenAiImageSelection } from './openai-image-fields';
|
|
13
13
|
import { toUnixSeconds } from './meinstadt.transformer';
|
|
14
|
-
import {
|
|
14
|
+
import { envkvCo2Value, envkvFuelConsumptionValue } from './google-feed-envkv';
|
|
15
15
|
|
|
16
16
|
// back_link parameters (ADV-324): the partner in `source`, so the reporting views, the CRM source and
|
|
17
17
|
// GA4 (utm_*) all see "autouncle". Legacy and utm pairs carry identical values (no parameterConflict).
|
|
@@ -19,9 +19,6 @@ import { GoogleEnvkvAttribute, envkvCo2Value, envkvFuelConsumptionValue } from '
|
|
|
19
19
|
export const AUTOUNCLE_LINK_PARAMS =
|
|
20
20
|
'source=autouncle&medium=listing&campaign=listing&utm_source=autouncle&utm_medium=listing&utm_campaign=listing';
|
|
21
21
|
|
|
22
|
-
// The EnVKV attributes the feed sends (fuel_economy, co2_emission); the gate knows more (range, kWh, …).
|
|
23
|
-
export const AUTOUNCLE_ENVKV_ATTRIBUTES: GoogleEnvkvAttribute[] = ['fuel_consumption', 'co2_emissions'];
|
|
24
|
-
|
|
25
22
|
export enum AutouncleExclusion {
|
|
26
23
|
duplicateId = 'duplicate_id',
|
|
27
24
|
filtered = 'filtered',
|
|
@@ -49,8 +46,6 @@ export interface AutouncleMappingContext {
|
|
|
49
46
|
images: string[];
|
|
50
47
|
// German label of a selector (TranslatorExtended.translateVal)
|
|
51
48
|
translate: (key: string) => string;
|
|
52
|
-
// EnVKV attributes whose value is a CBD estimate and must not be published (publication gate, enforce)
|
|
53
|
-
withheldEnvkv?: GoogleEnvkvAttribute[];
|
|
54
49
|
}
|
|
55
50
|
|
|
56
51
|
export type AutouncleMapResult = { ok: true; item: AutouncleJsonItem } | { ok: false; reason: AutouncleExclusion };
|
|
@@ -191,10 +186,6 @@ export const mapAutouncleItem = (car: DataSchemes.Car, ctx: AutouncleMappingCont
|
|
|
191
186
|
const sellerId = toString(get(car, 'metaData.ownerId', '')).trim();
|
|
192
187
|
const price = getAutounclePrice(get(car, 'price.currentSalesPrice', null));
|
|
193
188
|
const firstRegistration = get(car, 'mainData.firstRegistration', null);
|
|
194
|
-
// EnVKV (ADV-322): the WLTP picks of the Google feed (legacy fields only as their fallback), never an
|
|
195
|
-
// estimated value. The energy class is derived from CO2, so it goes with a withheld CO2 value.
|
|
196
|
-
const withheld = ctx.withheldEnvkv ?? [];
|
|
197
|
-
const co2Withheld = withheld.includes('co2_emissions');
|
|
198
189
|
|
|
199
190
|
return {
|
|
200
191
|
ok: true,
|
|
@@ -226,10 +217,12 @@ export const mapAutouncleItem = (car: DataSchemes.Car, ctx: AutouncleMappingCont
|
|
|
226
217
|
seller_id: sellerId,
|
|
227
218
|
description: toString(get(car, 'offer.offerDescription', '')),
|
|
228
219
|
images: ctx.images.map(url => ({ url })),
|
|
229
|
-
|
|
230
|
-
|
|
220
|
+
// EnVKV (ADV-322): the same values as the Google feed (WLTP first, legacy only as fallback; BEV: no
|
|
221
|
+
// fuel, CO2 0), as plain numbers.
|
|
222
|
+
fuel_economy: toString(envkvFuelConsumptionValue(car) ?? ''),
|
|
223
|
+
co2_emission: toString(envkvCo2Value(car) ?? ''),
|
|
231
224
|
// ADV-187: canonical CO2-derived WLTP class, not the unvalidated legacy NEFZ class.
|
|
232
|
-
co2_effiency_class:
|
|
225
|
+
co2_effiency_class: toString(get(car, 'environmentEmissions.wltpEnergyEfficiencyClass', '')),
|
|
233
226
|
euro_emission_class: toString(get(car, 'environmentEmissions.emissionClass', '')),
|
|
234
227
|
enviromental_sticker: toString(get(car, 'environmentEmissions.emissionSticker', '')),
|
|
235
228
|
},
|
|
@@ -9,6 +9,21 @@ import { de } from '@bytebrand/i18n-dictionaries';
|
|
|
9
9
|
import { UserService } from '../services';
|
|
10
10
|
import { MeinstadtJsonItem } from '../interfaces';
|
|
11
11
|
import { OpenAiImageSelection } from './openai-image-fields';
|
|
12
|
+
import { envkvCo2Value, envkvFuelConsumptionValue } from './google-feed-envkv';
|
|
13
|
+
|
|
14
|
+
// listingUrl parameters (ADV-327, same scheme as AutoUncle ADV-324): the partner in `source`, so the
|
|
15
|
+
// reporting views, the CRM source and GA4 (utm_*) all see "meinestadt". Legacy and utm pairs carry
|
|
16
|
+
// identical values; paid / unpaid (CPC) is a contract attribute and is not encoded in the medium.
|
|
17
|
+
export const MEINSTADT_LINK_PARAMS =
|
|
18
|
+
'source=meinestadt&medium=listing&campaign=listing&utm_source=meinestadt&utm_medium=listing&utm_campaign=listing';
|
|
19
|
+
|
|
20
|
+
// make, model, subModel joined; missing parts are left out (never the text "undefined" / "null").
|
|
21
|
+
export const getMeinstadtTitle = (car: DataSchemes.Car): string =>
|
|
22
|
+
['mainData.make', 'mainData.model', 'mainData.subModel']
|
|
23
|
+
.map(path => get(car, path, null))
|
|
24
|
+
.filter(part => typeof part === 'string' && part.trim())
|
|
25
|
+
.map(part => part.trim())
|
|
26
|
+
.join(' ');
|
|
12
27
|
|
|
13
28
|
/**
|
|
14
29
|
* The meinestadt `images` list from the shared validated clean selection (ADV-336, same rules as
|
|
@@ -96,8 +111,8 @@ export const mapMeinstadtItem = (car: DataSchemes.Car, ctx: MeinstadtMappingCont
|
|
|
96
111
|
make: get(car, 'mainData.make', null),
|
|
97
112
|
model: get(car, 'mainData.model', null),
|
|
98
113
|
cpc: ctx.cpc,
|
|
99
|
-
listingUrl: `https://www.${ctx.domain}/search/vehicle/${carId}
|
|
100
|
-
title:
|
|
114
|
+
listingUrl: `https://www.${ctx.domain}/search/vehicle/${carId}?${MEINSTADT_LINK_PARAMS}`,
|
|
115
|
+
title: getMeinstadtTitle(car),
|
|
101
116
|
accidentFree: toAccidentFree(get(car, 'mainData.accidentDamaged', null)),
|
|
102
117
|
mileage: get(car, 'mainData.mileage', null),
|
|
103
118
|
initialRegistration: toUnixSeconds(get(car, 'mainData.firstRegistration', null)),
|
|
@@ -109,8 +124,10 @@ export const mapMeinstadtItem = (car: DataSchemes.Car, ctx: MeinstadtMappingCont
|
|
|
109
124
|
price: get(car, 'price.currentSalesPrice', null),
|
|
110
125
|
// PS, not kW (workbook: "only put the value for PS")
|
|
111
126
|
power: get(car, 'engineData.powerPS', null),
|
|
112
|
-
|
|
113
|
-
|
|
127
|
+
// EnVKV: the same values as the Google / AutoUncle feeds (WLTP first, legacy only as fallback;
|
|
128
|
+
// BEV: no fuel consumption, CO2 0). Nothing usable -> null.
|
|
129
|
+
co2Emissions: envkvCo2Value(car) ?? null,
|
|
130
|
+
fuelConsumptionCombined: envkvFuelConsumptionValue(car) ?? null,
|
|
114
131
|
equipment: ctx.equipment,
|
|
115
132
|
color: translate(get(car, 'exterior.exteriorColor', null)),
|
|
116
133
|
bodyType: translate(get(car, 'mainData.category', null)),
|
|
@@ -7,7 +7,7 @@ import { tmpdir } from 'os';
|
|
|
7
7
|
import { join } from 'path';
|
|
8
8
|
|
|
9
9
|
import { FeedGuard, FeedGuardError, FeedGuardOptions } from '../src/services/feed-guard';
|
|
10
|
-
import { AutouncleFeedRun,
|
|
10
|
+
import { AutouncleFeedRun, AutouncleSourceCar } from '../src/services/autouncle-feed-run';
|
|
11
11
|
import { toAutouncleImages } from '../src/transformers/autouncle.transformer';
|
|
12
12
|
import { selectOpenAiImageFields } from '../src/transformers/openai-image-fields';
|
|
13
13
|
import { buildImagesService } from './fixtures/images-service.harness';
|
|
@@ -63,9 +63,7 @@ const guardOptions = (world: World, extra: Partial<FeedGuardOptions> = {}): Feed
|
|
|
63
63
|
...extra,
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const run = async (world: World, { at = NOW, guard = {}, gate }: { at?: Date; guard?: Partial<FeedGuardOptions>; gate?: Gate } = {}) => {
|
|
66
|
+
const run = async (world: World, { at = NOW, guard = {} }: { at?: Date; guard?: Partial<FeedGuardOptions> } = {}) => {
|
|
69
67
|
const forEachChunk = async (fn: (cars: AutouncleSourceCar[]) => Promise<void>) => {
|
|
70
68
|
const names = (await fs.readdir(world.source)).sort((a, b) => Number(a.split('.')[1]) - Number(b.split('.')[1]));
|
|
71
69
|
for (const name of names) {
|
|
@@ -87,7 +85,6 @@ const run = async (world: World, { at = NOW, guard = {}, gate }: { at?: Date; gu
|
|
|
87
85
|
imageStats: () => ({ ok: 1 }),
|
|
88
86
|
translate: key => key,
|
|
89
87
|
guard: new FeedGuard(guardOptions(world, guard)),
|
|
90
|
-
gate,
|
|
91
88
|
},
|
|
92
89
|
{
|
|
93
90
|
runId: 'test',
|
|
@@ -234,26 +231,6 @@ const tests: Array<[string, () => Promise<void>]> = [
|
|
|
234
231
|
assert.equal(await exists(world.json), false);
|
|
235
232
|
}],
|
|
236
233
|
|
|
237
|
-
['EnVKV gate: enforce sends estimated values empty, audit only counts them; off without a gate', async () => {
|
|
238
|
-
const world = await newWorld();
|
|
239
|
-
await writeSource(world, [source('v1'), source('v2')]);
|
|
240
|
-
// v1: the CO2 value it would send is a CBD estimate; the range is too, but the feed has no range key
|
|
241
|
-
const estimatedAttributes = (c: any): any[] => (c._id === 'v1' ? ['co2_emissions', 'electric_range'] : []);
|
|
242
|
-
|
|
243
|
-
const off = await run(world);
|
|
244
|
-
assert.deepEqual(off.envkv, { gate: 'off', estimated: {} });
|
|
245
|
-
|
|
246
|
-
const audit = await run(world, { at: new Date(NOW.getTime() + HOUR), gate: { mode: 'audit', estimatedAttributes } });
|
|
247
|
-
assert.deepEqual(audit.envkv, { gate: 'audit', estimated: { co2_emissions: 1 } });
|
|
248
|
-
assert.deepEqual((await readAds(world)).map((a: any) => a.co2_emission), ['137', '137']);
|
|
249
|
-
|
|
250
|
-
const enforce = await run(world, { at: new Date(NOW.getTime() + 2 * HOUR), gate: { mode: 'enforce', estimatedAttributes } });
|
|
251
|
-
assert.deepEqual(enforce.envkv, { gate: 'enforce', estimated: { co2_emissions: 1 } });
|
|
252
|
-
const ads = await readAds(world);
|
|
253
|
-
assert.deepEqual(ads.map((a: any) => [a.co2_emission, a.fuel_economy]), [['', '6.1'], ['137', '6.1']]);
|
|
254
|
-
assert.equal(ads.length, 2, 'a withheld value never drops the car');
|
|
255
|
-
}],
|
|
256
|
-
|
|
257
234
|
['second run with the same snapshot: byte-identical file (stable ids, order, links)', async () => {
|
|
258
235
|
const world = await newWorld();
|
|
259
236
|
await writeSource(world, [source('i1'), source('i2'), source('i3')]);
|