fix architecture and write new tests
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('../../lib/prisma', () => ({
|
||||
prisma: {
|
||||
complaint: { findMany: vi.fn(), count: vi.fn(), findFirst: vi.fn(), create: vi.fn(), update: vi.fn(), delete: vi.fn() },
|
||||
},
|
||||
}))
|
||||
|
||||
import { prisma } from '../../lib/prisma'
|
||||
import * as repo from './complaint.repo'
|
||||
|
||||
describe('complaint.repo edge behavior', () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
|
||||
it('lists complaints with company-scoped filters and deterministic ordering', async () => {
|
||||
vi.mocked(prisma.complaint.findMany).mockResolvedValue([] as never)
|
||||
vi.mocked(prisma.complaint.count).mockResolvedValue(0 as never)
|
||||
|
||||
await repo.findMany('company_1', { status: 'OPEN', severity: 'LEVEL_2' }, 40, 20)
|
||||
|
||||
const where = { companyId: 'company_1', status: 'OPEN', severity: 'LEVEL_2' }
|
||||
expect(prisma.complaint.findMany).toHaveBeenCalledWith(expect.objectContaining({
|
||||
where,
|
||||
skip: 40,
|
||||
take: 20,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
}))
|
||||
expect(prisma.complaint.count).toHaveBeenCalledWith({ where })
|
||||
})
|
||||
|
||||
it('creates complaints with linked reservation, review and customer ids preserved', async () => {
|
||||
await repo.create({
|
||||
companyId: 'company_1',
|
||||
reservationId: 'reservation_1',
|
||||
reviewId: 'review_1',
|
||||
customerId: 'customer_1',
|
||||
severity: 'LEVEL_3',
|
||||
category: 'DAMAGE_CLAIM',
|
||||
subject: 'Damage dispute',
|
||||
assignedTo: 'employee_1',
|
||||
})
|
||||
|
||||
expect(prisma.complaint.create).toHaveBeenCalledWith(expect.objectContaining({
|
||||
data: {
|
||||
companyId: 'company_1',
|
||||
reservationId: 'reservation_1',
|
||||
reviewId: 'review_1',
|
||||
customerId: 'customer_1',
|
||||
severity: 'LEVEL_3',
|
||||
category: 'DAMAGE_CLAIM',
|
||||
subject: 'Damage dispute',
|
||||
assignedTo: 'employee_1',
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
it('updates by complaint id without accepting a company id bypass from caller data', async () => {
|
||||
await repo.updateById('complaint_1', { status: 'RESOLVED', resolution: 'Refunded deposit' })
|
||||
|
||||
expect(prisma.complaint.update).toHaveBeenCalledWith(expect.objectContaining({
|
||||
where: { id: 'complaint_1' },
|
||||
data: { status: 'RESOLVED', resolution: 'Refunded deposit' },
|
||||
}))
|
||||
})
|
||||
|
||||
it('deletes by primary id', async () => {
|
||||
await repo.deleteById('complaint_1')
|
||||
|
||||
expect(prisma.complaint.delete).toHaveBeenCalledWith({ where: { id: 'complaint_1' } })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createSchema, updateSchema, listQuerySchema } from './complaint.schemas'
|
||||
|
||||
describe('complaint schemas', () => {
|
||||
it('defaults new complaints to the lowest severity when not provided', () => {
|
||||
expect(createSchema.parse({ category: 'BILLING', subject: 'Incorrect invoice' })).toMatchObject({
|
||||
category: 'BILLING',
|
||||
subject: 'Incorrect invoice',
|
||||
severity: 'LEVEL_1',
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects empty complaint subjects', () => {
|
||||
expect(() => createSchema.parse({ category: 'BILLING', subject: '' })).toThrow()
|
||||
})
|
||||
|
||||
it('accepts lifecycle updates without requiring immutable creation fields', () => {
|
||||
expect(updateSchema.parse({ status: 'RESOLVED', resolution: 'Refund issued' })).toEqual({
|
||||
status: 'RESOLVED',
|
||||
resolution: 'Refund issued',
|
||||
})
|
||||
})
|
||||
|
||||
it('coerces pagination and applies default page size for list queries', () => {
|
||||
expect(listQuerySchema.parse({ page: '2', severity: 'LEVEL_3' })).toEqual({
|
||||
page: 2,
|
||||
pageSize: 20,
|
||||
severity: 'LEVEL_3',
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects pathological page sizes before they hit the database', () => {
|
||||
expect(() => listQuerySchema.parse({ pageSize: '101' })).toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,86 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('./complaint.repo', () => ({
|
||||
findMany: vi.fn(),
|
||||
findById: vi.fn(),
|
||||
create: vi.fn(),
|
||||
updateById: vi.fn(),
|
||||
deleteById: vi.fn(),
|
||||
}))
|
||||
|
||||
import { NotFoundError } from '../../http/errors'
|
||||
import * as repo from './complaint.repo'
|
||||
import { createComplaint, deleteComplaint, getComplaint, listComplaints, updateComplaint } from './complaint.service'
|
||||
|
||||
describe('complaint.service', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.setSystemTime(new Date('2026-06-08T12:00:00.000Z'))
|
||||
})
|
||||
|
||||
it('builds filtered paginated complaint queries without leaking transport concerns into the repo', async () => {
|
||||
vi.mocked(repo.findMany).mockResolvedValue([[{ id: 'complaint_1' }], 11] as never)
|
||||
|
||||
const result = await listComplaints('company_1', {
|
||||
status: 'OPEN',
|
||||
severity: 'HIGH',
|
||||
category: 'BILLING',
|
||||
page: 3,
|
||||
pageSize: 5,
|
||||
})
|
||||
|
||||
expect(repo.findMany).toHaveBeenCalledWith('company_1', {
|
||||
status: 'OPEN',
|
||||
severity: 'HIGH',
|
||||
category: 'BILLING',
|
||||
}, 10, 5)
|
||||
expect(result).toEqual({
|
||||
data: [{ id: 'complaint_1' }],
|
||||
meta: { total: 11, page: 3, pageSize: 5, totalPages: 3 },
|
||||
})
|
||||
})
|
||||
|
||||
it('throws NotFoundError when a complaint cannot be found in the company tenant', async () => {
|
||||
vi.mocked(repo.findById).mockResolvedValue(null as never)
|
||||
|
||||
await expect(getComplaint('complaint_1', 'company_1')).rejects.toBeInstanceOf(NotFoundError)
|
||||
})
|
||||
|
||||
it('sets resolvedAt exactly when the complaint transitions into RESOLVED', async () => {
|
||||
vi.mocked(repo.findById).mockResolvedValue({ id: 'complaint_1', status: 'OPEN' } as never)
|
||||
vi.mocked(repo.updateById).mockResolvedValue({ id: 'complaint_1', status: 'RESOLVED' } as never)
|
||||
|
||||
await updateComplaint('complaint_1', 'company_1', { status: 'RESOLVED', resolution: 'Refund issued' })
|
||||
|
||||
expect(repo.updateById).toHaveBeenCalledWith('complaint_1', {
|
||||
status: 'RESOLVED',
|
||||
resolution: 'Refund issued',
|
||||
resolvedAt: new Date('2026-06-08T12:00:00.000Z'),
|
||||
})
|
||||
})
|
||||
|
||||
it('does not overwrite resolvedAt when an already resolved complaint is edited', async () => {
|
||||
vi.mocked(repo.findById).mockResolvedValue({ id: 'complaint_1', status: 'RESOLVED' } as never)
|
||||
|
||||
await updateComplaint('complaint_1', 'company_1', { notes: 'Follow-up call logged' })
|
||||
|
||||
expect(repo.updateById).toHaveBeenCalledWith('complaint_1', { notes: 'Follow-up call logged' })
|
||||
})
|
||||
|
||||
it('creates and deletes complaints through tenant-scoped repository calls', async () => {
|
||||
vi.mocked(repo.create).mockResolvedValue({ id: 'complaint_1' } as never)
|
||||
vi.mocked(repo.findById).mockResolvedValue({ id: 'complaint_1' } as never)
|
||||
|
||||
await createComplaint('company_1', {
|
||||
reservationId: 'reservation_1',
|
||||
severity: 'MEDIUM',
|
||||
category: 'SERVICE',
|
||||
subject: 'Late pickup',
|
||||
})
|
||||
const deleted = await deleteComplaint('complaint_1', 'company_1')
|
||||
|
||||
expect(repo.create).toHaveBeenCalledWith(expect.objectContaining({ companyId: 'company_1', reservationId: 'reservation_1' }))
|
||||
expect(repo.deleteById).toHaveBeenCalledWith('complaint_1')
|
||||
expect(deleted).toEqual({ success: true })
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user