Files
2026-04-28 15:13:50 +02:00

803 lines
24 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace WPO\IPS\EDI\Abstracts;
use WPO\IPS\EDI\Interfaces\HandlerInterface;
use WPO\IPS\EDI\Document;
use WPO\IPS\EDI\Standards\EN16931;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
abstract class AbstractHandler implements HandlerInterface {
public Document $document;
/**
* Constructor.
*
* @param Document $document The document instance.
*/
public function __construct( Document $document ) {
$this->document = $document;
}
/**
* Handle the data and return the formatted output.
*
* @param array $data The data to be handled.
* @param array $options Additional options for handling.
* @return array
*/
abstract public function handle( array $data, array $options = array() ): array;
/**
* Get the order customer VAT number.
*
* @param \WC_Order $order
* @return string|null
*/
protected function get_order_customer_vat_number( \WC_Order $order ): ?string {
return apply_filters(
'wpo_ips_edi_order_customer_vat_number',
wpo_wcpdf_get_order_customer_vat_number( $order ),
$order
);
}
/**
* Get the supplier identifiers data.
*
* @param string $key The data key (e.g., 'shop_name', 'coc_number', 'shop_address_line_1', 'shop_address_postcode').
* @return string
*/
protected function get_supplier_identifiers_data( string $key ): string {
$general_settings = WPO_WCPDF()->settings->general;
$language = wpo_ips_edi_get_settings( 'supplier_identifiers_language' );
if ( empty( $language ) ) {
$language = 'default';
}
return $general_settings->get_setting( $key, $language ) ?: '';
}
/**
* Returns the due date for the document.
*
* @return string
*/
protected function get_due_date(): string {
$due_date = is_callable( array( $this->document->order_document, 'get_due_date' ) )
? $this->document->order_document->get_due_date()
: 0;
return $this->normalize_date( $due_date, 'Y-m-d' );
}
/**
* Returns the due date days for the document.
*
* @return int
*/
protected function get_due_date_days(): int {
$due_date_days = is_callable( array( $this->document->order_document, 'get_setting' ) )
? absint( $this->document->order_document->get_setting( 'due_date_days' ) )
: 0;
return apply_filters( 'wpo_ips_edi_due_date_days', $due_date_days, $this->document->order_document, $this );
}
/**
* Get normalized WooCommerce payment data for the current order.
*
* @return array {
* @type string $type_code EN16931 TypeCode
* @type string $method WooCommerce method ID
* @type string $title WooCommerce method title
* @type string $iban Optional IBAN
* @type string $bic Optional BIC
* @type string $transaction_id Optional transaction ID
* @type string $account_name Optional account name
* }
*/
protected function get_payment_means_data(): array {
$order = \wpo_ips_edi_get_parent_order( $this->document->order );
$method_id = $order ? $order->get_payment_method() : '';
$title = $order ? $order->get_payment_method_title() : '';
$mapping = apply_filters( 'wpo_ips_edi_payment_means_code_mapping', array(
'cheque' => '20', // Cheque
'bacs' => '58', // SEPA Credit Transfer
'paypal' => '68', // Online payment
'stripe' => '54', // Credit card
'cod' => '46', // Interbank debit transfer
'default' => '97', // Clearing between partners
), $method_id, EN16931::get_payment(), $this );
$type_code = $mapping[ $method_id ] ?? $mapping['default'];
$data = array(
'type_code' => $type_code,
'method' => $method_id,
'title' => $title,
);
switch ( $method_id ) {
case 'bacs':
$accounts = get_option( 'woocommerce_bacs_accounts', array() );
if ( empty( $accounts ) ) {
break;
}
$account = apply_filters( 'wpo_ips_edi_payment_means_bacs_default_account', reset( $accounts ), $accounts, $this );
$data = array_merge( $data, $account );
break;
case 'paypal':
$data['transaction_id'] = $order->get_transaction_id();
break;
case 'stripe':
$data['transaction_id'] = $order->get_meta( '_stripe_source_id', true );
break;
}
return apply_filters( 'wpo_ips_edi_payment_means_data', $data, $method_id, $order, $this );
}
/**
* Normalize a raw date input into a specific format.
*
* @param mixed $raw The input date (DateTime, string, timestamp, etc.)
* @param string $format The output format (default: 'Y-m-d')
* @return string
*/
protected function normalize_date( $raw, string $format = 'Y-m-d' ): string {
if ( empty( $raw ) ) {
return '';
}
// Handle UNIX timestamp (int or numeric string)
if ( is_numeric( $raw ) && (int) $raw > 1000000000 ) {
try {
$datetime = new \DateTimeImmutable( '@' . (int) $raw );
$datetime = $datetime->setTimezone( wp_timezone() );
return $datetime->format( $format );
} catch ( \Exception $e ) {
return '';
}
}
// Handle DateTimeInterface objects
if ( $raw instanceof \DateTimeInterface ) {
return $raw->format( $format );
}
// Try WooCommerce string parser
if ( function_exists( 'wc_string_to_datetime' ) ) {
try {
$datetime = \wc_string_to_datetime( $raw );
return $datetime->format( $format );
} catch ( \Exception $e ) {
// fallback below
}
}
// Fallback with strtotime
$timestamp = strtotime( $raw );
if ( $timestamp ) {
$datetime = new \DateTimeImmutable( '@' . $timestamp );
$datetime = $datetime->setTimezone( function_exists( 'wp_timezone' ) ? \wp_timezone() : new \DateTimeZone( 'UTC' ) );
return $datetime->format( $format );
}
return '';
}
/**
* Format a decimal number to a string with fixed decimal places,
* using WooCommerce normalization and avoiding scientific notation.
*
* @param float|string $amount The amount to format.
* @param int $decimal_places Number of decimal places (default: 2).
* @return string
*/
protected function format_decimal( $amount, int $decimal_places = 2 ): string {
// Normalize using WooCommerce helper (handles locale, strings, etc.).
$value = (float) wc_format_decimal( $amount, $decimal_places, false );
// Treat tiny float residue as zero (10^-(decimal_places + 2)).
$tiny_threshold = pow( 10, - ( $decimal_places + 2 ) );
if ( abs( $value ) < $tiny_threshold ) {
$value = 0.0;
}
// Round to 2dp and avoid "-0.00".
$value = round( $value, $decimal_places );
if ( $value === -0.0 ) {
$value = 0.0;
}
// Emit plain decimal string (no exponent).
return number_format( $value, $decimal_places, '.', '' );
}
/**
* Get normalized zero-tax meta (scheme/category/reason), with filter support.
*
* @param \WC_Order_Item|\WC_Abstract_Order|null $context Optional context.
* @return array
*/
protected function get_zero_tax_meta( $context = null ): array {
$defaults = array(
'scheme' => 'VAT',
'category' => 'Z',
'reason' => 'NONE',
);
$meta = apply_filters(
'wpo_ips_edi_zero_tax_meta',
$defaults,
$context,
$this
);
$scheme = strtoupper( trim( (string) ( $meta['scheme'] ?? $defaults['scheme'] ) ) );
$category = strtoupper( trim( (string) ( $meta['category'] ?? $defaults['category'] ) ) );
$reason = strtoupper( trim( (string) ( $meta['reason'] ?? $defaults['reason'] ) ) );
if ( '' === $scheme ) {
$scheme = $defaults['scheme'];
}
if ( '' === $category ) {
$category = $defaults['category'];
}
if ( '' === $reason ) {
$reason = $defaults['reason'];
}
return compact( 'scheme', 'category', 'reason' );
}
/**
* Get grouped order tax data by rate, category, reason, and scheme.
*
* @return array
*/
protected function get_grouped_order_tax_data(): array {
$order = $this->document->order;
$groups = array();
$line_items = $order->get_items( array( 'line_item', 'fee', 'shipping' ) );
$order_category = wpo_ips_edi_get_tax_data_from_fallback( 'category', null, $order );
$order_reason = wpo_ips_edi_get_tax_data_from_fallback( 'reason', null, $order );
$order_scheme = wpo_ips_edi_get_tax_data_from_fallback( 'scheme', null, $order );
$zero_meta = $this->get_zero_tax_meta( $order );
$zero_category = $zero_meta['category'];
$zero_scheme = $zero_meta['scheme'];
$zero_reason = $zero_meta['reason'];
foreach ( $line_items as $item ) {
$parts = $this->compute_item_price_parts( $item, false );
$net_total = (float) $this->format_decimal( $parts['net_total'], 2 );
// Skip zero-value lines for grouping; Z group will be enforced separately.
if ( 0.0 === $net_total ) {
continue;
}
// Tax meta (scheme, category, percentage).
$tax_meta = $this->resolve_item_tax_meta( $item );
$percentage = (float) $tax_meta['percentage'];
$category = strtoupper( trim( (string) ( $tax_meta['category'] ?? $order_category ) ) );
$scheme = strtoupper( trim( (string) ( $tax_meta['scheme'] ?? $order_scheme ) ) );
if ( '' === $scheme ) {
$scheme = 'VAT';
}
if ( '' === $category ) {
$category = ( 0.0 === $percentage ) ? $zero_category : 'S';
}
// Reason:
// - if we have an order-level reason, use it;
// - otherwise, if this is a zero-tax line, use zero-tax reason;
// - otherwise, default to NONE.
$reason = strtoupper( trim( (string) $order_reason ) );
if ( '' === $reason || 'NONE' === $reason ) {
if ( 0.0 === $percentage && $category === $zero_category && $scheme === $zero_scheme ) {
$reason = $zero_reason;
} else {
$reason = 'NONE';
}
}
$key = implode( '|', array( $percentage, $category, $reason, $scheme ) );
// Item tax = sum of item tax rows (matching Woo's own storage).
$item_tax_rows = $this->get_item_tax_rows( $item );
$item_tax = 0.0;
foreach ( $item_tax_rows as $tax_amt ) {
if ( is_numeric( $tax_amt ) ) {
$item_tax += (float) $tax_amt;
}
}
if ( ! isset( $groups[ $key ] ) ) {
$groups[ $key ] = array(
'total_ex' => 0.0,
'total_tax' => 0.0,
'percentage' => $percentage,
'category' => $category,
'reason' => $reason,
'scheme' => $scheme,
'name' => '',
);
}
$groups[ $key ]['total_ex'] += $net_total;
$groups[ $key ]['total_tax'] += $item_tax;
}
// No tax lines at all: one zero-tax group with whole net.
if ( empty( $groups ) ) {
$lines_net = $this->get_lines_net_total( $order );
$groups[ sprintf( '0|%s|%s|%s', $zero_category, $zero_reason, $zero_scheme ) ] = array(
'total_ex' => $lines_net,
'total_tax' => 0.0,
'percentage' => 0.0,
'category' => $zero_category,
'reason' => $zero_reason,
'scheme' => $zero_scheme,
'name' => '',
);
}
// Enforce exactly one zero tax group if needed.
$groups = $this->ensure_one_zero_tax_group( $groups );
// Reindex so callers get a numeric array.
$groups = array_values( $groups );
return apply_filters( 'wpo_ips_edi_order_tax_data', $groups, $this );
}
/**
* Consolidate and ensure exactly one zero tax group in the grouped tax data.
*
* @param array $grouped_tax_data Grouped tax data.
* @return array Updated grouped tax data.
*/
protected function ensure_one_zero_tax_group( array $grouped_tax_data ): array {
$zero_meta = $this->get_zero_tax_meta( $this->document->order );
$zero_category = $zero_meta['category'];
$zero_scheme = $zero_meta['scheme'];
$zero_reason = $zero_meta['reason'];
$z_first_key = null;
// Consolidate any existing "zero tax" groups from $grouped_tax_data.
foreach ( $grouped_tax_data as $key => $g ) {
if (
strtoupper( $g['category'] ?? '' ) === strtoupper( $zero_category ) &&
(float) ( $g['percentage'] ?? 0 ) === 0.0 &&
strtoupper( $g['scheme'] ?? '' ) === strtoupper( $zero_scheme )
) {
if ( is_null( $z_first_key ) ) {
$z_first_key = $key;
} else {
unset( $grouped_tax_data[ $key ] );
}
}
}
// Compute the zero-tax taxable basis strictly from lines.
$z_basis_from_lines = 0.0;
$has_z_line = false;
foreach ( $this->document->order->get_items( array( 'line_item', 'fee', 'shipping' ) ) as $item ) {
$line_total = (float) $item->get_total();
$taxes = $item->get_taxes();
$rows = ( is_array( $taxes['total'] ?? null ) ) ? $taxes['total'] : array();
// Does this line have any non-zero tax amount?
$has_nonzero_row = false;
foreach ( $rows as $amt ) {
if ( is_numeric( $amt ) && (float) $amt !== 0.0 ) {
$has_nonzero_row = true;
break;
}
}
$line_is_z = false;
if ( $has_nonzero_row ) {
// Classify by the non-zero row's category/rate.
foreach ( $rows as $tax_id => $amt ) {
if ( ! is_numeric( $amt ) || (float) $amt === 0.0 ) {
continue;
}
$info = $this->document->order_tax_data[ $tax_id ] ?? array();
$cat = strtoupper( $info['category'] ?? '' );
$rate = (float) ( $info['percentage'] ?? 0 );
// zero-tax lines are either explicitly zero-category or 0% rate.
if ( $cat === strtoupper( $zero_category ) || 0.0 === $rate ) {
$line_is_z = true;
break;
}
}
} else {
// No non-zero tax rows at all → treat as zero-tax (uses zero_category).
$line_is_z = true;
}
if ( $line_is_z ) {
$has_z_line = true;
$z_basis_from_lines += $line_total; // contributes to zero-tax taxable amount
}
}
// Ensure exactly one zero-tax group if there is any zero-tax line (even with basis 0).
if ( $has_z_line || $z_first_key ) {
$z_key = $z_first_key ?: sprintf( '0|%s|%s|%s', $zero_category, $zero_reason, $zero_scheme );
$grouped_tax_data[ $z_key ] = array(
'total_ex' => $this->format_decimal( wc_round_tax_total( $z_basis_from_lines ) ),
'total_tax' => '0.00',
'percentage' => '0.0',
'category' => $zero_category,
'reason' => $zero_reason,
'scheme' => $zero_scheme,
'name' => ( ! is_null( $z_first_key ) && isset( $grouped_tax_data[ $z_first_key ]['name'] ) )
? $grouped_tax_data[ $z_first_key ]['name']
: '',
);
}
return $grouped_tax_data;
}
/**
* Get calculated payment totals for an order.
*
* @param \WC_Abstract_Order $order
* @return array
*/
protected function get_order_payment_totals( \WC_Abstract_Order $order ): array {
$grouped_tax_data = $this->get_grouped_order_tax_data();
$total_exc_raw = 0.0;
$total_tax_raw = 0.0;
foreach ( $grouped_tax_data as $g ) {
$ex_base = (float) ( $g['total_ex'] ?? 0 );
$rate = (float) ( $g['percentage'] ?? 0 );
// Sum taxable base
$total_exc_raw += $ex_base;
// Tax per category = base × rate / 100, rounded as tax.
$tax_calc = wc_round_tax_total( $ex_base * $rate / 100 );
$total_tax_raw += $tax_calc;
}
// Invoice total amount without VAT.
$total_exc_tax = (float) $this->format_decimal( $total_exc_raw, 2 );
// Invoice total VAT amount (sum of category tax).
$total_tax = (float) $this->format_decimal( $total_tax_raw, 2 );
// Invoice total amount with VAT = total_exc_tax + total_tax.
$total_inc_tax = (float) $this->format_decimal( $total_exc_tax + $total_tax, 2 );
// For diagnostics also compute invoice line net sum.
$lines_net = (float) $this->get_lines_net_total( $order );
$lines_net_rounded = (float) $this->format_decimal( $lines_net, 2 );
// Log if base of groups and base from lines differ materially.
$base_diff = $total_exc_tax - $lines_net_rounded;
if ( abs( $base_diff ) >= 0.01 ) {
wpo_ips_edi_log(
sprintf(
'Base mismatch for order #%d: grouped_ex=%s, lines_net=%s, diff=%s',
$order->get_id(),
$this->format_decimal( $total_exc_tax ),
$this->format_decimal( $lines_net_rounded ),
$this->format_decimal( $base_diff )
),
'warning'
);
}
// Reconcile to WooCommerce order total.
$order_total = (float) $order->get_total();
$rounding_diff = wc_round_tax_total( $order_total - $total_inc_tax );
// Config / inputs.
$has_due_days = ! empty( $this->get_due_date_days() );
$prepaid_amount = (float) apply_filters( 'wpo_ips_edi_prepaid_amount', 0.0, $order, $this );
// Threshold for treating rounding diff as significant.
$rounding_is_significant = ( abs( $rounding_diff ) >= 0.01 );
if ( $rounding_is_significant ) {
wpo_ips_edi_log(
'Rounding difference detected for order #' . $order->get_id() . ': ' .
'order_total=' . $order_total . ', total_inc_tax=' . $total_inc_tax .
', total_exc_tax=' . $total_exc_tax . ', total_tax=' . $total_tax .
', rounding_diff=' . $rounding_diff,
'warning'
);
}
// Gross invoice amount including rounding (should equal Woo order total).
$gross_total = (float) $this->format_decimal( $total_inc_tax + $rounding_diff, 2 );
// Default rule:
// - If there's NO due date AND no explicit prepaid set, treat as fully prepaid (paid on issue).
// - Otherwise, use the provided prepaid (or 0) and compute payable normally.
if ( $prepaid_amount <= 0.0 && ! $has_due_days ) {
// Fully prepaid by default.
$prepaid_amount = $gross_total;
$payable_amount = 0.0;
} else {
// Not fully prepaid; customer owes the remainder.
$payable_amount = $gross_total - $prepaid_amount;
}
$totals = compact(
'total_exc_tax',
'total_inc_tax',
'total_tax',
'prepaid_amount',
'rounding_diff',
'payable_amount',
'lines_net'
);
return apply_filters( 'wpo_ips_edi_order_payment_totals', $totals, $order, $this );
}
/**
* Get sum of line net amounts (excl. VAT).
*
* @param \WC_Abstract_Order $order
* @return float
*/
protected function get_lines_net_total( \WC_Abstract_Order $order ): float {
$lines_net = 0.0;
$include_coupon = apply_filters( 'wpo_ips_edi_ubl_discount_as_line', false, $this );
$line_items = $order->get_items( array( 'line_item', 'fee', 'shipping' ) );
foreach ( $line_items as $item ) {
$parts = $this->compute_item_price_parts( $item, (bool) $include_coupon );
$line_net_total = (float) $this->format_decimal( $parts['net_total'], 2 );
$lines_net += $line_net_total;
}
// If discounts are rendered as separate lines, include them as negative net amounts.
if ( $include_coupon ) {
$coupons = $order->get_items( 'coupon' );
foreach ( $coupons as $coupon_item ) {
if ( ! is_object( $coupon_item ) || ! method_exists( $coupon_item, 'get_discount' ) ) {
continue;
}
$discount_excl_tax = (float) $coupon_item->get_discount();
$net_total = -1.0 * (float) $this->format_decimal( $discount_excl_tax, 2 );
if ( 0.0 === $net_total ) {
continue;
}
$lines_net += $net_total;
}
}
return (float) $this->format_decimal( $lines_net, 2 );
}
/**
* Get the tax rows bucket for an order item ('subtotal' for products, 'total' otherwise).
*
* @param \WC_Order_Item $item
* @return array
*/
protected function get_item_tax_rows( \WC_Order_Item $item ): array {
$type = $item->get_type();
$taxes = $item->get_taxes();
$tax_bucket = ( 'line_item' === $type ) ? 'subtotal' : 'total';
return ( isset( $taxes[ $tax_bucket ] ) && is_array( $taxes[ $tax_bucket ] ) )
? $taxes[ $tax_bucket ]
: array();
}
/**
* Resolve tax meta (scheme/category/percentage) for an item by inspecting its first non-zero tax row.
*
* @param \WC_Order_Item $item
* @return array
*/
protected function resolve_item_tax_meta( \WC_Order_Item $item ): array {
$order_tax_data = $this->document->order_tax_data;
$scheme = 'VAT';
$category = null;
$percent = 0.0;
$rows = $this->get_item_tax_rows( $item );
foreach ( $rows as $tax_id => $tax_amt ) {
if ( ! is_numeric( $tax_amt ) || (float) $tax_amt == 0.0 ) {
continue;
}
$row = $order_tax_data[ $tax_id ] ?? array();
$scheme = strtoupper( $row['scheme'] ?? 'VAT' );
$category = strtoupper( $row['category'] ?? 'Z' );
$percent = (float) ( $row['percentage'] ?? 0 );
break;
}
// Fallback: no non-zero rows -> use zero-tax meta (0%).
if ( null === $category ) {
$percent = 0.0;
$zero_meta = $this->get_zero_tax_meta( $item );
$scheme = $zero_meta['scheme'];
$category = $zero_meta['category'];
}
return array(
'scheme' => $scheme,
'category' => $category,
'percentage' => $percent,
);
}
/**
* Compute line totals, unit prices and unit discount for an item.
*
* @param \WC_Order_Item $item
* @param bool $lock_net_to_subtotal
* @return array
*/
protected function compute_item_price_parts( $item, bool $lock_net_to_subtotal = false ): array {
if ( is_a( $item, 'WC_Order_Item_Product' ) ) {
$gross_total = (float) $item->get_subtotal(); // ex-VAT, before discounts
$net_total = (float) ( $lock_net_to_subtotal ? $item->get_subtotal() : $item->get_total() ); // ex-VAT, after discounts
$qty = max( 1, (int) $item->get_quantity() );
} else {
$gross_total = (float) $item->get_total();
$net_total = (float) $item->get_total();
$qty = 1;
}
$gross_unit = $qty > 0 ? $gross_total / $qty : 0.0;
$net_unit = $qty > 0 ? $net_total / $qty : (float) $item->get_total();
$unit_discount = max( 0.0, $gross_unit - $net_unit );
return compact( 'gross_total', 'net_total', 'qty', 'gross_unit', 'net_unit', 'unit_discount' );
}
/**
* Get order item meta.
*
* @param \WC_Order_Item $item Order item object.
* @param array $args
* @return array
*/
protected function get_item_meta( \WC_Order_Item $item, array $args = array() ): array {
$args = wp_parse_args(
$args,
array(
'include_hidden' => false,
'include_empty' => false,
'only_keys' => array(),
'exclude_keys' => array(),
'use_display_label' => true,
'max_length' => 0,
)
);
// Skip meta for certain items (override via filter if needed).
$skip_types = apply_filters(
'wpo_ips_edi_skip_item_meta_for_types',
array( 'WC_Order_Item_Shipping', 'WC_Order_Item_Fee', 'WC_Order_Item_Tax', 'WC_Order_Item_Coupon' ),
$item,
$args,
$this
);
foreach ( (array) $skip_types as $class ) {
if ( is_a( $item, $class ) ) {
$rows = apply_filters( 'wpo_ips_edi_get_item_meta', array(), $item, $args, $this );
return is_array( $rows ) ? $rows : array();
}
}
$meta_items = method_exists( $item, 'get_all_formatted_meta_data' )
? $item->get_all_formatted_meta_data()
: $item->get_formatted_meta_data();
// Ensure we can iterate even if something exotic was returned.
if ( ! is_iterable( $meta_items ) ) {
$meta_items = array();
}
$rows = array();
foreach ( $meta_items as $meta_id => $m ) {
$raw_key = isset( $m->key ) ? (string) $m->key : '';
$raw_value = isset( $m->value ) ? $m->value : '';
// Hidden meta starts with underscore.
if ( ! $args['include_hidden'] && '' !== $raw_key && '_' === substr( $raw_key, 0, 1 ) ) {
continue;
}
// Whitelist / blacklist
if ( $args['only_keys'] && ! in_array( $raw_key, (array) $args['only_keys'], true ) ) {
continue;
}
if ( $args['exclude_keys'] && in_array( $raw_key, (array) $args['exclude_keys'], true ) ) {
continue;
}
$label = $args['use_display_label'] && isset( $m->display_key ) && '' !== $m->display_key
? (string) $m->display_key
: $raw_key;
// Prefer WooCommerce's display_value (already humanized), but force plain text.
$value = isset( $m->display_value ) ? $m->display_value : $raw_value;
// Flatten arrays/objects deterministically.
if ( is_array( $value ) || is_object( $value ) ) {
$value = wp_json_encode( $value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES );
}
// Strip tags and normalize whitespace for XML.
$value = wp_strip_all_tags( (string) $value, true );
$normalized = preg_replace( '/\s+/u', ' ', $value ); // may return null on invalid UTF-8
$value = is_string( $normalized ) ? $normalized : (string) $value;
$value = trim( $value );
// Optional truncation.
if ( $args['max_length'] ) {
$len = (int) $args['max_length'];
$value = function_exists( 'mb_substr' ) ? mb_substr( $value, 0, $len ) : substr( $value, 0, $len );
}
if ( ! $args['include_empty'] && '' === $value ) {
continue;
}
$rows[] = array(
'name' => $label,
'value' => $value,
);
}
$rows = apply_filters( 'wpo_ips_edi_get_item_meta', $rows, $item, $args, $this );
if ( ! is_array( $rows ) ) {
$rows = array();
}
return $rows;
}
}