All files / src/domains/assets/services asset-disposal.service.ts

0% Statements 0/36
0% Branches 0/27
0% Functions 0/3
0% Lines 0/29

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                                                                                                                                                                             
import { Injectable, BadRequestException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, Types } from 'mongoose';
import { Asset, AssetDisposal } from '../schemas';
import { EventBusService } from '../../../platform/events/event-bus.service';
 
@Injectable()
export class AssetDisposalService {
  constructor(
    @InjectModel(Asset.name)
    private readonly assetModel: Model<Asset>,
    @InjectModel(AssetDisposal.name)
    private readonly disposalModel: Model<AssetDisposal>,
    private readonly eventBus: EventBusService
  ) {}
 
  async disposeAsset(
    tenantId: string,
    assetId: string,
    type: string, // 'Sale' | 'Scrap' | 'Write-off' | 'Donation'
    proceedsMinor: number,
    userId: string
  ): Promise<AssetDisposal> {
    const asset = await this.assetModel.findOne({ _id: assetId, tenantId }).exec();
    if (!asset) throw new BadRequestException('Asset not found');
    if (asset.status === 'disposed' || asset.status === 'sold' || asset.status === 'scrapped') {
      throw new BadRequestException('Asset is already disposed');
    }
 
    const nbv = asset.currentBookValueMinor;
    const gainLoss = proceedsMinor - nbv;
 
    const disposal = new this.disposalModel({
      tenantId,
      assetId: new Types.ObjectId(assetId),
      disposalType: type,
      disposalDate: new Date(),
      proceedsMinor,
      netBookValueMinor: nbv,
      gainLossMinor: gainLoss,
      status: 'pending_posting'
    });
    await disposal.save();
 
    // Trigger Finance Integration Posting Handshake
    await this.eventBus.publish('asset.disposal-posting-ready.v1', {
      disposalId: disposal._id.toString(),
      assetId: asset._id.toString(),
      assetNumber: asset.assetNumber,
      tenantId,
      disposalType: type,
      proceedsMinor,
      netBookValueMinor: nbv,
      gainLossMinor: gainLoss,
      accumulatedDepreciationMinor: asset.accumulatedDepreciationMinor,
      capitalizationCostMinor: asset.capitalizationCostMinor,
      userId
    }, tenantId);
 
    return disposal;
  }
 
  async handleFinanceDisposalPosted(tenantId: string, disposalId: string): Promise<AssetDisposal> {
    const disposal = await this.disposalModel.findOne({ _id: disposalId, tenantId }).exec();
    if (!disposal) throw new Error('Disposal not found');
 
    disposal.status = 'completed';
    await disposal.save();
 
    // Mark asset as retired / disposed
    let finalStatus = 'disposed';
    if (disposal.disposalType === 'Sale') finalStatus = 'sold';
    if (disposal.disposalType === 'Scrap') finalStatus = 'scrapped';
    if (disposal.disposalType === 'Write-off') finalStatus = 'written_off';
 
    await this.assetModel.updateOne(
      { _id: disposal.assetId },
      {
        status: finalStatus,
        currentBookValueMinor: 0 // Book value is released
      }
    );
 
    return disposal;
  }
}