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:
148
packages/backend/src/services/account.service.ts
Normal file
148
packages/backend/src/services/account.service.ts
Normal 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
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user