Skip to content

Conversation

@arifulhoque7
Copy link
Contributor

@arifulhoque7 arifulhoque7 commented Nov 25, 2025

Fix: PayPal Double Taxation & Webhook Payment Recording Issues

Close issue
Related PRO PR

This branch resolves multiple PayPal-related bugs involving tax calculations, payment breakdown handling, and webhook validation.


Issues Fixed

1. Double Taxation in PayPal

  • Problem: A $23 post with 10% tax should be $25.30, but PayPal was receiving $27.83.
  • Root Cause: The Pro version’s wpuf_amount_with_tax filter was re-applying tax on an already-taxed amount.
  • Solution: Preserve the correctly calculated amount before filters to prevent double taxation.

2. Incorrect PayPal Amount Breakdown

  • Problem: PayPal applied tax again because the request didn’t include a proper subtotal + tax breakdown.
  • Root Cause: The PayPal order request lacked the items array and proper amount.breakdown.
  • Solution: Added full breakdown structure (subtotal, tax, items) when tax is enabled.

3. Webhook Payments Not Recorded

  • Problem: Payments completed in PayPal were not saved into the transaction table.
  • Root Cause: Webhook validation compared PayPal total against WP User Frontend subtotal only.
  • Solution: Validation now checks against (subtotal + tax).

Files Updated

wp-content/plugins/wp-user-frontend/Lib/Gateway/Paypal.php

  • Lines 1296–1306: Added protection to avoid double taxation from filters
  • Lines 1457–1502: Added structured subtotal/tax breakdown for PayPal API
  • Lines 296–300: Fixed webhook validation to match total amount (subtotal + tax)
  • Lines 329–331: Use correct subtotal/tax values from custom_data during webhook processing
  • Line 1512: Added detailed error logging for PayPal API responses

Testing Results

✔️ Correct PayPal checkout amount: $25.30
✔️ Webhook correctly records completed PayPal payments
✔️ Correct breakdown sent:

  • Subtotal: $23.00
  • Tax: $2.30
  • Total: $25.30

Summary by CodeRabbit

  • Bug Fixes

    • Stronger validation of payment amounts with clearer "Expected vs Received" messages and richer error details for failed payments or invalid orders.
    • Corrected tax handling to avoid double taxation and ensure accurate totals for one-time and recurring payments.
  • Improvements

    • Restructured payment payloads to include explicit amounts, optional breakdowns/items, and richer payer/custom metadata.
    • Enhanced error reporting when approvals or order/subscription creation fail.
  • Compatibility

    • Form payment logic now supports both new Vue-based settings and legacy configuration paths.

✏️ Tip: You can customize this high-level summary in your review settings.

@arifulhoque7 arifulhoque7 requested a review from sapayth November 25, 2025 03:11
@arifulhoque7 arifulhoque7 self-assigned this Nov 25, 2025
@coderabbitai
Copy link

coderabbitai bot commented Nov 25, 2025

Walkthrough

Validate numeric subtotal and tax from custom_data, compute expected subtotal/tax/total and verify amounts; refactor PayPal payloads to a tax-aware purchase_unit with custom_id; improve PayPal error messages; add compatibility-aware pay-per-post detection with legacy fallback.

Changes

Cohort / File(s) Summary
PayPal gateway updates
Lib/Gateway/Paypal.php
Validate that custom_data contains numeric subtotal and tax; compute expected_subtotal, expected_tax, expected_total and verify against received amount in process_payment_capture; propagate derived subtotal/tax_amount for downstream processing; refactor prepare_to_send to construct a purchase_unit payload (amount, conditional breakdown, items) and embed a structured custom_id (type, user, coupon, subtotal, tax, item_number, payer); unify one-off and recurring flows to use the new payload and append detailed PayPal response info to error messages.
Frontend AJAX form compatibility
includes/Ajax/Frontend_Form_Ajax.php
Replace explicit pay-per-post check with computed $is_pay_per_post that reads new Vue form fields (choose_payment_option, payment_options) and falls back to legacy enable_pay_per_post; use this flag in charging/redirect logic to maintain backward compatibility.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • sapayth

Poem

🐇 I count each subtotal, tax, and dime,

I pack the purchase unit, tidy every time.
I tuck custom_id with user, coupon, and more,
I hop when totals match — and thump if numbers roar. 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Fix PayPal double taxation and webhook payment recording issues' accurately and specifically describes the main changes in the pull request, which address double taxation bugs and webhook validation problems.
✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4ec6d89 and c31c4a4.

📒 Files selected for processing (1)
  • Lib/Gateway/Paypal.php
🧰 Additional context used
🪛 GitHub Actions: Inspections
Lib/Gateway/Paypal.php

[warning] 1-1: Silencing errors is strongly discouraged. Use proper error checking instead. Found: @Header( 'Content-Type: application/json; charset='

🔇 Additional comments (6)
Lib/Gateway/Paypal.php (6)

296-314: LGTM! Webhook validation properly checks subtotal and tax.

The validation correctly ensures both subtotal and tax exist and are numeric before computing the expected total. The error message includes both expected and received amounts for easier debugging. This addresses the previous concern about silently coercing missing values to zero.


335-337: LGTM! Correctly uses validated values.

The code appropriately uses the already-validated expected_subtotal and expected_tax values from the custom data for creating the payment record.


1380-1383: LGTM! Tax rate calculation is safe.

The tax rate calculation correctly handles division by zero with appropriate guards, and the result is properly stored in the subscription's custom_id for later reference.


1474-1519: LGTM! Purchase unit structure correctly includes tax breakdown when applicable.

The code properly:

  • Checks $data['tax'] > 0 (not the old $tax_amount variable)
  • Includes the breakdown with item_total and tax_total only when tax is present
  • Formats all amounts correctly for PayPal (2 decimals, dot separator)
  • Includes an items array that matches the breakdown
  • Embeds all necessary metadata in custom_id for webhook processing

This ensures PayPal receives the pre-calculated tax and doesn't apply its own tax calculation.


1554-1556: LGTM! Enhanced error logging will aid debugging.

The code now includes PayPal's error details in the exception message, which will significantly help troubleshoot order creation failures. The error is properly thrown (not displayed directly to users), so there are no information disclosure concerns.


1285-1296: The filter restoration logic is sound and correctly prevents adding filters that weren't originally present.

The code stores the boolean result of remove_filter() in $pro_tax_removed, then only calls add_filter() when that condition is true (line 1294-1295). Since remove_filter() returns false if the filter wasn't attached, the conditional gate ensures add_filter() is never called for non-existent filters. This is the correct WordPress pattern.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@arifulhoque7 arifulhoque7 added needs: testing needs: dev review This PR needs review by a developer labels Nov 25, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Lib/Gateway/Paypal.php (1)

1-1: Fix trailing whitespace to satisfy PHPCS

The inspections pipeline reports:

1-1: Whitespace found at end of line (phpcs).

Please remove trailing whitespace on line 1 (and any others PHPCS flags). For example:

-<?php 
+<?php

This should clear the coding‑standards job.

🧹 Nitpick comments (2)
Lib/Gateway/Paypal.php (2)

1545-1548: Avoid surfacing raw PayPal error payloads directly to the end user

You now capture error details when no id is present:

$error_details = isset( $body['details'] ) ? wp_json_encode( $body['details'] ) : wp_remote_retrieve_body( $response );
throw new \Exception( 'Invalid response from PayPal - no order ID. Error: ' . $error_details );

Given that prepare_to_send wraps this in wp_die( $e->getMessage() );, this can expose raw API responses (including internal error structures) directly to visitors. That may be noisy for users and can leak implementation details.

Consider:

  • Logging the full $error_details (to a debug log or a dedicated logger) instead.
  • Throwing a generic, localized message to the UI, e.g. “Failed to create PayPal order, please contact support,” with maybe a short error code.

Example:

-                    $error_details = isset( $body['details'] ) ? wp_json_encode( $body['details'] ) : wp_remote_retrieve_body( $response );
-                    throw new \Exception( 'Invalid response from PayPal - no order ID. Error: ' . $error_details );
+                    $error_details = isset( $body['details'] ) ? wp_json_encode( $body['details'] ) : wp_remote_retrieve_body( $response );
+                    error_log( 'WPUF PayPal order error: ' . $error_details );
+                    throw new \Exception( __( 'Failed to create PayPal order. Please contact site administrator.', 'wp-user-frontend' ) );

1465-1485: Trim custom_id payload in payment order to follow PayPal best practices; subscription custom_id is already optimal

PayPal's custom_id field is limited to 255 characters and best practice is to store only a compact external identifier (e.g., your system's primary key) rather than large blobs or sensitive/PII data for reconciliation. Your current payload (211 chars) fits within the limit, but webhook handlers never read first_name, last_name, or email back from custom_id—user and post data are instead looked up via get_user_by('id', user_id) and get_post(item_number). Removing these fields reduces unnecessary PII exposure and aligns with PayPal's guidance.

Apply the suggested diff to line 1472 to keep only: type, user_id, item_number, coupon_id, subtotal, tax.

The subscription custom_id at line 1387 is already correctly structured without PII and needs no changes.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 47b6597 and 94179ec.

📒 Files selected for processing (1)
  • Lib/Gateway/Paypal.php (5 hunks)
🧰 Additional context used
🪛 GitHub Actions: Inspections
Lib/Gateway/Paypal.php

[error] 1-1: Whitespace found at end of line (phpcs).

@Rubaiyat-E-Mohammad Rubaiyat-E-Mohammad added bug QA Approved This PR is approved by the QA team and removed needs: testing bug labels Nov 25, 2025
Copy link
Member

@sapayth sapayth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

recheck the values with caution. we already have amount, tax amount etc. calculate and send the proper data to PayPal by separating them in billing amount and tax amount as per PayPal documentation. also recheck if we are storing data in our db table properly

$correct_billing_amount = $billing_amount;

// Apply filters but ensure we use the correct amount if tax was already calculated
$billing_amount = apply_filters( 'wpuf_payment_amount', $billing_amount, $post_id );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need to check from where the $billing_amount is getting modified using wpuf_payment_amount filter. if we revert the amount, the filter hook looses it's purpose

@sapayth sapayth added needs: author reply This PR needs author feedback or code changes and removed needs: dev review This PR needs review by a developer labels Dec 3, 2025
arifulhoque7 and others added 2 commits December 3, 2025 15:34
Improves PayPal payment processing by validating and separating subtotal and tax calculations, ensuring coupons are applied before tax, and adding debug logging. Updates pay-per-post logic in the frontend form AJAX handler to support both new and legacy form settings for better compatibility.
@arifulhoque7
Copy link
Contributor Author

done @sapayth vai

@arifulhoque7 arifulhoque7 added needs: dev review This PR needs review by a developer and removed needs: author reply This PR needs author feedback or code changes labels Dec 3, 2025
Eliminated error_log statements from the Paypal::prepare_to_send function to clean up unnecessary debug output and improve code clarity.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
Lib/Gateway/Paypal.php (2)

1467-1512: Well-structured PayPal purchase unit with proper tax handling.

The conditional inclusion of the breakdown and items array only when tax_amount > 0 ensures PayPal receives the pre-calculated tax rather than computing it again, which addresses the double taxation issue.

The custom_id payload comprehensively includes all necessary data for webhook processing.

One operational note: ensure merchants verify their PayPal account settings don't have automatic tax calculation enabled, as that could still cause tax to be applied even when a breakdown is provided. This is more of a configuration concern than a code issue.


1547-1549: Improved error logging will help with debugging PayPal issues.

Including the detailed error response from PayPal in the exception message is a valuable addition for troubleshooting API failures.

Minor optimization: you call wp_remote_retrieve_body( $response ) potentially twice (lines 1548 and 1549). Consider storing it in a variable once:

                if ( ! isset( $body['id'] ) ) {
-                    // Log the full error response for debugging
-                    $error_details = isset( $body['details'] ) ? wp_json_encode( $body['details'] ) : wp_remote_retrieve_body( $response );
+                    $response_body = wp_remote_retrieve_body( $response );
+                    $error_details = isset( $body['details'] ) ? wp_json_encode( $body['details'] ) : $response_body;
                    throw new \Exception( 'Invalid response from PayPal - no order ID. Error: ' . $error_details );
                }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4a582c0 and 49f7e01.

📒 Files selected for processing (1)
  • Lib/Gateway/Paypal.php (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
Lib/Gateway/Paypal.php (1)
includes/Traits/TaxableTrait.php (2)
  • wpuf_tax_enabled (23-31)
  • wpuf_current_tax_rate (385-447)
🔇 Additional comments (3)
Lib/Gateway/Paypal.php (3)

295-314: Validation improvements look solid.

The explicit validation of subtotal and tax being present and numeric addresses the previous review concern about silent coercion to zero. The exception messages are clear and will aid debugging.

One minor consideration: the exact string equality check (line 312) after number_format works well for typical cases but has no tolerance for rounding variance. If future requirements involve complex discount calculations or PayPal-side adjustments, consider allowing a small epsilon (e.g., ±$0.01) to prevent legitimate payments from being rejected due to floating-point precision issues.


335-337: LGTM!

Using the validated expected values from custom_data ensures consistency between what was sent to PayPal and what's recorded in the webhook handler.


1251-1252: Cleaner data source for return URL parameters.

Using $data['type'] and $data['item_number'] instead of extracting from $_GET is more reliable and maintainable, as it ensures consistency throughout the payment flow.

$data['subtotal'] = $billing_amount - $tax_amount;
$data['subtotal'] = $subtotal;
$data['tax'] = $tax_amount;
$billing_amount = apply_filters( 'wpuf_payment_amount', $billing_amount, $post_id );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • do not remove existing filter: wpuf_payment_amount.
  • Tax is a pro feature. move all the tax related calculation to pro.
  • send the tax separately to paypal. have a look at paypal doc

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done @sapayth vai, @Rubaiyat-E-Mohammad vai, please recheck this PR as we refactored the code.

@sapayth sapayth added needs: author reply This PR needs author feedback or code changes and removed needs: dev review This PR needs review by a developer labels Dec 30, 2025
arifulhoque7 and others added 2 commits December 30, 2025 13:02
Replaces direct tax calculation with filter-based approach for better compatibility with Pro's tax handling. Removes redundant tax rate retrieval and ensures tax breakdown is obtained via filters, aligning with Stripe's pattern and improving subtotal/tax breakdown accuracy for PayPal API.
@arifulhoque7 arifulhoque7 added needs: dev review This PR needs review by a developer and removed needs: author reply This PR needs author feedback or code changes labels Dec 30, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Lib/Gateway/Paypal.php (1)

203-203: Address PHPCS violations flagged by the pipeline.

The pipeline identified two code style issues:

  1. Line 203: Error suppression with @ operator is discouraged. The header() call should include proper error checking instead.
  2. Multi-line function calls: Opening parentheses should be the last content on the line.

Please resolve these PHPCS violations to maintain code quality standards.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 49f7e01 and c1f2637.

📒 Files selected for processing (1)
  • Lib/Gateway/Paypal.php
🧰 Additional context used
🪛 GitHub Actions: Inspections
Lib/Gateway/Paypal.php

[warning] 1-1: Silencing errors is strongly discouraged. Use proper error checking instead. Found: @Header( 'Content-Type: application/json; charset=' ...


[error] 1-1: Opening parenthesis of a multi-line function call must be the last content on the line. Command failed: vendor/bin/phpcs Lib/Gateway/Paypal.php includes/Ajax/Frontend_Form_Ajax.php -q --report=checkstyle | cs2pr --graceful-warnings

🔇 Additional comments (5)
Lib/Gateway/Paypal.php (5)

296-314: LGTM! Webhook validation properly rejects invalid data.

The validation now explicitly checks that both subtotal and tax exist and are numeric before using them to compute the expected total. This addresses the previous concern about silently coercing missing or non-numeric values to 0. The error message includes both expected and received amounts for debugging.


335-337: LGTM! Correctly uses validated subtotal and tax from custom data.

Using the validated expected_subtotal and expected_tax values from custom_data ensures the webhook processing uses the amounts that were calculated correctly at payment creation time.


1245-1251: LGTM! Using data-driven values for return URL is more reliable.

Constructing the return URL with $data['type'] and $data['item_number'] directly from the payment data array is cleaner and more reliable than deriving these from GET parameters.


1555-1557: LGTM! Improved error logging helps with debugging PayPal API issues.

Adding detailed error information from PayPal's response to the exception message will make it much easier to diagnose payment failures.


1286-1314: Verify Bank.php compatibility or standardize tax handling across gateways.

The refactored Paypal.php intentionally removes Pro's tax filter before applying wpuf_payment_amount, expecting it to output subtotal-before-tax rather than total-with-tax. However, Bank.php (line 85) maintains the opposite expectation:

$data['cost'] = apply_filters( 'wpuf_payment_amount', $data['price'], $post_id ); //price with tax from pro
$data['tax'] = floatval( $data['cost'] ) - floatval( $data['price'] );

Bank.php expects the filter to output total-with-tax, calculating tax as the difference. This same pattern is used throughout the codebase in Admin_Subscription.php, subscription.php, and Admin/Admin_Subscription.php.

This creates a breaking change: Third-party filters hooking into wpuf_payment_amount will malfunction when applied to the Bank gateway after being tested on Paypal, or vice versa. Either standardize tax handling across all gateways using the same approach, or update Bank.php and related files to align with Paypal.php's new pattern of delegating tax calculation to a separate wpuf_payment_tax_breakdown filter.

@arifulhoque7 arifulhoque7 requested a review from sapayth December 30, 2025 07:08
@arifulhoque7 arifulhoque7 added needs: testing and removed QA Approved This PR is approved by the QA team labels Dec 30, 2025
Removes unused $tax_amount variable and updates tax checks and values to use $data['tax'] directly. Ensures correct tax handling in purchase unit breakdown.
@sapayth sapayth self-assigned this Jan 1, 2026
@sapayth sapayth added dev: on-progress and removed needs: dev review This PR needs review by a developer labels Jan 1, 2026
@Rubaiyat-E-Mohammad
Copy link
Contributor

Rubaiyat-E-Mohammad commented Jan 3, 2026

Please provide zip for both the PR as paypal only works in live site. @arifulhoque7 vai, @sapayth vai

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs: dev review This PR needs review by a developer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants