Atraxion Middleware - Implementation Plan
Problem Statement
The current middleware has four main issues: 1. Full data on updates: Odoo sends complete records, making it impossible to know what changed 2. Partner hierarchy mismatch: Odoo's complex hierarchy (Main→Billing→Delivery→Contacts) cannot be represented in the current webshop 3. Stock performance: Querying stock for 2800+ products is slow 4. Price update explosion: When a pricelist changes, Odoo marks ALL 128k products as updated, causing days of processing - even though only a fraction have actual price changes
Solution Overview
Build a new Symfony middleware that: - Stores state to compute field-level diffs when Odoo pushes updates - Preserves hierarchy for partners to support future webshop upgrade - Queues outbound sync for orders and customers going to Odoo - Provides a new API optimized for the webshop's needs
Phase 1: Core Entities & Database Schema
Entities to Create
Product Entity (src/Entity/Product.php)
- id, createdAt, updatedAt
- odooId (nullable, Odoo's internal ID)
- externalId (unique, webshop reference)
- currentData (JSON - latest data from Odoo)
- previousData (JSON - data before last update, for diff computation)
- changedFields (JSON array - list of fields that changed in last update)
- processingState, errorMessage, retryCount
- context (price_update, product_spec, etc.)
- isNew (boolean - true if first sync)
Partner Entity (src/Entity/Partner.php)
- id, createdAt, updatedAt
- odooId, externalId
- type (B=Billing, C=Contact, D=Delivery, O=Anonymous Shipment Address)
- parentId (self-referencing FK for hierarchy)
- currentData, previousData, changedFields
- processingState, errorMessage, retryCount
- isNew
SaleOrder Entity (src/Entity/SaleOrder.php)
- id, createdAt, updatedAt
- odooId, externalId
- currentData, previousData, changedFields
- processingState, errorMessage, retryCount
- direction (INBOUND from Odoo, OUTBOUND to Odoo)
- isNew
Stock Entity (src/Entity/Stock.php)
- id, createdAt, updatedAt
- supplierExternalId
- productExternalId
- quantity
- purchasePrice, suggestedSalePrice
- supplierProductId
- Index on (supplierExternalId, productExternalId)
PricelistItem Entity (src/Entity/PricelistItem.php)
- id, createdAt, updatedAt
- pricelistExternalId (e.g., "-4" for GARAGE)
- pricelistName (e.g., "GARAGE")
- productExternalId (FK-like reference to product)
- price (nullable decimal)
- startDate, endDate (nullable)
- isPromoPrice, isAction, untilAtxStockIs0 (booleans)
- previousPrice (for change detection)
- priceChanged (boolean - true if price differs from previous)
- Composite index on (pricelistExternalId, productExternalId)
PurchaseOrder Entity (src/Entity/PurchaseOrder.php)
- id, createdAt, updatedAt
- odooId, name (e.g., "PO0151232")
- vendor, state, createdBy
- currentData, previousData, changedFields
- processingState, errorMessage, retryCount
- isNew
PurchaseOrderLine Entity (src/Entity/PurchaseOrderLine.php)
- id
- purchaseOrderId (FK to PurchaseOrder)
- productExternalId
- name, quantity, qtyReceived, priceUnit
- datePlanned
- repliedByVendor (JSON for vendor response)
OutboundQueue Entity (src/Entity/OutboundQueue.php)
- id, createdAt
- entityType (partner, sale_order)
- entityId
- payload (JSON)
- status (pending, processing, completed, failed)
- attempts, lastAttemptAt, lastError
- completedAt
Files to Create/Modify
src/Entity/Product.phpsrc/Entity/Partner.phpsrc/Entity/SaleOrder.phpsrc/Entity/Stock.phpsrc/Entity/OutboundQueue.phpmigrations/Version*.php(generated)
Phase 2: Change Detection Service
Core Service: ChangeDetectionService
class ChangeDetectionService
{
public function detectChanges(array $newData, ?array $previousData): ChangeResult
{
// Returns: changedFields[], isNew, hasChanges
}
public function computeFieldDiff(array $oldData, array $newData): array
{
// Deep comparison, handles nested objects/arrays
// Returns list of field paths that changed: ['name', 'price', 'category.name']
}
}
ChangeResult DTO
changedFields: array of field pathsisNew: boolean (no previous data)hasChanges: boolean
Files to Create
src/Service/ChangeDetectionService.phpsrc/DTO/ChangeResult.phptests/Unit/Service/ChangeDetectionServiceTest.php
Phase 3: Inbound API (Odoo → Middleware)
Endpoints
POST /api/v1/products - Receive product update from Odoo - Accept product data (single or batch) - Compute changes against stored state - Store new state with change metadata - Return acknowledgment
POST /api/v1/partners - Receive partner update from Odoo - Handle hierarchy (resolve parent references) - Compute changes - Store with change metadata
POST /api/v1/sale-orders - Receive sale order update from Odoo - Store order with lines, sets, pickings - Compute changes
POST /api/v1/stocks - Receive stock update from Odoo - Bulk update stock levels - Efficient upsert (update existing, insert new)
Request/Response Format
// POST /api/v1/products (Odoo pushes)
{
"products": [
{ "id": 383069, "external_id": "-383069", "name": "...", ... }
]
}
// Response
{
"success": true,
"processed": 1,
"errors": []
}
Files to Create
src/Controller/Api/V1/ProductController.phpsrc/Controller/Api/V1/PartnerController.phpsrc/Controller/Api/V1/SaleOrderController.phpsrc/Controller/Api/V1/StockController.phpsrc/Service/ProductSyncService.phpsrc/Service/PartnerSyncService.phpsrc/Service/SaleOrderSyncService.phpsrc/Service/StockSyncService.php
Phase 4: Outbound API (Middleware → WebShop)
Endpoints
GET /api/v1/updated-products - Fetch products with changes
- Query params: since (timestamp), limit, offset
- Returns products with changedFields metadata
- Includes isNew flag for new products
GET /api/v1/updated-partners - Fetch partners with changes - Same pattern as products - Includes hierarchy info (parentExternalId)
GET /api/v1/updated-sale-orders - Fetch sale orders with changes
- Filter: direction=inbound (from Odoo)
GET /api/v1/stocks - Fetch current stock levels - Query by supplier, product, or get all - Efficient pagination
DELETE /api/v1/updated-products/{id} - Mark product update as processed POST /api/v1/delete-updated-products - Batch mark as processed
Response Format with Change Metadata
{
"payload": [
{
"id": 123,
"externalId": "-383069",
"isNew": false,
"changedFields": ["name", "purchase_price", "product_specifications[0].value"],
"data": { /* full product data */ }
}
],
"meta": {
"total": 150,
"limit": 50,
"offset": 0
}
}
Files to Create
src/Controller/Api/V1/UpdatedProductController.phpsrc/Controller/Api/V1/UpdatedPartnerController.phpsrc/Controller/Api/V1/UpdatedSaleOrderController.phpsrc/Controller/Api/V1/StockQueryController.phpsrc/DTO/UpdatedProductResponse.phpsrc/DTO/UpdatedPartnerResponse.phpsrc/DTO/UpdatedSaleOrderResponse.php
Phase 5: Outbound Queue (Middleware → Odoo)
Queue Processing
Queue new customers/orders from webshop: - WebShop POSTs to middleware - Middleware stores and queues for Odoo sync - Background worker processes queue - Retry on failure with exponential backoff
Endpoints
POST /api/v1/partners (from WebShop) - Accepts new partner data - Stores locally - Queues for Odoo sync - Returns middleware ID immediately
POST /api/v1/sale-orders (from WebShop) - Same pattern for orders
GET /api/v1/queue/status/{entityType}/{entityId} - Check sync status
Queue Worker
Symfony Messenger component for: - Processing outbound items - Retry handling (3 attempts, exponential backoff) - Dead letter handling for persistent failures - Admin notifications on backlog
Files to Create
src/Message/SyncToOdooMessage.phpsrc/MessageHandler/SyncToOdooHandler.phpsrc/Service/OdooClient.php(HTTP client for Odoo API)config/packages/messenger.yaml
Phase 6: Partner Hierarchy
Hierarchy Storage
Partners store parentId (FK to self) representing:
Main Account (type=null or M?)
└── Billing Account (type=B)
└── Delivery Address (type=D)
└── Contact/User (type=C)
Online addresses (type=O) are linked to their parent account.
Hierarchy API
GET /api/v1/partners/{id}/hierarchy - Get full tree from any node GET /api/v1/partners/{id}/children - Get immediate children GET /api/v1/partners/{id}/parent - Get parent
Resolution Service
When processing partner updates:
1. Resolve external_parent_id to internal parentId
2. Create placeholder if parent not yet synced
3. Update placeholders when actual parent arrives
Files to Create
src/Service/PartnerHierarchyService.phpsrc/Controller/Api/V1/PartnerHierarchyController.php
Phase 7: Price & Purchase Order Extraction
The Price Update Problem
When a pricelist changes in Odoo, it marks ALL 128k products as updated. The webshop currently processes all of them, taking days.
Solution: Extract Prices to Separate Entity
When products arrive with embedded prices array:
1. Extract each price item to PricelistItem table
2. Compare with previously stored price for that (pricelist, product) pair
3. Set priceChanged = true only if price actually differs
4. Product's changedFields does NOT include prices (they're separate)
API for Price Updates
GET /api/v1/updated-prices - Fetch pricelist items where priceChanged = true
- Returns: pricelistExternalId, productExternalId, price, isNew
- After processing, webshop marks items as processed
GET /api/v1/pricelists/{pricelistId}/items - Get all items for a pricelist - For full sync scenarios
Purchase Order Extraction
Same pattern:
1. Extract purchase_orders from products into PurchaseOrder + PurchaseOrderLine
2. Track changes separately
3. Products don't carry PO data in their changedFields
GET /api/v1/updated-purchase-orders - Fetch changed POs GET /api/v1/purchase-orders/{id}/lines - Get lines for a PO
Future: Direct Endpoints
Phase 2 adds direct push endpoints from Odoo:
- POST /api/v1/pricelists - Direct pricelist updates (skips product extraction)
- POST /api/v1/purchase-orders - Direct PO updates
Files to Create
src/Service/PriceExtractorService.phpsrc/Service/PurchaseOrderExtractorService.phpsrc/Controller/Api/V1/PricelistController.phpsrc/Controller/Api/V1/PurchaseOrderController.php
Phase 8: Stock Optimization
Note: Phase numbering updated - Authentication moved to Phase 9.
Efficient Stock Storage
Instead of JSON blobs, use normalized table:
- stock table with (supplier_id, product_id, quantity, prices)
- Composite index for fast lookups
- Bulk upsert for updates
Delta Responses
Track stock changes:
- Add previousQuantity column or separate change log
- Return only changed stock in API response
- Support full catalog fetch when needed
Files to Modify
src/Entity/Stock.php(normalized structure)src/Service/StockSyncService.php(bulk upsert)src/Controller/Api/V1/StockQueryController.php(delta support)
Phase 9: Authentication & Security
API Authentication
Use the existing User entity with API tokens:
- ApiToken entity linked to User
- Token validation middleware
- Rate limiting per token
Endpoints Security
- Odoo endpoints: Odoo API token (env variable)
- WebShop endpoints: WebShop API token
- Admin endpoints: User JWT authentication
Files to Create
src/Entity/ApiToken.phpsrc/Security/ApiTokenAuthenticator.phpconfig/packages/security.yaml(update)
Implementation Order
Phase A: Foundation
- ✅ User auth (already done)
- Create core entities: Product, Partner, SaleOrder, Stock
- Create extraction entities: PricelistItem, PurchaseOrder, PurchaseOrderLine
- Run migrations
- Create ChangeDetectionService with tests
- Create PriceExtractorService with tests
Phase B: Inbound API (Odoo → Middleware)
- Product sync endpoint + service (extracts prices & POs)
- Partner sync endpoint + service (with hierarchy resolution)
- SaleOrder sync endpoint + service
- Stock sync endpoint + service
Phase C: Outbound API (Middleware → WebShop)
- Updated products API (with changedFields, excludes embedded prices)
- Updated partners API (with hierarchy info)
- Updated pricelists API (only actually changed prices)
- Updated purchase orders API
- Stock query endpoints
- Batch acknowledge endpoints
Phase D: Outbound Queue & Sync Back
- Outbound queue (Messenger) for orders/customers to Odoo
- OdooClient service for API calls
- Retry logic and dead letter handling
Phase E: Security & Polish
- API token authentication
- Rate limiting
- Integration tests
- Admin monitoring endpoints
Verification
Unit Tests
ChangeDetectionService- field comparison logic- Sync services - data transformation
Integration Tests
- Full flow: Odoo push → Store → WebShop fetch
- Queue processing
- Error handling and retry
Manual Testing
- POST product data to
/api/v1/products - Verify storage with changed fields computed
- GET
/api/v1/updated-productsand verifychangedFields - DELETE to mark as processed
- Test hierarchy with nested partners
- Test stock bulk updates
Critical Files Summary
New Entities
src/Entity/Product.phpsrc/Entity/Partner.phpsrc/Entity/SaleOrder.phpsrc/Entity/Stock.phpsrc/Entity/PricelistItem.php← NEW: Extracted pricessrc/Entity/PurchaseOrder.php← NEW: Extracted POssrc/Entity/PurchaseOrderLine.php← NEW: PO line itemssrc/Entity/OutboundQueue.phpsrc/Entity/ApiToken.php
Core Services
src/Service/ChangeDetectionService.php- Field-level diff computationsrc/Service/PriceExtractorService.php← NEW: Extracts prices from productssrc/Service/PurchaseOrderExtractorService.php← NEW: Extracts POs from productssrc/Service/ProductSyncService.phpsrc/Service/PartnerSyncService.phpsrc/Service/PartnerHierarchyService.phpsrc/Service/SaleOrderSyncService.phpsrc/Service/StockSyncService.phpsrc/Service/OdooClient.php
Controllers (Inbound from Odoo)
src/Controller/Api/V1/ProductController.phpsrc/Controller/Api/V1/PartnerController.phpsrc/Controller/Api/V1/SaleOrderController.phpsrc/Controller/Api/V1/StockController.php
Controllers (Outbound to WebShop)
src/Controller/Api/V1/UpdatedProductController.phpsrc/Controller/Api/V1/UpdatedPartnerController.phpsrc/Controller/Api/V1/UpdatedSaleOrderController.phpsrc/Controller/Api/V1/PricelistController.php← NEW: Price updatessrc/Controller/Api/V1/PurchaseOrderController.php← NEW: PO updatessrc/Controller/Api/V1/PartnerHierarchyController.phpsrc/Controller/Api/V1/StockQueryController.php
Key Architecture Decisions
- Change Detection: Middleware stores previous state to compute field-level diffs
- Price Extraction: Prices stored separately from products to avoid 128k update explosions
- Purchase Order Extraction: POs stored separately for same reason
- Partner Hierarchy: Self-referencing FK preserves full Odoo hierarchy
- Outbound Queue: Symfony Messenger for reliable sync back to Odoo
- Hybrid Approach: Initially extract from embedded data, add direct endpoints later