diff --git a/ADDON_DEVELOPMENT_GUIDE.md b/ADDON_DEVELOPMENT_GUIDE.md new file mode 100644 index 0000000..36e67fd --- /dev/null +++ b/ADDON_DEVELOPMENT_GUIDE.md @@ -0,0 +1,715 @@ +# WooNooW Addon Development Guide + +**Version:** 2.0.0 +**Last Updated:** November 9, 2025 +**Status:** Production Ready + +--- + +## 📋 Table of Contents + +1. [Overview](#overview) +2. [Addon Types](#addon-types) +3. [Quick Start](#quick-start) +4. [SPA Route Injection](#spa-route-injection) +5. [Hook System Integration](#hook-system-integration) +6. [Component Development](#component-development) +7. [Best Practices](#best-practices) +8. [Examples](#examples) +9. [Troubleshooting](#troubleshooting) + +--- + +## Overview + +WooNooW provides **two powerful addon systems**: + +### 1. **SPA Route Injection** (Admin UI) +- ✅ Register custom SPA routes +- ✅ Inject navigation menu items +- ✅ Add submenu items to existing sections +- ✅ Load React components dynamically +- ✅ Full isolation and safety + +### 2. **Hook System** (Functional Extension) +- ✅ Extend OrderForm, ProductForm, etc. +- ✅ Add custom fields and validation +- ✅ Inject components at specific points +- ✅ Zero coupling with core +- ✅ WordPress-style filters and actions + +**Both systems work together seamlessly!** + +--- + +## Addon Types + +### Type A: UI-Only Addon (Route Injection) +**Use when:** Adding new pages/sections to admin + +**Example:** Reports, Analytics, Custom Dashboard + +```php +// Registers routes + navigation +add_filter('woonoow/spa_routes', ...); +add_filter('woonoow/nav_tree', ...); +``` + +### Type B: Functional Addon (Hook System) +**Use when:** Extending existing functionality + +**Example:** Indonesia Shipping, Custom Fields, Validation + +```typescript +// Registers hooks +addFilter('woonoow_order_form_after_shipping', ...); +addAction('woonoow_order_created', ...); +``` + +### Type C: Full-Featured Addon (Both Systems) +**Use when:** Complex integration needed + +**Example:** Subscriptions, Bookings, Memberships + +```php +// Backend: Routes + Hooks +add_filter('woonoow/spa_routes', ...); +add_filter('woonoow/nav_tree', ...); + +// Frontend: Hook registration +addonLoader.register({ + init: () => { + addFilter('woonoow_order_form_custom_sections', ...); + } +}); +``` + +--- + +## Quick Start + +### Step 1: Create Plugin File + +```php + 'my-addon', + 'name' => 'My Addon', + 'version' => '1.0.0', + 'spa_bundle' => plugin_dir_url(__FILE__) . 'dist/addon.js', + 'dependencies' => ['woocommerce' => '8.0'], + ]; + return $addons; +}); + +// 2. Register routes (optional - for UI pages) +add_filter('woonoow/spa_routes', function($routes) { + $routes[] = [ + 'path' => '/my-addon', + 'component_url' => plugin_dir_url(__FILE__) . 'dist/MyPage.js', + 'capability' => 'manage_woocommerce', + 'title' => 'My Addon', + ]; + return $routes; +}); + +// 3. Add navigation (optional - for UI pages) +add_filter('woonoow/nav_tree', function($tree) { + $tree[] = [ + 'key' => 'my-addon', + 'label' => 'My Addon', + 'path' => '/my-addon', + 'icon' => 'puzzle', + ]; + return $tree; +}); +``` + +### Step 2: Create Frontend Integration + +```typescript +// admin-spa/src/index.ts + +import { addonLoader, addFilter } from '@woonoow/hooks'; + +addonLoader.register({ + id: 'my-addon', + name: 'My Addon', + version: '1.0.0', + init: () => { + // Register hooks here + addFilter('woonoow_order_form_custom_sections', (content, formData, setFormData) => { + return ( + <> + {content} + + + ); + }); + } +}); +``` + +### Step 3: Build + +```bash +npm run build +``` + +**Done!** Your addon is now integrated. + +--- + +## SPA Route Injection + +### Register Routes + +```php +add_filter('woonoow/spa_routes', function($routes) { + $base_url = plugin_dir_url(__FILE__) . 'dist/'; + + $routes[] = [ + 'path' => '/subscriptions', + 'component_url' => $base_url . 'SubscriptionsList.js', + 'capability' => 'manage_woocommerce', + 'title' => 'Subscriptions', + ]; + + $routes[] = [ + 'path' => '/subscriptions/:id', + 'component_url' => $base_url . 'SubscriptionDetail.js', + 'capability' => 'manage_woocommerce', + 'title' => 'Subscription Detail', + ]; + + return $routes; +}); +``` + +### Add Navigation + +```php +// Main menu item +add_filter('woonoow/nav_tree', function($tree) { + $tree[] = [ + 'key' => 'subscriptions', + 'label' => __('Subscriptions', 'my-addon'), + 'path' => '/subscriptions', + 'icon' => 'repeat', + 'children' => [ + [ + 'label' => __('All Subscriptions', 'my-addon'), + 'mode' => 'spa', + 'path' => '/subscriptions', + ], + [ + 'label' => __('New', 'my-addon'), + 'mode' => 'spa', + 'path' => '/subscriptions/new', + ], + ], + ]; + return $tree; +}); + +// Or inject into existing section +add_filter('woonoow/nav_tree/products/children', function($children) { + $children[] = [ + 'label' => __('Bundles', 'my-addon'), + 'mode' => 'spa', + 'path' => '/products/bundles', + ]; + return $children; +}); +``` + +--- + +## Hook System Integration + +### Available Hooks + +#### Order Form Hooks +```typescript +// Add fields after billing address +'woonoow_order_form_after_billing' + +// Add fields after shipping address +'woonoow_order_form_after_shipping' + +// Add custom shipping fields +'woonoow_order_form_shipping_fields' + +// Add custom sections +'woonoow_order_form_custom_sections' + +// Add validation rules +'woonoow_order_form_validation' + +// Modify form data before render +'woonoow_order_form_data' +``` + +#### Action Hooks +```typescript +// Before form submission +'woonoow_order_form_submit' + +// After order created +'woonoow_order_created' + +// After order updated +'woonoow_order_updated' +``` + +### Hook Registration Example + +```typescript +import { addonLoader, addFilter, addAction } from '@woonoow/hooks'; + +addonLoader.register({ + id: 'indonesia-shipping', + name: 'Indonesia Shipping', + version: '1.0.0', + init: () => { + // Filter: Add subdistrict selector + addFilter('woonoow_order_form_after_shipping', (content, formData, setFormData) => { + return ( + <> + {content} + setFormData({ + ...formData, + shipping: { ...formData.shipping, subdistrict_id: id } + })} + /> + + ); + }); + + // Filter: Add validation + addFilter('woonoow_order_form_validation', (errors, formData) => { + if (!formData.shipping?.subdistrict_id) { + errors.subdistrict = 'Subdistrict is required'; + } + return errors; + }); + + // Action: Log when order created + addAction('woonoow_order_created', (orderId, orderData) => { + console.log('Order created:', orderId); + }); + } +}); +``` + +### Hook System Benefits + +✅ **Zero Coupling** +```typescript +// WooNooW Core has no knowledge of your addon +{applyFilters('woonoow_order_form_after_shipping', null, formData, setFormData)} + +// If addon exists: Returns your component +// If addon doesn't exist: Returns null +// No import, no error! +``` + +✅ **Multiple Addons Can Hook** +```typescript +// Addon A +addFilter('woonoow_order_form_after_shipping', (content) => { + return <>{content}; +}); + +// Addon B +addFilter('woonoow_order_form_after_shipping', (content) => { + return <>{content}; +}); + +// Both render! +``` + +✅ **Type Safety** +```typescript +addFilter]>( + 'woonoow_order_form_after_shipping', + (content, formData, setFormData) => { + // TypeScript knows the types! + return ; + } +); +``` + +--- + +## Component Development + +### Basic Component + +```typescript +// dist/MyPage.tsx +import React from 'react'; + +export default function MyPage() { + return ( +
+
+

My Addon

+

Welcome!

+
+
+ ); +} +``` + +### Access WooNooW APIs + +```typescript +// Access REST API +const api = (window as any).WNW_API; +const response = await fetch(`${api.root}my-addon/endpoint`, { + headers: { 'X-WP-Nonce': api.nonce }, +}); + +// Access store data +const store = (window as any).WNW_STORE; +console.log('Currency:', store.currency); + +// Access site info +const wnw = (window as any).wnw; +console.log('Site Title:', wnw.siteTitle); +``` + +### Use WooNooW Components + +```typescript +import { __ } from '@/lib/i18n'; +import { formatMoney } from '@/lib/currency'; +import { Button } from '@/components/ui/button'; +import { Card } from '@/components/ui/card'; + +export default function MyPage() { + return ( + +

{__('My Addon', 'my-addon')}

+

{formatMoney(1234.56)}

+ +
+ ); +} +``` + +### Build Configuration + +```javascript +// vite.config.js +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + lib: { + entry: 'src/index.ts', + name: 'MyAddon', + fileName: 'addon', + formats: ['es'], + }, + rollupOptions: { + external: ['react', 'react-dom'], + output: { + globals: { + react: 'React', + 'react-dom': 'ReactDOM', + }, + }, + }, + }, +}); +``` + +--- + +## Best Practices + +### ✅ DO: + +1. **Use Hook System for Functional Extensions** + ```typescript + // ✅ Good - No hardcoding + addFilter('woonoow_order_form_after_shipping', ...); + ``` + +2. **Use Route Injection for New Pages** + ```php + // ✅ Good - Separate UI + add_filter('woonoow/spa_routes', ...); + ``` + +3. **Declare Dependencies** + ```php + 'dependencies' => ['woocommerce' => '8.0'] + ``` + +4. **Check Capabilities** + ```php + 'capability' => 'manage_woocommerce' + ``` + +5. **Internationalize Strings** + ```php + 'label' => __('My Addon', 'my-addon') + ``` + +6. **Handle Errors Gracefully** + ```typescript + try { + await api.post(...); + } catch (error) { + toast.error('Failed to save'); + } + ``` + +### ❌ DON'T: + +1. **Don't Hardcode Addon Components in Core** + ```typescript + // ❌ Bad - Breaks if addon not installed + import { SubdistrictSelector } from 'addon'; + + + // ✅ Good - Use hooks + {applyFilters('woonoow_order_form_after_shipping', null)} + ``` + +2. **Don't Skip Capability Checks** + ```php + // ❌ Bad + 'capability' => '' + + // ✅ Good + 'capability' => 'manage_woocommerce' + ``` + +3. **Don't Modify Core Navigation** + ```php + // ❌ Bad + unset($tree[0]); + + // ✅ Good + $tree[] = ['key' => 'my-addon', ...]; + ``` + +--- + +## Examples + +### Example 1: Simple UI Addon (Route Injection Only) + +```php + 'reports', + 'name' => 'Reports', + 'version' => '1.0.0', + ]; + return $addons; +}); + +add_filter('woonoow/spa_routes', function($routes) { + $routes[] = [ + 'path' => '/reports', + 'component_url' => plugin_dir_url(__FILE__) . 'dist/Reports.js', + 'title' => 'Reports', + ]; + return $routes; +}); + +add_filter('woonoow/nav_tree', function($tree) { + $tree[] = [ + 'key' => 'reports', + 'label' => 'Reports', + 'path' => '/reports', + 'icon' => 'bar-chart', + ]; + return $tree; +}); +``` + +### Example 2: Functional Addon (Hook System Only) + +```typescript +// Indonesia Shipping - No UI pages, just extends OrderForm + +import { addonLoader, addFilter } from '@woonoow/hooks'; +import { SubdistrictSelector } from './components/SubdistrictSelector'; + +addonLoader.register({ + id: 'indonesia-shipping', + name: 'Indonesia Shipping', + version: '1.0.0', + init: () => { + addFilter('woonoow_order_form_after_shipping', (content, formData, setFormData) => { + return ( + <> + {content} +
+

📍 Shipping Destination

+ setFormData({ + ...formData, + shipping: { ...formData.shipping, subdistrict_id: id } + })} + /> +
+ + ); + }); + } +}); +``` + +### Example 3: Full-Featured Addon (Both Systems) + +```php + 'subscriptions', + 'name' => 'Subscriptions', + 'version' => '1.0.0', + 'spa_bundle' => plugin_dir_url(__FILE__) . 'dist/addon.js', + ]; + return $addons; +}); + +add_filter('woonoow/spa_routes', function($routes) { + $routes[] = [ + 'path' => '/subscriptions', + 'component_url' => plugin_dir_url(__FILE__) . 'dist/SubscriptionsList.js', + ]; + return $routes; +}); + +add_filter('woonoow/nav_tree', function($tree) { + $tree[] = [ + 'key' => 'subscriptions', + 'label' => 'Subscriptions', + 'path' => '/subscriptions', + 'icon' => 'repeat', + ]; + return $tree; +}); +``` + +```typescript +// Frontend: Hook integration + +import { addonLoader, addFilter } from '@woonoow/hooks'; + +addonLoader.register({ + id: 'subscriptions', + name: 'Subscriptions', + version: '1.0.0', + init: () => { + // Add subscription fields to order form + addFilter('woonoow_order_form_custom_sections', (content, formData, setFormData) => { + return ( + <> + {content} + + + ); + }); + + // Add subscription fields to product form + addFilter('woonoow_product_form_fields', (content, formData, setFormData) => { + return ( + <> + {content} + + + ); + }); + } +}); +``` + +--- + +## Troubleshooting + +### Addon Not Appearing? +- Check dependencies are met +- Verify capability requirements +- Check browser console for errors +- Flush caches: `?flush_wnw_cache=1` + +### Route Not Loading? +- Verify `component_url` is correct +- Check file exists and is accessible +- Look for JS errors in console +- Ensure component exports `default` + +### Hook Not Firing? +- Check hook name is correct +- Verify addon is registered +- Check `window.WNW_ADDONS` in console +- Ensure `init()` function runs + +### Component Not Rendering? +- Check for React errors in console +- Verify component returns valid JSX +- Check props are passed correctly +- Test component in isolation + +--- + +## Support & Resources + +**Documentation:** +- `ADDON_INJECTION_GUIDE.md` - SPA route injection (legacy) +- `ADDON_HOOK_SYSTEM.md` - Hook system details (legacy) +- `BITESHIP_ADDON_SPEC.md` - Indonesia shipping example +- `SHIPPING_ADDON_RESEARCH.md` - Shipping integration patterns + +**Code References:** +- `includes/Compat/AddonRegistry.php` - Addon registration +- `includes/Compat/RouteRegistry.php` - Route management +- `includes/Compat/NavigationRegistry.php` - Navigation building +- `admin-spa/src/lib/hooks.ts` - Hook system implementation +- `admin-spa/src/App.tsx` - Dynamic route loading + +--- + +**End of Guide** + +**Version:** 2.0.0 +**Last Updated:** November 9, 2025 +**Status:** ✅ Production Ready + +**This is the single source of truth for WooNooW addon development.** diff --git a/ADDON_HOOK_SYSTEM.md b/ADDON_HOOK_SYSTEM.md deleted file mode 100644 index c8df8f6..0000000 --- a/ADDON_HOOK_SYSTEM.md +++ /dev/null @@ -1,579 +0,0 @@ -# WooNooW Addon Hook System - -## Problem Statement - -**Question:** How can WooNooW SPA support addons without hardcoding specific components? - -**Example of WRONG approach:** -```typescript -// ❌ This is hardcoding - breaks if addon doesn't exist -import { SubdistrictSelector } from 'woonoow-indonesia-shipping'; - - - {/* ❌ Error if plugin not installed */} - -``` - -**This is "supporting specific 3rd party addons" - exactly what we want to AVOID!** - ---- - -## **The Solution: WordPress-Style Hook System in React** - -### **Architecture Overview** - -``` -WooNooW Core (Base): -- Provides hook points -- Renders whatever addons register -- No knowledge of specific addons - -Addon Plugins: -- Register components via hooks -- Only loaded if plugin is active -- Self-contained functionality -``` - ---- - -## **Implementation** - -### **Step 1: Create Hook System in WooNooW Core** - -```typescript -// admin-spa/src/lib/hooks.ts - -type HookCallback = (...args: any[]) => any; - -class HookSystem { - private filters: Map = new Map(); - private actions: Map = new Map(); - - /** - * Add a filter hook - * Similar to WordPress add_filter() - */ - addFilter(hookName: string, callback: HookCallback, priority: number = 10) { - if (!this.filters.has(hookName)) { - this.filters.set(hookName, []); - } - - const hooks = this.filters.get(hookName)!; - hooks.push({ callback, priority }); - hooks.sort((a, b) => a.priority - b.priority); - } - - /** - * Apply filters - * Similar to WordPress apply_filters() - */ - applyFilters(hookName: string, value: any, ...args: any[]): any { - const hooks = this.filters.get(hookName) || []; - - return hooks.reduce((currentValue, { callback }) => { - return callback(currentValue, ...args); - }, value); - } - - /** - * Add an action hook - * Similar to WordPress add_action() - */ - addAction(hookName: string, callback: HookCallback, priority: number = 10) { - if (!this.actions.has(hookName)) { - this.actions.set(hookName, []); - } - - const hooks = this.actions.get(hookName)!; - hooks.push({ callback, priority }); - hooks.sort((a, b) => a.priority - b.priority); - } - - /** - * Do action - * Similar to WordPress do_action() - */ - doAction(hookName: string, ...args: any[]) { - const hooks = this.actions.get(hookName) || []; - hooks.forEach(({ callback }) => callback(...args)); - } -} - -// Export singleton instance -export const hooks = new HookSystem(); - -// Export helper functions -export const addFilter = hooks.addFilter.bind(hooks); -export const applyFilters = hooks.applyFilters.bind(hooks); -export const addAction = hooks.addAction.bind(hooks); -export const doAction = hooks.doAction.bind(hooks); -``` - -### **Step 2: Add Hook Points in OrderForm.tsx** - -```typescript -// admin-spa/src/routes/Orders/OrderForm.tsx -import { applyFilters, doAction } from '@/lib/hooks'; - -export function OrderForm() { - const [formData, setFormData] = useState(initialData); - - // Hook: Allow addons to modify form data - const processedFormData = applyFilters('woonoow_order_form_data', formData); - - // Hook: Allow addons to add validation - const validateForm = () => { - let errors = {}; - - // Core validation - if (!formData.customer_id) { - errors.customer_id = 'Customer is required'; - } - - // Hook: Let addons add their validation - errors = applyFilters('woonoow_order_form_validation', errors, formData); - - return errors; - }; - - return ( -
- {/* Customer Section */} - - - {/* Billing Address */} - - - {/* Hook: Allow addons to inject fields after billing */} - {applyFilters('woonoow_order_form_after_billing', null, formData, setFormData)} - - {/* Shipping Address */} - - - {/* Hook: Allow addons to inject fields after shipping */} - {applyFilters('woonoow_order_form_after_shipping', null, formData, setFormData)} - - {/* Shipping Method Selection */} - - {/* Core shipping method selector */} - + + + ); +} +``` + +--- + +## WooNooW Hook Integration + +```typescript +// admin-spa/src/index.ts + +import { addonLoader, addFilter } from '@woonoow/hooks'; + +addonLoader.register({ + id: 'indonesia-shipping', + name: 'Indonesia Shipping', + version: '1.0.0', + init: () => { + // Add subdistrict selector in order form + addFilter('woonoow_order_form_after_shipping', (content, formData, setFormData) => { + return ( + <> + {content} + setFormData({ + ...formData, + shipping: { ...formData.shipping, subdistrict_id: id } + })} + /> + + ); + }); + } +}); +``` + +--- + +## Implementation Timeline + +**Week 1: Backend** +- Day 1-2: Database schema + address data import +- Day 3-4: WooCommerce shipping method class +- Day 5: Biteship API integration + +**Week 2: Frontend** +- Day 1-2: REST API endpoints +- Day 3-4: React components +- Day 5: Hook integration + testing + +**Week 3: Polish** +- Day 1-2: Error handling + loading states +- Day 3: Rate caching +- Day 4-5: Documentation + testing + +--- + +**Status:** Specification Complete - Ready for Implementation diff --git a/admin-spa/src/App.tsx b/admin-spa/src/App.tsx index 87ffce7..dbc175e 100644 --- a/admin-spa/src/App.tsx +++ b/admin-spa/src/App.tsx @@ -196,6 +196,7 @@ import SettingsIndex from '@/routes/Settings'; import SettingsStore from '@/routes/Settings/Store'; import SettingsPayments from '@/routes/Settings/Payments'; import SettingsShipping from '@/routes/Settings/Shipping'; +import SettingsTax from '@/routes/Settings/Tax'; import MorePage from '@/routes/More'; // Addon Route Component - Dynamically loads addon components @@ -426,7 +427,7 @@ function AppRoutes() { } /> } /> } /> - } /> + } /> } /> } /> } /> diff --git a/admin-spa/src/routes/Settings/Tax.tsx b/admin-spa/src/routes/Settings/Tax.tsx new file mode 100644 index 0000000..5bc5581 --- /dev/null +++ b/admin-spa/src/routes/Settings/Tax.tsx @@ -0,0 +1,309 @@ +import React from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { api } from '@/lib/api'; +import { SettingsLayout } from './components/SettingsLayout'; +import { SettingsCard } from './components/SettingsCard'; +import { ToggleField } from './components/ToggleField'; +import { Button } from '@/components/ui/button'; +import { ExternalLink, RefreshCw } from 'lucide-react'; +import { toast } from 'sonner'; +import { __ } from '@/lib/i18n'; + +export default function TaxSettings() { + const queryClient = useQueryClient(); + const wcAdminUrl = (window as any).WNW_CONFIG?.wpAdminUrl || '/wp-admin'; + + // Fetch tax settings + const { data: settings, isLoading, refetch } = useQuery({ + queryKey: ['tax-settings'], + queryFn: () => api.get('/settings/tax'), + }); + + // Toggle tax calculation + const toggleMutation = useMutation({ + mutationFn: async (enabled: boolean) => { + return api.post('/settings/tax/toggle', { enabled }); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['tax-settings'] }); + toast.success(__('Tax settings updated')); + }, + onError: (error: any) => { + toast.error(error?.message || __('Failed to update tax settings')); + }, + }); + + if (isLoading) { + return ( + +
+ +
+
+ ); + } + + return ( + refetch()} + > + + {__('Refresh')} + + } + > +
+ {/* Enable Tax Calculation */} + + toggleMutation.mutate(checked)} + disabled={toggleMutation.isPending} + /> + + + {/* Tax Rates */} + {settings?.calc_taxes === 'yes' && ( + +
+
+
+
+

{__('Standard Rates')}

+

+ {__('Tax rates applied to standard products')} +

+ {settings?.standard_rates && settings.standard_rates.length > 0 ? ( +
+ {settings.standard_rates.map((rate: any, index: number) => ( +
+ {rate.country} {rate.state && `- ${rate.state}`} + {rate.rate}% +
+ ))} +
+ ) : ( +

+ {__('No standard rates configured')} +

+ )} +
+ +
+
+ +
+
+
+

{__('Reduced Rates')}

+

+ {__('Lower tax rates for specific products')} +

+ {settings?.reduced_rates && settings.reduced_rates.length > 0 ? ( +
+ {settings.reduced_rates.map((rate: any, index: number) => ( +
+ {rate.country} {rate.state && `- ${rate.state}`} + {rate.rate}% +
+ ))} +
+ ) : ( +

+ {__('No reduced rates configured')} +

+ )} +
+ +
+
+ +
+
+
+

{__('Zero Rates')}

+

+ {__('No tax for specific products or locations')} +

+ {settings?.zero_rates && settings.zero_rates.length > 0 ? ( +
+ {settings.zero_rates.map((rate: any, index: number) => ( +
+ {rate.country} {rate.state && `- ${rate.state}`} + 0% +
+ ))} +
+ ) : ( +

+ {__('No zero rates configured')} +

+ )} +
+ +
+
+
+
+ )} + + {/* Tax Options */} + {settings?.calc_taxes === 'yes' && ( + +
+
+
+

{__('Prices entered with tax')}

+

+ {settings?.prices_include_tax === 'yes' + ? __('Product prices include tax') + : __('Product prices exclude tax')} +

+
+ +
+ +
+
+

{__('Calculate tax based on')}

+

+ {settings?.tax_based_on === 'shipping' && __('Customer shipping address')} + {settings?.tax_based_on === 'billing' && __('Customer billing address')} + {settings?.tax_based_on === 'base' && __('Shop base address')} +

+
+ +
+ +
+
+

{__('Display prices in shop')}

+

+ {settings?.tax_display_shop === 'incl' && __('Including tax')} + {settings?.tax_display_shop === 'excl' && __('Excluding tax')} +

+
+ +
+
+
+ )} + + {/* Advanced Settings Link */} +
+

+ {__('For advanced tax configuration, use the WooCommerce settings page')} +

+ +
+
+
+ ); +} diff --git a/includes/Api/Routes.php b/includes/Api/Routes.php index 737fa40..defa7c7 100644 --- a/includes/Api/Routes.php +++ b/includes/Api/Routes.php @@ -10,6 +10,7 @@ use WooNooW\Api\AuthController; use WooNooW\API\PaymentsController; use WooNooW\API\StoreController; use WooNooW\Api\ShippingController; +use WooNooW\Api\TaxController; class Routes { public static function init() { @@ -54,6 +55,10 @@ class Routes { // Shipping controller $shipping_controller = new ShippingController(); $shipping_controller->register_routes(); + + // Tax controller + $tax_controller = new TaxController(); + $tax_controller->register_routes(); }); } } diff --git a/includes/Api/TaxController.php b/includes/Api/TaxController.php new file mode 100644 index 0000000..36029a0 --- /dev/null +++ b/includes/Api/TaxController.php @@ -0,0 +1,156 @@ + 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', + ), + ), + ) + ); + } + + /** + * 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; + } +}