PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.15
Yatra – Travel Booking & Tour Operator Software v3.0.15
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 2.0.10 All 83 releases
yatra / resources / js / services / TaxService.ts

TaxService.ts in Yatra – Travel Booking & Tour Operator Software 3.0.15, at resources/js/services/TaxService.ts

456 lines 11.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Frontend Tax Service
3 *
4 * Handles tax calculations for booking forms and displays
5 *
6 * @package Yatra.Services
7 * @since 3.0.0
8 */
9
10 import { formatYatraMoney } from "../lib/currency-display";
11 import { apiClient } from "../lib/api-client";
12 import { API_ENDPOINTS } from "../lib/api-endpoints";
13
14 interface TaxDetails {
15 tax_amount: number;
16 tax_rate: number;
17 tax_inclusive: boolean;
18 taxes: Array<{
19 name: string;
20 rate: number;
21 amount: number;
22 }>;
23 subtotal?: number;
24 total_amount?: number;
25 }
26
27 interface BookingTaxCalculation {
28 subtotal: number;
29 tax_amount: number;
30 total_amount: number;
31 tax_rate: number;
32 tax_inclusive: boolean;
33 taxes: Array<{
34 name: string;
35 rate: number;
36 amount: number;
37 formatted_amount: string;
38 formatted_rate: string;
39 }>;
40 tax_breakdown: string;
41 }
42
43 class TaxService {
44 private static instance: TaxService;
45 private taxSettings: any = null;
46
47 private constructor() {}
48
49 static getInstance(): TaxService {
50 if (!TaxService.instance) {
51 TaxService.instance = new TaxService();
52 }
53 return TaxService.instance;
54 }
55
56 /**
57 * Load tax settings from API
58 */
59 async loadTaxSettings(): Promise<void> {
60 try {
61 // Through the shared client so the REST nonce is sent — a bare fetch()
62 // was answered with 401 on every admin booking form, so tax settings
63 // never actually loaded here.
64 const settings = await apiClient.get(API_ENDPOINTS.SETTINGS);
65 this.taxSettings = (settings as any)?.data ?? settings;
66 } catch (error) {
67 console.error("Failed to load tax settings:", error);
68 }
69 }
70
71 /**
72 * Get tax settings
73 */
74 getTaxSettings(): any {
75 return this.taxSettings;
76 }
77
78 /**
79 * Check if tax is enabled
80 */
81 isTaxEnabled(): boolean {
82 return this.taxSettings?.enable_tax === true;
83 }
84
85 /**
86 * Calculate tax for booking
87 */
88 calculateTax(amount: number, country?: string): TaxDetails {
89 if (!this.isTaxEnabled()) {
90 return {
91 tax_amount: 0,
92 tax_rate: 0,
93 tax_inclusive: false,
94 taxes: [],
95 };
96 }
97
98 const taxInclusive = this.taxSettings?.tax_inclusive === true;
99 const multipleTaxesEnabled =
100 this.taxSettings?.multiple_taxes_enabled === true;
101
102 if (multipleTaxesEnabled) {
103 return this.calculateMultipleTaxes(amount, country, taxInclusive);
104 } else {
105 return this.calculateSingleTax(amount, country, taxInclusive);
106 }
107 }
108
109 /**
110 * Calculate single tax (backward compatibility)
111 */
112 private calculateSingleTax(
113 amount: number,
114 country?: string,
115 taxInclusive: boolean = false,
116 ): TaxDetails {
117 let taxRate = this.taxSettings?.tax_rate || 0;
118 const taxName =
119 this.taxSettings?.tax_name || this.taxSettings?.tax_label || "Tax";
120
121 // Check for country-specific tax
122 if (
123 this.taxSettings?.tax_by_country === true &&
124 country &&
125 this.taxSettings?.tax_rates?.[country]
126 ) {
127 taxRate = this.taxSettings.tax_rates[country];
128 }
129
130 let taxAmount: number;
131 if (taxInclusive) {
132 // Tax is included in the price
133 taxAmount = amount - amount / (1 + taxRate / 100);
134 } else {
135 // Tax is added to the price
136 taxAmount = amount * (taxRate / 100);
137 }
138
139 return {
140 tax_amount: Math.round(taxAmount * 100) / 100,
141 tax_rate: taxRate,
142 tax_inclusive: taxInclusive,
143 taxes: [
144 {
145 name: taxName,
146 rate: taxRate,
147 amount: Math.round(taxAmount * 100) / 100,
148 },
149 ],
150 };
151 }
152
153 /**
154 * Calculate multiple taxes
155 */
156 private calculateMultipleTaxes(
157 amount: number,
158 country?: string,
159 taxInclusive: boolean = false,
160 ): TaxDetails {
161 let taxes = this.taxSettings?.multiple_taxes || [];
162
163 // Check for country-specific taxes
164 if (country && this.taxSettings?.multiple_taxes_by_country?.[country]) {
165 taxes = this.taxSettings.multiple_taxes_by_country[country];
166 }
167
168 const calculatedTaxes: Array<{
169 name: string;
170 rate: number;
171 amount: number;
172 }> = [];
173 let totalTaxAmount = 0;
174
175 for (const tax of taxes) {
176 const taxRate = tax.rate || 0;
177 const taxName = tax.name || "Tax";
178
179 let taxAmount: number;
180 if (taxInclusive) {
181 // For tax-inclusive, calculate based on remaining amount
182 const baseAmount = amount - totalTaxAmount;
183 taxAmount = baseAmount * (taxRate / 100);
184 } else {
185 // Tax is added to the price
186 taxAmount = amount * (taxRate / 100);
187 }
188
189 taxAmount = Math.round(taxAmount * 100) / 100;
190 totalTaxAmount += taxAmount;
191
192 calculatedTaxes.push({
193 name: taxName,
194 rate: taxRate,
195 amount: taxAmount,
196 });
197 }
198
199 return {
200 tax_amount: Math.round(totalTaxAmount * 100) / 100,
201 tax_rate: taxes.reduce(
202 (sum: number, tax: any) => sum + (tax.rate || 0),
203 0,
204 ),
205 tax_inclusive: taxInclusive,
206 taxes: calculatedTaxes,
207 };
208 }
209
210 /**
211 * Calculate complete booking tax breakdown
212 */
213 calculateBookingTax(
214 subtotal: number,
215 country?: string,
216 ): BookingTaxCalculation {
217 const taxDetails = this.calculateTax(subtotal, country);
218
219 let finalSubtotal: number;
220 let finalTotal: number;
221
222 if (taxDetails.tax_inclusive) {
223 // Tax is included - extract tax from total
224 finalSubtotal = subtotal - taxDetails.tax_amount;
225 finalTotal = subtotal;
226 } else {
227 // Tax is added to subtotal
228 finalSubtotal = subtotal;
229 finalTotal = subtotal + taxDetails.tax_amount;
230 }
231
232 const formattedTaxes = taxDetails.taxes.map((tax) => ({
233 ...tax,
234 formatted_amount: this.formatPrice(tax.amount),
235 formatted_rate: `${tax.rate.toFixed(2)}%`,
236 }));
237
238 const taxBreakdown = formattedTaxes
239 .map(
240 (tax) => `${tax.name} (${tax.formatted_rate}): ${tax.formatted_amount}`,
241 )
242 .join("\n");
243
244 return {
245 subtotal: Math.round(finalSubtotal * 100) / 100,
246 tax_amount: taxDetails.tax_amount,
247 total_amount: Math.round(finalTotal * 100) / 100,
248 tax_rate: taxDetails.tax_rate,
249 tax_inclusive: taxDetails.tax_inclusive,
250 taxes: formattedTaxes,
251 tax_breakdown: taxBreakdown,
252 };
253 }
254
255 /**
256 * Format price display
257 */
258 formatPrice(amount: number): string {
259 const currency = this.taxSettings?.currency || "USD";
260 return formatYatraMoney(Number(amount) || 0, currency, {
261 zeroAsUnknown: false,
262 });
263 }
264
265 /**
266 * Get currency symbol
267 */
268 getCurrencySymbol(currency: string): string {
269 const symbols: { [key: string]: string } = {
270 USD: "$",
271 EUR: "",
272 GBP: "£",
273 JPY: "¥",
274 AUD: "A$",
275 CAD: "C$",
276 CHF: "CHF",
277 CNY: "¥",
278 INR: "",
279 };
280
281 return symbols[currency] || currency;
282 }
283
284 /**
285 * Get tax breakdown for display
286 */
287 getTaxBreakdown(taxDetails: TaxDetails): Array<{
288 name: string;
289 rate: number;
290 amount: number;
291 formatted_amount: string;
292 formatted_rate: string;
293 formatted_line: string;
294 }> {
295 return taxDetails.taxes.map((tax) => ({
296 name: tax.name,
297 rate: tax.rate,
298 amount: tax.amount,
299 formatted_amount: this.formatPrice(tax.amount),
300 formatted_rate: `${tax.rate.toFixed(2)}%`,
301 formatted_line: `${tax.name} (${tax.rate.toFixed(2)}%): ${this.formatPrice(tax.amount)}`,
302 }));
303 }
304
305 /**
306 * Format tax display for booking summary
307 */
308 formatTaxDisplay(taxDetails: TaxDetails): string {
309 const breakdown = this.getTaxBreakdown(taxDetails);
310 return breakdown.map((tax) => tax.formatted_line).join("\n");
311 }
312
313 /**
314 * Validate tax configuration
315 */
316 validateTaxConfiguration(): { valid: boolean; errors: string[] } {
317 const errors: string[] = [];
318
319 if (!this.isTaxEnabled()) {
320 return { valid: true, errors: [] };
321 }
322
323 const multipleTaxesEnabled =
324 this.taxSettings?.multiple_taxes_enabled === true;
325
326 if (multipleTaxesEnabled) {
327 const multipleTaxes = this.taxSettings?.multiple_taxes || [];
328
329 if (multipleTaxes.length === 0) {
330 errors.push("At least one tax must be configured");
331 }
332
333 let totalRate = 0;
334 for (let i = 0; i < multipleTaxes.length; i++) {
335 const tax = multipleTaxes[i];
336
337 if (!tax.name || tax.name.trim() === "") {
338 errors.push(`Tax ${i + 1}: Name is required`);
339 }
340
341 if (typeof tax.rate !== "number" || tax.rate < 0 || tax.rate > 100) {
342 errors.push(`Tax ${i + 1}: Rate must be between 0 and 100`);
343 } else {
344 totalRate += tax.rate;
345 }
346 }
347
348 if (totalRate > 100) {
349 errors.push("Total tax rate cannot exceed 100%");
350 }
351 } else {
352 const taxRate = this.taxSettings?.tax_rate || 0;
353 const taxName = this.taxSettings?.tax_name || "";
354
355 if (typeof taxRate !== "number" || taxRate < 0 || taxRate > 100) {
356 errors.push("Tax rate must be between 0 and 100");
357 }
358
359 if (!taxName || taxName.trim() === "") {
360 errors.push("Tax name is required");
361 }
362 }
363
364 return {
365 valid: errors.length === 0,
366 errors,
367 };
368 }
369
370 /**
371 * Get tax summary for reporting
372 */
373 getTaxSummary(bookings: any[]): {
374 total_tax_collected: number;
375 total_bookings_with_tax: number;
376 tax_breakdown: {
377 [key: string]: {
378 name: string;
379 total_amount: number;
380 count: number;
381 average_rate: number;
382 };
383 };
384 average_tax_rate: number;
385 } {
386 const summary = {
387 total_tax_collected: 0,
388 total_bookings_with_tax: 0,
389 tax_breakdown: {} as {
390 [key: string]: {
391 name: string;
392 total_amount: number;
393 count: number;
394 average_rate: number;
395 };
396 },
397 average_tax_rate: 0,
398 };
399
400 let totalTaxRate = 0;
401 let taxRateCount = 0;
402
403 for (const booking of bookings) {
404 const taxAmount = booking.tax_amount || 0;
405
406 if (taxAmount > 0) {
407 summary.total_tax_collected += taxAmount;
408 summary.total_bookings_with_tax++;
409
410 const taxRate = booking.tax_rate || 0;
411 if (taxRate > 0) {
412 totalTaxRate += taxRate;
413 taxRateCount++;
414 }
415
416 // Collect tax breakdown
417 const taxBreakdown = booking.tax_breakdown || [];
418 for (const tax of taxBreakdown) {
419 const taxName = tax.name;
420 if (!summary.tax_breakdown[taxName]) {
421 summary.tax_breakdown[taxName] = {
422 name: taxName,
423 total_amount: 0,
424 count: 0,
425 average_rate: 0,
426 };
427 }
428
429 summary.tax_breakdown[taxName].total_amount += tax.amount;
430 summary.tax_breakdown[taxName].count++;
431 summary.tax_breakdown[taxName].average_rate += tax.rate;
432 }
433 }
434 }
435
436 // Calculate averages
437 if (taxRateCount > 0) {
438 summary.average_tax_rate = totalTaxRate / taxRateCount;
439 }
440
441 // Calculate average rates for tax breakdown
442 for (const taxName in summary.tax_breakdown) {
443 const taxData = summary.tax_breakdown[taxName];
444 if (taxData.count > 0) {
445 taxData.average_rate = taxData.average_rate / taxData.count;
446 }
447 }
448
449 return summary;
450 }
451 }
452
453 // Export singleton instance
454 export const taxService = TaxService.getInstance();
455 export default taxService;
456