feat(people): align people, users, and categories with active inventory records

This commit is contained in:
2026-06-19 17:14:22 +02:00
parent c123170a46
commit 7b8a415c6a
9 changed files with 165 additions and 104 deletions
+18 -5
View File
@@ -3,9 +3,14 @@ import { paginate } from "@/lib/paginate"
import prisma from "@/lib/prisma" import prisma from "@/lib/prisma"
import type { Category, CategorySummary, CategoryWithItemsCount } from "@/types" import type { Category, CategorySummary, CategoryWithItemsCount } from "@/types"
const activeRecordWhere = {
deletedAt: null,
} as const
export const CategoryService = { export const CategoryService = {
findAll: async (): Promise<CategorySummary[]> => { findAll: async (): Promise<CategorySummary[]> => {
return prisma.category.findMany({ return prisma.category.findMany({
where: activeRecordWhere,
select: { select: {
id: true, id: true,
name: true, name: true,
@@ -15,6 +20,7 @@ export const CategoryService = {
findAllWithItemsCount: async (): Promise<CategoryWithItemsCount[]> => { findAllWithItemsCount: async (): Promise<CategoryWithItemsCount[]> => {
return prisma.category.findMany({ return prisma.category.findMany({
where: activeRecordWhere,
select: { select: {
id: true, id: true,
name: true, name: true,
@@ -38,6 +44,7 @@ export const CategoryService = {
page, page,
pageSize, pageSize,
where: { where: {
...activeRecordWhere,
...(search ...(search
? { ? {
name: { contains: search, mode: "insensitive" }, name: { contains: search, mode: "insensitive" },
@@ -58,7 +65,10 @@ export const CategoryService = {
db: Prisma.TransactionClient | typeof prisma = prisma, db: Prisma.TransactionClient | typeof prisma = prisma,
): Promise<Category | null> => { ): Promise<Category | null> => {
return db.category.findFirst({ return db.category.findFirst({
where: { name: { equals: name, mode: "insensitive" } }, where: {
name: { equals: name, mode: "insensitive" },
...activeRecordWhere,
},
}) })
}, },
@@ -66,15 +76,15 @@ export const CategoryService = {
id: string, id: string,
db: Prisma.TransactionClient | typeof prisma = prisma, db: Prisma.TransactionClient | typeof prisma = prisma,
): Promise<Category | null> => { ): Promise<Category | null> => {
return db.category.findUnique({ where: { id } }) return db.category.findFirst({ where: { id, ...activeRecordWhere } })
}, },
findByIdWithItemsCount: async ( findByIdWithItemsCount: async (
id: string, id: string,
db: Prisma.TransactionClient | typeof prisma = prisma, db: Prisma.TransactionClient | typeof prisma = prisma,
): Promise<CategoryWithItemsCount | null> => { ): Promise<CategoryWithItemsCount | null> => {
return db.category.findUnique({ return db.category.findFirst({
where: { id }, where: { id, ...activeRecordWhere },
select: { select: {
id: true, id: true,
name: true, name: true,
@@ -102,6 +112,9 @@ export const CategoryService = {
id: string, id: string,
db: Prisma.TransactionClient | typeof prisma = prisma, db: Prisma.TransactionClient | typeof prisma = prisma,
): Promise<Category> => { ): Promise<Category> => {
return db.category.delete({ where: { id } }) return db.category.update({
where: { id },
data: { deletedAt: new Date() },
})
}, },
} }
+11 -5
View File
@@ -18,6 +18,10 @@ const personWithUserSelect = {
}, },
} as const } as const
const activeRecordWhere = {
deletedAt: null,
} as const
export type PersonWithUser = Prisma.PersonGetPayload< export type PersonWithUser = Prisma.PersonGetPayload<
typeof personWithUserSelect typeof personWithUserSelect
> >
@@ -25,6 +29,7 @@ export type PersonWithUser = Prisma.PersonGetPayload<
export const PersonService = { export const PersonService = {
findAll: async (): Promise<Person[]> => { findAll: async (): Promise<Person[]> => {
return prisma.person.findMany({ return prisma.person.findMany({
where: activeRecordWhere,
orderBy: { orderBy: {
firstName: "asc", firstName: "asc",
}, },
@@ -45,6 +50,7 @@ export const PersonService = {
pageSize, pageSize,
include: personWithUserSelect.include, include: personWithUserSelect.include,
where: { where: {
...activeRecordWhere,
...(search ...(search
? { ? {
OR: [ OR: [
@@ -58,22 +64,22 @@ export const PersonService = {
}) })
}, },
findAllPeopleCount: async (): Promise<number> => { findAllPeopleCount: async (): Promise<number> => {
return prisma.person.count() return prisma.person.count({ where: activeRecordWhere })
}, },
findById: async ( findById: async (
id: string, id: string,
db: Prisma.TransactionClient | typeof prisma = prisma, db: Prisma.TransactionClient | typeof prisma = prisma,
): Promise<Person | null> => { ): Promise<Person | null> => {
return db.person.findUnique({ where: { id } }) return db.person.findFirst({ where: { id, ...activeRecordWhere } })
}, },
findByIdWithUser: async ( findByIdWithUser: async (
id: string, id: string,
db: Prisma.TransactionClient | typeof prisma = prisma, db: Prisma.TransactionClient | typeof prisma = prisma,
): Promise<PersonWithUser | null> => { ): Promise<PersonWithUser | null> => {
return db.person.findUnique({ return db.person.findFirst({
where: { id }, where: { id, ...activeRecordWhere },
include: personWithUserSelect.include, include: personWithUserSelect.include,
}) })
}, },
@@ -82,7 +88,7 @@ export const PersonService = {
email: string, email: string,
db: Prisma.TransactionClient | typeof prisma = prisma, db: Prisma.TransactionClient | typeof prisma = prisma,
): Promise<Person | null> => { ): Promise<Person | null> => {
return db.person.findFirst({ where: { email } }) return db.person.findFirst({ where: { email, ...activeRecordWhere } })
}, },
create: async ( create: async (
+14 -8
View File
@@ -43,6 +43,10 @@ type GetUsersParams = {
isActive?: boolean isActive?: boolean
} }
const activeRecordWhere = {
deletedAt: null,
} as const
function userStatusFromActive(isActive: boolean | undefined) { function userStatusFromActive(isActive: boolean | undefined) {
if (typeof isActive !== "boolean") return undefined if (typeof isActive !== "boolean") return undefined
return isActive ? UserStatus.ACTIVE : UserStatus.DISABLED return isActive ? UserStatus.ACTIVE : UserStatus.DISABLED
@@ -85,8 +89,8 @@ export async function getUserById(
id: string, id: string,
db: Prisma.TransactionClient | typeof prisma = prisma, db: Prisma.TransactionClient | typeof prisma = prisma,
) { ) {
const user = await db.user.findUnique({ const user = await db.user.findFirst({
where: { id }, where: { id, ...activeRecordWhere },
select: userWithoutPasswordSelect, select: userWithoutPasswordSelect,
}) })
@@ -96,8 +100,8 @@ export async function getUserById(
export async function getUserProfileById( export async function getUserProfileById(
id: string, id: string,
): Promise<UserWithoutPassword | null> { ): Promise<UserWithoutPassword | null> {
const user = await prisma.user.findUnique({ const user = await prisma.user.findFirst({
where: { id }, where: { id, ...activeRecordWhere },
select: userWithoutPasswordSelect, select: userWithoutPasswordSelect,
}) })
@@ -108,8 +112,8 @@ export async function getUserByEmail(
email: string, email: string,
db: Prisma.TransactionClient | typeof prisma = prisma, db: Prisma.TransactionClient | typeof prisma = prisma,
) { ) {
const user = await db.user.findUnique({ const user = await db.user.findFirst({
where: { emailNormalized: normalizeEmail(email) }, where: { emailNormalized: normalizeEmail(email), ...activeRecordWhere },
select: userWithoutPasswordSelect, select: userWithoutPasswordSelect,
}) })
@@ -120,8 +124,8 @@ export async function getUserCredentialsByEmail(
email: string, email: string,
db: Prisma.TransactionClient | typeof prisma = prisma, db: Prisma.TransactionClient | typeof prisma = prisma,
) { ) {
return await db.user.findUnique({ return await db.user.findFirst({
where: { emailNormalized: normalizeEmail(email) }, where: { emailNormalized: normalizeEmail(email), ...activeRecordWhere },
select: { select: {
id: true, id: true,
name: true, name: true,
@@ -145,6 +149,7 @@ export async function getUsers({
page, page,
pageSize, pageSize,
where: { where: {
...activeRecordWhere,
...(typeof isActive === "boolean" ...(typeof isActive === "boolean"
? { status: userStatusFromActive(isActive) } ? { status: userStatusFromActive(isActive) }
: {}), : {}),
@@ -231,6 +236,7 @@ export async function countActiveAdmins(
): Promise<number> { ): Promise<number> {
return db.user.count({ return db.user.count({
where: { where: {
...activeRecordWhere,
role: UserRole.ADMIN, role: UserRole.ADMIN,
status: UserStatus.ACTIVE, status: UserStatus.ACTIVE,
}, },
+1 -4
View File
@@ -248,10 +248,7 @@ export async function updatePersonUserUseCase(
try { try {
return await prisma.$transaction(async (tx) => { return await prisma.$transaction(async (tx) => {
const existing = await tx.person.findUnique({ const existing = await PersonService.findById(id, tx)
where: { id },
select: { id: true, userId: true },
})
if (!existing) { if (!existing) {
return personError({ id: ["Person not found"] }) return personError({ id: ["Person not found"] })
@@ -11,17 +11,20 @@ let prisma: PrismaClient
let createCategoryUseCase: typeof import("@/use-cases/category.use-cases").createCategoryUseCase let createCategoryUseCase: typeof import("@/use-cases/category.use-cases").createCategoryUseCase
let updateCategoryUseCase: typeof import("@/use-cases/category.use-cases").updateCategoryUseCase let updateCategoryUseCase: typeof import("@/use-cases/category.use-cases").updateCategoryUseCase
let deleteCategoryUseCase: typeof import("@/use-cases/category.use-cases").deleteCategoryUseCase let deleteCategoryUseCase: typeof import("@/use-cases/category.use-cases").deleteCategoryUseCase
let CategoryService: typeof import("@/services/category.service").CategoryService
beforeAll(async () => { beforeAll(async () => {
await startIntegrationTestDatabase() await startIntegrationTestDatabase()
const prismaModule = await import("@/lib/prisma") const prismaModule = await import("@/lib/prisma")
const categoryUseCases = await import("@/use-cases/category.use-cases") const categoryUseCases = await import("@/use-cases/category.use-cases")
const categoryService = await import("@/services/category.service")
prisma = prismaModule.prisma prisma = prismaModule.prisma
createCategoryUseCase = categoryUseCases.createCategoryUseCase createCategoryUseCase = categoryUseCases.createCategoryUseCase
updateCategoryUseCase = categoryUseCases.updateCategoryUseCase updateCategoryUseCase = categoryUseCases.updateCategoryUseCase
deleteCategoryUseCase = categoryUseCases.deleteCategoryUseCase deleteCategoryUseCase = categoryUseCases.deleteCategoryUseCase
CategoryService = categoryService.CategoryService
}) })
beforeEach(async () => { beforeEach(async () => {
@@ -35,77 +38,83 @@ afterAll(async () => {
describe("category use-cases", () => { describe("category use-cases", () => {
it("creates a category and rejects duplicate names", async () => { it("creates a category and rejects duplicate names", async () => {
await expect(createCategoryUseCase({ name: "Hardware" })).resolves.toEqual({ expect(await createCategoryUseCase({ name: "Hardware" })).toEqual({
success: true, success: true,
}) })
await expect( expect(
prisma.category.findUniqueOrThrow({ where: { name: "Hardware" } }), await prisma.category.findUniqueOrThrow({ where: { name: "Hardware" } }),
).resolves.toMatchObject({ name: "Hardware" }) ).toMatchObject({ name: "Hardware" })
await expect(createCategoryUseCase({ name: "Hardware" })).resolves.toEqual({ expect(await createCategoryUseCase({ name: "Hardware" })).toEqual({
success: false, success: false,
errors: { name: ["Category already exists"] }, errors: { name: ["Category already exists"] },
}) })
await expect(prisma.category.count()).resolves.toBe(1) expect(await prisma.category.count()).toBe(1)
}) })
it("updates a category and rejects unchanged or duplicate names", async () => { it("updates a category and rejects unchanged or duplicate names", async () => {
const category = await createTestCategory(prisma, { name: "Peripherals" }) const category = await createTestCategory(prisma, { name: "Peripherals" })
const other = await createTestCategory(prisma, { name: "Networking" }) const other = await createTestCategory(prisma, { name: "Networking" })
await expect( expect(
updateCategoryUseCase({ id: category.id, name: "Accessories" }), await updateCategoryUseCase({ id: category.id, name: "Accessories" }),
).resolves.toEqual({ success: true }) ).toEqual({ success: true })
await expect( expect(
prisma.category.findUniqueOrThrow({ where: { id: category.id } }), await prisma.category.findUniqueOrThrow({ where: { id: category.id } }),
).resolves.toMatchObject({ name: "Accessories" }) ).toMatchObject({ name: "Accessories" })
await expect( expect(
updateCategoryUseCase({ id: category.id, name: "Accessories" }), await updateCategoryUseCase({ id: category.id, name: "Accessories" }),
).resolves.toEqual({ ).toEqual({
success: false, success: false,
errors: { name: ["Category name is the same as the old one"] }, errors: { name: ["Category name is the same as the old one"] },
}) })
await expect( expect(
updateCategoryUseCase({ id: category.id, name: other.name }), await updateCategoryUseCase({ id: category.id, name: other.name }),
).resolves.toEqual({ ).toEqual({
success: false, success: false,
errors: { name: ["Category already exists"] }, errors: { name: ["Category already exists"] },
}) })
await expect( expect(
prisma.category.findUniqueOrThrow({ where: { id: category.id } }), await prisma.category.findUniqueOrThrow({ where: { id: category.id } }),
).resolves.toMatchObject({ name: "Accessories" }) ).toMatchObject({ name: "Accessories" })
}) })
it("deletes empty categories and blocks deleting categories with items", async () => { it("soft deletes empty categories and keeps deleted records out of active lists", async () => {
const categoryWithItems = await createTestCategory(prisma, { const categoryWithItems = await createTestCategory(prisma, {
name: "Computers", name: "Computers",
}) })
await createTestItem(prisma, { categoryId: categoryWithItems.id }) await createTestItem(prisma, { categoryId: categoryWithItems.id })
await expect(deleteCategoryUseCase(categoryWithItems.id)).resolves.toEqual({ expect(await deleteCategoryUseCase(categoryWithItems.id)).toEqual({
success: false, success: false,
errors: { id: ["Category has items"] }, errors: { id: ["Category has items"] },
}) })
await expect( expect(
prisma.category.findUnique({ where: { id: categoryWithItems.id } }), await prisma.category.findUnique({ where: { id: categoryWithItems.id } }),
).resolves.not.toBeNull() ).not.toBeNull()
await expect(prisma.item.count()).resolves.toBe(1) expect(await prisma.item.count()).toBe(1)
const emptyCategory = await createTestCategory(prisma, { name: "Cables" }) const emptyCategory = await createTestCategory(prisma, { name: "Cables" })
await expect(deleteCategoryUseCase(emptyCategory.id)).resolves.toEqual({ expect(await deleteCategoryUseCase(emptyCategory.id)).toEqual({
success: true, success: true,
}) })
await expect( const deletedCategory = await prisma.category.findUnique({
prisma.category.findUnique({ where: { id: emptyCategory.id } }), where: { id: emptyCategory.id },
).resolves.toBeNull() })
expect(deletedCategory).not.toBeNull()
expect(deletedCategory?.deletedAt).toBeInstanceOf(Date)
const activeCategories = await CategoryService.findAll()
expect(activeCategories).toHaveLength(1)
expect(activeCategories[0].id).toBe(categoryWithItems.id)
}) })
}) })
@@ -58,11 +58,11 @@ describe("createPersonUserUseCase", () => {
}) })
// No User record created // No User record created
await expect( expect(
prisma.user.findUnique({ await prisma.user.findUnique({
where: { emailNormalized: normalizeEmail("john@example.test") }, where: { emailNormalized: normalizeEmail("john@example.test") },
}), }),
).resolves.toBeNull() ).toBeNull()
}) })
it("creates a Person with null email when not providing email and role is NO_USER", async () => { it("creates a Person with null email when not providing email and role is NO_USER", async () => {
@@ -196,9 +196,9 @@ describe("createPersonUserUseCase", () => {
if (!user.passwordHash) throw new Error("Expected password hash") if (!user.passwordHash) throw new Error("Expected password hash")
const { verifyPassword } = await import("@/lib/security") const { verifyPassword } = await import("@/lib/security")
await expect( expect(await verifyPassword("plaintext-password", user.passwordHash)).toBe(
verifyPassword("plaintext-password", user.passwordHash), true,
).resolves.toBe(true) )
}) })
}) })
@@ -221,7 +221,7 @@ describe("createPersonUserUseCase", () => {
errors: { email: ["Email already exists"] }, errors: { email: ["Email already exists"] },
}) })
await expect(prisma.person.count()).resolves.toBe(1) expect(await prisma.person.count()).toBe(1)
}) })
it("rejects submission when email already exists in User table", async () => { it("rejects submission when email already exists in User table", async () => {
@@ -244,8 +244,8 @@ describe("createPersonUserUseCase", () => {
}) })
// No new Person or User was created // No new Person or User was created
await expect(prisma.person.count()).resolves.toBe(0) expect(await prisma.person.count()).toBe(0)
await expect(prisma.user.count()).resolves.toBe(1) expect(await prisma.user.count()).toBe(1)
}) })
it("accepts submission when email is unique across both tables", async () => { it("accepts submission when email is unique across both tables", async () => {
@@ -10,16 +10,19 @@ import {
let prisma: PrismaClient let prisma: PrismaClient
let createPersonUseCase: typeof import("@/use-cases/person.use-cases").createPersonUseCase let createPersonUseCase: typeof import("@/use-cases/person.use-cases").createPersonUseCase
let updatePersonUseCase: typeof import("@/use-cases/person.use-cases").updatePersonUseCase let updatePersonUseCase: typeof import("@/use-cases/person.use-cases").updatePersonUseCase
let PersonService: typeof import("@/services/person.service").PersonService
beforeAll(async () => { beforeAll(async () => {
await startIntegrationTestDatabase() await startIntegrationTestDatabase()
const prismaModule = await import("@/lib/prisma") const prismaModule = await import("@/lib/prisma")
const personUseCases = await import("@/use-cases/person.use-cases") const personUseCases = await import("@/use-cases/person.use-cases")
const personService = await import("@/services/person.service")
prisma = prismaModule.prisma prisma = prismaModule.prisma
createPersonUseCase = personUseCases.createPersonUseCase createPersonUseCase = personUseCases.createPersonUseCase
updatePersonUseCase = personUseCases.updatePersonUseCase updatePersonUseCase = personUseCases.updatePersonUseCase
PersonService = personService.PersonService
}) })
beforeEach(async () => { beforeEach(async () => {
@@ -43,11 +46,11 @@ describe("person use-cases", () => {
}), }),
).resolves.toEqual({ success: true }) ).resolves.toEqual({ success: true })
await expect( expect(
prisma.person.findFirstOrThrow({ await prisma.person.findFirstOrThrow({
where: { firstName: "Person", lastName: "One" }, where: { firstName: "Person", lastName: "One" },
}), }),
).resolves.toMatchObject({ ).toMatchObject({
firstName: "Person", firstName: "Person",
lastName: "One", lastName: "One",
department: "IT", department: "IT",
@@ -71,11 +74,11 @@ describe("person use-cases", () => {
}), }),
).resolves.toEqual({ success: true }) ).resolves.toEqual({ success: true })
await expect( expect(
prisma.person.findFirstOrThrow({ await prisma.person.findFirstOrThrow({
where: { firstName: "Linked" }, where: { firstName: "Linked" },
}), }),
).resolves.toMatchObject({ ).toMatchObject({
firstName: "Linked", firstName: "Linked",
lastName: "Person", lastName: "Person",
department: "ENGINEERING", department: "ENGINEERING",
@@ -102,7 +105,7 @@ describe("person use-cases", () => {
errors: { email: ["Email already exists"] }, errors: { email: ["Email already exists"] },
}) })
await expect(prisma.person.count()).resolves.toBe(1) expect(await prisma.person.count()).toBe(1)
}) })
it("updates a person and rejects duplicate emails", async () => { it("updates a person and rejects duplicate emails", async () => {
@@ -125,9 +128,7 @@ describe("person use-cases", () => {
}), }),
).resolves.toEqual({ success: true }) ).resolves.toEqual({ success: true })
await expect( expect(await prisma.person.findUniqueOrThrow({ where: { id: person.id } })).toMatchObject({
prisma.person.findUniqueOrThrow({ where: { id: person.id } }),
).resolves.toMatchObject({
firstName: "Edited", firstName: "Edited",
lastName: "Person", lastName: "Person",
department: "ENGINEERING", department: "ENGINEERING",
@@ -149,12 +150,10 @@ describe("person use-cases", () => {
errors: { email: ["Email already exists"] }, errors: { email: ["Email already exists"] },
}) })
await expect( expect(await prisma.person.findUniqueOrThrow({ where: { id: person.id } })).toMatchObject({
prisma.person.findUniqueOrThrow({ where: { id: person.id } }),
).resolves.toMatchObject({
email: "edited@example.test", email: "edited@example.test",
}) })
await expect(prisma.person.count()).resolves.toBe(2) expect(await prisma.person.count()).toBe(2)
}) })
it("searches by email and name in paginated results", async () => { it("searches by email and name in paginated results", async () => {
@@ -163,13 +162,15 @@ describe("person use-cases", () => {
lastName: "Smith", lastName: "Smith",
email: "alice@company.com", email: "alice@company.com",
}) })
await createTestPerson(prisma, { const archivedPerson = await createTestPerson(prisma, {
firstName: "Bob", firstName: "Archive",
lastName: "Jones", lastName: "Person",
email: "bob@other.com", email: "archive@company.com",
})
await prisma.person.update({
where: { id: archivedPerson.id },
data: { deletedAt: new Date() },
}) })
const { PersonService } = await import("@/services/person.service")
const emailResults = await PersonService.findAllPaginated({ const emailResults = await PersonService.findAllPaginated({
search: "company", search: "company",
@@ -178,13 +179,16 @@ describe("person use-cases", () => {
}) })
expect(emailResults.data).toHaveLength(1) expect(emailResults.data).toHaveLength(1)
expect(emailResults.data[0].firstName).toBe("Alice") expect(emailResults.data[0].firstName).toBe("Alice")
expect(await PersonService.findAllPeopleCount()).toBe(1)
expect(await PersonService.findById(archivedPerson.id)).toBeNull()
const nameResults = await PersonService.findAllPaginated({ const nameResults = await PersonService.findAllPaginated({
search: "Bob", search: "Alice",
page: 1, page: 1,
pageSize: 10, pageSize: 10,
}) })
expect(nameResults.data).toHaveLength(1) expect(nameResults.data).toHaveLength(1)
expect(nameResults.data[0].firstName).toBe("Bob") expect(nameResults.data[0].firstName).toBe("Alice")
}) })
}) })
@@ -213,7 +213,7 @@ describe("updatePersonUserUseCase", () => {
describe("validation errors", () => { describe("validation errors", () => {
it("returns error when person is not found", async () => { it("returns error when person is not found", async () => {
const result = await updatePersonUserUseCase({ const result = await updatePersonUserUseCase({
id: "nonexistent-id", id: "00000000-0000-0000-0000-000000000000",
firstName: "Ghost", firstName: "Ghost",
lastName: "Person", lastName: "Person",
department: "OTHER", department: "OTHER",
@@ -14,6 +14,9 @@ let updateUserUseCase: typeof import("@/use-cases/user.use-cases").updateUserUse
let setUserActiveUseCase: typeof import("@/use-cases/user.use-cases").setUserActiveUseCase let setUserActiveUseCase: typeof import("@/use-cases/user.use-cases").setUserActiveUseCase
let resetUserPasswordUseCase: typeof import("@/use-cases/user.use-cases").resetUserPasswordUseCase let resetUserPasswordUseCase: typeof import("@/use-cases/user.use-cases").resetUserPasswordUseCase
let verifyPassword: typeof import("@/lib/security").verifyPassword let verifyPassword: typeof import("@/lib/security").verifyPassword
let getUserById: typeof import("@/services/user.service").getUserById
let getUsers: typeof import("@/services/user.service").getUsers
let countActiveAdmins: typeof import("@/services/user.service").countActiveAdmins
beforeAll(async () => { beforeAll(async () => {
await startIntegrationTestDatabase() await startIntegrationTestDatabase()
@@ -21,6 +24,7 @@ beforeAll(async () => {
const prismaModule = await import("@/lib/prisma") const prismaModule = await import("@/lib/prisma")
const userUseCases = await import("@/use-cases/user.use-cases") const userUseCases = await import("@/use-cases/user.use-cases")
const security = await import("@/lib/security") const security = await import("@/lib/security")
const userService = await import("@/services/user.service")
prisma = prismaModule.prisma prisma = prismaModule.prisma
createUserUseCase = userUseCases.createUserUseCase createUserUseCase = userUseCases.createUserUseCase
@@ -28,6 +32,9 @@ beforeAll(async () => {
setUserActiveUseCase = userUseCases.setUserActiveUseCase setUserActiveUseCase = userUseCases.setUserActiveUseCase
resetUserPasswordUseCase = userUseCases.resetUserPasswordUseCase resetUserPasswordUseCase = userUseCases.resetUserPasswordUseCase
verifyPassword = security.verifyPassword verifyPassword = security.verifyPassword
getUserById = userService.getUserById
getUsers = userService.getUsers
countActiveAdmins = userService.countActiveAdmins
}) })
beforeEach(async () => { beforeEach(async () => {
@@ -88,12 +95,34 @@ describe("user use-cases", () => {
errors: { email: ["Email already exists"] }, errors: { email: ["Email already exists"] },
}) })
await expect(prisma.user.count()).resolves.toBe(1) expect(await prisma.user.count()).toBe(1)
await expect( expect(
prisma.user.findUniqueOrThrow({ await prisma.user.findUniqueOrThrow({
where: { emailNormalized: normalizeEmail("existing@example.test") }, where: { emailNormalized: normalizeEmail("existing@example.test") },
}), }),
).resolves.toMatchObject({ email: "existing@example.test" }) ).toMatchObject({ email: "existing@example.test" })
})
it("excludes soft-deleted users from active queries", async () => {
const activeUser = await createTestUser(prisma, {
email: "active-user@example.test",
role: "ADMIN",
})
const softDeletedUser = await createTestUser(prisma, {
email: "deleted-user@example.test",
role: "ADMIN",
})
await prisma.user.update({
where: { id: softDeletedUser.id },
data: { deletedAt: new Date() },
})
const users = await getUsers({ page: 1, pageSize: 10 })
expect(users.data).toHaveLength(1)
expect(users.data[0].id).toBe(activeUser.id)
expect(await getUserById(softDeletedUser.id)).toBeNull()
expect(await countActiveAdmins()).toBe(1)
}) })
it("updates a user while preserving uniqueness constraints", async () => { it("updates a user while preserving uniqueness constraints", async () => {
@@ -118,9 +147,7 @@ describe("user use-cases", () => {
}), }),
).resolves.toEqual({ success: true }) ).resolves.toEqual({ success: true })
await expect( expect(await prisma.user.findUniqueOrThrow({ where: { id: user.id } })).toMatchObject({
prisma.user.findUniqueOrThrow({ where: { id: user.id } }),
).resolves.toMatchObject({
name: "Edited User", name: "Edited User",
email: "edited@example.test", email: "edited@example.test",
role: "MANAGER", role: "MANAGER",
@@ -141,9 +168,7 @@ describe("user use-cases", () => {
errors: { email: ["Email already exists"] }, errors: { email: ["Email already exists"] },
}) })
await expect( expect(await prisma.user.findUniqueOrThrow({ where: { id: user.id } })).toMatchObject({
prisma.user.findUniqueOrThrow({ where: { id: user.id } }),
).resolves.toMatchObject({
email: "edited@example.test", email: "edited@example.test",
}) })
}) })
@@ -165,9 +190,10 @@ describe("user use-cases", () => {
errors: { id: ["You cannot remove your own administrator access"] }, errors: { id: ["You cannot remove your own administrator access"] },
}) })
await expect( expect(await prisma.user.findUniqueOrThrow({ where: { id: admin.id } })).toMatchObject({
prisma.user.findUniqueOrThrow({ where: { id: admin.id } }), role: "ADMIN",
).resolves.toMatchObject({ role: "ADMIN", status: "ACTIVE" }) status: "ACTIVE",
})
}) })
it("protects the last active administrator but allows deactivation when another active admin exists", async () => { it("protects the last active administrator but allows deactivation when another active admin exists", async () => {
@@ -206,9 +232,9 @@ describe("user use-cases", () => {
}), }),
).resolves.toEqual({ success: true }) ).resolves.toEqual({ success: true })
await expect( expect(await prisma.user.findUniqueOrThrow({ where: { id: firstAdmin.id } })).toMatchObject({
prisma.user.findUniqueOrThrow({ where: { id: firstAdmin.id } }), status: "DISABLED",
).resolves.toMatchObject({ status: "DISABLED" }) })
}) })
it("prevents self-deactivation", async () => { it("prevents self-deactivation", async () => {
@@ -226,9 +252,9 @@ describe("user use-cases", () => {
errors: { id: ["You cannot deactivate your own user"] }, errors: { id: ["You cannot deactivate your own user"] },
}) })
await expect( expect(await prisma.user.findUniqueOrThrow({ where: { id: admin.id } })).toMatchObject({
prisma.user.findUniqueOrThrow({ where: { id: admin.id } }), status: "ACTIVE",
).resolves.toMatchObject({ status: "ACTIVE" }) })
}) })
it("resets a user password and rejects missing users", async () => { it("resets a user password and rejects missing users", async () => {