Remove multi-tenant company_id scoping from entire codebase

Drop company_id column from all 22 domain tables via migration.
Remove companyId from JWT payload, auth plugins, all service method
signatures (~215 occurrences), all route handlers (~105 occurrences),
test runner, test suites, and frontend auth store/types.

The company table stays as store settings (name, timezone). Tenant
isolation in a SaaS deployment would be at the database level (one
DB per customer) not the application level.

All 107 API tests pass. Zero TSC errors across all packages.
This commit is contained in:
Ryan Moon
2026-03-29 14:58:33 -05:00
parent 55f8591cf1
commit d36c6f7135
35 changed files with 353 additions and 511 deletions

View File

@@ -31,19 +31,19 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
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, request.companyId, parsed.data)
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, request.companyId, params)
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, request.companyId, id)
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)
})
@@ -54,14 +54,14 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
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, request.companyId, id, parsed.data)
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, request.companyId, id)
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)
@@ -71,7 +71,7 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
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, request.companyId, params)
const result = await MemberService.list(app.db, params)
return reply.send(result)
})
@@ -83,20 +83,20 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
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, request.companyId, parsed.data)
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, request.companyId, accountId, params)
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, request.companyId, id)
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)
})
@@ -107,7 +107,7 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
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, request.companyId, id, parsed.data)
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)
})
@@ -120,16 +120,16 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
// If no accountId provided, create a new account from the member's name
if (!targetAccountId) {
const member = await MemberService.getById(app.db, request.companyId, id)
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, request.companyId, {
const account = await AccountService.create(app.db, {
name: `${member.firstName} ${member.lastName}`,
billingMode: 'consolidated',
})
targetAccountId = account.id
}
const member = await MemberService.move(app.db, request.companyId, id, targetAccountId)
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)
@@ -143,14 +143,14 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
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, request.companyId, parsed.data)
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, request.companyId, memberId, params)
const result = await MemberIdentifierService.listByMember(app.db, memberId, params)
return reply.send(result)
})
@@ -160,21 +160,21 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
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, request.companyId, id, parsed.data)
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, request.companyId, id)
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, request.companyId, id)
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)
})
@@ -187,14 +187,14 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
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, request.companyId, parsed.data)
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, request.companyId, accountId, params)
const result = await ProcessorLinkService.listByAccount(app.db, accountId, params)
return reply.send(result)
})
@@ -204,14 +204,14 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
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, request.companyId, id, parsed.data)
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, request.companyId, id)
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)
})
@@ -224,20 +224,20 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
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, request.companyId, parsed.data)
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, request.companyId, accountId, params)
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, request.companyId, id)
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)
})
@@ -248,14 +248,14 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
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, request.companyId, id, parsed.data)
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, request.companyId, id)
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)
})
@@ -268,20 +268,20 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
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, request.companyId, parsed.data)
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, request.companyId, accountId, params)
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, request.companyId, id)
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)
})
@@ -292,14 +292,14 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
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, request.companyId, id, parsed.data)
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, request.companyId, id, request.user.id)
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)
@@ -311,7 +311,7 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
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, request.companyId, id, request.user.id, reason)
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)