it; } /** * The sanitizes the purchase_unit items amount. * * @return void */ private function sanitize_item_amount_mismatch(): void { $item_mismatch = $this->calculate_item_mismatch(); if ($this->mode === self::MODE_EXTRA_LINE) { if ($item_mismatch < 0) { // Do floors on item amounts so item_mismatch is a positive value. foreach ($this->purchase_unit['items'] as $index => $item) { // Get a more intelligent adjustment mechanism. $increment = (new \WooCommerce\PayPalCommerce\ApiClient\Helper\MoneyFormatter())->minimum_increment($item['unit_amount']['currency_code']); // not floor items that will be negative then. if ((float) $item['unit_amount']['value'] < $increment) { continue; } $this->purchase_unit['items'][$index]['unit_amount'] = (new Money((float) $item['unit_amount']['value'] - $increment, $item['unit_amount']['currency_code']))->to_array(); } } $item_mismatch = $this->calculate_item_mismatch(); if ($item_mismatch > 0) { // Use appropriate category to preserve purely digital or physical goods baskets. $rounding_item_category = $this->determine_rounding_item_category(); // Add extra line item with roundings. $line_name = $this->extra_line_name; $roundings_money = new Money($item_mismatch, $this->currency_code()); $this->purchase_unit['items'][] = (new Item($line_name, $roundings_money, 1, '', null, '', $rounding_item_category))->to_array(); $this->set_last_message(__('Item amount mismatch. Extra line added.', 'woocommerce-paypal-payments')); } $item_mismatch = $this->calculate_item_mismatch(); } if ($item_mismatch !== 0.0) { // Ditch items. if ($this->allow_ditch_items && isset($this->purchase_unit['items'])) { unset($this->purchase_unit['items']); $this->set_last_message(__('Item amount mismatch. Items ditched.', 'woocommerce-paypal-payments')); } } } /** * Determines the appropriate category for rounding items based on existing items. * * @return string The category (Item::DIGITAL_GOODS or Item::PHYSICAL_GOODS) */ private function determine_rounding_item_category(): string { // Check if all items are digital goods. foreach ($this->items() as $item) { $category = $item['category'] ?? Item::PHYSICAL_GOODS; if ($category !== Item::DIGITAL_GOODS) { return Item::PHYSICAL_GOODS; } } // All items are digital goods. return Item::DIGITAL_GOODS; } /** * The sanitizes the purchase_unit items tax. * * @return void */ private function sanitize_item_tax_mismatch(): void { $tax_mismatch = $this->calculate_tax_mismatch(); if ($this->allow_ditch_items && $tax_mismatch !== 0.0) { // Unset tax in items. foreach ($this->purchase_unit['items'] as $index => $item) { if (isset($this->purchase_unit['items'][$index]['tax'])) { unset($this->purchase_unit['items'][$index]['tax']); } if (isset($this->purchase_unit['items'][$index]['tax_rate'])) { unset($this->purchase_unit['items'][$index]['tax_rate']); } } } } /** * The sanitizes the purchase_unit breakdown. * * @return void */ private function sanitize_breakdown_mismatch(): void { $breakdown_mismatch = $this->calculate_breakdown_mismatch(); if ($this->allow_ditch_items && $breakdown_mismatch !== 0.0) { // Ditch breakdowns and items. if (isset($this->purchase_unit['items'])) { unset($this->purchase_unit['items']); } if (isset($this->purchase_unit['amount']['breakdown'])) { unset($this->purchase_unit['amount']['breakdown']); } $this->has_ditched_items_breakdown = \true; $this->set_last_message(__('Breakdown mismatch. Items and breakdown ditched.', 'woocommerce-paypal-payments')); } } /** * The calculates amount mismatch of items sums with breakdown. * * @return float */ private function calculate_item_mismatch(): float { $item_total = $this->breakdown_value('item_total'); if (!$item_total) { return 0; } $remaining_item_total = array_reduce($this->items(), function (float $total, array $item): float { return $total - (float) $item['unit_amount']['value'] * (float) $item['quantity']; }, $item_total); return round($remaining_item_total, 2); } /** * The calculates tax mismatch of items sums with breakdown. * * @return float */ private function calculate_tax_mismatch(): float { $tax_total = $this->breakdown_value('tax_total'); $items_with_tax = array_filter($this->items(), function (array $item): bool { return isset($item['tax']); }); if (!$tax_total || empty($items_with_tax)) { return 0; } $remaining_tax_total = array_reduce($this->items(), function (float $total, array $item): float { $tax = $item['tax'] ?? \false; if ($tax) { $total -= (float) $tax['value'] * (float) $item['quantity']; } return $total; }, $tax_total); return round($remaining_tax_total, 2); } /** * The calculates mismatch of breakdown sums with total amount. * * @return float */ private function calculate_breakdown_mismatch(): float { $breakdown = $this->breakdown(); if (!$breakdown) { return 0; } $amount_total = 0.0; $amount_total += $this->breakdown_value('item_total'); $amount_total += $this->breakdown_value('tax_total'); $amount_total += $this->breakdown_value('shipping'); $amount_total -= $this->breakdown_value('discount'); $amount_total -= $this->breakdown_value('shipping_discount'); $amount_total += $this->breakdown_value('handling'); $amount_total += $this->breakdown_value('insurance'); $amount_str = $this->amount()['value'] ?? 0; $amount_total_str = (new Money($amount_total, $this->currency_code()))->value_str(); return $amount_str - $amount_total_str; } /** * Indicates if the items and breakdown were ditched. * * @return bool */ public function has_ditched_items_breakdown(): bool { return $this->has_ditched_items_breakdown; } /** * Returns the last sanitization message. * * @return string */ public function get_last_message(): string { return $this->last_message; } /** * Set the last sanitization message. * * @param string $message The message. */ public function set_last_message(string $message): void { $this->last_message = $message; } }