SureCart – OSS VAT Sales Report

Tech Articles | 22 January 2026 | Blog, Coding, SureCart, Wordpress

So I saw an interesting post on the Sure Crafted Facebook page about the need to do sales reports and find out what each country had been charged for One Stop Shop this is something that doesn’t effect me directly the UK is not part of europe anymore but theren is a reference affecting Northern Ireland.

I posted on this post that I thought all this information was available via the SureCart API and thus it was possible, Adam said that he would ask the team to build this but I wanted to see if I was right. Now I have said it before the SureCart API and there documentation is top notch and is a credit to the product and the team, they have also recently redone there documentation. I recommend you head over the there site and check it out.

My Thought Process

So when this one come up I thought to myself this should be possible all you need to do is

  • List the Orders
  • Retrieve the orders
  • Retrieve Customers Name but you still need to expand the customer anyway

This should be reasonably easy to do and I set about testing this theory using cURL and testing those outputs and the outcome.

Listing The Orders:

curl -X GET "https://api.surecart.com/v1/purchases?limit=100&page=1" \
  -H "Authorization: Bearer YOUR_API_KEY"

So Purchases in the link between our customer order, price and product information so we need to get our initial order values

I was looking to grab the following information from that returned response

{
  "id": "purchase_id",
  "initial_order": "order_id",
  "created_at": 1768148859
}

Retrieve the order:

So we now need to take our order ID and look up the order information, so our order_id comes from our list of purchases and we then need to look at the data I did that and the data and got a return containing infromation

curl -X GET "https://api.surecart.com/v1/orders/ORDER_ID" \
  -H "Authorization: Bearer YOUR_API_KEY"

the information we need here is

{
  "number": "example-0065",
  "checkout": {
    "total_amount": 3900,
    "tax_amount": 0,
    "tax_percent": 0.0,
    "currency": "usd",
    "paid_at": 1768148859,
    "inherited_billing_address": {
      "country": "GB"
    }
  }
}

Note: SureCart uses expandable relationships rather than separate address or nested endpoints. Many fields that look like IDs are actually expandable objects.

You can tell an expandable return in the new documentation as it is highlighted like in the below screenshot.

Checkout ID description with arrow - is Exapandable

So in practice we need to do some expansion here for shipping address and billing address

curl -X GET \
"https://api.surecart.com/v1/orders/ORDER_ID?expand[]=checkout.inherited_billing_address&expand[]=checkout.inherited_shipping_address" \
-H "Authorization: Bearer YOUR_API_KEY"

SureCart resolves all pricing, tax, and country data at checkout time. Expandable fields must be explicitly expanded on the order endpoint to access billing and shipping addresses.

Now one item I wasn’t sure if it was needed or not was the customer name but I thought it maybe depending on the rules so thought i’d add that to and that was achived and proved as working using the following curl command

curl -X GET "https://api.surecart.com/v1/customers/CUSTOMER_ID" \
  -H "Authorization: Bearer YOUR_API_KEY"

This allows us to pull up the customer name and other details

{
  "name": "Test Customer",
  "email": "[email protected]"
}

The next Steps:

Once I had the data I needed it was a case of writing a plugin / snippet to carry out the next steps for what I was calling SureCart Sales, we need to be able to set our token key as all our requests will require it, I assumed filtering by data and getting line item and totals would be required too. I also assumed as it was for tax purposes you’d probably need some sort of CSV output. Once this was all done I settled on an interface that looks a little like the below,

SureCart sales report by country

Please Note: This is a proof of concept I don’t plan on developing this in to a release plugin or carrying out bug fixes to it. Its purpose is to show the power of the SureCart API and expand my understanding and working within that API.

SureCart will release an official report for that but as I did the work I thought I would release the snippet, to give you access to it as soon as possible, this could all be adapted now using AI if you need changes.

Support my work

Some people have asked about supporting my work and this is always gratefully received you are able to do that by clicking donate or every article also has a link in the footer to my paid products and a donation link you can also subscribe to my channel, like and comment too to support what I do

Donate
<?php
/**
 * Plugin Name: SureCart Sales Report 
 * Description: Sales Report for Tax Purposes POS
 * Version: 1.0
 */

if (!defined('ABSPATH')) exit;

class SureCart_Sales_Report_Streamlined_Tax {
  const OPTION_TOKEN = 'surecart_api_token';
  const BASE_URL = 'https://api.surecart.com';

  const PAGE_SLUG = 'surecart-sales-report';
  const EXPORT_ACTION = 'surecart_sales_export';

  public function __construct() {
    add_action('admin_menu', [$this, 'menu']);
    add_action('admin_init', [$this, 'register_settings']);
    add_action('admin_init', [$this, 'maybe_export']); // tHis needs torun before any HTML output or we get interferance 
  }

  public function register_settings() {
    register_setting('surecart_sales_report', self::OPTION_TOKEN);
  }

  public function menu() {
    add_menu_page(
      'SureCart Sales Report',
      'SureCart Sales',
      'manage_options',
      self::PAGE_SLUG,
      [$this, 'page'],
      'dashicons-chart-bar',
      56
    );
  }

  private function api_token() {
    return trim((string) get_option(self::OPTION_TOKEN, ''));
  }

  /**
   * Build URL query string in a way SureCart understands for expand[].
   * Ensures repeated expand[]=. 
   * N2M: https://developer.surecart.com/api-reference/expanding-responses - When writing the Article make sure you include expanding responses.
   */
  private function build_url($path, $query = []) {
    $url = self::BASE_URL . $path;
    if (empty($query) || !is_array($query)) return $url;

    $parts = [];
    foreach ($query as $key => $value) {
      if (is_array($value)) {
        foreach ($value as $v) {
          $parts[] = rawurlencode($key) . '=' . rawurlencode((string) $v);
        }
      } else if ($value !== null && $value !== '') {
        $parts[] = rawurlencode($key) . '=' . rawurlencode((string) $value);
      }
    }

    if (!empty($parts)) {
      $url .= (strpos($url, '?') === false ? '?' : '&') . implode('&', $parts);
    }
    return $url;
  }

  private function sc_get($path, $query = []) {
    $token = $this->api_token();
    if (!$token) throw new Exception('SureCart API token is not configured.');

    $url = $this->build_url($path, $query);

    $res = wp_remote_get($url, [
      'timeout' => 30,
      'headers' => [
        'Authorization' => 'Bearer ' . $token,
        'Accept'        => 'application/json',
      ],
    ]);

    if (is_wp_error($res)) throw new Exception($res->get_error_message());

    $code = wp_remote_retrieve_response_code($res);
    $body = wp_remote_retrieve_body($res);

    if ($code < 200 || $code >= 300) {
      throw new Exception("HTTP $code :: $body");
    }

    $json = json_decode($body, true);
    if (!is_array($json)) throw new Exception('Invalid JSON from SureCart API.');
    return $json;
  }

  /**
   * Purchases pagination: pagination.count is TOTAL items available. 
   * N2M: I am pretty sure 100 is a hard limit but I can't for the life of me remember where this was referenced, should try this at some point see if its more 
   */
  private function fetch_all_purchases($limit = 100) {
    $all = [];
    $limit = max(1, min(100, (int) $limit));

    $first = $this->sc_get('/v1/purchases', ['limit' => $limit, 'page' => 1]);
    $data = $first['data'] ?? [];
    if (!is_array($data)) $data = [];
    $all = array_merge($all, $data);

    $pagination  = $first['pagination'] ?? [];
    $total_count = isset($pagination['count']) ? (int) $pagination['count'] : count($data);

    if ($total_count <= count($data)) return $all;

    $total_pages = (int) ceil($total_count / $limit);

    for ($page = 2; $page <= $total_pages; $page++) {
      $json = $this->sc_get('/v1/purchases', ['limit' => $limit, 'page' => $page]);
      $data = $json['data'] ?? [];
      if (!is_array($data) || empty($data)) break;
      $all = array_merge($all, $data);
    }

    return $all;
  }

  private function fetch_order($order_id) {
    if (!$order_id) return null;

    // Matches what you proved via curl:
    return $this->sc_get('/v1/orders/' . rawurlencode($order_id), [
      'expand[]' => [
        'checkout',
        'checkout.inherited_billing_address',
        'checkout.inherited_shipping_address',
      ],
    ]);
  }

  private function fetch_customer($customer_id) {
    if (!$customer_id) return null;
    return $this->sc_get('/v1/customers/' . rawurlencode($customer_id));
  }

  private function money_minor_to_major($minor) {
    return number_format(((int)$minor) / 100, 2, '.', '');
  }

  private function resolve_customer_name($customer) {
    if (!is_array($customer)) return '';
    if (!empty($customer['name'])) return (string) $customer['name'];

    $first = isset($customer['first_name']) ? trim((string)$customer['first_name']) : '';
    $last  = isset($customer['last_name']) ? trim((string)$customer['last_name']) : '';
    return trim($first . ' ' . $last);
  }

  /**
   * Country code + friendly name from expanded inherited address.
   * Prefers inherited_billing_address; falls back to inherited_shipping_address.
   */
  private function resolve_country_from_order($order) {
  $unknown = ['code' => 'Unknown', 'name' => 'Unknown'];

  if (!is_array($order) || empty($order['checkout']) || !is_array($order['checkout'])) return $unknown;

  $co = $order['checkout'];

  $addr = $co['inherited_billing_address'] ?? null;
  if (!is_array($addr)) $addr = $co['inherited_shipping_address'] ?? null;
  if (!is_array($addr)) return $unknown;

  $code = $addr['country'] ?? null;
  $code = $code ? strtoupper((string) $code) : 'Unknown';

  // IMPORTANT: do NOT use formatted_string here (it can contain full address).
  $name = $this->country_code_to_name($code);

  return [
    'code' => $code ?: 'Unknown',
    'name' => $name ?: ($code ?: 'Unknown'),
  ];
}


  /**
   * Streamlined money fields from checkout:
   * - amount = checkout.total_amount
   * - tax    = checkout.tax_amount
   * - percent = checkout.tax_percent (fallback to tax_rate*100)
   * Also returns a "tax_base" for effective rate calculations (prefer subtotal_amount).
   */
  private function resolve_amounts_and_tax($order) {
    $amount = 0;
    $tax = 0;
    $currency = 'usd';
    $tax_percent = null; // numeric percent e.g. 20.0
    $tax_base = 0;

    if (!is_array($order) || empty($order['checkout']) || !is_array($order['checkout'])) {
      return [$amount, $tax, $currency, $tax_percent, $tax_base];
    }

    $co = $order['checkout'];

    $currency = !empty($co['currency']) ? (string)$co['currency'] : 'usd';
    $amount   = isset($co['total_amount']) ? (int)$co['total_amount'] : 0;
    $tax      = isset($co['tax_amount']) ? (int)$co['tax_amount'] : 0;

    if (isset($co['tax_percent']) && is_numeric($co['tax_percent']) && (float)$co['tax_percent'] >= 0) {
      // tax_percent appears already in percent units
      $tax_percent = (float)$co['tax_percent'];
    } elseif (isset($co['tax_rate']) && is_numeric($co['tax_rate']) && (float)$co['tax_rate'] >= 0) {
      $tax_percent = (float)$co['tax_rate'] * 100.0;
    }

    $tax_base = isset($co['subtotal_amount']) ? (int)$co['subtotal_amount'] : $amount;

    return [$amount, $tax, $currency, $tax_percent, $tax_base];
  }

  private function effective_tax_percent($tax_minor, $base_minor) {
    $tax_minor = (int)$tax_minor;
    $base_minor = (int)$base_minor;
    if ($tax_minor <= 0 || $base_minor <= 0) return 0.0;
    return ($tax_minor / $base_minor) * 100.0;
  }

  private function country_code_to_name($code) {
    $code = strtoupper((string)$code);
    if ($code === '' || $code === 'UNKNOWN') return 'Unknown';

    static $map = [
      'GB' => 'United Kingdom',
      'US' => 'United States',
      'CA' => 'Canada',
      'AU' => 'Australia',
      'NZ' => 'New Zealand',
      'IE' => 'Ireland',
      'DE' => 'Germany',
      'FR' => 'France',
      'ES' => 'Spain',
      'IT' => 'Italy',
      'NL' => 'Netherlands',
      'BE' => 'Belgium',
      'SE' => 'Sweden',
      'NO' => 'Norway',
      'DK' => 'Denmark',
      'FI' => 'Finland',
      'CH' => 'Switzerland',
      'AT' => 'Austria',
      'PL' => 'Poland',
      'PT' => 'Portugal',
      'GR' => 'Greece',
      'CZ' => 'Czechia',
      'HU' => 'Hungary',
      'RO' => 'Romania',
      'BG' => 'Bulgaria',
      'TR' => 'Turkey',
      'ZA' => 'South Africa',
      'IN' => 'India',
      'SG' => 'Singapore',
      'HK' => 'Hong Kong',
      'JP' => 'Japan',
      'BR' => 'Brazil',
      'MX' => 'Mexico',
    ];

    return $map[$code] ?? $code;
  }

  /**
   * Convert YYYY-MM-DD to unix timestamps (site timezone).
   * Start => 00:00:00, End => 23:59:59
   * N2M: I assume if i set this to the local code it makes it more understandable to the local site, if I hard code it to UTC or GMT this would be incorrect in some countries.
   */
  private function date_range_to_timestamps($start_ymd, $end_ymd) {
    $tz = wp_timezone();
    $start_ts = null;
    $end_ts = null;

    if (!empty($start_ymd)) {
      $dt = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $start_ymd . ' 00:00:00', $tz);
      if ($dt) $start_ts = $dt->getTimestamp();
    }

    if (!empty($end_ymd)) {
      $dt = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $end_ymd . ' 23:59:59', $tz);
      if ($dt) $end_ts = $dt->getTimestamp();
    }

    return [$start_ts, $end_ts];
  }

  /**
   * Build report rows + totals.
   */
  private function build_report($start_ymd, $end_ymd, $include_revoked, $include_customer_name) {
    [$start_ts, $end_ts] = $this->date_range_to_timestamps($start_ymd, $end_ymd);

    $purchases = $this->fetch_all_purchases(100);

    $rows = [];
    $totals = []; // bucket: country_code|currency

    $order_cache = [];
    $customer_cache = [];

    foreach ($purchases as $purchase) {
      if (!is_array($purchase)) continue;

      $created_at = isset($purchase['created_at']) ? (int)$purchase['created_at'] : 0;
      if ($start_ts !== null && $created_at < $start_ts) continue;
      if ($end_ts !== null && $created_at > $end_ts) continue;

      if (!$include_revoked && !empty($purchase['revoked'])) continue;

      $order_id = $purchase['initial_order'] ?? null;
      $customer_id = $purchase['customer'] ?? null;

      if ($order_id && !isset($order_cache[$order_id])) {
        $order_cache[$order_id] = $this->fetch_order($order_id);
      }
      $order = $order_cache[$order_id] ?? null;

      $customer_name = '';
      if ($include_customer_name && $customer_id) {
        if (!isset($customer_cache[$customer_id])) {
          $customer_cache[$customer_id] = $this->fetch_customer($customer_id);
        }
        $customer = $customer_cache[$customer_id] ?? null;
        $customer_name = $this->resolve_customer_name($customer);
      }

      $invoice_number = (is_array($order) && !empty($order['number'])) ? (string)$order['number'] : ($order_id ?: '—');

      [$amount_minor, $tax_minor, $currency, $tax_percent, $tax_base_minor] = $this->resolve_amounts_and_tax($order);

      $country = $this->resolve_country_from_order($order);
      $country_code = $country['code'];
      $country_name = $country['name'];

      $effective_tax_percent = $this->effective_tax_percent($tax_minor, $tax_base_minor);

      $rows[] = [
        'invoice' => $invoice_number,
        'customer' => $customer_name,
        'country_code' => $country_code,
        'country_name' => $country_name,
        'amount' => (int)$amount_minor,
        'tax' => (int)$tax_minor,
        'currency' => (string)$currency,
        'tax_percent' => $tax_percent, // may be null
        'effective_tax_percent' => $effective_tax_percent,
        'tax_base' => (int)$tax_base_minor,
        'purchase_ts' => $created_at,
      ];

      $bucket = $country_code . '|' . $currency;
      if (!isset($totals[$bucket])) {
        $totals[$bucket] = [
          'country_code' => $country_code,
          'country_name' => $country_name,
          'currency' => $currency,
          'sales_minor' => 0,
          'tax_minor' => 0,
          'tax_base_minor' => 0,
        ];
      }

      $totals[$bucket]['sales_minor'] += (int)$amount_minor;
      $totals[$bucket]['tax_minor'] += (int)$tax_minor;
      $totals[$bucket]['tax_base_minor'] += (int)$tax_base_minor;
    }

    usort($rows, function($a, $b) {
      return ($b['purchase_ts'] <=> $a['purchase_ts']);
    });

    uasort($totals, function($a, $b) {
      return ($b['sales_minor'] <=> $a['sales_minor']);
    });

    return [$rows, $totals];
  }

  /**
   * Proper CSV streaming helper.
   */
  private function send_csv_headers($filename) {
    if (function_exists('ob_get_level')) {
      while (ob_get_level() > 0) {
        @ob_end_clean();
      }
    }

    nocache_headers();
    header('Content-Type: text/csv; charset=utf-8');
    header('Content-Disposition: attachment; filename=' . $filename);
    header('X-Content-Type-Options: nosniff');
  }

  private function export_line_items_csv($rows, $start_ymd, $end_ymd, $include_customer_name) {
    $filename = 'surecart-line-items-' . ($start_ymd ?: 'all') . '-to-' . ($end_ymd ?: 'all') . '.csv';
    $this->send_csv_headers($filename);

    $out = fopen('php://output', 'w');

    $headers = ['Invoice / Order #'];
    if ($include_customer_name) $headers[] = 'Customer';
    $headers = array_merge($headers, [
      'Country Code',
      'Country',
      'Amount',
      'Tax',
      'Tax % (stated)',
      'Tax % (effective)',
      'Currency',
      'Purchase Date'
    ]);

    fputcsv($out, $headers);

    $tz = wp_timezone();

    foreach ($rows as $r) {
      $date_str = '';
      if (!empty($r['purchase_ts'])) {
        $dt = (new DateTimeImmutable('@' . (int)$r['purchase_ts']))->setTimezone($tz);
        $date_str = $dt->format('Y-m-d H:i:s');
      }

      $row = [$r['invoice']];
      if ($include_customer_name) $row[] = $r['customer'];

      $row[] = $r['country_code'];
      $row[] = $r['country_name'];
      $row[] = $this->money_minor_to_major($r['amount']);
      $row[] = $this->money_minor_to_major($r['tax']);
      $row[] = ($r['tax_percent'] === null) ? '' : number_format((float)$r['tax_percent'], 2, '.', '');
      $row[] = number_format((float)$r['effective_tax_percent'], 2, '.', '');
      $row[] = strtoupper($r['currency']);
      $row[] = $date_str;

      fputcsv($out, $row);
    }

    fclose($out);
    exit;
  }

  private function export_summary_csv($totals, $start_ymd, $end_ymd) {
    $filename = 'surecart-summary-' . ($start_ymd ?: 'all') . '-to-' . ($end_ymd ?: 'all') . '.csv';
    $this->send_csv_headers($filename);

    $out = fopen('php://output', 'w');

    fputcsv($out, [
      'Country Code',
      'Country',
      'Sales Total',
      'Tax Total',
      'Effective Tax %',
      'Currency',
    ]);

    foreach ($totals as $t) {
      $eff = $this->effective_tax_percent($t['tax_minor'], $t['tax_base_minor']);
      fputcsv($out, [
        $t['country_code'],
        $t['country_name'],
        $this->money_minor_to_major($t['sales_minor']),
        $this->money_minor_to_major($t['tax_minor']),
        number_format((float)$eff, 2, '.', ''),
        strtoupper($t['currency']),
      ]);
    }

    fclose($out);
    exit;
  }

  /**
   * Handles CSV downloads *before* WordPress renders any admin HTML.
   * If we don't do this before it renders we will get the site headers!!
   */
  public function maybe_export() {
    if (!is_admin()) return;
    if (!current_user_can('manage_options')) return;

    // We only export from our page.
    $page = isset($_GET['page']) ? sanitize_text_field($_GET['page']) : '';
    if ($page !== self::PAGE_SLUG) return;

    $export = isset($_GET['sc_export']) ? sanitize_text_field($_GET['sc_export']) : '';
    if ($export === '') return;

    // Nonce check
    $nonce = isset($_GET['_wpnonce']) ? $_GET['_wpnonce'] : '';
    if (!wp_verify_nonce($nonce, self::EXPORT_ACTION)) {
      wp_die('Invalid export nonce.');
    }

    $start_date = isset($_GET['sc_start_date']) ? sanitize_text_field($_GET['sc_start_date']) : '';
    $end_date   = isset($_GET['sc_end_date']) ? sanitize_text_field($_GET['sc_end_date']) : '';
    $include_revoked = !empty($_GET['sc_include_revoked']);
    $include_customer_name = !empty($_GET['sc_include_customer_name']);

    try {
      [$rows, $totals] = $this->build_report($start_date, $end_date, $include_revoked, $include_customer_name);

      if ($export === 'line_items') {
        $this->export_line_items_csv($rows, $start_date, $end_date, $include_customer_name);
      } elseif ($export === 'summary') {
        $this->export_summary_csv($totals, $start_date, $end_date);
      } else {
        wp_die('Unknown export type.');
      }
    } catch (Exception $e) {
      wp_die('Export error: ' . esc_html($e->getMessage()));
    }
  }

  public function page() {
    if (!current_user_can('manage_options')) return;

    $token = $this->api_token();

    $start_date = isset($_GET['sc_start_date']) ? sanitize_text_field($_GET['sc_start_date']) : '';
    $end_date   = isset($_GET['sc_end_date']) ? sanitize_text_field($_GET['sc_end_date']) : '';
    $include_revoked = !empty($_GET['sc_include_revoked']);
    $include_customer_name = !empty($_GET['sc_include_customer_name']);

    $export_nonce = wp_create_nonce(self::EXPORT_ACTION);

    echo '<div class="wrap">';
    echo '<h1>SureCart Sales Report (by Country)</h1>';

    // Token settings
    echo '<form method="post" action="options.php" style="margin:16px 0; padding:12px; background:#fff; border:1px solid #ccd0d4;">';
    settings_fields('surecart_sales_report');
    echo '<h2 style="margin-top:0;">API Token</h2>';
    echo '<p>Enter your SureCart secret API token (stored in wp_options).</p>';
    echo '<input type="password" name="'.esc_attr(self::OPTION_TOKEN).'" value="'.esc_attr($token).'" style="width:420px; max-width:100%;"> ';
    submit_button('Save Token', 'primary', 'submit', false);
    echo '</form>';

    if (!$token) {
      echo '<p><strong>Set your token above, then reload this page.</strong></p>';
      echo '</div>';
      return;
    }

    // Filters
    echo '<form method="get" style="margin:16px 0; padding:12px; background:#fff; border:1px solid #ccd0d4;">';
    echo '<input type="hidden" name="page" value="'.esc_attr(self::PAGE_SLUG).'" />';

    echo '<h2 style="margin-top:0;">Filters</h2>';
    echo '<p style="margin:0 0 12px 0;">Leave dates blank to include all purchases.</p>';

    echo '<label style="display:inline-block; margin-right:12px;">Start date<br>';
    echo '<input type="date" name="sc_start_date" value="'.esc_attr($start_date).'"></label>';

    echo '<label style="display:inline-block; margin-right:12px;">End date<br>';
    echo '<input type="date" name="sc_end_date" value="'.esc_attr($end_date).'"></label>';

    echo '<label style="display:inline-block; margin-left:12px; vertical-align:bottom;">';
    echo '<input type="checkbox" name="sc_include_revoked" value="1" '.checked($include_revoked, true, false).'> Include revoked';
    echo '</label>';

    echo '<label style="display:inline-block; margin-left:12px; vertical-align:bottom;">';
    echo '<input type="checkbox" name="sc_include_customer_name" value="1" '.checked($include_customer_name, true, false).'> Include customer name';
    echo '</label>';

    echo '<div style="margin-top:12px;">';
    submit_button('Generate Report', 'primary', 'submit', false);

    // Export links (as buttons)
    $base_args = [
      'page' => self::PAGE_SLUG,
      'sc_start_date' => $start_date,
      'sc_end_date' => $end_date,
      'sc_include_revoked' => $include_revoked ? '1' : '',
      'sc_include_customer_name' => $include_customer_name ? '1' : '',
      '_wpnonce' => $export_nonce,
    ];

    $line_items_url = add_query_arg(array_merge($base_args, ['sc_export' => 'line_items']), admin_url('admin.php'));
    $summary_url    = add_query_arg(array_merge($base_args, ['sc_export' => 'summary']), admin_url('admin.php'));

    echo ' <a class="button" href="' . esc_url($line_items_url) . '">Export Line Items CSV</a>';
    echo ' <a class="button" href="' . esc_url($summary_url) . '">Export Summary CSV</a>';

    echo '</div>';
    echo '</form>';

    try {
      [$rows, $totals] = $this->build_report($start_date, $end_date, $include_revoked, $include_customer_name);

      // Line items table
      echo '<h2>Line Items</h2>';
      echo '<table class="widefat striped">';
      echo '<thead><tr>';
      echo '<th>Invoice / Order #</th>';
      if ($include_customer_name) echo '<th>Customer</th>';
      echo '<th>Country</th>';
      echo '<th>Amount</th>';
      echo '<th>Tax</th>';
      echo '<th>Tax %</th>';
      echo '<th>Effective Tax %</th>';
      echo '<th>Currency</th>';
      echo '<th>Purchase Date</th>';
      echo '</tr></thead><tbody>';

      $tz = wp_timezone();
      foreach ($rows as $r) {
        $date_str = '';
        if (!empty($r['purchase_ts'])) {
          $dt = (new DateTimeImmutable('@' . (int)$r['purchase_ts']))->setTimezone($tz);
          $date_str = $dt->format('Y-m-d H:i:s');
        }

        $tax_percent_str = ($r['tax_percent'] === null) ? '' : number_format((float)$r['tax_percent'], 2, '.', '') . '%';
        $eff_percent_str = number_format((float)$r['effective_tax_percent'], 2, '.', '') . '%';

        echo '<tr>';
        echo '<td>'.esc_html($r['invoice']).'</td>';
        if ($include_customer_name) echo '<td>'.esc_html($r['customer']).'</td>';
        echo '<td>'.esc_html($r['country_name']).' ('.esc_html($r['country_code']).')</td>';
        echo '<td>'.esc_html($this->money_minor_to_major($r['amount'])).'</td>';
        echo '<td>'.esc_html($this->money_minor_to_major($r['tax'])).'</td>';
        echo '<td>'.esc_html($tax_percent_str).'</td>';
        echo '<td>'.esc_html($eff_percent_str).'</td>';
        echo '<td>'.esc_html(strtoupper($r['currency'])).'</td>';
        echo '<td>'.esc_html($date_str).'</td>';
        echo '</tr>';
      }
      echo '</tbody></table>';

      // Summary totals
      echo '<h2 style="margin-top:24px;">Totals by Country</h2>';
      echo '<table class="widefat striped">';
      echo '<thead><tr>';
      echo '<th>Country</th><th>Sales Total</th><th>Tax Total</th><th>Effective Tax %</th><th>Currency</th>';
      echo '</tr></thead><tbody>';

      foreach ($totals as $t) {
        $eff = $this->effective_tax_percent($t['tax_minor'], $t['tax_base_minor']);
        $eff_str = number_format((float)$eff, 2, '.', '') . '%';

        echo '<tr>';
        echo '<td>'.esc_html($t['country_name']).' ('.esc_html($t['country_code']).')</td>';
        echo '<td>'.esc_html($this->money_minor_to_major($t['sales_minor'])).'</td>';
        echo '<td>'.esc_html($this->money_minor_to_major($t['tax_minor'])).'</td>';
        echo '<td>'.esc_html($eff_str).'</td>';
        echo '<td>'.esc_html(strtoupper($t['currency'])).'</td>';
        echo '</tr>';
      }

      echo '</tbody></table>';
      // Explination / User Notes
      echo '<p style="margin-top:12px;color:#666;">Totals are grouped by Country + Currency. Effective Tax % is computed as (Total Tax / Total Tax Base) × 100, using checkout.subtotal_amount as the tax base when present.</p>';

    } catch (Exception $e) {
      echo '<div class="notice notice-error"><p><strong>Error:</strong> ' . esc_html($e->getMessage()) . '</p></div>';
    }

    echo '</div>';
  }
}

new SureCart_Sales_Report_Streamlined_Tax();
PHP

A video of it in action

Support the Author

Support my work
Really Useful Plugin Logo
Wpvideobank temp
Appoligies for any spelling and grammer issue. As a dyslexic i need to rely on tools for this they like me are not perfect but I do try my best