Changeset 3476823
- Timestamp:
- 03/07/2026 02:38:56 AM (4 days ago)
- Location:
- taxproof-coupons-for-woocommerce
- Files:
-
- 12 added
- 3 edited
- 4 copied
-
tags/1.0.5 (copied) (copied from taxproof-coupons-for-woocommerce/trunk)
-
tags/1.0.5/README.md (copied) (copied from taxproof-coupons-for-woocommerce/trunk/README.md) (4 diffs)
-
tags/1.0.5/includes (added)
-
tags/1.0.5/includes/class-coupon-service.php (added)
-
tags/1.0.5/includes/class-plugin.php (added)
-
tags/1.0.5/includes/integrations (added)
-
tags/1.0.5/includes/integrations/class-storeabill-integration.php (added)
-
tags/1.0.5/includes/integrations/class-wpml-integration.php (added)
-
tags/1.0.5/readme.txt (copied) (copied from taxproof-coupons-for-woocommerce/trunk/readme.txt) (2 diffs)
-
tags/1.0.5/tax-proof-coupons-plugin.php (copied) (copied from taxproof-coupons-for-woocommerce/trunk/tax-proof-coupons-plugin.php) (2 diffs)
-
trunk/README.md (modified) (4 diffs)
-
trunk/includes (added)
-
trunk/includes/class-coupon-service.php (added)
-
trunk/includes/class-plugin.php (added)
-
trunk/includes/integrations (added)
-
trunk/includes/integrations/class-storeabill-integration.php (added)
-
trunk/includes/integrations/class-wpml-integration.php (added)
-
trunk/readme.txt (modified) (2 diffs)
-
trunk/tax-proof-coupons-plugin.php (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
taxproof-coupons-for-woocommerce/tags/1.0.5/README.md
r3381508 r3476823 4 4 **Tags:** woocommerce, coupon, tax, discount 5 5 **Requires at least:** 6.5 6 **Tested up to:** 6. 87 **Stable tag:** 1.0. 46 **Tested up to:** 6.9 7 **Stable tag:** 1.0.5 8 8 **License:** GPLv2 or later 9 9 **License URI:** https://www.gnu.org/licenses/gpl-2.0.html … … 14 14 15 15 - Adds **Apply coupon after tax** checkbox to coupon settings. 16 - Converts gross coupon values into precise net discounts with high accuracy.16 - Converts the gross coupon value into the net discount WooCommerce expects, while keeping the applied gross discount exact. 17 17 - Guarantees the exact gross amount is deducted in cart and checkout. 18 - **StoreaBill/Germanized Pro Integration** for accurate invoice generation.19 - ** Enhanced Admin Display** showing detailed net and gross amounts.20 - ** Precision Calculations** for complex tax scenarios.21 - ** Debug Logging** for development and troubleshooting.18 - Caps oversized coupons to the actually discountable gross cart value. 19 - **StoreaBill/Germanized Pro Integration** via a dedicated compatibility layer. 20 - **WPML/WCML Compatibility** via an isolated order-total correction layer. 21 - **Enhanced Admin Display** showing the persisted gross, net, and tax split. 22 22 23 23 ## Installation … … 29 29 ## Changelog 30 30 31 ### 1.0.5 32 33 - Refactored the plugin into a lean core service with isolated StoreaBill and WPML integrations. 34 - Fixed carts where the configured gross coupon amount exceeds the discountable order value. 35 - Removed release-time debug logging and total-adjustment hacks from the wp.org build. 36 - Persist the gross/net/tax split on order coupon items for more reliable invoice generation. 37 31 38 ### 1.0.4 32 39 … … 36 43 - **Admin-Verbesserungen**: Detaillierte Anzeige von Netto- und Bruttobeträgen in der WooCommerce Admin-Oberfläche 37 44 - **Erweiterte Metadaten-Speicherung**: Präzise Speicherung von Coupon-Beträgen mit hoher Genauigkeit 38 - **Debug-Funktionen**: Erweiterte Logging-Funktionen für bessere Entwicklung und Fehlerbehebung39 45 - **Hook-Integration**: Neue Hooks für bessere Integration mit WooCommerce und Drittanbieter-Plugins 40 46 - **Performance-Optimierungen**: Verbesserte Berechnungslogik für komplexe Steuerszenarien -
taxproof-coupons-for-woocommerce/tags/1.0.5/readme.txt
r3381508 r3476823 4 4 Tags: woocommerce, coupon, tax, discount 5 5 Requires at least: 6.5 6 Tested up to: 6. 87 Stable tag: 1.0. 46 Tested up to: 6.9 7 Stable tag: 1.0.5 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 27 27 28 28 == Changelog == 29 30 = 1.0.5 = 31 32 Release date: March 2026 33 34 * Refactored the plugin into a cleaner core service plus isolated WPML and StoreaBill compatibility layers. 35 * Fixed fixed-cart coupons whose configured gross amount is larger than the discountable cart total. 36 * Removed release-time debug logging and broad total-manipulation hooks that caused rounding drift in edge cases. 37 * Persist the gross, net, and tax components on order coupon items so Germanized Pro / StoreaBill can invoice consistently. 38 * Keep the displayed coupon amount capped to the effective gross discount that can actually be applied. 29 39 30 40 = 1.0.4 = -
taxproof-coupons-for-woocommerce/tags/1.0.5/tax-proof-coupons-plugin.php
r3381508 r3476823 4 4 * Plugin URI: https://github.com/s-a-s-k-i-a/tax-proof-coupons 5 5 * Description: Ensure fixed-value coupons always apply after tax, regardless of VAT rate or customer location. 6 * Version: 1.0. 46 * Version: 1.0.5 7 7 * Author: Saskia Teichmann 8 8 * Author URI: https://saskialund.de … … 16 16 17 17 if ( ! defined( 'ABSPATH' ) ) { 18 exit; // Exit if accessed directly. 18 exit; 19 19 } 20 20 21 /** 22 * Main plugin class for Tax‑Proof Coupons for WooCommerce functionality. 23 */ 24 class Plugin { 25 /** Plugin version. */ 26 public const VERSION = '1.0.4'; 21 require_once __DIR__ . '/includes/class-coupon-service.php'; 22 require_once __DIR__ . '/includes/integrations/class-storeabill-integration.php'; 23 require_once __DIR__ . '/includes/integrations/class-wpml-integration.php'; 24 require_once __DIR__ . '/includes/class-plugin.php'; 27 25 28 /** Singleton instance. */ 29 private static $instance = null; 30 31 /** 32 * Get or create the singleton instance. 33 * 34 * @return Plugin 35 */ 36 public static function instance(): Plugin { 37 if ( null === self::$instance ) { 38 self::$instance = new self(); 39 self::$instance->init_hooks(); 40 } 41 return self::$instance; 42 } 43 44 /** Prevent direct instantiation. */ 45 private function __construct() {} 46 47 /** 48 * Check if Germanized (free or Pro) is active. 49 * 50 * @return bool 51 */ 52 private function is_storeabill_active(): bool { 53 return function_exists( 'WC_GZD' ); 54 } 55 56 /** Initialize WP and WooCommerce hooks. */ 57 private function init_hooks(): void { 58 add_action( 'woocommerce_coupon_options', [ $this, 'add_apply_after_tax_checkbox' ] ); 59 add_action( 'woocommerce_coupon_options_save', [ $this, 'save_apply_after_tax_checkbox' ], 10, 2 ); 60 add_filter( 'woocommerce_coupon_get_discount_amount', [ $this, 'apply_coupon_after_tax' ], 20, 5 ); 61 add_filter( 'woocommerce_coupon_discount_amount_html', [ $this, 'adjust_coupon_display_amount' ], 20, 2 ); 62 add_filter( 'woocommerce_cart_totals_coupon_html', [ $this, 'adjust_cart_coupon_display' ], 20, 3 ); 63 64 // Store correct net amounts when creating order items 65 add_action( 'woocommerce_checkout_create_order', [ $this, 'store_coupon_net_amounts' ], 10, 2 ); 66 add_action( 'woocommerce_create_order_coupon_item', [ $this, 'set_correct_coupon_amounts' ], 10, 4 ); 67 68 // Admin display 69 add_filter( 'woocommerce_order_formatted_line_subtotal', [ $this, 'adjust_admin_coupon_display' ], 10, 3 ); 70 71 // Force correct calculation for invoices - use the correct hook 72 add_filter( 'woocommerce_order_get_total_discount', [ $this, 'adjust_total_discount_for_invoices' ], 10, 2 ); 73 74 // StoreaBill (Germanized Pro) specific hooks - only if available 75 if ( $this->is_storeabill_active() ) { 76 add_filter( 'storeabill_invoice_get_discount_total', [ $this, 'storeabill_get_discount_total_gross' ], 10, 2 ); 77 add_filter( 'storeabill_woo_order_voucher_total', [ $this, 'storeabill_voucher_total_gross' ], 10, 2 ); 78 79 // Hook sniffer to find the correct Germanized hooks (only in debug mode) 80 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { 81 add_action( 'all', [ $this, 'log_germanized_hooks' ], 1 ); 82 } 83 } 84 } 85 86 /** Add "Apply after tax" checkbox to the coupon admin screen. */ 87 public function add_apply_after_tax_checkbox(): void { 88 \woocommerce_wp_checkbox( [ 89 'id' => 'tpc_apply_after_tax', 90 'label' => __( 'Apply coupon after tax', 'taxproof-coupons-for-woocommerce' ), 91 'description' => __( 'Deduct the fixed coupon amount from the order total including tax, ensuring the coupon value remains constant across all tax rates and locations.', 'taxproof-coupons-for-woocommerce' ), 92 ] ); 93 } 94 95 /** 96 * Save the "Apply after tax" checkbox value. 97 * 98 * @param int $post_id Coupon post ID. 99 * @param \WC_Coupon $coupon Coupon object. 100 */ 101 public function save_apply_after_tax_checkbox( int $post_id, \WC_Coupon $coupon ): void { 102 // Verify nonce for security 103 if ( ! isset( $_POST['woocommerce_meta_nonce'] ) || 104 ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['woocommerce_meta_nonce'] ) ), 'woocommerce_save_data' ) ) { 105 return; 106 } 107 108 $apply_after_tax = isset( $_POST['tpc_apply_after_tax'] ) ? 'yes' : 'no'; 109 $coupon->update_meta_data( 'tpc_apply_after_tax', $apply_after_tax ); 110 $coupon->save(); 111 } 112 113 /** 114 * Apply fixed-cart coupons after tax: convert the gross amount into the correct net discount, 115 * then hand that to WC so that tax is re-applied correctly. 116 * 117 * @param float $discount Current discount amount for this item. 118 * @param float $discounting_amount The original line total being discounted. 119 * @param array $cart_item Cart item data. 120 * @param bool $single Single item flag. 121 * @param \WC_Coupon $coupon Coupon object. 122 * @return float Modified discount. 123 */ 124 public function apply_coupon_after_tax( 125 float $discount, 126 float $discounting_amount, 127 array $cart_item, 128 bool $single, 129 \WC_Coupon $coupon 130 ): float { 131 // Only target fixed-cart coupons with our checkbox enabled. 132 if ( 'fixed_cart' !== $coupon->get_discount_type() || 133 'yes' !== $coupon->get_meta( 'tpc_apply_after_tax', true ) ) { 134 return $discount; 135 } 136 137 static $applied = []; 138 $code = $coupon->get_code(); 139 140 // Only apply once per coupon code. 141 if ( in_array( $code, $applied, true ) ) { 142 return 0.0; 143 } 144 145 // Sum gross and net totals across all product line items. 146 $total_gross = 0.0; 147 $total_net = 0.0; 148 foreach ( WC()->cart->get_cart() as $item ) { 149 $total_net += wc_get_price_excluding_tax( $item['data'], [ 'qty' => $item['quantity'] ] ); 150 $total_gross += wc_get_price_including_tax( $item['data'], [ 'qty' => $item['quantity'] ] ); 151 } 152 153 if ( $total_net <= 0 || $total_gross <= 0 ) { 154 return 0.0; 155 } 156 157 // The gross amount the admin entered (e.g. 150). 158 $gross_coupon = floatval( $coupon->get_amount() ); 159 160 // Compute the average tax rate across items. 161 $avg_tax_rate = ( $total_gross / $total_net ) - 1; 162 163 // Convert the gross coupon into the correct net discount with precision handling 164 $net_discount = $this->calculate_precise_net_discount( 165 $gross_coupon, 166 $avg_tax_rate, 167 $total_net, 168 $total_gross 169 ); 170 171 // Mark as applied so we don't double-dip. 172 $applied[] = $code; 173 174 return $net_discount; 175 } 176 177 /** 178 * Adjust the coupon display amount to show the gross value. 179 * This ensures the customer always sees the advertised coupon amount. 180 * 181 * @param string $discount_amount_html The HTML string for the discount amount 182 * @param \WC_Coupon $coupon The coupon object 183 * @return string Modified HTML 184 */ 185 public function adjust_coupon_display_amount( string $discount_amount_html, \WC_Coupon $coupon ): string { 186 // Only adjust for our tax-proof coupons 187 if ( 'fixed_cart' !== $coupon->get_discount_type() || 188 'yes' !== $coupon->get_meta( 'tpc_apply_after_tax', true ) ) { 189 return $discount_amount_html; 190 } 191 192 // Get the original coupon amount (gross) 193 $gross_amount = floatval( $coupon->get_amount() ); 194 195 // Format it as WooCommerce would 196 $formatted_amount = wc_price( $gross_amount ); 197 198 error_log( sprintf( 'TPC Debug: Adjusting display from %s to %s', $discount_amount_html, $formatted_amount ) ); 199 200 return '-' . $formatted_amount; 201 } 202 203 /** 204 * Adjust the cart display to show the gross coupon amount. 205 * This filter specifically targets the cart and checkout displays. 206 * 207 * @param string $coupon_html Current HTML 208 * @param \WC_Coupon $coupon Coupon object 209 * @param string $discount_amount_html Discount amount HTML 210 * @return string Modified HTML 211 */ 212 public function adjust_cart_coupon_display( string $coupon_html, \WC_Coupon $coupon, string $discount_amount_html ): string { 213 // Only adjust for our tax-proof coupons 214 if ( 'fixed_cart' !== $coupon->get_discount_type() || 215 'yes' !== $coupon->get_meta( 'tpc_apply_after_tax', true ) ) { 216 return $coupon_html; 217 } 218 219 // Get the original coupon amount (gross) 220 $gross_amount = floatval( $coupon->get_amount() ); 221 222 // Format it as WooCommerce would 223 $formatted_amount = wc_price( $gross_amount ); 224 225 // Replace the discount amount in the HTML 226 $new_html = str_replace( $discount_amount_html, '-' . $formatted_amount, $coupon_html ); 227 228 error_log( sprintf( 'TPC Debug: Cart display adjustment - Original: %s, New: %s', $coupon_html, $new_html ) ); 229 230 return $new_html; 231 } 232 233 /** 234 * Calculate precise net discount that ensures the gross amount matches exactly. 235 * This method handles rounding issues by finding the optimal net amount. 236 * 237 * @param float $gross_coupon The gross coupon amount (e.g., 100.00) 238 * @param float $avg_tax_rate The average tax rate (e.g., 0.21 for 21%) 239 * @param float $total_net Total net amount in cart 240 * @param float $total_gross Total gross amount in cart 241 * @return float The precise net discount amount 242 */ 243 private function calculate_precise_net_discount( 244 float $gross_coupon, 245 float $avg_tax_rate, 246 float $total_net, 247 float $total_gross 248 ): float { 249 $decimals = wc_get_price_decimals(); 250 $precision_decimals = 4; // Use 4 decimals for internal precision 251 252 // Debug logging 253 error_log( 'TPC Debug: Starting precise calculation' ); 254 error_log( sprintf( 'TPC Debug: Gross coupon: %.4f', $gross_coupon ) ); 255 error_log( sprintf( 'TPC Debug: Avg tax rate: %.4f (%.2f%%)', $avg_tax_rate, $avg_tax_rate * 100 ) ); 256 error_log( sprintf( 'TPC Debug: Decimals: %d', $decimals ) ); 257 258 // Initial calculation with higher precision 259 $net_discount_exact = $gross_coupon / ( 1 + $avg_tax_rate ); 260 error_log( sprintf( 'TPC Debug: Exact net discount (unrounded): %.6f', $net_discount_exact ) ); 261 262 // Try both floor and ceil to see which gives us the target gross 263 $net_floor = floor( $net_discount_exact * pow( 10, $decimals ) ) / pow( 10, $decimals ); 264 $net_ceil = ceil( $net_discount_exact * pow( 10, $decimals ) ) / pow( 10, $decimals ); 265 266 error_log( sprintf( 'TPC Debug: Net floor: %.4f, Net ceil: %.4f', $net_floor, $net_ceil ) ); 267 268 // Calculate gross for both options 269 // IMPORTANT: For Germany with 19% MwSt: 270 // 84.03 * 1.19 = 99.9957 which rounds to 99.99 (NOT 100.00!) 271 // 84.04 * 1.19 = 100.0076 which rounds to 100.01 272 $gross_floor_exact = $net_floor * ( 1 + $avg_tax_rate ); 273 $gross_ceil_exact = $net_ceil * ( 1 + $avg_tax_rate ); 274 $gross_floor = round( $gross_floor_exact, $decimals ); 275 $gross_ceil = round( $gross_ceil_exact, $decimals ); 276 277 error_log( sprintf( 'TPC Debug: Floor exact: %.6f, Ceil exact: %.6f', $gross_floor_exact, $gross_ceil_exact ) ); 278 279 error_log( sprintf( 'TPC Debug: Gross from floor: %.4f, Gross from ceil: %.4f', $gross_floor, $gross_ceil ) ); 280 281 // Choose the net amount that gives us the exact gross we want 282 // The problem: With only 2 decimals, we can't always achieve the exact gross amount 283 // Solution: Return the EXACT net amount with more precision 284 285 if ( $gross_floor == $gross_coupon ) { 286 $net_discount = $net_floor; 287 error_log( 'TPC Debug: Using floor value for exact match' ); 288 } elseif ( $gross_ceil == $gross_coupon ) { 289 $net_discount = $net_ceil; 290 error_log( 'TPC Debug: Using ceil value for exact match' ); 291 } else { 292 // For 19% MwSt: 84.03 gives 99.9957 which rounds to 99.99, NOT 100.00 293 // We need to use 84.04 which gives 100.0076 and rounds to 100.01 294 // But we can accept 100.01 and show 100.00 in display 295 error_log( sprintf( 'TPC Debug: No exact match - floor gives %.4f, ceil gives %.4f', $gross_floor, $gross_ceil ) ); 296 297 // Prefer the value that's slightly over rather than under 298 if ( $gross_floor < $gross_coupon && $gross_ceil > $gross_coupon ) { 299 // If floor is under and ceil is over, use ceil 300 $net_discount = $net_ceil; 301 error_log( 'TPC Debug: Using ceil to avoid underpayment' ); 302 } else { 303 // If neither gives exact match, we need a more sophisticated approach 304 // Calculate what net amount would give us exactly the gross we want 305 // This might require adjusting by fractions of a cent 306 307 error_log( 'TPC Debug: Neither floor nor ceil gives exact match, calculating optimal value' ); 308 309 // We'll use a binary search approach to find the optimal value 310 $lower = $net_floor; 311 $upper = $net_ceil; 312 $best_net = $net_floor; 313 $best_diff = abs( $gross_floor - $gross_coupon ); 314 315 // Try intermediate values 316 for ( $i = 1; $i < 10; $i++ ) { 317 $test_net = $lower + ( $upper - $lower ) * ( $i / 10 ); 318 $test_net = round( $test_net, $decimals + 2 ); // Use extra precision 319 $test_gross = round( $test_net * ( 1 + $avg_tax_rate ), $decimals ); 320 $diff = abs( $test_gross - $gross_coupon ); 321 322 if ( $diff < $best_diff ) { 323 $best_net = $test_net; 324 $best_diff = $diff; 325 326 if ( $diff < 0.001 ) { 327 error_log( sprintf( 'TPC Debug: Found optimal value: %.4f', $best_net ) ); 328 break; 329 } 330 } 331 } 332 333 // Round to allowed decimals 334 $net_discount = round( $best_net, $decimals ); 335 336 // If this still doesn't give us the exact gross, adjust by one cent in the right direction 337 $final_gross = round( $net_discount * ( 1 + $avg_tax_rate ), $decimals ); 338 if ( $final_gross < $gross_coupon ) { 339 $net_discount += pow( 10, -$decimals ); 340 error_log( 'TPC Debug: Adjusted up by one cent for exact match' ); 341 } elseif ( $final_gross > $gross_coupon && $net_discount > pow( 10, -$decimals ) ) { 342 $net_discount -= pow( 10, -$decimals ); 343 error_log( 'TPC Debug: Adjusted down by one cent for exact match' ); 344 } 345 } 346 } 347 348 // Final step: Always return the EXACT mathematical value 349 // Don't try to round to 2 decimals - return with full precision 350 $exact_net = $gross_coupon / ( 1 + $avg_tax_rate ); 351 352 error_log( sprintf( 'TPC Debug: Final calculation - Gross %.4f / (1 + %.4f) = %.10f', 353 $gross_coupon, $avg_tax_rate, $exact_net ) ); 354 355 // Return with high precision to ensure exact calculations 356 return $exact_net; 357 } 358 359 /** 360 * Set correct coupon amounts when creating the order item. 361 * This runs BEFORE the item is saved, allowing us to set the correct net amount. 362 * 363 * @param \WC_Order_Item_Coupon $item The coupon item being created 364 * @param string $code Coupon code 365 * @param float $discount Discount amount 366 * @param \WC_Order $order Order object 367 */ 368 public function set_correct_coupon_amounts( \WC_Order_Item_Coupon $item, string $code, float $discount, \WC_Order $order ): void { 369 error_log( sprintf( 'TPC Debug: set_correct_coupon_amounts called - Code: %s, Discount: %.4f', $code, $discount ) ); 370 371 $coupon = new \WC_Coupon( $code ); 372 373 if ( 'fixed_cart' !== $coupon->get_discount_type() || 374 'yes' !== $coupon->get_meta( 'tpc_apply_after_tax', true ) ) { 375 error_log( 'TPC Debug: Not a tax-proof coupon, skipping' ); 376 return; 377 } 378 379 // Get the gross amount (what customer sees) 380 $gross_amount = floatval( $coupon->get_amount() ); 381 382 // Calculate tax rate from cart totals 383 $cart = WC()->cart; 384 $total_tax = $cart->get_total_tax(); 385 $subtotal = $cart->get_subtotal(); 386 $total = $cart->get_total( 'edit' ); 387 388 // Calculate average tax rate 389 $avg_tax_rate = $subtotal > 0 ? ( $total_tax / $subtotal ) : 0; 390 391 // Calculate precise net amount 392 $net_amount = $this->calculate_precise_net_discount( 393 $gross_amount, 394 $avg_tax_rate, 395 $subtotal, 396 $subtotal + $total_tax 397 ); 398 399 // Calculate tax amount 400 $tax_amount = $gross_amount - $net_amount; 401 402 // Set the correct amounts on the item BEFORE it's saved 403 // IMPORTANT: We store the EXACT net amount with more decimals for correct calculation 404 // This ensures that net * tax_rate = exactly the gross amount we want 405 $item->set_discount( $net_amount ); 406 $item->set_discount_tax( $tax_amount ); 407 408 // Store metadata for reference - with full precision 409 $item->add_meta_data( '_tpc_gross_discount', $gross_amount ); 410 $item->add_meta_data( '_tpc_net_discount', number_format( $net_amount, 10, '.', '' ) ); // Store with 10 decimals 411 $item->add_meta_data( '_tpc_tax_rate', $avg_tax_rate ); 412 $item->add_meta_data( '_tpc_applied_after_tax', 'yes' ); 413 414 error_log( sprintf( 415 'TPC Debug: Setting coupon amounts - Code: %s, Gross: %.4f, Net: %.4f, Tax: %.4f', 416 $code, 417 $gross_amount, 418 $net_amount, 419 $tax_amount 420 ) ); 421 } 422 423 /** 424 * Store precise net amounts after order is created. 425 * This ensures Germanized Pro uses the correct values for invoice generation. 426 * 427 * @param \WC_Order $order Order object 428 * @param array $data Checkout data 429 */ 430 public function store_coupon_net_amounts( \WC_Order $order, array $data ): void { 431 error_log( sprintf( 'TPC Debug: store_coupon_net_amounts called for order %s', $order->get_id() ) ); 432 433 foreach ( $order->get_coupons() as $item ) { 434 $coupon = new \WC_Coupon( $item->get_code() ); 435 436 if ( 'fixed_cart' !== $coupon->get_discount_type() || 437 'yes' !== $coupon->get_meta( 'tpc_apply_after_tax', true ) ) { 438 continue; 439 } 440 441 error_log( sprintf( 'TPC Debug: Processing tax-proof coupon %s', $item->get_code() ) ); 442 443 // Get the gross amount 444 $gross_amount = floatval( $coupon->get_amount() ); 445 446 // Calculate tax rate from order 447 // IMPORTANT: We need the subtotal BEFORE discount to get the correct tax rate 448 $order_subtotal_before_discount = 0; 449 $order_tax = 0; 450 451 // Calculate from line items (products) to get accurate rate 452 foreach ( $order->get_items() as $line_item ) { 453 $order_subtotal_before_discount += $line_item->get_subtotal(); 454 $order_tax += $line_item->get_subtotal_tax(); 455 } 456 457 $avg_tax_rate = $order_subtotal_before_discount > 0 ? ( $order_tax / $order_subtotal_before_discount ) : 0; 458 459 error_log( sprintf( 460 'TPC Debug: Order data - Subtotal (before discount): %.4f, Tax: %.4f, Calc Tax Rate: %.6f (%.2f%%)', 461 $order_subtotal_before_discount, 462 $order_tax, 463 $avg_tax_rate, 464 $avg_tax_rate * 100 465 ) ); 466 467 // Calculate exact net amount 468 $net_amount_exact = $gross_amount / ( 1 + $avg_tax_rate ); 469 $tax_amount_exact = $gross_amount - $net_amount_exact; 470 471 error_log( sprintf( 472 'TPC Debug: Calculation - Gross: %.4f / (1 + %.6f) = Net: %.10f, Tax: %.10f', 473 $gross_amount, 474 $avg_tax_rate, 475 $net_amount_exact, 476 $tax_amount_exact 477 ) ); 478 479 // Update the item with precise values 480 $item->set_discount( $net_amount_exact ); 481 $item->set_discount_tax( $tax_amount_exact ); 482 483 // Store metadata 484 $item->update_meta_data( '_tpc_gross_discount', $gross_amount ); 485 $item->update_meta_data( '_tpc_net_discount', $net_amount_exact ); 486 $item->update_meta_data( '_tpc_tax_rate', $avg_tax_rate ); 487 $item->update_meta_data( '_tpc_applied_after_tax', 'yes' ); 488 $item->save(); 489 490 error_log( sprintf( 491 'TPC Debug: Updated coupon %s - Gross: %.4f, Net: %.10f, Tax: %.10f', 492 $item->get_code(), 493 $gross_amount, 494 $net_amount_exact, 495 $tax_amount_exact 496 ) ); 497 } 498 499 // Save the order to persist all changes 500 $order->save(); 501 } 502 503 504 /** 505 * Adjust coupon display in admin order view. 506 * 507 * @param string $subtotal Formatted subtotal 508 * @param \WC_Order_Item $item Order item 509 * @param \WC_Order $order Order object 510 * @return string Modified subtotal 511 */ 512 public function adjust_admin_coupon_display( string $subtotal, \WC_Order_Item $item, \WC_Order $order ): string { 513 // Only process coupon items 514 if ( ! $item->is_type( 'coupon' ) ) { 515 return $subtotal; 516 } 517 518 $net_discount = $item->get_meta( '_tpc_net_discount' ); 519 $gross_discount = $item->get_meta( '_tpc_gross_discount' ); 520 521 error_log( sprintf( 'TPC Debug: Meta data - Net: %s, Gross: %s', 522 $net_discount ? $net_discount : 'NOT FOUND', 523 $gross_discount ? $gross_discount : 'NOT FOUND' 524 ) ); 525 526 if ( $net_discount && $gross_discount ) { 527 // Show detailed information in admin with 4 decimal places 528 $net_value = floatval( $net_discount ); 529 $net_display = number_format( $net_value, 4, ',', '' ); 530 $subtotal = sprintf( 531 '-<span class="woocommerce-Price-amount amount">%s <span class="woocommerce-Price-currencySymbol">€</span></span> <small class="woocommerce-help-tip" data-tip="Brutto: %s">(Brutto: %s)</small>', 532 $net_display, 533 wc_price( $gross_discount ), 534 wc_price( $gross_discount ) 535 ); 536 537 error_log( sprintf( 538 'TPC Debug: Admin display adjusted - Net: %.4f, Gross: %.4f', 539 $net_discount, 540 $gross_discount 541 ) ); 542 } 543 544 return $subtotal; 545 } 546 547 548 /** 549 * Adjust total discount to ensure it matches the gross coupon amount. 550 * This is critical for invoice generation. 551 * 552 * @param float $total_discount Current total discount 553 * @param \WC_Order $order Order object 554 * @return float Adjusted discount 555 */ 556 public function adjust_total_discount_for_invoices( float $total_discount, \WC_Order $order ): float { 557 $adjusted_total = 0; 558 559 foreach ( $order->get_coupons() as $item ) { 560 if ( $item->get_meta( '_tpc_applied_after_tax' ) === 'yes' ) { 561 // Use the gross discount amount for tax-proof coupons 562 $gross_discount = $item->get_meta( '_tpc_gross_discount' ); 563 if ( $gross_discount ) { 564 $adjusted_total += floatval( $gross_discount ); 565 error_log( sprintf( 566 'TPC Debug: Adjusting invoice discount for %s - Using gross: %.4f instead of net+tax', 567 $item->get_code(), 568 $gross_discount 569 ) ); 570 } else { 571 $adjusted_total += $item->get_discount() + $item->get_discount_tax(); 572 } 573 } else { 574 $adjusted_total += $item->get_discount() + $item->get_discount_tax(); 575 } 576 } 577 578 if ( $adjusted_total != $total_discount ) { 579 error_log( sprintf( 580 'TPC Debug: Adjusted total discount from %.4f to %.4f', 581 $total_discount, 582 $adjusted_total 583 ) ); 584 } 585 586 return $adjusted_total; 587 } 588 589 /** 590 * Adjust StoreaBill invoice discount total to use gross amounts. 591 * This filter is called when StoreaBill calculates the total discount for the invoice. 592 * 593 * @param float $total Current discount total 594 * @param object $invoice The invoice document 595 * @return float Adjusted total 596 */ 597 public function storeabill_get_discount_total_gross( float $total, $invoice ): float { 598 // Get the WooCommerce order 599 if ( ! method_exists( $invoice, 'get_order' ) ) { 600 return $total; 601 } 602 603 $order = $invoice->get_order(); 604 if ( ! $order ) { 605 return $total; 606 } 607 608 $adjusted_total = 0; 609 $has_tax_proof_coupons = false; 610 611 foreach ( $order->get_coupons() as $item ) { 612 $gross_discount = $item->get_meta( '_tpc_gross_discount' ); 613 if ( $gross_discount ) { 614 $adjusted_total += floatval( $gross_discount ); 615 $has_tax_proof_coupons = true; 616 error_log( sprintf( 617 'TPC Debug: StoreaBill discount total - Using gross: %.4f for %s', 618 $gross_discount, 619 $item->get_code() 620 ) ); 621 } else { 622 $adjusted_total += $item->get_discount() + $item->get_discount_tax(); 623 } 624 } 625 626 if ( $has_tax_proof_coupons && $adjusted_total != $total ) { 627 error_log( sprintf( 628 'TPC Debug: StoreaBill total discount changed from %.4f to %.4f', 629 $total, 630 $adjusted_total 631 ) ); 632 return $adjusted_total; 633 } 634 635 return $total; 636 } 637 638 /** 639 * Adjust StoreaBill voucher total to use gross amounts. 640 * 641 * @param float $total Current voucher total 642 * @param object $order_data_store The WooCommerce order data store 643 * @return float Adjusted total 644 */ 645 public function storeabill_voucher_total_gross( float $total, $order_data_store ): float { 646 error_log( sprintf( 'TPC Debug: storeabill_voucher_total_gross called - Total: %.4f', $total ) ); 647 648 if ( ! method_exists( $order_data_store, 'get_order' ) ) { 649 error_log( 'TPC Debug: No get_order method' ); 650 return $total; 651 } 652 653 $order = $order_data_store->get_order(); 654 if ( ! $order ) { 655 error_log( 'TPC Debug: No order found' ); 656 return $total; 657 } 658 659 $adjusted_total = 0; 660 661 foreach ( $order->get_coupons() as $item ) { 662 $gross_discount = $item->get_meta( '_tpc_gross_discount' ); 663 if ( $gross_discount ) { 664 $adjusted_total += floatval( $gross_discount ); 665 error_log( sprintf( 666 'TPC Debug: StoreaBill voucher total - Using gross: %.4f for %s', 667 $gross_discount, 668 $item->get_code() 669 ) ); 670 } 671 } 672 673 if ( $adjusted_total > 0 ) { 674 error_log( sprintf( 675 'TPC Debug: StoreaBill voucher total changed from %.4f to %.4f', 676 $total, 677 $adjusted_total 678 ) ); 679 return $adjusted_total; 680 } 681 682 return $total; 683 } 684 685 /** 686 * Log all Germanized/StoreaBill hooks to find the right one. 687 * This is a temporary debugging method. 688 * 689 * @param string $hook Hook name 690 */ 691 public function log_germanized_hooks( string $hook ): void { 692 // Only log during invoice generation (check if we're in admin) 693 if ( ! is_admin() ) { 694 return; 695 } 696 697 // Only log hooks that might be related to invoice/discount 698 if ( strpos( $hook, 'gzd' ) !== false || 699 strpos( $hook, 'germanized' ) !== false || 700 strpos( $hook, 'storeabill' ) !== false || 701 strpos( $hook, 'invoice' ) !== false ) { 702 703 // Only log filter hooks (not actions) to reduce noise 704 if ( strpos( $hook, 'discount' ) !== false || 705 strpos( $hook, 'total' ) !== false || 706 strpos( $hook, 'coupon' ) !== false ) { 707 error_log( sprintf( 'TPC Debug: Hook fired: %s', $hook ) ); 708 } 709 } 710 } 711 712 /** Public wakeup to satisfy PHP's magic method requirement. */ 713 public function __wakeup() {} 714 } 715 716 // Bootstrap the plugin. 717 add_action( 'plugins_loaded', [ Plugin::class, 'instance' ] ); 26 add_action( 'plugins_loaded', array( Plugin::class, 'instance' ) ); -
taxproof-coupons-for-woocommerce/trunk/README.md
r3381508 r3476823 4 4 **Tags:** woocommerce, coupon, tax, discount 5 5 **Requires at least:** 6.5 6 **Tested up to:** 6. 87 **Stable tag:** 1.0. 46 **Tested up to:** 6.9 7 **Stable tag:** 1.0.5 8 8 **License:** GPLv2 or later 9 9 **License URI:** https://www.gnu.org/licenses/gpl-2.0.html … … 14 14 15 15 - Adds **Apply coupon after tax** checkbox to coupon settings. 16 - Converts gross coupon values into precise net discounts with high accuracy.16 - Converts the gross coupon value into the net discount WooCommerce expects, while keeping the applied gross discount exact. 17 17 - Guarantees the exact gross amount is deducted in cart and checkout. 18 - **StoreaBill/Germanized Pro Integration** for accurate invoice generation.19 - ** Enhanced Admin Display** showing detailed net and gross amounts.20 - ** Precision Calculations** for complex tax scenarios.21 - ** Debug Logging** for development and troubleshooting.18 - Caps oversized coupons to the actually discountable gross cart value. 19 - **StoreaBill/Germanized Pro Integration** via a dedicated compatibility layer. 20 - **WPML/WCML Compatibility** via an isolated order-total correction layer. 21 - **Enhanced Admin Display** showing the persisted gross, net, and tax split. 22 22 23 23 ## Installation … … 29 29 ## Changelog 30 30 31 ### 1.0.5 32 33 - Refactored the plugin into a lean core service with isolated StoreaBill and WPML integrations. 34 - Fixed carts where the configured gross coupon amount exceeds the discountable order value. 35 - Removed release-time debug logging and total-adjustment hacks from the wp.org build. 36 - Persist the gross/net/tax split on order coupon items for more reliable invoice generation. 37 31 38 ### 1.0.4 32 39 … … 36 43 - **Admin-Verbesserungen**: Detaillierte Anzeige von Netto- und Bruttobeträgen in der WooCommerce Admin-Oberfläche 37 44 - **Erweiterte Metadaten-Speicherung**: Präzise Speicherung von Coupon-Beträgen mit hoher Genauigkeit 38 - **Debug-Funktionen**: Erweiterte Logging-Funktionen für bessere Entwicklung und Fehlerbehebung39 45 - **Hook-Integration**: Neue Hooks für bessere Integration mit WooCommerce und Drittanbieter-Plugins 40 46 - **Performance-Optimierungen**: Verbesserte Berechnungslogik für komplexe Steuerszenarien -
taxproof-coupons-for-woocommerce/trunk/readme.txt
r3381508 r3476823 4 4 Tags: woocommerce, coupon, tax, discount 5 5 Requires at least: 6.5 6 Tested up to: 6. 87 Stable tag: 1.0. 46 Tested up to: 6.9 7 Stable tag: 1.0.5 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 27 27 28 28 == Changelog == 29 30 = 1.0.5 = 31 32 Release date: March 2026 33 34 * Refactored the plugin into a cleaner core service plus isolated WPML and StoreaBill compatibility layers. 35 * Fixed fixed-cart coupons whose configured gross amount is larger than the discountable cart total. 36 * Removed release-time debug logging and broad total-manipulation hooks that caused rounding drift in edge cases. 37 * Persist the gross, net, and tax components on order coupon items so Germanized Pro / StoreaBill can invoice consistently. 38 * Keep the displayed coupon amount capped to the effective gross discount that can actually be applied. 29 39 30 40 = 1.0.4 = -
taxproof-coupons-for-woocommerce/trunk/tax-proof-coupons-plugin.php
r3381508 r3476823 4 4 * Plugin URI: https://github.com/s-a-s-k-i-a/tax-proof-coupons 5 5 * Description: Ensure fixed-value coupons always apply after tax, regardless of VAT rate or customer location. 6 * Version: 1.0. 46 * Version: 1.0.5 7 7 * Author: Saskia Teichmann 8 8 * Author URI: https://saskialund.de … … 16 16 17 17 if ( ! defined( 'ABSPATH' ) ) { 18 exit; // Exit if accessed directly. 18 exit; 19 19 } 20 20 21 /** 22 * Main plugin class for Tax‑Proof Coupons for WooCommerce functionality. 23 */ 24 class Plugin { 25 /** Plugin version. */ 26 public const VERSION = '1.0.4'; 21 require_once __DIR__ . '/includes/class-coupon-service.php'; 22 require_once __DIR__ . '/includes/integrations/class-storeabill-integration.php'; 23 require_once __DIR__ . '/includes/integrations/class-wpml-integration.php'; 24 require_once __DIR__ . '/includes/class-plugin.php'; 27 25 28 /** Singleton instance. */ 29 private static $instance = null; 30 31 /** 32 * Get or create the singleton instance. 33 * 34 * @return Plugin 35 */ 36 public static function instance(): Plugin { 37 if ( null === self::$instance ) { 38 self::$instance = new self(); 39 self::$instance->init_hooks(); 40 } 41 return self::$instance; 42 } 43 44 /** Prevent direct instantiation. */ 45 private function __construct() {} 46 47 /** 48 * Check if Germanized (free or Pro) is active. 49 * 50 * @return bool 51 */ 52 private function is_storeabill_active(): bool { 53 return function_exists( 'WC_GZD' ); 54 } 55 56 /** Initialize WP and WooCommerce hooks. */ 57 private function init_hooks(): void { 58 add_action( 'woocommerce_coupon_options', [ $this, 'add_apply_after_tax_checkbox' ] ); 59 add_action( 'woocommerce_coupon_options_save', [ $this, 'save_apply_after_tax_checkbox' ], 10, 2 ); 60 add_filter( 'woocommerce_coupon_get_discount_amount', [ $this, 'apply_coupon_after_tax' ], 20, 5 ); 61 add_filter( 'woocommerce_coupon_discount_amount_html', [ $this, 'adjust_coupon_display_amount' ], 20, 2 ); 62 add_filter( 'woocommerce_cart_totals_coupon_html', [ $this, 'adjust_cart_coupon_display' ], 20, 3 ); 63 64 // Store correct net amounts when creating order items 65 add_action( 'woocommerce_checkout_create_order', [ $this, 'store_coupon_net_amounts' ], 10, 2 ); 66 add_action( 'woocommerce_create_order_coupon_item', [ $this, 'set_correct_coupon_amounts' ], 10, 4 ); 67 68 // Admin display 69 add_filter( 'woocommerce_order_formatted_line_subtotal', [ $this, 'adjust_admin_coupon_display' ], 10, 3 ); 70 71 // Force correct calculation for invoices - use the correct hook 72 add_filter( 'woocommerce_order_get_total_discount', [ $this, 'adjust_total_discount_for_invoices' ], 10, 2 ); 73 74 // StoreaBill (Germanized Pro) specific hooks - only if available 75 if ( $this->is_storeabill_active() ) { 76 add_filter( 'storeabill_invoice_get_discount_total', [ $this, 'storeabill_get_discount_total_gross' ], 10, 2 ); 77 add_filter( 'storeabill_woo_order_voucher_total', [ $this, 'storeabill_voucher_total_gross' ], 10, 2 ); 78 79 // Hook sniffer to find the correct Germanized hooks (only in debug mode) 80 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { 81 add_action( 'all', [ $this, 'log_germanized_hooks' ], 1 ); 82 } 83 } 84 } 85 86 /** Add "Apply after tax" checkbox to the coupon admin screen. */ 87 public function add_apply_after_tax_checkbox(): void { 88 \woocommerce_wp_checkbox( [ 89 'id' => 'tpc_apply_after_tax', 90 'label' => __( 'Apply coupon after tax', 'taxproof-coupons-for-woocommerce' ), 91 'description' => __( 'Deduct the fixed coupon amount from the order total including tax, ensuring the coupon value remains constant across all tax rates and locations.', 'taxproof-coupons-for-woocommerce' ), 92 ] ); 93 } 94 95 /** 96 * Save the "Apply after tax" checkbox value. 97 * 98 * @param int $post_id Coupon post ID. 99 * @param \WC_Coupon $coupon Coupon object. 100 */ 101 public function save_apply_after_tax_checkbox( int $post_id, \WC_Coupon $coupon ): void { 102 // Verify nonce for security 103 if ( ! isset( $_POST['woocommerce_meta_nonce'] ) || 104 ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['woocommerce_meta_nonce'] ) ), 'woocommerce_save_data' ) ) { 105 return; 106 } 107 108 $apply_after_tax = isset( $_POST['tpc_apply_after_tax'] ) ? 'yes' : 'no'; 109 $coupon->update_meta_data( 'tpc_apply_after_tax', $apply_after_tax ); 110 $coupon->save(); 111 } 112 113 /** 114 * Apply fixed-cart coupons after tax: convert the gross amount into the correct net discount, 115 * then hand that to WC so that tax is re-applied correctly. 116 * 117 * @param float $discount Current discount amount for this item. 118 * @param float $discounting_amount The original line total being discounted. 119 * @param array $cart_item Cart item data. 120 * @param bool $single Single item flag. 121 * @param \WC_Coupon $coupon Coupon object. 122 * @return float Modified discount. 123 */ 124 public function apply_coupon_after_tax( 125 float $discount, 126 float $discounting_amount, 127 array $cart_item, 128 bool $single, 129 \WC_Coupon $coupon 130 ): float { 131 // Only target fixed-cart coupons with our checkbox enabled. 132 if ( 'fixed_cart' !== $coupon->get_discount_type() || 133 'yes' !== $coupon->get_meta( 'tpc_apply_after_tax', true ) ) { 134 return $discount; 135 } 136 137 static $applied = []; 138 $code = $coupon->get_code(); 139 140 // Only apply once per coupon code. 141 if ( in_array( $code, $applied, true ) ) { 142 return 0.0; 143 } 144 145 // Sum gross and net totals across all product line items. 146 $total_gross = 0.0; 147 $total_net = 0.0; 148 foreach ( WC()->cart->get_cart() as $item ) { 149 $total_net += wc_get_price_excluding_tax( $item['data'], [ 'qty' => $item['quantity'] ] ); 150 $total_gross += wc_get_price_including_tax( $item['data'], [ 'qty' => $item['quantity'] ] ); 151 } 152 153 if ( $total_net <= 0 || $total_gross <= 0 ) { 154 return 0.0; 155 } 156 157 // The gross amount the admin entered (e.g. 150). 158 $gross_coupon = floatval( $coupon->get_amount() ); 159 160 // Compute the average tax rate across items. 161 $avg_tax_rate = ( $total_gross / $total_net ) - 1; 162 163 // Convert the gross coupon into the correct net discount with precision handling 164 $net_discount = $this->calculate_precise_net_discount( 165 $gross_coupon, 166 $avg_tax_rate, 167 $total_net, 168 $total_gross 169 ); 170 171 // Mark as applied so we don't double-dip. 172 $applied[] = $code; 173 174 return $net_discount; 175 } 176 177 /** 178 * Adjust the coupon display amount to show the gross value. 179 * This ensures the customer always sees the advertised coupon amount. 180 * 181 * @param string $discount_amount_html The HTML string for the discount amount 182 * @param \WC_Coupon $coupon The coupon object 183 * @return string Modified HTML 184 */ 185 public function adjust_coupon_display_amount( string $discount_amount_html, \WC_Coupon $coupon ): string { 186 // Only adjust for our tax-proof coupons 187 if ( 'fixed_cart' !== $coupon->get_discount_type() || 188 'yes' !== $coupon->get_meta( 'tpc_apply_after_tax', true ) ) { 189 return $discount_amount_html; 190 } 191 192 // Get the original coupon amount (gross) 193 $gross_amount = floatval( $coupon->get_amount() ); 194 195 // Format it as WooCommerce would 196 $formatted_amount = wc_price( $gross_amount ); 197 198 error_log( sprintf( 'TPC Debug: Adjusting display from %s to %s', $discount_amount_html, $formatted_amount ) ); 199 200 return '-' . $formatted_amount; 201 } 202 203 /** 204 * Adjust the cart display to show the gross coupon amount. 205 * This filter specifically targets the cart and checkout displays. 206 * 207 * @param string $coupon_html Current HTML 208 * @param \WC_Coupon $coupon Coupon object 209 * @param string $discount_amount_html Discount amount HTML 210 * @return string Modified HTML 211 */ 212 public function adjust_cart_coupon_display( string $coupon_html, \WC_Coupon $coupon, string $discount_amount_html ): string { 213 // Only adjust for our tax-proof coupons 214 if ( 'fixed_cart' !== $coupon->get_discount_type() || 215 'yes' !== $coupon->get_meta( 'tpc_apply_after_tax', true ) ) { 216 return $coupon_html; 217 } 218 219 // Get the original coupon amount (gross) 220 $gross_amount = floatval( $coupon->get_amount() ); 221 222 // Format it as WooCommerce would 223 $formatted_amount = wc_price( $gross_amount ); 224 225 // Replace the discount amount in the HTML 226 $new_html = str_replace( $discount_amount_html, '-' . $formatted_amount, $coupon_html ); 227 228 error_log( sprintf( 'TPC Debug: Cart display adjustment - Original: %s, New: %s', $coupon_html, $new_html ) ); 229 230 return $new_html; 231 } 232 233 /** 234 * Calculate precise net discount that ensures the gross amount matches exactly. 235 * This method handles rounding issues by finding the optimal net amount. 236 * 237 * @param float $gross_coupon The gross coupon amount (e.g., 100.00) 238 * @param float $avg_tax_rate The average tax rate (e.g., 0.21 for 21%) 239 * @param float $total_net Total net amount in cart 240 * @param float $total_gross Total gross amount in cart 241 * @return float The precise net discount amount 242 */ 243 private function calculate_precise_net_discount( 244 float $gross_coupon, 245 float $avg_tax_rate, 246 float $total_net, 247 float $total_gross 248 ): float { 249 $decimals = wc_get_price_decimals(); 250 $precision_decimals = 4; // Use 4 decimals for internal precision 251 252 // Debug logging 253 error_log( 'TPC Debug: Starting precise calculation' ); 254 error_log( sprintf( 'TPC Debug: Gross coupon: %.4f', $gross_coupon ) ); 255 error_log( sprintf( 'TPC Debug: Avg tax rate: %.4f (%.2f%%)', $avg_tax_rate, $avg_tax_rate * 100 ) ); 256 error_log( sprintf( 'TPC Debug: Decimals: %d', $decimals ) ); 257 258 // Initial calculation with higher precision 259 $net_discount_exact = $gross_coupon / ( 1 + $avg_tax_rate ); 260 error_log( sprintf( 'TPC Debug: Exact net discount (unrounded): %.6f', $net_discount_exact ) ); 261 262 // Try both floor and ceil to see which gives us the target gross 263 $net_floor = floor( $net_discount_exact * pow( 10, $decimals ) ) / pow( 10, $decimals ); 264 $net_ceil = ceil( $net_discount_exact * pow( 10, $decimals ) ) / pow( 10, $decimals ); 265 266 error_log( sprintf( 'TPC Debug: Net floor: %.4f, Net ceil: %.4f', $net_floor, $net_ceil ) ); 267 268 // Calculate gross for both options 269 // IMPORTANT: For Germany with 19% MwSt: 270 // 84.03 * 1.19 = 99.9957 which rounds to 99.99 (NOT 100.00!) 271 // 84.04 * 1.19 = 100.0076 which rounds to 100.01 272 $gross_floor_exact = $net_floor * ( 1 + $avg_tax_rate ); 273 $gross_ceil_exact = $net_ceil * ( 1 + $avg_tax_rate ); 274 $gross_floor = round( $gross_floor_exact, $decimals ); 275 $gross_ceil = round( $gross_ceil_exact, $decimals ); 276 277 error_log( sprintf( 'TPC Debug: Floor exact: %.6f, Ceil exact: %.6f', $gross_floor_exact, $gross_ceil_exact ) ); 278 279 error_log( sprintf( 'TPC Debug: Gross from floor: %.4f, Gross from ceil: %.4f', $gross_floor, $gross_ceil ) ); 280 281 // Choose the net amount that gives us the exact gross we want 282 // The problem: With only 2 decimals, we can't always achieve the exact gross amount 283 // Solution: Return the EXACT net amount with more precision 284 285 if ( $gross_floor == $gross_coupon ) { 286 $net_discount = $net_floor; 287 error_log( 'TPC Debug: Using floor value for exact match' ); 288 } elseif ( $gross_ceil == $gross_coupon ) { 289 $net_discount = $net_ceil; 290 error_log( 'TPC Debug: Using ceil value for exact match' ); 291 } else { 292 // For 19% MwSt: 84.03 gives 99.9957 which rounds to 99.99, NOT 100.00 293 // We need to use 84.04 which gives 100.0076 and rounds to 100.01 294 // But we can accept 100.01 and show 100.00 in display 295 error_log( sprintf( 'TPC Debug: No exact match - floor gives %.4f, ceil gives %.4f', $gross_floor, $gross_ceil ) ); 296 297 // Prefer the value that's slightly over rather than under 298 if ( $gross_floor < $gross_coupon && $gross_ceil > $gross_coupon ) { 299 // If floor is under and ceil is over, use ceil 300 $net_discount = $net_ceil; 301 error_log( 'TPC Debug: Using ceil to avoid underpayment' ); 302 } else { 303 // If neither gives exact match, we need a more sophisticated approach 304 // Calculate what net amount would give us exactly the gross we want 305 // This might require adjusting by fractions of a cent 306 307 error_log( 'TPC Debug: Neither floor nor ceil gives exact match, calculating optimal value' ); 308 309 // We'll use a binary search approach to find the optimal value 310 $lower = $net_floor; 311 $upper = $net_ceil; 312 $best_net = $net_floor; 313 $best_diff = abs( $gross_floor - $gross_coupon ); 314 315 // Try intermediate values 316 for ( $i = 1; $i < 10; $i++ ) { 317 $test_net = $lower + ( $upper - $lower ) * ( $i / 10 ); 318 $test_net = round( $test_net, $decimals + 2 ); // Use extra precision 319 $test_gross = round( $test_net * ( 1 + $avg_tax_rate ), $decimals ); 320 $diff = abs( $test_gross - $gross_coupon ); 321 322 if ( $diff < $best_diff ) { 323 $best_net = $test_net; 324 $best_diff = $diff; 325 326 if ( $diff < 0.001 ) { 327 error_log( sprintf( 'TPC Debug: Found optimal value: %.4f', $best_net ) ); 328 break; 329 } 330 } 331 } 332 333 // Round to allowed decimals 334 $net_discount = round( $best_net, $decimals ); 335 336 // If this still doesn't give us the exact gross, adjust by one cent in the right direction 337 $final_gross = round( $net_discount * ( 1 + $avg_tax_rate ), $decimals ); 338 if ( $final_gross < $gross_coupon ) { 339 $net_discount += pow( 10, -$decimals ); 340 error_log( 'TPC Debug: Adjusted up by one cent for exact match' ); 341 } elseif ( $final_gross > $gross_coupon && $net_discount > pow( 10, -$decimals ) ) { 342 $net_discount -= pow( 10, -$decimals ); 343 error_log( 'TPC Debug: Adjusted down by one cent for exact match' ); 344 } 345 } 346 } 347 348 // Final step: Always return the EXACT mathematical value 349 // Don't try to round to 2 decimals - return with full precision 350 $exact_net = $gross_coupon / ( 1 + $avg_tax_rate ); 351 352 error_log( sprintf( 'TPC Debug: Final calculation - Gross %.4f / (1 + %.4f) = %.10f', 353 $gross_coupon, $avg_tax_rate, $exact_net ) ); 354 355 // Return with high precision to ensure exact calculations 356 return $exact_net; 357 } 358 359 /** 360 * Set correct coupon amounts when creating the order item. 361 * This runs BEFORE the item is saved, allowing us to set the correct net amount. 362 * 363 * @param \WC_Order_Item_Coupon $item The coupon item being created 364 * @param string $code Coupon code 365 * @param float $discount Discount amount 366 * @param \WC_Order $order Order object 367 */ 368 public function set_correct_coupon_amounts( \WC_Order_Item_Coupon $item, string $code, float $discount, \WC_Order $order ): void { 369 error_log( sprintf( 'TPC Debug: set_correct_coupon_amounts called - Code: %s, Discount: %.4f', $code, $discount ) ); 370 371 $coupon = new \WC_Coupon( $code ); 372 373 if ( 'fixed_cart' !== $coupon->get_discount_type() || 374 'yes' !== $coupon->get_meta( 'tpc_apply_after_tax', true ) ) { 375 error_log( 'TPC Debug: Not a tax-proof coupon, skipping' ); 376 return; 377 } 378 379 // Get the gross amount (what customer sees) 380 $gross_amount = floatval( $coupon->get_amount() ); 381 382 // Calculate tax rate from cart totals 383 $cart = WC()->cart; 384 $total_tax = $cart->get_total_tax(); 385 $subtotal = $cart->get_subtotal(); 386 $total = $cart->get_total( 'edit' ); 387 388 // Calculate average tax rate 389 $avg_tax_rate = $subtotal > 0 ? ( $total_tax / $subtotal ) : 0; 390 391 // Calculate precise net amount 392 $net_amount = $this->calculate_precise_net_discount( 393 $gross_amount, 394 $avg_tax_rate, 395 $subtotal, 396 $subtotal + $total_tax 397 ); 398 399 // Calculate tax amount 400 $tax_amount = $gross_amount - $net_amount; 401 402 // Set the correct amounts on the item BEFORE it's saved 403 // IMPORTANT: We store the EXACT net amount with more decimals for correct calculation 404 // This ensures that net * tax_rate = exactly the gross amount we want 405 $item->set_discount( $net_amount ); 406 $item->set_discount_tax( $tax_amount ); 407 408 // Store metadata for reference - with full precision 409 $item->add_meta_data( '_tpc_gross_discount', $gross_amount ); 410 $item->add_meta_data( '_tpc_net_discount', number_format( $net_amount, 10, '.', '' ) ); // Store with 10 decimals 411 $item->add_meta_data( '_tpc_tax_rate', $avg_tax_rate ); 412 $item->add_meta_data( '_tpc_applied_after_tax', 'yes' ); 413 414 error_log( sprintf( 415 'TPC Debug: Setting coupon amounts - Code: %s, Gross: %.4f, Net: %.4f, Tax: %.4f', 416 $code, 417 $gross_amount, 418 $net_amount, 419 $tax_amount 420 ) ); 421 } 422 423 /** 424 * Store precise net amounts after order is created. 425 * This ensures Germanized Pro uses the correct values for invoice generation. 426 * 427 * @param \WC_Order $order Order object 428 * @param array $data Checkout data 429 */ 430 public function store_coupon_net_amounts( \WC_Order $order, array $data ): void { 431 error_log( sprintf( 'TPC Debug: store_coupon_net_amounts called for order %s', $order->get_id() ) ); 432 433 foreach ( $order->get_coupons() as $item ) { 434 $coupon = new \WC_Coupon( $item->get_code() ); 435 436 if ( 'fixed_cart' !== $coupon->get_discount_type() || 437 'yes' !== $coupon->get_meta( 'tpc_apply_after_tax', true ) ) { 438 continue; 439 } 440 441 error_log( sprintf( 'TPC Debug: Processing tax-proof coupon %s', $item->get_code() ) ); 442 443 // Get the gross amount 444 $gross_amount = floatval( $coupon->get_amount() ); 445 446 // Calculate tax rate from order 447 // IMPORTANT: We need the subtotal BEFORE discount to get the correct tax rate 448 $order_subtotal_before_discount = 0; 449 $order_tax = 0; 450 451 // Calculate from line items (products) to get accurate rate 452 foreach ( $order->get_items() as $line_item ) { 453 $order_subtotal_before_discount += $line_item->get_subtotal(); 454 $order_tax += $line_item->get_subtotal_tax(); 455 } 456 457 $avg_tax_rate = $order_subtotal_before_discount > 0 ? ( $order_tax / $order_subtotal_before_discount ) : 0; 458 459 error_log( sprintf( 460 'TPC Debug: Order data - Subtotal (before discount): %.4f, Tax: %.4f, Calc Tax Rate: %.6f (%.2f%%)', 461 $order_subtotal_before_discount, 462 $order_tax, 463 $avg_tax_rate, 464 $avg_tax_rate * 100 465 ) ); 466 467 // Calculate exact net amount 468 $net_amount_exact = $gross_amount / ( 1 + $avg_tax_rate ); 469 $tax_amount_exact = $gross_amount - $net_amount_exact; 470 471 error_log( sprintf( 472 'TPC Debug: Calculation - Gross: %.4f / (1 + %.6f) = Net: %.10f, Tax: %.10f', 473 $gross_amount, 474 $avg_tax_rate, 475 $net_amount_exact, 476 $tax_amount_exact 477 ) ); 478 479 // Update the item with precise values 480 $item->set_discount( $net_amount_exact ); 481 $item->set_discount_tax( $tax_amount_exact ); 482 483 // Store metadata 484 $item->update_meta_data( '_tpc_gross_discount', $gross_amount ); 485 $item->update_meta_data( '_tpc_net_discount', $net_amount_exact ); 486 $item->update_meta_data( '_tpc_tax_rate', $avg_tax_rate ); 487 $item->update_meta_data( '_tpc_applied_after_tax', 'yes' ); 488 $item->save(); 489 490 error_log( sprintf( 491 'TPC Debug: Updated coupon %s - Gross: %.4f, Net: %.10f, Tax: %.10f', 492 $item->get_code(), 493 $gross_amount, 494 $net_amount_exact, 495 $tax_amount_exact 496 ) ); 497 } 498 499 // Save the order to persist all changes 500 $order->save(); 501 } 502 503 504 /** 505 * Adjust coupon display in admin order view. 506 * 507 * @param string $subtotal Formatted subtotal 508 * @param \WC_Order_Item $item Order item 509 * @param \WC_Order $order Order object 510 * @return string Modified subtotal 511 */ 512 public function adjust_admin_coupon_display( string $subtotal, \WC_Order_Item $item, \WC_Order $order ): string { 513 // Only process coupon items 514 if ( ! $item->is_type( 'coupon' ) ) { 515 return $subtotal; 516 } 517 518 $net_discount = $item->get_meta( '_tpc_net_discount' ); 519 $gross_discount = $item->get_meta( '_tpc_gross_discount' ); 520 521 error_log( sprintf( 'TPC Debug: Meta data - Net: %s, Gross: %s', 522 $net_discount ? $net_discount : 'NOT FOUND', 523 $gross_discount ? $gross_discount : 'NOT FOUND' 524 ) ); 525 526 if ( $net_discount && $gross_discount ) { 527 // Show detailed information in admin with 4 decimal places 528 $net_value = floatval( $net_discount ); 529 $net_display = number_format( $net_value, 4, ',', '' ); 530 $subtotal = sprintf( 531 '-<span class="woocommerce-Price-amount amount">%s <span class="woocommerce-Price-currencySymbol">€</span></span> <small class="woocommerce-help-tip" data-tip="Brutto: %s">(Brutto: %s)</small>', 532 $net_display, 533 wc_price( $gross_discount ), 534 wc_price( $gross_discount ) 535 ); 536 537 error_log( sprintf( 538 'TPC Debug: Admin display adjusted - Net: %.4f, Gross: %.4f', 539 $net_discount, 540 $gross_discount 541 ) ); 542 } 543 544 return $subtotal; 545 } 546 547 548 /** 549 * Adjust total discount to ensure it matches the gross coupon amount. 550 * This is critical for invoice generation. 551 * 552 * @param float $total_discount Current total discount 553 * @param \WC_Order $order Order object 554 * @return float Adjusted discount 555 */ 556 public function adjust_total_discount_for_invoices( float $total_discount, \WC_Order $order ): float { 557 $adjusted_total = 0; 558 559 foreach ( $order->get_coupons() as $item ) { 560 if ( $item->get_meta( '_tpc_applied_after_tax' ) === 'yes' ) { 561 // Use the gross discount amount for tax-proof coupons 562 $gross_discount = $item->get_meta( '_tpc_gross_discount' ); 563 if ( $gross_discount ) { 564 $adjusted_total += floatval( $gross_discount ); 565 error_log( sprintf( 566 'TPC Debug: Adjusting invoice discount for %s - Using gross: %.4f instead of net+tax', 567 $item->get_code(), 568 $gross_discount 569 ) ); 570 } else { 571 $adjusted_total += $item->get_discount() + $item->get_discount_tax(); 572 } 573 } else { 574 $adjusted_total += $item->get_discount() + $item->get_discount_tax(); 575 } 576 } 577 578 if ( $adjusted_total != $total_discount ) { 579 error_log( sprintf( 580 'TPC Debug: Adjusted total discount from %.4f to %.4f', 581 $total_discount, 582 $adjusted_total 583 ) ); 584 } 585 586 return $adjusted_total; 587 } 588 589 /** 590 * Adjust StoreaBill invoice discount total to use gross amounts. 591 * This filter is called when StoreaBill calculates the total discount for the invoice. 592 * 593 * @param float $total Current discount total 594 * @param object $invoice The invoice document 595 * @return float Adjusted total 596 */ 597 public function storeabill_get_discount_total_gross( float $total, $invoice ): float { 598 // Get the WooCommerce order 599 if ( ! method_exists( $invoice, 'get_order' ) ) { 600 return $total; 601 } 602 603 $order = $invoice->get_order(); 604 if ( ! $order ) { 605 return $total; 606 } 607 608 $adjusted_total = 0; 609 $has_tax_proof_coupons = false; 610 611 foreach ( $order->get_coupons() as $item ) { 612 $gross_discount = $item->get_meta( '_tpc_gross_discount' ); 613 if ( $gross_discount ) { 614 $adjusted_total += floatval( $gross_discount ); 615 $has_tax_proof_coupons = true; 616 error_log( sprintf( 617 'TPC Debug: StoreaBill discount total - Using gross: %.4f for %s', 618 $gross_discount, 619 $item->get_code() 620 ) ); 621 } else { 622 $adjusted_total += $item->get_discount() + $item->get_discount_tax(); 623 } 624 } 625 626 if ( $has_tax_proof_coupons && $adjusted_total != $total ) { 627 error_log( sprintf( 628 'TPC Debug: StoreaBill total discount changed from %.4f to %.4f', 629 $total, 630 $adjusted_total 631 ) ); 632 return $adjusted_total; 633 } 634 635 return $total; 636 } 637 638 /** 639 * Adjust StoreaBill voucher total to use gross amounts. 640 * 641 * @param float $total Current voucher total 642 * @param object $order_data_store The WooCommerce order data store 643 * @return float Adjusted total 644 */ 645 public function storeabill_voucher_total_gross( float $total, $order_data_store ): float { 646 error_log( sprintf( 'TPC Debug: storeabill_voucher_total_gross called - Total: %.4f', $total ) ); 647 648 if ( ! method_exists( $order_data_store, 'get_order' ) ) { 649 error_log( 'TPC Debug: No get_order method' ); 650 return $total; 651 } 652 653 $order = $order_data_store->get_order(); 654 if ( ! $order ) { 655 error_log( 'TPC Debug: No order found' ); 656 return $total; 657 } 658 659 $adjusted_total = 0; 660 661 foreach ( $order->get_coupons() as $item ) { 662 $gross_discount = $item->get_meta( '_tpc_gross_discount' ); 663 if ( $gross_discount ) { 664 $adjusted_total += floatval( $gross_discount ); 665 error_log( sprintf( 666 'TPC Debug: StoreaBill voucher total - Using gross: %.4f for %s', 667 $gross_discount, 668 $item->get_code() 669 ) ); 670 } 671 } 672 673 if ( $adjusted_total > 0 ) { 674 error_log( sprintf( 675 'TPC Debug: StoreaBill voucher total changed from %.4f to %.4f', 676 $total, 677 $adjusted_total 678 ) ); 679 return $adjusted_total; 680 } 681 682 return $total; 683 } 684 685 /** 686 * Log all Germanized/StoreaBill hooks to find the right one. 687 * This is a temporary debugging method. 688 * 689 * @param string $hook Hook name 690 */ 691 public function log_germanized_hooks( string $hook ): void { 692 // Only log during invoice generation (check if we're in admin) 693 if ( ! is_admin() ) { 694 return; 695 } 696 697 // Only log hooks that might be related to invoice/discount 698 if ( strpos( $hook, 'gzd' ) !== false || 699 strpos( $hook, 'germanized' ) !== false || 700 strpos( $hook, 'storeabill' ) !== false || 701 strpos( $hook, 'invoice' ) !== false ) { 702 703 // Only log filter hooks (not actions) to reduce noise 704 if ( strpos( $hook, 'discount' ) !== false || 705 strpos( $hook, 'total' ) !== false || 706 strpos( $hook, 'coupon' ) !== false ) { 707 error_log( sprintf( 'TPC Debug: Hook fired: %s', $hook ) ); 708 } 709 } 710 } 711 712 /** Public wakeup to satisfy PHP's magic method requirement. */ 713 public function __wakeup() {} 714 } 715 716 // Bootstrap the plugin. 717 add_action( 'plugins_loaded', [ Plugin::class, 'instance' ] ); 26 add_action( 'plugins_loaded', array( Plugin::class, 'instance' ) );
Note: See TracChangeset
for help on using the changeset viewer.