@bytebrand/export-cars 1.26.20 → 1.26.22
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/fb.transformer.js +2 -2
- package/dist/transformers/fb.transformer.js.map +1 -1
- package/dist/transformers/google-feed-envkv.d.ts +5 -0
- package/dist/transformers/google-feed-envkv.js +56 -0
- package/dist/transformers/google-feed-envkv.js.map +1 -0
- package/dist/transformers/google.transformer.d.ts +0 -2
- package/dist/transformers/google.transformer.js +7 -28
- package/dist/transformers/google.transformer.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +3 -2
- package/src/interfaces/export-item.interface.ts +2 -2
- package/src/transformers/fb.transformer.ts +5 -2
- package/src/transformers/google-feed-envkv.ts +63 -0
- package/src/transformers/google.transformer.ts +7 -29
- package/test/google-feed-envkv.test.ts +71 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bytebrand/export-cars",
|
|
3
|
-
"version": "1.26.
|
|
3
|
+
"version": "1.26.22",
|
|
4
4
|
"description": "",
|
|
5
5
|
"author": "bytebrand",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"prepublishOnly": "npm run build",
|
|
11
11
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
12
12
|
"lint": "eslint -c .eslintrc.js --ext .ts src/",
|
|
13
|
-
"start:dev": "nest start --watch"
|
|
13
|
+
"start:dev": "nest start --watch",
|
|
14
|
+
"test": "ts-node --transpile-only test/google-feed-envkv.test.ts"
|
|
14
15
|
},
|
|
15
16
|
"dependencies": {
|
|
16
17
|
"@bytebrand/autode-request": "^1.19.2",
|
|
@@ -199,11 +199,11 @@ export interface GoogleXmlItem {
|
|
|
199
199
|
doors?: number;
|
|
200
200
|
capacity?: number;
|
|
201
201
|
date_first_registered: string,
|
|
202
|
-
fuel_consumption
|
|
202
|
+
fuel_consumption?: string,
|
|
203
203
|
energy_consumption?: string,
|
|
204
204
|
electric_range?: string,
|
|
205
205
|
fuel_consumption_discharged_battery?: string,
|
|
206
|
-
co2_emissions
|
|
206
|
+
co2_emissions?: string,
|
|
207
207
|
identifier_exists: string,
|
|
208
208
|
ads_redirect?: string;
|
|
209
209
|
"certified_pre-owned"?: boolean;
|
|
@@ -413,6 +413,9 @@ export class FbTransformer {
|
|
|
413
413
|
return get(car, 'state.isOnline', false) ? 'available' : 'not_available';
|
|
414
414
|
}
|
|
415
415
|
|
|
416
|
+
// ADV-187: no fabricated class for an unknown value — the field previously
|
|
417
|
+
// defaulted every unresolved car to 'G' (worst class), which is a
|
|
418
|
+
// false/misleading statement, not a "no data" state. Empty string here.
|
|
416
419
|
private getEnergyEfficiencyClass(car: DataSchemes.Car): string {
|
|
417
420
|
const available = {
|
|
418
421
|
selector_energyEfficiencyClass_appp: 'A+',
|
|
@@ -425,10 +428,10 @@ export class FbTransformer {
|
|
|
425
428
|
selector_energyEfficiencyClass_e: 'E',
|
|
426
429
|
selector_energyEfficiencyClass_f: 'F',
|
|
427
430
|
selector_energyEfficiencyClass_g: 'G',
|
|
428
|
-
selector_unknown: '
|
|
431
|
+
selector_unknown: '',
|
|
429
432
|
};
|
|
430
433
|
const energyClass = get(car, 'environmentEmissions.wltpEnergyEfficiencyClass', 'selector_unknown');
|
|
431
|
-
return get(available, energyClass);
|
|
434
|
+
return get(available, energyClass, '');
|
|
432
435
|
}
|
|
433
436
|
|
|
434
437
|
private getMileage(car: DataSchemes.Car): number {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { get } from 'lodash';
|
|
2
|
+
import { DataSchemes } from '@bytebrand/car-schema';
|
|
3
|
+
|
|
4
|
+
// Google vehicle ads `engine`: gasoline, diesel, electric, hybrid, lpg, methane, natural_gas,
|
|
5
|
+
// plug-in_hybrid, other (https://support.google.com/merchants/answer/14156068). Any other value
|
|
6
|
+
// is rejected as "Missing attribute [engine]" — CarGas / NaturalGas did that for ~290 items.
|
|
7
|
+
const GOOGLE_ENGINE: Record<string, string> = {
|
|
8
|
+
selector_fuel_petrol: 'Gasoline',
|
|
9
|
+
selector_fuel_diesel: 'Diesel',
|
|
10
|
+
selector_fuel_electrical: 'Electric',
|
|
11
|
+
selector_fuel_hybridElectricalPetrol: 'Hybrid',
|
|
12
|
+
selector_fuel_hybridElectricalDiesel: 'Hybrid',
|
|
13
|
+
selector_fuel_carGas: 'lpg',
|
|
14
|
+
selector_fuel_naturalGas: 'natural_gas',
|
|
15
|
+
selector_fuel_ethanol: 'other',
|
|
16
|
+
selector_fuel_hydrogen: 'other',
|
|
17
|
+
selector_unknown: 'other',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const HYBRID_FUELS = ['selector_fuel_hybridElectricalPetrol', 'selector_fuel_hybridElectricalDiesel'];
|
|
21
|
+
|
|
22
|
+
const positive = (value: unknown): number | undefined => {
|
|
23
|
+
const n = Number(value);
|
|
24
|
+
return Number.isFinite(n) && n > 0 ? n : undefined;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const isBev = (car: DataSchemes.Car): boolean => get(car, 'consumption.fuel') === 'selector_fuel_electrical';
|
|
28
|
+
const isPlugIn = (car: DataSchemes.Car): boolean =>
|
|
29
|
+
HYBRID_FUELS.includes(get(car, 'consumption.fuel')) && !!get(car, 'engineData.hybridPlugin');
|
|
30
|
+
|
|
31
|
+
export function googleEngine(car: DataSchemes.Car): string {
|
|
32
|
+
const engine = GOOGLE_ENGINE[get(car, 'consumption.fuel', 'selector_unknown')] || 'other';
|
|
33
|
+
return engine === 'Hybrid' && get(car, 'engineData.hybridPlugin') ? 'plug-in_hybrid' : engine;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// BEV: combined WLTP range (CoC 49.2). Plug-in: the EAER (`wltpTotalRange`, CoC 49.5) when present,
|
|
37
|
+
// else the range the dealer delivered. City ranges are never used; 0 counts as missing.
|
|
38
|
+
export function googleElectricRange(car: DataSchemes.Car): string | undefined {
|
|
39
|
+
let range: number | undefined;
|
|
40
|
+
if (isBev(car)) {
|
|
41
|
+
range = positive(get(car, 'battery.wltpRange')) ?? positive(get(car, 'battery.batteryRangeElectric'));
|
|
42
|
+
} else if (isPlugIn(car)) {
|
|
43
|
+
range = positive(get(car, 'battery.wltpTotalRange'))
|
|
44
|
+
?? positive(get(car, 'battery.wltpRange'))
|
|
45
|
+
?? positive(get(car, 'battery.batteryRangeElectric'));
|
|
46
|
+
}
|
|
47
|
+
return range ? `${range}km` : undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Missing or 0 is omitted instead of sending "0l/100km". BEVs carry no fuel consumption
|
|
51
|
+
// (CBD mirrors kWh into consumptionCombined for them).
|
|
52
|
+
export function googleFuelConsumption(car: DataSchemes.Car): string | undefined {
|
|
53
|
+
if (isBev(car)) return undefined;
|
|
54
|
+
const value = positive(get(car, 'consumption.consumptionCombined'));
|
|
55
|
+
return value ? `${value}l/100km` : undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// A BEV emits 0 g/km by definition; for every other car a missing or 0 value is omitted.
|
|
59
|
+
export function googleCo2(car: DataSchemes.Car): string | undefined {
|
|
60
|
+
if (isBev(car)) return '0g/km';
|
|
61
|
+
const value = positive(get(car, 'environmentEmissions.co2'));
|
|
62
|
+
return value ? `${value}g/km` : undefined;
|
|
63
|
+
}
|
|
@@ -8,6 +8,7 @@ import { Injectable } from '@nestjs/common';
|
|
|
8
8
|
import { ConfigService, ImagesService, MappingService, TransformerService, UserService } from '../services';
|
|
9
9
|
import { GoogleXmlItem } from '../interfaces';
|
|
10
10
|
import { isValidVin } from '../utils';
|
|
11
|
+
import { googleCo2, googleElectricRange, googleEngine, googleFuelConsumption } from './google-feed-envkv';
|
|
11
12
|
import {
|
|
12
13
|
META_PUSH_PRIORITY_ALLOWED_VALUES,
|
|
13
14
|
META_PUSH_PRIORITY_STANDARD,
|
|
@@ -85,17 +86,17 @@ export class GoogleTransformer {
|
|
|
85
86
|
color: this.getColor(car),
|
|
86
87
|
vehicle_option: this.getExtrasField(car),
|
|
87
88
|
body_style: this.mapBodyStyle(car),
|
|
88
|
-
engine:
|
|
89
|
+
engine: googleEngine(car),
|
|
89
90
|
transmission: this.mapTransmission(car),
|
|
90
91
|
drive: this.mapDrive(car),
|
|
91
92
|
doors: this.getDoors(car),
|
|
92
93
|
capacity: this.getCapacity(car),
|
|
93
94
|
date_first_registered: this.getDateFirstRegistered(car),
|
|
94
|
-
fuel_consumption:
|
|
95
|
+
fuel_consumption: googleFuelConsumption(car),
|
|
95
96
|
energy_consumption: this.getEnergyConsumption(car),
|
|
96
|
-
electric_range:
|
|
97
|
+
electric_range: googleElectricRange(car),
|
|
97
98
|
fuel_consumption_discharged_battery: this.getFuelConsumptionDischargedBattery(car),
|
|
98
|
-
co2_emissions:
|
|
99
|
+
co2_emissions: googleCo2(car),
|
|
99
100
|
description: this.processText((await this.getDescription(car))),
|
|
100
101
|
identifier_exists: 'yes',
|
|
101
102
|
availability: 'in_stock',
|
|
@@ -121,6 +122,8 @@ export class GoogleTransformer {
|
|
|
121
122
|
'certification',
|
|
122
123
|
'energy_consumption',
|
|
123
124
|
'electric_range',
|
|
125
|
+
'fuel_consumption',
|
|
126
|
+
'co2_emissions',
|
|
124
127
|
'fuel_consumption_discharged_battery',
|
|
125
128
|
'installment',
|
|
126
129
|
'body_style',
|
|
@@ -341,10 +344,6 @@ export class GoogleTransformer {
|
|
|
341
344
|
return power ? `${power}kWh/100km` : undefined;
|
|
342
345
|
}
|
|
343
346
|
|
|
344
|
-
private getElectricRange(car: DataSchemes.Car): string | undefined {
|
|
345
|
-
const range = get(car, 'battery.batteryRangeElectric') || get(car, 'battery.wltpRange');
|
|
346
|
-
return range ? `${range}km` : undefined;
|
|
347
|
-
}
|
|
348
347
|
|
|
349
348
|
private getFuelConsumptionDischargedBattery(car: DataSchemes.Car): string | undefined {
|
|
350
349
|
if (!get(car, 'engineData.hybridPlugin')) {
|
|
@@ -386,27 +385,6 @@ export class GoogleTransformer {
|
|
|
386
385
|
}
|
|
387
386
|
}
|
|
388
387
|
|
|
389
|
-
private mapFuel(car: DataSchemes.Car): string {
|
|
390
|
-
const available = {
|
|
391
|
-
selector_fuel_petrol: 'Gasoline',
|
|
392
|
-
selector_fuel_diesel: 'Diesel',
|
|
393
|
-
selector_fuel_electrical: 'Electric',
|
|
394
|
-
selector_fuel_hybridElectricalPetrol: 'Hybrid',
|
|
395
|
-
selector_fuel_hybridElectricalDiesel: 'Hybrid',
|
|
396
|
-
selector_fuel_carGas: 'CarGas',
|
|
397
|
-
selector_fuel_naturalGas: 'NaturalGas',
|
|
398
|
-
selector_fuel_ethanol: 'Ethanol',
|
|
399
|
-
selector_fuel_hydrogen: 'Hydrogen',
|
|
400
|
-
selector_unknown: 'Other',
|
|
401
|
-
};
|
|
402
|
-
const fuel = get(car, 'consumption.fuel', 'selector_unknown');
|
|
403
|
-
let engine = get(available, fuel, 'Other')
|
|
404
|
-
const hybridPlugin = car?.engineData?.hybridPlugin;
|
|
405
|
-
if (engine === 'Hybrid' && hybridPlugin)
|
|
406
|
-
engine = 'plug-in_hybrid';
|
|
407
|
-
|
|
408
|
-
return engine;
|
|
409
|
-
}
|
|
410
388
|
|
|
411
389
|
private mapBodyStyle(car: DataSchemes.Car): string {
|
|
412
390
|
const available = {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Run: npm test (ts-node, no test framework needed)
|
|
2
|
+
import * as assert from 'node:assert/strict';
|
|
3
|
+
import { googleCo2, googleElectricRange, googleEngine, googleFuelConsumption } from '../src/transformers/google-feed-envkv';
|
|
4
|
+
|
|
5
|
+
const car = (fuel: string, extra: Record<string, unknown> = {}): any => ({
|
|
6
|
+
consumption: { fuel, ...(extra.consumption as object) },
|
|
7
|
+
engineData: { hybridPlugin: extra.hybridPlugin },
|
|
8
|
+
battery: extra.battery || {},
|
|
9
|
+
environmentEmissions: extra.environmentEmissions || {},
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const cases: Array<[string, () => void]> = [
|
|
13
|
+
['LPG and CNG get the values Google accepts', () => {
|
|
14
|
+
assert.equal(googleEngine(car('selector_fuel_carGas')), 'lpg');
|
|
15
|
+
assert.equal(googleEngine(car('selector_fuel_naturalGas')), 'natural_gas');
|
|
16
|
+
}],
|
|
17
|
+
['ethanol, hydrogen and unknown fuels become other', () => {
|
|
18
|
+
assert.equal(googleEngine(car('selector_fuel_ethanol')), 'other');
|
|
19
|
+
assert.equal(googleEngine(car('selector_fuel_hydrogen')), 'other');
|
|
20
|
+
assert.equal(googleEngine(car('selector_fuel_somethingNew')), 'other');
|
|
21
|
+
}],
|
|
22
|
+
['existing engine values are unchanged', () => {
|
|
23
|
+
assert.equal(googleEngine(car('selector_fuel_petrol')), 'Gasoline');
|
|
24
|
+
assert.equal(googleEngine(car('selector_fuel_diesel')), 'Diesel');
|
|
25
|
+
assert.equal(googleEngine(car('selector_fuel_electrical')), 'Electric');
|
|
26
|
+
assert.equal(googleEngine(car('selector_fuel_hybridElectricalPetrol')), 'Hybrid');
|
|
27
|
+
assert.equal(googleEngine(car('selector_fuel_hybridElectricalPetrol', { hybridPlugin: true })), 'plug-in_hybrid');
|
|
28
|
+
}],
|
|
29
|
+
['BEV range is the combined WLTP range, never the city range', () => {
|
|
30
|
+
assert.equal(googleElectricRange(car('selector_fuel_electrical', { battery: { wltpRange: 451, wltpRangeCity: 590 } })), '451km');
|
|
31
|
+
assert.equal(googleElectricRange(car('selector_fuel_electrical', { battery: { wltpRangeCity: 590 } })), undefined);
|
|
32
|
+
}],
|
|
33
|
+
['plug-in range prefers the EAER, then the delivered range', () => {
|
|
34
|
+
const phev = (battery: object) => car('selector_fuel_hybridElectricalPetrol', { hybridPlugin: true, battery });
|
|
35
|
+
assert.equal(googleElectricRange(phev({ wltpTotalRange: 62, wltpRange: 59 })), '62km');
|
|
36
|
+
assert.equal(googleElectricRange(phev({ wltpTotalRange: 0, wltpRange: 59 })), '59km');
|
|
37
|
+
assert.equal(googleElectricRange(phev({ wltpTotalRangeCity: 70 })), undefined);
|
|
38
|
+
}],
|
|
39
|
+
['a range of 0 counts as missing', () => {
|
|
40
|
+
assert.equal(googleElectricRange(car('selector_fuel_electrical', { battery: { wltpRange: 0 } })), undefined);
|
|
41
|
+
}],
|
|
42
|
+
['no range for combustion cars or non-plug-in hybrids', () => {
|
|
43
|
+
assert.equal(googleElectricRange(car('selector_fuel_petrol', { battery: { wltpRange: 50 } })), undefined);
|
|
44
|
+
assert.equal(googleElectricRange(car('selector_fuel_hybridElectricalPetrol', { battery: { wltpRange: 50 } })), undefined);
|
|
45
|
+
}],
|
|
46
|
+
['missing or 0 fuel consumption is omitted, never "0l/100km"', () => {
|
|
47
|
+
assert.equal(googleFuelConsumption(car('selector_fuel_petrol', { consumption: { consumptionCombined: 0 } })), undefined);
|
|
48
|
+
assert.equal(googleFuelConsumption(car('selector_fuel_petrol')), undefined);
|
|
49
|
+
assert.equal(googleFuelConsumption(car('selector_fuel_petrol', { consumption: { consumptionCombined: 6.3 } })), '6.3l/100km');
|
|
50
|
+
}],
|
|
51
|
+
['BEVs send no fuel consumption (CBD mirrors kWh into it)', () => {
|
|
52
|
+
assert.equal(googleFuelConsumption(car('selector_fuel_electrical', { consumption: { consumptionCombined: 18 } })), undefined);
|
|
53
|
+
}],
|
|
54
|
+
['CO2: BEV is 0 g/km, other cars omit a missing or 0 value', () => {
|
|
55
|
+
assert.equal(googleCo2(car('selector_fuel_electrical')), '0g/km');
|
|
56
|
+
assert.equal(googleCo2(car('selector_fuel_petrol', { environmentEmissions: { co2: 0 } })), undefined);
|
|
57
|
+
assert.equal(googleCo2(car('selector_fuel_petrol', { environmentEmissions: { co2: 144 } })), '144g/km');
|
|
58
|
+
}],
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
let failed = 0;
|
|
62
|
+
for (const [name, fn] of cases) {
|
|
63
|
+
try {
|
|
64
|
+
fn();
|
|
65
|
+
console.log(`ok ${name}`);
|
|
66
|
+
} catch (e) {
|
|
67
|
+
failed++;
|
|
68
|
+
console.log(`FAIL ${name}\n ${(e as Error).message}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (failed) process.exit(1);
|