@bytebrand/fe-ui-core-autobahn 1.0.51 → 1.0.54

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.
@@ -23,12 +23,12 @@
23
23
  // Every field path below was confirmed against the real code (file:line in
24
24
  // ./fieldMapping.ts), not guessed.
25
25
 
26
- import { getPriceRating, getPredictablePrice } from '../utils/CommonUtils';
26
+ import { getPriceRating, getPredictablePrice } from '../utils/priceRating';
27
27
  import { formatTimestamp } from '../utils/DateUtils';
28
28
  import { getPowerLabel } from '../utils/power';
29
29
 
30
30
  import { AutobahnCar, AutobahnPhev } from '../cards/types';
31
- import { TIER_LABELS, RatingTier, CO2_CLASSES, Co2Class } from '../constants';
31
+ import { TIER_LABELS, RatingTier, CO2_CLASSES, Co2Class } from '../constants/autobahn';
32
32
 
33
33
  /**
34
34
  * Last-resort placeholder shown in the carousel until the real images for a car
@@ -137,8 +137,9 @@ const CBD_DE_FALLBACK: Record<string, string> = {
137
137
  multimedia_bluetooth: 'Bluetooth',
138
138
  };
139
139
 
140
- /** translate a cbd:* selector code → human label. */
141
- function cbd(code: any, t?: (key: string, options?: object) => string): string {
140
+ /** translate a cbd:* selector code → human label. Exported: also used by the Meta AIA
141
+ * marketing-image renderer (plain Node backend, no i18next — relies on the built-in de fallback). */
142
+ export function cbd(code: any, t?: (key: string, options?: object) => string): string {
142
143
  if (code == null || code === 'N/A') return '';
143
144
  const key = String(code);
144
145
  if (t) {
@@ -155,7 +156,7 @@ function cbd(code: any, t?: (key: string, options?: object) => string): string {
155
156
  * colored InlineCls (which does CO2_CLASSES.indexOf) still resolves a band color.
156
157
  * Returns '' when there is no class (caller decides what to render).
157
158
  */
158
- function co2ClassLetter(code: any): Co2Class | '' {
159
+ export function co2ClassLetter(code: any): Co2Class | '' {
159
160
  if (code == null || code === 'N/A') return '';
160
161
  const raw = String(code);
161
162
  // selector_energyEfficiencyClass_appp → 'a', _ap → 'a', _b → 'b', …
@@ -202,13 +203,24 @@ const TIER_TONE: Record<RatingTier, string> = {
202
203
  4: 'high',
203
204
  };
204
205
 
205
- function mapRating(currentSalesPrice?: number, predicted?: number): { rating: string; ratingLabel: string } {
206
+ export function mapRating(currentSalesPrice?: number, predicted?: number): { rating: string; ratingLabel: string } {
206
207
  const legacy = getPriceRating(currentSalesPrice as number, predicted as number); // 0..5
207
208
  // 0 = noRating: keep neutral (tier 2) — never invent a flattering rating.
208
209
  const tier: RatingTier = legacy === 0 ? 2 : LEGACY_TO_TIER[legacy] ?? 2;
209
210
  return { rating: TIER_TONE[tier], ratingLabel: TIER_LABELS[tier] };
210
211
  }
211
212
 
213
+ /**
214
+ * Map the 5-tier rating to a 2-tone signal — only 'top'/'good' read as a genuinely good deal
215
+ * (green); 'fair'/'raised'/'high' all get the warn/peach tone (Figma: "Erhöhter-Preis" is
216
+ * peach, not green). Pure/non-React on purpose: shared by the web card's StatChip (via
217
+ * cards/cardParts.tsx's re-export) AND the Meta AIA marketing-image renderer (a plain Node
218
+ * service that can't import React components).
219
+ */
220
+ export function ratingTone(rating: string): 'good' | 'warn' {
221
+ return rating === 'top' || rating === 'good' ? 'good' : 'warn';
222
+ }
223
+
212
224
  /** Is this car a plug-in hybrid? Determined by fuel type, mirroring ADV-88 in VehicleInfo.tsx. */
213
225
  function isPluginHybrid(fuel: any): boolean {
214
226
  return fuel === 'selector_fuel_hybridElectricalDiesel' || fuel === 'selector_fuel_hybridElectricalPetrol';
@@ -381,6 +393,13 @@ export function toAutobahnCar(storeCar: any, deps: ToAutobahnCarDeps = {}): Auto
381
393
  savings = highest - purchase;
382
394
  }
383
395
 
396
+ // --- struck-through old rate per payment path — same isStrikeShown gate as the Kaufen
397
+ // strike above. getFinancing.ts/getLeasing.ts each compute their own real
398
+ // oldMonthlyInstallment from the same common.highestPrice; only emitted when the tabbed
399
+ // hero price actually has something real to strike against.
400
+ const oldMonthly = isStrikeShown ? num(price?.financing?.oldMonthlyInstallment) : undefined;
401
+ const oldLease = isStrikeShown && leasingActive ? num(price?.leasing?.oldMonthlyInstallment) : undefined;
402
+
384
403
  // --- price drop (REAL) — mirrors the legacy SRL green "−X €" badge: show the reduction only
385
404
  // when price.historyPriceDifference is a meaningful amount (≥ 500 €, the legacy threshold).
386
405
  const histDrop = num(price?.historyPriceDifference);
@@ -451,6 +470,8 @@ export function toAutobahnCar(storeCar: any, deps: ToAutobahnCarDeps = {}): Auto
451
470
  // optional (only when real)
452
471
  ...(listPrice != null ? { listPrice } : {}),
453
472
  ...(savings != null ? { savings } : {}),
473
+ ...(oldMonthly != null && oldMonthly > monthly ? { oldMonthly } : {}),
474
+ ...(oldLease != null && lease != null && oldLease > lease ? { oldLease } : {}),
454
475
  ...(lease != null ? { lease } : {}),
455
476
  ...(leaseMileage != null ? { leaseMileage } : {}),
456
477
  ...(leaseMonths != null ? { leaseMonths } : {}),
@@ -51,6 +51,9 @@
51
51
  },
52
52
  "cta": {
53
53
  "viewOffer": "Zum Angebot"
54
+ },
55
+ "badge": {
56
+ "offer": "Angebot"
54
57
  }
55
58
  },
56
59
  "finance": {
@@ -51,6 +51,9 @@
51
51
  },
52
52
  "cta": {
53
53
  "viewOffer": "View offer"
54
+ },
55
+ "badge": {
56
+ "offer": "Offer"
54
57
  }
55
58
  },
56
59
  "finance": {
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@bytebrand/fe-ui-core-autobahn",
3
- "version": "1.0.51",
4
- "description": "auto.de md3 (Autobahn) design system — UI primitives, cards, chrome, VDP modules, theme CSS and locales. Ships raw TypeScript source (like fe-ui-core before it); each consumer's bundler compiles it.",
3
+ "version": "1.0.54",
4
+ "description": "auto.de md3 (Autobahn) design system — UI primitives, cards, chrome, VDP modules, theme CSS and locales. Ships raw TypeScript source (like fe-ui-core before it); each consumer's bundler compiles it. A small set of pure, asset-require-free leaf modules (see tsconfig.node-safe.json) additionally ship a compiled .js/.d.ts sibling next to their .ts source, so plain-Node backends (e.g. image-generator's Meta AIA renderer) can require() them directly without a bundler — frontend consumers are unaffected (webpack resolves .ts/.tsx before .js).",
5
5
  "main": "index.ts",
6
+ "scripts": {
7
+ "build:node-safe": "tsc -p tsconfig.node-safe.json"
8
+ },
6
9
  "license": "UNLICENSED",
7
10
  "peerDependencies": {
8
11
  "react": "^18.0.0",
package/theme/md3.css CHANGED
@@ -957,6 +957,9 @@
957
957
  .md3 .srp-chips { flex: 1 1 260px; min-width: 0; }
958
958
  /* "NEU" badge on car cards (legacy new-car label). */
959
959
  .md3 .neu-badge { display: inline-flex; align-items: center; height: 22px; padding: 0 8px; border-radius: var(--md-r-full); background: var(--md-primary); color: #fff; font-size: 10.5px; font-weight: 700; letter-spacing: .3px; white-space: nowrap; box-shadow: var(--md-e1); }
960
+ /* "⚡ Angebot" badge — dark neutral pill above the gallery (card mockup), distinct from NEU. */
961
+ .md3 .offer-badge { display: inline-flex; align-items: center; gap: 4px; height: 24px; padding: 0 9px; border-radius: var(--md-r-full); background: var(--md-on-surface); color: #fff; font-size: 11px; font-weight: 600; white-space: nowrap; }
962
+ .md3 .offer-badge .lucide { width: 13px; height: 13px; }
960
963
  @media (max-width: 820px) { .md3 .srp-chips { order: 3; flex-basis: 100%; } }
961
964
  .md3 .vdp-grid { display: grid; grid-template-columns: 1fr 372px; gap: 28px; align-items: start; }
962
965
  .md3 .vdp-gallery { grid-column: 1; grid-row: 1; }
@@ -1120,9 +1123,13 @@
1120
1123
  /* financing hero (legacy-style): eyebrow → big blue rate → terms line (fills the upper gap) */
1121
1124
  .md3 .pp-eyebrow { font-size: 11px; font-weight: 700; letter-spacing: .4px; text-transform: uppercase;
1122
1125
  color: var(--md-on-surface-variant); margin-bottom: 4px; }
1123
- .md3 .pp-hero-rate { display: flex; align-items: baseline; gap: 5px; }
1126
+ .md3 .pp-hero-rate { display: flex; align-items: baseline; gap: 7px; flex-wrap: wrap; }
1124
1127
  .md3 .pp-hero-val { font-size: 32px; font-weight: 700; color: var(--md-primary); letter-spacing: -1px; line-height: 1; }
1125
1128
  .md3 .pp-hero-unit { font-size: 13px; color: var(--md-on-surface-variant); }
1129
+ /* struck-through reference rate/price shown next to the tabbed hero price (Figma card) —
1130
+ same red strike as .pp-old, sized for the hero context rather than the compact list row. */
1131
+ .md3 .pp-hero-old { font-size: 15px; color: var(--md-error); text-decoration: line-through; white-space: nowrap; }
1132
+ .md3 .pp-hero-val sup, .md3 .pp-hero-old sup, .md3 .pp-hero-unit sup { font-size: .6em; }
1126
1133
  .md3 .pp-hero-terms { margin-top: 9px; font-size: 11.5px; color: var(--md-on-surface-variant); }
1127
1134
  /* separator line between the financing block and the leasing row (legacy-design parity) */
1128
1135
  .md3 .pp-lease-row { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--md-outline-variant); }
@@ -1163,6 +1170,23 @@
1163
1170
  background: linear-gradient(90deg, var(--md-good) 0%, #E8B500 58%, #E8601A 100%); }
1164
1171
  .md3 .pp-bar-dot { position: absolute; top: 50%; width: 13px; height: 13px; border-radius: 50%; background: #fff;
1165
1172
  border: 2.5px solid var(--md-on-surface); transform: translate(-50%, -50%); box-shadow: var(--md-e1); }
1173
+ /* grid Car Card price block (Figma "Component | Car Card | Desktop") — the hero price/tabs are
1174
+ much larger here than the shared .pp-hero-* sizing used to give them (those classes are only
1175
+ consumed inside .cc-price now, so bumping them here is safe). */
1176
+ .md3 .cc-price .fin-tabs { margin-bottom: 12px; }
1177
+ .md3 .cc-price .fin-tab { font-size: 17px; font-weight: 700; padding: 8px 4px; }
1178
+ .md3 .cc-price .pp-eyebrow { font-size: 13px; margin-bottom: 2px; }
1179
+ .md3 .cc-price .pp-hero-val { font-size: 44px; letter-spacing: -1.5px; }
1180
+ .md3 .cc-price .pp-hero-old { font-size: 20px; }
1181
+ .md3 .cc-price .pp-hero-unit { font-size: 16px; }
1182
+ .md3 .cc-price .pp-bar { height: 7px; }
1183
+ .md3 .cc-price .pp-bar-dot { width: 16px; height: 16px; }
1184
+ /* card CTA row (Angebot anfragen / phone / Rate anpassen) — the shared .mbtn padding (0 24px)
1185
+ and 14px font were sized for full-width VDP buttons; at a ~330-360px card/column width three
1186
+ buttons with real text at that size overflow their flex share. Scoped-down here only. */
1187
+ .md3 .card-cta-row .mbtn { font-size: 12.5px; padding: 0 10px; gap: 5px; min-width: 0; }
1188
+ .md3 .card-cta-row .mbtn span, .md3 .card-cta-row .mbtn { overflow: hidden; text-overflow: ellipsis; }
1189
+ .md3 .card-cta-row .mbtn .lucide { width: 15px; height: 15px; flex: none; }
1166
1190
  /* muted buyer activity (#5) */
1167
1191
  .md3 .pp-activity { display: flex; align-items: center; gap: 5px; margin-top: 12px; font-size: 11px;
1168
1192
  color: var(--md-on-surface-variant); white-space: nowrap; }
@@ -0,0 +1,25 @@
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
+ }
@@ -6,9 +6,8 @@ import {
6
6
  FIRST_REGISTRATION, MANUFACTURER, MILEAGE, MODEL, BODY_TYPE, PRICE, RATE,
7
7
  DROP_DOWN_GROUP, RATE_DEFAULT, MMS_GROUPS_KEY, MMS_GROUP_FIELDS
8
8
  } from '../constants/SearchWidget';
9
- import { priceRatings, priceRatingConfig } from '../constants/price';
10
9
  import { HUNDRED } from '../constants/common';
11
- import { offers, PREDICTABLE_PRICE_INDEX } from '../constants/core';
10
+ import { offers } from '../constants/core';
12
11
  import { FILTERS_IN_TITLE, MANUFACTURER_KEY, MAX_FILTERS_IN_TITLE, MODEL_KEY } from '../constants/Search';
13
12
  import { isArray } from 'lodash';
14
13
 
@@ -324,52 +323,10 @@ export const getFormattedPrice = (price: number, format = '$,.0f', currency = ''
324
323
  };
325
324
 
326
325
 
327
- export const getPriceRatingConfig = (currentPrice: number, pricePredicted: number) => { // tslint:disable-line
328
- const round = (value: number) => {
329
- return Number.isFinite(pricePredicted)
330
- ? Math.round((pricePredicted * (1 + (value / 100))) / 100) * 100 // tslint:disable-line
331
- : '???';
332
- };
333
-
334
- if (currentPrice === 0) {
335
- console.log('Empty current price');
336
- }
337
-
338
- return Object.keys(priceRatingConfig).reduce((acc: any, key: any) => {
339
- const { min, max } = priceRatingConfig[key];
340
- return { ...acc, [key]: { min: round(min), max: round(max) } };
341
- }, {}); // tslint:disable-line
342
- };
343
-
344
- export const getPredictablePrice = (currentSalesPrice: number, predictablePrice: number) => {
345
- if (Number.isFinite(Number.parseFloat(`${predictablePrice}`))) {
346
- return predictablePrice;
347
- }
348
-
349
- return currentSalesPrice * PREDICTABLE_PRICE_INDEX;
350
- }
351
-
352
- export const getPriceRating = (currentSalesPrice: number, predictablePrice: number) => {
353
- const isCorrectCurrentSalesPrice = Number.isFinite(Number.parseFloat(`${currentSalesPrice}`));
354
- // const isCorrectPredictablePrice = Number.isFinite(Number.parseFloat(`${predictablePrice}`));
355
-
356
- if (!isCorrectCurrentSalesPrice) return 0;
357
-
358
-
359
- const validatedPredictablePrice = getPredictablePrice(currentSalesPrice, predictablePrice);
360
-
361
- const ratingConfig = getPriceRatingConfig(currentSalesPrice, validatedPredictablePrice);
362
-
363
- for (const key of Object.keys(ratingConfig)) {
364
- const { min, max } = ratingConfig[key];
365
- const TOP_PRICE = 5;
366
- if (key === 'highPrice' && currentSalesPrice === max) return 1;
367
- if (key === 'topPrice' && currentSalesPrice <= min) return TOP_PRICE;
368
- if (currentSalesPrice >= min && currentSalesPrice < max) return priceRatings.indexOf(key);
369
- }
370
-
371
- return 0;
372
- };
326
+ // Preisbewertung rating math moved to ./priceRating.ts (kept import-safe for plain Node
327
+ // consumers — this file pulls in ../constants/Search, which requires image assets). Re-exported
328
+ // here so existing CommonUtils consumers are unaffected.
329
+ export { getPriceRatingConfig, getPredictablePrice, getPriceRating } from './priceRating';
373
330
 
374
331
  export const fixNumber = (num: number | string) => {
375
332
  const isNum = typeof num === 'number' ? num : parseFloat(num);
@@ -0,0 +1,21 @@
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;
@@ -0,0 +1,97 @@
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;
@@ -21,7 +21,7 @@ export const formatTimestamp = ({ timestamp, format = 'X', parseFormat }: IForma
21
21
  // try to parse number timestamp as unix timestamp in case if parseFormat is not specified
22
22
  // use undefined for string timestamps with no parseFormat in hope that it will either be
23
23
  // ISO 8601 or RFC 2822 and moment will be able to figure it out
24
- let parseAs: string = parseFormat;
24
+ let parseAs: string | undefined = parseFormat;
25
25
  if (!parseFormat && typeof timestamp === 'number') parseAs = 'X';
26
26
  return moment.utc(timestamp, parseAs).format(format);
27
27
  };
@@ -0,0 +1,32 @@
1
+ /** Monetary figure → de-DE grouped string (e.g. 47400 → "47.400"). Feed rounded integers —
2
+ * toLocaleString on a fractional value prints a bogus 3-decimal grouping. */
3
+ export declare function formatEur(n: number): string;
4
+ /** Rate → de-DE percent string (e.g. 6.49 → "6,49 %"). */
5
+ export declare function formatPct(n: number): string;
6
+ export interface FinanceExampleFields {
7
+ price: number;
8
+ apr: number;
9
+ down: number;
10
+ /** Real config figure — falls back to price-down ONLY when genuinely absent. */
11
+ netLoanAmount?: number;
12
+ nominalRate?: number;
13
+ finalInstallment?: number;
14
+ grossAmount?: number;
15
+ }
16
+ export interface FinanceExampleLabels {
17
+ vehiclePrice: string;
18
+ priceSuffix: string;
19
+ apr: string;
20
+ nominal: string;
21
+ net: string;
22
+ down: string;
23
+ final: string;
24
+ gross: string;
25
+ }
26
+ /**
27
+ * The "Repräsentatives Beispiel" disclaimer line (PAngV). Only fields with a genuine value are
28
+ * included — nominalRate/finalInstallment/grossAmount are optional real config figures, never
29
+ * derived (see fields doc). Matches production wording/format exactly (VdpModules.tsx's
30
+ * `exampleLine`, byte-for-byte).
31
+ */
32
+ export declare function buildFinanceExampleLine(fields: FinanceExampleFields, labels: FinanceExampleLabels, footnoteMarker?: string): string;
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ // Autobahn (auto.de redesign) — shared financing-number formatting.
3
+ //
4
+ // Extracted from vdp/VdpModules.tsx's FinanceDetailsModal so non-React consumers (e.g. the
5
+ // Meta AIA marketing-image renderer) format the SAME real numbers the same way, instead of a
6
+ // second copy drifting from the VDP's. Pure TS, no React/JSX — safe to import from a plain
7
+ // Node service.
8
+ //
9
+ // The "only real figures, never a fabricated total" rule is load-bearing (legal: balloon
10
+ // financing means monthly×term is NOT the total) and lives here once, not per-caller.
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.formatEur = formatEur;
13
+ exports.formatPct = formatPct;
14
+ exports.buildFinanceExampleLine = buildFinanceExampleLine;
15
+ /** Monetary figure → de-DE grouped string (e.g. 47400 → "47.400"). Feed rounded integers —
16
+ * toLocaleString on a fractional value prints a bogus 3-decimal grouping. */
17
+ function formatEur(n) {
18
+ return n.toLocaleString('de-DE');
19
+ }
20
+ /** Rate → de-DE percent string (e.g. 6.49 → "6,49 %"). */
21
+ function formatPct(n) {
22
+ return `${n.toFixed(2).replace('.', ',')} %`;
23
+ }
24
+ /**
25
+ * The "Repräsentatives Beispiel" disclaimer line (PAngV). Only fields with a genuine value are
26
+ * included — nominalRate/finalInstallment/grossAmount are optional real config figures, never
27
+ * derived (see fields doc). Matches production wording/format exactly (VdpModules.tsx's
28
+ * `exampleLine`, byte-for-byte).
29
+ */
30
+ function buildFinanceExampleLine(fields, labels, footnoteMarker = '³') {
31
+ const netto = fields.netLoanAmount != null ? fields.netLoanAmount : Math.max(0, fields.price - fields.down);
32
+ const parts = [
33
+ `${labels.vehiclePrice}: ${formatEur(fields.price)} € ${labels.priceSuffix}`,
34
+ ...(fields.apr > 0 ? [`${labels.apr}: ${formatPct(fields.apr)}`] : []),
35
+ ...(fields.nominalRate != null && fields.nominalRate > 0 ? [`${labels.nominal}: ${formatPct(fields.nominalRate)}`] : []),
36
+ `${labels.net}: ${formatEur(netto)} €`,
37
+ `${labels.down}: ${formatEur(fields.down)} €`,
38
+ ...(fields.finalInstallment != null && fields.finalInstallment > 0 ? [`${labels.final}: ${formatEur(fields.finalInstallment)} €`] : []),
39
+ ...(fields.grossAmount != null && fields.grossAmount > 0 ? [`${labels.gross}: ${formatEur(fields.grossAmount)} €`] : []),
40
+ ];
41
+ return `${footnoteMarker} ${parts.join('; ')};`;
42
+ }
@@ -0,0 +1,62 @@
1
+ // Autobahn (auto.de redesign) — shared financing-number formatting.
2
+ //
3
+ // Extracted from vdp/VdpModules.tsx's FinanceDetailsModal so non-React consumers (e.g. the
4
+ // Meta AIA marketing-image renderer) format the SAME real numbers the same way, instead of a
5
+ // second copy drifting from the VDP's. Pure TS, no React/JSX — safe to import from a plain
6
+ // Node service.
7
+ //
8
+ // The "only real figures, never a fabricated total" rule is load-bearing (legal: balloon
9
+ // financing means monthly×term is NOT the total) and lives here once, not per-caller.
10
+
11
+ /** Monetary figure → de-DE grouped string (e.g. 47400 → "47.400"). Feed rounded integers —
12
+ * toLocaleString on a fractional value prints a bogus 3-decimal grouping. */
13
+ export function formatEur(n: number): string {
14
+ return n.toLocaleString('de-DE');
15
+ }
16
+
17
+ /** Rate → de-DE percent string (e.g. 6.49 → "6,49 %"). */
18
+ export function formatPct(n: number): string {
19
+ return `${n.toFixed(2).replace('.', ',')} %`;
20
+ }
21
+
22
+ export interface FinanceExampleFields {
23
+ price: number;
24
+ apr: number;
25
+ down: number;
26
+ /** Real config figure — falls back to price-down ONLY when genuinely absent. */
27
+ netLoanAmount?: number;
28
+ nominalRate?: number;
29
+ finalInstallment?: number;
30
+ grossAmount?: number;
31
+ }
32
+
33
+ export interface FinanceExampleLabels {
34
+ vehiclePrice: string;
35
+ priceSuffix: string;
36
+ apr: string;
37
+ nominal: string;
38
+ net: string;
39
+ down: string;
40
+ final: string;
41
+ gross: string;
42
+ }
43
+
44
+ /**
45
+ * The "Repräsentatives Beispiel" disclaimer line (PAngV). Only fields with a genuine value are
46
+ * included — nominalRate/finalInstallment/grossAmount are optional real config figures, never
47
+ * derived (see fields doc). Matches production wording/format exactly (VdpModules.tsx's
48
+ * `exampleLine`, byte-for-byte).
49
+ */
50
+ export function buildFinanceExampleLine(fields: FinanceExampleFields, labels: FinanceExampleLabels, footnoteMarker = '³'): string {
51
+ const netto = fields.netLoanAmount != null ? fields.netLoanAmount : Math.max(0, fields.price - fields.down);
52
+ const parts: string[] = [
53
+ `${labels.vehiclePrice}: ${formatEur(fields.price)} € ${labels.priceSuffix}`,
54
+ ...(fields.apr > 0 ? [`${labels.apr}: ${formatPct(fields.apr)}`] : []),
55
+ ...(fields.nominalRate != null && fields.nominalRate > 0 ? [`${labels.nominal}: ${formatPct(fields.nominalRate)}`] : []),
56
+ `${labels.net}: ${formatEur(netto)} €`,
57
+ `${labels.down}: ${formatEur(fields.down)} €`,
58
+ ...(fields.finalInstallment != null && fields.finalInstallment > 0 ? [`${labels.final}: ${formatEur(fields.finalInstallment)} €`] : []),
59
+ ...(fields.grossAmount != null && fields.grossAmount > 0 ? [`${labels.gross}: ${formatEur(fields.grossAmount)} €`] : []),
60
+ ];
61
+ return `${footnoteMarker} ${parts.join('; ')};`;
62
+ }
package/utils/index.ts CHANGED
@@ -20,9 +20,19 @@ export {
20
20
  checkingWhenLess18Age,
21
21
  getMonths,
22
22
  getYears,
23
- createDateAsUTC
23
+ createDateAsUTC,
24
+ fromUTCToDateString
24
25
  } from './DateUtils';
25
26
 
27
+ // Finance number formatting (pure — no React; safe for non-React consumers)
28
+
29
+ export {
30
+ formatEur,
31
+ formatPct,
32
+ buildFinanceExampleLine
33
+ } from './financeFormat';
34
+ export type { FinanceExampleFields, FinanceExampleLabels } from './financeFormat';
35
+
26
36
  // Common utils
27
37
 
28
38
  export {
@@ -0,0 +1 @@
1
+ export declare const getPowerLabel: (powerKW: number, powerPS: number) => string;
package/utils/power.js ADDED
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getPowerLabel = void 0;
4
+ // Pure power-label formatter, extracted from fe-ui-core's
5
+ // source/framework/vehiclesProps/decoratedProps.tsx (which stays in fe-ui-core
6
+ // because it drags React UI). Exactly one implementation lives here;
7
+ // decoratedProps re-exports it for backwards compatibility.
8
+ const getPowerLabel = (powerKW, powerPS) => {
9
+ const powerKWlabel = !!powerKW ? `${powerKW} kW ` : '';
10
+ const powerPSlabel = !!powerPS ? `${!!powerKW ? '(' : ''}${powerPS} PS${!!powerKW ? ')' : ''}` : '';
11
+ const powerKWpowerPS = `${powerKWlabel}${powerPSlabel}`;
12
+ return powerKWpowerPS;
13
+ };
14
+ exports.getPowerLabel = getPowerLabel;
@@ -0,0 +1,3 @@
1
+ export declare const getPriceRatingConfig: (currentPrice: number, pricePredicted: number) => any;
2
+ export declare const getPredictablePrice: (currentSalesPrice: number, predictablePrice: number) => number;
3
+ export declare const getPriceRating: (currentSalesPrice: number, predictablePrice: number) => number;
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getPriceRating = exports.getPredictablePrice = exports.getPriceRatingConfig = void 0;
4
+ // Autobahn (auto.de redesign) — Preisbewertung rating math, extracted from CommonUtils.ts.
5
+ //
6
+ // Pulled out on purpose: CommonUtils.ts transitively imports `../constants/Search`, which
7
+ // `require()`s image assets (webpack-only — breaks any plain Node `require`/`tsc` consumer).
8
+ // This file only imports `../constants/price` (zero further imports), so the Meta AIA
9
+ // marketing-image renderer (a plain Node backend, see ../data/toAutobahnCar.ts's `mapRating`)
10
+ // can import real Preisbewertung math without dragging in frontend-only asset requires.
11
+ // CommonUtils.ts re-exports these for existing consumers — one implementation, not a fork.
12
+ const price_1 = require("../constants/price");
13
+ const core_1 = require("../constants/core");
14
+ const getPriceRatingConfig = (currentPrice, pricePredicted) => {
15
+ const round = (value) => {
16
+ return Number.isFinite(pricePredicted)
17
+ ? Math.round((pricePredicted * (1 + (value / 100))) / 100) * 100 // tslint:disable-line
18
+ : '???';
19
+ };
20
+ if (currentPrice === 0) {
21
+ console.log('Empty current price');
22
+ }
23
+ return Object.keys(price_1.priceRatingConfig).reduce((acc, key) => {
24
+ const { min, max } = price_1.priceRatingConfig[key];
25
+ return { ...acc, [key]: { min: round(min), max: round(max) } };
26
+ }, {}); // tslint:disable-line
27
+ };
28
+ exports.getPriceRatingConfig = getPriceRatingConfig;
29
+ const getPredictablePrice = (currentSalesPrice, predictablePrice) => {
30
+ if (Number.isFinite(Number.parseFloat(`${predictablePrice}`))) {
31
+ return predictablePrice;
32
+ }
33
+ return currentSalesPrice * core_1.PREDICTABLE_PRICE_INDEX;
34
+ };
35
+ exports.getPredictablePrice = getPredictablePrice;
36
+ const getPriceRating = (currentSalesPrice, predictablePrice) => {
37
+ const isCorrectCurrentSalesPrice = Number.isFinite(Number.parseFloat(`${currentSalesPrice}`));
38
+ // const isCorrectPredictablePrice = Number.isFinite(Number.parseFloat(`${predictablePrice}`));
39
+ if (!isCorrectCurrentSalesPrice)
40
+ return 0;
41
+ const validatedPredictablePrice = (0, exports.getPredictablePrice)(currentSalesPrice, predictablePrice);
42
+ const ratingConfig = (0, exports.getPriceRatingConfig)(currentSalesPrice, validatedPredictablePrice);
43
+ for (const key of Object.keys(ratingConfig)) {
44
+ const { min, max } = ratingConfig[key];
45
+ const TOP_PRICE = 5;
46
+ if (key === 'highPrice' && currentSalesPrice === max)
47
+ return 1;
48
+ if (key === 'topPrice' && currentSalesPrice <= min)
49
+ return TOP_PRICE;
50
+ if (currentSalesPrice >= min && currentSalesPrice < max)
51
+ return price_1.priceRatings.indexOf(key);
52
+ }
53
+ return 0;
54
+ };
55
+ exports.getPriceRating = getPriceRating;