Inventory System
Related docs:
Data Models·SDLC/DEVELOPMENT·API Design
1. Overview
Pakashop's inventory system provides comprehensive stock management for multi-vendor marketplace operations. It supports real-time stock tracking, automated SKU generation, barcode printing, stock-taking workflows, low-stock alerts, and wholesale tier pricing.
2. Core Concepts
2.1 Stock Movement Tracking
Every change to stock quantity is recorded as a StockMovement record:
| Movement Type | Trigger | Example |
|---|---|---|
SALE | Order confirmed | Customer purchases 2 units |
RETURN | Return processed | Customer returns 1 unit |
ADJUSTMENT | Manual correction | Admin corrects count from 50 to 48 |
STOCK_TAKE | Physical count | Quarterly inventory audit |
RECEIPT | New stock arrival | Vendor receives 100 units from supplier |
DAMAGE | Damaged goods | 5 units damaged in warehouse |
2.2 Stock Quantity Calculation
Current Stock = SUM(RECEIPT) + SUM(RETURN) - SUM(SALE) - SUM(DAMAGE) + SUM(ADJUSTMENT) + SUM(STOCK_TAKE)
Stock quantity is calculated dynamically from StockMovement records, not stored as a simple counter (to prevent drift and enable audit trails).
3. SKU Auto-Generation
3.1 Format
{shopSlug}-{categoryCode}-{autoIncrement}
Example: banda-electronics-ELC-00042
| Component | Description |
|---|---|
shopSlug | URL-friendly shop name (e.g., banda-electronics) |
categoryCode | 3-letter category code (e.g., ELC for Electronics) |
autoIncrement | Zero-padded sequential number (6 digits) |
3.2 Generation Logic
// services/backend/src/services/inventory/sku.service.js
async function generateSku(shopId, categoryId) {
const shop = await prisma.shop.findUnique({ where: { id: shopId } });
const category = await prisma.category.findUnique({ where: { id: categoryId } });
const lastProduct = await prisma.product.findFirst({
where: { shopId },
orderBy: { createdAt: 'desc' }
});
const increment = lastProduct
? parseInt(lastProduct.sku.split('-').pop()) + 1
: 1;
return `${shop.slug}-${category.code}-${String(increment).padStart(6, '0')}`;
}
4. Barcode Generation
Barcodes are generated using bwip-js (Barcode Writer in Pure JavaScript) for product labels.
4.1 Supported Formats
| Format | Use Case |
|---|---|
CODE128 | General product labels |
EAN13 | Retail products (if applicable) |
QR | Digital scanning, receipt verification |
4.2 Generation Endpoint
// GET /api/v1/shops/:shopId/products/:id/barcode
const bwipjs = require('bwip-js');
async function generateBarcode(productId, format = 'CODE128') {
const product = await prisma.product.findUnique({ where: { id: productId } });
const png = await bwipjs.toBuffer({
bcid: format,
text: product.sku,
scale: 3,
height: 10,
includetext: true,
textxalign: 'center',
});
return png; // Buffer containing PNG image
}
5. Stock-Taking Workflow
5.1 Process
5.2 Stock Take Record
model StockTake {
id String @id @default(cuid())
shopId String
status String @default("IN_PROGRESS") // IN_PROGRESS, COMPLETED, CANCELLED
initiatedBy String
completedBy String?
startedAt DateTime @default(now())
completedAt DateTime?
items StockTakeItem[]
}
model StockTakeItem {
id String @id @default(cuid())
stockTakeId String
productId String
variantId String?
expectedQty Int
actualQty Int
discrepancy Int // actualQty - expectedQty
adjusted Boolean @default(false)
stockTake StockTake @relation(fields: [stockTakeId], references: [id])
}
6. Low-Stock Alerts
6.1 Alert Triggers
Alerts are triggered when currentStock <= lowStockThreshold:
| Threshold | Default | Configurable per product |
|---|---|---|
| Low stock | 10 units | Yes |
| Critical stock | 5 units | Yes |
6.2 Notification Channels
- In-app notification to shop owner
- Email alert via Resend
- WhatsApp alert (if enabled)
6.3 BullMQ Job
// services/scheduler/src/jobs/low-stock-alert.js
const { Queue } = require('bullmq');
const alertQueue = new Queue('inventory-alerts', { connection: redis });
async function checkLowStock() {
const lowStockProducts = await prisma.product.findMany({
where: {
stockQuantity: { lte: prisma.product.fields.lowStockThreshold }
},
include: { shop: { include: { owners: { include: { user: true } } } } }
});
for (const product of lowStockProducts) {
for (const owner of product.shop.owners) {
await alertQueue.add('low-stock', {
userId: owner.userId,
productId: product.id,
productName: product.name,
currentStock: product.stockQuantity,
threshold: product.lowStockThreshold
});
}
}
}
// Run every hour
cron.schedule('0 * * * *', checkLowStock);
7. Wholesale Tiers
7.1 Tier Structure
Per-product volume discount tiers:
model WholesaleTier {
id String @id @default(cuid())
productId String
minQuantity Int
maxQuantity Int? // null = no upper limit
price Decimal @db.Decimal(10, 2)
createdAt DateTime @default(now())
product Product @relation(fields: [productId], references: [id])
}
7.2 Example Tiers
| Tier | Min Qty | Max Qty | Price (K) |
|---|---|---|---|
| Standard | 1 | 9 | 100.00 |
| Bulk | 10 | 49 | 85.00 |
| Wholesale | 50 | null | 70.00 |
7.3 Price Resolution
// services/backend/src/services/pricing/wholesale.service.js
async function resolveWholesalePrice(productId, quantity) {
const tiers = await prisma.wholesaleTier.findMany({
where: { productId },
orderBy: { minQuantity: 'asc' }
});
for (const tier of tiers) {
if (quantity >= tier.minQuantity &&
(tier.maxQuantity === null || quantity <= tier.maxQuantity)) {
return tier.price;
}
}
// Fallback to base price
const product = await prisma.product.findUnique({ where: { id: productId } });
return product.price;
}
8. ZRA Fields
Products include fields for ZRA Smart Invoice compliance:
| Field | Type | Description |
|---|---|---|
zraTaxCode | String | ZRA tax classification code |
zraItemCode | String | ZRA item classification code |
isZraExempt | Boolean | Whether item is tax-exempt |
These fields are included in Smart Invoice payloads transmitted to the ZRA VSDC API.
9. API Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /shops/:shopId/inventory | SHOP_OWNER | View current inventory |
POST | /shops/:shopId/inventory/adjust | SHOP_OWNER | Manual stock adjustment |
POST | /shops/:shopId/inventory/stock-take | SHOP_OWNER | Initiate stock take |
PUT | /shops/:shopId/inventory/stock-take/:id | SHOP_OWNER | Complete stock take |
GET | /shops/:shopId/products/:id/barcode | SHOP_OWNER | Generate barcode |
POST | /shops/:shopId/products/:id/wholesale-tiers | SHOP_OWNER | Add wholesale tier |
PUT | /shops/:shopId/products/:id/wholesale-tiers/:tierId | SHOP_OWNER | Update wholesale tier |
DELETE | /shops/:shopId/products/:id/wholesale-tiers/:tierId | SHOP_OWNER | Remove wholesale tier |
For internal use only. Do not distribute outside Pakashop engineering.