Move tests to __tests__ folders, add unit tests for shared utils and services

Restructure tests into __tests__/ directories at package root so they can
be excluded from production builds. Add unit tests for dates, currency,
lookup service, payment method default logic, and tax exemption state
transitions.
This commit is contained in:
Ryan Moon
2026-03-27 21:14:42 -05:00
parent 0a2d6e23af
commit 01d6ff3fa3
10 changed files with 545 additions and 6 deletions

View File

@@ -0,0 +1,491 @@
import { describe, it, expect, beforeAll, beforeEach, afterAll } from 'bun:test'
import type { FastifyInstance } from 'fastify'
import {
createTestApp,
cleanDb,
seedTestCompany,
registerAndLogin,
TEST_COMPANY_ID,
} from '../../../src/test/helpers.js'
describe('Processor link routes', () => {
let app: FastifyInstance
let token: string
let accountId: string
beforeAll(async () => {
app = await createTestApp()
})
beforeEach(async () => {
await cleanDb(app)
await seedTestCompany(app)
const auth = await registerAndLogin(app, { email: `pl-${Date.now()}@test.com` })
token = auth.token
const res = await app.inject({
method: 'POST',
url: '/v1/accounts',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Link Test Account' },
})
accountId = res.json().id
})
afterAll(async () => {
await app.close()
})
it('creates a processor link', async () => {
const res = await app.inject({
method: 'POST',
url: `/v1/accounts/${accountId}/processor-links`,
headers: { authorization: `Bearer ${token}` },
payload: { processor: 'stripe', processorCustomerId: 'cus_test123' },
})
expect(res.statusCode).toBe(201)
expect(res.json().processor).toBe('stripe')
expect(res.json().processorCustomerId).toBe('cus_test123')
expect(res.json().accountId).toBe(accountId)
})
it('lists processor links for an account', async () => {
await app.inject({
method: 'POST',
url: `/v1/accounts/${accountId}/processor-links`,
headers: { authorization: `Bearer ${token}` },
payload: { processor: 'stripe', processorCustomerId: 'cus_1' },
})
await app.inject({
method: 'POST',
url: `/v1/accounts/${accountId}/processor-links`,
headers: { authorization: `Bearer ${token}` },
payload: { processor: 'global_payments', processorCustomerId: 'gp_1' },
})
const res = await app.inject({
method: 'GET',
url: `/v1/accounts/${accountId}/processor-links`,
headers: { authorization: `Bearer ${token}` },
})
expect(res.statusCode).toBe(200)
expect(res.json().data.length).toBe(2)
})
it('updates a processor link', async () => {
const createRes = await app.inject({
method: 'POST',
url: `/v1/accounts/${accountId}/processor-links`,
headers: { authorization: `Bearer ${token}` },
payload: { processor: 'stripe', processorCustomerId: 'cus_1' },
})
const id = createRes.json().id
const res = await app.inject({
method: 'PATCH',
url: `/v1/processor-links/${id}`,
headers: { authorization: `Bearer ${token}` },
payload: { isActive: false },
})
expect(res.statusCode).toBe(200)
expect(res.json().isActive).toBe(false)
})
it('deletes a processor link', async () => {
const createRes = await app.inject({
method: 'POST',
url: `/v1/accounts/${accountId}/processor-links`,
headers: { authorization: `Bearer ${token}` },
payload: { processor: 'stripe', processorCustomerId: 'cus_del' },
})
const id = createRes.json().id
const delRes = await app.inject({
method: 'DELETE',
url: `/v1/processor-links/${id}`,
headers: { authorization: `Bearer ${token}` },
})
expect(delRes.statusCode).toBe(200)
})
})
describe('Payment method routes', () => {
let app: FastifyInstance
let token: string
let accountId: string
beforeAll(async () => {
app = await createTestApp()
})
beforeEach(async () => {
await cleanDb(app)
await seedTestCompany(app)
const auth = await registerAndLogin(app, { email: `pm-${Date.now()}@test.com` })
token = auth.token
const res = await app.inject({
method: 'POST',
url: '/v1/accounts',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Payment Test Account' },
})
accountId = res.json().id
})
afterAll(async () => {
await app.close()
})
it('creates a payment method', async () => {
const res = await app.inject({
method: 'POST',
url: `/v1/accounts/${accountId}/payment-methods`,
headers: { authorization: `Bearer ${token}` },
payload: {
processor: 'stripe',
processorPaymentMethodId: 'pm_test123',
cardBrand: 'visa',
lastFour: '4242',
expMonth: 12,
expYear: 2027,
isDefault: true,
},
})
expect(res.statusCode).toBe(201)
expect(res.json().cardBrand).toBe('visa')
expect(res.json().lastFour).toBe('4242')
expect(res.json().isDefault).toBe(true)
})
it('lists payment methods for an account', async () => {
await app.inject({
method: 'POST',
url: `/v1/accounts/${accountId}/payment-methods`,
headers: { authorization: `Bearer ${token}` },
payload: { processor: 'stripe', processorPaymentMethodId: 'pm_1', lastFour: '1111' },
})
await app.inject({
method: 'POST',
url: `/v1/accounts/${accountId}/payment-methods`,
headers: { authorization: `Bearer ${token}` },
payload: { processor: 'stripe', processorPaymentMethodId: 'pm_2', lastFour: '2222' },
})
const res = await app.inject({
method: 'GET',
url: `/v1/accounts/${accountId}/payment-methods`,
headers: { authorization: `Bearer ${token}` },
})
expect(res.statusCode).toBe(200)
expect(res.json().data.length).toBe(2)
})
it('sets new default and unsets old default', async () => {
const first = await app.inject({
method: 'POST',
url: `/v1/accounts/${accountId}/payment-methods`,
headers: { authorization: `Bearer ${token}` },
payload: { processor: 'stripe', processorPaymentMethodId: 'pm_1', isDefault: true },
})
const firstId = first.json().id
const second = await app.inject({
method: 'POST',
url: `/v1/accounts/${accountId}/payment-methods`,
headers: { authorization: `Bearer ${token}` },
payload: { processor: 'stripe', processorPaymentMethodId: 'pm_2', isDefault: true },
})
expect(second.json().isDefault).toBe(true)
// First should no longer be default
const getFirst = await app.inject({
method: 'GET',
url: `/v1/payment-methods/${firstId}`,
headers: { authorization: `Bearer ${token}` },
})
expect(getFirst.json().isDefault).toBe(false)
})
it('deletes a payment method', async () => {
const createRes = await app.inject({
method: 'POST',
url: `/v1/accounts/${accountId}/payment-methods`,
headers: { authorization: `Bearer ${token}` },
payload: { processor: 'stripe', processorPaymentMethodId: 'pm_del' },
})
const delRes = await app.inject({
method: 'DELETE',
url: `/v1/payment-methods/${createRes.json().id}`,
headers: { authorization: `Bearer ${token}` },
})
expect(delRes.statusCode).toBe(200)
})
})
describe('Tax exemption routes', () => {
let app: FastifyInstance
let token: string
let accountId: string
beforeAll(async () => {
app = await createTestApp()
})
beforeEach(async () => {
await cleanDb(app)
await seedTestCompany(app)
const auth = await registerAndLogin(app, { email: `tax-${Date.now()}@test.com` })
token = auth.token
const res = await app.inject({
method: 'POST',
url: '/v1/accounts',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Tax Test Account' },
})
accountId = res.json().id
})
afterAll(async () => {
await app.close()
})
it('creates a tax exemption', async () => {
const res = await app.inject({
method: 'POST',
url: `/v1/accounts/${accountId}/tax-exemptions`,
headers: { authorization: `Bearer ${token}` },
payload: {
certificateNumber: 'TX-12345',
certificateType: 'resale',
issuingState: 'TX',
expiresAt: '2027-12-31',
},
})
expect(res.statusCode).toBe(201)
expect(res.json().certificateNumber).toBe('TX-12345')
expect(res.json().status).toBe('pending')
expect(res.json().issuingState).toBe('TX')
})
it('lists tax exemptions for an account', async () => {
await app.inject({
method: 'POST',
url: `/v1/accounts/${accountId}/tax-exemptions`,
headers: { authorization: `Bearer ${token}` },
payload: { certificateNumber: 'CERT-1' },
})
const res = await app.inject({
method: 'GET',
url: `/v1/accounts/${accountId}/tax-exemptions`,
headers: { authorization: `Bearer ${token}` },
})
expect(res.statusCode).toBe(200)
expect(res.json().data.length).toBe(1)
})
it('approves a tax exemption', async () => {
const createRes = await app.inject({
method: 'POST',
url: `/v1/accounts/${accountId}/tax-exemptions`,
headers: { authorization: `Bearer ${token}` },
payload: { certificateNumber: 'CERT-APPROVE' },
})
const id = createRes.json().id
const res = await app.inject({
method: 'POST',
url: `/v1/tax-exemptions/${id}/approve`,
headers: { authorization: `Bearer ${token}` },
})
expect(res.statusCode).toBe(200)
expect(res.json().status).toBe('approved')
expect(res.json().approvedBy).toBeTruthy()
expect(res.json().approvedAt).toBeTruthy()
})
it('revokes a tax exemption with reason', async () => {
const createRes = await app.inject({
method: 'POST',
url: `/v1/accounts/${accountId}/tax-exemptions`,
headers: { authorization: `Bearer ${token}` },
payload: { certificateNumber: 'CERT-REVOKE' },
})
const id = createRes.json().id
// Approve first
await app.inject({
method: 'POST',
url: `/v1/tax-exemptions/${id}/approve`,
headers: { authorization: `Bearer ${token}` },
})
const res = await app.inject({
method: 'POST',
url: `/v1/tax-exemptions/${id}/revoke`,
headers: { authorization: `Bearer ${token}` },
payload: { reason: 'Certificate expired' },
})
expect(res.statusCode).toBe(200)
expect(res.json().status).toBe('none')
expect(res.json().revokedReason).toBe('Certificate expired')
})
it('rejects revoke without reason', async () => {
const createRes = await app.inject({
method: 'POST',
url: `/v1/accounts/${accountId}/tax-exemptions`,
headers: { authorization: `Bearer ${token}` },
payload: { certificateNumber: 'CERT-NO-REASON' },
})
const res = await app.inject({
method: 'POST',
url: `/v1/tax-exemptions/${createRes.json().id}/revoke`,
headers: { authorization: `Bearer ${token}` },
payload: {},
})
expect(res.statusCode).toBe(400)
})
it('updates a tax exemption', async () => {
const createRes = await app.inject({
method: 'POST',
url: `/v1/accounts/${accountId}/tax-exemptions`,
headers: { authorization: `Bearer ${token}` },
payload: { certificateNumber: 'OLD-CERT' },
})
const res = await app.inject({
method: 'PATCH',
url: `/v1/tax-exemptions/${createRes.json().id}`,
headers: { authorization: `Bearer ${token}` },
payload: { certificateNumber: 'NEW-CERT', issuingState: 'CA' },
})
expect(res.statusCode).toBe(200)
expect(res.json().certificateNumber).toBe('NEW-CERT')
expect(res.json().issuingState).toBe('CA')
})
})
describe('Lookup table routes', () => {
let app: FastifyInstance
let token: string
beforeAll(async () => {
app = await createTestApp()
})
beforeEach(async () => {
await cleanDb(app)
await seedTestCompany(app)
const auth = await registerAndLogin(app, { email: `lookup-${Date.now()}@test.com` })
token = auth.token
})
afterAll(async () => {
await app.close()
})
describe('Unit statuses', () => {
it('lists system-seeded unit statuses', async () => {
const res = await app.inject({
method: 'GET',
url: '/v1/unit-statuses',
headers: { authorization: `Bearer ${token}` },
})
expect(res.statusCode).toBe(200)
const slugs = res.json().data.map((s: { slug: string }) => s.slug)
expect(slugs).toContain('available')
expect(slugs).toContain('sold')
expect(slugs).toContain('on_trial')
expect(slugs).toContain('layaway')
expect(slugs).toContain('lost')
})
it('creates a custom status', async () => {
const res = await app.inject({
method: 'POST',
url: '/v1/unit-statuses',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'On Display', slug: 'on_display', description: 'Showroom floor' },
})
expect(res.statusCode).toBe(201)
expect(res.json().slug).toBe('on_display')
expect(res.json().isSystem).toBe(false)
})
it('rejects duplicate slug', async () => {
const res = await app.inject({
method: 'POST',
url: '/v1/unit-statuses',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Available Copy', slug: 'available' },
})
expect(res.statusCode).toBe(409)
})
it('blocks deleting a system status', async () => {
const list = await app.inject({
method: 'GET',
url: '/v1/unit-statuses',
headers: { authorization: `Bearer ${token}` },
})
const systemStatus = list.json().data.find((s: { isSystem: boolean }) => s.isSystem)
const res = await app.inject({
method: 'DELETE',
url: `/v1/unit-statuses/${systemStatus.id}`,
headers: { authorization: `Bearer ${token}` },
})
expect(res.statusCode).toBe(403)
})
it('allows deleting a custom status', async () => {
const createRes = await app.inject({
method: 'POST',
url: '/v1/unit-statuses',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Temp', slug: 'temp_status' },
})
const res = await app.inject({
method: 'DELETE',
url: `/v1/unit-statuses/${createRes.json().id}`,
headers: { authorization: `Bearer ${token}` },
})
expect(res.statusCode).toBe(200)
})
})
describe('Item conditions', () => {
it('lists system-seeded item conditions', async () => {
const res = await app.inject({
method: 'GET',
url: '/v1/item-conditions',
headers: { authorization: `Bearer ${token}` },
})
expect(res.statusCode).toBe(200)
const slugs = res.json().data.map((c: { slug: string }) => c.slug)
expect(slugs).toContain('new')
expect(slugs).toContain('excellent')
expect(slugs).toContain('good')
expect(slugs).toContain('fair')
expect(slugs).toContain('poor')
})
it('creates a custom condition', async () => {
const res = await app.inject({
method: 'POST',
url: '/v1/item-conditions',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Refurbished', slug: 'refurbished', description: 'Professionally restored' },
})
expect(res.statusCode).toBe(201)
expect(res.json().slug).toBe('refurbished')
})
})
})

View File

@@ -0,0 +1,327 @@
import { describe, it, expect, beforeAll, beforeEach, afterAll } from 'bun:test'
import type { FastifyInstance } from 'fastify'
import {
createTestApp,
cleanDb,
seedTestCompany,
registerAndLogin,
TEST_COMPANY_ID,
} from '../../../src/test/helpers.js'
describe('Account routes', () => {
let app: FastifyInstance
let token: string
beforeAll(async () => {
app = await createTestApp()
})
beforeEach(async () => {
await cleanDb(app)
await seedTestCompany(app)
const auth = await registerAndLogin(app)
token = auth.token
})
afterAll(async () => {
await app.close()
})
describe('POST /v1/accounts', () => {
it('creates an account', async () => {
const response = await app.inject({
method: 'POST',
url: '/v1/accounts',
headers: { authorization: `Bearer ${token}` },
payload: {
name: 'Smith Family',
email: 'smith@example.com',
phone: '512-555-1234',
billingMode: 'consolidated',
},
})
expect(response.statusCode).toBe(201)
const body = response.json()
expect(body.name).toBe('Smith Family')
expect(body.email).toBe('smith@example.com')
expect(body.companyId).toBe(TEST_COMPANY_ID)
expect(body.billingMode).toBe('consolidated')
})
it('rejects missing name', async () => {
const response = await app.inject({
method: 'POST',
url: '/v1/accounts',
headers: { authorization: `Bearer ${token}` },
payload: { email: 'noname@test.com' },
})
expect(response.statusCode).toBe(400)
})
})
describe('GET /v1/accounts', () => {
it('lists accounts for the company', async () => {
await app.inject({
method: 'POST',
url: '/v1/accounts',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Account One' },
})
await app.inject({
method: 'POST',
url: '/v1/accounts',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Account Two' },
})
const response = await app.inject({
method: 'GET',
url: '/v1/accounts',
headers: { authorization: `Bearer ${token}` },
})
expect(response.statusCode).toBe(200)
const body = response.json()
expect(body.data.length).toBe(2)
expect(body.pagination.total).toBe(2)
expect(body.pagination.page).toBe(1)
})
})
describe('GET /v1/accounts?q=', () => {
it('searches by name', async () => {
await app.inject({
method: 'POST',
url: '/v1/accounts',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Johnson Family', phone: '555-9999' },
})
await app.inject({
method: 'POST',
url: '/v1/accounts',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Williams Music' },
})
const response = await app.inject({
method: 'GET',
url: '/v1/accounts?q=johnson',
headers: { authorization: `Bearer ${token}` },
})
expect(response.statusCode).toBe(200)
const body = response.json()
expect(body.data.length).toBe(1)
expect(body.data[0].name).toBe('Johnson Family')
})
it('searches by phone', async () => {
await app.inject({
method: 'POST',
url: '/v1/accounts',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Phone Test', phone: '512-867-5309' },
})
const response = await app.inject({
method: 'GET',
url: '/v1/accounts?q=867-5309',
headers: { authorization: `Bearer ${token}` },
})
expect(response.statusCode).toBe(200)
expect(response.json().data.length).toBe(1)
})
it('paginates results', async () => {
for (let i = 0; i < 5; i++) {
await app.inject({
method: 'POST',
url: '/v1/accounts',
headers: { authorization: `Bearer ${token}` },
payload: { name: `Account ${i}` },
})
}
const response = await app.inject({
method: 'GET',
url: '/v1/accounts?page=1&limit=2',
headers: { authorization: `Bearer ${token}` },
})
const body = response.json()
expect(body.data.length).toBe(2)
expect(body.pagination.total).toBe(5)
expect(body.pagination.totalPages).toBe(3)
expect(body.pagination.page).toBe(1)
})
})
describe('PATCH /v1/accounts/:id', () => {
it('updates an account', async () => {
const createRes = await app.inject({
method: 'POST',
url: '/v1/accounts',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Original Name' },
})
const id = createRes.json().id
const response = await app.inject({
method: 'PATCH',
url: `/v1/accounts/${id}`,
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Updated Name' },
})
expect(response.statusCode).toBe(200)
expect(response.json().name).toBe('Updated Name')
})
})
describe('DELETE /v1/accounts/:id', () => {
it('soft-deletes an account', async () => {
const createRes = await app.inject({
method: 'POST',
url: '/v1/accounts',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'To Delete' },
})
const id = createRes.json().id
const delRes = await app.inject({
method: 'DELETE',
url: `/v1/accounts/${id}`,
headers: { authorization: `Bearer ${token}` },
})
expect(delRes.statusCode).toBe(200)
expect(delRes.json().isActive).toBe(false)
// Should not appear in list
const listRes = await app.inject({
method: 'GET',
url: '/v1/accounts',
headers: { authorization: `Bearer ${token}` },
})
expect(listRes.json().data.length).toBe(0)
})
})
})
describe('Member routes', () => {
let app: FastifyInstance
let token: string
let accountId: string
beforeAll(async () => {
app = await createTestApp()
})
beforeEach(async () => {
await cleanDb(app)
await seedTestCompany(app)
const auth = await registerAndLogin(app, { email: `member-test-${Date.now()}@test.com` })
token = auth.token
const accountRes = await app.inject({
method: 'POST',
url: '/v1/accounts',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Test Family' },
})
accountId = accountRes.json().id
})
afterAll(async () => {
await app.close()
})
describe('POST /v1/accounts/:accountId/members', () => {
it('creates a member', async () => {
const response = await app.inject({
method: 'POST',
url: `/v1/accounts/${accountId}/members`,
headers: { authorization: `Bearer ${token}` },
payload: {
firstName: 'Emma',
lastName: 'Chen',
dateOfBirth: '2015-03-15',
email: 'emma@test.com',
},
})
expect(response.statusCode).toBe(201)
const body = response.json()
expect(body.firstName).toBe('Emma')
expect(body.lastName).toBe('Chen')
expect(body.isMinor).toBe(true)
expect(body.accountId).toBe(accountId)
})
it('marks adult members as not minor', async () => {
const response = await app.inject({
method: 'POST',
url: `/v1/accounts/${accountId}/members`,
headers: { authorization: `Bearer ${token}` },
payload: {
firstName: 'John',
lastName: 'Chen',
dateOfBirth: '1985-06-20',
},
})
expect(response.statusCode).toBe(201)
expect(response.json().isMinor).toBe(false)
})
})
describe('GET /v1/accounts/:accountId/members', () => {
it('lists members for an account', async () => {
await app.inject({
method: 'POST',
url: `/v1/accounts/${accountId}/members`,
headers: { authorization: `Bearer ${token}` },
payload: { firstName: 'Child', lastName: 'One' },
})
await app.inject({
method: 'POST',
url: `/v1/accounts/${accountId}/members`,
headers: { authorization: `Bearer ${token}` },
payload: { firstName: 'Child', lastName: 'Two' },
})
const response = await app.inject({
method: 'GET',
url: `/v1/accounts/${accountId}/members`,
headers: { authorization: `Bearer ${token}` },
})
expect(response.statusCode).toBe(200)
expect(response.json().data.length).toBe(2)
})
})
describe('PATCH /members/:id', () => {
it('updates a member and recalculates isMinor', async () => {
const createRes = await app.inject({
method: 'POST',
url: `/v1/accounts/${accountId}/members`,
headers: { authorization: `Bearer ${token}` },
payload: { firstName: 'Kid', lastName: 'Test', dateOfBirth: '2015-01-01' },
})
const memberId = createRes.json().id
const response = await app.inject({
method: 'PATCH',
url: `/v1/members/${memberId}`,
headers: { authorization: `Bearer ${token}` },
payload: { dateOfBirth: '1980-01-01' },
})
expect(response.statusCode).toBe(200)
expect(response.json().isMinor).toBe(false)
})
})
})

View File

@@ -0,0 +1,166 @@
import { describe, it, expect, beforeAll, beforeEach, afterAll } from 'bun:test'
import type { FastifyInstance } from 'fastify'
import { createTestApp, cleanDb, seedTestCompany, TEST_COMPANY_ID } from '../../../src/test/helpers.js'
describe('Auth routes', () => {
let app: FastifyInstance
beforeAll(async () => {
app = await createTestApp()
})
beforeEach(async () => {
await cleanDb(app)
await seedTestCompany(app)
})
afterAll(async () => {
await app.close()
})
describe('POST /v1/auth/register', () => {
it('creates a user and returns token', async () => {
const response = await app.inject({
method: 'POST',
url: '/v1/auth/register',
headers: { 'x-company-id': TEST_COMPANY_ID },
payload: {
email: 'staff@musicstore.com',
password: 'securepassword',
firstName: 'Jane',
lastName: 'Doe',
role: 'staff',
},
})
expect(response.statusCode).toBe(201)
const body = response.json()
expect(body.user.email).toBe('staff@musicstore.com')
expect(body.user.firstName).toBe('Jane')
expect(body.user.role).toBe('staff')
expect(body.token).toBeDefined()
expect(body.user.passwordHash).toBeUndefined()
})
it('rejects duplicate email within same company', async () => {
await app.inject({
method: 'POST',
url: '/v1/auth/register',
headers: { 'x-company-id': TEST_COMPANY_ID },
payload: {
email: 'dupe@test.com',
password: 'password123',
firstName: 'First',
lastName: 'User',
},
})
const response = await app.inject({
method: 'POST',
url: '/v1/auth/register',
headers: { 'x-company-id': TEST_COMPANY_ID },
payload: {
email: 'dupe@test.com',
password: 'password456',
firstName: 'Second',
lastName: 'User',
},
})
expect(response.statusCode).toBe(409)
})
it('rejects invalid email', async () => {
const response = await app.inject({
method: 'POST',
url: '/v1/auth/register',
headers: { 'x-company-id': TEST_COMPANY_ID },
payload: {
email: 'not-an-email',
password: 'password123',
firstName: 'Bad',
lastName: 'Email',
},
})
expect(response.statusCode).toBe(400)
})
it('rejects short password', async () => {
const response = await app.inject({
method: 'POST',
url: '/v1/auth/register',
headers: { 'x-company-id': TEST_COMPANY_ID },
payload: {
email: 'short@test.com',
password: '123',
firstName: 'Short',
lastName: 'Pass',
},
})
expect(response.statusCode).toBe(400)
})
})
describe('POST /v1/auth/login', () => {
beforeEach(async () => {
await app.inject({
method: 'POST',
url: '/v1/auth/register',
headers: { 'x-company-id': TEST_COMPANY_ID },
payload: {
email: 'login@test.com',
password: 'correctpassword',
firstName: 'Login',
lastName: 'User',
},
})
})
it('returns token with valid credentials', async () => {
const response = await app.inject({
method: 'POST',
url: '/v1/auth/login',
headers: { 'x-company-id': TEST_COMPANY_ID },
payload: {
email: 'login@test.com',
password: 'correctpassword',
},
})
expect(response.statusCode).toBe(200)
const body = response.json()
expect(body.token).toBeDefined()
expect(body.user.email).toBe('login@test.com')
})
it('rejects wrong password', async () => {
const response = await app.inject({
method: 'POST',
url: '/v1/auth/login',
headers: { 'x-company-id': TEST_COMPANY_ID },
payload: {
email: 'login@test.com',
password: 'wrongpassword',
},
})
expect(response.statusCode).toBe(401)
})
it('rejects nonexistent email', async () => {
const response = await app.inject({
method: 'POST',
url: '/v1/auth/login',
headers: { 'x-company-id': TEST_COMPANY_ID },
payload: {
email: 'nobody@test.com',
password: 'whatever',
},
})
expect(response.statusCode).toBe(401)
})
})
})

View File

@@ -0,0 +1,30 @@
import { describe, it, expect, beforeAll, afterAll } from 'bun:test'
import type { FastifyInstance } from 'fastify'
import { createTestApp } from '../../../src/test/helpers.js'
describe('GET /v1/health', () => {
let app: FastifyInstance
beforeAll(async () => {
app = await createTestApp()
})
afterAll(async () => {
await app.close()
})
it('returns ok when db and redis are connected', async () => {
const response = await app.inject({
method: 'GET',
url: '/v1/health',
})
expect(response.statusCode).toBe(200)
const body = response.json()
expect(body.status).toBe('ok')
expect(body.db).toBe('connected')
expect(body.redis).toBe('connected')
expect(body.timestamp).toBeDefined()
})
})

View File

@@ -0,0 +1,193 @@
import { describe, it, expect, beforeAll, beforeEach, afterAll } from 'bun:test'
import type { FastifyInstance } from 'fastify'
import {
createTestApp,
cleanDb,
seedTestCompany,
registerAndLogin,
} from '../../../src/test/helpers.js'
describe('Category routes', () => {
let app: FastifyInstance
let token: string
beforeAll(async () => {
app = await createTestApp()
})
beforeEach(async () => {
await cleanDb(app)
await seedTestCompany(app)
const auth = await registerAndLogin(app)
token = auth.token
})
afterAll(async () => {
await app.close()
})
it('creates a category', async () => {
const response = await app.inject({
method: 'POST',
url: '/v1/categories',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Guitars', description: 'All guitars', sortOrder: 1 },
})
expect(response.statusCode).toBe(201)
expect(response.json().name).toBe('Guitars')
expect(response.json().sortOrder).toBe(1)
})
it('creates a child category', async () => {
const parentRes = await app.inject({
method: 'POST',
url: '/v1/categories',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Instruments' },
})
const parentId = parentRes.json().id
const childRes = await app.inject({
method: 'POST',
url: '/v1/categories',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Brass', parentId },
})
expect(childRes.statusCode).toBe(201)
expect(childRes.json().parentId).toBe(parentId)
})
it('lists categories sorted by sortOrder', async () => {
await app.inject({
method: 'POST',
url: '/v1/categories',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Zzz Last', sortOrder: 99 },
})
await app.inject({
method: 'POST',
url: '/v1/categories',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Aaa First', sortOrder: 1 },
})
const response = await app.inject({
method: 'GET',
url: '/v1/categories',
headers: { authorization: `Bearer ${token}` },
})
expect(response.statusCode).toBe(200)
const body = response.json()
expect(body.data.length).toBe(2)
expect(body.data[0].name).toBe('Aaa First')
})
it('soft-deletes a category', async () => {
const createRes = await app.inject({
method: 'POST',
url: '/v1/categories',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'To Delete' },
})
const delRes = await app.inject({
method: 'DELETE',
url: `/v1/categories/${createRes.json().id}`,
headers: { authorization: `Bearer ${token}` },
})
expect(delRes.json().isActive).toBe(false)
const listRes = await app.inject({
method: 'GET',
url: '/v1/categories',
headers: { authorization: `Bearer ${token}` },
})
expect(listRes.json().data.length).toBe(0)
})
})
describe('Supplier routes', () => {
let app: FastifyInstance
let token: string
beforeAll(async () => {
app = await createTestApp()
})
beforeEach(async () => {
await cleanDb(app)
await seedTestCompany(app)
const auth = await registerAndLogin(app, { email: `supplier-${Date.now()}@test.com` })
token = auth.token
})
afterAll(async () => {
await app.close()
})
it('creates a supplier', async () => {
const response = await app.inject({
method: 'POST',
url: '/v1/suppliers',
headers: { authorization: `Bearer ${token}` },
payload: {
name: 'RS Musical',
contactName: 'Bob Smith',
email: 'bob@rsmusical.com',
paymentTerms: 'Net 30',
},
})
expect(response.statusCode).toBe(201)
expect(response.json().name).toBe('RS Musical')
expect(response.json().paymentTerms).toBe('Net 30')
})
it('searches suppliers by name', async () => {
await app.inject({
method: 'POST',
url: '/v1/suppliers',
headers: { authorization: `Bearer ${token}` },
payload: { name: "Ferree's Tools" },
})
await app.inject({
method: 'POST',
url: '/v1/suppliers',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Allied Supply' },
})
const response = await app.inject({
method: 'GET',
url: '/v1/suppliers?q=ferree',
headers: { authorization: `Bearer ${token}` },
})
expect(response.statusCode).toBe(200)
expect(response.json().data.length).toBe(1)
expect(response.json().data[0].name).toBe("Ferree's Tools")
})
it('updates a supplier', async () => {
const createRes = await app.inject({
method: 'POST',
url: '/v1/suppliers',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Old Name' },
})
const updateRes = await app.inject({
method: 'PATCH',
url: `/v1/suppliers/${createRes.json().id}`,
headers: { authorization: `Bearer ${token}` },
payload: { name: 'New Name', paymentTerms: 'COD' },
})
expect(updateRes.statusCode).toBe(200)
expect(updateRes.json().name).toBe('New Name')
expect(updateRes.json().paymentTerms).toBe('COD')
})
})

View File

@@ -0,0 +1,222 @@
import { describe, it, expect, beforeAll, beforeEach, afterAll } from 'bun:test'
import type { FastifyInstance } from 'fastify'
import {
createTestApp,
cleanDb,
seedTestCompany,
registerAndLogin,
} from '../../../src/test/helpers.js'
describe('Product routes', () => {
let app: FastifyInstance
let token: string
beforeAll(async () => {
app = await createTestApp()
})
beforeEach(async () => {
await cleanDb(app)
await seedTestCompany(app)
const auth = await registerAndLogin(app)
token = auth.token
})
afterAll(async () => {
await app.close()
})
it('creates a non-serialized product', async () => {
const response = await app.inject({
method: 'POST',
url: '/v1/products',
headers: { authorization: `Bearer ${token}` },
payload: {
name: 'Guitar Strings - D\'Addario EJ16',
sku: 'STR-DAD-EJ16',
upc: '019954121266',
brand: "D'Addario",
price: 6.99,
qtyOnHand: 24,
qtyReorderPoint: 10,
},
})
expect(response.statusCode).toBe(201)
const body = response.json()
expect(body.name).toBe('Guitar Strings - D\'Addario EJ16')
expect(body.isSerialized).toBe(false)
expect(body.qtyOnHand).toBe(24)
})
it('creates a serialized product', async () => {
const response = await app.inject({
method: 'POST',
url: '/v1/products',
headers: { authorization: `Bearer ${token}` },
payload: {
name: 'Yamaha YTR-2330 Trumpet',
sku: 'BRASS-YAM-2330',
brand: 'Yamaha',
model: 'YTR-2330',
isSerialized: true,
price: 1299.99,
minPrice: 1100.00,
},
})
expect(response.statusCode).toBe(201)
expect(response.json().isSerialized).toBe(true)
})
it('searches by name, sku, and brand', async () => {
await app.inject({
method: 'POST',
url: '/v1/products',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Fender Stratocaster', brand: 'Fender', sku: 'GTR-FND-STRAT' },
})
await app.inject({
method: 'POST',
url: '/v1/products',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Gibson Les Paul', brand: 'Gibson', sku: 'GTR-GIB-LP' },
})
const byName = await app.inject({
method: 'GET',
url: '/v1/products?q=stratocaster',
headers: { authorization: `Bearer ${token}` },
})
expect(byName.json().data.length).toBe(1)
const bySku = await app.inject({
method: 'GET',
url: '/v1/products?q=GTR-GIB',
headers: { authorization: `Bearer ${token}` },
})
expect(bySku.json().data.length).toBe(1)
const byBrand = await app.inject({
method: 'GET',
url: '/v1/products?q=fender',
headers: { authorization: `Bearer ${token}` },
})
expect(byBrand.json().data.length).toBe(1)
})
it('logs price change on update', async () => {
const createRes = await app.inject({
method: 'POST',
url: '/v1/products',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Test Item', price: 10.00 },
})
const id = createRes.json().id
const updateRes = await app.inject({
method: 'PATCH',
url: `/v1/products/${id}`,
headers: { authorization: `Bearer ${token}` },
payload: { price: 12.50 },
})
expect(updateRes.statusCode).toBe(200)
expect(updateRes.json().price).toBe('12.50')
})
})
describe('Inventory unit routes', () => {
let app: FastifyInstance
let token: string
let productId: string
beforeAll(async () => {
app = await createTestApp()
})
beforeEach(async () => {
await cleanDb(app)
await seedTestCompany(app)
const auth = await registerAndLogin(app, { email: `unit-${Date.now()}@test.com` })
token = auth.token
const productRes = await app.inject({
method: 'POST',
url: '/v1/products',
headers: { authorization: `Bearer ${token}` },
payload: { name: 'Yamaha Trumpet', isSerialized: true, price: 1200 },
})
productId = productRes.json().id
})
afterAll(async () => {
await app.close()
})
it('creates an inventory unit with serial number', async () => {
const response = await app.inject({
method: 'POST',
url: `/v1/products/${productId}/units`,
headers: { authorization: `Bearer ${token}` },
payload: {
serialNumber: 'YAM-2024-001234',
condition: 'new',
purchaseDate: '2024-09-15',
purchaseCost: 650.00,
},
})
expect(response.statusCode).toBe(201)
const body = response.json()
expect(body.serialNumber).toBe('YAM-2024-001234')
expect(body.condition).toBe('new')
expect(body.status).toBe('available')
expect(body.purchaseCost).toBe('650.00')
})
it('lists units for a product', async () => {
await app.inject({
method: 'POST',
url: `/v1/products/${productId}/units`,
headers: { authorization: `Bearer ${token}` },
payload: { serialNumber: 'SN-001' },
})
await app.inject({
method: 'POST',
url: `/v1/products/${productId}/units`,
headers: { authorization: `Bearer ${token}` },
payload: { serialNumber: 'SN-002' },
})
const response = await app.inject({
method: 'GET',
url: `/v1/products/${productId}/units`,
headers: { authorization: `Bearer ${token}` },
})
expect(response.statusCode).toBe(200)
expect(response.json().data.length).toBe(2)
})
it('updates unit status and condition', async () => {
const createRes = await app.inject({
method: 'POST',
url: `/v1/products/${productId}/units`,
headers: { authorization: `Bearer ${token}` },
payload: { serialNumber: 'SN-UPDATE' },
})
const unitId = createRes.json().id
const updateRes = await app.inject({
method: 'PATCH',
url: `/v1/units/${unitId}`,
headers: { authorization: `Bearer ${token}` },
payload: { status: 'rented', condition: 'good' },
})
expect(updateRes.statusCode).toBe(200)
expect(updateRes.json().status).toBe('rented')
expect(updateRes.json().condition).toBe('good')
})
})

View File

@@ -0,0 +1,240 @@
import { describe, it, expect, beforeAll, beforeEach, afterAll } from 'bun:test'
import type { FastifyInstance } from 'fastify'
import { createTestApp, cleanDb, seedTestCompany, registerAndLogin, TEST_COMPANY_ID } from '../../src/test/helpers.js'
import { AccountService, PaymentMethodService, TaxExemptionService } from '../../src/services/account.service.js'
describe('PaymentMethodService', () => {
let app: FastifyInstance
let accountId: string
beforeAll(async () => {
app = await createTestApp()
})
beforeEach(async () => {
await cleanDb(app)
await seedTestCompany(app)
const auth = await registerAndLogin(app, { email: `pm-svc-${Date.now()}@test.com` })
const account = await AccountService.create(app.db, TEST_COMPANY_ID, { name: 'PM Test' })
accountId = account.id
})
afterAll(async () => {
await app.close()
})
it('creates a payment method', async () => {
const pm = await PaymentMethodService.create(app.db, TEST_COMPANY_ID, {
accountId,
processor: 'stripe',
processorPaymentMethodId: 'pm_abc',
cardBrand: 'visa',
lastFour: '4242',
expMonth: 12,
expYear: 2027,
isDefault: false,
})
expect(pm.processorPaymentMethodId).toBe('pm_abc')
expect(pm.isDefault).toBe(false)
})
it('unsets old default when creating a new default', async () => {
const first = await PaymentMethodService.create(app.db, TEST_COMPANY_ID, {
accountId,
processor: 'stripe',
processorPaymentMethodId: 'pm_first',
isDefault: true,
})
expect(first.isDefault).toBe(true)
const second = await PaymentMethodService.create(app.db, TEST_COMPANY_ID, {
accountId,
processor: 'stripe',
processorPaymentMethodId: 'pm_second',
isDefault: true,
})
expect(second.isDefault).toBe(true)
// First should no longer be default
const firstRefreshed = await PaymentMethodService.getById(app.db, TEST_COMPANY_ID, first.id)
expect(firstRefreshed!.isDefault).toBe(false)
})
it('unsets old default when updating to default', async () => {
const first = await PaymentMethodService.create(app.db, TEST_COMPANY_ID, {
accountId,
processor: 'stripe',
processorPaymentMethodId: 'pm_1',
isDefault: true,
})
const second = await PaymentMethodService.create(app.db, TEST_COMPANY_ID, {
accountId,
processor: 'stripe',
processorPaymentMethodId: 'pm_2',
isDefault: false,
})
await PaymentMethodService.update(app.db, TEST_COMPANY_ID, second.id, { isDefault: true })
const firstRefreshed = await PaymentMethodService.getById(app.db, TEST_COMPANY_ID, first.id)
expect(firstRefreshed!.isDefault).toBe(false)
const secondRefreshed = await PaymentMethodService.getById(app.db, TEST_COMPANY_ID, second.id)
expect(secondRefreshed!.isDefault).toBe(true)
})
it('does not affect other accounts when setting default', async () => {
const otherAccount = await AccountService.create(app.db, TEST_COMPANY_ID, { name: 'Other' })
const otherPm = await PaymentMethodService.create(app.db, TEST_COMPANY_ID, {
accountId: otherAccount.id,
processor: 'stripe',
processorPaymentMethodId: 'pm_other',
isDefault: true,
})
// Create default on our account — should not touch the other account's default
await PaymentMethodService.create(app.db, TEST_COMPANY_ID, {
accountId,
processor: 'stripe',
processorPaymentMethodId: 'pm_ours',
isDefault: true,
})
const otherRefreshed = await PaymentMethodService.getById(app.db, TEST_COMPANY_ID, otherPm.id)
expect(otherRefreshed!.isDefault).toBe(true)
})
it('lists only methods for the requested account', async () => {
const otherAccount = await AccountService.create(app.db, TEST_COMPANY_ID, { name: 'Other' })
await PaymentMethodService.create(app.db, TEST_COMPANY_ID, {
accountId,
processor: 'stripe',
processorPaymentMethodId: 'pm_a',
isDefault: false,
})
await PaymentMethodService.create(app.db, TEST_COMPANY_ID, {
accountId: otherAccount.id,
processor: 'stripe',
processorPaymentMethodId: 'pm_b',
isDefault: false,
})
const methods = await PaymentMethodService.listByAccount(app.db, TEST_COMPANY_ID, accountId)
expect(methods.length).toBe(1)
expect(methods[0].processorPaymentMethodId).toBe('pm_a')
})
it('deletes a payment method', async () => {
const pm = await PaymentMethodService.create(app.db, TEST_COMPANY_ID, {
accountId,
processor: 'stripe',
processorPaymentMethodId: 'pm_del',
isDefault: false,
})
const deleted = await PaymentMethodService.delete(app.db, TEST_COMPANY_ID, pm.id)
expect(deleted).not.toBeNull()
const fetched = await PaymentMethodService.getById(app.db, TEST_COMPANY_ID, pm.id)
expect(fetched).toBeNull()
})
})
describe('TaxExemptionService', () => {
let app: FastifyInstance
let accountId: string
let userId: string
beforeAll(async () => {
app = await createTestApp()
})
beforeEach(async () => {
await cleanDb(app)
await seedTestCompany(app)
const auth = await registerAndLogin(app, { email: `tax-svc-${Date.now()}@test.com` })
userId = (auth.user as { id: string }).id
const account = await AccountService.create(app.db, TEST_COMPANY_ID, { name: 'Tax Test' })
accountId = account.id
})
afterAll(async () => {
await app.close()
})
it('creates an exemption in pending status', async () => {
const exemption = await TaxExemptionService.create(app.db, TEST_COMPANY_ID, {
accountId,
certificateNumber: 'TX-001',
certificateType: 'resale',
issuingState: 'TX',
})
expect(exemption.status).toBe('pending')
expect(exemption.certificateNumber).toBe('TX-001')
expect(exemption.approvedBy).toBeNull()
})
it('approves a pending exemption', async () => {
const exemption = await TaxExemptionService.create(app.db, TEST_COMPANY_ID, {
accountId,
certificateNumber: 'TX-002',
})
const approved = await TaxExemptionService.approve(app.db, TEST_COMPANY_ID, exemption.id, userId)
expect(approved!.status).toBe('approved')
expect(approved!.approvedBy).toBe(userId)
expect(approved!.approvedAt).toBeTruthy()
})
it('revokes an exemption with reason', async () => {
const exemption = await TaxExemptionService.create(app.db, TEST_COMPANY_ID, {
accountId,
certificateNumber: 'TX-003',
})
await TaxExemptionService.approve(app.db, TEST_COMPANY_ID, exemption.id, userId)
const revoked = await TaxExemptionService.revoke(app.db, TEST_COMPANY_ID, exemption.id, userId, 'Expired')
expect(revoked!.status).toBe('none')
expect(revoked!.revokedBy).toBe(userId)
expect(revoked!.revokedReason).toBe('Expired')
expect(revoked!.revokedAt).toBeTruthy()
})
it('lists exemptions for an account', async () => {
await TaxExemptionService.create(app.db, TEST_COMPANY_ID, {
accountId,
certificateNumber: 'CERT-A',
})
await TaxExemptionService.create(app.db, TEST_COMPANY_ID, {
accountId,
certificateNumber: 'CERT-B',
})
const list = await TaxExemptionService.listByAccount(app.db, TEST_COMPANY_ID, accountId)
expect(list.length).toBe(2)
})
it('updates exemption details', async () => {
const exemption = await TaxExemptionService.create(app.db, TEST_COMPANY_ID, {
accountId,
certificateNumber: 'OLD',
})
const updated = await TaxExemptionService.update(app.db, TEST_COMPANY_ID, exemption.id, {
certificateNumber: 'NEW',
issuingState: 'CA',
})
expect(updated!.certificateNumber).toBe('NEW')
expect(updated!.issuingState).toBe('CA')
})
it('returns null for nonexistent exemption', async () => {
const result = await TaxExemptionService.getById(app.db, TEST_COMPANY_ID, '00000000-0000-0000-0000-000000000000')
expect(result).toBeNull()
})
})

View File

@@ -0,0 +1,152 @@
import { describe, it, expect, beforeAll, beforeEach, afterAll } from 'bun:test'
import type { FastifyInstance } from 'fastify'
import { createTestApp, cleanDb, seedTestCompany, TEST_COMPANY_ID } from '../../src/test/helpers.js'
import { UnitStatusService, ItemConditionService } from '../../src/services/lookup.service.js'
describe('UnitStatusService', () => {
let app: FastifyInstance
beforeAll(async () => {
app = await createTestApp()
})
beforeEach(async () => {
await cleanDb(app)
await seedTestCompany(app)
})
afterAll(async () => {
await app.close()
})
describe('seedForCompany', () => {
it('seeds system statuses on first call', async () => {
const statuses = await UnitStatusService.list(app.db, TEST_COMPANY_ID)
expect(statuses.length).toBeGreaterThanOrEqual(8)
expect(statuses.every((s) => s.isSystem)).toBe(true)
})
it('is idempotent — second seed does not duplicate', async () => {
await UnitStatusService.seedForCompany(app.db, TEST_COMPANY_ID)
const statuses = await UnitStatusService.list(app.db, TEST_COMPANY_ID)
const slugs = statuses.map((s) => s.slug)
const uniqueSlugs = new Set(slugs)
expect(slugs.length).toBe(uniqueSlugs.size)
})
})
describe('getBySlug', () => {
it('returns a system status by slug', async () => {
const status = await UnitStatusService.getBySlug(app.db, TEST_COMPANY_ID, 'available')
expect(status).not.toBeNull()
expect(status!.slug).toBe('available')
expect(status!.isSystem).toBe(true)
})
it('returns null for nonexistent slug', async () => {
const status = await UnitStatusService.getBySlug(app.db, TEST_COMPANY_ID, 'nonexistent')
expect(status).toBeNull()
})
})
describe('validateSlug', () => {
it('returns true for valid active slug', async () => {
expect(await UnitStatusService.validateSlug(app.db, TEST_COMPANY_ID, 'sold')).toBe(true)
})
it('returns false for nonexistent slug', async () => {
expect(await UnitStatusService.validateSlug(app.db, TEST_COMPANY_ID, 'bogus')).toBe(false)
})
})
describe('create', () => {
it('creates a custom (non-system) status', async () => {
const custom = await UnitStatusService.create(app.db, TEST_COMPANY_ID, {
name: 'In Transit',
slug: 'in_transit',
description: 'Being shipped between locations',
sortOrder: 99,
})
expect(custom.slug).toBe('in_transit')
expect(custom.isSystem).toBe(false)
})
})
describe('delete', () => {
it('throws when deleting a system status', async () => {
const system = await UnitStatusService.getBySlug(app.db, TEST_COMPANY_ID, 'available')
expect(system).not.toBeNull()
expect(() => UnitStatusService.delete(app.db, TEST_COMPANY_ID, system!.id)).toThrow('system')
})
it('deletes a custom status', async () => {
const custom = await UnitStatusService.create(app.db, TEST_COMPANY_ID, {
name: 'Temp',
slug: 'temp',
sortOrder: 0,
})
const deleted = await UnitStatusService.delete(app.db, TEST_COMPANY_ID, custom.id)
expect(deleted).not.toBeNull()
expect(deleted!.slug).toBe('temp')
})
it('returns null for nonexistent id', async () => {
const result = await UnitStatusService.delete(app.db, TEST_COMPANY_ID, '00000000-0000-0000-0000-000000000000')
expect(result).toBeNull()
})
})
describe('update', () => {
it('throws when deactivating a system status', async () => {
const system = await UnitStatusService.getBySlug(app.db, TEST_COMPANY_ID, 'sold')
expect(() => UnitStatusService.update(app.db, TEST_COMPANY_ID, system!.id, { isActive: false })).toThrow('system')
})
it('allows renaming a custom status', async () => {
const custom = await UnitStatusService.create(app.db, TEST_COMPANY_ID, {
name: 'Old Name',
slug: 'custom_rename',
sortOrder: 0,
})
const updated = await UnitStatusService.update(app.db, TEST_COMPANY_ID, custom.id, { name: 'New Name' })
expect(updated!.name).toBe('New Name')
})
})
})
describe('ItemConditionService', () => {
let app: FastifyInstance
beforeAll(async () => {
app = await createTestApp()
})
beforeEach(async () => {
await cleanDb(app)
await seedTestCompany(app)
})
afterAll(async () => {
await app.close()
})
it('seeds system conditions', async () => {
const conditions = await ItemConditionService.list(app.db, TEST_COMPANY_ID)
const slugs = conditions.map((c) => c.slug)
expect(slugs).toContain('new')
expect(slugs).toContain('excellent')
expect(slugs).toContain('good')
expect(slugs).toContain('fair')
expect(slugs).toContain('poor')
})
it('creates a custom condition', async () => {
const custom = await ItemConditionService.create(app.db, TEST_COMPANY_ID, {
name: 'Refurbished',
slug: 'refurbished',
sortOrder: 10,
})
expect(custom.isSystem).toBe(false)
expect(await ItemConditionService.validateSlug(app.db, TEST_COMPANY_ID, 'refurbished')).toBe(true)
})
})