Skip to content

Commit 1e09cb8

Browse files
authored
Merge branch 'develop' into refactor/data-massaging-for-ece-and-hk-based-addresses
2 parents e64a4a3 + 65c9e45 commit 1e09cb8

File tree

7 files changed

+344
-98
lines changed

7 files changed

+344
-98
lines changed

changelog/fix-TRAPLAT-3642-order-note

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: patch
2+
Type: add
3+
4+
Add tax to the fee breakdown on order notes

client/payment-details/timeline/map-events.js

Lines changed: 45 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,32 @@ const isFXEvent = ( event = {} ) => {
208208
);
209209
};
210210

211+
/**
212+
* Given the fee amount and currency, converts it to the store currency if necessary and formats using formatCurrency.
213+
*
214+
* @param {number} feeAmount Fee amount to convert and format.
215+
* @param {string} feeCurrency Fee currency to convert from.
216+
* @param {Object} event Event object containing fee rates and transaction details.
217+
*
218+
* @return {string} Formatted fee amount in the store currency.
219+
*/
220+
const convertAndFormatFeeAmount = ( feeAmount, feeCurrency, event ) => {
221+
if ( ! isFXEvent( event ) || ! event.fee_rates?.fee_exchange_rate ) {
222+
return formatCurrency( -Math.abs( feeAmount ), feeCurrency );
223+
}
224+
225+
const { rate, fromCurrency } = event.fee_rates.fee_exchange_rate;
226+
const storeCurrency = event.transaction_details.store_currency;
227+
228+
// Convert based on the direction of the exchange rate
229+
const convertedAmount =
230+
feeCurrency === fromCurrency
231+
? feeAmount * rate // Converting from store currency to customer currency
232+
: feeAmount / rate; // Converting from customer currency to store currency
233+
234+
return formatCurrency( -Math.abs( convertedAmount ), storeCurrency );
235+
};
236+
211237
/**
212238
* Returns a boolean indicating whether only fee applied is the base fee
213239
*
@@ -263,44 +289,15 @@ export const composeTaxString = ( event ) => {
263289
? ` ${ getLocalizedTaxDescription( tax.description ) }`
264290
: '';
265291

266-
// Validate tax percentage rate is within reasonable bounds (0-100%)
267-
if (
268-
tax.percentage_rate !== undefined &&
269-
( tax.percentage_rate < 0 || tax.percentage_rate > 1 )
270-
) {
271-
return sprintf(
272-
/* translators: 1: tax description 2: tax amount */
273-
__( 'Tax%1$s: %2$s', 'woocommerce-payments' ),
274-
taxDescription,
275-
formatCurrency( -Math.abs( tax.amount ), tax.currency )
276-
);
277-
}
278-
279292
const taxPercentage = tax.percentage_rate
280293
? ` (${ ( tax.percentage_rate * 100 ).toFixed( 2 ) }%)`
281294
: '';
282295

283-
let formattedTaxAmount;
284-
if ( isFXEvent( event ) && event.fee_rates.fee_exchange_rate ) {
285-
const { rate, fromCurrency } = event.fee_rates.fee_exchange_rate;
286-
const storeCurrency = event.transaction_details.store_currency;
287-
288-
// Convert based on the direction of the exchange rate
289-
const convertedTaxAmount =
290-
tax.currency === fromCurrency
291-
? tax.amount * rate // Converting from store currency to customer currency
292-
: tax.amount / rate; // Converting from customer currency to store currency
293-
294-
formattedTaxAmount = formatCurrency(
295-
-Math.abs( convertedTaxAmount ),
296-
storeCurrency
297-
);
298-
} else {
299-
formattedTaxAmount = formatCurrency(
300-
-Math.abs( tax.amount ),
301-
tax.currency
302-
);
303-
}
296+
const formattedTaxAmount = convertAndFormatFeeAmount(
297+
tax.amount,
298+
tax.currency,
299+
event
300+
);
304301

305302
return sprintf(
306303
/* translators: 1: tax description 2: tax percentage 3: tax amount */
@@ -334,8 +331,12 @@ export const composeFeeString = ( event ) => {
334331
// Get the appropriate fee amounts and currencies
335332
let feeAmount, feeCurrency, baseFee, baseFeeCurrency;
336333
if ( isFXEvent( event ) ) {
337-
feeAmount = event.transaction_details.store_fee;
338-
feeCurrency = event.transaction_details.store_currency;
334+
feeAmount =
335+
event.fee_rates?.before_tax?.amount ||
336+
event.transaction_details.store_fee;
337+
feeCurrency =
338+
event.fee_rates?.before_tax?.currency ||
339+
event.transaction_details.store_currency;
339340
baseFee = fixed || 0;
340341
baseFeeCurrency = fixedCurrency || feeCurrency;
341342
} else {
@@ -349,12 +350,18 @@ export const composeFeeString = ( event ) => {
349350
baseFeeCurrency = fixedCurrency;
350351
}
351352

353+
const formattedFeeAmount = convertAndFormatFeeAmount(
354+
feeAmount,
355+
feeCurrency,
356+
event
357+
);
358+
352359
if ( isBaseFeeOnly( event ) && history[ 0 ]?.capped ) {
353360
return sprintf(
354361
'%1$s (capped at %2$s): %3$s',
355362
baseFeeLabel,
356363
formatCurrency( baseFee, baseFeeCurrency ),
357-
formatCurrency( -feeAmount, feeCurrency )
364+
formattedFeeAmount
358365
);
359366
}
360367

@@ -369,7 +376,7 @@ export const composeFeeString = ( event ) => {
369376
formatFee( percentage ),
370377
formatCurrency( baseFee, baseFeeCurrency ),
371378
hasIdenticalSymbol ? ` ${ baseFeeCurrency }` : '',
372-
formatCurrency( -feeAmount, feeCurrency ),
379+
formattedFeeAmount,
373380
hasIdenticalSymbol ? ` ${ feeCurrency }` : ''
374381
);
375382
};

client/payment-details/timeline/test/map-events.js

Lines changed: 0 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -927,35 +927,6 @@ describe( 'composeTaxString', () => {
927927
);
928928
} );
929929

930-
// New test cases for tax percentage rate validation
931-
it( 'should handle negative tax percentage rate', () => {
932-
const event = {
933-
fee_rates: {
934-
tax: {
935-
amount: 10,
936-
currency: 'EUR',
937-
percentage_rate: -0.21,
938-
description: 'ES VAT',
939-
},
940-
},
941-
};
942-
expect( composeTaxString( event ) ).toBe( 'Tax ES VAT: -€0.10' );
943-
} );
944-
945-
it( 'should handle tax percentage rate over 100%', () => {
946-
const event = {
947-
fee_rates: {
948-
tax: {
949-
amount: 10,
950-
currency: 'EUR',
951-
percentage_rate: 1.5,
952-
description: 'ES VAT',
953-
},
954-
},
955-
};
956-
expect( composeTaxString( event ) ).toBe( 'Tax ES VAT: -€0.10' );
957-
} );
958-
959930
it( 'should handle tax percentage rate at boundary (0%)', () => {
960931
const event = {
961932
fee_rates: {
@@ -985,31 +956,4 @@ describe( 'composeTaxString', () => {
985956
'Tax ES VAT (100.00%): -€0.10'
986957
);
987958
} );
988-
989-
it( 'should handle invalid tax percentage rate with different currency', () => {
990-
const event = {
991-
fee_rates: {
992-
tax: {
993-
amount: 100,
994-
currency: 'JPY',
995-
percentage_rate: -0.15,
996-
description: 'JP JCT',
997-
},
998-
},
999-
};
1000-
expect( composeTaxString( event ) ).toBe( 'Tax JP JCT: -¥1' );
1001-
} );
1002-
1003-
it( 'should handle invalid tax percentage rate with no description', () => {
1004-
const event = {
1005-
fee_rates: {
1006-
tax: {
1007-
amount: 10,
1008-
currency: 'EUR',
1009-
percentage_rate: 1.5,
1010-
},
1011-
},
1012-
};
1013-
expect( composeTaxString( event ) ).toBe( 'Tax: -€0.10' );
1014-
} );
1015959
} );

includes/class-wc-payments-captured-event-note.php

Lines changed: 146 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ public function generate_html_note(): string {
6262
$lines = array_merge( $lines, $fee_breakdown_lines );
6363
}
6464

65+
if ( $this->has_tax() ) {
66+
$lines[] = $this->compose_tax_string();
67+
}
68+
6569
$lines[] = $this->compose_net_string();
6670

6771
$html = '';
@@ -106,8 +110,16 @@ public function compose_fee_string(): string {
106110
$fixed = WC_Payments_Utils::interpret_stripe_amount( (int) $fee_rates['fixed'], $fixed_currency );
107111
$history = $fee_rates['history'];
108112

109-
$fee_currency = $data['transaction_details']['store_currency'];
110-
$fee_amount = WC_Payments_Utils::interpret_stripe_amount( (int) $data['transaction_details']['store_fee'], $fee_currency );
113+
if ( $this->has_tax() ) {
114+
$before_tax = $data['fee_rates']['before_tax'];
115+
$fee_amount = $before_tax['amount'];
116+
$fee_currency = $before_tax['currency'];
117+
} else {
118+
$fee_currency = $data['transaction_details']['store_currency'];
119+
$fee_amount = (int) $data['transaction_details']['store_fee'];
120+
}
121+
122+
$formatted_fee_amount = $this->convert_and_format_fee_amount( $fee_amount, $fee_currency );
111123

112124
$base_fee_label = $this->is_base_fee_only()
113125
? __( 'Base fee', 'woocommerce-payments' )
@@ -120,7 +132,7 @@ public function compose_fee_string(): string {
120132
'%1$s (capped at %2$s): %3$s',
121133
$base_fee_label,
122134
WC_Payments_Utils::format_currency( $fixed, $fixed_currency ),
123-
WC_Payments_Utils::format_currency( - $fee_amount, $fee_currency )
135+
$formatted_fee_amount
124136
);
125137
}
126138
$is_same_symbol = $this->has_same_currency_symbol( $data['transaction_details']['store_currency'], $data['transaction_details']['customer_currency'] );
@@ -131,7 +143,7 @@ public function compose_fee_string(): string {
131143
self::format_fee( $percentage ),
132144
WC_Payments_Utils::format_currency( $fixed, $fixed_currency ),
133145
$is_same_symbol ? ' ' . $data['transaction_details']['customer_currency'] : '',
134-
WC_Payments_Utils::format_currency( -$fee_amount, $fee_currency ),
146+
$formatted_fee_amount,
135147
$is_same_symbol ? " $fee_currency" : ''
136148
);
137149
}
@@ -273,6 +285,38 @@ public function get_fee_breakdown() {
273285
return $fee_history_strings;
274286
}
275287

288+
/**
289+
* Compose tax string.
290+
*
291+
* @return string|null
292+
*/
293+
public function compose_tax_string(): ?string {
294+
if ( ! $this->has_tax() ) {
295+
return null;
296+
}
297+
298+
$tax = $this->captured_event['fee_rates']['tax'];
299+
$tax_amount = $tax['amount'];
300+
if ( 0 === $tax_amount ) {
301+
return null;
302+
}
303+
304+
$tax_currency = $tax['currency'];
305+
$formatted_amount = $this->convert_and_format_fee_amount( $tax_amount, $tax_currency );
306+
307+
$tax_description = ' ' . $this->get_localized_tax_description();
308+
$percentage_rate = $tax['percentage_rate'];
309+
$formatted_percentage = ' (' . self::format_fee( $percentage_rate ) . '%)';
310+
311+
return sprintf(
312+
/* translators: 1: tax description 2: tax percentage 3: tax amount */
313+
__( 'Tax%1$s%2$s: %3$s', 'woocommerce-payments' ),
314+
$tax_description,
315+
$formatted_percentage,
316+
$formatted_amount
317+
);
318+
}
319+
276320
/**
277321
* Check if this is a FX event.
278322
*
@@ -461,4 +505,102 @@ private function format_explicit_currency_with_base( float $amount, string $curr
461505
private function has_same_currency_symbol( string $base_currency, string $currency ): bool {
462506
return strcasecmp( $base_currency, $currency ) !== 0 && get_woocommerce_currency_symbol( $base_currency ) === get_woocommerce_currency_symbol( $currency );
463507
}
508+
509+
/**
510+
* Check if the event has tax information.
511+
*
512+
* @return bool
513+
*/
514+
private function has_tax(): bool {
515+
return isset( $this->captured_event['fee_rates']['tax'] );
516+
}
517+
518+
/**
519+
* Get localized tax description based on the tax description ID contained in the captured event.
520+
*
521+
* @return string|null
522+
*/
523+
private function get_localized_tax_description(): ?string {
524+
if ( ! isset( $this->captured_event['fee_rates']['tax']['description'] ) ) {
525+
return null;
526+
}
527+
528+
$tax_description_id = $this->captured_event['fee_rates']['tax']['description'];
529+
530+
$tax_descriptions = [
531+
// European Union VAT.
532+
'AT VAT' => __( 'AT VAT', 'woocommerce-payments' ), // Austria.
533+
'BE VAT' => __( 'BE VAT', 'woocommerce-payments' ), // Belgium.
534+
'BG VAT' => __( 'BG VAT', 'woocommerce-payments' ), // Bulgaria.
535+
'CY VAT' => __( 'CY VAT', 'woocommerce-payments' ), // Cyprus.
536+
'CZ VAT' => __( 'CZ VAT', 'woocommerce-payments' ), // Czech Republic.
537+
'DE VAT' => __( 'DE VAT', 'woocommerce-payments' ), // Germany.
538+
'DK VAT' => __( 'DK VAT', 'woocommerce-payments' ), // Denmark.
539+
'EE VAT' => __( 'EE VAT', 'woocommerce-payments' ), // Estonia.
540+
'ES VAT' => __( 'ES VAT', 'woocommerce-payments' ), // Spain.
541+
'FI VAT' => __( 'FI VAT', 'woocommerce-payments' ), // Finland.
542+
'FR VAT' => __( 'FR VAT', 'woocommerce-payments' ), // France.
543+
'GB VAT' => __( 'UK VAT', 'woocommerce-payments' ), // United Kingdom.
544+
'GR VAT' => __( 'GR VAT', 'woocommerce-payments' ), // Greece.
545+
'HR VAT' => __( 'HR VAT', 'woocommerce-payments' ), // Croatia.
546+
'HU VAT' => __( 'HU VAT', 'woocommerce-payments' ), // Hungary.
547+
'IE VAT' => __( 'IE VAT', 'woocommerce-payments' ), // Ireland.
548+
'IT VAT' => __( 'IT VAT', 'woocommerce-payments' ), // Italy.
549+
'LT VAT' => __( 'LT VAT', 'woocommerce-payments' ), // Lithuania.
550+
'LU VAT' => __( 'LU VAT', 'woocommerce-payments' ), // Luxembourg.
551+
'LV VAT' => __( 'LV VAT', 'woocommerce-payments' ), // Latvia.
552+
'MT VAT' => __( 'MT VAT', 'woocommerce-payments' ), // Malta.
553+
'NL VAT' => __( 'NL VAT', 'woocommerce-payments' ), // Netherlands.
554+
'PL VAT' => __( 'PL VAT', 'woocommerce-payments' ), // Poland.
555+
'PT VAT' => __( 'PT VAT', 'woocommerce-payments' ), // Portugal.
556+
'RO VAT' => __( 'RO VAT', 'woocommerce-payments' ), // Romania.
557+
'SE VAT' => __( 'SE VAT', 'woocommerce-payments' ), // Sweden.
558+
'SI VAT' => __( 'SI VAT', 'woocommerce-payments' ), // Slovenia.
559+
'SK VAT' => __( 'SK VAT', 'woocommerce-payments' ), // Slovakia.
560+
561+
// GST Countries.
562+
'AU GST' => __( 'AU GST', 'woocommerce-payments' ), // Australia.
563+
'NZ GST' => __( 'NZ GST', 'woocommerce-payments' ), // New Zealand.
564+
'SG GST' => __( 'SG GST', 'woocommerce-payments' ), // Singapore.
565+
566+
// Other Tax Systems.
567+
'CH VAT' => __( 'CH VAT', 'woocommerce-payments' ), // Switzerland.
568+
'JP JCT' => __( 'JP JCT', 'woocommerce-payments' ), // Japan Consumption Tax.
569+
];
570+
571+
return $tax_descriptions[ $tax_description_id ] ?? __( 'Tax', 'woocommerce-payments' );
572+
}
573+
574+
/**
575+
* Given the fee amount and currency, converts it to the store currency if necessary and formats using formatCurrency.
576+
*
577+
* @param float $fee_amount Fee amount to convert and format.
578+
* @param string $fee_currency Fee currency to convert from.
579+
*
580+
* @return string Formatted fee amount in the store currency.
581+
*/
582+
private function convert_and_format_fee_amount( float $fee_amount, string $fee_currency ) {
583+
$fee_exchange_rate = $this->captured_event['fee_rates']['fee_exchange_rate'] ?? null;
584+
if ( ! $this->is_fx_event() || ! $fee_exchange_rate ) {
585+
return WC_Payments_Utils::format_currency(
586+
-abs( WC_Payments_Utils::interpret_stripe_amount( $fee_amount, $fee_currency ) ),
587+
$fee_currency
588+
);
589+
}
590+
591+
$rate = $fee_exchange_rate['rate'];
592+
$from_currency = $fee_exchange_rate['from_currency'] ?? null;
593+
$store_currency = $this->captured_event['transaction_details']['store_currency'] ?? null;
594+
595+
// Convert based on the direction of the exchange rate.
596+
$converted_amount =
597+
$fee_currency === $from_currency
598+
? $fee_amount * $rate // Converting from store currency to customer currency.
599+
: $fee_amount / $rate; // Converting from customer currency to store currency.
600+
601+
return WC_Payments_Utils::format_currency(
602+
-abs( WC_Payments_Utils::interpret_stripe_amount( $converted_amount, $store_currency ) ),
603+
$store_currency
604+
);
605+
}
464606
}

0 commit comments

Comments
 (0)