@bytebrand/export-cars 1.21.7 → 1.21.8

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.
Files changed (156) hide show
  1. package/.editorconfig +8 -8
  2. package/.eslintignore +1 -1
  3. package/.eslintrc.js +24 -24
  4. package/.prettierrc.js +7 -7
  5. package/README.md +65 -65
  6. package/dist/configs/index.d.ts +1 -0
  7. package/dist/configs/index.js +1 -0
  8. package/dist/configs/index.js.map +1 -1
  9. package/dist/configs/santander.config.d.ts +5 -0
  10. package/dist/configs/santander.config.js +18 -0
  11. package/dist/configs/santander.config.js.map +1 -0
  12. package/dist/enums/export-type.enum.d.ts +1 -0
  13. package/dist/enums/export-type.enum.js +1 -0
  14. package/dist/enums/export-type.enum.js.map +1 -1
  15. package/dist/export.command.js +1 -1
  16. package/dist/export.command.js.map +1 -1
  17. package/dist/export.schema.d.ts +6 -2
  18. package/dist/export.schema.js +4 -0
  19. package/dist/export.schema.js.map +1 -1
  20. package/dist/exporters/index.d.ts +1 -0
  21. package/dist/exporters/index.js +1 -0
  22. package/dist/exporters/index.js.map +1 -1
  23. package/dist/exporters/santander.exporter.d.ts +12 -0
  24. package/dist/exporters/santander.exporter.js +63 -0
  25. package/dist/exporters/santander.exporter.js.map +1 -0
  26. package/dist/formats/auto24-xml.builder.js +2 -2
  27. package/dist/formats/google-xml.builder.js +3 -3
  28. package/dist/formats/modix-xml.builder.js +2 -2
  29. package/dist/formats/ooyyo-xml.builder.js +1 -1
  30. package/dist/formats/santander-csv.builder.d.ts +6 -0
  31. package/dist/formats/santander-csv.builder.js +17 -0
  32. package/dist/formats/santander-csv.builder.js.map +1 -0
  33. package/dist/tsconfig.build.tsbuildinfo +1 -1
  34. package/nest-cli.json +5 -5
  35. package/package.json +61 -61
  36. package/src/app.module.ts +38 -38
  37. package/src/configs/alleskralle.config.ts +15 -15
  38. package/src/configs/as24.config.ts +13 -13
  39. package/src/configs/auto24.config.ts +15 -15
  40. package/src/configs/autoboerse.config.ts +16 -16
  41. package/src/configs/autouncle.config.ts +14 -14
  42. package/src/configs/awin.config.ts +15 -15
  43. package/src/configs/by-owner.config.ts +14 -14
  44. package/src/configs/config.ts +83 -83
  45. package/src/configs/criteo.config.ts +17 -17
  46. package/src/configs/dasparking.config.ts +15 -15
  47. package/src/configs/eauto.config.ts +15 -15
  48. package/src/configs/evium.config.ts +18 -18
  49. package/src/configs/facebook-shop.config.ts +16 -16
  50. package/src/configs/fb.config.ts +16 -16
  51. package/src/configs/google.config.ts +14 -14
  52. package/src/configs/index.ts +22 -21
  53. package/src/configs/kleinanzeigeFocus.config.ts +14 -14
  54. package/src/configs/marktde.config.ts +18 -18
  55. package/src/configs/meinstadt.config.ts +15 -15
  56. package/src/configs/mitula.config.ts +14 -14
  57. package/src/configs/ooyyo.config.ts +15 -15
  58. package/src/configs/santander.config.ts +15 -0
  59. package/src/configs/trovit.config.ts +14 -14
  60. package/src/configs/yescar.config.ts +17 -17
  61. package/src/constants/autouncle.contants.ts +4 -4
  62. package/src/constants/fb.constants.ts +7 -7
  63. package/src/constants/index.ts +1 -1
  64. package/src/constants/modix/body-color-name.constant.ts +20 -20
  65. package/src/constants/modix/category.constant.ts +14 -14
  66. package/src/constants/modix/fuel.constant.ts +14 -14
  67. package/src/constants/modix/index.ts +7 -7
  68. package/src/constants/modix/manufacturers.constant.ts +7498 -7498
  69. package/src/constants/modix/transmission.constant.ts +8 -8
  70. package/src/constants/modix/usage-type.constant.ts +13 -13
  71. package/src/constants/modix/vehicle-class.constant.ts +17 -17
  72. package/src/enums/environment.enum.ts +5 -5
  73. package/src/enums/export-type.enum.ts +33 -32
  74. package/src/enums/file-types.enum.ts +8 -8
  75. package/src/enums/image-type.enum.ts +4 -4
  76. package/src/enums/index.ts +4 -4
  77. package/src/export.command.ts +37 -37
  78. package/src/export.schema.ts +134 -128
  79. package/src/exporters/as24.exporter.ts +69 -69
  80. package/src/exporters/auto24.exporter.ts +48 -48
  81. package/src/exporters/autouncle.exporter.ts +60 -60
  82. package/src/exporters/awin.exporter.ts +47 -47
  83. package/src/exporters/by-owner.exporter.ts +44 -44
  84. package/src/exporters/facebook-shop.exporter.ts +51 -51
  85. package/src/exporters/fb.exporter.ts +52 -52
  86. package/src/exporters/google.exporter.ts +45 -45
  87. package/src/exporters/index.ts +15 -14
  88. package/src/exporters/meinstadt.exporter.ts +55 -55
  89. package/src/exporters/mobile.exporter.ts +64 -64
  90. package/src/exporters/modix.exporter.ts +56 -56
  91. package/src/exporters/ooyyo.exporter.ts +48 -48
  92. package/src/exporters/rubrikk.exporter.ts +53 -53
  93. package/src/exporters/santander.exporter.ts +50 -0
  94. package/src/exporters/trovit.exporter.ts +51 -51
  95. package/src/formats/auto24-xml.builder.ts +54 -54
  96. package/src/formats/autouncle-json.builder.ts +20 -20
  97. package/src/formats/awin-csv.builder.ts +16 -16
  98. package/src/formats/facebook-shop.builder.ts +69 -69
  99. package/src/formats/fb-xml.builder.ts +71 -71
  100. package/src/formats/google-xml.builder.ts +35 -35
  101. package/src/formats/index.ts +5 -5
  102. package/src/formats/meinstadt-json.builder.ts +19 -19
  103. package/src/formats/modix-xml.builder.ts +180 -180
  104. package/src/formats/ooyyo-xml.builder.ts +48 -48
  105. package/src/formats/rubrikk-xml.builder.ts +27 -27
  106. package/src/formats/santander-csv.builder.ts +15 -0
  107. package/src/formats/trovit-xml.builder.ts +44 -44
  108. package/src/interfaces/atributes-wrap-item.intreface.ts +6 -6
  109. package/src/interfaces/dealers-mapping.interface.ts +7 -7
  110. package/src/interfaces/description.interface.ts +24 -24
  111. package/src/interfaces/export-item.interface.ts +366 -366
  112. package/src/interfaces/exporter.interface.ts +3 -3
  113. package/src/interfaces/index.ts +3 -3
  114. package/src/interfaces/input.interface.ts +6 -6
  115. package/src/interfaces/mapping-rules.interface.ts +11 -11
  116. package/src/interfaces/modix/index.ts +1 -1
  117. package/src/interfaces/modix/modix-item-list.interface.ts +5 -5
  118. package/src/interfaces/params.interface.ts +13 -13
  119. package/src/interfaces/rule-parser.interface.ts +3 -3
  120. package/src/main.ts +14 -14
  121. package/src/rules/abstract.rule.ts +37 -37
  122. package/src/rules/date.rule.ts +19 -19
  123. package/src/rules/number.rule.ts +15 -15
  124. package/src/rules/string.rule.ts +8 -8
  125. package/src/services/car.service.ts +49 -49
  126. package/src/services/config.service.ts +15 -15
  127. package/src/services/images.service.ts +207 -207
  128. package/src/services/index.ts +9 -9
  129. package/src/services/loader.service.ts +29 -29
  130. package/src/services/mapping.service.ts +91 -91
  131. package/src/services/request.service.ts +44 -44
  132. package/src/services/rules.service.ts +61 -61
  133. package/src/services/transformer.service.ts +129 -129
  134. package/src/services/user.service.ts +57 -57
  135. package/src/transformers/as24.transformer.ts +253 -253
  136. package/src/transformers/auto24.transfomer.ts +155 -155
  137. package/src/transformers/autouncle.transformer.ts +236 -236
  138. package/src/transformers/awin.transformer.ts +193 -193
  139. package/src/transformers/by-owner.transformer.ts +354 -354
  140. package/src/transformers/facebook-shop.transformer.ts +93 -93
  141. package/src/transformers/fb-description.transformer.ts +316 -316
  142. package/src/transformers/fb-equipment.transformer.ts +161 -161
  143. package/src/transformers/fb.transformer.ts +655 -655
  144. package/src/transformers/google.transformer.ts +169 -169
  145. package/src/transformers/index.ts +5 -5
  146. package/src/transformers/meinstadt.transformer.ts +92 -92
  147. package/src/transformers/mobile.transformer.ts +482 -482
  148. package/src/transformers/modix.transformer.ts +227 -227
  149. package/src/transformers/ooyyo.transformer.ts +109 -109
  150. package/src/transformers/rubrikk.transformer.ts +200 -200
  151. package/src/transformers/trovit.transformer.ts +63 -63
  152. package/src/utils/converter.utils.ts +15 -15
  153. package/src/utils/index.ts +2 -2
  154. package/src/utils/validator.utils.ts +7 -7
  155. package/tsconfig.build.json +4 -4
  156. package/tsconfig.json +29 -29
@@ -1,655 +1,655 @@
1
- import { Cache } from 'cache-manager';
2
- import * as moment from 'moment';
3
- import {
4
- escape,
5
- findIndex,
6
- get,
7
- includes,
8
- isArray,
9
- isBoolean,
10
- isEmpty,
11
- isString,
12
- join,
13
- toSafeInteger,
14
- toString,
15
- trim,
16
- truncate,
17
- } from 'lodash';
18
-
19
- import { CACHE_MANAGER } from '@nestjs/cache-manager';
20
- import { Inject, Injectable } from '@nestjs/common';
21
-
22
- import { DePostalCodesEntryInterface } from '@bytebrand/car-basic-data-scope/dist/interfaces/DePostalCodesInterface';
23
- import { de, deVehicleDetailsPage, deVehicleProps } from '@bytebrand/i18n-dictionaries';
24
- import { DePostalCodesService } from '@bytebrand/car-basic-data-scope';
25
- import { ImboImageResponseInterface } from '@bytebrand/chunkable';
26
- import { TranslatorExtended } from '@bytebrand/schema-tools';
27
- import { DataSchemes } from '@bytebrand/car-schema';
28
-
29
- import { Description } from '../interfaces/description.interface';
30
- import { FbDescription } from './fb-description.transformer';
31
- import { ConfigService, ImagesService } from '../services';
32
- import { FbEquipment } from './fb-equipment.transformer';
33
- import { isValidVin } from '../utils';
34
- import { UserService } from 'src/services';
35
- import { ImageType } from '../enums';
36
- import {
37
- DEFAULT_ADDR1,
38
- DEFAULT_CITY,
39
- DEFAULT_ZIP,
40
- FB_MAX_TRIM_LENGTH,
41
- FB_MIN_MILEAGES,
42
- TIMESTAMP_FIRST_YEAR,
43
- } from '../constants/fb.constants';
44
-
45
- @Injectable()
46
- export class FbTransformer {
47
- private translator: TranslatorExtended;
48
- private postalCodesService: DePostalCodesService;
49
- private imagesService: ImagesService;
50
-
51
- constructor(
52
- @Inject(CACHE_MANAGER) private cacheManager: Cache,
53
- private readonly configService: ConfigService,
54
- private readonly userService: UserService,
55
- ) {
56
- this.imagesService = new ImagesService(configService, userService);
57
- const dict = { ...de, ...deVehicleProps, ...deVehicleDetailsPage };
58
- this.translator = new TranslatorExtended(dict);
59
- this.postalCodesService = DePostalCodesService.getInstance();
60
- }
61
-
62
- async transform({
63
- car,
64
- images,
65
- owner,
66
- }: {
67
- car: DataSchemes.Car;
68
- images: ImboImageResponseInterface[];
69
- owner: unknown;
70
- }): Promise<unknown> {
71
- const autodeId = toString(get(car, '_id', ''));
72
- const year = this.getYear(car);
73
- const dealerInfo = this.getDealerItems(car);
74
- // console.time(`Address ${car._id}`);
75
- const address = await this.getAddress(car, owner);
76
- // console.timeEnd(`Address ${car._id}`);
77
- // console.time(`Images ${car._id}`);
78
- const image_links = await this.imagesService.getImages(car, images, ImageType.marketing, false);
79
- // console.timeEnd(`Images ${car._id}`);
80
- const transformedImages = image_links.map(el => {
81
- return { url: el };
82
- });
83
-
84
- return {
85
- fb_page_id: '176306386132',
86
- ...address,
87
- ...dealerInfo,
88
- vehicle_id: get(car, 'metaData.vehicleId', autodeId),
89
- title: this.getTitle(car),
90
- features: this.getHighlights(car),
91
- description: this.getDescription(car), // be careful it changes car object
92
- url: `https://www.${this.configService.config.domain}/search/vehicle/${autodeId}`,
93
- dealer_url: `https://www.${this.configService.config.domain}/search/vehicle/${autodeId}?source=FACEBOOK&medium=direkt&campaign=marketplace_link`,
94
- make: toString(get(car, 'mainData.make', '')),
95
- model: toString(get(car, 'mainData.model', '')),
96
- trim: this.getTrim(car),
97
- image: transformedImages,
98
- year: toString(year),
99
- mileage: {
100
- value: this.getMileage(car),
101
- unit: 'KM',
102
- },
103
- drivetrain: this.mapDrivetrain(car),
104
- vin: this.getVin(car),
105
- body_style: this.mapBodyStyle(car),
106
- fuel_type: this.mapFuel(car),
107
- transmission: this.mapTransmission(car),
108
- price: this.getPrice(car),
109
- exterior_color: this.getExteriorColor(car),
110
- interior_color: this.getInteriorColor(car),
111
- state_of_vehicle: this.mapState(car),
112
- energy_efficiency_class_eu: this.getEnergyEfficiencyClass(car),
113
- vehicle_specifications: [
114
- {
115
- type: 'gas_mileage_combined',
116
- units: 'l/100km',
117
- value: get(car, 'consumption.consumptionCombined', ''),
118
- },
119
- {
120
- type: 'gas_mileage_city',
121
- units: 'l/100km',
122
- value: get(car, 'consumption.consumptionInner', ''),
123
- },
124
- {
125
- type: 'gas_mileage_highway',
126
- units: 'l/100km',
127
- value: get(car, 'consumption.consumptionOuter', ''),
128
- },
129
- {
130
- type: 'CO2_emissions',
131
- units: 'g/km',
132
- value: get(car, 'environmentEmissions.co2', ''),
133
- },
134
- ],
135
- };
136
- }
137
-
138
- private getYear(car: DataSchemes.Car): number {
139
- const firstModelsProductionDate = get(car, 'mainData.firstModelsProductionDate', 0) as number;
140
- const firstModelsProductionYear = moment(firstModelsProductionDate * 1000).format('YYYY');
141
-
142
- const firstRegistration = get(car, 'mainData.firstRegistration', 0) as number;
143
- const firstRegistrationYear = moment(firstRegistration * 1000).format('YYYY');
144
-
145
- const constructionYear = get(car, 'mainData.constructionYear', '') as string;
146
-
147
- const metaCreationDate = get(car, 'metaData.creationDate') as number;
148
- const metaCreationYear = moment(metaCreationDate * 1000).format('YYYY');
149
-
150
- const currentYear = moment().format('YYYY');
151
-
152
- return getFirstValidYear(
153
- firstModelsProductionYear,
154
- firstRegistrationYear,
155
- constructionYear,
156
- metaCreationYear,
157
- currentYear,
158
- );
159
- }
160
-
161
- private getDealerItems(car: DataSchemes.Car): {
162
- dealer_communication_channel: string;
163
- dealer_id: unknown;
164
- dealer_privacy_policy_url: string;
165
- dealer_email: string;
166
- dealer_phone: string;
167
- } {
168
- return {
169
- dealer_communication_channel: 'LEAD_FORM',
170
- dealer_id: get(car, 'metaData.ownerId'),
171
- dealer_privacy_policy_url: 'https://www.auto.de/einwilligungserklaerung-datenschutz-agb/',
172
- dealer_email: 'auto@auto.de',
173
- dealer_phone: '+498000008300',
174
- };
175
- }
176
-
177
- private getTitle(car: DataSchemes.Car): string {
178
- const year = toString(get(car, 'mainData.constructionYear', ''));
179
- const make = toString(get(car, 'mainData.make', ''));
180
- const model = toString(get(car, 'mainData.model', ''));
181
- const subModel = toString(get(car, 'mainData.subModel', ''));
182
- return trim(escape(join([year, make, model, subModel], ' ')));
183
- }
184
-
185
- private getTrim(car: DataSchemes.Car): string {
186
- const trim = toString(get(car, 'mainData.subModel', ''));
187
- return truncate(trim, { length: FB_MAX_TRIM_LENGTH, separator: ' ' });
188
- }
189
-
190
- private getDescription(car: DataSchemes.Car): Description[] {
191
- return FbDescription.create(car, this.translator, new FbEquipment()).getDescriptionArray();
192
- }
193
-
194
- private mapDrivetrain(car: DataSchemes.Car): string {
195
- const available = {
196
- selector_unknown: 'Other',
197
- selector_driveType_frontWheelDrive: 'FWD',
198
- selector_driveType_rearWheelDrive: 'RWD',
199
- selector_driveType_allWheelDrive: 'AWD',
200
- };
201
- const driveType = get(car, 'driveSuspension.driveType', 'selector_unknown');
202
- return get(available, driveType);
203
- }
204
-
205
- private getVin(car: DataSchemes.Car): string {
206
- let vin: string = get(car, 'metaData.vin', '');
207
-
208
- if (!isValidVin(vin)) {
209
- vin = get(car, 'marketing.fakeVIN', '');
210
- }
211
-
212
- return vin;
213
- }
214
-
215
- private mapBodyStyle(car: DataSchemes.Car): string {
216
- const available = {
217
- selector_unknown: 'OTHER',
218
- selector_category_combi: 'WAGON',
219
- selector_category_sedan: 'SEDAN',
220
- selector_category_compact: 'SMALL_CAR',
221
- selector_category_coupe: 'COUPE',
222
- selector_category_convertible: 'CONVERTIBLE',
223
- selector_category_suv: 'SUV',
224
- selector_category_van: 'VAN',
225
- selector_category_transporter: 'TRUCK',
226
- selector_category_minibus: 'MINIVAN',
227
- };
228
- const category = get(car, 'mainData.category', 'selector_unknown');
229
- return get(available, category);
230
- }
231
-
232
- private mapFuel(car: DataSchemes.Car): string {
233
- const available = {
234
- selector_fuel_petrol: 'GASOLINE',
235
- selector_fuel_diesel: 'DIESEL',
236
- selector_fuel_electrical: 'ELECTRIC',
237
- selector_fuel_hybridElectricalPetrol: 'HYBRID',
238
- selector_fuel_hybridElectricalDiesel: 'HYBRID',
239
- selector_fuel_carGas: 'OTHER',
240
- selector_fuel_naturalGas: 'OTHER',
241
- selector_fuel_ethanol: 'OTHER',
242
- selector_fuel_hydrogen: 'OTHER',
243
- selector_unknown: 'OTHER',
244
- };
245
- const fuel = get(car, 'consumption.fuel', 'selector_unknown');
246
- return get(available, fuel);
247
- }
248
-
249
- private mapTransmission(car: DataSchemes.Car): string {
250
- const available = {
251
- selector_gearbox_manualShift: 'MANUAL',
252
- selector_gearbox_semiAutomatic: 'AUTOMATIC',
253
- selector_gearbox_automatic: 'AUTOMATIC',
254
- selector_unknown: 'OTHER',
255
- };
256
- const transmission = get(car, 'driveSuspension.gearbox', 'selector_unknown');
257
- return get(available, transmission);
258
- }
259
-
260
- private getPrice(car: DataSchemes.Car): string {
261
- const price = get(car, 'price.currentSalesPrice', 0);
262
- return `${price} EUR`;
263
- }
264
-
265
- private async getAddress(
266
- car: DataSchemes.Car,
267
- owner: unknown,
268
- ): Promise<{
269
- address: { city: string; region: string; country: string; postal_code: string; addr1: string; };
270
- latitude: number;
271
- longitude: number;
272
- }> {
273
- let addressPath = 'businessAddress';
274
- if (this.isEmptyAddress(get(owner, addressPath, {}))) {
275
- addressPath = 'mainAddress';
276
- }
277
- let city: string = get(owner, `${addressPath}.city`, '');
278
- let zip: string = get(owner, `${addressPath}.zip`, '');
279
- let addr1 = get(owner, `${addressPath}.street`, '');
280
- let locationEntity = await this.getLocationByInfo({ city, zip: toSafeInteger(zip) });
281
-
282
- if (this.isEmptyLocation(locationEntity)) {
283
- addr1 = DEFAULT_ADDR1;
284
- city = DEFAULT_CITY;
285
- zip = DEFAULT_ZIP;
286
- locationEntity = await this.getLocationByInfo({ city, zip: toSafeInteger(zip) });
287
- }
288
-
289
- const coords = await this.getCoords(car, locationEntity);
290
- const country: string = get(owner, `${addressPath}.country.name`, '');
291
- return {
292
- ...coords,
293
- address: {
294
- addr1,
295
- city,
296
- region: get(locationEntity, 'Place Name', ''),
297
- country: country || 'Deutschland',
298
- postal_code: zip,
299
- },
300
- };
301
- }
302
-
303
- private isEmptyLocation(location: DePostalCodesEntryInterface): boolean {
304
- if (!location) return true;
305
- return !location['Place Name'] || !(location.Latitude && location.Longitude);
306
- }
307
-
308
- private isEmptyAddress(address: unknown): boolean {
309
- if (!address) return true;
310
- return ['city', 'zip', 'country'].every((key: string) => isEmpty(address[key]));
311
- }
312
-
313
- private async getCoords(
314
- car: DataSchemes.Car,
315
- locationEntry: DePostalCodesEntryInterface,
316
- ): Promise<Pick<DataSchemes.Location, 'latitude' | 'longitude'>> {
317
- const data: Pick<DataSchemes.Location, 'latitude' | 'longitude'> = {
318
- latitude: get(car, 'location.latitude'),
319
- longitude: get(car, 'location.longitude'),
320
- };
321
-
322
- if (data.latitude && data.longitude) {
323
- return data;
324
- }
325
-
326
- return {
327
- longitude: get(locationEntry, 'Longitude'),
328
- latitude: get(locationEntry, 'Latitude'),
329
- };
330
- }
331
-
332
- private async getLocationByInfo(info: { zip: number; city: string }): Promise<DePostalCodesEntryInterface> {
333
- const cacheKey = `${info.city}_${info.zip}`;
334
-
335
- let entry = await this.cacheManager.get(cacheKey);
336
- if (entry) {
337
- return entry as DePostalCodesEntryInterface;
338
- }
339
- if (entry === 'no_data') {
340
- return undefined;
341
- }
342
-
343
- entry = await this.postalCodesService.getLocationByPostalCode(info.zip);
344
- entry && (await this.cacheManager.set(cacheKey, entry));
345
-
346
- if (!entry) {
347
- entry = await this.postalCodesService.getLocationByCity(info.city);
348
- if (entry) {
349
- await this.cacheManager.set(cacheKey, entry);
350
- } else {
351
- await this.cacheManager.set(cacheKey, 'no_data');
352
- }
353
- }
354
- return entry as DePostalCodesEntryInterface;
355
- }
356
-
357
- private getExteriorColor(car: DataSchemes.Car): string {
358
- let colorSelection = get(car, 'exterior.exteriorColor', 'other');
359
- if (!colorSelection || colorSelection === 'selector_unknown') {
360
- colorSelection = 'other';
361
- }
362
- return this.translator.translateVal(colorSelection);
363
- }
364
-
365
- private getInteriorColor(car: DataSchemes.Car): string {
366
- let colorSelection = get(car, 'interior.interiorColor', 'other');
367
- if (!colorSelection || colorSelection === 'selector_unknown') {
368
- colorSelection = 'other';
369
- }
370
- return this.translator.translateVal(colorSelection);
371
- }
372
-
373
- private mapState(car: DataSchemes.Car): string {
374
- const available = {
375
- selector_condition_new: 'CPO',
376
- selector_condition_used: 'USED',
377
- selector_unknown: 'USED',
378
- };
379
- const condition = get(car, 'mainData.condition', 'selector_unknown');
380
- return get(available, condition);
381
- }
382
-
383
- private getEnergyEfficiencyClass(car: DataSchemes.Car): string {
384
- const available = {
385
- selector_energyEfficiencyClass_appp: 'A+',
386
- selector_energyEfficiencyClass_app: 'A+',
387
- selector_energyEfficiencyClass_ap: 'A+',
388
- selector_energyEfficiencyClass_a: 'A',
389
- selector_energyEfficiencyClass_b: 'B',
390
- selector_energyEfficiencyClass_c: 'C',
391
- selector_energyEfficiencyClass_d: 'D',
392
- selector_energyEfficiencyClass_e: 'E',
393
- selector_energyEfficiencyClass_f: 'F',
394
- selector_energyEfficiencyClass_g: 'G',
395
- selector_unknown: 'G',
396
- };
397
- const energyClass = get(car, 'environmentEmissions.energyEfficiencyClass', 'selector_unknown');
398
- return get(available, energyClass);
399
- }
400
-
401
- private getMileage(car: DataSchemes.Car): number {
402
- const mileage = get(car, 'mainData.mileage', 0);
403
- return mileage <= FB_MIN_MILEAGES ? FB_MIN_MILEAGES + 1 : mileage;
404
- }
405
-
406
- private getHighlights(car: DataSchemes.Car): { type: unknown; value: unknown }[] {
407
- const facebookFeatures = [
408
- { key: 'assistants.abs', type: 'abs_brakes', value: 'ABS' },
409
- { key: 'assistants.adaptiveCruiseControl', type: 'adaptive_cruise_control', value: 'Abstandsregeltempomat' },
410
- { key: 'theftProtection.alarmSystem', type: 'alarm', value: 'Alarmanlage' },
411
- { key: 'multimedia.auxiliaryAudioInput', type: 'aux_audio_input', value: 'AUX-Anschluss' },
412
- { key: 'assistants.blindSpotDetection', type: 'blind_spot_alert', value: 'Totwinkel-Assistent' },
413
- { key: 'multimedia.bluetooth', type: 'bluetooth', value: 'Bluetooth' },
414
- { key: 'assistants.collisionAvoidance', type: 'collision_avoidance', value: 'Notbrems Assistent' },
415
- { key: 'assistants.cruiseControl', type: 'cruise_control', value: 'Tempomat' },
416
- { key: 'lights.daytimeRunningLights', type: 'daytime_running_lights', value: 'Tagfahrlicht' },
417
- { key: 'lights.daytimeRunningLightsLED', type: 'daytime_running_lights', value: 'Tagfahrlicht LED' },
418
- { key: 'lights.frontFogLights', type: 'fog_lights', value: 'Nebelscheinwerfer' },
419
- { key: 'wheel.heatedSteeringWheel', type: 'heated_steering_wheel', value: 'Lenkradheizung' },
420
- { key: 'lights.xenonHeadlights', type: 'hid_headlights', value: 'Xenonscheinwerfer' },
421
- { key: 'assistants.hillStartDrivingAssistant', type: 'hill_start_assist', value: 'Berganfahrhilfe' },
422
- { key: 'doors.keylessEntry', type: 'keyless_entry', value: 'Keyless-Go' },
423
- { key: 'doors.keylessEntry', type: 'keyless_start', value: 'Keyless-Go' },
424
- { key: 'assistants.laneAssistant', type: 'lane_keep_assist', value: 'Spurhalte-Assistent' },
425
- { key: 'lights.ledHeadlights', type: 'led_headlights', value: 'LED Scheinwerfer' },
426
- { key: 'window.sunroof', type: 'moon_roof', value: 'Schiebedach' },
427
- { key: 'multimedia.mp3Interface', type: 'mp3 MP3', value: 'Schnittstelle' },
428
- { key: 'multimedia.navigationSystem', type: 'navigation', value: 'Navigationssystem' },
429
- { key: 'assistants.nightVisionAssistant', type: 'night_view', value: 'Nachsicht Assistent' },
430
- { key: 'window.panoramicGlassRoof', type: 'panorama_roof', value: 'Panoramaschiebedach' },
431
- { key: 'doors.centralLocking', type: 'power_door_locks', value: 'Zentralverriegelung' },
432
- { key: 'doors.electricTailGate', type: 'power_liftgate', value: 'elektrische Heckklappe' },
433
- { key: 'assistants.powerAssistedSteering', type: 'power_steering', value: 'powerAssistedSteering' },
434
- { key: 'window.electricWindows', type: 'power_windows', value: 'Elektrische Fenster' },
435
- { key: 'extras.roofBars', type: 'roof_rack', value: 'Dachgepäckträger' },
436
- { key: 'extras.roofRails', type: 'roof_rack', value: 'Dachreling' },
437
- { key: 'assistants.esp', type: 'stability_control', value: 'ESP' },
438
- { key: 'multimedia.onBoardComputer', type: 'telematics', value: 'Bordcomputer' },
439
- { key: 'wheel.adjustableSteeringWheel', type: 'tilt_steering_wheel', value: 'höhenverst Lenkrad' },
440
- { key: 'window.tintedWindowBack', type: 'tinted_windows', value: 'getönte Scheiben hinten' },
441
- { key: 'assistants.tirePressureMonitor', type: 'tire_pressure_monitor', value: 'Reifendruckkontrolle' },
442
- { key: 'trailerCoupling.trailerCouplingFix', type: 'tow_package', value: 'Anhängerkupplung fest' },
443
- { key: 'trailerCoupling.trailerCouplingRemovable', type: 'tow_package', value: 'Anhängerkupplung abnehmbar' },
444
- { key: 'trailerCoupling.trailerCouplingRotatable', type: 'tow_package', value: 'Anhängerkupplung schwenkbar' },
445
- { key: 'assistants.tractionControlSystem', type: 'traction_control', value: 'Traktionskontrolle' },
446
- {
447
- key: 'heatingCooling.climatisation',
448
- type: 'air_conditioning',
449
- selector: 'selector_unknown',
450
- value: 'Unbekannt',
451
- },
452
- {
453
- key: 'heatingCooling.climatisation',
454
- type: 'air_conditioning',
455
- selector: 'selector_climatisation_no_airCondition',
456
- value: 'Keine Klimaanlage oder -automatik',
457
- },
458
- {
459
- key: 'heatingCooling.climatisation',
460
- type: 'air_conditioning',
461
- selector: 'selector_climatisation_airCondition',
462
- value: 'Klimaanlage oder -automatik',
463
- },
464
- {
465
- key: 'heatingCooling.climatisation',
466
- type: 'air_conditioning',
467
- selector: 'selector_climatisation_automaticAirCondition',
468
- value: 'Klimaautomatik',
469
- },
470
- {
471
- key: 'heatingCooling.climatisation',
472
- type: 'air_conditioning',
473
- selector: 'selector_climatisation_2_automaticAirCondition',
474
- value: '2-Zonen Klimaautomatik',
475
- },
476
- {
477
- key: 'heatingCooling.climatisation',
478
- type: 'air_conditioning',
479
- selector: 'selector_climatisation_3_automaticAirCondition',
480
- value: '3-Zonen Klimaautomatik',
481
- },
482
- {
483
- key: 'heatingCooling.climatisation',
484
- type: 'air_conditioning',
485
- selector: 'selector_climatisation_4_automaticAirCondition',
486
- value: '4-Zonen Klimaautomatik',
487
- },
488
- {
489
- key: 'wheels.summerTiresRims',
490
- type: 'alloy_wheels',
491
- selector: 'selector_tiresRims_alloy',
492
- value: 'Leichtmetallfelgen',
493
- },
494
- {
495
- key: 'assistants.parkingAssistants',
496
- type: 'backup_camera',
497
- selector: 'selector_parkingAssistants_camera',
498
- value: 'Rückfahrkamera',
499
- },
500
- {
501
- key: 'seats.driverSeats',
502
- type: 'heated_seats',
503
- selector: 'selector_driverSeats_electricHeated',
504
- value: 'Sitzheizung Fahrersitz',
505
- },
506
- {
507
- key: 'seats.coDriverSeats',
508
- type: 'heated_seats',
509
- selector: 'selector_coDriverSeats_electricHeated',
510
- value: 'Sitzheizung Beifahrersitz',
511
- },
512
- {
513
- key: 'seats.backSeats',
514
- type: 'heated_seats',
515
- selector: 'selector_backSeats_electricHeated',
516
- value: 'Sitzheizung Rücksitz',
517
- },
518
- {
519
- key: 'interior.interiorType',
520
- type: 'leather',
521
- selector: 'selector_interiorType_fullLeather',
522
- value: 'Vollleder',
523
- },
524
- {
525
- key: 'interior.interiorType',
526
- type: 'leather',
527
- selector: 'selector_interiorType_partLeather',
528
- value: 'Teilleder',
529
- },
530
- { key: 'assistants.parkingAssistants', type: 'park_assist', selector: 'selector_unknown', value: 'Unbekannt' },
531
- {
532
- key: 'assistants.parkingAssistants',
533
- type: 'park_assist',
534
- selector: 'selector_parkingAssistants_other',
535
- value: 'Andere',
536
- },
537
- {
538
- key: 'assistants.parkingAssistants',
539
- type: 'park_assist',
540
- selector: 'selector_parkingAssistants_front',
541
- value: 'Vorderseite',
542
- },
543
- {
544
- key: 'assistants.parkingAssistants',
545
- type: 'park_assist',
546
- selector: 'selector_parkingAssistants_rear',
547
- value: 'Rückseite',
548
- },
549
- {
550
- key: 'assistants.parkingAssistants',
551
- type: 'park_assist',
552
- selector: 'selector_parkingAssistants_camera',
553
- value: 'Kamera',
554
- },
555
- {
556
- key: 'assistants.parkingAssistants',
557
- type: 'park_assist',
558
- selector: 'selector_parkingAssistants_selfSteeringSystems',
559
- value: 'Selbstlenkende Systeme',
560
- },
561
- {
562
- key: 'assistants.parkingAssistants',
563
- type: 'park_assist',
564
- selector: 'selector_parkingAssistants_360Camera',
565
- value: '360° Kamera',
566
- },
567
- {
568
- key: 'seats.driverSeats',
569
- type: 'power_seats',
570
- selector: 'selector_driverSeats_electricAdjustable',
571
- value: 'Fahrersitz elektr. einstellbar',
572
- },
573
- {
574
- key: 'seats.coDriverSeats',
575
- type: 'power_seats',
576
- selector: 'selector_coDriverSeats_electricAdjustable',
577
- value: 'Beifahrersitz elektr. einstellbar',
578
- },
579
- {
580
- key: 'assistants.parkingAssistants',
581
- type: 'surround_view_camera',
582
- selector: 'selector_parkingAssistants_360Camera',
583
- value: '360° Kamera',
584
- },
585
- {
586
- key: 'seats.driverSeats',
587
- type: 'ventilated_seats',
588
- selector: 'selector_driverSeats_seatVentilationSystem',
589
- value: 'Sitzbelüftung Fahrersitz',
590
- },
591
- {
592
- key: 'seats.coDriverSeats',
593
- type: 'ventilated_seats',
594
- selector: 'selector_coDriverSeats_seatVentilationSystem',
595
- value: 'Sitzbelüftung Beifahrersitz',
596
- },
597
- {
598
- key: 'seats.backSeats',
599
- type: 'ventilated_seats',
600
- selector: 'selector_backSeats_seatVentilationSystem',
601
- value: 'Sitzbelüftung Rücksitz',
602
- },
603
- ];
604
- return facebookFeatures
605
- .map(feature => {
606
- const value = get(car, feature.key);
607
- if (isBoolean(value)) {
608
- return this.getBooleanValue(value, feature);
609
- }
610
- if (isString(value)) {
611
- return this.getStringValue(value, feature);
612
- }
613
- if (isArray(value) && includes(value, feature.selector)) {
614
- const index = findIndex(value, item => item === feature.selector);
615
- return this.getStringValue(value[index], feature);
616
- }
617
- return null;
618
- })
619
- .filter(el => el);
620
- }
621
-
622
- private getBooleanValue(
623
- value: boolean,
624
- feature:
625
- | { key: string; type: string; value: string; selector?: undefined }
626
- | { key: string; type: string; selector: string; value: string },
627
- ): { type: string; value: string } {
628
- if (value) {
629
- return {
630
- type: feature.type,
631
- value: feature.value,
632
- };
633
- }
634
- return null;
635
- }
636
-
637
- private getStringValue(
638
- selector: string,
639
- feature:
640
- | { key: string; type: string; value: string; selector?: undefined }
641
- | { key: string; type: string; selector: string; value: string },
642
- ): { type: string; value: string } {
643
- if (selector === feature.selector) {
644
- return {
645
- type: feature.type,
646
- value: feature.value,
647
- };
648
- }
649
- return null;
650
- }
651
- }
652
-
653
- function getFirstValidYear(...years: string[]): number {
654
- return years.map((year: string) => toSafeInteger(year)).filter((year: number) => year > TIMESTAMP_FIRST_YEAR)[0];
655
- }
1
+ import { Cache } from 'cache-manager';
2
+ import * as moment from 'moment';
3
+ import {
4
+ escape,
5
+ findIndex,
6
+ get,
7
+ includes,
8
+ isArray,
9
+ isBoolean,
10
+ isEmpty,
11
+ isString,
12
+ join,
13
+ toSafeInteger,
14
+ toString,
15
+ trim,
16
+ truncate,
17
+ } from 'lodash';
18
+
19
+ import { CACHE_MANAGER } from '@nestjs/cache-manager';
20
+ import { Inject, Injectable } from '@nestjs/common';
21
+
22
+ import { DePostalCodesEntryInterface } from '@bytebrand/car-basic-data-scope/dist/interfaces/DePostalCodesInterface';
23
+ import { de, deVehicleDetailsPage, deVehicleProps } from '@bytebrand/i18n-dictionaries';
24
+ import { DePostalCodesService } from '@bytebrand/car-basic-data-scope';
25
+ import { ImboImageResponseInterface } from '@bytebrand/chunkable';
26
+ import { TranslatorExtended } from '@bytebrand/schema-tools';
27
+ import { DataSchemes } from '@bytebrand/car-schema';
28
+
29
+ import { Description } from '../interfaces/description.interface';
30
+ import { FbDescription } from './fb-description.transformer';
31
+ import { ConfigService, ImagesService } from '../services';
32
+ import { FbEquipment } from './fb-equipment.transformer';
33
+ import { isValidVin } from '../utils';
34
+ import { UserService } from 'src/services';
35
+ import { ImageType } from '../enums';
36
+ import {
37
+ DEFAULT_ADDR1,
38
+ DEFAULT_CITY,
39
+ DEFAULT_ZIP,
40
+ FB_MAX_TRIM_LENGTH,
41
+ FB_MIN_MILEAGES,
42
+ TIMESTAMP_FIRST_YEAR,
43
+ } from '../constants/fb.constants';
44
+
45
+ @Injectable()
46
+ export class FbTransformer {
47
+ private translator: TranslatorExtended;
48
+ private postalCodesService: DePostalCodesService;
49
+ private imagesService: ImagesService;
50
+
51
+ constructor(
52
+ @Inject(CACHE_MANAGER) private cacheManager: Cache,
53
+ private readonly configService: ConfigService,
54
+ private readonly userService: UserService,
55
+ ) {
56
+ this.imagesService = new ImagesService(configService, userService);
57
+ const dict = { ...de, ...deVehicleProps, ...deVehicleDetailsPage };
58
+ this.translator = new TranslatorExtended(dict);
59
+ this.postalCodesService = DePostalCodesService.getInstance();
60
+ }
61
+
62
+ async transform({
63
+ car,
64
+ images,
65
+ owner,
66
+ }: {
67
+ car: DataSchemes.Car;
68
+ images: ImboImageResponseInterface[];
69
+ owner: unknown;
70
+ }): Promise<unknown> {
71
+ const autodeId = toString(get(car, '_id', ''));
72
+ const year = this.getYear(car);
73
+ const dealerInfo = this.getDealerItems(car);
74
+ // console.time(`Address ${car._id}`);
75
+ const address = await this.getAddress(car, owner);
76
+ // console.timeEnd(`Address ${car._id}`);
77
+ // console.time(`Images ${car._id}`);
78
+ const image_links = await this.imagesService.getImages(car, images, ImageType.marketing, false);
79
+ // console.timeEnd(`Images ${car._id}`);
80
+ const transformedImages = image_links.map(el => {
81
+ return { url: el };
82
+ });
83
+
84
+ return {
85
+ fb_page_id: '176306386132',
86
+ ...address,
87
+ ...dealerInfo,
88
+ vehicle_id: get(car, 'metaData.vehicleId', autodeId),
89
+ title: this.getTitle(car),
90
+ features: this.getHighlights(car),
91
+ description: this.getDescription(car), // be careful it changes car object
92
+ url: `https://www.${this.configService.config.domain}/search/vehicle/${autodeId}`,
93
+ dealer_url: `https://www.${this.configService.config.domain}/search/vehicle/${autodeId}?source=FACEBOOK&medium=direkt&campaign=marketplace_link`,
94
+ make: toString(get(car, 'mainData.make', '')),
95
+ model: toString(get(car, 'mainData.model', '')),
96
+ trim: this.getTrim(car),
97
+ image: transformedImages,
98
+ year: toString(year),
99
+ mileage: {
100
+ value: this.getMileage(car),
101
+ unit: 'KM',
102
+ },
103
+ drivetrain: this.mapDrivetrain(car),
104
+ vin: this.getVin(car),
105
+ body_style: this.mapBodyStyle(car),
106
+ fuel_type: this.mapFuel(car),
107
+ transmission: this.mapTransmission(car),
108
+ price: this.getPrice(car),
109
+ exterior_color: this.getExteriorColor(car),
110
+ interior_color: this.getInteriorColor(car),
111
+ state_of_vehicle: this.mapState(car),
112
+ energy_efficiency_class_eu: this.getEnergyEfficiencyClass(car),
113
+ vehicle_specifications: [
114
+ {
115
+ type: 'gas_mileage_combined',
116
+ units: 'l/100km',
117
+ value: get(car, 'consumption.consumptionCombined', ''),
118
+ },
119
+ {
120
+ type: 'gas_mileage_city',
121
+ units: 'l/100km',
122
+ value: get(car, 'consumption.consumptionInner', ''),
123
+ },
124
+ {
125
+ type: 'gas_mileage_highway',
126
+ units: 'l/100km',
127
+ value: get(car, 'consumption.consumptionOuter', ''),
128
+ },
129
+ {
130
+ type: 'CO2_emissions',
131
+ units: 'g/km',
132
+ value: get(car, 'environmentEmissions.co2', ''),
133
+ },
134
+ ],
135
+ };
136
+ }
137
+
138
+ private getYear(car: DataSchemes.Car): number {
139
+ const firstModelsProductionDate = get(car, 'mainData.firstModelsProductionDate', 0) as number;
140
+ const firstModelsProductionYear = moment(firstModelsProductionDate * 1000).format('YYYY');
141
+
142
+ const firstRegistration = get(car, 'mainData.firstRegistration', 0) as number;
143
+ const firstRegistrationYear = moment(firstRegistration * 1000).format('YYYY');
144
+
145
+ const constructionYear = get(car, 'mainData.constructionYear', '') as string;
146
+
147
+ const metaCreationDate = get(car, 'metaData.creationDate') as number;
148
+ const metaCreationYear = moment(metaCreationDate * 1000).format('YYYY');
149
+
150
+ const currentYear = moment().format('YYYY');
151
+
152
+ return getFirstValidYear(
153
+ firstModelsProductionYear,
154
+ firstRegistrationYear,
155
+ constructionYear,
156
+ metaCreationYear,
157
+ currentYear,
158
+ );
159
+ }
160
+
161
+ private getDealerItems(car: DataSchemes.Car): {
162
+ dealer_communication_channel: string;
163
+ dealer_id: unknown;
164
+ dealer_privacy_policy_url: string;
165
+ dealer_email: string;
166
+ dealer_phone: string;
167
+ } {
168
+ return {
169
+ dealer_communication_channel: 'LEAD_FORM',
170
+ dealer_id: get(car, 'metaData.ownerId'),
171
+ dealer_privacy_policy_url: 'https://www.auto.de/einwilligungserklaerung-datenschutz-agb/',
172
+ dealer_email: 'auto@auto.de',
173
+ dealer_phone: '+498000008300',
174
+ };
175
+ }
176
+
177
+ private getTitle(car: DataSchemes.Car): string {
178
+ const year = toString(get(car, 'mainData.constructionYear', ''));
179
+ const make = toString(get(car, 'mainData.make', ''));
180
+ const model = toString(get(car, 'mainData.model', ''));
181
+ const subModel = toString(get(car, 'mainData.subModel', ''));
182
+ return trim(escape(join([year, make, model, subModel], ' ')));
183
+ }
184
+
185
+ private getTrim(car: DataSchemes.Car): string {
186
+ const trim = toString(get(car, 'mainData.subModel', ''));
187
+ return truncate(trim, { length: FB_MAX_TRIM_LENGTH, separator: ' ' });
188
+ }
189
+
190
+ private getDescription(car: DataSchemes.Car): Description[] {
191
+ return FbDescription.create(car, this.translator, new FbEquipment()).getDescriptionArray();
192
+ }
193
+
194
+ private mapDrivetrain(car: DataSchemes.Car): string {
195
+ const available = {
196
+ selector_unknown: 'Other',
197
+ selector_driveType_frontWheelDrive: 'FWD',
198
+ selector_driveType_rearWheelDrive: 'RWD',
199
+ selector_driveType_allWheelDrive: 'AWD',
200
+ };
201
+ const driveType = get(car, 'driveSuspension.driveType', 'selector_unknown');
202
+ return get(available, driveType);
203
+ }
204
+
205
+ private getVin(car: DataSchemes.Car): string {
206
+ let vin: string = get(car, 'metaData.vin', '');
207
+
208
+ if (!isValidVin(vin)) {
209
+ vin = get(car, 'marketing.fakeVIN', '');
210
+ }
211
+
212
+ return vin;
213
+ }
214
+
215
+ private mapBodyStyle(car: DataSchemes.Car): string {
216
+ const available = {
217
+ selector_unknown: 'OTHER',
218
+ selector_category_combi: 'WAGON',
219
+ selector_category_sedan: 'SEDAN',
220
+ selector_category_compact: 'SMALL_CAR',
221
+ selector_category_coupe: 'COUPE',
222
+ selector_category_convertible: 'CONVERTIBLE',
223
+ selector_category_suv: 'SUV',
224
+ selector_category_van: 'VAN',
225
+ selector_category_transporter: 'TRUCK',
226
+ selector_category_minibus: 'MINIVAN',
227
+ };
228
+ const category = get(car, 'mainData.category', 'selector_unknown');
229
+ return get(available, category);
230
+ }
231
+
232
+ private mapFuel(car: DataSchemes.Car): string {
233
+ const available = {
234
+ selector_fuel_petrol: 'GASOLINE',
235
+ selector_fuel_diesel: 'DIESEL',
236
+ selector_fuel_electrical: 'ELECTRIC',
237
+ selector_fuel_hybridElectricalPetrol: 'HYBRID',
238
+ selector_fuel_hybridElectricalDiesel: 'HYBRID',
239
+ selector_fuel_carGas: 'OTHER',
240
+ selector_fuel_naturalGas: 'OTHER',
241
+ selector_fuel_ethanol: 'OTHER',
242
+ selector_fuel_hydrogen: 'OTHER',
243
+ selector_unknown: 'OTHER',
244
+ };
245
+ const fuel = get(car, 'consumption.fuel', 'selector_unknown');
246
+ return get(available, fuel);
247
+ }
248
+
249
+ private mapTransmission(car: DataSchemes.Car): string {
250
+ const available = {
251
+ selector_gearbox_manualShift: 'MANUAL',
252
+ selector_gearbox_semiAutomatic: 'AUTOMATIC',
253
+ selector_gearbox_automatic: 'AUTOMATIC',
254
+ selector_unknown: 'OTHER',
255
+ };
256
+ const transmission = get(car, 'driveSuspension.gearbox', 'selector_unknown');
257
+ return get(available, transmission);
258
+ }
259
+
260
+ private getPrice(car: DataSchemes.Car): string {
261
+ const price = get(car, 'price.currentSalesPrice', 0);
262
+ return `${price} EUR`;
263
+ }
264
+
265
+ private async getAddress(
266
+ car: DataSchemes.Car,
267
+ owner: unknown,
268
+ ): Promise<{
269
+ address: { city: string; region: string; country: string; postal_code: string; addr1: string; };
270
+ latitude: number;
271
+ longitude: number;
272
+ }> {
273
+ let addressPath = 'businessAddress';
274
+ if (this.isEmptyAddress(get(owner, addressPath, {}))) {
275
+ addressPath = 'mainAddress';
276
+ }
277
+ let city: string = get(owner, `${addressPath}.city`, '');
278
+ let zip: string = get(owner, `${addressPath}.zip`, '');
279
+ let addr1 = get(owner, `${addressPath}.street`, '');
280
+ let locationEntity = await this.getLocationByInfo({ city, zip: toSafeInteger(zip) });
281
+
282
+ if (this.isEmptyLocation(locationEntity)) {
283
+ addr1 = DEFAULT_ADDR1;
284
+ city = DEFAULT_CITY;
285
+ zip = DEFAULT_ZIP;
286
+ locationEntity = await this.getLocationByInfo({ city, zip: toSafeInteger(zip) });
287
+ }
288
+
289
+ const coords = await this.getCoords(car, locationEntity);
290
+ const country: string = get(owner, `${addressPath}.country.name`, '');
291
+ return {
292
+ ...coords,
293
+ address: {
294
+ addr1,
295
+ city,
296
+ region: get(locationEntity, 'Place Name', ''),
297
+ country: country || 'Deutschland',
298
+ postal_code: zip,
299
+ },
300
+ };
301
+ }
302
+
303
+ private isEmptyLocation(location: DePostalCodesEntryInterface): boolean {
304
+ if (!location) return true;
305
+ return !location['Place Name'] || !(location.Latitude && location.Longitude);
306
+ }
307
+
308
+ private isEmptyAddress(address: unknown): boolean {
309
+ if (!address) return true;
310
+ return ['city', 'zip', 'country'].every((key: string) => isEmpty(address[key]));
311
+ }
312
+
313
+ private async getCoords(
314
+ car: DataSchemes.Car,
315
+ locationEntry: DePostalCodesEntryInterface,
316
+ ): Promise<Pick<DataSchemes.Location, 'latitude' | 'longitude'>> {
317
+ const data: Pick<DataSchemes.Location, 'latitude' | 'longitude'> = {
318
+ latitude: get(car, 'location.latitude'),
319
+ longitude: get(car, 'location.longitude'),
320
+ };
321
+
322
+ if (data.latitude && data.longitude) {
323
+ return data;
324
+ }
325
+
326
+ return {
327
+ longitude: get(locationEntry, 'Longitude'),
328
+ latitude: get(locationEntry, 'Latitude'),
329
+ };
330
+ }
331
+
332
+ private async getLocationByInfo(info: { zip: number; city: string }): Promise<DePostalCodesEntryInterface> {
333
+ const cacheKey = `${info.city}_${info.zip}`;
334
+
335
+ let entry = await this.cacheManager.get(cacheKey);
336
+ if (entry) {
337
+ return entry as DePostalCodesEntryInterface;
338
+ }
339
+ if (entry === 'no_data') {
340
+ return undefined;
341
+ }
342
+
343
+ entry = await this.postalCodesService.getLocationByPostalCode(info.zip);
344
+ entry && (await this.cacheManager.set(cacheKey, entry));
345
+
346
+ if (!entry) {
347
+ entry = await this.postalCodesService.getLocationByCity(info.city);
348
+ if (entry) {
349
+ await this.cacheManager.set(cacheKey, entry);
350
+ } else {
351
+ await this.cacheManager.set(cacheKey, 'no_data');
352
+ }
353
+ }
354
+ return entry as DePostalCodesEntryInterface;
355
+ }
356
+
357
+ private getExteriorColor(car: DataSchemes.Car): string {
358
+ let colorSelection = get(car, 'exterior.exteriorColor', 'other');
359
+ if (!colorSelection || colorSelection === 'selector_unknown') {
360
+ colorSelection = 'other';
361
+ }
362
+ return this.translator.translateVal(colorSelection);
363
+ }
364
+
365
+ private getInteriorColor(car: DataSchemes.Car): string {
366
+ let colorSelection = get(car, 'interior.interiorColor', 'other');
367
+ if (!colorSelection || colorSelection === 'selector_unknown') {
368
+ colorSelection = 'other';
369
+ }
370
+ return this.translator.translateVal(colorSelection);
371
+ }
372
+
373
+ private mapState(car: DataSchemes.Car): string {
374
+ const available = {
375
+ selector_condition_new: 'CPO',
376
+ selector_condition_used: 'USED',
377
+ selector_unknown: 'USED',
378
+ };
379
+ const condition = get(car, 'mainData.condition', 'selector_unknown');
380
+ return get(available, condition);
381
+ }
382
+
383
+ private getEnergyEfficiencyClass(car: DataSchemes.Car): string {
384
+ const available = {
385
+ selector_energyEfficiencyClass_appp: 'A+',
386
+ selector_energyEfficiencyClass_app: 'A+',
387
+ selector_energyEfficiencyClass_ap: 'A+',
388
+ selector_energyEfficiencyClass_a: 'A',
389
+ selector_energyEfficiencyClass_b: 'B',
390
+ selector_energyEfficiencyClass_c: 'C',
391
+ selector_energyEfficiencyClass_d: 'D',
392
+ selector_energyEfficiencyClass_e: 'E',
393
+ selector_energyEfficiencyClass_f: 'F',
394
+ selector_energyEfficiencyClass_g: 'G',
395
+ selector_unknown: 'G',
396
+ };
397
+ const energyClass = get(car, 'environmentEmissions.energyEfficiencyClass', 'selector_unknown');
398
+ return get(available, energyClass);
399
+ }
400
+
401
+ private getMileage(car: DataSchemes.Car): number {
402
+ const mileage = get(car, 'mainData.mileage', 0);
403
+ return mileage <= FB_MIN_MILEAGES ? FB_MIN_MILEAGES + 1 : mileage;
404
+ }
405
+
406
+ private getHighlights(car: DataSchemes.Car): { type: unknown; value: unknown }[] {
407
+ const facebookFeatures = [
408
+ { key: 'assistants.abs', type: 'abs_brakes', value: 'ABS' },
409
+ { key: 'assistants.adaptiveCruiseControl', type: 'adaptive_cruise_control', value: 'Abstandsregeltempomat' },
410
+ { key: 'theftProtection.alarmSystem', type: 'alarm', value: 'Alarmanlage' },
411
+ { key: 'multimedia.auxiliaryAudioInput', type: 'aux_audio_input', value: 'AUX-Anschluss' },
412
+ { key: 'assistants.blindSpotDetection', type: 'blind_spot_alert', value: 'Totwinkel-Assistent' },
413
+ { key: 'multimedia.bluetooth', type: 'bluetooth', value: 'Bluetooth' },
414
+ { key: 'assistants.collisionAvoidance', type: 'collision_avoidance', value: 'Notbrems Assistent' },
415
+ { key: 'assistants.cruiseControl', type: 'cruise_control', value: 'Tempomat' },
416
+ { key: 'lights.daytimeRunningLights', type: 'daytime_running_lights', value: 'Tagfahrlicht' },
417
+ { key: 'lights.daytimeRunningLightsLED', type: 'daytime_running_lights', value: 'Tagfahrlicht LED' },
418
+ { key: 'lights.frontFogLights', type: 'fog_lights', value: 'Nebelscheinwerfer' },
419
+ { key: 'wheel.heatedSteeringWheel', type: 'heated_steering_wheel', value: 'Lenkradheizung' },
420
+ { key: 'lights.xenonHeadlights', type: 'hid_headlights', value: 'Xenonscheinwerfer' },
421
+ { key: 'assistants.hillStartDrivingAssistant', type: 'hill_start_assist', value: 'Berganfahrhilfe' },
422
+ { key: 'doors.keylessEntry', type: 'keyless_entry', value: 'Keyless-Go' },
423
+ { key: 'doors.keylessEntry', type: 'keyless_start', value: 'Keyless-Go' },
424
+ { key: 'assistants.laneAssistant', type: 'lane_keep_assist', value: 'Spurhalte-Assistent' },
425
+ { key: 'lights.ledHeadlights', type: 'led_headlights', value: 'LED Scheinwerfer' },
426
+ { key: 'window.sunroof', type: 'moon_roof', value: 'Schiebedach' },
427
+ { key: 'multimedia.mp3Interface', type: 'mp3 MP3', value: 'Schnittstelle' },
428
+ { key: 'multimedia.navigationSystem', type: 'navigation', value: 'Navigationssystem' },
429
+ { key: 'assistants.nightVisionAssistant', type: 'night_view', value: 'Nachsicht Assistent' },
430
+ { key: 'window.panoramicGlassRoof', type: 'panorama_roof', value: 'Panoramaschiebedach' },
431
+ { key: 'doors.centralLocking', type: 'power_door_locks', value: 'Zentralverriegelung' },
432
+ { key: 'doors.electricTailGate', type: 'power_liftgate', value: 'elektrische Heckklappe' },
433
+ { key: 'assistants.powerAssistedSteering', type: 'power_steering', value: 'powerAssistedSteering' },
434
+ { key: 'window.electricWindows', type: 'power_windows', value: 'Elektrische Fenster' },
435
+ { key: 'extras.roofBars', type: 'roof_rack', value: 'Dachgepäckträger' },
436
+ { key: 'extras.roofRails', type: 'roof_rack', value: 'Dachreling' },
437
+ { key: 'assistants.esp', type: 'stability_control', value: 'ESP' },
438
+ { key: 'multimedia.onBoardComputer', type: 'telematics', value: 'Bordcomputer' },
439
+ { key: 'wheel.adjustableSteeringWheel', type: 'tilt_steering_wheel', value: 'höhenverst Lenkrad' },
440
+ { key: 'window.tintedWindowBack', type: 'tinted_windows', value: 'getönte Scheiben hinten' },
441
+ { key: 'assistants.tirePressureMonitor', type: 'tire_pressure_monitor', value: 'Reifendruckkontrolle' },
442
+ { key: 'trailerCoupling.trailerCouplingFix', type: 'tow_package', value: 'Anhängerkupplung fest' },
443
+ { key: 'trailerCoupling.trailerCouplingRemovable', type: 'tow_package', value: 'Anhängerkupplung abnehmbar' },
444
+ { key: 'trailerCoupling.trailerCouplingRotatable', type: 'tow_package', value: 'Anhängerkupplung schwenkbar' },
445
+ { key: 'assistants.tractionControlSystem', type: 'traction_control', value: 'Traktionskontrolle' },
446
+ {
447
+ key: 'heatingCooling.climatisation',
448
+ type: 'air_conditioning',
449
+ selector: 'selector_unknown',
450
+ value: 'Unbekannt',
451
+ },
452
+ {
453
+ key: 'heatingCooling.climatisation',
454
+ type: 'air_conditioning',
455
+ selector: 'selector_climatisation_no_airCondition',
456
+ value: 'Keine Klimaanlage oder -automatik',
457
+ },
458
+ {
459
+ key: 'heatingCooling.climatisation',
460
+ type: 'air_conditioning',
461
+ selector: 'selector_climatisation_airCondition',
462
+ value: 'Klimaanlage oder -automatik',
463
+ },
464
+ {
465
+ key: 'heatingCooling.climatisation',
466
+ type: 'air_conditioning',
467
+ selector: 'selector_climatisation_automaticAirCondition',
468
+ value: 'Klimaautomatik',
469
+ },
470
+ {
471
+ key: 'heatingCooling.climatisation',
472
+ type: 'air_conditioning',
473
+ selector: 'selector_climatisation_2_automaticAirCondition',
474
+ value: '2-Zonen Klimaautomatik',
475
+ },
476
+ {
477
+ key: 'heatingCooling.climatisation',
478
+ type: 'air_conditioning',
479
+ selector: 'selector_climatisation_3_automaticAirCondition',
480
+ value: '3-Zonen Klimaautomatik',
481
+ },
482
+ {
483
+ key: 'heatingCooling.climatisation',
484
+ type: 'air_conditioning',
485
+ selector: 'selector_climatisation_4_automaticAirCondition',
486
+ value: '4-Zonen Klimaautomatik',
487
+ },
488
+ {
489
+ key: 'wheels.summerTiresRims',
490
+ type: 'alloy_wheels',
491
+ selector: 'selector_tiresRims_alloy',
492
+ value: 'Leichtmetallfelgen',
493
+ },
494
+ {
495
+ key: 'assistants.parkingAssistants',
496
+ type: 'backup_camera',
497
+ selector: 'selector_parkingAssistants_camera',
498
+ value: 'Rückfahrkamera',
499
+ },
500
+ {
501
+ key: 'seats.driverSeats',
502
+ type: 'heated_seats',
503
+ selector: 'selector_driverSeats_electricHeated',
504
+ value: 'Sitzheizung Fahrersitz',
505
+ },
506
+ {
507
+ key: 'seats.coDriverSeats',
508
+ type: 'heated_seats',
509
+ selector: 'selector_coDriverSeats_electricHeated',
510
+ value: 'Sitzheizung Beifahrersitz',
511
+ },
512
+ {
513
+ key: 'seats.backSeats',
514
+ type: 'heated_seats',
515
+ selector: 'selector_backSeats_electricHeated',
516
+ value: 'Sitzheizung Rücksitz',
517
+ },
518
+ {
519
+ key: 'interior.interiorType',
520
+ type: 'leather',
521
+ selector: 'selector_interiorType_fullLeather',
522
+ value: 'Vollleder',
523
+ },
524
+ {
525
+ key: 'interior.interiorType',
526
+ type: 'leather',
527
+ selector: 'selector_interiorType_partLeather',
528
+ value: 'Teilleder',
529
+ },
530
+ { key: 'assistants.parkingAssistants', type: 'park_assist', selector: 'selector_unknown', value: 'Unbekannt' },
531
+ {
532
+ key: 'assistants.parkingAssistants',
533
+ type: 'park_assist',
534
+ selector: 'selector_parkingAssistants_other',
535
+ value: 'Andere',
536
+ },
537
+ {
538
+ key: 'assistants.parkingAssistants',
539
+ type: 'park_assist',
540
+ selector: 'selector_parkingAssistants_front',
541
+ value: 'Vorderseite',
542
+ },
543
+ {
544
+ key: 'assistants.parkingAssistants',
545
+ type: 'park_assist',
546
+ selector: 'selector_parkingAssistants_rear',
547
+ value: 'Rückseite',
548
+ },
549
+ {
550
+ key: 'assistants.parkingAssistants',
551
+ type: 'park_assist',
552
+ selector: 'selector_parkingAssistants_camera',
553
+ value: 'Kamera',
554
+ },
555
+ {
556
+ key: 'assistants.parkingAssistants',
557
+ type: 'park_assist',
558
+ selector: 'selector_parkingAssistants_selfSteeringSystems',
559
+ value: 'Selbstlenkende Systeme',
560
+ },
561
+ {
562
+ key: 'assistants.parkingAssistants',
563
+ type: 'park_assist',
564
+ selector: 'selector_parkingAssistants_360Camera',
565
+ value: '360° Kamera',
566
+ },
567
+ {
568
+ key: 'seats.driverSeats',
569
+ type: 'power_seats',
570
+ selector: 'selector_driverSeats_electricAdjustable',
571
+ value: 'Fahrersitz elektr. einstellbar',
572
+ },
573
+ {
574
+ key: 'seats.coDriverSeats',
575
+ type: 'power_seats',
576
+ selector: 'selector_coDriverSeats_electricAdjustable',
577
+ value: 'Beifahrersitz elektr. einstellbar',
578
+ },
579
+ {
580
+ key: 'assistants.parkingAssistants',
581
+ type: 'surround_view_camera',
582
+ selector: 'selector_parkingAssistants_360Camera',
583
+ value: '360° Kamera',
584
+ },
585
+ {
586
+ key: 'seats.driverSeats',
587
+ type: 'ventilated_seats',
588
+ selector: 'selector_driverSeats_seatVentilationSystem',
589
+ value: 'Sitzbelüftung Fahrersitz',
590
+ },
591
+ {
592
+ key: 'seats.coDriverSeats',
593
+ type: 'ventilated_seats',
594
+ selector: 'selector_coDriverSeats_seatVentilationSystem',
595
+ value: 'Sitzbelüftung Beifahrersitz',
596
+ },
597
+ {
598
+ key: 'seats.backSeats',
599
+ type: 'ventilated_seats',
600
+ selector: 'selector_backSeats_seatVentilationSystem',
601
+ value: 'Sitzbelüftung Rücksitz',
602
+ },
603
+ ];
604
+ return facebookFeatures
605
+ .map(feature => {
606
+ const value = get(car, feature.key);
607
+ if (isBoolean(value)) {
608
+ return this.getBooleanValue(value, feature);
609
+ }
610
+ if (isString(value)) {
611
+ return this.getStringValue(value, feature);
612
+ }
613
+ if (isArray(value) && includes(value, feature.selector)) {
614
+ const index = findIndex(value, item => item === feature.selector);
615
+ return this.getStringValue(value[index], feature);
616
+ }
617
+ return null;
618
+ })
619
+ .filter(el => el);
620
+ }
621
+
622
+ private getBooleanValue(
623
+ value: boolean,
624
+ feature:
625
+ | { key: string; type: string; value: string; selector?: undefined }
626
+ | { key: string; type: string; selector: string; value: string },
627
+ ): { type: string; value: string } {
628
+ if (value) {
629
+ return {
630
+ type: feature.type,
631
+ value: feature.value,
632
+ };
633
+ }
634
+ return null;
635
+ }
636
+
637
+ private getStringValue(
638
+ selector: string,
639
+ feature:
640
+ | { key: string; type: string; value: string; selector?: undefined }
641
+ | { key: string; type: string; selector: string; value: string },
642
+ ): { type: string; value: string } {
643
+ if (selector === feature.selector) {
644
+ return {
645
+ type: feature.type,
646
+ value: feature.value,
647
+ };
648
+ }
649
+ return null;
650
+ }
651
+ }
652
+
653
+ function getFirstValidYear(...years: string[]): number {
654
+ return years.map((year: string) => toSafeInteger(year)).filter((year: number) => year > TIMESTAMP_FIRST_YEAR)[0];
655
+ }