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,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()
})
})