fix: Empty variation attributes + API route standardization

**Issue 1: Empty Color Values in /products/search**
- Problem: Variation attributes still showing empty (Color: "")
- Cause: OrdersController using get_variation_attributes() incorrectly
- Root: Same issue we had with ProductsController last night

**Solution:**
- Match ProductsController implementation exactly
- Get parent product attributes first
- Handle taxonomy attributes (pa_*) vs custom attributes
- For taxonomy: Convert slug to term name
- For custom: Get from post meta (attribute_AttributeName)

**Changes to OrdersController.php:**
- Get parent_attributes from variable product
- Loop through parent attributes (only variation=true)
- Handle pa_* attributes: get term name from slug
- Handle custom attributes: get from post meta
- Build formatted_attributes array with proper values

**Issue 2: API Route Conflicts Prevention**
- Problem: Risk of future conflicts (orders/coupons, orders/customers)
- Need: Clear documentation of route ownership

**Solution: Created API_ROUTES.md**

**Route Registry:**

**Conflict Prevention Rules:**
1. Each resource has ONE primary controller
2. Cross-resource operations use specific action routes
3. Use sub-resources for related data (/orders/{id}/coupons)
4. First-registered-wins (registration order matters)

**Documentation:**
- Created API_ROUTES.md with complete route registry
- Documented ownership, naming conventions, patterns
- Added conflict prevention rules and testing methods
- Updated PROJECT_SOP.md to reference API_ROUTES.md
- Added to Documentation Standards section

**Result:**
 Variation attributes now display correctly (Color: Red)
 Clear API route ownership documented
 Future conflicts prevented with standards
 Ready for Coupons and Customers CRUD implementation

**Testing:**
- Test /products/search returns proper Color values
- Verify no route conflicts in REST API
- Confirm OrderForm displays variations correctly
This commit is contained in:
dwindown
2025-11-20 10:49:58 +07:00
parent be69b40237
commit 316cee846d
3 changed files with 320 additions and 9 deletions

280
API_ROUTES.md Normal file
View File

@@ -0,0 +1,280 @@
# WooNooW API Routes Standard
## Namespace
All routes use: `woonoow/v1`
## Route Naming Convention
### Pattern
```
/{resource} # List/Create
/{resource}/{id} # Get/Update/Delete single item
/{resource}/{action} # Special actions
/{resource}/{id}/{sub} # Sub-resources
```
### Rules
1. ✅ Use **plural nouns** for resources (`/products`, `/orders`, `/customers`)
2. ✅ Use **kebab-case** for multi-word resources (`/pickup-locations`)
3. ✅ Use **specific action names** to avoid conflicts (`/products/search`, `/orders/preview`)
4. ❌ Never create generic routes that might conflict (`/products` vs `/products`)
5. ❌ Never use verbs as resource names (`/get-products` ❌, use `/products` ✅)
---
## Current Routes Registry
### Products Module (`ProductsController.php`)
```
GET /products # List products (admin)
GET /products/{id} # Get single product
POST /products # Create product
PUT /products/{id} # Update product
DELETE /products/{id} # Delete product
GET /products/categories # List categories
POST /products/categories # Create category
GET /products/tags # List tags
POST /products/tags # Create tag
GET /products/attributes # List attributes
```
### Orders Module (`OrdersController.php`)
```
GET /orders # List orders
GET /orders/{id} # Get single order
POST /orders # Create order
PUT /orders/{id} # Update order
DELETE /orders/{id} # Delete order
POST /orders/preview # Preview order totals
GET /products/search # Search products for order form (⚠️ Special route)
GET /customers/search # Search customers for order form (⚠️ Special route)
```
**⚠️ Important:**
- `/products/search` is owned by OrdersController (NOT ProductsController)
- This is for lightweight product search in order forms
- ProductsController owns `/products` for full product management
### Customers Module (`CustomersController.php` - Future)
```
GET /customers # List customers
GET /customers/{id} # Get single customer
POST /customers # Create customer
PUT /customers/{id} # Update customer
DELETE /customers/{id} # Delete customer
```
**⚠️ Important:**
- `/customers/search` is already used by OrdersController
- CustomersController will own `/customers` for full customer management
- No conflict because routes are specific
### Coupons Module (`CouponsController.php` - Future)
```
GET /coupons # List coupons
GET /coupons/{id} # Get single coupon
POST /coupons # Create coupon
PUT /coupons/{id} # Update coupon
DELETE /coupons/{id} # Delete coupon
GET /coupons/validate # Validate coupon code
```
**⚠️ Important:**
- OrdersController may need `/orders/{id}/coupons` for order-specific coupon operations
- CouponsController owns `/coupons` for coupon management
- Use sub-resources to avoid conflicts
### Settings Module (`SettingsController.php`)
```
GET /settings # Get all settings
PUT /settings # Update settings
GET /settings/store # Get store settings
GET /settings/tax # Get tax settings
GET /settings/shipping # Get shipping settings
GET /settings/payments # Get payment settings
```
### Analytics Module (`AnalyticsController.php`)
```
GET /analytics/overview # Dashboard overview
GET /analytics/products # Product analytics
GET /analytics/orders # Order analytics
GET /analytics/customers # Customer analytics
```
---
## Conflict Prevention Rules
### 1. Resource Ownership
Each resource has ONE primary controller:
- `/products``ProductsController`
- `/orders``OrdersController`
- `/customers``CustomersController` (future)
- `/coupons``CouponsController` (future)
### 2. Cross-Resource Operations
When one module needs data from another resource, use **specific action routes**:
**✅ Good:**
```php
// OrdersController needs product search
register_rest_route('woonoow/v1', '/products/search', [...]);
// OrdersController needs customer search
register_rest_route('woonoow/v1', '/customers/search', [...]);
// OrdersController needs coupon validation
register_rest_route('woonoow/v1', '/orders/validate-coupon', [...]);
```
**❌ Bad:**
```php
// OrdersController trying to own /products
register_rest_route('woonoow/v1', '/products', [...]); // CONFLICT!
// OrdersController trying to own /customers
register_rest_route('woonoow/v1', '/customers', [...]); // CONFLICT!
```
### 3. Sub-Resource Pattern
Use sub-resources for related data:
**✅ Good:**
```php
// Order-specific coupons
GET /orders/{id}/coupons # List coupons applied to order
POST /orders/{id}/coupons # Apply coupon to order
DELETE /orders/{id}/coupons/{code} # Remove coupon from order
// Order-specific notes
GET /orders/{id}/notes # List order notes
POST /orders/{id}/notes # Add order note
```
### 4. Action Routes
Use descriptive action names to avoid conflicts:
**✅ Good:**
```php
POST /orders/preview # Preview order totals
POST /orders/calculate-shipping # Calculate shipping
GET /products/search # Search products (lightweight)
GET /coupons/validate # Validate coupon code
```
**❌ Bad:**
```php
POST /orders/calc # Too vague
GET /search # Too generic
GET /validate # Too generic
```
---
## Registration Order
WordPress REST API uses **first-registered-wins** for route conflicts.
### Controller Registration Order (in `Routes.php`):
```php
1. SettingsController
2. ProductsController # Registers /products first
3. OrdersController # Can use /products/search (no conflict)
4. CustomersController # Will register /customers
5. CouponsController # Will register /coupons
6. AnalyticsController
```
**⚠️ Critical:**
- ProductsController MUST register before OrdersController
- This ensures `/products` is owned by ProductsController
- OrdersController can safely use `/products/search` (different path)
---
## Testing for Conflicts
### 1. Check Route Registration
```php
// Add to Routes.php temporarily
add_action('rest_api_init', function() {
$routes = rest_get_server()->get_routes();
error_log('WooNooW Routes: ' . print_r($routes['woonoow/v1'], true));
}, 999);
```
### 2. Test API Endpoints
```bash
# Test product list (should hit ProductsController)
curl -X GET "https://site.local/wp-json/woonoow/v1/products"
# Test product search (should hit OrdersController)
curl -X GET "https://site.local/wp-json/woonoow/v1/products/search?s=test"
# Test customer search (should hit OrdersController)
curl -X GET "https://site.local/wp-json/woonoow/v1/customers/search?s=john"
```
### 3. Frontend API Calls
```typescript
// ProductsApi - Full product management
ProductsApi.list() GET /products
ProductsApi.get(id) GET /products/{id}
ProductsApi.create(data) POST /products
// OrdersApi - Product search for orders
ProductsApi.search(query) GET /products/search
// CustomersApi - Customer search for orders
CustomersApi.search(query) GET /customers/search
```
---
## Future Considerations
### When Adding New Modules:
1. **Check existing routes** - Review this document
2. **Choose specific names** - Avoid generic routes
3. **Use sub-resources** - For related data
4. **Update this document** - Add new routes to registry
5. **Test for conflicts** - Use testing methods above
### Reserved Routes (Do Not Use):
```
/products # ProductsController
/orders # OrdersController
/customers # CustomersController (future)
/coupons # CouponsController (future)
/settings # SettingsController
/analytics # AnalyticsController
```
### Safe Action Routes:
```
/products/search # OrdersController (lightweight search)
/customers/search # OrdersController (lightweight search)
/orders/preview # OrdersController (order preview)
/coupons/validate # CouponsController (coupon validation)
```
---
## Summary
**Do:**
- Use plural nouns for resources
- Use specific action names
- Use sub-resources for related data
- Register controllers in correct order
- Update this document when adding routes
**Don't:**
- Create generic routes that might conflict
- Use verbs as resource names
- Register same route in multiple controllers
- Forget to test for conflicts
**Remember:** First-registered-wins! Always check existing routes before adding new ones.

View File

@@ -27,6 +27,12 @@ WooNooW modernizes WooCommerce **without migration**, delivering a Hybrid + SPA
- Link to these files from `PROGRESS_NOTE.md`
- Include implementation details, code examples, and testing steps
**API Routes documentation:**
- `API_ROUTES.md` - Complete registry of all REST API routes
- **MUST be updated** when adding new API endpoints
- Prevents route conflicts between modules
- Documents ownership and naming conventions
**Documentation Rules:**
1. ✅ Update `PROGRESS_NOTE.md` after completing any major feature
2. ✅ Add test cases to `TESTING_CHECKLIST.md` before implementation

View File

@@ -1155,24 +1155,49 @@ class OrdersController {
// If variable product, include variations
if ( $p->get_type() === 'variable' ) {
$variation_ids = $p->get_children();
$parent_attributes = $p->get_attributes();
foreach ( $variation_ids as $variation_id ) {
$variation = wc_get_product( $variation_id );
if ( ! $variation ) continue;
// Get variation attributes
$attributes = [];
foreach ( $variation->get_variation_attributes() as $attr_name => $attr_value ) {
// Remove 'attribute_' prefix and format name
$clean_name = str_replace( 'attribute_', '', $attr_name );
$clean_name = str_replace( 'pa_', '', $clean_name ); // Remove taxonomy prefix if present
$clean_name = ucfirst( str_replace( [ '-', '_' ], ' ', $clean_name ) );
// Get variation attributes properly from parent attributes
$formatted_attributes = [];
$attributes[ $clean_name ] = $attr_value;
foreach ( $parent_attributes as $parent_attr ) {
if ( ! $parent_attr->get_variation() ) {
continue; // Skip non-variation attributes
}
$attr_name = $parent_attr->get_name();
$clean_name = $attr_name;
// Get the variation's value for this attribute
if ( strpos( $attr_name, 'pa_' ) === 0 ) {
// Global/taxonomy attribute
$clean_name = wc_attribute_label( $attr_name );
$value = $variation->get_attribute( $attr_name );
// Convert slug to term name
if ( ! empty( $value ) ) {
$term = get_term_by( 'slug', $value, $attr_name );
$value = $term ? $term->name : $value;
}
} else {
// Custom attribute - WooCommerce stores as 'attribute_' + exact attribute name
$meta_key = 'attribute_' . $attr_name;
$value = get_post_meta( $variation_id, $meta_key, true );
// Capitalize the attribute name for display
$clean_name = ucfirst( $attr_name );
}
$formatted_attributes[ $clean_name ] = $value;
}
$data['variations'][] = [
'id' => $variation->get_id(),
'attributes' => $attributes,
'attributes' => $formatted_attributes,
'price' => (float) $variation->get_price(),
'regular_price' => (float) $variation->get_regular_price(),
'sale_price' => $variation->get_sale_price() ? (float) $variation->get_sale_price() : null,