@bytebrand/export-cars 1.24.0 → 1.25.0

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.24.0",
3
+ "version": "1.25.0",
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
  }
@@ -17,6 +17,12 @@ export class FacebookShopXmlBuilder extends XMLBuilderBase {
17
17
  const tags = Object.keys(car);
18
18
 
19
19
  for (const tag of tags) {
20
+ if (tag === 'image' && Array.isArray(car[tag])) {
21
+ item += (car[tag] as unknown as string[])
22
+ .map((url: string) => `<image><url>${escape(url)}</url></image>\n`)
23
+ .join('');
24
+ continue;
25
+ }
20
26
  item += this.buildTagElement(tag, car[tag]);
21
27
  }
22
28
  item += '</item>';
@@ -16,8 +16,17 @@ export class FbXmlBuilder extends XMLBuilderBase {
16
16
  if (key === 'description') {
17
17
  return this.getDescriptionElement(key, data);
18
18
  }
19
- if (key === 'additional_image_link' && Array.isArray(data)) {
20
- return data.map((value: string) => this.getElement(key, value, true)).join('');
19
+ if (key === 'image' && Array.isArray(data)) {
20
+ return this.getElementsArray(key, data.map((url: string) => ({ url })));
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);
21
30
  }
22
31
  if (Array.isArray(data)) {
23
32
  return this.getElementsArray(key, data);
@@ -207,6 +207,7 @@ export interface GoogleXmlItem {
207
207
  identifier_exists: string,
208
208
  ads_redirect?: string;
209
209
  "certified_pre-owned"?: boolean;
210
+ user_id?: string;
210
211
  custom_label_0?: string;
211
212
  custom_label_1?: string,
212
213
  custom_label_2?: string,
@@ -351,11 +352,10 @@ export interface FacebookShopItem {
351
352
  title: FacebookShopTag;
352
353
  description: FacebookShopTag;
353
354
  availability: FacebookShopTag;
354
- condition: FacebookShopTag;
355
+ state_of_vehicle: FacebookShopTag;
355
356
  price: FacebookShopTag;
356
357
  link: FacebookShopTag;
357
- image_link: FacebookShopTag;
358
- additional_image_link: FacebookShopTag;
358
+ image: string[];
359
359
  make: FacebookShopTag;
360
360
  model: FacebookShopTag;
361
361
  year: FacebookShopTag;
@@ -78,13 +78,12 @@ export class FacebookShopTransformer {
78
78
 
79
79
  const base = {
80
80
  vehicle_id: this.tag(vehicleId, true),
81
- availability: this.tag('in stock', true),
82
- condition: this.tag(this.getCondition(car), true),
81
+ availability: this.tag(this.getAvailability(car), false),
82
+ state_of_vehicle: this.tag(this.getCondition(car), false),
83
83
  price: this.tag(`${price} EUR`, true),
84
84
  sale_price: this.tag(`${price} EUR`, true),
85
85
  link: this.tag(this.getCarLink(car), true),
86
- image_link: this.tag(imageLinks[0], true),
87
- additional_image_link: this.tag(imageLinks[0], false),
86
+ image: imageLinks.slice(0, 2), // main + 2nd image, per ticket
88
87
  make: this.tag(make, false),
89
88
  model: this.tag(model, false),
90
89
  year: this.tag(this.getYear(car), false),
@@ -252,14 +251,18 @@ export class FacebookShopTransformer {
252
251
 
253
252
  private getCondition(car: DataSchemes.Car): string {
254
253
  const available = {
255
- selector_condition_new: 'new',
256
- selector_condition_used: 'used',
257
- selector_unknown: 'used',
254
+ selector_condition_new: 'New',
255
+ selector_condition_used: 'Used',
256
+ selector_unknown: 'Used',
258
257
  };
259
258
  const condition = get(car, 'mainData.condition', 'selector_unknown');
260
259
  return get(available, condition);
261
260
  }
262
261
 
262
+ private getAvailability(car: DataSchemes.Car): string {
263
+ return get(car, 'state.isOnline', false) ? 'available' : 'not_available';
264
+ }
265
+
263
266
  private getCarLink(car: DataSchemes.Car): string {
264
267
  return `https://${this.configService.config.domain}/search/vehicle/${car._id}`;
265
268
  }
@@ -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',
@@ -99,8 +106,7 @@ export class FbTransformer {
99
106
  make: toString(get(car, 'mainData.make', '')),
100
107
  model: toString(get(car, 'mainData.model', '')),
101
108
  trim: this.getTrim(car),
102
- image_link: image_links[0],
103
- additional_image_link: image_links.slice(1, 21), // Meta allows max 20 additional images
109
+ image: image_links.slice(0, 20), // Meta caps the whole image array at 20 items
104
110
  year: toString(year),
105
111
  mileage: {
106
112
  value: this.getMileage(car),
@@ -114,7 +120,7 @@ export class FbTransformer {
114
120
  price: this.getPrice(car),
115
121
  exterior_color: this.getExteriorColor(car),
116
122
  interior_color: this.getInteriorColor(car),
117
- condition: this.mapCondition(car),
123
+ state_of_vehicle: this.mapCondition(car),
118
124
  energy_efficiency_class_eu: this.getEnergyEfficiencyClass(car),
119
125
  vehicle_specifications: this.getVehicleSpecifications(car),
120
126
  };
@@ -218,6 +224,13 @@ export class FbTransformer {
218
224
  vin = get(car, 'marketing.fakeVIN', '');
219
225
  }
220
226
 
227
+ // Meta requires a vin on every listing. Real + fake VIN are both absent for a small
228
+ // share of cars (marketing.fakeVIN is populated by a different upstream service) —
229
+ // same generated-VIN fallback already used by mobile.transformer.ts/santander.transformer.ts.
230
+ if (!isValidVin(vin)) {
231
+ vin = vinGenerator.generateVin();
232
+ }
233
+
221
234
  return vin;
222
235
  }
223
236
 
@@ -381,16 +394,16 @@ export class FbTransformer {
381
394
 
382
395
  private mapCondition(car: DataSchemes.Car): string {
383
396
  const available = {
384
- selector_condition_new: 'new',
385
- selector_condition_used: 'used',
386
- selector_unknown: 'used',
397
+ selector_condition_new: 'New',
398
+ selector_condition_used: 'Used',
399
+ selector_unknown: 'Used',
387
400
  };
388
401
  const condition = get(car, 'mainData.condition', 'selector_unknown');
389
402
  return get(available, condition);
390
403
  }
391
404
 
392
405
  private mapAvailability(car: DataSchemes.Car): string {
393
- return get(car, 'state.isOnline', false) ? 'in stock' : 'out of stock';
406
+ return get(car, 'state.isOnline', false) ? 'available' : 'not_available';
394
407
  }
395
408
 
396
409
  private getEnergyEfficiencyClass(car: DataSchemes.Car): string {
@@ -1,13 +1,17 @@
1
- import { escape, get, isNumber, join, round, toSafeInteger, toString, trim, truncate } from 'lodash';
1
+ import { escape, get, includes, isNumber, join, round, toSafeInteger, toString, trim, truncate } from 'lodash';
2
2
  import { CarDescriptionFactory, DescriptionTypeEnum } from '@bytebrand/car-description';
3
3
  import { DataSchemes } from '@bytebrand/car-schema';
4
4
  import { TranslatorExtended } from '@bytebrand/schema-tools';
5
5
  import { de, deVehicleDetailsPage, deVehicleProps } from '@bytebrand/i18n-dictionaries';
6
6
  import { ImboImageResponseInterface } from '@bytebrand/chunkable';
7
7
  import { Injectable } from '@nestjs/common';
8
- import { ConfigService, ImagesService, TransformerService, UserService } from '../services';
8
+ import { ConfigService, ImagesService, MappingService, TransformerService, UserService } from '../services';
9
9
  import { GoogleXmlItem } from '../interfaces';
10
10
  import { isValidVin } from '../utils';
11
+ import {
12
+ META_PUSH_PRIORITY_ALLOWED_VALUES,
13
+ META_PUSH_PRIORITY_STANDARD,
14
+ } from '../constants/fb.constants';
11
15
 
12
16
  const GOOGLE_MAX_TRIM_LENGTH = 50;
13
17
  const TIMESTAMP_FIRST_YEAR = 1970;
@@ -21,6 +25,7 @@ export class GoogleTransformer {
21
25
  private configService: ConfigService,
22
26
  private imageService: ImagesService,
23
27
  private readonly transformService: TransformerService,
28
+ private readonly mappingService: MappingService,
24
29
  ) {
25
30
  const dict = { ...de, ...deVehicleProps, ...deVehicleDetailsPage };
26
31
  this.translator = new TranslatorExtended(dict);
@@ -50,6 +55,8 @@ export class GoogleTransformer {
50
55
  ...transformedImages.feat
51
56
  ]
52
57
 
58
+ const ownerId = get(car, 'metaData.ownerId', null);
59
+
53
60
  const item: GoogleXmlItem = {
54
61
  google_product_category: '916',
55
62
  vehicle_fulfillment: {
@@ -92,11 +99,12 @@ export class GoogleTransformer {
92
99
  description: this.processText((await this.getDescription(car))),
93
100
  identifier_exists: 'yes',
94
101
  availability: 'in_stock',
102
+ user_id: ownerId,
103
+ custom_label_4: await this.getMetaPushPriority(ownerId),
95
104
  // custom_label_0: '',
96
105
  // custom_label_1: '',
97
106
  // custom_label_2: '',
98
107
  // custom_label_3: '',
99
- // custom_label_4: '',
100
108
 
101
109
  // availability: this.getAvailability(car),
102
110
  // sale_price: this.getPrice(car),
@@ -131,6 +139,13 @@ export class GoogleTransformer {
131
139
  return item;
132
140
  }
133
141
 
142
+ private async getMetaPushPriority(ownerId: string): Promise<string> {
143
+ const priorities = await this.mappingService.getMetaPushPriorityMapping();
144
+ const entry = priorities.find(priority => priority.dealer_id === ownerId);
145
+ const value = get(entry, 'meta_push_priority', '');
146
+ return includes(META_PUSH_PRIORITY_ALLOWED_VALUES, value) ? value : META_PUSH_PRIORITY_STANDARD;
147
+ }
148
+
134
149
  private checkCarValidity(car: DataSchemes.Car, images: ImboImageResponseInterface[]) {
135
150
  if (images.length === 0)
136
151
  return false