A production-grade simulation of bidirectional data synchronization between a Shopify storefront and an Odoo ERP system. Built with FastAPI, SQLite, and a vanilla-JS SPA frontend.
No external APIs required. Both Shopify and Odoo are fully mocked in-process with realistic data stores.
- Features
- Architecture
- Quick Start
- Docker
- API Reference
- Configuration
- Frontend Views
- Testing
- Sync Engine Details
- Project Structure
- Development
- Resume Keywords
- License
- Mock Shopify Store -- 20 products with variants (S/M/L/XL), inventory levels per variant, and 50 realistic orders with line items.
- Mock Odoo ERP -- Product templates, stock quants, and sale orders mirroring Odoo's XML-RPC external API patterns.
- Product Sync -- Map Shopify fields to Odoo fields (title -> name, price -> list_price, SKU -> default_code, etc.) with configurable transforms (direct, uppercase, lowercase, round2).
- Inventory Sync -- Synchronize stock levels between systems with conflict detection and resolution strategies.
- Order Sync -- Push Shopify orders to Odoo sale orders with line item mapping.
- Configurable Field Mappings -- Full CRUD for mapping rules. Choose which Shopify field maps to which Odoo field and apply transforms.
- Retry with Exponential Back-off -- 1s, 2s, 4s, 8s delays on simulated transient failures. Configurable max retries and base delay.
- Dead Letter Queue (DLQ) -- Records that fail after max retries are stored with full payload and error details. Manual retry available.
- Sync Job History -- Every sync run creates a job with per-record logs (success / fail / skipped).
- Dashboard -- Last sync time, records synced, failure rate, DLQ count, and system-pair inventory counts.
- Conflict Resolution -- Two strategies: last-write-wins (default) and source-priority (configurable which system wins).
- Dry Run Mode -- Preview what would sync without writing data.
+--------------------+ +-------------------+
| Shopify Mock | <---> | Sync Engine | <---> | Odoo Mock |
| (in-memory) | | - field mapping | | (in-memory) |
| - products/variants| | - conflict resolve| | - product.templ |
| - inventory levels | | - retry + backoff | | - stock.quant |
| - orders | | - DLQ management | | - sale.order |
+--------------------+ +-------------------+ +-----------------+
|
SQLite DB
(jobs, logs, DLQ,
field mappings)
# Clone
git clone <repo-url>
cd shopify-odoo-sync
# Option A: use start.sh
chmod +x start.sh
./start.sh
# Option B: manual
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
mkdir -p instance
uvicorn app:app --host 0.0.0.0 --port 8033 --reloadOpen http://localhost:8033 in your browser.
- Click Seed Mock Data on the dashboard (or
POST /api/seed). - Trigger individual syncs or use Sync All.
- View job history, logs, and dead-letter queue via the SPA.
# Build and run
docker compose up --build -d
# View logs
docker compose logs -f
# Stop
docker compose down| Method | Path | Description |
|---|---|---|
| POST | /api/seed | Populate mock Shopify & Odoo data |
| POST | /api/sync/products | Sync products (Shopify -> Odoo) |
| POST | /api/sync/inventory | Sync inventory levels |
| POST | /api/sync/orders | Sync orders (Shopify -> Odoo) |
| GET | /api/jobs | List sync jobs |
| GET | /api/jobs/{id} | Get job detail |
| GET | /api/jobs/{id}/logs | Get per-record logs for a job |
| GET | /api/mappings | List all field mappings |
| POST | /api/mappings | Create a field mapping |
| PUT | /api/mappings/{id} | Update a field mapping |
| DELETE | /api/mappings/{id} | Delete a field mapping |
| GET | /api/dead-letter | List dead-letter entries |
| POST | /api/dead-letter/{id}/retry | Retry a failed DLQ entry |
| GET | /api/health | Health check |
| GET | /api/dashboard | Dashboard statistics |
# Seed first
curl -X POST http://localhost:8033/api/seed
# Run product sync
curl -X POST http://localhost:8033/api/sync/products \
-H "Content-Type: application/json" \
-d '{"direction": "shopify_to_odoo", "dry_run": false}'Response:
{
"id": 1,
"job_type": "products",
"status": "completed",
"total_records": 20,
"success_count": 18,
"fail_count": 2,
"skip_count": 0
}# Create custom mapping
curl -X POST http://localhost:8033/api/mappings \
-H "Content-Type: application/json" \
-d '{"record_type": "product", "shopify_field": "handle", "odoo_field": "website_slug", "transform": "lowercase"}'| Variable | Default | Description |
|---|---|---|
| SYNC_PORT | 8033 | Server port |
| SYNC_DEBUG | false | Enable debug mode |
| DATABASE_URL | sqlite | Database connection string |
| SYNC_FAILURE_RATE | 0.10 | Simulated failure rate (0.0-1.0) |
| MAX_RETRIES | 4 | Maximum retry attempts |
| RETRY_BACKOFF_BASE | 1.0 | Base retry delay (seconds) |
| RETRY_BACKOFF_MULTIPLIER | 2.0 | Backoff multiplier |
| DLQ_MAX_SIZE | 5000 | Maximum dead-letter queue size |
| SYNC_BATCH_SIZE | 50 | Records per sync batch |
| CONFLICT_STRATEGY | last_write_wins | Conflict resolution strategy |
| PRIORITY_SOURCE | shopify | Priority source (source_priority) |
| Route | Description |
|---|---|
| #/ | Dashboard: sync status, counts, quick actions |
| #/sync | Sync Control: trigger product/inventory/order sync |
| #/jobs | Job History: list jobs with clickable log detail |
| #/mappings | Field Mappings: CRUD editor with tabs per type |
| #/dead-letter | Dead Letter Queue: failed records with retry |
# Run all tests
pytest tests/ -v
# Run with coverage
pytest tests/ -v --cov=. --cov-report=term-missing
# Run specific test file
pytest tests/test_api.py -v
pytest tests/test_services.py -v
pytest tests/test_models.py -v
pytest tests/test_sync_engine.py -vEach mapping defines:
- record_type: product, inventory, or order
- shopify_field: dot-notation path (e.g.,
variants.0.price) - odoo_field: target field name (e.g.,
list_price) - transform: direct, uppercase, lowercase, round2, to_float, to_int, to_str
Two strategies are supported:
-
last_write_wins (default): The most recent write takes precedence. During sync, the source system's data always overwrites the target.
-
source_priority: A designated system always wins. Set
PRIORITY_SOURCE=shopifyorPRIORITY_SOURCE=odoo. When the priority source is the target, incoming updates are skipped.
Attempt 1: immediate
Attempt 2: wait 1.0s (base * 2^0)
Attempt 3: wait 2.0s (base * 2^1)
Attempt 4: wait 4.0s (base * 2^2)
Attempt 5: wait 8.0s (base * 2^3)
-> Dead Letter Queue
Configurable via MAX_RETRIES, RETRY_BACKOFF_BASE, RETRY_BACKOFF_MULTIPLIER.
Records that exhaust all retries are placed in the DLQ with:
- Full original payload (JSON)
- Error message from last attempt
- Retry count and max retries
- Manual retry endpoint:
POST /api/dead-letter/{id}/retry
shopify-odoo-sync/
├── app.py # FastAPI entry point (no app.run)
├── config.py # Environment-based configuration
├── requirements.txt # Python dependencies
├── README.md # This file
├── Dockerfile # Container image
├── docker-compose.yml # Docker Compose stack
├── start.sh # Quick-start script
├── LICENSE # MIT License
├── .gitignore # Git ignore rules
├── .github/
│ └── workflows/
│ └── ci.yml # GitHub Actions CI pipeline
├── models/
│ ├── __init__.py # Package exports
│ ├── database.py # SQLAlchemy engine, tables, init_db
│ └── schemas.py # Pydantic schemas
├── routes/
│ ├── __init__.py # Package exports
│ ├── api.py # REST API endpoints
│ └── views.py # HTML view (SPA shell)
├── services/
│ ├── __init__.py # Package exports
│ ├── mock_shopify.py # Simulated Shopify Admin API
│ ├── mock_odoo.py # Simulated Odoo XML-RPC API
│ └── sync_engine.py # Bidirectional sync logic
├── frontend/
│ ├── index.html # SPA shell
│ └── static/
│ ├── css/
│ │ └── app.css # Application styles (300+ lines)
│ └── js/
│ └── app.js # SPA JavaScript (500+ lines)
├── tests/
│ ├── conftest.py # Shared fixtures
│ ├── test_api.py # API route tests
│ ├── test_models.py # Schema tests
│ ├── test_services.py # Mock service tests
│ └── test_sync_engine.py # Sync engine integration tests
└── seed_data/
└── data.json # Initial mock data
- Add mock data to
services/mock_shopify.pyandservices/mock_odoo.py - Create field mappings in
models/database.py_seed_default_mappings() - Add sync function in
services/sync_engine.py - Register API endpoint in
routes/api.py - Add frontend tab in
frontend/static/js/app.js
Set SYNC_FAILURE_RATE to control how often sync operations fail:
0.0-- 100% success (good for demos)0.10-- 10% failure rate (default, realistic)0.50-- 50% failure rate (stress-test DLQ)1.0-- 100% failure (all records go to DLQ)
Add new transforms to _TRANSFORMS in services/sync_engine.py:
_TRANSFORMS["my_transform"] = lambda v: custom_logic(v)Then reference it in field mappings via the API or database.
E-commerce integration, ERP synchronization, Shopify API, Odoo XML-RPC, bidirectional sync, field mapping engine, conflict resolution, last-write-wins, source-priority, exponential backoff, retry logic, dead letter queue, data pipeline, ETL, FastAPI, SQLAlchemy, SQLite, REST API, SPA dashboard, job scheduling, per-record logging, configurable transforms, dry-run mode, Docker, CI/CD, GitHub Actions, production-grade simulation.
MIT License. See LICENSE for details.