@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.
@@ -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 first;
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 (to be created by an admin — not done)
37
+ ## Cronicle event
38
38
 
39
- - Plugin "Export cars", chained after "1.0 Base export" (same as the Google/Meta exports), daily.
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.32",
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
- | { ok: true; row: OpenAiAdsBaseRow; outOfStockReason?: OpenAiAdsOutOfStockReason }
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
- return outOfStockReason ? { ok: true, row, outOfStockReason } : { ok: true, row };
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
- export const buildOpenAiTitle = (car: DataSchemes.Car): string => {
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
+ }