@bytebrand/export-cars 1.1.1 → 1.2.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.
Files changed (40) hide show
  1. package/dist/config/kleinanzeigeFocus.config.d.ts +5 -0
  2. package/dist/config/kleinanzeigeFocus.config.js +15 -0
  3. package/dist/config/kleinanzeigeFocus.config.js.map +1 -0
  4. package/dist/enums/export-type.enum.d.ts +2 -1
  5. package/dist/enums/export-type.enum.js +1 -0
  6. package/dist/enums/export-type.enum.js.map +1 -1
  7. package/dist/export.schema.d.ts +6 -0
  8. package/dist/export.schema.js +6 -0
  9. package/dist/export.schema.js.map +1 -1
  10. package/dist/exporters/index.d.ts +1 -0
  11. package/dist/exporters/index.js +18 -0
  12. package/dist/exporters/index.js.map +1 -0
  13. package/dist/exporters/rubrikk.exporter.d.ts +13 -0
  14. package/dist/exporters/rubrikk.exporter.js +68 -0
  15. package/dist/exporters/rubrikk.exporter.js.map +1 -0
  16. package/dist/formats/index.d.ts +1 -0
  17. package/dist/formats/index.js +18 -0
  18. package/dist/formats/index.js.map +1 -0
  19. package/dist/formats/rubrikk-xml.builder.d.ts +7 -0
  20. package/dist/formats/rubrikk-xml.builder.js +25 -0
  21. package/dist/formats/rubrikk-xml.builder.js.map +1 -0
  22. package/dist/interfaces/export-item.interface.d.ts +53 -0
  23. package/dist/transformers/index.d.ts +1 -0
  24. package/dist/transformers/index.js +18 -0
  25. package/dist/transformers/index.js.map +1 -0
  26. package/dist/transformers/rubrikk.transformer.d.ts +29 -0
  27. package/dist/transformers/rubrikk.transformer.js +211 -0
  28. package/dist/transformers/rubrikk.transformer.js.map +1 -0
  29. package/dist/tsconfig.build.tsbuildinfo +1 -1
  30. package/package.json +2 -2
  31. package/src/config/kleinanzeigeFocus.config.ts +12 -0
  32. package/src/enums/export-type.enum.ts +1 -0
  33. package/src/export.schema.ts +6 -0
  34. package/src/exporters/index.ts +1 -0
  35. package/src/exporters/rubrikk.exporter.ts +53 -0
  36. package/src/formats/index.ts +1 -0
  37. package/src/formats/rubrikk-xml.builder.ts +27 -0
  38. package/src/interfaces/export-item.interface.ts +54 -0
  39. package/src/transformers/index.ts +1 -0
  40. package/src/transformers/rubrikk.transformer.ts +237 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytebrand/export-cars",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "",
5
5
  "author": "bytebrand",
6
6
  "license": "UNLICENSED",
@@ -65,5 +65,5 @@
65
65
  "engines": {
66
66
  "node": ">=14.0.0"
67
67
  },
68
- "gitHead": "4006e3bfe30c1ec48542fdf3d1f98e83cb5718e2"
68
+ "gitHead": "d5ac6fed61ba0e2f5a476b547a1b039fb97e881a"
69
69
  }
@@ -0,0 +1,12 @@
1
+ import { Params } from '../interfaces/params.interface';
2
+ import { Config } from './config';
3
+
4
+ export class KleinanzeigeFocusConfig extends Config {
5
+ constructor(params: Params) {
6
+ super(params);
7
+ this.pathesConfig.localConvertedDirectory = this.environmentConfig[params.environment].localConvertedDirectory;
8
+ this.pathesConfig.remoteDirectory = this.environmentConfig[params.environment].remoteDirectory;
9
+ this.imageUrl = this.environmentConfig[params.environment].imageUrl;
10
+ this.outputFiles = ['api_export_cars_RUBRIKK.xml', 'autode.tar.gz'];
11
+ }
12
+ }
@@ -13,4 +13,5 @@ export enum ExportType {
13
13
  AS24 = 'as24',
14
14
  ALLESKRALLE = 'alleskralle',
15
15
  DASPARKING = 'dasparking',
16
+ KLEINANZEIGE_FOCUS = 'kleinanzeigefocus',
16
17
  }
@@ -12,6 +12,7 @@ import { MarktdeConfig } from './config/marktde.config';
12
12
  import { MeinstadtConfig } from './config/meinstadt.config';
13
13
  import { OoyyoConfig } from './config/ooyyo.config';
14
14
  import { YescarConfig } from './config/yescar.config';
15
+ import { KleinanzeigeFocusConfig } from './config/kleinanzeigeFocus.config';
15
16
  import { ExportType } from './enums/export-type.enum';
16
17
  import { As24Exporter } from './exporters/as24.exporter';
17
18
  import { Auto24Exporter } from './exporters/auto24.exporter';
@@ -22,6 +23,7 @@ import { GoogleExporter } from './exporters/google.exporter';
22
23
  import { MeinstadtExporter } from './exporters/meinstadt.exporter';
23
24
  import { MobileExporter } from './exporters/mobile.exporter';
24
25
  import { OoyyoExporter } from './exporters/ooyyo.exporter';
26
+ import { RubrikkExporter } from './exporters';
25
27
 
26
28
  export const exportSchema = {
27
29
  [ExportType.EAUTO]: {
@@ -80,4 +82,8 @@ export const exportSchema = {
80
82
  config: DasparkingConfig,
81
83
  exporter: Auto24Exporter,
82
84
  },
85
+ [ExportType.KLEINANZEIGE_FOCUS]: {
86
+ config: KleinanzeigeFocusConfig,
87
+ exporter: RubrikkExporter,
88
+ },
83
89
  };
@@ -0,0 +1 @@
1
+ export * from './rubrikk.exporter';
@@ -0,0 +1,53 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { Exporter } from '../interfaces/exporter.interface';
3
+ import { CarService } from '../services/car.service';
4
+ import { ConfigService } from '../services/config.service';
5
+ import { LoaderService } from '../services/loader.service';
6
+ import { ModuleRef } from '@nestjs/core';
7
+ import { CronicleUtils } from '@bytebrand/cronicle-helper';
8
+ import { Zipper } from '@bytebrand/zipper';
9
+ import { RubrikkXmlBuilder } from '../formats';
10
+ import { RubrikkTransformer } from '../transformers';
11
+ import { FsChunkInterface } from '@bytebrand/chunkable';
12
+
13
+ @Injectable()
14
+ export class RubrikkExporter implements Exporter {
15
+ constructor(
16
+ private carService: CarService,
17
+ private configService: ConfigService,
18
+ private loaderService: LoaderService,
19
+ private moduleRef: ModuleRef,
20
+ ) {}
21
+ async export(): Promise<void> {
22
+ for (const file of this.configService.config.outputFiles) {
23
+ const exists = await CronicleUtils.exists(file);
24
+ exists && (await CronicleUtils.unlink(file));
25
+ }
26
+
27
+ const [fileXml, fileGZ] = this.configService.config.outputFiles;
28
+
29
+ const rubrikkXmlBuilder = new RubrikkXmlBuilder(fileXml);
30
+ const rubrikkTransformer = await this.moduleRef.create(RubrikkTransformer);
31
+
32
+ await rubrikkXmlBuilder.buildStart();
33
+ await this.loaderService.process(async (chunk: FsChunkInterface) => {
34
+ if (chunk && chunk.data) {
35
+ for (let i = 0; i < chunk.data.length; i += 1) {
36
+ const data = chunk.data[i];
37
+ const { car, images } = data;
38
+
39
+ const transformed_car = await rubrikkTransformer.transform({ car, images });
40
+
41
+ await rubrikkXmlBuilder.addItem(transformed_car);
42
+ this.carService.counter.countCar(images);
43
+ }
44
+ chunk.data = null;
45
+ }
46
+ });
47
+ await rubrikkXmlBuilder.buildEnd();
48
+
49
+ if (fileGZ) {
50
+ await Zipper.tar({ dest: fileGZ, fileList: [fileXml] });
51
+ }
52
+ }
53
+ }
@@ -0,0 +1 @@
1
+ export * from './rubrikk-xml.builder';
@@ -0,0 +1,27 @@
1
+ import { XMLBuilderBase } from '@bytebrand/cronicle-helper';
2
+ import { RubrikkXmlItem } from '../interfaces/export-item.interface';
3
+
4
+ export class RubrikkXmlBuilder extends XMLBuilderBase {
5
+ async buildStart(): Promise<void> {
6
+ await this.write('<?xml version="1.0" encoding="UTF-8"?><rubrikk>');
7
+ }
8
+
9
+ async addItem(car: RubrikkXmlItem): Promise<void> {
10
+ let item = '\n<ad>\n';
11
+
12
+ const tags = Object.keys(car);
13
+
14
+ for (const tag of tags) {
15
+ const value = car[tag];
16
+ item += this.getElement(tag, value, true);
17
+ }
18
+ item += '</ad>';
19
+
20
+ await this.write(item);
21
+ }
22
+
23
+ async buildEnd(): Promise<void> {
24
+ await this.write('</rubrikk>');
25
+ await this.end();
26
+ }
27
+ }
@@ -144,3 +144,57 @@ export interface GoogleXmlItem {
144
144
  color: string;
145
145
  installment: { paybackPeriod: any; monthlyInstallment: any };
146
146
  }
147
+
148
+ export interface RubrikkXmlItem {
149
+ ad__all_imageurls: string[];
150
+ ad__condition_new_used_etc: string;
151
+ ad__description: string;
152
+ ad__headline: string;
153
+ ad__imageurl: string;
154
+ ad__price: number;
155
+ ad__price_currency: string;
156
+ product__type: string;
157
+ ad__url: string;
158
+ efficiency_class: string;
159
+ ad__number_ad__number_reference_id: string;
160
+ category_original: string;
161
+ maincategory_original: string;
162
+ advertiser__company_homepage_url: string;
163
+ advertiser__company_logo_url: string;
164
+ advertiser__email: string;
165
+ advertiser__name_company_name: string;
166
+ advertiser__phone: string;
167
+ advertiser__type: string;
168
+ advertiser__company_employee: string;
169
+ location__abroad_region: string;
170
+ location__country: string;
171
+ location__municipality_city: string;
172
+ location__zip_postal_code: string;
173
+ location__streetaddress: string;
174
+ location__latitude: number;
175
+ location__longitude: number;
176
+ motoring__body_type: string;
177
+ motoring__color: string;
178
+ motoring__drivetrain: string;
179
+ motoring__emission_class: string;
180
+ motoring__emission_co2: number;
181
+ motoring__emission_co2_unit: string;
182
+ motoring__emission_sticker: string;
183
+ motoring__engine_displacement_cubic_centimeters: number;
184
+ motoring__fuel_consumption_combined: number;
185
+ motoring__fuel_consumption_extra_urban: number;
186
+ motoring__fuel_consumption_extra_urban_unit: string;
187
+ motoring__fuel_consumption_combined_unit: string;
188
+ motoring__fuel_consumption_urban: number;
189
+ motoring__fuel_consumption_urban_unit: string;
190
+ motoring__fuel_type: string;
191
+ motoring__make: string;
192
+ motoring__mileage_kilometers: number;
193
+ motoring__model: string;
194
+ motoring__model_year: number;
195
+ motoring__number_of_doors: number;
196
+ motoring__number_of_seats: number;
197
+ motoring__power_kw: number;
198
+ motoring__power_hp: number;
199
+ motoring__transmission: string;
200
+ }
@@ -0,0 +1 @@
1
+ export * from './rubrikk.transformer';
@@ -0,0 +1,237 @@
1
+ import { escape, get, isEmpty, join, toNumber, toSafeInteger, toString, trim } from 'lodash';
2
+ import { Injectable } from '@nestjs/common';
3
+
4
+ import { DePostalCodesEntryInterface } from '@bytebrand/car-basic-data-scope/dist/interfaces/DePostalCodesInterface';
5
+ import { de, deVehicleDetailsPage, deVehicleProps } from '@bytebrand/i18n-dictionaries';
6
+ import { DePostalCodesService } from '@bytebrand/car-basic-data-scope';
7
+ import { TranslatorExtended } from '@bytebrand/schema-tools';
8
+ import { DataSchemes } from '@bytebrand/car-schema';
9
+
10
+ import { RubrikkXmlItem } from '../interfaces/export-item.interface';
11
+ import { ConfigService } from '../services/config.service';
12
+ import * as moment from 'moment/moment';
13
+ import { ImboImageResponseInterface } from '@bytebrand/chunkable';
14
+ import { isNotEmptyArray } from '../utils/validator.utils';
15
+
16
+ @Injectable()
17
+ export class RubrikkTransformer {
18
+ private translator: TranslatorExtended;
19
+ private postalCodesService: DePostalCodesService;
20
+
21
+ constructor(private readonly configService: ConfigService) {
22
+ const dict = { ...de, ...deVehicleProps, ...deVehicleDetailsPage };
23
+ this.postalCodesService = DePostalCodesService.getInstance();
24
+ this.translator = new TranslatorExtended(dict);
25
+ }
26
+
27
+ async transform({
28
+ car,
29
+ images,
30
+ }: {
31
+ car: DataSchemes.Car;
32
+ images: ImboImageResponseInterface[];
33
+ }): Promise<RubrikkXmlItem> {
34
+ const autodeId = toString(get(car, '_id', ''));
35
+ const region = await this.getRegion(car);
36
+ const { longitude, latitude } = await this.getCoords(car);
37
+
38
+ return {
39
+ //Common Fields
40
+ ad__all_imageurls: this.getImagesLinks(autodeId, images),
41
+ ad__condition_new_used_etc: this.getCondition(car),
42
+ ad__description: toString(get(car, 'offer.offerDescription', '')),
43
+ ad__headline: this.getTitle(car),
44
+ ad__imageurl: this.getImageLink(car),
45
+ ad__price: toNumber(get(car, 'price.currentSalesPrice', null)),
46
+ ad__price_currency: toString(get(car, 'price.currency', 'EUR')),
47
+ product__type: this.getCategory(car),
48
+ ad__url: this.getCarLink(car),
49
+ efficiency_class: this.getEnergyClass(car),
50
+ ad__number_ad__number_reference_id: toString(get(car, 'metaData.vehicleId', '')),
51
+
52
+ //Categorization
53
+ category_original: 'Vehicles',
54
+ maincategory_original: 'Cars',
55
+
56
+ //Advertiser Info
57
+ advertiser__company_homepage_url: 'https://www.auto.de',
58
+ advertiser__company_logo_url:
59
+ 'https://autode-static.de/wp-content/themes/autode/front/img/header/b_logo_autode.svg',
60
+ advertiser__email: 'auto@auto.de',
61
+ advertiser__name_company_name: 'Auto.de',
62
+ advertiser__phone: '+498000008300',
63
+ advertiser__type: 'Dealer',
64
+ advertiser__company_employee: 'auto.de',
65
+
66
+ // Geography Info
67
+ location__abroad_region: region,
68
+ location__country: 'Deutschland',
69
+ location__municipality_city: get(car, 'location.city', ''),
70
+ location__zip_postal_code: toString(get(car, 'location.zipCode', '')),
71
+ location__streetaddress: this.getStreetAddress(car),
72
+ location__longitude: longitude,
73
+ location__latitude: latitude,
74
+
75
+ // Motoring Fields
76
+ motoring__body_type: this.translateSelector(get(car, 'mainData.category', '')),
77
+ motoring__color: this.getColor(car),
78
+ motoring__drivetrain: this.getDrivetrain(car),
79
+ motoring__emission_class: this.translateSelector(get(car, 'environmentEmissions.emissionClass', '')),
80
+ motoring__emission_co2: get(car, 'environmentEmissions.co2', null),
81
+ motoring__emission_co2_unit: 'g/km',
82
+ motoring__emission_sticker: this.translateSelector(get(car, 'environmentEmissions.emissionSticker', '')),
83
+ motoring__engine_displacement_cubic_centimeters: toNumber(get(car, 'engineData.cubicCapacity', null)),
84
+ motoring__fuel_consumption_combined: get(car, 'consumption.consumptionCombined', null),
85
+ motoring__fuel_consumption_combined_unit: 'l',
86
+ motoring__fuel_consumption_extra_urban: get(car, 'consumption.consumptionOuter', null),
87
+ motoring__fuel_consumption_extra_urban_unit: 'l',
88
+ motoring__fuel_consumption_urban: get(car, 'consumption.consumptionInner', null),
89
+ motoring__fuel_consumption_urban_unit: 'l',
90
+ motoring__fuel_type: this.translateSelector(get(car, 'consumption.fuel', '')),
91
+ motoring__make: toString(get(car, 'mainData.make', '')),
92
+ motoring__mileage_kilometers: get(car, 'mainData.mileage', null),
93
+ motoring__model: toString(get(car, 'mainData.model', '')),
94
+ motoring__model_year: this.getYear(car),
95
+ motoring__number_of_doors: get(car, 'mainData.doors', null),
96
+ motoring__number_of_seats: get(car, 'mainData.seats', null),
97
+ motoring__power_kw: get(car, 'engineData.powerKW', null),
98
+ motoring__power_hp: get(car, 'engineData.powerPS', null),
99
+ motoring__transmission: this.getDrivetrain(car),
100
+ };
101
+ }
102
+
103
+ private translateSelector(selector: string): string {
104
+ if (isEmpty(selector) || selector === 'selector_unknown') {
105
+ return '';
106
+ }
107
+ return this.translator.translateVal(selector);
108
+ }
109
+
110
+ private getTitle(car: DataSchemes.Car): string {
111
+ const make = toString(get(car, 'mainData.make', ''));
112
+ const model = toString(get(car, 'mainData.model', ''));
113
+ const subModel = toString(get(car, 'mainData.subModel', ''));
114
+ return trim(escape(join([make, model, subModel], ' ')));
115
+ }
116
+
117
+ private getCondition(car: DataSchemes.Car): string {
118
+ const available = {
119
+ selector_condition_new: 'Neu',
120
+ selector_condition_used: 'Gebraucht',
121
+ selector_unknown: 'Gebraucht',
122
+ };
123
+ const condition = get(car, 'mainData.condition', 'selector_unknown');
124
+ return get(available, condition);
125
+ }
126
+
127
+ private getCategory(car: DataSchemes.Car): string {
128
+ return this.translator.translateVal(get(car, 'mainData.category', ''));
129
+ }
130
+
131
+ private getCarLink(car: DataSchemes.Car): string {
132
+ const autodeId = toString(get(car, '_id', ''));
133
+ return `https://www.${this.configService.config.url}/search/vehicle/${autodeId}?source=classifieds&medium=listing&campaign=focus`;
134
+ }
135
+
136
+ private getImageLink(car: DataSchemes.Car): string {
137
+ const mainImageId = get(car, 'metaData.mainImageId', '');
138
+ return mainImageId
139
+ ? `https://images.auto.de/carimage/${car._id}/${mainImageId}/large-cached.webp`
140
+ : 'https://images.auto.de/carimage/noimage/7pNvWELuWhZX/large-cached.jpeg';
141
+ }
142
+
143
+ private getImagesLinks(carId: string, images: ImboImageResponseInterface[]): string[] {
144
+ if (isNotEmptyArray(images)) {
145
+ return images.map((image: ImboImageResponseInterface) => {
146
+ return `${this.configService.config.url}/carimage/${carId}/${image.imageIdentifier}/large`;
147
+ });
148
+ }
149
+ return [];
150
+ }
151
+
152
+ private getEnergyClass(car: DataSchemes.Car): string {
153
+ const available = {
154
+ selector_energyEfficiencyClass_a: 'A',
155
+ selector_energyEfficiencyClass_ap: 'A+',
156
+ selector_energyEfficiencyClass_app: 'A++',
157
+ selector_energyEfficiencyClass_appp: 'A+++',
158
+ selector_energyEfficiencyClass_b: 'B',
159
+ selector_energyEfficiencyClass_c: 'C',
160
+ selector_energyEfficiencyClass_d: 'D',
161
+ selector_energyEfficiencyClass_e: 'E',
162
+ selector_energyEfficiencyClass_f: 'F',
163
+ selector_energyEfficiencyClass_g: 'G',
164
+ selector_unknown: 'default',
165
+ };
166
+ const energyClass = get(car, 'environmentEmissions.energyEfficiencyClass', 'selector_unknown');
167
+ return get(available, energyClass);
168
+ }
169
+
170
+ private getStreetAddress(car: DataSchemes.Car): string {
171
+ const street = get(car, 'location.street', '');
172
+ const streetNumber = get(car, 'location.streetNumber', '');
173
+ return trim(escape(join([street, streetNumber], ' ')));
174
+ }
175
+
176
+ private async getRegion(car: DataSchemes.Car): Promise<string> {
177
+ const city: string = get(car, 'location.city', '');
178
+ const zipCode: number = toNumber(get(car, 'location.zipCode', ''));
179
+
180
+ const locationEntity = await this.getLocationByInfo({ city, zip: toSafeInteger(zipCode) });
181
+ return get(locationEntity, 'Place Name', 'Deutschland');
182
+ }
183
+
184
+ private async getCoords(car: DataSchemes.Car): Promise<Pick<DataSchemes.Location, 'latitude' | 'longitude'>> {
185
+ const city: string = get(car, 'location.city', '');
186
+ const zipCode: number = toNumber(get(car, 'location.zipCode', ''));
187
+ const locationEntity = await this.getLocationByInfo({ city, zip: toSafeInteger(zipCode) });
188
+
189
+ const data: Pick<DataSchemes.Location, 'latitude' | 'longitude'> = {
190
+ latitude: get(car, 'location.latitude'),
191
+ longitude: get(car, 'location.longitude'),
192
+ };
193
+
194
+ if (data.latitude && data.longitude) return data;
195
+
196
+ return {
197
+ longitude: get(locationEntity, 'Longitude'),
198
+ latitude: get(locationEntity, 'Latitude'),
199
+ };
200
+ }
201
+
202
+ private async getLocationByInfo(info: { zip: number; city: string }): Promise<DePostalCodesEntryInterface> {
203
+ let entry: DePostalCodesEntryInterface = await this.postalCodesService.getLocationByPostalCode(info.zip);
204
+
205
+ if (!entry) {
206
+ entry = await this.postalCodesService.getLocationByCity(info.city);
207
+ }
208
+ return entry;
209
+ }
210
+
211
+ private getColor(car: DataSchemes.Car): string {
212
+ let colorSelection = get(car, 'exterior.exteriorColor', 'other');
213
+ if (!colorSelection || colorSelection === 'selector_unknown') {
214
+ colorSelection = 'other';
215
+ }
216
+ return this.translator.translateVal(colorSelection);
217
+ }
218
+
219
+ private getDrivetrain(car: DataSchemes.Car): string {
220
+ const available = {
221
+ selector_unknown: 'Other',
222
+ selector_driveType_frontWheelDrive: 'FWD',
223
+ selector_driveType_rearWheelDrive: 'RWD',
224
+ selector_driveType_allWheelDrive: 'AWD',
225
+ };
226
+ const driveType = get(car, 'driveSuspension.driveType', 'selector_unknown');
227
+ return get(available, driveType);
228
+ }
229
+
230
+ private getYear(car: DataSchemes.Car): number {
231
+ const firstRegistration = get(car, 'mainData.firstRegistration', 0) as number;
232
+ if (firstRegistration !== 0) {
233
+ return toSafeInteger(moment(firstRegistration * 1000).format('YYYY'));
234
+ }
235
+ return 0;
236
+ }
237
+ }