import type { FastifyPluginAsync } from 'fastify' import { AccountCreateSchema, AccountUpdateSchema, MemberCreateSchema, MemberUpdateSchema, PaginationSchema, ProcessorLinkCreateSchema, ProcessorLinkUpdateSchema, PaymentMethodCreateSchema, PaymentMethodUpdateSchema, TaxExemptionCreateSchema, TaxExemptionUpdateSchema, MemberIdentifierCreateSchema, MemberIdentifierUpdateSchema, } from '@forte/shared/schemas' import { AccountService, MemberService, MemberIdentifierService, ProcessorLinkService, PaymentMethodService, TaxExemptionService, } from '../../services/account.service.js' export const accountRoutes: FastifyPluginAsync = async (app) => { // --- Accounts --- app.post('/accounts', { preHandler: [app.authenticate, app.requirePermission('accounts.edit')] }, async (request, reply) => { const parsed = AccountCreateSchema.safeParse(request.body) if (!parsed.success) { return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } }) } const account = await AccountService.create(app.db, parsed.data) return reply.status(201).send(account) }) app.get('/accounts', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => { const params = PaginationSchema.parse(request.query) const result = await AccountService.list(app.db, params) return reply.send(result) }) app.get('/accounts/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => { const { id } = request.params as { id: string } const account = await AccountService.getById(app.db, id) if (!account) return reply.status(404).send({ error: { message: 'Account not found', statusCode: 404 } }) return reply.send(account) }) app.patch('/accounts/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.edit')] }, async (request, reply) => { const { id } = request.params as { id: string } const parsed = AccountUpdateSchema.safeParse(request.body) if (!parsed.success) { return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } }) } const account = await AccountService.update(app.db, id, parsed.data) if (!account) return reply.status(404).send({ error: { message: 'Account not found', statusCode: 404 } }) return reply.send(account) }) app.delete('/accounts/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.admin')] }, async (request, reply) => { const { id } = request.params as { id: string } const account = await AccountService.softDelete(app.db, id) if (!account) return reply.status(404).send({ error: { message: 'Account not found', statusCode: 404 } }) request.log.info({ accountId: id, userId: request.user.id }, 'Account soft-deleted') return reply.send(account) }) // --- Members (top-level) --- app.get('/members', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => { const params = PaginationSchema.parse(request.query) const result = await MemberService.list(app.db, params) return reply.send(result) }) // --- Members (scoped to account) --- app.post('/accounts/:accountId/members', { preHandler: [app.authenticate, app.requirePermission('accounts.edit')] }, async (request, reply) => { const { accountId } = request.params as { accountId: string } const parsed = MemberCreateSchema.safeParse({ ...(request.body as object), accountId }) if (!parsed.success) { return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } }) } const member = await MemberService.create(app.db, parsed.data) return reply.status(201).send(member) }) app.get('/accounts/:accountId/members', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => { const { accountId } = request.params as { accountId: string } const params = PaginationSchema.parse(request.query) const result = await MemberService.listByAccount(app.db, accountId, params) return reply.send(result) }) app.get('/members/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => { const { id } = request.params as { id: string } const member = await MemberService.getById(app.db, id) if (!member) return reply.status(404).send({ error: { message: 'Member not found', statusCode: 404 } }) return reply.send(member) }) app.patch('/members/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.edit')] }, async (request, reply) => { const { id } = request.params as { id: string } const parsed = MemberUpdateSchema.safeParse(request.body) if (!parsed.success) { return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } }) } const member = await MemberService.update(app.db, id, parsed.data) if (!member) return reply.status(404).send({ error: { message: 'Member not found', statusCode: 404 } }) return reply.send(member) }) app.post('/members/:id/move', { preHandler: [app.authenticate, app.requirePermission('accounts.admin')] }, async (request, reply) => { const { id } = request.params as { id: string } const { accountId } = (request.body as { accountId?: string }) ?? {} let targetAccountId = accountId // If no accountId provided, create a new account from the member's name if (!targetAccountId) { const member = await MemberService.getById(app.db, id) if (!member) return reply.status(404).send({ error: { message: 'Member not found', statusCode: 404 } }) const account = await AccountService.create(app.db, { name: `${member.firstName} ${member.lastName}`, billingMode: 'consolidated', }) targetAccountId = account.id } const member = await MemberService.move(app.db, id, targetAccountId) if (!member) return reply.status(404).send({ error: { message: 'Member not found', statusCode: 404 } }) request.log.info({ memberId: id, targetAccountId, userId: request.user.id }, 'Member moved to account') return reply.send(member) }) // --- Member Identifiers --- app.post('/members/:memberId/identifiers', { preHandler: [app.authenticate, app.requirePermission('accounts.edit')] }, async (request, reply) => { const { memberId } = request.params as { memberId: string } const parsed = MemberIdentifierCreateSchema.safeParse({ ...(request.body as object), memberId }) if (!parsed.success) { return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } }) } const identifier = await MemberIdentifierService.create(app.db, parsed.data) return reply.status(201).send(identifier) }) app.get('/members/:memberId/identifiers', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => { const { memberId } = request.params as { memberId: string } const params = PaginationSchema.parse(request.query) const result = await MemberIdentifierService.listByMember(app.db, memberId, params) return reply.send(result) }) app.patch('/identifiers/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.edit')] }, async (request, reply) => { const { id } = request.params as { id: string } const parsed = MemberIdentifierUpdateSchema.safeParse(request.body) if (!parsed.success) { return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } }) } const identifier = await MemberIdentifierService.update(app.db, id, parsed.data) if (!identifier) return reply.status(404).send({ error: { message: 'Identifier not found', statusCode: 404 } }) return reply.send(identifier) }) app.delete('/identifiers/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.admin')] }, async (request, reply) => { const { id } = request.params as { id: string } const identifier = await MemberIdentifierService.delete(app.db, id) if (!identifier) return reply.status(404).send({ error: { message: 'Identifier not found', statusCode: 404 } }) return reply.send(identifier) }) app.delete('/members/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.admin')] }, async (request, reply) => { const { id } = request.params as { id: string } const member = await MemberService.delete(app.db, id) if (!member) return reply.status(404).send({ error: { message: 'Member not found', statusCode: 404 } }) return reply.send(member) }) // --- Processor Links --- app.post('/accounts/:accountId/processor-links', { preHandler: [app.authenticate, app.requirePermission('accounts.edit')] }, async (request, reply) => { const { accountId } = request.params as { accountId: string } const parsed = ProcessorLinkCreateSchema.safeParse({ ...(request.body as object), accountId }) if (!parsed.success) { return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } }) } const link = await ProcessorLinkService.create(app.db, parsed.data) return reply.status(201).send(link) }) app.get('/accounts/:accountId/processor-links', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => { const { accountId } = request.params as { accountId: string } const params = PaginationSchema.parse(request.query) const result = await ProcessorLinkService.listByAccount(app.db, accountId, params) return reply.send(result) }) app.patch('/processor-links/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.edit')] }, async (request, reply) => { const { id } = request.params as { id: string } const parsed = ProcessorLinkUpdateSchema.safeParse(request.body) if (!parsed.success) { return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } }) } const link = await ProcessorLinkService.update(app.db, id, parsed.data) if (!link) return reply.status(404).send({ error: { message: 'Processor link not found', statusCode: 404 } }) return reply.send(link) }) app.delete('/processor-links/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.admin')] }, async (request, reply) => { const { id } = request.params as { id: string } const link = await ProcessorLinkService.delete(app.db, id) if (!link) return reply.status(404).send({ error: { message: 'Processor link not found', statusCode: 404 } }) return reply.send(link) }) // --- Payment Methods --- app.post('/accounts/:accountId/payment-methods', { preHandler: [app.authenticate, app.requirePermission('accounts.edit')] }, async (request, reply) => { const { accountId } = request.params as { accountId: string } const parsed = PaymentMethodCreateSchema.safeParse({ ...(request.body as object), accountId }) if (!parsed.success) { return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } }) } const method = await PaymentMethodService.create(app.db, parsed.data) return reply.status(201).send(method) }) app.get('/accounts/:accountId/payment-methods', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => { const { accountId } = request.params as { accountId: string } const params = PaginationSchema.parse(request.query) const result = await PaymentMethodService.listByAccount(app.db, accountId, params) return reply.send(result) }) app.get('/payment-methods/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => { const { id } = request.params as { id: string } const method = await PaymentMethodService.getById(app.db, id) if (!method) return reply.status(404).send({ error: { message: 'Payment method not found', statusCode: 404 } }) return reply.send(method) }) app.patch('/payment-methods/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.edit')] }, async (request, reply) => { const { id } = request.params as { id: string } const parsed = PaymentMethodUpdateSchema.safeParse(request.body) if (!parsed.success) { return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } }) } const method = await PaymentMethodService.update(app.db, id, parsed.data) if (!method) return reply.status(404).send({ error: { message: 'Payment method not found', statusCode: 404 } }) return reply.send(method) }) app.delete('/payment-methods/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.admin')] }, async (request, reply) => { const { id } = request.params as { id: string } const method = await PaymentMethodService.delete(app.db, id) if (!method) return reply.status(404).send({ error: { message: 'Payment method not found', statusCode: 404 } }) return reply.send(method) }) // --- Tax Exemptions --- app.post('/accounts/:accountId/tax-exemptions', { preHandler: [app.authenticate, app.requirePermission('accounts.edit')] }, async (request, reply) => { const { accountId } = request.params as { accountId: string } const parsed = TaxExemptionCreateSchema.safeParse({ ...(request.body as object), accountId }) if (!parsed.success) { return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } }) } const exemption = await TaxExemptionService.create(app.db, parsed.data) return reply.status(201).send(exemption) }) app.get('/accounts/:accountId/tax-exemptions', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => { const { accountId } = request.params as { accountId: string } const params = PaginationSchema.parse(request.query) const result = await TaxExemptionService.listByAccount(app.db, accountId, params) return reply.send(result) }) app.get('/tax-exemptions/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => { const { id } = request.params as { id: string } const exemption = await TaxExemptionService.getById(app.db, id) if (!exemption) return reply.status(404).send({ error: { message: 'Tax exemption not found', statusCode: 404 } }) return reply.send(exemption) }) app.patch('/tax-exemptions/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.edit')] }, async (request, reply) => { const { id } = request.params as { id: string } const parsed = TaxExemptionUpdateSchema.safeParse(request.body) if (!parsed.success) { return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } }) } const exemption = await TaxExemptionService.update(app.db, id, parsed.data) if (!exemption) return reply.status(404).send({ error: { message: 'Tax exemption not found', statusCode: 404 } }) return reply.send(exemption) }) app.post('/tax-exemptions/:id/approve', { preHandler: [app.authenticate, app.requirePermission('accounts.admin')] }, async (request, reply) => { const { id } = request.params as { id: string } const exemption = await TaxExemptionService.approve(app.db, id, request.user.id) if (!exemption) return reply.status(404).send({ error: { message: 'Tax exemption not found', statusCode: 404 } }) request.log.info({ exemptionId: id, accountId: exemption.accountId, userId: request.user.id }, 'Tax exemption approved') return reply.send(exemption) }) app.post('/tax-exemptions/:id/revoke', { preHandler: [app.authenticate, app.requirePermission('accounts.admin')] }, async (request, reply) => { const { id } = request.params as { id: string } const { reason } = (request.body as { reason?: string }) ?? {} if (!reason) { return reply.status(400).send({ error: { message: 'Reason is required to revoke a tax exemption', statusCode: 400 } }) } const exemption = await TaxExemptionService.revoke(app.db, id, request.user.id, reason) if (!exemption) return reply.status(404).send({ error: { message: 'Tax exemption not found', statusCode: 404 } }) request.log.warn({ exemptionId: id, accountId: exemption.accountId, userId: request.user.id, reason }, 'Tax exemption revoked') return reply.send(exemption) }) }