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