PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.16
Yatra – Travel Booking & Tour Operator Software v3.0.16
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
yatra / resources / js / lib / currency-display.ts

currency-display.ts in Yatra – Travel Booking & Tour Operator Software 3.0.16, at resources/js/lib/currency-display.ts

198 lines 6.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Central currency symbol placement for Yatra admin + public UIs.
3 *
4 * Settings → Currency uses: left, right, left_space, right_space.
5 * Legacy stored values: before, after (mapped for backward compatibility).
6 */
7
8 import { __ } from "./i18n";
9 import { getCurrency, getCurrencySymbol } from "../data/currencies";
10
11 export type CurrencyPlacement = "left" | "right" | "left_space" | "right_space";
12
13 /**
14 * Normalize option / legacy values to one of the four placement modes.
15 */
16 export function normalizeCurrencyPosition(
17 raw: string | undefined | null,
18 ): CurrencyPlacement {
19 const p = (raw ?? "before").toString().toLowerCase().trim();
20 if (p === "before") {
21 return "left_space";
22 }
23 if (p === "after") {
24 return "right_space";
25 }
26 if (
27 p === "right" ||
28 p === "right_space" ||
29 p === "left" ||
30 p === "left_space"
31 ) {
32 return p as CurrencyPlacement;
33 }
34 return "left_space";
35 }
36
37 /**
38 * Join a pre-formatted numeric string with the currency symbol per placement.
39 */
40 export function applyCurrencyPosition(
41 formattedAmount: string,
42 currencySymbol: string,
43 rawPosition?: string | null,
44 ): string {
45 const mode = normalizeCurrencyPosition(rawPosition);
46 switch (mode) {
47 case "right":
48 return `${formattedAmount}${currencySymbol}`;
49 case "right_space":
50 return `${formattedAmount} ${currencySymbol}`;
51 case "left":
52 return `${currencySymbol}${formattedAmount}`;
53 case "left_space":
54 default:
55 return `${currencySymbol} ${formattedAmount}`;
56 }
57 }
58
59 type AdminWindow = Window & {
60 yatraAdmin?: Record<string, unknown>;
61 yatraBookingData?: Record<string, unknown>;
62 yatraTripData?: Record<string, unknown>;
63 yatraAccountPage?: Record<string, unknown>;
64 };
65
66 /**
67 * Read raw currency position from localized globals (camelCase or snake_case).
68 */
69 export function readYatraCurrencyPositionFromWindow(): string {
70 if (typeof window === "undefined") {
71 return "before";
72 }
73 const w = window as AdminWindow;
74 const pick = (o: Record<string, unknown> | undefined): string | undefined => {
75 if (!o) {
76 return undefined;
77 }
78 const a = o.currencyPosition;
79 const b = o.currency_position;
80 if (typeof a === "string" && a !== "") {
81 return a;
82 }
83 if (typeof b === "string" && b !== "") {
84 return b;
85 }
86 return undefined;
87 };
88
89 return (
90 pick(w.yatraAdmin as Record<string, unknown> | undefined) ??
91 pick(w.yatraBookingData as Record<string, unknown> | undefined) ??
92 pick(w.yatraTripData as Record<string, unknown> | undefined) ??
93 pick(w.yatraAccountPage as Record<string, unknown> | undefined) ??
94 "before"
95 );
96 }
97
98 function readYatraNumberFormatConfig(): {
99 decimalPlaces: number;
100 thousandSeparator: string;
101 decimalSeparator: string;
102 } {
103 const fallback = {
104 decimalPlaces: 2,
105 thousandSeparator: ",",
106 decimalSeparator: ".",
107 };
108 if (typeof window === "undefined") {
109 return fallback;
110 }
111 // Read from the SAME set of globals as the currency position, not just
112 // yatraAdmin. On public pages (customer account, checkout, single trip)
113 // window.yatraAdmin is absent, so reading separators only from it made the
114 // decimal/thousand separators silently fall back to defaults ("." / ",")
115 // while the position — read from these public globals — was correct. That is
116 // the "decimal separator ignored in payment amounts" bug on the storefront.
117 const w = window as AdminWindow;
118 const sources: Array<Record<string, unknown> | undefined> = [
119 w.yatraAdmin as Record<string, unknown> | undefined,
120 w.yatraBookingData as Record<string, unknown> | undefined,
121 w.yatraTripData as Record<string, unknown> | undefined,
122 w.yatraAccountPage as Record<string, unknown> | undefined,
123 ];
124 const pickString = (field: string): string | undefined => {
125 for (const o of sources) {
126 const v = o?.[field];
127 if (typeof v === "string" && v !== "") {
128 return v;
129 }
130 }
131 return undefined;
132 };
133 const pickNumber = (...fields: string[]): number | undefined => {
134 for (const o of sources) {
135 if (!o) {
136 continue;
137 }
138 for (const field of fields) {
139 const v = o[field];
140 if (v !== undefined && v !== null && v !== "" && !Number.isNaN(Number(v))) {
141 return Number(v);
142 }
143 }
144 }
145 return undefined;
146 };
147 const decimals = pickNumber("decimalPlaces", "currency_decimals") ?? 2;
148 return {
149 decimalPlaces: Math.max(0, Math.min(4, decimals)),
150 thousandSeparator: pickString("thousandSeparator") ?? ",",
151 decimalSeparator: pickString("decimalSeparator") ?? ".",
152 };
153 }
154
155 /**
156 * Format a numeric amount using Yatra currency + separator settings and symbol placement.
157 *
158 * @param zeroAsUnknown When true, zero/empty shows “Contact for pricing” (trip/marketing UIs).
159 */
160 export function formatYatraMoney(
161 amount: number,
162 currencyCode: string,
163 options?: { zeroAsUnknown?: boolean },
164 ): string {
165 const zeroAsUnknown = options?.zeroAsUnknown === true;
166 const num = Number(amount) || 0;
167 if (zeroAsUnknown && num === 0) {
168 return __("Contact for pricing", "yatra");
169 }
170
171 const { decimalPlaces, thousandSeparator, decimalSeparator } =
172 readYatraNumberFormatConfig();
173 const currencyData = getCurrency(currencyCode);
174 const decimals =
175 currencyData?.decimalDigits !== undefined
176 ? Math.max(0, Math.min(4, currencyData.decimalDigits))
177 : decimalPlaces;
178
179 // Format with a FIXED "en-US" base (grouping ",", decimal ".") so the
180 // replace-with-configured-separators step below is deterministic. Using the
181 // viewer's locale here (undefined) would already yield locale separators and
182 // the naive comma/dot replacement would swap them for e.g. German browsers.
183 const formattedCore = new Intl.NumberFormat("en-US", {
184 minimumFractionDigits: decimals,
185 maximumFractionDigits: decimals,
186 })
187 .format(num)
188 .replace(/,/g, "TEMP_THOUSAND")
189 .replace(/\./g, "TEMP_DECIMAL")
190 .replace(/TEMP_THOUSAND/g, thousandSeparator)
191 .replace(/TEMP_DECIMAL/g, decimalSeparator);
192
193 const symbol = getCurrencySymbol(currencyCode);
194 const position = readYatraCurrencyPositionFromWindow();
195
196 return applyCurrencyPosition(formattedCore, symbol, position);
197 }
198