PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.9
Yatra – Travel Booking & Tour Operator Software v3.0.2.9
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 / services / TaxService.ts

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

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