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 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; import { GoodsReceipt, ServiceReceipt } from '../schemas/receipt.schema'; import { PurchaseOrderService } from '../../purchase-orders/services/purchase-order.service'; import { ConfigurationService } from '../../configuration/services/configuration.service'; import { EventBusService } from '../../../../platform/events/event-bus.service'; import { AuditLogService } from '../../../../platform/audit/audit-log.service'; @Injectable() export class ReceiptService { constructor( @InjectModel(GoodsReceipt.name) private readonly goodsModel: Model<GoodsReceipt>, @InjectModel(ServiceReceipt.name) private readonly serviceModel: Model<ServiceReceipt>, private readonly poService: PurchaseOrderService, private readonly configService: ConfigurationService, private readonly eventBus: EventBusService, private readonly auditLog: AuditLogService, ) {} async createGoodsReceipt(tenantId: string, data: any, userId: string): Promise<GoodsReceipt> { const po = await this.poService.getPurchaseOrderById(tenantId, data.purchaseOrderId); if (!po) throw new NotFoundException('Purchase Order not found'); const config = await this.configService.getConfiguration(tenantId); const count = await this.goodsModel.countDocuments({ tenantId }).exec(); const receiptNumber = `GRN-${new Date().getFullYear()}-${(count + 1).toString().padStart(5, '0')}`; // Over-receipt tolerance checking for (const line of data.lines) { const poLine = po.items[line.poLineIndex]; if (!poLine) throw new BadRequestException(`Invalid PO line index: ${line.poLineIndex}`); const allowedMax = poLine.quantity * (1 + config.overReceiptTolerancePercent / 100); if (line.quantityReceived > allowedMax) { throw new BadRequestException( `Quantity received (${line.quantityReceived}) exceeds ordered (${poLine.quantity}) beyond tolerance (${config.overReceiptTolerancePercent}%)` ); } } const grn = await this.goodsModel.create({ ...data, tenantId, receiptNumber, status: 'approved', }); // Update PO status await this.poService.recordReceipt(tenantId, data.purchaseOrderId, 'fully_received'); await this.eventBus.publish('procurement.goods-receipt.approved.v1', { eventId: `evt-grn-${(grn as any)._id.toString()}`, occurredAt: new Date(), idempotencyKey: `idemp-grn-${(grn as any)._id.toString()}`, tenantId, legalEntityId: (po as any).legalEntityId || 'LE-1', branchId: (po as any).branchId || 'BR-1', purchaseOrderId: data.purchaseOrderId, goodsReceiptId: (grn as any)._id.toString(), vendorId: po.vendorId, currency: (po as any).currency || 'USD', lines: data.lines.map((line: any, idx: number) => { const poLine = po.items[line.poLineIndex]; return { purchaseOrderLineId: (poLine as any)?.id || `po-line-${line.poLineIndex}`, goodsReceiptLineId: `${(grn as any)._id.toString()}-line-${idx}`, itemReference: (poLine as any)?.itemId || poLine?.description, productReference: (poLine as any)?.productId || poLine?.description, freeTextReference: poLine?.description, uom: (poLine as any)?.uom || 'PCS', receivedQuantity: line.quantityReceived, acceptedQuantity: line.quantityReceived - (line.quantityDamaged || 0), rejectedQuantity: 0, damagedQuantity: line.quantityDamaged || 0, quarantinedQuantity: line.quarantinedQuantity || 0, warehouseReference: line.warehouseId || 'WH-MAIN', locationReference: line.locationId || 'LOC-BIN-1', batchDetails: line.batchNumber || null, serialDetails: line.serialNumber || null, manufacturingDate: line.manufacturingDate || null, expiryDate: line.expiryDate || null, unitCostSnapshot: poLine?.unitPrice || 0, taxSnapshot: (poLine as any)?.taxSnapshots || [], }; }), }, tenantId); await this.auditLog.log({ tenantId, userId, action: 'APPROVE_GOODS_RECEIPT', resource: 'GoodsReceipt', resourceId: (grn as any)._id.toString(), newValues: { receiptNumber }, }); return grn; } async reverseGoodsReceipt(tenantId: string, id: string, userId: string): Promise<GoodsReceipt> { const grn = await this.goodsModel.findOneAndUpdate( { _id: id, tenantId, status: 'approved' }, { status: 'reversed' }, { new: true } ).exec(); if (!grn) throw new NotFoundException('Approved Goods Receipt not found'); await this.eventBus.publish('procurement.goods-receipt.reversed.v1', { eventId: `evt-rev-${grn._id.toString()}`, occurredAt: new Date(), idempotencyKey: `idemp-rev-${grn._id.toString()}`, tenantId, purchaseOrderId: grn.purchaseOrderId, goodsReceiptId: grn._id.toString(), lines: grn.lines.map((line: any, idx: number) => ({ goodsReceiptLineId: `${grn._id.toString()}-line-${idx}`, receivedQuantity: -line.quantityReceived, })), }, tenantId); await this.auditLog.log({ tenantId, userId, action: 'REVERSE_GOODS_RECEIPT', resource: 'GoodsReceipt', resourceId: grn._id.toString(), newValues: { status: 'reversed' }, }); return grn; } async createServiceReceipt(tenantId: string, data: any, userId: string): Promise<ServiceReceipt> { const po = await this.poService.getPurchaseOrderById(tenantId, data.purchaseOrderId); if (!po) throw new NotFoundException('Purchase Order not found'); const count = await this.serviceModel.countDocuments({ tenantId }).exec(); const receiptNumber = `SRN-${new Date().getFullYear()}-${(count + 1).toString().padStart(5, '0')}`; const srn = await this.serviceModel.create({ ...data, tenantId, receiptNumber, status: 'approved', }); await this.eventBus.publish('procurement.service-receipt.approved.v1', { eventId: `evt-srn-${(srn as any)._id.toString()}`, occurredAt: new Date(), idempotencyKey: `idemp-srn-${(srn as any)._id.toString()}`, tenantId, purchaseOrderId: data.purchaseOrderId, serviceReceiptId: (srn as any)._id.toString(), lines: data.lines.map((line: any, idx: number) => ({ poLineIndex: line.poLineIndex, amountCertified: line.amountCertified, })), }, tenantId); await this.auditLog.log({ tenantId, userId, action: 'APPROVE_SERVICE_RECEIPT', resource: 'ServiceReceipt', resourceId: (srn as any)._id.toString(), newValues: { receiptNumber }, }); return srn; } async getGoodsReceipts(tenantId: string): Promise<GoodsReceipt[]> { return this.goodsModel.find({ tenantId }).exec(); } async getServiceReceipts(tenantId: string): Promise<ServiceReceipt[]> { return this.serviceModel.find({ tenantId }).exec(); } } |