@bytebrand/export-cars 1.26.43 → 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/interfaces/export-item.interface.d.ts +2 -2
- 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/meinestadt/AUDIT.md +20 -4
- package/docs/meinestadt/PARTNER-QUESTIONS.md +1 -1
- package/package.json +1 -1
- package/src/interfaces/export-item.interface.ts +3 -2
- package/src/transformers/meinstadt.transformer.ts +21 -4
- package/test/meinstadt-mapping.test.ts +35 -1
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
|
@@ -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;
|
|
@@ -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)),
|
|
@@ -6,6 +6,8 @@ import { de } from '@bytebrand/i18n-dictionaries';
|
|
|
6
6
|
import { MeinstadtConfig, parseMeinstadtCpc } from '../src/configs/meinstadt.config';
|
|
7
7
|
import { Environment, ExportType, ImageType } from '../src/enums';
|
|
8
8
|
import {
|
|
9
|
+
MEINSTADT_LINK_PARAMS,
|
|
10
|
+
getMeinstadtTitle,
|
|
9
11
|
mapMeinstadtItem,
|
|
10
12
|
toAccidentFree,
|
|
11
13
|
toMeinstadtImages,
|
|
@@ -119,6 +121,38 @@ const tests: Array<[string, () => void | Promise<void>]> = [
|
|
|
119
121
|
assert.equal(mapMeinstadtItem(car('l2', { 'mainData.condition': 'selector_unknown', 'mainData.mileage': 5 }), ctx()).usageState, 'Neu');
|
|
120
122
|
}],
|
|
121
123
|
|
|
124
|
+
['listingUrl (ADV-327): partner in source, identical legacy + utm pairs, no paid medium, stable per car', () => {
|
|
125
|
+
assert.equal(MEINSTADT_LINK_PARAMS, 'source=meinestadt&medium=listing&campaign=listing&utm_source=meinestadt&utm_medium=listing&utm_campaign=listing');
|
|
126
|
+
const a = mapMeinstadtItem(car('car-a'), ctx()).listingUrl;
|
|
127
|
+
assert.equal(a, `https://www.auto.de/search/vehicle/car-a?${MEINSTADT_LINK_PARAMS}`);
|
|
128
|
+
assert.equal(mapMeinstadtItem(car('car-a'), ctx()).listingUrl, a);
|
|
129
|
+
assert.notEqual(mapMeinstadtItem(car('car-b'), ctx()).listingUrl, a);
|
|
130
|
+
assert.ok(!/medium=cpc|source=classifieds/.test(a));
|
|
131
|
+
}],
|
|
132
|
+
|
|
133
|
+
['EnVKV: WLTP picks of the Google feed, legacy only as fallback; BEV no fuel, CO2 0; 0 / missing -> null', () => {
|
|
134
|
+
const wltp = mapMeinstadtItem(car('e1', { 'consumption.wltpCombined': 5.8, 'environmentEmissions.wltpCo2': 132 }), ctx());
|
|
135
|
+
assert.deepEqual([wltp.fuelConsumptionCombined, wltp.co2Emissions], [5.8, 132]);
|
|
136
|
+
// fixture has only the legacy fields (6.1 l, 137 g) -> sent as before
|
|
137
|
+
const legacy = mapMeinstadtItem(car('e2'), ctx());
|
|
138
|
+
assert.deepEqual([legacy.fuelConsumptionCombined, legacy.co2Emissions], [6.1, 137]);
|
|
139
|
+
// CBD mirrors kWh into consumptionCombined for BEVs: never sent as fuel consumption
|
|
140
|
+
const bev = mapMeinstadtItem(car('e3', { 'consumption.fuel': 'selector_fuel_electrical', 'consumption.consumptionCombined': 16.5, 'environmentEmissions.co2': null }), ctx());
|
|
141
|
+
assert.deepEqual([bev.fuelConsumptionCombined, bev.co2Emissions], [null, 0]);
|
|
142
|
+
const none = mapMeinstadtItem(car('e4', { 'consumption.consumptionCombined': 0, 'environmentEmissions.co2': null }), ctx());
|
|
143
|
+
assert.deepEqual([none.fuelConsumptionCombined, none.co2Emissions], [null, null]);
|
|
144
|
+
}],
|
|
145
|
+
|
|
146
|
+
['title: make model subModel, missing parts left out (never "undefined" / "null")', () => {
|
|
147
|
+
assert.equal(getMeinstadtTitle(car('t1')), 'Opel Astra 1.2 Turbo Edition');
|
|
148
|
+
assert.equal(getMeinstadtTitle(car('t2', { 'mainData.subModel': null })), 'Opel Astra');
|
|
149
|
+
assert.equal(getMeinstadtTitle(car('t3', { 'mainData.subModel': ' ' })), 'Opel Astra');
|
|
150
|
+
const noSub = car('t4');
|
|
151
|
+
delete noSub.mainData.subModel;
|
|
152
|
+
assert.equal(mapMeinstadtItem(noSub, ctx()).title, 'Opel Astra');
|
|
153
|
+
assert.equal(getMeinstadtTitle(car('t5', { 'mainData.subModel': ' 1.5 TSI ' })), 'Opel Astra 1.5 TSI');
|
|
154
|
+
}],
|
|
155
|
+
|
|
122
156
|
['item: historical keys and order, slash keys kept literally, no city/zip/lat/long', () => {
|
|
123
157
|
const item = mapMeinstadtItem(car('k1'), ctx({ images: ['https://x/1.jpeg', 'https://x/2.jpeg'] }));
|
|
124
158
|
assert.deepEqual(Object.keys(item), [
|
|
@@ -133,7 +167,7 @@ const tests: Array<[string, () => void | Promise<void>]> = [
|
|
|
133
167
|
make: 'Opel',
|
|
134
168
|
model: 'Astra',
|
|
135
169
|
cpc: '0.99',
|
|
136
|
-
listingUrl: 'https://www.auto.de/search/vehicle/k1?source=
|
|
170
|
+
listingUrl: 'https://www.auto.de/search/vehicle/k1?source=meinestadt&medium=listing&campaign=listing&utm_source=meinestadt&utm_medium=listing&utm_campaign=listing',
|
|
137
171
|
title: 'Opel Astra 1.2 Turbo Edition',
|
|
138
172
|
accidentFree: true,
|
|
139
173
|
mileage: 13000,
|