import type { Prisma } from "@/generated/prisma/client" import prisma from "@/lib/prisma" import type { CreateAssignmentData, UpdateAssignmentData, } from "@/schemas/assignment.schema" import { AssetService } from "@/services/asset.service" import { AssignmentService, ConcurrentReturnError, } from "@/services/assignment.service" import { ItemService } from "@/services/item.service" import { MovementService } from "@/services/movement.service" import type { Assignment, AssignmentStockLineInput, AssignmentStockReturnInput, } from "@/types" type FieldErrors = Record type CreateAssignmentUseCaseInput = Omit< CreateAssignmentData, "itemId" | "quantity" > & { actorId: string itemId?: string quantity?: number lines?: AssignmentStockLineInput[] } type ReturnAssignmentUseCaseInput = { id: string actorId: string returns?: AssignmentStockReturnInput[] } type UpdateAssignmentUseCaseInput = Omit< UpdateAssignmentData, "itemId" | "quantity" > & { actorId: string itemId?: string quantity?: number lines?: AssignmentStockLineInput[] } type CreateAssignmentUseCaseResult = | { success: true assignmentId: string } | { success: false errors: FieldErrors } type ReturnAssignmentUseCaseResult = | { success: true } | { success: false errors: FieldErrors } type UpdateAssignmentUseCaseResult = | { success: true } | { success: false errors: FieldErrors } function createAssignmentError( errors: FieldErrors, ): CreateAssignmentUseCaseResult { return { success: false, errors, } } function returnAssignmentError( errors: FieldErrors, ): ReturnAssignmentUseCaseResult { return { success: false, errors, } } function updateAssignmentError( errors: FieldErrors, ): UpdateAssignmentUseCaseResult { return { success: false, errors, } } function resolveAssignmentLine( input: | Pick< CreateAssignmentUseCaseInput, "itemId" | "quantity" | "notes" | "lines" > | Pick< UpdateAssignmentUseCaseInput, "itemId" | "quantity" | "notes" | "lines" >, ): AssignmentStockLineInput | null { const providedLine = input.lines?.[0] if (providedLine) { return providedLine } if (!input.itemId || typeof input.quantity !== "number") { return null } return { itemId: input.itemId, quantity: input.quantity, notes: input.notes, } } export type ReassignAssignmentArgs = { oldAssignment: Assignment newPersonId: string newItemId?: string newAssetId?: string newQuantity: number newNotes?: string actorId: string tx: Prisma.TransactionClient } export async function reassignAssignment( args: ReassignAssignmentArgs, ): Promise<{ newAssignment: Assignment }> { const { oldAssignment, newPersonId, newItemId, newAssetId, newQuantity, newNotes, actorId, tx, } = args const returnedQuantity = oldAssignment.quantity ?? 1 // 1. Close old assignment (handles both QUANTITY and SERIALIZED via markReturnedIfActive) await AssignmentService.markReturnedIfActive(oldAssignment.id, actorId, tx) // 2. Mutate item.stock to restore the returned quantity, then write RETURN movement // Skip for SERIALIZED items (stock is constrained to 0 and is not affected by assignments). if (oldAssignment.itemId) { const oldItem = await ItemService.findById(oldAssignment.itemId, tx) if (oldItem && oldItem.trackingType === "QUANTITY") { await ItemService.updateStock(oldAssignment.itemId, returnedQuantity, tx) } } await MovementService.create( { type: "RETURN", quantity: returnedQuantity, itemId: oldAssignment.itemId || undefined, assetId: oldAssignment.assetId || undefined, personId: oldAssignment.personId || undefined, assignmentId: oldAssignment.id, userId: actorId, }, tx, ) // 3. Create new assignment for the new person const newAssignment = await AssignmentService.create( { personId: newPersonId, itemId: newItemId, assetId: newAssetId, quantity: newQuantity, notes: newNotes, assignmentDate: new Date(), createdBy: actorId, }, tx, ) // 4. Mutate item.stock to deduct the new assigned quantity, then write ASSIGNMENT movement if (newItemId) { const newItem = await ItemService.findById(newItemId, tx) if (newItem && newItem.trackingType === "QUANTITY") { await ItemService.updateStock(newItemId, -newQuantity, tx) } } await MovementService.create( { type: "ASSIGNMENT", quantity: newQuantity, itemId: newItemId, assetId: newAssetId, personId: newPersonId, assignmentId: newAssignment.id, userId: actorId, }, tx, ) return { newAssignment } } export async function createAssignmentUseCase( input: CreateAssignmentUseCaseInput, ): Promise { const { actorId, assetId, personId } = input const line = resolveAssignmentLine(input) if (!line || !personId || line.quantity <= 0) { return createAssignmentError({ error: ["Invalid assignment data"] }) } const { itemId, quantity, notes } = line if (!itemId || !personId || quantity <= 0) { return createAssignmentError({ error: ["Invalid assignment data"] }) } return prisma.$transaction(async (tx) => { const item = await ItemService.findById(itemId, tx) if (!item) { return createAssignmentError({ itemId: ["Item not found"] }) } if (item.trackingType === "QUANTITY" && item.stock < quantity) { return createAssignmentError({ quantity: ["Item does not have enough stock"], }) } if (assetId) { const asset = await AssetService.findById(assetId, tx) if (!asset) { return createAssignmentError({ assetId: ["Asset not found"] }) } if (asset.itemId !== item.id) { return createAssignmentError({ assetId: ["Asset does not belong to item"], }) } } if (item.trackingType === "QUANTITY") { const stockWasDecremented = await ItemService.decrementStockIfAvailable( itemId, quantity, tx, ) if (!stockWasDecremented) { return createAssignmentError({ quantity: ["Item does not have enough stock"], }) } } if (assetId) { await AssetService.update( assetId, { status: "ASSIGNED", }, tx, ) } const createdAssignment = await AssignmentService.create( { itemId, assetId: assetId || undefined, quantity, personId, notes: notes ?? input.notes, assignmentDate: input.assignmentDate, createdBy: actorId, }, tx, ) await MovementService.create( { itemId, assetId: assetId || undefined, quantity, type: "ASSIGNMENT", personId, assignmentId: createdAssignment.id, userId: actorId, }, tx, ) return { success: true, assignmentId: createdAssignment.id, } }) } export async function updateAssignmentUseCase( input: UpdateAssignmentUseCaseInput, ): Promise { const { actorId, id, personId, assetId } = input const line = resolveAssignmentLine(input) const itemId = line?.itemId ?? input.itemId const quantity = line?.quantity ?? input.quantity ?? 0 const notes = line?.notes ?? input.notes const assignmentDate = input.assignmentDate if (!id) { return updateAssignmentError({ id: ["Assignment ID is required"] }) } return prisma.$transaction(async (tx) => { const assignment = await AssignmentService.findById(id, tx) if (!assignment) { return updateAssignmentError({ id: ["Assignment not found"] }) } if (assignment.status === "RETURNED" || assignment.status === "CANCELLED") { return updateAssignmentError({ id: ["Assignment is closed"] }) } let item = null if (itemId) { item = await ItemService.findById(itemId, tx) if (!item) { return updateAssignmentError({ itemId: ["Item not found"] }) } } if (assetId) { const asset = await AssetService.findById(assetId, tx) if (!asset) { return updateAssignmentError({ assetId: ["Asset not found"] }) } if (asset.itemId !== itemId) { return updateAssignmentError({ assetId: ["Asset does not belong to item"], }) } } if (assignment.personId !== personId) { await reassignAssignment({ oldAssignment: assignment, newPersonId: personId, newItemId: itemId, newAssetId: assetId, newQuantity: quantity, newNotes: notes, actorId, tx, }) } else { if (item && assignment.quantity !== quantity) { const stockDelta = quantity - (assignment.quantity ?? 0) const adjustmentItemId = item.id await ItemService.updateStock(adjustmentItemId, -stockDelta, tx) await MovementService.create( { type: "ADJUSTMENT", itemId: adjustmentItemId, quantity: Math.abs(stockDelta), stockDeltaSign: stockDelta < 0 ? 1 : -1, userId: actorId, }, tx, ) } await AssignmentService.update( id, { personId: personId, itemId, assetId, quantity, notes, assignmentDate, returnDate: null, }, tx, ) } return { success: true, } }) } export async function returnAssignmentUseCase( input: ReturnAssignmentUseCaseInput, ): Promise { const { id, actorId, returns } = input if (!id) { return returnAssignmentError({ id: ["Assignment ID is required"] }) } try { return await prisma.$transaction(async (tx) => { const assignment = await AssignmentService.findById(id, tx) if (!assignment) { return returnAssignmentError({ id: ["Assignment not found"] }) } if (assignment.returnDate) { return returnAssignmentError({ id: ["Assignment already returned"] }) } const returnResult = await AssignmentService.returnStockAssignment( id, actorId, returns, tx, ) if (!returnResult) { return returnAssignmentError({ error: ["Invalid assignment data"] }) } for (const returnedLine of returnResult.returnedLines) { await ItemService.updateStock( returnedLine.itemId, returnedLine.quantity, tx, ) await MovementService.create( { type: "RETURN", quantity: returnedLine.quantity, itemId: returnedLine.itemId, assetId: assignment.assetId || undefined, assignmentId: id, userId: actorId, }, tx, ) } if (assignment.assetId) { await AssetService.update( assignment.assetId, { status: "AVAILABLE", }, tx, ) } return { success: true, } }) } catch (error) { if (error instanceof ConcurrentReturnError) { return returnAssignmentError({ error: ["errorConcurrent"] }) } throw error } }