All files / src/domains/procurement/sourcing/services sourcing.service.ts

0% Statements 0/55
0% Branches 0/32
0% Functions 0/9
0% Lines 0/47

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                                                                                                                                                                                                                                                                                                                                                 
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { RequestForQuotation, VendorQuotation } from '../schemas/sourcing.schema';
import { EventBusService } from '../../../../platform/events/event-bus.service';
import { AuditLogService } from '../../../../platform/audit/audit-log.service';
 
@Injectable()
export class SourcingService {
  constructor(
    @InjectModel(RequestForQuotation.name)
    private readonly rfqModel: Model<RequestForQuotation>,
    @InjectModel(VendorQuotation.name)
    private readonly quotationModel: Model<VendorQuotation>,
    private readonly eventBus: EventBusService,
    private readonly auditLog: AuditLogService,
  ) {}
 
  async createRfq(tenantId: string, data: any, userId: string): Promise<RequestForQuotation> {
    const count = await this.rfqModel.countDocuments({ tenantId }).exec();
    const rfqNumber = `RFQ-${new Date().getFullYear()}-${(count + 1).toString().padStart(5, '0')}`;
 
    const rfq = await this.rfqModel.create({
      ...data,
      tenantId,
      rfqNumber,
      status: 'published',
    });
 
    await this.eventBus.publish('procurement.rfq.created.v1', {
      rfqId: (rfq as any)._id.toString(),
      tenantId,
      rfqNumber,
    }, tenantId);
 
    await this.auditLog.log({
      tenantId,
      userId,
      action: 'CREATE',
      resource: 'RequestForQuotation',
      resourceId: (rfq as any)._id.toString(),
      newValues: { rfqNumber },
    });
 
    return rfq;
  }
 
  async getRfqs(tenantId: string): Promise<RequestForQuotation[]> {
    return this.rfqModel.find({ tenantId }).exec();
  }
 
  async getRfqById(tenantId: string, id: string): Promise<RequestForQuotation> {
    const rfq = await this.rfqModel.findOne({ _id: id, tenantId }).exec();
    if (!rfq) throw new NotFoundException('RFQ not found');
    return rfq;
  }
 
  async submitQuotation(tenantId: string, data: any, userId: string): Promise<VendorQuotation> {
    const rfq = await this.rfqModel.findOne({ _id: data.rfqId, tenantId }).exec();
    if (!rfq) throw new NotFoundException('RFQ not found');
 
    // Deadline check
    if (new Date() > new Date(rfq.deadlineDate)) {
      throw new BadRequestException('Quotation submission deadline has passed');
    }
 
    const subtotal = data.items.reduce((sum: number, item: any) => sum + (item.quantity * item.unitPrice), 0);
    const tax = subtotal * 0.1; // Sourcing calculation base defaults to basic or resolved
    const totalAmount = subtotal + tax;
 
    const quotation = await this.quotationModel.create({
      ...data,
      tenantId,
      subtotal,
      tax,
      totalAmount,
      status: 'submitted',
    });
 
    await this.eventBus.publish('procurement.quotation.submitted.v1', {
      quotationId: (quotation as any)._id.toString(),
      tenantId,
      rfqId: data.rfqId,
      totalAmount,
    }, tenantId);
 
    await this.auditLog.log({
      tenantId,
      userId,
      action: 'SUBMIT_QUOTATION',
      resource: 'VendorQuotation',
      resourceId: (quotation as any)._id.toString(),
      newValues: { rfqId: data.rfqId, totalAmount },
    });
 
    return quotation;
  }
 
  async getQuotationsForRfq(tenantId: string, rfqId: string, userId: string): Promise<VendorQuotation[]> {
    const rfq = await this.rfqModel.findOne({ _id: rfqId, tenantId }).exec();
    if (!rfq) throw new NotFoundException('RFQ not found');
 
    // Confidentiality Check: Sealed bidding checks
    if (rfq.sealedBidEnabled && new Date() < new Date(rfq.deadlineDate)) {
      throw new BadRequestException('Bids are currently sealed until deadline is reached');
    }
 
    return this.quotationModel.find({ tenantId, rfqId }).exec();
  }
 
  async evaluateQuotation(
    tenantId: string,
    id: string,
    scores: { technicalScore: number; commercialScore: number },
    userId: string
  ): Promise<VendorQuotation> {
    const quotation = await this.quotationModel.findOneAndUpdate(
      { _id: id, tenantId },
      { $set: scores },
      { new: true }
    ).exec();
    if (!quotation) throw new NotFoundException('Quotation not found');
 
    await this.auditLog.log({
      tenantId,
      userId,
      action: 'EVALUATE_QUOTATION',
      resource: 'VendorQuotation',
      resourceId: id,
      newValues: scores,
    });
 
    return quotation;
  }
 
  async awardQuotation(tenantId: string, id: string, userId: string): Promise<VendorQuotation> {
    const quotation = await this.quotationModel.findOneAndUpdate(
      { _id: id, tenantId },
      { status: 'awarded' },
      { new: true }
    ).exec();
    if (!quotation) throw new NotFoundException('Quotation not found');
 
    // Reject other competitor quotations for this RFQ
    await this.quotationModel.updateMany(
      { tenantId, rfqId: quotation.rfqId, _id: { $ne: id } },
      { status: 'rejected' }
    ).exec();
 
    await this.eventBus.publish('procurement.award.approved.v1', {
      quotationId: id,
      tenantId,
      rfqId: quotation.rfqId,
      vendorId: quotation.vendorId,
    }, tenantId);
 
    await this.auditLog.log({
      tenantId,
      userId,
      action: 'AWARD_QUOTATION',
      resource: 'VendorQuotation',
      resourceId: id,
      newValues: { status: 'awarded', rfqId: quotation.rfqId },
    });
 
    return quotation;
  }
}