File manager - Edit - /home/verseaumee/maze/wp-content/plugins/woocommerce-payments/includes/class-wc-payments-captured-event-note.php
Back
<?php /** * Class WC_Payments_Captured_Event_Note * * @package WooCommerce\Payments */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * Utility class generating detailed captured note for successful payments. */ class WC_Payments_Captured_Event_Note { const HTML_BLACK_BULLET = '<span style="font-size: 7px;vertical-align: middle;">●</span>'; const HTML_WHITE_BULLET = '<span style="font-size: 7px;vertical-align: middle;">○</span>'; const HTML_SPACE = ' '; const HTML_BR = '<br>'; /** * Captured event data. * * @var array */ private $captured_event; /** * Constructor. * * @param array $captured_event Captured event data. * * @throws Exception */ public function __construct( array $captured_event ) { $is_captured_event = isset( $captured_event['type'] ) && 'captured' === $captured_event['type']; if ( ! $is_captured_event ) { throw new Exception( 'Not a captured event' ); } $this->captured_event = $captured_event; } /** * Generate the HTML note. * * @return string */ public function generate_html_note(): string { // When the server attached a fee_breakdown_v1 envelope, render from it // verbatim. This covers Amazon Pay non-card, dispute fees, partial // refunds, and future fee quirks uniformly — no per-case branches. // The server gates the envelope behind its own feature option; its // absence is the signal to run the legacy composer below. // // Defense-in-depth: skip the envelope path when its shape is // incomplete (malformed / partial payload) so we fall back to the // legacy composer instead of emitting PHP notices mid-render. if ( ! empty( $this->captured_event['fee_breakdown_v1'] ) && self::is_renderable_breakdown( $this->captured_event['fee_breakdown_v1'] ) ) { return $this->generate_html_note_from_breakdown( $this->captured_event['fee_breakdown_v1'] ); } $lines = []; $fx_string = $this->compose_fx_string(); if ( null !== $fx_string ) { $lines[] = $fx_string; } $lines[] = $this->compose_fee_string(); $fee_breakdown_lines = $this->compose_fee_break_down(); if ( null !== $fee_breakdown_lines ) { $lines = array_merge( $lines, $fee_breakdown_lines ); } if ( $this->has_tax() ) { $lines[] = $this->compose_tax_string(); } $lines[] = $this->compose_net_string(); $html = ''; foreach ( $lines as $line ) { $html .= '<p>' . $line . '</p>' . PHP_EOL; } return '<div class="captured-event-details">' . PHP_EOL . $html . '</div>'; } /** * FEE_BREAKDOWN_FORK_CLONE: remove when envelope is the only path. * * Render the HTML note from a server-driven fee_breakdown_v1 envelope. * * Derived from: generate_html_note() in the same class — the legacy * composer that builds the order note from fee_rates/transaction_details * via compose_fx_string / compose_fee_string / compose_fee_break_down / * compose_tax_string / compose_net_string. Same line order and layout, * but all arithmetic is removed: values come straight from the envelope's * totals, rows, and notes. * * Takes server-authoritative rows / totals / notes and renders one HTML * paragraph per line — mirroring the legacy layout without any of the * per-event-type branching or client-side arithmetic. * * @param array $breakdown The fee_breakdown_v1 envelope. * @return string */ private function generate_html_note_from_breakdown( array $breakdown ): string { // This HTML is persisted as an order note, so every server-provided // string reaching the `<p>` output runs through `esc_html` below — // cheap defense-in-depth against a future attacker-controlled label, // currency code, or note reaching WooCommerce verbatim, even though // the envelope itself is built by our own Fee_Breakdown_Builder and // shipped via signed transport. Currency formatters strip tags // internally, so the wrapping `esc_html` is idempotent for normal // amounts and only matters if a hostile currency code escapes their // final `$amount . ' ' . $code` concatenation. $store_currency = $breakdown['totals']['fee']['currency']; $total_fee_amount = (int) $breakdown['totals']['fee']['amount']; $total_tax_amount = (int) $breakdown['totals']['tax']['amount']; // Read capture-time net so the order note (a historical record of // the capture) doesn't drift if regenerated after a later refund // or dispute. Matches the timeline captured event's "Net payout" // line on the JS side (compose.js reads totals.capture_net too). // Falls back to totals.net for older envelopes that pre-date the // capture_net split. $total_net_amount = isset( $breakdown['totals']['capture_net']['amount'] ) ? (int) $breakdown['totals']['capture_net']['amount'] : (int) $breakdown['totals']['net']['amount']; $net_currency = $breakdown['totals']['capture_net']['currency'] ?? ( $breakdown['totals']['net']['currency'] ?? $store_currency ); $lines = []; $fx_string = $this->compose_fx_string(); if ( null !== $fx_string ) { $lines[] = $fx_string; } $total_rate_text = self::format_rate_text( $breakdown['totals']['fee']['rate'] ?? null, $store_currency ); $fee_amount_text = WC_Payments_Utils::format_explicit_currency( WC_Payments_Utils::interpret_stripe_amount( $total_fee_amount, $store_currency ), $store_currency, false ); // Server may flag the totals row with a typed `key` (e.g. // 'processing_fee' for the Amazon Pay non-card case, where our // application fee was refunded). Fall back to "Fee" otherwise. $totals_key = isset( $breakdown['totals']['fee']['key'] ) ? (string) $breakdown['totals']['fee']['key'] : ''; $fee_line_label = self::fee_label_from_key( $totals_key ); $lines[] = '' !== $total_rate_text ? sprintf( /* translators: 1: fee label (e.g. "Fee") 2: fee rate (e.g. 2.9% + $0.30) 3: monetary amount */ __( '%1$s (%2$s): %3$s', 'woocommerce-payments' ), esc_html( $fee_line_label ), esc_html( $total_rate_text ), esc_html( $fee_amount_text ) ) : sprintf( /* translators: 1: fee label (e.g. "Fee" or "Processing fee") 2: monetary amount */ __( '%1$s: %2$s', 'woocommerce-payments' ), esc_html( $fee_line_label ), esc_html( $fee_amount_text ) ); // Show the per-row breakdown when it adds information: skip it // when there's a single fee row (the "Fee (rate): amount" line // above already says everything). Sub-rows display rate only, // matching the legacy "Base fee: 2.9% + $0.30" format. $fee_rows = array_values( array_filter( $breakdown['rows'], static function ( array $row ) { return 'tax' !== ( $row['kind'] ?? '' ); } ) ); if ( count( $fee_rows ) > 1 ) { $indent = str_repeat( self::HTML_SPACE, 4 ); $sub_indent = str_repeat( self::HTML_SPACE, 8 ); foreach ( $fee_rows as $row ) { $label = self::label_from_row( $row ); $row_curr = $row['rate']['fixed_currency'] ?? ( $row['currency'] ?? $store_currency ); // Adjustment row with both percentage and fixed components // (e.g., "-0.15% + -£0.20" promo discount): render parent // label and split the components into sub-bullets, mirroring // the legacy `compose_fee_break_down` HTML_WHITE_BULLET // layout and the JS `composeAdjustmentSplitFeeRow` output. // Fusing them into one rate string reads as arithmetic // rather than two independent discount rules. $adjustment_split = self::adjustment_split_lines( $row, $row_curr, $indent, $sub_indent ); if ( null !== $adjustment_split ) { $lines = array_merge( $lines, $adjustment_split ); continue; } $rate_text = self::format_rate_text( $row['rate'] ?? null, $row_curr ); $lines[] = $indent . ( '' !== $rate_text ? sprintf( '%1$s: %2$s', esc_html( $label ), esc_html( $rate_text ) ) : esc_html( $label ) ); } } if ( 0 !== $total_tax_amount ) { // Pull description + percentage off the tax row (populated by // the server builder from Transaction_Fee_Detail tax record) // to match the legacy "Tax IT VAT (22.00%): -$X.XX" format. $tax_row = null; foreach ( $breakdown['rows'] as $candidate ) { if ( 'tax' === ( $candidate['kind'] ?? '' ) ) { $tax_row = $candidate; break; } } $tax_description = ''; if ( null !== $tax_row && ! empty( $tax_row['label'] ) ) { // `localize_tax_description_code` maps to a dictionary of // `__()` translations or returns the "Tax" fallback — it // never reflects the raw label back. `esc_html` is still // applied in case a translator ever adds markup. $tax_description = ' ' . self::localize_tax_description_code( (string) $tax_row['label'] ); } $tax_percentage = ''; if ( null !== $tax_row && isset( $tax_row['rate']['percentage'] ) && 0.0 !== (float) $tax_row['rate']['percentage'] ) { $tax_percentage = ' (' . number_format( (float) $tax_row['rate']['percentage'] * 100, 2 ) . '%)'; } $tax_amount_text = WC_Payments_Utils::format_currency( -abs( WC_Payments_Utils::interpret_stripe_amount( $total_tax_amount, $breakdown['totals']['tax']['currency'] ) ), $breakdown['totals']['tax']['currency'] ); $lines[] = sprintf( /* translators: 1: tax description 2: tax percentage 3: tax amount */ __( 'Tax%1$s%2$s: %3$s', 'woocommerce-payments' ), esc_html( $tax_description ), esc_html( $tax_percentage ), esc_html( $tax_amount_text ) ); } $lines[] = sprintf( /* translators: %s is a monetary amount */ __( 'Net payout: %s', 'woocommerce-payments' ), esc_html( WC_Payments_Utils::format_explicit_currency( WC_Payments_Utils::interpret_stripe_amount( $total_net_amount, $net_currency ), $net_currency, false ) ) ); if ( ! empty( $breakdown['notes'] ) ) { foreach ( $breakdown['notes'] as $note ) { $note_text = self::text_from_note( $note ); if ( null !== $note_text && '' !== $note_text ) { // `text_from_note` already escapes its return — we // don't re-escape here so translators can't accidentally // double-encode entities in the copy. $lines[] = $note_text; } } } $html = ''; foreach ( $lines as $line ) { $html .= '<p>' . $line . '</p>' . PHP_EOL; } return '<div class="captured-event-details">' . PHP_EOL . $html . '</div>'; } /** * Check whether an envelope has the minimum shape required by the * renderer. Returns false for missing/malformed payloads so * generate_html_note() can fall back to the legacy composer instead * of emitting PHP notices mid-render. * * We control both sides of the wire, so a malformed envelope here * represents either a rollout glitch or a manual fixture — in either * case, reverting to the legacy composer is the safer outcome than * a partially-rendered note. * * @param array $breakdown The fee_breakdown_v1 envelope candidate. * @return bool */ private static function is_renderable_breakdown( array $breakdown ): bool { return isset( $breakdown['totals']['fee']['amount'], $breakdown['totals']['fee']['currency'], $breakdown['totals']['tax']['amount'], $breakdown['rows'] ) && is_array( $breakdown['rows'] ) && ( isset( $breakdown['totals']['capture_net']['amount'] ) || isset( $breakdown['totals']['net']['amount'] ) ); } /** * Server emits a typed `key` on `totals.fee` for cases where the * default "Fee" wording is misleading — currently `processing_fee` * for the Amazon Pay non-card path where our application fee was * refunded and only Stripe's passthrough remains. Unknown or empty * keys fall back to "Fee". * * @param string $key Server-provided key, or '' when absent. * @return string */ private static function fee_label_from_key( string $key ): string { switch ( $key ) { case 'processing_fee': return __( 'Processing fee', 'woocommerce-payments' ); default: return __( 'Fee', 'woocommerce-payments' ); } } /** * Derived from the inline label-mapping inside compose_fee_break_down() * in the same class (the branches that turn `type` + `additional_type` * into "Base fee" / "International card fee" / "Currency conversion fee" * / "Discount"). Now keyed by the server's typed row key so the envelope * can teach clients new labels without a PHP release. * * @param array $row Row entry from the envelope. * @return string */ private static function label_from_row( array $row ): string { if ( ! empty( $row['label'] ) ) { // Server-provided label — escape on return so any downstream // concat into the HTML order note can't leak attacker- // controlled markup if the envelope is ever compromised // upstream. The dictionary branch below returns `__()` strings // that are plain text, so `esc_html` there is idempotent. return esc_html( (string) $row['label'] ); } $key = (string) ( $row['key'] ?? '' ); $map = [ 'base' => __( 'Base fee', 'woocommerce-payments' ), 'additional.international' => __( 'International card fee', 'woocommerce-payments' ), 'additional.fx' => __( 'Currency conversion fee', 'woocommerce-payments' ), 'additional.wcpay-subscription' => __( 'Subscription transaction fee', 'woocommerce-payments' ), 'additional.device' => __( 'Device fee', 'woocommerce-payments' ), 'tax_on_fee' => __( 'Tax on fee', 'woocommerce-payments' ), 'dispute_fee' => __( 'Dispute fee', 'woocommerce-payments' ), 'dispute_fee_refund' => __( 'Dispute fee refund', 'woocommerce-payments' ), 'refund_fee' => __( 'Refund fee', 'woocommerce-payments' ), 'financing_paydown' => __( 'Loan paydown', 'woocommerce-payments' ), ]; if ( isset( $map[ $key ] ) ) { return $map[ $key ]; } if ( 0 === strpos( $key, 'discount.' ) ) { return __( 'Discount', 'woocommerce-payments' ); } return $key; } /** * For an adjustment row (e.g. discount) carrying both a percentage and a * fixed component, return the parent label line plus two sub-bullet * lines — mirrors the legacy `compose_fee_break_down` HTML_WHITE_BULLET * layout and the JS counterpart in `compose.js::composeAdjustmentSplitFeeRow`. * * Signs are preserved (e.g. "Variable fee: -0.15%", "Fixed fee: -£0.20") * so the rendering matches the legacy snapshots; the database stores * discounts as negative deltas applied to the cumulative effective rate. * * Returns null when the row doesn't qualify (non-adjustment, no rate, or * either component is zero) so the caller can fall through to the * single-line render. * * @param array $row Row entry from the envelope. * @param string $row_curr Currency to format the fixed amount in. * @param string $indent Indent string for the parent label line. * @param string $sub_indent Indent string for the sub-bullet lines. * @return array<string>|null */ private static function adjustment_split_lines( array $row, string $row_curr, string $indent, string $sub_indent ): ?array { if ( 'adjustment' !== ( $row['kind'] ?? '' ) || empty( $row['rate'] ) ) { return null; } $rate = $row['rate']; $percentage = isset( $rate['percentage'] ) ? (float) $rate['percentage'] : 0.0; $fixed_minor = isset( $rate['fixed'] ) ? (int) $rate['fixed'] : 0; if ( 0.0 === $percentage || 0 === $fixed_minor ) { return null; } $label = self::label_from_row( $row ); $variable_text = self::format_fee( $percentage ) . '%'; $fixed_text = WC_Payments_Utils::format_currency( WC_Payments_Utils::interpret_stripe_amount( $fixed_minor, $row_curr ), $row_curr ); return [ $indent . esc_html( $label ), $sub_indent . self::HTML_WHITE_BULLET . ' ' . esc_html( sprintf( /* translators: %s is a percentage number */ __( 'Variable fee: %s', 'woocommerce-payments' ), $variable_text ) ), $sub_indent . self::HTML_WHITE_BULLET . ' ' . esc_html( sprintf( /* translators: %s is a monetary amount */ __( 'Fixed fee: %s', 'woocommerce-payments' ), $fixed_text ) ), ]; } /** * Format a fee rate (percentage + fixed) for display, matching the * legacy "2.9% + $0.30" style. Returns an empty string when the rate * has no percentage and no fixed part. * * Derived from: the sprintf('%1$s (%2$f%% + %3$s ...)') block inside * compose_fee_string() and the "capped at" branch in the same class — * extracted so the envelope path can render rates without any of the * legacy fee_rates/history plumbing. * * @param array|null $rate Rate array with percentage/fixed/fixed_currency keys. * @param string $store_currency Fallback currency for the fixed part. * @return string */ private static function format_rate_text( ?array $rate, string $store_currency ): string { if ( null === $rate ) { return ''; } // Capped fee: render "capped at $X" instead of the percent+fixed // combo, matching the legacy "Base fee: capped at $5" treatment. if ( ! empty( $rate['capped'] ) ) { $cap_amount = isset( $rate['cap_amount'] ) ? (int) $rate['cap_amount'] : (int) ( $rate['fixed'] ?? 0 ); $cap_curr = $rate['fixed_currency'] ?? $store_currency; return sprintf( /* translators: %s is a monetary amount */ __( 'capped at %s', 'woocommerce-payments' ), WC_Payments_Utils::format_currency( WC_Payments_Utils::interpret_stripe_amount( $cap_amount, $cap_curr ), $cap_curr ) ); } $parts = []; $percentage = isset( $rate['percentage'] ) ? (float) $rate['percentage'] : 0.0; $fixed_minor = isset( $rate['fixed'] ) ? (int) $rate['fixed'] : 0; $fixed_curr = $rate['fixed_currency'] ?? $store_currency; if ( 0.0 !== $percentage ) { $parts[] = self::format_fee( $percentage ) . '%'; } if ( 0 !== $fixed_minor ) { $parts[] = WC_Payments_Utils::format_currency( WC_Payments_Utils::interpret_stripe_amount( $fixed_minor, $fixed_curr ), $fixed_curr ); } return implode( ' + ', $parts ); } /** * Returns null when the note has no merchant-facing text so the caller * can suppress it — the server emits internal-only codes (e.g. refund * provenance) for telemetry and support that must never surface in the * order note as raw strings. * * @param array $note Note entry from the envelope. * @return string|null */ private static function text_from_note( array $note ): ?string { $code = (string) ( $note['code'] ?? '' ); $meta = is_array( $note['meta'] ?? null ) ? $note['meta'] : []; switch ( $code ) { case 'application_fee_refunded': $refunded_amount = isset( $meta['refunded_amount'] ) ? (int) $meta['refunded_amount'] : 0; $refunded_currency = (string) ( $meta['refunded_currency'] ?? '' ); if ( $refunded_amount <= 0 || '' === $refunded_currency ) { return __( 'WooPayments refunded its application fee on this transaction.', 'woocommerce-payments' ); } // `format_explicit_currency` strips HTML internally but // falls back to `$amount . ' ' . $currency` when the // formatted output doesn't contain the currency code — // meaning a hostile `refunded_currency` would concatenate // raw. Escape the final composed string so this can't reach // the `<p>`-wrapped order note verbatim. $formatted = WC_Payments_Utils::format_explicit_currency( WC_Payments_Utils::interpret_stripe_amount( $refunded_amount, $refunded_currency ), $refunded_currency, false ); return esc_html( sprintf( /* translators: %s is a monetary amount */ __( 'WooPayments refunded its %s application fee on this transaction.', 'woocommerce-payments' ), $formatted ) ); } // Unknown codes are internal-only — drop them silently so server-side // telemetry additions never leak raw identifiers to merchants. return null; } /** * Generate FX string. * * @return string|null */ public function compose_fx_string() { if ( ! $this->is_fx_event() ) { return null; } $customer_currency = $this->captured_event['transaction_details']['customer_currency']; $customer_amount_captured = $this->captured_event['transaction_details']['customer_amount_captured']; $store_currency = $this->captured_event['transaction_details']['store_currency']; $store_amount_captured = $this->captured_event['transaction_details']['store_amount_captured']; return $this->format_fx( $customer_currency, $customer_amount_captured, $store_currency, $store_amount_captured ); } /** * Generate fee string. * * @return string */ public function compose_fee_string(): string { $data = $this->captured_event; $fee_rates = $data['fee_rates']; $percentage = $fee_rates['percentage']; $fixed_currency = $fee_rates['fixed_currency']; $fixed = WC_Payments_Utils::interpret_stripe_amount( (int) $fee_rates['fixed'], $fixed_currency ); $history = $fee_rates['history']; if ( $this->has_tax() ) { $before_tax = $data['fee_rates']['before_tax']; $fee_amount = $before_tax['amount']; $fee_currency = $before_tax['currency']; } else { $fee_currency = $data['transaction_details']['customer_currency']; $fee_amount = (int) $data['transaction_details']['customer_fee']; } $formatted_fee_amount = $this->convert_and_format_fee_amount( $fee_amount, $fee_currency ); $base_fee_label = $this->is_base_fee_only() ? __( 'Base fee', 'woocommerce-payments' ) : __( 'Fee', 'woocommerce-payments' ); $is_capped = isset( $history[0]['capped'] ) && true === $history[0]['capped']; if ( $this->is_base_fee_only() && $is_capped ) { return sprintf( '%1$s (capped at %2$s): %3$s', $base_fee_label, WC_Payments_Utils::format_currency( $fixed, $fixed_currency ), $formatted_fee_amount ); } $is_same_symbol = $this->has_same_currency_symbol( $data['transaction_details']['store_currency'], $data['transaction_details']['customer_currency'] ); return sprintf( '%1$s (%2$s%% + %3$s%4$s): %5$s%6$s', $base_fee_label, self::format_fee( $percentage ), WC_Payments_Utils::format_currency( $fixed, $fixed_currency ), $is_same_symbol ? ' ' . $data['transaction_details']['customer_currency'] : '', $formatted_fee_amount, $is_same_symbol ? " {$data['transaction_details']['store_currency']}" : '' ); } /** * Generate an array including HTML formatted breakdown lines. * * @return array<string>|null */ public function compose_fee_break_down() { $fee_history_strings = $this->get_fee_breakdown(); if ( null === $fee_history_strings ) { return null; } if ( 0 === count( $fee_history_strings ) ) { return null; } $res = []; foreach ( $fee_history_strings as $type => $fee ) { $res[] = self::HTML_BLACK_BULLET . ' ' . ( 'discount' === $type ? $fee['label'] : $fee ); if ( 'discount' === $type ) { $res[] = str_repeat( self::HTML_SPACE . ' ', 2 ) . self::HTML_WHITE_BULLET . ' ' . $fee['variable']; $res[] = str_repeat( self::HTML_SPACE . ' ', 2 ) . self::HTML_WHITE_BULLET . ' ' . $fee['fixed']; } } return $res; } /** * Generate net string. * * @return string */ public function compose_net_string(): string { $data = $this->captured_event['transaction_details']; // Determine the type of payment and select the appropriate amounts and currencies. if ( $this->is_fx_event() ) { // For fx events, we need the store amount and currency to display the net amount // in the store currency. $amount = $data['store_amount']; $captured_amount = $data['store_amount_captured']; $fee = $data['store_fee']; $currency = $data['store_currency']; } else { $amount = $data['customer_amount']; $captured_amount = $data['customer_amount_captured']; $fee = $data['customer_fee']; $currency = $data['customer_currency']; } $gross_amount = $captured_amount ?? $amount; $net = WC_Payments_Utils::interpret_stripe_amount( (int) ( $gross_amount - $fee ), $currency ); // Format and return the net string. return sprintf( /* translators: %s is a monetary amount */ __( 'Net payout: %s', 'woocommerce-payments' ), WC_Payments_Utils::format_explicit_currency( $net, $currency ) ); } /** * Returns an associative array containing fee breakdown. * Keys are fee types such as base, additional-fx, etc, except for "discount" that is an associative array including more discount details. * * @return array|null */ public function get_fee_breakdown() { $data = $this->captured_event; if ( ! isset( $data['fee_rates']['history'] ) ) { return null; } $history = $data['fee_rates']['history']; // Hide breakdown when there's only a base fee. if ( $this->is_base_fee_only() ) { return null; } $fee_history_strings = []; foreach ( $history as $fee ) { $label_type = $fee['type']; if ( $fee['additional_type'] ?? '' ) { $label_type .= '-' . $fee['additional_type']; } $percentage_rate = (float) $fee['percentage_rate']; $fixed_rate = (int) $fee['fixed_rate']; $currency = strtoupper( $fee['currency'] ); $is_capped = isset( $fee['capped'] ) && true === $fee['capped']; $percentage_rate_formatted = self::format_fee( $percentage_rate ); $fix_rate_formatted = WC_Payments_Utils::format_currency( WC_Payments_Utils::interpret_stripe_amount( $fixed_rate ), $currency ); if ( $this->has_same_currency_symbol( $data['transaction_details']['customer_currency'], $data['transaction_details']['store_currency'] ) ) { $fix_rate_formatted = $fix_rate_formatted . ' ' . $data['transaction_details']['store_currency']; } $label = sprintf( $this->fee_label_mapping( $fixed_rate, $is_capped )[ $label_type ], $percentage_rate_formatted, $fix_rate_formatted ); if ( 'discount' === $label_type ) { $fee_history_strings[ $label_type ] = [ 'label' => $label, 'variable' => sprintf( /* translators: %s is a percentage number */ __( 'Variable fee: %s', 'woocommerce-payments' ), $percentage_rate_formatted ) . '%', 'fixed' => sprintf( /* translators: %s is a monetary amount */ __( 'Fixed fee: %s', 'woocommerce-payments' ), $fix_rate_formatted ), ]; } else { $fee_history_strings[ $label_type ] = $label; } } return $fee_history_strings; } /** * Compose tax string. * * @return string|null */ public function compose_tax_string(): ?string { if ( ! $this->has_tax() ) { return null; } $tax = $this->captured_event['fee_rates']['tax']; $tax_amount = $tax['amount']; if ( 0 === $tax_amount ) { return null; } $tax_currency = $tax['currency']; $formatted_amount = $this->convert_and_format_fee_amount( $tax_amount, $tax_currency ); $tax_description = ' ' . $this->get_localized_tax_description(); $percentage_rate = $tax['percentage_rate']; $formatted_percentage = ' (' . self::format_fee( $percentage_rate ) . '%)'; return sprintf( /* translators: 1: tax description 2: tax percentage 3: tax amount */ __( 'Tax%1$s%2$s: %3$s', 'woocommerce-payments' ), $tax_description, $formatted_percentage, $formatted_amount ); } /** * Check if this is a FX event. * * @return bool */ private function is_fx_event(): bool { $customer_currency = $this->captured_event['transaction_details']['customer_currency'] ?? null; $store_currency = $this->captured_event['transaction_details']['store_currency'] ?? null; return ! ( is_null( $customer_currency ) || is_null( $store_currency ) || $customer_currency === $store_currency ); } /** * Return a boolean indicating whether only fee applied is the base fee. * * @return bool True if the only applied fee is the base fee */ private function is_base_fee_only(): bool { if ( ! isset( $this->captured_event['fee_rates']['history'] ) ) { return false; } $history = $this->captured_event['fee_rates']['history']; return 1 === ( is_countable( $history ) ? count( $history ) : 0 ) && 'base' === $history[0]['type']; } /** * Get the mapping format for all types of fees. * * @param int $fixed_rate Fixed rate amount in Stripe format. * @param bool $is_capped True if the fee is capped. * * @return array An associative array with keys are fee types, values are string formats. */ private function fee_label_mapping( int $fixed_rate, bool $is_capped ) { $res = []; $res['base'] = $is_capped /* translators: %2$s is the capped fee */ ? __( 'Base fee: capped at %2$s', 'woocommerce-payments' ) : ( 0 !== $fixed_rate /* translators: %1$s% is the fee percentage and %2$s is the fixed rate */ ? __( 'Base fee: %1$s%% + %2$s', 'woocommerce-payments' ) /* translators: %1$s% is the fee percentage */ : __( 'Base fee: %1$s%%', 'woocommerce-payments' ) ); $res['additional-international'] = 0 !== $fixed_rate /* translators: %1$s% is the fee percentage and %2$s is the fixed rate */ ? __( 'International card fee: %1$s%% + %2$s', 'woocommerce-payments' ) /* translators: %1$s% is the fee percentage */ : __( 'International card fee: %1$s%%', 'woocommerce-payments' ); $res['additional-fx'] = 0 !== $fixed_rate /* translators: %1$s% is the fee percentage and %2$s is the fixed rate */ ? __( 'Currency conversion fee: %1$s%% + %2$s', 'woocommerce-payments' ) /* translators: %1$s% is the fee percentage */ : __( 'Currency conversion fee: %1$s%%', 'woocommerce-payments' ); $res['additional-wcpay-subscription'] = 0 !== $fixed_rate /* translators: %1$s% is the fee percentage and %2$s is the fixed rate */ ? __( 'Subscription transaction fee: %1$s%% + %2$s', 'woocommerce-payments' ) /* translators: %1$s% is the fee percentage */ : __( 'Subscription transaction fee: %1$s%%', 'woocommerce-payments' ); $res['discount'] = __( 'Discount', 'woocommerce-payments' ); return $res; } /** * Return a given decimal fee as a percentage with a maximum of 3 decimal places. * * @param float $percentage Percentage as float. * * @return string */ private static function format_fee( float $percentage ): string { return (string) round( $percentage * 100, 3 ); } /** * Format FX string based on the two provided currencies. * * @param string $from_currency 3-letter code for original currency. * @param int $from_amount Amount (Stripe-type) for original currency. * @param string $to_currency 3-letter code for converted currency. * @param int $to_amount Amount (Stripe-type) for converted currency. * * @return string Formatted FX string. */ private function format_fx( string $from_currency, int $from_amount, string $to_currency, int $to_amount ): string { $exchange_rate = (float) ( 0 !== $from_amount ? $to_amount / $from_amount : 0 ); if ( WC_Payments_Utils::is_zero_decimal_currency( strtolower( $to_currency ) ) ) { $exchange_rate *= 100; } if ( WC_Payments_Utils::is_zero_decimal_currency( strtolower( $from_currency ) ) ) { $exchange_rate /= 100; } $to_display_amount = WC_Payments_Utils::interpret_stripe_amount( $to_amount, $to_currency ); return sprintf( '%1$s → %2$s: %3$s', self::format_explicit_currency_with_base( 1, $from_currency, $to_currency, true ), self::format_exchange_rate( $exchange_rate, $to_currency ), WC_Payments_Utils::format_explicit_currency( $to_display_amount, $to_currency, false ) ); } /** * Format exchange rate. * * @param float $rate Exchange rate. * @param string $currency 3-letter currency code. * * @return string */ private function format_exchange_rate( float $rate, string $currency ): string { $num_decimals = $rate > 1 ? 5 : 6; $formatted = WC_Payments_Utils::format_explicit_currency( $rate, $currency, true, [ 'decimals' => $num_decimals ] ); $func_remove_ending_zeros = function ( $str ) { return rtrim( $str, '0' ); }; // Remove ending zeroes after the decimal separator if they exist. return implode( ' ', array_map( $func_remove_ending_zeros, explode( ' ', $formatted ) ) ); } /** * Format amount for a given currency but according to the base currency's format. * * @param float $amount Amount. * @param string $currency 3-letter currency code. * @param string $base_currency 3-letter base currency code. * @param bool $skip_symbol Optional. If true, trims off the short currency symbol. Default false. * * @return string */ private function format_explicit_currency_with_base( float $amount, string $currency, string $base_currency, bool $skip_symbol = false ) { $custom_format = WC_Payments_Utils::get_currency_format_for_wc_price( $base_currency ); unset( $custom_format['currency'] ); // Given this is used to display the $amount, the decimals for $base_currency shouldn't interfere with decimals for $currency. $custom_format['decimals'] = WC_Payments_Utils::get_currency_format_for_wc_price( $currency )['decimals']; return WC_Payments_Utils::format_explicit_currency( $amount, $currency, $skip_symbol, $custom_format ); } /** * Compare does two currencies have the same symbol. * * @param string $base_currency Base currency. * @param string $currency Currency to compare. * * @return bool */ private function has_same_currency_symbol( string $base_currency, string $currency ): bool { return strcasecmp( $base_currency, $currency ) !== 0 && get_woocommerce_currency_symbol( $base_currency ) === get_woocommerce_currency_symbol( $currency ); } /** * Check if the event has tax information. * * @return bool */ private function has_tax(): bool { return isset( $this->captured_event['fee_rates']['tax'] ); } /** * Get localized tax description based on the tax description ID contained in the captured event. * * @return string|null */ private function get_localized_tax_description(): ?string { if ( ! isset( $this->captured_event['fee_rates']['tax']['description'] ) ) { return null; } return self::localize_tax_description_code( $this->captured_event['fee_rates']['tax']['description'] ); } /** * Localize a raw tax description code (e.g. "IT VAT" → "IT VAT" in * the active locale, or "Tax" if the code is unknown). * * @param string $tax_description_id Raw code like "IT VAT" or "JP JCT". * @return string */ private static function localize_tax_description_code( string $tax_description_id ): string { $tax_descriptions = [ // European Union VAT. 'AT VAT' => __( 'AT VAT', 'woocommerce-payments' ), // Austria. 'BE VAT' => __( 'BE VAT', 'woocommerce-payments' ), // Belgium. 'BG VAT' => __( 'BG VAT', 'woocommerce-payments' ), // Bulgaria. 'CY VAT' => __( 'CY VAT', 'woocommerce-payments' ), // Cyprus. 'CZ VAT' => __( 'CZ VAT', 'woocommerce-payments' ), // Czech Republic. 'DE VAT' => __( 'DE VAT', 'woocommerce-payments' ), // Germany. 'DK VAT' => __( 'DK VAT', 'woocommerce-payments' ), // Denmark. 'EE VAT' => __( 'EE VAT', 'woocommerce-payments' ), // Estonia. 'ES VAT' => __( 'ES VAT', 'woocommerce-payments' ), // Spain. 'FI VAT' => __( 'FI VAT', 'woocommerce-payments' ), // Finland. 'FR VAT' => __( 'FR VAT', 'woocommerce-payments' ), // France. 'GB VAT' => __( 'UK VAT', 'woocommerce-payments' ), // United Kingdom. 'GR VAT' => __( 'GR VAT', 'woocommerce-payments' ), // Greece. 'HR VAT' => __( 'HR VAT', 'woocommerce-payments' ), // Croatia. 'HU VAT' => __( 'HU VAT', 'woocommerce-payments' ), // Hungary. 'IE VAT' => __( 'IE VAT', 'woocommerce-payments' ), // Ireland. 'IT VAT' => __( 'IT VAT', 'woocommerce-payments' ), // Italy. 'LT VAT' => __( 'LT VAT', 'woocommerce-payments' ), // Lithuania. 'LU VAT' => __( 'LU VAT', 'woocommerce-payments' ), // Luxembourg. 'LV VAT' => __( 'LV VAT', 'woocommerce-payments' ), // Latvia. 'MT VAT' => __( 'MT VAT', 'woocommerce-payments' ), // Malta. 'NO VAT' => __( 'NO VAT', 'woocommerce-payments' ), // Norway. 'NL VAT' => __( 'NL VAT', 'woocommerce-payments' ), // Netherlands. 'PL VAT' => __( 'PL VAT', 'woocommerce-payments' ), // Poland. 'PT VAT' => __( 'PT VAT', 'woocommerce-payments' ), // Portugal. 'RO VAT' => __( 'RO VAT', 'woocommerce-payments' ), // Romania. 'SE VAT' => __( 'SE VAT', 'woocommerce-payments' ), // Sweden. 'SI VAT' => __( 'SI VAT', 'woocommerce-payments' ), // Slovenia. 'SK VAT' => __( 'SK VAT', 'woocommerce-payments' ), // Slovakia. // GST Countries. 'AU GST' => __( 'AU GST', 'woocommerce-payments' ), // Australia. 'NZ GST' => __( 'NZ GST', 'woocommerce-payments' ), // New Zealand. 'SG GST' => __( 'SG GST', 'woocommerce-payments' ), // Singapore. // Other Tax Systems. 'CH VAT' => __( 'CH VAT', 'woocommerce-payments' ), // Switzerland. 'JP JCT' => __( 'JP JCT', 'woocommerce-payments' ), // Japan Consumption Tax. ]; return $tax_descriptions[ $tax_description_id ] ?? __( 'Tax', 'woocommerce-payments' ); } /** * Given the fee amount and currency, converts it to the store currency if necessary and formats using formatCurrency. * * @param float $fee_amount Fee amount to convert and format. * @param string $fee_currency Fee currency to convert from. * * @return string Formatted fee amount in the store currency. */ private function convert_and_format_fee_amount( float $fee_amount, string $fee_currency ) { $fee_exchange_rate = $this->captured_event['fee_rates']['fee_exchange_rate'] ?? null; $store_currency = $this->captured_event['transaction_details']['store_currency'] ?? null; if ( ( strtoupper( $fee_currency ) === strtoupper( $store_currency ) ) || ! $this->is_fx_event() || ! $fee_exchange_rate ) { return WC_Payments_Utils::format_currency( -abs( WC_Payments_Utils::interpret_stripe_amount( $fee_amount, $fee_currency ) ), $fee_currency ); } $rate = $fee_exchange_rate['rate']; $from_currency = $fee_exchange_rate['from_currency'] ?? null; // Convert based on the direction of the exchange rate. $converted_amount = strtoupper( $fee_currency ) === strtoupper( $from_currency ) ? $fee_amount / $rate // Converting from store currency to customer currency. : $fee_amount * $rate; // Converting from customer currency to store currency. return WC_Payments_Utils::format_currency( -abs( WC_Payments_Utils::interpret_stripe_amount( $converted_amount, $store_currency ) ), $store_currency ); } }
| ver. 1.4 |
Github
|
.
| PHP 8.5.7 | Generation time: 0 |
proxy
|
phpinfo
|
Settings