516 lines
13 KiB
TypeScript
516 lines
13 KiB
TypeScript
import {
|
|
type Assignment,
|
|
type ItemStatus,
|
|
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"
|
|
|
|
type FieldErrors = Record<string, string[]>
|
|
|
|
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: ItemStatus
|
|
nextStatus: ItemStatus
|
|
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?.recipientId !== nextPersonId,
|
|
}
|
|
}
|
|
|
|
export async function createAssetUseCase(
|
|
input: CreateAssetUseCaseInput,
|
|
): Promise<CreateAssetUseCaseResult> {
|
|
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?.recipientId || 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<UpdateAssetUseCaseResult> {
|
|
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, 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.recipientId || 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.recipientId || 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,
|
|
recipientId: transition.nextPersonId,
|
|
quantity: 1,
|
|
returnDate: null,
|
|
},
|
|
tx,
|
|
)
|
|
} else {
|
|
await AssignmentService.update(
|
|
activeAssignment.id,
|
|
{
|
|
itemId,
|
|
assetId: id,
|
|
recipientId: 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
|
|
}
|
|
}
|