@bytebrand/export-cars 1.22.18 → 1.22.20

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.18",
3
+ "version": "1.22.20",
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;
@@ -196,7 +196,9 @@ export interface GoogleXmlItem {
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;
@@ -21,10 +21,15 @@ export class UserService {
21
21
  return cachedResponse;
22
22
  }
23
23
 
24
- let result: any = {};
24
+ // A valid-but-empty description schema. car-description's MobileDescription does
25
+ // `getBodySchema(schema.body)` / `buildFreeText(schema.{header,footer})` with no guards,
26
+ // so `{}` would crash on `undefined.map`. Empty arrays produce an empty description safely.
27
+ const emptySchema = { body: [], header: [], footer: [] };
28
+
29
+ let result: any = emptySchema;
25
30
  try {
26
31
  const defaultSummary = await CronicleUtils.retry(() => this.requestService.userActionsRequest.getDefaultSummaryByDealerId(dealerId), 1, 30, true);
27
- result = get(defaultSummary, 'data.schema', {});
32
+ result = get(defaultSummary, 'data.schema', emptySchema);
28
33
  } catch (e) {
29
34
  // Dealer has no default summary template (user-actions returns 404) — fall back to an
30
35
  // empty schema instead of letting the rejection crash the whole export job.
@@ -50,17 +50,13 @@ export class GoogleTransformer {
50
50
  ...transformedImages.feat
51
51
  ]
52
52
 
53
- return {
53
+ const item: GoogleXmlItem = {
54
54
  google_product_category: '916',
55
55
  vehicle_fulfillment: {
56
56
  option: 'online',
57
57
  store_code: 'Online_shop'
58
58
  },
59
- certification: {
60
- certification_authority: 'BMWK',
61
- certification_name: 'Vehicle_CO2_Class',
62
- certification_value: `${this.getEnergyClass(car)}`
63
- },
59
+ certification: this.getCertification(car),
64
60
  title: this.getTitle(car),
65
61
  product_type: this.getCategory(car),
66
62
  VIN: this.transformService.vinCode(car),
@@ -84,7 +80,9 @@ export class GoogleTransformer {
84
80
  engine: this.mapFuel(car),
85
81
  date_first_registered: this.getDateFirstRegistered(car),
86
82
  fuel_consumption: `${this.transformService.fuelConsumption(car)}l/100km`,
87
- energy_consumption: this.getWLTP(car),
83
+ energy_consumption: this.getEnergyConsumption(car),
84
+ electric_range: this.getElectricRange(car),
85
+ fuel_consumption_discharged_battery: this.getFuelConsumptionDischargedBattery(car),
88
86
  co2_emissions: `${this.transformService.cO2Emission(car)}g/km`,
89
87
  description: this.processText((await this.getDescription(car))),
90
88
  identifier_exists: 'no',
@@ -101,6 +99,25 @@ export class GoogleTransformer {
101
99
  installment: this.getInstallment(car),
102
100
  // custom_label_0: `${this.getInstallment(car)} EUR mtl.`,
103
101
  };
102
+
103
+ // Google rejects empty/partial values — getElement() emits `<tag/>` for falsy
104
+ // values, which Merchant treats as "missing". Drop optional attributes that have
105
+ // no data (certification when CO2 class unknown, EV-only metrics on non-EV cars,
106
+ // installment when APR is missing) instead of emitting empty tags.
107
+ const optionalKeys: Array<keyof GoogleXmlItem> = [
108
+ 'certification',
109
+ 'energy_consumption',
110
+ 'electric_range',
111
+ 'fuel_consumption_discharged_battery',
112
+ 'installment',
113
+ ];
114
+ for (const key of optionalKeys) {
115
+ if (!item[key]) {
116
+ delete item[key];
117
+ }
118
+ }
119
+
120
+ return item;
104
121
  }
105
122
 
106
123
  private checkCarValidity(car: DataSchemes.Car, images: ImboImageResponseInterface[]) {
@@ -240,16 +257,62 @@ export class GoogleTransformer {
240
257
  };
241
258
  const energyClass = get(car, 'environmentEmissions.energyEfficiencyClass', 'selector_unknown');
242
259
  const value = get(available, energyClass);
243
- return value.replace(/\+/g, '');
260
+ return value ? value.replace(/\+/g, '') : '';
261
+ }
262
+
263
+ private getCertification(car: DataSchemes.Car) {
264
+ const value = this.getEnergyClass(car);
265
+ if (!value || value === 'default') {
266
+ return undefined;
267
+ }
268
+ return {
269
+ certification_authority: 'BMWK',
270
+ certification_name: 'Vehicle_CO2_Class',
271
+ certification_value: value,
272
+ };
273
+ }
274
+
275
+ private getEnergyConsumption(car: DataSchemes.Car): string | undefined {
276
+ const fuel = get(car, 'consumption.fuel');
277
+ const isElectricOrHybrid =
278
+ ['selector_fuel_electrical', 'selector_fuel_hybridElectricalPetrol', 'selector_fuel_hybridElectricalDiesel'].includes(fuel) ||
279
+ !!get(car, 'engineData.hybridPlugin');
280
+ if (!isElectricOrHybrid) {
281
+ return undefined;
282
+ }
283
+ const power = get(car, 'consumption.wltpPowerCombined') || get(car, 'consumption.consumptionPowerCombined');
284
+ return power ? `${power}kWh/100km` : undefined;
285
+ }
286
+
287
+ private getElectricRange(car: DataSchemes.Car): string | undefined {
288
+ const range = get(car, 'battery.batteryRangeElectric') || get(car, 'battery.wltpRange');
289
+ return range ? `${range}km` : undefined;
290
+ }
291
+
292
+ private getFuelConsumptionDischargedBattery(car: DataSchemes.Car): string | undefined {
293
+ if (!get(car, 'engineData.hybridPlugin')) {
294
+ return undefined;
295
+ }
296
+ const consumption = get(car, 'consumption.consumptionCombined') || get(car, 'consumption.wltpCombined');
297
+ return consumption ? `${consumption}l/100km` : undefined;
244
298
  }
245
299
 
246
300
  private getInstallment(car: DataSchemes.Car) {
247
301
  const monthlyInstallment = round(get(car, 'rate.monthlyInstallment', null));
248
302
  const paybackPeriod = round(get(car, 'rate.paybackPeriod', null));
303
+ const apr = toString(get(car, 'financing.annualPercentageRate', '')).trim();
304
+
305
+ // Google requires `apr` inside the installment block; a partial installment
306
+ // block (without apr) makes Google reject the whole attribute. If financing
307
+ // data is incomplete, omit installment entirely so the rest of the item stays valid.
308
+ if (!monthlyInstallment || !paybackPeriod || !apr) {
309
+ return undefined;
310
+ }
249
311
 
250
312
  return {
251
313
  paybackPeriod,
252
314
  monthlyInstallment,
315
+ apr,
253
316
  };
254
317
  }
255
318
 
@@ -280,7 +343,7 @@ export class GoogleTransformer {
280
343
  selector_unknown: 'Other',
281
344
  };
282
345
  const fuel = get(car, 'consumption.fuel', 'selector_unknown');
283
- let engine = get(available, fuel)
346
+ let engine = get(available, fuel, 'Other')
284
347
  const hybridPlugin = car?.engineData?.hybridPlugin;
285
348
  if (engine === 'Hybrid' && hybridPlugin)
286
349
  engine = 'plug-in_hybrid';