@bytebrand/export-cars 1.26.35 → 1.26.37
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/criteo.exporter.js +7 -1
- package/dist/exporters/criteo.exporter.js.map +1 -1
- package/dist/exporters/facebook-shop.exporter.js +1 -3
- package/dist/exporters/facebook-shop.exporter.js.map +1 -1
- package/dist/formats/facebook-shop.builder.d.ts +1 -1
- package/dist/formats/facebook-shop.builder.js +6 -9
- package/dist/formats/facebook-shop.builder.js.map +1 -1
- package/dist/transformers/criteo.transformer.d.ts +1 -1
- package/dist/transformers/criteo.transformer.js +4 -1
- package/dist/transformers/criteo.transformer.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/exporters/criteo.exporter.ts +6 -1
- package/src/exporters/facebook-shop.exporter.ts +1 -3
- package/src/formats/facebook-shop.builder.ts +8 -9
- package/src/transformers/criteo.transformer.ts +7 -2
- package/test/criteo-images.test.ts +54 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bytebrand/export-cars",
|
|
3
|
-
"version": "1.26.
|
|
3
|
+
"version": "1.26.37",
|
|
4
4
|
"description": "",
|
|
5
5
|
"author": "bytebrand",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
12
12
|
"lint": "eslint -c .eslintrc.js --ext .ts src/",
|
|
13
13
|
"start:dev": "nest start --watch",
|
|
14
|
-
"test": "ts-node --transpile-only test/google-feed-envkv.test.ts && ts-node --transpile-only test/google-feed-item-row.test.ts && ts-node --transpile-only test/publication-gate.test.ts && ts-node --transpile-only test/images-regression.test.ts && ts-node --transpile-only test/clean-images.test.ts && ts-node --transpile-only test/image-url-validator.test.ts && ts-node --transpile-only test/openai-image-fields.test.ts && ts-node --transpile-only test/openai-ads-mapping.test.ts && ts-node --transpile-only test/openai-ads-feed-run.test.ts && ts-node --transpile-only test/openai-ads-finance.test.ts && ts-node --transpile-only test/openai-ads-price-cap.test.ts && ts-node --transpile-only test/meinstadt-mapping.test.ts && ts-node --transpile-only test/meinstadt-feed-run.test.ts && ts-node --transpile-only test/autouncle-mapping.test.ts && ts-node --transpile-only test/autouncle-feed-run.test.ts"
|
|
14
|
+
"test": "ts-node --transpile-only test/google-feed-envkv.test.ts && ts-node --transpile-only test/google-feed-item-row.test.ts && ts-node --transpile-only test/publication-gate.test.ts && ts-node --transpile-only test/images-regression.test.ts && ts-node --transpile-only test/clean-images.test.ts && ts-node --transpile-only test/image-url-validator.test.ts && ts-node --transpile-only test/openai-image-fields.test.ts && ts-node --transpile-only test/openai-ads-mapping.test.ts && ts-node --transpile-only test/openai-ads-feed-run.test.ts && ts-node --transpile-only test/openai-ads-finance.test.ts && ts-node --transpile-only test/openai-ads-price-cap.test.ts && ts-node --transpile-only test/meinstadt-mapping.test.ts && ts-node --transpile-only test/meinstadt-feed-run.test.ts && ts-node --transpile-only test/autouncle-mapping.test.ts && ts-node --transpile-only test/autouncle-feed-run.test.ts && ts-node --transpile-only test/criteo-images.test.ts"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@bytebrand/autode-request": "^1.19.2",
|
|
@@ -28,19 +28,24 @@ export class CriteoExporter implements Exporter {
|
|
|
28
28
|
await googleXmlBuilder.buildStart(this.configService.config.domain);
|
|
29
29
|
|
|
30
30
|
const criteoTransformer = await this.moduleRef.create(CriteoTransformer);
|
|
31
|
+
let withoutImage = 0;
|
|
31
32
|
|
|
32
33
|
await this.loaderService.process(async (chunk: FsChunkInterface) => {
|
|
33
34
|
chunk.data = chunk.data.filter(car => this.carService.filterCar(car, this.rulesService.rules));
|
|
34
35
|
|
|
35
36
|
for (const data of chunk.data) {
|
|
36
37
|
const car = await criteoTransformer.transform(data);
|
|
37
|
-
if (car)
|
|
38
|
+
if (car) {
|
|
38
39
|
await googleXmlBuilder.addItem(car);
|
|
40
|
+
} else {
|
|
41
|
+
withoutImage += 1;
|
|
42
|
+
}
|
|
39
43
|
this.carService.counter.countCar(data.images);
|
|
40
44
|
}
|
|
41
45
|
});
|
|
42
46
|
|
|
43
47
|
await googleXmlBuilder.buildEnd();
|
|
44
48
|
console.log(this.carService.counter.getCountedCars());
|
|
49
|
+
console.log(`criteo: ${withoutImage} cars skipped without a marketing image`);
|
|
45
50
|
}
|
|
46
51
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Injectable } from '@nestjs/common';
|
|
2
2
|
import { ModuleRef } from '@nestjs/core';
|
|
3
|
-
import { basename } from 'path';
|
|
4
3
|
|
|
5
4
|
import { CronicleUtils } from '@bytebrand/cronicle-helper';
|
|
6
5
|
import { FsChunkInterface } from '@bytebrand/chunkable';
|
|
@@ -25,9 +24,8 @@ export class FacebookShopExporter implements Exporter {
|
|
|
25
24
|
await CronicleUtils.ensureDir(this.configService.config.pathesConfig.localConvertedDirectory);
|
|
26
25
|
await CronicleUtils.emptyDir(this.configService.config.pathesConfig.localConvertedDirectory);
|
|
27
26
|
|
|
28
|
-
const selfUrl = `https://ftp.${this.configService.config.domain}/feeds/${basename(fileXml)}`;
|
|
29
27
|
const facebookShopXmlBuilder = new FacebookShopXmlBuilder(fileXml);
|
|
30
|
-
await facebookShopXmlBuilder.buildStart(
|
|
28
|
+
await facebookShopXmlBuilder.buildStart();
|
|
31
29
|
|
|
32
30
|
const facebookShopTransformer = await this.moduleRef.create(FacebookShopTransformer);
|
|
33
31
|
|
|
@@ -8,18 +8,17 @@ export class FacebookShopXmlBuilder extends XMLBuilderBase {
|
|
|
8
8
|
// No xmlns:g / g: prefixes here — that namespace belongs to the old Google Merchant /
|
|
9
9
|
// Facebook Shop product-catalog schema. Meta's Automotive Model Ads Vehicle Offers Feed
|
|
10
10
|
// uses plain, undecorated tag names (see facebook-shop.builder field reference above).
|
|
11
|
-
|
|
11
|
+
// Envelope is <listings>/<listing>, not RSS <rss><channel>/<item>: Meta Commerce Manager
|
|
12
|
+
// rejects the RSS form for a Vehicle Offers catalog as an invalid file format (ADV-369).
|
|
13
|
+
async buildStart(): Promise<void> {
|
|
12
14
|
await this.write(
|
|
13
|
-
'<?xml version="1.0" encoding="utf-8"?>\n<
|
|
14
|
-
'\n<
|
|
15
|
-
'\n<description>Automotive Model Ads Vehicle Offers Feed für auto.de</description>' +
|
|
16
|
-
'\n<link>https://www.auto.de/</link>' +
|
|
17
|
-
`\n<atom:link href="${escape(selfUrl)}" rel="self" type="application/rss+xml" />\n`,
|
|
15
|
+
'<?xml version="1.0" encoding="utf-8"?>\n<listings>' +
|
|
16
|
+
'\n<title>auto.de Vehicle Offers Feed</title>\n',
|
|
18
17
|
);
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
async addItem(car: FacebookShopItem): Promise<void> {
|
|
22
|
-
let item = '\n<
|
|
21
|
+
let item = '\n<listing>\n';
|
|
23
22
|
|
|
24
23
|
const { image, ...tags } = car;
|
|
25
24
|
|
|
@@ -32,7 +31,7 @@ export class FacebookShopXmlBuilder extends XMLBuilderBase {
|
|
|
32
31
|
.map((url: string) => `<image><url>${escape(url)}</url></image>\n`)
|
|
33
32
|
.join('');
|
|
34
33
|
|
|
35
|
-
item += '</
|
|
34
|
+
item += '</listing>';
|
|
36
35
|
|
|
37
36
|
await this.write(item);
|
|
38
37
|
}
|
|
@@ -53,7 +52,7 @@ export class FacebookShopXmlBuilder extends XMLBuilderBase {
|
|
|
53
52
|
}
|
|
54
53
|
|
|
55
54
|
async buildEnd(): Promise<void> {
|
|
56
|
-
await this.write('\n</
|
|
55
|
+
await this.write('\n</listings>\n');
|
|
57
56
|
await this.end();
|
|
58
57
|
}
|
|
59
58
|
}
|
|
@@ -20,14 +20,19 @@ export class CriteoTransformer {
|
|
|
20
20
|
this.translator = new TranslatorExtended(dict);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
// Returns null for a car without a real marketing image — such cars are not sent to Criteo
|
|
24
|
+
// (no noimage placeholders in the feed).
|
|
25
|
+
async transform({ car, images }: { car: DataSchemes.Car; images: Array<any> }): Promise<CriteoXmlItem | null> {
|
|
24
26
|
const carId = toString(get(car, '_id', ''));
|
|
25
27
|
const transformedImages = await this.imageService.getImages(
|
|
26
28
|
car,
|
|
27
29
|
images,
|
|
28
30
|
this.configService.config.imageType,
|
|
29
|
-
|
|
31
|
+
true,
|
|
30
32
|
);
|
|
33
|
+
if (!transformedImages[0]) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
31
36
|
|
|
32
37
|
return {
|
|
33
38
|
id: carId,
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Run: npm test (ts-node, no test framework needed)
|
|
2
|
+
// ADV-319: a car without a real Criteo marketing image is left out of the feed; no noimage placeholders.
|
|
3
|
+
import * as assert from 'node:assert/strict';
|
|
4
|
+
import { CriteoTransformer } from '../src/transformers/criteo.transformer';
|
|
5
|
+
|
|
6
|
+
const car: any = {
|
|
7
|
+
_id: '00000000-0000-4000-8000-000000000001',
|
|
8
|
+
mainData: { make: 'Volkswagen', model: 'Golf', condition: 'selector_condition_used', category: 'selector_category_limousine' },
|
|
9
|
+
price: { currentSalesPrice: 19990 },
|
|
10
|
+
offer: { availabilityMode: 'selector_availabilityMode_always' },
|
|
11
|
+
metaData: { ownerId: 'test-dealer', imagesCount: 5 },
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const makeTransformer = (images: string[]) => {
|
|
15
|
+
const calls: boolean[] = [];
|
|
16
|
+
const imageService: any = {
|
|
17
|
+
getImages: async (_car: any, _images: any, _type: any, ignoreDefImages: boolean) => {
|
|
18
|
+
calls.push(ignoreDefImages);
|
|
19
|
+
return images;
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
const configService: any = { config: { isCriteo: true, imageType: 'marketing', domain: 'autode-dev.de' } };
|
|
23
|
+
return { transformer: new CriteoTransformer({} as any, configService, imageService), calls };
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const tests: Array<[string, () => Promise<void>]> = [
|
|
27
|
+
['car with a marketing image is exported with it as image_link', async () => {
|
|
28
|
+
const url = 'https://images.autode-dev.de/generated_images/0/0/0/00000000-0000-4000-8000-000000000001/criteo-Test.jpg';
|
|
29
|
+
const { transformer, calls } = makeTransformer([url]);
|
|
30
|
+
const item = await transformer.transform({ car, images: [] });
|
|
31
|
+
assert.equal(item?.image_link, url);
|
|
32
|
+
assert.equal(item?.id, car._id);
|
|
33
|
+
assert.deepEqual(calls, [true]);
|
|
34
|
+
}],
|
|
35
|
+
['car without a marketing image is skipped and placeholders are never requested', async () => {
|
|
36
|
+
const { transformer, calls } = makeTransformer([]);
|
|
37
|
+
assert.equal(await transformer.transform({ car, images: [] }), null);
|
|
38
|
+
assert.deepEqual(calls, [true]);
|
|
39
|
+
}],
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
(async () => {
|
|
43
|
+
let failed = 0;
|
|
44
|
+
for (const [name, fn] of tests) {
|
|
45
|
+
try {
|
|
46
|
+
await fn();
|
|
47
|
+
console.log(`ok ${name}`);
|
|
48
|
+
} catch (e) {
|
|
49
|
+
failed += 1;
|
|
50
|
+
console.error(`FAIL ${name}\n ${e.message}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (failed) process.exit(1);
|
|
54
|
+
})();
|