(string) $order->get_currency(), 'items' => array(), 'coupons' => array(), ); foreach ( $order->get_items( 'line_item' ) as $item_id => $item ) { $payload['items'][] = array( 'id' => (int) $item_id, 'product_id' => (int) $item->get_product_id(), 'variation_id' => (int) $item->get_variation_id(), 'qty' => self::norm_decimal( (string) $item->get_quantity() ), 'line_total' => self::norm_decimal( (string) $item->get_total() ), 'line_subtotal' => self::norm_decimal( (string) $item->get_subtotal() ), ); } foreach ( $order->get_items( 'coupon' ) as $item_id => $item ) { $payload['coupons'][] = array( 'id' => (int) $item_id, 'code' => (string) $item->get_code(), 'discount' => self::norm_decimal( (string) $item->get_discount() ), ); } return self::hash_payload( $payload ); } /** * Hash from a normalized payload (unit tests / fixtures). * * @param array $payload Material payload. * @return string */ public static function hash_payload( array $payload ) { $normalized = self::normalize_payload( $payload ); return hash( 'sha256', (string) wp_json_encode( $normalized ) ); } /** * Deterministic sort + decimal normalization. * * @param array $payload Raw. * @return array */ public static function normalize_payload( array $payload ) { $items = isset( $payload['items'] ) && is_array( $payload['items'] ) ? $payload['items'] : array(); $coupons = isset( $payload['coupons'] ) && is_array( $payload['coupons'] ) ? $payload['coupons'] : array(); $norm_items = array(); foreach ( $items as $item ) { if ( ! is_array( $item ) ) { continue; } $norm_items[] = array( 'id' => isset( $item['id'] ) ? (int) $item['id'] : 0, 'product_id' => isset( $item['product_id'] ) ? (int) $item['product_id'] : 0, 'variation_id' => isset( $item['variation_id'] ) ? (int) $item['variation_id'] : 0, 'qty' => self::norm_decimal( isset( $item['qty'] ) ? (string) $item['qty'] : '0' ), 'line_total' => self::norm_decimal( isset( $item['line_total'] ) ? (string) $item['line_total'] : '0' ), 'line_subtotal' => self::norm_decimal( isset( $item['line_subtotal'] ) ? (string) $item['line_subtotal'] : '0' ), ); } usort( $norm_items, static function ( $a, $b ) { return $a['id'] <=> $b['id']; } ); $norm_coupons = array(); foreach ( $coupons as $c ) { if ( ! is_array( $c ) ) { continue; } $norm_coupons[] = array( 'id' => isset( $c['id'] ) ? (int) $c['id'] : 0, 'code' => isset( $c['code'] ) ? (string) $c['code'] : '', 'discount' => self::norm_decimal( isset( $c['discount'] ) ? (string) $c['discount'] : '0' ), ); } usort( $norm_coupons, static function ( $a, $b ) { return $a['id'] <=> $b['id']; } ); return array( 'currency' => isset( $payload['currency'] ) ? (string) $payload['currency'] : '', 'items' => $norm_items, 'coupons' => $norm_coupons, ); } /** * Normalize decimal strings so "10.0" === "10.00". * * @param string $value Raw. * @return string */ public static function norm_decimal( $value ) { $value = trim( (string) $value ); if ( '' === $value || ! is_numeric( $value ) ) { return $value; } if ( function_exists( 'bcadd' ) ) { $normalized = bcadd( $value, '0', 6 ); $normalized = rtrim( rtrim( $normalized, '0' ), '.' ); return '' === $normalized ? '0' : $normalized; } // Fallback only when BCMath unavailable (plugin requires ext-bcmath at runtime). $raw = trim( (string) $value ); if ( ! preg_match( '/^-?\d+(?:\.\d+)?$/', $raw ) ) { return '0'; } $neg = '-' === $raw[0]; $clean = ltrim( $raw, '-' ); if ( false === strpos( $clean, '.' ) ) { $clean .= '.000000'; } $parts = explode( '.', $clean, 2 ); $whole = $parts[0]; $frac = str_pad( substr( $parts[1], 0, 6 ), 6, '0', STR_PAD_RIGHT ); $result = $whole . '.' . $frac; return ( $neg ? '-' : '' ) . $result; } }