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.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) }) }) })