## ✅ TAX SETTINGS - COMPLETE ### Backend (TaxController.php): - ✅ GET /settings/tax - Get all tax settings - ✅ POST /settings/tax/toggle - Enable/disable tax - ✅ GET /settings/tax/suggested - Smart suggestions based on selling locations - ✅ POST /settings/tax/rates - Create tax rate - ✅ PUT /settings/tax/rates/{id} - Update tax rate - ✅ DELETE /settings/tax/rates/{id} - Delete tax rate **Predefined Rates:** - Indonesia: 11% (PPN) - Malaysia: 6% (SST) - Singapore: 9% (GST) - Thailand: 7% (VAT) - Philippines: 12% (VAT) - Vietnam: 10% (VAT) - + Australia, NZ, UK, Germany, France, Italy, Spain, Canada **Smart Detection:** - Reads WooCommerce "Selling location(s)" setting - If specific countries selected → Show those rates - If sell to all → Show store base country rate - Zero re-selection needed! ### Frontend (Tax.tsx): - ✅ Toggle to enable/disable tax - ✅ Suggested rates card (based on selling locations) - ✅ Quick "Add Rate" button for suggested rates - ✅ Tax rates list with Edit/Delete - ✅ Add/Edit tax rate dialog - ✅ Display settings (prices include tax, shop/cart display) - ✅ Link to WooCommerce advanced settings **User Flow:** 1. Enable tax toggle 2. See: "🇮🇩 Indonesia: 11% (PPN)" [Add Rate] 3. Click Add Rate 4. Done! Tax working. ## ✅ CHECKOUT FIELDS - COMPLETE ### Backend (CheckoutController.php): - ✅ POST /checkout/fields - Get fields with all filters applied **Features:** - Listens to WooCommerce `woocommerce_checkout_fields` filter - Respects addon hide/show logic: - Checks `hidden` class - Checks `enabled` flag - Checks `hide` class - Respects digital-only products logic (hides shipping) - Returns field metadata: - required, hidden, type, options, priority - Flags custom fields (from addons) - Includes validation rules **How It Works:** 1. Addon adds field via filter 2. API applies all filters 3. Returns fields with metadata 4. Frontend renders dynamically **Example:** ```php // Indonesian Shipping Addon add_filter('woocommerce_checkout_fields', function($fields) { $fields['shipping']['shipping_subdistrict'] = [ 'required' => true, 'type' => 'select', 'options' => get_subdistricts(), ]; return $fields; }); ``` WooNooW automatically: - Fetches field - Sees required=true - Renders it - Validates it ## Benefits: **Tax:** - Zero learning curve (30 seconds setup) - No re-selecting countries - Smart suggestions - Scales for single/multi-country **Checkout Fields:** - Addon responsibility (not hardcoded) - Works with ANY addon - Respects hide/show logic - Preserves digital-only logic - Future-proof ## Next: Frontend integration for checkout fields
425 lines
11 KiB
PHP
425 lines
11 KiB
PHP
<?php
|
|
/**
|
|
* Tax Settings REST API Controller
|
|
*
|
|
* @package WooNooW
|
|
*/
|
|
|
|
namespace WooNooW\Api;
|
|
|
|
use WP_REST_Controller;
|
|
use WP_REST_Server;
|
|
use WP_REST_Request;
|
|
use WP_REST_Response;
|
|
use WP_Error;
|
|
|
|
class TaxController extends WP_REST_Controller {
|
|
|
|
/**
|
|
* Register routes
|
|
*/
|
|
public function register_routes() {
|
|
$namespace = 'woonoow/v1';
|
|
|
|
// Get tax settings
|
|
register_rest_route(
|
|
$namespace,
|
|
'/settings/tax',
|
|
array(
|
|
'methods' => WP_REST_Server::READABLE,
|
|
'callback' => array( $this, 'get_settings' ),
|
|
'permission_callback' => array( $this, 'check_permission' ),
|
|
)
|
|
);
|
|
|
|
// Toggle tax calculation
|
|
register_rest_route(
|
|
$namespace,
|
|
'/settings/tax/toggle',
|
|
array(
|
|
'methods' => WP_REST_Server::CREATABLE,
|
|
'callback' => array( $this, 'toggle_tax' ),
|
|
'permission_callback' => array( $this, 'check_permission' ),
|
|
'args' => array(
|
|
'enabled' => array(
|
|
'required' => true,
|
|
'type' => 'boolean',
|
|
'sanitize_callback' => 'rest_sanitize_boolean',
|
|
),
|
|
),
|
|
)
|
|
);
|
|
|
|
// Get suggested tax rates based on selling locations
|
|
register_rest_route(
|
|
$namespace,
|
|
'/settings/tax/suggested',
|
|
array(
|
|
'methods' => WP_REST_Server::READABLE,
|
|
'callback' => array( $this, 'get_suggested_rates' ),
|
|
'permission_callback' => array( $this, 'check_permission' ),
|
|
)
|
|
);
|
|
|
|
// Create tax rate
|
|
register_rest_route(
|
|
$namespace,
|
|
'/settings/tax/rates',
|
|
array(
|
|
'methods' => WP_REST_Server::CREATABLE,
|
|
'callback' => array( $this, 'create_rate' ),
|
|
'permission_callback' => array( $this, 'check_permission' ),
|
|
)
|
|
);
|
|
|
|
// Update tax rate
|
|
register_rest_route(
|
|
$namespace,
|
|
'/settings/tax/rates/(?P<id>\d+)',
|
|
array(
|
|
'methods' => WP_REST_Server::EDITABLE,
|
|
'callback' => array( $this, 'update_rate' ),
|
|
'permission_callback' => array( $this, 'check_permission' ),
|
|
)
|
|
);
|
|
|
|
// Delete tax rate
|
|
register_rest_route(
|
|
$namespace,
|
|
'/settings/tax/rates/(?P<id>\d+)',
|
|
array(
|
|
'methods' => WP_REST_Server::DELETABLE,
|
|
'callback' => array( $this, 'delete_rate' ),
|
|
'permission_callback' => array( $this, 'check_permission' ),
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check permission
|
|
*/
|
|
public function check_permission() {
|
|
return current_user_can( 'manage_woocommerce' );
|
|
}
|
|
|
|
/**
|
|
* Get tax settings
|
|
*/
|
|
public function get_settings( WP_REST_Request $request ) {
|
|
try {
|
|
$settings = array(
|
|
'calc_taxes' => get_option( 'woocommerce_calc_taxes', 'no' ),
|
|
'prices_include_tax' => get_option( 'woocommerce_prices_include_tax', 'no' ),
|
|
'tax_based_on' => get_option( 'woocommerce_tax_based_on', 'shipping' ),
|
|
'tax_display_shop' => get_option( 'woocommerce_tax_display_shop', 'excl' ),
|
|
'tax_display_cart' => get_option( 'woocommerce_tax_display_cart', 'excl' ),
|
|
'standard_rates' => $this->get_tax_rates( 'standard' ),
|
|
'reduced_rates' => $this->get_tax_rates( 'reduced-rate' ),
|
|
'zero_rates' => $this->get_tax_rates( 'zero-rate' ),
|
|
);
|
|
|
|
return new WP_REST_Response( $settings, 200 );
|
|
} catch ( \Exception $e ) {
|
|
return new WP_REST_Response(
|
|
array(
|
|
'error' => 'fetch_failed',
|
|
'message' => $e->getMessage(),
|
|
),
|
|
500
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Toggle tax calculation
|
|
*/
|
|
public function toggle_tax( WP_REST_Request $request ) {
|
|
try {
|
|
$enabled = $request->get_param( 'enabled' );
|
|
$value = $enabled ? 'yes' : 'no';
|
|
|
|
update_option( 'woocommerce_calc_taxes', $value );
|
|
|
|
// Clear WooCommerce cache
|
|
\WC_Cache_Helper::invalidate_cache_group( 'taxes' );
|
|
\WC_Cache_Helper::get_transient_version( 'shipping', true );
|
|
|
|
return new WP_REST_Response(
|
|
array(
|
|
'success' => true,
|
|
'enabled' => $enabled,
|
|
'message' => $enabled
|
|
? __( 'Tax calculation enabled', 'woonoow' )
|
|
: __( 'Tax calculation disabled', 'woonoow' ),
|
|
),
|
|
200
|
|
);
|
|
} catch ( \Exception $e ) {
|
|
return new WP_REST_Response(
|
|
array(
|
|
'error' => 'update_failed',
|
|
'message' => $e->getMessage(),
|
|
),
|
|
500
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get tax rates for a specific class
|
|
*/
|
|
private function get_tax_rates( $tax_class ) {
|
|
global $wpdb;
|
|
|
|
$rates = $wpdb->get_results(
|
|
$wpdb->prepare(
|
|
"SELECT * FROM {$wpdb->prefix}woocommerce_tax_rates
|
|
WHERE tax_rate_class = %s
|
|
ORDER BY tax_rate_order ASC",
|
|
$tax_class
|
|
)
|
|
);
|
|
|
|
$formatted_rates = array();
|
|
|
|
foreach ( $rates as $rate ) {
|
|
$formatted_rates[] = array(
|
|
'id' => $rate->tax_rate_id,
|
|
'country' => $rate->tax_rate_country,
|
|
'state' => $rate->tax_rate_state,
|
|
'rate' => $rate->tax_rate,
|
|
'name' => $rate->tax_rate_name,
|
|
'priority' => $rate->tax_rate_priority,
|
|
'compound' => $rate->tax_rate_compound,
|
|
'shipping' => $rate->tax_rate_shipping,
|
|
);
|
|
}
|
|
|
|
return $formatted_rates;
|
|
}
|
|
|
|
/**
|
|
* Get suggested tax rates based on WooCommerce selling locations
|
|
*/
|
|
public function get_suggested_rates( WP_REST_Request $request ) {
|
|
try {
|
|
// Predefined tax rates by country
|
|
$predefined_rates = array(
|
|
'ID' => array( 'country' => 'Indonesia', 'rate' => 11, 'name' => 'PPN (VAT)' ),
|
|
'MY' => array( 'country' => 'Malaysia', 'rate' => 6, 'name' => 'SST' ),
|
|
'SG' => array( 'country' => 'Singapore', 'rate' => 9, 'name' => 'GST' ),
|
|
'TH' => array( 'country' => 'Thailand', 'rate' => 7, 'name' => 'VAT' ),
|
|
'PH' => array( 'country' => 'Philippines', 'rate' => 12, 'name' => 'VAT' ),
|
|
'VN' => array( 'country' => 'Vietnam', 'rate' => 10, 'name' => 'VAT' ),
|
|
'AU' => array( 'country' => 'Australia', 'rate' => 10, 'name' => 'GST' ),
|
|
'NZ' => array( 'country' => 'New Zealand', 'rate' => 15, 'name' => 'GST' ),
|
|
'GB' => array( 'country' => 'United Kingdom', 'rate' => 20, 'name' => 'VAT' ),
|
|
'DE' => array( 'country' => 'Germany', 'rate' => 19, 'name' => 'VAT' ),
|
|
'FR' => array( 'country' => 'France', 'rate' => 20, 'name' => 'VAT' ),
|
|
'IT' => array( 'country' => 'Italy', 'rate' => 22, 'name' => 'VAT' ),
|
|
'ES' => array( 'country' => 'Spain', 'rate' => 21, 'name' => 'VAT' ),
|
|
'CA' => array( 'country' => 'Canada', 'rate' => 5, 'name' => 'GST' ),
|
|
);
|
|
|
|
// Get WooCommerce selling locations
|
|
$selling_locations = get_option( 'woocommerce_allowed_countries', 'all' );
|
|
$suggested = array();
|
|
|
|
if ( $selling_locations === 'specific' ) {
|
|
// User selected specific countries
|
|
$countries = get_option( 'woocommerce_specific_allowed_countries', array() );
|
|
|
|
foreach ( $countries as $country_code ) {
|
|
if ( isset( $predefined_rates[ $country_code ] ) ) {
|
|
$suggested[] = array_merge(
|
|
array( 'code' => $country_code ),
|
|
$predefined_rates[ $country_code ]
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
// Sell to all countries - suggest store base country
|
|
$store_country = get_option( 'woocommerce_default_country', '' );
|
|
$country_code = substr( $store_country, 0, 2 ); // Extract country code (e.g., 'ID' from 'ID:JB')
|
|
|
|
if ( isset( $predefined_rates[ $country_code ] ) ) {
|
|
$suggested[] = array_merge(
|
|
array( 'code' => $country_code ),
|
|
$predefined_rates[ $country_code ]
|
|
);
|
|
}
|
|
}
|
|
|
|
return new WP_REST_Response(
|
|
array(
|
|
'suggested' => $suggested,
|
|
'all_rates' => $predefined_rates,
|
|
),
|
|
200
|
|
);
|
|
} catch ( \Exception $e ) {
|
|
return new WP_REST_Response(
|
|
array(
|
|
'error' => 'fetch_failed',
|
|
'message' => $e->getMessage(),
|
|
),
|
|
500
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create tax rate
|
|
*/
|
|
public function create_rate( WP_REST_Request $request ) {
|
|
try {
|
|
global $wpdb;
|
|
|
|
$country = $request->get_param( 'country' );
|
|
$state = $request->get_param( 'state' ) ?? '';
|
|
$rate = $request->get_param( 'rate' );
|
|
$name = $request->get_param( 'name' );
|
|
$tax_class = $request->get_param( 'tax_class' ) ?? '';
|
|
$priority = $request->get_param( 'priority' ) ?? 1;
|
|
$compound = $request->get_param( 'compound' ) ?? 0;
|
|
$shipping = $request->get_param( 'shipping' ) ?? 1;
|
|
|
|
$wpdb->insert(
|
|
$wpdb->prefix . 'woocommerce_tax_rates',
|
|
array(
|
|
'tax_rate_country' => $country,
|
|
'tax_rate_state' => $state,
|
|
'tax_rate' => $rate,
|
|
'tax_rate_name' => $name,
|
|
'tax_rate_priority' => $priority,
|
|
'tax_rate_compound' => $compound,
|
|
'tax_rate_shipping' => $shipping,
|
|
'tax_rate_order' => 0,
|
|
'tax_rate_class' => $tax_class,
|
|
),
|
|
array( '%s', '%s', '%s', '%s', '%d', '%d', '%d', '%d', '%s' )
|
|
);
|
|
|
|
$rate_id = $wpdb->insert_id;
|
|
|
|
// Clear cache
|
|
\WC_Cache_Helper::invalidate_cache_group( 'taxes' );
|
|
|
|
return new WP_REST_Response(
|
|
array(
|
|
'success' => true,
|
|
'id' => $rate_id,
|
|
'message' => __( 'Tax rate created', 'woonoow' ),
|
|
),
|
|
201
|
|
);
|
|
} catch ( \Exception $e ) {
|
|
return new WP_REST_Response(
|
|
array(
|
|
'error' => 'create_failed',
|
|
'message' => $e->getMessage(),
|
|
),
|
|
500
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update tax rate
|
|
*/
|
|
public function update_rate( WP_REST_Request $request ) {
|
|
try {
|
|
global $wpdb;
|
|
|
|
$rate_id = $request->get_param( 'id' );
|
|
$country = $request->get_param( 'country' );
|
|
$state = $request->get_param( 'state' ) ?? '';
|
|
$rate = $request->get_param( 'rate' );
|
|
$name = $request->get_param( 'name' );
|
|
$tax_class = $request->get_param( 'tax_class' ) ?? '';
|
|
$priority = $request->get_param( 'priority' ) ?? 1;
|
|
$compound = $request->get_param( 'compound' ) ?? 0;
|
|
$shipping = $request->get_param( 'shipping' ) ?? 1;
|
|
|
|
$wpdb->update(
|
|
$wpdb->prefix . 'woocommerce_tax_rates',
|
|
array(
|
|
'tax_rate_country' => $country,
|
|
'tax_rate_state' => $state,
|
|
'tax_rate' => $rate,
|
|
'tax_rate_name' => $name,
|
|
'tax_rate_priority' => $priority,
|
|
'tax_rate_compound' => $compound,
|
|
'tax_rate_shipping' => $shipping,
|
|
'tax_rate_class' => $tax_class,
|
|
),
|
|
array( 'tax_rate_id' => $rate_id ),
|
|
array( '%s', '%s', '%s', '%s', '%d', '%d', '%d', '%s' ),
|
|
array( '%d' )
|
|
);
|
|
|
|
// Clear cache
|
|
\WC_Cache_Helper::invalidate_cache_group( 'taxes' );
|
|
|
|
return new WP_REST_Response(
|
|
array(
|
|
'success' => true,
|
|
'message' => __( 'Tax rate updated', 'woonoow' ),
|
|
),
|
|
200
|
|
);
|
|
} catch ( \Exception $e ) {
|
|
return new WP_REST_Response(
|
|
array(
|
|
'error' => 'update_failed',
|
|
'message' => $e->getMessage(),
|
|
),
|
|
500
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete tax rate
|
|
*/
|
|
public function delete_rate( WP_REST_Request $request ) {
|
|
try {
|
|
global $wpdb;
|
|
|
|
$rate_id = $request->get_param( 'id' );
|
|
|
|
$wpdb->delete(
|
|
$wpdb->prefix . 'woocommerce_tax_rates',
|
|
array( 'tax_rate_id' => $rate_id ),
|
|
array( '%d' )
|
|
);
|
|
|
|
// Also delete rate locations
|
|
$wpdb->delete(
|
|
$wpdb->prefix . 'woocommerce_tax_rate_locations',
|
|
array( 'tax_rate_id' => $rate_id ),
|
|
array( '%d' )
|
|
);
|
|
|
|
// Clear cache
|
|
\WC_Cache_Helper::invalidate_cache_group( 'taxes' );
|
|
|
|
return new WP_REST_Response(
|
|
array(
|
|
'success' => true,
|
|
'message' => __( 'Tax rate deleted', 'woonoow' ),
|
|
),
|
|
200
|
|
);
|
|
} catch ( \Exception $e ) {
|
|
return new WP_REST_Response(
|
|
array(
|
|
'error' => 'delete_failed',
|
|
'message' => $e->getMessage(),
|
|
),
|
|
500
|
|
);
|
|
}
|
|
}
|
|
}
|