@bytebrand/export-cars 1.23.3 → 1.24.1

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.23.3",
3
+ "version": "1.24.1",
4
4
  "description": "",
5
5
  "author": "bytebrand",
6
6
  "license": "UNLICENSED",
@@ -41,6 +41,11 @@ export class FbExporter implements Exporter {
41
41
  const car = await fbTransformer.transform({ owner, ...data });
42
42
  console.timeEnd(`Transform ${data.car._id}`);
43
43
 
44
+ if (!car) {
45
+ console.log(`Skip car ${data.car._id} because it has no real images`);
46
+ continue;
47
+ }
48
+
44
49
  await fbXmlBuilder.addItem(car as FbXmlItem);
45
50
  this.carService.counter.countCar(data.images);
46
51
  }
@@ -19,6 +19,15 @@ export class FbXmlBuilder extends XMLBuilderBase {
19
19
  if (key === 'additional_image_link' && Array.isArray(data)) {
20
20
  return data.map((value: string) => this.getElement(key, value, true)).join('');
21
21
  }
22
+ if (key === 'vehicle_specifications' && Array.isArray(data)) {
23
+ // getElement() treats falsy values as "missing" — stringify so a legitimate 0
24
+ // (e.g. CO2_emissions for an EV) still renders instead of an empty <value>.
25
+ const stringified = data.map((spec: { type: string; units: string; value: number }) => ({
26
+ ...spec,
27
+ value: String(spec.value),
28
+ }));
29
+ return this.getElementsArray(key, stringified);
30
+ }
22
31
  if (Array.isArray(data)) {
23
32
  return this.getElementsArray(key, data);
24
33
  }
@@ -202,7 +202,7 @@ export class ImagesService {
202
202
 
203
203
 
204
204
  if (!ignoreDefImages && imageWithUrls.length < FB_MAX_IMAGE_COUNT_FOR_DEAFULT) {
205
- imageWithUrls.map(i => i.url).push(...this.DEFAULT_IMAGES);
205
+ this.DEFAULT_IMAGES.forEach(url => imageWithUrls.push({ url }));
206
206
  }
207
207
 
208
208
  return imageWithUrls.map(i => i.url);
@@ -44,6 +44,9 @@ import {
44
44
  TIMESTAMP_FIRST_YEAR,
45
45
  } from '../constants/fb.constants';
46
46
 
47
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
48
+ const vinGenerator = require('vin-generator');
49
+
47
50
  @Injectable()
48
51
  export class FbTransformer {
49
52
  private translator: TranslatorExtended;
@@ -71,6 +74,13 @@ export class FbTransformer {
71
74
  images: ImboImageResponseInterface[];
72
75
  owner: unknown;
73
76
  }): Promise<unknown> {
77
+ // ignoreDefImages=true: cars with no real photos must be excluded, not padded with
78
+ // placeholders (matches facebook-shop.transformer.ts's AMA feed policy).
79
+ const image_links = await this.imagesService.getImages(car, images, ImageType.marketing, true);
80
+ if (!image_links.length) {
81
+ return null;
82
+ }
83
+
74
84
  const autodeId = toString(get(car, '_id', ''));
75
85
  const year = this.getYear(car);
76
86
  const ownerId = get(car, 'metaData.ownerId', null);
@@ -78,9 +88,6 @@ export class FbTransformer {
78
88
  // console.time(`Address ${car._id}`);
79
89
  const address = await this.getAddress(car, owner);
80
90
  // console.timeEnd(`Address ${car._id}`);
81
- // console.time(`Images ${car._id}`);
82
- const image_links = await this.imagesService.getImages(car, images, ImageType.marketing, false);
83
- // console.timeEnd(`Images ${car._id}`);
84
91
 
85
92
  return {
86
93
  fb_page_id: '176306386132',
@@ -116,31 +123,24 @@ export class FbTransformer {
116
123
  interior_color: this.getInteriorColor(car),
117
124
  condition: this.mapCondition(car),
118
125
  energy_efficiency_class_eu: this.getEnergyEfficiencyClass(car),
119
- vehicle_specifications: [
120
- {
121
- type: 'gas_mileage_combined',
122
- units: 'l/100km',
123
- value: get(car, 'consumption.wltpCombined', ''),
124
- },
125
- {
126
- type: 'gas_mileage_city',
127
- units: 'l/100km',
128
- value: get(car, 'consumption.wltpSlow', ''),
129
- },
130
- {
131
- type: 'gas_mileage_highway',
132
- units: 'l/100km',
133
- value: get(car, 'consumption.wltpVeryFast', ''),
134
- },
135
- {
136
- type: 'CO2_emissions',
137
- units: 'g/km',
138
- value: get(car, 'environmentEmissions.wltpCo2', ''),
139
- },
140
- ],
126
+ vehicle_specifications: this.getVehicleSpecifications(car),
141
127
  };
142
128
  }
143
129
 
130
+ // wltp* fields are @IsOptional on the schema — genuinely absent for a meaningful share of
131
+ // cars (older listings, incomplete imports). Omit the whole entry rather than emit a spec
132
+ // with an empty <value>, but keep legitimate 0 (e.g. CO2 for an EV) — isPresent, not truthy.
133
+ private getVehicleSpecifications(car: DataSchemes.Car): { type: string; units: string; value: number }[] {
134
+ const isPresent = (value: unknown): value is number => value !== null && value !== undefined && value !== '';
135
+
136
+ return [
137
+ { type: 'gas_mileage_combined', units: 'l/100km', value: get(car, 'consumption.wltpCombined') },
138
+ { type: 'gas_mileage_city', units: 'l/100km', value: get(car, 'consumption.wltpSlow') },
139
+ { type: 'gas_mileage_highway', units: 'l/100km', value: get(car, 'consumption.wltpVeryFast') },
140
+ { type: 'CO2_emissions', units: 'g/km', value: get(car, 'environmentEmissions.wltpCo2') },
141
+ ].filter(spec => isPresent(spec.value));
142
+ }
143
+
144
144
  private getYear(car: DataSchemes.Car): number {
145
145
  const firstModelsProductionDate = get(car, 'mainData.firstModelsProductionDate', 0) as number;
146
146
  const firstModelsProductionYear = moment(firstModelsProductionDate * 1000).format('YYYY');
@@ -225,6 +225,13 @@ export class FbTransformer {
225
225
  vin = get(car, 'marketing.fakeVIN', '');
226
226
  }
227
227
 
228
+ // Meta requires a vin on every listing. Real + fake VIN are both absent for a small
229
+ // share of cars (marketing.fakeVIN is populated by a different upstream service) —
230
+ // same generated-VIN fallback already used by mobile.transformer.ts/santander.transformer.ts.
231
+ if (!isValidVin(vin)) {
232
+ vin = vinGenerator.generateVin();
233
+ }
234
+
228
235
  return vin;
229
236
  }
230
237
 
@@ -444,7 +451,7 @@ export class FbTransformer {
444
451
  { key: 'assistants.laneAssistant', type: 'lane_keep_assist', value: 'Spurhalte-Assistent' },
445
452
  { key: 'lights.ledHeadlights', type: 'led_headlights', value: 'LED Scheinwerfer' },
446
453
  { key: 'window.sunroof', type: 'moon_roof', value: 'Schiebedach' },
447
- { key: 'multimedia.mp3Interface', type: 'mp3 MP3', value: 'Schnittstelle' },
454
+ { key: 'multimedia.mp3Interface', type: 'mp3', value: 'Schnittstelle' },
448
455
  { key: 'multimedia.navigationSystem', type: 'navigation', value: 'Navigationssystem' },
449
456
  { key: 'assistants.nightVisionAssistant', type: 'night_view', value: 'Nachsicht Assistent' },
450
457
  { key: 'window.panoramicGlassRoof', type: 'panorama_roof', value: 'Panoramaschiebedach' },
@@ -670,6 +677,12 @@ export class FbTransformer {
670
677
  }
671
678
  }
672
679
 
680
+ // Upper-bounded so a raw unix timestamp leaking through a non-date field (e.g.
681
+ // mainData.constructionYear stored as an epoch instead of a 4-digit year, seen in prod) can't
682
+ // be mistaken for a valid year — a timestamp is astronomically larger than any real car year.
673
683
  function getFirstValidYear(...years: string[]): number {
674
- return years.map((year: string) => toSafeInteger(year)).filter((year: number) => year > TIMESTAMP_FIRST_YEAR)[0];
684
+ const currentYear = toSafeInteger(moment().format('YYYY'));
685
+ return years
686
+ .map((year: string) => toSafeInteger(year))
687
+ .filter((year: number) => year > TIMESTAMP_FIRST_YEAR && year <= currentYear + 1)[0];
675
688
  }