Add member address, state normalization, account inheritance, fix member form

- Address field on member table (jsonb, same format as account)
- Members inherit email, phone, address from account when not provided
- State normalization: "Texas" → "TX", "california" → "CA" via shared util
- Member form drops zodResolver to fix optional field validation flashing
- Account name auto-format: "First Last - Account"
- US state lookup with full name + code support
This commit is contained in:
Ryan Moon
2026-03-28 12:31:02 -05:00
parent ce2a61ced9
commit b9e984cfa3
10 changed files with 243 additions and 28 deletions

View File

@@ -21,7 +21,7 @@ import type {
TaxExemptionUpdateInput,
PaginationInput,
} from '@forte/shared/schemas'
import { isMinor } from '@forte/shared/utils'
import { isMinor, normalizeStateCode } from '@forte/shared/utils'
import {
withPagination,
withSort,
@@ -49,6 +49,14 @@ async function generateUniqueNumber(
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)
@@ -61,7 +69,7 @@ export const AccountService = {
name: input.name,
email: input.email,
phone: input.phone,
address: input.address,
address: normalizeAddress(input.address),
billingMode: input.billingMode,
notes: input.notes,
})
@@ -155,9 +163,9 @@ export const MemberService = {
lastName: string
dateOfBirth?: string
isMinor?: boolean
isPrimary?: boolean
email?: string
phone?: string
address?: { street?: string; city?: string; state?: string; zip?: string }
notes?: string
},
) {
@@ -165,6 +173,17 @@ export const MemberService = {
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({
@@ -175,18 +194,12 @@ export const MemberService = {
lastName: input.lastName,
dateOfBirth: input.dateOfBirth,
isMinor: minor,
email: input.email,
phone: input.phone,
email,
phone,
address,
notes: input.notes,
})
.returning()
// Auto-set as primary if this is the only member on the account
const [account] = await db
.select()
.from(accounts)
.where(eq(accounts.id, input.accountId))
.limit(1)
if (account && !account.primaryMemberId) {
await db
.update(accounts)