PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.10
Yatra – Travel Booking & Tour Operator Software v3.0.10
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.10, at resources/js/services/TaxService.ts

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