@bytebrand/fe-ui-core-autobahn 1.0.53 → 1.0.55

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.
@@ -1,89 +0,0 @@
1
- import { AutobahnCar } from '../cards/types';
2
- import { Co2Class } from '../constants/autobahn';
3
- /**
4
- * Last-resort placeholder shown in the carousel until the real images for a car
5
- * are loaded. The SRP container SHOULD inject the app's real no-image asset via
6
- * `deps.placeholder` (fe-main exposes PLACEHOLDER_IMAGE_LARGE_URL /
7
- * PLACEHOLDER_IMAGE_SMALL_URL in framework/constants/app.ts, both derived from
8
- * config.imagesAPI.imgUrlBase). We do NOT import that here to keep the mapper
9
- * free of app-config coupling; this empty-data-URI default just guarantees a
10
- * non-broken <img> in tests / before the container wires the real one.
11
- */
12
- export declare const PLACEHOLDER_IMAGE = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
13
- /**
14
- * Dependencies the SRP container injects. Everything here is what the mapper
15
- * needs from the outside world — keeping the function itself pure.
16
- */
17
- export interface ToAutobahnCarDeps {
18
- /**
19
- * Build a ready-to-use image URL for a car image.
20
- * Pass `imagesStore.getSupportedImageFormat` straight through (it already
21
- * picks webp/jpeg and the cached size). Signature matches ImagesStore:
22
- * (vehicleID, imageIdentifier, size) => url
23
- */
24
- getImageUrl?: (vehicleID: string, imageIdentifier: string, size: 'small' | 'medium' | 'large') => string;
25
- /**
26
- * Loaded gallery images for this car from `imagesStore.images.get(id)`
27
- * (array of { imageUrlSmall, imageUrlMedium, imageUrlLarge, imageUrl, ... }).
28
- * When present we use these real URLs; otherwise we fall back to the single
29
- * main image via getImageUrl, then to the placeholder. Never faked.
30
- */
31
- loadedImages?: Array<{
32
- imageUrlSmall?: string;
33
- imageUrlMedium?: string;
34
- imageUrlLarge?: string;
35
- imageUrl?: string;
36
- }>;
37
- /** image size used when building gallery URLs from the main image id. */
38
- imageSize?: 'small' | 'medium' | 'large';
39
- /** translator for cbd:* selector codes (fuel/gearbox). Falls back to a built-in de map, then the raw code. */
40
- t?: (key: string, options?: object) => string;
41
- /** active UI language ('de' | 'en' …). Drives de-DE vs en-IN number grouping for the EnVKV line. */
42
- language?: string;
43
- /** placeholder image URL override (defaults to PLACEHOLDER_IMAGE). */
44
- placeholder?: string;
45
- /**
46
- * Buyer-activity stats for this car — UserActionsAPI 'stats/viewers' response, loaded async and
47
- * keyed by carId (`searchStore.statsData.get(storeCar.id)`). Pass it straight through; absent
48
- * (not loaded yet, or no stats for this car) → viewsCount/favoritesCount stay undefined and the
49
- * gallery badge row simply omits those pills. Never fabricated.
50
- */
51
- stats?: {
52
- totalCarImpCount?: number;
53
- totalFavCount?: number;
54
- };
55
- }
56
- /** translate a cbd:* selector code → human label. Exported: also used by the Meta AIA
57
- * marketing-image renderer (plain Node backend, no i18next — relies on the built-in de fallback). */
58
- export declare function cbd(code: any, t?: (key: string, options?: object) => string): string;
59
- /**
60
- * Map the store's energy-efficiency-class selector code (or a bare letter) to a
61
- * single CO2_CLASSES letter ('A'…'G'). A+/A++/A+++ collapse to 'A' so the card's
62
- * colored InlineCls (which does CO2_CLASSES.indexOf) still resolves a band color.
63
- * Returns '' when there is no class (caller decides what to render).
64
- */
65
- export declare function co2ClassLetter(code: any): Co2Class | '';
66
- export declare function mapRating(currentSalesPrice?: number, predicted?: number): {
67
- rating: string;
68
- ratingLabel: string;
69
- };
70
- /**
71
- * Map the 5-tier rating to a 2-tone signal — only 'top'/'good' read as a genuinely good deal
72
- * (green); 'fair'/'raised'/'high' all get the warn/peach tone (Figma: "Erhöhter-Preis" is
73
- * peach, not green). Pure/non-React on purpose: shared by the web card's StatChip (via
74
- * cards/cardParts.tsx's re-export) AND the Meta AIA marketing-image renderer (a plain Node
75
- * service that can't import React components).
76
- */
77
- export declare function ratingTone(rating: string): 'good' | 'warn';
78
- /**
79
- * Convert one fe-main store car → AutobahnCar.
80
- *
81
- * @param storeCar the object from SearchResultsStore.getCarsForPage()
82
- * (shape = transformDataForVehicles output). `any` because the store car is
83
- * itself untyped (`getCarsForPage: (page) => any[]`).
84
- * @param deps injected services/locale (see ToAutobahnCarDeps). Optional so the
85
- * mapper still runs in tests with zero wiring (gallery → placeholder, labels →
86
- * built-in de fallback).
87
- */
88
- export declare function toAutobahnCar(storeCar: any, deps?: ToAutobahnCarDeps): AutobahnCar;
89
- export default toAutobahnCar;
@@ -1,421 +0,0 @@
1
- "use strict";
2
- // Autobahn (auto.de redesign) — store-car → AutobahnCar mapper (AB-P0-6).
3
- //
4
- // PURE adapter: converts the existing fe-main "store car" (the object
5
- // `SearchResultsStore.getCarsForPage()` returns to the SRP, produced by
6
- // `transformDataForVehicles` in framework/DataTransformers.ts) into the
7
- // `AutobahnCar` view-model the new Autobahn cards consume
8
- // (components/autobahn/cards/types.ts).
9
- //
10
- // WHY a pure function with injected deps:
11
- // The Autobahn cards are pure/presentational and never touch the store. This
12
- // mapper is the single seam that reads the real payload. It is kept pure (no
13
- // store singletons imported here) so it is unit-testable and the SRP container
14
- // injects the real stores/services (imagesStore.getSupportedImageFormat,
15
- // appStore.language). Everything it needs arrives through `deps`.
16
- //
17
- // NEEDS-DATA discipline (the point of the ticket):
18
- // Fields with NO real source on the search payload are left `undefined` so the
19
- // cards render their 'Daten folgt' marker instead of a fabricated number. See
20
- // the per-field rationale in ./fieldMapping.ts. We NEVER synthesize:
21
- // drop, marketLow, marketAvg, marketHigh, watchers, views.
22
- // listPrice / savings are emitted ONLY if a real strike/list price exists.
23
- //
24
- // Every field path below was confirmed against the real code (file:line in
25
- // ./fieldMapping.ts), not guessed.
26
- Object.defineProperty(exports, "__esModule", { value: true });
27
- exports.PLACEHOLDER_IMAGE = void 0;
28
- exports.cbd = cbd;
29
- exports.co2ClassLetter = co2ClassLetter;
30
- exports.mapRating = mapRating;
31
- exports.ratingTone = ratingTone;
32
- exports.toAutobahnCar = toAutobahnCar;
33
- const priceRating_1 = require("../utils/priceRating");
34
- const DateUtils_1 = require("../utils/DateUtils");
35
- const power_1 = require("../utils/power");
36
- const autobahn_1 = require("../constants/autobahn");
37
- /**
38
- * Last-resort placeholder shown in the carousel until the real images for a car
39
- * are loaded. The SRP container SHOULD inject the app's real no-image asset via
40
- * `deps.placeholder` (fe-main exposes PLACEHOLDER_IMAGE_LARGE_URL /
41
- * PLACEHOLDER_IMAGE_SMALL_URL in framework/constants/app.ts, both derived from
42
- * config.imagesAPI.imgUrlBase). We do NOT import that here to keep the mapper
43
- * free of app-config coupling; this empty-data-URI default just guarantees a
44
- * non-broken <img> in tests / before the container wires the real one.
45
- */
46
- exports.PLACEHOLDER_IMAGE = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
47
- // ---------------------------------------------------------------------------
48
- // small pure helpers
49
- // ---------------------------------------------------------------------------
50
- const DEFAULT_LANG = 'de';
51
- /** locale tag for Number#toLocaleString — de-DE (comma decimals) unless en. */
52
- function localeTag(language) {
53
- return language === 'en' ? 'en-IN' : 'de-DE';
54
- }
55
- /** finite-number coercion: accepts number | numeric string, else undefined. */
56
- function num(v) {
57
- if (v == null)
58
- return undefined;
59
- const n = typeof v === 'number' ? v : Number.parseFloat(String(v));
60
- return Number.isFinite(n) ? n : undefined;
61
- }
62
- /**
63
- * Minimal de-DE fallback for the handful of cbd:* selector codes the cards show
64
- * (fuel + gearbox). Used only when no `t` is injected (e.g. in unit tests).
65
- * The full set lives in the cbd locale dictionary (@bytebrand/i18n-dictionaries).
66
- */
67
- const CBD_DE_FALLBACK = {
68
- selector_fuel_petrol: 'Benzin',
69
- selector_fuel_diesel: 'Diesel',
70
- selector_fuel_electrical: 'Elektro',
71
- selector_fuel_ethanol: 'Ethanol',
72
- selector_fuel_hybridElectricalDiesel: 'Hybrid (Elektro / Diesel)',
73
- selector_fuel_hybridElectricalPetrol: 'Hybrid (Elektro / Benzin)',
74
- selector_fuel_hydrogen: 'Wasserstoff',
75
- selector_fuel_naturalGas: 'Erdgas',
76
- selector_gearbox_automatic: 'Automatik',
77
- selector_gearbox_manualShift: 'Manuell',
78
- selector_gearbox_semiAutomatic: 'Halbautomatik',
79
- // highlight-chip keys (DataTransformers highlights) — used when no `t` is injected
80
- multimedia_navigationSystem: 'Navigationssystem',
81
- lights_ledHeadlights: 'LED-Scheinwerfer',
82
- lights_xenonHeadlights: 'Xenon-Scheinwerfer',
83
- window_panoramicGlassRoof: 'Panoramadach',
84
- assistants_headUpDisplay: 'Head-up-Display',
85
- assistants_adaptiveCruiseControl: 'Abstandstempomat',
86
- search_seatHeating: 'Sitzheizung',
87
- selector_climatisation_automaticAirCondition: 'Klimaautomatik',
88
- selector_climatisation_airCondition: 'Klimaanlage',
89
- search_parkingAssistants: 'Einparkhilfe',
90
- selector_driveType_allWheelDrive: 'Allradantrieb',
91
- search_trailerHitch: 'Anhängerkupplung',
92
- assistants_cruiseControl: 'Tempomat',
93
- assistants_trafficSignRecognition: 'Verkehrszeichenerkennung',
94
- assistants_collisionAvoidance: 'Notbremsassistent',
95
- window_sunroof: 'Schiebedach',
96
- seats_electricAdjustable: 'Elektr. Sitze',
97
- exterior_metallic: 'Metallic-Lackierung',
98
- multimedia_handsFreePhoneSystem: 'Freisprecheinrichtung',
99
- heatingCooling_auxiliaryHeating: 'Standheizung',
100
- multimedia_bluetooth: 'Bluetooth',
101
- };
102
- /** translate a cbd:* selector code → human label. Exported: also used by the Meta AIA
103
- * marketing-image renderer (plain Node backend, no i18next — relies on the built-in de fallback). */
104
- function cbd(code, t) {
105
- if (code == null || code === 'N/A')
106
- return '';
107
- const key = String(code);
108
- if (t) {
109
- const out = t(`cbd:${key}`);
110
- // i18next returns the key back when missing; treat that as "no translation".
111
- if (out && out !== `cbd:${key}` && out !== key)
112
- return out;
113
- }
114
- return CBD_DE_FALLBACK[key] || key;
115
- }
116
- /**
117
- * Map the store's energy-efficiency-class selector code (or a bare letter) to a
118
- * single CO2_CLASSES letter ('A'…'G'). A+/A++/A+++ collapse to 'A' so the card's
119
- * colored InlineCls (which does CO2_CLASSES.indexOf) still resolves a band color.
120
- * Returns '' when there is no class (caller decides what to render).
121
- */
122
- function co2ClassLetter(code) {
123
- if (code == null || code === 'N/A')
124
- return '';
125
- const raw = String(code);
126
- // selector_energyEfficiencyClass_appp → 'a', _ap → 'a', _b → 'b', …
127
- const m = raw.match(/selector_energyEfficiencyClass_(a|b|c|d|e|f|g)/i);
128
- const letter = (m ? m[1] : raw.replace(/\+/g, '').trim().charAt(0)).toUpperCase();
129
- return autobahn_1.CO2_CLASSES.includes(letter) ? letter : '';
130
- }
131
- /**
132
- * Real Preisbewertung → AutobahnCar rating tone + German label.
133
- *
134
- * Source of truth (confirmed against the live SRP card,
135
- * VehicleSmallCard/VehicleInfo.tsx:160): the legacy card renders
136
- * getPriceRating(price.currentSalesPrice, price.mlCurrentSalesPricePredicted)
137
- * which returns a numeric tier using priceRatings:
138
- * 0 noRating · 1 highPrice · 2 increasedPrice · 3 fairPrice · 4 goodPrice · 5 topPrice
139
- * (constants/price.ts in this package).
140
- *
141
- * We map that real number onto the Autobahn 5-tier scale (TIER_LABELS,
142
- * components/autobahn/constants.ts), where index 0=Top … 4=Hoch:
143
- * topPrice(5)→0 'Top Preis' · goodPrice(4)→1 'Guter Preis' ·
144
- * fairPrice(3)→2 'Fairer Preis' · increasedPrice(2)→3 'Erhöht' ·
145
- * highPrice(1)→4 'Hoher Preis'.
146
- * noRating(0) has no real comparison → neutral 'Fairer Preis' (tier 2) WITHOUT a
147
- * flattering label (documented in fieldMapping.ts). The card only special-cases
148
- * the 'fair' tone (warn pill); every other tone is the good/green pill.
149
- *
150
- * AutobahnCar.rating is a free string; CarCard does `rating === 'fair' ? warn : good`.
151
- * We emit the autobahn tier key ('top'|'good'|'fair'|'raised'|'high') so the
152
- * neutral 'fair' maps to the warn pill exactly like the design intends.
153
- */
154
- const LEGACY_TO_TIER = {
155
- 5: 0, // topPrice → Top Preis
156
- 4: 1, // goodPrice → Guter Preis
157
- 3: 2, // fairPrice → Fairer Preis
158
- 2: 3, // increasedPrice → Erhöht
159
- 1: 4, // highPrice → Hoher Preis
160
- };
161
- const TIER_TONE = {
162
- 0: 'top',
163
- 1: 'good',
164
- 2: 'fair',
165
- 3: 'raised',
166
- 4: 'high',
167
- };
168
- function mapRating(currentSalesPrice, predicted) {
169
- var _a;
170
- const legacy = (0, priceRating_1.getPriceRating)(currentSalesPrice, predicted); // 0..5
171
- // 0 = noRating: keep neutral (tier 2) — never invent a flattering rating.
172
- const tier = legacy === 0 ? 2 : (_a = LEGACY_TO_TIER[legacy]) !== null && _a !== void 0 ? _a : 2;
173
- return { rating: TIER_TONE[tier], ratingLabel: autobahn_1.TIER_LABELS[tier] };
174
- }
175
- /**
176
- * Map the 5-tier rating to a 2-tone signal — only 'top'/'good' read as a genuinely good deal
177
- * (green); 'fair'/'raised'/'high' all get the warn/peach tone (Figma: "Erhöhter-Preis" is
178
- * peach, not green). Pure/non-React on purpose: shared by the web card's StatChip (via
179
- * cards/cardParts.tsx's re-export) AND the Meta AIA marketing-image renderer (a plain Node
180
- * service that can't import React components).
181
- */
182
- function ratingTone(rating) {
183
- return rating === 'top' || rating === 'good' ? 'good' : 'warn';
184
- }
185
- /** Is this car a plug-in hybrid? Determined by fuel type, mirroring ADV-88 in VehicleInfo.tsx. */
186
- function isPluginHybrid(fuel) {
187
- return fuel === 'selector_fuel_hybridElectricalDiesel' || fuel === 'selector_fuel_hybridElectricalPetrol';
188
- }
189
- /** Build the gallery URL list from real images, falling back to main image, then placeholder. */
190
- function buildGallery(storeCar, deps) {
191
- const placeholder = deps.placeholder || exports.PLACEHOLDER_IMAGE;
192
- // Only 'small' and 'large' variants are pre-generated by image-generator; 'medium' has no cached file.
193
- const size = deps.imageSize || 'small';
194
- // 1) Real loaded images from imagesStore.images.get(id).
195
- if (Array.isArray(deps.loadedImages) && deps.loadedImages.length) {
196
- const urls = deps.loadedImages
197
- .map((img) => size === 'large'
198
- ? img.imageUrlLarge || img.imageUrlMedium || img.imageUrl
199
- : size === 'small'
200
- ? img.imageUrlSmall || img.imageUrlMedium || img.imageUrl
201
- : img.imageUrlMedium || img.imageUrlLarge || img.imageUrl)
202
- .filter((u) => !!u);
203
- if (urls.length)
204
- return urls;
205
- }
206
- // 2) Not loaded yet, but we have a main image id + a URL builder → single real image.
207
- const id = storeCar.id;
208
- const mainImageId = storeCar.mainImageId;
209
- const hasImages = num(storeCar.imagesCount);
210
- if (deps.getImageUrl && id && mainImageId && hasImages) {
211
- const url = deps.getImageUrl(id, mainImageId, size);
212
- if (url)
213
- return [url];
214
- }
215
- // 3) Nothing real yet — single placeholder (never a fabricated photo).
216
- return [placeholder];
217
- }
218
- /** Build the optional PHEV EnVKV sub-block when the car is a plug-in hybrid and weighted data exists. */
219
- function buildPhev(storeCar, language) {
220
- var _a, _b, _c;
221
- const c = storeCar.consumption || {};
222
- const e = storeCar.environmentEmissions || {};
223
- if (!isPluginHybrid(c.fuel))
224
- return undefined;
225
- const consEl = (_a = num(c.wltpWeightedPowerCombined)) !== null && _a !== void 0 ? _a : num(c.consumptionPowerCombined);
226
- const consFuel = num(c.wltpWeightedCombined);
227
- const consDis = (_b = num(c.wltpCombined)) !== null && _b !== void 0 ? _b : num(c.consumptionCombined);
228
- const co2w = (_c = num(e.wltpCo2)) !== null && _c !== void 0 ? _c : num(e.co2);
229
- const clsW = co2ClassLetter(e.wltpEnergyEfficiencyClass);
230
- const clsD = co2ClassLetter(e.wltpDischargedEnergyEfficiencyClass);
231
- // Only emit the dual PHEV line when we actually have the weighted figures the
232
- // EcoLine PHEV branch needs; otherwise let the normal single line render.
233
- if (consEl == null || consFuel == null || co2w == null)
234
- return undefined;
235
- return {
236
- consEl: `${consEl.toLocaleString(localeTag(language))} kWh/100 km`,
237
- consFuel: `${consFuel.toLocaleString(localeTag(language))} l/100 km`,
238
- consDis: consDis != null ? `${consDis.toLocaleString(localeTag(language))} l/100 km` : '–',
239
- co2w: `${co2w.toLocaleString(localeTag(language))} g/km`,
240
- co2classW: clsW || '–',
241
- co2classD: clsD || clsW || '–',
242
- };
243
- }
244
- // ---------------------------------------------------------------------------
245
- // main mapper
246
- // ---------------------------------------------------------------------------
247
- /**
248
- * Convert one fe-main store car → AutobahnCar.
249
- *
250
- * @param storeCar the object from SearchResultsStore.getCarsForPage()
251
- * (shape = transformDataForVehicles output). `any` because the store car is
252
- * itself untyped (`getCarsForPage: (page) => any[]`).
253
- * @param deps injected services/locale (see ToAutobahnCarDeps). Optional so the
254
- * mapper still runs in tests with zero wiring (gallery → placeholder, labels →
255
- * built-in de fallback).
256
- */
257
- function toAutobahnCar(storeCar, deps = {}) {
258
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z;
259
- const language = deps.language || DEFAULT_LANG;
260
- const t = deps.t;
261
- const title = storeCar.title || {};
262
- const info = storeCar.info || {};
263
- const engineData = storeCar.engineData || {};
264
- const consumption = storeCar.consumption || {};
265
- const emissions = storeCar.environmentEmissions || {};
266
- const price = storeCar.price || {};
267
- // --- price / monthly (REQUIRED) ---
268
- // monthly ← price.financing.monthlyInstallment. This is the financing rate
269
- // produced by FinancingStore via getComputedFinancing(config).financing
270
- // (getFinancing.ts:82 `monthlyInstallment`), spread into price.financing by
271
- // transformDataForVehicles (DataTransformers.ts:345). It is the LEAD price.
272
- const monthly = (_b = num((_a = price === null || price === void 0 ? void 0 : price.financing) === null || _a === void 0 ? void 0 : _a.monthlyInstallment)) !== null && _b !== void 0 ? _b : 0;
273
- // financing terms the monthly rate was computed with — same merged per-car
274
- // config (dealer rate + the user's saved localStorage adjustments, ADV-157).
275
- // Emitted only when real; the cards fall back to the default-config terms.
276
- // paybackPeriod FIRST: it carries the user's saved term, while rawPaybackPeriod
277
- // is stamped from the dealer rate at normalize time and never reflects
278
- // adjustments (normalizeRateObject.ts `rawPaybackPeriod: paybackPeriod`).
279
- const finFirstInstallment = num((_c = price === null || price === void 0 ? void 0 : price.financing) === null || _c === void 0 ? void 0 : _c.firstInstallment);
280
- const finMonths = (_e = num((_d = price === null || price === void 0 ? void 0 : price.financing) === null || _d === void 0 ? void 0 : _d.paybackPeriod)) !== null && _e !== void 0 ? _e : num((_f = price === null || price === void 0 ? void 0 : price.financing) === null || _f === void 0 ? void 0 : _f.rawPaybackPeriod);
281
- // price ← price.currentSalesPrice (DataTransformers.ts:352).
282
- const purchase = (_g = num(price === null || price === void 0 ? void 0 : price.currentSalesPrice)) !== null && _g !== void 0 ? _g : 0;
283
- // --- rating (REQUIRED-WHEN-PRICED) ← real Preisbewertung ---
284
- // BL-08 (P1, MONEY): getPriceRating(0, 0) mathematically resolves to tier 5
285
- // ("Top Preis" — the BEST possible rating). Its internal min/max bands are all
286
- // `predicted * (1 ± pct)`, which collapse to 0 when predicted is 0, and the
287
- // topPrice branch checks `currentSalesPrice <= min` — 0 <= 0 is true. A
288
- // missing/broken price payload must NEVER get dressed up as the best deal on
289
- // the lot. Only compute/emit a rating when there is a real, positive sales
290
- // price; otherwise rating/ratingLabel stay '' (falsy) — every consumer
291
- // (CarCard.tsx, cardParts.tsx, FinanceCalculator.tsx) already treats a falsy
292
- // rating as "no badge", so no type change is needed here.
293
- const predicted = purchase > 0 ? (0, priceRating_1.getPredictablePrice)(purchase, num(price === null || price === void 0 ? void 0 : price.mlCurrentSalesPricePredicted)) : undefined;
294
- const { rating, ratingLabel } = purchase > 0 ? mapRating(purchase, predicted) : { rating: '', ratingLabel: '' };
295
- // --- key specs ---
296
- const powerKW = num(engineData.powerKW);
297
- const powerPS = num(engineData.powerPS);
298
- const power = (0, power_1.getPowerLabel)(powerKW, powerPS); // "150 kW (204 PS)"
299
- const regTs = num(info.regDate);
300
- const reg = regTs ? (0, DateUtils_1.formatTimestamp)({ timestamp: regTs, format: 'MM/YYYY' }) : '';
301
- const ownersN = num(info.numberOfPreviousOwners);
302
- // --- EnVKV consumption line (REQUIRED on every German car ad) ---
303
- // Prefer WLTP figures when present (the legacy cards do too), else NEDC.
304
- const consVal = (_h = num(consumption.wltpCombined)) !== null && _h !== void 0 ? _h : num(consumption.consumptionCombined);
305
- const co2Val = (_j = num(emissions.wltpCo2)) !== null && _j !== void 0 ? _j : num(emissions.co2);
306
- const cons = consVal != null ? `${consVal.toLocaleString(localeTag(language))} l/100 km` : '';
307
- const co2 = co2Val != null ? `${co2Val.toLocaleString(localeTag(language))} g/km` : '';
308
- const co2class = co2ClassLetter(emissions.wltpEnergyEfficiencyClass);
309
- const phev = buildPhev(storeCar, language);
310
- // --- media ---
311
- const gallery = buildGallery(storeCar, deps);
312
- const photos = (_k = num(storeCar.imagesCount)) !== null && _k !== void 0 ? _k : 0;
313
- // --- optional: leasing rate — ONLY when leasing is actually active for this car.
314
- // SearchResultsStore.getCarsForPage stamps price.leasing.isActive via getLeasingActive
315
- // (car age + mileage within the leasing limits). Legacy SRP shows "Leasing ---" when
316
- // inactive; we mirror that by leaving `lease` undefined so PricePaths omits the row
317
- // (instead of computing/showing a leasing rate the car isn't eligible for).
318
- const leasingActive = !!(price && price.leasing && price.leasing.isActive);
319
- const lease = leasingActive ? num((_l = price === null || price === void 0 ? void 0 : price.leasing) === null || _l === void 0 ? void 0 : _l.monthlyInstallment) : undefined;
320
- // leasing terms the lease rate was computed with — same merged per-car config
321
- // as the financing terms above (annualMileage/paybackPeriod carry the user's
322
- // saved adjustments). Only meaningful alongside an active lease rate.
323
- const leaseMileage = leasingActive ? num((_m = price === null || price === void 0 ? void 0 : price.leasing) === null || _m === void 0 ? void 0 : _m.annualMileage) : undefined;
324
- const leaseMonths = leasingActive ? num((_o = price === null || price === void 0 ? void 0 : price.leasing) === null || _o === void 0 ? void 0 : _o.paybackPeriod) : undefined;
325
- // --- optional: list price / savings — ONLY from a real strike price ---
326
- // transformDataForVehicles exposes oldMonthlyInstallment / highestPrice via
327
- // the financing spread, and price.historyPriceDifference. There is NO reliable
328
- // purchase-price strike on the search payload today, so we leave listPrice /
329
- // savings UNDEFINED unless a real highestPrice strike exists above the sale
330
- // price. See fieldMapping.ts — flagged for backend confirmation.
331
- const isStrikeShown = !!((_p = price === null || price === void 0 ? void 0 : price.financing) === null || _p === void 0 ? void 0 : _p.isStrikeShown);
332
- const highest = (_r = num((_q = price === null || price === void 0 ? void 0 : price.financing) === null || _q === void 0 ? void 0 : _q.highestPrice)) !== null && _r !== void 0 ? _r : num(price === null || price === void 0 ? void 0 : price.highestPrice);
333
- let listPrice;
334
- let savings;
335
- if (isStrikeShown && highest != null && purchase > 0 && highest > purchase) {
336
- listPrice = highest;
337
- savings = highest - purchase;
338
- }
339
- // --- struck-through old rate per payment path — same isStrikeShown gate as the Kaufen
340
- // strike above. getFinancing.ts/getLeasing.ts each compute their own real
341
- // oldMonthlyInstallment from the same common.highestPrice; only emitted when the tabbed
342
- // hero price actually has something real to strike against.
343
- const oldMonthly = isStrikeShown ? num((_s = price === null || price === void 0 ? void 0 : price.financing) === null || _s === void 0 ? void 0 : _s.oldMonthlyInstallment) : undefined;
344
- const oldLease = isStrikeShown && leasingActive ? num((_t = price === null || price === void 0 ? void 0 : price.leasing) === null || _t === void 0 ? void 0 : _t.oldMonthlyInstallment) : undefined;
345
- // --- price drop (REAL) — mirrors the legacy SRL green "−X €" badge: show the reduction only
346
- // when price.historyPriceDifference is a meaningful amount (≥ 500 €, the legacy threshold).
347
- const histDrop = num(price === null || price === void 0 ? void 0 : price.historyPriceDifference);
348
- const drop = histDrop != null && histDrop >= 500 ? histDrop : undefined;
349
- // percent reduction (historyPriceDifferencePerCent) — paired with `drop` for the −X% badge.
350
- const histPct = num(price === null || price === void 0 ? void 0 : price.historyPriceDifferencePerCent);
351
- const dropPct = drop != null && histPct != null && histPct > 0 ? Math.round(histPct) : undefined;
352
- // --- NEU badge (REAL) — the legacy showNewLabel flag carried on the store car. ---
353
- const isNew = !!storeCar.showNewLabel;
354
- // --- availability (REAL) — offer.availabilityMode. 'always' → immediately available. ---
355
- const offer = storeCar.offer && typeof storeCar.offer === 'object' ? storeCar.offer : {};
356
- const availability = offer.availabilityMode === 'selector_availabilityMode_always'
357
- ? t
358
- ? t('autobahn:availability.immediately')
359
- : 'Sofort verfügbar'
360
- : undefined;
361
- // 0 € Anzahlung available — transform sets offer.showDownPayment = (raw showDownPayment && zero first installment).
362
- const zeroDown = !!offer.showDownPayment;
363
- // --- gallery activity strip (REAL) — UserActionsAPI 'stats/viewers', injected via deps.stats. ---
364
- const viewsCount = num((_u = deps.stats) === null || _u === void 0 ? void 0 : _u.totalCarImpCount);
365
- const favoritesCount = num((_v = deps.stats) === null || _v === void 0 ? void 0 : _v.totalFavCount);
366
- const carView = {
367
- // identity / headline
368
- make: String((_w = title.make) !== null && _w !== void 0 ? _w : ''),
369
- model: String((_x = title.model) !== null && _x !== void 0 ? _x : ''),
370
- trim: String((_y = title.subModel) !== null && _y !== void 0 ? _y : ''),
371
- // key specs
372
- km: (_z = num(info.mileage)) !== null && _z !== void 0 ? _z : 0,
373
- reg,
374
- power,
375
- fuel: cbd(consumption.fuel, t),
376
- gear: cbd(info.gearbox, t),
377
- owners: ownersN !== null && ownersN !== void 0 ? ownersN : 0,
378
- // price
379
- monthly,
380
- ...(finFirstInstallment != null ? { finFirstInstallment } : {}),
381
- ...(finMonths != null ? { finMonths } : {}),
382
- price: purchase,
383
- // media
384
- gallery,
385
- photos,
386
- // EnVKV
387
- cons,
388
- co2,
389
- co2class: co2class || '',
390
- ...(phev ? { phev } : {}),
391
- // trust / features — equipment highlight chips (Matrix-LED, Virtual Cockpit, AHK, ACC, …).
392
- // transformDataForVehicles extracted the present highlight selector-keys via getActualHighlights
393
- // (reading the raw equipment categories); translate them to labels here (cbd → t('cbd:<key>')).
394
- // First 4, like the design's card. Empty when the car has no notable equipment.
395
- features: (Array.isArray(storeCar.highlights) ? storeCar.highlights : [])
396
- .map((k) => cbd(k, t))
397
- .filter((s) => !!s)
398
- .slice(0, 4),
399
- rating,
400
- ratingLabel,
401
- // optional (only when real)
402
- ...(listPrice != null ? { listPrice } : {}),
403
- ...(savings != null ? { savings } : {}),
404
- ...(oldMonthly != null && oldMonthly > monthly ? { oldMonthly } : {}),
405
- ...(oldLease != null && lease != null && oldLease > lease ? { oldLease } : {}),
406
- ...(lease != null ? { lease } : {}),
407
- ...(leaseMileage != null ? { leaseMileage } : {}),
408
- ...(leaseMonths != null ? { leaseMonths } : {}),
409
- ...(drop != null ? { drop } : {}),
410
- ...(dropPct != null ? { dropPct } : {}),
411
- ...(zeroDown ? { zeroDown } : {}),
412
- ...(isNew ? { isNew } : {}),
413
- ...(availability ? { availability } : {}),
414
- ...(viewsCount != null ? { viewsCount } : {}),
415
- ...(favoritesCount != null ? { favoritesCount } : {}),
416
- // NEEDS-DATA: deliberately omitted (marketLow, marketAvg, marketHigh, watchers, views).
417
- // The cards render 'Daten folgt' for these until a real backend exists.
418
- };
419
- return carView;
420
- }
421
- exports.default = toAutobahnCar;
@@ -1,25 +0,0 @@
1
- {
2
- "//": "Compiles ONLY the pure, asset-require-free leaf modules a plain-Node backend can safely import (currently: image-generator's Meta AIA renderer). Emits .js/.d.ts NEXT TO each .ts source (no outDir/rootDir — setting outDir to '.' makes tsc auto-exclude the whole package as its own output dir). Frontend (webpack) consumers are unaffected: resolve.extensions puts .ts/.tsx before .js. Run `npm run build:node-safe` before publishing. Do NOT add a file here unless you've confirmed its full transitive import closure never reaches a .tsx file or a webpack-only asset require() (see constants/autobahn.ts's header comment for why that matters).",
3
- "compilerOptions": {
4
- "target": "es2019",
5
- "module": "commonjs",
6
- "moduleResolution": "node",
7
- "esModuleInterop": true,
8
- "declaration": true,
9
- "skipLibCheck": true,
10
- "strict": false,
11
- "noEmitOnError": true
12
- },
13
- "include": [
14
- "data/toAutobahnCar.ts",
15
- "utils/priceRating.ts",
16
- "utils/DateUtils.ts",
17
- "utils/power.ts",
18
- "utils/financeFormat.ts",
19
- "cards/types.ts",
20
- "constants/autobahn.ts",
21
- "constants/price.ts",
22
- "constants/core.ts",
23
- "constants/selectors.ts"
24
- ]
25
- }
@@ -1,21 +0,0 @@
1
- import { MomentInput } from 'moment';
2
- export interface IFormatTimestamp {
3
- timestamp?: MomentInput;
4
- format?: string;
5
- parseFormat?: string;
6
- }
7
- export declare const timestampToDate: (timestamp: number) => string;
8
- export declare const formatTimestamp: ({ timestamp, format, parseFormat }: IFormatTimestamp) => string;
9
- export declare const getSearchYears: () => any[];
10
- export declare const getCurrentYear: () => number;
11
- export declare const getMonths: () => string[];
12
- export declare function isDateInDaysPeriod(timestamp: number, days: number): boolean;
13
- export declare const handleMaxDate: () => string;
14
- export declare const handleMaxDateForPicker: () => string;
15
- export declare const _minDate: Date;
16
- export declare const сheckingMinDateIsCorrect: (value: any) => boolean;
17
- export declare const сheckingMaxDateIsCorrect: (value: any) => boolean;
18
- export declare const checkingWhenLess18Age: (value: any) => boolean;
19
- export declare const getYears: (minYear?: number, maxYear?: number, reverse?: boolean) => string[];
20
- export declare const createDateAsUTC: (date: Date) => Date;
21
- export declare const fromUTCToDateString: (UTCDate: Date) => string;
@@ -1,97 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.fromUTCToDateString = exports.createDateAsUTC = exports.getYears = exports.checkingWhenLess18Age = exports.сheckingMaxDateIsCorrect = exports.сheckingMinDateIsCorrect = exports._minDate = exports.handleMaxDateForPicker = exports.handleMaxDate = exports.getMonths = exports.getCurrentYear = exports.getSearchYears = exports.formatTimestamp = exports.timestampToDate = void 0;
7
- exports.isDateInDaysPeriod = isDateInDaysPeriod;
8
- const moment_1 = __importDefault(require("moment"));
9
- const timestampToDate = (timestamp) => {
10
- if (!timestamp)
11
- return 'N/A';
12
- const date = new Date(timestamp * 1000);
13
- const year = date.getFullYear();
14
- const month = date.getMonth() + 1;
15
- const monthFormatted = month < 10 ? `0${month}` : month;
16
- return (`${monthFormatted}/${year}`);
17
- };
18
- exports.timestampToDate = timestampToDate;
19
- // accepts ISO 8601, RFC 2822 or Unix timestamp in seconds and converts it to the given format
20
- // invocation without arguments creates a current UNIX timestamp in seconds
21
- const formatTimestamp = ({ timestamp, format = 'X', parseFormat }) => {
22
- // try to parse number timestamp as unix timestamp in case if parseFormat is not specified
23
- // use undefined for string timestamps with no parseFormat in hope that it will either be
24
- // ISO 8601 or RFC 2822 and moment will be able to figure it out
25
- let parseAs = parseFormat;
26
- if (!parseFormat && typeof timestamp === 'number')
27
- parseAs = 'X';
28
- return moment_1.default.utc(timestamp, parseAs).format(format);
29
- };
30
- exports.formatTimestamp = formatTimestamp;
31
- const getSearchYears = () => {
32
- const currentYear = (new Date()).getFullYear();
33
- const years = [];
34
- let nextYear = currentYear;
35
- while (nextYear >= 2011) { // tslint:disable-line
36
- years.push(nextYear);
37
- nextYear = nextYear - 1;
38
- }
39
- return [...years];
40
- };
41
- exports.getSearchYears = getSearchYears;
42
- const getCurrentYear = () => (new Date()).getFullYear();
43
- exports.getCurrentYear = getCurrentYear;
44
- const getMonths = () => {
45
- const count = [...new Array(12)]; // tslint:disable-line
46
- return count.map((_, index) => (String(1 + index)));
47
- };
48
- exports.getMonths = getMonths;
49
- function isDateInDaysPeriod(timestamp, days) {
50
- const today = new Date();
51
- // tslint:disable-next-line: no-magic-numbers
52
- const period = 1000 * 60 * 60 * 24 * days; // 1000 * 60 * 60 * 24 * days
53
- const ms = 1000;
54
- const result = !!timestamp && today.getTime() - timestamp * ms < period;
55
- return result;
56
- }
57
- // for checkout datepicker
58
- const handleMaxDate = () => {
59
- const day = +String(new Date().getDate()).padStart(2, '0');
60
- const month = String(new Date().getMonth() + 1).padStart(2, '0');
61
- const year = new Date().getFullYear() - 18;
62
- return `${month}/${day}/${year}`;
63
- };
64
- exports.handleMaxDate = handleMaxDate;
65
- const handleMaxDateForPicker = () => {
66
- const day = String(new Date().getDate()).padStart(2, '0');
67
- const month = String(new Date().getMonth() + 1).padStart(2, '0');
68
- const year = new Date().getFullYear() - 18;
69
- return `${month}/${day}/${year}`;
70
- };
71
- exports.handleMaxDateForPicker = handleMaxDateForPicker;
72
- exports._minDate = new Date('01/01/1900');
73
- const сheckingMinDateIsCorrect = (value) => exports._minDate.getTime() < new Date(value).getTime();
74
- exports.сheckingMinDateIsCorrect = сheckingMinDateIsCorrect;
75
- const сheckingMaxDateIsCorrect = (value) => new Date((0, exports.handleMaxDate)()).getTime() > new Date(value).getTime();
76
- exports.сheckingMaxDateIsCorrect = сheckingMaxDateIsCorrect;
77
- const checkingWhenLess18Age = (value) => new Date().getTime() >= new Date(value).getTime();
78
- exports.checkingWhenLess18Age = checkingWhenLess18Age;
79
- const getYears = (minYear = 1980, maxYear = (0, moment_1.default)().year(), reverse = false) => {
80
- const years = [];
81
- for (let i = maxYear; i >= minYear; i -= 1) {
82
- years.push(`${i}`);
83
- }
84
- return reverse ? years.sort() : years;
85
- };
86
- exports.getYears = getYears;
87
- const createDateAsUTC = (date) => new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds()));
88
- exports.createDateAsUTC = createDateAsUTC;
89
- // end for checkout datepicker
90
- const fromUTCToDateString = (UTCDate) => {
91
- const date = new Date(UTCDate);
92
- const day = date.getUTCDate().toString().padStart(2, '0');
93
- const month = (date.getUTCMonth() + 1).toString().padStart(2, '0');
94
- const year = date.getUTCFullYear();
95
- return `${day}.${month}.${year}`;
96
- };
97
- exports.fromUTCToDateString = fromUTCToDateString;