# Forward Order Module Implementation Summary

## Overview
The Forward Order module extends the Portal module to provide specialized functionality for forward orders, bypassing stock validation and applying custom pricing.

## Key Features Implemented

### Architecture Decision: Independent Block Implementation
The ForwardOrderGrid block is completely independent from Portal's Supergrid to ensure:
- Complete isolation between Portal and Forward Order functionality
- Prevention of price/stock contamination between forms
- Easier maintenance and debugging
- Clear separation of concerns

### 1. Stock Bypass Mechanism
Forward orders completely bypass stock validation and don't affect inventory levels through 6 dedicated plugins:

- **Quote-level bypass**: `Plugin/Quote/BypassStockValidation.php`
  - Bypasses stock checks during quote operations
  - Returns unlimited quantity availability for forward orders

- **Product salability bypass**: `Plugin/Catalog/Product/BypassSalabilityCheck.php`
  - Makes all products salable for forward orders
  - Handles both `isSalable()` and `isSaleable()` methods

- **Reservation prevention**: `Plugin/InventorySales/PreventForwardOrderReservation.php`
  - Prevents inventory reservation creation for forward orders
  - Checks `myoba_type='SF'` on orders

- **Legacy stock prevention**: `Plugin/CatalogInventory/Observer/PreventStockDeduction.php`
  - Prevents the SubtractQuoteInventoryObserver from reducing stock

- **Product pricing**: `Plugin/Catalog/Product/ForwardOrderPricing.php`
  - Applies forward order prices in context

- **Session cleanup**: `Plugin/Session/ClearForwardOrderFlag.php`
  - Deprecated - session flags replaced by route-based detection

- **Myoba Price Type Override**: `Plugin/Myoba/PriceTypePlugin.php`
  - Intercepts `getMyobPrice()` to use 'F' prices for forward orders
  - **Detection**: Checks `Generic` session flag `getIsForwardOrder()` (set by Submit controller)
  - **Fallback**: Route-based detection via `Context::isForwardOrderRoute()`
  - **Throws exception** if F price not found for forward order item

### 2. Context Detection
Forward orders use route-based context detection:

- **Helper**: `Helper/Context.php`
  - Route-based detection via `isForwardOrderRoute()`
  - Quote-based detection for checkout scenarios
  - Replaces deprecated session-based flags

### 3. Order Identification
Forward orders are identified using the existing `myoba_type` column:

- **Value**: `'SF'` (Sales Forward)
- **Set at**: Quote creation and order placement
- **Field conversion**: Configured in `etc/fieldset.xml` for quote-to-order transfer
- **Controller enhancement**: `Controller/Order/Submit.php` ensures myoba_type is set

### 4. Custom Pricing
Forward order pricing is loaded from database:

- **Price Helper**: `Helper/PriceHelper.php`
  - Retrieves forward order prices based on customer's price class
  - Handles currency-specific pricing
  - Supports extension prices (RetailPrice, TradePrice)

- **Product Plugin**: `Plugin/Catalog/Product/ForwardOrderPricing.php`
  - Applies forward order prices to products in forward order context

- **JavaScript Handler**: `view/frontend/web/js/forward-order-price-handler.js`
  - Handles tax-aware pricing display
  - Shows 4 prices: NEW RETAIL, NEW TRADE, FWD ORDER (ex/incl tax)

### 5. Configuration Independence
Module no longer falls back to Portal settings:

- Added `getPrepayDiscount()` method with forward order specific logic
- Added `getDropshipSurcharge()` method with forward order specific logic
- Removed dependencies on Portal configuration values

### 6. Sales Rep Access (Story 1.3.2)
Enables sales representatives to place forward orders on behalf of their assigned dealers.

### 7. Performance Optimization (2025-12-16)
Block caching with proper cache key isolation:
- `_construct()`: cache_lifetime 86400 (24h), cache_tags: FORWARD_ORDER_PRODUCTS
- `getCacheKeyInfo()`: _cacheKey, _categoryId, _forwardOrderStoreId, _currencyCode, _taxExempt

Key optimizations:
- Batch tax rate calculations (N+1 fix)
- O(1) attribute label lookups via optionLookup map
- Color caching to prevent duplicate attribute lookups
- UPPER() removal from SKU JOIN for index usage

**Key Components:**

- **Controller**: `Controller/SalesRep/DealerOrder.php`
  - AJAX endpoint to load forward order form for selected dealer
  - Validates dealer eligibility (price class + ordering period)
  - Applies store emulation for dealer's store and currency

- **Block**: `Block/SalesRep/AccountChooser.php`
  - Renders dealer selection dropdown
  - Filters dealers by rep level (Default, Super, Regional, Country)
  - Shows only dealers eligible for forward orders

- **Template**: `view/frontend/templates/salesrep/account-chooser.phtml`
  - Dealer selection interface with Select2 dropdown
  - AJAX form loading on dealer selection
  - Loading indicators and error handling

- **Layout**: `view/frontend/layout/forward_order_order_index_GROUP_rep.xml`
  - Special layout for sales rep customer groups
  - Adds dealer chooser before forward order grid

**Session Management:**
- **Problem**: Portal's `getCurrentDealerId()` defaults to logged-in user's ID
- **Solution**: Modified `Controller/Order/Index.php::handleSalesRepAccess()`
  ```php
  // Clear sales rep's own ID from session
  if ($dealerIdInSession == $salesRepId) {
      $this->_session->unsCurrentCustomerId();
      $dealerIdInSession = null;
  }
  ```
- **Result**: No premature validation errors on initial page load

**Navigation Plugin:**
- **File**: `Plugin/Portal/Block/Account/AddForwardOrderNavigation.php`
- **Behavior**:
  - Sales reps: Always show "Create Forward Order" link
  - Dealers: Show only if eligible (price class + ordering period)
- **Implementation**: Uses `PortalHelper::isSalesRep()` to differentiate

**JavaScript AJAX Architecture:**

**Dealer Loader Module** (`forward-order-dealer-loader.js`):
- Handles dealer selection change events
- Performs AJAX request to load dealer-specific form
- **Critical**: Reinitializes all JavaScript modules after AJAX DOM injection
- Pattern based on Portal's `dealer-loader.js`

**Module Reinitialization Pattern:**
```javascript
reinitializeModules: function() {
    // After AJAX loads new DOM, reinit all handlers
    uiHandler.init();           // Category collapse/expand
    filterHandler.init();       // Product filtering
    formHandler.init();         // Form validation
    priceHandler.init();        // Price calculations
    stockHandler.init();        // Stock display
    exportHandler.init();       // CSV export
    importHandler.init();       // CSV import
}
```

**Window Variable Timing Issue:**
- **Problem**: Module-level variables assigned at load time before AJAX
  ```javascript
  // BROKEN for sales reps (AJAX flow):
  var dealerPriceList = window.forwardPriceList; // undefined at load
  ```
- **Solution**: Read window variables in `init()` method
  ```javascript
  // WORKS for both scenarios:
  var dealerPriceList; // declare only
  init: function() {
      dealerPriceList = window.forwardPriceList; // read on each init
  }
  ```
- **Why**: Sales rep AJAX flow: page loads → module loads → AJAX loads form → window vars exist → init() called

**Two User Scenarios:**
1. **Regular Dealer**: Page loads with form → window vars exist → module loads → works ✓
2. **Sales Rep**: Page loads without form → module loads → AJAX loads form → window vars populated → init() called → works ✓

## Technical Architecture

### Plugin System
The module uses Magento 2's plugin (interceptor) pattern to modify core behavior:

```xml
<!-- di.xml configuration -->
<type name="Magento\InventorySalesApi\Model\PlaceReservationsForSalesEventInterface">
    <plugin name="zhik_forward_order_prevent_reservation" />
</type>

<type name="Magento\CatalogInventory\Api\StockStateInterface">
    <plugin name="zhik_forward_order_bypass_stock_validation" />
</type>

<type name="Magento\Catalog\Model\Product">
    <plugin name="zhik_forward_order_bypass_salability" />
    <plugin name="zhik_forward_order_product_pricing" />
</type>
```

### Field Mapping
Quote-to-order field conversion is configured via fieldset.xml:

```xml
<fieldset id="sales_convert_quote">
    <field name="myoba_type">
        <aspect name="to_order"/>
    </field>
</fieldset>
```

### JavaScript Module Mapping
RequireJS configuration overrides Portal's price handler:

```javascript
requirejs.config({
    map: {
        '*': {
            'Zhik_Portal/js/order/price-handler': 'Zhik_ForwardOrder/js/forward-order-price-handler'
        }
    }
});
```

## Testing

### Test Coverage
- Stock bypass validation (qty unlimited)
- Inventory reservation prevention
- Order identification (myoba_type='SF')
- Custom pricing application
- Quote-to-order field transfer

### Test Script
`test_forward_order_complete.php` provides comprehensive testing:
- Customer session setup
- Quote creation with FO flag
- Large quantity product addition
- Order placement
- Stock impact verification
- Reservation check

## Database Schema

### Key Fields
- `sales_order.myoba_type` - Identifies forward order sales (value: 'SF')
- `zhik_forward_order_pricing` - Stores forward order pricing data

**Note**: The `quote` table does NOT have a `myoba_type` column. Forward order context during quote operations is tracked via Generic session (`$this->generic->setIsForwardOrder(true)`) set in `Controller/Order/Submit.php`.

## Known Limitations

1. **Product SKU Requirements**: Products must exist in catalog for forward orders
2. **Price Class Dependency**: Customer must have valid myoba_price_class
3. **Currency Handling**: Prices must be configured for customer's currency

## Future Enhancements

1. **Console Commands**: Add CLI commands for price import/management
2. **Admin UI**: Enhanced admin interface for pricing management
3. **Reporting**: Forward order specific reports and analytics
4. **API Integration**: REST API endpoints for forward order operations

## Deployment Notes

After deployment, ensure:
1. Run `bin/magento setup:di:compile`
2. Clear all caches: `bin/magento cache:clean`
3. Deploy static content if needed: `bin/magento setup:static-content:deploy`
4. Verify fieldset.xml is properly loaded

## Troubleshooting

### Common Issues

1. **Stock still reducing**:
   - Check myoba_type is set on order
   - Verify plugins are enabled in di.xml
   - Clear DI compilation

2. **Prices showing as NaN**:
   - Verify pricing data exists in database
   - Check customer has valid price class
   - Ensure JavaScript files are deployed

3. **Order placement fails**:
   - Check shipping method availability
   - Verify payment method configuration
   - Review order validation rules

### Sales Rep Specific Issues (Story 1.3.2)

4. **"Selected dealer does not have access" error on initial load (sales rep)**:
   - **Cause**: Sales rep's own ID defaulting in session
   - **Check**: Verify `handleSalesRepAccess()` clears rep's own ID from session
   - **Fix**: Ensure `Controller/Order/Index.php` has session clearing logic (lines 133-145)

5. **Categories not collapsing after dealer selection**:
   - **Cause**: JavaScript modules not reinitialized after AJAX
   - **Check**: Console for "Reinitializing modules after dealer load" message
   - **Fix**: Verify `forward-order-dealer-loader.js` calls `reinitializeModules()`
   - **Debug**: Check `uiHandler.init()` is called in success callback

6. **dealerPriceList undefined error (sales rep AJAX flow)**:
   - **Symptom**: `TypeError: can't access property "hasOwnProperty", dealerPriceList is undefined`
   - **Cause**: Module-level variables assigned before window variables exist
   - **Check**: Verify `forward-order-price-handler.js` reads window vars in `init()`
   - **Pattern**: Variables should be declared at module level, assigned in `init()`
   - **Example**:
     ```javascript
     // At module level - DECLARE ONLY
     var dealerPriceList;

     // In init() method - ASSIGN HERE
     init: function() {
         dealerPriceList = window.forwardPriceList;
     }
     ```

7. **Form breaks after dealer selection**:
   - **Cause**: Module reinitialization not called or failed
   - **Check**: Browser console for JavaScript errors during AJAX
   - **Verify**: All modules exist and are properly loaded:
     - `uiHandler`
     - `filterHandler`
     - `formHandler`
     - `priceHandler`
     - `stockHandler`
     - `exportHandler`
     - `importHandler`
   - **Fix**: Ensure `forward-order-dealer-loader.js` is loaded in template

8. **Navigation link not showing for sales reps**:
   - **Cause**: Navigation plugin checking ordering period for reps
   - **Check**: Verify `AddForwardOrderNavigation::afterUpdateLinks()` detects sales reps
   - **Fix**: Plugin should call `$this->portalHelper->isSalesRep()` and skip eligibility check
   - **File**: `Plugin/Portal/Block/Account/AddForwardOrderNavigation.php` (lines 80-93)

9. **Dealer dropdown shows all dealers (ignoring eligibility)**:
   - **Cause**: AccountChooser block not filtering by price class or ordering period
   - **Check**: Verify `getEligibleDealers()` applies both filters
   - **Debug**: Check system.log for dealer eligibility filtering messages
   - **Fix**: Ensure both `isPriceClassAllowed()` and `canAccessForwardOrders()` are called

10. **Window variables undefined for regular dealers**:
    - **Symptom**: Works for sales reps but not direct dealer access
    - **Cause**: Different initialization order between scenarios
    - **Fix**: Reading window vars in `init()` should work for BOTH scenarios
    - **Test**: Verify as regular dealer and as sales rep selecting dealer

11. **Forward orders charged P prices instead of F prices** (Fixed 2026-02-17):
    - **Symptom**: Order items have Portal trade prices (P/C/N) instead of F prices
    - **Root cause**: `quote` table has NO `myoba_type` column - in-memory value lost on reload
    - **Solution**: `PriceTypePlugin` now uses Generic session flag instead of quote data
    - **Check**: Verify `PriceTypePlugin::isForwardOrderContext()` returns true
    - **Debug**: Check `var/log/debug.log` for "Using F prices for forward order"
    - **File**: `Plugin/Myoba/PriceTypePlugin.php`