npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@rytass/wms-base-nestjs-module

v0.1.7

Published

Rytass Warehouse Management System Module

Downloads

137

Readme

📦 Rytass WMS Base NestJS Module

Enterprise-grade Warehouse Management System (WMS) module for NestJS applications. Build comprehensive inventory tracking, location management, batch processing, and stock control systems with support for custom entity extensions and hierarchical warehouse organization.

npm version License: MIT

🎯 Learning Path

This guide follows a progressive learning approach - choose your starting point:

| Level | Time | Focus | Audience | |-------|------|--------|----------| | 🚀 Quick Start | 5 min | Get running immediately | New users | | 📚 Fundamentals | 15 min | Understand architecture | Developers | | 🛠️ Basic Implementation | 30 min | Build working features | Implementation teams | | 🔧 Intermediate Usage | 1 hour | Custom entities & workflows | Advanced developers | | 🚀 Advanced Patterns | 2 hours | Production optimization | Architecture teams | | 🏭 Enterprise Examples | 4+ hours | Complete real-world systems | Enterprise teams |


🚀 Quick Start

Get a working WMS in 5 minutes:

Installation

npm install @rytass/wms-base-nestjs-module
# or
yarn add @rytass/wms-base-nestjs-module

Minimal Setup

// app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { WMSBaseModule } from '@rytass/wms-base-nestjs-module';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'sqlite',
      database: 'wms.sqlite',
      autoLoadEntities: true,
      synchronize: true, // Only for development
    }),
    WMSBaseModule.forRoot({
      allowNegativeStock: false,
    }),
  ],
})
export class AppModule {}

First Inventory Operations

// inventory.service.ts
import { Injectable } from '@nestjs/common';
import { 
  LocationService, 
  MaterialService, 
  OrderService, 
  StockService 
} from '@rytass/wms-base-nestjs-module';

@Injectable()
export class QuickStartService {
  constructor(
    private readonly locationService: LocationService,
    private readonly materialService: MaterialService,
    private readonly orderService: OrderService,
    private readonly stockService: StockService,
  ) {}

  async setupWarehouse(): Promise<void> {
    // 1. Create location
    await this.locationService.create({
      id: 'MAIN-A1',
      name: 'Main Warehouse - Section A1',
    });

    // 2. Create product
    await this.materialService.create({
      id: 'PRODUCT-001',
      name: 'Widget Pro',
    });

    // 3. Receive inventory (inbound)
    await this.orderService.createOrder(OrderEntity, {
      order: {},
      batches: [{
        id: 'RECEIPT-001',
        locationId: 'MAIN-A1',
        materialId: 'PRODUCT-001',
        quantity: 100, // Positive = inbound
      }],
    });

    console.log('✅ Warehouse setup complete!');
  }

  async checkInventory(): Promise<number> {
    const stockLevel = await this.stockService.find({
      materialIds: ['PRODUCT-001'],
      locationIds: ['MAIN-A1'],
    });

    console.log(`📊 Current stock: ${stockLevel} units`);
    return stockLevel;
  }

  async shipOrder(quantity: number): Promise<void> {
    await this.orderService.createOrder(OrderEntity, {
      order: {},
      batches: [{
        id: `SHIP-${Date.now()}`,
        locationId: 'MAIN-A1',
        materialId: 'PRODUCT-001',
        quantity: -quantity, // Negative = outbound
      }],
    });

    console.log(`📦 Shipped ${quantity} units`);
  }
}

Test Your Setup:

const service = new QuickStartService(/* inject dependencies */);
await service.setupWarehouse();    // Create location & product, receive 100 units
await service.checkInventory();    // Returns: 100
await service.shipOrder(30);       // Ship 30 units  
await service.checkInventory();    // Returns: 70

✅ Success Criteria:

  • [x] WMS module loads without errors
  • [x] Can create locations and materials
  • [x] Can process inbound/outbound orders
  • [x] Stock levels update correctly

🎯 Next Steps: Ready for more? Continue to Fundamental Concepts to understand the architecture.


📚 Fundamental Concepts

🏗️ Entity Architecture

The WMS module is built on 5 core entities with hierarchical relationships:

📦 WMS Core Architecture

MaterialEntity ────┐
    │              │
    └─ BatchEntity │
         │         │
         └─ StockEntity ──── OrderEntity
              │
              └─ LocationEntity (Tree Structure)

Core Entities Overview

| Entity | Purpose | Key Features | Relationships | |--------|---------|--------------|---------------| | LocationEntity | Warehouse locations | Hierarchical tree, materialized path | Parent-child, contains Stock | | MaterialEntity | Products/SKUs | Master product data | Has many Batches & Stock | | BatchEntity | Lot tracking | Batch identification & metadata | Belongs to Material | | OrderEntity | Transactions | Stock movements & operations | Creates Stock entries | | StockEntity | Inventory levels | Real-time stock & transaction log | Links Material, Location, Order |

🔄 Stock Transaction Model

Every inventory change is tracked as a stock transaction:

interface StockTransaction {
  id: string;              // Unique transaction ID
  materialId: string;      // What product
  locationId: string;      // Where in warehouse  
  batchId: string;         // Which batch/lot
  orderId: string;         // Why (which order caused this)
  quantity: number;        // How much (+ inbound, - outbound)
  createdAt: Date;         // When
}

Transaction Types:

  • Positive quantity = Stock increase (receipts, returns, adjustments up)
  • Negative quantity = Stock decrease (shipments, transfers out, adjustments down)

🌳 Hierarchical Location System

Locations form a materialized path tree for efficient warehouse organization:

Warehouse
├── Receiving-Zone
│   ├── Dock-1
│   └── Dock-2
├── Storage-Zone
│   ├── Aisle-A
│   │   ├── Rack-A1 
│   │   │   ├── Shelf-A1-1
│   │   │   └── Shelf-A1-2
│   │   └── Rack-A2
│   └── Aisle-B
└── Shipping-Zone
    └── Staging-Area

Key Benefits:

  • Efficient Queries: Get all stock in "Storage-Zone" includes all child locations
  • Flexible Hierarchy: Unlimited nesting depth
  • Fast Lookups: Materialized path enables SQL LIKE queries

🧬 Child Entity Extension Pattern

Extend base entities with custom business fields using TypeORM's Table Inheritance:

// Base entity (provided by module)
@Entity('materials')
@TableInheritance({ column: { type: 'varchar', name: 'entityName' } })
export class MaterialEntity {
  @PrimaryColumn({ type: 'varchar' })
  id: string;
  // Base fields...
}

// Your custom entity
@ChildEntity()
export class ProductEntity extends MaterialEntity {
  @Column('varchar')
  name: string;
  
  @Column('varchar')
  sku: string;
  
  @Column('decimal', { precision: 10, scale: 2 })
  unitPrice: number;
  
  @Column('json')
  dimensions: {
    length: number;
    width: number; 
    height: number;
    weight: number;
  };
}

Why Use Child Entities?

  • Single Table: All data stored in one database table
  • Type Safety: Full TypeScript support for custom fields
  • Service Integration: Works seamlessly with all WMS services
  • Migration Friendly: Add fields without breaking existing data

🛠️ Basic Implementation

Complete E-commerce Inventory Service

Build a production-ready inventory management service with error handling and business logic:

// entities/product.entity.ts
import { ChildEntity, Column } from 'typeorm';
import { MaterialEntity } from '@rytass/wms-base-nestjs-module';

@ChildEntity()
export class ProductEntity extends MaterialEntity {
  @Column('varchar')
  name: string;
  
  @Column('varchar')
  sku: string;
  
  @Column('varchar')
  brand: string;
  
  @Column('decimal', { precision: 10, scale: 2 })
  unitPrice: number;
  
  @Column('int')
  reorderLevel: number;
  
  @Column('varchar')
  category: string;
  
  @Column('json')
  attributes: Record<string, any>;
}
// entities/warehouse-location.entity.ts
import { ChildEntity, Column } from 'typeorm';
import { LocationEntity } from '@rytass/wms-base-nestjs-module';

@ChildEntity()
export class WarehouseLocationEntity extends LocationEntity {
  @Column('varchar')
  code: string;
  
  @Column('varchar')
  zone: string;
  
  @Column('varchar') 
  type: 'RECEIVING' | 'STORAGE' | 'PICKING' | 'SHIPPING' | 'STAGING';
  
  @Column('int')
  capacity: number;
  
  @Column('boolean', { default: true })
  isActive: boolean;
  
  @Column('json', { nullable: true })
  restrictions?: {
    temperature?: { min: number; max: number };
    hazmat?: boolean;
    maxWeight?: number;
  };
}
// services/inventory.service.ts
import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common';
import { 
  LocationService, 
  MaterialService, 
  OrderService, 
  StockService,
  OrderEntity,
  StockQuantityNotEnoughError,
  LocationNotFoundError
} from '@rytass/wms-base-nestjs-module';
import { ProductEntity } from '../entities/product.entity';
import { WarehouseLocationEntity } from '../entities/warehouse-location.entity';

export interface InventoryMovement {
  productSku: string;
  locationCode: string;
  quantity: number;
  referenceNumber: string;
  type: 'RECEIPT' | 'SHIPMENT' | 'TRANSFER' | 'ADJUSTMENT';
}

export interface StockLevel {
  productSku: string;
  productName: string;
  locationCode: string;
  locationName: string;
  quantity: number;
  reorderLevel: number;
  needsReorder: boolean;
}

@Injectable()
export class InventoryService {
  constructor(
    private readonly locationService: LocationService<WarehouseLocationEntity>,
    private readonly materialService: MaterialService<ProductEntity>,
    private readonly orderService: OrderService,
    private readonly stockService: StockService,
  ) {}

  /**
   * Receive inventory into warehouse (inbound)
   */
  async receiveInventory(movement: InventoryMovement): Promise<void> {
    if (movement.quantity <= 0) {
      throw new BadRequestException('Receive quantity must be positive');
    }

    // Validate product exists
    const product = await this.findProductBySku(movement.productSku);
    
    // Validate location exists and is active
    const location = await this.findLocationByCode(movement.locationCode);
    if (!location.isActive) {
      throw new BadRequestException(`Location ${movement.locationCode} is inactive`);
    }

    try {
      await this.orderService.createOrder(OrderEntity, {
        order: {},
        batches: [{
          id: `${movement.type}-${movement.referenceNumber}`,
          locationId: location.id,
          materialId: product.id,
          quantity: movement.quantity,
        }],
      });

      console.log(`✅ Received ${movement.quantity} units of ${product.sku} at ${location.code}`);
    } catch (error) {
      if (error instanceof LocationNotFoundError) {
        throw new BadRequestException(`Invalid location: ${movement.locationCode}`);
      }
      throw error;
    }
  }

  /**
   * Ship inventory from warehouse (outbound)
   */
  async shipInventory(movement: InventoryMovement): Promise<void> {
    if (movement.quantity <= 0) {
      throw new BadRequestException('Ship quantity must be positive');
    }

    const product = await this.findProductBySku(movement.productSku);
    const location = await this.findLocationByCode(movement.locationCode);

    // Check available stock
    const availableStock = await this.stockService.find({
      materialIds: [product.id],
      locationIds: [location.id],
      exactLocationMatch: true,
    });

    if (availableStock < movement.quantity) {
      throw new BadRequestException(
        `Insufficient stock. Available: ${availableStock}, Requested: ${movement.quantity}`
      );
    }

    try {
      await this.orderService.createOrder(OrderEntity, {
        order: {},
        batches: [{
          id: `${movement.type}-${movement.referenceNumber}`,
          locationId: location.id,
          materialId: product.id,
          quantity: -movement.quantity, // Negative for outbound
        }],
      });

      console.log(`📦 Shipped ${movement.quantity} units of ${product.sku} from ${location.code}`);
    } catch (error) {
      if (error instanceof StockQuantityNotEnoughError) {
        throw new BadRequestException('Insufficient stock for shipment');
      }
      throw error;
    }
  }

  /**
   * Transfer inventory between locations
   */
  async transferInventory(
    productSku: string,
    fromLocationCode: string,
    toLocationCode: string,
    quantity: number,
    referenceNumber: string
  ): Promise<void> {
    if (quantity <= 0) {
      throw new BadRequestException('Transfer quantity must be positive');
    }

    const product = await this.findProductBySku(productSku);
    const fromLocation = await this.findLocationByCode(fromLocationCode);
    const toLocation = await this.findLocationByCode(toLocationCode);

    // Check available stock at source location
    const availableStock = await this.stockService.find({
      materialIds: [product.id],
      locationIds: [fromLocation.id],
      exactLocationMatch: true,
    });

    if (availableStock < quantity) {
      throw new BadRequestException(
        `Insufficient stock at ${fromLocationCode}. Available: ${availableStock}, Requested: ${quantity}`
      );
    }

    // Create transfer as a single order with two batches
    await this.orderService.createOrder(OrderEntity, {
      order: {},
      batches: [
        {
          id: `TRANSFER-OUT-${referenceNumber}`,
          locationId: fromLocation.id,
          materialId: product.id,
          quantity: -quantity, // Remove from source
        },
        {
          id: `TRANSFER-IN-${referenceNumber}`,
          locationId: toLocation.id,
          materialId: product.id,
          quantity: quantity, // Add to destination
        },
      ],
    });

    console.log(`🔄 Transferred ${quantity} units of ${product.sku} from ${fromLocationCode} to ${toLocationCode}`);
  }

  /**
   * Get current stock levels with reorder analysis
   */
  async getStockLevels(filter?: {
    productSkus?: string[];
    locationCodes?: string[];
    lowStockOnly?: boolean;
  }): Promise<StockLevel[]> {
    const stockLevels: StockLevel[] = [];

    // Get products
    const products = filter?.productSkus?.length
      ? await Promise.all(filter.productSkus.map(sku => this.findProductBySku(sku)))
      : await this.materialService.findAll();

    // Get locations  
    const locations = filter?.locationCodes?.length
      ? await Promise.all(filter.locationCodes.map(code => this.findLocationByCode(code)))
      : await this.locationService.findAll();

    for (const product of products) {
      for (const location of locations) {
        const quantity = await this.stockService.find({
          materialIds: [product.id],
          locationIds: [location.id],
          exactLocationMatch: true,
        });

        const stockLevel: StockLevel = {
          productSku: product.sku,
          productName: product.name,
          locationCode: location.code,
          locationName: location.code,
          quantity,
          reorderLevel: product.reorderLevel,
          needsReorder: quantity <= product.reorderLevel,
        };

        // Apply low stock filter
        if (filter?.lowStockOnly && !stockLevel.needsReorder) {
          continue;
        }

        stockLevels.push(stockLevel);
      }
    }

    return stockLevels.sort((a, b) => {
      // Sort by reorder priority, then by product SKU
      if (a.needsReorder && !b.needsReorder) return -1;
      if (!a.needsReorder && b.needsReorder) return 1;
      return a.productSku.localeCompare(b.productSku);
    });
  }

  /**
   * Perform stock adjustment (physical count correction)
   */
  async adjustStock(
    productSku: string,
    locationCode: string,
    actualQuantity: number,
    referenceNumber: string,
    reason: string
  ): Promise<{ previousQuantity: number; adjustment: number; newQuantity: number }> {
    const product = await this.findProductBySku(productSku);
    const location = await this.findLocationByCode(locationCode);

    // Get current system quantity
    const currentQuantity = await this.stockService.find({
      materialIds: [product.id],
      locationIds: [location.id],
      exactLocationMatch: true,
    });

    const adjustmentQuantity = actualQuantity - currentQuantity;

    if (adjustmentQuantity !== 0) {
      await this.orderService.createOrder(OrderEntity, {
        order: {},
        batches: [{
          id: `ADJUSTMENT-${referenceNumber}`,
          locationId: location.id,
          materialId: product.id,
          quantity: adjustmentQuantity,
        }],
      });

      console.log(`📝 Stock adjustment: ${product.sku} at ${location.code}: ${currentQuantity} → ${actualQuantity} (${adjustmentQuantity >= 0 ? '+' : ''}${adjustmentQuantity})`);
      console.log(`📋 Reason: ${reason}`);
    } else {
      console.log(`✅ Stock correct: ${product.sku} at ${location.code}: ${currentQuantity} units`);
    }

    return {
      previousQuantity: currentQuantity,
      adjustment: adjustmentQuantity,
      newQuantity: actualQuantity,
    };
  }

  // Helper methods
  private async findProductBySku(sku: string): Promise<ProductEntity> {
    // Note: This requires a custom find method or query
    // For now, using findById assuming SKU = ID
    const product = await this.materialService.findById(sku);
    if (!product) {
      throw new NotFoundException(`Product not found: ${sku}`);
    }
    return product;
  }

  private async findLocationByCode(code: string): Promise<WarehouseLocationEntity> {
    // Note: This requires a custom find method or query  
    // For now, using findById assuming code = ID
    const location = await this.locationService.findById(code);
    if (!location) {
      throw new NotFoundException(`Location not found: ${code}`);
    }
    return location;
  }
}

Usage Example

// Complete workflow example
async function runInventoryOperations() {
  const inventoryService = new InventoryService(/* inject dependencies */);

  // 1. Setup products and locations (one-time setup)
  await setupWarehouseData(inventoryService);

  // 2. Receive inventory
  await inventoryService.receiveInventory({
    productSku: 'WIDGET-001',
    locationCode: 'RECV-DOCK-1',
    quantity: 1000,
    referenceNumber: 'PO-2024-001',
    type: 'RECEIPT',
  });

  // 3. Transfer to storage
  await inventoryService.transferInventory(
    'WIDGET-001',
    'RECV-DOCK-1',
    'STORAGE-A1-SHELF-1',
    800,
    'TRANSFER-001'
  );

  // 4. Ship customer orders
  await inventoryService.shipInventory({
    productSku: 'WIDGET-001',
    locationCode: 'STORAGE-A1-SHELF-1',
    quantity: 50,
    referenceNumber: 'SO-2024-100',
    type: 'SHIPMENT',
  });

  // 5. Check stock levels
  const stockLevels = await inventoryService.getStockLevels({
    productSkus: ['WIDGET-001'],
  });

  console.log('📊 Current Stock Levels:', stockLevels);

  // 6. Perform stock count & adjustment
  const adjustment = await inventoryService.adjustStock(
    'WIDGET-001',
    'STORAGE-A1-SHELF-1',
    745, // Physical count
    'CYCLE-COUNT-001',
    'Monthly cycle count - found 5 unit discrepancy'
  );

  console.log('📝 Stock Adjustment:', adjustment);
}

✅ What You've Built:

  • Complete inventory management service
  • Error handling for all scenarios
  • Stock level monitoring with reorder alerts
  • Transfer operations between locations
  • Stock adjustment workflows
  • Type-safe custom entities

🎯 Next Steps: Ready for advanced features? Continue to Intermediate Usage.


🔧 Intermediate Usage

Child Entity Extension Masterclass

Learn to extend every base entity with custom fields for real-world business requirements.

🏗️ Architecture Overview

The WMS module uses TypeORM Table Inheritance to support custom entity extensions:

// Base entities use @TableInheritance
@Entity('materials')
@TableInheritance({ column: { type: 'varchar', name: 'entityName' } })
export class MaterialEntity { /* base fields */ }

// Your extensions use @ChildEntity  
@ChildEntity()
export class ProductEntity extends MaterialEntity { /* custom fields */ }

Key Benefits:

  • Single Database Table: All variants stored together
  • Type Safety: Full TypeScript intellisense and validation
  • Service Compatibility: Works with all WMS services out-of-the-box
  • Migration Safe: Add fields without data loss

📦 MaterialEntity Extensions

Common Business Scenarios:

E-commerce Products

// entities/product.entity.ts
import { ChildEntity, Column } from 'typeorm';
import { MaterialEntity } from '@rytass/wms-base-nestjs-module';

@ChildEntity()
export class ProductEntity extends MaterialEntity {
  @Column('varchar', { length: 100 })
  name: string;
  
  @Column('varchar', { length: 50, unique: true })
  sku: string;
  
  @Column('varchar', { length: 13, nullable: true })
  upc: string; // Universal Product Code
  
  @Column('varchar', { length: 50 })
  brand: string;
  
  @Column('varchar', { length: 100 })
  category: string;
  
  @Column('decimal', { precision: 10, scale: 2 })
  unitPrice: number;
  
  @Column('decimal', { precision: 10, scale: 4 })
  weight: number; // in kg
  
  @Column('json')
  dimensions: {
    length: number; // cm
    width: number;  // cm
    height: number; // cm
  };
  
  @Column('int', { default: 10 })
  reorderLevel: number;
  
  @Column('int', { default: 100 })
  maxStockLevel: number;
  
  @Column('varchar', { nullable: true })
  supplierSku?: string;
  
  @Column('boolean', { default: true })
  isActive: boolean;
  
  @Column('boolean', { default: false })
  isHazmat: boolean;
  
  @Column('boolean', { default: false })
  requiresSerialNumber: boolean;
  
  @Column('json', { nullable: true })
  customAttributes?: Record<string, any>;
}

Manufacturing Raw Materials

@ChildEntity()
export class RawMaterialEntity extends MaterialEntity {
  @Column('varchar')
  materialCode: string;
  
  @Column('varchar')
  specification: string;
  
  @Column('varchar')
  grade: string; // A, B, C grade
  
  @Column('varchar')
  supplier: string;
  
  @Column('decimal', { precision: 8, scale: 4 })
  unitCost: number;
  
  @Column('varchar')
  unitOfMeasure: string; // kg, lbs, meters, etc.
  
  @Column('int', { nullable: true })
  shelfLifeDays?: number;
  
  @Column('json', { nullable: true })
  chemicalProperties?: {
    density?: number;
    viscosity?: number;
    flashPoint?: number;
    ph?: number;
  };
  
  @Column('boolean', { default: false })
  requiresQualityControl: boolean;
  
  @Column('varchar', { nullable: true })
  msdsNumber?: string; // Material Safety Data Sheet
}

Pharmaceutical Products

@ChildEntity()
export class PharmaceuticalEntity extends MaterialEntity {
  @Column('varchar')
  genericName: string;
  
  @Column('varchar')
  brandName: string;
  
  @Column('varchar')
  ndcNumber: string; // National Drug Code
  
  @Column('varchar')
  dosageForm: string; // tablet, capsule, liquid
  
  @Column('varchar')
  strength: string; // 500mg, 10ml, etc.
  
  @Column('varchar')
  manufacturer: string;
  
  @Column('int')
  shelfLifeMonths: number;
  
  @Column('boolean', { default: false })
  isControlledSubstance: boolean;
  
  @Column('varchar', { nullable: true })
  controlSchedule?: string; // I, II, III, IV, V
  
  @Column('boolean', { default: false })
  requiresRefrigeration: boolean;
  
  @Column('json', { nullable: true })
  storageConditions?: {
    temperatureMin: number; // Celsius
    temperatureMax: number; // Celsius
    humidityMax?: number;   // Percentage
    lightSensitive?: boolean;
  };
  
  @Column('varchar', { nullable: true })
  therapeuticClass?: string;
}

📍 LocationEntity Extensions

Warehouse Locations with Zones

@ChildEntity()
export class WarehouseLocationEntity extends LocationEntity {
  @Column('varchar', { length: 20, unique: true })
  code: string; // WH-A-01-R1-S3
  
  @Column('varchar', { length: 100 })
  description: string;
  
  @Column('varchar', { length: 20 })
  zone: string; // RECEIVING, STORAGE, PICKING, SHIPPING
  
  @Column('varchar', { length: 20 })
  type: 'WAREHOUSE' | 'ZONE' | 'AISLE' | 'RACK' | 'SHELF' | 'BIN';
  
  @Column('int', { default: 0 })
  capacity: number; // cubic meters or units
  
  @Column('decimal', { precision: 10, scale: 2, default: 0 })
  maxWeight: number; // kg
  
  @Column('boolean', { default: true })
  isActive: boolean;
  
  @Column('boolean', { default: false })
  isPicking: boolean; // Available for picking operations
  
  @Column('boolean', { default: false })
  isReceiving: boolean; // Available for receiving
  
  @Column('json', { nullable: true })
  coordinates?: {
    x: number;
    y: number;
    z?: number; // floor level
  };
  
  @Column('json', { nullable: true })
  environmentalControls?: {
    temperature?: { min: number; max: number };
    humidity?: { min: number; max: number };
    hasClimateControl?: boolean;
  };
  
  @Column('json', { nullable: true })
  restrictions?: {
    hazmatApproved?: boolean;
    weightLimit?: number;
    heightLimit?: number;
    accessEquipment?: string[]; // ['forklift', 'crane', 'ladder']
  };
  
  @Column('varchar', { nullable: true })
  barcode?: string; // For scanning operations
}

Cold Chain Locations

@ChildEntity()
export class ColdChainLocationEntity extends LocationEntity {
  @Column('varchar')
  locationCode: string;
  
  @Column('varchar')
  temperatureZone: 'FROZEN' | 'REFRIGERATED' | 'CONTROLLED' | 'AMBIENT';
  
  @Column('decimal', { precision: 4, scale: 1 })
  targetTemperature: number; // Celsius
  
  @Column('decimal', { precision: 4, scale: 1 })
  temperatureTolerance: number; // +/- degrees
  
  @Column('boolean', { default: true })
  hasTemperatureMonitoring: boolean;
  
  @Column('varchar', { nullable: true })
  sensorId?: string;
  
  @Column('timestamp', { nullable: true })
  lastTemperatureCheck?: Date;
  
  @Column('decimal', { precision: 4, scale: 1, nullable: true })
  currentTemperature?: number;
  
  @Column('boolean', { default: false })
  hasTemperatureAlert: boolean;
  
  @Column('varchar', { nullable: true })
  certificationNumber?: string; // FDA, GMP, etc.
  
  @Column('json', { nullable: true })
  validationData?: {
    lastCalibration?: Date;
    nextCalibration?: Date;
    calibrationCertificate?: string;
  };
}

📋 OrderEntity Extensions

Purchase Orders (Inbound)

@ChildEntity()
export class PurchaseOrderEntity extends OrderEntity {
  @Column('varchar', { length: 50, unique: true })
  purchaseOrderNumber: string;
  
  @Column('varchar')
  supplierName: string;
  
  @Column('varchar', { nullable: true })
  supplierContact?: string;
  
  @Column('date')
  orderDate: Date;
  
  @Column('date', { nullable: true })
  expectedDeliveryDate?: Date;
  
  @Column('date', { nullable: true })
  actualDeliveryDate?: Date;
  
  @Column('varchar', { default: 'PENDING' })
  status: 'PENDING' | 'CONFIRMED' | 'SHIPPED' | 'DELIVERED' | 'CANCELLED';
  
  @Column('decimal', { precision: 12, scale: 2 })
  totalAmount: number;
  
  @Column('varchar')
  currency: string;
  
  @Column('varchar', { nullable: true })
  paymentTerms?: string;
  
  @Column('text', { nullable: true })
  notes?: string;
  
  @Column('varchar', { nullable: true })
  shippingTrackingNumber?: string;
  
  @Column('varchar', { nullable: true })
  receivingClerk?: string; // User who received the order
  
  @Column('json', { nullable: true })
  qualityControlResults?: {
    inspectedBy?: string;
    inspectionDate?: Date;
    passed?: boolean;
    defectRate?: number;
    notes?: string;
  };
}

Sales Orders (Outbound)

@ChildEntity()
export class SalesOrderEntity extends OrderEntity {
  @Column('varchar', { length: 50, unique: true })
  salesOrderNumber: string;
  
  @Column('varchar')
  customerName: string;
  
  @Column('varchar', { nullable: true })
  customerPO?: string;
  
  @Column('date')
  orderDate: Date;
  
  @Column('date')
  requestedShipDate: Date;
  
  @Column('date', { nullable: true })
  actualShipDate?: Date;
  
  @Column('varchar', { default: 'NEW' })
  status: 'NEW' | 'CONFIRMED' | 'PICKING' | 'PACKED' | 'SHIPPED' | 'DELIVERED' | 'CANCELLED';
  
  @Column('varchar', { default: 'STANDARD' })
  priority: 'LOW' | 'STANDARD' | 'HIGH' | 'URGENT';
  
  @Column('varchar')
  shippingMethod: string;
  
  @Column('json')
  shippingAddress: {
    name: string;
    address1: string;
    address2?: string;
    city: string;
    state: string;
    zipCode: string;
    country: string;
    phone?: string;
  };
  
  @Column('decimal', { precision: 12, scale: 2 })
  totalAmount: number;
  
  @Column('varchar', { nullable: true })
  trackingNumber?: string;
  
  @Column('varchar', { nullable: true })
  pickedBy?: string;
  
  @Column('varchar', { nullable: true })
  packedBy?: string;
  
  @Column('timestamp', { nullable: true })
  pickCompletedAt?: Date;
  
  @Column('timestamp', { nullable: true })
  packCompletedAt?: Date;
}

Transfer Orders (Internal)

@ChildEntity()
export class TransferOrderEntity extends OrderEntity {
  @Column('varchar', { length: 50, unique: true })
  transferOrderNumber: string;
  
  @Column('varchar')
  fromWarehouse: string;
  
  @Column('varchar')
  toWarehouse: string;
  
  @Column('date')
  requestDate: Date;
  
  @Column('date', { nullable: true })
  plannedMoveDate?: Date;
  
  @Column('date', { nullable: true })
  actualMoveDate?: Date;
  
  @Column('varchar', { default: 'REQUESTED' })
  status: 'REQUESTED' | 'APPROVED' | 'IN_TRANSIT' | 'COMPLETED' | 'CANCELLED';
  
  @Column('varchar')
  reason: 'REBALANCING' | 'PROMOTION' | 'SEASONAL' | 'DAMAGED_GOODS' | 'OTHER';
  
  @Column('varchar', { nullable: true })
  requestedBy?: string;
  
  @Column('varchar', { nullable: true })
  approvedBy?: string;
  
  @Column('varchar', { nullable: true })
  transportMethod?: string;
  
  @Column('text', { nullable: true })
  notes?: string;
  
  @Column('json', { nullable: true })
  transitDetails?: {
    carrier?: string;
    trackingNumber?: string;
    estimatedArrival?: Date;
    actualArrival?: Date;
  };
}

🏷️ BatchEntity Extensions

Lot Tracking with Expiry

@ChildEntity()
export class LotBatchEntity extends BatchEntity {
  @Column('varchar', { length: 50, unique: true })
  lotNumber: string;
  
  @Column('date')
  manufacturingDate: Date;
  
  @Column('date', { nullable: true })
  expiryDate?: Date;
  
  @Column('varchar', { nullable: true })
  supplierLotNumber?: string;
  
  @Column('varchar', { default: 'ACTIVE' })
  status: 'ACTIVE' | 'QUARANTINE' | 'EXPIRED' | 'RECALLED';
  
  @Column('json', { nullable: true })
  qualityTestResults?: {
    testDate?: Date;
    testedBy?: string;
    passed?: boolean;
    testResults?: Record<string, any>;
    certificateNumber?: string;
  };
  
  @Column('varchar', { nullable: true })
  countryOfOrigin?: string;
  
  @Column('boolean', { default: false })
  isBlocked: boolean;
  
  @Column('varchar', { nullable: true })
  blockReason?: string;
  
  @Column('text', { nullable: true })
  notes?: string;

  // Helper methods
  get isExpired(): boolean {
    return this.expiryDate ? new Date() > this.expiryDate : false;
  }
  
  get daysUntilExpiry(): number | null {
    if (!this.expiryDate) return null;
    const diffTime = this.expiryDate.getTime() - new Date().getTime();
    return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
  }
}

Serial Number Tracking

@ChildEntity()
export class SerialBatchEntity extends BatchEntity {
  @Column('varchar', { length: 100, unique: true })
  serialNumber: string;
  
  @Column('varchar', { nullable: true })
  manufacturerSerialNumber?: string;
  
  @Column('date')
  manufacturingDate: Date;
  
  @Column('date', { nullable: true })
  warrantyExpiry?: Date;
  
  @Column('varchar', { default: 'NEW' })
  condition: 'NEW' | 'REFURBISHED' | 'USED' | 'DEFECTIVE';
  
  @Column('varchar', { default: 'AVAILABLE' })
  status: 'AVAILABLE' | 'SOLD' | 'RESERVED' | 'RMA' | 'SCRAPPED';
  
  @Column('json', { nullable: true })
  specifications?: Record<string, any>;
  
  @Column('varchar', { nullable: true })
  firmwareVersion?: string;
  
  @Column('json', { nullable: true })
  serviceHistory?: Array<{
    date: Date;
    type: string;
    description: string;
    technician?: string;
  }>;
  
  @Column('decimal', { precision: 12, scale: 2, nullable: true })
  purchasePrice?: number;
  
  @Column('decimal', { precision: 12, scale: 2, nullable: true })
  currentValue?: number;
}

🔧 Module Configuration with Custom Entities

Single Entity Extension

// app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { WMSBaseModule } from '@rytass/wms-base-nestjs-module';
import { ProductEntity } from './entities/product.entity';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: 'localhost',
      port: 5432,
      username: 'wms_user',
      password: 'password',
      database: 'wms_db',
      autoLoadEntities: true,
      synchronize: false, // Use migrations in production
    }),
    TypeOrmModule.forFeature([ProductEntity]),
    WMSBaseModule.forRootAsync({
      imports: [TypeOrmModule.forFeature([ProductEntity])],
      useFactory: () => ({
        allowNegativeStock: false,
        materialEntity: ProductEntity,
      }),
    }),
  ],
})
export class AppModule {}

Multiple Entity Extensions

// app.module.ts with all custom entities
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { WMSBaseModule } from '@rytass/wms-base-nestjs-module';
import { ProductEntity } from './entities/product.entity';
import { WarehouseLocationEntity } from './entities/warehouse-location.entity';
import { PurchaseOrderEntity } from './entities/purchase-order.entity';
import { SalesOrderEntity } from './entities/sales-order.entity';
import { LotBatchEntity } from './entities/lot-batch.entity';

const CUSTOM_ENTITIES = [
  ProductEntity,
  WarehouseLocationEntity,
  PurchaseOrderEntity,
  SalesOrderEntity,
  LotBatchEntity,
];

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: process.env.DB_HOST || 'localhost',
      port: parseInt(process.env.DB_PORT || '5432'),
      username: process.env.DB_USERNAME || 'wms_user',
      password: process.env.DB_PASSWORD || 'password',
      database: process.env.DB_DATABASE || 'wms_db',
      entities: [/* TypeORM will auto-discover entities */],
      autoLoadEntities: true,
      synchronize: process.env.NODE_ENV !== 'production',
      logging: process.env.NODE_ENV === 'development',
    }),
    TypeOrmModule.forFeature(CUSTOM_ENTITIES),
    WMSBaseModule.forRootAsync({
      imports: [TypeOrmModule.forFeature(CUSTOM_ENTITIES)],
      useFactory: () => ({
        allowNegativeStock: false,
        // Configure all custom entities
        materialEntity: ProductEntity,
        locationEntity: WarehouseLocationEntity,
        orderEntity: SalesOrderEntity, // Default order type
        batchEntity: LotBatchEntity,
        // stockEntity: CustomStockEntity, // If needed
      }),
    }),
  ],
})
export class AppModule {}

📝 TypeScript Typing Patterns

Generic Service Usage

// services/warehouse.service.ts
import { Injectable } from '@nestjs/common';
import { 
  LocationService, 
  MaterialService, 
  OrderService 
} from '@rytass/wms-base-nestjs-module';
import { ProductEntity } from '../entities/product.entity';
import { WarehouseLocationEntity } from '../entities/warehouse-location.entity';
import { PurchaseOrderEntity } from '../entities/purchase-order.entity';

@Injectable()
export class WarehouseService {
  constructor(
    // Type services with custom entities
    private readonly locationService: LocationService<WarehouseLocationEntity>,
    private readonly materialService: MaterialService<ProductEntity>,
    private readonly orderService: OrderService,
  ) {}

  async createProduct(productData: Partial<ProductEntity>): Promise<ProductEntity> {
    return this.materialService.create(productData);
  }

  async createLocation(locationData: Partial<WarehouseLocationEntity>): Promise<WarehouseLocationEntity> {
    return this.locationService.create(locationData);
  }

  async createPurchaseOrder(orderData: {
    order: Partial<PurchaseOrderEntity>;
    batches: Array<{
      id: string;
      materialId: string;
      locationId: string;
      quantity: number;
    }>;
  }): Promise<PurchaseOrderEntity> {
    return this.orderService.createOrder(PurchaseOrderEntity, orderData);
  }

  // Type-safe method with custom entity fields
  async getProductsByCategoryAndBrand(
    category: string,
    brand: string
  ): Promise<ProductEntity[]> {
    // Note: This would require a custom repository method
    // For demonstration purposes
    const allProducts = await this.materialService.findAll();
    return allProducts.filter(p => 
      p.category === category && p.brand === brand
    );
  }

  async getLocationsByZone(zone: string): Promise<WarehouseLocationEntity[]> {
    const allLocations = await this.locationService.findAll();
    return allLocations.filter(l => l.zone === zone);
  }
}

Interface Segregation

// interfaces/inventory.interface.ts
export interface IProductCreate {
  name: string;
  sku: string;
  brand: string;
  category: string;
  unitPrice: number;
  weight: number;
  dimensions: {
    length: number;
    width: number;
    height: number;
  };
  reorderLevel?: number;
  maxStockLevel?: number;
}

export interface ILocationCreate {
  code: string;
  description: string;
  zone: string;
  type: 'WAREHOUSE' | 'ZONE' | 'AISLE' | 'RACK' | 'SHELF' | 'BIN';
  capacity?: number;
  maxWeight?: number;
  parentId?: string;
}

export interface IStockMovement {
  productId: string;
  locationId: string;
  quantity: number;
  batchId?: string;
  referenceNumber: string;
  movementType: 'INBOUND' | 'OUTBOUND' | 'TRANSFER' | 'ADJUSTMENT';
}

// Usage in service
@Injectable()
export class TypeSafeInventoryService {
  constructor(
    private readonly locationService: LocationService<WarehouseLocationEntity>,
    private readonly materialService: MaterialService<ProductEntity>,
    private readonly orderService: OrderService,
  ) {}

  async createProduct(productData: IProductCreate): Promise<ProductEntity> {
    return this.materialService.create({
      id: productData.sku, // Use SKU as ID
      ...productData,
      isActive: true,
      isHazmat: false,
      requiresSerialNumber: false,
    });
  }

  async createLocation(locationData: ILocationCreate): Promise<WarehouseLocationEntity> {
    return this.locationService.create({
      id: locationData.code, // Use code as ID
      ...locationData,
      isActive: true,
      isPicking: locationData.zone === 'STORAGE',
      isReceiving: locationData.zone === 'RECEIVING',
    });
  }
}

🔄 Migration Strategies

Adding New Fields to Existing Entities

// migrations/001-add-product-fields.ts
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddProductFields001 implements MigrationInterface {
  name = 'AddProductFields001'

  public async up(queryRunner: QueryRunner): Promise<void> {
    // Add new fields with safe defaults
    await queryRunner.query(`
      ALTER TABLE "materials" 
      ADD COLUMN "brand" varchar,
      ADD COLUMN "category" varchar,
      ADD COLUMN "unitPrice" decimal(10,2),
      ADD COLUMN "reorderLevel" integer DEFAULT 10,
      ADD COLUMN "isHazmat" boolean DEFAULT false
    `);

    // Update existing records with reasonable defaults
    await queryRunner.query(`
      UPDATE "materials" 
      SET 
        "brand" = 'Unknown',
        "category" = 'General',
        "unitPrice" = 0.00
      WHERE "entityName" = 'ProductEntity'
        AND ("brand" IS NULL OR "category" IS NULL OR "unitPrice" IS NULL)
    `);

    // Add NOT NULL constraints after setting defaults
    await queryRunner.query(`
      ALTER TABLE "materials" 
      ALTER COLUMN "brand" SET NOT NULL,
      ALTER COLUMN "category" SET NOT NULL,
      ALTER COLUMN "unitPrice" SET NOT NULL
    `);
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`
      ALTER TABLE "materials" 
      DROP COLUMN "brand",
      DROP COLUMN "category", 
      DROP COLUMN "unitPrice",
      DROP COLUMN "reorderLevel",
      DROP COLUMN "isHazmat"
    `);
  }
}

Entity Migration Script

// scripts/migrate-to-custom-entities.ts
import { DataSource } from 'typeorm';
import { MaterialEntity } from '@rytass/wms-base-nestjs-module';
import { ProductEntity } from '../entities/product.entity';

export async function migrateBasicMaterialsToProducts(dataSource: DataSource) {
  const materialRepo = dataSource.getRepository(MaterialEntity);
  const productRepo = dataSource.getRepository(ProductEntity);

  const basicMaterials = await materialRepo.find();

  for (const material of basicMaterials) {
    // Create new ProductEntity record with enhanced data
    const product = productRepo.create({
      id: material.id,
      createdAt: material.createdAt,
      updatedAt: material.updatedAt,
      // Add required ProductEntity fields
      name: `Product ${material.id}`,
      sku: material.id,
      brand: 'Unknown',
      category: 'General',
      unitPrice: 0.00,
      weight: 0.1,
      dimensions: { length: 10, width: 10, height: 10 },
      reorderLevel: 10,
      maxStockLevel: 1000,
      isActive: true,
      isHazmat: false,
      requiresSerialNumber: false,
    });

    await productRepo.save(product);
  }

  console.log(`✅ Migrated ${basicMaterials.length} materials to ProductEntity`);
}

✅ What You've Mastered:

  • Custom entity extension for all 5 base entity types
  • Real-world business scenarios and field definitions
  • Module configuration patterns for single and multiple entities
  • TypeScript typing strategies with generic services
  • Interface segregation for clean architecture
  • Migration strategies for adding fields and entity upgrades

🎯 Next Steps: Ready for production optimization? Continue to Advanced Patterns.


🚀 Advanced Patterns

Production Optimization Strategies

🎯 Performance Optimization

Query Optimization Patterns
// services/optimized-stock.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, EntityManager } from 'typeorm';
import { StockService } from '@rytass/wms-base-nestjs-module';
import { ProductEntity } from '../entities/product.entity';

@Injectable()
export class OptimizedStockService {
  constructor(
    @InjectRepository(ProductEntity)
    private readonly productRepo: Repository<ProductEntity>,
    private readonly stockService: StockService,
  ) {}

  /**
   * Batch stock lookup for multiple products - 90% faster than individual queries
   */
  async getBatchStockLevels(productSkus: string[]): Promise<Map<string, number>> {
    const stockMap = new Map<string, number>();

    // Single query instead of N queries
    const stockLevels = await this.stockService.findTransactions({
      materialIds: productSkus,
    });

    // Aggregate by product
    for (const transaction of stockLevels.transactionLogs) {
      const current = stockMap.get(transaction.materialId) || 0;
      stockMap.set(transaction.materialId, current + transaction.quantity);
    }

    return stockMap;
  }

  /**
   * Optimized location-based stock query with descendant lookup
   */
  async getLocationStockOptimized(
    locationId: string,
    includeChildren: boolean = true
  ): Promise<{ productSku: string; quantity: number; locationCode: string }[]> {
    return this.productRepo.manager.query(`
      WITH RECURSIVE location_tree AS (
        SELECT id, code, mpath FROM locations WHERE id = $1
        UNION ALL
        SELECT l.id, l.code, l.mpath 
        FROM locations l
        JOIN location_tree lt ON l.mpath LIKE CONCAT(lt.mpath, '%')
        WHERE $2 = true
      )
      SELECT 
        p.sku as "productSku",
        SUM(s.quantity) as quantity,
        l.code as "locationCode"
      FROM stocks s
      JOIN materials p ON p.id = s."materialId"
      JOIN location_tree l ON l.id = s."locationId"
      WHERE s."deletedAt" IS NULL
      GROUP BY p.sku, l.code
      ORDER BY p.sku, l.code
    `, [locationId, includeChildren]);
  }
}
Caching Strategies
// services/cached-inventory.service.ts
import { Injectable, CacheManager } from '@nestjs/common';
import { InjectCache } from '@nestjs/cache-manager';
import { StockService, LocationService } from '@rytass/wms-base-nestjs-module';

@Injectable()
export class CachedInventoryService {
  constructor(
    @InjectCache() private cacheManager: CacheManager,
    private readonly stockService: StockService,
    private readonly locationService: LocationService,
  ) {}

  /**
   * Cache frequently accessed stock levels with smart invalidation
   */
  async getCachedStockLevel(
    productId: string,
    locationId: string
  ): Promise<number> {
    const cacheKey = `stock:${productId}:${locationId}`;
    
    let stockLevel = await this.cacheManager.get<number>(cacheKey);
    
    if (stockLevel === undefined) {
      stockLevel = await this.stockService.find({
        materialIds: [productId],
        locationIds: [locationId],
        exactLocationMatch: true,
      });
      
      // Cache for 5 minutes
      await this.cacheManager.set(cacheKey, stockLevel, 300);
    }
    
    return stockLevel;
  }

  /**
   * Invalidate cache on stock movements
   */
  async invalidateStockCache(productId: string, locationId: string): Promise<void> {
    const patterns = [
      `stock:${productId}:${locationId}`,
      `stock:${productId}:*`,
      `location:${locationId}:*`,
    ];

    for (const pattern of patterns) {
      await this.cacheManager.del(pattern);
    }
  }

  /**
   * Warm cache with most accessed products
   */
  async warmCache(popularProducts: string[]): Promise<void> {
    const activeLocations = await this.locationService.findAll();
    
    const promises = popularProducts.flatMap(productId =>
      activeLocations.map(location =>
        this.getCachedStockLevel(productId, location.id)
      )
    );

    await Promise.all(promises);
    console.log(`✅ Warmed cache for ${popularProducts.length} products across ${activeLocations.length} locations`);
  }
}

🏗️ Multi-Tenant Architecture

Tenant-Isolated WMS
// services/multi-tenant-wms.service.ts
import { Injectable, Scope } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Inject } from '@nestjs/common';
import { Request } from 'express';
import { 
  LocationService, 
  MaterialService, 
  StockService, 
  OrderService 
} from '@rytass/wms-base-nestjs-module';

// Tenant-aware entity extensions
@ChildEntity()
export class TenantProductEntity extends MaterialEntity {
  @Column('varchar')
  tenantId: string; // Tenant isolation

  @Column('varchar')
  name: string;
  
  @Column('varchar')
  sku: string;
  
  @Column('decimal', { precision: 10, scale: 2 })
  unitPrice: number;
}

@ChildEntity()
export class TenantLocationEntity extends LocationEntity {
  @Column('varchar')
  tenantId: string; // Tenant isolation

  @Column('varchar')
  code: string;
  
  @Column('varchar')
  zone: string;
}

interface TenantRequest extends Request {
  tenantId: string;
}

@Injectable({ scope: Scope.REQUEST })
export class MultiTenantWMSService {
  private readonly tenantId: string;

  constructor(
    @Inject(REQUEST) private readonly request: TenantRequest,
    private readonly locationService: LocationService<TenantLocationEntity>,
    private readonly materialService: MaterialService<TenantProductEntity>,
    private readonly stockService: StockService,
    private readonly orderService: OrderService,
  ) {
    this.tenantId = request.tenantId;
  }

  /**
   * Tenant-aware product creation
   */
  async createProduct(productData: Omit<TenantProductEntity, 'tenantId' | 'id'>): Promise<TenantProductEntity> {
    return this.materialService.create({
      ...productData,
      id: `${this.tenantId}-${productData.sku}`,
      tenantId: this.tenantId,
    });
  }

  /**
   * Tenant-aware location creation
   */
  async createLocation(locationData: Omit<TenantLocationEntity, 'tenantId' | 'id'>): Promise<TenantLocationEntity> {
    return this.locationService.create({
      ...locationData,
      id: `${this.tenantId}-${locationData.code}`,
      tenantId: this.tenantId,
    });
  }

  /**
   * Tenant-isolated stock levels
   */
  async getTenantStockLevels(): Promise<Array<{
    productSku: string;
    locationCode: string;
    quantity: number;
  }>> {
    // Get all tenant products and locations
    const products = await this.materialService.findAll();
    const locations = await this.locationService.findAll();

    const stockLevels = [];

    for (const product of products) {
      if (!product.tenantId.startsWith(this.tenantId)) continue;

      for (const location of locations) {
        if (!location.tenantId.startsWith(this.tenantId)) continue;

        const quantity = await this.stockService.find({
          materialIds: [product.id],
          locationIds: [location.id],
          exactLocationMatch: true,
        });

        if (quantity > 0) {
          stockLevels.push({
            productSku: product.sku,
            locationCode: location.code,
            quantity,
          });
        }
      }
    }

    return stockLevels;
  }

  /**
   * Cross-tenant transfer (with authorization)
   */
  async transferBetweenTenants(
    productSku: string,
    fromLocationCode: string,
    toTenantId: string,
    toLocationCode: string,
    quantity: number,
    authorizationCode: string
  ): Promise<void> {
    // Verify authorization for cross-tenant operations
    if (!this.isAuthorizedForCrossTenantTransfer(authorizationCode)) {
      throw new Error('Unauthorized cross-tenant transfer');
    }

    const fromProduct = await this.findTenantProduct(productSku);
    const fromLocation = await this.findTenantLocation(fromLocationCode);
    
    // Create dual orders for cross-tenant transfer
    await Promise.all([
      // Outbound from current tenant
      this.orderService.createOrder(OrderEntity, {
        order: {},
        batches: [{
          id: `XFER-OUT-${Date.now()}`,
          locationId: fromLocation.id,
          materialId: fromProduct.id,
          quantity: -quantity,
        }],
      }),
      // Inbound to target tenant
      this.orderService.createOrder(OrderEntity, {
        order: {},
        batches: [{
          id: `XFER-IN-${Date.now()}`,
          locationId: `${toTenantId}-${toLocationCode}`,
          materialId: `${toTenantId}-${productSku}`,
          quantity: quantity,
        }],
      }),
    ]);
  }

  private async findTenantProduct(sku: string): Promise<TenantProductEntity> {
    const product = await this.materialService.findById(`${this.tenantId}-${sku}`);
    if (!product || product.tenantId !== this.tenantId) {
      throw new Error(`Product ${sku} not found for tenant ${this.tenantId}`);
    }
    return product;
  }

  private async findTenantLocation(code: string): Promise<TenantLocationEntity> {
    const location = await this.locationService.findById(`${this.tenantId}-${code}`);
    if (!location || location.tenantId !== this.tenantId) {
      throw new Error(`Location ${code} not found for tenant ${this.tenantId}`);
    }
    return location;
  }

  private isAuthorizedForCrossTenantTransfer(authCode: string): boolean {
    // Implement authorization logic
    return authCode.startsWith('ADMIN-');
  }
}
Tenant Configuration Module
// modules/tenant-wms.module.ts
import { Module, DynamicModule } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { WMSBaseModule } from '@rytass/wms-base-nestjs-module';
import { TenantProductEntity, TenantLocationEntity } from '../entities/tenant.entities';
import { MultiTenantWMSService } from '../services/multi-tenant-wms.service';

export interface TenantWMSModuleOptions {
  tenantId: string;
  databaseConfig: {
    host: string;
    port: number;
    username: string;
    password: string;
    database: string;
  };
}

@Module({})
export class TenantWMSModule {
  static forTenant(options: TenantWMSModuleOptions): DynamicModule {
    return {
      module: TenantWMSModule,
      imports: [
        TypeOrmModule.forRoot({
          name: `tenant-${options.tenantId}`,
          type: 'postgres',
          ...options.databaseConfig,
          entities: [TenantProductEntity, TenantLocationEntity],
          synchronize: false,
        }),
        TypeOrmModule.forFeature([TenantProductEntity, TenantLocationEntity], `tenant-${options.tenantId}`),
        WMSBaseModule.forRootAsync({
          imports: [TypeOrmModule.forFeature([TenantProductEntity, TenantLocationEntity], `tenant-${options.tenantId}`)],
          useFactory: () => ({
            materialEntity: TenantProductEntity,
            locationEntity: TenantLocationEntity,
          }),
        }),
      ],
      providers: [MultiTenantWMSService],
      exports: [MultiTenantWMSService],
    };
  }
}

🔐 Security & Audit Patterns

Audit Trail Implementation
// entities/audit-trail.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn } from 'typeorm';

@Entity('audit_logs')
export class AuditTrailEntity {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column('varchar')
  entityType: string; // 'Product', 'Location', 'Order', etc.

  @Column('varchar')
  entityId: string;

  @Column('varchar')
  action: string; // 'CREATE', 'UPDATE', 'DELETE', 'STOCK_MOVE'

  @Column('json', { nullable: true })
  previousValues?: Record<string, any>;

  @Column('json', { nullable: true })
  newValues?: Record<string, any>;

  @Column('varchar')
  userId: string;

  @Column('varchar', { nullable: true })
  sessionId?: string;

  @Column('varchar', { nullable: true })
  ipAddress?: string;

  @Column('text', { nullable: true })
  reason?: string;

  @CreateDateColumn()
  timestamp: Date;

  @Column('json', { nullable: true })
  metadata?: Record<string, any>;
}

// services/audit.service.ts
@Injectable()
export class AuditService {
  constructor(
    @InjectRepository(AuditTrailEntity)
    private readonly auditRepo: Repository<AuditTrailEntity>,
  ) {}

  async logAction(
    entityType: string,
    entityId: string,
    action: string,
    userId: string,
    changes?: {
      previous?: Record<string, any>;
      new?: Record<string, any>;
    },
    metadata?: Record<string, any>
  ): Promise<void> {
    await this.auditRepo.save({
      entityType,
      entityId,
      action,
      userId,
      previousValues: changes?.previous,
      newValues: changes?.new,
      metadata,
    });
  }

  async getAuditTrail(
    entityType?: string,
    entityId?: string,
    startDate?: Date,
    endDate?: Date
  ): Promise<AuditTrailEntity[]> {
    const query = this.auditRepo.createQueryBuilder('audit');

    if (entityType) {
      query.andWhere('audit.entityType = :entityType', { entityType });
    }

    if (entityId) {
      query.andWhere('audit.entityId = :entityId', { entityId });
    }

    if (startDate) {
      query.andWhere('audit.timestamp >= :startDate', { startDate });
    }

    if (endDate) {
      query.andWhere('audit.timestamp <= :endDate', { endDate });
    }

    return query.orderBy('audit.timestamp', 'DESC').getMany();
  }
}
Audited WMS Service
// services/audited-wms.service.ts
@Injectable()
export class AuditedWMSService {
  constructor(
    private readonly materialService: MaterialService<ProductEntity>,
    private readonly locationService: LocationService<WarehouseLocationEntity>,
    private readonly orderService: OrderService,
    private readonly auditService: AuditService,
  ) {}

  async createProductWithAudit(
    productData: Partial<ProductEntity>,
    userId: string,
    reason?: string
  ): Promise<ProductEntity> {
    const product = await this.materialService.create(productData);

    await this.auditService.logAction(
      'Product',
      product.id,
      'CREATE',
      userId,
      { new: productData },
      { reason }
    );

    return product;
  }

  async moveInventoryWithAudit(
    productId: string,
    fromLocationId: string,
    toLocationId: string,
    quantity: number,
    userId: string,
    reason: string
  ): Promise<void> {
    // Record before state
    const beforeFromStock = await this.stockService.find({
      materialIds: [productId],
      locationIds: [fromLocationId],
      exactLocationMatch: true,
    });
    
    const beforeToStock = await this.stockService.find({
      materialIds: [productId],
      locationIds: [toLocationId],
      exactLocationMatch: true,
    });

    // Execute transfer
    await this.orderService.createOrder(OrderEntity, {
      order: {},
      batches: [
        {
          id: `XFER-OUT-${Date.now()}`,
          locationId: fromLocationId,
          materialId: productId,
          quantity: -quantity,
        },
        {
          id: `XFER-IN-${Date.now()}`,
          locationId: toLocationId,
          materialId: productId,
          quantity: quantity,
        },
      ],
    });

    // Record after state
    const afterFromStock = await this.stockService.find({
      materialIds: [productId],
      locationIds: [fromLocationId],
      exactLocationMatch: true,
    });
    
    const afterToStock = await this.stockService.find({
      materialIds: [productId],
      locationIds: [toLocationId],
      exactLocationMatch: true,
    });

    // Log audit trail
    await this.auditService.logAction(
      'Inventory',
      productId,
      'TRANSFER',
      userId,
      {
        previous: {
          fromLocation: { id: fromLocationId, stock: beforeFromStock },
          toLocation: { id: toLocationId, stock: beforeToStock },
        },
        new: {
          fromLocation: { id: fromLocationId, stock: afterFromStock },
          toLocation: { id: toLocationId, stock: afterToStock },
        },
      },
      { reason, transferQuantity: quantity }
    );
  }
}

📊 Analytics & Reporting

Advanced Analytics Service
// services/analytics.service.ts
@Injectable()
export class AnalyticsService {
  constructor(
    private readonly stockService: StockService,
    @InjectRepository(ProductEntity)
    private readonly productRepo: Repository<ProductEntity>,
  ) {}

  /**
   * ABC Analysis - categorize products by value/movement
   */
  async performABCAnalysis(): Promise<{
    A: ProductEntity[]; // 80% of value
    B: ProductEntity[]; // 15% of value  
    C: ProductEntity[]; // 5% of value
  }> {
    // Get all products with movement data
    const products = await this.productRepo.query(`
      SELECT 
        p.*,
        COALESCE(SUM(ABS(s.quantity)), 0) as total_movement,
        COALESCE(SUM(ABS(s.quantity)) * p."unitPrice", 0) as total_value
      FROM materials p
      LEFT JOIN stocks s ON s."materialId" = p.id
      WHERE p."entityName" = 'ProductEntity'
        AND s."createdAt" >= NOW() - INTERVAL '90 days'
      GROUP BY p.id
      ORDER BY total_value DESC
    `);

    const totalValue = products.reduce((sum, p) => sum + parseFloat(p.total_value), 0);
    let runningValue = 0;
    
    const result = { A: [], B: [], C: [] };

    for (const product of products) {
      const productValue = parseFloat(product.total_value);
      runningValue += productValue;
      const percentOfTotal = runningValue / totalValue;

      if (percentOfTotal <= 0.8) {
        result.A.push(product);
      } else if (percentOfTotal <= 0.95) {
        result.B.push(product);
      } else {
        result.C.push(product);
      }
    }

    return result;
  }

  /**
   * Turnover Analysis
   */
  async calculateTurnoverRates(days: number = 90): Promise<Array<{
    productSku: string;
    averageStock: number;
    totalMovement: number;
    turnoverRate: number;
    daysOfStock: number;
  }>> {
    return this.productRepo.query(`
      WITH stock_summary AS (
        SELECT 
          p.sku as product_sku,
          AVG(daily_stock.stock_level) as average_stock,
          SUM(CASE WHEN s.quantity < 0 THEN ABS(s.quantity) ELSE 0 END) as total_outbound
        FROM materials p
        CROSS JOIN LATERAL (
          SELECT SUM(s2.quantity) as stock_level
          FROM stocks s2 
          WHERE s2."materialId" = p.id 
            AND s2."createdAt" <= s."createdAt"
        ) daily_stock
        JOIN stocks s ON s."materialId" = p.id
        WHERE p."entityName" = 'ProductEntity'
          AND s."createdAt" >= NOW() - INTERVAL '${days} days'
        GROUP BY p.id, p.sku
        HAVING AVG(daily_stock.stock_level) > 0
      )
      SELECT 
        product_sku as "productSku",
        average_stock as "averageStock", 
        total_outbound as "totalMovement",
        CASE 
          WHEN average_stock > 0 
          THEN total_outbound / average_stock * (365.0 / ${days})
          ELSE 0 
        END as "turnoverRate",
        CASE 
          WHEN total_outbound > 0 
          THEN average_stock / (total_outbound / ${days})
          ELSE 999 
        END as "daysOfStock"
      FROM stock_summary
      ORDER BY "turnoverRate" DESC
    `);
  }

  /**
   * Seasonal Analysis
   */
  async analyzeSeasonalTrends(productIds?: string[]): Promise<Array<{
    productSku: string;
    month: number;
    averageDemand: number;
    seasonalIndex: number;
  }>> {
    const productFilter = productIds?.length 
      ? `AND p.id IN (${productIds.map(id => `'${id}'`).join(',')})`
      : '';

    return this.productRepo.query(`
      WITH monthly_demand AS (
        SELECT 
          p.sku as product_sku,
          EXTRACT(MONTH FROM s."createdAt") as month,
          AVG(ABS(s.quantity)) as avg_demand
        FROM materials p
        JOIN stocks s ON s."materialId" = p.id
        WHERE p."entityName" = 'ProductEntity'
          AND s.quantity < 0  -- Only outbound movements
          AND s."createdAt" >= NOW() - INTERVAL '2 years'
          ${productFilter}
        GROUP BY p.id, p.sku, EXTRACT(MONTH FROM s."createdAt")
      ),
      overall_average AS (
        SELECT 
          product_sku,
          AVG(avg_demand) as yearly_avg
        FROM monthly_demand
        GROUP BY product_sku
      )
      SELECT 
        md.product_sku as "productSku",
        md.month::int as "month",
        md.avg_demand as "averageDemand",
        CASE 
          WHEN oa.yearly_avg > 0 
          THEN md.avg_demand / oa.yearly_avg 
          ELSE 1 
        END as "seasonalIndex"
      FROM monthly_demand md
      JOIN overall_average oa ON oa.product_sku = md.product_sku
      ORDER BY md.product_sku, md.month
    `);
  }
}

✅ What You've Achieved:

  • Production-ready performance optimization techniques
  • Multi-tenant architecture with data isolation
  • Comprehensive audit trail and security patterns
  • Advanced analytics including ABC analysis and turnover rates
  • Scalable caching strategies and query optimization

🎯 Next Steps: Ready for complete enterprise implementations? Continue to Enterprise Examples.


🏭 Enterprise Examples

Real-world implementation patterns for production systems.

E-commerce Fulfillment Center

Complete implementation for high-volume e-commerce operations with order processing, pick-pack workflows, and shipping integration.

// entities/ecommerce-entities.ts
@ChildEntity()
export class EcommerceProductEntity extends MaterialEntity {
  @Column('varchar', { length: 100 })
  name: string;

  @Column('varchar', { length: 50, unique: true })
  sku: string;

  @Column('decimal', { precision: 10, scale: 2 })
  price: number;

  @Column('decimal', { precision: 8, scale: 3 })
  weight: number;

  @Column('json')
  dimensions: { length: number; width: number; height: number; };

  @Column('varchar', { length: 50 })
  category: string;

  @Column('boolean', { default: true })
  isActive: boolean;
}

@ChildEntity()
export class WarehouseZoneEntity extends LocationEntity {
  @Column('varchar', { length: 30 })
  zoneType: 'RECEIVING' | 'STORAGE' | 'PICKING' | 'PACKING' | 'SHIPPING';

  @Column('varchar', { length: 10 })
  temperature: 'AMBIEN