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:
Ryan Moon
2026-03-29 14:58:33 -05:00
parent 55f8591cf1
commit d36c6f7135
35 changed files with 353 additions and 511 deletions

1
.gitignore vendored
View File

@@ -37,3 +37,4 @@ logs/
*.pem
*.key
credentials.json
packages/backend/data/

View File

@@ -11,7 +11,6 @@ interface User {
interface AuthState {
token: string | null
user: User | null
companyId: string | null
permissions: Set<string>
permissionsLoaded: boolean
setAuth: (token: string, user: User) => void
@@ -49,12 +48,7 @@ function expandPermissions(slugs: string[]): Set<string> {
return expanded
}
function decodeJwtPayload(token: string): { id: string; companyId: string; role: string } {
const payload = token.split('.')[1]
return JSON.parse(atob(payload))
}
function loadSession(): { token: string; user: User; companyId: string; permissions?: string[] } | null {
function loadSession(): { token: string; user: User; permissions?: string[] } | null {
try {
const raw = sessionStorage.getItem('forte-auth')
if (!raw) return null
@@ -64,8 +58,8 @@ function loadSession(): { token: string; user: User; companyId: string; permissi
}
}
function saveSession(token: string, user: User, companyId: string, permissions?: string[]) {
sessionStorage.setItem('forte-auth', JSON.stringify({ token, user, companyId, permissions }))
function saveSession(token: string, user: User, permissions?: string[]) {
sessionStorage.setItem('forte-auth', JSON.stringify({ token, user, permissions }))
}
function clearSession() {
@@ -78,22 +72,19 @@ export const useAuthStore = create<AuthState>((set, get) => {
return {
token: initial?.token ?? null,
user: initial?.user ?? null,
companyId: initial?.companyId ?? null,
permissions: initialPerms,
permissionsLoaded: initialPerms.size > 0,
setAuth: (token, user) => {
const payload = decodeJwtPayload(token)
saveSession(token, user, payload.companyId)
set({ token, user, companyId: payload.companyId })
saveSession(token, user)
set({ token, user })
},
setPermissions: (slugs: string[]) => {
const expanded = expandPermissions(slugs)
// Update session storage to include permissions
const { token, user, companyId } = get()
if (token && user && companyId) {
saveSession(token, user, companyId, slugs)
const { token, user } = get()
if (token && user) {
saveSession(token, user, slugs)
}
set({ permissions: expanded, permissionsLoaded: true })
},
@@ -104,6 +95,6 @@ export const useAuthStore = create<AuthState>((set, get) => {
logout: () => {
clearSession()
set({ token: null, user: null, companyId: null, permissions: new Set(), permissionsLoaded: false })
set({ token: null, user: null, permissions: new Set(), permissionsLoaded: false })
},
}})

View File

@@ -1,6 +1,5 @@
export interface Account {
id: string
companyId: string
accountNumber: string | null
name: string
primaryMemberId: string | null
@@ -25,7 +24,6 @@ export interface Account {
export interface Member {
id: string
accountId: string
companyId: string
memberNumber: string | null
firstName: string
lastName: string
@@ -42,7 +40,6 @@ export interface Member {
export interface ProcessorLink {
id: string
accountId: string
companyId: string
processor: 'stripe' | 'global_payments'
processorCustomerId: string
isActive: boolean
@@ -52,7 +49,6 @@ export interface ProcessorLink {
export interface PaymentMethod {
id: string
accountId: string
companyId: string
processor: 'stripe' | 'global_payments'
processorPaymentMethodId: string
cardBrand: string | null
@@ -67,7 +63,6 @@ export interface PaymentMethod {
export interface MemberIdentifier {
id: string
memberId: string
companyId: string
type: 'drivers_license' | 'passport' | 'school_id'
label: string | null
value: string
@@ -85,7 +80,6 @@ export interface MemberIdentifier {
export interface TaxExemption {
id: string
accountId: string
companyId: string
status: 'none' | 'pending' | 'approved'
certificateNumber: string
certificateType: string | null

View File

@@ -1,6 +1,5 @@
export interface RepairTicket {
id: string
companyId: string
locationId: string | null
repairBatchId: string | null
ticketNumber: string | null
@@ -42,7 +41,6 @@ export interface RepairLineItem {
export interface RepairBatch {
id: string
companyId: string
locationId: string | null
batchNumber: string | null
accountId: string
@@ -80,7 +78,6 @@ export interface RepairNote {
export interface RepairServiceTemplate {
id: string
companyId: string
name: string
instrumentType: string | null
size: string | null

View File

@@ -45,7 +45,6 @@ describe('Account routes', () => {
const body = response.json()
expect(body.name).toBe('Smith Family')
expect(body.email).toBe('smith@example.com')
expect(body.companyId).toBe(TEST_COMPANY_ID)
expect(body.billingMode).toBe('consolidated')
})

View File

@@ -59,17 +59,17 @@ async function setupDatabase() {
END $$
`)
// Seed company + location
// Seed company + location (company table stays as store settings)
await testSql`INSERT INTO company (id, name, timezone) VALUES (${COMPANY_ID}, 'Test Music Co.', 'America/Chicago')`
await testSql`INSERT INTO location (id, company_id, name) VALUES (${LOCATION_ID}, ${COMPANY_ID}, 'Test Location')`
await testSql`INSERT INTO location (id, name) VALUES (${LOCATION_ID}, 'Test Location')`
// Seed lookup tables
const { SYSTEM_UNIT_STATUSES, SYSTEM_ITEM_CONDITIONS } = await import('../src/db/schema/lookups.js')
for (const s of SYSTEM_UNIT_STATUSES) {
await testSql`INSERT INTO inventory_unit_status (company_id, name, slug, description, is_system, sort_order) VALUES (${COMPANY_ID}, ${s.name}, ${s.slug}, ${s.description}, true, ${s.sortOrder})`
await testSql`INSERT INTO inventory_unit_status (name, slug, description, is_system, sort_order) VALUES (${s.name}, ${s.slug}, ${s.description}, true, ${s.sortOrder})`
}
for (const c of SYSTEM_ITEM_CONDITIONS) {
await testSql`INSERT INTO item_condition (company_id, name, slug, description, is_system, sort_order) VALUES (${COMPANY_ID}, ${c.name}, ${c.slug}, ${c.description}, true, ${c.sortOrder})`
await testSql`INSERT INTO item_condition (name, slug, description, is_system, sort_order) VALUES (${c.name}, ${c.slug}, ${c.description}, true, ${c.sortOrder})`
}
// Seed RBAC permissions and default roles
@@ -82,7 +82,7 @@ async function setupDatabase() {
const permMap = new Map(permRows.map((r: any) => [r.slug, r.id]))
for (const roleDef of DEFAULT_ROLES) {
const [role] = await testSql`INSERT INTO role (company_id, name, slug, description, is_system) VALUES (${COMPANY_ID}, ${roleDef.name}, ${roleDef.slug}, ${roleDef.description}, true) RETURNING id`
const [role] = await testSql`INSERT INTO role (name, slug, description, is_system) VALUES (${roleDef.name}, ${roleDef.slug}, ${roleDef.description}, true) RETURNING id`
for (const permSlug of roleDef.permissions) {
const permId = permMap.get(permSlug)
if (permId) {
@@ -149,8 +149,7 @@ async function startBackend(): Promise<Subprocess> {
async function registerTestUser(): Promise<string> {
const testPassword = 'testpassword1234'
// Register needs x-company-id header
const headers = { 'Content-Type': 'application/json', 'x-company-id': COMPANY_ID }
const headers = { 'Content-Type': 'application/json' }
const registerRes = await fetch(`${BASE_URL}/v1/auth/register`, {
method: 'POST',
headers,
@@ -167,7 +166,7 @@ async function registerTestUser(): Promise<string> {
// Assign admin role to the user via direct SQL
if (registerRes.status === 201 && registerData.user) {
const assignSql = postgres(`postgresql://${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/${TEST_DB}`)
const [adminRole] = await assignSql`SELECT id FROM role WHERE company_id = ${COMPANY_ID} AND slug = 'admin' LIMIT 1`
const [adminRole] = await assignSql`SELECT id FROM role WHERE slug = 'admin' LIMIT 1`
if (adminRole) {
await assignSql`INSERT INTO user_role_assignment (user_id, role_id) VALUES (${registerData.user.id}, ${adminRole.id}) ON CONFLICT DO NOTHING`
}

View File

@@ -6,10 +6,10 @@ suite('RBAC', { tags: ['rbac', 'permissions'] }, (t) => {
const email = `restricted-${Date.now()}@test.com`
const password = 'testpassword1234'
// Register via raw fetch (needs x-company-id)
// Register via raw fetch
const registerRes = await fetch(`${t.baseUrl}/v1/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-company-id': 'a0000000-0000-0000-0000-000000000001' },
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, firstName: 'Restricted', lastName: 'User', role: 'staff' }),
})
const registerData = await registerRes.json() as { token: string }
@@ -31,7 +31,7 @@ suite('RBAC', { tags: ['rbac', 'permissions'] }, (t) => {
const registerRes = await fetch(`${t.baseUrl}/v1/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-company-id': 'a0000000-0000-0000-0000-000000000001' },
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, firstName: roleSlug, lastName: 'User', role: 'staff' }),
})
const registerData = await registerRes.json() as { user: { id: string } }
@@ -143,7 +143,7 @@ suite('RBAC', { tags: ['rbac', 'permissions'] }, (t) => {
const password = 'testpassword1234'
const regRes = await fetch(`${t.baseUrl}/v1/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-company-id': 'a0000000-0000-0000-0000-000000000001' },
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, firstName: 'Inherit', lastName: 'Test', role: 'staff' }),
})
const regData = await regRes.json() as { user: { id: string } }
@@ -233,7 +233,7 @@ suite('RBAC', { tags: ['rbac', 'permissions'] }, (t) => {
// Create a user with a distinctive name
await fetch(`${t.baseUrl}/v1/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-company-id': 'a0000000-0000-0000-0000-000000000001' },
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: `searchme-${Date.now()}@test.com`, password: 'testpassword1234', firstName: 'Searchable', lastName: 'Pessoa', role: 'staff' }),
})
@@ -257,7 +257,7 @@ suite('RBAC', { tags: ['rbac', 'permissions'] }, (t) => {
const password = 'testpassword1234'
const regRes = await fetch(`${t.baseUrl}/v1/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-company-id': 'a0000000-0000-0000-0000-000000000001' },
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, firstName: 'Disable', lastName: 'Me', role: 'staff' }),
})
const regData = await regRes.json() as { user: { id: string } }

View File

@@ -0,0 +1,27 @@
-- Remove company_id columns and FK constraints from all domain tables.
-- The company table stays as store settings, but tenant scoping is removed.
-- In a SaaS deployment, isolation is at the database level, not application level.
-- Drop company_id from each table (CASCADE drops the FK constraint automatically)
ALTER TABLE "account" DROP COLUMN IF EXISTS "company_id";
ALTER TABLE "member" DROP COLUMN IF EXISTS "company_id";
ALTER TABLE "member_identifier" DROP COLUMN IF EXISTS "company_id";
ALTER TABLE "account_processor_link" DROP COLUMN IF EXISTS "company_id";
ALTER TABLE "account_payment_method" DROP COLUMN IF EXISTS "company_id";
ALTER TABLE "tax_exemption" DROP COLUMN IF EXISTS "company_id";
ALTER TABLE "file" DROP COLUMN IF EXISTS "company_id";
ALTER TABLE "category" DROP COLUMN IF EXISTS "company_id";
ALTER TABLE "supplier" DROP COLUMN IF EXISTS "company_id";
ALTER TABLE "product" DROP COLUMN IF EXISTS "company_id";
ALTER TABLE "inventory_unit" DROP COLUMN IF EXISTS "company_id";
ALTER TABLE "stock_receipt" DROP COLUMN IF EXISTS "company_id";
ALTER TABLE "price_history" DROP COLUMN IF EXISTS "company_id";
ALTER TABLE "consignment_detail" DROP COLUMN IF EXISTS "company_id";
ALTER TABLE "inventory_unit_status" DROP COLUMN IF EXISTS "company_id";
ALTER TABLE "item_condition" DROP COLUMN IF EXISTS "company_id";
ALTER TABLE "role" DROP COLUMN IF EXISTS "company_id";
ALTER TABLE "repair_batch" DROP COLUMN IF EXISTS "company_id";
ALTER TABLE "repair_ticket" DROP COLUMN IF EXISTS "company_id";
ALTER TABLE "repair_service_template" DROP COLUMN IF EXISTS "company_id";
ALTER TABLE "location" DROP COLUMN IF EXISTS "company_id";
ALTER TABLE "user" DROP COLUMN IF EXISTS "company_id";

View File

@@ -148,6 +148,13 @@
"when": 1774800000000,
"tag": "0020_repair_default_new",
"breakpoints": true
},
{
"idx": 21,
"version": "7",
"when": 1774810000000,
"tag": "0021_remove_company_scoping",
"breakpoints": true
}
]
}

View File

@@ -10,16 +10,12 @@ import {
date,
pgEnum,
} from 'drizzle-orm/pg-core'
import { companies } from './stores.js'
export const billingModeEnum = pgEnum('billing_mode', ['consolidated', 'split'])
export const taxExemptStatusEnum = pgEnum('tax_exempt_status', ['none', 'pending', 'approved'])
export const accounts = pgTable('account', {
id: uuid('id').primaryKey().defaultRandom(),
companyId: uuid('company_id')
.notNull()
.references(() => companies.id),
accountNumber: varchar('account_number', { length: 50 }),
name: varchar('name', { length: 255 }).notNull(),
email: varchar('email', { length: 255 }),
@@ -46,9 +42,6 @@ export const members = pgTable('member', {
accountId: uuid('account_id')
.notNull()
.references(() => accounts.id),
companyId: uuid('company_id')
.notNull()
.references(() => companies.id),
memberNumber: varchar('member_number', { length: 50 }),
firstName: varchar('first_name', { length: 100 }).notNull(),
lastName: varchar('last_name', { length: 100 }).notNull(),
@@ -73,9 +66,6 @@ export const memberIdentifiers = pgTable('member_identifier', {
memberId: uuid('member_id')
.notNull()
.references(() => members.id),
companyId: uuid('company_id')
.notNull()
.references(() => companies.id),
type: varchar('type', { length: 50 }).notNull(),
label: varchar('label', { length: 100 }),
value: varchar('value', { length: 255 }).notNull(),
@@ -100,9 +90,6 @@ export const accountProcessorLinks = pgTable('account_processor_link', {
accountId: uuid('account_id')
.notNull()
.references(() => accounts.id),
companyId: uuid('company_id')
.notNull()
.references(() => companies.id),
processor: processorEnum('processor').notNull(),
processorCustomerId: varchar('processor_customer_id', { length: 255 }).notNull(),
isActive: boolean('is_active').notNull().default(true),
@@ -117,9 +104,6 @@ export const accountPaymentMethods = pgTable('account_payment_method', {
accountId: uuid('account_id')
.notNull()
.references(() => accounts.id),
companyId: uuid('company_id')
.notNull()
.references(() => companies.id),
processor: processorEnum('processor').notNull(),
processorPaymentMethodId: varchar('processor_payment_method_id', { length: 255 }).notNull(),
cardBrand: varchar('card_brand', { length: 50 }),
@@ -139,9 +123,6 @@ export const taxExemptions = pgTable('tax_exemption', {
accountId: uuid('account_id')
.notNull()
.references(() => accounts.id),
companyId: uuid('company_id')
.notNull()
.references(() => companies.id),
status: taxExemptStatusEnum('status').notNull().default('pending'),
certificateNumber: varchar('certificate_number', { length: 255 }).notNull(),
certificateType: varchar('certificate_type', { length: 100 }),

View File

@@ -1,11 +1,7 @@
import { pgTable, uuid, varchar, integer, timestamp } from 'drizzle-orm/pg-core'
import { companies } from './stores.js'
export const files = pgTable('file', {
id: uuid('id').primaryKey().defaultRandom(),
companyId: uuid('company_id')
.notNull()
.references(() => companies.id),
path: varchar('path', { length: 1000 }).notNull(),
filename: varchar('filename', { length: 255 }).notNull(),
contentType: varchar('content_type', { length: 100 }).notNull(),

View File

@@ -9,13 +9,10 @@ import {
numeric,
date,
} from 'drizzle-orm/pg-core'
import { companies, locations } from './stores.js'
import { locations } from './stores.js'
export const categories = pgTable('category', {
id: uuid('id').primaryKey().defaultRandom(),
companyId: uuid('company_id')
.notNull()
.references(() => companies.id),
parentId: uuid('parent_id'),
name: varchar('name', { length: 255 }).notNull(),
description: text('description'),
@@ -27,9 +24,6 @@ export const categories = pgTable('category', {
export const suppliers = pgTable('supplier', {
id: uuid('id').primaryKey().defaultRandom(),
companyId: uuid('company_id')
.notNull()
.references(() => companies.id),
name: varchar('name', { length: 255 }).notNull(),
contactName: varchar('contact_name', { length: 255 }),
email: varchar('email', { length: 255 }),
@@ -49,9 +43,6 @@ export const suppliers = pgTable('supplier', {
export const products = pgTable('product', {
id: uuid('id').primaryKey().defaultRandom(),
companyId: uuid('company_id')
.notNull()
.references(() => companies.id),
locationId: uuid('location_id').references(() => locations.id),
sku: varchar('sku', { length: 100 }),
upc: varchar('upc', { length: 100 }),
@@ -79,9 +70,6 @@ export const inventoryUnits = pgTable('inventory_unit', {
productId: uuid('product_id')
.notNull()
.references(() => products.id),
companyId: uuid('company_id')
.notNull()
.references(() => companies.id),
locationId: uuid('location_id').references(() => locations.id),
serialNumber: varchar('serial_number', { length: 255 }),
condition: varchar('condition', { length: 100 }).notNull().default('new'),
@@ -112,9 +100,6 @@ export type Supplier = typeof suppliers.$inferSelect
export type SupplierInsert = typeof suppliers.$inferInsert
export const stockReceipts = pgTable('stock_receipt', {
id: uuid('id').primaryKey().defaultRandom(),
companyId: uuid('company_id')
.notNull()
.references(() => companies.id),
locationId: uuid('location_id').references(() => locations.id),
productId: uuid('product_id')
.notNull()
@@ -136,9 +121,6 @@ export const priceHistory = pgTable('price_history', {
productId: uuid('product_id')
.notNull()
.references(() => products.id),
companyId: uuid('company_id')
.notNull()
.references(() => companies.id),
previousPrice: numeric('previous_price', { precision: 10, scale: 2 }),
newPrice: numeric('new_price', { precision: 10, scale: 2 }).notNull(),
previousMinPrice: numeric('previous_min_price', { precision: 10, scale: 2 }),
@@ -158,9 +140,6 @@ export const consignmentDetails = pgTable('consignment_detail', {
productId: uuid('product_id')
.notNull()
.references(() => products.id),
companyId: uuid('company_id')
.notNull()
.references(() => companies.id),
consignorAccountId: uuid('consignor_account_id').notNull(),
commissionPercent: numeric('commission_percent', { precision: 5, scale: 2 }).notNull(),
minPrice: numeric('min_price', { precision: 10, scale: 2 }),

View File

@@ -1,5 +1,4 @@
import { pgTable, uuid, varchar, text, timestamp, boolean, integer } from 'drizzle-orm/pg-core'
import { companies } from './stores.js'
/**
* Lookup tables replace hard-coded pgEnums for values that stores may want to customize.
@@ -9,9 +8,6 @@ import { companies } from './stores.js'
export const inventoryUnitStatuses = pgTable('inventory_unit_status', {
id: uuid('id').primaryKey().defaultRandom(),
companyId: uuid('company_id')
.notNull()
.references(() => companies.id),
name: varchar('name', { length: 100 }).notNull(),
slug: varchar('slug', { length: 100 }).notNull(),
description: text('description'),
@@ -23,9 +19,6 @@ export const inventoryUnitStatuses = pgTable('inventory_unit_status', {
export const itemConditions = pgTable('item_condition', {
id: uuid('id').primaryKey().defaultRandom(),
companyId: uuid('company_id')
.notNull()
.references(() => companies.id),
name: varchar('name', { length: 100 }).notNull(),
slug: varchar('slug', { length: 100 }).notNull(),
description: text('description'),

View File

@@ -1,5 +1,4 @@
import { pgTable, uuid, varchar, text, timestamp, boolean, uniqueIndex } from 'drizzle-orm/pg-core'
import { companies } from './stores.js'
import { users } from './users.js'
export const permissions = pgTable('permission', {
@@ -13,9 +12,6 @@ export const permissions = pgTable('permission', {
export const roles = pgTable('role', {
id: uuid('id').primaryKey().defaultRandom(),
companyId: uuid('company_id')
.notNull()
.references(() => companies.id),
name: varchar('name', { length: 100 }).notNull(),
slug: varchar('slug', { length: 100 }).notNull(),
description: text('description'),

View File

@@ -9,7 +9,7 @@ import {
numeric,
pgEnum,
} from 'drizzle-orm/pg-core'
import { companies, locations } from './stores.js'
import { locations } from './stores.js'
import { accounts } from './accounts.js'
import { inventoryUnits, products } from './inventory.js'
import { users } from './users.js'
@@ -66,9 +66,6 @@ export const repairBatchApprovalEnum = pgEnum('repair_batch_approval', [
// Defined before repairTickets because tickets FK to batches
export const repairBatches = pgTable('repair_batch', {
id: uuid('id').primaryKey().defaultRandom(),
companyId: uuid('company_id')
.notNull()
.references(() => companies.id),
locationId: uuid('location_id').references(() => locations.id),
batchNumber: varchar('batch_number', { length: 50 }),
accountId: uuid('account_id')
@@ -97,9 +94,6 @@ export const repairBatches = pgTable('repair_batch', {
export const repairTickets = pgTable('repair_ticket', {
id: uuid('id').primaryKey().defaultRandom(),
companyId: uuid('company_id')
.notNull()
.references(() => companies.id),
locationId: uuid('location_id').references(() => locations.id),
repairBatchId: uuid('repair_batch_id').references(() => repairBatches.id),
ticketNumber: varchar('ticket_number', { length: 50 }),
@@ -163,9 +157,6 @@ export type RepairNoteInsert = typeof repairNotes.$inferInsert
export const repairServiceTemplates = pgTable('repair_service_template', {
id: uuid('id').primaryKey().defaultRandom(),
companyId: uuid('company_id')
.notNull()
.references(() => companies.id),
name: varchar('name', { length: 255 }).notNull(),
instrumentType: varchar('instrument_type', { length: 100 }),
size: varchar('size', { length: 50 }),

View File

@@ -1,5 +1,4 @@
import { pgTable, uuid, varchar, timestamp, pgEnum, uniqueIndex, boolean } from 'drizzle-orm/pg-core'
import { companies } from './stores.js'
export const userRoleEnum = pgEnum('user_role', [
'admin',
@@ -11,9 +10,6 @@ export const userRoleEnum = pgEnum('user_role', [
export const users = pgTable('user', {
id: uuid('id').primaryKey().defaultRandom(),
companyId: uuid('company_id')
.notNull()
.references(() => companies.id),
email: varchar('email', { length: 255 }).notNull().unique(),
passwordHash: varchar('password_hash', { length: 255 }).notNull(),
firstName: varchar('first_name', { length: 100 }).notNull(),

View File

@@ -39,7 +39,7 @@ async function seed() {
}
// --- Admin user (if not exists) ---
const [adminUser] = await sql`SELECT id FROM "user" WHERE email = 'admin@forte.dev'`
const [adminUser] = await sql`SELECT id, company_id FROM "user" WHERE email = 'admin@forte.dev'`
if (!adminUser) {
const bcrypt = await import('bcrypt')
const hashedPw = await (bcrypt.default || bcrypt).hash('admin1234', 10)
@@ -50,9 +50,14 @@ async function seed() {
}
console.log(' Created admin user: admin@forte.dev / admin1234')
} else {
// Make sure admin role is assigned
// Ensure admin is in the right company and has the admin role
if (adminUser.company_id !== COMPANY_ID) {
await sql`UPDATE "user" SET company_id = ${COMPANY_ID} WHERE id = ${adminUser.id}`
console.log(' Moved admin user to correct company')
}
const [adminRole] = await sql`SELECT id FROM role WHERE company_id = ${COMPANY_ID} AND slug = 'admin' LIMIT 1`
if (adminRole) {
await sql`DELETE FROM user_role_assignment WHERE user_id = ${adminUser.id}`
await sql`INSERT INTO user_role_assignment (user_id, role_id) VALUES (${adminUser.id}, ${adminRole.id}) ON CONFLICT DO NOTHING`
}
}

View File

@@ -6,17 +6,16 @@ import { RbacService } from '../services/rbac.service.js'
declare module 'fastify' {
interface FastifyRequest {
companyId: string
locationId: string
user: { id: string; companyId: string; role: string }
user: { id: string; role: string }
permissions: Set<string>
}
}
declare module '@fastify/jwt' {
interface FastifyJWT {
payload: { id: string; companyId: string; role: string }
user: { id: string; companyId: string; role: string }
payload: { id: string; role: string }
user: { id: string; role: string }
}
}
@@ -61,10 +60,7 @@ export const authPlugin = fp(async (app) => {
sign: { expiresIn: '24h' },
})
// Set companyId from header on all requests (for unauthenticated routes like register/login).
// Authenticated routes override this with the JWT payload via the authenticate decorator.
app.addHook('onRequest', async (request) => {
request.companyId = (request.headers['x-company-id'] as string) ?? ''
request.locationId = (request.headers['x-location-id'] as string) ?? ''
request.permissions = new Set()
})
@@ -72,7 +68,6 @@ export const authPlugin = fp(async (app) => {
app.decorate('authenticate', async function (request: any, reply: any) {
try {
await request.jwtVerify()
request.companyId = request.user.companyId
// Check if user account is active
const [dbUser] = await app.db

View File

@@ -2,9 +2,8 @@ import fp from 'fastify-plugin'
declare module 'fastify' {
interface FastifyRequest {
companyId: string
locationId: string
user: { id: string; companyId: string; role: string }
user: { id: string; role: string }
}
}
@@ -18,18 +17,14 @@ export const devAuthPlugin = fp(async (app) => {
}
app.addHook('onRequest', async (request) => {
const companyId =
(request.headers['x-company-id'] as string) ?? '00000000-0000-0000-0000-000000000001'
const locationId =
(request.headers['x-location-id'] as string) ?? '00000000-0000-0000-0000-000000000010'
const userId =
(request.headers['x-user-id'] as string) ?? '00000000-0000-0000-0000-000000000001'
request.companyId = companyId
request.locationId = locationId
request.user = {
id: userId,
companyId,
role: 'admin',
}
})

View File

@@ -31,19 +31,19 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const account = await AccountService.create(app.db, request.companyId, parsed.data)
const account = await AccountService.create(app.db, parsed.data)
return reply.status(201).send(account)
})
app.get('/accounts', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => {
const params = PaginationSchema.parse(request.query)
const result = await AccountService.list(app.db, request.companyId, params)
const result = await AccountService.list(app.db, params)
return reply.send(result)
})
app.get('/accounts/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const account = await AccountService.getById(app.db, request.companyId, id)
const account = await AccountService.getById(app.db, id)
if (!account) return reply.status(404).send({ error: { message: 'Account not found', statusCode: 404 } })
return reply.send(account)
})
@@ -54,14 +54,14 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const account = await AccountService.update(app.db, request.companyId, id, parsed.data)
const account = await AccountService.update(app.db, id, parsed.data)
if (!account) return reply.status(404).send({ error: { message: 'Account not found', statusCode: 404 } })
return reply.send(account)
})
app.delete('/accounts/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const account = await AccountService.softDelete(app.db, request.companyId, id)
const account = await AccountService.softDelete(app.db, id)
if (!account) return reply.status(404).send({ error: { message: 'Account not found', statusCode: 404 } })
request.log.info({ accountId: id, userId: request.user.id }, 'Account soft-deleted')
return reply.send(account)
@@ -71,7 +71,7 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
app.get('/members', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => {
const params = PaginationSchema.parse(request.query)
const result = await MemberService.list(app.db, request.companyId, params)
const result = await MemberService.list(app.db, params)
return reply.send(result)
})
@@ -83,20 +83,20 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const member = await MemberService.create(app.db, request.companyId, parsed.data)
const member = await MemberService.create(app.db, parsed.data)
return reply.status(201).send(member)
})
app.get('/accounts/:accountId/members', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => {
const { accountId } = request.params as { accountId: string }
const params = PaginationSchema.parse(request.query)
const result = await MemberService.listByAccount(app.db, request.companyId, accountId, params)
const result = await MemberService.listByAccount(app.db, accountId, params)
return reply.send(result)
})
app.get('/members/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const member = await MemberService.getById(app.db, request.companyId, id)
const member = await MemberService.getById(app.db, id)
if (!member) return reply.status(404).send({ error: { message: 'Member not found', statusCode: 404 } })
return reply.send(member)
})
@@ -107,7 +107,7 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const member = await MemberService.update(app.db, request.companyId, id, parsed.data)
const member = await MemberService.update(app.db, id, parsed.data)
if (!member) return reply.status(404).send({ error: { message: 'Member not found', statusCode: 404 } })
return reply.send(member)
})
@@ -120,16 +120,16 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
// If no accountId provided, create a new account from the member's name
if (!targetAccountId) {
const member = await MemberService.getById(app.db, request.companyId, id)
const member = await MemberService.getById(app.db, id)
if (!member) return reply.status(404).send({ error: { message: 'Member not found', statusCode: 404 } })
const account = await AccountService.create(app.db, request.companyId, {
const account = await AccountService.create(app.db, {
name: `${member.firstName} ${member.lastName}`,
billingMode: 'consolidated',
})
targetAccountId = account.id
}
const member = await MemberService.move(app.db, request.companyId, id, targetAccountId)
const member = await MemberService.move(app.db, id, targetAccountId)
if (!member) return reply.status(404).send({ error: { message: 'Member not found', statusCode: 404 } })
request.log.info({ memberId: id, targetAccountId, userId: request.user.id }, 'Member moved to account')
return reply.send(member)
@@ -143,14 +143,14 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const identifier = await MemberIdentifierService.create(app.db, request.companyId, parsed.data)
const identifier = await MemberIdentifierService.create(app.db, parsed.data)
return reply.status(201).send(identifier)
})
app.get('/members/:memberId/identifiers', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => {
const { memberId } = request.params as { memberId: string }
const params = PaginationSchema.parse(request.query)
const result = await MemberIdentifierService.listByMember(app.db, request.companyId, memberId, params)
const result = await MemberIdentifierService.listByMember(app.db, memberId, params)
return reply.send(result)
})
@@ -160,21 +160,21 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const identifier = await MemberIdentifierService.update(app.db, request.companyId, id, parsed.data)
const identifier = await MemberIdentifierService.update(app.db, id, parsed.data)
if (!identifier) return reply.status(404).send({ error: { message: 'Identifier not found', statusCode: 404 } })
return reply.send(identifier)
})
app.delete('/identifiers/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const identifier = await MemberIdentifierService.delete(app.db, request.companyId, id)
const identifier = await MemberIdentifierService.delete(app.db, id)
if (!identifier) return reply.status(404).send({ error: { message: 'Identifier not found', statusCode: 404 } })
return reply.send(identifier)
})
app.delete('/members/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const member = await MemberService.delete(app.db, request.companyId, id)
const member = await MemberService.delete(app.db, id)
if (!member) return reply.status(404).send({ error: { message: 'Member not found', statusCode: 404 } })
return reply.send(member)
})
@@ -187,14 +187,14 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const link = await ProcessorLinkService.create(app.db, request.companyId, parsed.data)
const link = await ProcessorLinkService.create(app.db, parsed.data)
return reply.status(201).send(link)
})
app.get('/accounts/:accountId/processor-links', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => {
const { accountId } = request.params as { accountId: string }
const params = PaginationSchema.parse(request.query)
const result = await ProcessorLinkService.listByAccount(app.db, request.companyId, accountId, params)
const result = await ProcessorLinkService.listByAccount(app.db, accountId, params)
return reply.send(result)
})
@@ -204,14 +204,14 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const link = await ProcessorLinkService.update(app.db, request.companyId, id, parsed.data)
const link = await ProcessorLinkService.update(app.db, id, parsed.data)
if (!link) return reply.status(404).send({ error: { message: 'Processor link not found', statusCode: 404 } })
return reply.send(link)
})
app.delete('/processor-links/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const link = await ProcessorLinkService.delete(app.db, request.companyId, id)
const link = await ProcessorLinkService.delete(app.db, id)
if (!link) return reply.status(404).send({ error: { message: 'Processor link not found', statusCode: 404 } })
return reply.send(link)
})
@@ -224,20 +224,20 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const method = await PaymentMethodService.create(app.db, request.companyId, parsed.data)
const method = await PaymentMethodService.create(app.db, parsed.data)
return reply.status(201).send(method)
})
app.get('/accounts/:accountId/payment-methods', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => {
const { accountId } = request.params as { accountId: string }
const params = PaginationSchema.parse(request.query)
const result = await PaymentMethodService.listByAccount(app.db, request.companyId, accountId, params)
const result = await PaymentMethodService.listByAccount(app.db, accountId, params)
return reply.send(result)
})
app.get('/payment-methods/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const method = await PaymentMethodService.getById(app.db, request.companyId, id)
const method = await PaymentMethodService.getById(app.db, id)
if (!method) return reply.status(404).send({ error: { message: 'Payment method not found', statusCode: 404 } })
return reply.send(method)
})
@@ -248,14 +248,14 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const method = await PaymentMethodService.update(app.db, request.companyId, id, parsed.data)
const method = await PaymentMethodService.update(app.db, id, parsed.data)
if (!method) return reply.status(404).send({ error: { message: 'Payment method not found', statusCode: 404 } })
return reply.send(method)
})
app.delete('/payment-methods/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const method = await PaymentMethodService.delete(app.db, request.companyId, id)
const method = await PaymentMethodService.delete(app.db, id)
if (!method) return reply.status(404).send({ error: { message: 'Payment method not found', statusCode: 404 } })
return reply.send(method)
})
@@ -268,20 +268,20 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const exemption = await TaxExemptionService.create(app.db, request.companyId, parsed.data)
const exemption = await TaxExemptionService.create(app.db, parsed.data)
return reply.status(201).send(exemption)
})
app.get('/accounts/:accountId/tax-exemptions', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => {
const { accountId } = request.params as { accountId: string }
const params = PaginationSchema.parse(request.query)
const result = await TaxExemptionService.listByAccount(app.db, request.companyId, accountId, params)
const result = await TaxExemptionService.listByAccount(app.db, accountId, params)
return reply.send(result)
})
app.get('/tax-exemptions/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const exemption = await TaxExemptionService.getById(app.db, request.companyId, id)
const exemption = await TaxExemptionService.getById(app.db, id)
if (!exemption) return reply.status(404).send({ error: { message: 'Tax exemption not found', statusCode: 404 } })
return reply.send(exemption)
})
@@ -292,14 +292,14 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const exemption = await TaxExemptionService.update(app.db, request.companyId, id, parsed.data)
const exemption = await TaxExemptionService.update(app.db, id, parsed.data)
if (!exemption) return reply.status(404).send({ error: { message: 'Tax exemption not found', statusCode: 404 } })
return reply.send(exemption)
})
app.post('/tax-exemptions/:id/approve', { preHandler: [app.authenticate, app.requirePermission('accounts.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const exemption = await TaxExemptionService.approve(app.db, request.companyId, id, request.user.id)
const exemption = await TaxExemptionService.approve(app.db, id, request.user.id)
if (!exemption) return reply.status(404).send({ error: { message: 'Tax exemption not found', statusCode: 404 } })
request.log.info({ exemptionId: id, accountId: exemption.accountId, userId: request.user.id }, 'Tax exemption approved')
return reply.send(exemption)
@@ -311,7 +311,7 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
if (!reason) {
return reply.status(400).send({ error: { message: 'Reason is required to revoke a tax exemption', statusCode: 400 } })
}
const exemption = await TaxExemptionService.revoke(app.db, request.companyId, id, request.user.id, reason)
const exemption = await TaxExemptionService.revoke(app.db, id, request.user.id, reason)
if (!exemption) return reply.status(404).send({ error: { message: 'Tax exemption not found', statusCode: 404 } })
request.log.warn({ exemptionId: id, accountId: exemption.accountId, userId: request.user.id, reason }, 'Tax exemption revoked')
return reply.send(exemption)

View File

@@ -3,7 +3,6 @@ import { eq } from 'drizzle-orm'
import bcrypt from 'bcrypt'
import { RegisterSchema, LoginSchema } from '@forte/shared/schemas'
import { users } from '../../db/schema/users.js'
import { companies } from '../../db/schema/stores.js'
const SALT_ROUNDS = 10
@@ -27,28 +26,8 @@ export const authRoutes: FastifyPluginAsync = async (app) => {
}
const { email, password, firstName, lastName, role } = parsed.data
const companyId = request.companyId
// Validate that the company exists
if (!companyId) {
return reply.status(400).send({
error: { message: 'Company ID is required (x-company-id header)', statusCode: 400 },
})
}
const [company] = await app.db
.select({ id: companies.id })
.from(companies)
.where(eq(companies.id, companyId))
.limit(1)
if (!company) {
return reply.status(400).send({
error: { message: 'Invalid company', statusCode: 400 },
})
}
// Email is globally unique across all companies
// Email is globally unique
const existing = await app.db
.select({ id: users.id })
.from(users)
@@ -66,7 +45,6 @@ export const authRoutes: FastifyPluginAsync = async (app) => {
const [user] = await app.db
.insert(users)
.values({
companyId,
email,
passwordHash,
firstName,
@@ -84,11 +62,10 @@ export const authRoutes: FastifyPluginAsync = async (app) => {
const token = app.jwt.sign({
id: user.id,
companyId,
role: user.role,
})
request.log.info({ userId: user.id, email: user.email, companyId }, 'User registered')
request.log.info({ userId: user.id, email: user.email }, 'User registered')
return reply.status(201).send({ user, token })
})
@@ -126,7 +103,6 @@ export const authRoutes: FastifyPluginAsync = async (app) => {
const token = app.jwt.sign({
id: user.id,
companyId: user.companyId,
role: user.role,
})

View File

@@ -18,8 +18,7 @@ export const fileRoutes: FastifyPluginAsync = async (app) => {
throw new ValidationError('entityType and entityId query params required')
}
// Files are company-scoped in the service — companyId from JWT ensures access control
const fileRecords = await FileService.listByEntity(app.db, request.companyId, entityType, entityId)
const fileRecords = await FileService.listByEntity(app.db, entityType, entityId)
const data = await Promise.all(
fileRecords.map(async (f) => ({ ...f, url: await app.storage.getUrl(f.path) })),
)
@@ -59,7 +58,7 @@ export const fileRoutes: FastifyPluginAsync = async (app) => {
const buffer = await data.toBuffer()
const file = await FileService.upload(app.db, app.storage, request.companyId, {
const file = await FileService.upload(app.db, app.storage, {
data: buffer,
filename: data.filename,
contentType: data.mimetype,
@@ -76,15 +75,14 @@ export const fileRoutes: FastifyPluginAsync = async (app) => {
})
// Serve file content (for local provider)
// Path traversal protection: validate the path starts with the requesting company's ID
app.get('/files/serve/*', { preHandler: [app.authenticate, app.requirePermission('files.view')] }, async (request, reply) => {
const filePath = (request.params as { '*': string })['*']
if (!filePath) {
throw new ValidationError('Path required')
}
// Path traversal protection: must start with company ID, no '..' allowed
if (filePath.includes('..') || !filePath.startsWith(request.companyId)) {
// Path traversal protection: no '..' allowed
if (filePath.includes('..')) {
return reply.status(403).send({ error: { message: 'Access denied', statusCode: 403 } })
}
@@ -106,7 +104,7 @@ export const fileRoutes: FastifyPluginAsync = async (app) => {
// Get file metadata
app.get('/files/:id', { preHandler: [app.authenticate, app.requirePermission('files.view')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const file = await FileService.getById(app.db, request.companyId, id)
const file = await FileService.getById(app.db, id)
if (!file) return reply.status(404).send({ error: { message: 'File not found', statusCode: 404 } })
const url = await app.storage.getUrl(file.path)
return reply.send({ ...file, url })
@@ -115,12 +113,12 @@ export const fileRoutes: FastifyPluginAsync = async (app) => {
// Generate signed URL for a file (short-lived token in query string)
app.get('/files/signed-url/:id', { preHandler: [app.authenticate, app.requirePermission('files.view')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const file = await FileService.getById(app.db, request.companyId, id)
const file = await FileService.getById(app.db, id)
if (!file) return reply.status(404).send({ error: { message: 'File not found', statusCode: 404 } })
// Sign a short-lived token with the file path
const token = app.jwt.sign(
{ path: file.path, companyId: request.companyId, purpose: 'file-access' } as any,
{ path: file.path, purpose: 'file-access' } as any,
{ expiresIn: '15m' },
)
@@ -139,14 +137,10 @@ export const fileRoutes: FastifyPluginAsync = async (app) => {
// Verify the signed token
try {
const payload = app.jwt.verify(token) as { path: string; companyId: string; purpose: string }
const payload = app.jwt.verify(token) as { path: string; purpose: string }
if (payload.purpose !== 'file-access' || payload.path !== filePath) {
return reply.status(403).send({ error: { message: 'Invalid token', statusCode: 403 } })
}
// Validate company isolation — file path must start with the token's companyId
if (payload.companyId && !filePath.startsWith(payload.companyId)) {
return reply.status(403).send({ error: { message: 'Access denied', statusCode: 403 } })
}
} catch {
return reply.status(403).send({ error: { message: 'Token expired or invalid', statusCode: 403 } })
}
@@ -174,7 +168,7 @@ export const fileRoutes: FastifyPluginAsync = async (app) => {
// Delete a file
app.delete('/files/:id', { preHandler: [app.authenticate, app.requirePermission('files.delete')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const file = await FileService.delete(app.db, app.storage, request.companyId, id)
const file = await FileService.delete(app.db, app.storage, id)
if (!file) return reply.status(404).send({ error: { message: 'File not found', statusCode: 404 } })
request.log.info({ fileId: id, path: file.path }, 'File deleted')
return reply.send(file)

View File

@@ -16,19 +16,19 @@ export const inventoryRoutes: FastifyPluginAsync = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const category = await CategoryService.create(app.db, request.companyId, parsed.data)
const category = await CategoryService.create(app.db, parsed.data)
return reply.status(201).send(category)
})
app.get('/categories', { preHandler: [app.authenticate, app.requirePermission('inventory.view')] }, async (request, reply) => {
const params = PaginationSchema.parse(request.query)
const result = await CategoryService.list(app.db, request.companyId, params)
const result = await CategoryService.list(app.db, params)
return reply.send(result)
})
app.get('/categories/:id', { preHandler: [app.authenticate, app.requirePermission('inventory.view')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const category = await CategoryService.getById(app.db, request.companyId, id)
const category = await CategoryService.getById(app.db, id)
if (!category) return reply.status(404).send({ error: { message: 'Category not found', statusCode: 404 } })
return reply.send(category)
})
@@ -39,14 +39,14 @@ export const inventoryRoutes: FastifyPluginAsync = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const category = await CategoryService.update(app.db, request.companyId, id, parsed.data)
const category = await CategoryService.update(app.db, id, parsed.data)
if (!category) return reply.status(404).send({ error: { message: 'Category not found', statusCode: 404 } })
return reply.send(category)
})
app.delete('/categories/:id', { preHandler: [app.authenticate, app.requirePermission('inventory.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const category = await CategoryService.softDelete(app.db, request.companyId, id)
const category = await CategoryService.softDelete(app.db, id)
if (!category) return reply.status(404).send({ error: { message: 'Category not found', statusCode: 404 } })
return reply.send(category)
})
@@ -58,19 +58,19 @@ export const inventoryRoutes: FastifyPluginAsync = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const supplier = await SupplierService.create(app.db, request.companyId, parsed.data)
const supplier = await SupplierService.create(app.db, parsed.data)
return reply.status(201).send(supplier)
})
app.get('/suppliers', { preHandler: [app.authenticate, app.requirePermission('inventory.view')] }, async (request, reply) => {
const params = PaginationSchema.parse(request.query)
const result = await SupplierService.list(app.db, request.companyId, params)
const result = await SupplierService.list(app.db, params)
return reply.send(result)
})
app.get('/suppliers/:id', { preHandler: [app.authenticate, app.requirePermission('inventory.view')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const supplier = await SupplierService.getById(app.db, request.companyId, id)
const supplier = await SupplierService.getById(app.db, id)
if (!supplier) return reply.status(404).send({ error: { message: 'Supplier not found', statusCode: 404 } })
return reply.send(supplier)
})
@@ -81,14 +81,14 @@ export const inventoryRoutes: FastifyPluginAsync = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const supplier = await SupplierService.update(app.db, request.companyId, id, parsed.data)
const supplier = await SupplierService.update(app.db, id, parsed.data)
if (!supplier) return reply.status(404).send({ error: { message: 'Supplier not found', statusCode: 404 } })
return reply.send(supplier)
})
app.delete('/suppliers/:id', { preHandler: [app.authenticate, app.requirePermission('inventory.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const supplier = await SupplierService.softDelete(app.db, request.companyId, id)
const supplier = await SupplierService.softDelete(app.db, id)
if (!supplier) return reply.status(404).send({ error: { message: 'Supplier not found', statusCode: 404 } })
return reply.send(supplier)
})

View File

@@ -6,7 +6,7 @@ import { ConflictError, ValidationError } from '../../lib/errors.js'
function createLookupRoutes(prefix: string, service: typeof UnitStatusService) {
const routes: FastifyPluginAsync = async (app) => {
app.get(`/${prefix}`, { preHandler: [app.authenticate, app.requirePermission('inventory.view')] }, async (request, reply) => {
const data = await service.list(app.db, request.companyId)
const data = await service.list(app.db)
return reply.send({ data })
})
@@ -16,12 +16,12 @@ function createLookupRoutes(prefix: string, service: typeof UnitStatusService) {
throw new ValidationError('Validation failed', parsed.error.flatten())
}
const existing = await service.getBySlug(app.db, request.companyId, parsed.data.slug)
const existing = await service.getBySlug(app.db, parsed.data.slug)
if (existing) {
throw new ConflictError(`Slug "${parsed.data.slug}" already exists`)
}
const row = await service.create(app.db, request.companyId, parsed.data)
const row = await service.create(app.db, parsed.data)
return reply.status(201).send(row)
})
@@ -31,14 +31,14 @@ function createLookupRoutes(prefix: string, service: typeof UnitStatusService) {
if (!parsed.success) {
throw new ValidationError('Validation failed', parsed.error.flatten())
}
const row = await service.update(app.db, request.companyId, id, parsed.data)
const row = await service.update(app.db, id, parsed.data)
if (!row) return reply.status(404).send({ error: { message: 'Not found', statusCode: 404 } })
return reply.send(row)
})
app.delete(`/${prefix}/:id`, { preHandler: [app.authenticate, app.requirePermission('inventory.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const row = await service.delete(app.db, request.companyId, id)
const row = await service.delete(app.db, id)
if (!row) return reply.status(404).send({ error: { message: 'Not found', statusCode: 404 } })
return reply.send(row)
})

View File

@@ -16,19 +16,19 @@ export const productRoutes: FastifyPluginAsync = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const product = await ProductService.create(app.db, request.companyId, parsed.data)
const product = await ProductService.create(app.db, parsed.data)
return reply.status(201).send(product)
})
app.get('/products', { preHandler: [app.authenticate, app.requirePermission('inventory.view')] }, async (request, reply) => {
const params = PaginationSchema.parse(request.query)
const result = await ProductService.list(app.db, request.companyId, params)
const result = await ProductService.list(app.db, params)
return reply.send(result)
})
app.get('/products/:id', { preHandler: [app.authenticate, app.requirePermission('inventory.view')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const product = await ProductService.getById(app.db, request.companyId, id)
const product = await ProductService.getById(app.db, id)
if (!product) return reply.status(404).send({ error: { message: 'Product not found', statusCode: 404 } })
return reply.send(product)
})
@@ -39,14 +39,14 @@ export const productRoutes: FastifyPluginAsync = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const product = await ProductService.update(app.db, request.companyId, id, parsed.data, request.user.id)
const product = await ProductService.update(app.db, id, parsed.data, request.user.id)
if (!product) return reply.status(404).send({ error: { message: 'Product not found', statusCode: 404 } })
return reply.send(product)
})
app.delete('/products/:id', { preHandler: [app.authenticate, app.requirePermission('inventory.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const product = await ProductService.softDelete(app.db, request.companyId, id)
const product = await ProductService.softDelete(app.db, id)
if (!product) return reply.status(404).send({ error: { message: 'Product not found', statusCode: 404 } })
return reply.send(product)
})
@@ -59,20 +59,20 @@ export const productRoutes: FastifyPluginAsync = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const unit = await InventoryUnitService.create(app.db, request.companyId, parsed.data)
const unit = await InventoryUnitService.create(app.db, parsed.data)
return reply.status(201).send(unit)
})
app.get('/products/:productId/units', { preHandler: [app.authenticate, app.requirePermission('inventory.view')] }, async (request, reply) => {
const { productId } = request.params as { productId: string }
const params = PaginationSchema.parse(request.query)
const result = await InventoryUnitService.listByProduct(app.db, request.companyId, productId, params)
const result = await InventoryUnitService.listByProduct(app.db, productId, params)
return reply.send(result)
})
app.get('/units/:id', { preHandler: [app.authenticate, app.requirePermission('inventory.view')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const unit = await InventoryUnitService.getById(app.db, request.companyId, id)
const unit = await InventoryUnitService.getById(app.db, id)
if (!unit) return reply.status(404).send({ error: { message: 'Unit not found', statusCode: 404 } })
return reply.send(unit)
})
@@ -83,7 +83,7 @@ export const productRoutes: FastifyPluginAsync = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const unit = await InventoryUnitService.update(app.db, request.companyId, id, parsed.data)
const unit = await InventoryUnitService.update(app.db, id, parsed.data)
if (!unit) return reply.status(404).send({ error: { message: 'Unit not found', statusCode: 404 } })
return reply.send(unit)
})

View File

@@ -1,5 +1,5 @@
import type { FastifyPluginAsync } from 'fastify'
import { eq, and, count, sql, type Column } from 'drizzle-orm'
import { eq, count, sql, type Column } from 'drizzle-orm'
import { PaginationSchema } from '@forte/shared/schemas'
import { RbacService } from '../../services/rbac.service.js'
import { ValidationError } from '../../lib/errors.js'
@@ -12,13 +12,12 @@ export const rbacRoutes: FastifyPluginAsync = async (app) => {
app.get('/users', { preHandler: [app.authenticate, app.requirePermission('users.view')] }, async (request, reply) => {
const params = PaginationSchema.parse(request.query)
const baseWhere = eq(users.companyId, request.companyId)
const searchCondition = params.q
? buildSearchCondition(params.q, [users.firstName, users.lastName, users.email])
: undefined
const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere
const where = searchCondition ?? undefined
const sortableColumns: Record<string, Column> = {
name: users.lastName,
@@ -96,7 +95,7 @@ export const rbacRoutes: FastifyPluginAsync = async (app) => {
const [updated] = await app.db
.update(users)
.set({ isActive, updatedAt: new Date() })
.where(and(eq(users.id, userId), eq(users.companyId, request.companyId)))
.where(eq(users.id, userId))
.returning({ id: users.id, isActive: users.isActive })
if (!updated) return reply.status(404).send({ error: { message: 'User not found', statusCode: 404 } })
@@ -116,7 +115,7 @@ export const rbacRoutes: FastifyPluginAsync = async (app) => {
app.get('/roles', { preHandler: [app.authenticate, app.requirePermission('users.view')] }, async (request, reply) => {
const params = PaginationSchema.parse(request.query)
const result = await RbacService.listRoles(app.db, request.companyId, params)
const result = await RbacService.listRoles(app.db, params)
return reply.send(result)
})
@@ -125,14 +124,14 @@ export const rbacRoutes: FastifyPluginAsync = async (app) => {
const data = await app.db
.select()
.from(roles)
.where(and(eq(roles.companyId, request.companyId), eq(roles.isActive, true)))
.where(eq(roles.isActive, true))
.orderBy(roles.name)
return reply.send({ data })
})
app.get('/roles/:id', { preHandler: [app.authenticate, app.requirePermission('users.view')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const role = await RbacService.getRoleWithPermissions(app.db, request.companyId, id)
const role = await RbacService.getRoleWithPermissions(app.db, id)
if (!role) return reply.status(404).send({ error: { message: 'Role not found', statusCode: 404 } })
return reply.send(role)
})
@@ -153,7 +152,7 @@ export const rbacRoutes: FastifyPluginAsync = async (app) => {
throw new ValidationError('slug must be lowercase alphanumeric with underscores')
}
const role = await RbacService.createRole(app.db, request.companyId, {
const role = await RbacService.createRole(app.db, {
name,
slug,
description,
@@ -172,7 +171,7 @@ export const rbacRoutes: FastifyPluginAsync = async (app) => {
permissionSlugs?: string[]
}
const role = await RbacService.updateRole(app.db, request.companyId, id, {
const role = await RbacService.updateRole(app.db, id, {
name,
description,
permissionSlugs,
@@ -185,7 +184,7 @@ export const rbacRoutes: FastifyPluginAsync = async (app) => {
app.delete('/roles/:id', { preHandler: [app.authenticate, app.requirePermission('users.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const role = await RbacService.deleteRole(app.db, request.companyId, id)
const role = await RbacService.deleteRole(app.db, id)
if (!role) return reply.status(404).send({ error: { message: 'Role not found', statusCode: 404 } })
request.log.info({ roleId: id, roleName: role.name, userId: request.user.id }, 'Role deleted')
return reply.send(role)

View File

@@ -23,7 +23,7 @@ export const repairRoutes: FastifyPluginAsync = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const ticket = await RepairTicketService.create(app.db, request.companyId, parsed.data)
const ticket = await RepairTicketService.create(app.db, parsed.data)
return reply.status(201).send(ticket)
})
@@ -44,13 +44,13 @@ export const repairRoutes: FastifyPluginAsync = async (app) => {
completedDateTo: query.completedDateTo,
}
const result = await RepairTicketService.list(app.db, request.companyId, params, filters)
const result = await RepairTicketService.list(app.db, params, filters)
return reply.send(result)
})
app.get('/repair-tickets/:id', { preHandler: [app.authenticate, app.requirePermission('repairs.view')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const ticket = await RepairTicketService.getById(app.db, request.companyId, id)
const ticket = await RepairTicketService.getById(app.db, id)
if (!ticket) return reply.status(404).send({ error: { message: 'Repair ticket not found', statusCode: 404 } })
return reply.send(ticket)
})
@@ -61,7 +61,7 @@ export const repairRoutes: FastifyPluginAsync = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const ticket = await RepairTicketService.update(app.db, request.companyId, id, parsed.data)
const ticket = await RepairTicketService.update(app.db, id, parsed.data)
if (!ticket) return reply.status(404).send({ error: { message: 'Repair ticket not found', statusCode: 404 } })
return reply.send(ticket)
})
@@ -72,14 +72,14 @@ export const repairRoutes: FastifyPluginAsync = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const ticket = await RepairTicketService.updateStatus(app.db, request.companyId, id, parsed.data.status)
const ticket = await RepairTicketService.updateStatus(app.db, id, parsed.data.status)
if (!ticket) return reply.status(404).send({ error: { message: 'Repair ticket not found', statusCode: 404 } })
return reply.send(ticket)
})
app.delete('/repair-tickets/:id', { preHandler: [app.authenticate, app.requirePermission('repairs.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const ticket = await RepairTicketService.delete(app.db, request.companyId, id)
const ticket = await RepairTicketService.delete(app.db, id)
if (!ticket) return reply.status(404).send({ error: { message: 'Repair ticket not found', statusCode: 404 } })
return reply.send(ticket)
})
@@ -109,14 +109,14 @@ export const repairRoutes: FastifyPluginAsync = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const item = await RepairLineItemService.update(app.db, request.companyId, id, parsed.data)
const item = await RepairLineItemService.update(app.db, id, parsed.data)
if (!item) return reply.status(404).send({ error: { message: 'Line item not found', statusCode: 404 } })
return reply.send(item)
})
app.delete('/repair-line-items/:id', { preHandler: [app.authenticate, app.requirePermission('repairs.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const item = await RepairLineItemService.delete(app.db, request.companyId, id)
const item = await RepairLineItemService.delete(app.db, id)
if (!item) return reply.status(404).send({ error: { message: 'Line item not found', statusCode: 404 } })
return reply.send(item)
})
@@ -128,19 +128,19 @@ export const repairRoutes: FastifyPluginAsync = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const batch = await RepairBatchService.create(app.db, request.companyId, parsed.data)
const batch = await RepairBatchService.create(app.db, parsed.data)
return reply.status(201).send(batch)
})
app.get('/repair-batches', { preHandler: [app.authenticate, app.requirePermission('repairs.view')] }, async (request, reply) => {
const params = PaginationSchema.parse(request.query)
const result = await RepairBatchService.list(app.db, request.companyId, params)
const result = await RepairBatchService.list(app.db, params)
return reply.send(result)
})
app.get('/repair-batches/:id', { preHandler: [app.authenticate, app.requirePermission('repairs.view')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const batch = await RepairBatchService.getById(app.db, request.companyId, id)
const batch = await RepairBatchService.getById(app.db, id)
if (!batch) return reply.status(404).send({ error: { message: 'Repair batch not found', statusCode: 404 } })
return reply.send(batch)
})
@@ -151,7 +151,7 @@ export const repairRoutes: FastifyPluginAsync = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const batch = await RepairBatchService.update(app.db, request.companyId, id, parsed.data)
const batch = await RepairBatchService.update(app.db, id, parsed.data)
if (!batch) return reply.status(404).send({ error: { message: 'Repair batch not found', statusCode: 404 } })
return reply.send(batch)
})
@@ -162,21 +162,21 @@ export const repairRoutes: FastifyPluginAsync = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const batch = await RepairBatchService.updateStatus(app.db, request.companyId, id, parsed.data.status)
const batch = await RepairBatchService.updateStatus(app.db, id, parsed.data.status)
if (!batch) return reply.status(404).send({ error: { message: 'Repair batch not found', statusCode: 404 } })
return reply.send(batch)
})
app.post('/repair-batches/:id/approve', { preHandler: [app.authenticate, app.requirePermission('repairs.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const batch = await RepairBatchService.approve(app.db, request.companyId, id, request.user.id)
const batch = await RepairBatchService.approve(app.db, id, request.user.id)
if (!batch) return reply.status(404).send({ error: { message: 'Repair batch not found', statusCode: 404 } })
return reply.send(batch)
})
app.post('/repair-batches/:id/reject', { preHandler: [app.authenticate, app.requirePermission('repairs.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const batch = await RepairBatchService.reject(app.db, request.companyId, id)
const batch = await RepairBatchService.reject(app.db, id)
if (!batch) return reply.status(404).send({ error: { message: 'Repair batch not found', statusCode: 404 } })
return reply.send(batch)
})
@@ -184,7 +184,7 @@ export const repairRoutes: FastifyPluginAsync = async (app) => {
app.get('/repair-batches/:batchId/tickets', { preHandler: [app.authenticate, app.requirePermission('repairs.view')] }, async (request, reply) => {
const { batchId } = request.params as { batchId: string }
const params = PaginationSchema.parse(request.query)
const result = await RepairTicketService.listByBatch(app.db, request.companyId, batchId, params)
const result = await RepairTicketService.listByBatch(app.db, batchId, params)
return reply.send(result)
})
@@ -196,7 +196,7 @@ export const repairRoutes: FastifyPluginAsync = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const ticket = await RepairTicketService.getById(app.db, request.companyId, ticketId)
const ticket = await RepairTicketService.getById(app.db, ticketId)
if (!ticket) return reply.status(404).send({ error: { message: 'Repair ticket not found', statusCode: 404 } })
// Look up author name from users table
@@ -217,7 +217,7 @@ export const repairRoutes: FastifyPluginAsync = async (app) => {
app.delete('/repair-notes/:id', { preHandler: [app.authenticate, app.requirePermission('repairs.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const note = await RepairNoteService.delete(app.db, request.companyId, id)
const note = await RepairNoteService.delete(app.db, id)
if (!note) return reply.status(404).send({ error: { message: 'Note not found', statusCode: 404 } })
return reply.send(note)
})
@@ -229,13 +229,13 @@ export const repairRoutes: FastifyPluginAsync = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const template = await RepairServiceTemplateService.create(app.db, request.companyId, parsed.data)
const template = await RepairServiceTemplateService.create(app.db, parsed.data)
return reply.status(201).send(template)
})
app.get('/repair-service-templates', { preHandler: [app.authenticate, app.requirePermission('repairs.view')] }, async (request, reply) => {
const params = PaginationSchema.parse(request.query)
const result = await RepairServiceTemplateService.list(app.db, request.companyId, params)
const result = await RepairServiceTemplateService.list(app.db, params)
return reply.send(result)
})
@@ -245,14 +245,14 @@ export const repairRoutes: FastifyPluginAsync = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const template = await RepairServiceTemplateService.update(app.db, request.companyId, id, parsed.data)
const template = await RepairServiceTemplateService.update(app.db, id, parsed.data)
if (!template) return reply.status(404).send({ error: { message: 'Template not found', statusCode: 404 } })
return reply.send(template)
})
app.delete('/repair-service-templates/:id', { preHandler: [app.authenticate, app.requirePermission('repairs.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const template = await RepairServiceTemplateService.delete(app.db, request.companyId, id)
const template = await RepairServiceTemplateService.delete(app.db, id)
if (!template) return reply.status(404).send({ error: { message: 'Template not found', statusCode: 404 } })
return reply.send(template)
})

View File

@@ -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
},

View File

@@ -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

View File

@@ -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
},

View File

@@ -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
},
}

View File

@@ -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
},

View File

@@ -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

View File

@@ -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))

View File

@@ -1,6 +1,6 @@
import type { FastifyInstance } from 'fastify'
import { buildApp } from '../main.js'
import { sql, eq, and } from 'drizzle-orm'
import { sql, eq } from 'drizzle-orm'
import { companies, locations } from '../db/schema/stores.js'
import { UnitStatusService, ItemConditionService } from '../services/lookup.service.js'
import { RbacService } from '../services/rbac.service.js'
@@ -42,12 +42,12 @@ export async function seedTestCompany(app: FastifyInstance): Promise<void> {
name: 'Test Location',
})
await UnitStatusService.seedForCompany(app.db, TEST_COMPANY_ID)
await ItemConditionService.seedForCompany(app.db, TEST_COMPANY_ID)
await UnitStatusService.seedDefaults(app.db)
await ItemConditionService.seedDefaults(app.db)
// Seed RBAC permissions and default roles
await RbacService.seedPermissions(app.db)
await RbacService.seedRolesForCompany(app.db, TEST_COMPANY_ID)
await RbacService.seedDefaultRoles(app.db)
}
export async function registerAndLogin(
@@ -80,7 +80,7 @@ export async function registerAndLogin(
const [adminRole] = await app.db
.select()
.from(roles)
.where(and(eq(roles.companyId, TEST_COMPANY_ID), eq(roles.slug, 'admin')))
.where(eq(roles.slug, 'admin'))
.limit(1)
if (adminRole) {