docs: Shipping field hooks + Tax selling locations strategy
## ✅ Issue #1: Shipping Fields - Addon Responsibility Created SHIPPING_FIELD_HOOKS.md documenting: **The Right Approach:** - ❌ NO hardcoding (if country === ID → show subdistrict) - ✅ YES listen to WooCommerce hooks - ✅ Addons declare their own field requirements **How It Works:** 1. Addon adds field via `woocommerce_checkout_fields` filter 2. WooNooW fetches fields via API: `GET /checkout/fields` 3. Frontend renders fields dynamically 4. Validation based on `required` flag **Benefits:** - Addon responsibility (not WooNooW) - No hardcoding assumptions - Works with ANY addon (Indonesian, UPS, custom) - Future-proof and extensible **Example:** ```php // Indonesian Shipping Addon add_filter('woocommerce_checkout_fields', function($fields) { $fields['shipping']['shipping_subdistrict'] = [ 'required' => true, // ... ]; return $fields; }); ``` WooNooW automatically renders it! ## ✅ Issue #2: Tax - Grab Selling Locations Updated TAX_SETTINGS_DESIGN.md: **Your Brilliant Idea:** - Read WooCommerce "Selling location(s)" setting - Show predefined tax rates for those countries - No re-selecting! **Scenarios:** 1. **Specific countries** (ID, MY) → Show both rates 2. **All countries** → Show store country + add button 3. **Continent** (Asia) → Suggest all Asian country rates **Smart Detection:** ```php $selling_locations = get_option('woocommerce_allowed_countries'); if ($selling_locations === 'specific') { $countries = get_option('woocommerce_specific_allowed_countries'); // Show predefined rates for these countries } ``` **Benefits:** - Zero re-selection (data already in WooCommerce) - Smart suggestions based on user's actual selling regions - Scales for single/multi-country/continent - Combines your idea + my proposal perfectly! ## Next: Implementation Plan Ready
This commit is contained in:
283
SHIPPING_FIELD_HOOKS.md
Normal file
283
SHIPPING_FIELD_HOOKS.md
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
# Shipping Address Fields - Dynamic via Hooks
|
||||||
|
|
||||||
|
## Philosophy: Addon Responsibility, Not Hardcoding
|
||||||
|
|
||||||
|
WooNooW should **listen to WooCommerce hooks** to determine which fields are required, not hardcode assumptions about Indonesian vs International shipping.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Problem with Hardcoding
|
||||||
|
|
||||||
|
**Bad Approach (What we almost did):**
|
||||||
|
```javascript
|
||||||
|
// ❌ DON'T DO THIS
|
||||||
|
if (country === 'ID') {
|
||||||
|
showSubdistrict = true; // Hardcoded assumption
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why it's bad:**
|
||||||
|
- Assumes all Indonesian shipping needs subdistrict
|
||||||
|
- Breaks if addon changes requirements
|
||||||
|
- Not extensible for other countries
|
||||||
|
- Violates separation of concerns
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Right Approach: Listen to Hooks
|
||||||
|
|
||||||
|
**WooCommerce Core Hooks:**
|
||||||
|
|
||||||
|
### 1. `woocommerce_checkout_fields` Filter
|
||||||
|
Addons use this to add/modify/remove fields:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Example: Indonesian Shipping Addon
|
||||||
|
add_filter('woocommerce_checkout_fields', function($fields) {
|
||||||
|
// Add subdistrict field
|
||||||
|
$fields['shipping']['shipping_subdistrict'] = [
|
||||||
|
'label' => __('Subdistrict'),
|
||||||
|
'required' => true,
|
||||||
|
'class' => ['form-row-wide'],
|
||||||
|
'priority' => 65,
|
||||||
|
];
|
||||||
|
|
||||||
|
return $fields;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. `woocommerce_default_address_fields` Filter
|
||||||
|
Modifies default address fields:
|
||||||
|
|
||||||
|
```php
|
||||||
|
add_filter('woocommerce_default_address_fields', function($fields) {
|
||||||
|
// Make postal code required for UPS
|
||||||
|
$fields['postcode']['required'] = true;
|
||||||
|
|
||||||
|
return $fields;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Field Validation Hooks
|
||||||
|
```php
|
||||||
|
add_action('woocommerce_checkout_process', function() {
|
||||||
|
if (empty($_POST['shipping_subdistrict'])) {
|
||||||
|
wc_add_notice(__('Subdistrict is required'), 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation in WooNooW
|
||||||
|
|
||||||
|
### Backend: Expose Checkout Fields via API
|
||||||
|
|
||||||
|
**New Endpoint:** `GET /checkout/fields`
|
||||||
|
|
||||||
|
```php
|
||||||
|
// includes/Api/CheckoutController.php
|
||||||
|
|
||||||
|
public function get_checkout_fields(WP_REST_Request $request) {
|
||||||
|
// Get fields with all filters applied
|
||||||
|
$fields = WC()->checkout()->get_checkout_fields();
|
||||||
|
|
||||||
|
// Format for frontend
|
||||||
|
$formatted = [];
|
||||||
|
|
||||||
|
foreach ($fields as $fieldset_key => $fieldset) {
|
||||||
|
foreach ($fieldset as $key => $field) {
|
||||||
|
$formatted[] = [
|
||||||
|
'key' => $key,
|
||||||
|
'fieldset' => $fieldset_key, // billing, shipping, account, order
|
||||||
|
'type' => $field['type'] ?? 'text',
|
||||||
|
'label' => $field['label'] ?? '',
|
||||||
|
'placeholder' => $field['placeholder'] ?? '',
|
||||||
|
'required' => $field['required'] ?? false,
|
||||||
|
'class' => $field['class'] ?? [],
|
||||||
|
'priority' => $field['priority'] ?? 10,
|
||||||
|
'options' => $field['options'] ?? null, // For select fields
|
||||||
|
'custom' => $field['custom'] ?? false, // Custom field flag
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by priority
|
||||||
|
usort($formatted, function($a, $b) {
|
||||||
|
return $a['priority'] <=> $b['priority'];
|
||||||
|
});
|
||||||
|
|
||||||
|
return new WP_REST_Response($formatted, 200);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend: Dynamic Field Rendering
|
||||||
|
|
||||||
|
**Create Order - Address Section:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Fetch checkout fields from API
|
||||||
|
const { data: checkoutFields = [] } = useQuery({
|
||||||
|
queryKey: ['checkout-fields'],
|
||||||
|
queryFn: () => api.get('/checkout/fields'),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter shipping fields
|
||||||
|
const shippingFields = checkoutFields.filter(
|
||||||
|
field => field.fieldset === 'shipping'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Render dynamically
|
||||||
|
{shippingFields.map(field => {
|
||||||
|
// Standard WooCommerce fields
|
||||||
|
if (['first_name', 'last_name', 'address_1', 'address_2', 'city', 'state', 'postcode', 'country'].includes(field.key)) {
|
||||||
|
return <StandardField key={field.key} field={field} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom fields (e.g., subdistrict from addon)
|
||||||
|
if (field.custom) {
|
||||||
|
return <CustomField key={field.key} field={field} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Field Components:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function StandardField({ field }) {
|
||||||
|
return (
|
||||||
|
<div className={cn('form-field', field.class)}>
|
||||||
|
<label>
|
||||||
|
{field.label}
|
||||||
|
{field.required && <span className="required">*</span>}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type={field.type}
|
||||||
|
name={field.key}
|
||||||
|
placeholder={field.placeholder}
|
||||||
|
required={field.required}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CustomField({ field }) {
|
||||||
|
// Handle custom field types (select, textarea, etc.)
|
||||||
|
if (field.type === 'select') {
|
||||||
|
return (
|
||||||
|
<div className={cn('form-field', field.class)}>
|
||||||
|
<label>
|
||||||
|
{field.label}
|
||||||
|
{field.required && <span className="required">*</span>}
|
||||||
|
</label>
|
||||||
|
<select name={field.key} required={field.required}>
|
||||||
|
{field.options?.map(option => (
|
||||||
|
<option key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <StandardField field={field} />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How Addons Work
|
||||||
|
|
||||||
|
### Example: Indonesian Shipping Addon
|
||||||
|
|
||||||
|
**Addon adds subdistrict field:**
|
||||||
|
```php
|
||||||
|
add_filter('woocommerce_checkout_fields', function($fields) {
|
||||||
|
$fields['shipping']['shipping_subdistrict'] = [
|
||||||
|
'type' => 'select',
|
||||||
|
'label' => __('Subdistrict'),
|
||||||
|
'required' => true,
|
||||||
|
'class' => ['form-row-wide'],
|
||||||
|
'priority' => 65,
|
||||||
|
'options' => get_subdistricts(), // Addon provides this
|
||||||
|
'custom' => true, // Flag as custom field
|
||||||
|
];
|
||||||
|
|
||||||
|
return $fields;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**WooNooW automatically:**
|
||||||
|
1. Fetches fields via API
|
||||||
|
2. Sees `shipping_subdistrict` with `required: true`
|
||||||
|
3. Renders it in Create Order form
|
||||||
|
4. Validates it on submit
|
||||||
|
|
||||||
|
**No hardcoding needed!**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
✅ **Addon responsibility** - Addons declare their own requirements
|
||||||
|
✅ **No hardcoding** - WooNooW just renders what WooCommerce says
|
||||||
|
✅ **Extensible** - Works with ANY addon (Indonesian, UPS, custom)
|
||||||
|
✅ **Future-proof** - New addons work automatically
|
||||||
|
✅ **Separation of concerns** - Each addon manages its own fields
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Edge Cases
|
||||||
|
|
||||||
|
### Case 1: Subdistrict for Indonesian Shipping
|
||||||
|
- Addon adds `shipping_subdistrict` field
|
||||||
|
- WooNooW renders it
|
||||||
|
- ✅ Works!
|
||||||
|
|
||||||
|
### Case 2: UPS Requires Postal Code
|
||||||
|
- UPS addon sets `postcode.required = true`
|
||||||
|
- WooNooW renders it as required
|
||||||
|
- ✅ Works!
|
||||||
|
|
||||||
|
### Case 3: Custom Shipping Needs Extra Field
|
||||||
|
- Addon adds `shipping_delivery_notes` field
|
||||||
|
- WooNooW renders it
|
||||||
|
- ✅ Works!
|
||||||
|
|
||||||
|
### Case 4: No Custom Fields
|
||||||
|
- Standard WooCommerce fields only
|
||||||
|
- WooNooW renders them
|
||||||
|
- ✅ Works!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
1. **Backend:**
|
||||||
|
- Create `GET /checkout/fields` endpoint
|
||||||
|
- Return fields with all filters applied
|
||||||
|
- Include field metadata (type, required, options, etc.)
|
||||||
|
|
||||||
|
2. **Frontend:**
|
||||||
|
- Fetch checkout fields on Create Order page
|
||||||
|
- Render fields dynamically based on API response
|
||||||
|
- Handle standard + custom field types
|
||||||
|
- Validate based on `required` flag
|
||||||
|
|
||||||
|
3. **Testing:**
|
||||||
|
- Test with no addons (standard fields only)
|
||||||
|
- Test with Indonesian shipping addon (subdistrict)
|
||||||
|
- Test with UPS addon (postal code required)
|
||||||
|
- Test with custom addon (custom fields)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. Create `CheckoutController.php` with `get_checkout_fields` endpoint
|
||||||
|
2. Update Create Order to fetch and render fields dynamically
|
||||||
|
3. Test with Indonesian shipping addon
|
||||||
|
4. Document for addon developers
|
||||||
@@ -48,9 +48,42 @@ Tax Settings Page
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Predefined Tax Rates by Country
|
## Predefined Tax Rates - Smart Detection
|
||||||
|
|
||||||
When user's store country is set, we show the standard rate:
|
**Source:** WooCommerce General Settings → "Selling location(s)"
|
||||||
|
|
||||||
|
### Scenario 1: Sell to Specific Countries
|
||||||
|
If user selected specific countries (e.g., Indonesia, Malaysia):
|
||||||
|
```
|
||||||
|
Predefined Tax Rates
|
||||||
|
├── 🇮🇩 Indonesia: 11% (PPN)
|
||||||
|
└── 🇲🇾 Malaysia: 6% (SST)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scenario 2: Sell to All Countries
|
||||||
|
If user selected "Sell to all countries":
|
||||||
|
```
|
||||||
|
Predefined Tax Rates
|
||||||
|
└── Based on store country:
|
||||||
|
🇮🇩 Indonesia: 11% (PPN)
|
||||||
|
|
||||||
|
Additional Tax Rates
|
||||||
|
└── [+ Add Tax Rate] → Shows all countries
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scenario 3: Sell to Specific Continents
|
||||||
|
If user selected "Asia":
|
||||||
|
```
|
||||||
|
Suggested Tax Rates (Asia)
|
||||||
|
├── 🇮🇩 Indonesia: 11%
|
||||||
|
├── 🇲🇾 Malaysia: 6%
|
||||||
|
├── 🇸🇬 Singapore: 9%
|
||||||
|
├── 🇹🇭 Thailand: 7%
|
||||||
|
├── 🇵🇭 Philippines: 12%
|
||||||
|
└── 🇻🇳 Vietnam: 10%
|
||||||
|
```
|
||||||
|
|
||||||
|
### Standard Tax Rates by Country
|
||||||
|
|
||||||
| Country | Standard Rate | Note |
|
| Country | Standard Rate | Note |
|
||||||
|---------|---------------|------|
|
|---------|---------------|------|
|
||||||
@@ -124,7 +157,23 @@ Add Tax Rate
|
|||||||
- `POST /settings/tax/rates` - Create tax rate
|
- `POST /settings/tax/rates` - Create tax rate
|
||||||
- `PUT /settings/tax/rates/{id}` - Update tax rate
|
- `PUT /settings/tax/rates/{id}` - Update tax rate
|
||||||
- `DELETE /settings/tax/rates/{id}` - Delete tax rate
|
- `DELETE /settings/tax/rates/{id}` - Delete tax rate
|
||||||
- `GET /settings/tax/predefined` - Get predefined rates by country
|
- `GET /settings/tax/suggested` - Get suggested rates based on selling locations
|
||||||
|
|
||||||
|
### Get Selling Locations:
|
||||||
|
```php
|
||||||
|
// Get WooCommerce selling locations setting
|
||||||
|
$selling_locations = get_option('woocommerce_allowed_countries');
|
||||||
|
// Options: 'all', 'all_except', 'specific'
|
||||||
|
|
||||||
|
if ($selling_locations === 'specific') {
|
||||||
|
$countries = get_option('woocommerce_specific_allowed_countries');
|
||||||
|
// Returns array: ['ID', 'MY', 'SG']
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get store base country
|
||||||
|
$store_country = get_option('woocommerce_default_country');
|
||||||
|
// Returns: 'ID:JB' (country:state) or 'ID'
|
||||||
|
```
|
||||||
|
|
||||||
### Predefined Rates Data:
|
### Predefined Rates Data:
|
||||||
```json
|
```json
|
||||||
|
|||||||
Reference in New Issue
Block a user