Files
stock-manager/src/use-cases/asset.use-cases.ts
T

520 lines
13 KiB
TypeScript

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<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: 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<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?.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<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,
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
}
}