Files
WooNooW/SHIPPING_FIELD_HOOKS.md
dwindown c1f09041ef 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
2025-11-10 11:40:49 +07:00

7.2 KiB

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):

// ❌ 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:

// 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:

add_filter('woocommerce_default_address_fields', function($fields) {
    // Make postal code required for UPS
    $fields['postcode']['required'] = true;
    
    return $fields;
});

3. Field Validation Hooks

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

// 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:

// 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:

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:

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