import { type AssetStatus, Prisma, } from "@/generated/prisma/client" import prisma from "@/lib/prisma" import type { CreateAssetFormType, UpdateAssetFormType, } from "@/schemas/asset.schema" import { AssetService } from "@/services/asset.service" import { AssignmentService } from "@/services/assignment.service" import { ItemService } from "@/services/item.service" import { MovementService } from "@/services/movement.service" import type { Assignment } from "@/types" type FieldErrors = Record type CreateAssetUseCaseInput = CreateAssetFormType & { actorId: string } type UpdateAssetUseCaseInput = UpdateAssetFormType & { actorId: string } type CreateAssetUseCaseResult = | { success: true assetId: string } | { success: false errors: FieldErrors } type UpdateAssetUseCaseResult = | { success: true } | { success: false errors: FieldErrors } function createAssetError(errors: FieldErrors): CreateAssetUseCaseResult { return { success: false, errors, } } function updateAssetError(errors: FieldErrors): UpdateAssetUseCaseResult { return { success: false, errors, } } class AssetTransitionError extends Error { constructor(readonly errors: FieldErrors) { super("Asset transition failed") } } type AssetTransitionInput = { previousStatus: AssetStatus nextStatus: AssetStatus previousItemId: string | null nextItemId: string activeAssignment: Assignment | null nextPersonId?: string } function getAssetTransition({ previousStatus, nextStatus, previousItemId, nextItemId, activeAssignment, nextPersonId, }: AssetTransitionInput) { return { previousStatus, nextStatus, previousItemId, nextItemId, statusChanged: previousStatus !== nextStatus, itemChanged: previousItemId !== nextItemId, activeAssignment, nextPersonId, hasPerson: Boolean(nextPersonId), wasAssigned: previousStatus === "ASSIGNED", willBeAssigned: nextStatus === "ASSIGNED", wasAvailable: previousStatus === "AVAILABLE", willBeAvailable: nextStatus === "AVAILABLE", personChanged: activeAssignment?.personId !== nextPersonId, } } export async function createAssetUseCase( input: CreateAssetUseCaseInput, ): Promise { const { actorId, itemId, serialNumber, deliveryNote, status, notes, personId, } = input try { return await prisma.$transaction(async (tx) => { const item = await ItemService.findByIdWithCategory(itemId, tx) if (!item) { return createAssetError({ itemId: ["Item not found"] }) } const existentAsset = await AssetService.findBySerialNumber( serialNumber, tx, ) if (existentAsset) { return createAssetError({ serialNumber: ["This serial number already exists"], }) } const newAsset = await AssetService.create( { item: { connect: { id: itemId } }, serialNumber, deliveryNote, status, notes, }, tx, ) const createdAssignment = status === "ASSIGNED" && personId ? await AssignmentService.create( { notes: "", itemId, assetId: newAsset.id, quantity: 1, personId, assignmentDate: new Date(), createdBy: actorId, }, tx, ) : null await MovementService.create( { itemId, assetId: newAsset.id, quantity: 1, type: status === "ASSIGNED" ? "ASSIGNMENT" : "IN", personId: createdAssignment?.personId || undefined, assignmentId: createdAssignment?.id, userId: actorId, }, tx, ) if (status === "AVAILABLE") { await ItemService.updateStock(itemId, 1, tx) } return { success: true, assetId: newAsset.id, } }) } catch (error) { if ( error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002" ) { return createAssetError({ serialNumber: ["This serial number already exists"], }) } throw error } } export async function updateAssetUseCase( input: UpdateAssetUseCaseInput, ): Promise { const { actorId, id, itemId, serialNumber, deliveryNote, status, notes, personId, } = input try { return await prisma.$transaction(async (tx) => { const item = await ItemService.findByIdWithCategory(itemId, tx) if (!item) { return updateAssetError({ itemId: ["Item not found"] }) } const currentAsset = await AssetService.findById(id, tx) if (!currentAsset) { return updateAssetError({ id: ["Asset not found"] }) } const transition = getAssetTransition({ previousStatus: currentAsset.status, nextStatus: status, previousItemId: currentAsset.itemId, nextItemId: itemId, activeAssignment: currentAsset.assignment, nextPersonId: personId, }) const existentAsset = await AssetService.findBySerialNumber( serialNumber, tx, ) if ( existentAsset && id !== existentAsset.id && existentAsset.serialNumber === serialNumber ) { return updateAssetError({ serialNumber: ["This serial number already exists"], }) } await AssetService.update( id, { item: { connect: { id: itemId } }, serialNumber, deliveryNote, status, notes, }, tx, ) let closedActiveAssignment = false if (transition.activeAssignment && !transition.willBeAssigned) { const activeAssignment = transition.activeAssignment const assignmentWasReturned = await AssignmentService.markReturnedIfActive( activeAssignment.id, actorId, tx, ) if (!assignmentWasReturned) { throw new AssetTransitionError({ id: ["Assignment already returned"], }) } await MovementService.create( { type: "RETURN", quantity: activeAssignment.quantity || 1, itemId: transition.willBeAvailable ? transition.nextItemId : activeAssignment.itemId || undefined, assetId: activeAssignment.assetId || undefined, personId: activeAssignment.personId || undefined, assignmentId: activeAssignment.id, userId: actorId, }, tx, ) closedActiveAssignment = true } const shouldIncrementNextItemStock = transition.willBeAvailable && (!transition.wasAvailable || transition.itemChanged) const shouldDecrementPreviousItemStock = transition.wasAvailable && (!transition.willBeAvailable || transition.itemChanged) if ( transition.statusChanged && !transition.hasPerson && !closedActiveAssignment ) { const statusMovementItemId = transition.willBeAssigned ? transition.nextItemId : shouldDecrementPreviousItemStock && !shouldIncrementNextItemStock && transition.previousItemId ? transition.previousItemId : transition.nextItemId await MovementService.create( { itemId: statusMovementItemId, assetId: id, quantity: 1, type: transition.willBeAssigned ? "ASSIGNMENT" : transition.nextStatus === "AVAILABLE" ? "IN" : "ADJUSTMENT", details: `Status changed from ${transition.previousStatus} to ${transition.nextStatus}`, userId: actorId, }, tx, ) } if ( transition.itemChanged && transition.wasAvailable && transition.willBeAvailable ) { if (!transition.previousItemId) { throw new AssetTransitionError({ itemId: ["Previous item not found for available asset"], }) } await MovementService.create( { itemId: transition.previousItemId, assetId: id, quantity: 1, type: "OUT", details: `Asset moved from item ${transition.previousItemId} to ${transition.nextItemId}`, userId: actorId, }, tx, ) await MovementService.create( { itemId: transition.nextItemId, assetId: id, quantity: 1, type: "IN", details: `Asset moved from item ${transition.previousItemId} to ${transition.nextItemId}`, userId: actorId, }, tx, ) } if ( transition.itemChanged && transition.wasAvailable && transition.willBeAssigned ) { if (!transition.previousItemId) { throw new AssetTransitionError({ itemId: ["Previous item not found for available asset"], }) } await MovementService.create( { itemId: transition.previousItemId, assetId: id, quantity: 1, type: "OUT", details: `Asset assigned from item ${transition.previousItemId} to item ${transition.nextItemId}`, userId: actorId, }, tx, ) } if (shouldIncrementNextItemStock) { await ItemService.updateStock(transition.nextItemId, 1, tx) } if (shouldDecrementPreviousItemStock) { if (!transition.previousItemId) { throw new AssetTransitionError({ itemId: ["Previous item not found for available asset"], }) } const stockWasDecremented = await ItemService.decrementStockIfAvailable( transition.previousItemId, 1, tx, ) if (!stockWasDecremented) { throw new AssetTransitionError({ stock: ["Item does not have enough stock"], }) } } if (transition.willBeAssigned && transition.nextPersonId) { const activeAssignment = transition.activeAssignment if (activeAssignment) { if (transition.personChanged) { await MovementService.create( { type: "RETURN", quantity: activeAssignment.quantity || 1, itemId: activeAssignment.itemId || undefined, assetId: activeAssignment.assetId || undefined, personId: activeAssignment.personId || undefined, assignmentId: activeAssignment.id, userId: actorId, }, tx, ) await MovementService.create( { type: "ASSIGNMENT", quantity: 1, itemId, assetId: id, personId: transition.nextPersonId, assignmentId: activeAssignment.id, userId: actorId, }, tx, ) await AssignmentService.update( activeAssignment.id, { createdBy: actorId, itemId, assetId: id, personId: transition.nextPersonId, quantity: 1, returnDate: null, }, tx, ) } else { await AssignmentService.update( activeAssignment.id, { itemId, assetId: id, personId: transition.nextPersonId, quantity: 1, returnDate: null, }, tx, ) } } else { const createdAssignment = await AssignmentService.create( { itemId, assetId: id, quantity: 1, personId: transition.nextPersonId, assignmentDate: new Date(), createdBy: actorId, }, tx, ) await MovementService.create( { type: "ASSIGNMENT", quantity: 1, itemId, assetId: id, personId: transition.nextPersonId, assignmentId: createdAssignment.id, userId: actorId, }, tx, ) } } return { success: true, } }) } catch (error) { if (error instanceof AssetTransitionError) { return updateAssetError(error.errors) } if ( error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002" ) { return updateAssetError({ serialNumber: ["This serial number already exists"], }) } throw error } }