diff --git a/packages/backend/api-tests/suites/accounts.ts b/packages/backend/api-tests/suites/accounts.ts index 2b19cc7..bb7ec56 100644 --- a/packages/backend/api-tests/suites/accounts.ts +++ b/packages/backend/api-tests/suites/accounts.ts @@ -100,4 +100,61 @@ suite('Accounts', { tags: ['accounts', 'crud'] }, (t) => { const refreshed = await t.api.get(`/v1/accounts/${acct.data.id}`) t.assert.equal(refreshed.data.primaryMemberId, first.data.id) }) + + t.test('searches by email', { tags: ['search'] }, async () => { + await t.api.post('/v1/accounts', { name: 'Email Search', email: 'findme-unique@test.com' }) + + const res = await t.api.get('/v1/accounts', { q: 'findme-unique@test' }) + t.assert.status(res, 200) + t.assert.ok(res.data.data.some((a: { email: string }) => a.email === 'findme-unique@test.com')) + }) + + t.test('searches by phone', { tags: ['search'] }, async () => { + await t.api.post('/v1/accounts', { name: 'Phone Search', phone: '555-UNIQUE-9876' }) + + const res = await t.api.get('/v1/accounts', { q: '555-UNIQUE-9876' }) + t.assert.status(res, 200) + t.assert.ok(res.data.data.some((a: { phone: string }) => a.phone === '555-UNIQUE-9876')) + }) + + t.test('paginates with page and limit', { tags: ['read', 'pagination'] }, async () => { + for (let i = 0; i < 5; i++) { + await t.api.post('/v1/accounts', { name: `Paginate ${i}` }) + } + + const page1 = await t.api.get('/v1/accounts', { page: 1, limit: 2 }) + t.assert.status(page1, 200) + t.assert.equal(page1.data.data.length, 2) + t.assert.greaterThan(page1.data.pagination.totalPages, 1) + + const page2 = await t.api.get('/v1/accounts', { page: 2, limit: 2 }) + t.assert.status(page2, 200) + t.assert.equal(page2.data.data.length, 2) + t.assert.notEqual(page1.data.data[0].id, page2.data.data[0].id) + }) + + t.test('sorts by name descending', { tags: ['read', 'sort'] }, async () => { + await t.api.post('/v1/accounts', { name: 'AAA Sort First' }) + await t.api.post('/v1/accounts', { name: 'ZZZ Sort Last' }) + + const res = await t.api.get('/v1/accounts', { sort: 'name', order: 'desc', limit: 100 }) + t.assert.status(res, 200) + const names = res.data.data.map((a: { name: string }) => a.name) + const zIdx = names.findIndex((n: string) => n.includes('ZZZ')) + const aIdx = names.findIndex((n: string) => n.includes('AAA')) + t.assert.ok(zIdx < aIdx, 'ZZZ should come before AAA in desc order') + }) + + t.test('creates account with billing mode split', { tags: ['create'] }, async () => { + const res = await t.api.post('/v1/accounts', { name: 'Split Billing', billingMode: 'split' }) + t.assert.status(res, 201) + t.assert.equal(res.data.billingMode, 'split') + }) + + t.test('updates billing mode', { tags: ['update'] }, async () => { + const created = await t.api.post('/v1/accounts', { name: 'Change Billing' }) + const res = await t.api.patch(`/v1/accounts/${created.data.id}`, { billingMode: 'split' }) + t.assert.status(res, 200) + t.assert.equal(res.data.billingMode, 'split') + }) }) diff --git a/packages/backend/api-tests/suites/members.ts b/packages/backend/api-tests/suites/members.ts index 0b3e540..37ad4ba 100644 --- a/packages/backend/api-tests/suites/members.ts +++ b/packages/backend/api-tests/suites/members.ts @@ -163,4 +163,69 @@ suite('Members', { tags: ['members', 'crud'] }, (t) => { const res = await t.api.del(`/v1/members/${member.data.id}`) t.assert.status(res, 200) }) + + t.test('lists members for a specific account', { tags: ['read'] }, async () => { + const acct1 = await t.api.post('/v1/accounts', { name: 'Scoped List A' }) + const acct2 = await t.api.post('/v1/accounts', { name: 'Scoped List B' }) + await t.api.post(`/v1/accounts/${acct1.data.id}/members`, { firstName: 'In', lastName: 'Scope' }) + await t.api.post(`/v1/accounts/${acct2.data.id}/members`, { firstName: 'Out', lastName: 'Scope' }) + + const res = await t.api.get(`/v1/accounts/${acct1.data.id}/members`) + t.assert.status(res, 200) + t.assert.equal(res.data.data.length, 1) + t.assert.equal(res.data.data[0].firstName, 'In') + }) + + t.test('derives isMinor from child DOB', { tags: ['create'] }, async () => { + const acct = await t.api.post('/v1/accounts', { name: 'Minor DOB Test' }) + const res = await t.api.post(`/v1/accounts/${acct.data.id}/members`, { + firstName: 'Young', + lastName: 'Kid', + dateOfBirth: '2020-01-01', + }) + t.assert.status(res, 201) + t.assert.equal(res.data.isMinor, true) + }) + + t.test('derives isMinor as false from adult DOB', { tags: ['create'] }, async () => { + const acct = await t.api.post('/v1/accounts', { name: 'Adult DOB Test' }) + const res = await t.api.post(`/v1/accounts/${acct.data.id}/members`, { + firstName: 'Grown', + lastName: 'Up', + dateOfBirth: '1985-06-15', + }) + t.assert.status(res, 201) + t.assert.equal(res.data.isMinor, false) + }) + + t.test('explicit isMinor overrides DOB calculation', { tags: ['create'] }, async () => { + const acct = await t.api.post('/v1/accounts', { name: 'Override Minor' }) + const res = await t.api.post(`/v1/accounts/${acct.data.id}/members`, { + firstName: 'Override', + lastName: 'Test', + dateOfBirth: '1985-06-15', + isMinor: true, + }) + t.assert.status(res, 201) + t.assert.equal(res.data.isMinor, true) + }) + + t.test('update recalculates isMinor when DOB changes', { tags: ['update'] }, async () => { + const acct = await t.api.post('/v1/accounts', { name: 'Recalc Test' }) + const member = await t.api.post(`/v1/accounts/${acct.data.id}/members`, { + firstName: 'Age', + lastName: 'Change', + dateOfBirth: '2020-01-01', + }) + t.assert.equal(member.data.isMinor, true) + + const res = await t.api.patch(`/v1/members/${member.data.id}`, { dateOfBirth: '1980-01-01' }) + t.assert.status(res, 200) + t.assert.equal(res.data.isMinor, false) + }) + + t.test('returns 404 for missing member', { tags: ['read'] }, async () => { + const res = await t.api.get('/v1/members/a0000000-0000-0000-0000-999999999999') + t.assert.status(res, 404) + }) })