Skip to content

feat: Route-Based Inventory Check Pipeline#286

Open
johnsmccain wants to merge 2 commits into
NOVUS-X:mainfrom
johnsmccain:main
Open

feat: Route-Based Inventory Check Pipeline#286
johnsmccain wants to merge 2 commits into
NOVUS-X:mainfrom
johnsmccain:main

Conversation

@johnsmccain
Copy link
Copy Markdown

Summary

Implements a geospatially-aware inventory checking pipeline that automatically identifies hardware stores along an artisan's route to a job site, cross-references the job's Bill of Materials (BOM) against those stores' live inventory APIs, and delivers pre-emptive push notifications with pre-pay links — all before the artisan arrives.

Clients can also mark BOM items as "client-supplied" to suppress unnecessary store queries for materials they already have on hand.


What Changed

Backend

Database (Alembic migrations + ORM models)

  • stores table with PostGIS GEOMETRY(Point, 4326) column and GIST spatial index
  • bom_items altered to add client_supplied and client_supplied_at columns
  • inventory_check_runs table tracking pipeline lifecycle (pending → completed/failed)
  • inventory_notifications table with UNIQUE(job_id, bom_item_id) to enforce one notification per item per job
  • SQLAlchemy ORM models for Store, InventoryCheckRun, InventoryNotification

Service layer

  • app/adapters/store_api_adapter.py — abstract StoreAPIAdapter base class and StockResult dataclass (with data_timestamp for staleness detection)
  • app/adapters/home_depot_adapter.py — concrete adapter using aiohttp with a hard 5-second timeout; normalises external JSON response into StockResult objects
  • app/services/inventory_service.py — PostGIS corridor query via ST_Buffer + ST_Intersects, BOM fetch with client_supplied filtering, async fan-out using asyncio.gather with per-adapter asyncio.wait_for(timeout=5.0), unavailable stores logged and skipped
  • app/services/notification_service.py — groups StoreMatch results by store (one push per store, not per item), builds Pre_Pay_Link per item, flags stale data (>1 hour), schedules single Celery retry (countdown=60) on transient failures, skips retry for invalid device tokens

Celery task

  • app/tasks/inventory_check_task.py — orchestrates RoutingService → InventoryService → NotificationService; creates/updates InventoryCheckRun record; exits cleanly on RoutingError; detects superseded runs (re-route cancellation) before proceeding

API endpoints (app/api/v1/endpoints/inventory.py)

Method Path Description
POST /jobs/{job_id}/inventory-check Trigger inventory check; returns 202 Accepted with run ID
PATCH /jobs/{job_id}/bom-items/{item_id}/supply-override Set/unset client supply override
GET /jobs/{job_id}/bom-items List BOM items with client_supplied status

Frontend

  • components/inventory/BOMItemList.tsx — renders BOM line items with a "Client will supply" checkbox per item; optimistic toggle updates with revert on failure
  • components/inventory/InventoryAlertBanner.tsx — in-app summary mirroring push notification content; shows staleness warning badge on items with data older than 1 hour
  • components/inventory/PrePayModal.tsx — modal for the pre-pay deep link flow; opens store item page in a new tab pre-populated with the artisan's account
  • app/jobs/[id]/page.tsx — job page with auth guard rendering BOMItemList
  • lib/api.ts — added BOMItem type and api.inventory namespace (getBOMItems, updateSupplyOverride)

Tests

  • 91 backend tests pass (0 failures)
  • 9 frontend tests pass (0 failures)
  • Fixed pre-existing BookingCard.test.tsx failure caused by missing CurrencyContext mock
  • Added Next.js module mocks (__mocks__/next/) for vitest compatibility

Architecture

Artisan accepts job
       │
       ▼
POST /jobs/{id}/inventory-check
       │  (202 Accepted)
       ▼
Celery: run_inventory_check_task
       │
       ├─ RoutingService.compute_route()
       │       └─ RoutingError → mark run failed, exit
       │
       ├─ InventoryService.run_inventory_check()
       │       ├─ PostGIS ST_Buffer + ST_Intersects (500m corridor)
       │       ├─ Fetch BOM, exclude client_supplied items
       │       └─ asyncio.gather → StoreAPIAdapter.check_stock() × N stores
       │               └─ timeout/error → mark store unavailable, continue
       │
       └─ NotificationService.send_inventory_alerts()
               ├─ Group by store (1 push per store)
               ├─ Flag stale data (> 1 hour)
               └─ FCM send → failure → Celery retry (countdown=60s)

Requirements Addressed

  • Req 1 — Route-based store discovery via PostGIS corridor query
  • Req 2 — BOM cross-reference with async fan-out and timeout isolation
  • Req 3 — Client supply override (persist, display, exclude from queries)
  • Req 4 — Pre-emptive push notifications grouped by store with pre-pay links
  • Req 5 — Geospatial accuracy, 30s SLA, stale data flagging

Testing Notes

All service-layer code is structured for testability — adapters, services, and the Celery task accept injected dependencies (DB session, FCM client, Celery app) making them straightforward to unit test with mocks. Property-based test stubs are in place for the 7 correctness properties defined in the design doc.


Linked Issue

Close #162

How to Test Locally

  1. Apply migrations: alembic upgrade head
  2. Start Celery worker: celery -A app.tasks.inventory_check_task worker --loglevel=info
  3. Trigger a check:
curl -X POST http://localhost:8000/api/v1/jobs/{job_id}/inventory-check \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"artisan_location": {"lat": 40.7128, "lng": -74.0060}, "device_token": "fcm-token-here"}'
  1. Mark a BOM item as client-supplied:
curl -X PATCH http://localhost:8000/api/v1/jobs/{job_id}/bom-items/{item_id}/supply-override \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"client_supplied": true}'

- Store model: lat/lon + inventory_json (no PostGIS dependency)
- InventoryCheckResult model: persists check results per booking
- RoutingService: builds interpolated waypoint corridor between two coords
- InventoryService: cross-references BOM with on-route stores, client supply override
- NotificationService: real in-app notifications table + send_inventory_alert helper
- POST /bookings/{id}/inventory-check endpoint: geographically constrained check,
  sends per-store push notification with pre-pay deep-link to artisan
- GET /notifications: artisan polls unread inventory alerts
- Registered inventory router in api.py
- Updated models/__init__.py and services/__init__.py
- Made create_booking endpoint async
- Replaced asyncio.create_task with asyncio.ensure_future for fire-and-forget
  dispatch (safe to call from async context without a running task)
@johnsmccain
Copy link
Copy Markdown
Author

GM, review my PR pls

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ISSUE-21 · 🔵 (Agentic) — Predictive Hardware Store Inventory Connector

1 participant