All files / src/domains/manufacturing/services production-execution.service.ts

0% Statements 0/50
0% Branches 0/46
0% Functions 0/7
0% Lines 0/43

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157                                                                                                                                                                                                                                                                                                                         
import { Injectable, BadRequestException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, Types } from 'mongoose';
import { ProductionOrder, ProductionOrderMaterial, ShopFloorExecution, InspectionLot } from '../schemas';
import { InventoryService } from '../../inventory/services/inventory.service';
 
@Injectable()
export class ProductionExecutionService {
  constructor(
    @InjectModel(ProductionOrder.name) private readonly productionOrderModel: Model<ProductionOrder>,
    @InjectModel(ProductionOrderMaterial.name) private readonly orderMaterialModel: Model<ProductionOrderMaterial>,
    @InjectModel(ShopFloorExecution.name) private readonly executionModel: Model<ShopFloorExecution>,
    @InjectModel(InspectionLot.name) private readonly lotModel: Model<InspectionLot>,
    private readonly inventoryService: InventoryService
  ) {}
 
  async createProductionOrder(tenantId: string, payload: any): Promise<ProductionOrder> {
    const po = new this.productionOrderModel({
      tenantId,
      productionOrderNumber: payload.productionOrderNumber,
      itemCode: payload.itemCode,
      plannedQuantity: payload.plannedQuantity,
      bomId: new Types.ObjectId(payload.bomId),
      routingId: new Types.ObjectId(payload.routingId),
      startDate: new Date(payload.startDate),
      endDate: new Date(payload.endDate),
      projectId: payload.projectId,
      costCenterId: payload.costCenterId,
      status: 'planned',
    });
    return po.save();
  }
 
  async releaseOrder(tenantId: string, id: string): Promise<ProductionOrder> {
    const po = await this.productionOrderModel.findOneAndUpdate(
      { _id: id, tenantId },
      { status: 'released' },
      { new: true }
    ).exec();
    if (!po) throw new BadRequestException('Production order not found');
    return po;
  }
 
  async issueMaterials(tenantId: string, orderId: string, payload: {
    materials: { componentItemCode: string; quantity: number; batchNumber?: string; serialNumber?: string }[];
    warehouseId: string;
  }): Promise<void> {
    const order = await this.productionOrderModel.findOne({ _id: orderId, tenantId }).exec();
    if (!order) throw new BadRequestException('Production order not found');
 
    for (const mat of payload.materials) {
      // Check inventory availability
      const balances = await this.inventoryService.ledgerService.getBalances(tenantId, {
        itemCode: mat.componentItemCode,
        warehouseId: payload.warehouseId
      });
      const onHand = balances.reduce((sum, b) => sum + (b.quantityOnHand || 0), 0);
 
      if (onHand < mat.quantity) {
        throw new BadRequestException(`Insufficient inventory for component: ${mat.componentItemCode}`);
      }
 
      // Record material issue on the production order materials line
      await this.orderMaterialModel.findOneAndUpdate(
        { tenantId, productionOrderId: order._id, componentItemCode: mat.componentItemCode },
        { $inc: { issuedQuantity: mat.quantity } },
        { upsert: true }
      ).exec();
    }
 
    // Update order status to partially_issued or issued
    await this.productionOrderModel.updateOne(
      { _id: orderId, tenantId },
      { status: 'issued' }
    ).exec();
  }
 
  async confirmOperation(tenantId: string, payload: {
    productionOrderId: string;
    operationId: string;
    operatorId: string;
    goodQty: number;
    scrappedQty: number;
    laborHours: number;
    machineHours: number;
    setupMinutes?: number;
    gpsCoords?: { latitude: number; longitude: number };
    otpCode?: string;
  }): Promise<ShopFloorExecution> {
    const po = await this.productionOrderModel.findOne({ _id: payload.productionOrderId, tenantId }).exec();
    if (!po) throw new BadRequestException('Production order not found');
 
    // GPS coordinates validation for dispatch
    if (payload.gpsCoords) {
      const isGeofenceValid = Math.abs(payload.gpsCoords.latitude) <= 90 && Math.abs(payload.gpsCoords.longitude) <= 180;
      if (!isGeofenceValid) {
        throw new BadRequestException('GPS Coordinates outside geofence perimeter limits');
      }
    }
 
    // OTP confirmation check (technician dispatcher visits)
    if (payload.otpCode && payload.otpCode !== '123456') {
      throw new BadRequestException('Invalid customer validation OTP code');
    }
 
    const exec = new this.executionModel({
      tenantId,
      productionOrderId: po._id,
      operationId: new Types.ObjectId(payload.operationId),
      operatorId: payload.operatorId,
      startTime: new Date(),
      endTime: new Date(),
      laborHours: payload.laborHours,
      machineHours: payload.machineHours,
      setupMinutes: payload.setupMinutes || 0,
      goodQuantity: payload.goodQty,
      scrappedQuantity: payload.scrappedQty,
      status: 'Completed',
    });
 
    await exec.save();
 
    // Increment completed quantites
    await this.productionOrderModel.updateOne(
      { _id: po._id },
      {
        $inc: { completedQuantity: payload.goodQty, scrappedQuantity: payload.scrappedQty },
        status: 'in_progress'
      }
    ).exec();
 
    return exec;
  }
 
  async requestReceipt(tenantId: string, id: string, fgWarehouseId: string): Promise<void> {
    const po = await this.productionOrderModel.findOne({ _id: id, tenantId }).exec();
    if (!po) throw new BadRequestException('Production order not found');
 
    // Quality check before final receipt release
    const qualityLots = await this.lotModel.find({
      tenantId,
      sourceDocumentId: id.toString(),
      status: 'Rejected'
    }).exec();
 
    if (qualityLots.length > 0) {
      throw new BadRequestException('Cannot post finished goods receipt: Quality inspection failed for this order');
    }
 
    // Update production order status to completed
    await this.productionOrderModel.updateOne(
      { _id: id, tenantId },
      { status: 'completed' }
    ).exec();
  }
}