<?php
/**
* Install WordPress Hooks.
*
* @package Stripe\StripeTaxForWooCommerce\WordPress
*/
namespace Stripe\StripeTaxForWooCommerce\WordPress;
// Exit if script started not from WordPress.
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Utilities\NumberUtil;
use Exception;
use Stripe\StripeTaxForWooCommerce\SDK\lib\Exception\ApiErrorException;
use Stripe\StripeTaxForWooCommerce\SDK\lib\Service\AccountService;
use Stripe\StripeTaxForWooCommerce\SDK\lib\Stripe;
use Stripe\StripeTaxForWooCommerce\SDK\lib\StripeClient;
use Stripe\StripeTaxForWooCommerce\Stripe\CalculateTax;
use Stripe\StripeTaxForWooCommerce\Stripe\Exception\CountryStateException;
use Stripe\StripeTaxForWooCommerce\Stripe\Exception\CountrySupportException;
use Stripe\StripeTaxForWooCommerce\Stripe\StripeCalculationTracker;
use Stripe\StripeTaxForWooCommerce\Stripe\StripeTaxPluginHelper;
use Stripe\StripeTaxForWooCommerce\Stripe\TaxCodeList;
use Stripe\StripeTaxForWooCommerce\Stripe\TaxExemptions;
use Stripe\StripeTaxForWooCommerce\Stripe\TaxRegistrations;
use Stripe\StripeTaxForWooCommerce\Stripe\TaxSettings;
use Stripe\StripeTaxForWooCommerce\Stripe\TaxTransaction;
use Stripe\StripeTaxForWooCommerce\Stripe\TaxTransactionReversal;
use Stripe\StripeTaxForWooCommerce\Stripe\StripeTaxLogger;
use Stripe\StripeTaxForWooCommerce\WooCommerce\Connect;
use Stripe\StripeTaxForWooCommerce\WooCommerce\ErrorRenderer;
use Stripe\StripeTaxForWooCommerce\WooCommerce\ExtendedProduct;
use Stripe\StripeTaxForWooCommerce\WooCommerce\StripeOrderItemTax;
use Stripe\StripeTaxForWooCommerce\WooCommerce\StripeTax;
use WC_Data;
use WP_CLI;
use WP_REST_Request;
use WP_REST_Response;
use Stripe\StripeTaxForWooCommerce\WooCommerce\StripeTaxTaxRateMemRepo;
use Stripe\StripeTaxForWooCommerce\WordPress\StringTaxRateIdFixerScheduledAction;
use Stripe\StripeTaxForWooCommerce\WordPress\StripeTax_Plugin;
use Stripe\StripeTaxForWooCommerce\Stripe\Tax_Calculation\Data;
/**
* Class for adding WordPress actions, filter, registering styles and scripts
*/
class Hooks {
/**
* Contains WP Hooks initialization state.
*
* @var bool $hooks_initialized
*/
protected static $hooks_initialized;
/**
* Tax exemptions object.
*
* @var TaxExemptions|null $tax_exemptions
*/
protected static $tax_exemptions;
/**
* Map line item type name to line item group name.
*
* @var array $type_to_group
*/
protected static $type_to_group = array(
'line_item' => 'line_items',
'tax' => 'tax_lines',
'shipping' => 'shipping_lines',
'fee' => 'fee_lines',
'coupon' => 'coupon_lines',
);
/**
* Tax transaction object.
*
* @var TaxTransaction|null
*/
protected static $tax_transaction;
/**
* If true then the errors are collected and added to the REST response if $error_reporting_collect_enabled is true.
*
* @var bool $error_reporting_collect
*/
protected static $error_reporting_collect = false;
/**
* If true then the collected errors are added to the REST response.
*
* @var bool $error_reporting_collect
*/
protected static $error_reporting_collect_enabled = false;
/**
* Create and return tax transaction object.
*
* @return TaxTransaction Returns the tax transaction object.
*/
protected static function get_tax_transaction() {
if ( is_null( static::$tax_transaction ) ) {
// @phpstan-ignore-next-line
static::$tax_transaction = new TaxTransaction( Options::get_current_mode_key() );
}
return static::$tax_transaction;
}
/**
* Register plugin's CSS styles.
*/
public static function action_admin_enqueue_styles() {
wp_enqueue_style(
'stripe_tax_for_woocommerce_admin',
STRIPE_TAX_FOR_WOOCOMMERCE_ASSETS_CSS_URL . 'stripe_tax_for_woocommerce_admin.css',
array(),
filemtime( STRIPE_TAX_FOR_WOOCOMMERCE_ASSETS_CSS_DIR . 'stripe_tax_for_woocommerce_admin.css' )
);
}
/**
* Register JS and CSS files required for the admin panel.
*/
protected static function admin_enqueue_styles() {
add_action( 'admin_enqueue_scripts', array( static::class, 'action_admin_enqueue_styles' ), 10, 0 );
}
/**
* Register JS files required for WP Admin(backend page) and add settings names localizations.
*/
public static function action_admin_enqueue_scripts() {
wp_enqueue_script(
'stripe_tax_for_woocommerce_admin',
STRIPE_TAX_FOR_WOOCOMMERCE_ASSETS_JS_URL . 'stripe_tax_for_woocommerce_admin.js',
array( 'jquery' ),
filemtime( STRIPE_TAX_FOR_WOOCOMMERCE_ASSETS_JS_DIR . 'stripe_tax_for_woocommerce_admin.js' ),
true
);
global $current_section;
$localize_script = array(
'ajax_url' => admin_url( 'admin-ajax.php' ),
'current_section_url' => admin_url( 'admin.php?page=wc-settings&tab=stripe_tax_for_woocommerce§ion=' . $current_section ),
'city_label' => __( 'City', 'stripe-tax-for-woocommerce' ),
'city_is_district_label' => __( 'District', 'stripe-tax-for-woocommerce' ),
'city_is_district_countries' => array_keys( StripeTaxPluginHelper::get_city_is_district_countries() ),
'city_is_town_or_city_label' => __( 'Town or City', 'stripe-tax-for-woocommerce' ),
'city_is_town_or_city_countries' => array_keys( StripeTaxPluginHelper::get_city_is_town_or_city_countries() ),
'postal_code_label' => __( 'Postal code', 'stripe-tax-for-woocommerce' ),
'postal_code_is_eircode_label' => __( 'Eircode', 'stripe-tax-for-woocommerce' ),
'postal_code_is_eircode_countries' => array_keys( StripeTaxPluginHelper::get_postal_code_is_eircode_countries() ),
'postal_code_is_zip_label' => __( 'ZIP', 'stripe-tax-for-woocommerce' ) . StripeTaxPluginHelper::get_required_field_mark_html(),
'postal_code_is_zip_countries' => array_keys( StripeTaxPluginHelper::get_postal_code_is_zip_countries() ),
'postal_code_no_city_countries' => array_keys( StripeTaxPluginHelper::get_no_city_countries() ),
'postal_code_no_postal_code_countries' => array_keys( StripeTaxPluginHelper::get_no_postal_code_countries() ),
'tax_registrations_lease_and_amusement_tax_use_states' => StripeTaxPluginHelper::get_tax_registration_lease_and_amusement_tax_us_states(),
'tax_registrations_local_communications_tax_us_states' => StripeTaxPluginHelper::get_tax_registration_local_communications_tax_us_states(),
'tax_registrations_no_sales_tax_us_states' => StripeTaxPluginHelper::get_tax_registration_no_sales_tax_us_states(),
'tax_registrations_eu_countries' => StripeTaxPluginHelper::get_tax_registration_eu_countries(),
/* translators: %s: country code, eg. 'US' */
'tax_registrations_localize_domestic' => __( 'Domestic (registered in %s)', 'stripe-tax-for-woocommerce' ),
/* translators: %s: country code, eg. 'US' */
'tax_registrations_localize_domestic_description' => __( 'Common for businesses selling goods and services to customers in %s.', 'stripe-tax-for-woocommerce' ),
/* translators: %s: country code, eg. 'US' */
'tax_registrations_localize_local_communications' => __( '%s State and Local Communications Tax', 'stripe-tax-for-woocommerce' ),
/* translators: %s: country code, eg. 'US' */
'tax_registrations_localize_local_communications_description' => __( 'Common for businesses selling video or audio streaming to customers in %s. This includes the Communications Services Tax, Communications Services Gross Receipts Tax and Local Communications Services Tax.', 'stripe-tax-for-woocommerce' ),
/* translators: %s: country code, eg. 'US' */
'tax_registrations_localize_no_sales_description' => __( 'You don’t need to add a registration in %s because there’s no sales tax in this state.', 'stripe-tax-for-woocommerce' ),
'tax_ids_html' => CalculateTax::get_tax_ids_admin_html(),
'disconnect_from_stripe_message_confirmation' => __( 'Are you sure you want to disconnect Stripe Tax plugin from Stripe Account?', 'stripe-tax-for-woocommerce' ),
);
if ( self::is_stripe_tab_selected() ) {
$api_key = Options::get_current_mode_key();
try {
$tax_registrations = new TaxRegistrations( $api_key );
// $locks array contains prepared for easy check list of already added tax registrations.
// In this case it used to lock "on-the-fly" checkboxes as checked and disabled by JavaScript.
$locks = $tax_registrations->get_locks();
$localize_script['tax_registrations_locks'] = $locks;
// phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
} catch ( \Throwable $e ) {
$stripe_tax_error_message = 'Error fetching tax registrations: ' . $e->getMessage();
}
wp_localize_script( 'stripe_tax_for_woocommerce_admin', 'stripe_tax_for_woocommerce', $localize_script );
}
}
/**
* Register WP Admin JS.
*/
protected static function admin_enqueue_scripts() {
add_action( 'admin_enqueue_scripts', array( static::class, 'action_admin_enqueue_scripts' ), 10, 0 );
}
/**
* Register AJAX requests handlers for connection testing and disconnection functionality.
*/
protected static function admin_ajax() {
add_action(
'wp_ajax_stripe_tax_for_woocommerce_test_connection',
array(
AdminAjax::class,
'test_connection',
),
10,
0
);
add_action(
'wp_ajax_stripe_tax_for_woocommerce_disconnect_from_stripe',
array(
AdminAjax::class,
'disconnect_from_stripe',
),
10,
0
);
add_action(
'woocommerce_admin_order_items_after_fees',
array( static::class, 'render_admin_error_message' ),
10,
1
);
}
/**
* Filter adds Stripe Tax tab into WooCommerce Settings.
*
* @param array $settings Settings.
*/
public static function filter_add_stripe_tax_settings( $settings ) {
$settings[] = new StripeTax();
return $settings;
}
/**
* Saves additional fields for WooCommerce Product on save action.
*
* @param WC_Data $wc_data WooCommerce data.
*
* @throws Exception If something goes wrong.
*/
public function action_woocommerce_after_product_object_save( WC_Data $wc_data ) {
$stripe_wc_product = new ExtendedProduct( $wc_data->get_id() );
$posted_tax_code = $stripe_wc_product->get_on_save_post_parameter_tax_code( $wc_data );
$stripe_wc_product->save_extended_product(
array(
'tax_code' => $posted_tax_code,
)
);
}
/**
* Admin init action.
*/
public static function action_admin_init() {
// Because there is no "classic" WordPress "nonce" - we disable CodeSniffer "NonceVerification" check.
// Actual CSRF checks here made by using "wcs_stripe_state" GET parameter and validated in method get_stripe_oauth_keys().
// phpcs:disable WordPress.Security.NonceVerification.Recommended
$current_tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : '';
$wcs_stripe_code = isset( $_GET['wcs_stripe_code'] ) ? sanitize_text_field( wp_unslash( $_GET['wcs_stripe_code'] ) ) : '';
$wcs_stripe_state = isset( $_GET['wcs_stripe_state'] ) ? sanitize_text_field( wp_unslash( $_GET['wcs_stripe_state'] ) ) : '';
// phpcs:enable
if ( 'stripe_tax_for_woocommerce' === $current_tab ) {
if ( ! empty( $wcs_stripe_code ) ) {
try {
$secret_key = Connect::get_stripe_oauth_keys( $wcs_stripe_code, $wcs_stripe_state );
Options::update_option( Options::OPTION_LIVE_MODE_SECRET_KEY, $secret_key );
Connect::set_woocommerce_connect_last_error( __( 'Connect with Stripe successful', 'stripe-tax-for-woocommerce' ) );
} catch ( \Throwable $e ) {
Connect::set_woocommerce_connect_last_error( $e->getMessage() );
}
wp_safe_redirect( admin_url( 'admin.php?page=wc-settings&tab=stripe_tax_for_woocommerce' ) );
static::do_exit();
}
}
}
// phpcs:disable Squiz.Commenting.FunctionComment.InvalidNoReturn
/**
* Wrapper around the PHP's built-in die function.
*
* @return never-return
*/
protected static function do_exit() {
exit;
}
// phpcs:enable Squiz.Commenting.FunctionComment.InvalidNoReturn
/**
* Adds additional fields on WooCommerce Product page.
*/
public static function action_woocommerce_product_options_tax() {
global $product_object;
try {
$tax_settings = new TaxSettings( Options::get_current_mode_key() );
$stripe_wc_product = new ExtendedProduct( $product_object->get_id() );
$default_tax_code = Options::get_tax_code();
$tax_codes = ( new TaxCodeList( Options::get_current_mode_key() ) )->get_as_key_value_formatted();
woocommerce_wp_select(
array(
'id' => '_stripe_tax_for_woocommerce_tax_code',
'value' => isset( $stripe_wc_product->get_extended_product()['tax_code'] ) ? $stripe_wc_product->get_extended_product()['tax_code'] : 'stfwc_inherit',
'label' => __( 'Stripe Tax - Product tax code', 'stripe-tax-for-woocommerce' ),
'options' => array_merge( array( 'stfwc_inherit' => __( 'Default' ) . ' (' . $default_tax_code . ' - ' . TaxCodeList::format_single( $default_tax_code, Options::get_current_mode_key() ) . ')' ), $tax_codes ),
'desc_tip' => 'true',
'description' => __( 'Choose a stripe tax code for this product.', 'stripe-tax-for-woocommerce' ),
)
);
} catch ( \Throwable $e ) {
ErrorRenderer::set_error_object( 'product_tax_code_retrieve_error', esc_html( 'Stripe Tax: ' . $e->getMessage() ), 'error' );
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo esc_html( ErrorRenderer::get_rendered_error( 'product_tax_code_retrieve_error' ) );
}
}
/**
* Anything needed to be done on WooCommerce meta box render event.
*/
public static function action_add_meta_boxes() {
add_action(
'woocommerce_product_options_tax',
array(
static::class,
'action_woocommerce_product_options_tax',
),
10,
0
);
}
/**
* Anything needed to be done, when we are on admin page.
*/
public static function action_init() {
add_action( 'add_meta_boxes', array( static::class, 'action_add_meta_boxes' ), 30, 0 );
}
/**
* Show action links on the plugin screen.
*
* @param mixed $links Plugin Action links.
*
* @return array
*/
public static function plugin_action_links( $links ) {
$action_links = array(
'settings' => '<a href="' . admin_url( 'admin.php?page=wc-settings&tab=stripe_tax_for_woocommerce' ) . '" aria-label="' . esc_attr__( 'View Stripe Tax settings', 'stripe_tax_for_woocommerce' ) . '">' . esc_html__( 'Settings', 'stripe-tax-for-woocommerce' ) . '</a>',
);
return array_merge( $action_links, $links );
}
/**
* Add plugin actions.
*
* @return void
*/
protected function add_actions(): void {
static::add_action_tax_exemptions( static::$tax_exemptions );
add_action(
'woocommerce_after_product_object_save',
array(
$this,
'action_woocommerce_after_product_object_save',
),
10,
1
);
add_action( 'init', array( static::class, 'action_init' ), 10, 0 );
add_action(
'rest_dispatch_request',
array(
static::class,
'detect_error_reporting_collect',
),
10,
0
);
add_action(
'woocommerce_hydration_dispatch_request',
array(
static::class,
'detect_error_reporting_collect',
),
10,
0
);
add_action(
'woocommerce_hydration_request_after_callbacks',
array(
static::class,
'add_collected_errors_to_response',
),
10,
3
);
add_action(
'rest_request_after_callbacks',
array(
static::class,
'add_collected_errors_to_response',
),
10,
3
);
add_action(
'woocommerce_order_partially_refunded',
array(
static::class,
'action_woocommerce_order_partially_refunded',
),
20,
2
);
add_action(
'woocommerce_order_fully_refunded',
array(
static::class,
'action_woocommerce_order_fully_refunded',
),
20,
1
);
add_action( 'admin_notices', array( static::class, 'render_admin_notices' ) );
add_action(
'update_option_woocommerce_prices_include_tax',
function ( $old_value, $value ) {
if ( 'yes' === $value ) {
update_option( 'woocommerce_tax_display_shop', 'incl' );
update_option( 'woocommerce_tax_display_cart', 'incl' );
} else {
update_option( 'woocommerce_tax_display_shop', 'excl' );
update_option( 'woocommerce_tax_display_cart', 'excl' );
}
},
10,
2
);
add_action(
'admin_footer',
function () {
global $pagenow, $post;
if ( in_array( $pagenow, array( 'post.php', 'post-new.php' ), true ) && 'product' === get_post_type( $post ) ) {
?>
<script>
jQuery(document).ready(function($) {
$('#_tax_status').closest('p.form-field').remove();
$('#_tax_class').closest('p.form-field').remove();
function removeTaxClassFromVariations() {
$('.form-row.form-row-full[class*="tax_class"]').remove();
}
// Initial call (in case variations already visible)
removeTaxClassFromVariations();
// Also call when new variation fields are loaded (after clicking "Add variation", etc.)
$(document).on('woocommerce_variations_loaded woocommerce_variations_added', function() {
removeTaxClassFromVariations();
});
});
</script>
<?php
}
}
);
add_action(
'admin_footer-edit.php',
function () {
global $typenow;
if ( 'product' === $typenow ) {
?>
<script>
jQuery(document).ready(function($) {
// Remove the tax class field from quick and bulk edit UI
$('fieldset.inline-edit-col-right .tax_status').closest('label').remove();
$('fieldset.inline-edit-col-left .tax_status').closest('label').remove();
$('fieldset.inline-edit-col-right .tax_class').closest('label').remove();
$('fieldset.inline-edit-col-left .tax_class').closest('label').remove();
});
</script>
<?php
}
}
);
add_action(
'woocommerce_store_api_checkout_order_processed',
array(
static::class,
'handle_checkout_order_notice',
),
10,
2
);
}
/**
* Check if current tab is Stripe tab.
*
* @return bool If is on Stripe tab.
*/
private static function is_stripe_tab_selected(): bool {
// There are no nonce exists or needed, because it is just a regular page view without any changes made by user input.
// phpcs:disable WordPress.Security.NonceVerification.Recommended
return isset( $_GET['page'] ) && 'wc-settings' === $_GET['page'] && isset( $_GET['tab'] ) && 'stripe_tax_for_woocommerce' === $_GET['tab'];
}
/**
* Add plugin filters.
*
* @return void
*/
protected static function add_filters(): void {
add_filter(
'woocommerce_order_type_to_group',
array(
static::class,
'filter_woocommerce_order_type_to_group',
),
100,
1
);
add_filter(
'woocommerce_get_order_item_classname',
array(
static::class,
'filter_woocommerce_get_order_item_classname',
),
5,
2
);
add_filter(
'woocommerce_cart_hide_zero_taxes',
'__return_false',
20,
0
);
// Using this to ignore Woocommerce rates to be displayed in shop.
if ( Options::is_live_mode_enabled() ) {
add_filter(
'woocommerce_find_rates',
array(
static::class,
'filter_woocommerce_find_rates',
),
10,
0
);
}
add_filter( 'pre_option_wc_connect_taxes_enabled', fn () => Options::is_live_mode_enabled() );
add_filter(
'woocommerce_rest_prepare_shop_order_object',
array(
static::class,
'filter_rest_prepare_shop_order_object',
),
10,
1
);
// @phpstan-ignore-next-line
add_filter(
'woocommerce_order_item_get_formatted_meta_data',
function ( $formatted_meta ) {
$meta_to_hide = array(
'_stripe_not_subtotal_include_tax',
'__stripe_tax_item_subtotal_tax',
'__stripe_tax_price_inclusive_tax',
'__stripe_tax_behavior',
Data::CART_ITEM_REFERENCE_META_NAME,
Data::TAX_EXCLUDED_META_NAME,
'_stripe_tax_checkout_total_tax_inclusive',
'_stripe_tax_checkout_subtotal_tax_inclusive',
);
foreach ( $formatted_meta as $key => $meta ) {
if ( in_array( $meta->key, $meta_to_hide, true ) ) {
unset( $formatted_meta[ $key ] );
}
}
return $formatted_meta;
},
10,
2
);
add_filter(
'woocommerce_get_sections_tax',
function ( $classes ) {
return array( '' => $classes[''] );
},
10,
1
);
add_filter(
'option_woocommerce_tax_based_on',
array(
static::class,
'action_option_woocommerce_tax_based_on',
),
10,
1
);
add_filter(
'option_woocommerce_tax_round_at_subtotal',
function () {
return 'no';
},
10,
0
);
add_filter(
'woocommerce_tax_settings',
function ( $settings ) {
return array_filter(
$settings,
function ( $setting ) {
if ( array_key_exists( 'id', $setting ) && in_array( $setting['id'], array( 'woocommerce_tax_display_shop', 'woocommerce_tax_display_cart' ), true ) ) {
return false;
}
if ( array_key_exists( 'type', $setting ) && 'conflict_error' === $setting['type'] ) {
return false;
}
return true;
}
);
}
);
}
/**
* "option_woocommerce_tax_based_on" hook handler
*/
public static function action_option_woocommerce_tax_based_on() {
return 'shipping';
}
/**
* Register wp cli commands.
*/
protected static function register_commands() {
if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) {
return;
}
WP_CLI::add_command( 'stripe-tax-transaction-reverse', TaxTransactionReversal::class );
}
/**
* Using this to ignore Woocommerce rates to be displayed in shop.
*/
public static function filter_woocommerce_find_rates() {
return array();
}
/**
* Handles exceptions.
* If the request is REST then it adds the error message to ErrorRenderer,
* otherwise it formats and outputs the error message.
*
* @param Exception $e Handled exception.
*/
public static function handle_calculate_tax_error( $e ) {
$message_id = 'calculate_tax_error';
if ( $e instanceof CountrySupportException ) {
$message_id = 'setting_country_error';
}
if ( $e instanceof CountryStateException ) {
$message_id = 'setting_state_error';
}
if ( ! ErrorRenderer::get_error_object( $message_id )->message ) {
$formatted_message = StripeTaxPluginHelper::format_api_error_message( $e );
ErrorRenderer::set_error_object( $message_id, 'Stripe Tax: ' . $formatted_message, 'error' );
} else {
// Error already reported.
return;
}
if ( ! static::$error_reporting_collect ) {
echo wp_kses( ErrorRenderer::get_rendered_error( $message_id ), StripeTaxPluginHelper::get_admin_allowed_html() );
} else {
static::$error_reporting_collect_enabled = true;
}
}
/**
* Preserve original WooCommerce order types to group plus any added by another plugin.
*
* @param array $type_to_group Type to group.
*/
public static function filter_woocommerce_order_type_to_group( $type_to_group ) {
static::$type_to_group = $type_to_group;
return $type_to_group;
}
/**
* Render admin notices.
*
* @return void
*/
public static function render_admin_notices() {
if ( get_transient( 'stripe_tax_for_woocommerce_activate' ) ) {
include STRIPE_TAX_FOR_WOOCOMMERCE_TEMPLATES_DIR . 'plugin-activate-notice.php';
delete_transient( 'stripe_tax_for_woocommerce_activate' );
}
}
/**
* WooCommerce order partially refunded action.
*
* @param int $order_id Order id.
* @param int $refund_id Refund id.
*
* @throws Exception If something goes wrong.
*/
public static function action_woocommerce_order_partially_refunded( $order_id, $refund_id ) {
try {
TaxTransaction::create_order_refund_reversal( $refund_id );
} catch ( \Throwable $err ) {
StripeTaxLogger::log_error( $err->getMessage() );
}
}
/**
* WooCommerce order fully refunded action.
*
* @param int $order_id Order id.
*
* @throws ApiErrorException In case of API error.
*/
public static function action_woocommerce_order_fully_refunded( $order_id ) {
try {
TaxTransaction::reverse_order_last_transaction( $order_id );
} catch ( \Throwable $err ) {
StripeTaxLogger::log_error( $err->getMessage() );
}
}
/**
* Replace WooCommerce Order Item Tax class to ours StripeOrderItemTax class
*
* @param class-string $classname The classname.
* @param string $item_type The item type.
*
* @return class-string
*/
public static function filter_woocommerce_get_order_item_classname( $classname, $item_type ) {
if ( 'tax' === $item_type ) {
return StripeOrderItemTax::class;
}
return $classname;
}
/**
* Checks whether WooCommerce is installed and active
*
* @return bool
*/
public static function is_woocommerce_activated() {
return class_exists( 'WooCommerce' );
}
/**
* Sets app info for Stripe Tax
*
* @return void
*/
public static function set_app_info() {
if ( ! Stripe::getAppInfo() ) {
if ( ! function_exists( 'get_plugin_data' ) ) {
include_once ABSPATH . 'wp-admin/includes/plugin.php';
}
if ( file_exists( __DIR__ . '/../stripe-tax-for-woocommerce.php' ) ) {
$plugin_data = get_plugin_data( __DIR__ . '/../stripe-tax-for-woocommerce.php' );
Stripe::setAppInfo(
$plugin_data['Name'],
$plugin_data['Version'],
null,
STRIPE_TAX_PARTNER_ID
);
}
}
}
/**
* Entry point for adding WordPress actions, filter, registering styles and scripts
*
* @param bool $force Force flag.
*
* @return void
*/
public function init( bool $force = false ): void {
static::admin_ajax();
add_action( 'admin_init', array( static::class, 'action_admin_init' ), 5, 0 );
add_action( 'woocommerce_system_status_report', array( static::class, 'system_status_report' ) );
add_filter( 'woocommerce_get_settings_pages', array( static::class, 'filter_add_stripe_tax_settings' ), 10, 1 );
add_filter(
'plugin_action_links_' . STRIPE_TAX_FOR_WOOCOMMERCE_PLUGIN_BASENAME,
array(
static::class,
'plugin_action_links',
)
);
static::admin_enqueue_styles();
static::admin_enqueue_scripts();
if ( self::can_init( $force ) ) {
static::$tax_exemptions = new TaxExemptions();
static::$hooks_initialized = true;
$this->add_actions();
static::add_filters();
static::register_commands();
StripeCalculationTracker::init();
static::check_migrations();
static::check_string_tax_rate_id_fixer();
StripeTax_Plugin::load();
}
}
/**
* Check if the plugin can perform init action.
*
* @param bool $force Force flag.
*
* @return bool
*/
private static function can_init( bool $force = false ): bool {
if ( ! empty( static::$hooks_initialized ) && ! $force ) {
return false;
}
if ( ! static::is_woocommerce_activated() ) {
add_action(
'admin_notices',
function () {
/* translators: 1. URL link. */
echo '<div class="error"><p><strong>' . sprintf( esc_html__( 'Stripe Tax requires WooCommerce to be installed and active. You can download %s here.', 'stripe-tax-for-woocommerce' ), '<a href="https://woocommerce.com/" target="_blank">WooCommerce</a>' ) . '</strong></p></div>';
}
);
return false;
}
return ! empty( Options::get_current_mode_key() );
}
/**
* Check if migrations needed.
*/
public static function check_migrations() {
$migration_version = intval( get_option( 'stripe_tax_migration', '0' ) );
if ( $migration_version < 1 ) {
PluginActivate::maybe_migrate_tax_transactions_table();
update_option( 'stripe_tax_migration', '1' );
$migration_version = 1;
}
if ( $migration_version < 2 ) {
PluginActivate::maybe_add_time_index_to_calculate_tax_table();
update_option( 'stripe_tax_migration', '2' );
}
}
/**
* Starts string tax rate ids fixer if needed
*/
public static function check_string_tax_rate_id_fixer() {
StringTaxRateIdFixerScheduledAction::register();
}
/**
* Adds tax exemptions action.
*
* @param object $instance The instance.
*/
protected static function add_action_tax_exemptions( $instance ) {
add_action( 'show_user_profile', array( $instance, 'custom_user_profile_fields' ) );
add_action( 'edit_user_profile', array( $instance, 'custom_user_profile_fields' ) );
add_action( 'personal_options_update', array( $instance, 'save_custom_user_profile_fields' ) );
add_action( 'edit_user_profile_update', array( $instance, 'save_custom_user_profile_fields' ) );
}
/**
* Calculates an order or cart line item totals
*
* @param array $line_items Orde or cart item lines.
* @param object $response API Response.
*/
public static function get_totals_from_response( $line_items, $response ) {
$subtotal = 0;
$subtotal_tax = 0;
$cart_contents_total = 0;
$cart_contents = array();
$cart_contents_tax = 0;
$cart_contents_taxes = array();
$discount_total = 0;
$discount_tax = 0;
foreach ( $line_items as $item_key => $line_item ) {
$line_tax_data = array(
'subtotal' => array(),
'total' => array(),
'percentage' => array(),
);
foreach ( $response->line_items->data as $datum ) {
if ( $datum->reference !== $line_item['reference'] ) {
continue;
}
$prices_include_tax = ! ( 'exclusive' === $datum->tax_behavior );
$has_discount = $line_item['line_subtotal'] !== $line_item['line_total'];
$line_total_rate_percentage = 0;
$line_subtotal = $line_item['line_subtotal'];
$line_total = $datum->amount - ( ! $prices_include_tax ? 0 : $datum->amount_tax );
$line_tax = $datum->amount_tax;
foreach ( $datum->tax_breakdown as $tax_breakdown ) {
if ( ! isset( $tax_breakdown->tax_rate_details ) && ! is_object( $tax_breakdown->tax_rate_details ) ) {
continue;
}
$rate_name = $tax_breakdown->jurisdiction->display_name . ' ' . $tax_breakdown->tax_rate_details->display_name;
$rate_percentage = $tax_breakdown->tax_rate_details->percentage_decimal;
$tax_type = $tax_breakdown->tax_rate_details->tax_type;
$rate_key = StripeTaxTaxRateMemRepo::find_or_create(
$tax_breakdown->jurisdiction->country,
$tax_breakdown->jurisdiction->state,
(float) $tax_breakdown->tax_rate_details->percentage_decimal,
$tax_breakdown->jurisdiction->display_name . ' ' . $tax_breakdown->tax_rate_details->display_name
);
if ( ! array_key_exists( $rate_key, $cart_contents_taxes ) ) {
$cart_contents_taxes[ $rate_key ] = 0;
}
if ( ! array_key_exists( $rate_key, $line_tax_data['subtotal'] ) ) {
$line_tax_data['subtotal'][ $rate_key ] = 0;
$line_tax_data['total'][ $rate_key ] = 0;
$line_tax_data['percentage'][ $rate_key ] = 0;
}
$line_total_rate_percentage += $rate_percentage;
$cart_contents_taxes[ $rate_key ] += $tax_breakdown->amount;
$line_tax_data['total'][ $rate_key ] += $tax_breakdown->amount;
$line_tax_data['percentage'][ $rate_key ] += $rate_percentage;
}
if ( ! $prices_include_tax ) {
$line_subtotal = $line_item['line_subtotal'];
} else {
$line_subtotal = ! $has_discount ? NumberUtil::round( wc_remove_number_precision( $line_total ), wc_get_price_decimals() ) : $line_item['line_subtotal'] * 100 / ( 100 + $line_total_rate_percentage );
}
foreach ( $line_tax_data['total'] as $rate_key => $line_tax_data_total ) {
if ( $has_discount ) {
$line_tax_data['subtotal'][ $rate_key ] = $line_subtotal * $line_tax_data['percentage'][ $rate_key ];
} else {
$line_tax_data['subtotal'][ $rate_key ] = $line_tax_data_total;
}
}
$cart_contents[ $item_key ]['line_tax_data'] = array(
'total' => wc_remove_number_precision_deep( $line_tax_data['total'] ),
'subtotal' => wc_remove_number_precision_deep( $line_tax_data['subtotal'] ),
);
$line_subtotal_tax = NumberUtil::round( wc_remove_number_precision( array_sum( $line_tax_data['subtotal'] ) ), wc_get_price_decimals() );
$line_tax = NumberUtil::round( wc_remove_number_precision( array_sum( $line_tax_data['total'] ) ), wc_get_price_decimals() );
$line_total = ! $prices_include_tax ? $line_item['line_total'] : $line_item['line_total'] - $line_tax;
$cart_contents[ $item_key ]['line_total'] = $line_total;
$cart_contents[ $item_key ]['line_subtotal'] = $line_subtotal;
$cart_contents[ $item_key ]['line_subtotal_tax'] = $line_subtotal_tax;
$cart_contents[ $item_key ]['line_tax'] = $line_tax;
$subtotal_tax += $line_subtotal_tax;
$cart_contents_tax += $line_tax;
$cart_contents_total += $line_total;
$subtotal += $line_subtotal;
if ( $has_discount ) {
$discount_total += ( $line_subtotal - $line_total );
$discount_tax += ( $line_subtotal_tax - $line_tax );
}
}
}
return array(
'cart_contents' => $cart_contents,
'cart_contents_taxes' => wc_remove_number_precision_deep( $cart_contents_taxes ),
'subtotal' => $subtotal,
'cart_contents_total' => $cart_contents_total,
'cart_contents_tax' => $cart_contents_tax,
'subtotal_tax' => $subtotal_tax,
'discount_total' => $discount_total,
'discount_tax' => $discount_tax,
);
}
/**
* On REST requests enables error collecting mechanism.
*/
public static function detect_error_reporting_collect() {
static::$error_reporting_collect = true;
}
/**
* Adds collected errors to the REST response.
*
* @param WP_REST_Response $response REST response.
* @param array $handler REST handler.
* @param WP_REST_Request $request REST request.
*/
public static function add_collected_errors_to_response( $response, $handler, $request ) {
$request_route = $request->get_route();
if ( substr( $request_route, -6 ) === '/batch' ) {
return $response;
}
if ( ! static::$error_reporting_collect_enabled ) {
return $response;
}
$calculate_tax_error_message = ErrorRenderer::get_error_object( 'calculate_tax_error' )->message;
if ( ! $calculate_tax_error_message ) {
return $response;
}
$response->data['errors'][] = array(
'code' => 'calculate_tax_error',
'message' => $calculate_tax_error_message,
);
return $response;
}
/**
* Hooks extra necessary sections into the system status report template
*/
public static function system_status_report(): void {
?>
<table class="wc_status_table widefat">
<thead>
<tr>
<th colspan="5" data-export-label="Stripe">
<h2>
<?php esc_html_e( 'Stripe', 'stripe_tax_for_woocommerce' ); ?>
<?php echo wp_kses_post( wc_help_tip( esc_html__( 'This section shows details of Stripe.', 'stripe_tax_for_woocommerce' ) ) ); ?>
</h2>
</th>
</tr>
</thead>
<tbody>
<tr>
<td data-export-label="Account ID">
<?php esc_html_e( 'Account ID', 'stripe_tax_for_woocommerce' ); ?>:
</td>
<td class="help">
<?php echo wp_kses_post( wc_help_tip( esc_html__( 'Stripe Account ID.', 'stripe_tax_for_woocommerce' ) ) ); ?>
</td>
<td>
<?php
try {
if ( ! empty( Options::get_current_mode_key() ) ) {
if ( ! empty( Options::get_option( Options::OPTION_LIVE_MODE_ACCOUNT_ID ) ) ) {
echo esc_html( Options::get_option( Options::OPTION_LIVE_MODE_ACCOUNT_ID ) );
} else {
$stripe_client = new StripeClient( Options::get_current_mode_key() );
$account_service = new AccountService( $stripe_client );
$api_response = $account_service->retrieve();
if ( ! isset( $api_response->object ) || 'account' !== $api_response->object ) {
echo esc_html__( 'Account ID could not be retrieved, please try to reconnect to Stripe.', 'stripe_tax_for_woocommerce' );
} else {
Options::update_option( Options::OPTION_LIVE_MODE_ACCOUNT_ID, $api_response->{ 'id' } );
echo esc_html( Options::get_option( Options::OPTION_LIVE_MODE_ACCOUNT_ID ) );
}
}
} else {
echo esc_html__( 'Account ID could not be retrieved, please try to reconnect to Stripe.', 'stripe_tax_for_woocommerce' );
}
} catch ( \Throwable $e ) {
echo esc_html__( 'Account ID could not be retrieved, please try to reconnect to Stripe.', 'stripe_tax_for_woocommerce' );
}
?>
</td>
</tr>
</tbody>
</table>
<?php
}
/**
* Converts tax rate ids from string to integer.
*
* @param WP_REST_Response $response REST response.
*/
public static function filter_rest_prepare_shop_order_object( $response ) {
// phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter
$data = $response->get_data();
$counter = 5000000000;
if ( isset( $data['line_items'] ) ) {
foreach ( $data['line_items'] as $idx_line => $line_item ) {
if ( isset( $line_item['taxes'] ) ) {
$order_id = $data['id'];
foreach ( $line_item['taxes'] as $idx_tax => $line_item_tax ) {
++$counter;
$rate_id = $line_item_tax['id'];
if ( is_numeric( $rate_id ) || strpos( $rate_id, 'stripe_tax_for_woocommerce__' ) === false ) {
break 2;
}
$new_rate_id = $order_id * 1000 + $counter;
$idx_tax_2 = null;
foreach ( $data['tax_lines'] as $idx_tax_2 => $tax_line ) {
if ( $tax_line['rate_id'] === $rate_id ) {
break;
}
}
$data['line_items'][ $idx_line ]['taxes'][ $idx_tax ]['id'] = $new_rate_id;
if ( ! is_null( $idx_tax_2 ) ) {
$data['tax_lines'][ $idx_tax_2 ]['rate_id'] = $new_rate_id;
}
}
}
}
}
if ( isset( $data['shipping_lines'] ) ) {
foreach ( $data['shipping_lines'] as $idx_line => $line_item ) {
if ( isset( $line_item['taxes'] ) ) {
$order_id = $data['id'];
foreach ( $line_item['taxes'] as $idx_tax => $line_item_tax ) {
++$counter;
$rate_id = $line_item_tax['id'];
if ( is_numeric( $rate_id ) || strpos( $rate_id, 'stripe_tax_for_woocommerce__' ) === false ) {
break 2;
}
$new_rate_id = $order_id * 1000 + $counter;
$idx_tax_2 = null;
foreach ( $data['tax_lines'] as $idx_tax_2 => $tax_line ) {
if ( $tax_line['rate_id'] === $rate_id ) {
break;
}
}
$data['shipping_lines'][ $idx_line ]['taxes'][ $idx_tax ]['id'] = $new_rate_id;
if ( ! is_null( $idx_tax_2 ) ) {
$data['tax_lines'][ $idx_tax_2 ]['rate_id'] = $new_rate_id;
}
}
}
}
}
$response->set_data( $data );
return $response;
}
/**
* Renders a stored admin error message for a WooCommerce order.
*
* @param int $order_id WooCommerce order ID.
*/
public static function render_admin_error_message( $order_id ): void {
$msg = get_post_meta( $order_id, '_stripe_tax_last_error', true );
if ( $msg ) {
delete_post_meta( $order_id, '_stripe_tax_last_error' );
echo '<div class="notice notice-error"><p><strong>'
. esc_html__( 'Stripe Tax:', 'stripe-tax-for-woocommerce' )
. '</strong> ' . esc_html( $msg ) . '</p></div>';
}
}
/**
* Stop placing the order if tax calculation has failed.
*
* @param \WC_Order $order The order being updated during checkout.
*
* @throws \Automattic\WooCommerce\StoreApi\Exceptions\RouteException If taxes have not been calculated.
*/
public static function handle_checkout_order_notice( $order ) {
$handlers_class = null;
if ( class_exists( __NAMESPACE__ . '\\Hook_Handlers' ) ) {
$handlers_class = __NAMESPACE__ . '\\Hook_Handlers';
}
if ( $handlers_class && ! $handlers_class::is_enabled() ) {
$exception_class = '\Automattic\WooCommerce\StoreApi\Exceptions\RouteException';
if ( class_exists( $exception_class ) ) {
$message = $handlers_class::get_current_request_error_message();
if ( '' === $message ) {
$message = esc_html__( 'Taxes have not been calculated at the moment.', 'stripe-tax-for-woocommerce' );
}
throw new \Automattic\WooCommerce\StoreApi\Exceptions\RouteException(
'stripe_tax_calculation_warning',
esc_html( $message ),
400
);
}
}
}
}