@bytebrand/export-cars 1.26.40 → 1.26.42

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 (32) hide show
  1. package/dist/configs/autouncle.config.js +22 -0
  2. package/dist/configs/autouncle.config.js.map +1 -1
  3. package/dist/exporters/autouncle.exporter.js +10 -0
  4. package/dist/exporters/autouncle.exporter.js.map +1 -1
  5. package/dist/services/autouncle-feed-run.d.ts +6 -0
  6. package/dist/services/autouncle-feed-run.js +11 -1
  7. package/dist/services/autouncle-feed-run.js.map +1 -1
  8. package/dist/transformers/autouncle.transformer.d.ts +4 -0
  9. package/dist/transformers/autouncle.transformer.js +10 -5
  10. package/dist/transformers/autouncle.transformer.js.map +1 -1
  11. package/dist/transformers/google-feed-envkv.d.ts +2 -0
  12. package/dist/transformers/google-feed-envkv.js +9 -1
  13. package/dist/transformers/google-feed-envkv.js.map +1 -1
  14. package/dist/tsconfig.build.tsbuildinfo +1 -1
  15. package/docs/autouncle/AUDIT.md +103 -97
  16. package/docs/autouncle/PARTNER-QUESTIONS.md +3 -1
  17. package/docs/autouncle/RUNBOOK.md +2 -0
  18. package/docs/criteo/RUNBOOK.md +86 -86
  19. package/package.json +1 -1
  20. package/src/configs/autouncle.config.ts +26 -1
  21. package/src/configs/criteo.config.ts +54 -54
  22. package/src/exporters/autouncle.exporter.ts +12 -0
  23. package/src/exporters/criteo.exporter.ts +63 -63
  24. package/src/services/autouncle-feed-run.ts +19 -2
  25. package/src/services/criteo-feed-run.ts +253 -253
  26. package/src/transformers/autouncle.transformer.ts +20 -4
  27. package/src/transformers/google-feed-envkv.ts +10 -0
  28. package/test/autouncle-feed-run.test.ts +25 -2
  29. package/test/autouncle-mapping.test.ts +41 -2
  30. package/test/criteo-feed-run.test.ts +365 -365
  31. package/test/criteo-mapping.test.ts +122 -122
  32. package/test/fixtures/criteo-20-cases.json +581 -581
@@ -1,122 +1,122 @@
1
- // Run: npm test (ts-node, no test framework needed)
2
- // ADV-319: Criteo item mapping bugs found in the DEV feed of 27.10.2026.
3
- // - custom_label_0 was "[object Object] EUR mtl." for every car;
4
- // - a car without a rate got "<g:months>0</g:months><g:amount>0 EUR</g:amount>";
5
- // - a title with an apostrophe was escaped twice ("cee&amp;#39;d");
6
- // - an unknown energy class was sent as the literal "default".
7
- import * as assert from 'node:assert/strict';
8
- import * as fs from 'node:fs';
9
- import * as os from 'node:os';
10
- import * as path from 'node:path';
11
- import { CriteoTransformer } from '../src/transformers/criteo.transformer';
12
- import { GoogleXmlBuilder } from '../src/formats/google-xml.builder';
13
-
14
- const IMAGE = 'https://images.autode-dev.de/generated_images/0/0/0/00000000-0000-4000-8000-000000000001/criteo-Test.jpg';
15
-
16
- const baseCar = (patch: any = {}): any => ({
17
- _id: '00000000-0000-4000-8000-000000000001',
18
- mainData: { make: 'Volkswagen', model: 'Golf', condition: 'selector_condition_used', category: 'selector_category_limousine' },
19
- price: { currentSalesPrice: 19990 },
20
- offer: { availabilityMode: 'selector_availabilityMode_always' },
21
- environmentEmissions: { wltpEnergyEfficiencyClass: 'selector_energyEfficiencyClass_d' },
22
- rate: { monthlyInstallment: 164.6, paybackPeriod: 60 },
23
- metaData: { ownerId: 'test-dealer', imagesCount: 5 },
24
- ...patch,
25
- });
26
-
27
- const transformer = new CriteoTransformer(
28
- {} as any,
29
- { config: { isCriteo: true, imageType: 'marketing', domain: 'autode-dev.de' } } as any,
30
- { getImages: async () => [IMAGE] } as any,
31
- );
32
-
33
- const renderItem = async (item: any): Promise<string> => {
34
- const file = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'criteo-mapping-')), 'feed.xml');
35
- const builder = new GoogleXmlBuilder(file);
36
- await builder.buildStart('autode-dev.de');
37
- await builder.addItem(item);
38
- await builder.buildEnd();
39
- return fs.readFileSync(file, 'utf8');
40
- };
41
-
42
- const tests: Array<[string, () => Promise<void>]> = [
43
- ['custom_label_0 carries the rounded monthly rate, installment keeps months and amount', async () => {
44
- const item = await transformer.transform({ car: baseCar(), images: [] });
45
- assert.equal(item?.custom_label_0, '165 EUR mtl.');
46
- assert.deepEqual(item?.installment, { paybackPeriod: 60, monthlyInstallment: 165 });
47
- const xml = await renderItem(item);
48
- assert.match(xml, /<g:months>60<\/g:months>/);
49
- assert.match(xml, /<g:amount>165 EUR<\/g:amount>/);
50
- assert.ok(!xml.includes('[object Object]'));
51
- }],
52
- ['car without a rate has no installment and no custom_label_0', async () => {
53
- for (const rate of [undefined, null, {}, { monthlyInstallment: 0, paybackPeriod: 0 }, { monthlyInstallment: 199 }, { paybackPeriod: 48 }]) {
54
- const item = await transformer.transform({ car: baseCar({ rate }), images: [] });
55
- assert.ok(item, `item for rate ${JSON.stringify(rate)}`);
56
- assert.ok(!('installment' in item), `installment for rate ${JSON.stringify(rate)}`);
57
- assert.ok(!('custom_label_0' in item), `custom_label_0 for rate ${JSON.stringify(rate)}`);
58
- const xml = await renderItem(item);
59
- assert.ok(!xml.includes('g:installment') && !xml.includes('g:custom_label_0'));
60
- }
61
- }],
62
- ['title with an apostrophe is escaped once, by the XML builder', async () => {
63
- const item = await transformer.transform({
64
- car: baseCar({ mainData: { make: 'Kia', model: "cee'd", condition: 'selector_condition_used' } }),
65
- images: [],
66
- });
67
- assert.equal(item?.title, "Kia cee'd");
68
- const xml = await renderItem(item);
69
- assert.match(xml, /<g:title>Kia cee&apos;d<\/g:title>/);
70
- assert.ok(!xml.includes('&amp;#39;'));
71
- }],
72
- ['title keeps the Mercedes / VW brand aliases (ACF-6332, ACF-6534)', async () => {
73
- const vw = await transformer.transform({ car: baseCar(), images: [] });
74
- assert.equal(vw?.title, 'VW Golf');
75
- assert.equal(vw?.brand, 'Volkswagen');
76
- const mb = await transformer.transform({
77
- car: baseCar({ mainData: { make: 'Mercedes-Benz', model: 'A 180', condition: 'selector_condition_used' } }),
78
- images: [],
79
- });
80
- assert.equal(mb?.title, 'Mercedes A 180');
81
- }],
82
- ['known energy class is sent, unknown or missing class leaves the element out', async () => {
83
- const known = await transformer.transform({ car: baseCar(), images: [] });
84
- assert.equal(known?.energy_efficiency_class, 'D');
85
- for (const environmentEmissions of [{ wltpEnergyEfficiencyClass: 'selector_unknown' }, {}, undefined]) {
86
- const item = await transformer.transform({ car: baseCar({ environmentEmissions }), images: [] });
87
- assert.ok(item && !('energy_efficiency_class' in item));
88
- const xml = await renderItem(item);
89
- assert.ok(!xml.includes('g:energy_efficiency_class') && !xml.includes('>default<'));
90
- }
91
- }],
92
- ['unchanged legacy fields: id, link params, price, condition, availability', async () => {
93
- const item = await transformer.transform({ car: baseCar(), images: [] });
94
- assert.equal(item?.id, '00000000-0000-4000-8000-000000000001');
95
- assert.equal(
96
- item?.link,
97
- 'https://www.autode-dev.de/search/vehicle/00000000-0000-4000-8000-000000000001?source=criteo&medium=cpc&campaign=retargeting',
98
- );
99
- assert.equal(item?.price, '19990 EUR');
100
- assert.equal(item?.sale_price, '19990 EUR');
101
- assert.equal(item?.condition, 'Gebraucht');
102
- assert.equal(item?.availability, 'Sofort');
103
- assert.deepEqual(Object.keys(item as any), [
104
- 'id', 'title', 'description', 'google_product_category', 'product_type', 'link', 'image_link', 'condition',
105
- 'availability', 'brand', 'price', 'sale_price', 'energy_efficiency_class', 'color', 'installment', 'custom_label_0',
106
- ]);
107
- }],
108
- ];
109
-
110
- (async () => {
111
- let failed = 0;
112
- for (const [name, fn] of tests) {
113
- try {
114
- await fn();
115
- console.log(`ok ${name}`);
116
- } catch (e) {
117
- failed += 1;
118
- console.error(`FAIL ${name}\n ${e.message}`);
119
- }
120
- }
121
- if (failed) process.exit(1);
122
- })();
1
+ // Run: npm test (ts-node, no test framework needed)
2
+ // ADV-319: Criteo item mapping bugs found in the DEV feed of 27.10.2026.
3
+ // - custom_label_0 was "[object Object] EUR mtl." for every car;
4
+ // - a car without a rate got "<g:months>0</g:months><g:amount>0 EUR</g:amount>";
5
+ // - a title with an apostrophe was escaped twice ("cee&amp;#39;d");
6
+ // - an unknown energy class was sent as the literal "default".
7
+ import * as assert from 'node:assert/strict';
8
+ import * as fs from 'node:fs';
9
+ import * as os from 'node:os';
10
+ import * as path from 'node:path';
11
+ import { CriteoTransformer } from '../src/transformers/criteo.transformer';
12
+ import { GoogleXmlBuilder } from '../src/formats/google-xml.builder';
13
+
14
+ const IMAGE = 'https://images.autode-dev.de/generated_images/0/0/0/00000000-0000-4000-8000-000000000001/criteo-Test.jpg';
15
+
16
+ const baseCar = (patch: any = {}): any => ({
17
+ _id: '00000000-0000-4000-8000-000000000001',
18
+ mainData: { make: 'Volkswagen', model: 'Golf', condition: 'selector_condition_used', category: 'selector_category_limousine' },
19
+ price: { currentSalesPrice: 19990 },
20
+ offer: { availabilityMode: 'selector_availabilityMode_always' },
21
+ environmentEmissions: { wltpEnergyEfficiencyClass: 'selector_energyEfficiencyClass_d' },
22
+ rate: { monthlyInstallment: 164.6, paybackPeriod: 60 },
23
+ metaData: { ownerId: 'test-dealer', imagesCount: 5 },
24
+ ...patch,
25
+ });
26
+
27
+ const transformer = new CriteoTransformer(
28
+ {} as any,
29
+ { config: { isCriteo: true, imageType: 'marketing', domain: 'autode-dev.de' } } as any,
30
+ { getImages: async () => [IMAGE] } as any,
31
+ );
32
+
33
+ const renderItem = async (item: any): Promise<string> => {
34
+ const file = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'criteo-mapping-')), 'feed.xml');
35
+ const builder = new GoogleXmlBuilder(file);
36
+ await builder.buildStart('autode-dev.de');
37
+ await builder.addItem(item);
38
+ await builder.buildEnd();
39
+ return fs.readFileSync(file, 'utf8');
40
+ };
41
+
42
+ const tests: Array<[string, () => Promise<void>]> = [
43
+ ['custom_label_0 carries the rounded monthly rate, installment keeps months and amount', async () => {
44
+ const item = await transformer.transform({ car: baseCar(), images: [] });
45
+ assert.equal(item?.custom_label_0, '165 EUR mtl.');
46
+ assert.deepEqual(item?.installment, { paybackPeriod: 60, monthlyInstallment: 165 });
47
+ const xml = await renderItem(item);
48
+ assert.match(xml, /<g:months>60<\/g:months>/);
49
+ assert.match(xml, /<g:amount>165 EUR<\/g:amount>/);
50
+ assert.ok(!xml.includes('[object Object]'));
51
+ }],
52
+ ['car without a rate has no installment and no custom_label_0', async () => {
53
+ for (const rate of [undefined, null, {}, { monthlyInstallment: 0, paybackPeriod: 0 }, { monthlyInstallment: 199 }, { paybackPeriod: 48 }]) {
54
+ const item = await transformer.transform({ car: baseCar({ rate }), images: [] });
55
+ assert.ok(item, `item for rate ${JSON.stringify(rate)}`);
56
+ assert.ok(!('installment' in item), `installment for rate ${JSON.stringify(rate)}`);
57
+ assert.ok(!('custom_label_0' in item), `custom_label_0 for rate ${JSON.stringify(rate)}`);
58
+ const xml = await renderItem(item);
59
+ assert.ok(!xml.includes('g:installment') && !xml.includes('g:custom_label_0'));
60
+ }
61
+ }],
62
+ ['title with an apostrophe is escaped once, by the XML builder', async () => {
63
+ const item = await transformer.transform({
64
+ car: baseCar({ mainData: { make: 'Kia', model: "cee'd", condition: 'selector_condition_used' } }),
65
+ images: [],
66
+ });
67
+ assert.equal(item?.title, "Kia cee'd");
68
+ const xml = await renderItem(item);
69
+ assert.match(xml, /<g:title>Kia cee&apos;d<\/g:title>/);
70
+ assert.ok(!xml.includes('&amp;#39;'));
71
+ }],
72
+ ['title keeps the Mercedes / VW brand aliases (ACF-6332, ACF-6534)', async () => {
73
+ const vw = await transformer.transform({ car: baseCar(), images: [] });
74
+ assert.equal(vw?.title, 'VW Golf');
75
+ assert.equal(vw?.brand, 'Volkswagen');
76
+ const mb = await transformer.transform({
77
+ car: baseCar({ mainData: { make: 'Mercedes-Benz', model: 'A 180', condition: 'selector_condition_used' } }),
78
+ images: [],
79
+ });
80
+ assert.equal(mb?.title, 'Mercedes A 180');
81
+ }],
82
+ ['known energy class is sent, unknown or missing class leaves the element out', async () => {
83
+ const known = await transformer.transform({ car: baseCar(), images: [] });
84
+ assert.equal(known?.energy_efficiency_class, 'D');
85
+ for (const environmentEmissions of [{ wltpEnergyEfficiencyClass: 'selector_unknown' }, {}, undefined]) {
86
+ const item = await transformer.transform({ car: baseCar({ environmentEmissions }), images: [] });
87
+ assert.ok(item && !('energy_efficiency_class' in item));
88
+ const xml = await renderItem(item);
89
+ assert.ok(!xml.includes('g:energy_efficiency_class') && !xml.includes('>default<'));
90
+ }
91
+ }],
92
+ ['unchanged legacy fields: id, link params, price, condition, availability', async () => {
93
+ const item = await transformer.transform({ car: baseCar(), images: [] });
94
+ assert.equal(item?.id, '00000000-0000-4000-8000-000000000001');
95
+ assert.equal(
96
+ item?.link,
97
+ 'https://www.autode-dev.de/search/vehicle/00000000-0000-4000-8000-000000000001?source=criteo&medium=cpc&campaign=retargeting',
98
+ );
99
+ assert.equal(item?.price, '19990 EUR');
100
+ assert.equal(item?.sale_price, '19990 EUR');
101
+ assert.equal(item?.condition, 'Gebraucht');
102
+ assert.equal(item?.availability, 'Sofort');
103
+ assert.deepEqual(Object.keys(item as any), [
104
+ 'id', 'title', 'description', 'google_product_category', 'product_type', 'link', 'image_link', 'condition',
105
+ 'availability', 'brand', 'price', 'sale_price', 'energy_efficiency_class', 'color', 'installment', 'custom_label_0',
106
+ ]);
107
+ }],
108
+ ];
109
+
110
+ (async () => {
111
+ let failed = 0;
112
+ for (const [name, fn] of tests) {
113
+ try {
114
+ await fn();
115
+ console.log(`ok ${name}`);
116
+ } catch (e) {
117
+ failed += 1;
118
+ console.error(`FAIL ${name}\n ${e.message}`);
119
+ }
120
+ }
121
+ if (failed) process.exit(1);
122
+ })();