Case Study: Designing Airgoods’ Reorder Center

Airgoods Reorder Center - Mock
Airgoods Reorder Center - Component Library
My approach
I started by defining the product boundary: the POS remains the source of truth, while Airgoods makes it easier to act on that inventory data. From there, I designed the data model, APIs, asynchronous sync flow, failure states, frontend components, and interactive mocks.
Requirements
Functional requirements
Retailers should be able to:
- View connected inventory sources, such as Square POS, Shopify Online, Toast, or ECRS.
- See products that are out of stock, low stock, or nearing reorder threshold by source and location.
- See when each source was last synced and manually refresh a source.
- Review reorder recommendations, including current stock, reorder threshold, and suggested reorder quantity.
- Select one or more recommendations and add them to the Airgoods cart.
- Only show items in the main reorder table if they map to an orderable Airgoods product variant.
MVP assumption:
integration_product_dataincludes enough metadata, such as SKU, UPC, barcode, or Airgoods product variant reference, to determine whether a source item maps to an orderable Airgoods variant.
Non-functional requirements
- The Reorder Center should load quickly without waiting on live POS/ecommerce API calls during page render.
- Inventory freshness must be visible with
lastSyncedAtper source. - Inventory and recommendations must be scoped by
integration_connection_idandexternal_location_id. - Sync jobs should be retryable and tolerant of provider failures, rate limits, and outages.
- Add-to-cart should be idempotent so retries or double-clicks do not duplicate cart items.
Out of scope
- Auto-submitting orders without retailer confirmation.
- Replacing the retailer’s POS inventory system.
- Advanced forecasting, proactive email/in-app notifications, manual mapping review, and reordering POS items Airgoods does not sell.
Core entities
- User: authenticated Airgoods user.
- Retailer: buyer account using Airgoods.
- Seller: supplier or brand selling products through Airgoods.
- ProductVariant: exact orderable Airgoods item, including size, pack count, flavor, SKU, or sellable-unit details.
- IntegrationConnection: retailer-specific connected inventory source, such as Square, Shopify, Toast, or ECRS.
- InventorySnapshot: normalized inventory observation from an external source at a point in time.
- IntegrationProductData: external product and variant metadata from the retailer’s source catalog.
- ProductVariantMapping: reliable bridge from an external source item to an Airgoods product variant.
- ReorderSetting: threshold and target stock rule for a retailer, source, location, and item.
- ReorderRecommendation: computed low-stock or out-of-stock recommendation shown to the client.
- Cart: retailer’s active Airgoods cart.
- SyncLog: record of inventory sync attempts, provider errors, refreshes, and sync timestamps.
API / System Interface
GET /api/retailers/{retailerId}/inventory-sourcesList connected inventory sources, sync status, enabled/paused state, and available locations.
{
"sources": [
{
"integrationConnectionId": "ic_square_123",
"provider": "square",
"displayName": "Square POS",
"status": "active",
"syncStatus": "active",
"lastSyncedAt": "2026-07-01T21:12:00Z",
"lastSyncError": null,
"locations": [
{ "externalLocationId": "loc_main", "name": "Main Store" }
]
}
]
}GET /api/retailers/{retailerId}/reorder-recommendations?integrationConnectionId=...&locationId=...&urgency=...Get summary counts and low-stock recommendations for the main table.
{
"summary": {
"outOfStockCount": 4,
"lowInventoryCount": 18,
"watchlistCount": 9
},
"items": [
{
"recommendationId": "rec_123",
"productVariantId": "pv_123",
"productName": "ZaZa Za'atar Pita Chips",
"sellerName": "ZaZa Snacks",
"integrationConnectionId": "ic_square_123",
"sourceName": "Square POS",
"externalLocationId": "loc_main",
"locationName": "Main Store",
"currentQuantity": 0,
"reorderThreshold": 12,
"recommendedQuantity": 48,
"urgency": "out_of_stock"
}
]
}This endpoint only returns source items that reliably map to orderable Airgoods product variants. It does not return POS/source items Airgoods cannot sell.
POST /api/retailers/{retailerId}/cart/items/bulkAdd selected recommendations to cart with chosen quantities.
{
"idempotencyKey": "idem_abc123",
"items": [
{
"recommendationId": "rec_123",
"productVariantId": "pv_123",
"quantity": 48
}
]
}{
"cartId": "cart_123",
"addedItemCount": 1,
"skippedItems": []
}POST /api/retailers/{retailerId}/inventory-sources/{integrationConnectionId}/refreshManually enqueue an inventory refresh for one source.
{
"externalLocationId": "loc_main"
}{
"syncJobId": "sync_123",
"status": "queued"
}After refresh, the client treats the response as asynchronous. It shows a syncing state, then polls GET /inventory-sources every few seconds until the source syncStatus changes from syncing to active or failed, or until lastSyncedAt updates. Once the sync completes, the client refetches GET /reorder-recommendations.
Manual refresh jobs should be prioritized over routine scheduled syncs. The backend should also dedupe queued/running syncs for the same source/location to avoid flooding provider APIs.
If the sync fails, the page keeps showing the last successful inventory snapshot and displays lastSyncError with a stale-data warning.
Database tables
| Table | Purpose |
|---|---|
users | Authenticated users and permissions. |
retailers | Buyer accounts using Airgoods. |
sellers | Brands/suppliers selling through Airgoods. |
products | Airgoods catalog products. |
product_variants | Orderable Airgoods variants/SKUs. |
orders / order_details | Existing order history, useful later for reorder behavior and sales velocity. |
integrations | Provider types, such as Square, Shopify, Toast, or ECRS. |
integration_connections | Retailer-specific connected source with auth, provider, and sync status. |
integration_product_data | External product metadata imported from connected sources, including fields used to map to Airgoods variants. |
inventory_snapshots | Normalized inventory observations from connected sources. |
product_variant_mappings | Reliable mappings from external source products/variants to Airgoods product_variants. |
inventory_reorder_settings | Reorder thresholds and target stock levels by retailer/source/location/item. |
reorder_recommendations | Computed low-stock recommendations shown in the Reorder Center. |
sync_logs | Sync attempts, success/failure, provider errors, refreshes, and timestamps per source. |
inventory_snapshots
- id
- retailer_id
- integration_connection_id
- external_product_id
- external_variant_id
- external_location_id
- quantity_on_hand
- quantity_reserved
- quantity_available
- observed_at
- created_atproduct_variant_mappings
- id
- retailer_id
- integration_connection_id
- external_product_id
- external_variant_id
- product_variant_id
- match_method -- sku | upc | barcode | explicit_reference
- created_at
- updated_atreorder_recommendations
- id
- retailer_id
- integration_connection_id
- external_location_id
- product_variant_id
- current_quantity
- reorder_threshold
- recommended_quantity
- urgency -- out_of_stock | low | watchlist
- status -- active | dismissed | added_to_cart | expired
- generated_at
- expires_atRecommendation logic
For v1, I would use explainable threshold-based rules:
if quantity_available <= 0:
urgency = out_of_stock
else if quantity_available <= reorder_threshold:
urgency = low
else if quantity_available <= reorder_threshold * 1.5:
urgency = watchlistDefinitions:
- Out of stock:
quantity_available <= 0. - Low stock:
quantity_available > 0andquantity_available <= reorder_threshold. - Watchlist:
quantity_available > reorder_thresholdandquantity_available <= reorder_threshold * 1.5.
Recommended quantity:
recommended_quantity = max(
default_reorder_quantity,
target_stock_level - quantity_available
)This keeps v1 explainable; forecasting can be added later using sales velocity and order history.
Frontend components
Visual component sketches are in component_sketches.html.
| Component | What it does |
|---|---|
ReorderCenterPage | Owns page-level data fetching, selected filters, selected recommendation IDs, and layout. |
InventorySourceManager | Shows connected sources, sync status, enabled/paused state, manual refresh buttons, and source selection. |
SyncStatusBanner | Communicates freshness and stale/error states so users trust the inventory data. |
ReorderSummaryCards | Gives a quick overview of inventory urgency and recommendation volume. |
ReorderFilters | Lets retailers narrow recommendations when they have many products or locations. |
LowInventoryTable | Displays only source items with reliable Airgoods product variant mappings that are safe to reorder. |
ReorderRecommendationRow | Lets the retailer inspect and act on one recommendation. |
RecommendedQuantityControl | Lets the retailer adjust the recommended reorder quantity before adding to cart. |
BulkReorderActionBar | Lets the retailer add selected recommendations to cart in one action. |
Data flows
Page load / read flow
Retailer opens Reorder Center
-> client calls GET /inventory-sources
-> client calls GET /reorder-recommendations
-> Backend API reads precomputed source status and recommendations from Postgres
-> client renders source filters, sync freshness, summary cards, and low-stock tableThe page does not call Square, Shopify, Toast, or ECRS directly during render.
Background sync / recommendation flow
Scheduled sync, provider webhook, or manual refresh
-> sync job is added to the queue
-> sync worker calls the external provider API
-> provider adapter normalizes inventory into Airgoods fields
-> inventory snapshots are stored
-> recommendation job is added to the queue
-> recommendation worker joins snapshots, reliable mappings, and reorder settings
-> reorder recommendations are written to PostgresItems without a reliable Airgoods mapping are excluded from MVP reorder recommendations.
Manual refresh flow
Retailer clicks Refresh on a source
-> client calls POST /inventory-sources/{integrationConnectionId}/refresh
-> Backend API validates access and enqueues a sync job
-> API returns { syncJobId, status: "queued" }
-> client shows syncing state and polls GET /inventory-sources
-> when syncStatus becomes active/failed or lastSyncedAt changes, client refetches recommendationsIf refresh fails, the client keeps showing the last successful snapshot and displays a stale-data warning.
Add-to-cart flow
Retailer selects recommendations
-> client calls POST /cart/items/bulk with recommendation IDs and quantities
-> Backend API validates recommendation ownership and orderability
-> Backend API writes cart items idempotently
-> client shows updated cart stateHigh-level design
Path 1: user-facing read/action path
flowchart LR Client[Reorder Center<br/>client] Gateway[API Gateway<br/>Load Balancer] API[Backend API] DB[(Postgres)] Queue[[Queue]] Client -->|GET sources + recs| Gateway Client -->|POST add to cart| Gateway Client -->|POST refresh| Gateway Gateway --> API API -->|read| DB API -->|write cart| DB API -->|enqueue| Queue
- Reads are served from stored source status and precomputed recommendations.
- Add-to-cart writes selected Airgoods product variants to the buyer account’s active cart.
- Manual refresh only enqueues work; it does not block on a live provider sync.
Path 2: async sync/recommendation path
flowchart LR Cron[Scheduled sync / cron] Webhook[Provider webhook handler] Manual[Manual refresh API] SyncQueue[[Sync queue]] SyncWorkers[Sync workers<br/>provider adapters] Providers[External providers<br/>Square / Shopify<br/>Toast / ECRS] DB[(Postgres<br/>snapshots<br/>mappings + settings<br/>recommendations)] RecQueue[[Recommendation queue]] RecWorker[Recommendation<br/>worker] Cron -->|enqueue sync job| SyncQueue Webhook -->|enqueue sync job| SyncQueue Manual -->|enqueue sync job| SyncQueue SyncQueue --> SyncWorkers SyncWorkers -->|fetch latest inventory| Providers SyncWorkers -->|write snapshots| DB SyncWorkers -->|enqueue rec job| RecQueue RecQueue --> RecWorker DB -->|snapshots + mappings + settings| RecWorker RecWorker -->|write recommendations| DB
- Scheduled syncs, provider webhooks, and manual refreshes all enqueue sync jobs instead of doing provider work inline.
- Manual refresh jobs get higher priority than scheduled syncs and are deduped by source/location.
- The client reads from Airgoods snapshots/recommendations, so page render never blocks on live provider calls.
Key boundaries:
- External providers are the inventory source of truth.
- Airgoods stores snapshots and recommendations in Postgres.
- Only reliably mapped Airgoods variants are orderable.