@bytebrand/export-cars 1.26.32 → 1.26.33
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/interfaces/openai-ads.interface.d.ts +2 -1
- package/dist/interfaces/openai-ads.interface.js +1 -0
- package/dist/interfaces/openai-ads.interface.js.map +1 -1
- package/dist/services/openai-ads-feed-run.d.ts +1 -0
- package/dist/services/openai-ads-feed-run.js +4 -0
- package/dist/services/openai-ads-feed-run.js.map +1 -1
- package/dist/transformers/openai-ads-finance.d.ts +2 -0
- package/dist/transformers/openai-ads-finance.js +39 -0
- package/dist/transformers/openai-ads-finance.js.map +1 -0
- package/dist/transformers/openai-ads.transformer.d.ts +9 -1
- package/dist/transformers/openai-ads.transformer.js +25 -5
- package/dist/transformers/openai-ads.transformer.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/openai-ads/AUDIT.md +2 -1
- package/docs/openai-ads/RUNBOOK.md +26 -2
- package/package.json +2 -2
- package/src/interfaces/openai-ads.interface.ts +2 -0
- package/src/services/openai-ads-feed-run.ts +6 -0
- package/src/transformers/openai-ads-finance.ts +60 -0
- package/src/transformers/openai-ads.transformer.ts +32 -6
- package/test/fixtures/openai-ads-finance-titles.json +325 -0
- package/test/openai-ads-finance.test.ts +109 -0
package/docs/openai-ads/AUDIT.md
CHANGED
|
@@ -182,6 +182,7 @@ Filters before mapping (all reasons counted in the run report):
|
|
|
182
182
|
| Q11 | OAI-AdsBot / OAI-SearchBot reachability of `www.auto.de` VDP and `images.auto.de` behind Cloudflare (js_challenge rules from ADV-193). Test only, no CF change without approval. | Eng + approval | go-live |
|
|
183
183
|
| Q12 | Image: ADV-336 clean profile is used (no overlay). Approval of plain photos + `ext_lr_main` question open in ADV-336 | ADV-336 | image_url |
|
|
184
184
|
| Q13 | Is a "Stable" vs "Draft" schema selector visible in your docs/account? The public page shows none on 09.10. | Marketing | — |
|
|
185
|
+
| Q15 | Finance claims: closed 13.10 — no finance wording in the feed (`finance_claim` rule, RUNBOOK). Same dealer titles in Google/Meta feeds and on the CDP: separate ticket candidate. | — | done (1.26.33) |
|
|
185
186
|
| Q14 | Make mapping at import (out of feed scope): DEV car titled "Jeep SWM G01a" — model SONSTIGE, variant names SWM (a different manufacturer), so the feed sends `brand=Jeep` for a non-Jeep car. Fix belongs to the import make/model mapping (sheet / CBD), not to the exporter. | Import owner | brand correctness |
|
|
186
187
|
|
|
187
188
|
---
|
|
@@ -230,7 +231,7 @@ fails the job itself (`exitError` + exit 1). The global fix is a separate task.
|
|
|
230
231
|
|
|
231
232
|
Not done (next steps / human):
|
|
232
233
|
- transport: hosted URL or SFTP, after Q10;
|
|
233
|
-
- Cronicle event: DEV dry-run
|
|
234
|
+
- Cronicle event: DEV event `emv4ycd4g2y` exists (dry-run, manual runs; no schedule / failure e-mail on DEV by decision, closed 12.10); PROD event not created;
|
|
234
235
|
- a real DEV/PROD dry-run plus the 20-car reconciliation;
|
|
235
236
|
- delta PATCH: deferred, needs API access;
|
|
236
237
|
- crawler access, per ADV-336: robots.txt has `Disallow: /search/` for OAI-AdsBot, plus the Cloudflare rules and the SPA shell without SSR on `/search*`.
|
|
@@ -34,10 +34,15 @@ State dir: `ledger.json` (items sent in_stock, for out_of_stock), `last-good.jso
|
|
|
34
34
|
No transport is implemented yet. Nothing leaves the server until a hosted-URL location or an SFTP upload is set
|
|
35
35
|
up for the **live** output only (separate, approved step).
|
|
36
36
|
|
|
37
|
-
## Cronicle event
|
|
37
|
+
## Cronicle event
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
**DEV (exists, closed 12.10):** event `openai_ads` = `emv4ycd4g2y`, dry-run with the default params, manual runs
|
|
40
|
+
only. By user decision it has no schedule and no failure e-mail — nothing to configure on DEV.
|
|
41
|
+
|
|
42
|
+
**PROD (not created):** to be set up by an admin once the go-live gate is passed:
|
|
43
|
+
- Plugin "Export cars", run daily after "1.0 Base export" (own timing about 1 h after it, or chained).
|
|
40
44
|
Items expire at OpenAI after 2 weeks without refresh — never rely on manual uploads.
|
|
45
|
+
- Failure notification (e-mail) on — a blocked run is the only signal that the feed is not refreshed.
|
|
41
46
|
- Concurrency 1 (the job also takes `run.lock`).
|
|
42
47
|
- Params (all text; unset = default):
|
|
43
48
|
|
|
@@ -89,6 +94,25 @@ running after a failure as before.
|
|
|
89
94
|
Only `openai_ads` is protected: the feed guard keeps the last good file, and the job exits with code 1
|
|
90
95
|
(`exitError` + exit 1 in `openai-ads.exporter.ts`). It has no chain, so this stops nothing else.
|
|
91
96
|
|
|
97
|
+
## No finance claims (`finance_claim`, since 1.26.33)
|
|
98
|
+
|
|
99
|
+
OpenAI's ad policy lists "Auto loans and leasing" outside the US as "Restricted to Select Approved Advertisers"
|
|
100
|
+
(category table in help.openai.com/en/articles/20001534-troubleshooting-common-onboarding-and-policy-issues;
|
|
101
|
+
openai.com/policies/ad-policies). The ADV-335/336 launch gate (13.10) therefore allows no finance wording in the feed.
|
|
102
|
+
|
|
103
|
+
- Source: only the dealer variant text (`mainData.subModel`), e.g. "GLB 200 4,49%Finz./Progressive",
|
|
104
|
+
"Tourneo Custom L1 0% Zins". The description is generated from structured fields and only repeats the title.
|
|
105
|
+
- `transformers/openai-ads-finance.ts`: before the title is normalised, rates (`x,xx %`, `0 %`) and finance words
|
|
106
|
+
(Finz./Finanzierung/finanzier*, Zins, Rate(n), Leasing, mtl./monatlich, `€/Monat`, Anzahlung, Schlussrate, Sollzins,
|
|
107
|
+
effektiver Jahreszins, Kredit, Darlehen, Santander) are cut. A percentage next to SoH/Akku/Batterie/elektrisch/
|
|
108
|
+
Schaden is kept ("99% SoH", "0% Schaden").
|
|
109
|
+
- Fail-closed: if a broader check still finds finance wording in title or description, the car is not sent
|
|
110
|
+
(`excluded.finance_claim`). Cleaned in_stock rows are counted as `cleaned.finance_terms_removed`.
|
|
111
|
+
- Rows re-sent from the ledger as out_of_stock are cleaned the same way (brand only if wording is left).
|
|
112
|
+
- DEV 12.10 (1.26.31): 64 of 1,002 titles had such wording (60 Mercedes "4,49%"/"5,99%"/"Finz.", 4 Ford "0% Zins").
|
|
113
|
+
- Out of scope, candidate for a separate ticket: the same dealer titles go into the Google/Meta feeds and are shown
|
|
114
|
+
on the CDP.
|
|
115
|
+
|
|
92
116
|
## Known source-data issues (not fixed by the feed)
|
|
93
117
|
|
|
94
118
|
- Wrong make from the import mapping: e.g. DEV "Jeep SWM G01a" (model SONSTIGE, the variant names another
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bytebrand/export-cars",
|
|
3
|
-
"version": "1.26.
|
|
3
|
+
"version": "1.26.33",
|
|
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/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/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"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@bytebrand/autode-request": "^1.19.2",
|
|
@@ -53,6 +53,8 @@ export enum OpenAiAdsExclusion {
|
|
|
53
53
|
dealerInactive = 'dealer_inactive',
|
|
54
54
|
dealerUnknown = 'dealer_unknown',
|
|
55
55
|
noImage = 'no_image',
|
|
56
|
+
// finance wording left after stripping (rates, Zins, Leasing, mtl., …) — ADV-335/336 launch gate
|
|
57
|
+
financeClaim = 'finance_claim',
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
// Why an item is sent as out_of_stock.
|
|
@@ -63,6 +63,8 @@ export interface OpenAiAdsRunReport {
|
|
|
63
63
|
rows: { inStock: number; outOfStock: number; total: number };
|
|
64
64
|
outOfStock: Record<string, number>;
|
|
65
65
|
excluded: Record<string, number>;
|
|
66
|
+
// in_stock rows whose title had finance wording cut (finance_terms_removed)
|
|
67
|
+
cleaned: Record<string, number>;
|
|
66
68
|
reservedNeverSent: number;
|
|
67
69
|
ledger: { before: number; after: number; expired: number };
|
|
68
70
|
price: { min: number | null; max: number | null };
|
|
@@ -103,6 +105,7 @@ export class OpenAiAdsFeedRun {
|
|
|
103
105
|
rows: { inStock: 0, outOfStock: 0, total: 0 },
|
|
104
106
|
outOfStock: {},
|
|
105
107
|
excluded: {},
|
|
108
|
+
cleaned: {},
|
|
106
109
|
reservedNeverSent: 0,
|
|
107
110
|
ledger: { before: 0, after: 0, expired: 0 },
|
|
108
111
|
price: { min: null, max: null },
|
|
@@ -176,6 +179,9 @@ export class OpenAiAdsFeedRun {
|
|
|
176
179
|
continue;
|
|
177
180
|
}
|
|
178
181
|
await builder.addItem(row);
|
|
182
|
+
if (mapped.financeTermsRemoved) {
|
|
183
|
+
increment(report.cleaned, 'finance_terms_removed');
|
|
184
|
+
}
|
|
179
185
|
inStockIds.add(row.item_id);
|
|
180
186
|
ledger.markInStock(row, startedAt);
|
|
181
187
|
report.rows.inStock += 1;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* No finance claims in the OpenAI Ads feed (ADV-335/336 launch gate, 13.10). OpenAI's ads policy
|
|
3
|
+
* lists "Auto loans and leasing" outside the US as "Restricted to Select Approved Advertisers", so
|
|
4
|
+
* the feed must not carry rates, interest, leasing or monthly-payment wording at all.
|
|
5
|
+
*
|
|
6
|
+
* The only source today is the dealer's variant text (mainData.subModel), e.g. "GLB 200
|
|
7
|
+
* 4,49%Finz./Progressive" or "Tourneo Custom L1 0% Zins" — the description is generated from
|
|
8
|
+
* structured fields and repeats the title. Two layers:
|
|
9
|
+
* - stripFinanceClaims: removes finance tokens (word-bounded) before the title is normalised;
|
|
10
|
+
* - hasFinanceClaim: a broader substring check on the final title/description; a match keeps the
|
|
11
|
+
* car out of the feed (exclusion `finance_claim`), so anything the stripping misses fails closed.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// A percentage next to these words is a battery/damage figure, not an interest rate:
|
|
15
|
+
// "99% SoH", "Akku 95%", "100% elektrisch", "0% Schaden".
|
|
16
|
+
const NON_FINANCE_PERCENT_CONTEXT = /soh|akku|batter|kapazit|elektr|schaden|unfall|reichweite|ladung/i;
|
|
17
|
+
const PERCENT = /\d{1,3}(?:[.,]\d{1,2})?\s*%-?/g;
|
|
18
|
+
|
|
19
|
+
const FINANCE_TOKENS: RegExp[] = [
|
|
20
|
+
// monthly amounts: "ab 199 € mtl.", "199€/Monat", "mtl. 199 €", "pro Monat"
|
|
21
|
+
/(?:\bab\s+)?\d[\d.,]*\s*(?:€|\beur\b|\beuro\b)?\s*(?:\/\s*|pro\s+|im\s+)?(?:\bmonat\b|\bmtl\b\.?|\bmonatl\b\.?|\bmonatlich\b)/gi,
|
|
22
|
+
/(?:\bmtl\b\.?|\bmonatl\b\.?|\bmonatlich\w*)\s*(?:ab\s+)?(?:\d[\d.,]*\s*(?:€|\beur\b|\beuro\b)?)?/gi,
|
|
23
|
+
/(?:€|\beur\b|\beuro\b)?\s*(?:\/\s*|\bpro\s+|\bim\s+)monat\b/gi,
|
|
24
|
+
// finance words
|
|
25
|
+
/\beffektiv(?:er|en)?\s+jahreszins\w*/gi,
|
|
26
|
+
/\b(?:finz\b\.?|finanz\w*|ballonfinanz\w*|zins\w*|sollzins\w*|jahreszins\w*|schlussrate\w*|monatsrate\w*|raten?|leasing\w*|leasen|anzahlung\w*|kredit\w*|darlehen\w*|santander)\b\.?/gi,
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
// Broader than the stripping patterns (substrings, glued words like "AMGFinanzierung").
|
|
30
|
+
const FINANCE_CLAIM = /finanz|\bfinz\b|zins|leasing|leasen|santander|darlehen|kredit|anzahlung|schlussrate|monatsrate|\bmtl\b|monatl|\braten?\b|\/\s*monat|pro\s+monat|im\s+monat/i;
|
|
31
|
+
|
|
32
|
+
const CUT = '\u0000';
|
|
33
|
+
const SEPARATORS = '[\\s/,;|+&\\-–]*';
|
|
34
|
+
const CUT_WITH_SEPARATORS = new RegExp(`${SEPARATORS}${CUT}(?:${SEPARATORS}${CUT})*${SEPARATORS}`, 'g');
|
|
35
|
+
|
|
36
|
+
// Only the word right before the number or right after the "%" counts ("Akku 95%", "99% SoH"), so
|
|
37
|
+
// "Elektromotor V 0,0%" is still a rate.
|
|
38
|
+
const isFinancePercent = (match: string, offset: number, text: string): boolean => {
|
|
39
|
+
const before = /([^\s/,;|]*)[\s/,;|]*$/.exec(text.slice(0, offset))[1];
|
|
40
|
+
const after = /^[\s/,;|]*([^\s/,;|]*)/.exec(text.slice(offset + match.length))[1];
|
|
41
|
+
return !NON_FINANCE_PERCENT_CONTEXT.test(before) && !NON_FINANCE_PERCENT_CONTEXT.test(after);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Finance tokens out, together with the separators around them ("SB 4,49%/AMG" -> "SB AMG").
|
|
45
|
+
export const stripFinanceClaims = (text: string): string => {
|
|
46
|
+
let out = text || '';
|
|
47
|
+
for (const pattern of FINANCE_TOKENS) {
|
|
48
|
+
out = out.replace(pattern, CUT);
|
|
49
|
+
}
|
|
50
|
+
out = out.replace(PERCENT, (match: string, offset: number, whole: string) => (isFinancePercent(match, offset, whole) ? CUT : match));
|
|
51
|
+
return out.includes(CUT) ? out.replace(CUT_WITH_SEPARATORS, ' ').replace(/\s+/g, ' ').trim() : text || '';
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const hasFinanceClaim = (text: string): boolean => {
|
|
55
|
+
const value = text || '';
|
|
56
|
+
if (FINANCE_CLAIM.test(value)) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
return [...value.matchAll(PERCENT)].some(match => isFinancePercent(match[0], match.index, value));
|
|
60
|
+
};
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
OpenAiAdsRow,
|
|
10
10
|
} from '../interfaces/openai-ads.interface';
|
|
11
11
|
import { OpenAiImageFields, encodeCommasInUrl } from './openai-image-fields';
|
|
12
|
+
import { hasFinanceClaim, stripFinanceClaims } from './openai-ads-finance';
|
|
12
13
|
|
|
13
14
|
// OpenAI product file limits (guidance): title ≤ 150, description ≤ 5,000 characters.
|
|
14
15
|
export const OPENAI_TITLE_MAX_LENGTH = 150;
|
|
@@ -51,7 +52,8 @@ export interface OpenAiAdsMappingContext {
|
|
|
51
52
|
export type OpenAiAdsBaseRow = Omit<OpenAiAdsRow, 'image_url' | 'additional_image_urls'>;
|
|
52
53
|
|
|
53
54
|
export type OpenAiAdsMapResult =
|
|
54
|
-
|
|
55
|
+
// financeTermsRemoved: the dealer variant text carried finance wording that was cut from the title
|
|
56
|
+
| { ok: true; row: OpenAiAdsBaseRow; outOfStockReason?: OpenAiAdsOutOfStockReason; financeTermsRemoved?: boolean }
|
|
55
57
|
| { ok: false; reason: OpenAiAdsExclusion };
|
|
56
58
|
|
|
57
59
|
/**
|
|
@@ -98,8 +100,19 @@ export const mapOpenAiAdsVehicle = (car: DataSchemes.Car, ctx: OpenAiAdsMappingC
|
|
|
98
100
|
is_ads_eligible: true,
|
|
99
101
|
};
|
|
100
102
|
|
|
103
|
+
// fail-closed: whatever finance wording survived the stripping keeps the car out of the feed
|
|
104
|
+
if (hasFinanceClaim(row.title) || hasFinanceClaim(row.description)) {
|
|
105
|
+
return { ok: false, reason: OpenAiAdsExclusion.financeClaim };
|
|
106
|
+
}
|
|
107
|
+
|
|
101
108
|
const outOfStockReason = OFFER_AVAILABILITY_OUT_OF_STOCK[toString(get(car, 'offer.offerAvailability', ''))];
|
|
102
|
-
|
|
109
|
+
const financeTermsRemoved = title !== buildOpenAiTitle(car, { keepFinanceTerms: true });
|
|
110
|
+
return {
|
|
111
|
+
ok: true,
|
|
112
|
+
row,
|
|
113
|
+
...(outOfStockReason ? { outOfStockReason } : {}),
|
|
114
|
+
...(financeTermsRemoved ? { financeTermsRemoved } : {}),
|
|
115
|
+
};
|
|
103
116
|
};
|
|
104
117
|
|
|
105
118
|
export const withOpenAiImages = (row: OpenAiAdsBaseRow, images: OpenAiImageFields): OpenAiAdsRow => ({
|
|
@@ -112,11 +125,22 @@ export const withOpenAiImages = (row: OpenAiAdsBaseRow, images: OpenAiImageField
|
|
|
112
125
|
// The same vehicle, switched off: kept in the file so OpenAI stops delivering it instead of
|
|
113
126
|
// relying on the row disappearing (not documented as a removal).
|
|
114
127
|
export const asOutOfStock = (row: OpenAiAdsRow): OpenAiAdsRow => ({
|
|
115
|
-
...row,
|
|
128
|
+
...withoutFinanceClaims(row),
|
|
116
129
|
availability: 'out_of_stock',
|
|
117
130
|
is_ads_eligible: false,
|
|
118
131
|
});
|
|
119
132
|
|
|
133
|
+
// Rows re-sent from the ledger may predate the finance rule: strip again, and if wording is still
|
|
134
|
+
// left, fall back to the brand alone (the row only switches the item off).
|
|
135
|
+
export const withoutFinanceClaims = <T extends { title: string; description: string; brand: string }>(row: T): T => {
|
|
136
|
+
const title = stripFinanceClaims(row.title);
|
|
137
|
+
const description = stripFinanceClaims(row.description);
|
|
138
|
+
if (!hasFinanceClaim(title) && !hasFinanceClaim(description)) {
|
|
139
|
+
return title === row.title && description === row.description ? row : { ...row, title, description };
|
|
140
|
+
}
|
|
141
|
+
return { ...row, title: row.brand, description: row.brand };
|
|
142
|
+
};
|
|
143
|
+
|
|
120
144
|
// Purchase price (price.currentSalesPrice, EUR gross) in major units with exactly two decimals and
|
|
121
145
|
// no thousands separator: 17990 -> "17990.00 EUR". Never a monthly financing/leasing rate.
|
|
122
146
|
// null = not a valid positive price; the car is not exported.
|
|
@@ -140,10 +164,12 @@ export const toOpenAiMinorUnits = (value: unknown): number | null => {
|
|
|
140
164
|
// "<make> <model> <variant>" as delivered (German market names), dealer junk punctuation removed
|
|
141
165
|
// with the same normalizer as the Meta feed, cut at a word boundary. No HTML escaping: CSV quoting
|
|
142
166
|
// handles every character. A placeholder model (SONSTIGE) is left out ("MG 1.5 Hybrid Luxury");
|
|
143
|
-
// with neither a real model nor a variant there is no title and the car is not sent.
|
|
144
|
-
|
|
167
|
+
// with neither a real model nor a variant there is no title and the car is not sent. Finance wording
|
|
168
|
+
// in the dealer text ("4,49%Finz.", "0% Zins") is cut first (openai-ads-finance.ts).
|
|
169
|
+
export const buildOpenAiTitle = (car: DataSchemes.Car, options: { keepFinanceTerms?: boolean } = {}): string => {
|
|
145
170
|
const [make, model, subModel] = ['mainData.make', 'mainData.model', 'mainData.subModel']
|
|
146
|
-
.map(path => repairMojibake(trim(toString(get(car, path, '')))))
|
|
171
|
+
.map(path => repairMojibake(trim(toString(get(car, path, '')))))
|
|
172
|
+
.map(part => (options.keepFinanceTerms ? part : stripFinanceClaims(part)));
|
|
147
173
|
const realModel = PLACEHOLDER_NAMES.test(model) ? '' : model;
|
|
148
174
|
if (!normalizeFeedText(`${realModel} ${subModel}`)) {
|
|
149
175
|
return '';
|
|
@@ -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
|
+
}
|