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:
dwindown
2025-11-10 11:40:49 +07:00
parent 8bebd3abe5
commit c1f09041ef
2 changed files with 335 additions and 3 deletions

283
SHIPPING_FIELD_HOOKS.md Normal file
View 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

View File

@@ -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