Skip to content

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/:

  1. Shared Traits (src/Entity/Trait/)
  2. SyncableTrait.php - odooId, externalId, currentData, previousData, changedFields, processingState, errorMessage, retryCount, isNew
  3. TimestampableTrait.php - createdAt, updatedAt with lifecycle callbacks

  4. Core Entities

  5. Product.php - Uses SyncableTrait, has context field
  6. Partner.php - Uses SyncableTrait, has type (B/C/D/O), self-referencing parent_id for hierarchy, externalParentId
  7. SaleOrder.php - Uses SyncableTrait, has direction (inbound/outbound)
  8. Stock.php - Normalized table with supplierExternalId, productExternalId, quantity, prices, previousQuantity, quantityChanged

  9. Extraction Entities (solve 128k update explosion)

  10. PricelistItem.php - Extracted prices with priceChanged flag, isNew, isPromoPrice, isAction, dates
  11. PurchaseOrder.php - Uses SyncableTrait, has name, vendor, state, createdBy, orderDate
  12. PurchaseOrderLine.php - FK to PurchaseOrder, productExternalId, quantity, qtyReceived, priceUnit, datePlanned, repliedByVendor

  13. Infrastructure Entities

  14. OutboundQueue.php - For middleware→Odoo sync queue (entityType, entityId, payload, status, attempts, lastError)
  15. 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/:

  1. ChangeDetectionService.php - Field-level diff computation
  2. detectChanges(newData, previousData) → ChangeResult
  3. computeFieldDiff(oldData, newData) → string[] of changed field paths
  4. Handles nested objects, arrays, floats with tolerance
  5. Excludes 'prices', 'purchase_orders', timestamps by default

  6. PriceExtractorService.php - Extracts prices from products

  7. extractFromProduct(productData, externalId) → int (count)
  8. extractFromProducts(products) → int (bulk upsert)
  9. Only sets priceChanged=true when price actually differs

  10. PurchaseOrderExtractorService.php - Extracts POs from products

  11. extractFromProduct(productData, externalId) → int
  12. extractFromProducts(products) → int
  13. processDirect(poData) → PurchaseOrder (for direct PO endpoint)

  14. ProductSyncService.php

  15. sync(productData) → Product
  16. syncBatch(productsData) → {processed, created, updated, errors}
  17. Calls PriceExtractorService and PurchaseOrderExtractorService

  18. PartnerSyncService.php

  19. sync(partnerData) → Partner
  20. syncBatch(partnersData) → {processed, created, updated, errors}
  21. resolveUnresolvedParents() → int (resolved count)
  22. Handles hierarchy with deferred parent resolution

  23. SaleOrderSyncService.php

  24. sync(orderData) → SaleOrder (inbound)
  25. syncBatch(ordersData) → {processed, created, updated, errors}
  26. createOutbound(orderData) → SaleOrder (from webshop)

  27. StockSyncService.php

  28. sync(stockData) → Stock
  29. syncBatch(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/:

  1. ProductController.php - POST /api/v1/products
  2. Accepts single product or batch {products: [...]}
  3. Returns {success, processed, created, updated, errors}

  4. PartnerController.php - POST /api/v1/partners

  5. Same pattern as products
  6. Handles hierarchy resolution

  7. SaleOrderController.php - POST /api/v1/sale-orders

  8. Accepts {orders: [...]} or single order

  9. StockController.php - POST /api/v1/stocks

  10. Accepts {stocks: [...]} or single stock
  11. Uses bulk upsert for efficiency

Phase C: Outbound API (Middleware → WebShop) ✅

Controllers in src/Controller/Api/V1/:

  1. UpdatedProductController.php
  2. GET /api/v1/updated-products - List with changedFields, pagination, since filter
  3. GET /api/v1/updated-products/{id} - Single product detail
  4. DELETE /api/v1/updated-products/{id} - Mark as processed
  5. POST /api/v1/updated-products/batch-delete - Batch mark as processed

  6. UpdatedPartnerController.php

  7. Same pattern as products
  8. GET /api/v1/updated-partners/{id}/hierarchy - Full hierarchy tree

  9. UpdatedSaleOrderController.php

  10. Same pattern with direction filter (inbound/outbound)

  11. PricelistController.php - KEY FOR SOLVING 128K EXPLOSION

  12. GET /api/v1/pricelists/updated - Only prices where priceChanged=true
  13. GET /api/v1/pricelists - List distinct pricelists
  14. GET /api/v1/pricelists/{pricelistId}/items - All items for full sync
  15. POST /api/v1/pricelists/updated/batch-delete - Mark as processed

  16. PurchaseOrderQueryController.php

  17. GET /api/v1/purchase-orders/updated - Updated POs
  18. GET /api/v1/purchase-orders/{id} - PO with lines
  19. GET /api/v1/purchase-orders/{id}/lines - Just lines

  20. StockQueryController.php

  21. GET /api/v1/stocks/changed - Only where quantityChanged=true (delta)
  22. GET /api/v1/stocks/by-supplier/{supplierId} - Full supplier stock
  23. GET /api/v1/stocks/by-product/{productId} - All suppliers for product
  24. GET /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)

  1. Create Symfony Messenger Message
  2. src/Message/SyncToOdooMessage.php - entityType, entityId

  3. Create Message Handler

  4. src/MessageHandler/SyncToOdooHandler.php
  5. Retry logic (3 attempts, exponential backoff)
  6. Dead letter handling

  7. Create OdooClient Service

  8. src/Service/OdooClient.php
  9. HTTP client for Odoo XML-RPC or REST API
  10. Methods: createPartner, createSaleOrder, etc.

  11. Configure Messenger

  12. config/packages/messenger.yaml
  13. Transport (doctrine or redis)
  14. Retry strategy

  15. Create Queue Management Endpoints (optional)

  16. GET /api/v1/queue/status/{entityType}/{entityId}
  17. Admin endpoints for queue monitoring

Phase E: Security & Polish

  1. API Token Authentication
  2. src/Security/ApiTokenAuthenticator.php - Custom authenticator
  3. Update config/packages/security.yaml:

    • Firewall for /api/v1/*
    • Token validation
    • Scope checking (odoo, webshop, admin)
  4. Rate Limiting

  5. Configure Symfony RateLimiter for API endpoints
  6. Per-token limits

  7. Integration Tests

  8. Full flow tests: Odoo push → Store → WebShop fetch
  9. Queue processing tests
  10. Error handling tests

  11. Admin Monitoring Endpoints (optional)

  12. Dashboard data: counts, errors, queue status
  13. 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