Add lookup tables, payment methods, tax exemptions, and processor link APIs

Replace unit_status and item_condition pgEnums with company-scoped lookup
tables that support custom values. Add account_payment_method table,
tax_exemption table with approve/revoke workflow, and CRUD routes for
processor links. Validate inventory unit status/condition against lookup
tables at service layer.
This commit is contained in:
Ryan Moon
2026-03-27 20:53:30 -05:00
parent e7853f59f2
commit 0a2d6e23af
17 changed files with 1431 additions and 28 deletions

View File

@@ -1,7 +1,23 @@
import { eq, and, sql, count } from 'drizzle-orm'
import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
import { accounts, members } from '../db/schema/accounts.js'
import type { AccountCreateInput, AccountUpdateInput, PaginationInput } from '@forte/shared/schemas'
import {
accounts,
members,
accountProcessorLinks,
accountPaymentMethods,
taxExemptions,
} from '../db/schema/accounts.js'
import type {
AccountCreateInput,
AccountUpdateInput,
ProcessorLinkCreateInput,
ProcessorLinkUpdateInput,
PaymentMethodCreateInput,
PaymentMethodUpdateInput,
TaxExemptionCreateInput,
TaxExemptionUpdateInput,
PaginationInput,
} from '@forte/shared/schemas'
import { isMinor } from '@forte/shared/utils'
import {
withPagination,
@@ -193,3 +209,223 @@ export const MemberService = {
return member ?? null
},
}
export const ProcessorLinkService = {
async create(db: PostgresJsDatabase, companyId: string, input: ProcessorLinkCreateInput) {
const [link] = await db
.insert(accountProcessorLinks)
.values({
companyId,
accountId: input.accountId,
processor: input.processor,
processorCustomerId: input.processorCustomerId,
})
.returning()
return link
},
async getById(db: PostgresJsDatabase, companyId: string, id: string) {
const [link] = await db
.select()
.from(accountProcessorLinks)
.where(and(eq(accountProcessorLinks.id, id), eq(accountProcessorLinks.companyId, companyId)))
.limit(1)
return link ?? null
},
async listByAccount(db: PostgresJsDatabase, companyId: string, accountId: string) {
return db
.select()
.from(accountProcessorLinks)
.where(
and(
eq(accountProcessorLinks.companyId, companyId),
eq(accountProcessorLinks.accountId, accountId),
),
)
},
async update(db: PostgresJsDatabase, companyId: string, id: string, input: ProcessorLinkUpdateInput) {
const [link] = await db
.update(accountProcessorLinks)
.set(input)
.where(and(eq(accountProcessorLinks.id, id), eq(accountProcessorLinks.companyId, companyId)))
.returning()
return link ?? null
},
async delete(db: PostgresJsDatabase, companyId: string, id: string) {
const [link] = await db
.delete(accountProcessorLinks)
.where(and(eq(accountProcessorLinks.id, id), eq(accountProcessorLinks.companyId, companyId)))
.returning()
return link ?? null
},
}
export const PaymentMethodService = {
async create(db: PostgresJsDatabase, companyId: string, input: PaymentMethodCreateInput) {
// If this is the default, unset any existing default for this account
if (input.isDefault) {
await db
.update(accountPaymentMethods)
.set({ isDefault: false })
.where(
and(
eq(accountPaymentMethods.companyId, companyId),
eq(accountPaymentMethods.accountId, input.accountId),
eq(accountPaymentMethods.isDefault, true),
),
)
}
const [method] = await db
.insert(accountPaymentMethods)
.values({
companyId,
accountId: input.accountId,
processor: input.processor,
processorPaymentMethodId: input.processorPaymentMethodId,
cardBrand: input.cardBrand,
lastFour: input.lastFour,
expMonth: input.expMonth,
expYear: input.expYear,
isDefault: input.isDefault,
})
.returning()
return method
},
async getById(db: PostgresJsDatabase, companyId: string, id: string) {
const [method] = await db
.select()
.from(accountPaymentMethods)
.where(and(eq(accountPaymentMethods.id, id), eq(accountPaymentMethods.companyId, companyId)))
.limit(1)
return method ?? null
},
async listByAccount(db: PostgresJsDatabase, companyId: string, accountId: string) {
return db
.select()
.from(accountPaymentMethods)
.where(
and(
eq(accountPaymentMethods.companyId, companyId),
eq(accountPaymentMethods.accountId, accountId),
),
)
},
async update(db: PostgresJsDatabase, companyId: string, id: string, input: PaymentMethodUpdateInput) {
// If setting as default, unset existing default
if (input.isDefault) {
const existing = await this.getById(db, companyId, id)
if (existing) {
await db
.update(accountPaymentMethods)
.set({ isDefault: false })
.where(
and(
eq(accountPaymentMethods.companyId, companyId),
eq(accountPaymentMethods.accountId, existing.accountId),
eq(accountPaymentMethods.isDefault, true),
),
)
}
}
const [method] = await db
.update(accountPaymentMethods)
.set(input)
.where(and(eq(accountPaymentMethods.id, id), eq(accountPaymentMethods.companyId, companyId)))
.returning()
return method ?? null
},
async delete(db: PostgresJsDatabase, companyId: string, id: string) {
const [method] = await db
.delete(accountPaymentMethods)
.where(and(eq(accountPaymentMethods.id, id), eq(accountPaymentMethods.companyId, companyId)))
.returning()
return method ?? null
},
}
export const TaxExemptionService = {
async create(db: PostgresJsDatabase, companyId: string, input: TaxExemptionCreateInput) {
const [exemption] = await db
.insert(taxExemptions)
.values({
companyId,
accountId: input.accountId,
certificateNumber: input.certificateNumber,
certificateType: input.certificateType,
issuingState: input.issuingState,
expiresAt: input.expiresAt,
notes: input.notes,
status: 'pending',
})
.returning()
return exemption
},
async getById(db: PostgresJsDatabase, companyId: string, id: string) {
const [exemption] = await db
.select()
.from(taxExemptions)
.where(and(eq(taxExemptions.id, id), eq(taxExemptions.companyId, companyId)))
.limit(1)
return exemption ?? null
},
async listByAccount(db: PostgresJsDatabase, companyId: string, accountId: string) {
return db
.select()
.from(taxExemptions)
.where(
and(
eq(taxExemptions.companyId, companyId),
eq(taxExemptions.accountId, accountId),
),
)
},
async update(db: PostgresJsDatabase, companyId: string, id: string, input: TaxExemptionUpdateInput) {
const [exemption] = await db
.update(taxExemptions)
.set({ ...input, updatedAt: new Date() })
.where(and(eq(taxExemptions.id, id), eq(taxExemptions.companyId, companyId)))
.returning()
return exemption ?? null
},
async approve(db: PostgresJsDatabase, companyId: string, id: string, approvedBy: string) {
const [exemption] = await db
.update(taxExemptions)
.set({
status: 'approved',
approvedBy,
approvedAt: new Date(),
updatedAt: new Date(),
})
.where(and(eq(taxExemptions.id, id), eq(taxExemptions.companyId, companyId)))
.returning()
return exemption ?? null
},
async revoke(db: PostgresJsDatabase, companyId: string, id: string, revokedBy: string, reason: string) {
const [exemption] = await db
.update(taxExemptions)
.set({
status: 'none',
revokedBy,
revokedAt: new Date(),
revokedReason: reason,
updatedAt: new Date(),
})
.where(and(eq(taxExemptions.id, id), eq(taxExemptions.companyId, companyId)))
.returning()
return exemption ?? null
},
}