@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.
@@ -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` | — | 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 |
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 | OK |
65
- | 21 | `fuelConsumptionCombined` | Float | `consumption.consumptionCombined` | same | OK | 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,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytebrand/export-cars",
3
- "version": "1.26.43",
3
+ "version": "1.26.44",
4
4
  "description": "",
5
5
  "author": "bytebrand",
6
6
  "license": "UNLICENSED",
@@ -88,8 +88,9 @@ export interface MeinstadtJsonItem {
88
88
  price: number;
89
89
  // PS
90
90
  power: number;
91
- co2Emissions: number;
92
- fuelConsumptionCombined: number;
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}?source=classifieds&medium=listing&campaign=meinestadt`,
100
- title: `${car.mainData.make} ${car.mainData.model} ${car.mainData.subModel}`,
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
- co2Emissions: get(car, 'environmentEmissions.co2', null),
113
- fuelConsumptionCombined: get(car, 'consumption.consumptionCombined', null),
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=classifieds&medium=listing&campaign=meinestadt',
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,