diff --git a/ROADMAP.md b/ROADMAP.md index d5075ea..8912295 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -13,10 +13,12 @@ Order is approximate. Each item is sized so the surrounding system remains shipp - **Bulk recompute task.** Celery beat schedule that walks the catalog in shards, emits suggestions, applies the auto-approve set, queues the rest for review. - **Webhook ingest.** Inbound `products/update` webhook with HMAC verification (already implemented in `shopify/client.py:verify_webhook`) drives a recompute task for the affected SKU. - **Rate-limit-aware Shopify client.** Backoff on `X-Shopify-Shop-Api-Call-Limit`. Batch variant updates where possible. +- **Per-merchant automation suspend.** Emergency-stop flag on the merchant row. Suspended merchants skip the auto-apply path; the audit trail still records manual changes. Complements rollback โ€” rollback fixes the past, suspend prevents the next move. ## Later - **Pricing-signal layer.** Replace the toy `demand_signal - inventory_pressure` proposal with something that reads real signals. The guardrail layer is the safety surface โ€” the proposal is swappable. - **Per-merchant policy.** Today `MerchantPolicy` is constructed with module defaults. Move to a per-merchant config row. +- **Asymmetric guardrail caps.** Split `max_change_pct` into `max_increase_pct` and `max_decrease_pct`. Most merchants want prices to fall slower than they rise. - **A/B price testing.** Treatment / control groups, lift measurement against an audited baseline. - **Margin-based reporting.** Slice realized margin by SKU, by category, by guardrail-fired rate. diff --git a/ecom-dynamic-pricing/.env.example b/ecom-dynamic-pricing/.env.example deleted file mode 100644 index 11e3518..0000000 --- a/ecom-dynamic-pricing/.env.example +++ /dev/null @@ -1,28 +0,0 @@ -# FastAPI -APP_ENV=local -APP_DEBUG=true -APP_HOST=0.0.0.0 -APP_PORT=8000 - -# Database -POSTGRES_USER=ecom -POSTGRES_PASSWORD=ecom_password -POSTGRES_DB=ecom_db -POSTGRES_HOST=db -POSTGRES_PORT=5432 - -# Redis / Celery -REDIS_URL=redis://redis:6379/0 - -# Shopify -SHOPIFY_APP_API_KEY=your_shopify_public_key_here -SHOPIFY_APP_API_SECRET=your_shopify_api_secret_here -SHOPIFY_APP_SCOPES=read_products,write_products,read_orders -SHOPIFY_APP_REDIRECT_URI=https://your-ngrok-or-domain.com/api/shopify/oauth/callback - -# App -JWT_SECRET=super_secret_change_me -JWT_ALGORITHM=HS256 - -# Error Monitoring (optional - leave empty to disable) -SENTRY_DSN= diff --git a/ecom-dynamic-pricing/.gitignore b/ecom-dynamic-pricing/.gitignore deleted file mode 100644 index 2e0fe40..0000000 --- a/ecom-dynamic-pricing/.gitignore +++ /dev/null @@ -1,54 +0,0 @@ -# Environment variables (contains secrets) -.env - -# Python -__pycache__/ -*.py[cod] -*$py.class -*.so -.Python -env/ -venv/ -ENV/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -*.egg-info/ -.installed.cfg -*.egg - -# IDEs -.vscode/ -.idea/ -*.swp -*.swo -*~ - -# OS -.DS_Store -Thumbs.db - -# Docker -docker-compose.override.yml - -# Postgres data -*.sql -pgdata/ - -# Logs -*.log -logs/ - -# Testing -.pytest_cache/ -.coverage -htmlcov/ diff --git a/ecom-dynamic-pricing/HANDOFF.md b/ecom-dynamic-pricing/HANDOFF.md deleted file mode 100644 index 02476f6..0000000 --- a/ecom-dynamic-pricing/HANDOFF.md +++ /dev/null @@ -1,566 +0,0 @@ -# System Handoff Document - -**Date**: 2025-01-13 -**Version**: 1.0.0 -**Status**: Production Ready โœ… - ---- - -## ๐ŸŽฏ Executive Summary - -This is a **complete, production-ready SaaS platform** for automated Shopify product pricing. It's been built with one goal in mind: - -> **"Make the system stable enough that someone else can run it without bothering you."** - -### โœ… What's Complete - -| Component | Status | Files | Lines of Code | -|-----------|--------|-------|---------------| -| Backend API | โœ… Complete | 15+ files | ~2,500 LOC | -| Frontend UI | โœ… Complete | 20 files | ~1,400 LOC | -| Database Schema | โœ… Complete | 3 models | ~150 LOC | -| Documentation | โœ… Complete | 4 docs | ~3,500 lines | -| Docker Setup | โœ… Complete | 2 files | ~100 LOC | -| **Total** | **โœ… Ready** | **50+ files** | **~7,650 LOC** | - -### ๐Ÿ’ก Key Achievement - -**Zero Hand-Holding Required** - -- Merchants understand the system in 5 minutes (MERCHANT_QUICKSTART.md) -- Operators can diagnose 95% of issues (OPERATOR_RUNBOOK.md) -- Developers can extend features easily (README.md + inline docs) - ---- - -## ๐Ÿ“ฆ What You're Getting - -### **1. Backend Infrastructure** - -``` -FastAPI + PostgreSQL + Redis + Celery + Docker -``` - -**Capabilities:** -- Shopify OAuth integration -- Product sync (background jobs) -- Dynamic pricing engine with guardrails -- Preview mode (test before applying) -- Rollback functionality (undo changes) -- Suspend/resume automation -- Rate limiting (Shopify API) -- Retry logic (3 attempts, exponential backoff) -- Timeout guardrails (no infinite loops) -- Health checks (Postgres, Redis, Celery) -- Structured logging -- Sentry integration (error monitoring) - -**Stability Features:** -- โœ… Tasks never hang (timeout limits) -- โœ… API never crashes (retry logic) -- โœ… Logs tell you exactly what happened -- โœ… Errors auto-forward to Sentry -- โœ… Health checks show system status in real-time - -### **2. Frontend Dashboards** - -``` -React 18 + TypeScript + Tailwind CSS + Vite -``` - -**Merchant Dashboard** (`/merchant`): -- Control panel (suspend/resume/preview/rollback) -- Guardrails editor (min/max prices, protection days) -- Pricing history (full audit trail) -- Real-time stats - -**Operator Panel** (`/operator`): -- System health monitor -- Worker status -- Recent activity across stores -- Quick actions -- Runbook reference - -### **3. Documentation** - -| Document | Purpose | Audience | -|----------|---------|----------| -| `README.md` | Complete system overview | Everyone | -| `MERCHANT_QUICKSTART.md` | 5-minute setup guide | Merchants | -| `OPERATOR_RUNBOOK.md` | Operations manual | Support team | -| `frontend/README.md` | Frontend development | Developers | -| `HANDOFF.md` | This document | New team lead | - ---- - -## โš ๏ธ Before You Start - -**Read [TECHNICAL_DEBT.md](./TECHNICAL_DEBT.md) end-to-end. Treat it as the primary backlog for hardening.** - -This document gives you complete transparency on all known gaps, workarounds, and the prioritized remediation plan. No surprises. - ---- - -## ๐Ÿš€ Quick Start for New Owner - -### **Day 1: Get It Running (30 minutes)** - -```bash -# 1. Clone repo -git clone https://github.com/MacFall7/E-Commerce.git -cd E-Commerce/ecom-dynamic-pricing - -# 2. Configure -cp .env.example .env -# Edit .env with Shopify credentials - -# 3. Start backend -docker-compose up -d --build - -# 4. Verify health -curl http://localhost:8000/api/health/detailed - -# 5. Start frontend (optional) -cd frontend && npm install && npm run dev - -# 6. Access dashboards -open http://localhost:3000 -``` - -### **Day 2: Understand Architecture (1 hour)** - -1. Read [`README.md`](./README.md) - Architecture section -2. Review [`backend/app/main.py`](./backend/app/main.py) - See how routing works -3. Review [`backend/app/pricing_engine.py`](./backend/app/pricing_engine.py) - Understand pricing logic -4. Review [`frontend/src/pages/merchant/MerchantDashboard.tsx`](./frontend/src/pages/merchant/MerchantDashboard.tsx) - See UI structure - -### **Day 3: Connect Test Store (30 minutes)** - -1. Create Shopify Partner account (free) -2. Create test app -3. Add credentials to `.env` -4. Install on test store -5. Watch products sync -6. Run preview mode -7. Apply pricing -8. Test rollback - -### **Week 1: Make Your First Change (2 hours)** - -**Suggested First Change**: Add email notifications - -1. Install `sendgrid` package -2. Add email config to `config.py` -3. Create `utils/email.py` with send function -4. Call from `tasks.py` after pricing cycle -5. Test locally -6. Deploy - ---- - -## ๐Ÿ—‚๏ธ Repository Structure - -``` -ecom-dynamic-pricing/ -โ”‚ -โ”œโ”€โ”€ backend/ # FastAPI backend -โ”‚ โ”œโ”€โ”€ app/ -โ”‚ โ”‚ โ”œโ”€โ”€ main.py # Entry point -โ”‚ โ”‚ โ”œโ”€โ”€ config.py # Settings -โ”‚ โ”‚ โ”œโ”€โ”€ database.py # SQLAlchemy -โ”‚ โ”‚ โ”œโ”€โ”€ models.py # DB models (Store, Product, PricingEvent) -โ”‚ โ”‚ โ”œโ”€โ”€ schemas.py # Pydantic schemas -โ”‚ โ”‚ โ”œโ”€โ”€ pricing_engine.py # Pricing logic โญ -โ”‚ โ”‚ โ”œโ”€โ”€ shopify_client.py # Shopify API wrapper -โ”‚ โ”‚ โ”œโ”€โ”€ celery_app.py # Celery config -โ”‚ โ”‚ โ”œโ”€โ”€ tasks.py # Background tasks โญ -โ”‚ โ”‚ โ””โ”€โ”€ routes/ -โ”‚ โ”‚ โ”œโ”€โ”€ shopify.py # OAuth + webhooks -โ”‚ โ”‚ โ”œโ”€โ”€ operator.py # Operator controls โญ -โ”‚ โ”‚ โ””โ”€โ”€ health.py # Health checks -โ”‚ โ”œโ”€โ”€ requirements.txt -โ”‚ โ””โ”€โ”€ Dockerfile -โ”‚ -โ”œโ”€โ”€ frontend/ # React frontend -โ”‚ โ”œโ”€โ”€ src/ -โ”‚ โ”‚ โ”œโ”€โ”€ components/ -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ merchant/ # Merchant UI โญ -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ operator/ # Operator UI โญ -โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ shared/ # Reusable components -โ”‚ โ”‚ โ”œโ”€โ”€ lib/ -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ api.ts # API client โญ -โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ types.ts # TypeScript types -โ”‚ โ”‚ โ”œโ”€โ”€ pages/ -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ merchant/ -โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ operator/ -โ”‚ โ”‚ โ””โ”€โ”€ App.tsx -โ”‚ โ”œโ”€โ”€ package.json -โ”‚ โ””โ”€โ”€ vite.config.ts -โ”‚ -โ”œโ”€โ”€ docker-compose.yml # Service orchestration โญ -โ”œโ”€โ”€ .env.example # Environment template -โ”œโ”€โ”€ README.md # Main documentation โญ -โ”œโ”€โ”€ MERCHANT_QUICKSTART.md # Merchant guide -โ”œโ”€โ”€ OPERATOR_RUNBOOK.md # Operator manual -โ””โ”€โ”€ HANDOFF.md # This document - -โญ = Critical files to understand -``` - ---- - -## ๐Ÿ”‘ Critical Concepts - -### **1. Pricing Flow** - -``` -Product Sync (Celery) โ†’ Pricing Analysis (Engine) โ†’ Preview (UI) โ†’ Apply (API) โ†’ Event Log (DB) - โ†“ - Rollback (if needed) -``` - -### **2. Safety Mechanisms** - -| Mechanism | Location | Purpose | -|-----------|----------|---------| -| **Min/Max Limits** | `pricing_engine.py` | Never go too low/high | -| **Cost Floor** | `pricing_engine.py` | Protect margins (cost ร— 1.1) | -| **New Product Protection** | `pricing_engine.py` | Don't touch new items | -| **Preview Mode** | `tasks.py` | Test before apply | -| **Rollback** | `routes/operator.py` | Undo mistakes | -| **Suspend Toggle** | `models.py` | Emergency stop | - -### **3. Data Models** - -**Store**: -```python -id, shop_domain, access_token, automation_suspended, -min_price_limit, max_price_limit, new_product_protection_days -``` - -**Product**: -```python -id, store_id, shopify_variant_id, title, current_price, -cost, inventory_quantity, first_seen_at -``` - -**PricingEvent**: -```python -id, store_id, shopify_variant_id, old_price, new_price, -reason, rolled_back, created_at -``` - -### **4. Background Tasks** - -| Task | Schedule | What It Does | -|------|----------|-------------| -| `sync_store_products` | Manual or periodic | Fetches products from Shopify, updates DB | -| `run_pricing_cycle` | Manual or periodic | Analyzes products, applies price changes | - ---- - -## ๐Ÿ› ๏ธ Common Operations - -### **Add a New Store** - -```bash -# Get install URL -curl "http://localhost:8000/api/shopify/oauth/install?shop=new-store.myshopify.com" - -# Merchant visits URL and authorizes -# System auto-creates store record and syncs products -``` - -### **Change Pricing Logic** - -**File**: `backend/app/pricing_engine.py` - -```python -# Current logic (lines 70-81): -if sales_velocity > 5 and inventory > 20: - # increase price - new_price = min(price + delta, price * (1 + self.max_increase_pct)) - reason = "fast_seller_increase" - -# To modify: -# 1. Change thresholds (5, 20) -# 2. Change step_pct (default 0.05 = 5%) -# 3. Add new conditions (e.g., seasonality, competitor prices) -``` - -### **Suspend Automation for All Stores** - -```bash -# Loop through all stores -for store_id in $(seq 1 10); do - curl -X POST http://localhost:8000/api/operator/stores/$store_id/suspend -done -``` - -### **View Recent Errors** - -```bash -# Check logs -docker-compose logs --tail=100 backend | grep ERROR - -# Or use Sentry dashboard -open https://sentry.io/organizations/your-org/issues/ -``` - -### **Scale Workers** - -```bash -# Increase to 3 workers -docker-compose up -d --scale worker=3 - -# Verify -curl http://localhost:8000/api/health/detailed -# Should show 3 workers -``` - ---- - -## ๐Ÿ“Š Monitoring Checklist - -### **Daily (2 minutes)** - -- [ ] Check system health: `curl http://localhost:8000/api/health/detailed` -- [ ] Review Sentry errors: Visit Sentry dashboard -- [ ] Check Celery queue depth: `docker-compose exec redis redis-cli LLEN celery` (should be < 100) - -### **Weekly (10 minutes)** - -- [ ] Review pricing events: Check for anomalies -- [ ] Check disk usage: `docker system df` -- [ ] Review merchant feedback -- [ ] Update dependencies (security patches) - -### **Monthly (30 minutes)** - -- [ ] Database vacuum: `docker-compose exec db psql -U ecom -d ecom_db -c "VACUUM ANALYZE;"` -- [ ] Backup database: `docker-compose exec db pg_dump -U ecom ecom_db > backup_$(date +%Y%m%d).sql` -- [ ] Review and optimize slow queries -- [ ] Update roadmap based on merchant requests - ---- - -## ๐Ÿšจ Emergency Procedures - -### **System Down** - -```bash -# 1. Check container status -docker-compose ps - -# 2. Check logs -docker-compose logs --tail=100 - -# 3. Restart all services -docker-compose restart - -# 4. If still failing, rebuild -docker-compose down -docker-compose up -d --build - -# 5. Verify health -curl http://localhost:8000/api/health/detailed -``` - -### **Database Corrupted** - -```bash -# 1. Stop services -docker-compose down - -# 2. Restore from backup -cat backup_20250113.sql | docker-compose exec -T db psql -U ecom -d ecom_db - -# 3. Restart -docker-compose up -d - -# 4. Verify -curl http://localhost:8000/api/operator/stores/1/status -``` - -### **Merchant Reports Wrong Prices** - -```bash -# 1. Suspend automation -curl -X POST http://localhost:8000/api/operator/stores/{store_id}/suspend - -# 2. Check recent events -curl http://localhost:8000/api/operator/stores/{store_id}/pricing-history - -# 3. Rollback if needed -curl -X POST "http://localhost:8000/api/operator/stores/{store_id}/rollback?limit=10" - -# 4. Investigate logs -docker-compose logs worker | grep "store_id={store_id}" -``` - ---- - -## ๐Ÿ”ฎ Future Enhancements - -### **Short Term (1-2 weeks each)** - -- [ ] Add email notifications on pricing changes -- [ ] Scheduled pricing cycles (cron-like) -- [ ] Export pricing history to CSV -- [ ] Multi-store dashboard view -- [ ] Advanced filters on pricing history - -### **Medium Term (1-2 months each)** - -- [ ] Machine learning price optimization -- [ ] Competitor price monitoring -- [ ] A/B testing for pricing strategies -- [ ] Seasonal pricing rules -- [ ] Custom pricing rules per product tag - -### **Long Term (3-6 months each)** - -- [ ] Multi-channel support (Amazon, eBay) -- [ ] Mobile app (React Native) -- [ ] White-label solution for agencies -- [ ] Marketplace for pricing strategies -- [ ] Advanced analytics dashboard - ---- - -## ๐Ÿ‘ฅ Team Recommendations - -### **Ideal Team Structure** - -| Role | Hours/Week | Responsibilities | -|------|------------|------------------| -| **DevOps Engineer** | 5-10 | Maintain infrastructure, monitor health, handle incidents | -| **Backend Developer** | 10-20 | Add features, optimize pricing engine, API enhancements | -| **Frontend Developer** | 5-10 | UI improvements, new dashboards, mobile app (future) | -| **Customer Success** | 10-20 | Merchant support, feedback collection, documentation updates | - -**Total**: 30-60 hours/week for active development + support - -### **Skills Needed** - -- **Must Have**: Python, Docker, PostgreSQL -- **Should Have**: React, TypeScript, FastAPI -- **Nice to Have**: Celery, Redis, Shopify API - ---- - -## ๐Ÿ“š Learning Resources - -### **For Backend** - -- [FastAPI Tutorial](https://fastapi.tiangolo.com/tutorial/) -- [Celery Documentation](https://docs.celeryq.dev/) -- [SQLAlchemy ORM](https://docs.sqlalchemy.org/en/20/orm/) - -### **For Frontend** - -- [React Documentation](https://react.dev/) -- [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/intro.html) -- [Tailwind CSS](https://tailwindcss.com/docs) - -### **For Shopify** - -- [Shopify API Reference](https://shopify.dev/api) -- [OAuth Flow](https://shopify.dev/apps/auth/oauth) - ---- - -## โœ… Handoff Checklist - -Before final handoff, ensure: - -- [ ] All code is committed and pushed -- [ ] `.env.example` is up to date -- [ ] Documentation is current (README, runbooks) -- [ ] Production environment is configured -- [ ] Sentry is set up and receiving errors -- [ ] Database backups are automated -- [ ] At least one test merchant is onboarded -- [ ] Support email is set up -- [ ] Monitoring alerts are configured -- [ ] New team has access to: - - [ ] GitHub repository - - [ ] Shopify Partner account - - [ ] Sentry account - - [ ] Production servers - - [ ] Database credentials - ---- - -## ๐ŸŽ“ Knowledge Transfer Sessions - -### **Recommended Schedule** - -**Session 1 (1 hour)**: System Overview -- Architecture walkthrough -- Data flow explanation -- Key files review - -**Session 2 (1 hour)**: Backend Deep Dive -- Pricing engine logic -- Celery tasks -- Database models -- API endpoints - -**Session 3 (1 hour)**: Frontend Deep Dive -- Component structure -- API integration -- State management -- Routing - -**Session 4 (30 minutes)**: Operations -- Health monitoring -- Common issues -- Emergency procedures -- Deployment process - -**Session 5 (30 minutes)**: Next Steps -- Roadmap review -- Feature prioritization -- Questions & answers - ---- - -## ๐Ÿ“ž Point of Contact - -**Original Developer**: [Your Name] -**Handoff Date**: 2025-01-13 -**Contact**: [Your Email] - -**Availability for Questions**: 30 days after handoff - -**Emergency Contact**: Same as above (for critical production issues only) - ---- - -## ๐ŸŽ‰ Final Words - -This system has been built with care to be: - -โœ… **Stable** - Retry logic, timeouts, health checks -โœ… **Safe** - Guardrails, preview mode, rollback -โœ… **Clear** - Documentation, logging, error messages -โœ… **Extensible** - Clean architecture, TypeScript types -โœ… **Handoff-Ready** - Everything needed to run independently - -**You're inheriting a production-ready system.** The hard work is done. Your job is to: -1. Keep it running smoothly -2. Add value through new features -3. Support merchants - -**Good luck! ๐Ÿš€** - ---- - -
- -**Questions? Start with the README.md** - -[README](./README.md) โ€ข [Merchant Guide](./MERCHANT_QUICKSTART.md) โ€ข [Operator Manual](./OPERATOR_RUNBOOK.md) - -
diff --git a/ecom-dynamic-pricing/MERCHANT_QUICKSTART.md b/ecom-dynamic-pricing/MERCHANT_QUICKSTART.md deleted file mode 100644 index 193b54b..0000000 --- a/ecom-dynamic-pricing/MERCHANT_QUICKSTART.md +++ /dev/null @@ -1,212 +0,0 @@ -# Merchant Quickstart Guide -## E-Commerce Dynamic Pricing - -### What This Does - -Automatically adjusts your product prices based on sales velocity and inventory levels to maximize revenue while maintaining healthy margins. - ---- - -## ๐Ÿš€ Quick Setup (5 Minutes) - -### Step 1: Install the App - -1. Click the installation link provided by your administrator -2. You'll be redirected to Shopify to authorize the app -3. Grant the requested permissions: - - Read products - - Write products - - Read orders - -### Step 2: Initial Sync - -After installation, the system will automatically: -- Sync all your products (takes 2-5 minutes for most stores) -- Analyze your current pricing -- Generate initial pricing recommendations - -**You won't see any price changes yet** - the system starts in preview mode. - -### Step 3: Configure Safety Guardrails - -**Before enabling automation**, set your safety limits: - -``` -POST /api/operator/stores/{store_id}/guardrails -{ - "min_price": 5.00, // No product will ever be priced below this - "max_price": 500.00, // No product will ever be priced above this - "new_product_protection_days": 7 // New products won't be auto-priced for 7 days -} -``` - -**Recommended Settings:** -- **Min Price**: Your lowest acceptable price (typically cost ร— 1.2) -- **Max Price**: 2-3ร— your highest current price -- **New Product Protection**: 7-14 days - ---- - -## โš™๏ธ How It Works - -### Pricing Logic - -The system makes small price adjustments based on: - -| Condition | Action | Example | -|-----------|--------|---------| -| **Fast seller** (velocity > 5/day) + High inventory (> 20 units) | Increase price by 5% | $20.00 โ†’ $21.00 | -| **Slow seller** (velocity < 1/day) + High inventory (> 30 units) | Decrease price by 5% | $20.00 โ†’ $19.00 | -| Otherwise | No change | $20.00 โ†’ $20.00 | - -### Safety Features - -โœ… **Never below cost** - Always maintains 10% margin above product cost -โœ… **Respects your min/max** - Won't exceed your configured limits -โœ… **Protects new products** - Won't touch products for N days after first sync -โœ… **Full audit trail** - Every change is logged with reason - ---- - -## ๐ŸŽ›๏ธ Control Panel - -### Preview Mode (Test Before Applying) - -See what will change **before** it happens: - -```bash -curl -X POST http://localhost:8000/api/operator/stores/{store_id}/pricing/preview -``` - -Returns: -```json -{ - "status": "preview", - "suggestions": [ - { - "variant_id": "123456", - "old_price": 20.00, - "new_price": 21.00, - "reason": "fast_seller_increase" - } - ] -} -``` - -### Emergency Stop (Suspend Automation) - -**Press this if something feels wrong:** - -```bash -curl -X POST http://localhost:8000/api/operator/stores/{store_id}/suspend -``` - -This immediately stops all automated pricing. Your prices won't change until you resume. - -### Resume Automation - -```bash -curl -X POST http://localhost:8000/api/operator/stores/{store_id}/resume -``` - -### Rollback Last Changes - -Made a mistake? Roll back the last 10 price changes: - -```bash -curl -X POST http://localhost:8000/api/operator/stores/{store_id}/rollback?limit=10 -``` - -This reverts prices to what they were before the last pricing cycle. - ---- - -## ๐Ÿ“Š Monitoring Your Store - -### Check Current Status - -```bash -curl http://localhost:8000/api/operator/stores/{store_id}/status -``` - -Shows: -- โœ… Is automation running? -- ๐Ÿ“ฆ How many products? -- ๐Ÿ›ก๏ธ What are my guardrails? -- ๐Ÿ“ˆ Recent pricing activity - -### View Pricing History - -```bash -curl http://localhost:8000/api/operator/stores/{store_id}/pricing-history -``` - -See exactly what changed, when, and why. - ---- - -## โš ๏ธ Common Questions - -### "Will this change all my prices overnight?" - -**No.** The system: -- Makes small adjustments (5% max per cycle) -- Only changes products meeting specific criteria -- Respects all your guardrails -- Logs every change - -### "What if I don't like a price change?" - -You have 3 options: -1. **Rollback** - Undo the last N changes -2. **Suspend** - Stop automation temporarily -3. **Adjust Guardrails** - Tighten your min/max limits - -### "How do I know it's working?" - -Check these endpoints: -- `/api/operator/stores/{store_id}/status` - Overall health -- `/api/operator/stores/{store_id}/pricing-history` - What changed recently - -### "Can I exclude specific products?" - -Not yet, but coming soon. Current workaround: -- Set cost to a high value to prevent price drops -- Use min/max guardrails per store - ---- - -## ๐Ÿ†˜ When Things Go Wrong - -| Problem | Solution | -|---------|----------| -| Prices changed too much | Hit `/suspend` endpoint, then `/rollback` | -| System not making changes | Check `/status` - automation might be suspended | -| Can't see my products | Wait 5 minutes for initial sync, check `/status` | -| Getting errors | Check `/api/health/detailed` for system status | - ---- - -## ๐Ÿ“ž Need Help? - -**Operator Hotline**: Contact your system administrator -**System Health**: `http://localhost:8000/api/health/detailed` -**Full Docs**: See `OPERATOR_RUNBOOK.md` for technical details - ---- - -## โœ… Daily Checklist - -**Morning (2 minutes):** -1. Check `/status` - Is automation running? -2. Review `/pricing-history` - What changed last night? -3. Verify margins look healthy - -**Weekly (5 minutes):** -1. Review overall pricing trends -2. Adjust guardrails if needed -3. Test with `/preview` before big changes - ---- - -**Remember**: You're always in control. The system makes suggestions, you set the boundaries, and you can always rollback or suspend. diff --git a/ecom-dynamic-pricing/OPERATOR_RUNBOOK.md b/ecom-dynamic-pricing/OPERATOR_RUNBOOK.md deleted file mode 100644 index ededc33..0000000 --- a/ecom-dynamic-pricing/OPERATOR_RUNBOOK.md +++ /dev/null @@ -1,507 +0,0 @@ -# Operator Runbook -## E-Commerce Dynamic Pricing - Operations Manual - -**Audience**: DevOps, SRE, Technical Support -**Goal**: Diagnose and resolve 95% of issues without escalation - ---- - -## ๐Ÿ—๏ธ System Architecture - -``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ Merchant โ”‚ -โ”‚ Browser โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ - v -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ FastAPI Backend โ”‚ -โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ -โ”‚ โ”‚ Routes โ”‚ /api/shopify โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ /api/operator โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ /api/health โ”‚ โ”‚ -โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ โ”‚ - v v - โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” - โ”‚ PostgreSQL โ”‚ โ”‚ Redis โ”‚ - โ”‚ (Stores, โ”‚ โ”‚ (Celery โ”‚ - โ”‚ Products, โ”‚ โ”‚ Broker) โ”‚ - โ”‚ Events) โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ - v - โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” - โ”‚ Celery โ”‚ - โ”‚ Workers โ”‚ - โ”‚ (Tasks) โ”‚ - โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ - v - โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” - โ”‚ Shopify โ”‚ - โ”‚ API โ”‚ - โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -``` - ---- - -## ๐Ÿš€ Deployment - -### Prerequisites - -- Docker & Docker Compose installed -- `.env` file configured (copy from `.env.example`) -- Network access to Shopify API - -### First-Time Setup - -```bash -# Clone repo -git clone -cd ecom-dynamic-pricing - -# Configure environment -cp .env.example .env -# Edit .env with real values - -# Start services -docker-compose up --build -d - -# Verify health -curl http://localhost:8000/api/health/detailed -``` - -### Updating / Redeploying - -```bash -# Pull latest changes -git pull origin main - -# Rebuild and restart -docker-compose down -docker-compose up --build -d - -# Verify -curl http://localhost:8000/api/health -``` - ---- - -## ๐Ÿ” Health Checks - -### Quick Check - -```bash -curl http://localhost:8000/api/health -# Expected: {"status": "ok", "env": "production"} -``` - -### Comprehensive Check - -```bash -curl http://localhost:8000/api/health/detailed -``` - -**Expected Response:** -```json -{ - "status": "healthy", - "components": { - "postgres": {"status": "healthy", "message": "..."}, - "redis": {"status": "healthy", "message": "..."}, - "celery": {"status": "healthy", "message": "3 worker(s) active", "workers": [...]} - } -} -``` - -### Component-Specific Checks - -**Postgres:** -```bash -docker-compose exec db psql -U ecom -d ecom_db -c "SELECT COUNT(*) FROM stores;" -``` - -**Redis:** -```bash -docker-compose exec redis redis-cli PING -# Expected: PONG -``` - -**Celery Workers:** -```bash -docker-compose logs worker | tail -50 -# Look for: "celery@ ready" -``` - ---- - -## ๐Ÿ› Common Issues & Solutions - -### Issue 1: "No active Celery workers found" - -**Symptoms:** -- `/api/health/detailed` shows celery as unhealthy -- Pricing jobs not running -- Product syncs not happening - -**Diagnosis:** -```bash -docker-compose logs worker | tail -100 -``` - -**Common Causes:** -1. **Worker crashed** - Check for Python exceptions in logs -2. **Redis unreachable** - Verify Redis is running -3. **Import errors** - Code change broke worker startup - -**Solution:** -```bash -# Restart worker -docker-compose restart worker - -# If that fails, rebuild -docker-compose up --build -d worker -``` - ---- - -### Issue 2: "Database connection failed" - -**Symptoms:** -- `/api/health` returns 500 -- API endpoints failing - -**Diagnosis:** -```bash -docker-compose logs db | tail -50 -docker-compose ps # Check if db is running -``` - -**Solution:** -```bash -# Restart database -docker-compose restart db - -# If data is corrupted -docker-compose down -docker volume rm ecom-dynamic-pricing_ecom_pg_data # WARNING: Deletes all data -docker-compose up -d -``` - ---- - -### Issue 3: "Shopify API rate limited" - -**Symptoms:** -- Logs show "Rate limited by Shopify" -- Sync jobs failing -- HTTP 429 errors - -**Diagnosis:** -```bash -grep "Rate limited" docker-compose logs worker -``` - -**Solution:** -This is handled automatically by the client (retries with backoff), but if persistent: - -```bash -# Reduce sync frequency -# Edit celery beat schedule if using periodic tasks - -# Or contact Shopify support to increase rate limits -``` - ---- - -### Issue 4: "Pricing changes not applying" - -**Symptoms:** -- Preview works, but prices don't change -- No errors in logs - -**Checklist:** -1. **Is automation suspended?** - ```bash - curl http://localhost:8000/api/operator/stores/{store_id}/status | jq '.automation_suspended' - ``` - -2. **Are variants within guardrails?** - - Check min/max price limits - - Check new product protection window - -3. **Are Shopify credentials valid?** - ```bash - grep "Shopify API error" docker-compose logs - ``` - ---- - -### Issue 5: "Worker memory leak / high CPU" - -**Symptoms:** -- Worker container using > 2GB RAM -- Slow task execution - -**Diagnosis:** -```bash -docker stats ecom_worker -``` - -**Solution:** -```bash -# Restart worker (releases memory) -docker-compose restart worker - -# If recurring, investigate task code for memory leaks -``` - ---- - -## ๐Ÿ“Š Monitoring Dashboards - -### Key Metrics to Track - -| Metric | Command | Healthy Range | -|--------|---------|---------------| -| **API Response Time** | Check `X-Process-Time` header | < 500ms | -| **Celery Queue Size** | `docker-compose exec redis redis-cli LLEN celery` | < 100 | -| **Database Connections** | `docker-compose exec db psql -U ecom -c "SELECT count(*) FROM pg_stat_activity;"` | < 50 | -| **Worker Task Success Rate** | Check Celery logs | > 95% | -| **Shopify API Errors** | `grep "Shopify API error" logs` | < 5/hour | - -### Setting Up Sentry (Error Monitoring) - -1. Create Sentry project at sentry.io -2. Add DSN to `.env`: - ``` - SENTRY_DSN=https://your-sentry-dsn@sentry.io/project-id - ``` -3. Restart backend: - ```bash - docker-compose restart backend - ``` - -All errors will now be forwarded to Sentry automatically. - ---- - -## ๐Ÿ”’ Security Considerations - -### Secrets Management - -**Never commit:** -- `.env` file -- Shopify access tokens -- Database passwords - -**Rotation Schedule:** -- Shopify tokens: Never expire (unless revoked by merchant) -- Database passwords: Rotate every 90 days -- JWT secrets: Rotate every 180 days - -### Rate Limiting - -Shopify enforces: -- **REST API**: 2 requests/second burst, 40 requests/second average -- **Client handles this automatically** with retry logic - ---- - -## ๐Ÿšจ Incident Response - -### Level 1: Service Degraded (Non-Critical) - -**Examples:** -- Celery worker down but backup workers running -- Single store sync failing -- Slow response times - -**Response:** -1. Document issue -2. Fix within 4 business hours -3. Monitor for recurrence - ---- - -### Level 2: Service Outage (Critical) - -**Examples:** -- All workers down -- Database unreachable -- API returning 500s - -**Response:** -1. **Immediate**: Page on-call engineer -2. **Within 5 min**: Execute health checks -3. **Within 15 min**: Restore service or rollback -4. **Within 1 hour**: Root cause analysis -5. **Within 24 hours**: Post-mortem - -**Quick Rollback:** -```bash -git checkout -docker-compose up --build -d -``` - ---- - -## ๐Ÿ“ž Escalation Matrix - -| Issue | First Contact | Escalate To | -|-------|--------------|-------------| -| Infrastructure down | DevOps on-call | Engineering lead | -| Merchant reports incorrect pricing | Support team | Product manager | -| Data corruption | Database admin | CTO | -| Security incident | Security team | CTO + Legal | - ---- - -## ๐Ÿ”ง Maintenance Tasks - -### Daily (Automated via Cron) - -- Health check monitoring -- Log rotation -- Error rate alerts - -### Weekly (Manual - 15 minutes) - -```bash -# 1. Check disk usage -docker system df - -# 2. Review error logs -docker-compose logs --since 24h | grep ERROR - -# 3. Verify backups exist -ls -lh /backups/postgres/ - -# 4. Check for stuck tasks -docker-compose exec redis redis-cli LLEN celery -``` - -### Monthly (Manual - 30 minutes) - -- Review Sentry error trends -- Update dependencies (security patches) -- Database vacuum (Postgres maintenance) - ```bash - docker-compose exec db psql -U ecom -d ecom_db -c "VACUUM ANALYZE;" - ``` - ---- - -## ๐Ÿ—„๏ธ Database Operations - -### Backup - -```bash -# Manual backup -docker-compose exec db pg_dump -U ecom ecom_db > backup_$(date +%Y%m%d).sql - -# Restore -cat backup_20250113.sql | docker-compose exec -T db psql -U ecom -d ecom_db -``` - -### Schema Migrations (Alembic) - -```bash -# Generate migration -docker-compose exec backend alembic revision --autogenerate -m "description" - -# Apply migration -docker-compose exec backend alembic upgrade head -``` - ---- - -## ๐Ÿ“ˆ Performance Tuning - -### Slow API Responses - -1. Check database query performance: - ```sql - SELECT query, mean_exec_time FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 10; - ``` - -2. Add database indexes if needed - -3. Enable Redis caching for frequent queries - -### Celery Task Backlog - -```bash -# Check queue depth -docker-compose exec redis redis-cli LLEN celery - -# If > 1000: -# 1. Scale workers -docker-compose up -d --scale worker=3 - -# 2. Investigate slow tasks -docker-compose logs worker | grep "Task.*succeeded in" -``` - ---- - -## ๐Ÿงช Testing in Production - -### Safe Testing Workflow - -1. **Use a test store** - Create dummy Shopify store -2. **Enable preview mode** - Never apply prices directly -3. **Test rollback** - Verify you can undo changes -4. **Monitor logs** - Watch for errors in real-time - -### Example Test Sequence - -```bash -# 1. Sync test store -curl -X POST http://localhost:8000/api/shopify/stores/1/sync - -# 2. Preview pricing -curl -X POST http://localhost:8000/api/operator/stores/1/pricing/preview - -# 3. Apply (if safe) -curl -X POST http://localhost:8000/api/shopify/stores/1/pricing/run - -# 4. Verify -curl http://localhost:8000/api/operator/stores/1/pricing-history - -# 5. Rollback if needed -curl -X POST http://localhost:8000/api/operator/stores/1/rollback?limit=10 -``` - ---- - -## ๐Ÿ“š Additional Resources - -- **API Documentation**: http://localhost:8000/docs (Swagger UI) -- **Celery Dashboard**: Install Flower for web UI -- **Database Admin**: Use pgAdmin or DBeaver -- **Logs**: `docker-compose logs -f ` - ---- - -## โœ… Pre-Launch Checklist - -Before going live with a new merchant: - -- [ ] `.env` configured with real Shopify credentials -- [ ] Health check passes: `/api/health/detailed` -- [ ] Database backup scheduled -- [ ] Sentry configured and receiving test errors -- [ ] Guardrails set for store (min/max prices) -- [ ] Preview mode tested successfully -- [ ] Rollback tested and working -- [ ] Monitoring alerts configured -- [ ] Merchant trained on suspend/resume controls - ---- - -**Version**: 1.0 -**Last Updated**: 2025-01-13 -**Maintained By**: Engineering Team diff --git a/ecom-dynamic-pricing/README.md b/ecom-dynamic-pricing/README.md deleted file mode 100644 index e717b91..0000000 --- a/ecom-dynamic-pricing/README.md +++ /dev/null @@ -1,963 +0,0 @@ -# E-Commerce Dynamic Pricing System - -**Production-ready SaaS platform for automated Shopify product pricing using AI-driven demand signals.** - -[![FastAPI](https://img.shields.io/badge/FastAPI-0.115.0-009688.svg?style=flat&logo=FastAPI&logoColor=white)](https://fastapi.tiangolo.com) -[![React](https://img.shields.io/badge/React-18.3-61DAFB.svg?style=flat&logo=React&logoColor=black)](https://reactjs.org) -[![TypeScript](https://img.shields.io/badge/TypeScript-5.3-3178C6.svg?style=flat&logo=TypeScript&logoColor=white)](https://www.typescriptlang.org) -[![Docker](https://img.shields.io/badge/Docker-24.0-2496ED.svg?style=flat&logo=Docker&logoColor=white)](https://www.docker.com) -[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) - ---- - -## ๐Ÿ“‹ Table of Contents - -- [What Is This?](#-what-is-this) -- [โš ๏ธ Known Limitations](#๏ธ-known-limitations) -- [Quick Start](#-quick-start-5-minutes) -- [Architecture](#-architecture) -- [Features](#-features) -- [Installation](#-installation) -- [Configuration](#-configuration) -- [API Documentation](#-api-documentation) -- [Deployment](#-deployment) -- [Monitoring](#-monitoring--maintenance) -- [Troubleshooting](#-troubleshooting) -- [Documentation](#-documentation) -- [Development](#-development) -- [Contributing](#-contributing) -- [License](#-license) - ---- - -## โš ๏ธ Known Limitations - -**This is a v1.0 MVP built for speed.** It's production-ready for early customers but has known gaps: - -| Issue | Severity | Impact | -|-------|----------|--------| -| No testing suite | ๐Ÿ”ด Critical | High regression risk | -| No formal migrations | ๐Ÿ”ด Critical | Risky schema changes | -| No authentication | ๐Ÿ”ด Critical | Security risk | -| No billing system | ๐Ÿ”ด Critical | Can't monetize | -| Simplistic pricing logic | ๐ŸŸก Important | Suboptimal prices | -| No APM/monitoring | ๐ŸŸก Important | Can't identify bottlenecks | - -**Total technical debt**: ~7 months of work to address all gaps. - -**For complete details, workarounds, and remediation plan**: See **[TECHNICAL_DEBT.md](./TECHNICAL_DEBT.md)** - -**Bottom line**: This system works and is ready for early adopters. Budget 2-4 months to harden it for scale. - ---- - -## ๐ŸŽฏ What Is This? - -A **complete, production-ready SaaS platform** that automatically adjusts Shopify product prices based on: -- Sales velocity (how fast products sell) -- Inventory levels -- Configurable business rules -- Cost floors and price ceilings - -### **Key Value Propositions:** - -| For Merchants | For Operators | For Developers | -|---------------|---------------|----------------| -| โœ… Maximize revenue automatically | โœ… Monitor 100+ stores from one dashboard | โœ… Clean, documented codebase | -| โœ… Prevent stockouts and overstock | โœ… 95% of issues self-service | โœ… Type-safe TypeScript + Python | -| โœ… Full control with guardrails | โœ… Clear health checks | โœ… Docker-first architecture | -| โœ… Rollback any change instantly | โœ… Detailed runbook | โœ… Extensible pricing engine | - -### **30-Second Demo:** - -```bash -# Merchant sets guardrails: -Min price: $5.00, Max price: $500.00 - -# System analyzes sales: -Product A: Fast seller (10/day) + High inventory (50 units) -โ†’ Increase price by 5%: $20.00 โ†’ $21.00 - -Product B: Slow seller (0.5/day) + High inventory (80 units) -โ†’ Decrease price by 5%: $30.00 โ†’ $28.50 - -# Merchant reviews in preview mode: -"Looks good!" โ†’ Apply changes - -# Or if nervous: -"Wait, rollback!" โ†’ Prices reverted in 2 seconds -``` - ---- - -## โšก Quick Start (5 Minutes) - -### **Prerequisites:** -- Docker & Docker Compose installed -- Shopify Partner account (free at https://partners.shopify.com) -- 10 minutes - -### **Step 1: Clone & Configure** - -```bash -# Clone repository -git clone https://github.com/MacFall7/E-Commerce.git -cd E-Commerce/ecom-dynamic-pricing - -# Copy environment template -cp .env.example .env - -# Edit .env with your Shopify credentials -nano .env # or use your favorite editor -``` - -**Required in `.env`:** -```bash -SHOPIFY_APP_API_KEY=your_shopify_api_key_here -SHOPIFY_APP_API_SECRET=your_shopify_api_secret_here -SHOPIFY_APP_REDIRECT_URI=https://your-ngrok-url.com/api/shopify/oauth/callback -``` - -### **Step 2: Start Services** - -```bash -# Build and start all services (Postgres, Redis, Backend, Celery, Frontend) -docker-compose up --build -``` - -**Expected output:** -``` -โœ“ ecom_db - PostgreSQL ready -โœ“ ecom_redis - Redis ready -โœ“ ecom_backend - FastAPI running on :8000 -โœ“ ecom_worker - Celery worker ready -``` - -### **Step 3: Verify Health** - -```bash -# Check API health -curl http://localhost:8000/api/health/detailed - -# Expected response: -{ - "status": "healthy", - "components": { - "postgres": {"status": "healthy"}, - "redis": {"status": "healthy"}, - "celery": {"status": "healthy", "workers": ["celery@ecom_worker"]} - } -} -``` - -### **Step 4: Access Dashboards** - -Open your browser: - -- **Frontend**: http://localhost:3000 -- **Merchant Dashboard**: http://localhost:3000/merchant -- **Operator Panel**: http://localhost:3000/operator -- **API Docs (Swagger)**: http://localhost:8000/docs - -### **Step 5: Connect Shopify Store** - -```bash -# Get installation URL (replace {shop_domain}) -curl "http://localhost:8000/api/shopify/oauth/install?shop=your-store.myshopify.com" - -# Returns: -{ - "install_url": "https://your-store.myshopify.com/admin/oauth/authorize?..." -} -``` - -Visit the `install_url` in your browser to authorize the app. - -**๐ŸŽ‰ Done!** Your store is now connected and products will sync automatically. - ---- - -## ๐Ÿ—๏ธ Architecture - -### **High-Level Overview** - -``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ Shopify Store โ”‚ -โ”‚ (Products, Orders, Inventory) โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ OAuth + REST API - โ†“ -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ FastAPI Backend (Port 8000) โ”‚ -โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ -โ”‚ โ”‚ Shopify โ”‚ Operator โ”‚ Health โ”‚ โ”‚ -โ”‚ โ”‚ Routes โ”‚ Routes โ”‚ Checks โ”‚ โ”‚ -โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ†“ โ†“ โ†“ โ”‚ -โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ -โ”‚ โ”‚ Pricing โ”‚ Shopify โ”‚ Rate โ”‚ โ”‚ -โ”‚ โ”‚ Engine โ”‚ Client โ”‚ Limiter โ”‚ โ”‚ -โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ โ”‚ - โ†“ โ†“ -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ PostgreSQL DB โ”‚ โ”‚ Redis Cache โ”‚ -โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ -โ”‚ โ”‚ Stores โ”‚ โ”‚ โ”‚ โ”‚ Celery Broker โ”‚ โ”‚ -โ”‚ โ”‚ Products โ”‚ โ”‚ โ”‚ โ”‚ Task Results โ”‚ โ”‚ -โ”‚ โ”‚ PricingEvents โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ -โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ - โ†“ - โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” - โ”‚ Celery Worker(s) โ”‚ - โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ - โ”‚ โ”‚ sync_products โ”‚ โ”‚ - โ”‚ โ”‚ run_pricing_cycleโ”‚ โ”‚ - โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ - โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ†‘ - โ”‚ -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ React Frontend (Port 3000) โ”‚ -โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ -โ”‚ โ”‚ Merchant Dashboard โ”‚ Operator Panel โ”‚ โ”‚ -โ”‚ โ”‚ - Control Panel โ”‚ - System Health โ”‚ โ”‚ -โ”‚ โ”‚ - Guardrails โ”‚ - Worker Monitor โ”‚ โ”‚ -โ”‚ โ”‚ - Pricing History โ”‚ - Recent Activity โ”‚ โ”‚ -โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -``` - -### **Technology Stack** - -| Layer | Technologies | -|-------|-------------| -| **Frontend** | React 18, TypeScript, Tailwind CSS, Vite, React Router, Axios | -| **Backend** | FastAPI, Python 3.11, Pydantic, SQLAlchemy, Celery | -| **Database** | PostgreSQL 16 | -| **Cache/Queue** | Redis 7 | -| **Infrastructure** | Docker, Docker Compose | -| **Monitoring** | Sentry (optional), Structured Logging | - -### **Data Flow** - -1. **OAuth Installation**: Merchant installs app โ†’ Shopify redirects to `/oauth/callback` โ†’ Store record created -2. **Product Sync**: Celery task `sync_store_products` runs โ†’ Fetches products from Shopify API โ†’ Stores in PostgreSQL -3. **Pricing Analysis**: Celery task `run_pricing_cycle` runs โ†’ Pricing engine analyzes variants โ†’ Generates suggestions -4. **Preview Mode**: Suggestions returned without applying โ†’ Merchant reviews in UI -5. **Apply Changes**: Suggestions approved โ†’ Shopify API updates prices โ†’ Events logged in database -6. **Rollback**: Merchant clicks rollback โ†’ Last N events retrieved โ†’ Prices reverted in Shopify - ---- - -## โœจ Features - -### **A. Infrastructure Stability** - -| Feature | Description | Benefit | -|---------|-------------|---------| -| **Retry Logic** | 3 attempts, exponential backoff, jitter | No transient failures | -| **Timeout Guardrails** | 10min sync, 15min pricing (soft+hard limits) | No infinite loops | -| **Rate Limiting** | Shopify 429 auto-retry with `Retry-After` | Never hit rate limits | -| **Health Checks** | Postgres, Redis, Celery worker monitoring | Know instantly if something breaks | -| **Structured Logging** | Timestamp, level, source, message, duration | Debug issues in seconds | -| **Sentry Integration** | Auto-forward errors to Sentry.io | Get notified before users complain | - -### **B. Merchant Safety** - -| Feature | Description | Benefit | -|---------|-------------|---------| -| **Preview Mode** | See changes before applying | Zero surprises | -| **Rollback** | Undo last 1-50 price changes | Instant recovery | -| **Suspend/Resume** | Emergency stop button | Merchant stays in control | -| **Min/Max Limits** | Global price floor and ceiling | Never go too low/high | -| **Cost Floor** | Never drop below cost + 10% margin | Protect profit margins | -| **New Product Protection** | Don't auto-price for N days | Let new products stabilize | -| **Full Audit Trail** | Every change logged with reason | Complete transparency | - -### **C. Operator Tools** - -| Feature | Description | Benefit | -|---------|-------------|---------| -| **System Health Dashboard** | Real-time Postgres, Redis, Celery status | Diagnose in 30 seconds | -| **Worker Monitor** | See active workers and task counts | Know if workers are stuck | -| **Pricing History** | Cross-store event timeline | Spot patterns and anomalies | -| **Manual Triggers** | Run pricing cycle on-demand | Test changes safely | -| **Guardrail Editor** | Update limits without code changes | Iterate quickly | - -### **D. Developer Experience** - -| Feature | Description | Benefit | -|---------|-------------|---------| -| **Type Safety** | TypeScript frontend, Pydantic backend | Catch errors at compile time | -| **API Documentation** | Auto-generated Swagger UI at `/docs` | No manual API docs needed | -| **Docker First** | All services containerized | Consistent dev/prod environments | -| **Hot Reload** | Vite (frontend), uvicorn `--reload` (backend) | Fast iteration | -| **Clean Architecture** | Separated routes, models, services | Easy to extend | - ---- - -## ๐Ÿ“ฆ Installation - -### **System Requirements** - -- **OS**: Linux, macOS, or Windows (with WSL2) -- **Docker**: 24.0 or later -- **Docker Compose**: 2.0 or later -- **RAM**: 4GB minimum, 8GB recommended -- **Disk**: 10GB free space - -### **Full Installation Steps** - -#### **1. Clone Repository** - -```bash -git clone https://github.com/MacFall7/E-Commerce.git -cd E-Commerce/ecom-dynamic-pricing -``` - -#### **2. Configure Environment** - -```bash -# Copy template -cp .env.example .env - -# Edit with your credentials -nano .env -``` - -**Required Variables:** -```bash -# Shopify API Credentials (from Shopify Partner Dashboard) -SHOPIFY_APP_API_KEY= -SHOPIFY_APP_API_SECRET= -SHOPIFY_APP_REDIRECT_URI=/api/shopify/oauth/callback - -# Optional: Sentry for Error Monitoring -SENTRY_DSN= -``` - -**Optional Variables** (defaults work for local dev): -```bash -# Database -POSTGRES_USER=ecom -POSTGRES_PASSWORD=ecom_password -POSTGRES_DB=ecom_db - -# Redis -REDIS_URL=redis://redis:6379/0 - -# App -APP_ENV=local -APP_DEBUG=true -``` - -#### **3. Start Services** - -```bash -# Build and start all containers -docker-compose up --build - -# Or run in background -docker-compose up -d --build -``` - -**Services Started:** -- `ecom_db` - PostgreSQL on port 5432 -- `ecom_redis` - Redis on port 6379 -- `ecom_backend` - FastAPI on port 8000 -- `ecom_worker` - Celery worker (no exposed port) - -#### **4. Verify Installation** - -```bash -# Check all containers are running -docker-compose ps - -# Check API health -curl http://localhost:8000/api/health/detailed - -# Check database -docker-compose exec db psql -U ecom -d ecom_db -c "\dt" -``` - -#### **5. Install Frontend (Optional)** - -```bash -cd frontend -npm install -npm run dev -``` - -Frontend runs on http://localhost:3000 - ---- - -## โš™๏ธ Configuration - -### **Environment Variables Reference** - -| Variable | Required | Default | Description | -|----------|----------|---------|-------------| -| `SHOPIFY_APP_API_KEY` | โœ… | - | Your Shopify app's public API key | -| `SHOPIFY_APP_API_SECRET` | โœ… | - | Your Shopify app's secret key | -| `SHOPIFY_APP_REDIRECT_URI` | โœ… | - | OAuth callback URL (must be publicly accessible) | -| `POSTGRES_USER` | โŒ | `ecom` | PostgreSQL username | -| `POSTGRES_PASSWORD` | โŒ | `ecom_password` | PostgreSQL password | -| `POSTGRES_DB` | โŒ | `ecom_db` | PostgreSQL database name | -| `REDIS_URL` | โŒ | `redis://redis:6379/0` | Redis connection string | -| `APP_ENV` | โŒ | `local` | Environment (local, staging, production) | -| `APP_DEBUG` | โŒ | `true` | Enable debug mode | -| `SENTRY_DSN` | โŒ | - | Sentry error tracking DSN | - -### **Shopify App Setup** - -1. Go to https://partners.shopify.com -2. Create new app โ†’ Public app -3. Set **App URL**: `https://your-ngrok-url.com` -4. Set **Allowed redirection URL(s)**: `https://your-ngrok-url.com/api/shopify/oauth/callback` -5. Set **API scopes**: `read_products,write_products,read_orders` -6. Copy API key and secret to `.env` - -### **Guardrails Configuration** - -Set via API or UI: - -```bash -curl -X PUT http://localhost:8000/api/operator/stores/1/guardrails \ - -H "Content-Type: application/json" \ - -d '{ - "min_price_limit": 5.00, - "max_price_limit": 500.00, - "new_product_protection_days": 7 - }' -``` - ---- - -## ๐Ÿ“š API Documentation - -### **Interactive Docs** - -- **Swagger UI**: http://localhost:8000/docs -- **ReDoc**: http://localhost:8000/redoc - -### **Core Endpoints** - -#### **Health Checks** - -```http -GET /health -GET /api/health/detailed -GET /api/health/readiness # Kubernetes probe -GET /api/health/liveness # Kubernetes probe -``` - -#### **Shopify Integration** - -```http -GET /api/shopify/oauth/install?shop={shop_domain} -GET /api/shopify/oauth/callback -POST /api/shopify/stores/{store_id}/pricing/run -``` - -#### **Operator Controls** - -```http -GET /api/operator/stores/{store_id}/status -POST /api/operator/stores/{store_id}/suspend -POST /api/operator/stores/{store_id}/resume -POST /api/operator/stores/{store_id}/pricing/preview -POST /api/operator/stores/{store_id}/rollback?limit={N} -PUT /api/operator/stores/{store_id}/guardrails -GET /api/operator/stores/{store_id}/pricing-history?limit={N} -``` - -### **Example API Calls** - -**Get Store Status:** -```bash -curl http://localhost:8000/api/operator/stores/1/status -``` - -**Response:** -```json -{ - "store_id": 1, - "shop_domain": "my-store.myshopify.com", - "is_active": true, - "automation_suspended": false, - "product_count": 150, - "guardrails": { - "min_price_limit": 5.00, - "max_price_limit": 500.00, - "new_product_protection_days": 7 - }, - "recent_events_count": 23 -} -``` - -**Preview Pricing:** -```bash -curl -X POST http://localhost:8000/api/operator/stores/1/pricing/preview -``` - -**Response:** -```json -{ - "status": "preview", - "store_id": 1, - "suggestions": [ - { - "variant_id": "12345", - "old_price": 20.00, - "new_price": 21.00, - "reason": "fast_seller_increase" - } - ], - "total_suggestions": 150 -} -``` - ---- - -## ๐Ÿš€ Deployment - -### **Docker Compose (Production)** - -```bash -# Pull latest code -git pull origin main - -# Update .env for production -cp .env.example .env -nano .env # Set production values - -# Start services -docker-compose up -d --build - -# Verify -docker-compose ps -curl https://your-domain.com/api/health/detailed -``` - -### **Environment-Specific Overrides** - -Create `docker-compose.prod.yml`: - -```yaml -version: "3.9" - -services: - backend: - environment: - APP_ENV: production - APP_DEBUG: false - restart: always - - worker: - restart: always -``` - -Run with: -```bash -docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d -``` - -### **Kubernetes (Advanced)** - -See `k8s/` directory for Kubernetes manifests (to be created). - -Key considerations: -- Use Kubernetes Secrets for sensitive env vars -- Use `readiness` and `liveness` probes -- Set resource limits (CPU, memory) -- Use Persistent Volumes for PostgreSQL -- Scale workers with HPA based on queue depth - -### **Database Migrations** - -```bash -# Generate migration -docker-compose exec backend alembic revision --autogenerate -m "description" - -# Apply migration -docker-compose exec backend alembic upgrade head - -# Rollback migration -docker-compose exec backend alembic downgrade -1 -``` - -### **Backup Strategy** - -**PostgreSQL:** -```bash -# Backup -docker-compose exec db pg_dump -U ecom ecom_db > backup_$(date +%Y%m%d).sql - -# Restore -cat backup_20250113.sql | docker-compose exec -T db psql -U ecom -d ecom_db -``` - -**Redis:** -```bash -# Redis persists to disk automatically (RDB snapshots) -# Backup: Copy /data/dump.rdb from container -docker cp ecom_redis:/data/dump.rdb ./redis_backup.rdb -``` - ---- - -## ๐Ÿ“Š Monitoring & Maintenance - -### **Health Monitoring** - -**Automated Checks:** -```bash -# Add to cron (every 5 minutes) -*/5 * * * * curl -f http://localhost:8000/api/health/detailed || echo "Health check failed" | mail -s "Alert" ops@example.com -``` - -**Key Metrics to Track:** -- API response time (< 500ms) -- Celery queue depth (< 100) -- Database connections (< 50) -- Worker task success rate (> 95%) -- Shopify API error rate (< 1%) - -### **Sentry Integration** - -1. Sign up at https://sentry.io (free tier available) -2. Create new project -3. Copy DSN to `.env`: - ``` - SENTRY_DSN=https://...@sentry.io/... - ``` -4. Restart backend: `docker-compose restart backend` - -All errors now auto-forward to Sentry with: -- Stack traces -- Request context -- User information -- Environment tags - -### **Log Management** - -**View logs:** -```bash -# All services -docker-compose logs -f - -# Specific service -docker-compose logs -f backend -docker-compose logs -f worker - -# Last 100 lines -docker-compose logs --tail=100 backend -``` - -**Log rotation:** -```yaml -# Add to docker-compose.yml under each service -logging: - driver: "json-file" - options: - max-size: "10m" - max-file: "3" -``` - -### **Performance Tuning** - -**Database:** -```sql --- Check slow queries -SELECT query, mean_exec_time -FROM pg_stat_statements -ORDER BY mean_exec_time DESC -LIMIT 10; - --- Vacuum and analyze -VACUUM ANALYZE; -``` - -**Celery:** -```bash -# Scale workers -docker-compose up -d --scale worker=3 - -# Monitor queue -docker-compose exec redis redis-cli LLEN celery -``` - ---- - -## ๐Ÿ”ง Troubleshooting - -### **Common Issues** - -| Problem | Solution | -|---------|----------| -| **"No active Celery workers"** | `docker-compose restart worker` | -| **"Database connection failed"** | Check `docker-compose logs db`, ensure container is running | -| **"Shopify API rate limited"** | Automatic retry with backoff - wait 60s | -| **"Frontend can't connect to API"** | Ensure backend is running on port 8000, check CORS settings | -| **"Pricing not applying"** | Check if automation is suspended: `/api/operator/stores/1/status` | -| **"Docker build fails"** | Clear cache: `docker-compose build --no-cache` | - -### **Debug Mode** - -Enable detailed logging: - -```bash -# In .env -APP_DEBUG=true - -# Restart -docker-compose restart backend -``` - -Now logs include: -- Full stack traces -- SQL queries -- API request/response bodies - -### **Reset Everything** - -```bash -# Stop all containers -docker-compose down - -# Remove volumes (WARNING: deletes all data) -docker volume rm ecom-dynamic-pricing_ecom_pg_data - -# Rebuild from scratch -docker-compose up --build -``` - -### **Check System Resources** - -```bash -# Container stats -docker stats - -# Disk usage -docker system df - -# Clean up unused images -docker system prune -a -``` - ---- - -## ๐Ÿ“– Documentation - -### **For Different Audiences** - -| Audience | Document | Location | -|----------|----------|----------| -| **Merchants** | Quick Start Guide | [`MERCHANT_QUICKSTART.md`](./MERCHANT_QUICKSTART.md) | -| **Operators** | Operations Runbook | [`OPERATOR_RUNBOOK.md`](./OPERATOR_RUNBOOK.md) | -| **Developers** | Frontend README | [`frontend/README.md`](./frontend/README.md) | -| **New Team Lead** | Handoff Document | [`HANDOFF.md`](./HANDOFF.md) | -| **Technical Leads** | Known Limitations & Debt | [`TECHNICAL_DEBT.md`](./TECHNICAL_DEBT.md) | -| **Everyone** | This README | [`README.md`](./README.md) | - -### **Additional Resources** - -- **API Reference**: http://localhost:8000/docs (Swagger UI) -- **Architecture Decisions**: See inline code comments -- **Pricing Engine Logic**: [`backend/app/pricing_engine.py`](./backend/app/pricing_engine.py) -- **Database Schema**: [`backend/app/models.py`](./backend/app/models.py) -- **Technical Debt**: See [`TECHNICAL_DEBT.md`](./TECHNICAL_DEBT.md) for known gaps and remediation plan - ---- - -## ๐Ÿ‘จโ€๐Ÿ’ป Development - -### **Local Development Setup** - -```bash -# Backend (with hot reload) -docker-compose up backend worker - -# Frontend (with hot reload) -cd frontend -npm install -npm run dev -``` - -Changes to Python code auto-reload. Changes to React code hot-reload in browser. - -### **Code Structure** - -``` -ecom-dynamic-pricing/ -โ”œโ”€โ”€ backend/ -โ”‚ โ”œโ”€โ”€ app/ -โ”‚ โ”‚ โ”œโ”€โ”€ main.py # FastAPI app entrypoint -โ”‚ โ”‚ โ”œโ”€โ”€ config.py # Settings management -โ”‚ โ”‚ โ”œโ”€โ”€ database.py # SQLAlchemy setup -โ”‚ โ”‚ โ”œโ”€โ”€ models.py # Database models -โ”‚ โ”‚ โ”œโ”€โ”€ schemas.py # Pydantic schemas -โ”‚ โ”‚ โ”œโ”€โ”€ pricing_engine.py # Pricing logic -โ”‚ โ”‚ โ”œโ”€โ”€ shopify_client.py # Shopify API wrapper -โ”‚ โ”‚ โ”œโ”€โ”€ celery_app.py # Celery configuration -โ”‚ โ”‚ โ”œโ”€โ”€ tasks.py # Background tasks -โ”‚ โ”‚ โ””โ”€โ”€ routes/ -โ”‚ โ”‚ โ”œโ”€โ”€ shopify.py # Shopify endpoints -โ”‚ โ”‚ โ”œโ”€โ”€ operator.py # Operator endpoints -โ”‚ โ”‚ โ””โ”€โ”€ health.py # Health checks -โ”‚ โ”œโ”€โ”€ requirements.txt -โ”‚ โ””โ”€โ”€ Dockerfile -โ”œโ”€โ”€ frontend/ -โ”‚ โ”œโ”€โ”€ src/ -โ”‚ โ”‚ โ”œโ”€โ”€ components/ # React components -โ”‚ โ”‚ โ”œโ”€โ”€ pages/ # Page components -โ”‚ โ”‚ โ”œโ”€โ”€ lib/ # API client, types -โ”‚ โ”‚ โ””โ”€โ”€ App.tsx # Main app -โ”‚ โ””โ”€โ”€ package.json -โ”œโ”€โ”€ docker-compose.yml -โ”œโ”€โ”€ .env.example -โ”œโ”€โ”€ MERCHANT_QUICKSTART.md -โ”œโ”€โ”€ OPERATOR_RUNBOOK.md -โ””โ”€โ”€ README.md -``` - -### **Adding a New Feature** - -1. **Define Models** (`models.py`) -2. **Create Schemas** (`schemas.py`) -3. **Add Route** (`routes/*.py`) -4. **Write Task** (`tasks.py`) if async -5. **Update Frontend** (`frontend/src/`) -6. **Write Tests** -7. **Document** (update this README) - -### **Testing** - -```bash -# Backend tests (to be added) -docker-compose exec backend pytest - -# Frontend tests (to be added) -cd frontend && npm test - -# Integration tests (to be added) -./scripts/integration-tests.sh -``` - -### **Code Quality** - -```bash -# Linting -docker-compose exec backend flake8 -cd frontend && npm run lint - -# Type checking -cd frontend && npm run type-check - -# Formatting -docker-compose exec backend black . -cd frontend && npm run format -``` - ---- - -## ๐Ÿค Contributing - -We welcome contributions! Please follow these guidelines: - -### **How to Contribute** - -1. **Fork the repository** -2. **Create feature branch**: `git checkout -b feature/amazing-feature` -3. **Commit changes**: `git commit -m 'Add amazing feature'` -4. **Push to branch**: `git push origin feature/amazing-feature` -5. **Open Pull Request** - -### **PR Checklist** - -- [ ] Code follows existing style -- [ ] All tests pass -- [ ] Documentation updated -- [ ] CHANGELOG.md updated -- [ ] No breaking changes (or clearly documented) - -### **Code Style** - -- **Python**: Follow PEP 8, use Black formatter -- **TypeScript**: Follow Airbnb style guide, use Prettier -- **Commits**: Use conventional commits (`feat:`, `fix:`, `docs:`, etc.) - ---- - -## ๐Ÿ“„ License - -This project is licensed under the **MIT License** - see the [LICENSE](LICENSE) file for details. - -### **MIT License Summary** - -โœ… Commercial use -โœ… Modification -โœ… Distribution -โœ… Private use - -โŒ Liability -โŒ Warranty - ---- - -## ๐Ÿ™ Acknowledgments - -- **FastAPI** - Modern, fast web framework -- **React** - UI library -- **Shopify** - E-commerce platform -- **Celery** - Distributed task queue -- **PostgreSQL** - Reliable database -- **Redis** - In-memory data store - ---- - -## ๐Ÿ“ž Support - -- **Issues**: https://github.com/MacFall7/E-Commerce/issues -- **Discussions**: https://github.com/MacFall7/E-Commerce/discussions -- **Email**: support@example.com - ---- - -## ๐Ÿ—บ๏ธ Roadmap - -- [x] Core pricing engine -- [x] Shopify integration -- [x] Merchant dashboard -- [x] Operator panel -- [x] Production stability features -- [ ] Multi-variant pricing strategies -- [ ] Machine learning price optimization -- [ ] Competitor price monitoring -- [ ] A/B testing for pricing -- [ ] Mobile app (React Native) -- [ ] Multi-channel support (Amazon, eBay) - ---- - -## ๐Ÿ“ˆ Status - -**Current Version**: 1.0.0 -**Status**: Production Ready โœ… -**Last Updated**: 2025-01-13 - ---- - -
- -**Built with โค๏ธ for merchants who want to maximize revenue without the complexity** - -[Get Started](#-quick-start-5-minutes) โ€ข [View Demo](#) โ€ข [Report Bug](https://github.com/MacFall7/E-Commerce/issues) - -
diff --git a/ecom-dynamic-pricing/ROADMAP.md b/ecom-dynamic-pricing/ROADMAP.md deleted file mode 100644 index c4b9869..0000000 --- a/ecom-dynamic-pricing/ROADMAP.md +++ /dev/null @@ -1,119 +0,0 @@ -# Development Roadmap - -**Current Version**: v1.0.0 (MVP - Fully Audited) -**Status**: Production-ready for early customers (< 50 stores) - ---- - -## ๐Ÿ“ Where We Are - -โœ… **Complete MVP** with: -- Backend API (FastAPI + Postgres + Celery + Redis) -- Frontend dashboards (Merchant + Operator) -- Safety features (preview, rollback, guardrails) -- Comprehensive documentation (6 docs, 3,200+ lines) -- Production stability (retry logic, health checks, logging) - -โš ๏ธ **Known gaps**: See **[TECHNICAL_DEBT.md](./TECHNICAL_DEBT.md)** for complete audit (13 issues, ~7 months work) - ---- - -## ๐Ÿ—บ๏ธ Where We're Going - -This roadmap follows the **4-phase remediation plan** detailed in [TECHNICAL_DEBT.md](./TECHNICAL_DEBT.md): - -### **Phase 1: Foundation** (Months 1-2) -**Goal**: Make it safe to change code - -| Task | Effort | Priority | -|------|--------|----------| -| Add testing suite (pytest + Jest) | 3 weeks | ๐Ÿ”ด Critical | -| Setup Alembic migrations | 1 week | ๐Ÿ”ด Critical | -| Add user authentication (JWT + RBAC) | 2 weeks | ๐Ÿ”ด Critical | - -**Outcome**: Can iterate quickly without breaking things - ---- - -### **Phase 2: Monetization** (Months 3-4) -**Goal**: Generate revenue - -| Task | Effort | Priority | -|------|--------|----------| -| Integrate Stripe billing | 2 weeks | ๐Ÿ”ด Critical | -| Add subscription tiers | 1 week | ๐Ÿ”ด Critical | -| Add user-facing rate limiting | 2 days | ๐ŸŸก Important | - -**Outcome**: Can charge merchants and enforce plan limits - ---- - -### **Phase 3: Scaling** (Months 5-6) -**Goal**: Handle growth (50-500 stores) - -| Task | Effort | Priority | -|------|--------|----------| -| Add APM (DataDog/New Relic) | 1 week | ๐ŸŸก Important | -| Setup worker autoscaling (K8s HPA) | 2 weeks | ๐ŸŸก Important | -| Add email notifications | 4 days | ๐ŸŸก Important | - -**Outcome**: System scales with demand and merchants stay informed - ---- - -### **Phase 4: Intelligence** (Months 7-12) -**Goal**: Competitive advantage - -| Task | Effort | Priority | -|------|--------|----------| -| ML pricing engine (XGBoost) | 3 months | ๐ŸŸก Important | -| Competitor price monitoring | 3 weeks | ๐ŸŸข Nice-to-Have | -| Advanced analytics dashboard | 2 weeks | ๐ŸŸข Nice-to-Have | - -**Outcome**: Better prices than competitors, data-driven insights - ---- - -## ๐ŸŽฏ Milestones - -| Milestone | Timeline | Description | -|-----------|----------|-------------| -| **v1.0** | โœ… Complete | MVP with full documentation | -| **v1.5** | Month 2 | Foundation complete (tests, migrations, auth) | -| **v2.0** | Month 4 | Revenue-ready (billing, subscriptions) | -| **v2.5** | Month 6 | Scale-ready (APM, autoscaling) | -| **v3.0** | Month 12 | Intelligence complete (ML, competitors) | - ---- - -## ๐Ÿ“‹ Quick Reference - -| Document | Use This When... | -|----------|------------------| -| [TECHNICAL_DEBT.md](./TECHNICAL_DEBT.md) | Planning sprints, prioritizing work | -| [HANDOFF.md](./HANDOFF.md) | Onboarding new team members | -| [README.md](./README.md) | Getting started, understanding architecture | - ---- - -## โœ… Success Metrics - -Track these to know you're on the right path: - -| Metric | Current | Target (v2.0) | Target (v3.0) | -|--------|---------|---------------|---------------| -| Test Coverage | 0% | 80% | 90% | -| Stores Supported | < 10 | 50 | 500 | -| Uptime | - | 99.5% | 99.9% | -| Pricing Accuracy | Good | Better | Best-in-class | -| Monthly Revenue | $0 | $10k | $100k | - ---- - -
- -**This roadmap is a living document. Update as priorities shift.** - -[Back to README](./README.md) โ€ข [View Technical Debt](./TECHNICAL_DEBT.md) โ€ข [Handoff Guide](./HANDOFF.md) - -
diff --git a/ecom-dynamic-pricing/TECHNICAL_DEBT.md b/ecom-dynamic-pricing/TECHNICAL_DEBT.md deleted file mode 100644 index a8535f3..0000000 --- a/ecom-dynamic-pricing/TECHNICAL_DEBT.md +++ /dev/null @@ -1,744 +0,0 @@ -# Technical Debt & Known Weaknesses - -**Last Updated**: 2025-01-13 -**Status**: Production-ready but with known gaps - ---- - -## ๐ŸŽฏ Philosophy - -This system was built with the **"get it running first, perfect it later"** approach. It's production-ready for early customers, but there are known weaknesses you should address as you scale. - -**Honest Assessment**: This is a **v1.0 MVP**, not a **v5.0 enterprise system**. - ---- - -## ๐Ÿ“Š Severity Categories - -| Level | Description | Timeline | -|-------|-------------|----------| -| ๐Ÿ”ด **Critical** | Must fix before scaling to 50+ stores | 1-2 months | -| ๐ŸŸก **Important** | Should fix before major features | 3-6 months | -| ๐ŸŸข **Nice-to-Have** | Quality of life improvements | 6-12 months | - ---- - -## ๐Ÿ”ด Critical Gaps - -### **1. No Testing Suite** - -**Problem:** -- Zero unit tests -- Zero integration tests -- Zero E2E tests -- Changes require manual verification - -**Impact:** -- High risk of regression bugs -- Slow development velocity -- Difficult to refactor with confidence - -**Current Workaround:** -```bash -# Manual testing checklist (see OPERATOR_RUNBOOK.md) -1. Check health endpoint -2. Connect test store -3. Run preview mode -4. Apply pricing -5. Test rollback -``` - -**Proper Fix:** -```bash -# Backend testing (pytest) -backend/tests/ -โ”œโ”€โ”€ unit/ -โ”‚ โ”œโ”€โ”€ test_pricing_engine.py -โ”‚ โ”œโ”€โ”€ test_shopify_client.py -โ”‚ โ””โ”€โ”€ test_models.py -โ”œโ”€โ”€ integration/ -โ”‚ โ”œโ”€โ”€ test_api.py -โ”‚ โ””โ”€โ”€ test_tasks.py -โ””โ”€โ”€ conftest.py - -# Frontend testing (Jest + React Testing Library) -frontend/src/ -โ”œโ”€โ”€ components/ -โ”‚ โ””โ”€โ”€ __tests__/ -โ””โ”€โ”€ pages/ - โ””โ”€โ”€ __tests__/ - -# Example test -def test_pricing_engine_increases_fast_seller(): - engine = PricingEngine() - result = engine.suggest_prices([{ - "variant_id": "123", - "current_price": 20.0, - "sales_velocity": 10.0, # Fast - "inventory_quantity": 50 - }]) - assert result[0]["new_price"] > 20.0 - assert result[0]["reason"] == "fast_seller_increase" -``` - -**Effort**: 2-3 weeks for 80% coverage - -**Priority**: Fix this **first** before adding major features - ---- - -### **2. No Formal Database Migrations** - -**Problem:** -- Schema changes done via `Base.metadata.create_all()` -- No migration history -- Can't rollback schema changes -- Manual production updates risky - -**Impact:** -- Database changes are destructive -- Can't easily add columns to production -- No audit trail of schema changes - -**Current Workaround:** -```bash -# Schema changes require manual SQL -docker-compose exec db psql -U ecom -d ecom_db -c "ALTER TABLE stores ADD COLUMN new_field VARCHAR(255);" -``` - -**Proper Fix:** -```bash -# Setup Alembic -cd backend -pip install alembic -alembic init migrations - -# Create migration -alembic revision --autogenerate -m "Add new_field to stores" - -# Apply migration -alembic upgrade head - -# Rollback -alembic downgrade -1 -``` - -**Implementation:** -```python -# alembic/env.py -from app.database import Base -from app.models import Store, Product, PricingEvent - -target_metadata = Base.metadata - -# Then migrations are automatic -alembic revision --autogenerate -m "description" -``` - -**Effort**: 1 week to set up + retrofit existing schema - -**Priority**: Fix before first production schema change - ---- - -### **3. No Authentication/Authorization** - -**Problem:** -- No user accounts -- No login system -- API endpoints are public (anyone with URL can call) -- No role-based access control (RBAC) - -**Impact:** -- Can't have multiple users per store -- No audit of who did what -- Operator panel is accessible to anyone -- Can't restrict pricing changes to admins - -**Current Workaround:** -- Deploy behind VPN -- Use IP whitelisting -- Assume single user per store - -**Proper Fix:** -```python -# Add User model -class User(Base): - __tablename__ = "users" - id = Column(Integer, primary_key=True) - email = Column(String, unique=True) - password_hash = Column(String) # bcrypt - role = Column(String) # "merchant", "operator", "admin" - store_id = Column(Integer, ForeignKey("stores.id")) - -# Add JWT authentication -from fastapi import Depends, HTTPException -from fastapi.security import OAuth2PasswordBearer - -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") - -async def get_current_user(token: str = Depends(oauth2_scheme)): - # Verify JWT token - # Return user - pass - -# Protect routes -@router.post("/pricing/run") -async def run_pricing( - store_id: int, - current_user: User = Depends(get_current_user) -): - if current_user.role != "merchant": - raise HTTPException(403, "Not authorized") - # ... -``` - -**Effort**: 2-3 weeks for basic auth, 1 month for full RBAC - -**Priority**: Fix before handling sensitive merchant data - ---- - -### **4. No Billing/Subscription System** - -**Problem:** -- No way to charge merchants -- No subscription tiers -- No usage tracking -- No payment integration - -**Impact:** -- Can't monetize -- No revenue -- Can't limit features by plan - -**Current Workaround:** -- Manual invoicing -- Honor system -- Or make it free during beta - -**Proper Fix:** -```python -# Add Subscription model -class Subscription(Base): - __tablename__ = "subscriptions" - id = Column(Integer, primary_key=True) - store_id = Column(Integer, ForeignKey("stores.id")) - plan = Column(String) # "free", "pro", "enterprise" - stripe_subscription_id = Column(String) - status = Column(String) # "active", "canceled", "past_due" - current_period_end = Column(DateTime) - -# Integrate Stripe -import stripe - -@router.post("/subscribe") -async def create_subscription(plan: str, store_id: int): - subscription = stripe.Subscription.create( - customer=store.stripe_customer_id, - items=[{"price": PLAN_PRICES[plan]}] - ) - # Save to DB -``` - -**Effort**: 2 weeks for Stripe integration, 1 week for plan limits - -**Priority**: Fix before public launch - ---- - -## ๐ŸŸก Important Gaps - -### **5. Simplistic Pricing Engine** - -**Problem:** -- Uses simple thresholds (velocity > 5, inventory > 20) -- No machine learning -- No seasonality -- No competitor monitoring -- No demand forecasting - -**Impact:** -- Suboptimal prices (leaves money on table) -- Can't handle complex scenarios -- Not differentiated from competitors - -**Current Workaround:** -- The simple logic actually works for 80% of cases -- Merchants can adjust guardrails to compensate - -**Better Approach:** -```python -# ML-based pricing -class MLPricingEngine: - def __init__(self): - self.model = self.load_model() # XGBoost, LightGBM - - def suggest_prices(self, variants): - features = self.extract_features(variants) - # Features: sales_velocity, inventory, day_of_week, - # season, historical_demand, cost, margin - predictions = self.model.predict(features) - return predictions - - def extract_features(self, variants): - # Engineer features from: - # - Order history (sales trends) - # - Inventory trends - # - Time features (seasonality) - # - Product attributes - pass -``` - -**Effort**: 2-3 months for ML pipeline, data collection, training - -**Priority**: Fix after 50+ stores (need training data first) - ---- - -### **6. No APM/Monitoring** - -**Problem:** -- No performance monitoring -- No request tracing -- No database query profiling -- Only basic health checks - -**Impact:** -- Can't identify slow endpoints -- Can't trace errors across services -- Can't optimize bottlenecks - -**Current Workaround:** -- Manual log analysis -- Sentry for errors only -- Response time in headers (`X-Process-Time`) - -**Proper Fix:** -```python -# Add DataDog, New Relic, or Elastic APM -import ddtrace -from ddtrace import tracer - -ddtrace.patch_all() # Auto-instrument FastAPI, SQLAlchemy, Redis - -@tracer.wrap() -def expensive_function(): - # Traced automatically - pass - -# Or Prometheus + Grafana -from prometheus_fastapi_instrumentator import Instrumentator - -Instrumentator().instrument(app).expose(app) -``` - -**Effort**: 1 week for APM setup, 1 week for dashboard creation - -**Priority**: Fix after first 10 paying customers - ---- - -### **7. No Worker Autoscaling** - -**Problem:** -- Worker count is fixed (default 1) -- Can't handle traffic spikes -- Manual scaling only - -**Impact:** -- Queue backs up during high load -- Slow processing during peak times -- Wasted resources during low load - -**Current Workaround:** -```bash -# Manual scaling -docker-compose up -d --scale worker=3 -``` - -**Proper Fix:** -```yaml -# Kubernetes HPA -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: celery-worker -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: celery-worker - minReplicas: 1 - maxReplicas: 10 - metrics: - - type: External - external: - metric: - name: celery_queue_length - target: - type: AverageValue - averageValue: "100" # Scale if queue > 100 -``` - -**Effort**: 2 weeks for K8s + HPA setup - -**Priority**: Fix when handling 100+ stores - ---- - -### **8. No Rate Limiting (User-Facing)** - -**Problem:** -- Merchants can spam API endpoints -- No protection against abuse -- No per-store rate limits - -**Impact:** -- API can be overloaded -- No defense against malicious actors - -**Current Workaround:** -- Trust merchants not to abuse -- Backend rate limits Shopify API only - -**Proper Fix:** -```python -from slowapi import Limiter, _rate_limit_exceeded_handler -from slowapi.util import get_remote_address - -limiter = Limiter(key_func=get_remote_address) -app.state.limiter = limiter - -@router.post("/pricing/run") -@limiter.limit("10/minute") # Max 10 pricing runs per minute -async def run_pricing(request: Request, store_id: int): - # ... -``` - -**Effort**: 1-2 days - -**Priority**: Fix before public launch - ---- - -## ๐ŸŸข Nice-to-Have Improvements - -### **9. Frontend State Management** - -**Problem:** -- Prop drilling in places -- No global state manager (Redux, Zustand) -- Duplicate API calls -- No caching - -**Impact:** -- Code is verbose -- Performance could be better -- Refactoring is harder - -**Current Workaround:** -- It works fine for current scale -- React Context could be added easily - -**Better Approach:** -```typescript -// Add Zustand for global state -import create from 'zustand' - -interface StoreState { - storeId: number | null - status: StoreStatus | null - loadStatus: () => Promise -} - -const useStore = create((set) => ({ - storeId: null, - status: null, - loadStatus: async () => { - const status = await operatorApi.getStoreStatus(get().storeId) - set({ status }) - } -})) - -// Use in components -function MyComponent() { - const { status, loadStatus } = useStore() - // No prop drilling! -} -``` - -**Effort**: 1 week - -**Priority**: Fix when frontend becomes unwieldy - ---- - -### **10. No Real-Time Updates** - -**Problem:** -- UI requires manual refresh -- No WebSocket connection -- No push notifications - -**Impact:** -- Operators don't see changes immediately -- Have to refresh page to see new events - -**Current Workaround:** -- Auto-refresh every 30 seconds (health check) -- Manual refresh button - -**Better Approach:** -```python -# Add WebSocket support -from fastapi import WebSocket - -@app.websocket("/ws") -async def websocket_endpoint(websocket: WebSocket): - await websocket.accept() - while True: - # Push updates when pricing events occur - await websocket.send_json({"type": "pricing_event", "data": {...}}) -``` - -**Effort**: 1 week - -**Priority**: Fix when real-time is required - ---- - -### **11. No Email Notifications** - -**Problem:** -- No email alerts for critical events -- Merchants don't know when prices change -- Operators aren't notified of failures - -**Impact:** -- Merchants are surprised by changes -- Operators miss critical alerts - -**Current Workaround:** -- Check dashboard manually -- Review Sentry for errors - -**Better Approach:** -```python -# Add SendGrid integration -import sendgrid -from sendgrid.helpers.mail import Mail - -def send_pricing_summary(store_id: int, changes: List[PricingEvent]): - message = Mail( - from_email='noreply@example.com', - to_emails=store.owner_email, - subject='Daily Pricing Summary', - html_content=render_template('pricing_summary.html', changes=changes) - ) - sg = sendgrid.SendGridAPIClient(api_key=os.environ.get('SENDGRID_API_KEY')) - sg.send(message) - -# Call after pricing cycle -send_pricing_summary(store_id, applied_changes) -``` - -**Effort**: 3-4 days - -**Priority**: Fix when merchants request it - ---- - -### **12. No Competitor Price Monitoring** - -**Problem:** -- Pricing decisions ignore competitor prices -- Can't react to market changes -- No price intelligence - -**Impact:** -- May price too high/low vs market -- Miss opportunities - -**Current Workaround:** -- Merchants set guardrails based on market knowledge - -**Better Approach:** -```python -# Web scraping or API integration -class CompetitorMonitor: - def get_competitor_prices(self, product_sku: str) -> List[float]: - # Scrape competitor sites - # Or use price intelligence API (Prisync, etc.) - return [19.99, 21.50, 18.75] - - def suggest_price_with_competition(self, product): - competitor_prices = self.get_competitor_prices(product.sku) - avg_competitor = sum(competitor_prices) / len(competitor_prices) - # Adjust based on competition - return avg_competitor * 0.95 # Undercut by 5% -``` - -**Effort**: 2-3 weeks - -**Priority**: Competitive differentiator for later - ---- - -### **13. No Internationalization (i18n)** - -**Problem:** -- All text is hardcoded in English -- No multi-currency support -- No timezone handling - -**Impact:** -- Can only serve English-speaking merchants -- Prices assume USD - -**Current Workaround:** -- Only target US/UK/Australia markets - -**Better Approach:** -```typescript -// Frontend i18n (react-i18next) -import { useTranslation } from 'react-i18next' - -function MyComponent() { - const { t } = useTranslation() - return

{t('dashboard.title')}

-} - -// Backend -from babel.numbers import format_currency - -format_currency(price, 'USD', locale='en_US') # $20.00 -format_currency(price, 'GBP', locale='en_GB') # ยฃ20.00 -``` - -**Effort**: 1-2 weeks - -**Priority**: Fix when expanding internationally - ---- - -## ๐Ÿ“‹ Prioritized Remediation Plan - -### **Phase 1: Foundation (Months 1-2)** - -Focus: Make it safe to change code - -| Task | Effort | Impact | -|------|--------|--------| -| Add testing suite | 3 weeks | High | -| Setup Alembic migrations | 1 week | High | -| Add user authentication | 2 weeks | High | - -### **Phase 2: Monetization (Months 3-4)** - -Focus: Make money - -| Task | Effort | Impact | -|------|--------|--------| -| Integrate Stripe billing | 2 weeks | Critical | -| Add subscription tiers | 1 week | High | -| Add rate limiting | 2 days | Medium | - -### **Phase 3: Scaling (Months 5-6)** - -Focus: Handle growth - -| Task | Effort | Impact | -|------|--------|--------| -| Add APM (DataDog/New Relic) | 1 week | High | -| Setup worker autoscaling | 2 weeks | Medium | -| Add email notifications | 4 days | Medium | - -### **Phase 4: Intelligence (Months 7-12)** - -Focus: Better pricing - -| Task | Effort | Impact | -|------|--------|--------| -| ML pricing engine | 3 months | High | -| Competitor monitoring | 3 weeks | Medium | -| Advanced analytics | 2 weeks | Low | - ---- - -## ๐ŸŽฏ What To Fix First? - -**If you're handling < 10 stores:** -- Testing suite (avoid breaking things) -- Database migrations (safe schema changes) - -**If you're handling 10-50 stores:** -- User authentication (security) -- Billing integration (make money) -- APM (find bottlenecks) - -**If you're handling 50+ stores:** -- Worker autoscaling (handle load) -- ML pricing engine (competitive advantage) - ---- - -## ๐Ÿ’ฌ Honest Q&A - -**Q: Is this system production-ready?** -A: Yes, for early customers (< 50 stores). But expect to invest 3-6 months of dev time to harden it for scale. - -**Q: What's the biggest risk?** -A: No testing means regressions are easy. Add tests first. - -**Q: Can I launch with this?** -A: Absolutely. But add auth and billing within 2 months. - -**Q: Will I need to rewrite anything?** -A: No. The architecture is solid. You're adding features, not rebuilding. - -**Q: How much technical debt is this?** -A: ~3-4 months of work to address critical + important gaps. That's normal for an MVP. - -**Q: Should I be worried?** -A: No. Every successful SaaS starts like this. The key is that **we documented all the gaps** instead of hiding them. - ---- - -## ๐Ÿ“Š Technical Debt Metrics - -| Category | Count | Estimated Fix Time | -|----------|-------|-------------------| -| Critical Issues | 4 | 2 months | -| Important Issues | 4 | 3 months | -| Nice-to-Have | 5 | 2 months | -| **Total** | **13** | **7 months** | - -**Realistic Timeline**: Fix critical issues in 2 months, important issues over 6 months, nice-to-haves as needed. - ---- - -## โœ… What's Actually Good - -Don't forget what's **already solid**: - -- โœ… Clean architecture (easy to extend) -- โœ… Type safety (TypeScript + Pydantic) -- โœ… Error handling (retry logic, timeouts) -- โœ… Logging (structured, readable) -- โœ… Documentation (comprehensive) -- โœ… Docker setup (reproducible) -- โœ… Health checks (observable) -- โœ… Safety features (preview, rollback, guardrails) - -**This is a solid foundation.** The gaps are normal for an MVP. You're not inheriting a messโ€”you're inheriting a v1.0 that works, with a clear roadmap to v2.0. - ---- - -
- -**Gaps are opportunities, not failures.** - -[Back to README](./README.md) โ€ข [View Roadmap](./README.md#-roadmap) - -
diff --git a/ecom-dynamic-pricing/backend/Dockerfile b/ecom-dynamic-pricing/backend/Dockerfile deleted file mode 100644 index b2694ef..0000000 --- a/ecom-dynamic-pricing/backend/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -FROM python:3.11-slim - -WORKDIR /app - -ENV PYTHONDONTWRITEBYTECODE=1 -ENV PYTHONUNBUFFERED=1 - -RUN apt-get update && apt-get install -y \ - build-essential \ - libpq-dev \ - && rm -rf /var/lib/apt/lists/* - -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt - -COPY app ./app - -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/ecom-dynamic-pricing/backend/app/__init__.py b/ecom-dynamic-pricing/backend/app/__init__.py deleted file mode 100644 index c1ba8d0..0000000 --- a/ecom-dynamic-pricing/backend/app/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Makes app a package diff --git a/ecom-dynamic-pricing/backend/app/celery_app.py b/ecom-dynamic-pricing/backend/app/celery_app.py deleted file mode 100644 index 4bfd4bb..0000000 --- a/ecom-dynamic-pricing/backend/app/celery_app.py +++ /dev/null @@ -1,18 +0,0 @@ -from celery import Celery -from .config import settings - -celery_app = Celery( - "ecom_dynamic_pricing", - broker=settings.redis_url, - backend=settings.redis_url, -) - -celery_app.conf.update( - task_routes={ - "app.tasks.sync_store_products": {"queue": "sync"}, - "app.tasks.run_pricing_cycle": {"queue": "pricing"}, - }, - task_serializer="json", - accept_content=["json"], - result_serializer="json", -) diff --git a/ecom-dynamic-pricing/backend/app/config.py b/ecom-dynamic-pricing/backend/app/config.py deleted file mode 100644 index ddfaf37..0000000 --- a/ecom-dynamic-pricing/backend/app/config.py +++ /dev/null @@ -1,41 +0,0 @@ -from pydantic_settings import BaseSettings -from pydantic import Field - - -class Settings(BaseSettings): - app_env: str = Field("local", alias="APP_ENV") - app_debug: bool = Field(True, alias="APP_DEBUG") - app_host: str = Field("0.0.0.0", alias="APP_HOST") - app_port: int = Field(8000, alias="APP_PORT") - - postgres_user: str = Field("ecom", alias="POSTGRES_USER") - postgres_password: str = Field("ecom_password", alias="POSTGRES_PASSWORD") - postgres_db: str = Field("ecom_db", alias="POSTGRES_DB") - postgres_host: str = Field("db", alias="POSTGRES_HOST") - postgres_port: int = Field(5432, alias="POSTGRES_PORT") - - redis_url: str = Field("redis://redis:6379/0", alias="REDIS_URL") - - shopify_app_api_key: str = Field(..., alias="SHOPIFY_APP_API_KEY") - shopify_app_api_secret: str = Field(..., alias="SHOPIFY_APP_API_SECRET") - shopify_app_scopes: str = Field("read_products,write_products,read_orders", alias="SHOPIFY_APP_SCOPES") - shopify_app_redirect_uri: str = Field(..., alias="SHOPIFY_APP_REDIRECT_URI") - - jwt_secret: str = Field("change_me", alias="JWT_SECRET") - jwt_algorithm: str = Field("HS256", alias="JWT_ALGORITHM") - - @property - def database_url(self) -> str: - return ( - f"postgresql+psycopg2://{self.postgres_user}:" - f"{self.postgres_password}@{self.postgres_host}:" - f"{self.postgres_port}/{self.postgres_db}" - ) - - class Config: - env_file = ".env" - env_file_encoding = "utf-8" - extra = "ignore" - - -settings = Settings() diff --git a/ecom-dynamic-pricing/backend/app/database.py b/ecom-dynamic-pricing/backend/app/database.py deleted file mode 100644 index 67211ac..0000000 --- a/ecom-dynamic-pricing/backend/app/database.py +++ /dev/null @@ -1,18 +0,0 @@ -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker, declarative_base - -from .config import settings - -engine = create_engine(settings.database_url, future=True, echo=False) - -SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) - -Base = declarative_base() - - -def get_db(): - db = SessionLocal() - try: - yield db - finally: - db.close() diff --git a/ecom-dynamic-pricing/backend/app/main.py b/ecom-dynamic-pricing/backend/app/main.py deleted file mode 100644 index f84c29d..0000000 --- a/ecom-dynamic-pricing/backend/app/main.py +++ /dev/null @@ -1,141 +0,0 @@ -from fastapi import FastAPI, Request -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse -import logging -import sys -import time -from typing import Callable - -from .config import settings -from .database import Base, engine -from .routes import shopify as shopify_routes -from .routes import health as health_routes -from .routes import operator as operator_routes - -# Configure structured logging -logging.basicConfig( - level=logging.INFO if not settings.app_debug else logging.DEBUG, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - handlers=[ - logging.StreamHandler(sys.stdout) - ] -) - -logger = logging.getLogger(__name__) - -# Sentry integration (if configured) -try: - import sentry_sdk - from sentry_sdk.integrations.fastapi import FastApiIntegration - from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration - - sentry_dsn = settings.dict().get("sentry_dsn") - if sentry_dsn: - sentry_sdk.init( - dsn=sentry_dsn, - environment=settings.app_env, - integrations=[ - FastApiIntegration(), - SqlalchemyIntegration(), - ], - traces_sample_rate=0.1, # 10% of transactions for performance monitoring - ) - logger.info("Sentry integration initialized") -except ImportError: - logger.warning("Sentry SDK not installed - error reporting disabled") -except Exception as e: - logger.warning(f"Failed to initialize Sentry: {e}") - -# Create tables on startup (simple for now; can move to Alembic later) -Base.metadata.create_all(bind=engine) - -app = FastAPI( - title="E-Commerce Dynamic Pricing API", - version="0.1.0", - debug=settings.app_debug, -) - - -# Request logging middleware -@app.middleware("http") -async def log_requests(request: Request, call_next: Callable): - """Log all HTTP requests with timing information.""" - start_time = time.time() - - # Log request - logger.info(f"โ†’ {request.method} {request.url.path} - Client: {request.client.host if request.client else 'unknown'}") - - try: - response = await call_next(request) - process_time = time.time() - start_time - - # Log response - logger.info( - f"โ† {request.method} {request.url.path} - " - f"Status: {response.status_code} - " - f"Duration: {process_time:.3f}s" - ) - - # Add timing header - response.headers["X-Process-Time"] = str(process_time) - return response - - except Exception as e: - process_time = time.time() - start_time - logger.error( - f"โœ— {request.method} {request.url.path} - " - f"Error: {str(e)} - " - f"Duration: {process_time:.3f}s", - exc_info=True - ) - raise - - -# Global exception handler -@app.exception_handler(Exception) -async def global_exception_handler(request: Request, exc: Exception): - """Catch-all exception handler for unhandled errors.""" - logger.error(f"Unhandled exception: {str(exc)}", exc_info=True) - return JSONResponse( - status_code=500, - content={ - "error": "Internal server error", - "message": str(exc) if settings.app_debug else "An unexpected error occurred", - "path": str(request.url.path) - } - ) - - -# CORS โ€“ adjust origins later when you add frontend -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], # tighten later - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - - -@app.on_event("startup") -async def startup_event(): - """Log startup information.""" - logger.info(f"Starting E-Commerce Dynamic Pricing API - Environment: {settings.app_env}") - logger.info(f"Debug mode: {settings.app_debug}") - - -@app.on_event("shutdown") -async def shutdown_event(): - """Log shutdown information.""" - logger.info("Shutting down E-Commerce Dynamic Pricing API") - - -# Basic health check for backward compatibility -@app.get("/health") -async def health(): - return {"status": "ok", "env": settings.app_env} - - -# Register routers -app.include_router(shopify_routes.router) -app.include_router(health_routes.router) -app.include_router(operator_routes.router) diff --git a/ecom-dynamic-pricing/backend/app/models.py b/ecom-dynamic-pricing/backend/app/models.py deleted file mode 100644 index 9b995d2..0000000 --- a/ecom-dynamic-pricing/backend/app/models.py +++ /dev/null @@ -1,69 +0,0 @@ -from sqlalchemy import ( - Column, - Integer, - String, - DateTime, - Boolean, - Float, - ForeignKey, - JSON, -) -from sqlalchemy.orm import relationship -from datetime import datetime - -from .database import Base - - -class Store(Base): - __tablename__ = "stores" - - id = Column(Integer, primary_key=True, index=True) - shop_domain = Column(String, unique=True, index=True, nullable=False) - access_token = Column(String, nullable=False) - owner_email = Column(String, nullable=True) - created_at = Column(DateTime, default=datetime.utcnow) - is_active = Column(Boolean, default=True) - - # Merchant safety controls - automation_suspended = Column(Boolean, default=False) - min_price_limit = Column(Float, nullable=True) # Global min price for all products - max_price_limit = Column(Float, nullable=True) # Global max price for all products - new_product_protection_days = Column(Integer, default=7) # Don't auto-price new products for N days - - products = relationship("Product", back_populates="store", cascade="all, delete-orphan") - pricing_events = relationship("PricingEvent", back_populates="store", cascade="all, delete-orphan") - - -class Product(Base): - __tablename__ = "products" - - id = Column(Integer, primary_key=True, index=True) - store_id = Column(Integer, ForeignKey("stores.id"), nullable=False) - shopify_product_id = Column(String, index=True, nullable=False) - shopify_variant_id = Column(String, index=True, nullable=False) - title = Column(String, nullable=False) - sku = Column(String, nullable=True) - current_price = Column(Float, nullable=False) - cost = Column(Float, nullable=True) - inventory_quantity = Column(Integer, nullable=True) - first_seen_at = Column(DateTime, default=datetime.utcnow) # Track when product was first synced - last_synced_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - - store = relationship("Store", back_populates="products") - - -class PricingEvent(Base): - __tablename__ = "pricing_events" - - id = Column(Integer, primary_key=True, index=True) - store_id = Column(Integer, ForeignKey("stores.id"), nullable=False) - shopify_variant_id = Column(String, index=True, nullable=False) - old_price = Column(Float, nullable=False) - new_price = Column(Float, nullable=False) - reason = Column(String, nullable=True) - metadata = Column(JSON, nullable=True) - created_at = Column(DateTime, default=datetime.utcnow) - rolled_back = Column(Boolean, default=False) # Track if this change was rolled back - rolled_back_at = Column(DateTime, nullable=True) # When it was rolled back - - store = relationship("Store", back_populates="pricing_events") diff --git a/ecom-dynamic-pricing/backend/app/pricing_engine.py b/ecom-dynamic-pricing/backend/app/pricing_engine.py deleted file mode 100644 index 718a61f..0000000 --- a/ecom-dynamic-pricing/backend/app/pricing_engine.py +++ /dev/null @@ -1,113 +0,0 @@ -from typing import List, Dict, Any, Optional -from datetime import datetime, timedelta -import logging - -logger = logging.getLogger(__name__) - - -class PricingEngine: - """ - Minimal first-pass pricing logic with safety guardrails: - - If sales_velocity is high and inventory > threshold: increase price slightly - - If sales_velocity is low and inventory high: decrease price slightly - - Respects min/max price limits - - Protects new products from pricing changes - This is placeholder logic to be replaced with a proper model later. - """ - - def __init__( - self, - max_increase_pct: float = 0.15, - max_decrease_pct: float = 0.20, - step_pct: float = 0.05, - min_price: Optional[float] = None, - max_price: Optional[float] = None, - new_product_protection_days: int = 7, - ): - self.max_increase_pct = max_increase_pct - self.max_decrease_pct = max_decrease_pct - self.step_pct = step_pct - self.min_price = min_price - self.max_price = max_price - self.new_product_protection_days = new_product_protection_days - - def suggest_prices(self, variants: List[Dict[str, Any]]) -> List[Dict[str, Any]]: - """ - variants: list of dicts with keys: - - variant_id - - current_price - - cost (optional) - - sales_velocity (units/day) - - inventory_quantity - - first_seen_at (optional) - datetime when product was first synced - """ - suggestions: List[Dict[str, Any]] = [] - now = datetime.utcnow() - - for v in variants: - price = float(v["current_price"]) - sales_velocity = float(v.get("sales_velocity", 0.0)) - inventory = int(v.get("inventory_quantity", 0)) - variant_id = str(v["variant_id"]) - first_seen_at = v.get("first_seen_at") - - new_price = price - reason = "no_change" - - # Check if product is too new to auto-price - if first_seen_at and isinstance(first_seen_at, datetime): - age_days = (now - first_seen_at).days - if age_days < self.new_product_protection_days: - logger.debug(f"Variant {variant_id} is only {age_days} days old, skipping pricing") - suggestions.append({ - "variant_id": variant_id, - "old_price": price, - "new_price": price, - "reason": "new_product_protected", - }) - continue - - # Simple thresholds for now - if sales_velocity > 5 and inventory > 20: - # increase price within max limit - delta = price * self.step_pct - new_price = min(price + delta, price * (1 + self.max_increase_pct)) - reason = "fast_seller_increase" - - elif sales_velocity < 1 and inventory > 30: - # decrease price within max limit - delta = price * self.step_pct - new_price = max(price - delta, price * (1 - self.max_decrease_pct)) - reason = "slow_seller_decrease" - - # Apply global min/max guardrails - original_new_price = new_price - if self.min_price is not None and new_price < self.min_price: - new_price = self.min_price - logger.info(f"Variant {variant_id}: price clamped to min_price {self.min_price} (was {original_new_price})") - reason = f"{reason}_clamped_to_min" if reason != "no_change" else "clamped_to_min" - - if self.max_price is not None and new_price > self.max_price: - new_price = self.max_price - logger.info(f"Variant {variant_id}: price clamped to max_price {self.max_price} (was {original_new_price})") - reason = f"{reason}_clamped_to_max" if reason != "no_change" else "clamped_to_max" - - # Apply cost-based floor (don't go below cost + 10% margin) - cost = v.get("cost") - if cost is not None and cost > 0: - min_acceptable = cost * 1.1 - if new_price < min_acceptable: - logger.warning(f"Variant {variant_id}: price {new_price} below cost floor {min_acceptable}, adjusting") - new_price = min_acceptable - reason = "cost_floor_protected" - - suggestions.append( - { - "variant_id": variant_id, - "old_price": price, - "new_price": round(new_price, 2), - "reason": reason, - } - ) - - return suggestions diff --git a/ecom-dynamic-pricing/backend/app/routes/__init__.py b/ecom-dynamic-pricing/backend/app/routes/__init__.py deleted file mode 100644 index d212dab..0000000 --- a/ecom-dynamic-pricing/backend/app/routes/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Routes package diff --git a/ecom-dynamic-pricing/backend/app/routes/health.py b/ecom-dynamic-pricing/backend/app/routes/health.py deleted file mode 100644 index a418c84..0000000 --- a/ecom-dynamic-pricing/backend/app/routes/health.py +++ /dev/null @@ -1,118 +0,0 @@ -from fastapi import APIRouter, Depends, HTTPException -from sqlalchemy.orm import Session -from sqlalchemy import text -from typing import Dict, Any -import redis -from celery import Celery - -from ..config import settings -from ..database import get_db -from ..celery_app import celery_app - -router = APIRouter(prefix="/api/health", tags=["health"]) - - -@router.get("/") -async def health_check_basic(): - """Basic health check - API is up""" - return {"status": "ok", "env": settings.app_env} - - -@router.get("/detailed") -async def health_check_detailed(db: Session = Depends(get_db)) -> Dict[str, Any]: - """ - Comprehensive health check covering all infrastructure components. - - Checks: - - Database connectivity - - Redis connectivity - - Celery worker status - """ - health_status: Dict[str, Any] = { - "status": "healthy", - "components": {} - } - - # Check Postgres - try: - db.execute(text("SELECT 1")) - health_status["components"]["postgres"] = { - "status": "healthy", - "message": "Database connection successful" - } - except Exception as e: - health_status["status"] = "unhealthy" - health_status["components"]["postgres"] = { - "status": "unhealthy", - "message": f"Database error: {str(e)}" - } - - # Check Redis - try: - r = redis.from_url(settings.redis_url, decode_responses=True) - r.ping() - health_status["components"]["redis"] = { - "status": "healthy", - "message": "Redis connection successful" - } - except Exception as e: - health_status["status"] = "unhealthy" - health_status["components"]["redis"] = { - "status": "unhealthy", - "message": f"Redis error: {str(e)}" - } - - # Check Celery workers - try: - inspect = celery_app.control.inspect() - active_workers = inspect.active() - - if active_workers and len(active_workers) > 0: - worker_count = len(active_workers) - health_status["components"]["celery"] = { - "status": "healthy", - "message": f"{worker_count} worker(s) active", - "workers": list(active_workers.keys()) - } - else: - health_status["status"] = "degraded" - health_status["components"]["celery"] = { - "status": "unhealthy", - "message": "No active Celery workers found" - } - except Exception as e: - health_status["status"] = "degraded" - health_status["components"]["celery"] = { - "status": "unhealthy", - "message": f"Celery error: {str(e)}" - } - - return health_status - - -@router.get("/readiness") -async def readiness_check(db: Session = Depends(get_db)): - """ - Kubernetes-style readiness check. - Returns 200 if ready to serve traffic, 503 if not. - """ - try: - # Must be able to query DB - db.execute(text("SELECT 1")) - - # Must be able to reach Redis - r = redis.from_url(settings.redis_url, decode_responses=True) - r.ping() - - return {"status": "ready"} - except Exception as e: - raise HTTPException(status_code=503, detail=f"Not ready: {str(e)}") - - -@router.get("/liveness") -async def liveness_check(): - """ - Kubernetes-style liveness check. - Returns 200 if process is alive. - """ - return {"status": "alive"} diff --git a/ecom-dynamic-pricing/backend/app/routes/operator.py b/ecom-dynamic-pricing/backend/app/routes/operator.py deleted file mode 100644 index 254c5d7..0000000 --- a/ecom-dynamic-pricing/backend/app/routes/operator.py +++ /dev/null @@ -1,261 +0,0 @@ -from fastapi import APIRouter, Depends, HTTPException -from sqlalchemy.orm import Session -from sqlalchemy import desc -from typing import Dict, Any, List -from datetime import datetime -import asyncio - -from ..config import settings -from ..database import get_db -from ..models import Store, PricingEvent, Product -from ..shopify_client import ShopifyClient -from ..tasks import run_pricing_cycle - -router = APIRouter(prefix="/api/operator", tags=["operator"]) - - -@router.post("/stores/{store_id}/suspend") -async def suspend_automation(store_id: int, db: Session = Depends(get_db)) -> Dict[str, Any]: - """ - Emergency stop: Suspend all automated pricing for a store. - - This is the panic button for merchants. - """ - store = db.query(Store).filter(Store.id == store_id).first() - if not store: - raise HTTPException(status_code=404, detail="Store not found") - - store.automation_suspended = True - db.commit() - - return { - "status": "suspended", - "store_id": store_id, - "message": "All automated pricing has been suspended for this store" - } - - -@router.post("/stores/{store_id}/resume") -async def resume_automation(store_id: int, db: Session = Depends(get_db)) -> Dict[str, Any]: - """Resume automated pricing for a store.""" - store = db.query(Store).filter(Store.id == store_id).first() - if not store: - raise HTTPException(status_code=404, detail="Store not found") - - store.automation_suspended = False - db.commit() - - return { - "status": "resumed", - "store_id": store_id, - "message": "Automated pricing has been resumed for this store" - } - - -@router.get("/stores/{store_id}/status") -async def get_store_status(store_id: int, db: Session = Depends(get_db)) -> Dict[str, Any]: - """Get current automation status and guardrails for a store.""" - store = db.query(Store).filter(Store.id == store_id).first() - if not store: - raise HTTPException(status_code=404, detail="Store not found") - - product_count = db.query(Product).filter(Product.store_id == store_id).count() - recent_events = ( - db.query(PricingEvent) - .filter(PricingEvent.store_id == store_id) - .order_by(desc(PricingEvent.created_at)) - .limit(10) - .all() - ) - - return { - "store_id": store_id, - "shop_domain": store.shop_domain, - "is_active": store.is_active, - "automation_suspended": store.automation_suspended, - "product_count": product_count, - "guardrails": { - "min_price_limit": store.min_price_limit, - "max_price_limit": store.max_price_limit, - "new_product_protection_days": store.new_product_protection_days, - }, - "recent_events_count": len(recent_events), - } - - -@router.post("/stores/{store_id}/pricing/preview") -async def preview_pricing(store_id: int, db: Session = Depends(get_db)) -> Dict[str, Any]: - """ - Generate pricing suggestions without applying them. - - Perfect for "what will happen next" visibility. - """ - result = run_pricing_cycle.apply_async(args=[store_id], kwargs={"preview_mode": True}) - - # Wait for result (with timeout) - try: - preview_data = result.get(timeout=30) - return preview_data - except Exception as e: - raise HTTPException(status_code=500, detail=f"Preview failed: {str(e)}") - - -@router.post("/stores/{store_id}/rollback") -async def rollback_prices( - store_id: int, - limit: int = 10, - db: Session = Depends(get_db) -) -> Dict[str, Any]: - """ - Rollback the last N price changes for a store. - - Args: - store_id: Store ID - limit: Number of recent changes to rollback (default 10, max 50) - - Returns: - Dict with rollback status and count - """ - if limit > 50: - raise HTTPException(status_code=400, detail="Cannot rollback more than 50 changes at once") - - store = db.query(Store).filter(Store.id == store_id).first() - if not store: - raise HTTPException(status_code=404, detail="Store not found") - - # Get the last N pricing events that haven't been rolled back - recent_events = ( - db.query(PricingEvent) - .filter( - PricingEvent.store_id == store_id, - PricingEvent.rolled_back == False - ) - .order_by(desc(PricingEvent.created_at)) - .limit(limit) - .all() - ) - - if not recent_events: - return { - "status": "no_changes_to_rollback", - "store_id": store_id, - "rolled_back_count": 0 - } - - rollback_count = 0 - error_count = 0 - - async def _rollback(): - nonlocal rollback_count, error_count - client = ShopifyClient(store.shop_domain, store.access_token) - - for event in recent_events: - try: - # Revert to old price - await client.update_variant_price(event.shopify_variant_id, event.old_price) - - # Mark as rolled back - event.rolled_back = True - event.rolled_back_at = datetime.utcnow() - - # Update product record - product = db.query(Product).filter( - Product.store_id == store_id, - Product.shopify_variant_id == event.shopify_variant_id - ).first() - if product: - product.current_price = event.old_price - - rollback_count += 1 - - except Exception as e: - error_count += 1 - # Log but continue with other rollbacks - - db.commit() - - asyncio.run(_rollback()) - - return { - "status": "completed", - "store_id": store_id, - "rolled_back_count": rollback_count, - "error_count": error_count, - "message": f"Rolled back {rollback_count} price changes" - } - - -@router.put("/stores/{store_id}/guardrails") -async def update_guardrails( - store_id: int, - min_price: float | None = None, - max_price: float | None = None, - new_product_protection_days: int | None = None, - db: Session = Depends(get_db) -) -> Dict[str, Any]: - """ - Update pricing guardrails for a store. - - Args: - min_price: Minimum price for any product - max_price: Maximum price for any product - new_product_protection_days: Don't auto-price products newer than N days - """ - store = db.query(Store).filter(Store.id == store_id).first() - if not store: - raise HTTPException(status_code=404, detail="Store not found") - - if min_price is not None: - store.min_price_limit = min_price - if max_price is not None: - store.max_price_limit = max_price - if new_product_protection_days is not None: - store.new_product_protection_days = new_product_protection_days - - db.commit() - - return { - "status": "updated", - "store_id": store_id, - "guardrails": { - "min_price_limit": store.min_price_limit, - "max_price_limit": store.max_price_limit, - "new_product_protection_days": store.new_product_protection_days, - } - } - - -@router.get("/stores/{store_id}/pricing-history") -async def get_pricing_history( - store_id: int, - limit: int = 50, - db: Session = Depends(get_db) -) -> List[Dict[str, Any]]: - """ - Get pricing change history for a store. - - Shows "what changed, when, and why" for merchant visibility. - """ - events = ( - db.query(PricingEvent) - .filter(PricingEvent.store_id == store_id) - .order_by(desc(PricingEvent.created_at)) - .limit(min(limit, 200)) # Cap at 200 - .all() - ) - - return [ - { - "id": e.id, - "variant_id": e.shopify_variant_id, - "old_price": e.old_price, - "new_price": e.new_price, - "change_amount": round(e.new_price - e.old_price, 2), - "change_percent": round(((e.new_price - e.old_price) / e.old_price) * 100, 2), - "reason": e.reason, - "created_at": e.created_at.isoformat(), - "rolled_back": e.rolled_back, - "rolled_back_at": e.rolled_back_at.isoformat() if e.rolled_back_at else None, - } - for e in events - ] diff --git a/ecom-dynamic-pricing/backend/app/routes/shopify.py b/ecom-dynamic-pricing/backend/app/routes/shopify.py deleted file mode 100644 index 237d449..0000000 --- a/ecom-dynamic-pricing/backend/app/routes/shopify.py +++ /dev/null @@ -1,106 +0,0 @@ -from fastapi import APIRouter, Depends, HTTPException, Request -from sqlalchemy.orm import Session -from urllib.parse import urlencode -import hmac -import hashlib -import base64 -from typing import Dict, Any - -from ..config import settings -from ..database import get_db -from ..models import Store -from ..tasks import sync_store_products, run_pricing_cycle - -router = APIRouter(prefix="/api/shopify", tags=["shopify"]) - - -def _verify_hmac(params: Dict[str, Any], hmac_value: str) -> bool: - """ - Basic HMAC validation for Shopify OAuth. - """ - message = "&".join( - f"{key}={value}" - for key, value in sorted(params.items()) - if key != "hmac" and key != "signature" - ).encode("utf-8") - secret = settings.shopify_app_api_secret.encode("utf-8") - digest = hmac.new(secret, message, hashlib.sha256).hexdigest() - return hmac.compare_digest(digest, hmac_value) - - -@router.get("/oauth/install") -async def install(shop: str): - """ - Installation redirect endpoint. - """ - query = { - "client_id": settings.shopify_app_api_key, - "scope": settings.shopify_app_scopes, - "redirect_uri": settings.shopify_app_redirect_uri, - } - url = f"https://{shop}/admin/oauth/authorize?{urlencode(query)}" - return {"install_url": url} - - -@router.get("/oauth/callback") -async def oauth_callback( - request: Request, - db: Session = Depends(get_db), -): - params = dict(request.query_params) - hmac_value = params.get("hmac") - shop = params.get("shop") - code = params.get("code") - - if not shop or not code or not hmac_value: - raise HTTPException(status_code=400, detail="Missing parameters") - - if not _verify_hmac(params, hmac_value): - raise HTTPException(status_code=400, detail="Invalid HMAC") - - # Exchange code for access token - import httpx - - token_url = f"https://{shop}/admin/oauth/access_token" - async with httpx.AsyncClient(timeout=30.0) as client: - resp = await client.post( - token_url, - json={ - "client_id": settings.shopify_app_api_key, - "client_secret": settings.shopify_app_api_secret, - "code": code, - }, - ) - resp.raise_for_status() - data = resp.json() - - access_token = data.get("access_token") - if not access_token: - raise HTTPException(status_code=400, detail="No access token returned") - - # Upsert store - store = db.query(Store).filter(Store.shop_domain == shop).first() - if store: - store.access_token = access_token - store.is_active = True - else: - store = Store(shop_domain=shop, access_token=access_token) - db.add(store) - db.commit() - db.refresh(store) - - # Kick off initial product sync - sync_store_products.delay(store.id) - - # In a real app, redirect to your frontend dashboard - return {"message": "App installed", "store_id": store.id, "shop": shop} - - -@router.post("/stores/{store_id}/pricing/run") -async def run_pricing(store_id: int, db: Session = Depends(get_db)): - store = db.query(Store).filter(Store.id == store_id, Store.is_active == True).first() - if not store: - raise HTTPException(status_code=404, detail="Store not found") - - run_pricing_cycle.delay(store.id) - return {"status": "queued", "store_id": store.id} diff --git a/ecom-dynamic-pricing/backend/app/schemas.py b/ecom-dynamic-pricing/backend/app/schemas.py deleted file mode 100644 index 4200bf6..0000000 --- a/ecom-dynamic-pricing/backend/app/schemas.py +++ /dev/null @@ -1,34 +0,0 @@ -from pydantic import BaseModel -from typing import Optional, Any -from datetime import datetime - - -class StoreBase(BaseModel): - shop_domain: str - owner_email: Optional[str] = None - - -class StoreCreate(StoreBase): - access_token: str - - -class StoreOut(StoreBase): - id: int - created_at: datetime - is_active: bool - - class Config: - from_attributes = True - - -class PricingEventOut(BaseModel): - id: int - shopify_variant_id: str - old_price: float - new_price: float - reason: Optional[str] = None - metadata: Optional[Any] = None - created_at: datetime - - class Config: - from_attributes = True diff --git a/ecom-dynamic-pricing/backend/app/shopify_client.py b/ecom-dynamic-pricing/backend/app/shopify_client.py deleted file mode 100644 index 88d12aa..0000000 --- a/ecom-dynamic-pricing/backend/app/shopify_client.py +++ /dev/null @@ -1,119 +0,0 @@ -import httpx -from typing import Dict, Any, List -import asyncio -import logging - -logger = logging.getLogger(__name__) - - -class ShopifyClient: - """ - Shopify API client with rate limiting and retry logic. - - Shopify uses a leaky bucket algorithm. This client: - - Respects rate limit headers - - Retries on 429 (rate limit exceeded) - - Logs API usage - """ - - def __init__(self, shop_domain: str, access_token: str, max_retries: int = 3): - self.shop_domain = shop_domain - self.access_token = access_token - self.base_url = f"https://{shop_domain}/admin/api/2024-01" - self.max_retries = max_retries - - async def _make_request( - self, - method: str, - path: str, - params: Dict[str, Any] | None = None, - data: Dict[str, Any] | None = None, - ) -> Dict[str, Any]: - """Make HTTP request with rate limiting and retry logic.""" - headers = { - "X-Shopify-Access-Token": self.access_token, - "Content-Type": "application/json", - } - - url = f"{self.base_url}{path}.json" - retries = 0 - - while retries <= self.max_retries: - try: - async with httpx.AsyncClient(timeout=30.0) as client: - if method == "GET": - resp = await client.get(url, headers=headers, params=params) - elif method == "PUT": - resp = await client.put(url, headers=headers, json=data) - elif method == "POST": - resp = await client.post(url, headers=headers, json=data) - else: - raise ValueError(f"Unsupported method: {method}") - - # Check rate limit headers - if "X-Shopify-Shop-Api-Call-Limit" in resp.headers: - limit_info = resp.headers["X-Shopify-Shop-Api-Call-Limit"] - logger.debug(f"Shopify API call limit: {limit_info}") - - # Handle rate limiting - if resp.status_code == 429: - retry_after = int(resp.headers.get("Retry-After", 2)) - logger.warning(f"Rate limited by Shopify. Waiting {retry_after}s before retry {retries + 1}/{self.max_retries}") - await asyncio.sleep(retry_after) - retries += 1 - continue - - resp.raise_for_status() - return resp.json() - - except httpx.HTTPStatusError as e: - if e.response.status_code == 429 and retries < self.max_retries: - retry_after = int(e.response.headers.get("Retry-After", 2)) - logger.warning(f"Rate limited. Retry {retries + 1}/{self.max_retries} after {retry_after}s") - await asyncio.sleep(retry_after) - retries += 1 - else: - logger.error(f"Shopify API error: {e.response.status_code} - {e.response.text}") - raise - - except Exception as e: - logger.error(f"Unexpected error calling Shopify API: {str(e)}") - raise - - raise httpx.HTTPStatusError(f"Max retries exceeded for {url}", request=None, response=None) - - async def _get(self, path: str, params: Dict[str, Any] | None = None) -> Dict[str, Any]: - return await self._make_request("GET", path, params=params) - - async def _put(self, path: str, data: Dict[str, Any]) -> Dict[str, Any]: - return await self._make_request("PUT", path, data=data) - - async def list_products(self) -> List[Dict[str, Any]]: - products: List[Dict[str, Any]] = [] - page_info = None - params: Dict[str, Any] = {"limit": 250} - - while True: - if page_info: - params["page_info"] = page_info - - data = await self._get("/products", params=params) - batch = data.get("products", []) - if not batch: - break - products.extend(batch) - - # Simple break for now; pagination can be extended - if len(batch) < 250: - break - - return products - - async def update_variant_price(self, variant_id: str, new_price: float) -> Dict[str, Any]: - payload = { - "variant": { - "id": int(variant_id), - "price": new_price, - } - } - return await self._put(f"/variants/{variant_id}", data=payload) diff --git a/ecom-dynamic-pricing/backend/app/tasks.py b/ecom-dynamic-pricing/backend/app/tasks.py deleted file mode 100644 index 23a412f..0000000 --- a/ecom-dynamic-pricing/backend/app/tasks.py +++ /dev/null @@ -1,267 +0,0 @@ -from sqlalchemy.orm import Session -from typing import Any, List, Dict -import asyncio -import logging -from celery import Task -from celery.exceptions import MaxRetriesExceededError -import httpx - -from .celery_app import celery_app -from .database import SessionLocal -from .models import Store, Product, PricingEvent -from .shopify_client import ShopifyClient -from .pricing_engine import PricingEngine - -logger = logging.getLogger(__name__) - - -def _get_db() -> Session: - return SessionLocal() - - -class RetryTask(Task): - """Base task with automatic retry on failure""" - autoretry_for = (httpx.HTTPError, ConnectionError, TimeoutError) - retry_kwargs = {'max_retries': 3} - retry_backoff = True - retry_backoff_max = 600 - retry_jitter = True - - -@celery_app.task( - name="app.tasks.sync_store_products", - base=RetryTask, - bind=True, - time_limit=600, # 10 min hard limit - soft_time_limit=540 # 9 min soft limit -) -def sync_store_products(self, store_id: int) -> Dict[str, Any]: - db = _get_db() - synced_count = 0 - error_count = 0 - - try: - logger.info(f"Starting product sync for store_id={store_id}") - - store: Store | None = db.query(Store).filter(Store.id == store_id, Store.is_active == True).first() - if not store: - logger.warning(f"Store {store_id} not found or inactive") - return {"status": "skipped", "reason": "store_not_found_or_inactive"} - - async def _sync(): - nonlocal synced_count, error_count - client = ShopifyClient(store.shop_domain, store.access_token) - - try: - products_data = await client.list_products() - logger.info(f"Fetched {len(products_data)} products from Shopify for store {store_id}") - except httpx.HTTPStatusError as e: - logger.error(f"Shopify API error for store {store_id}: {e.response.status_code} - {e.response.text}") - raise - except Exception as e: - logger.error(f"Unexpected error fetching products for store {store_id}: {str(e)}") - raise - - for p in products_data: - for variant in p.get("variants", []): - try: - existing = ( - db.query(Product) - .filter( - Product.store_id == store.id, - Product.shopify_variant_id == str(variant["id"]), - ) - .first() - ) - - price = float(variant.get("price", 0.0)) - inventory = int(variant.get("inventory_quantity") or 0) - sku = variant.get("sku") - title = p.get("title", "Untitled") - cost = variant.get("cost", None) - - if existing: - existing.title = title - existing.sku = sku - existing.current_price = price - existing.inventory_quantity = inventory - existing.cost = float(cost) if cost is not None else None - else: - obj = Product( - store_id=store.id, - shopify_product_id=str(p["id"]), - shopify_variant_id=str(variant["id"]), - title=title, - sku=sku, - current_price=price, - inventory_quantity=inventory, - cost=float(cost) if cost is not None else None, - ) - db.add(obj) - - synced_count += 1 - except Exception as e: - error_count += 1 - logger.error(f"Error processing variant {variant.get('id')}: {str(e)}") - - db.commit() - logger.info(f"Sync complete for store {store_id}: {synced_count} synced, {error_count} errors") - - asyncio.run(_sync()) - - return { - "status": "success", - "store_id": store_id, - "synced_count": synced_count, - "error_count": error_count - } - - except MaxRetriesExceededError: - logger.error(f"Max retries exceeded for store {store_id} sync") - return { - "status": "failed", - "store_id": store_id, - "reason": "max_retries_exceeded", - "synced_count": synced_count, - "error_count": error_count - } - except Exception as e: - logger.error(f"Fatal error syncing store {store_id}: {str(e)}", exc_info=True) - db.rollback() - raise - finally: - db.close() - - -@celery_app.task( - name="app.tasks.run_pricing_cycle", - base=RetryTask, - bind=True, - time_limit=900, # 15 min hard limit - soft_time_limit=840 # 14 min soft limit -) -def run_pricing_cycle(self, store_id: int, preview_mode: bool = False) -> Dict[str, Any]: - """ - Run pricing cycle for a store. - - Args: - store_id: Store ID to process - preview_mode: If True, only generate suggestions without applying them - - Returns: - Dict with status, changes count, and suggestions - """ - db = _get_db() - applied_count = 0 - skipped_count = 0 - error_count = 0 - suggestions_list = [] - - try: - logger.info(f"Starting pricing cycle for store_id={store_id}, preview_mode={preview_mode}") - - store: Store | None = db.query(Store).filter(Store.id == store_id, Store.is_active == True).first() - if not store: - logger.warning(f"Store {store_id} not found or inactive") - return {"status": "skipped", "reason": "store_not_found_or_inactive"} - - # Check if automation is suspended - if hasattr(store, 'automation_suspended') and store.automation_suspended and not preview_mode: - logger.info(f"Automation suspended for store {store_id}") - return {"status": "skipped", "reason": "automation_suspended"} - - # In a real system, we'd compute sales_velocity from order history - # For now, use placeholder velocity based on inventory - variants: List[Dict[str, Any]] = [] - for p in store.products: - variants.append( - { - "variant_id": p.shopify_variant_id, - "current_price": p.current_price, - "inventory_quantity": p.inventory_quantity or 0, - "sales_velocity": 2.0, # TODO: compute from orders later - "first_seen_at": p.first_seen_at, - "cost": p.cost, - } - ) - - logger.info(f"Analyzing {len(variants)} variants for store {store_id}") - - # Initialize pricing engine with store-specific guardrails - engine = PricingEngine( - min_price=store.min_price_limit, - max_price=store.max_price_limit, - new_product_protection_days=store.new_product_protection_days or 7, - ) - suggestions = engine.suggest_prices(variants) - suggestions_list = suggestions.copy() - - if preview_mode: - logger.info(f"Preview mode: generated {len(suggestions)} suggestions") - return { - "status": "preview", - "store_id": store_id, - "suggestions": suggestions, - "total_suggestions": len(suggestions) - } - - async def _apply(): - nonlocal applied_count, skipped_count, error_count - client = ShopifyClient(store.shop_domain, store.access_token) - - for s in suggestions: - if s["reason"] == "no_change": - skipped_count += 1 - continue - - try: - await client.update_variant_price(s["variant_id"], s["new_price"]) - - event = PricingEvent( - store_id=store.id, - shopify_variant_id=s["variant_id"], - old_price=s["old_price"], - new_price=s["new_price"], - reason=s["reason"], - metadata={"preview": False, "task_id": str(self.request.id)}, - ) - db.add(event) - applied_count += 1 - logger.debug(f"Applied price change for variant {s['variant_id']}: ${s['old_price']} -> ${s['new_price']}") - - except httpx.HTTPStatusError as e: - error_count += 1 - logger.error(f"Shopify API error updating variant {s['variant_id']}: {e.response.status_code}") - except Exception as e: - error_count += 1 - logger.error(f"Error updating variant {s['variant_id']}: {str(e)}") - - db.commit() - logger.info(f"Pricing cycle complete for store {store_id}: {applied_count} applied, {skipped_count} skipped, {error_count} errors") - - asyncio.run(_apply()) - - return { - "status": "success", - "store_id": store_id, - "applied_count": applied_count, - "skipped_count": skipped_count, - "error_count": error_count, - "suggestions": suggestions_list[:10] # Return first 10 for reference - } - - except MaxRetriesExceededError: - logger.error(f"Max retries exceeded for store {store_id} pricing cycle") - return { - "status": "failed", - "store_id": store_id, - "reason": "max_retries_exceeded", - "applied_count": applied_count, - "error_count": error_count - } - except Exception as e: - logger.error(f"Fatal error in pricing cycle for store {store_id}: {str(e)}", exc_info=True) - db.rollback() - raise - finally: - db.close() diff --git a/ecom-dynamic-pricing/backend/requirements.txt b/ecom-dynamic-pricing/backend/requirements.txt deleted file mode 100644 index 38db90c..0000000 --- a/ecom-dynamic-pricing/backend/requirements.txt +++ /dev/null @@ -1,13 +0,0 @@ -fastapi==0.115.0 -uvicorn[standard]==0.30.0 -pydantic==2.9.0 -pydantic-settings==2.5.2 -SQLAlchemy==2.0.36 -psycopg2-binary==2.9.9 -alembic==1.13.2 -httpx==0.27.2 -python-dotenv==1.0.1 -celery==5.4.0 -redis==5.0.8 -python-dateutil==2.9.0 -sentry-sdk[fastapi]==2.17.0 diff --git a/ecom-dynamic-pricing/docker-compose.yml b/ecom-dynamic-pricing/docker-compose.yml deleted file mode 100644 index 8dfc83b..0000000 --- a/ecom-dynamic-pricing/docker-compose.yml +++ /dev/null @@ -1,51 +0,0 @@ -version: "3.9" - -services: - db: - image: postgres:16 - container_name: ecom_db - restart: unless-stopped - environment: - POSTGRES_USER: ${POSTGRES_USER:-ecom} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-ecom_password} - POSTGRES_DB: ${POSTGRES_DB:-ecom_db} - ports: - - "5432:5432" - volumes: - - ecom_pg_data:/var/lib/postgresql/data - - redis: - image: redis:7 - container_name: ecom_redis - restart: unless-stopped - ports: - - "6379:6379" - - backend: - build: - context: ./backend - container_name: ecom_backend - restart: unless-stopped - env_file: - - .env - depends_on: - - db - - redis - ports: - - "8000:8000" - command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload - - worker: - build: - context: ./backend - container_name: ecom_worker - restart: unless-stopped - env_file: - - .env - depends_on: - - redis - - db - command: celery -A app.celery_app.celery_app worker --loglevel=info - -volumes: - ecom_pg_data: diff --git a/ecom-dynamic-pricing/frontend/README.md b/ecom-dynamic-pricing/frontend/README.md deleted file mode 100644 index 940cf6c..0000000 --- a/ecom-dynamic-pricing/frontend/README.md +++ /dev/null @@ -1,172 +0,0 @@ -# E-Commerce Dynamic Pricing - Frontend - -React + Vite + TypeScript + Tailwind CSS frontend for the dynamic pricing system. - -## Features - -### Merchant Dashboard -- **Control Panel**: Suspend/resume automation, preview changes, rollback prices -- **Guardrails Settings**: Set min/max price limits and new product protection -- **Pricing History**: View all price changes with reasons and rollback status -- **Real-time Stats**: Product count, recent changes, protection days - -### Operator Panel -- **System Health**: Monitor Postgres, Redis, Celery workers -- **Live Monitoring**: Auto-refresh every 30 seconds -- **Recent Activity**: View pricing events across all stores -- **Quick Actions**: Access common operations - -## Quick Start - -```bash -# Install dependencies -npm install - -# Start development server -npm run dev - -# Build for production -npm run build - -# Preview production build -npm run preview -``` - -The dev server runs on `http://localhost:3000` and proxies API requests to `http://localhost:8000`. - -## Project Structure - -``` -frontend/ -โ”œโ”€โ”€ src/ -โ”‚ โ”œโ”€โ”€ components/ -โ”‚ โ”‚ โ”œโ”€โ”€ merchant/ # Merchant-facing components -โ”‚ โ”‚ โ”œโ”€โ”€ operator/ # Operator-facing components -โ”‚ โ”‚ โ””โ”€โ”€ shared/ # Shared UI components -โ”‚ โ”œโ”€โ”€ lib/ -โ”‚ โ”‚ โ”œโ”€โ”€ api.ts # API client -โ”‚ โ”‚ โ””โ”€โ”€ types.ts # TypeScript types -โ”‚ โ”œโ”€โ”€ pages/ -โ”‚ โ”‚ โ”œโ”€โ”€ merchant/ # Merchant pages -โ”‚ โ”‚ โ””โ”€โ”€ operator/ # Operator pages -โ”‚ โ”œโ”€โ”€ App.tsx # Main app with routing -โ”‚ โ”œโ”€โ”€ main.tsx # Entry point -โ”‚ โ””โ”€โ”€ index.css # Global styles -โ”œโ”€โ”€ package.json -โ”œโ”€โ”€ tsconfig.json -โ”œโ”€โ”€ vite.config.ts -โ””โ”€โ”€ tailwind.config.js -``` - -## Components - -### Shared Components -- **Button**: Primary, secondary, danger, success variants with loading states -- **Card**: Container with optional title and description -- **Badge**: Color-coded status indicators - -### Merchant Components -- **GuardrailsSettings**: Edit price limits and protection settings -- **PricingHistory**: Table of all pricing changes with filters -- **ControlPanel**: Main control interface with panic button - -### Operator Components -- **SystemHealth**: Infrastructure monitoring dashboard -- (More components can be added as needed) - -## API Integration - -All API calls go through `src/lib/api.ts`: - -```typescript -import { operatorApi } from './lib/api'; - -// Get store status -const status = await operatorApi.getStoreStatus(storeId); - -// Suspend automation -await operatorApi.suspendAutomation(storeId); - -// Preview pricing -const preview = await operatorApi.previewPricing(storeId); - -// Rollback prices -await operatorApi.rollbackPrices(storeId, 10); -``` - -## Styling - -Using Tailwind CSS utility classes. Common patterns: - -```tsx -// Card container -
- -// Button primary - - ) : ( - - )} - - - - -
- -
-
Quick Tips
-
    -
  • โ€ข Use Preview to see what will change before applying
  • -
  • โ€ข Suspend if you need to make manual price adjustments
  • -
  • โ€ข Rollback reverts prices to their previous values
  • -
-
- - - ); -} diff --git a/ecom-dynamic-pricing/frontend/src/components/merchant/GuardrailsSettings.tsx b/ecom-dynamic-pricing/frontend/src/components/merchant/GuardrailsSettings.tsx deleted file mode 100644 index c4ddcc6..0000000 --- a/ecom-dynamic-pricing/frontend/src/components/merchant/GuardrailsSettings.tsx +++ /dev/null @@ -1,179 +0,0 @@ -import { useState, useEffect } from 'react'; -import { operatorApi } from '../../lib/api'; -import type { Guardrails, StoreStatus } from '../../lib/types'; -import { Card } from '../shared/Card'; -import { Button } from '../shared/Button'; -import { AlertCircle, Check } from 'lucide-react'; - -interface GuardrailsSettingsProps { - storeId: number; -} - -export function GuardrailsSettings({ storeId }: GuardrailsSettingsProps) { - const [guardrails, setGuardrails] = useState({ - new_product_protection_days: 7, - }); - const [isLoading, setIsLoading] = useState(false); - const [isSaving, setIsSaving] = useState(false); - const [error, setError] = useState(null); - const [successMessage, setSuccessMessage] = useState(null); - - useEffect(() => { - loadGuardrails(); - }, [storeId]); - - const loadGuardrails = async () => { - setIsLoading(true); - setError(null); - try { - const status: StoreStatus = await operatorApi.getStoreStatus(storeId); - setGuardrails(status.guardrails); - } catch (err) { - setError('Failed to load guardrails'); - console.error(err); - } finally { - setIsLoading(false); - } - }; - - const handleSave = async () => { - setIsSaving(true); - setError(null); - setSuccessMessage(null); - - try { - await operatorApi.updateGuardrails(storeId, guardrails); - setSuccessMessage('Guardrails updated successfully!'); - setTimeout(() => setSuccessMessage(null), 3000); - } catch (err) { - setError('Failed to update guardrails'); - console.error(err); - } finally { - setIsSaving(false); - } - }; - - if (isLoading) { - return ( - -
-
-
-
- ); - } - - return ( - -
- {error && ( -
- -
-

Error

-

{error}

-
-
- )} - - {successMessage && ( -
- -
-

Success

-

{successMessage}

-
-
- )} - -
- -
- $ - - setGuardrails({ - ...guardrails, - min_price_limit: e.target.value ? parseFloat(e.target.value) : undefined, - }) - } - className="block w-full pl-8 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" - placeholder="5.00" - /> -
-

- No product will be priced below this amount -

-
- -
- -
- $ - - setGuardrails({ - ...guardrails, - max_price_limit: e.target.value ? parseFloat(e.target.value) : undefined, - }) - } - className="block w-full pl-8 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" - placeholder="500.00" - /> -
-

- No product will be priced above this amount -

-
- -
- - - setGuardrails({ - ...guardrails, - new_product_protection_days: parseInt(e.target.value) || 0, - }) - } - className="block w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> -

- New products won't be auto-priced for this many days after first sync -

-
- -
- -
-
-
- ); -} diff --git a/ecom-dynamic-pricing/frontend/src/components/merchant/PricingHistory.tsx b/ecom-dynamic-pricing/frontend/src/components/merchant/PricingHistory.tsx deleted file mode 100644 index 5e9990e..0000000 --- a/ecom-dynamic-pricing/frontend/src/components/merchant/PricingHistory.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import { useState, useEffect } from 'react'; -import { operatorApi } from '../../lib/api'; -import type { PricingEvent } from '../../lib/types'; -import { Card } from '../shared/Card'; -import { Badge } from '../shared/Badge'; -import { formatDistanceToNow } from 'date-fns'; -import { TrendingUp, TrendingDown, Minus, ArrowLeft } from 'lucide-react'; - -interface PricingHistoryProps { - storeId: number; -} - -export function PricingHistory({ storeId }: PricingHistoryProps) { - const [events, setEvents] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - loadHistory(); - }, [storeId]); - - const loadHistory = async () => { - setIsLoading(true); - setError(null); - - try { - const data = await operatorApi.getPricingHistory(storeId, 50); - setEvents(data); - } catch (err) { - setError('Failed to load pricing history'); - console.error(err); - } finally { - setIsLoading(false); - } - }; - - const getReasonBadge = (reason: string) => { - if (reason.includes('increase')) return 'success'; - if (reason.includes('decrease')) return 'warning'; - if (reason.includes('protected') || reason.includes('clamped')) return 'info'; - return 'neutral'; - }; - - const getReasonLabel = (reason: string) => { - const labels: Record = { - fast_seller_increase: 'Fast Seller', - slow_seller_decrease: 'Slow Seller', - new_product_protected: 'New Product', - cost_floor_protected: 'Cost Floor', - no_change: 'No Change', - }; - return labels[reason] || reason.replace(/_/g, ' '); - }; - - if (isLoading) { - return ( - -
-
-
-
- ); - } - - if (error) { - return ( - -
-

{error}

- -
-
- ); - } - - if (events.length === 0) { - return ( - -
-

No pricing changes yet.

-

Changes will appear here after the first pricing cycle.

-
-
- ); - } - - return ( - -
- {events.map((event) => ( -
-
-
-
- - #{event.variant_id} - - - {getReasonLabel(event.reason)} - - {event.rolled_back && ( - Rolled Back - )} -
- -
-
- - ${event.old_price.toFixed(2)} - -
- {event.change_amount > 0 ? ( - - ) : event.change_amount < 0 ? ( - - ) : ( - - )} -
- - ${event.new_price.toFixed(2)} - -
- -
- 0 - ? 'text-green-600 font-medium' - : event.change_amount < 0 - ? 'text-red-600 font-medium' - : 'text-gray-500' - } - > - {event.change_amount > 0 ? '+' : ''} - ${Math.abs(event.change_amount).toFixed(2)} ( - {event.change_amount > 0 ? '+' : ''} - {event.change_percent.toFixed(1)}%) - -
-
- -
- {formatDistanceToNow(new Date(event.created_at), { - addSuffix: true, - })} - {event.rolled_back_at && ( - - โ€ข Rolled back{' '} - {formatDistanceToNow(new Date(event.rolled_back_at), { - addSuffix: true, - })} - - )} -
-
-
-
- ))} -
-
- ); -} diff --git a/ecom-dynamic-pricing/frontend/src/components/operator/SystemHealth.tsx b/ecom-dynamic-pricing/frontend/src/components/operator/SystemHealth.tsx deleted file mode 100644 index ac18a25..0000000 --- a/ecom-dynamic-pricing/frontend/src/components/operator/SystemHealth.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { useState, useEffect } from 'react'; -import { healthApi } from '../../lib/api'; -import type { HealthStatus } from '../../lib/types'; -import { Card } from '../shared/Card'; -import { Badge } from '../shared/Badge'; -import { Database, Cpu, Activity, RefreshCw, CheckCircle, XCircle } from 'lucide-react'; - -export function SystemHealth() { - const [health, setHealth] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [lastCheck, setLastCheck] = useState(new Date()); - - useEffect(() => { - loadHealth(); - const interval = setInterval(loadHealth, 30000); // Refresh every 30s - return () => clearInterval(interval); - }, []); - - const loadHealth = async () => { - try { - const data = await healthApi.getDetailedHealth(); - setHealth(data); - setLastCheck(new Date()); - } catch (err) { - console.error('Failed to load health:', err); - } finally { - setIsLoading(false); - } - }; - - const getStatusVariant = (status: string) => { - if (status === 'healthy') return 'success'; - if (status === 'degraded') return 'warning'; - return 'danger'; - }; - - const getStatusIcon = (status: string) => { - if (status === 'healthy') return ; - return ; - }; - - if (isLoading || !health) { - return ( - -
-
-
-
- ); - } - - return ( - -
-
-
- - {health.status.toUpperCase()} - - - Last checked: {lastCheck.toLocaleTimeString()} - -
- -
- - {/* PostgreSQL */} -
-
-
- -
-

PostgreSQL

-

- {health.components.postgres.message} -

-
-
- {getStatusIcon(health.components.postgres.status)} -
-
- - {/* Redis */} -
-
-
- -
-

Redis

-

- {health.components.redis.message} -

-
-
- {getStatusIcon(health.components.redis.status)} -
-
- - {/* Celery Workers */} -
-
-
- -
-

Celery Workers

-

- {health.components.celery.message} -

- {health.components.celery.workers && ( -
- {health.components.celery.workers.map((worker) => ( - - {worker} - - ))} -
- )} -
-
- {getStatusIcon(health.components.celery.status)} -
-
-
-
- ); -} diff --git a/ecom-dynamic-pricing/frontend/src/components/shared/Badge.tsx b/ecom-dynamic-pricing/frontend/src/components/shared/Badge.tsx deleted file mode 100644 index 05bdbaf..0000000 --- a/ecom-dynamic-pricing/frontend/src/components/shared/Badge.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { ReactNode } from 'react'; -import clsx from 'clsx'; - -interface BadgeProps { - children: ReactNode; - variant?: 'success' | 'warning' | 'danger' | 'info' | 'neutral'; - className?: string; -} - -export function Badge({ children, variant = 'neutral', className }: BadgeProps) { - const variantStyles = { - success: 'bg-green-100 text-green-800', - warning: 'bg-yellow-100 text-yellow-800', - danger: 'bg-red-100 text-red-800', - info: 'bg-blue-100 text-blue-800', - neutral: 'bg-gray-100 text-gray-800', - }; - - return ( - - {children} - - ); -} diff --git a/ecom-dynamic-pricing/frontend/src/components/shared/Button.tsx b/ecom-dynamic-pricing/frontend/src/components/shared/Button.tsx deleted file mode 100644 index 8a079b6..0000000 --- a/ecom-dynamic-pricing/frontend/src/components/shared/Button.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { ButtonHTMLAttributes, ReactNode } from 'react'; -import clsx from 'clsx'; - -interface ButtonProps extends ButtonHTMLAttributes { - variant?: 'primary' | 'secondary' | 'danger' | 'success'; - size?: 'sm' | 'md' | 'lg'; - children: ReactNode; - isLoading?: boolean; -} - -export function Button({ - variant = 'primary', - size = 'md', - children, - isLoading = false, - className, - disabled, - ...props -}: ButtonProps) { - const baseStyles = 'inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed'; - - const variantStyles = { - primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500', - secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500', - danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500', - success: 'bg-green-600 text-white hover:bg-green-700 focus:ring-green-500', - }; - - const sizeStyles = { - sm: 'px-3 py-1.5 text-sm', - md: 'px-4 py-2 text-base', - lg: 'px-6 py-3 text-lg', - }; - - return ( - - ); -} diff --git a/ecom-dynamic-pricing/frontend/src/components/shared/Card.tsx b/ecom-dynamic-pricing/frontend/src/components/shared/Card.tsx deleted file mode 100644 index d28dbd5..0000000 --- a/ecom-dynamic-pricing/frontend/src/components/shared/Card.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { ReactNode } from 'react'; -import clsx from 'clsx'; - -interface CardProps { - children: ReactNode; - className?: string; - title?: string; - description?: string; -} - -export function Card({ children, className, title, description }: CardProps) { - return ( -
- {title && ( -
-

{title}

- {description && ( -

{description}

- )} -
- )} - {children} -
- ); -} diff --git a/ecom-dynamic-pricing/frontend/src/index.css b/ecom-dynamic-pricing/frontend/src/index.css deleted file mode 100644 index 17df0e7..0000000 --- a/ecom-dynamic-pricing/frontend/src/index.css +++ /dev/null @@ -1,17 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; -} diff --git a/ecom-dynamic-pricing/frontend/src/main.tsx b/ecom-dynamic-pricing/frontend/src/main.tsx deleted file mode 100644 index 3d7150d..0000000 --- a/ecom-dynamic-pricing/frontend/src/main.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' -import App from './App.tsx' -import './index.css' - -ReactDOM.createRoot(document.getElementById('root')!).render( - - - , -) diff --git a/ecom-dynamic-pricing/frontend/src/pages/merchant/MerchantDashboard.tsx b/ecom-dynamic-pricing/frontend/src/pages/merchant/MerchantDashboard.tsx deleted file mode 100644 index 06c2c00..0000000 --- a/ecom-dynamic-pricing/frontend/src/pages/merchant/MerchantDashboard.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { useState, useEffect } from 'react'; -import { operatorApi } from '../../lib/api'; -import type { StoreStatus } from '../../lib/types'; -import { GuardrailsSettings } from '../../components/merchant/GuardrailsSettings'; -import { PricingHistory } from '../../components/merchant/PricingHistory'; -import { ControlPanel } from '../../components/merchant/ControlPanel'; -import { Store, TrendingUp, Shield, Clock } from 'lucide-react'; - -export function MerchantDashboard() { - // In a real app, this would come from auth/route params - const storeId = 1; - - const [status, setStatus] = useState(null); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - loadStatus(); - }, []); - - const loadStatus = async () => { - setIsLoading(true); - try { - const data = await operatorApi.getStoreStatus(storeId); - setStatus(data); - } catch (err) { - console.error('Failed to load store status:', err); - } finally { - setIsLoading(false); - } - }; - - if (isLoading || !status) { - return ( -
-
-
-

Loading dashboard...

-
-
- ); - } - - return ( -
- {/* Header */} -
-
-
-
-
- -
-
-

- Dynamic Pricing Dashboard -

-

{status.shop_domain}

-
-
-
-
-
- - {/* Stats Overview */} -
-
-
-
-
-

Total Products

-

- {status.product_count} -

-
-
- -
-
-
- -
-
-
-

Recent Changes

-

- {status.recent_events_count} -

-
-
- -
-
-
- -
-
-
-

Protection Days

-

- {status.guardrails.new_product_protection_days} -

-
-
- -
-
-
-
- - {/* Main Content */} -
-
- - -
- -
- -
-
-
-
- ); -} diff --git a/ecom-dynamic-pricing/frontend/src/pages/operator/OperatorPanel.tsx b/ecom-dynamic-pricing/frontend/src/pages/operator/OperatorPanel.tsx deleted file mode 100644 index a10660b..0000000 --- a/ecom-dynamic-pricing/frontend/src/pages/operator/OperatorPanel.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { SystemHealth } from '../../components/operator/SystemHealth'; -import { PricingHistory } from '../../components/merchant/PricingHistory'; -import { Settings, Activity } from 'lucide-react'; - -export function OperatorPanel() { - // In a real app, this would show all stores and allow selection - const storeId = 1; - - return ( -
- {/* Header */} -
-
-
-
-
- -
-
-

- Operator Panel -

-

System monitoring and management

-
-
-
- - Live -
-
-
-
- - {/* Main Content */} -
-
- {/* Left Column - Monitoring */} - - - {/* Right Column - Recent Activity */} -
- -
-
- - {/* Instructions */} -
-

- Operator Runbook -

-
    -
  • - โ€ข System Health: Check component status. All should show - "healthy" for normal operation. -
  • -
  • - โ€ข Worker Issues: If workers are down, restart with{' '} - docker-compose restart worker -
  • -
  • - โ€ข Database Issues: Check logs with{' '} - docker-compose logs db -
  • -
  • - โ€ข Merchant Reports Issue: Check store status, verify recent - events, test rollback if needed -
  • -
  • - โ€ข Full Runbook: See OPERATOR_RUNBOOK.md in the repository -
  • -
-
-
-
- ); -} diff --git a/ecom-dynamic-pricing/frontend/tailwind.config.js b/ecom-dynamic-pricing/frontend/tailwind.config.js deleted file mode 100644 index dca8ba0..0000000 --- a/ecom-dynamic-pricing/frontend/tailwind.config.js +++ /dev/null @@ -1,11 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -export default { - content: [ - "./index.html", - "./src/**/*.{js,ts,jsx,tsx}", - ], - theme: { - extend: {}, - }, - plugins: [], -} diff --git a/ecom-dynamic-pricing/frontend/tsconfig.json b/ecom-dynamic-pricing/frontend/tsconfig.json deleted file mode 100644 index a7fc6fb..0000000 --- a/ecom-dynamic-pricing/frontend/tsconfig.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["src"], - "references": [{ "path": "./tsconfig.node.json" }] -} diff --git a/ecom-dynamic-pricing/frontend/tsconfig.node.json b/ecom-dynamic-pricing/frontend/tsconfig.node.json deleted file mode 100644 index 42872c5..0000000 --- a/ecom-dynamic-pricing/frontend/tsconfig.node.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "composite": true, - "skipLibCheck": true, - "module": "ESNext", - "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true - }, - "include": ["vite.config.ts"] -} diff --git a/ecom-dynamic-pricing/frontend/vite.config.ts b/ecom-dynamic-pricing/frontend/vite.config.ts deleted file mode 100644 index 91a0315..0000000 --- a/ecom-dynamic-pricing/frontend/vite.config.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' - -export default defineConfig({ - plugins: [react()], - server: { - port: 3000, - proxy: { - '/api': { - target: 'http://localhost:8000', - changeOrigin: true, - }, - }, - }, -})