HEX
Server: Apache/2.4.58 (Ubuntu)
System: Linux bsx-1-dev 6.8.0-101-generic #101-Ubuntu SMP PREEMPT_DYNAMIC Mon Feb 9 10:15:05 UTC 2026 x86_64
User: www-data (33)
PHP: 8.3.6
Disabled: NONE
Upload Files
File: /var/www/html/wp-content/plugins/memberpress/app/models/MeprTaxRate.php
<?php

if (!defined('ABSPATH')) {
    die('You are not allowed to call this page directly.');
}

class MeprTaxRate extends MeprBaseModel
{
    /**
     * The customer type.
     *
     * @var string
     */
    public $customer_type = 'customer';

    /**
     * The tax reversal.
     *
     * @var boolean
     */
    public $reversal = false;

    /**
     * Constructor for the MeprTaxRate class.
     *
     * @param mixed $obj Optional object or ID to initialize the tax rate.
     */
    public function __construct($obj = null)
    {
        $this->initialize(
            [
                'id'           => 0,
                'tax_country'  => '',
                'tax_state'    => '',
                'tax_rate'     => 0.00,
                'tax_desc'     => '',
                'tax_priority' => 0,
                'tax_compound' => 0,
                'tax_shipping' => 1,
                'tax_order'    => 0,
                'tax_class'    => 'standard',
                'cities'       => [],
                'postcodes'    => [],
            ],
            $obj
        );

        if (is_integer($obj) && $obj > 0) {
            $this->rec->cities    = [];
            $this->rec->postcodes = [];

            $locations = self::get_locations_by_tax_rate($this->id);

            if (is_array($locations) && !empty($locations)) {
                foreach ($locations as $location) {
                    if ($location->location_type === 'city') {
                        $this->rec->cities[] = $location->location_code;
                    } elseif ($location->location_type === 'postcode') {
                        $this->rec->postcodes[] = $location->location_code;
                    }
                }
            }
        }
    }

    /**
     * Retrieves a single tax rate by ID.
     *
     * @param integer $id          The ID of the tax rate.
     * @param string  $return_type The return type (default: OBJECT).
     *
     * @return mixed The tax rate object or array.
     */
    public static function get_one($id, $return_type = OBJECT)
    {
        $mepr_db = new MeprDb();
        $args    = compact('id');
        return $mepr_db->get_one_record($mepr_db->tax_rates, $args, $return_type);
    }

    /**
     * Retrieves locations associated with a tax rate.
     *
     * @param integer $tax_rate_id The ID of the tax rate.
     * @param string  $return_type The return type (default: OBJECT).
     *
     * @return mixed The locations associated with the tax rate.
     */
    public static function get_locations_by_tax_rate($tax_rate_id, $return_type = OBJECT)
    {
        $mepr_db = new MeprDb();
        $args    = compact('tax_rate_id');
        return $mepr_db->get_records($mepr_db->tax_rate_locations, $args, '', '', $return_type);
    }

    /**
     * Finds a tax rate based on provided arguments.
     *
     * @param array  $args        The arguments for finding the tax rate.
     * @param string $return_type The return type (default: OBJECT).
     *
     * @return MeprTaxRate The found tax rate.
     */
    public static function find_rate($args, $return_type = OBJECT)
    {
        global $wpdb;

        // Get product ID if there is data from webhook.
        $prd_id = !empty($args['prd_id']) ? (int) $args['prd_id'] : 0;

        if (empty($prd_id) && !empty($_POST['mepr_product_id'])) {
            $prd_id = (int) $_POST['mepr_product_id'];
        }

        $prd = new MeprProduct($prd_id);

        if ($prd && isset($prd->tax_class) && !empty($prd->tax_class)) {
            $args['tax_class'] = $prd->tax_class;
        }

        $defaults = [
            'street'    => '',
            'country'   => '',
            'state'     => '',
            'city'      => '',
            'postcode'  => '',
            'tax_class' => 'standard',
            'user'      => null,
        ];

        $args = wp_parse_args($args, $defaults);

        extract($args, EXTR_SKIP);

        // Just return defaults.
        if (!$country) {
            return new MeprTaxRate();
        }

        // Handle postcodes.
        $valid_postcodes = ['*', strtoupper(MeprUtils::clean($postcode))];

        // Work out possible valid wildcard postcodes.
        $postcode_length   = strlen($postcode);
        $wildcard_postcode = strtoupper(MeprUtils::clean($postcode));

        for ($i = 0; $i < $postcode_length; $i++) {
            $wildcard_postcode = substr($wildcard_postcode, 0, -1);
            $valid_postcodes[] = $wildcard_postcode . '*';
        }

        // Attempt to cache the rate for a day using transients.
        $rate_transient_key = 'mepr_tax_rate_id_' . md5(sprintf('%s+%s+%s+%s+%s', $country, $state, $city, implode(',', $valid_postcodes), $tax_class));
        $tax_rate           = get_transient($rate_transient_key);

        if (!$tax_rate instanceof MeprTaxRate) {
            // Create placeholders for the valid_postcodes IN clauses.
            $postcode_placeholders = array_fill(0, count($valid_postcodes), '%s');
            $postcode_in_clause = join(', ', $postcode_placeholders);

            $q = "
        SELECT txr.*, pc.location_code AS postcode, ct.location_code AS city
          FROM {$wpdb->mepr_tax_rates} AS txr
          LEFT JOIN {$wpdb->mepr_tax_rate_locations} AS pc
            ON pc.tax_rate_id=txr.id
           AND pc.location_type='postcode'
          LEFT JOIN {$wpdb->mepr_tax_rate_locations} AS ct
            ON ct.tax_rate_id=txr.id
           AND ct.location_type='city'
         WHERE txr.tax_country IN ( %s, '' )
           AND txr.tax_state IN ( %s, '' )
           AND txr.tax_class = %s
           AND (
             (
                pc.location_code IN ($postcode_in_clause)
                AND ct.location_code = %s
             ) OR (
                pc.location_code IS NULL
                AND ct.location_code = %s
             ) OR (
                pc.location_code IN ($postcode_in_clause)
                AND ct.location_code IS NULL
             ) OR (
                pc.location_code IS NULL
                AND ct.location_code IS NULL
             )
           )
         ORDER BY txr.tax_priority, txr.tax_order";

            // Prepare the parameters array.
            $prepare_params = array_merge(
                [
                    strtoupper($country),
                    strtoupper($state),
                    strtolower($tax_class),
                ],
                $valid_postcodes, // First set of postcodes for first IN clause.
                [strtoupper($city)], // First city parameter.
                [strtoupper($city)], // Second city parameter.
                $valid_postcodes  // Second set of postcodes for second IN clause.
            );

            $q = $wpdb->prepare($q, ...$prepare_params); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

            $found_rates = $wpdb->get_results($q); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter

            if (!empty($found_rates)) {
                $tax_rate = new MeprTaxRate($found_rates[0]->id);
            } else {
                $tax_rate = new MeprTaxRate();
            }

            $tax_rate = MeprHooks::apply_filters('mepr_found_tax_rate', $tax_rate, $country, $state, $postcode, $city, $street, $user);

            if ($tax_rate instanceof MeprTaxRate) {
                set_transient($rate_transient_key, $tax_rate, DAY_IN_SECONDS);
            } else {
                $tax_rate = new MeprTaxRate();
            }
        }

        return MeprHooks::apply_filters('mepr_find_tax_rate', $tax_rate, $country, $state, $postcode, $city, $street, $user, $prd_id);
    }

    /**
     * Retrieves the count of tax rates.
     *
     * @return integer The count of tax rates.
     */
    public static function get_count()
    {
        $mepr_db = new MeprDb();
        return $mepr_db->get_count($mepr_db->tax_rates);
    }

    /**
     * Retrieves all tax rates.
     *
     * @param string $separator   The separator for concatenating location codes.
     * @param string $return_type The return type (default: OBJECT).
     *
     * @return mixed The list of all tax rates.
     */
    public static function get_all($separator = ',', $return_type = OBJECT)
    {
        global $wpdb;

        // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $q = $wpdb->prepare(
            "SELECT txr.*,
              ( SELECT GROUP_CONCAT(
                         DISTINCT txrp.location_code
                         ORDER BY txrp.location_code
                         SEPARATOR %s
                       )
                  FROM {$wpdb->mepr_tax_rate_locations} AS txrp
                 WHERE txrp.location_type = %s
                   AND txrp.tax_rate_id=txr.id
              ) AS postcodes,
              ( SELECT GROUP_CONCAT(
                         DISTINCT txrc.location_code
                         ORDER BY txrc.location_code
                         SEPARATOR %s
                       )
                  FROM {$wpdb->mepr_tax_rate_locations} AS txrc
                 WHERE txrc.location_type = %s
                   AND txrc.tax_rate_id=txr.id
              ) AS cities
         FROM {$wpdb->mepr_tax_rates} AS txr
        ORDER BY txr.tax_country, txr.tax_state, postcodes, cities",
            $separator,
            'postcode',
            $separator,
            'city'
        ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

        return $wpdb->get_results($q, $return_type); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter
    }

    /**
     * Destroys all tax rates and clears related transients.
     *
     * @return void
     */
    public static function destroy_all()
    {
        global $wpdb;
        $tax_rates = self::get_all();

        foreach ($tax_rates as $tr) {
            $obj = new MeprTaxRate($tr->id);
            $obj->destroy();
        }

        // We should prolly clear out all transients here yo!
        $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '%_mepr_tax_rate_id_%'"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
    }

    /**
     * Stores the tax rate in the database.
     *
     * @return mixed The ID of the stored tax rate.
     */
    public function store()
    {
        $this->prepare_fields();

        $mepr_db            = new MeprDb();
        $create_update_vals = $vals = (array)$this->rec;

        if (isset($create_update_vals['cities'])) {
            unset($create_update_vals['cities']);
        }

        if (isset($create_update_vals['postcodes'])) {
            unset($create_update_vals['postcodes']);
        }

        $this->destroy_locations_by_tax_rate();

        if (isset($this->id) && !is_null($this->id) && (int)$this->id > 0) {
            MeprHooks::apply_filters('mepr_tax_rate_update', $mepr_db->update_record($mepr_db->tax_rates, $this->id, $create_update_vals), $create_update_vals);
        } else {
            $this->id = MeprHooks::apply_filters('mepr_tax_rate_create', $mepr_db->create_record($mepr_db->tax_rates, $create_update_vals, false), $create_update_vals);
        }

        $locations = [
            'cities'    => 'city',
            'postcodes' => 'postcode',
        ];
        foreach ($locations as $col => $location) {
            if (isset($vals[$col]) && is_array($vals[$col])) {
                $this->import_locations($this->id, $location, $vals[$col]);
            }
        }

        return MeprHooks::apply_filters('mepr_tax_rate_store', $this->id, $vals);
    }

    /**
     * Imports tax rates from an array of rows.
     * country code, state code, postcodes, cities, rate, tax name, #priority, #compound, #shipping, #tax class
     *
     * @param array $rows The rows to import.
     *
     * @return void
     */
    public static function import($rows)
    {
        if (!empty($rows) && is_array($rows)) {
            foreach ($rows as $row) {
                self::import_row($row);
            }
        }
    }

    // TODO: Throw some exceptions on error.
    /**
     * Imports a tax rate row.
     *
     * @param array $row The row to import.
     *
     * @return void
     */
    private static function import_row($row)
    {
        $tax_rate_info = [
            'tax_country'  => MeprUtils::clean($row['tax_country']),
            'tax_state'    => MeprUtils::clean($row['tax_state']),
            'tax_rate'     => (float)trim(str_replace('%', '', str_replace(',', '.', $row['tax_rate']))),
            'tax_desc'     => trim($row['tax_desc']),
            'tax_priority' => isset($row['tax_priority']) ? $row['tax_priority'] : '',
            'tax_compound' => isset($row['tax_compound']) ? $row['tax_compound'] : '',
            'tax_shipping' => isset($row['tax_shipping']) ? $row['tax_shipping'] : '',
            // 'tax_order' => 0,
            'tax_class'    => ((isset($row['tax_class']) && !empty($row['tax_class'])) ? $row['tax_class'] : 'standard'),
        ];

        $locations = ['cities', 'postcodes'];
        foreach ($locations as $col) {
            if (isset($row[$col])) {
                $val = trim(MeprUtils::clean($row[$col]));
                if ($val === '*' || empty($val)) {
                    $tax_rate_info[$col] = [];
                } else {
                    $tax_rate_info[$col] = explode(';', preg_replace('#\s*;\s*#', ';', $val));
                }
            }
        }

        $tax_rate = new MeprTaxRate();
        $tax_rate->load_from_array($tax_rate_info);
        $tax_rate->store();
    }

    /**
     * Destroys the tax rate and its associated locations.
     *
     * @return mixed The result of the destruction process.
     */
    public function destroy()
    {
        $mepr_db = new MeprDb();
        $id      = $this->id;
        $args    = compact('id');
        $this->destroy_locations_by_tax_rate();
        return MeprHooks::apply_filters('mepr_tax_rate_destroy', $mepr_db->delete_records($mepr_db->tax_rates, $args), $args);
    }

    /**
     * Imports locations for a tax rate.
     *
     * @param integer $tax_rate_id The ID of the tax rate.
     * @param string  $type        The type of location (e.g., 'city', 'postcode').
     * @param array   $locations   The locations to import.
     *
     * @return void
     */
    public static function import_locations($tax_rate_id, $type, $locations)
    {
        $mepr_db = new MeprDb();
        foreach ($locations as $location) {
            $vals = [
                'location_code' => $location,
                'location_type' => $type,
                'tax_rate_id'   => $tax_rate_id,
            ];
            MeprHooks::apply_filters('mepr_tax_rate_location_create', $mepr_db->create_record($mepr_db->tax_rate_locations, $vals, false), $vals);
        }
    }

    /**
     * Destroys locations associated with a tax rate.
     *
     * @return mixed The result of the destruction process.
     */
    public function destroy_locations_by_tax_rate()
    {
        $mepr_db     = new MeprDb();
        $tax_rate_id = $this->id;
        $args        = compact('tax_rate_id');
        return MeprHooks::apply_filters('mepr_tax_rate_locations_destroy', $mepr_db->delete_records($mepr_db->tax_rate_locations, $args), $args);
    }

    /**
     * Prepares fields for storing the tax rate.
     *
     * @return void
     */
    private function prepare_fields()
    {
        $this->tax_country = MeprUtils::clean(strtoupper($this->tax_country));
        $this->tax_country = $this->tax_country === '*' ? '' : $this->tax_country;

        $this->tax_state = MeprUtils::clean(strtoupper($this->tax_state));
        $this->tax_state = $this->tax_state === '*' ? '' : $this->tax_state;

        foreach ($this->postcodes as $i => $postcode) {
            $postcode = trim(MeprUtils::clean($postcode));
            if ($postcode !== '*' && !empty($postcode)) {
                $this->rec->postcodes[$i] = $postcode = MeprUtils::clean(strtoupper($postcode));
            }
        }

        foreach ($this->cities as $i => $city) {
            $city = trim(MeprUtils::clean($city));
            if ($city !== '*' && !empty($city)) {
                $this->rec->cities[$i] = $city = MeprUtils::clean(strtoupper($city));
            }
        }
    }
}