| name | tsh-stock-realtime |
| description | Real-time stock synchronization for TSH Clients Console. Use when: (1) Debugging stock sync issues (webhooks not firing, stock not updating) (2) Understanding webhook event handling (3) Implementing stock-affecting features (4) Troubleshooting stock discrepancies between Zoho and app (5) Adding new stock-affecting transaction handlers (6) Monitoring stock sync health (7) Understanding the multi-layer sync architecture |
TSH Stock Real-Time Synchronization
Architecture Overview
┌─────────────────────────────────────────────────────────────────────┐
│ TSH STOCK SYNC ARCHITECTURE │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ LAYER 1: WEBHOOKS (Instant, < 5 seconds) │
│ ├── Zoho transaction occurs │
│ ├── POST /api/webhooks/zoho │
│ ├── quickSyncStock(itemIds) │
│ └── Update Redis + Revalidate ISR │
│ │
│ LAYER 2: PERIODIC SYNC (Every 15 minutes) │
│ ├── Vercel Cron job │
│ ├── POST /api/sync/stock │
│ ├── syncStockFromBooks() │
│ └── Full cache refresh │
│ │
│ LAYER 3: ON-DEMAND SYNC (Manual trigger) │
│ ├── Admin triggers via API │
│ ├── GET /api/sync/stock?action=sync&force=true │
│ └── Immediate full refresh │
│ │
│ LAYER 4: HEALTH MONITORING (Continuous) │
│ ├── GET /api/sync/stock?action=status │
│ ├── Check: itemCount > 400 │
│ ├── Check: cache age < 4 hours │
│ └── Alert if unhealthy │
│ │
└─────────────────────────────────────────────────────────────────────┘
Webhook Event Reference
Stock-Affecting Transactions
| Transaction | Stock Effect | Webhook Event | Handler |
|---|---|---|---|
| Invoice Created | Decreases | invoice.created |
revalidateProducts() |
| Invoice Updated | May change | invoice.updated |
revalidateProducts() |
| Bill Created | Increases | bill.created |
revalidateProducts() |
| Bill Updated | May change | bill.updated |
revalidateProducts() |
| Sales Order Created | Commits stock | salesorder.created |
revalidateProducts() |
| Sales Order Updated | May release | salesorder.updated |
revalidateProducts() |
| Credit Note Created | May increase | creditnote.created |
revalidateProducts() |
| Inventory Adjustment | Changes | inventoryadjustment.created |
revalidateProducts() |
| Sales Return Received | Increases | salesreturnreceive.created |
revalidateProducts() |
| Package Shipped | Physical decrease | package.shipped |
revalidateProducts() |
| Item Updated | May change | item.updated |
revalidateProducts() |
Webhook Handler Location
// src/app/api/webhooks/zoho/route.ts
export async function POST(request: Request) {
const payload = await request.json();
const { event_type, resource_name, resource_id } = payload;
// Extract affected item IDs from payload
const itemIds = extractItemIds(payload);
switch (resource_name) {
case "invoice":
case "bill":
case "salesorder":
case "creditnote":
case "inventoryadjustment":
case "salesreturnreceive":
case "package":
// Sync stock for affected items
await quickSyncStock(itemIds);
await revalidateTag('products');
break;
case "item":
// Direct item update
await quickSyncStock([resource_id]);
await revalidateTag('products');
break;
}
}
Quick Sync Function
// src/lib/zoho/stock-cache.ts
export async function quickSyncStock(
itemIds: string[],
reason: string
): Promise<void> {
const WHOLESALE_LOCATION_NAME = 'Main WareHouse';
for (const itemId of itemIds) {
try {
// Fetch item with locations from Zoho Books
const item = await getItemWithLocations(itemId);
// Extract warehouse-specific stock
const location = item.locations?.find(
loc => loc.location_name === WHOLESALE_LOCATION_NAME
);
const stock = location?.location_available_for_sale_stock || 0;
// Update Redis cache
await redis.set(`stock:${itemId}`, stock, { ex: STOCK_CACHE_TTL });
console.log(`[quickSyncStock] ${itemId}: ${stock} (${reason})`);
} catch (error) {
console.error(`[quickSyncStock] Failed for ${itemId}:`, error);
}
}
}
Unified Stock Functions
ALWAYS Use These Functions
// For single item (product detail page)
import { getUnifiedStock } from '@/lib/zoho/stock-cache';
const { stock, source } = await getUnifiedStock(itemId, {
fetchOnMiss: true, // Fetch from API if not in cache
context: 'product-detail',
});
// source: 'cache' | 'api' | 'unavailable'
// For multiple items (shop list page)
import { getUnifiedStockBulk } from '@/lib/zoho/stock-cache';
const stockMap = await getUnifiedStockBulk(itemIds, {
context: 'shop-list',
});
// Returns: Map<itemId, stock>
Why Unified Functions?
PROBLEM SOLVED:
- Shop list showed different stock than product detail page
- Root cause: Different fallback sources when Redis cache missed
- List fell back to Books API (total across ALL warehouses)
- Detail fell back to Inventory API (warehouse-specific)
SOLUTION:
- Unified functions use ONLY Redis cache as stock source
- On cache miss, detail page fetches from API and caches result
- Next list page load sees the cached value
- NEVER fall back to Books item.available_stock
Stock Data Flow
┌──────────────────────────────────────────────────────────────────┐
│ STOCK DATA FLOW │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ZOHO BOOKS/INVENTORY │
│ │ │
│ ├── Transaction occurs (Invoice, Bill, etc.) │
│ │ │ │
│ │ └──► Webhook fires to /api/webhooks/zoho │
│ │ │ │
│ │ ├── Extract item IDs from payload │
│ │ │ │
│ │ └──► quickSyncStock(itemIds) │
│ │ │ │
│ │ ├── Fetch item with locations from API │
│ │ │ │
│ │ ├── Extract Main WareHouse stock │
│ │ │ └── location_available_for_sale_stock │
│ │ │ │
│ │ └──► Update Redis cache │
│ │ │ │
│ │ └──► Revalidate Next.js ISR cache │
│ │ │
│ └── Periodic sync (every 15 min) │
│ │ │
│ └──► Full cache refresh from Books API │
│ │
│ REDIS CACHE (Upstash) │
│ │ │
│ ├── Key: stock:{itemId} │
│ ├── Value: number (warehouse-specific stock) │
│ ├── TTL: 4 hours (14400 seconds) │
│ │ │
│ └──► Read by: getUnifiedStock(), getUnifiedStockBulk() │
│ │
│ NEXT.JS APPLICATION │
│ │ │
│ ├── Shop List Page │
│ │ └── getUnifiedStockBulk(itemIds) → Redis │
│ │ │
│ └── Product Detail Page │
│ └── getUnifiedStock(itemId) → Redis → API (if miss) │
│ │
└──────────────────────────────────────────────────────────────────┘
Cache Configuration
| Setting | Value | Notes |
|---|---|---|
| Stock Cache TTL | 4 hours (14400s) | Reduced from 24h for freshness |
| Redis Provider | Upstash | https://fine-mole-41883.upstash.io |
| Key Pattern | stock:{itemId} |
One key per item |
| Freshness Check | 10 minutes | For periodic sync trigger |
| Sync Frequency | Every 15 minutes | Vercel Cron |
Debugging Stock Sync
Check Cache Status
# Check cache health
curl "https://www.tsh.sale/api/sync/stock?action=status"
# Expected response:
{
"status": "healthy",
"itemCount": 450,
"lastSync": "2025-12-11T10:30:00Z",
"cacheAge": "2 hours"
}
Force Full Sync
# Trigger full sync
curl "https://www.tsh.sale/api/sync/stock?action=sync&secret=tsh-stock-sync-2024&force=true"
# Expected response:
{
"success": true,
"itemsSynced": 450,
"duration": "45s"
}
Revalidate Caches
# Revalidate all caches
curl "https://www.tsh.sale/api/revalidate?tag=all&secret=tsh-revalidate-2024"
# Revalidate products only
curl "https://www.tsh.sale/api/revalidate?tag=products&secret=tsh-revalidate-2024"
Check Specific Item Stock
# Debug specific item
curl "https://www.tsh.sale/api/debug/stock?itemId=2646610000109854052"
# Expected response:
{
"itemId": "2646610000109854052",
"cachedStock": 15,
"apiStock": 15,
"source": "cache",
"warehouse": "Main WareHouse"
}
Troubleshooting
Stock Shows 0 When It Shouldn't
| Check | Command | Fix |
|---|---|---|
| Cache empty? | ?action=status |
Run full sync |
| Wrong warehouse? | Check location_name |
Must be "Main WareHouse" |
| Stale cache? | Check lastSync |
Force sync if > 4h old |
| Webhook not firing? | Check Zoho webhook logs | Reconfigure webhook |
Stock Discrepancy Between List/Detail
| Cause | Symptoms | Fix |
|---|---|---|
| Cache miss | Detail shows different | Run sync, check TTL |
| Wrong function | Using deprecated fn | Use getUnifiedStock() |
| Books fallback | List shows ALL warehouse | Never use item.available_stock |
Webhook Not Updating Stock
Check Zoho Webhook Configuration
- Go to Zoho Settings → Webhooks
- Verify URL:
https://www.tsh.sale/api/webhooks/zoho - Verify events are enabled
Check Vercel Logs
vercel logs --follow | grep webhookVerify Webhook Payload
- Add logging to webhook handler
- Check if
resource_namematches expected
Test Webhook Manually
curl -X POST https://www.tsh.sale/api/webhooks/zoho \ -H "Content-Type: application/json" \ -d '{"resource_name":"item","event_type":"updated","resource_id":"123"}'
Adding New Stock-Affecting Feature
When adding features that affect stock:
1. Identify Webhook Events
Example: Adding "Stock Transfer" feature
Events needed:
- inventorytransfer.created
- inventorytransfer.updated
2. Update Webhook Handler
// src/app/api/webhooks/zoho/route.ts
case "inventorytransfer":
const itemIds = extractItemIdsFromTransfer(payload);
await quickSyncStock(itemIds, `stock transfer: ${eventType}`);
await revalidateProducts(`stock transfer: ${eventType}`);
break;
3. Test the Flow
# 1. Create test transaction in Zoho
# 2. Check webhook received
curl "https://www.tsh.sale/api/debug/webhook-log"
# 3. Verify stock updated
curl "https://www.tsh.sale/api/debug/stock?itemId=<ITEM_ID>"
Golden Rules
Single Source of Truth: ALWAYS use
getUnifiedStock()orgetUnifiedStockBulk()Never Use Books Fallback: NEVER use
item.available_stock(combines ALL warehouses)Warehouse Isolation: ONLY show stock from "Main WareHouse" (ID: 2646610000000077024)
Cache Before Display: Ensure Redis cache is populated before displaying stock
Graceful Degradation: Show "Check availability" if stock unavailable, NOT zero
Sync Before Deploy: Run full stock sync before deploying stock-related changes
Related Files
| File | Purpose |
|---|---|
src/lib/zoho/stock-cache.ts |
Stock caching, unified functions |
src/lib/zoho/products.ts |
Product fetching, stock extraction |
src/app/api/webhooks/zoho/route.ts |
Webhook handler |
src/app/api/sync/stock/route.ts |
Sync endpoint |
.claude/STOCK_RULES.md |
Stock display rules |
.claude/skills/tsh-stock/SKILL.md |
Basic stock skill |
Checklist for Stock Changes
Before modifying stock-related code:
- Read
.claude/STOCK_RULES.md - Check Redis cache status
- Understand which webhook events affect the change
- Use unified stock functions
- Test on staging first
- Run full sync after deploy
- Verify list/detail stock consistency