Skip to main content

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 TypeTriggerExample
SALEOrder confirmedCustomer purchases 2 units
RETURNReturn processedCustomer returns 1 unit
ADJUSTMENTManual correctionAdmin corrects count from 50 to 48
STOCK_TAKEPhysical countQuarterly inventory audit
RECEIPTNew stock arrivalVendor receives 100 units from supplier
DAMAGEDamaged goods5 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

ComponentDescription
shopSlugURL-friendly shop name (e.g., banda-electronics)
categoryCode3-letter category code (e.g., ELC for Electronics)
autoIncrementZero-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

FormatUse Case
CODE128General product labels
EAN13Retail products (if applicable)
QRDigital 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:

ThresholdDefaultConfigurable per product
Low stock10 unitsYes
Critical stock5 unitsYes

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

TierMin QtyMax QtyPrice (K)
Standard19100.00
Bulk104985.00
Wholesale50null70.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:

FieldTypeDescription
zraTaxCodeStringZRA tax classification code
zraItemCodeStringZRA item classification code
isZraExemptBooleanWhether item is tax-exempt

These fields are included in Smart Invoice payloads transmitted to the ZRA VSDC API.


9. API Endpoints

MethodPathAuthDescription
GET/shops/:shopId/inventorySHOP_OWNERView current inventory
POST/shops/:shopId/inventory/adjustSHOP_OWNERManual stock adjustment
POST/shops/:shopId/inventory/stock-takeSHOP_OWNERInitiate stock take
PUT/shops/:shopId/inventory/stock-take/:idSHOP_OWNERComplete stock take
GET/shops/:shopId/products/:id/barcodeSHOP_OWNERGenerate barcode
POST/shops/:shopId/products/:id/wholesale-tiersSHOP_OWNERAdd wholesale tier
PUT/shops/:shopId/products/:id/wholesale-tiers/:tierIdSHOP_OWNERUpdate wholesale tier
DELETE/shops/:shopId/products/:id/wholesale-tiers/:tierIdSHOP_OWNERRemove wholesale tier

For internal use only. Do not distribute outside Pakashop engineering.