@bytebrand/export-cars 1.26.32 → 1.26.34

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 (31) hide show
  1. package/dist/configs/openai-ads.config.js +2 -2
  2. package/dist/configs/openai-ads.config.js.map +1 -1
  3. package/dist/exporters/openai-ads.exporter.d.ts +3 -0
  4. package/dist/exporters/openai-ads.exporter.js +13 -2
  5. package/dist/exporters/openai-ads.exporter.js.map +1 -1
  6. package/dist/interfaces/openai-ads.interface.d.ts +3 -1
  7. package/dist/interfaces/openai-ads.interface.js +2 -0
  8. package/dist/interfaces/openai-ads.interface.js.map +1 -1
  9. package/dist/services/openai-ads-feed-run.d.ts +3 -1
  10. package/dist/services/openai-ads-feed-run.js +7 -2
  11. package/dist/services/openai-ads-feed-run.js.map +1 -1
  12. package/dist/transformers/openai-ads-finance.d.ts +2 -0
  13. package/dist/transformers/openai-ads-finance.js +39 -0
  14. package/dist/transformers/openai-ads-finance.js.map +1 -0
  15. package/dist/transformers/openai-ads.transformer.d.ts +9 -1
  16. package/dist/transformers/openai-ads.transformer.js +25 -5
  17. package/dist/transformers/openai-ads.transformer.js.map +1 -1
  18. package/dist/tsconfig.build.tsbuildinfo +1 -1
  19. package/docs/openai-ads/AUDIT.md +5 -4
  20. package/docs/openai-ads/RUNBOOK.md +36 -3
  21. package/package.json +2 -2
  22. package/src/configs/openai-ads.config.ts +4 -4
  23. package/src/exporters/openai-ads.exporter.ts +18 -1
  24. package/src/interfaces/openai-ads.interface.ts +4 -0
  25. package/src/services/openai-ads-feed-run.ts +12 -4
  26. package/src/transformers/openai-ads-finance.ts +60 -0
  27. package/src/transformers/openai-ads.transformer.ts +32 -6
  28. package/test/fixtures/openai-ads-finance-titles.json +325 -0
  29. package/test/openai-ads-feed-run.test.ts +20 -1
  30. package/test/openai-ads-finance.test.ts +109 -0
  31. package/test/openai-ads-price-cap.test.ts +69 -0
@@ -0,0 +1,325 @@
1
+ {
2
+ "source": "DEV openai_ads dry-run jmv50ahfr34 (export-cars 1.26.31, 12.10.2026): every title with finance wording, as sent",
3
+ "titles": [
4
+ {
5
+ "id": "09e44a3c-923b-4eff-adb8-23f038d5a18f",
6
+ "make": "Ford",
7
+ "variant": "Capri h Elektromotor V 0,0%"
8
+ },
9
+ {
10
+ "id": "b790c04d-05b7-45c8-95df-31ca88c89876",
11
+ "make": "Ford",
12
+ "variant": "Tourneo Custom L1 0% Zins Active FWD"
13
+ },
14
+ {
15
+ "id": "a9ba40a8-4704-4b6f-942c-06be932d26b8",
16
+ "make": "Ford",
17
+ "variant": "Transit 9-Sitzer L2H2 Trend Diesel 0%"
18
+ },
19
+ {
20
+ "id": "f6fac693-2848-4b9a-8b86-d3a5dcf33c21",
21
+ "make": "Ford",
22
+ "variant": "Transit Custom L1H1Plug-In 0% Zins"
23
+ },
24
+ {
25
+ "id": "5625b1d7-59d7-47f1-873b-229d9629502d",
26
+ "make": "Mercedes-Benz",
27
+ "variant": "GLE 450 AMG d 4M Coupé 4,49%/AMG/MULTIBEAM/AHK /Fahrass"
28
+ },
29
+ {
30
+ "id": "b99ab46c-d213-4f91-aff5-a796ae9c9772",
31
+ "make": "Mercedes-Benz",
32
+ "variant": "CLA 200 CLA 200 SB 4,49%/AMG/MBeam/Pano-D/Keyless/Memory"
33
+ },
34
+ {
35
+ "id": "c84f8480-f481-46dd-80cf-fcb6a6840a3d",
36
+ "make": "Mercedes-Benz",
37
+ "variant": "CLA 250 CLA 250 SB 4,49%/Night/LED/Memory/Keyless/Pano"
38
+ },
39
+ {
40
+ "id": "916ee86d-9932-44e4-b396-61be12ecc0f3",
41
+ "make": "Mercedes-Benz",
42
+ "variant": "GLB 200 4,49%Finz./Progressive/MBeam/AHK/Pano-D"
43
+ },
44
+ {
45
+ "id": "b4e816ae-0731-4ce9-a5f2-f96b7214b58c",
46
+ "make": "Mercedes-Benz",
47
+ "variant": "CLA 250 CLA 250 SB 4,49%Finz/AMG/MBeam/SuperScreen/360"
48
+ },
49
+ {
50
+ "id": "093e7e55-1c40-4794-8d4b-fac115b40f96",
51
+ "make": "Mercedes-Benz",
52
+ "variant": "GLA 180 4,49%/LED/Totwinkel/Spiegel-P/Winter-P /RfCam"
53
+ },
54
+ {
55
+ "id": "93bd5ce5-1b33-4ece-bd3e-d46dadbca789",
56
+ "make": "Mercedes-Benz",
57
+ "variant": "CLA 200 CLA 200 SB 4,49%/AMG/MBeam/AHK/Fahrass/Memory"
58
+ },
59
+ {
60
+ "id": "3a6b2a34-9093-484f-bf6b-e75a7e6fc2b8",
61
+ "make": "Mercedes-Benz",
62
+ "variant": "A 200 4,49%Finz/AMG/MBeam/Totw/KeylessGO/RfCam"
63
+ },
64
+ {
65
+ "id": "73e66e54-8e1f-4eca-b486-e3e274014b84",
66
+ "make": "Mercedes-Benz",
67
+ "variant": "GLC 200 d 4M 4,49%/Avantgarde/LED/AHK/DISTRONIC /Memory"
68
+ },
69
+ {
70
+ "id": "5a6775c6-9b4d-43a3-b14e-fc831a28caba",
71
+ "make": "Mercedes-Benz",
72
+ "variant": "S 580 e 4M 5,99%/AMG/MUTLIBEAM/Fahrass/Pano/AIRMT"
73
+ },
74
+ {
75
+ "id": "2ae99c9c-14db-421a-be41-f1b1f973405b",
76
+ "make": "Mercedes-Benz",
77
+ "variant": "C 220 d 4,49%/AMG/Night/LED/AHK/DISTRONIC/Memory /Totw"
78
+ },
79
+ {
80
+ "id": "6525b604-a83b-47b0-af25-ee45887ce3ef",
81
+ "make": "Mercedes-Benz",
82
+ "variant": "GLC 200 d 4M 4,49%/AMG/DigitalLight/AHK/DISTRONIC/Memory"
83
+ },
84
+ {
85
+ "id": "4f6a378e-950b-4a18-9cde-376588832494",
86
+ "make": "Mercedes-Benz",
87
+ "variant": "GLE 450 AMG d 4M Coupé 4,49%/AMG/MULTIBEAM/AHK /Fahrass"
88
+ },
89
+ {
90
+ "id": "f4c72125-6d1a-4c95-92de-222eb0144940",
91
+ "make": "Mercedes-Benz",
92
+ "variant": "GLB 200 4,49%Finz./Progressive/MBeam/AHK/Pano-D"
93
+ },
94
+ {
95
+ "id": "e6744adf-257b-472a-808a-ee1a731012b0",
96
+ "make": "Mercedes-Benz",
97
+ "variant": "CLA 250 CLA 250 SB 4,49%/Night/LED/Memory/Keyless/Pano"
98
+ },
99
+ {
100
+ "id": "abe2b3e2-bb21-43d3-9c78-755ca75fc4be",
101
+ "make": "Mercedes-Benz",
102
+ "variant": "CLA 250 CLA 250 SB 4,49%Finz/AMG/MBeam/SuperScreen/360"
103
+ },
104
+ {
105
+ "id": "7ed3d127-bf0c-4aae-9f83-ae42581cae98",
106
+ "make": "Mercedes-Benz",
107
+ "variant": "CLA 200 CLA 200 SB 4,49%/AMG/MBeam/Pano-D/Keyless/Memory"
108
+ },
109
+ {
110
+ "id": "05708374-3c45-4a32-b68e-44dd5f117c94",
111
+ "make": "Mercedes-Benz",
112
+ "variant": "GLA 180 4,49%/LED/Totwinkel/Spiegel-P/Winter-P /RfCam"
113
+ },
114
+ {
115
+ "id": "00bc34e2-c799-4374-af6f-8215f62d7140",
116
+ "make": "Mercedes-Benz",
117
+ "variant": "A 200 4,49%Finz/AMG/MBeam/Totw/KeylessGO/RfCam"
118
+ },
119
+ {
120
+ "id": "ffcfbc8f-24e8-47ed-87dc-5cd6061403d3",
121
+ "make": "Mercedes-Benz",
122
+ "variant": "CLA 200 CLA 200 SB 4,49%/AMG/MBeam/AHK/Fahrass/Memory"
123
+ },
124
+ {
125
+ "id": "127ed89f-e171-48d9-bfb3-f43b41f22762",
126
+ "make": "Mercedes-Benz",
127
+ "variant": "C 220 d 4,49%/AMG/Night/LED/AHK/DISTRONIC/Memory /Totw"
128
+ },
129
+ {
130
+ "id": "b752713e-3dc6-475b-832a-0be8e581c6d5",
131
+ "make": "Mercedes-Benz",
132
+ "variant": "GLC 200 d 4M 4,49%/AMG/DigitalLight/AHK/DISTRONIC/Memory"
133
+ },
134
+ {
135
+ "id": "a50dceb2-d3f4-4a65-b415-5bf99d5668ae",
136
+ "make": "Mercedes-Benz",
137
+ "variant": "S 580 e 4M 5,99%/AMG/MUTLIBEAM/Fahrass/Pano/AIRMT"
138
+ },
139
+ {
140
+ "id": "05b07358-c326-4742-880e-646c758f3f3e",
141
+ "make": "Mercedes-Benz",
142
+ "variant": "GLC 200 d 4M 4,49%/Avantgarde/LED/AHK/DISTRONIC /Memory"
143
+ },
144
+ {
145
+ "id": "bac5efc5-4bd6-4839-85cb-ada3eac94e8c",
146
+ "make": "Mercedes-Benz",
147
+ "variant": "GLE 450 AMG d 4M Coupé 4,49%/AMG/MULTIBEAM/AHK /Fahrass"
148
+ },
149
+ {
150
+ "id": "f65d5e01-9f1b-4865-b841-77dc7f4a4a95",
151
+ "make": "Mercedes-Benz",
152
+ "variant": "CLA 250 CLA 250 SB 4,49%/Night/LED/Memory/Keyless/Pano"
153
+ },
154
+ {
155
+ "id": "2e2083a1-0ab8-4f01-a10b-3bec0a5a9030",
156
+ "make": "Mercedes-Benz",
157
+ "variant": "GLB 200 4,49%Finz./Progressive/MBeam/AHK/Pano-D"
158
+ },
159
+ {
160
+ "id": "b0dcdb67-3049-45b1-bb72-0b3d59c223a5",
161
+ "make": "Mercedes-Benz",
162
+ "variant": "CLA 200 CLA 200 SB 4,49%/AMG/MBeam/Pano-D/Keyless/Memory"
163
+ },
164
+ {
165
+ "id": "2648b724-c1a5-453f-b728-80efe7f7998e",
166
+ "make": "Mercedes-Benz",
167
+ "variant": "CLA 250 CLA 250 SB 4,49%Finz/AMG/MBeam/SuperScreen/360"
168
+ },
169
+ {
170
+ "id": "2f2fafee-06a1-4742-954d-fa165a236e60",
171
+ "make": "Mercedes-Benz",
172
+ "variant": "GLA 180 4,49%/LED/Totwinkel/Spiegel-P/Winter-P /RfCam"
173
+ },
174
+ {
175
+ "id": "e9fffebd-db17-46a7-9763-084cd40b8b05",
176
+ "make": "Mercedes-Benz",
177
+ "variant": "CLA 200 CLA 200 SB 4,49%/AMG/MBeam/AHK/Fahrass/Memory"
178
+ },
179
+ {
180
+ "id": "7929c6c0-f612-4da3-bc1a-f85bd04b93d4",
181
+ "make": "Mercedes-Benz",
182
+ "variant": "A 200 4,49%Finz/AMG/MBeam/Totw/KeylessGO/RfCam"
183
+ },
184
+ {
185
+ "id": "145d9a13-94c4-4f4d-b678-11b7c2ecc5f0",
186
+ "make": "Mercedes-Benz",
187
+ "variant": "S 580 e 4M 5,99%/AMG/MUTLIBEAM/Fahrass/Pano/AIRMT"
188
+ },
189
+ {
190
+ "id": "0e029d65-e50c-4a30-bb96-a9867605fbb6",
191
+ "make": "Mercedes-Benz",
192
+ "variant": "C 220 d 4,49%/AMG/Night/LED/AHK/DISTRONIC/Memory /Totw"
193
+ },
194
+ {
195
+ "id": "bd79a204-b68c-430d-9858-9cb305ca880e",
196
+ "make": "Mercedes-Benz",
197
+ "variant": "GLC 200 d 4M 4,49%/Avantgarde/LED/AHK/DISTRONIC /Memory"
198
+ },
199
+ {
200
+ "id": "f01fd443-3ee5-412c-a520-163f73906326",
201
+ "make": "Mercedes-Benz",
202
+ "variant": "GLC 200 d 4M 4,49%/AMG/DigitalLight/AHK/DISTRONIC/Memory"
203
+ },
204
+ {
205
+ "id": "eafce82e-aea0-4473-9755-8d2a35cdb925",
206
+ "make": "Mercedes-Benz",
207
+ "variant": "GLE 450 AMG d 4M Coupé 4,49%/AMG/MULTIBEAM/AHK /Fahrass"
208
+ },
209
+ {
210
+ "id": "2b705214-241e-4d42-9556-fdc132d03379",
211
+ "make": "Mercedes-Benz",
212
+ "variant": "CLA 250 CLA 250 SB 4,49%/Night/LED/Memory/Keyless/Pano"
213
+ },
214
+ {
215
+ "id": "8ae9bb34-b49a-4f13-b10e-cae16a99ddce",
216
+ "make": "Mercedes-Benz",
217
+ "variant": "GLB 200 4,49%Finz./Progressive/MBeam/AHK/Pano-D"
218
+ },
219
+ {
220
+ "id": "e2fda3c9-5a22-4534-9ab4-86782e87dba9",
221
+ "make": "Mercedes-Benz",
222
+ "variant": "CLA 200 CLA 200 SB 4,49%/AMG/MBeam/Pano-D/Keyless/Memory"
223
+ },
224
+ {
225
+ "id": "90c478f9-3667-47e7-9f57-5eaf92adc1be",
226
+ "make": "Mercedes-Benz",
227
+ "variant": "CLA 250 CLA 250 SB 4,49%Finz/AMG/MBeam/SuperScreen/360"
228
+ },
229
+ {
230
+ "id": "9f258739-2225-4015-acdb-b2e546a3c7f1",
231
+ "make": "Mercedes-Benz",
232
+ "variant": "GLA 180 4,49%/LED/Totwinkel/Spiegel-P/Winter-P /RfCam"
233
+ },
234
+ {
235
+ "id": "38d89811-f53b-4e62-9312-8e1916220289",
236
+ "make": "Mercedes-Benz",
237
+ "variant": "CLA 200 CLA 200 SB 4,49%/AMG/MBeam/AHK/Fahrass/Memory"
238
+ },
239
+ {
240
+ "id": "ebb117a0-5b62-4631-b2ea-a27c03b6bff5",
241
+ "make": "Mercedes-Benz",
242
+ "variant": "A 200 4,49%Finz/AMG/MBeam/Totw/KeylessGO/RfCam"
243
+ },
244
+ {
245
+ "id": "e6a10fcc-3b6e-4b1b-8144-fe23d60f67e6",
246
+ "make": "Mercedes-Benz",
247
+ "variant": "C 220 d 4,49%/AMG/Night/LED/AHK/DISTRONIC/Memory /Totw"
248
+ },
249
+ {
250
+ "id": "8a9e90fa-d28e-4b18-995d-d0f33e57b5b3",
251
+ "make": "Mercedes-Benz",
252
+ "variant": "GLC 200 d 4M 4,49%/AMG/DigitalLight/AHK/DISTRONIC/Memory"
253
+ },
254
+ {
255
+ "id": "f56e88b7-b1ef-4274-b406-87873a573086",
256
+ "make": "Mercedes-Benz",
257
+ "variant": "S 580 e 4M 5,99%/AMG/MUTLIBEAM/Fahrass/Pano/AIRMT"
258
+ },
259
+ {
260
+ "id": "40b2c9c6-d0df-48fd-a1cf-faae9fc9ec5e",
261
+ "make": "Mercedes-Benz",
262
+ "variant": "GLC 200 d 4M 4,49%/Avantgarde/LED/AHK/DISTRONIC /Memory"
263
+ },
264
+ {
265
+ "id": "45fe1863-1f8e-49f1-a729-931391aef140",
266
+ "make": "Mercedes-Benz",
267
+ "variant": "GLE 450 AMG d 4M Coupé 4,49%/AMG/MULTIBEAM/AHK /Fahrass"
268
+ },
269
+ {
270
+ "id": "79c2ede3-4945-4efe-9ec8-31e53f97cccb",
271
+ "make": "Mercedes-Benz",
272
+ "variant": "CLA 200 CLA 200 SB 4,49%/AMG/MBeam/Pano-D/Keyless/Memory"
273
+ },
274
+ {
275
+ "id": "8d3534e9-9672-4b15-968a-c5b8acb1e68a",
276
+ "make": "Mercedes-Benz",
277
+ "variant": "CLA 250 CLA 250 SB 4,49%/Night/LED/Memory/Keyless/Pano"
278
+ },
279
+ {
280
+ "id": "461c65b9-c672-4afc-86c2-6ac2858c4324",
281
+ "make": "Mercedes-Benz",
282
+ "variant": "GLB 200 4,49%Finz./Progressive/MBeam/AHK/Pano-D"
283
+ },
284
+ {
285
+ "id": "bedc074c-8e23-4d7c-9323-c6c7b4b85c24",
286
+ "make": "Mercedes-Benz",
287
+ "variant": "CLA 250 CLA 250 SB 4,49%Finz/AMG/MBeam/SuperScreen/360"
288
+ },
289
+ {
290
+ "id": "20c1166c-ee32-45fe-8d6d-918b78335a60",
291
+ "make": "Mercedes-Benz",
292
+ "variant": "GLA 180 4,49%/LED/Totwinkel/Spiegel-P/Winter-P /RfCam"
293
+ },
294
+ {
295
+ "id": "2999c62d-b66b-46f0-898e-c4ac78c3a7bc",
296
+ "make": "Mercedes-Benz",
297
+ "variant": "CLA 200 CLA 200 SB 4,49%/AMG/MBeam/AHK/Fahrass/Memory"
298
+ },
299
+ {
300
+ "id": "0ce7953c-03cf-455d-b0cc-aed4912cf025",
301
+ "make": "Mercedes-Benz",
302
+ "variant": "A 200 4,49%Finz/AMG/MBeam/Totw/KeylessGO/RfCam"
303
+ },
304
+ {
305
+ "id": "8ca1b20a-8a88-49c2-af1c-fb23b107748d",
306
+ "make": "Mercedes-Benz",
307
+ "variant": "C 220 d 4,49%/AMG/Night/LED/AHK/DISTRONIC/Memory /Totw"
308
+ },
309
+ {
310
+ "id": "4e93d6eb-72a2-445d-b9c4-aa3671c6aea2",
311
+ "make": "Mercedes-Benz",
312
+ "variant": "GLC 200 d 4M 4,49%/Avantgarde/LED/AHK/DISTRONIC /Memory"
313
+ },
314
+ {
315
+ "id": "32ccecf9-2fd5-46cd-a2ed-028c473504d3",
316
+ "make": "Mercedes-Benz",
317
+ "variant": "GLC 200 d 4M 4,49%/AMG/DigitalLight/AHK/DISTRONIC/Memory"
318
+ },
319
+ {
320
+ "id": "98acad5f-0c2c-4e9b-b961-03b671e6c637",
321
+ "make": "Mercedes-Benz",
322
+ "variant": "S 580 e 4M 5,99%/AMG/MUTLIBEAM/Fahrass/Pano/AIRMT"
323
+ }
324
+ ]
325
+ }
@@ -9,6 +9,7 @@ import { join } from 'path';
9
9
  import { FeedGuard, FeedGuardError, FeedGuardOptions } from '../src/services/feed-guard';
10
10
  import { DealerStatus, OpenAiAdsFeedRun, SourceCar } from '../src/services/openai-ads-feed-run';
11
11
  import { selectOpenAiImageFields } from '../src/transformers/openai-image-fields';
12
+ import { OpenAiAdsExclusion } from '../src/interfaces/openai-ads.interface';
12
13
  import { buildImagesService } from './fixtures/images-service.harness';
13
14
  import { NOW, car, csvObjects, images, translate } from './fixtures/openai-ads.fixture';
14
15
 
@@ -69,7 +70,10 @@ const run = async (world: World, at: Date = NOW, extra: Partial<FeedGuardOptions
69
70
  const feed = new OpenAiAdsFeedRun(
70
71
  {
71
72
  forEachChunk,
72
- filterCar: ({ car: c }) => (c?.metaData?.imagesCount ?? 0) > 0,
73
+ filterCar: ({ car: c }) => {
74
+ if (!((c?.metaData?.imagesCount ?? 0) > 0)) return OpenAiAdsExclusion.filtered;
75
+ return Number(c?.price?.currentSalesPrice) > 80_000 ? OpenAiAdsExclusion.priceCap : null;
76
+ },
73
77
  dealerStatus: async ownerId => world.dealers[ownerId] ?? 'active',
74
78
  resolveImages: async ({ car: c, images: list }) => {
75
79
  const clean = imagesService.getCleanImages(c._id, list);
@@ -160,6 +164,21 @@ const cases: Array<[string, () => Promise<void>]> = [
160
164
  assert.equal(back['car-1'].availability, 'in_stock');
161
165
  assert.equal(Object.keys(back).length, 5);
162
166
  }],
167
+ ['price crosses the €80k cap: advertised car switches to out_of_stock (same id), 80,000.00 stays', async () => {
168
+ const w = await newWorld();
169
+ await writeSource(w, [src('car-0', { 'price.currentSalesPrice': 79_990 }), src('car-1', { 'price.currentSalesPrice': 79_000 }), ...fleet(5).slice(2)]);
170
+ await run(w);
171
+ const next = new Date(NOW.getTime() + DAY);
172
+ await writeSource(w, [src('car-0', { 'price.currentSalesPrice': 80_000.01 }), src('car-1', { 'price.currentSalesPrice': 80_000 }), ...fleet(5).slice(2)], 2, next);
173
+ const report = await run(w, next);
174
+ const rows = byId(await published(w));
175
+ assert.equal(rows['car-0'].availability, 'out_of_stock');
176
+ assert.equal(rows['car-0'].price, '79990.00 EUR', 'last advertised data, not the new price');
177
+ assert.equal(rows['car-1'].availability, 'in_stock');
178
+ assert.equal(rows['car-1'].price, '80000.00 EUR');
179
+ assert.equal(report.excluded.price_cap, 1);
180
+ assert.deepEqual(report.outOfStock, { not_in_snapshot: 1 });
181
+ }],
163
182
  ['out_of_stock rows stop after the retention window', async () => {
164
183
  const w = await newWorld();
165
184
  await writeSource(w, fleet(5));
@@ -0,0 +1,109 @@
1
+ // Run: npm test (ts-node, no test framework needed)
2
+ // ADV-335/336 launch gate: no finance wording (rates, Zins, Leasing, monthly amounts) in the OpenAI feed.
3
+ import * as assert from 'node:assert/strict';
4
+ import { readFileSync } from 'fs';
5
+ import { join } from 'path';
6
+
7
+ import { OpenAiAdsExclusion as X } from '../src/interfaces/openai-ads.interface';
8
+ import { hasFinanceClaim, stripFinanceClaims } from '../src/transformers/openai-ads-finance';
9
+ import { asOutOfStock, mapOpenAiAdsVehicle } from '../src/transformers/openai-ads.transformer';
10
+ import { normalizeFeedText } from '../src/utils';
11
+ import { NOW, car, translate } from './fixtures/openai-ads.fixture';
12
+
13
+ const ctx = { domain: 'auto.de', sellerName: 'auto.de', translate, now: NOW };
14
+ const map = (make: string, subModel: string, model = '') =>
15
+ mapOpenAiAdsVehicle(car('id-1', { 'mainData.make': make, 'mainData.model': model, 'mainData.subModel': subModel }), ctx) as any;
16
+
17
+ const real: { titles: Array<{ id: string; make: string; variant: string }> } =
18
+ JSON.parse(readFileSync(join(__dirname, 'fixtures', 'openai-ads-finance-titles.json'), 'utf8'));
19
+
20
+ const cases: Array<[string, () => void]> = [
21
+ ['all 64 real DEV titles with finance wording: cleaned, still sent, nothing left in title or description', () => {
22
+ assert.equal(real.titles.length, 64);
23
+ for (const { id, make, variant } of real.titles) {
24
+ const result = map(make, variant);
25
+ assert.equal(result.ok, true, `${id} excluded: ${variant}`);
26
+ assert.equal(result.financeTermsRemoved, true, `${id} not marked: ${variant}`);
27
+ assert.ok(!hasFinanceClaim(result.row.title), `${id} title: ${result.row.title}`);
28
+ assert.ok(!hasFinanceClaim(result.row.description), `${id} description: ${result.row.description}`);
29
+ assert.ok(!/%/.test(result.row.title.replace(/\d+\s*%\s*SoH/i, '')), `${id} percent left: ${result.row.title}`);
30
+ }
31
+ }],
32
+ ['real examples before -> after', () => {
33
+ const expected: Array<[string, string, string]> = [
34
+ ['Mercedes-Benz', 'GLB 200 4,49%Finz./Progressive/MBeam/AHK', 'Mercedes-Benz GLB 200 Progressive/MBeam/AHK'],
35
+ ['Mercedes-Benz', 'CLA 250 CLA 250 SB 4,49%Finz/AMG/MBeam', 'Mercedes-Benz CLA 250 CLA 250 SB AMG/MBeam'],
36
+ ['Mercedes-Benz', 'CLA 200 CLA 200 SB 4,49%/AMG/MBeam/Pano-D', 'Mercedes-Benz CLA 200 CLA 200 SB AMG/MBeam/Pano-D'],
37
+ ['Mercedes-Benz', 'GLE 450 AMG d 4M Coupé 4,49%/AMG/MULTIBEAM', 'Mercedes-Benz GLE 450 AMG d 4M Coupé AMG/MULTIBEAM'],
38
+ ['Mercedes-Benz', 'GLC 300 4M 5,99%/AMG/MUTLI', 'Mercedes-Benz GLC 300 4M AMG/MUTLI'],
39
+ ['Ford', 'Tourneo Custom L1 0% Zins Active FWD', 'Ford Tourneo Custom L1 Active FWD'],
40
+ ['Ford', 'Transit Custom L1H1Plug-In 0% Zins', 'Ford Transit Custom L1H1Plug-In'],
41
+ ['Ford', 'Transit 9-Sitzer L2H2 Trend Diesel 0%', 'Ford Transit 9-Sitzer L2H2 Trend Diesel'],
42
+ ['Ford', 'Capri h Elektromotor V 0,0%', 'Ford Capri h Elektromotor V'],
43
+ ];
44
+ for (const [make, variant, title] of expected) {
45
+ assert.equal(map(make, variant).row.title, title);
46
+ }
47
+ }],
48
+ ['kept: battery / damage percentages, emission class, warranty', () => {
49
+ for (const variant of ['Model 3 Highland RWD 99% SoH LFP', '100% elektrisch Reichweite 400 km', 'Golf 0% Schaden', 'Akku 95% Kapazität',
50
+ 'Comfortline (EURO 6d)', 'Euro 6e', 'Fabia 5J. Garantie Klimaauto.', 'Arona 5J GARANTIE SHZ', 'Kamera Separate Klima']) {
51
+ const result = map('Volkswagen', variant);
52
+ assert.equal(result.ok, true, variant);
53
+ assert.equal(result.financeTermsRemoved, undefined, variant);
54
+ assert.equal(result.row.title, `Volkswagen ${normalizeFeedText(variant)}`, variant);
55
+ assert.equal(stripFinanceClaims(variant), variant);
56
+ assert.equal(hasFinanceClaim(variant), false, variant);
57
+ }
58
+ }],
59
+ ['other finance wording is cut: monthly amounts, leasing, credit, Santander, APR', () => {
60
+ const cleaned: Array<[string, string]> = [
61
+ ['Golf Life ab 199 € mtl. Navi', 'Golf Life Navi'],
62
+ ['Golf Life 199€/Monat Navi', 'Golf Life Navi'],
63
+ ['Golf Life mtl. 199 € Navi', 'Golf Life Navi'],
64
+ ['Golf Life Leasing ab 299 Navi', 'Golf Life ab 299 Navi'],
65
+ ['Golf Life Finanzierung/Leasing/Kredit/Darlehen möglich', 'Golf Life möglich'],
66
+ ['Golf Life Santander Finanzierung Anzahlung 0 Schlussrate', 'Golf Life 0'],
67
+ ['Golf Life effektiver Jahreszins 5,99 % Sollzins 5,82%', 'Golf Life'],
68
+ ['Golf Life 0%-Finanzierung Navi', 'Golf Life Navi'],
69
+ ['Golf Life Raten ab 99', 'Golf Life ab 99'],
70
+ ];
71
+ for (const [variant, expected] of cleaned) {
72
+ assert.equal(stripFinanceClaims(variant), expected, variant);
73
+ assert.equal(hasFinanceClaim(stripFinanceClaims(variant)), false, variant);
74
+ }
75
+ }],
76
+ ['fail-closed: wording the stripping cannot cut keeps the car out (finance_claim)', () => {
77
+ assert.equal(map('Mercedes-Benz', 'GLA 200 AMGFinanzierung Navi').reason, X.financeClaim);
78
+ assert.equal(map('Ford', 'Kuga TopZinsangebot').reason, X.financeClaim);
79
+ assert.equal(hasFinanceClaim('Golf 199 EUR/Monat'), true);
80
+ assert.equal(hasFinanceClaim('Golf 4,49%'), true);
81
+ assert.equal(hasFinanceClaim('Golf 99% SoH'), false);
82
+ }],
83
+ ['ledger rows from before the rule are cleaned when re-sent as out_of_stock', () => {
84
+ const old: any = {
85
+ item_id: 'x', brand: 'Mercedes-Benz', title: 'Mercedes-Benz GLB 200 4,49%Finz./Progressive',
86
+ description: 'Gebrauchtwagen: Mercedes-Benz GLB 200 4,49%Finz./Progressive. Kilometerstand: 10 km',
87
+ availability: 'in_stock', is_ads_eligible: true,
88
+ };
89
+ const row = asOutOfStock(old);
90
+ assert.equal(row.title, 'Mercedes-Benz GLB 200 Progressive');
91
+ assert.equal(row.description, 'Gebrauchtwagen: Mercedes-Benz GLB 200 Progressive. Kilometerstand: 10 km');
92
+ assert.equal(row.availability, 'out_of_stock');
93
+ const glued = asOutOfStock({ ...old, title: 'Mercedes-Benz GLB AMGFinanzierung', description: 'Gebrauchtwagen: Mercedes-Benz GLB AMGFinanzierung.' });
94
+ assert.equal(glued.title, 'Mercedes-Benz');
95
+ assert.equal(glued.description, 'Mercedes-Benz');
96
+ }],
97
+ ];
98
+
99
+ let failed = 0;
100
+ for (const [name, fn] of cases) {
101
+ try {
102
+ fn();
103
+ console.log(`ok ${name}`);
104
+ } catch (e) {
105
+ failed += 1;
106
+ console.error(`FAIL ${name}\n ${e.message}`);
107
+ }
108
+ }
109
+ if (failed) process.exit(1);
@@ -0,0 +1,69 @@
1
+ // Run: npm test (ts-node, no test framework needed)
2
+ // Decision 14.10: the OpenAI feed uses the same €80k selling-price cap as Google/Meta.
3
+ import * as assert from 'node:assert/strict';
4
+
5
+ import { OpenAiAdsConfig } from '../src/configs/openai-ads.config';
6
+ import { MAX_FEED_SALES_PRICE_EUR, resolveMaxFeedSalesPrice } from '../src/constants/feed-price.constants';
7
+ import { CarService } from '../src/services/car.service';
8
+ import { classifyPartnerFilter } from '../src/exporters/openai-ads.exporter';
9
+ import { OpenAiAdsExclusion as X } from '../src/interfaces/openai-ads.interface';
10
+ import { car } from './fixtures/openai-ads.fixture';
11
+
12
+ const openAiConfig = (maxCurrentSalesPrice?: string) =>
13
+ new OpenAiAdsConfig({ environment: 'develop', exportType: 'openai_ads', maxCurrentSalesPrice } as any);
14
+
15
+ // The real shared gate, with the OpenAI config (no Cronicle rules).
16
+ const filterCar = (config: OpenAiAdsConfig, price: unknown, imagesCount = 3) => {
17
+ const service = new CarService(null as any, null as any, { config } as any);
18
+ return service.filterCar({ car: car('id', { 'price.currentSalesPrice': price, 'metaData.imagesCount': imagesCount }) }, []);
19
+ };
20
+
21
+ const cases: Array<[string, () => void]> = [
22
+ ['config: default cap is the shared 80,000 (same resolver as Google/Meta)', () => {
23
+ assert.equal(MAX_FEED_SALES_PRICE_EUR, 80_000);
24
+ for (const raw of [undefined, '', ' ', 'abc', '0', '-5']) {
25
+ assert.equal(openAiConfig(raw).maxCurrentSalesPrice, 80_000, String(raw));
26
+ // google/fb/facebook-shop configs call the same resolver
27
+ assert.equal(openAiConfig(raw).maxCurrentSalesPrice, resolveMaxFeedSalesPrice(raw));
28
+ }
29
+ }],
30
+ ['config: a valid positive param wins (50000)', () => {
31
+ assert.equal(openAiConfig('50000').maxCurrentSalesPrice, 50_000);
32
+ }],
33
+ ['gate (CarService.filterCar): 80,000.00 included, 80,000.01 excluded', () => {
34
+ const config = openAiConfig();
35
+ assert.equal(filterCar(config, 80_000), true);
36
+ assert.equal(filterCar(config, '80000.00'), true);
37
+ assert.equal(filterCar(config, 79_999.99), true);
38
+ assert.equal(filterCar(config, 80_000.01), false);
39
+ assert.equal(filterCar(config, 222_148), false);
40
+ for (const bad of [0, -1, null, 'abc']) {
41
+ assert.equal(filterCar(config, bad), false, String(bad));
42
+ }
43
+ }],
44
+ ['gate with param 50000: 50,000 in, 50,000.01 out', () => {
45
+ const config = openAiConfig('50000');
46
+ assert.equal(filterCar(config, 50_000), true);
47
+ assert.equal(filterCar(config, 50_000.01), false);
48
+ }],
49
+ ['report reason: above the cap = price_cap; no photos or invalid price = filtered; passed = null', () => {
50
+ const source = (price: unknown, imagesCount = 3) => ({ car: car('id', { 'price.currentSalesPrice': price, 'metaData.imagesCount': imagesCount }), images: [] });
51
+ assert.equal(classifyPartnerFilter(false, source(80_000.01), 80_000), X.priceCap);
52
+ assert.equal(classifyPartnerFilter(false, source(95_000, 0), 80_000), X.filtered);
53
+ assert.equal(classifyPartnerFilter(false, source(0), 80_000), X.filtered);
54
+ assert.equal(classifyPartnerFilter(false, source(17_990), 80_000), X.filtered);
55
+ assert.equal(classifyPartnerFilter(true, source(17_990), 80_000), null);
56
+ }],
57
+ ];
58
+
59
+ let failed = 0;
60
+ for (const [name, fn] of cases) {
61
+ try {
62
+ fn();
63
+ console.log(`ok ${name}`);
64
+ } catch (e) {
65
+ failed += 1;
66
+ console.error(`FAIL ${name}\n ${e.message}`);
67
+ }
68
+ }
69
+ if (failed) process.exit(1);