@bytebrand/export-cars 1.26.8 → 1.26.10
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/transformers/fb.transformer.js +2 -2
- package/dist/transformers/fb.transformer.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/utils/converter.utils.d.ts +1 -1
- package/dist/utils/converter.utils.js +8 -8
- package/dist/utils/converter.utils.js.map +1 -1
- package/package.json +2 -2
- package/src/configs/config.ts +4 -2
- package/src/configs/facebook-shop.config.ts +1 -3
- package/src/constants/fb.constants.ts +7 -0
- package/src/exporters/fb.exporter.ts +2 -22
- package/src/services/images.service.ts +57 -29
- package/src/services/user.service.ts +0 -13
- package/src/transformers/facebook-shop.transformer.ts +7 -4
- package/src/transformers/fb-description.transformer.ts +13 -4
- package/src/transformers/fb.transformer.ts +8 -8
- package/src/utils/converter.utils.ts +0 -17
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export declare function convertToCSV(data: unknown): string;
|
|
2
2
|
export declare const xmlStringReplacer: (str: string) => string;
|
|
3
|
-
export declare function
|
|
3
|
+
export declare function normalizeFeedText(text: string): string;
|
|
4
4
|
export declare function parseCsvToObjects<T>(csv: string): T[];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.parseCsvToObjects = exports.
|
|
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,16 +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
|
|
20
|
-
const
|
|
21
|
-
function
|
|
22
|
-
return (
|
|
23
|
-
.replace(
|
|
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
24
|
.replace(/([.,\-/])\1+/g, '$1')
|
|
25
25
|
.replace(/\s+/g, ' ')
|
|
26
|
-
.replace(
|
|
26
|
+
.replace(EDGE_SEPARATORS, '');
|
|
27
27
|
}
|
|
28
|
-
exports.
|
|
28
|
+
exports.normalizeFeedText = normalizeFeedText;
|
|
29
29
|
function parseCsvToObjects(csv) {
|
|
30
30
|
const rows = [];
|
|
31
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;AAOF,MAAM,
|
|
1
|
+
{"version":3,"file":"converter.utils.js","sourceRoot":"","sources":["../../src/utils/converter.utils.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bytebrand/export-cars",
|
|
3
|
-
"version": "1.26.
|
|
3
|
+
"version": "1.26.10",
|
|
4
4
|
"description": "",
|
|
5
5
|
"author": "bytebrand",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"@bytebrand/car-schema": "^3.14.15",
|
|
20
20
|
"@bytebrand/chunkable": "^5.19.8",
|
|
21
21
|
"@bytebrand/cronicle-helper": "^1.21.8",
|
|
22
|
-
"@bytebrand/fe-financing": "^5.14.
|
|
22
|
+
"@bytebrand/fe-financing": "^5.14.19",
|
|
23
23
|
"@bytebrand/i18n-dictionaries": "^1.5.0",
|
|
24
24
|
"@bytebrand/import-helper": "^0.45.0",
|
|
25
25
|
"@bytebrand/schema-tools": "^3.18.8",
|
package/src/configs/config.ts
CHANGED
|
@@ -11,12 +11,12 @@ export class Config {
|
|
|
11
11
|
imageUrl: string;
|
|
12
12
|
baseImageUrl: string;
|
|
13
13
|
marketingImageUrl: string;
|
|
14
|
+
metaAiaImageUrl: string;
|
|
14
15
|
imageType: ImageType;
|
|
15
16
|
downloadImage: boolean;
|
|
16
17
|
isNewCar: boolean;
|
|
17
18
|
isCriteo: boolean;
|
|
18
19
|
exportType: ExportType;
|
|
19
|
-
environment: Environment;
|
|
20
20
|
domain: string;
|
|
21
21
|
input: {
|
|
22
22
|
stringIN: string;
|
|
@@ -60,7 +60,6 @@ export class Config {
|
|
|
60
60
|
constructor(params: Params) {
|
|
61
61
|
this.apikey = params.apikey;
|
|
62
62
|
this.exportType = params.exportType;
|
|
63
|
-
this.environment = params.environment;
|
|
64
63
|
this.cpc = params.cpc;
|
|
65
64
|
this.domain = this.environmentConfig[params.environment].domain;
|
|
66
65
|
this.baseImageUrl = this.environmentConfig[params.environment].baseImageUrl;
|
|
@@ -72,6 +71,9 @@ export class Config {
|
|
|
72
71
|
this.userActionsUrl = `https://www.${this.domain}/service/user-actions`;
|
|
73
72
|
this.imboUrl = `https://www.${this.domain}/service/image-imbo`;
|
|
74
73
|
this.imageUrl = `https://images.${this.domain}`;
|
|
74
|
+
// Same FTP_ROOT (/generated_images) image-generator's MetaAiaImagesService saves to —
|
|
75
|
+
// independent of marketingImageUrl, which points legacy fb/criteo channels at their own path.
|
|
76
|
+
this.metaAiaImageUrl = `${this.imageUrl}/generated_images`;
|
|
75
77
|
this.upaUrl = `https://www.${this.domain}/service/user-profile-admin`;
|
|
76
78
|
this.input = {
|
|
77
79
|
stringIN: params.String,
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import { Params } from '../interfaces/params.interface';
|
|
2
2
|
import { Config } from './config';
|
|
3
|
-
import { ExportType, FileType
|
|
3
|
+
import { ExportType, FileType } from '../enums';
|
|
4
4
|
|
|
5
5
|
export class FacebookShopConfig extends Config {
|
|
6
6
|
constructor(params: Params) {
|
|
7
7
|
super(params);
|
|
8
8
|
this.pathesConfig.localConvertedDirectory = `resources/${ExportType.FACEBOOK_SHOP}-export`;
|
|
9
9
|
this.imageUrl = this.environmentConfig[params.environment].baseImageUrl;
|
|
10
|
-
this.imageType = ImageType.marketing;
|
|
11
|
-
this.marketingImageConfigId = this.environmentConfig[params.environment].fbMarketingImageId;
|
|
12
10
|
this.outputFiles = [
|
|
13
11
|
`${this.pathesConfig.localConvertedDirectory}/api_export_cars_${ExportType.FACEBOOK_SHOP}.${FileType.XML}`,
|
|
14
12
|
];
|
|
@@ -8,6 +8,13 @@ export const TIMESTAMP_FIRST_YEAR = 1970;
|
|
|
8
8
|
export const META_PUSH_PRIORITY_STANDARD = 'standard';
|
|
9
9
|
export const META_PUSH_PRIORITY_ALLOWED_VALUES = ['dealer_priority_1', 'dealer_priority_2'];
|
|
10
10
|
|
|
11
|
+
// MarketingImageExportConfig channel code for the Meta AIA overlay images (image-generator's
|
|
12
|
+
// MetaAiaImagesService.CHANNEL_CODE) — distinct from this feed's now-disabled legacy "fb" config.
|
|
13
|
+
export const META_AIA_CHANNEL_CODE = 'meta_aia';
|
|
14
|
+
// Only the financing variant exists so far; leasing/other modes will be added once image-generator
|
|
15
|
+
// renders them too (see MetaAiaImagesService's MetaAiaMode).
|
|
16
|
+
export const META_AIA_MODE_FINANCING = 'financing';
|
|
17
|
+
|
|
11
18
|
// Values must match Meta's Automotive Model Ads offer_type enum exactly (lease/finance/cash) —
|
|
12
19
|
// the old 'financing'/'leasing'/'buy' values are not recognized by the AMA Vehicle Offers Feed.
|
|
13
20
|
export enum OfferType {
|
|
@@ -8,11 +8,6 @@ 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];
|
|
16
11
|
|
|
17
12
|
@Injectable()
|
|
18
13
|
export class FbExporter implements Exporter {
|
|
@@ -34,12 +29,6 @@ export class FbExporter implements Exporter {
|
|
|
34
29
|
|
|
35
30
|
const fbTransformer = await this.moduleRef.create(FbTransformer);
|
|
36
31
|
|
|
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
|
-
|
|
43
32
|
await this.loaderService.process(async (chunk: FsChunkInterface) => {
|
|
44
33
|
const chunkData = chunk.data.filter(car => this.carService.filterCar(car, this.rulesService.rules));
|
|
45
34
|
chunk.data = null;
|
|
@@ -53,24 +42,15 @@ export class FbExporter implements Exporter {
|
|
|
53
42
|
console.timeEnd(`Transform ${data.car._id}`);
|
|
54
43
|
|
|
55
44
|
if (!car) {
|
|
56
|
-
console.log(`Skip car ${data.car._id}
|
|
45
|
+
console.log(`Skip car ${data.car._id} because it has no real images`);
|
|
57
46
|
continue;
|
|
58
47
|
}
|
|
59
48
|
|
|
60
|
-
|
|
49
|
+
await fbXmlBuilder.addItem(car as FbXmlItem);
|
|
61
50
|
this.carService.counter.countCar(data.images);
|
|
62
51
|
}
|
|
63
52
|
});
|
|
64
53
|
|
|
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
|
-
|
|
74
54
|
await fbXmlBuilder.buildEnd();
|
|
75
55
|
console.log(this.carService.counter.getCountedCars());
|
|
76
56
|
}
|
|
@@ -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 } from '../constants/fb.constants';
|
|
11
|
+
import { FB_MAX_IMAGE_COUNT_FOR_DEAFULT, META_AIA_CHANNEL_CODE } 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 {
|
|
16
|
+
import { ImageSize, ImageType } from '../enums';
|
|
17
17
|
|
|
18
18
|
@Injectable()
|
|
19
19
|
export class ImagesService {
|
|
@@ -176,28 +176,10 @@ export class ImagesService {
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
async getMarketingImages(carId: string, images: ImboImageResponseInterface[], ignoreDefImages: boolean): Promise<string[]> {
|
|
179
|
-
const
|
|
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')
|
|
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. baseImageUrl is the one field neither Config nor FbConfig
|
|
196
|
-
// ever suffixes with a path, so it's the only safe base to build /generated_images on.
|
|
197
|
-
const imageUrl = config.channelCode === 'meta_aia'
|
|
198
|
-
? `${this.configService.config.baseImageUrl}/generated_images`
|
|
199
|
-
: this.configService.config.marketingImageUrl;
|
|
179
|
+
const imageUrl = this.configService.config.marketingImageUrl;
|
|
200
180
|
|
|
181
|
+
const transformedImages = this.transformImagesArrayIntoObject(images);
|
|
182
|
+
const config = await this.userService.getMarketingImageConfig(this.configService.config.marketingImageConfigId);
|
|
201
183
|
const marketingImageConfigs = config.marketingImages.map(i => ({ marketingImageCodes: i.imageCodes }));
|
|
202
184
|
|
|
203
185
|
const imageWithUrls: Partial<ImboImageResponseInterface & { url: string }>[] = [];
|
|
@@ -209,7 +191,7 @@ export class ImagesService {
|
|
|
209
191
|
if (imageId) {
|
|
210
192
|
imageWithUrls.push({
|
|
211
193
|
imageCodePredicted: imageCode,
|
|
212
|
-
url: this.getImagePathWithExtension(imageUrl, carId, imageId, config)
|
|
194
|
+
url: this.getImagePathWithExtension(imageUrl, carId, imageId, config)
|
|
213
195
|
});
|
|
214
196
|
break;
|
|
215
197
|
}
|
|
@@ -226,6 +208,56 @@ export class ImagesService {
|
|
|
226
208
|
return imageWithUrls.map(i => i.url);
|
|
227
209
|
}
|
|
228
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Meta AIA marketing image (see MetaAiaImagesService in image-generator) for a single car —
|
|
213
|
+
* unlike getMarketingImages, this is one channel/mode = one link, not a per-imageCode array.
|
|
214
|
+
* Reads the "meta_aia" MarketingImageExportConfig for its allowed Image codes and the real photo
|
|
215
|
+
* imageId behind them, and builds the same `meta-aia-{mode}-{imageId}.{ext}` filename
|
|
216
|
+
* MetaAiaImagesService saves under the shared /generated_images FTP root. Returns null if the
|
|
217
|
+
* admin config doesn't exist yet or the car has no eligible photo — callers should skip the car
|
|
218
|
+
* rather than fall back to a placeholder.
|
|
219
|
+
*/
|
|
220
|
+
async getMetaAiaImage(carId: string, images: ImboImageResponseInterface[], mode: string): Promise<string | null> {
|
|
221
|
+
const configs = await this.userService.getMarketingImageConfigs();
|
|
222
|
+
const match = configs.find(c => c.channelCode === META_AIA_CHANNEL_CODE);
|
|
223
|
+
if (!match) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const config = await this.userService.getMarketingImageConfig(match.id);
|
|
228
|
+
const eligibleCodes = new Set(config.marketingImages.flatMap(mi => mi.imageCodes ?? []));
|
|
229
|
+
if (!eligibleCodes.size) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const transformedImages = this.transformImagesArrayIntoObject(images);
|
|
234
|
+
const candidateImageIds = [...eligibleCodes].map(code => transformedImages[code]).filter(Boolean);
|
|
235
|
+
const channelPath = this.getChannelPath(this.configService.config.metaAiaImageUrl, carId);
|
|
236
|
+
|
|
237
|
+
// A matching imageCode only proves the car HAS an eligible photo, not that image-generator's
|
|
238
|
+
// pubsub-triggered render actually succeeded/landed on the CDN yet (it fails closed and skips
|
|
239
|
+
// silently on config/dealer-scope gaps, race conditions, etc.) — HEAD-check for real, don't
|
|
240
|
+
// just trust the URL shape. Tries every eligible photo (e.g. ext_f_left then ext_f_right)
|
|
241
|
+
// before giving up, in case only one of them was actually rendered.
|
|
242
|
+
for (const imageId of candidateImageIds) {
|
|
243
|
+
const url = `${channelPath}/meta-aia-${mode}-${imageId}.${config.imageExtension}`;
|
|
244
|
+
if (await this.imageUrlExists(url)) {
|
|
245
|
+
return url;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private async imageUrlExists(url: string): Promise<boolean> {
|
|
253
|
+
try {
|
|
254
|
+
const response = await axios.head(url, { timeout: 5000, validateStatus: status => status === 200 });
|
|
255
|
+
return response.status === 200;
|
|
256
|
+
} catch {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
229
261
|
private transformImagesArrayIntoObject(images: ImboImageResponseInterface[]): { [imageCode: string]: string } {
|
|
230
262
|
return images.reduce((result, image) => {
|
|
231
263
|
const imageCode = image?.imageCodePredicted?.split('-')[0];
|
|
@@ -242,11 +274,7 @@ export class ImagesService {
|
|
|
242
274
|
|
|
243
275
|
protected getImagePathWithExtension(imageUrl: string, carId: string, imageId: string, config: MarketingImageConfigsInterface): string {
|
|
244
276
|
const { channelCode, imageExtension } = config;
|
|
245
|
-
|
|
246
|
-
// `meta-aia-leasing-{imageId}` (hyphenated, mode baked into the name) — not derived from
|
|
247
|
-
// channelCode the way other channels are. FB only wants the financing variant.
|
|
248
|
-
const filenamePrefix = channelCode === 'meta_aia' ? 'meta-aia-financing' : channelCode.replace(/ /g, '-');
|
|
249
|
-
return `${this.getChannelPath(imageUrl, carId)}/${filenamePrefix}-${imageId}.${imageExtension}`;
|
|
277
|
+
return `${this.getChannelPath(imageUrl, carId)}/${channelCode.replace(/ /g, '-')}-${imageId}.${imageExtension}`;
|
|
250
278
|
}
|
|
251
279
|
|
|
252
280
|
sortImages(images: ImboImageResponseInterface[]): ImboImageResponseInterface[] {
|
|
@@ -67,17 +67,4 @@ 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
|
-
}
|
|
83
70
|
}
|
|
@@ -13,6 +13,7 @@ import { FacebookShopItem } from '../interfaces';
|
|
|
13
13
|
import { ConfigService, ImagesService, MappingService } from '../services';
|
|
14
14
|
import {
|
|
15
15
|
AMOUNT_QUALIFIER_APR,
|
|
16
|
+
META_AIA_MODE_FINANCING,
|
|
16
17
|
META_PUSH_PRIORITY_ALLOWED_VALUES,
|
|
17
18
|
META_PUSH_PRIORITY_STANDARD,
|
|
18
19
|
OFFER_DISCLAIMER,
|
|
@@ -60,9 +61,11 @@ export class FacebookShopTransformer {
|
|
|
60
61
|
car: DataSchemes.Car;
|
|
61
62
|
images: ImboImageResponseInterface[];
|
|
62
63
|
}): Promise<FacebookShopItem[]> {
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
// Same Meta AIA financing image for every offer type on this car (per ticket) — leasing/other
|
|
65
|
+
// modes will be added once image-generator renders them too. No eligible image => exclude the
|
|
66
|
+
// car rather than fall back to a placeholder.
|
|
67
|
+
const metaAiaImage = await this.imageService.getMetaAiaImage(car._id, images, META_AIA_MODE_FINANCING);
|
|
68
|
+
if (!metaAiaImage) {
|
|
66
69
|
return [];
|
|
67
70
|
}
|
|
68
71
|
|
|
@@ -83,7 +86,7 @@ export class FacebookShopTransformer {
|
|
|
83
86
|
model,
|
|
84
87
|
year: this.getYear(car),
|
|
85
88
|
url: this.getCarLink(car),
|
|
86
|
-
image:
|
|
89
|
+
image: [metaAiaImage],
|
|
87
90
|
price: `${price} EUR`,
|
|
88
91
|
availability: this.getAvailability(car),
|
|
89
92
|
exterior_color: this.getColor(car),
|
|
@@ -46,8 +46,13 @@ export class FbDescription {
|
|
|
46
46
|
key: get(this.car, 'engineData.cylinders', '') as string,
|
|
47
47
|
},
|
|
48
48
|
},
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
{
|
|
50
|
+
title: 'accordion.captionCapacity',
|
|
51
|
+
value: {
|
|
52
|
+
key: 'title.cubicCapacity',
|
|
53
|
+
data: { cubicCapacity: get(this.car, 'engineData.cubicCapacity') },
|
|
54
|
+
},
|
|
55
|
+
},
|
|
51
56
|
{
|
|
52
57
|
title: 'accordion.captionPower',
|
|
53
58
|
value: {
|
|
@@ -161,8 +166,12 @@ export class FbDescription {
|
|
|
161
166
|
},
|
|
162
167
|
},
|
|
163
168
|
},
|
|
164
|
-
|
|
165
|
-
|
|
169
|
+
{
|
|
170
|
+
title: 'title.base',
|
|
171
|
+
value: {
|
|
172
|
+
key: get(this.car, 'driveSuspension.wheelBase', '') as string,
|
|
173
|
+
},
|
|
174
|
+
},
|
|
166
175
|
{
|
|
167
176
|
title: 'title.loadingVolume',
|
|
168
177
|
value: {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Cache } from 'cache-manager';
|
|
2
2
|
import * as moment from 'moment';
|
|
3
3
|
import {
|
|
4
|
+
escape,
|
|
4
5
|
findIndex,
|
|
5
6
|
get,
|
|
6
7
|
includes,
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
join,
|
|
12
13
|
toSafeInteger,
|
|
13
14
|
toString,
|
|
15
|
+
trim,
|
|
14
16
|
truncate,
|
|
15
17
|
} from 'lodash';
|
|
16
18
|
|
|
@@ -28,7 +30,7 @@ import { Description } from '../interfaces/description.interface';
|
|
|
28
30
|
import { FbDescription } from './fb-description.transformer';
|
|
29
31
|
import { ConfigService, ImagesService, MappingService } from '../services';
|
|
30
32
|
import { FbEquipment } from './fb-equipment.transformer';
|
|
31
|
-
import { isValidVin
|
|
33
|
+
import { isValidVin } from '../utils';
|
|
32
34
|
import { UserService } from 'src/services';
|
|
33
35
|
import { ImageType } from '../enums';
|
|
34
36
|
import {
|
|
@@ -186,16 +188,14 @@ export class FbTransformer {
|
|
|
186
188
|
}
|
|
187
189
|
|
|
188
190
|
private getTitle(car: DataSchemes.Car): string {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
191
|
+
let year = toString(get(car, 'mainData.constructionYear', ''));
|
|
192
|
+
if (year === '0') {
|
|
193
|
+
year = '';
|
|
194
|
+
}
|
|
193
195
|
const make = toString(get(car, 'mainData.make', ''));
|
|
194
196
|
const model = toString(get(car, 'mainData.model', ''));
|
|
195
197
|
const subModel = toString(get(car, 'mainData.subModel', ''));
|
|
196
|
-
|
|
197
|
-
// decoded by the consumer and leak literally ("Monte Carlo & KESSY").
|
|
198
|
-
return normalizeTitle(join([year, make, model, subModel], ' '));
|
|
198
|
+
return trim(escape(join([year, make, model, subModel], ' ')));
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
private getTrim(car: DataSchemes.Car): string {
|
|
@@ -14,23 +14,6 @@ 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 #") which must not reach Meta. Keep letters, digits and
|
|
19
|
-
// the punctuation that carries meaning in German trim names — "." "," "-" "/" "(" ")" "%",
|
|
20
|
-
// "°" (360° Kamera), '"' (Zoll) and "&" (audio brands: B&O, h&k) — and turn everything else
|
|
21
|
-
// into a space so neighbouring words never fuse.
|
|
22
|
-
const TITLE_DISALLOWED_CHARS = /[^\p{L}\p{N} .,\-/()%"°&]/gu;
|
|
23
|
-
// Leading/trailing separators only; '"' and "%" are legitimate title endings (`LM18"`, `-35%`).
|
|
24
|
-
const TITLE_EDGE_SEPARATORS = /^[\s.,\-/&]+|[\s.,\-/&]+$/g;
|
|
25
|
-
|
|
26
|
-
export function normalizeTitle(title: string): string {
|
|
27
|
-
return (title || '')
|
|
28
|
-
.replace(TITLE_DISALLOWED_CHARS, ' ')
|
|
29
|
-
.replace(/([.,\-/])\1+/g, '$1') // "L4H2 //Klimaanlage" -> "L4H2 /Klimaanlage"
|
|
30
|
-
.replace(/\s+/g, ' ')
|
|
31
|
-
.replace(TITLE_EDGE_SEPARATORS, '');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
17
|
// Parses a "Publish to web" Google Sheets CSV export (RFC4180-ish, quoted fields may
|
|
35
18
|
// contain commas/newlines) into row arrays, then maps rows to objects keyed by header.
|
|
36
19
|
export function parseCsvToObjects<T>(csv: string): T[] {
|