@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytebrand/export-cars",
3
- "version": "1.26.20",
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: string,
202
+ fuel_consumption?: string,
203
203
  energy_consumption?: string,
204
204
  electric_range?: string,
205
205
  fuel_consumption_discharged_battery?: string,
206
- co2_emissions: string,
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: 'G',
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: this.mapFuel(car),
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: `${this.transformService.fuelConsumption(car)}l/100km`,
95
+ fuel_consumption: googleFuelConsumption(car),
95
96
  energy_consumption: this.getEnergyConsumption(car),
96
- electric_range: this.getElectricRange(car),
97
+ electric_range: googleElectricRange(car),
97
98
  fuel_consumption_discharged_battery: this.getFuelConsumptionDischargedBattery(car),
98
- co2_emissions: `${this.transformService.cO2Emission(car)}g/km`,
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);