- StorageProvider interface with LocalProvider (S3 placeholder) - File table with entity_type/entity_id references, content type, path - POST /v1/files (multipart upload), GET /v1/files (list by entity), GET /v1/files/:id (metadata), GET /v1/files/serve/* (content), DELETE /v1/files/:id - member_identifier drops base64 columns, uses file_id FKs - File validation: type whitelist, size limits, per-entity max - Fastify storage plugin injects provider into app - 6 API tests for upload, list, get, delete, validation - Test runner kills stale port before starting backend
672 lines
20 KiB
TypeScript
672 lines
20 KiB
TypeScript
import { eq, and, sql, count, exists } from 'drizzle-orm'
|
|
import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
|
|
import {
|
|
accounts,
|
|
members,
|
|
memberIdentifiers,
|
|
accountProcessorLinks,
|
|
accountPaymentMethods,
|
|
taxExemptions,
|
|
} from '../db/schema/accounts.js'
|
|
import type {
|
|
AccountCreateInput,
|
|
AccountUpdateInput,
|
|
MemberIdentifierCreateInput,
|
|
MemberIdentifierUpdateInput,
|
|
ProcessorLinkCreateInput,
|
|
ProcessorLinkUpdateInput,
|
|
PaymentMethodCreateInput,
|
|
PaymentMethodUpdateInput,
|
|
TaxExemptionCreateInput,
|
|
TaxExemptionUpdateInput,
|
|
PaginationInput,
|
|
} from '@forte/shared/schemas'
|
|
import { isMinor, normalizeStateCode } from '@forte/shared/utils'
|
|
import {
|
|
withPagination,
|
|
withSort,
|
|
buildSearchCondition,
|
|
paginatedResponse,
|
|
} from '../utils/pagination.js'
|
|
|
|
async function generateUniqueNumber(
|
|
db: PostgresJsDatabase,
|
|
table: typeof accounts | typeof members,
|
|
column: typeof accounts.accountNumber | typeof members.memberNumber,
|
|
companyId: string,
|
|
companyIdColumn: typeof accounts.companyId,
|
|
): Promise<string> {
|
|
for (let attempt = 0; attempt < 10; attempt++) {
|
|
const num = String(Math.floor(100000 + Math.random() * 900000))
|
|
const [existing] = await db
|
|
.select({ id: table.id })
|
|
.from(table)
|
|
.where(and(eq(companyIdColumn, companyId), eq(column, num)))
|
|
.limit(1)
|
|
if (!existing) return num
|
|
}
|
|
// Fallback to 8 digits if 6-digit space is crowded
|
|
return String(Math.floor(10000000 + Math.random() * 90000000))
|
|
}
|
|
|
|
function normalizeAddress(address?: { street?: string; city?: string; state?: string; zip?: string } | null) {
|
|
if (!address) return address
|
|
return {
|
|
...address,
|
|
state: address.state ? (normalizeStateCode(address.state) ?? address.state) : address.state,
|
|
}
|
|
}
|
|
|
|
export const AccountService = {
|
|
async create(db: PostgresJsDatabase, companyId: string, input: AccountCreateInput) {
|
|
const accountNumber = await generateUniqueNumber(db, accounts, accounts.accountNumber, companyId, accounts.companyId)
|
|
|
|
const [account] = await db
|
|
.insert(accounts)
|
|
.values({
|
|
companyId,
|
|
accountNumber,
|
|
name: input.name,
|
|
email: input.email,
|
|
phone: input.phone,
|
|
address: normalizeAddress(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 list(db: PostgresJsDatabase, companyId: string, params: PaginationInput) {
|
|
const baseWhere = and(eq(accounts.companyId, companyId), eq(accounts.isActive, true))
|
|
|
|
const accountSearch = params.q
|
|
? buildSearchCondition(params.q, [accounts.name, accounts.email, accounts.phone, accounts.accountNumber])
|
|
: undefined
|
|
|
|
// Also search across member names on this account
|
|
const memberSearch = params.q
|
|
? exists(
|
|
db.select({ id: members.id })
|
|
.from(members)
|
|
.where(and(
|
|
eq(members.accountId, accounts.id),
|
|
buildSearchCondition(params.q, [members.firstName, members.lastName, members.email])!,
|
|
)),
|
|
)
|
|
: undefined
|
|
|
|
const searchCondition = accountSearch && memberSearch
|
|
? sql`(${accountSearch} OR ${memberSearch})`
|
|
: undefined
|
|
|
|
const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere
|
|
|
|
const sortableColumns: Record<string, typeof accounts.name> = {
|
|
name: accounts.name,
|
|
email: accounts.email,
|
|
created_at: accounts.createdAt,
|
|
account_number: accounts.accountNumber,
|
|
}
|
|
|
|
let query = db.select().from(accounts).where(where).$dynamic()
|
|
query = withSort(query, params.sort, params.order, sortableColumns, accounts.name)
|
|
query = withPagination(query, params.page, params.limit)
|
|
|
|
const [data, [{ total }]] = await Promise.all([
|
|
query,
|
|
db.select({ total: count() }).from(accounts).where(where),
|
|
])
|
|
|
|
return paginatedResponse(data, total, params.page, params.limit)
|
|
},
|
|
}
|
|
|
|
export const MemberService = {
|
|
async create(
|
|
db: PostgresJsDatabase,
|
|
companyId: string,
|
|
input: {
|
|
accountId: string
|
|
firstName: string
|
|
lastName: string
|
|
dateOfBirth?: string
|
|
isMinor?: boolean
|
|
email?: string
|
|
phone?: string
|
|
address?: { street?: string; city?: string; state?: string; zip?: string }
|
|
notes?: string
|
|
},
|
|
) {
|
|
// isMinor: explicit flag wins, else derive from DOB, else false
|
|
const minor = input.isMinor ?? (input.dateOfBirth ? isMinor(input.dateOfBirth) : false)
|
|
const memberNumber = await generateUniqueNumber(db, members, members.memberNumber, companyId, members.companyId)
|
|
|
|
// Inherit email, phone, address from account if not provided
|
|
const [account] = await db
|
|
.select()
|
|
.from(accounts)
|
|
.where(eq(accounts.id, input.accountId))
|
|
.limit(1)
|
|
|
|
const email = input.email ?? account?.email ?? undefined
|
|
const phone = input.phone ?? account?.phone ?? undefined
|
|
const address = normalizeAddress(input.address ?? account?.address ?? undefined)
|
|
|
|
const [member] = await db
|
|
.insert(members)
|
|
.values({
|
|
companyId,
|
|
memberNumber,
|
|
accountId: input.accountId,
|
|
firstName: input.firstName,
|
|
lastName: input.lastName,
|
|
dateOfBirth: input.dateOfBirth,
|
|
isMinor: minor,
|
|
email,
|
|
phone,
|
|
address,
|
|
notes: input.notes,
|
|
})
|
|
.returning()
|
|
if (account && !account.primaryMemberId) {
|
|
await db
|
|
.update(accounts)
|
|
.set({ primaryMemberId: member.id, updatedAt: new Date() })
|
|
.where(eq(accounts.id, input.accountId))
|
|
}
|
|
|
|
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 list(db: PostgresJsDatabase, companyId: string, params: PaginationInput) {
|
|
const baseWhere = eq(members.companyId, companyId)
|
|
|
|
const searchCondition = params.q
|
|
? buildSearchCondition(params.q, [members.firstName, members.lastName, members.email, members.phone])
|
|
: undefined
|
|
|
|
const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere
|
|
|
|
const sortableColumns: Record<string, typeof members.firstName> = {
|
|
first_name: members.firstName,
|
|
last_name: members.lastName,
|
|
email: members.email,
|
|
created_at: members.createdAt,
|
|
}
|
|
|
|
let query = db.select({
|
|
id: members.id,
|
|
accountId: members.accountId,
|
|
companyId: members.companyId,
|
|
firstName: members.firstName,
|
|
lastName: members.lastName,
|
|
dateOfBirth: members.dateOfBirth,
|
|
isMinor: members.isMinor,
|
|
email: members.email,
|
|
phone: members.phone,
|
|
notes: members.notes,
|
|
legacyId: members.legacyId,
|
|
createdAt: members.createdAt,
|
|
updatedAt: members.updatedAt,
|
|
accountName: accounts.name,
|
|
})
|
|
.from(members)
|
|
.leftJoin(accounts, eq(members.accountId, accounts.id))
|
|
.where(where)
|
|
.$dynamic()
|
|
query = withSort(query, params.sort, params.order, sortableColumns, members.lastName)
|
|
query = withPagination(query, params.page, params.limit)
|
|
|
|
const [data, [{ total }]] = await Promise.all([
|
|
query,
|
|
db.select({ total: count() }).from(members).where(where),
|
|
])
|
|
|
|
return paginatedResponse(data, total, params.page, params.limit)
|
|
},
|
|
|
|
async listByAccount(
|
|
db: PostgresJsDatabase,
|
|
companyId: string,
|
|
accountId: string,
|
|
params: PaginationInput,
|
|
) {
|
|
const where = and(eq(members.companyId, companyId), eq(members.accountId, accountId))
|
|
|
|
const sortableColumns: Record<string, typeof members.firstName> = {
|
|
first_name: members.firstName,
|
|
last_name: members.lastName,
|
|
created_at: members.createdAt,
|
|
}
|
|
|
|
let query = db.select().from(members).where(where).$dynamic()
|
|
query = withSort(query, params.sort, params.order, sortableColumns, members.lastName)
|
|
query = withPagination(query, params.page, params.limit)
|
|
|
|
const [data, [{ total }]] = await Promise.all([
|
|
query,
|
|
db.select({ total: count() }).from(members).where(where),
|
|
])
|
|
|
|
return paginatedResponse(data, total, params.page, params.limit)
|
|
},
|
|
|
|
async update(
|
|
db: PostgresJsDatabase,
|
|
companyId: string,
|
|
id: string,
|
|
input: {
|
|
firstName?: string
|
|
lastName?: string
|
|
dateOfBirth?: string
|
|
isMinor?: boolean
|
|
email?: string
|
|
phone?: string
|
|
notes?: string
|
|
},
|
|
) {
|
|
const updates: Record<string, unknown> = { ...input, updatedAt: new Date() }
|
|
|
|
// isMinor: explicit flag wins, else derive from DOB if provided
|
|
if (input.isMinor !== undefined) {
|
|
updates.isMinor = input.isMinor
|
|
} else 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 move(db: PostgresJsDatabase, companyId: string, memberId: string, targetAccountId: string) {
|
|
const member = await this.getById(db, companyId, memberId)
|
|
if (!member) return null
|
|
|
|
const [updated] = await db
|
|
.update(members)
|
|
.set({ accountId: targetAccountId, updatedAt: new Date() })
|
|
.where(and(eq(members.id, memberId), eq(members.companyId, companyId)))
|
|
.returning()
|
|
|
|
// If target account has no primary, set this member
|
|
const [targetAccount] = await db
|
|
.select()
|
|
.from(accounts)
|
|
.where(and(eq(accounts.id, targetAccountId), eq(accounts.companyId, companyId)))
|
|
.limit(1)
|
|
if (targetAccount && !targetAccount.primaryMemberId) {
|
|
await db
|
|
.update(accounts)
|
|
.set({ primaryMemberId: memberId, updatedAt: new Date() })
|
|
.where(eq(accounts.id, targetAccountId))
|
|
}
|
|
|
|
return updated
|
|
},
|
|
|
|
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
|
|
},
|
|
}
|
|
|
|
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
|
|
},
|
|
}
|
|
|
|
export const MemberIdentifierService = {
|
|
async create(db: PostgresJsDatabase, companyId: string, input: MemberIdentifierCreateInput) {
|
|
// If setting as primary, unset existing primary for this member
|
|
if (input.isPrimary) {
|
|
await db
|
|
.update(memberIdentifiers)
|
|
.set({ isPrimary: false })
|
|
.where(
|
|
and(
|
|
eq(memberIdentifiers.memberId, input.memberId),
|
|
eq(memberIdentifiers.isPrimary, true),
|
|
),
|
|
)
|
|
}
|
|
|
|
const [identifier] = await db
|
|
.insert(memberIdentifiers)
|
|
.values({
|
|
companyId,
|
|
memberId: input.memberId,
|
|
type: input.type,
|
|
label: input.label,
|
|
value: input.value,
|
|
issuingAuthority: input.issuingAuthority,
|
|
issuedDate: input.issuedDate,
|
|
expiresAt: input.expiresAt,
|
|
imageFrontFileId: input.imageFrontFileId,
|
|
imageBackFileId: input.imageBackFileId,
|
|
notes: input.notes,
|
|
isPrimary: input.isPrimary,
|
|
})
|
|
.returning()
|
|
return identifier
|
|
},
|
|
|
|
async listByMember(db: PostgresJsDatabase, companyId: string, memberId: string) {
|
|
return db
|
|
.select()
|
|
.from(memberIdentifiers)
|
|
.where(
|
|
and(
|
|
eq(memberIdentifiers.companyId, companyId),
|
|
eq(memberIdentifiers.memberId, memberId),
|
|
),
|
|
)
|
|
},
|
|
|
|
async getById(db: PostgresJsDatabase, companyId: string, id: string) {
|
|
const [identifier] = await db
|
|
.select()
|
|
.from(memberIdentifiers)
|
|
.where(and(eq(memberIdentifiers.id, id), eq(memberIdentifiers.companyId, companyId)))
|
|
.limit(1)
|
|
return identifier ?? null
|
|
},
|
|
|
|
async update(db: PostgresJsDatabase, companyId: string, id: string, input: MemberIdentifierUpdateInput) {
|
|
if (input.isPrimary) {
|
|
const existing = await this.getById(db, companyId, id)
|
|
if (existing) {
|
|
await db
|
|
.update(memberIdentifiers)
|
|
.set({ isPrimary: false })
|
|
.where(
|
|
and(
|
|
eq(memberIdentifiers.memberId, existing.memberId),
|
|
eq(memberIdentifiers.isPrimary, true),
|
|
),
|
|
)
|
|
}
|
|
}
|
|
|
|
const [identifier] = await db
|
|
.update(memberIdentifiers)
|
|
.set({ ...input, updatedAt: new Date() })
|
|
.where(and(eq(memberIdentifiers.id, id), eq(memberIdentifiers.companyId, companyId)))
|
|
.returning()
|
|
return identifier ?? null
|
|
},
|
|
|
|
async delete(db: PostgresJsDatabase, companyId: string, id: string) {
|
|
const [identifier] = await db
|
|
.delete(memberIdentifiers)
|
|
.where(and(eq(memberIdentifiers.id, id), eq(memberIdentifiers.companyId, companyId)))
|
|
.returning()
|
|
return identifier ?? null
|
|
},
|
|
}
|