@bytebrand/export-cars 1.26.47 → 1.26.48
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/configs/ooyyo.config.d.ts +14 -0
- package/dist/configs/ooyyo.config.js +40 -6
- package/dist/configs/ooyyo.config.js.map +1 -1
- package/dist/exporters/ooyyo.exporter.d.ts +5 -4
- package/dist/exporters/ooyyo.exporter.js +45 -25
- package/dist/exporters/ooyyo.exporter.js.map +1 -1
- package/dist/formats/ooyyo-xml.builder.d.ts +7 -2
- package/dist/formats/ooyyo-xml.builder.js +59 -36
- package/dist/formats/ooyyo-xml.builder.js.map +1 -1
- package/dist/interfaces/export-item.interface.d.ts +30 -2
- package/dist/interfaces/params.interface.d.ts +3 -0
- package/dist/services/ooyyo-feed-run.d.ts +81 -0
- package/dist/services/ooyyo-feed-run.js +183 -0
- package/dist/services/ooyyo-feed-run.js.map +1 -0
- package/dist/services/ooyyo-feed-verify.d.ts +13 -0
- package/dist/services/ooyyo-feed-verify.js +104 -0
- package/dist/services/ooyyo-feed-verify.js.map +1 -0
- package/dist/transformers/ooyyo.transformer.d.ts +51 -11
- package/dist/transformers/ooyyo.transformer.js +268 -95
- package/dist/transformers/ooyyo.transformer.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/marktde/AUDIT.md +84 -84
- package/docs/marktde/PARTNER-QUESTIONS.md +49 -49
- package/docs/marktde/RUNBOOK.md +58 -58
- package/docs/marktde/TRACKING.md +75 -69
- package/docs/meinestadt/PARTNER-QUESTIONS.md +17 -0
- package/docs/meinestadt/TRACKING.md +107 -0
- package/docs/ooyyo/AUDIT.md +81 -0
- package/docs/ooyyo/PARTNER-QUESTIONS.md +32 -0
- package/docs/ooyyo/RUNBOOK.md +67 -0
- package/docs/ooyyo/TRACKING.md +54 -0
- package/package.json +66 -65
- package/src/configs/marktde.config.ts +70 -70
- package/src/configs/ooyyo.config.ts +65 -15
- package/src/exporters/marktde.exporter.ts +78 -78
- package/src/exporters/ooyyo.exporter.ts +75 -48
- package/src/formats/ooyyo-xml.builder.ts +87 -48
- package/src/interfaces/export-item.interface.ts +43 -2
- package/src/interfaces/params.interface.ts +5 -0
- package/src/services/marktde-feed-run.ts +233 -233
- package/src/services/ooyyo-feed-run.ts +265 -0
- package/src/services/ooyyo-feed-verify.ts +131 -0
- package/src/transformers/marktde.transformer.ts +255 -255
- package/src/transformers/ooyyo.transformer.ts +378 -109
- package/test/fixtures/ooyyo/OoyyoFormat.xml +125 -0
- package/test/fixtures/ooyyo/OoyyoFormat.xsd +116 -0
- package/test/fixtures/ooyyo.fixture.ts +53 -0
- package/test/mapping-csv.test.ts +65 -65
- package/test/marktde-feed-run.test.ts +263 -263
- package/test/marktde-mapping.test.ts +280 -280
- package/test/ooyyo-feed-run.test.ts +293 -0
- package/test/ooyyo-mapping.test.ts +276 -0
|
@@ -1,280 +1,280 @@
|
|
|
1
|
-
// Run: npm test (ts-node, no test framework needed)
|
|
2
|
-
// markt.de columns (ADV-328) on top of the legacy mobile.de-style row — pure functions, no I/O.
|
|
3
|
-
import * as assert from 'node:assert/strict';
|
|
4
|
-
|
|
5
|
-
import { MarktdeConfig } from '../src/configs/marktde.config';
|
|
6
|
-
import { Environment, ExportType, ImageType } from '../src/enums';
|
|
7
|
-
import {
|
|
8
|
-
MARKTDE_COLUMNS,
|
|
9
|
-
MARKTDE_LINK_PARAMS,
|
|
10
|
-
MarktdeExclusion,
|
|
11
|
-
applyMarktdeColumns,
|
|
12
|
-
getMarktdeExclusion,
|
|
13
|
-
getMarktdeListingUrl,
|
|
14
|
-
getMarktdePriceColumns,
|
|
15
|
-
getMarktdeVin,
|
|
16
|
-
sanitizeMarktdeDescription,
|
|
17
|
-
toMarktdeImages,
|
|
18
|
-
} from '../src/transformers/marktde.transformer';
|
|
19
|
-
import { selectOpenAiImageFields } from '../src/transformers/openai-image-fields';
|
|
20
|
-
import { convertToCSV } from '../src/utils/converter.utils';
|
|
21
|
-
import { buildImagesService } from './fixtures/images-service.harness';
|
|
22
|
-
import { car, eightImages, images } from './fixtures/meinstadt.fixture';
|
|
23
|
-
|
|
24
|
-
const imagesService = buildImagesService();
|
|
25
|
-
const C = MARKTDE_COLUMNS;
|
|
26
|
-
const ctx = { domain: 'auto.de', images: ['https://images.auto.de/carimage/x/hero1/large-cached.jpeg', 'https://images.auto.de/carimage/x/side1/large-cached.jpeg'] };
|
|
27
|
-
|
|
28
|
-
// The row MobileTransformer builds from the mapping sheet (only the columns the test looks at).
|
|
29
|
-
const legacyRow = (c: any): Record<string, unknown> => ({
|
|
30
|
-
'0': '04109003',
|
|
31
|
-
'1': c.metaData.vehicleId,
|
|
32
|
-
'3': c.mainData.make,
|
|
33
|
-
'4': `${c.mainData.model} ${c.mainData.subModel}`,
|
|
34
|
-
'10': 'legacy-price',
|
|
35
|
-
'11': 'legacy-flag',
|
|
36
|
-
'14': 'legacy-vin',
|
|
37
|
-
'25': c.offer?.offerDescription ? 'Fahrzeugbeschreibung\\\\**Ausstattung**\\\\* ABS\\\\' : undefined,
|
|
38
|
-
'26': 'legacy-images',
|
|
39
|
-
'29': 'legacy-rate',
|
|
40
|
-
'98': '6,1',
|
|
41
|
-
'99': 'undefined',
|
|
42
|
-
'402': `https://www.auto.de${c._id}?source=classifieds&medium=listing&campaign=marktde`,
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
const tests: Array<[string, () => void | Promise<void>]> = [
|
|
46
|
-
['column letters: B=1, K=10, L=11, O=14, Z=25, AA=26, AD=29, OM=402 (historic ACF-4370 mapping)', () => {
|
|
47
|
-
const letters = (index: number): string => {
|
|
48
|
-
let n = index + 1;
|
|
49
|
-
let out = '';
|
|
50
|
-
while (n > 0) {
|
|
51
|
-
const r = (n - 1) % 26;
|
|
52
|
-
out = String.fromCharCode(65 + r) + out;
|
|
53
|
-
n = Math.floor((n - 1) / 26);
|
|
54
|
-
}
|
|
55
|
-
return out;
|
|
56
|
-
};
|
|
57
|
-
assert.deepEqual(
|
|
58
|
-
Object.fromEntries(Object.entries(C).map(([k, v]) => [k, letters(Number(v))])),
|
|
59
|
-
{ vehicleId: 'B', price: 'K', vatFlag: 'L', vin: 'O', description: 'Z', images: 'AA', vatRate: 'AD', fuelCombined: 'CU', co2: 'CV', listingUrl: 'OM' },
|
|
60
|
-
);
|
|
61
|
-
}],
|
|
62
|
-
|
|
63
|
-
['link: auto.de vehicle page with source=marktde + identical utm_* (no classifieds, no paid medium)', () => {
|
|
64
|
-
assert.equal(
|
|
65
|
-
getMarktdeListingUrl('auto.de', 'abc123'),
|
|
66
|
-
'https://www.auto.de/search/vehicle/abc123?source=marktde&medium=listing&campaign=listing&utm_source=marktde&utm_medium=listing&utm_campaign=listing',
|
|
67
|
-
);
|
|
68
|
-
assert.ok(!MARKTDE_LINK_PARAMS.includes('classifieds'));
|
|
69
|
-
assert.ok(!/cpc|paid/.test(MARKTDE_LINK_PARAMS));
|
|
70
|
-
}],
|
|
71
|
-
|
|
72
|
-
['VIN: own valid VIN (upper-cased) or empty — never a generated / marketing fake VIN', () => {
|
|
73
|
-
assert.equal(getMarktdeVin(car('a', { 'metaData.vin': 'wvwzzz1kzaw000001' })), 'WVWZZZ1KZAW000001');
|
|
74
|
-
assert.equal(getMarktdeVin(car('a', { 'metaData.vin': '' })), '');
|
|
75
|
-
assert.equal(getMarktdeVin(car('a', { 'marketing.fakeVIN': 'WVWZZZ1KZAW999999' })), '');
|
|
76
|
-
assert.equal(getMarktdeVin(car('a', { 'metaData.vin': 'WVWZZZ1KZAW00000' })), '', '16 chars');
|
|
77
|
-
assert.equal(getMarktdeVin(car('a', { 'metaData.vin': 'WVWZZZ1KZAWO00001' })), '', 'letter O is not allowed');
|
|
78
|
-
assert.equal(getMarktdeVin(car('a', { 'metaData.vin': 'unbekannt' })), '');
|
|
79
|
-
}],
|
|
80
|
-
|
|
81
|
-
['price: VAT-deductible -> net K, L=0, AD=rate; gross is restored exactly as K*(1+AD)', () => {
|
|
82
|
-
const p = getMarktdePriceColumns(car('a', { 'price.currentSalesPrice': 23800, 'price.vatDeductible': true, 'price.vatRate': 19 }));
|
|
83
|
-
assert.deepEqual(p, { price: '20000.00', vatFlag: '0', vatRate: '19', deductible: true, rateDefaulted: false });
|
|
84
|
-
assert.equal(Math.round(Number(p.price) * 1.19), 23800);
|
|
85
|
-
}],
|
|
86
|
-
|
|
87
|
-
['price: not deductible (margin scheme / exempt) -> gross K as stored, L=1, AD=0 — never +19 % on a gross price', () => {
|
|
88
|
-
assert.deepEqual(
|
|
89
|
-
getMarktdePriceColumns(car('a', { 'price.currentSalesPrice': 15148, 'price.vatDeductible': false })),
|
|
90
|
-
{ price: '15148', vatFlag: '1', vatRate: '0', deductible: false, rateDefaulted: false },
|
|
91
|
-
);
|
|
92
|
-
assert.equal(getMarktdePriceColumns(car('a', { 'price.currentSalesPrice': 15148 })).vatFlag, '1', 'unknown = not deductible');
|
|
93
|
-
}],
|
|
94
|
-
|
|
95
|
-
['price: deductible with null / 0 vatRate -> 19 % for BOTH K and AD (legacy kept K gross but AD=19)', () => {
|
|
96
|
-
for (const rate of [null, 0, undefined]) {
|
|
97
|
-
const p = getMarktdePriceColumns(car('a', { 'price.currentSalesPrice': 11900, 'price.vatDeductible': true, 'price.vatRate': rate }));
|
|
98
|
-
assert.deepEqual(p, { price: '10000.00', vatFlag: '0', vatRate: '19', deductible: true, rateDefaulted: true });
|
|
99
|
-
}
|
|
100
|
-
}],
|
|
101
|
-
|
|
102
|
-
['price: missing / 0 / negative / text -> null (car is excluded)', () => {
|
|
103
|
-
for (const value of [null, undefined, '', 0, -5, 'abc', true]) {
|
|
104
|
-
assert.equal(getMarktdePriceColumns(car('a', { 'price.currentSalesPrice': value })), null, String(value));
|
|
105
|
-
}
|
|
106
|
-
}],
|
|
107
|
-
|
|
108
|
-
['exclusions: sold / reserved / no price / no vehicle id / no _id', () => {
|
|
109
|
-
assert.equal(getMarktdeExclusion(car('a')), null);
|
|
110
|
-
assert.equal(getMarktdeExclusion(car('a', { 'offer.offerAvailability': 'selector_offerAvailability_sold' })), MarktdeExclusion.notAvailable);
|
|
111
|
-
assert.equal(getMarktdeExclusion(car('a', { 'offer.offerAvailability': 'selector_offerAvailability_reserved' })), MarktdeExclusion.notAvailable);
|
|
112
|
-
assert.equal(getMarktdeExclusion(car('a', { 'offer.offerAvailability': 'selector_offerAvailability_available' })), null);
|
|
113
|
-
assert.equal(getMarktdeExclusion(car('a', { 'price.currentSalesPrice': null })), MarktdeExclusion.noPrice);
|
|
114
|
-
assert.equal(getMarktdeExclusion(car('a', { 'metaData.vehicleId': '' })), MarktdeExclusion.noVehicleId);
|
|
115
|
-
assert.equal(getMarktdeExclusion(car('', {})), MarktdeExclusion.noId);
|
|
116
|
-
}],
|
|
117
|
-
|
|
118
|
-
['images: validated clean list, hero first, no placeholders; no valid hero -> []', () => {
|
|
119
|
-
const clean = imagesService.getCleanImages('car1', eightImages('car1'));
|
|
120
|
-
const all = toMarktdeImages(selectOpenAiImageFields(clean, clean.map(i => ({ url: i.url, ok: true }))));
|
|
121
|
-
assert.equal(all.length, 8);
|
|
122
|
-
assert.match(all[0], /hero1\/large-cached\.jpeg$/);
|
|
123
|
-
assert.ok(all.every(u => !u.includes('noimage')));
|
|
124
|
-
assert.equal(toMarktdeImages(selectOpenAiImageFields(clean, clean.map(i => ({ url: i.url, ok: true }))), 3).length, 3);
|
|
125
|
-
const brokenHeroes = clean.map(i => ({ url: i.url, ok: !i.heroCandidate }));
|
|
126
|
-
assert.deepEqual(toMarktdeImages(selectOpenAiImageFields(clean, brokenHeroes)), []);
|
|
127
|
-
assert.deepEqual(toMarktdeImages(selectOpenAiImageFields(imagesService.getCleanImages('c', []), [])), []);
|
|
128
|
-
}],
|
|
129
|
-
|
|
130
|
-
['description: URLs, e-mails, phone numbers, street / postcode lines removed; vehicle facts and markup stay', () => {
|
|
131
|
-
const text = 'Autohaus Muster GmbH\\\\Tel.: 0341 / 123 456 78\\\\Mobil: +49 (0) 171 2345678\\\\E-Mail: info@autohaus-muster.de\\\\'
|
|
132
|
-
+ 'Besuchen Sie uns auf www.autohaus-muster.de oder https://autohaus-muster.de/angebote\\\\Max-Planck-Straße 19, 06796 Sandersdorf-Brehna\\\\'
|
|
133
|
-
+ '**Ausstattung**\\\\* ABS\\\\HU: 08/2027\\\\Erstzulassung: 01.08.2021\\\\Kilometerstand: 125000 Km\\\\Preis: 15990 Euro\\\\Hubraum: 1598 ccm\\\\';
|
|
134
|
-
const { text: out, removed } = sanitizeMarktdeDescription(text);
|
|
135
|
-
assert.deepEqual(removed, { url: 2, email: 1, phone: 2, address: 2 });
|
|
136
|
-
for (const forbidden of ['0341', '171', '@', 'www.', 'http', 'autohaus-muster.de', 'Planck', '06796', 'Tel.:', 'E-Mail:']) {
|
|
137
|
-
assert.ok(!out.includes(forbidden), `${forbidden} left in: ${out}`);
|
|
138
|
-
}
|
|
139
|
-
for (const kept of ['Autohaus Muster GmbH', '**Ausstattung**', '* ABS', 'HU: 08/2027', 'Erstzulassung: 01.08.2021', '125000 Km', '15990 Euro', '1598 ccm']) {
|
|
140
|
-
assert.ok(out.includes(kept), `${kept} missing in: ${out}`);
|
|
141
|
-
}
|
|
142
|
-
}],
|
|
143
|
-
|
|
144
|
-
['description: the seller name auto.de stays (brand + auto.de GmbH), links to it and contact lines go; markup untouched', () => {
|
|
145
|
-
const text = ' **auto.de | Einfach - Sicher - Online - Kaufen**\\\\\\\\ ----\\\\so funktioniert auto.de\\\\'
|
|
146
|
-
+ 'Sollten Sie ein günstigeres Angebot finden als auf auto.de, dann passen wir es an.\\\\Auto.de legt höchsten Wert auf Zufriedenheit.\\\\'
|
|
147
|
-
+ 'auto.de-ID: 96BAE0\\\\Tel.: 0800 0000 466\\\\WhatsApp: 0151 225 94 090\\\\SMS: 0151 225 94 090\\\\E-Mail: auto@auto.de\\\\ ---- **Basic data**\\\\'
|
|
148
|
-
+ 'Mehr unter https://www.auto.de/angebote und www.auto.de\\\\';
|
|
149
|
-
const { text: out, removed } = sanitizeMarktdeDescription(text);
|
|
150
|
-
assert.deepEqual(removed, { url: 2, email: 1, phone: 3, address: 0 });
|
|
151
|
-
assert.equal(
|
|
152
|
-
out,
|
|
153
|
-
' **auto.de | Einfach - Sicher - Online - Kaufen**\\\\\\\\ ----\\\\so funktioniert auto.de\\\\'
|
|
154
|
-
+ 'Sollten Sie ein günstigeres Angebot finden als auf auto.de, dann passen wir es an.\\\\Auto.de legt höchsten Wert auf Zufriedenheit.\\\\'
|
|
155
|
-
+ 'auto.de-ID: 96BAE0\\\\\\\\\\\\\\\\\\\\ ---- **Basic data**\\\\'
|
|
156
|
-
+ 'Mehr unter und\\\\',
|
|
157
|
-
);
|
|
158
|
-
}],
|
|
159
|
-
|
|
160
|
-
['description: other bare domains go ("autohaus-muster.de"), words with dots stay ("z.B.", "ca.", "1.6 TDI")', () => {
|
|
161
|
-
const { text: out, removed } = sanitizeMarktdeDescription('Alle Fahrzeuge auf autohaus-muster.de ansehen, z.B. 1.6 TDI ca. 5 l\\\\');
|
|
162
|
-
assert.equal(removed.url, 1);
|
|
163
|
-
assert.equal(out, 'Alle Fahrzeuge auf ansehen, z.B. 1.6 TDI ca. 5 l\\\\');
|
|
164
|
-
// DEV 12.11: a model name looked like a street ("…ring" + number)
|
|
165
|
-
const model = 'Untermodell: M3 Competition xDrive Touring 3.0 8-Gang St...\\\\Hubraum: 2993\\\\';
|
|
166
|
-
assert.deepEqual(sanitizeMarktdeDescription(model), { text: model, removed: { url: 0, email: 0, phone: 0, address: 0 } });
|
|
167
|
-
}],
|
|
168
|
-
|
|
169
|
-
['description: text without contact data is returned unchanged (byte-identical)', () => {
|
|
170
|
-
const text = '**Ausstattung**\\\\* ABS\\\\* Klimaanlage\\\\Leistung: 85 kW (116 PS)\\\\Verbrauch: 6,1 l/100km\\\\';
|
|
171
|
-
const result = sanitizeMarktdeDescription(text);
|
|
172
|
-
assert.equal(result.text, text);
|
|
173
|
-
assert.deepEqual(result.removed, { url: 0, email: 0, phone: 0, address: 0 });
|
|
174
|
-
assert.deepEqual(sanitizeMarktdeDescription('').removed, { url: 0, email: 0, phone: 0, address: 0 });
|
|
175
|
-
}],
|
|
176
|
-
|
|
177
|
-
['applyMarktdeColumns: only the markt.de columns change, every other column stays', () => {
|
|
178
|
-
const c = car('car1', { 'metaData.vin': 'WVWZZZ1KZAW000001', 'price.vatDeductible': true, 'price.vatRate': 19, 'price.currentSalesPrice': 23800, 'offer.offerDescription': 'x' });
|
|
179
|
-
const row = legacyRow(c);
|
|
180
|
-
const before = { ...row };
|
|
181
|
-
const facts = applyMarktdeColumns(row, c, ctx);
|
|
182
|
-
assert.equal(row[C.price], '20000.00');
|
|
183
|
-
assert.equal(row[C.vatFlag], '0');
|
|
184
|
-
assert.equal(row[C.vatRate], '19');
|
|
185
|
-
assert.equal(row[C.vin], 'WVWZZZ1KZAW000001');
|
|
186
|
-
assert.equal(row[C.images], ctx.images.join(';'));
|
|
187
|
-
assert.equal(row[C.listingUrl], getMarktdeListingUrl('auto.de', 'car1'));
|
|
188
|
-
assert.equal(row[C.co2], '137');
|
|
189
|
-
assert.equal(row[C.fuelCombined], '6,1');
|
|
190
|
-
assert.equal(row[C.description], before[C.description], 'description without contact data unchanged');
|
|
191
|
-
for (const key of ['0', '1', '3', '4']) {
|
|
192
|
-
assert.equal(row[key], before[key], `column ${key}`);
|
|
193
|
-
}
|
|
194
|
-
assert.equal(facts.vin, 'sent');
|
|
195
|
-
assert.equal(facts.descriptionRemovals.url, 0);
|
|
196
|
-
}],
|
|
197
|
-
|
|
198
|
-
['applyMarktdeColumns: BEV -> fuel empty, CO2 0; nothing usable -> empty (never "undefined")', () => {
|
|
199
|
-
const bev = car('bev1', { 'consumption.fuel': 'selector_fuel_electrical', 'environmentEmissions.co2': null, 'consumption.consumptionCombined': 16.5 });
|
|
200
|
-
const row = legacyRow(bev);
|
|
201
|
-
applyMarktdeColumns(row, bev, ctx);
|
|
202
|
-
assert.equal(row[C.fuelCombined], '');
|
|
203
|
-
assert.equal(row[C.co2], '0');
|
|
204
|
-
const none = car('none1', { 'environmentEmissions.co2': null, 'consumption.consumptionCombined': null });
|
|
205
|
-
const row2 = legacyRow(none);
|
|
206
|
-
const facts = applyMarktdeColumns(row2, none, ctx);
|
|
207
|
-
assert.equal(row2[C.co2], '');
|
|
208
|
-
assert.equal(row2[C.fuelCombined], '');
|
|
209
|
-
assert.ok(facts.co2Empty && facts.fuelEmpty);
|
|
210
|
-
assert.ok(!convertToCSV(row2).includes('undefined'));
|
|
211
|
-
}],
|
|
212
|
-
|
|
213
|
-
['applyMarktdeColumns: invalid stored VIN reported as invalid_dropped, missing as empty', () => {
|
|
214
|
-
const bad = car('v1', { 'metaData.vin': 'XYZ' });
|
|
215
|
-
assert.equal(applyMarktdeColumns(legacyRow(bad), bad, ctx).vin, 'invalid_dropped');
|
|
216
|
-
const missing = car('v2', {});
|
|
217
|
-
assert.equal(applyMarktdeColumns(legacyRow(missing), missing, ctx).vin, 'empty');
|
|
218
|
-
}],
|
|
219
|
-
|
|
220
|
-
['20 fixtures: one CSV line each, 403 columns, B = vehicle id, OM = link with the car id, UTF-8 German text', () => {
|
|
221
|
-
const variants: Array<Record<string, any>> = [
|
|
222
|
-
{}, { 'price.vatDeductible': true, 'price.vatRate': 19 }, { 'price.vatDeductible': true, 'price.vatRate': null },
|
|
223
|
-
{ 'consumption.fuel': 'selector_fuel_electrical' }, { 'metaData.vin': 'WVWZZZ1KZAW000001' }, { 'metaData.vin': 'bad' },
|
|
224
|
-
{ 'mainData.subModel': null }, { 'mainData.make': 'Škoda', 'mainData.model': 'Octavia Combi' }, { 'mainData.firstRegistration': null },
|
|
225
|
-
{ 'environmentEmissions.co2': null }, { 'consumption.consumptionCombined': null }, { 'offer.offerDescription': 'x' },
|
|
226
|
-
{ 'price.currentSalesPrice': 79999 }, { 'price.currentSalesPrice': 1 }, { 'mainData.mileage': 0 },
|
|
227
|
-
{ 'mainData.make': 'Mercedes-Benz', 'mainData.model': 'C 200 "Avantgarde"' }, { 'metaData.ownerId': 'dealer-2' },
|
|
228
|
-
{ 'price.vatDeductible': true, 'price.vatRate': 7 }, { 'mainData.accidentDamaged': true }, { 'consumption.fuel': 'selector_fuel_diesel' },
|
|
229
|
-
];
|
|
230
|
-
assert.equal(variants.length, 20);
|
|
231
|
-
variants.forEach((overrides, i) => {
|
|
232
|
-
const id = `fx${String(i).padStart(2, '0')}abcdef`;
|
|
233
|
-
const c = car(id, overrides);
|
|
234
|
-
const row = legacyRow(c);
|
|
235
|
-
applyMarktdeColumns(row, c, { domain: 'auto.de', images: imagesService.getCleanImages(id, images(id)).map(x => x.url) });
|
|
236
|
-
const line = convertToCSV(row);
|
|
237
|
-
assert.ok(!line.includes('\n'), 'one line per car');
|
|
238
|
-
// quote-aware split: the image cell carries `;` between URLs inside its quotes
|
|
239
|
-
const cells = line.match(/"(?:[^"]|"")*"|(?<=^|;)(?=;|$)/g);
|
|
240
|
-
assert.equal(cells.length, 403, `fixture ${i}: column count`);
|
|
241
|
-
assert.equal(cells[1], `"${c.metaData.vehicleId}"`);
|
|
242
|
-
assert.ok(cells[26].includes('hero1') && cells[26].includes(';'));
|
|
243
|
-
assert.ok(cells[402].includes(`/search/vehicle/${id}?source=marktde`));
|
|
244
|
-
assert.ok(!line.includes('undefined'), `fixture ${i}`);
|
|
245
|
-
});
|
|
246
|
-
}],
|
|
247
|
-
|
|
248
|
-
['config: dry-run by default (marktde-dryrun), live only with marktdeDryRun=false; legacy sheet + file names', () => {
|
|
249
|
-
const base = { environment: Environment.PRODUCTION, exportType: ExportType.MARKTDE, apikey: 'k' } as any;
|
|
250
|
-
const dry = new MarktdeConfig(base);
|
|
251
|
-
assert.equal(dry.dryRun, true);
|
|
252
|
-
assert.deepEqual(dry.outputFiles, ['resources/marktde-dryrun/api_export_cars_marktde.csv', 'resources/marktde-dryrun/api_export_cars_marktde.zip']);
|
|
253
|
-
assert.equal(dry.stateDir, 'resources/marktde-state/dryrun');
|
|
254
|
-
assert.equal(dry.mappingSpreadsheetTitle, 'mobile_yescar');
|
|
255
|
-
assert.equal(dry.campaign, 'marktde');
|
|
256
|
-
assert.equal(dry.imageType, ImageType.default);
|
|
257
|
-
assert.equal(dry.domain, 'auto.de');
|
|
258
|
-
assert.equal(new MarktdeConfig({ ...base, marktdeDryRun: 'true' }).dryRun, true);
|
|
259
|
-
assert.equal(new MarktdeConfig({ ...base, marktdeDryRun: 'nope' }).dryRun, true);
|
|
260
|
-
const live = new MarktdeConfig({ ...base, marktdeDryRun: 'false' });
|
|
261
|
-
assert.equal(live.dryRun, false);
|
|
262
|
-
assert.equal(live.outputFiles[0], 'resources/marktde-export/api_export_cars_marktde.csv');
|
|
263
|
-
assert.equal(live.feedGuard.minPublishedItems, 1);
|
|
264
|
-
assert.equal(new MarktdeConfig({ ...base, feedGuardMinPublishedItems: '50' }).feedGuard.minPublishedItems, 50);
|
|
265
|
-
}],
|
|
266
|
-
];
|
|
267
|
-
|
|
268
|
-
(async () => {
|
|
269
|
-
let failed = 0;
|
|
270
|
-
for (const [name, fn] of tests) {
|
|
271
|
-
try {
|
|
272
|
-
await fn();
|
|
273
|
-
console.log(`ok ${name}`);
|
|
274
|
-
} catch (e) {
|
|
275
|
-
failed += 1;
|
|
276
|
-
console.error(`FAIL ${name}\n ${e.message}`);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
if (failed) process.exit(1);
|
|
280
|
-
})();
|
|
1
|
+
// Run: npm test (ts-node, no test framework needed)
|
|
2
|
+
// markt.de columns (ADV-328) on top of the legacy mobile.de-style row — pure functions, no I/O.
|
|
3
|
+
import * as assert from 'node:assert/strict';
|
|
4
|
+
|
|
5
|
+
import { MarktdeConfig } from '../src/configs/marktde.config';
|
|
6
|
+
import { Environment, ExportType, ImageType } from '../src/enums';
|
|
7
|
+
import {
|
|
8
|
+
MARKTDE_COLUMNS,
|
|
9
|
+
MARKTDE_LINK_PARAMS,
|
|
10
|
+
MarktdeExclusion,
|
|
11
|
+
applyMarktdeColumns,
|
|
12
|
+
getMarktdeExclusion,
|
|
13
|
+
getMarktdeListingUrl,
|
|
14
|
+
getMarktdePriceColumns,
|
|
15
|
+
getMarktdeVin,
|
|
16
|
+
sanitizeMarktdeDescription,
|
|
17
|
+
toMarktdeImages,
|
|
18
|
+
} from '../src/transformers/marktde.transformer';
|
|
19
|
+
import { selectOpenAiImageFields } from '../src/transformers/openai-image-fields';
|
|
20
|
+
import { convertToCSV } from '../src/utils/converter.utils';
|
|
21
|
+
import { buildImagesService } from './fixtures/images-service.harness';
|
|
22
|
+
import { car, eightImages, images } from './fixtures/meinstadt.fixture';
|
|
23
|
+
|
|
24
|
+
const imagesService = buildImagesService();
|
|
25
|
+
const C = MARKTDE_COLUMNS;
|
|
26
|
+
const ctx = { domain: 'auto.de', images: ['https://images.auto.de/carimage/x/hero1/large-cached.jpeg', 'https://images.auto.de/carimage/x/side1/large-cached.jpeg'] };
|
|
27
|
+
|
|
28
|
+
// The row MobileTransformer builds from the mapping sheet (only the columns the test looks at).
|
|
29
|
+
const legacyRow = (c: any): Record<string, unknown> => ({
|
|
30
|
+
'0': '04109003',
|
|
31
|
+
'1': c.metaData.vehicleId,
|
|
32
|
+
'3': c.mainData.make,
|
|
33
|
+
'4': `${c.mainData.model} ${c.mainData.subModel}`,
|
|
34
|
+
'10': 'legacy-price',
|
|
35
|
+
'11': 'legacy-flag',
|
|
36
|
+
'14': 'legacy-vin',
|
|
37
|
+
'25': c.offer?.offerDescription ? 'Fahrzeugbeschreibung\\\\**Ausstattung**\\\\* ABS\\\\' : undefined,
|
|
38
|
+
'26': 'legacy-images',
|
|
39
|
+
'29': 'legacy-rate',
|
|
40
|
+
'98': '6,1',
|
|
41
|
+
'99': 'undefined',
|
|
42
|
+
'402': `https://www.auto.de${c._id}?source=classifieds&medium=listing&campaign=marktde`,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const tests: Array<[string, () => void | Promise<void>]> = [
|
|
46
|
+
['column letters: B=1, K=10, L=11, O=14, Z=25, AA=26, AD=29, OM=402 (historic ACF-4370 mapping)', () => {
|
|
47
|
+
const letters = (index: number): string => {
|
|
48
|
+
let n = index + 1;
|
|
49
|
+
let out = '';
|
|
50
|
+
while (n > 0) {
|
|
51
|
+
const r = (n - 1) % 26;
|
|
52
|
+
out = String.fromCharCode(65 + r) + out;
|
|
53
|
+
n = Math.floor((n - 1) / 26);
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
};
|
|
57
|
+
assert.deepEqual(
|
|
58
|
+
Object.fromEntries(Object.entries(C).map(([k, v]) => [k, letters(Number(v))])),
|
|
59
|
+
{ vehicleId: 'B', price: 'K', vatFlag: 'L', vin: 'O', description: 'Z', images: 'AA', vatRate: 'AD', fuelCombined: 'CU', co2: 'CV', listingUrl: 'OM' },
|
|
60
|
+
);
|
|
61
|
+
}],
|
|
62
|
+
|
|
63
|
+
['link: auto.de vehicle page with source=marktde + identical utm_* (no classifieds, no paid medium)', () => {
|
|
64
|
+
assert.equal(
|
|
65
|
+
getMarktdeListingUrl('auto.de', 'abc123'),
|
|
66
|
+
'https://www.auto.de/search/vehicle/abc123?source=marktde&medium=listing&campaign=listing&utm_source=marktde&utm_medium=listing&utm_campaign=listing',
|
|
67
|
+
);
|
|
68
|
+
assert.ok(!MARKTDE_LINK_PARAMS.includes('classifieds'));
|
|
69
|
+
assert.ok(!/cpc|paid/.test(MARKTDE_LINK_PARAMS));
|
|
70
|
+
}],
|
|
71
|
+
|
|
72
|
+
['VIN: own valid VIN (upper-cased) or empty — never a generated / marketing fake VIN', () => {
|
|
73
|
+
assert.equal(getMarktdeVin(car('a', { 'metaData.vin': 'wvwzzz1kzaw000001' })), 'WVWZZZ1KZAW000001');
|
|
74
|
+
assert.equal(getMarktdeVin(car('a', { 'metaData.vin': '' })), '');
|
|
75
|
+
assert.equal(getMarktdeVin(car('a', { 'marketing.fakeVIN': 'WVWZZZ1KZAW999999' })), '');
|
|
76
|
+
assert.equal(getMarktdeVin(car('a', { 'metaData.vin': 'WVWZZZ1KZAW00000' })), '', '16 chars');
|
|
77
|
+
assert.equal(getMarktdeVin(car('a', { 'metaData.vin': 'WVWZZZ1KZAWO00001' })), '', 'letter O is not allowed');
|
|
78
|
+
assert.equal(getMarktdeVin(car('a', { 'metaData.vin': 'unbekannt' })), '');
|
|
79
|
+
}],
|
|
80
|
+
|
|
81
|
+
['price: VAT-deductible -> net K, L=0, AD=rate; gross is restored exactly as K*(1+AD)', () => {
|
|
82
|
+
const p = getMarktdePriceColumns(car('a', { 'price.currentSalesPrice': 23800, 'price.vatDeductible': true, 'price.vatRate': 19 }));
|
|
83
|
+
assert.deepEqual(p, { price: '20000.00', vatFlag: '0', vatRate: '19', deductible: true, rateDefaulted: false });
|
|
84
|
+
assert.equal(Math.round(Number(p.price) * 1.19), 23800);
|
|
85
|
+
}],
|
|
86
|
+
|
|
87
|
+
['price: not deductible (margin scheme / exempt) -> gross K as stored, L=1, AD=0 — never +19 % on a gross price', () => {
|
|
88
|
+
assert.deepEqual(
|
|
89
|
+
getMarktdePriceColumns(car('a', { 'price.currentSalesPrice': 15148, 'price.vatDeductible': false })),
|
|
90
|
+
{ price: '15148', vatFlag: '1', vatRate: '0', deductible: false, rateDefaulted: false },
|
|
91
|
+
);
|
|
92
|
+
assert.equal(getMarktdePriceColumns(car('a', { 'price.currentSalesPrice': 15148 })).vatFlag, '1', 'unknown = not deductible');
|
|
93
|
+
}],
|
|
94
|
+
|
|
95
|
+
['price: deductible with null / 0 vatRate -> 19 % for BOTH K and AD (legacy kept K gross but AD=19)', () => {
|
|
96
|
+
for (const rate of [null, 0, undefined]) {
|
|
97
|
+
const p = getMarktdePriceColumns(car('a', { 'price.currentSalesPrice': 11900, 'price.vatDeductible': true, 'price.vatRate': rate }));
|
|
98
|
+
assert.deepEqual(p, { price: '10000.00', vatFlag: '0', vatRate: '19', deductible: true, rateDefaulted: true });
|
|
99
|
+
}
|
|
100
|
+
}],
|
|
101
|
+
|
|
102
|
+
['price: missing / 0 / negative / text -> null (car is excluded)', () => {
|
|
103
|
+
for (const value of [null, undefined, '', 0, -5, 'abc', true]) {
|
|
104
|
+
assert.equal(getMarktdePriceColumns(car('a', { 'price.currentSalesPrice': value })), null, String(value));
|
|
105
|
+
}
|
|
106
|
+
}],
|
|
107
|
+
|
|
108
|
+
['exclusions: sold / reserved / no price / no vehicle id / no _id', () => {
|
|
109
|
+
assert.equal(getMarktdeExclusion(car('a')), null);
|
|
110
|
+
assert.equal(getMarktdeExclusion(car('a', { 'offer.offerAvailability': 'selector_offerAvailability_sold' })), MarktdeExclusion.notAvailable);
|
|
111
|
+
assert.equal(getMarktdeExclusion(car('a', { 'offer.offerAvailability': 'selector_offerAvailability_reserved' })), MarktdeExclusion.notAvailable);
|
|
112
|
+
assert.equal(getMarktdeExclusion(car('a', { 'offer.offerAvailability': 'selector_offerAvailability_available' })), null);
|
|
113
|
+
assert.equal(getMarktdeExclusion(car('a', { 'price.currentSalesPrice': null })), MarktdeExclusion.noPrice);
|
|
114
|
+
assert.equal(getMarktdeExclusion(car('a', { 'metaData.vehicleId': '' })), MarktdeExclusion.noVehicleId);
|
|
115
|
+
assert.equal(getMarktdeExclusion(car('', {})), MarktdeExclusion.noId);
|
|
116
|
+
}],
|
|
117
|
+
|
|
118
|
+
['images: validated clean list, hero first, no placeholders; no valid hero -> []', () => {
|
|
119
|
+
const clean = imagesService.getCleanImages('car1', eightImages('car1'));
|
|
120
|
+
const all = toMarktdeImages(selectOpenAiImageFields(clean, clean.map(i => ({ url: i.url, ok: true }))));
|
|
121
|
+
assert.equal(all.length, 8);
|
|
122
|
+
assert.match(all[0], /hero1\/large-cached\.jpeg$/);
|
|
123
|
+
assert.ok(all.every(u => !u.includes('noimage')));
|
|
124
|
+
assert.equal(toMarktdeImages(selectOpenAiImageFields(clean, clean.map(i => ({ url: i.url, ok: true }))), 3).length, 3);
|
|
125
|
+
const brokenHeroes = clean.map(i => ({ url: i.url, ok: !i.heroCandidate }));
|
|
126
|
+
assert.deepEqual(toMarktdeImages(selectOpenAiImageFields(clean, brokenHeroes)), []);
|
|
127
|
+
assert.deepEqual(toMarktdeImages(selectOpenAiImageFields(imagesService.getCleanImages('c', []), [])), []);
|
|
128
|
+
}],
|
|
129
|
+
|
|
130
|
+
['description: URLs, e-mails, phone numbers, street / postcode lines removed; vehicle facts and markup stay', () => {
|
|
131
|
+
const text = 'Autohaus Muster GmbH\\\\Tel.: 0341 / 123 456 78\\\\Mobil: +49 (0) 171 2345678\\\\E-Mail: info@autohaus-muster.de\\\\'
|
|
132
|
+
+ 'Besuchen Sie uns auf www.autohaus-muster.de oder https://autohaus-muster.de/angebote\\\\Max-Planck-Straße 19, 06796 Sandersdorf-Brehna\\\\'
|
|
133
|
+
+ '**Ausstattung**\\\\* ABS\\\\HU: 08/2027\\\\Erstzulassung: 01.08.2021\\\\Kilometerstand: 125000 Km\\\\Preis: 15990 Euro\\\\Hubraum: 1598 ccm\\\\';
|
|
134
|
+
const { text: out, removed } = sanitizeMarktdeDescription(text);
|
|
135
|
+
assert.deepEqual(removed, { url: 2, email: 1, phone: 2, address: 2 });
|
|
136
|
+
for (const forbidden of ['0341', '171', '@', 'www.', 'http', 'autohaus-muster.de', 'Planck', '06796', 'Tel.:', 'E-Mail:']) {
|
|
137
|
+
assert.ok(!out.includes(forbidden), `${forbidden} left in: ${out}`);
|
|
138
|
+
}
|
|
139
|
+
for (const kept of ['Autohaus Muster GmbH', '**Ausstattung**', '* ABS', 'HU: 08/2027', 'Erstzulassung: 01.08.2021', '125000 Km', '15990 Euro', '1598 ccm']) {
|
|
140
|
+
assert.ok(out.includes(kept), `${kept} missing in: ${out}`);
|
|
141
|
+
}
|
|
142
|
+
}],
|
|
143
|
+
|
|
144
|
+
['description: the seller name auto.de stays (brand + auto.de GmbH), links to it and contact lines go; markup untouched', () => {
|
|
145
|
+
const text = ' **auto.de | Einfach - Sicher - Online - Kaufen**\\\\\\\\ ----\\\\so funktioniert auto.de\\\\'
|
|
146
|
+
+ 'Sollten Sie ein günstigeres Angebot finden als auf auto.de, dann passen wir es an.\\\\Auto.de legt höchsten Wert auf Zufriedenheit.\\\\'
|
|
147
|
+
+ 'auto.de-ID: 96BAE0\\\\Tel.: 0800 0000 466\\\\WhatsApp: 0151 225 94 090\\\\SMS: 0151 225 94 090\\\\E-Mail: auto@auto.de\\\\ ---- **Basic data**\\\\'
|
|
148
|
+
+ 'Mehr unter https://www.auto.de/angebote und www.auto.de\\\\';
|
|
149
|
+
const { text: out, removed } = sanitizeMarktdeDescription(text);
|
|
150
|
+
assert.deepEqual(removed, { url: 2, email: 1, phone: 3, address: 0 });
|
|
151
|
+
assert.equal(
|
|
152
|
+
out,
|
|
153
|
+
' **auto.de | Einfach - Sicher - Online - Kaufen**\\\\\\\\ ----\\\\so funktioniert auto.de\\\\'
|
|
154
|
+
+ 'Sollten Sie ein günstigeres Angebot finden als auf auto.de, dann passen wir es an.\\\\Auto.de legt höchsten Wert auf Zufriedenheit.\\\\'
|
|
155
|
+
+ 'auto.de-ID: 96BAE0\\\\\\\\\\\\\\\\\\\\ ---- **Basic data**\\\\'
|
|
156
|
+
+ 'Mehr unter und\\\\',
|
|
157
|
+
);
|
|
158
|
+
}],
|
|
159
|
+
|
|
160
|
+
['description: other bare domains go ("autohaus-muster.de"), words with dots stay ("z.B.", "ca.", "1.6 TDI")', () => {
|
|
161
|
+
const { text: out, removed } = sanitizeMarktdeDescription('Alle Fahrzeuge auf autohaus-muster.de ansehen, z.B. 1.6 TDI ca. 5 l\\\\');
|
|
162
|
+
assert.equal(removed.url, 1);
|
|
163
|
+
assert.equal(out, 'Alle Fahrzeuge auf ansehen, z.B. 1.6 TDI ca. 5 l\\\\');
|
|
164
|
+
// DEV 12.11: a model name looked like a street ("…ring" + number)
|
|
165
|
+
const model = 'Untermodell: M3 Competition xDrive Touring 3.0 8-Gang St...\\\\Hubraum: 2993\\\\';
|
|
166
|
+
assert.deepEqual(sanitizeMarktdeDescription(model), { text: model, removed: { url: 0, email: 0, phone: 0, address: 0 } });
|
|
167
|
+
}],
|
|
168
|
+
|
|
169
|
+
['description: text without contact data is returned unchanged (byte-identical)', () => {
|
|
170
|
+
const text = '**Ausstattung**\\\\* ABS\\\\* Klimaanlage\\\\Leistung: 85 kW (116 PS)\\\\Verbrauch: 6,1 l/100km\\\\';
|
|
171
|
+
const result = sanitizeMarktdeDescription(text);
|
|
172
|
+
assert.equal(result.text, text);
|
|
173
|
+
assert.deepEqual(result.removed, { url: 0, email: 0, phone: 0, address: 0 });
|
|
174
|
+
assert.deepEqual(sanitizeMarktdeDescription('').removed, { url: 0, email: 0, phone: 0, address: 0 });
|
|
175
|
+
}],
|
|
176
|
+
|
|
177
|
+
['applyMarktdeColumns: only the markt.de columns change, every other column stays', () => {
|
|
178
|
+
const c = car('car1', { 'metaData.vin': 'WVWZZZ1KZAW000001', 'price.vatDeductible': true, 'price.vatRate': 19, 'price.currentSalesPrice': 23800, 'offer.offerDescription': 'x' });
|
|
179
|
+
const row = legacyRow(c);
|
|
180
|
+
const before = { ...row };
|
|
181
|
+
const facts = applyMarktdeColumns(row, c, ctx);
|
|
182
|
+
assert.equal(row[C.price], '20000.00');
|
|
183
|
+
assert.equal(row[C.vatFlag], '0');
|
|
184
|
+
assert.equal(row[C.vatRate], '19');
|
|
185
|
+
assert.equal(row[C.vin], 'WVWZZZ1KZAW000001');
|
|
186
|
+
assert.equal(row[C.images], ctx.images.join(';'));
|
|
187
|
+
assert.equal(row[C.listingUrl], getMarktdeListingUrl('auto.de', 'car1'));
|
|
188
|
+
assert.equal(row[C.co2], '137');
|
|
189
|
+
assert.equal(row[C.fuelCombined], '6,1');
|
|
190
|
+
assert.equal(row[C.description], before[C.description], 'description without contact data unchanged');
|
|
191
|
+
for (const key of ['0', '1', '3', '4']) {
|
|
192
|
+
assert.equal(row[key], before[key], `column ${key}`);
|
|
193
|
+
}
|
|
194
|
+
assert.equal(facts.vin, 'sent');
|
|
195
|
+
assert.equal(facts.descriptionRemovals.url, 0);
|
|
196
|
+
}],
|
|
197
|
+
|
|
198
|
+
['applyMarktdeColumns: BEV -> fuel empty, CO2 0; nothing usable -> empty (never "undefined")', () => {
|
|
199
|
+
const bev = car('bev1', { 'consumption.fuel': 'selector_fuel_electrical', 'environmentEmissions.co2': null, 'consumption.consumptionCombined': 16.5 });
|
|
200
|
+
const row = legacyRow(bev);
|
|
201
|
+
applyMarktdeColumns(row, bev, ctx);
|
|
202
|
+
assert.equal(row[C.fuelCombined], '');
|
|
203
|
+
assert.equal(row[C.co2], '0');
|
|
204
|
+
const none = car('none1', { 'environmentEmissions.co2': null, 'consumption.consumptionCombined': null });
|
|
205
|
+
const row2 = legacyRow(none);
|
|
206
|
+
const facts = applyMarktdeColumns(row2, none, ctx);
|
|
207
|
+
assert.equal(row2[C.co2], '');
|
|
208
|
+
assert.equal(row2[C.fuelCombined], '');
|
|
209
|
+
assert.ok(facts.co2Empty && facts.fuelEmpty);
|
|
210
|
+
assert.ok(!convertToCSV(row2).includes('undefined'));
|
|
211
|
+
}],
|
|
212
|
+
|
|
213
|
+
['applyMarktdeColumns: invalid stored VIN reported as invalid_dropped, missing as empty', () => {
|
|
214
|
+
const bad = car('v1', { 'metaData.vin': 'XYZ' });
|
|
215
|
+
assert.equal(applyMarktdeColumns(legacyRow(bad), bad, ctx).vin, 'invalid_dropped');
|
|
216
|
+
const missing = car('v2', {});
|
|
217
|
+
assert.equal(applyMarktdeColumns(legacyRow(missing), missing, ctx).vin, 'empty');
|
|
218
|
+
}],
|
|
219
|
+
|
|
220
|
+
['20 fixtures: one CSV line each, 403 columns, B = vehicle id, OM = link with the car id, UTF-8 German text', () => {
|
|
221
|
+
const variants: Array<Record<string, any>> = [
|
|
222
|
+
{}, { 'price.vatDeductible': true, 'price.vatRate': 19 }, { 'price.vatDeductible': true, 'price.vatRate': null },
|
|
223
|
+
{ 'consumption.fuel': 'selector_fuel_electrical' }, { 'metaData.vin': 'WVWZZZ1KZAW000001' }, { 'metaData.vin': 'bad' },
|
|
224
|
+
{ 'mainData.subModel': null }, { 'mainData.make': 'Škoda', 'mainData.model': 'Octavia Combi' }, { 'mainData.firstRegistration': null },
|
|
225
|
+
{ 'environmentEmissions.co2': null }, { 'consumption.consumptionCombined': null }, { 'offer.offerDescription': 'x' },
|
|
226
|
+
{ 'price.currentSalesPrice': 79999 }, { 'price.currentSalesPrice': 1 }, { 'mainData.mileage': 0 },
|
|
227
|
+
{ 'mainData.make': 'Mercedes-Benz', 'mainData.model': 'C 200 "Avantgarde"' }, { 'metaData.ownerId': 'dealer-2' },
|
|
228
|
+
{ 'price.vatDeductible': true, 'price.vatRate': 7 }, { 'mainData.accidentDamaged': true }, { 'consumption.fuel': 'selector_fuel_diesel' },
|
|
229
|
+
];
|
|
230
|
+
assert.equal(variants.length, 20);
|
|
231
|
+
variants.forEach((overrides, i) => {
|
|
232
|
+
const id = `fx${String(i).padStart(2, '0')}abcdef`;
|
|
233
|
+
const c = car(id, overrides);
|
|
234
|
+
const row = legacyRow(c);
|
|
235
|
+
applyMarktdeColumns(row, c, { domain: 'auto.de', images: imagesService.getCleanImages(id, images(id)).map(x => x.url) });
|
|
236
|
+
const line = convertToCSV(row);
|
|
237
|
+
assert.ok(!line.includes('\n'), 'one line per car');
|
|
238
|
+
// quote-aware split: the image cell carries `;` between URLs inside its quotes
|
|
239
|
+
const cells = line.match(/"(?:[^"]|"")*"|(?<=^|;)(?=;|$)/g);
|
|
240
|
+
assert.equal(cells.length, 403, `fixture ${i}: column count`);
|
|
241
|
+
assert.equal(cells[1], `"${c.metaData.vehicleId}"`);
|
|
242
|
+
assert.ok(cells[26].includes('hero1') && cells[26].includes(';'));
|
|
243
|
+
assert.ok(cells[402].includes(`/search/vehicle/${id}?source=marktde`));
|
|
244
|
+
assert.ok(!line.includes('undefined'), `fixture ${i}`);
|
|
245
|
+
});
|
|
246
|
+
}],
|
|
247
|
+
|
|
248
|
+
['config: dry-run by default (marktde-dryrun), live only with marktdeDryRun=false; legacy sheet + file names', () => {
|
|
249
|
+
const base = { environment: Environment.PRODUCTION, exportType: ExportType.MARKTDE, apikey: 'k' } as any;
|
|
250
|
+
const dry = new MarktdeConfig(base);
|
|
251
|
+
assert.equal(dry.dryRun, true);
|
|
252
|
+
assert.deepEqual(dry.outputFiles, ['resources/marktde-dryrun/api_export_cars_marktde.csv', 'resources/marktde-dryrun/api_export_cars_marktde.zip']);
|
|
253
|
+
assert.equal(dry.stateDir, 'resources/marktde-state/dryrun');
|
|
254
|
+
assert.equal(dry.mappingSpreadsheetTitle, 'mobile_yescar');
|
|
255
|
+
assert.equal(dry.campaign, 'marktde');
|
|
256
|
+
assert.equal(dry.imageType, ImageType.default);
|
|
257
|
+
assert.equal(dry.domain, 'auto.de');
|
|
258
|
+
assert.equal(new MarktdeConfig({ ...base, marktdeDryRun: 'true' }).dryRun, true);
|
|
259
|
+
assert.equal(new MarktdeConfig({ ...base, marktdeDryRun: 'nope' }).dryRun, true);
|
|
260
|
+
const live = new MarktdeConfig({ ...base, marktdeDryRun: 'false' });
|
|
261
|
+
assert.equal(live.dryRun, false);
|
|
262
|
+
assert.equal(live.outputFiles[0], 'resources/marktde-export/api_export_cars_marktde.csv');
|
|
263
|
+
assert.equal(live.feedGuard.minPublishedItems, 1);
|
|
264
|
+
assert.equal(new MarktdeConfig({ ...base, feedGuardMinPublishedItems: '50' }).feedGuard.minPublishedItems, 50);
|
|
265
|
+
}],
|
|
266
|
+
];
|
|
267
|
+
|
|
268
|
+
(async () => {
|
|
269
|
+
let failed = 0;
|
|
270
|
+
for (const [name, fn] of tests) {
|
|
271
|
+
try {
|
|
272
|
+
await fn();
|
|
273
|
+
console.log(`ok ${name}`);
|
|
274
|
+
} catch (e) {
|
|
275
|
+
failed += 1;
|
|
276
|
+
console.error(`FAIL ${name}\n ${e.message}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (failed) process.exit(1);
|
|
280
|
+
})();
|