diff --git a/API_ROUTES.md b/API_ROUTES.md new file mode 100644 index 0000000..816176a --- /dev/null +++ b/API_ROUTES.md @@ -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. diff --git a/PROJECT_SOP.md b/PROJECT_SOP.md index 95507a9..338a17f 100644 --- a/PROJECT_SOP.md +++ b/PROJECT_SOP.md @@ -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 diff --git a/includes/Api/OrdersController.php b/includes/Api/OrdersController.php index 97d3765..b7dd208 100644 --- a/includes/Api/OrdersController.php +++ b/includes/Api/OrdersController.php @@ -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 = []; + + foreach ( $parent_attributes as $parent_attr ) { + if ( ! $parent_attr->get_variation() ) { + continue; // Skip non-variation attributes + } - $attributes[ $clean_name ] = $attr_value; + $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,