@bytebrand/export-cars 1.26.12 → 1.26.13
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/dist/exporters/fb.exporter.js +9 -2
- package/dist/exporters/fb.exporter.js.map +1 -1
- package/dist/services/images.service.js +14 -3
- package/dist/services/images.service.js.map +1 -1
- package/dist/services/user.service.d.ts +1 -0
- package/dist/services/user.service.js +8 -0
- package/dist/services/user.service.js.map +1 -1
- package/dist/transformers/fb-description.transformer.js +0 -13
- package/dist/transformers/fb-description.transformer.js.map +1 -1
- package/dist/transformers/fb.transformer.js +3 -6
- package/dist/transformers/fb.transformer.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/utils/converter.utils.d.ts +1 -0
- package/dist/utils/converter.utils.js +11 -1
- package/dist/utils/converter.utils.js.map +1 -1
- package/package.json +1 -1
- package/src/exporters/fb.exporter.ts +22 -2
- package/src/services/images.service.ts +30 -6
- package/src/services/user.service.ts +13 -0
- package/src/transformers/fb-description.transformer.ts +4 -13
- package/src/transformers/fb.transformer.ts +12 -9
- package/src/utils/converter.utils.ts +19 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.parseCsvToObjects = exports.xmlStringReplacer = exports.convertToCSV = void 0;
|
|
3
|
+
exports.parseCsvToObjects = exports.normalizeFeedText = exports.xmlStringReplacer = exports.convertToCSV = void 0;
|
|
4
4
|
const lodash_1 = require("lodash");
|
|
5
5
|
function convertToCSV(data) {
|
|
6
6
|
const converted = [];
|
|
@@ -16,6 +16,16 @@ const xmlStringReplacer = (str) => {
|
|
|
16
16
|
return typeof str === 'string' ? str.replace(/&/g, '&').replace(/[<>]/g, '') : '';
|
|
17
17
|
};
|
|
18
18
|
exports.xmlStringReplacer = xmlStringReplacer;
|
|
19
|
+
const DISALLOWED_TEXT_CHARS = /[^\p{L}\p{N} .,\-/()%"°]/gu;
|
|
20
|
+
const EDGE_SEPARATORS = /^[\s.,\-/]+|[\s.,\-/]+$/g;
|
|
21
|
+
function normalizeFeedText(text) {
|
|
22
|
+
return (text || '')
|
|
23
|
+
.replace(DISALLOWED_TEXT_CHARS, ' ')
|
|
24
|
+
.replace(/([.,\-/])\1+/g, '$1')
|
|
25
|
+
.replace(/\s+/g, ' ')
|
|
26
|
+
.replace(EDGE_SEPARATORS, '');
|
|
27
|
+
}
|
|
28
|
+
exports.normalizeFeedText = normalizeFeedText;
|
|
19
29
|
function parseCsvToObjects(csv) {
|
|
20
30
|
const rows = [];
|
|
21
31
|
let row = [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"converter.utils.js","sourceRoot":"","sources":["../../src/utils/converter.utils.ts"],"names":[],"mappings":";;;AAAA,mCAA6B;AAE7B,SAAgB,YAAY,CAAC,IAAa;IACxC,MAAM,SAAS,GAAG,EAAE,CAAC;IACrB,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE;QAC/B,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CAAa,CAAC;QAC7B,MAAM,QAAQ,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC;QAChE,IAAA,YAAG,EAAC,SAAS,EAAE,CAAC,EAAE,IAAI,QAAQ,GAAG,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;IACH,OAAO,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC7B,CAAC;AARD,oCAQC;AAEM,MAAM,iBAAiB,GAAG,CAAC,GAAW,EAAU,EAAE;IACvD,OAAO,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AACxF,CAAC,CAAC;AAFW,QAAA,iBAAiB,qBAE5B;
|
|
1
|
+
{"version":3,"file":"converter.utils.js","sourceRoot":"","sources":["../../src/utils/converter.utils.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -8,6 +8,11 @@ import { FbXmlBuilder } from '../formats/fb-xml.builder';
|
|
|
8
8
|
import { ModuleRef } from '@nestjs/core';
|
|
9
9
|
import { FbTransformer } from '../transformers/fb.transformer';
|
|
10
10
|
import { FbXmlItem } from '../interfaces';
|
|
11
|
+
import { META_PUSH_PRIORITY_ALLOWED_VALUES, META_PUSH_PRIORITY_STANDARD } from '../constants/fb.constants';
|
|
12
|
+
|
|
13
|
+
// dealer_priority_1 first, then dealer_priority_2, then everyone else (standard/unlisted) —
|
|
14
|
+
// mirrors the CSV's own priority tiers (see MappingService.getMetaPushPriorityMapping).
|
|
15
|
+
const PRIORITY_ORDER = [...META_PUSH_PRIORITY_ALLOWED_VALUES, META_PUSH_PRIORITY_STANDARD];
|
|
11
16
|
|
|
12
17
|
@Injectable()
|
|
13
18
|
export class FbExporter implements Exporter {
|
|
@@ -29,6 +34,12 @@ export class FbExporter implements Exporter {
|
|
|
29
34
|
|
|
30
35
|
const fbTransformer = await this.moduleRef.create(FbTransformer);
|
|
31
36
|
|
|
37
|
+
// Buffered (not streamed straight to XML) so the whole feed can be sorted by dealer
|
|
38
|
+
// priority before writing — see the sort below. FbXmlItem doesn't declare custom_label_4
|
|
39
|
+
// (that interface predates ADV-159/161); the transformer always sets it, so extend locally
|
|
40
|
+
// rather than touching the shared interface other export types also cast through.
|
|
41
|
+
const cars: (FbXmlItem & { custom_label_4: string })[] = [];
|
|
42
|
+
|
|
32
43
|
await this.loaderService.process(async (chunk: FsChunkInterface) => {
|
|
33
44
|
const chunkData = chunk.data.filter(car => this.carService.filterCar(car, this.rulesService.rules));
|
|
34
45
|
chunk.data = null;
|
|
@@ -42,15 +53,24 @@ export class FbExporter implements Exporter {
|
|
|
42
53
|
console.timeEnd(`Transform ${data.car._id}`);
|
|
43
54
|
|
|
44
55
|
if (!car) {
|
|
45
|
-
console.log(`Skip car ${data.car._id}
|
|
56
|
+
console.log(`Skip car ${data.car._id} — no real images`);
|
|
46
57
|
continue;
|
|
47
58
|
}
|
|
48
59
|
|
|
49
|
-
|
|
60
|
+
cars.push(car as FbXmlItem & { custom_label_4: string });
|
|
50
61
|
this.carService.counter.countCar(data.images);
|
|
51
62
|
}
|
|
52
63
|
});
|
|
53
64
|
|
|
65
|
+
// ADV-161-follow-up: order by the meta push priority sheet (metaPushPriorityCsvUrl)
|
|
66
|
+
// instead of filtering non-listed dealers out — dealer_priority_1 first, then
|
|
67
|
+
// dealer_priority_2, then everyone else, in whatever order they were transformed.
|
|
68
|
+
cars.sort((a, b) => PRIORITY_ORDER.indexOf(a.custom_label_4) - PRIORITY_ORDER.indexOf(b.custom_label_4));
|
|
69
|
+
|
|
70
|
+
for (const car of cars) {
|
|
71
|
+
await fbXmlBuilder.addItem(car);
|
|
72
|
+
}
|
|
73
|
+
|
|
54
74
|
await fbXmlBuilder.buildEnd();
|
|
55
75
|
console.log(this.carService.counter.getCountedCars());
|
|
56
76
|
}
|
|
@@ -8,12 +8,12 @@ import { MarketingImageConfigsInterface } from '@bytebrand/autode-request';
|
|
|
8
8
|
import { ImboImageResponseInterface } from '@bytebrand/chunkable';
|
|
9
9
|
import { DataSchemes } from '@bytebrand/car-schema';
|
|
10
10
|
|
|
11
|
-
import { FB_MAX_IMAGE_COUNT_FOR_DEAFULT, META_AIA_CHANNEL_CODE } from '../constants/fb.constants';
|
|
11
|
+
import { FB_MAX_IMAGE_COUNT_FOR_DEAFULT, META_AIA_CHANNEL_CODE, META_AIA_MODE_FINANCING } from '../constants/fb.constants';
|
|
12
12
|
import { ConfigService } from './config.service';
|
|
13
13
|
import { UserService } from './user.service';
|
|
14
14
|
import { isNotEmptyArray } from '../utils';
|
|
15
15
|
import { Config } from '../configs/config';
|
|
16
|
-
import { ImageSize, ImageType } from '../enums';
|
|
16
|
+
import { ExportType, ImageSize, ImageType } from '../enums';
|
|
17
17
|
|
|
18
18
|
@Injectable()
|
|
19
19
|
export class ImagesService {
|
|
@@ -176,10 +176,28 @@ export class ImagesService {
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
async getMarketingImages(carId: string, images: ImboImageResponseInterface[], ignoreDefImages: boolean): Promise<string[]> {
|
|
179
|
-
const imageUrl = this.configService.config.marketingImageUrl;
|
|
180
|
-
|
|
181
179
|
const transformedImages = this.transformImagesArrayIntoObject(images);
|
|
182
|
-
|
|
180
|
+
// fbMarketingImageId is a hardcoded per-environment id in config.ts and has drifted from
|
|
181
|
+
// the real meta_aia config at least once (dev's id pointed at a stale, never-populated "fb"
|
|
182
|
+
// channel) — the FB feed resolves its config by channelCode instead of trusting that id.
|
|
183
|
+
const config = this.configService.config.exportType === ExportType.FB
|
|
184
|
+
? await this.userService.getMarketingImageConfigByChannelCode(META_AIA_CHANNEL_CODE)
|
|
185
|
+
: await this.userService.getMarketingImageConfig(this.configService.config.marketingImageConfigId);
|
|
186
|
+
|
|
187
|
+
if (!config) {
|
|
188
|
+
console.log('getMarketingImages: no marketing image config found (channelCode meta_aia missing?)');
|
|
189
|
+
return ignoreDefImages ? [] : [...this.DEFAULT_IMAGES];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// marketingImageUrl AND config.imageUrl (FbConfig overrides the latter to the same
|
|
193
|
+
// per-env value right after super()) both point at the old "fb" channel's base path
|
|
194
|
+
// (/cache_test on dev, already /generated_images on prod) — not where image-generator
|
|
195
|
+
// publishes meta_aia composites. metaAiaImageUrl is derived from the un-suffixed
|
|
196
|
+
// baseImageUrl, so it's the one safe /generated_images base.
|
|
197
|
+
const imageUrl = config.channelCode === META_AIA_CHANNEL_CODE
|
|
198
|
+
? this.configService.config.metaAiaImageUrl
|
|
199
|
+
: this.configService.config.marketingImageUrl;
|
|
200
|
+
|
|
183
201
|
const marketingImageConfigs = config.marketingImages.map(i => ({ marketingImageCodes: i.imageCodes }));
|
|
184
202
|
|
|
185
203
|
const imageWithUrls: Partial<ImboImageResponseInterface & { url: string }>[] = [];
|
|
@@ -274,7 +292,13 @@ export class ImagesService {
|
|
|
274
292
|
|
|
275
293
|
protected getImagePathWithExtension(imageUrl: string, carId: string, imageId: string, config: MarketingImageConfigsInterface): string {
|
|
276
294
|
const { channelCode, imageExtension } = config;
|
|
277
|
-
|
|
295
|
+
// image-generator's MetaAiaImagesService writes files as `meta-aia-financing-{imageId}` /
|
|
296
|
+
// `meta-aia-leasing-{imageId}` (hyphenated, mode baked into the name) — not derived from
|
|
297
|
+
// channelCode the way other channels are. FB only wants the financing variant.
|
|
298
|
+
const filenamePrefix = channelCode === META_AIA_CHANNEL_CODE
|
|
299
|
+
? `meta-aia-${META_AIA_MODE_FINANCING}`
|
|
300
|
+
: channelCode.replace(/ /g, '-');
|
|
301
|
+
return `${this.getChannelPath(imageUrl, carId)}/${filenamePrefix}-${imageId}.${imageExtension}`;
|
|
278
302
|
}
|
|
279
303
|
|
|
280
304
|
sortImages(images: ImboImageResponseInterface[]): ImboImageResponseInterface[] {
|
|
@@ -67,4 +67,17 @@ export class UserService {
|
|
|
67
67
|
|
|
68
68
|
return result;
|
|
69
69
|
}
|
|
70
|
+
|
|
71
|
+
// The per-environment fbMarketingImageId config.ts hardcodes doesn't reliably point at the
|
|
72
|
+
// meta_aia channel (dev's id 8 turned out to be a stale, never-populated "fb" channel) —
|
|
73
|
+
// resolve by channelCode instead of trusting a fixed id.
|
|
74
|
+
async getMarketingImageConfigByChannelCode(channelCode: string): Promise<MarketingImageConfigInterface | null> {
|
|
75
|
+
const configs = await this.getMarketingImageConfigs();
|
|
76
|
+
const match = configs.find(config => config.channelCode === channelCode);
|
|
77
|
+
if (!match) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return this.getMarketingImageConfig(match.id);
|
|
82
|
+
}
|
|
70
83
|
}
|
|
@@ -46,13 +46,8 @@ export class FbDescription {
|
|
|
46
46
|
key: get(this.car, 'engineData.cylinders', '') as string,
|
|
47
47
|
},
|
|
48
48
|
},
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
value: {
|
|
52
|
-
key: 'title.cubicCapacity',
|
|
53
|
-
data: { cubicCapacity: get(this.car, 'engineData.cubicCapacity') },
|
|
54
|
-
},
|
|
55
|
-
},
|
|
49
|
+
// "Hubraum" is not exported: the value key resolved to the label itself
|
|
50
|
+
// (title.cubicCapacity === "Hubraum"), so every listing read "Hubraum: Hubraum".
|
|
56
51
|
{
|
|
57
52
|
title: 'accordion.captionPower',
|
|
58
53
|
value: {
|
|
@@ -166,12 +161,8 @@ export class FbDescription {
|
|
|
166
161
|
},
|
|
167
162
|
},
|
|
168
163
|
},
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
value: {
|
|
172
|
-
key: get(this.car, 'driveSuspension.wheelBase', '') as string,
|
|
173
|
-
},
|
|
174
|
-
},
|
|
164
|
+
// "Radstand" is not exported: driveSuspension.wheelBase is stored as null across the
|
|
165
|
+
// catalogue, so every listing read "Radstand: ;".
|
|
175
166
|
{
|
|
176
167
|
title: 'title.loadingVolume',
|
|
177
168
|
value: {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Cache } from 'cache-manager';
|
|
2
2
|
import * as moment from 'moment';
|
|
3
3
|
import {
|
|
4
|
-
escape,
|
|
5
4
|
findIndex,
|
|
6
5
|
get,
|
|
7
6
|
includes,
|
|
@@ -12,7 +11,6 @@ import {
|
|
|
12
11
|
join,
|
|
13
12
|
toSafeInteger,
|
|
14
13
|
toString,
|
|
15
|
-
trim,
|
|
16
14
|
truncate,
|
|
17
15
|
} from 'lodash';
|
|
18
16
|
|
|
@@ -30,7 +28,7 @@ import { Description } from '../interfaces/description.interface';
|
|
|
30
28
|
import { FbDescription } from './fb-description.transformer';
|
|
31
29
|
import { ConfigService, ImagesService, MappingService } from '../services';
|
|
32
30
|
import { FbEquipment } from './fb-equipment.transformer';
|
|
33
|
-
import { isValidVin } from '../utils';
|
|
31
|
+
import { isValidVin, normalizeFeedText } from '../utils';
|
|
34
32
|
import { UserService } from 'src/services';
|
|
35
33
|
import { ImageType } from '../enums';
|
|
36
34
|
import {
|
|
@@ -188,18 +186,23 @@ export class FbTransformer {
|
|
|
188
186
|
}
|
|
189
187
|
|
|
190
188
|
private getTitle(car: DataSchemes.Car): string {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
189
|
+
// mainData.constructionYear holds a raw epoch for part of the catalogue — the same
|
|
190
|
+
// corruption getFirstValidYear() already guards <year> against. Without the guard titles
|
|
191
|
+
// ship as "1609459200 Ford Galaxy …"; an invalid year is dropped instead.
|
|
192
|
+
const year = toString(getFirstValidYear(toString(get(car, 'mainData.constructionYear', ''))));
|
|
195
193
|
const make = toString(get(car, 'mainData.make', ''));
|
|
196
194
|
const model = toString(get(car, 'mainData.model', ''));
|
|
197
195
|
const subModel = toString(get(car, 'mainData.subModel', ''));
|
|
198
|
-
|
|
196
|
+
// No escape() here: FbXmlBuilder writes <title> inside CDATA, so HTML entities are not
|
|
197
|
+
// decoded by the consumer and leak literally ("Monte Carlo & KESSY").
|
|
198
|
+
return normalizeFeedText(join([year, make, model, subModel], ' '));
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
private getTrim(car: DataSchemes.Car): string {
|
|
202
|
-
|
|
202
|
+
// Same subModel source as the title, so it needs the same cleanup. Normalise before
|
|
203
|
+
// truncating: on the raw string the cut could land mid-junk, and lodash's "..." omission
|
|
204
|
+
// marker would then be collapsed by the repeated-separator rule.
|
|
205
|
+
const trim = normalizeFeedText(toString(get(car, 'mainData.subModel', '')));
|
|
203
206
|
return truncate(trim, { length: FB_MAX_TRIM_LENGTH, separator: ' ' });
|
|
204
207
|
}
|
|
205
208
|
|
|
@@ -14,6 +14,25 @@ export const xmlStringReplacer = (str: string): string => {
|
|
|
14
14
|
return typeof str === 'string' ? str.replace(/&/g, '&').replace(/[<>]/g, '') : '';
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
// Dealer-supplied subModel strings are full of decorative punctuation ("*AHK*Leder*Navi*",
|
|
18
|
+
// "|ACC|PANO/AHK", "#PANO #NAVI #SONY #", "Head-Up+B&O+ACC") which must not reach Meta. Keep
|
|
19
|
+
// letters, digits and the punctuation that carries meaning in German trim names — "." "," "-"
|
|
20
|
+
// "/" "(" ")" "%", "°" (360° Kamera) and '"' (Zoll). Everything else, "&" included, becomes a
|
|
21
|
+
// space rather than being deleted, so neighbouring words never fuse ("B&O" -> "B O").
|
|
22
|
+
const DISALLOWED_TEXT_CHARS = /[^\p{L}\p{N} .,\-/()%"°]/gu;
|
|
23
|
+
// Leading/trailing separators only; '"' and "%" are legitimate endings (`LM18"`, `-35%`).
|
|
24
|
+
const EDGE_SEPARATORS = /^[\s.,\-/]+|[\s.,\-/]+$/g;
|
|
25
|
+
|
|
26
|
+
// Applies to the free-text feed fields only (title, trim). URLs, dealer_email and dealer_phone
|
|
27
|
+
// carry "/" ":" "@" "+" as load-bearing characters and must never be run through this.
|
|
28
|
+
export function normalizeFeedText(text: string): string {
|
|
29
|
+
return (text || '')
|
|
30
|
+
.replace(DISALLOWED_TEXT_CHARS, ' ')
|
|
31
|
+
.replace(/([.,\-/])\1+/g, '$1') // "L4H2 //Klimaanlage" -> "L4H2 /Klimaanlage"
|
|
32
|
+
.replace(/\s+/g, ' ')
|
|
33
|
+
.replace(EDGE_SEPARATORS, '');
|
|
34
|
+
}
|
|
35
|
+
|
|
17
36
|
// Parses a "Publish to web" Google Sheets CSV export (RFC4180-ish, quoted fields may
|
|
18
37
|
// contain commas/newlines) into row arrays, then maps rows to objects keyed by header.
|
|
19
38
|
export function parseCsvToObjects<T>(csv: string): T[] {
|