@bytebrand/export-cars 1.22.19 → 1.22.21

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.22.19",
3
+ "version": "1.22.21",
4
4
  "description": "",
5
5
  "author": "bytebrand",
6
6
  "license": "UNLICENSED",
@@ -62,10 +62,11 @@ export class GoogleXmlBuilder extends XMLBuilderBase {
62
62
  </g:${rootTag}>`;
63
63
  }
64
64
 
65
- private getInstallmentElement(rootTag: string, data: { paybackPeriod: string; monthlyInstallment: string }): string {
65
+ private getInstallmentElement(rootTag: string, data: { paybackPeriod: string; monthlyInstallment: string; apr?: string }): string {
66
66
  return `<g:${rootTag}>
67
67
  <g:months>${data.paybackPeriod}</g:months>
68
- <g:amount>${data.monthlyInstallment} EUR</g:amount>
68
+ <g:amount>${data.monthlyInstallment} EUR</g:amount>${data.apr ? `
69
+ <g:apr>${data.apr}</g:apr>` : ''}
69
70
  </g:${rootTag}>`;
70
71
  }
71
72
 
@@ -172,13 +172,13 @@ export interface GoogleXmlItem {
172
172
  sale_price?: string;
173
173
  availability?: string;
174
174
  energy_efficiency_class?: string;
175
- installment?: { paybackPeriod: any; monthlyInstallment: any };
175
+ installment?: { paybackPeriod: any; monthlyInstallment: any; apr: string };
176
176
 
177
177
  vehicle_fulfillment: {
178
178
  option: string;
179
179
  store_code: string;
180
180
  };
181
- certification: {
181
+ certification?: {
182
182
  certification_authority: string;
183
183
  certification_name: string;
184
184
  certification_code?: string;
@@ -187,16 +187,18 @@ export interface GoogleXmlItem {
187
187
  link_template: string;
188
188
  additional_image_link: string[];
189
189
  model: string;
190
- VIN: string;
190
+ VIN?: string;
191
191
  trim: string;
192
192
  year: number;
193
193
  mileage: string;
194
194
  vehicle_option: string[];
195
- body_style: string,
195
+ body_style?: string,
196
196
  engine: string,
197
197
  date_first_registered: string,
198
198
  fuel_consumption: string,
199
- energy_consumption: string,
199
+ energy_consumption?: string,
200
+ electric_range?: string,
201
+ fuel_consumption_discharged_battery?: string,
200
202
  co2_emissions: string,
201
203
  identifier_exists: string,
202
204
  ads_redirect?: string;
@@ -7,6 +7,7 @@ import { ImboImageResponseInterface } from '@bytebrand/chunkable';
7
7
  import { Injectable } from '@nestjs/common';
8
8
  import { ConfigService, ImagesService, TransformerService, UserService } from '../services';
9
9
  import { GoogleXmlItem } from '../interfaces';
10
+ import { isValidVin } from '../utils';
10
11
  import moment from 'moment';
11
12
 
12
13
  const GOOGLE_MAX_TRIM_LENGTH = 50;
@@ -50,20 +51,16 @@ export class GoogleTransformer {
50
51
  ...transformedImages.feat
51
52
  ]
52
53
 
53
- return {
54
+ const item: GoogleXmlItem = {
54
55
  google_product_category: '916',
55
56
  vehicle_fulfillment: {
56
57
  option: 'online',
57
58
  store_code: 'Online_shop'
58
59
  },
59
- certification: {
60
- certification_authority: 'BMWK',
61
- certification_name: 'Vehicle_CO2_Class',
62
- certification_value: `${this.getEnergyClass(car)}`
63
- },
60
+ certification: this.getCertification(car),
64
61
  title: this.getTitle(car),
65
62
  product_type: this.getCategory(car),
66
- VIN: this.transformService.vinCode(car),
63
+ VIN: this.getVin(car),
67
64
  id: carId,
68
65
  image_link: allImages[0],
69
66
  additional_image_link: allImages.slice(1, 10),
@@ -84,7 +81,9 @@ export class GoogleTransformer {
84
81
  engine: this.mapFuel(car),
85
82
  date_first_registered: this.getDateFirstRegistered(car),
86
83
  fuel_consumption: `${this.transformService.fuelConsumption(car)}l/100km`,
87
- energy_consumption: this.getWLTP(car),
84
+ energy_consumption: this.getEnergyConsumption(car),
85
+ electric_range: this.getElectricRange(car),
86
+ fuel_consumption_discharged_battery: this.getFuelConsumptionDischargedBattery(car),
88
87
  co2_emissions: `${this.transformService.cO2Emission(car)}g/km`,
89
88
  description: this.processText((await this.getDescription(car))),
90
89
  identifier_exists: 'no',
@@ -101,18 +100,41 @@ export class GoogleTransformer {
101
100
  installment: this.getInstallment(car),
102
101
  // custom_label_0: `${this.getInstallment(car)} EUR mtl.`,
103
102
  };
103
+
104
+ // Google rejects empty/partial values — getElement() emits `<tag/>` for falsy
105
+ // values, which Merchant treats as "missing". Drop optional attributes that have
106
+ // no data (certification when CO2 class unknown, EV-only metrics on non-EV cars,
107
+ // installment when APR is missing) instead of emitting empty tags.
108
+ const optionalKeys: Array<keyof GoogleXmlItem> = [
109
+ 'certification',
110
+ 'energy_consumption',
111
+ 'electric_range',
112
+ 'fuel_consumption_discharged_battery',
113
+ 'installment',
114
+ 'VIN',
115
+ 'body_style',
116
+ ];
117
+ for (const key of optionalKeys) {
118
+ if (!item[key]) {
119
+ delete item[key];
120
+ }
121
+ }
122
+
123
+ return item;
104
124
  }
105
125
 
106
126
  private checkCarValidity(car: DataSchemes.Car, images: ImboImageResponseInterface[]) {
107
127
  if (images.length === 0)
108
128
  return false
109
129
 
110
- if (!this.mapBodyStyle(car))
111
- return false
112
-
113
130
  return true
114
131
  }
115
132
 
133
+ private getVin(car: DataSchemes.Car): string | undefined {
134
+ const vin = get(car, 'metaData.vin', '');
135
+ return isValidVin(vin) ? vin : undefined;
136
+ }
137
+
116
138
  private getTitle(car: DataSchemes.Car): string {
117
139
  const model = toString(get(car, 'mainData.model', ''));
118
140
  const brand = trim(this.getBrand(car));
@@ -240,16 +262,62 @@ export class GoogleTransformer {
240
262
  };
241
263
  const energyClass = get(car, 'environmentEmissions.energyEfficiencyClass', 'selector_unknown');
242
264
  const value = get(available, energyClass);
243
- return value.replace(/\+/g, '');
265
+ return value ? value.replace(/\+/g, '') : '';
266
+ }
267
+
268
+ private getCertification(car: DataSchemes.Car) {
269
+ const value = this.getEnergyClass(car);
270
+ if (!value || value === 'default') {
271
+ return undefined;
272
+ }
273
+ return {
274
+ certification_authority: 'BMWK',
275
+ certification_name: 'Vehicle_CO2_Class',
276
+ certification_value: value,
277
+ };
278
+ }
279
+
280
+ private getEnergyConsumption(car: DataSchemes.Car): string | undefined {
281
+ const fuel = get(car, 'consumption.fuel');
282
+ const isElectricOrHybrid =
283
+ ['selector_fuel_electrical', 'selector_fuel_hybridElectricalPetrol', 'selector_fuel_hybridElectricalDiesel'].includes(fuel) ||
284
+ !!get(car, 'engineData.hybridPlugin');
285
+ if (!isElectricOrHybrid) {
286
+ return undefined;
287
+ }
288
+ const power = get(car, 'consumption.wltpPowerCombined') || get(car, 'consumption.consumptionPowerCombined');
289
+ return power ? `${power}kWh/100km` : undefined;
290
+ }
291
+
292
+ private getElectricRange(car: DataSchemes.Car): string | undefined {
293
+ const range = get(car, 'battery.batteryRangeElectric') || get(car, 'battery.wltpRange');
294
+ return range ? `${range}km` : undefined;
295
+ }
296
+
297
+ private getFuelConsumptionDischargedBattery(car: DataSchemes.Car): string | undefined {
298
+ if (!get(car, 'engineData.hybridPlugin')) {
299
+ return undefined;
300
+ }
301
+ const consumption = get(car, 'consumption.consumptionCombined') || get(car, 'consumption.wltpCombined');
302
+ return consumption ? `${consumption}l/100km` : undefined;
244
303
  }
245
304
 
246
305
  private getInstallment(car: DataSchemes.Car) {
247
306
  const monthlyInstallment = round(get(car, 'rate.monthlyInstallment', null));
248
307
  const paybackPeriod = round(get(car, 'rate.paybackPeriod', null));
308
+ const apr = toString(get(car, 'financing.annualPercentageRate', '')).trim();
309
+
310
+ // Google requires `apr` inside the installment block; a partial installment
311
+ // block (without apr) makes Google reject the whole attribute. If financing
312
+ // data is incomplete, omit installment entirely so the rest of the item stays valid.
313
+ if (!monthlyInstallment || !paybackPeriod || !apr) {
314
+ return undefined;
315
+ }
249
316
 
250
317
  return {
251
318
  paybackPeriod,
252
319
  monthlyInstallment,
320
+ apr,
253
321
  };
254
322
  }
255
323
 
@@ -280,7 +348,7 @@ export class GoogleTransformer {
280
348
  selector_unknown: 'Other',
281
349
  };
282
350
  const fuel = get(car, 'consumption.fuel', 'selector_unknown');
283
- let engine = get(available, fuel)
351
+ let engine = get(available, fuel, 'Other')
284
352
  const hybridPlugin = car?.engineData?.hybridPlugin;
285
353
  if (engine === 'Hybrid' && hybridPlugin)
286
354
  engine = 'plug-in_hybrid';