Plugin Directory

Changeset 3381508


Ignore:
Timestamp:
10/20/2025 07:33:48 PM (4 months ago)
Author:
Jyria
Message:

tagging version 1.0.4

Location:
taxproof-coupons-for-woocommerce
Files:
3 edited
4 copied

Legend:

Unmodified
Added
Removed
  • taxproof-coupons-for-woocommerce/tags/1.0.4/README.md

    r3343132 r3381508  
    55**Requires at least:** 6.5 
    66**Tested up to:** 6.8 
    7 **Stable tag:** 1.0.3
     7**Stable tag:** 1.0.4
    88**License:** GPLv2 or later 
    99**License URI:** https://www.gnu.org/licenses/gpl-2.0.html
     
    1414
    1515- Adds **Apply coupon after tax** checkbox to coupon settings.
    16 - Converts gross coupon values into precise net discounts.
     16- Converts gross coupon values into precise net discounts with high accuracy.
    1717- 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.
    1822
    1923## Installation
     
    2428
    2529## Changelog
     30
     31### 1.0.4
     32
     33- **Verbesserte Präzision**: Neue präzise Berechnungsmethode für exakte Steuerumrechnungen
     34- **Erweiterte Anzeige-Funktionen**: Verbesserte Coupon-Anzeige im Warenkorb und Checkout
     35- **StoreaBill/Germanized Pro Integration**: Vollständige Kompatibilität mit Germanized Pro für Rechnungsgenerierung
     36- **Admin-Verbesserungen**: Detaillierte Anzeige von Netto- und Bruttobeträgen in der WooCommerce Admin-Oberfläche
     37- **Erweiterte Metadaten-Speicherung**: Präzise Speicherung von Coupon-Beträgen mit hoher Genauigkeit
     38- **Debug-Funktionen**: Erweiterte Logging-Funktionen für bessere Entwicklung und Fehlerbehebung
     39- **Hook-Integration**: Neue Hooks für bessere Integration mit WooCommerce und Drittanbieter-Plugins
     40- **Performance-Optimierungen**: Verbesserte Berechnungslogik für komplexe Steuerszenarien
    2641
    2742### 1.0.3
  • taxproof-coupons-for-woocommerce/tags/1.0.4/readme.txt

    r3343132 r3381508  
    55Requires at least: 6.5
    66Tested up to: 6.8
    7 Stable tag: 1.0.3
     7Stable tag: 1.0.4
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    2828== Changelog ==
    2929
     30= 1.0.4 =
     31
     32Release date: January 2025
     33
     34* **Verbesserte Präzision**: Neue präzise Berechnungsmethode für exakte Steuerumrechnungen
     35* **Erweiterte Anzeige-Funktionen**: Verbesserte Coupon-Anzeige im Warenkorb und Checkout
     36* **StoreaBill/Germanized Pro Integration**: Vollständige Kompatibilität mit Germanized Pro für Rechnungsgenerierung
     37* **Admin-Verbesserungen**: Detaillierte Anzeige von Netto- und Bruttobeträgen in der WooCommerce Admin-Oberfläche
     38* **Erweiterte Metadaten-Speicherung**: Präzise Speicherung von Coupon-Beträgen mit hoher Genauigkeit
     39* **Debug-Funktionen**: Erweiterte Logging-Funktionen für bessere Entwicklung und Fehlerbehebung
     40* **Hook-Integration**: Neue Hooks für bessere Integration mit WooCommerce und Drittanbieter-Plugins
     41* **Performance-Optimierungen**: Verbesserte Berechnungslogik für komplexe Steuerszenarien
     42
    3043= 1.0.3 =
    3144
  • taxproof-coupons-for-woocommerce/tags/1.0.4/tax-proof-coupons-plugin.php

    r3343132 r3381508  
    44 * Plugin URI:        https://github.com/s-a-s-k-i-a/tax-proof-coupons
    55 * Description:       Ensure fixed-value coupons always apply after tax, regardless of VAT rate or customer location.
    6  * Version:           1.0.3
     6 * Version:           1.0.4
    77 * Author:            Saskia Teichmann
    88 * Author URI:        https://saskialund.de
     
    2424class Plugin {
    2525    /** Plugin version. */
    26     public const VERSION = '1.0.2';
     26    public const VERSION = '1.0.4';
    2727
    2828    /** Singleton instance. */
     
    4444    /** Prevent direct instantiation. */
    4545    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    }
    4655
    4756    /** Initialize WP and WooCommerce hooks. */
     
    5059        add_action( 'woocommerce_coupon_options_save', [ $this, 'save_apply_after_tax_checkbox' ], 10, 2 );
    5160        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        }
    5284    }
    5385
     
    129161        $avg_tax_rate = ( $total_gross / $total_net ) - 1;
    130162
    131         // Convert the gross coupon into the correct net discount.
    132         $net_discount = $gross_coupon / ( 1 + $avg_tax_rate );
    133         $net_discount = round( $net_discount, wc_get_price_decimals() );
     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        );
    134170
    135171        // Mark as applied so we don't double-dip.
     
    139175    }
    140176
    141     /** Public wakeup to satisfy PHP’s magic method requirement. */
     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&nbsp;<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. */
    142713    public function __wakeup() {}
    143714}
  • taxproof-coupons-for-woocommerce/trunk/README.md

    r3343132 r3381508  
    55**Requires at least:** 6.5 
    66**Tested up to:** 6.8 
    7 **Stable tag:** 1.0.3
     7**Stable tag:** 1.0.4
    88**License:** GPLv2 or later 
    99**License URI:** https://www.gnu.org/licenses/gpl-2.0.html
     
    1414
    1515- Adds **Apply coupon after tax** checkbox to coupon settings.
    16 - Converts gross coupon values into precise net discounts.
     16- Converts gross coupon values into precise net discounts with high accuracy.
    1717- 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.
    1822
    1923## Installation
     
    2428
    2529## Changelog
     30
     31### 1.0.4
     32
     33- **Verbesserte Präzision**: Neue präzise Berechnungsmethode für exakte Steuerumrechnungen
     34- **Erweiterte Anzeige-Funktionen**: Verbesserte Coupon-Anzeige im Warenkorb und Checkout
     35- **StoreaBill/Germanized Pro Integration**: Vollständige Kompatibilität mit Germanized Pro für Rechnungsgenerierung
     36- **Admin-Verbesserungen**: Detaillierte Anzeige von Netto- und Bruttobeträgen in der WooCommerce Admin-Oberfläche
     37- **Erweiterte Metadaten-Speicherung**: Präzise Speicherung von Coupon-Beträgen mit hoher Genauigkeit
     38- **Debug-Funktionen**: Erweiterte Logging-Funktionen für bessere Entwicklung und Fehlerbehebung
     39- **Hook-Integration**: Neue Hooks für bessere Integration mit WooCommerce und Drittanbieter-Plugins
     40- **Performance-Optimierungen**: Verbesserte Berechnungslogik für komplexe Steuerszenarien
    2641
    2742### 1.0.3
  • taxproof-coupons-for-woocommerce/trunk/readme.txt

    r3343132 r3381508  
    55Requires at least: 6.5
    66Tested up to: 6.8
    7 Stable tag: 1.0.3
     7Stable tag: 1.0.4
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    2828== Changelog ==
    2929
     30= 1.0.4 =
     31
     32Release date: January 2025
     33
     34* **Verbesserte Präzision**: Neue präzise Berechnungsmethode für exakte Steuerumrechnungen
     35* **Erweiterte Anzeige-Funktionen**: Verbesserte Coupon-Anzeige im Warenkorb und Checkout
     36* **StoreaBill/Germanized Pro Integration**: Vollständige Kompatibilität mit Germanized Pro für Rechnungsgenerierung
     37* **Admin-Verbesserungen**: Detaillierte Anzeige von Netto- und Bruttobeträgen in der WooCommerce Admin-Oberfläche
     38* **Erweiterte Metadaten-Speicherung**: Präzise Speicherung von Coupon-Beträgen mit hoher Genauigkeit
     39* **Debug-Funktionen**: Erweiterte Logging-Funktionen für bessere Entwicklung und Fehlerbehebung
     40* **Hook-Integration**: Neue Hooks für bessere Integration mit WooCommerce und Drittanbieter-Plugins
     41* **Performance-Optimierungen**: Verbesserte Berechnungslogik für komplexe Steuerszenarien
     42
    3043= 1.0.3 =
    3144
  • taxproof-coupons-for-woocommerce/trunk/tax-proof-coupons-plugin.php

    r3343132 r3381508  
    44 * Plugin URI:        https://github.com/s-a-s-k-i-a/tax-proof-coupons
    55 * Description:       Ensure fixed-value coupons always apply after tax, regardless of VAT rate or customer location.
    6  * Version:           1.0.3
     6 * Version:           1.0.4
    77 * Author:            Saskia Teichmann
    88 * Author URI:        https://saskialund.de
     
    2424class Plugin {
    2525    /** Plugin version. */
    26     public const VERSION = '1.0.2';
     26    public const VERSION = '1.0.4';
    2727
    2828    /** Singleton instance. */
     
    4444    /** Prevent direct instantiation. */
    4545    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    }
    4655
    4756    /** Initialize WP and WooCommerce hooks. */
     
    5059        add_action( 'woocommerce_coupon_options_save', [ $this, 'save_apply_after_tax_checkbox' ], 10, 2 );
    5160        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        }
    5284    }
    5385
     
    129161        $avg_tax_rate = ( $total_gross / $total_net ) - 1;
    130162
    131         // Convert the gross coupon into the correct net discount.
    132         $net_discount = $gross_coupon / ( 1 + $avg_tax_rate );
    133         $net_discount = round( $net_discount, wc_get_price_decimals() );
     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        );
    134170
    135171        // Mark as applied so we don't double-dip.
     
    139175    }
    140176
    141     /** Public wakeup to satisfy PHP’s magic method requirement. */
     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&nbsp;<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. */
    142713    public function __wakeup() {}
    143714}
Note: See TracChangeset for help on using the changeset viewer.