Atraxion Middleware - Implementation Status
Date: 2026-01-31 Status: Phase A, B, C mostly complete. Phases D & E remaining.
COMPLETED WORK
Phase A: Foundation ✅
Entities Created
All entities are in src/Entity/:
- Shared Traits (
src/Entity/Trait/) SyncableTrait.php- odooId, externalId, currentData, previousData, changedFields, processingState, errorMessage, retryCount, isNew-
TimestampableTrait.php- createdAt, updatedAt with lifecycle callbacks -
Core Entities
Product.php- Uses SyncableTrait, has context fieldPartner.php- Uses SyncableTrait, has type (B/C/D/O), self-referencing parent_id for hierarchy, externalParentIdSaleOrder.php- Uses SyncableTrait, has direction (inbound/outbound)-
Stock.php- Normalized table with supplierExternalId, productExternalId, quantity, prices, previousQuantity, quantityChanged -
Extraction Entities (solve 128k update explosion)
PricelistItem.php- Extracted prices with priceChanged flag, isNew, isPromoPrice, isAction, datesPurchaseOrder.php- Uses SyncableTrait, has name, vendor, state, createdBy, orderDate-
PurchaseOrderLine.php- FK to PurchaseOrder, productExternalId, quantity, qtyReceived, priceUnit, datePlanned, repliedByVendor -
Infrastructure Entities
OutboundQueue.php- For middleware→Odoo sync queue (entityType, entityId, payload, status, attempts, lastError)ApiToken.php- API authentication (name, token, scope, user FK, isActive, expiresAt, lastUsedAt)
Repositories Created
All in src/Repository/:
- ProductRepository.php - findByExternalId, findUpdatedSince, findPending, markAsProcessed
- PartnerRepository.php - Same + findByType, findChildrenByParentId, findWithUnresolvedParent
- SaleOrderRepository.php - Same + direction filter, findInbound, findOutbound
- StockRepository.php - findBySupplierAndProduct, findChanged, bulkUpsert (PostgreSQL ON CONFLICT)
- PricelistItemRepository.php - findByPricelistAndProduct, findChanged, bulkUpsert, getDistinctPricelists
- PurchaseOrderRepository.php - findByName, findByState, findByVendor
- PurchaseOrderLineRepository.php - findByPurchaseOrderId, findPendingDeliveries
- OutboundQueueRepository.php - findPending, findFailed, findStuckProcessing, getStatusCounts, cleanupCompleted
- ApiTokenRepository.php - findValidByToken, findByScope, deactivateExpired
Core Services Created
All in src/Service/:
- ChangeDetectionService.php - Field-level diff computation
detectChanges(newData, previousData)→ ChangeResultcomputeFieldDiff(oldData, newData)→ string[] of changed field paths- Handles nested objects, arrays, floats with tolerance
-
Excludes 'prices', 'purchase_orders', timestamps by default
-
PriceExtractorService.php - Extracts prices from products
extractFromProduct(productData, externalId)→ int (count)extractFromProducts(products)→ int (bulk upsert)-
Only sets priceChanged=true when price actually differs
-
PurchaseOrderExtractorService.php - Extracts POs from products
extractFromProduct(productData, externalId)→ intextractFromProducts(products)→ int-
processDirect(poData)→ PurchaseOrder (for direct PO endpoint) -
ProductSyncService.php
sync(productData)→ ProductsyncBatch(productsData)→ {processed, created, updated, errors}-
Calls PriceExtractorService and PurchaseOrderExtractorService
-
PartnerSyncService.php
sync(partnerData)→ PartnersyncBatch(partnersData)→ {processed, created, updated, errors}resolveUnresolvedParents()→ int (resolved count)-
Handles hierarchy with deferred parent resolution
-
SaleOrderSyncService.php
sync(orderData)→ SaleOrder (inbound)syncBatch(ordersData)→ {processed, created, updated, errors}-
createOutbound(orderData)→ SaleOrder (from webshop) -
StockSyncService.php
sync(stockData)→ StocksyncBatch(stocksData)→ {processed, errors} (uses bulk upsert)
DTO Created
src/DTO/ChangeResult.php- changedFields[], isNew, hasChanges with factory methods
Migration Created
migrations/Version20260131100000.php- PostgreSQL schema for all entities with indexes
Phase B: Inbound API (Odoo → Middleware) ✅
Controllers in src/Controller/Api/V1/:
- ProductController.php -
POST /api/v1/products - Accepts single product or batch {products: [...]}
-
Returns {success, processed, created, updated, errors}
-
PartnerController.php -
POST /api/v1/partners - Same pattern as products
-
Handles hierarchy resolution
-
SaleOrderController.php -
POST /api/v1/sale-orders -
Accepts {orders: [...]} or single order
-
StockController.php -
POST /api/v1/stocks - Accepts {stocks: [...]} or single stock
- Uses bulk upsert for efficiency
Phase C: Outbound API (Middleware → WebShop) ✅
Controllers in src/Controller/Api/V1/:
- UpdatedProductController.php
GET /api/v1/updated-products- List with changedFields, pagination, since filterGET /api/v1/updated-products/{id}- Single product detailDELETE /api/v1/updated-products/{id}- Mark as processed-
POST /api/v1/updated-products/batch-delete- Batch mark as processed -
UpdatedPartnerController.php
- Same pattern as products
-
GET /api/v1/updated-partners/{id}/hierarchy- Full hierarchy tree -
UpdatedSaleOrderController.php
-
Same pattern with direction filter (inbound/outbound)
-
PricelistController.php - KEY FOR SOLVING 128K EXPLOSION
GET /api/v1/pricelists/updated- Only prices where priceChanged=trueGET /api/v1/pricelists- List distinct pricelistsGET /api/v1/pricelists/{pricelistId}/items- All items for full sync-
POST /api/v1/pricelists/updated/batch-delete- Mark as processed -
PurchaseOrderQueryController.php
GET /api/v1/purchase-orders/updated- Updated POsGET /api/v1/purchase-orders/{id}- PO with lines-
GET /api/v1/purchase-orders/{id}/lines- Just lines -
StockQueryController.php
GET /api/v1/stocks/changed- Only where quantityChanged=true (delta)GET /api/v1/stocks/by-supplier/{supplierId}- Full supplier stockGET /api/v1/stocks/by-product/{productId}- All suppliers for productGET /api/v1/stocks/lookup?supplier_id=X&product_id=Y- Single lookup
Tests Created ✅
All in tests/Unit/Service/:
- ChangeDetectionServiceTest.php - 18 tests
- PriceExtractorServiceTest.php - 11 tests
- PurchaseOrderExtractorServiceTest.php - 10 tests
Total: 57 tests, 176 assertions, ALL PASSING
REMAINING WORK
Phase D: Outbound Queue & Sync Back (Middleware → Odoo)
- Create Symfony Messenger Message
-
src/Message/SyncToOdooMessage.php- entityType, entityId -
Create Message Handler
src/MessageHandler/SyncToOdooHandler.php- Retry logic (3 attempts, exponential backoff)
-
Dead letter handling
-
Create OdooClient Service
src/Service/OdooClient.php- HTTP client for Odoo XML-RPC or REST API
-
Methods: createPartner, createSaleOrder, etc.
-
Configure Messenger
config/packages/messenger.yaml- Transport (doctrine or redis)
-
Retry strategy
-
Create Queue Management Endpoints (optional)
GET /api/v1/queue/status/{entityType}/{entityId}- Admin endpoints for queue monitoring
Phase E: Security & Polish
- API Token Authentication
src/Security/ApiTokenAuthenticator.php- Custom authenticator-
Update
config/packages/security.yaml:- Firewall for
/api/v1/* - Token validation
- Scope checking (odoo, webshop, admin)
- Firewall for
-
Rate Limiting
- Configure Symfony RateLimiter for API endpoints
-
Per-token limits
-
Integration Tests
- Full flow tests: Odoo push → Store → WebShop fetch
- Queue processing tests
-
Error handling tests
-
Admin Monitoring Endpoints (optional)
- Dashboard data: counts, errors, queue status
- Health check endpoint
QUICK START FOR NEXT SESSION
# Run tests
/opt/homebrew/Cellar/php@8.4/8.4.17/bin/php bin/phpunit tests/Unit/
# Check code style
/opt/homebrew/Cellar/php@8.4/8.4.17/bin/php vendor/bin/php-cs-fixer fix --dry-run --diff
# Run PHPStan (needs more memory)
/opt/homebrew/Cellar/php@8.4/8.4.17/bin/php -d memory_limit=512M vendor/bin/phpstan analyse src/ --level 6
# Generate migration (if DB running)
/opt/homebrew/Cellar/php@8.4/8.4.17/bin/php -d variables_order=EGPCS bin/console doctrine:migrations:diff
FILE STRUCTURE CREATED
src/
├── Controller/Api/V1/
│ ├── ProductController.php # Inbound
│ ├── PartnerController.php # Inbound
│ ├── SaleOrderController.php # Inbound
│ ├── StockController.php # Inbound
│ ├── UpdatedProductController.php # Outbound
│ ├── UpdatedPartnerController.php # Outbound
│ ├── UpdatedSaleOrderController.php# Outbound
│ ├── PricelistController.php # Outbound - KEY
│ ├── PurchaseOrderQueryController.php # Outbound
│ └── StockQueryController.php # Outbound
├── DTO/
│ └── ChangeResult.php
├── Entity/
│ ├── Trait/
│ │ ├── SyncableTrait.php
│ │ └── TimestampableTrait.php
│ ├── Product.php
│ ├── Partner.php
│ ├── SaleOrder.php
│ ├── Stock.php
│ ├── PricelistItem.php
│ ├── PurchaseOrder.php
│ ├── PurchaseOrderLine.php
│ ├── OutboundQueue.php
│ └── ApiToken.php
├── Repository/
│ ├── ProductRepository.php
│ ├── PartnerRepository.php
│ ├── SaleOrderRepository.php
│ ├── StockRepository.php
│ ├── PricelistItemRepository.php
│ ├── PurchaseOrderRepository.php
│ ├── PurchaseOrderLineRepository.php
│ ├── OutboundQueueRepository.php
│ └── ApiTokenRepository.php
└── Service/
├── ChangeDetectionService.php
├── PriceExtractorService.php
├── PurchaseOrderExtractorService.php
├── ProductSyncService.php
├── PartnerSyncService.php
├── SaleOrderSyncService.php
└── StockSyncService.php
migrations/
└── Version20260131100000.php
tests/Unit/Service/
├── ChangeDetectionServiceTest.php
├── PriceExtractorServiceTest.php
└── PurchaseOrderExtractorServiceTest.php