Add accounts, members, and processor-agnostic payment linking

- account table (billing entity, soft-delete, company-scoped)
- member table (people on an account, is_minor from DOB)
- account_processor_link table (maps accounts to any payment
  processor — stripe, global_payments — instead of stripe_customer_id
  directly on account)
- Full CRUD routes + search (name, email, phone, account_number)
- Member routes nested under accounts with isMinor auto-calculation
- Zod validation schemas in @forte/shared
- 19 tests passing
This commit is contained in:
Ryan Moon
2026-03-27 17:41:33 -05:00
parent 979a9a2c00
commit 5ff31ad782
12 changed files with 1429 additions and 1 deletions

View File

@@ -0,0 +1,148 @@
import { eq, and, or, ilike } from 'drizzle-orm'
import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
import { accounts, members } from '../db/schema/accounts.js'
import type { AccountCreateInput, AccountUpdateInput } from '@forte/shared/schemas'
import { isMinor } from '@forte/shared/utils'
export const AccountService = {
async create(db: PostgresJsDatabase, companyId: string, input: AccountCreateInput) {
const [account] = await db
.insert(accounts)
.values({
companyId,
name: input.name,
email: input.email,
phone: input.phone,
address: input.address,
billingMode: input.billingMode,
notes: input.notes,
})
.returning()
return account
},
async getById(db: PostgresJsDatabase, companyId: string, id: string) {
const [account] = await db
.select()
.from(accounts)
.where(and(eq(accounts.id, id), eq(accounts.companyId, companyId)))
.limit(1)
return account ?? null
},
async update(db: PostgresJsDatabase, companyId: string, id: string, input: AccountUpdateInput) {
const [account] = await db
.update(accounts)
.set({ ...input, updatedAt: new Date() })
.where(and(eq(accounts.id, id), eq(accounts.companyId, companyId)))
.returning()
return account ?? null
},
async softDelete(db: PostgresJsDatabase, companyId: string, id: string) {
const [account] = await db
.update(accounts)
.set({ isActive: false, updatedAt: new Date() })
.where(and(eq(accounts.id, id), eq(accounts.companyId, companyId)))
.returning()
return account ?? null
},
async search(db: PostgresJsDatabase, companyId: string, query: string) {
const pattern = `%${query}%`
const results = await db
.select()
.from(accounts)
.where(
and(
eq(accounts.companyId, companyId),
eq(accounts.isActive, true),
or(
ilike(accounts.name, pattern),
ilike(accounts.email, pattern),
ilike(accounts.phone, pattern),
ilike(accounts.accountNumber, pattern),
),
),
)
.limit(50)
return results
},
async list(db: PostgresJsDatabase, companyId: string) {
return db
.select()
.from(accounts)
.where(and(eq(accounts.companyId, companyId), eq(accounts.isActive, true)))
.limit(100)
},
}
export const MemberService = {
async create(db: PostgresJsDatabase, companyId: string, input: { accountId: string; firstName: string; lastName: string; dateOfBirth?: string; email?: string; phone?: string; notes?: string }) {
const minor = input.dateOfBirth ? isMinor(input.dateOfBirth) : false
const [member] = await db
.insert(members)
.values({
companyId,
accountId: input.accountId,
firstName: input.firstName,
lastName: input.lastName,
dateOfBirth: input.dateOfBirth,
isMinor: minor,
email: input.email,
phone: input.phone,
notes: input.notes,
})
.returning()
return member
},
async getById(db: PostgresJsDatabase, companyId: string, id: string) {
const [member] = await db
.select()
.from(members)
.where(and(eq(members.id, id), eq(members.companyId, companyId)))
.limit(1)
return member ?? null
},
async listByAccount(db: PostgresJsDatabase, companyId: string, accountId: string) {
return db
.select()
.from(members)
.where(and(eq(members.companyId, companyId), eq(members.accountId, accountId)))
},
async update(db: PostgresJsDatabase, companyId: string, id: string, input: { firstName?: string; lastName?: string; dateOfBirth?: string; email?: string; phone?: string; notes?: string }) {
const updates: Record<string, unknown> = { ...input, updatedAt: new Date() }
if (input.dateOfBirth) {
updates.isMinor = isMinor(input.dateOfBirth)
}
const [member] = await db
.update(members)
.set(updates)
.where(and(eq(members.id, id), eq(members.companyId, companyId)))
.returning()
return member ?? null
},
async delete(db: PostgresJsDatabase, companyId: string, id: string) {
const [member] = await db
.delete(members)
.where(and(eq(members.id, id), eq(members.companyId, companyId)))
.returning()
return member ?? null
},
}