Remove multi-tenant company_id scoping from entire codebase
Drop company_id column from all 22 domain tables via migration. Remove companyId from JWT payload, auth plugins, all service method signatures (~215 occurrences), all route handlers (~105 occurrences), test runner, test suites, and frontend auth store/types. The company table stays as store settings (name, timezone). Tenant isolation in a SaaS deployment would be at the database level (one DB per customer) not the application level. All 107 API tests pass. Zero TSC errors across all packages.
This commit is contained in:
@@ -33,15 +33,13 @@ async function generateUniqueNumber(
|
||||
db: PostgresJsDatabase<any>,
|
||||
table: typeof accounts | typeof members,
|
||||
column: typeof accounts.accountNumber | typeof members.memberNumber,
|
||||
companyId: string,
|
||||
companyIdColumn: Column,
|
||||
): 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)))
|
||||
.where(eq(column, num))
|
||||
.limit(1)
|
||||
if (!existing) return num
|
||||
}
|
||||
@@ -58,13 +56,12 @@ function normalizeAddress(address?: { street?: string; city?: string; state?: st
|
||||
}
|
||||
|
||||
export const AccountService = {
|
||||
async create(db: PostgresJsDatabase<any>, companyId: string, input: AccountCreateInput) {
|
||||
const accountNumber = await generateUniqueNumber(db, accounts, accounts.accountNumber, companyId, accounts.companyId)
|
||||
async create(db: PostgresJsDatabase<any>, input: AccountCreateInput) {
|
||||
const accountNumber = await generateUniqueNumber(db, accounts, accounts.accountNumber)
|
||||
|
||||
const [account] = await db
|
||||
.insert(accounts)
|
||||
.values({
|
||||
companyId,
|
||||
accountNumber,
|
||||
name: input.name,
|
||||
email: input.email,
|
||||
@@ -78,38 +75,38 @@ export const AccountService = {
|
||||
return account
|
||||
},
|
||||
|
||||
async getById(db: PostgresJsDatabase<any>, companyId: string, id: string) {
|
||||
async getById(db: PostgresJsDatabase<any>, id: string) {
|
||||
const [account] = await db
|
||||
.select()
|
||||
.from(accounts)
|
||||
.where(and(eq(accounts.id, id), eq(accounts.companyId, companyId)))
|
||||
.where(eq(accounts.id, id))
|
||||
.limit(1)
|
||||
|
||||
return account ?? null
|
||||
},
|
||||
|
||||
async update(db: PostgresJsDatabase<any>, companyId: string, id: string, input: AccountUpdateInput) {
|
||||
async update(db: PostgresJsDatabase<any>, 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)))
|
||||
.where(eq(accounts.id, id))
|
||||
.returning()
|
||||
|
||||
return account ?? null
|
||||
},
|
||||
|
||||
async softDelete(db: PostgresJsDatabase<any>, companyId: string, id: string) {
|
||||
async softDelete(db: PostgresJsDatabase<any>, id: string) {
|
||||
const [account] = await db
|
||||
.update(accounts)
|
||||
.set({ isActive: false, updatedAt: new Date() })
|
||||
.where(and(eq(accounts.id, id), eq(accounts.companyId, companyId)))
|
||||
.where(eq(accounts.id, id))
|
||||
.returning()
|
||||
|
||||
return account ?? null
|
||||
},
|
||||
|
||||
async list(db: PostgresJsDatabase<any>, companyId: string, params: PaginationInput) {
|
||||
const baseWhere = and(eq(accounts.companyId, companyId), eq(accounts.isActive, true))
|
||||
async list(db: PostgresJsDatabase<any>, params: PaginationInput) {
|
||||
const baseWhere = eq(accounts.isActive, true)
|
||||
|
||||
const accountSearch = params.q
|
||||
? buildSearchCondition(params.q, [accounts.name, accounts.email, accounts.phone, accounts.accountNumber])
|
||||
@@ -156,7 +153,6 @@ export const AccountService = {
|
||||
export const MemberService = {
|
||||
async create(
|
||||
db: PostgresJsDatabase<any>,
|
||||
companyId: string,
|
||||
input: {
|
||||
accountId: string
|
||||
firstName: string
|
||||
@@ -171,7 +167,7 @@ export const MemberService = {
|
||||
) {
|
||||
// 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)
|
||||
const memberNumber = await generateUniqueNumber(db, members, members.memberNumber)
|
||||
|
||||
// Inherit email, phone, address from account if not provided
|
||||
const [account] = await db
|
||||
@@ -187,7 +183,6 @@ export const MemberService = {
|
||||
const [member] = await db
|
||||
.insert(members)
|
||||
.values({
|
||||
companyId,
|
||||
memberNumber,
|
||||
accountId: input.accountId,
|
||||
firstName: input.firstName,
|
||||
@@ -210,25 +205,21 @@ export const MemberService = {
|
||||
return member
|
||||
},
|
||||
|
||||
async getById(db: PostgresJsDatabase<any>, companyId: string, id: string) {
|
||||
async getById(db: PostgresJsDatabase<any>, id: string) {
|
||||
const [member] = await db
|
||||
.select()
|
||||
.from(members)
|
||||
.where(and(eq(members.id, id), eq(members.companyId, companyId)))
|
||||
.where(eq(members.id, id))
|
||||
.limit(1)
|
||||
|
||||
return member ?? null
|
||||
},
|
||||
|
||||
async list(db: PostgresJsDatabase<any>, companyId: string, params: PaginationInput) {
|
||||
const baseWhere = eq(members.companyId, companyId)
|
||||
|
||||
async list(db: PostgresJsDatabase<any>, params: PaginationInput) {
|
||||
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, Column> = {
|
||||
first_name: members.firstName,
|
||||
last_name: members.lastName,
|
||||
@@ -239,7 +230,6 @@ export const MemberService = {
|
||||
let query = db.select({
|
||||
id: members.id,
|
||||
accountId: members.accountId,
|
||||
companyId: members.companyId,
|
||||
firstName: members.firstName,
|
||||
lastName: members.lastName,
|
||||
dateOfBirth: members.dateOfBirth,
|
||||
@@ -254,14 +244,14 @@ export const MemberService = {
|
||||
})
|
||||
.from(members)
|
||||
.leftJoin(accounts, eq(members.accountId, accounts.id))
|
||||
.where(where)
|
||||
.where(searchCondition)
|
||||
.$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),
|
||||
db.select({ total: count() }).from(members).where(searchCondition),
|
||||
])
|
||||
|
||||
return paginatedResponse(data, total, params.page, params.limit)
|
||||
@@ -269,11 +259,10 @@ export const MemberService = {
|
||||
|
||||
async listByAccount(
|
||||
db: PostgresJsDatabase<any>,
|
||||
companyId: string,
|
||||
accountId: string,
|
||||
params: PaginationInput,
|
||||
) {
|
||||
const where = and(eq(members.companyId, companyId), eq(members.accountId, accountId))
|
||||
const where = eq(members.accountId, accountId)
|
||||
|
||||
const sortableColumns: Record<string, Column> = {
|
||||
first_name: members.firstName,
|
||||
@@ -295,7 +284,6 @@ export const MemberService = {
|
||||
|
||||
async update(
|
||||
db: PostgresJsDatabase<any>,
|
||||
companyId: string,
|
||||
id: string,
|
||||
input: {
|
||||
firstName?: string
|
||||
@@ -319,27 +307,27 @@ export const MemberService = {
|
||||
const [member] = await db
|
||||
.update(members)
|
||||
.set(updates)
|
||||
.where(and(eq(members.id, id), eq(members.companyId, companyId)))
|
||||
.where(eq(members.id, id))
|
||||
.returning()
|
||||
|
||||
return member ?? null
|
||||
},
|
||||
|
||||
async move(db: PostgresJsDatabase<any>, companyId: string, memberId: string, targetAccountId: string) {
|
||||
const member = await this.getById(db, companyId, memberId)
|
||||
async move(db: PostgresJsDatabase<any>, memberId: string, targetAccountId: string) {
|
||||
const member = await this.getById(db, 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)))
|
||||
.where(eq(members.id, memberId))
|
||||
.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)))
|
||||
.where(eq(accounts.id, targetAccountId))
|
||||
.limit(1)
|
||||
if (targetAccount && !targetAccount.primaryMemberId) {
|
||||
await db
|
||||
@@ -351,10 +339,10 @@ export const MemberService = {
|
||||
return updated
|
||||
},
|
||||
|
||||
async delete(db: PostgresJsDatabase<any>, companyId: string, id: string) {
|
||||
async delete(db: PostgresJsDatabase<any>, id: string) {
|
||||
const [member] = await db
|
||||
.delete(members)
|
||||
.where(and(eq(members.id, id), eq(members.companyId, companyId)))
|
||||
.where(eq(members.id, id))
|
||||
.returning()
|
||||
|
||||
return member ?? null
|
||||
@@ -362,11 +350,10 @@ export const MemberService = {
|
||||
}
|
||||
|
||||
export const ProcessorLinkService = {
|
||||
async create(db: PostgresJsDatabase<any>, companyId: string, input: ProcessorLinkCreateInput) {
|
||||
async create(db: PostgresJsDatabase<any>, input: ProcessorLinkCreateInput) {
|
||||
const [link] = await db
|
||||
.insert(accountProcessorLinks)
|
||||
.values({
|
||||
companyId,
|
||||
accountId: input.accountId,
|
||||
processor: input.processor,
|
||||
processorCustomerId: input.processorCustomerId,
|
||||
@@ -375,17 +362,17 @@ export const ProcessorLinkService = {
|
||||
return link
|
||||
},
|
||||
|
||||
async getById(db: PostgresJsDatabase<any>, companyId: string, id: string) {
|
||||
async getById(db: PostgresJsDatabase<any>, id: string) {
|
||||
const [link] = await db
|
||||
.select()
|
||||
.from(accountProcessorLinks)
|
||||
.where(and(eq(accountProcessorLinks.id, id), eq(accountProcessorLinks.companyId, companyId)))
|
||||
.where(eq(accountProcessorLinks.id, id))
|
||||
.limit(1)
|
||||
return link ?? null
|
||||
},
|
||||
|
||||
async listByAccount(db: PostgresJsDatabase<any>, companyId: string, accountId: string, params: PaginationInput) {
|
||||
const baseWhere = and(eq(accountProcessorLinks.companyId, companyId), eq(accountProcessorLinks.accountId, accountId))
|
||||
async listByAccount(db: PostgresJsDatabase<any>, accountId: string, params: PaginationInput) {
|
||||
const baseWhere = eq(accountProcessorLinks.accountId, accountId)
|
||||
const searchCondition = params.q
|
||||
? buildSearchCondition(params.q, [accountProcessorLinks.processorCustomerId, accountProcessorLinks.processor])
|
||||
: undefined
|
||||
@@ -408,26 +395,26 @@ export const ProcessorLinkService = {
|
||||
return paginatedResponse(data, total, params.page, params.limit)
|
||||
},
|
||||
|
||||
async update(db: PostgresJsDatabase<any>, companyId: string, id: string, input: ProcessorLinkUpdateInput) {
|
||||
async update(db: PostgresJsDatabase<any>, id: string, input: ProcessorLinkUpdateInput) {
|
||||
const [link] = await db
|
||||
.update(accountProcessorLinks)
|
||||
.set(input)
|
||||
.where(and(eq(accountProcessorLinks.id, id), eq(accountProcessorLinks.companyId, companyId)))
|
||||
.where(eq(accountProcessorLinks.id, id))
|
||||
.returning()
|
||||
return link ?? null
|
||||
},
|
||||
|
||||
async delete(db: PostgresJsDatabase<any>, companyId: string, id: string) {
|
||||
async delete(db: PostgresJsDatabase<any>, id: string) {
|
||||
const [link] = await db
|
||||
.delete(accountProcessorLinks)
|
||||
.where(and(eq(accountProcessorLinks.id, id), eq(accountProcessorLinks.companyId, companyId)))
|
||||
.where(eq(accountProcessorLinks.id, id))
|
||||
.returning()
|
||||
return link ?? null
|
||||
},
|
||||
}
|
||||
|
||||
export const PaymentMethodService = {
|
||||
async create(db: PostgresJsDatabase<any>, companyId: string, input: PaymentMethodCreateInput) {
|
||||
async create(db: PostgresJsDatabase<any>, input: PaymentMethodCreateInput) {
|
||||
// If this is the default, unset any existing default for this account
|
||||
if (input.isDefault) {
|
||||
await db
|
||||
@@ -435,7 +422,6 @@ export const PaymentMethodService = {
|
||||
.set({ isDefault: false })
|
||||
.where(
|
||||
and(
|
||||
eq(accountPaymentMethods.companyId, companyId),
|
||||
eq(accountPaymentMethods.accountId, input.accountId),
|
||||
eq(accountPaymentMethods.isDefault, true),
|
||||
),
|
||||
@@ -445,7 +431,6 @@ export const PaymentMethodService = {
|
||||
const [method] = await db
|
||||
.insert(accountPaymentMethods)
|
||||
.values({
|
||||
companyId,
|
||||
accountId: input.accountId,
|
||||
processor: input.processor,
|
||||
processorPaymentMethodId: input.processorPaymentMethodId,
|
||||
@@ -459,17 +444,17 @@ export const PaymentMethodService = {
|
||||
return method
|
||||
},
|
||||
|
||||
async getById(db: PostgresJsDatabase<any>, companyId: string, id: string) {
|
||||
async getById(db: PostgresJsDatabase<any>, id: string) {
|
||||
const [method] = await db
|
||||
.select()
|
||||
.from(accountPaymentMethods)
|
||||
.where(and(eq(accountPaymentMethods.id, id), eq(accountPaymentMethods.companyId, companyId)))
|
||||
.where(eq(accountPaymentMethods.id, id))
|
||||
.limit(1)
|
||||
return method ?? null
|
||||
},
|
||||
|
||||
async listByAccount(db: PostgresJsDatabase<any>, companyId: string, accountId: string, params: PaginationInput) {
|
||||
const baseWhere = and(eq(accountPaymentMethods.companyId, companyId), eq(accountPaymentMethods.accountId, accountId))
|
||||
async listByAccount(db: PostgresJsDatabase<any>, accountId: string, params: PaginationInput) {
|
||||
const baseWhere = eq(accountPaymentMethods.accountId, accountId)
|
||||
const searchCondition = params.q
|
||||
? buildSearchCondition(params.q, [accountPaymentMethods.cardBrand, accountPaymentMethods.lastFour])
|
||||
: undefined
|
||||
@@ -493,17 +478,16 @@ export const PaymentMethodService = {
|
||||
return paginatedResponse(data, total, params.page, params.limit)
|
||||
},
|
||||
|
||||
async update(db: PostgresJsDatabase<any>, companyId: string, id: string, input: PaymentMethodUpdateInput) {
|
||||
async update(db: PostgresJsDatabase<any>, id: string, input: PaymentMethodUpdateInput) {
|
||||
// If setting as default, unset existing default
|
||||
if (input.isDefault) {
|
||||
const existing = await this.getById(db, companyId, id)
|
||||
const existing = await this.getById(db, 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),
|
||||
),
|
||||
@@ -514,26 +498,25 @@ export const PaymentMethodService = {
|
||||
const [method] = await db
|
||||
.update(accountPaymentMethods)
|
||||
.set(input)
|
||||
.where(and(eq(accountPaymentMethods.id, id), eq(accountPaymentMethods.companyId, companyId)))
|
||||
.where(eq(accountPaymentMethods.id, id))
|
||||
.returning()
|
||||
return method ?? null
|
||||
},
|
||||
|
||||
async delete(db: PostgresJsDatabase<any>, companyId: string, id: string) {
|
||||
async delete(db: PostgresJsDatabase<any>, id: string) {
|
||||
const [method] = await db
|
||||
.delete(accountPaymentMethods)
|
||||
.where(and(eq(accountPaymentMethods.id, id), eq(accountPaymentMethods.companyId, companyId)))
|
||||
.where(eq(accountPaymentMethods.id, id))
|
||||
.returning()
|
||||
return method ?? null
|
||||
},
|
||||
}
|
||||
|
||||
export const TaxExemptionService = {
|
||||
async create(db: PostgresJsDatabase<any>, companyId: string, input: TaxExemptionCreateInput) {
|
||||
async create(db: PostgresJsDatabase<any>, input: TaxExemptionCreateInput) {
|
||||
const [exemption] = await db
|
||||
.insert(taxExemptions)
|
||||
.values({
|
||||
companyId,
|
||||
accountId: input.accountId,
|
||||
certificateNumber: input.certificateNumber,
|
||||
certificateType: input.certificateType,
|
||||
@@ -546,17 +529,17 @@ export const TaxExemptionService = {
|
||||
return exemption
|
||||
},
|
||||
|
||||
async getById(db: PostgresJsDatabase<any>, companyId: string, id: string) {
|
||||
async getById(db: PostgresJsDatabase<any>, id: string) {
|
||||
const [exemption] = await db
|
||||
.select()
|
||||
.from(taxExemptions)
|
||||
.where(and(eq(taxExemptions.id, id), eq(taxExemptions.companyId, companyId)))
|
||||
.where(eq(taxExemptions.id, id))
|
||||
.limit(1)
|
||||
return exemption ?? null
|
||||
},
|
||||
|
||||
async listByAccount(db: PostgresJsDatabase<any>, companyId: string, accountId: string, params: PaginationInput) {
|
||||
const baseWhere = and(eq(taxExemptions.companyId, companyId), eq(taxExemptions.accountId, accountId))
|
||||
async listByAccount(db: PostgresJsDatabase<any>, accountId: string, params: PaginationInput) {
|
||||
const baseWhere = eq(taxExemptions.accountId, accountId)
|
||||
const searchCondition = params.q
|
||||
? buildSearchCondition(params.q, [taxExemptions.certificateNumber, taxExemptions.certificateType, taxExemptions.issuingState])
|
||||
: undefined
|
||||
@@ -581,16 +564,16 @@ export const TaxExemptionService = {
|
||||
return paginatedResponse(data, total, params.page, params.limit)
|
||||
},
|
||||
|
||||
async update(db: PostgresJsDatabase<any>, companyId: string, id: string, input: TaxExemptionUpdateInput) {
|
||||
async update(db: PostgresJsDatabase<any>, 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)))
|
||||
.where(eq(taxExemptions.id, id))
|
||||
.returning()
|
||||
return exemption ?? null
|
||||
},
|
||||
|
||||
async approve(db: PostgresJsDatabase<any>, companyId: string, id: string, approvedBy: string) {
|
||||
async approve(db: PostgresJsDatabase<any>, id: string, approvedBy: string) {
|
||||
const [exemption] = await db
|
||||
.update(taxExemptions)
|
||||
.set({
|
||||
@@ -599,12 +582,12 @@ export const TaxExemptionService = {
|
||||
approvedAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(and(eq(taxExemptions.id, id), eq(taxExemptions.companyId, companyId)))
|
||||
.where(eq(taxExemptions.id, id))
|
||||
.returning()
|
||||
return exemption ?? null
|
||||
},
|
||||
|
||||
async revoke(db: PostgresJsDatabase<any>, companyId: string, id: string, revokedBy: string, reason: string) {
|
||||
async revoke(db: PostgresJsDatabase<any>, id: string, revokedBy: string, reason: string) {
|
||||
const [exemption] = await db
|
||||
.update(taxExemptions)
|
||||
.set({
|
||||
@@ -614,14 +597,14 @@ export const TaxExemptionService = {
|
||||
revokedReason: reason,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(and(eq(taxExemptions.id, id), eq(taxExemptions.companyId, companyId)))
|
||||
.where(eq(taxExemptions.id, id))
|
||||
.returning()
|
||||
return exemption ?? null
|
||||
},
|
||||
}
|
||||
|
||||
export const MemberIdentifierService = {
|
||||
async create(db: PostgresJsDatabase<any>, companyId: string, input: MemberIdentifierCreateInput) {
|
||||
async create(db: PostgresJsDatabase<any>, input: MemberIdentifierCreateInput) {
|
||||
// If setting as primary, unset existing primary for this member
|
||||
if (input.isPrimary) {
|
||||
await db
|
||||
@@ -638,7 +621,6 @@ export const MemberIdentifierService = {
|
||||
const [identifier] = await db
|
||||
.insert(memberIdentifiers)
|
||||
.values({
|
||||
companyId,
|
||||
memberId: input.memberId,
|
||||
type: input.type,
|
||||
label: input.label,
|
||||
@@ -655,8 +637,8 @@ export const MemberIdentifierService = {
|
||||
return identifier
|
||||
},
|
||||
|
||||
async listByMember(db: PostgresJsDatabase<any>, companyId: string, memberId: string, params: PaginationInput) {
|
||||
const baseWhere = and(eq(memberIdentifiers.companyId, companyId), eq(memberIdentifiers.memberId, memberId))
|
||||
async listByMember(db: PostgresJsDatabase<any>, memberId: string, params: PaginationInput) {
|
||||
const baseWhere = eq(memberIdentifiers.memberId, memberId)
|
||||
const searchCondition = params.q
|
||||
? buildSearchCondition(params.q, [memberIdentifiers.value, memberIdentifiers.label, memberIdentifiers.issuingAuthority])
|
||||
: undefined
|
||||
@@ -679,18 +661,18 @@ export const MemberIdentifierService = {
|
||||
return paginatedResponse(data, total, params.page, params.limit)
|
||||
},
|
||||
|
||||
async getById(db: PostgresJsDatabase<any>, companyId: string, id: string) {
|
||||
async getById(db: PostgresJsDatabase<any>, id: string) {
|
||||
const [identifier] = await db
|
||||
.select()
|
||||
.from(memberIdentifiers)
|
||||
.where(and(eq(memberIdentifiers.id, id), eq(memberIdentifiers.companyId, companyId)))
|
||||
.where(eq(memberIdentifiers.id, id))
|
||||
.limit(1)
|
||||
return identifier ?? null
|
||||
},
|
||||
|
||||
async update(db: PostgresJsDatabase<any>, companyId: string, id: string, input: MemberIdentifierUpdateInput) {
|
||||
async update(db: PostgresJsDatabase<any>, id: string, input: MemberIdentifierUpdateInput) {
|
||||
if (input.isPrimary) {
|
||||
const existing = await this.getById(db, companyId, id)
|
||||
const existing = await this.getById(db, id)
|
||||
if (existing) {
|
||||
await db
|
||||
.update(memberIdentifiers)
|
||||
@@ -707,15 +689,15 @@ export const MemberIdentifierService = {
|
||||
const [identifier] = await db
|
||||
.update(memberIdentifiers)
|
||||
.set({ ...input, updatedAt: new Date() })
|
||||
.where(and(eq(memberIdentifiers.id, id), eq(memberIdentifiers.companyId, companyId)))
|
||||
.where(eq(memberIdentifiers.id, id))
|
||||
.returning()
|
||||
return identifier ?? null
|
||||
},
|
||||
|
||||
async delete(db: PostgresJsDatabase<any>, companyId: string, id: string) {
|
||||
async delete(db: PostgresJsDatabase<any>, id: string) {
|
||||
const [identifier] = await db
|
||||
.delete(memberIdentifiers)
|
||||
.where(and(eq(memberIdentifiers.id, id), eq(memberIdentifiers.companyId, companyId)))
|
||||
.where(eq(memberIdentifiers.id, id))
|
||||
.returning()
|
||||
return identifier ?? null
|
||||
},
|
||||
|
||||
@@ -26,7 +26,6 @@ export const FileService = {
|
||||
async upload(
|
||||
db: PostgresJsDatabase<any>,
|
||||
storage: StorageProvider,
|
||||
companyId: string,
|
||||
input: {
|
||||
data: Buffer
|
||||
filename: string
|
||||
@@ -54,7 +53,6 @@ export const FileService = {
|
||||
.from(files)
|
||||
.where(
|
||||
and(
|
||||
eq(files.companyId, companyId),
|
||||
eq(files.entityType, input.entityType),
|
||||
eq(files.entityId, input.entityId),
|
||||
),
|
||||
@@ -66,7 +64,7 @@ export const FileService = {
|
||||
// Generate path
|
||||
const fileId = randomUUID()
|
||||
const ext = getExtension(input.contentType)
|
||||
const path = `${companyId}/${input.entityType}/${input.entityId}/${input.category}-${fileId}.${ext}`
|
||||
const path = `${input.entityType}/${input.entityId}/${input.category}-${fileId}.${ext}`
|
||||
|
||||
// Write to storage
|
||||
await storage.put(path, input.data, input.contentType)
|
||||
@@ -76,7 +74,6 @@ export const FileService = {
|
||||
.insert(files)
|
||||
.values({
|
||||
id: fileId,
|
||||
companyId,
|
||||
path,
|
||||
filename: input.filename,
|
||||
contentType: input.contentType,
|
||||
@@ -91,18 +88,17 @@ export const FileService = {
|
||||
return file
|
||||
},
|
||||
|
||||
async getById(db: PostgresJsDatabase<any>, companyId: string, id: string) {
|
||||
async getById(db: PostgresJsDatabase<any>, id: string) {
|
||||
const [file] = await db
|
||||
.select()
|
||||
.from(files)
|
||||
.where(and(eq(files.id, id), eq(files.companyId, companyId)))
|
||||
.where(eq(files.id, id))
|
||||
.limit(1)
|
||||
return file ?? null
|
||||
},
|
||||
|
||||
async listByEntity(
|
||||
db: PostgresJsDatabase<any>,
|
||||
companyId: string,
|
||||
entityType: string,
|
||||
entityId: string,
|
||||
) {
|
||||
@@ -111,7 +107,6 @@ export const FileService = {
|
||||
.from(files)
|
||||
.where(
|
||||
and(
|
||||
eq(files.companyId, companyId),
|
||||
eq(files.entityType, entityType),
|
||||
eq(files.entityId, entityId),
|
||||
),
|
||||
@@ -122,17 +117,16 @@ export const FileService = {
|
||||
async delete(
|
||||
db: PostgresJsDatabase<any>,
|
||||
storage: StorageProvider,
|
||||
companyId: string,
|
||||
id: string,
|
||||
) {
|
||||
const file = await this.getById(db, companyId, id)
|
||||
const file = await this.getById(db, id)
|
||||
if (!file) return null
|
||||
|
||||
await storage.delete(file.path)
|
||||
|
||||
const [deleted] = await db
|
||||
.delete(files)
|
||||
.where(and(eq(files.id, id), eq(files.companyId, companyId)))
|
||||
.where(eq(files.id, id))
|
||||
.returning()
|
||||
|
||||
return deleted ?? null
|
||||
|
||||
@@ -16,25 +16,25 @@ import {
|
||||
} from '../utils/pagination.js'
|
||||
|
||||
export const CategoryService = {
|
||||
async create(db: PostgresJsDatabase<any>, companyId: string, input: CategoryCreateInput) {
|
||||
async create(db: PostgresJsDatabase<any>, input: CategoryCreateInput) {
|
||||
const [category] = await db
|
||||
.insert(categories)
|
||||
.values({ companyId, ...input })
|
||||
.values({ ...input })
|
||||
.returning()
|
||||
return category
|
||||
},
|
||||
|
||||
async getById(db: PostgresJsDatabase<any>, companyId: string, id: string) {
|
||||
async getById(db: PostgresJsDatabase<any>, id: string) {
|
||||
const [category] = await db
|
||||
.select()
|
||||
.from(categories)
|
||||
.where(and(eq(categories.id, id), eq(categories.companyId, companyId)))
|
||||
.where(eq(categories.id, id))
|
||||
.limit(1)
|
||||
return category ?? null
|
||||
},
|
||||
|
||||
async list(db: PostgresJsDatabase<any>, companyId: string, params: PaginationInput) {
|
||||
const baseWhere = and(eq(categories.companyId, companyId), eq(categories.isActive, true))
|
||||
async list(db: PostgresJsDatabase<any>, params: PaginationInput) {
|
||||
const baseWhere = eq(categories.isActive, true)
|
||||
|
||||
const searchCondition = params.q
|
||||
? buildSearchCondition(params.q, [categories.name])
|
||||
@@ -60,45 +60,45 @@ export const CategoryService = {
|
||||
return paginatedResponse(data, total, params.page, params.limit)
|
||||
},
|
||||
|
||||
async update(db: PostgresJsDatabase<any>, companyId: string, id: string, input: CategoryUpdateInput) {
|
||||
async update(db: PostgresJsDatabase<any>, id: string, input: CategoryUpdateInput) {
|
||||
const [category] = await db
|
||||
.update(categories)
|
||||
.set({ ...input, updatedAt: new Date() })
|
||||
.where(and(eq(categories.id, id), eq(categories.companyId, companyId)))
|
||||
.where(eq(categories.id, id))
|
||||
.returning()
|
||||
return category ?? null
|
||||
},
|
||||
|
||||
async softDelete(db: PostgresJsDatabase<any>, companyId: string, id: string) {
|
||||
async softDelete(db: PostgresJsDatabase<any>, id: string) {
|
||||
const [category] = await db
|
||||
.update(categories)
|
||||
.set({ isActive: false, updatedAt: new Date() })
|
||||
.where(and(eq(categories.id, id), eq(categories.companyId, companyId)))
|
||||
.where(eq(categories.id, id))
|
||||
.returning()
|
||||
return category ?? null
|
||||
},
|
||||
}
|
||||
|
||||
export const SupplierService = {
|
||||
async create(db: PostgresJsDatabase<any>, companyId: string, input: SupplierCreateInput) {
|
||||
async create(db: PostgresJsDatabase<any>, input: SupplierCreateInput) {
|
||||
const [supplier] = await db
|
||||
.insert(suppliers)
|
||||
.values({ companyId, ...input })
|
||||
.values({ ...input })
|
||||
.returning()
|
||||
return supplier
|
||||
},
|
||||
|
||||
async getById(db: PostgresJsDatabase<any>, companyId: string, id: string) {
|
||||
async getById(db: PostgresJsDatabase<any>, id: string) {
|
||||
const [supplier] = await db
|
||||
.select()
|
||||
.from(suppliers)
|
||||
.where(and(eq(suppliers.id, id), eq(suppliers.companyId, companyId)))
|
||||
.where(eq(suppliers.id, id))
|
||||
.limit(1)
|
||||
return supplier ?? null
|
||||
},
|
||||
|
||||
async list(db: PostgresJsDatabase<any>, companyId: string, params: PaginationInput) {
|
||||
const baseWhere = and(eq(suppliers.companyId, companyId), eq(suppliers.isActive, true))
|
||||
async list(db: PostgresJsDatabase<any>, params: PaginationInput) {
|
||||
const baseWhere = eq(suppliers.isActive, true)
|
||||
|
||||
const searchCondition = params.q
|
||||
? buildSearchCondition(params.q, [suppliers.name, suppliers.contactName, suppliers.email])
|
||||
@@ -123,20 +123,20 @@ export const SupplierService = {
|
||||
return paginatedResponse(data, total, params.page, params.limit)
|
||||
},
|
||||
|
||||
async update(db: PostgresJsDatabase<any>, companyId: string, id: string, input: SupplierUpdateInput) {
|
||||
async update(db: PostgresJsDatabase<any>, id: string, input: SupplierUpdateInput) {
|
||||
const [supplier] = await db
|
||||
.update(suppliers)
|
||||
.set({ ...input, updatedAt: new Date() })
|
||||
.where(and(eq(suppliers.id, id), eq(suppliers.companyId, companyId)))
|
||||
.where(eq(suppliers.id, id))
|
||||
.returning()
|
||||
return supplier ?? null
|
||||
},
|
||||
|
||||
async softDelete(db: PostgresJsDatabase<any>, companyId: string, id: string) {
|
||||
async softDelete(db: PostgresJsDatabase<any>, id: string) {
|
||||
const [supplier] = await db
|
||||
.update(suppliers)
|
||||
.set({ isActive: false, updatedAt: new Date() })
|
||||
.where(and(eq(suppliers.id, id), eq(suppliers.companyId, companyId)))
|
||||
.where(eq(suppliers.id, id))
|
||||
.returning()
|
||||
return supplier ?? null
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { eq, and } from 'drizzle-orm'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
|
||||
import { ForbiddenError } from '../lib/errors.js'
|
||||
import {
|
||||
@@ -14,46 +14,44 @@ function createLookupService(
|
||||
systemSeeds: ReadonlyArray<{ slug: string; name: string; description: string; sortOrder: number }>,
|
||||
) {
|
||||
return {
|
||||
async seedForCompany(db: PostgresJsDatabase<any>, companyId: string) {
|
||||
async seedDefaults(db: PostgresJsDatabase<any>) {
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(table)
|
||||
.where(and(eq(table.companyId, companyId), eq(table.isSystem, true)))
|
||||
.where(eq(table.isSystem, true))
|
||||
.limit(1)
|
||||
|
||||
if (existing.length > 0) return // already seeded
|
||||
|
||||
await db.insert(table).values(
|
||||
systemSeeds.map((seed) => ({
|
||||
companyId,
|
||||
...seed,
|
||||
isSystem: true,
|
||||
})),
|
||||
)
|
||||
},
|
||||
|
||||
async list(db: PostgresJsDatabase<any>, companyId: string) {
|
||||
async list(db: PostgresJsDatabase<any>) {
|
||||
return db
|
||||
.select()
|
||||
.from(table)
|
||||
.where(and(eq(table.companyId, companyId), eq(table.isActive, true)))
|
||||
.where(eq(table.isActive, true))
|
||||
.orderBy(table.sortOrder)
|
||||
},
|
||||
|
||||
async getBySlug(db: PostgresJsDatabase<any>, companyId: string, slug: string) {
|
||||
async getBySlug(db: PostgresJsDatabase<any>, slug: string) {
|
||||
const [row] = await db
|
||||
.select()
|
||||
.from(table)
|
||||
.where(and(eq(table.companyId, companyId), eq(table.slug, slug)))
|
||||
.where(eq(table.slug, slug))
|
||||
.limit(1)
|
||||
return row ?? null
|
||||
},
|
||||
|
||||
async create(db: PostgresJsDatabase<any>, companyId: string, input: LookupCreateInput) {
|
||||
async create(db: PostgresJsDatabase<any>, input: LookupCreateInput) {
|
||||
const [row] = await db
|
||||
.insert(table)
|
||||
.values({
|
||||
companyId,
|
||||
name: input.name,
|
||||
slug: input.slug,
|
||||
description: input.description,
|
||||
@@ -64,12 +62,12 @@ function createLookupService(
|
||||
return row
|
||||
},
|
||||
|
||||
async update(db: PostgresJsDatabase<any>, companyId: string, id: string, input: LookupUpdateInput) {
|
||||
async update(db: PostgresJsDatabase<any>, id: string, input: LookupUpdateInput) {
|
||||
// Prevent modifying system rows' slug or system flag
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(table)
|
||||
.where(and(eq(table.id, id), eq(table.companyId, companyId)))
|
||||
.where(eq(table.id, id))
|
||||
.limit(1)
|
||||
|
||||
if (!existing[0]) return null
|
||||
@@ -80,16 +78,16 @@ function createLookupService(
|
||||
const [row] = await db
|
||||
.update(table)
|
||||
.set(input)
|
||||
.where(and(eq(table.id, id), eq(table.companyId, companyId)))
|
||||
.where(eq(table.id, id))
|
||||
.returning()
|
||||
return row ?? null
|
||||
},
|
||||
|
||||
async delete(db: PostgresJsDatabase<any>, companyId: string, id: string) {
|
||||
async delete(db: PostgresJsDatabase<any>, id: string) {
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(table)
|
||||
.where(and(eq(table.id, id), eq(table.companyId, companyId)))
|
||||
.where(eq(table.id, id))
|
||||
.limit(1)
|
||||
|
||||
if (!existing[0]) return null
|
||||
@@ -99,13 +97,13 @@ function createLookupService(
|
||||
|
||||
const [row] = await db
|
||||
.delete(table)
|
||||
.where(and(eq(table.id, id), eq(table.companyId, companyId)))
|
||||
.where(eq(table.id, id))
|
||||
.returning()
|
||||
return row ?? null
|
||||
},
|
||||
|
||||
async validateSlug(db: PostgresJsDatabase<any>, companyId: string, slug: string): Promise<boolean> {
|
||||
const row = await this.getBySlug(db, companyId, slug)
|
||||
async validateSlug(db: PostgresJsDatabase<any>, slug: string): Promise<boolean> {
|
||||
const row = await this.getBySlug(db, slug)
|
||||
return row !== null && row.isActive
|
||||
},
|
||||
}
|
||||
|
||||
@@ -18,11 +18,10 @@ import {
|
||||
import { UnitStatusService, ItemConditionService } from './lookup.service.js'
|
||||
|
||||
export const ProductService = {
|
||||
async create(db: PostgresJsDatabase<any>, companyId: string, input: ProductCreateInput) {
|
||||
async create(db: PostgresJsDatabase<any>, input: ProductCreateInput) {
|
||||
const [product] = await db
|
||||
.insert(products)
|
||||
.values({
|
||||
companyId,
|
||||
...input,
|
||||
price: input.price?.toString(),
|
||||
minPrice: input.minPrice?.toString(),
|
||||
@@ -32,17 +31,17 @@ export const ProductService = {
|
||||
return product
|
||||
},
|
||||
|
||||
async getById(db: PostgresJsDatabase<any>, companyId: string, id: string) {
|
||||
async getById(db: PostgresJsDatabase<any>, id: string) {
|
||||
const [product] = await db
|
||||
.select()
|
||||
.from(products)
|
||||
.where(and(eq(products.id, id), eq(products.companyId, companyId)))
|
||||
.where(eq(products.id, id))
|
||||
.limit(1)
|
||||
return product ?? null
|
||||
},
|
||||
|
||||
async list(db: PostgresJsDatabase<any>, companyId: string, params: PaginationInput) {
|
||||
const baseWhere = and(eq(products.companyId, companyId), eq(products.isActive, true))
|
||||
async list(db: PostgresJsDatabase<any>, params: PaginationInput) {
|
||||
const baseWhere = eq(products.isActive, true)
|
||||
|
||||
const searchCondition = params.q
|
||||
? buildSearchCondition(params.q, [products.name, products.sku, products.upc, products.brand])
|
||||
@@ -72,17 +71,15 @@ export const ProductService = {
|
||||
|
||||
async update(
|
||||
db: PostgresJsDatabase<any>,
|
||||
companyId: string,
|
||||
id: string,
|
||||
input: ProductUpdateInput,
|
||||
changedBy?: string,
|
||||
) {
|
||||
if (input.price !== undefined || input.minPrice !== undefined) {
|
||||
const existing = await this.getById(db, companyId, id)
|
||||
const existing = await this.getById(db, id)
|
||||
if (existing) {
|
||||
await db.insert(priceHistory).values({
|
||||
productId: id,
|
||||
companyId,
|
||||
previousPrice: existing.price,
|
||||
newPrice: input.price?.toString() ?? existing.price ?? '0',
|
||||
previousMinPrice: existing.minPrice,
|
||||
@@ -101,36 +98,35 @@ export const ProductService = {
|
||||
const [product] = await db
|
||||
.update(products)
|
||||
.set(updates)
|
||||
.where(and(eq(products.id, id), eq(products.companyId, companyId)))
|
||||
.where(eq(products.id, id))
|
||||
.returning()
|
||||
return product ?? null
|
||||
},
|
||||
|
||||
async softDelete(db: PostgresJsDatabase<any>, companyId: string, id: string) {
|
||||
async softDelete(db: PostgresJsDatabase<any>, id: string) {
|
||||
const [product] = await db
|
||||
.update(products)
|
||||
.set({ isActive: false, updatedAt: new Date() })
|
||||
.where(and(eq(products.id, id), eq(products.companyId, companyId)))
|
||||
.where(eq(products.id, id))
|
||||
.returning()
|
||||
return product ?? null
|
||||
},
|
||||
}
|
||||
|
||||
export const InventoryUnitService = {
|
||||
async create(db: PostgresJsDatabase<any>, companyId: string, input: InventoryUnitCreateInput) {
|
||||
async create(db: PostgresJsDatabase<any>, input: InventoryUnitCreateInput) {
|
||||
if (input.condition) {
|
||||
const valid = await ItemConditionService.validateSlug(db, companyId, input.condition)
|
||||
const valid = await ItemConditionService.validateSlug(db, input.condition)
|
||||
if (!valid) throw new ValidationError(`Invalid condition: "${input.condition}"`)
|
||||
}
|
||||
if (input.status) {
|
||||
const valid = await UnitStatusService.validateSlug(db, companyId, input.status)
|
||||
const valid = await UnitStatusService.validateSlug(db, input.status)
|
||||
if (!valid) throw new ValidationError(`Invalid status: "${input.status}"`)
|
||||
}
|
||||
|
||||
const [unit] = await db
|
||||
.insert(inventoryUnits)
|
||||
.values({
|
||||
companyId,
|
||||
productId: input.productId,
|
||||
locationId: input.locationId,
|
||||
serialNumber: input.serialNumber,
|
||||
@@ -144,25 +140,21 @@ export const InventoryUnitService = {
|
||||
return unit
|
||||
},
|
||||
|
||||
async getById(db: PostgresJsDatabase<any>, companyId: string, id: string) {
|
||||
async getById(db: PostgresJsDatabase<any>, id: string) {
|
||||
const [unit] = await db
|
||||
.select()
|
||||
.from(inventoryUnits)
|
||||
.where(and(eq(inventoryUnits.id, id), eq(inventoryUnits.companyId, companyId)))
|
||||
.where(eq(inventoryUnits.id, id))
|
||||
.limit(1)
|
||||
return unit ?? null
|
||||
},
|
||||
|
||||
async listByProduct(
|
||||
db: PostgresJsDatabase<any>,
|
||||
companyId: string,
|
||||
productId: string,
|
||||
params: PaginationInput,
|
||||
) {
|
||||
const where = and(
|
||||
eq(inventoryUnits.companyId, companyId),
|
||||
eq(inventoryUnits.productId, productId),
|
||||
)
|
||||
const where = eq(inventoryUnits.productId, productId)
|
||||
|
||||
const sortableColumns: Record<string, Column> = {
|
||||
serial_number: inventoryUnits.serialNumber,
|
||||
@@ -185,16 +177,15 @@ export const InventoryUnitService = {
|
||||
|
||||
async update(
|
||||
db: PostgresJsDatabase<any>,
|
||||
companyId: string,
|
||||
id: string,
|
||||
input: InventoryUnitUpdateInput,
|
||||
) {
|
||||
if (input.condition) {
|
||||
const valid = await ItemConditionService.validateSlug(db, companyId, input.condition)
|
||||
const valid = await ItemConditionService.validateSlug(db, input.condition)
|
||||
if (!valid) throw new ValidationError(`Invalid condition: "${input.condition}"`)
|
||||
}
|
||||
if (input.status) {
|
||||
const valid = await UnitStatusService.validateSlug(db, companyId, input.status)
|
||||
const valid = await UnitStatusService.validateSlug(db, input.status)
|
||||
if (!valid) throw new ValidationError(`Invalid status: "${input.status}"`)
|
||||
}
|
||||
|
||||
@@ -204,7 +195,7 @@ export const InventoryUnitService = {
|
||||
const [unit] = await db
|
||||
.update(inventoryUnits)
|
||||
.set(updates)
|
||||
.where(and(eq(inventoryUnits.id, id), eq(inventoryUnits.companyId, companyId)))
|
||||
.where(eq(inventoryUnits.id, id))
|
||||
.returning()
|
||||
return unit ?? null
|
||||
},
|
||||
|
||||
@@ -18,16 +18,16 @@ export const RbacService = {
|
||||
await db.insert(permissions).values(toInsert)
|
||||
},
|
||||
|
||||
/** Seed default roles for a company */
|
||||
async seedRolesForCompany(db: PostgresJsDatabase<any>, companyId: string) {
|
||||
/** Seed default roles */
|
||||
async seedDefaultRoles(db: PostgresJsDatabase<any>) {
|
||||
const existingRoles = await db
|
||||
.select({ slug: roles.slug })
|
||||
.from(roles)
|
||||
.where(and(eq(roles.companyId, companyId), eq(roles.isSystem, true)))
|
||||
.where(eq(roles.isSystem, true))
|
||||
|
||||
if (existingRoles.length > 0) return // already seeded
|
||||
|
||||
// Get all permission records for slug → id mapping
|
||||
// Get all permission records for slug -> id mapping
|
||||
const allPerms = await db.select().from(permissions)
|
||||
const permMap = new Map(allPerms.map((p) => [p.slug, p.id]))
|
||||
|
||||
@@ -35,7 +35,6 @@ export const RbacService = {
|
||||
const [role] = await db
|
||||
.insert(roles)
|
||||
.values({
|
||||
companyId,
|
||||
name: roleDef.name,
|
||||
slug: roleDef.slug,
|
||||
description: roleDef.description,
|
||||
@@ -91,9 +90,9 @@ export const RbacService = {
|
||||
return db.select().from(permissions).orderBy(permissions.domain, permissions.action)
|
||||
},
|
||||
|
||||
/** List roles for a company */
|
||||
async listRoles(db: PostgresJsDatabase<any>, companyId: string, params: PaginationInput) {
|
||||
const baseWhere = and(eq(roles.companyId, companyId), eq(roles.isActive, true))
|
||||
/** List roles */
|
||||
async listRoles(db: PostgresJsDatabase<any>, params: PaginationInput) {
|
||||
const baseWhere = eq(roles.isActive, true)
|
||||
|
||||
const searchCondition = params.q
|
||||
? buildSearchCondition(params.q, [roles.name, roles.slug])
|
||||
@@ -120,11 +119,11 @@ export const RbacService = {
|
||||
},
|
||||
|
||||
/** Get role with its permissions */
|
||||
async getRoleWithPermissions(db: PostgresJsDatabase<any>, companyId: string, roleId: string) {
|
||||
async getRoleWithPermissions(db: PostgresJsDatabase<any>, roleId: string) {
|
||||
const [role] = await db
|
||||
.select()
|
||||
.from(roles)
|
||||
.where(and(eq(roles.id, roleId), eq(roles.companyId, companyId)))
|
||||
.where(eq(roles.id, roleId))
|
||||
.limit(1)
|
||||
|
||||
if (!role) return null
|
||||
@@ -141,13 +140,11 @@ export const RbacService = {
|
||||
/** Create a custom role */
|
||||
async createRole(
|
||||
db: PostgresJsDatabase<any>,
|
||||
companyId: string,
|
||||
input: { name: string; slug: string; description?: string; permissionSlugs: string[] },
|
||||
) {
|
||||
const [role] = await db
|
||||
.insert(roles)
|
||||
.values({
|
||||
companyId,
|
||||
name: input.name,
|
||||
slug: input.slug,
|
||||
description: input.description,
|
||||
@@ -156,7 +153,7 @@ export const RbacService = {
|
||||
.returning()
|
||||
|
||||
await this.setRolePermissions(db, role.id, input.permissionSlugs)
|
||||
return this.getRoleWithPermissions(db, companyId, role.id)
|
||||
return this.getRoleWithPermissions(db, role.id)
|
||||
},
|
||||
|
||||
/** Update role permissions (replace all) */
|
||||
@@ -182,7 +179,6 @@ export const RbacService = {
|
||||
/** Update a role */
|
||||
async updateRole(
|
||||
db: PostgresJsDatabase<any>,
|
||||
companyId: string,
|
||||
roleId: string,
|
||||
input: { name?: string; description?: string; permissionSlugs?: string[] },
|
||||
) {
|
||||
@@ -194,22 +190,22 @@ export const RbacService = {
|
||||
...(input.description !== undefined ? { description: input.description } : {}),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(and(eq(roles.id, roleId), eq(roles.companyId, companyId)))
|
||||
.where(eq(roles.id, roleId))
|
||||
}
|
||||
|
||||
if (input.permissionSlugs) {
|
||||
await this.setRolePermissions(db, roleId, input.permissionSlugs)
|
||||
}
|
||||
|
||||
return this.getRoleWithPermissions(db, companyId, roleId)
|
||||
return this.getRoleWithPermissions(db, roleId)
|
||||
},
|
||||
|
||||
/** Delete a custom role */
|
||||
async deleteRole(db: PostgresJsDatabase<any>, companyId: string, roleId: string) {
|
||||
async deleteRole(db: PostgresJsDatabase<any>, roleId: string) {
|
||||
const [role] = await db
|
||||
.select()
|
||||
.from(roles)
|
||||
.where(and(eq(roles.id, roleId), eq(roles.companyId, companyId)))
|
||||
.where(eq(roles.id, roleId))
|
||||
.limit(1)
|
||||
|
||||
if (!role) return null
|
||||
|
||||
@@ -30,15 +30,13 @@ async function generateUniqueNumber(
|
||||
db: PostgresJsDatabase<any>,
|
||||
table: typeof repairTickets | typeof repairBatches,
|
||||
column: typeof repairTickets.ticketNumber | typeof repairBatches.batchNumber,
|
||||
companyId: string,
|
||||
companyIdColumn: Column,
|
||||
): 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)))
|
||||
.where(eq(column, num))
|
||||
.limit(1)
|
||||
if (!existing) return num
|
||||
}
|
||||
@@ -46,15 +44,14 @@ async function generateUniqueNumber(
|
||||
}
|
||||
|
||||
export const RepairTicketService = {
|
||||
async create(db: PostgresJsDatabase<any>, companyId: string, input: RepairTicketCreateInput) {
|
||||
async create(db: PostgresJsDatabase<any>, input: RepairTicketCreateInput) {
|
||||
const ticketNumber = await generateUniqueNumber(
|
||||
db, repairTickets, repairTickets.ticketNumber, companyId, repairTickets.companyId,
|
||||
db, repairTickets, repairTickets.ticketNumber,
|
||||
)
|
||||
|
||||
const [ticket] = await db
|
||||
.insert(repairTickets)
|
||||
.values({
|
||||
companyId,
|
||||
ticketNumber,
|
||||
customerName: input.customerName,
|
||||
customerPhone: input.customerPhone,
|
||||
@@ -76,16 +73,16 @@ export const RepairTicketService = {
|
||||
return ticket
|
||||
},
|
||||
|
||||
async getById(db: PostgresJsDatabase<any>, companyId: string, id: string) {
|
||||
async getById(db: PostgresJsDatabase<any>, id: string) {
|
||||
const [ticket] = await db
|
||||
.select()
|
||||
.from(repairTickets)
|
||||
.where(and(eq(repairTickets.id, id), eq(repairTickets.companyId, companyId)))
|
||||
.where(eq(repairTickets.id, id))
|
||||
.limit(1)
|
||||
return ticket ?? null
|
||||
},
|
||||
|
||||
async list(db: PostgresJsDatabase<any>, companyId: string, params: PaginationInput, filters?: {
|
||||
async list(db: PostgresJsDatabase<any>, params: PaginationInput, filters?: {
|
||||
status?: string[]
|
||||
conditionIn?: string[]
|
||||
isBatch?: boolean
|
||||
@@ -97,7 +94,7 @@ export const RepairTicketService = {
|
||||
completedDateFrom?: string
|
||||
completedDateTo?: string
|
||||
}) {
|
||||
const conditions: SQL[] = [eq(repairTickets.companyId, companyId)]
|
||||
const conditions: SQL[] = []
|
||||
|
||||
if (params.q) {
|
||||
const search = buildSearchCondition(params.q, [
|
||||
@@ -128,7 +125,7 @@ export const RepairTicketService = {
|
||||
if (filters?.completedDateFrom) conditions.push(gte(repairTickets.completedDate, new Date(filters.completedDateFrom)))
|
||||
if (filters?.completedDateTo) conditions.push(lte(repairTickets.completedDate, new Date(filters.completedDateTo)))
|
||||
|
||||
const where = and(...conditions)
|
||||
const where = conditions.length > 0 ? and(...conditions) : undefined
|
||||
|
||||
const sortableColumns: Record<string, Column> = {
|
||||
ticket_number: repairTickets.ticketNumber,
|
||||
@@ -151,8 +148,8 @@ export const RepairTicketService = {
|
||||
return paginatedResponse(data, total, params.page, params.limit)
|
||||
},
|
||||
|
||||
async listByBatch(db: PostgresJsDatabase<any>, companyId: string, batchId: string, params: PaginationInput) {
|
||||
const baseWhere = and(eq(repairTickets.companyId, companyId), eq(repairTickets.repairBatchId, batchId))
|
||||
async listByBatch(db: PostgresJsDatabase<any>, batchId: string, params: PaginationInput) {
|
||||
const baseWhere = eq(repairTickets.repairBatchId, batchId)
|
||||
const searchCondition = params.q
|
||||
? buildSearchCondition(params.q, [repairTickets.ticketNumber, repairTickets.customerName, repairTickets.instrumentDescription])
|
||||
: undefined
|
||||
@@ -177,7 +174,7 @@ export const RepairTicketService = {
|
||||
return paginatedResponse(data, total, params.page, params.limit)
|
||||
},
|
||||
|
||||
async update(db: PostgresJsDatabase<any>, companyId: string, id: string, input: RepairTicketUpdateInput) {
|
||||
async update(db: PostgresJsDatabase<any>, id: string, input: RepairTicketUpdateInput) {
|
||||
const values: Record<string, unknown> = { ...input, updatedAt: new Date() }
|
||||
if (input.estimatedCost !== undefined) values.estimatedCost = input.estimatedCost.toString()
|
||||
if (input.promisedDate !== undefined) values.promisedDate = input.promisedDate ? new Date(input.promisedDate) : null
|
||||
@@ -185,12 +182,12 @@ export const RepairTicketService = {
|
||||
const [ticket] = await db
|
||||
.update(repairTickets)
|
||||
.set(values)
|
||||
.where(and(eq(repairTickets.id, id), eq(repairTickets.companyId, companyId)))
|
||||
.where(eq(repairTickets.id, id))
|
||||
.returning()
|
||||
return ticket ?? null
|
||||
},
|
||||
|
||||
async updateStatus(db: PostgresJsDatabase<any>, companyId: string, id: string, status: string) {
|
||||
async updateStatus(db: PostgresJsDatabase<any>, id: string, status: string) {
|
||||
const updates: Record<string, unknown> = { status, updatedAt: new Date() }
|
||||
if (status === 'ready' || status === 'picked_up' || status === 'delivered') {
|
||||
updates.completedDate = new Date()
|
||||
@@ -199,17 +196,17 @@ export const RepairTicketService = {
|
||||
const [ticket] = await db
|
||||
.update(repairTickets)
|
||||
.set(updates)
|
||||
.where(and(eq(repairTickets.id, id), eq(repairTickets.companyId, companyId)))
|
||||
.where(eq(repairTickets.id, id))
|
||||
.returning()
|
||||
return ticket ?? null
|
||||
},
|
||||
|
||||
async delete(db: PostgresJsDatabase<any>, companyId: string, id: string) {
|
||||
async delete(db: PostgresJsDatabase<any>, id: string) {
|
||||
// Soft-cancel: set status to cancelled rather than hard delete
|
||||
const [ticket] = await db
|
||||
.update(repairTickets)
|
||||
.set({ status: 'cancelled', updatedAt: new Date() })
|
||||
.where(and(eq(repairTickets.id, id), eq(repairTickets.companyId, companyId)))
|
||||
.where(eq(repairTickets.id, id))
|
||||
.returning()
|
||||
return ticket ?? null
|
||||
},
|
||||
@@ -254,19 +251,7 @@ export const RepairLineItemService = {
|
||||
return paginatedResponse(data, total, params.page, params.limit)
|
||||
},
|
||||
|
||||
async verifyOwnership(db: PostgresJsDatabase<any>, companyId: string, lineItemId: string): Promise<boolean> {
|
||||
const [item] = await db
|
||||
.select({ ticketCompanyId: repairTickets.companyId })
|
||||
.from(repairLineItems)
|
||||
.innerJoin(repairTickets, eq(repairLineItems.repairTicketId, repairTickets.id))
|
||||
.where(and(eq(repairLineItems.id, lineItemId), eq(repairTickets.companyId, companyId)))
|
||||
.limit(1)
|
||||
return !!item
|
||||
},
|
||||
|
||||
async update(db: PostgresJsDatabase<any>, companyId: string, id: string, input: RepairLineItemUpdateInput) {
|
||||
if (!(await this.verifyOwnership(db, companyId, id))) return null
|
||||
|
||||
async update(db: PostgresJsDatabase<any>, id: string, input: RepairLineItemUpdateInput) {
|
||||
const values: Record<string, unknown> = { ...input }
|
||||
if (input.qty !== undefined) values.qty = input.qty.toString()
|
||||
if (input.unitPrice !== undefined) values.unitPrice = input.unitPrice.toString()
|
||||
@@ -281,9 +266,7 @@ export const RepairLineItemService = {
|
||||
return item ?? null
|
||||
},
|
||||
|
||||
async delete(db: PostgresJsDatabase<any>, companyId: string, id: string) {
|
||||
if (!(await this.verifyOwnership(db, companyId, id))) return null
|
||||
|
||||
async delete(db: PostgresJsDatabase<any>, id: string) {
|
||||
const [item] = await db
|
||||
.delete(repairLineItems)
|
||||
.where(eq(repairLineItems.id, id))
|
||||
@@ -293,15 +276,14 @@ export const RepairLineItemService = {
|
||||
}
|
||||
|
||||
export const RepairBatchService = {
|
||||
async create(db: PostgresJsDatabase<any>, companyId: string, input: RepairBatchCreateInput) {
|
||||
async create(db: PostgresJsDatabase<any>, input: RepairBatchCreateInput) {
|
||||
const batchNumber = await generateUniqueNumber(
|
||||
db, repairBatches, repairBatches.batchNumber, companyId, repairBatches.companyId,
|
||||
db, repairBatches, repairBatches.batchNumber,
|
||||
)
|
||||
|
||||
const [batch] = await db
|
||||
.insert(repairBatches)
|
||||
.values({
|
||||
companyId,
|
||||
batchNumber,
|
||||
accountId: input.accountId,
|
||||
locationId: input.locationId,
|
||||
@@ -317,21 +299,19 @@ export const RepairBatchService = {
|
||||
return batch
|
||||
},
|
||||
|
||||
async getById(db: PostgresJsDatabase<any>, companyId: string, id: string) {
|
||||
async getById(db: PostgresJsDatabase<any>, id: string) {
|
||||
const [batch] = await db
|
||||
.select()
|
||||
.from(repairBatches)
|
||||
.where(and(eq(repairBatches.id, id), eq(repairBatches.companyId, companyId)))
|
||||
.where(eq(repairBatches.id, id))
|
||||
.limit(1)
|
||||
return batch ?? null
|
||||
},
|
||||
|
||||
async list(db: PostgresJsDatabase<any>, companyId: string, params: PaginationInput) {
|
||||
const baseWhere = eq(repairBatches.companyId, companyId)
|
||||
async list(db: PostgresJsDatabase<any>, params: PaginationInput) {
|
||||
const searchCondition = params.q
|
||||
? buildSearchCondition(params.q, [repairBatches.batchNumber, repairBatches.contactName, repairBatches.contactEmail])
|
||||
: undefined
|
||||
const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere
|
||||
|
||||
const sortableColumns: Record<string, Column> = {
|
||||
batch_number: repairBatches.batchNumber,
|
||||
@@ -340,19 +320,19 @@ export const RepairBatchService = {
|
||||
created_at: repairBatches.createdAt,
|
||||
}
|
||||
|
||||
let query = db.select().from(repairBatches).where(where).$dynamic()
|
||||
let query = db.select().from(repairBatches).where(searchCondition).$dynamic()
|
||||
query = withSort(query, params.sort, params.order, sortableColumns, repairBatches.createdAt)
|
||||
query = withPagination(query, params.page, params.limit)
|
||||
|
||||
const [data, [{ total }]] = await Promise.all([
|
||||
query,
|
||||
db.select({ total: count() }).from(repairBatches).where(where),
|
||||
db.select({ total: count() }).from(repairBatches).where(searchCondition),
|
||||
])
|
||||
|
||||
return paginatedResponse(data, total, params.page, params.limit)
|
||||
},
|
||||
|
||||
async update(db: PostgresJsDatabase<any>, companyId: string, id: string, input: RepairBatchUpdateInput) {
|
||||
async update(db: PostgresJsDatabase<any>, id: string, input: RepairBatchUpdateInput) {
|
||||
const values: Record<string, unknown> = { ...input, updatedAt: new Date() }
|
||||
if (input.pickupDate !== undefined) values.pickupDate = input.pickupDate ? new Date(input.pickupDate) : null
|
||||
if (input.dueDate !== undefined) values.dueDate = input.dueDate ? new Date(input.dueDate) : null
|
||||
@@ -360,12 +340,12 @@ export const RepairBatchService = {
|
||||
const [batch] = await db
|
||||
.update(repairBatches)
|
||||
.set(values)
|
||||
.where(and(eq(repairBatches.id, id), eq(repairBatches.companyId, companyId)))
|
||||
.where(eq(repairBatches.id, id))
|
||||
.returning()
|
||||
return batch ?? null
|
||||
},
|
||||
|
||||
async updateStatus(db: PostgresJsDatabase<any>, companyId: string, id: string, status: string) {
|
||||
async updateStatus(db: PostgresJsDatabase<any>, id: string, status: string) {
|
||||
const updates: Record<string, unknown> = { status, updatedAt: new Date() }
|
||||
if (status === 'completed') updates.completedDate = new Date()
|
||||
if (status === 'delivered') updates.deliveredDate = new Date()
|
||||
@@ -373,12 +353,12 @@ export const RepairBatchService = {
|
||||
const [batch] = await db
|
||||
.update(repairBatches)
|
||||
.set(updates)
|
||||
.where(and(eq(repairBatches.id, id), eq(repairBatches.companyId, companyId)))
|
||||
.where(eq(repairBatches.id, id))
|
||||
.returning()
|
||||
return batch ?? null
|
||||
},
|
||||
|
||||
async approve(db: PostgresJsDatabase<any>, companyId: string, id: string, approvedBy: string) {
|
||||
async approve(db: PostgresJsDatabase<any>, id: string, approvedBy: string) {
|
||||
const [batch] = await db
|
||||
.update(repairBatches)
|
||||
.set({
|
||||
@@ -387,30 +367,29 @@ export const RepairBatchService = {
|
||||
approvedAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(and(eq(repairBatches.id, id), eq(repairBatches.companyId, companyId)))
|
||||
.where(eq(repairBatches.id, id))
|
||||
.returning()
|
||||
return batch ?? null
|
||||
},
|
||||
|
||||
async reject(db: PostgresJsDatabase<any>, companyId: string, id: string) {
|
||||
async reject(db: PostgresJsDatabase<any>, id: string) {
|
||||
const [batch] = await db
|
||||
.update(repairBatches)
|
||||
.set({
|
||||
approvalStatus: 'rejected',
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(and(eq(repairBatches.id, id), eq(repairBatches.companyId, companyId)))
|
||||
.where(eq(repairBatches.id, id))
|
||||
.returning()
|
||||
return batch ?? null
|
||||
},
|
||||
}
|
||||
|
||||
export const RepairServiceTemplateService = {
|
||||
async create(db: PostgresJsDatabase<any>, companyId: string, input: RepairServiceTemplateCreateInput) {
|
||||
async create(db: PostgresJsDatabase<any>, input: RepairServiceTemplateCreateInput) {
|
||||
const [template] = await db
|
||||
.insert(repairServiceTemplates)
|
||||
.values({
|
||||
companyId,
|
||||
name: input.name,
|
||||
instrumentType: input.instrumentType,
|
||||
size: input.size,
|
||||
@@ -424,8 +403,8 @@ export const RepairServiceTemplateService = {
|
||||
return template
|
||||
},
|
||||
|
||||
async list(db: PostgresJsDatabase<any>, companyId: string, params: PaginationInput) {
|
||||
const baseWhere = and(eq(repairServiceTemplates.companyId, companyId), eq(repairServiceTemplates.isActive, true))
|
||||
async list(db: PostgresJsDatabase<any>, params: PaginationInput) {
|
||||
const baseWhere = eq(repairServiceTemplates.isActive, true)
|
||||
const searchCondition = params.q
|
||||
? buildSearchCondition(params.q, [repairServiceTemplates.name, repairServiceTemplates.instrumentType, repairServiceTemplates.size, repairServiceTemplates.description])
|
||||
: undefined
|
||||
@@ -451,7 +430,7 @@ export const RepairServiceTemplateService = {
|
||||
return paginatedResponse(data, total, params.page, params.limit)
|
||||
},
|
||||
|
||||
async update(db: PostgresJsDatabase<any>, companyId: string, id: string, input: RepairServiceTemplateUpdateInput) {
|
||||
async update(db: PostgresJsDatabase<any>, id: string, input: RepairServiceTemplateUpdateInput) {
|
||||
const values: Record<string, unknown> = { ...input, updatedAt: new Date() }
|
||||
if (input.defaultPrice !== undefined) values.defaultPrice = input.defaultPrice.toString()
|
||||
if (input.defaultCost !== undefined) values.defaultCost = input.defaultCost.toString()
|
||||
@@ -459,16 +438,16 @@ export const RepairServiceTemplateService = {
|
||||
const [template] = await db
|
||||
.update(repairServiceTemplates)
|
||||
.set(values)
|
||||
.where(and(eq(repairServiceTemplates.id, id), eq(repairServiceTemplates.companyId, companyId)))
|
||||
.where(eq(repairServiceTemplates.id, id))
|
||||
.returning()
|
||||
return template ?? null
|
||||
},
|
||||
|
||||
async delete(db: PostgresJsDatabase<any>, companyId: string, id: string) {
|
||||
async delete(db: PostgresJsDatabase<any>, id: string) {
|
||||
const [template] = await db
|
||||
.update(repairServiceTemplates)
|
||||
.set({ isActive: false, updatedAt: new Date() })
|
||||
.where(and(eq(repairServiceTemplates.id, id), eq(repairServiceTemplates.companyId, companyId)))
|
||||
.where(eq(repairServiceTemplates.id, id))
|
||||
.returning()
|
||||
return template ?? null
|
||||
},
|
||||
@@ -514,16 +493,7 @@ export const RepairNoteService = {
|
||||
return paginatedResponse(data, total, params.page, params.limit)
|
||||
},
|
||||
|
||||
async delete(db: PostgresJsDatabase<any>, companyId: string, id: string) {
|
||||
// Verify note belongs to a ticket owned by this company
|
||||
const [owned] = await db
|
||||
.select({ id: repairNotes.id })
|
||||
.from(repairNotes)
|
||||
.innerJoin(repairTickets, eq(repairNotes.repairTicketId, repairTickets.id))
|
||||
.where(and(eq(repairNotes.id, id), eq(repairTickets.companyId, companyId)))
|
||||
.limit(1)
|
||||
if (!owned) return null
|
||||
|
||||
async delete(db: PostgresJsDatabase<any>, id: string) {
|
||||
const [note] = await db
|
||||
.delete(repairNotes)
|
||||
.where(eq(repairNotes.id, id))
|
||||
|
||||
Reference in New Issue
Block a user