From d36c6f7135488b5dc82124c64980b30ec641548c Mon Sep 17 00:00:00 2001 From: Ryan Moon Date: Sun, 29 Mar 2026 14:58:33 -0500 Subject: [PATCH] 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. --- .gitignore | 1 + packages/admin/src/stores/auth.store.ts | 27 ++-- packages/admin/src/types/account.ts | 6 - packages/admin/src/types/repair.ts | 3 - .../__tests__/routes/v1/accounts.test.ts | 1 - packages/backend/api-tests/run.ts | 15 +- packages/backend/api-tests/suites/rbac.ts | 12 +- .../0021_remove_company_scoping.sql | 27 ++++ .../src/db/migrations/meta/_journal.json | 7 + packages/backend/src/db/schema/accounts.ts | 19 --- packages/backend/src/db/schema/files.ts | 4 - packages/backend/src/db/schema/inventory.ts | 23 +-- packages/backend/src/db/schema/lookups.ts | 7 - packages/backend/src/db/schema/rbac.ts | 4 - packages/backend/src/db/schema/repairs.ts | 11 +- packages/backend/src/db/schema/users.ts | 4 - packages/backend/src/db/seeds/dev-seed.ts | 9 +- packages/backend/src/plugins/auth.ts | 11 +- packages/backend/src/plugins/dev-auth.ts | 7 +- packages/backend/src/routes/v1/accounts.ts | 66 ++++---- packages/backend/src/routes/v1/auth.ts | 28 +--- packages/backend/src/routes/v1/files.ts | 24 ++- packages/backend/src/routes/v1/inventory.ts | 20 +-- packages/backend/src/routes/v1/lookups.ts | 10 +- packages/backend/src/routes/v1/products.ts | 18 +-- packages/backend/src/routes/v1/rbac.ts | 19 ++- packages/backend/src/routes/v1/repairs.ts | 44 +++--- .../backend/src/services/account.service.ts | 148 ++++++++---------- packages/backend/src/services/file.service.ts | 16 +- .../backend/src/services/inventory.service.ts | 40 ++--- .../backend/src/services/lookup.service.ts | 34 ++-- .../backend/src/services/product.service.ts | 45 +++--- packages/backend/src/services/rbac.service.ts | 32 ++-- .../backend/src/services/repair.service.ts | 112 +++++-------- packages/backend/src/test/helpers.ts | 10 +- 35 files changed, 353 insertions(+), 511 deletions(-) create mode 100644 packages/backend/src/db/migrations/0021_remove_company_scoping.sql diff --git a/.gitignore b/.gitignore index 0a44b01..166ab38 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ logs/ *.pem *.key credentials.json +packages/backend/data/ diff --git a/packages/admin/src/stores/auth.store.ts b/packages/admin/src/stores/auth.store.ts index 911aa52..1257383 100644 --- a/packages/admin/src/stores/auth.store.ts +++ b/packages/admin/src/stores/auth.store.ts @@ -11,7 +11,6 @@ interface User { interface AuthState { token: string | null user: User | null - companyId: string | null permissions: Set permissionsLoaded: boolean setAuth: (token: string, user: User) => void @@ -49,12 +48,7 @@ function expandPermissions(slugs: string[]): Set { 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((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((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 }) }, }}) diff --git a/packages/admin/src/types/account.ts b/packages/admin/src/types/account.ts index b1f8562..9d79778 100644 --- a/packages/admin/src/types/account.ts +++ b/packages/admin/src/types/account.ts @@ -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 diff --git a/packages/admin/src/types/repair.ts b/packages/admin/src/types/repair.ts index cde869b..6f357da 100644 --- a/packages/admin/src/types/repair.ts +++ b/packages/admin/src/types/repair.ts @@ -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 diff --git a/packages/backend/__tests__/routes/v1/accounts.test.ts b/packages/backend/__tests__/routes/v1/accounts.test.ts index ee9b0fd..978a005 100644 --- a/packages/backend/__tests__/routes/v1/accounts.test.ts +++ b/packages/backend/__tests__/routes/v1/accounts.test.ts @@ -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') }) diff --git a/packages/backend/api-tests/run.ts b/packages/backend/api-tests/run.ts index ea27fe4..e319269 100644 --- a/packages/backend/api-tests/run.ts +++ b/packages/backend/api-tests/run.ts @@ -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 { async function registerTestUser(): Promise { 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 { // 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` } diff --git a/packages/backend/api-tests/suites/rbac.ts b/packages/backend/api-tests/suites/rbac.ts index 62e04ee..345d503 100644 --- a/packages/backend/api-tests/suites/rbac.ts +++ b/packages/backend/api-tests/suites/rbac.ts @@ -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 } } diff --git a/packages/backend/src/db/migrations/0021_remove_company_scoping.sql b/packages/backend/src/db/migrations/0021_remove_company_scoping.sql new file mode 100644 index 0000000..258360f --- /dev/null +++ b/packages/backend/src/db/migrations/0021_remove_company_scoping.sql @@ -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"; diff --git a/packages/backend/src/db/migrations/meta/_journal.json b/packages/backend/src/db/migrations/meta/_journal.json index d5f0975..15bd167 100644 --- a/packages/backend/src/db/migrations/meta/_journal.json +++ b/packages/backend/src/db/migrations/meta/_journal.json @@ -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 } ] } \ No newline at end of file diff --git a/packages/backend/src/db/schema/accounts.ts b/packages/backend/src/db/schema/accounts.ts index ee143a8..11c5f6d 100644 --- a/packages/backend/src/db/schema/accounts.ts +++ b/packages/backend/src/db/schema/accounts.ts @@ -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 }), diff --git a/packages/backend/src/db/schema/files.ts b/packages/backend/src/db/schema/files.ts index 95a26ff..5b3c234 100644 --- a/packages/backend/src/db/schema/files.ts +++ b/packages/backend/src/db/schema/files.ts @@ -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(), diff --git a/packages/backend/src/db/schema/inventory.ts b/packages/backend/src/db/schema/inventory.ts index 5ac30c9..dbe61b6 100644 --- a/packages/backend/src/db/schema/inventory.ts +++ b/packages/backend/src/db/schema/inventory.ts @@ -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 }), diff --git a/packages/backend/src/db/schema/lookups.ts b/packages/backend/src/db/schema/lookups.ts index d45b5c8..ee5445e 100644 --- a/packages/backend/src/db/schema/lookups.ts +++ b/packages/backend/src/db/schema/lookups.ts @@ -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'), diff --git a/packages/backend/src/db/schema/rbac.ts b/packages/backend/src/db/schema/rbac.ts index 206e18e..4926d0e 100644 --- a/packages/backend/src/db/schema/rbac.ts +++ b/packages/backend/src/db/schema/rbac.ts @@ -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'), diff --git a/packages/backend/src/db/schema/repairs.ts b/packages/backend/src/db/schema/repairs.ts index 2257b1f..c5f5044 100644 --- a/packages/backend/src/db/schema/repairs.ts +++ b/packages/backend/src/db/schema/repairs.ts @@ -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 }), diff --git a/packages/backend/src/db/schema/users.ts b/packages/backend/src/db/schema/users.ts index c988047..3f8fea0 100644 --- a/packages/backend/src/db/schema/users.ts +++ b/packages/backend/src/db/schema/users.ts @@ -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(), diff --git a/packages/backend/src/db/seeds/dev-seed.ts b/packages/backend/src/db/seeds/dev-seed.ts index 0dc44b5..fbc5794 100644 --- a/packages/backend/src/db/seeds/dev-seed.ts +++ b/packages/backend/src/db/seeds/dev-seed.ts @@ -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` } } diff --git a/packages/backend/src/plugins/auth.ts b/packages/backend/src/plugins/auth.ts index d18a207..7ab1562 100644 --- a/packages/backend/src/plugins/auth.ts +++ b/packages/backend/src/plugins/auth.ts @@ -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 } } 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 diff --git a/packages/backend/src/plugins/dev-auth.ts b/packages/backend/src/plugins/dev-auth.ts index 6693f10..6f9e799 100644 --- a/packages/backend/src/plugins/dev-auth.ts +++ b/packages/backend/src/plugins/dev-auth.ts @@ -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', } }) diff --git a/packages/backend/src/routes/v1/accounts.ts b/packages/backend/src/routes/v1/accounts.ts index 63b9b77..a6e0781 100644 --- a/packages/backend/src/routes/v1/accounts.ts +++ b/packages/backend/src/routes/v1/accounts.ts @@ -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) diff --git a/packages/backend/src/routes/v1/auth.ts b/packages/backend/src/routes/v1/auth.ts index 169215a..2cd170c 100644 --- a/packages/backend/src/routes/v1/auth.ts +++ b/packages/backend/src/routes/v1/auth.ts @@ -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, }) diff --git a/packages/backend/src/routes/v1/files.ts b/packages/backend/src/routes/v1/files.ts index 0023939..a335523 100644 --- a/packages/backend/src/routes/v1/files.ts +++ b/packages/backend/src/routes/v1/files.ts @@ -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) diff --git a/packages/backend/src/routes/v1/inventory.ts b/packages/backend/src/routes/v1/inventory.ts index 0a3f2ad..245f4ce 100644 --- a/packages/backend/src/routes/v1/inventory.ts +++ b/packages/backend/src/routes/v1/inventory.ts @@ -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) }) diff --git a/packages/backend/src/routes/v1/lookups.ts b/packages/backend/src/routes/v1/lookups.ts index c65de4d..58a02bf 100644 --- a/packages/backend/src/routes/v1/lookups.ts +++ b/packages/backend/src/routes/v1/lookups.ts @@ -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) }) diff --git a/packages/backend/src/routes/v1/products.ts b/packages/backend/src/routes/v1/products.ts index 1f8c492..978d481 100644 --- a/packages/backend/src/routes/v1/products.ts +++ b/packages/backend/src/routes/v1/products.ts @@ -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) }) diff --git a/packages/backend/src/routes/v1/rbac.ts b/packages/backend/src/routes/v1/rbac.ts index 7462e12..e2b7b68 100644 --- a/packages/backend/src/routes/v1/rbac.ts +++ b/packages/backend/src/routes/v1/rbac.ts @@ -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 = { 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) diff --git a/packages/backend/src/routes/v1/repairs.ts b/packages/backend/src/routes/v1/repairs.ts index 4a2de49..60499b3 100644 --- a/packages/backend/src/routes/v1/repairs.ts +++ b/packages/backend/src/routes/v1/repairs.ts @@ -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) }) diff --git a/packages/backend/src/services/account.service.ts b/packages/backend/src/services/account.service.ts index eae745f..4be8da6 100644 --- a/packages/backend/src/services/account.service.ts +++ b/packages/backend/src/services/account.service.ts @@ -33,15 +33,13 @@ async function generateUniqueNumber( db: PostgresJsDatabase, table: typeof accounts | typeof members, column: typeof accounts.accountNumber | typeof members.memberNumber, - companyId: string, - companyIdColumn: Column, ): Promise { 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, companyId: string, input: AccountCreateInput) { - const accountNumber = await generateUniqueNumber(db, accounts, accounts.accountNumber, companyId, accounts.companyId) + async create(db: PostgresJsDatabase, 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, companyId: string, id: string) { + async getById(db: PostgresJsDatabase, 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, companyId: string, id: string, input: AccountUpdateInput) { + async update(db: PostgresJsDatabase, 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, companyId: string, id: string) { + async softDelete(db: PostgresJsDatabase, 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, companyId: string, params: PaginationInput) { - const baseWhere = and(eq(accounts.companyId, companyId), eq(accounts.isActive, true)) + async list(db: PostgresJsDatabase, 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, - 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, companyId: string, id: string) { + async getById(db: PostgresJsDatabase, 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, companyId: string, params: PaginationInput) { - const baseWhere = eq(members.companyId, companyId) - + async list(db: PostgresJsDatabase, 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 = { 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, - 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 = { first_name: members.firstName, @@ -295,7 +284,6 @@ export const MemberService = { async update( db: PostgresJsDatabase, - 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, companyId: string, memberId: string, targetAccountId: string) { - const member = await this.getById(db, companyId, memberId) + async move(db: PostgresJsDatabase, 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, companyId: string, id: string) { + async delete(db: PostgresJsDatabase, 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, companyId: string, input: ProcessorLinkCreateInput) { + async create(db: PostgresJsDatabase, 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, companyId: string, id: string) { + async getById(db: PostgresJsDatabase, 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, companyId: string, accountId: string, params: PaginationInput) { - const baseWhere = and(eq(accountProcessorLinks.companyId, companyId), eq(accountProcessorLinks.accountId, accountId)) + async listByAccount(db: PostgresJsDatabase, 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, companyId: string, id: string, input: ProcessorLinkUpdateInput) { + async update(db: PostgresJsDatabase, 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, companyId: string, id: string) { + async delete(db: PostgresJsDatabase, 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, companyId: string, input: PaymentMethodCreateInput) { + async create(db: PostgresJsDatabase, 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, companyId: string, id: string) { + async getById(db: PostgresJsDatabase, 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, companyId: string, accountId: string, params: PaginationInput) { - const baseWhere = and(eq(accountPaymentMethods.companyId, companyId), eq(accountPaymentMethods.accountId, accountId)) + async listByAccount(db: PostgresJsDatabase, 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, companyId: string, id: string, input: PaymentMethodUpdateInput) { + async update(db: PostgresJsDatabase, 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, companyId: string, id: string) { + async delete(db: PostgresJsDatabase, 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, companyId: string, input: TaxExemptionCreateInput) { + async create(db: PostgresJsDatabase, 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, companyId: string, id: string) { + async getById(db: PostgresJsDatabase, 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, companyId: string, accountId: string, params: PaginationInput) { - const baseWhere = and(eq(taxExemptions.companyId, companyId), eq(taxExemptions.accountId, accountId)) + async listByAccount(db: PostgresJsDatabase, 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, companyId: string, id: string, input: TaxExemptionUpdateInput) { + async update(db: PostgresJsDatabase, 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, companyId: string, id: string, approvedBy: string) { + async approve(db: PostgresJsDatabase, 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, companyId: string, id: string, revokedBy: string, reason: string) { + async revoke(db: PostgresJsDatabase, 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, companyId: string, input: MemberIdentifierCreateInput) { + async create(db: PostgresJsDatabase, 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, companyId: string, memberId: string, params: PaginationInput) { - const baseWhere = and(eq(memberIdentifiers.companyId, companyId), eq(memberIdentifiers.memberId, memberId)) + async listByMember(db: PostgresJsDatabase, 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, companyId: string, id: string) { + async getById(db: PostgresJsDatabase, 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, companyId: string, id: string, input: MemberIdentifierUpdateInput) { + async update(db: PostgresJsDatabase, 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, companyId: string, id: string) { + async delete(db: PostgresJsDatabase, 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 }, diff --git a/packages/backend/src/services/file.service.ts b/packages/backend/src/services/file.service.ts index e8fd001..a52bec3 100644 --- a/packages/backend/src/services/file.service.ts +++ b/packages/backend/src/services/file.service.ts @@ -26,7 +26,6 @@ export const FileService = { async upload( db: PostgresJsDatabase, 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, companyId: string, id: string) { + async getById(db: PostgresJsDatabase, 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, - 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, 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 diff --git a/packages/backend/src/services/inventory.service.ts b/packages/backend/src/services/inventory.service.ts index e4a0360..8f051af 100644 --- a/packages/backend/src/services/inventory.service.ts +++ b/packages/backend/src/services/inventory.service.ts @@ -16,25 +16,25 @@ import { } from '../utils/pagination.js' export const CategoryService = { - async create(db: PostgresJsDatabase, companyId: string, input: CategoryCreateInput) { + async create(db: PostgresJsDatabase, input: CategoryCreateInput) { const [category] = await db .insert(categories) - .values({ companyId, ...input }) + .values({ ...input }) .returning() return category }, - async getById(db: PostgresJsDatabase, companyId: string, id: string) { + async getById(db: PostgresJsDatabase, 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, companyId: string, params: PaginationInput) { - const baseWhere = and(eq(categories.companyId, companyId), eq(categories.isActive, true)) + async list(db: PostgresJsDatabase, 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, companyId: string, id: string, input: CategoryUpdateInput) { + async update(db: PostgresJsDatabase, 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, companyId: string, id: string) { + async softDelete(db: PostgresJsDatabase, 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, companyId: string, input: SupplierCreateInput) { + async create(db: PostgresJsDatabase, input: SupplierCreateInput) { const [supplier] = await db .insert(suppliers) - .values({ companyId, ...input }) + .values({ ...input }) .returning() return supplier }, - async getById(db: PostgresJsDatabase, companyId: string, id: string) { + async getById(db: PostgresJsDatabase, 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, companyId: string, params: PaginationInput) { - const baseWhere = and(eq(suppliers.companyId, companyId), eq(suppliers.isActive, true)) + async list(db: PostgresJsDatabase, 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, companyId: string, id: string, input: SupplierUpdateInput) { + async update(db: PostgresJsDatabase, 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, companyId: string, id: string) { + async softDelete(db: PostgresJsDatabase, 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 }, diff --git a/packages/backend/src/services/lookup.service.ts b/packages/backend/src/services/lookup.service.ts index 131690a..bb3f431 100644 --- a/packages/backend/src/services/lookup.service.ts +++ b/packages/backend/src/services/lookup.service.ts @@ -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, companyId: string) { + async seedDefaults(db: PostgresJsDatabase) { 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, companyId: string) { + async list(db: PostgresJsDatabase) { 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, companyId: string, slug: string) { + async getBySlug(db: PostgresJsDatabase, 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, companyId: string, input: LookupCreateInput) { + async create(db: PostgresJsDatabase, 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, companyId: string, id: string, input: LookupUpdateInput) { + async update(db: PostgresJsDatabase, 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, companyId: string, id: string) { + async delete(db: PostgresJsDatabase, 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, companyId: string, slug: string): Promise { - const row = await this.getBySlug(db, companyId, slug) + async validateSlug(db: PostgresJsDatabase, slug: string): Promise { + const row = await this.getBySlug(db, slug) return row !== null && row.isActive }, } diff --git a/packages/backend/src/services/product.service.ts b/packages/backend/src/services/product.service.ts index d458aac..9e2a473 100644 --- a/packages/backend/src/services/product.service.ts +++ b/packages/backend/src/services/product.service.ts @@ -18,11 +18,10 @@ import { import { UnitStatusService, ItemConditionService } from './lookup.service.js' export const ProductService = { - async create(db: PostgresJsDatabase, companyId: string, input: ProductCreateInput) { + async create(db: PostgresJsDatabase, 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, companyId: string, id: string) { + async getById(db: PostgresJsDatabase, 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, companyId: string, params: PaginationInput) { - const baseWhere = and(eq(products.companyId, companyId), eq(products.isActive, true)) + async list(db: PostgresJsDatabase, 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, - 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, companyId: string, id: string) { + async softDelete(db: PostgresJsDatabase, 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, companyId: string, input: InventoryUnitCreateInput) { + async create(db: PostgresJsDatabase, 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, companyId: string, id: string) { + async getById(db: PostgresJsDatabase, 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, - 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 = { serial_number: inventoryUnits.serialNumber, @@ -185,16 +177,15 @@ export const InventoryUnitService = { async update( db: PostgresJsDatabase, - 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 }, diff --git a/packages/backend/src/services/rbac.service.ts b/packages/backend/src/services/rbac.service.ts index 172badd..c6c9eaf 100644 --- a/packages/backend/src/services/rbac.service.ts +++ b/packages/backend/src/services/rbac.service.ts @@ -18,16 +18,16 @@ export const RbacService = { await db.insert(permissions).values(toInsert) }, - /** Seed default roles for a company */ - async seedRolesForCompany(db: PostgresJsDatabase, companyId: string) { + /** Seed default roles */ + async seedDefaultRoles(db: PostgresJsDatabase) { 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, companyId: string, params: PaginationInput) { - const baseWhere = and(eq(roles.companyId, companyId), eq(roles.isActive, true)) + /** List roles */ + async listRoles(db: PostgresJsDatabase, 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, companyId: string, roleId: string) { + async getRoleWithPermissions(db: PostgresJsDatabase, 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, - 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, - 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, companyId: string, roleId: string) { + async deleteRole(db: PostgresJsDatabase, 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 diff --git a/packages/backend/src/services/repair.service.ts b/packages/backend/src/services/repair.service.ts index fcdf81a..a8ee785 100644 --- a/packages/backend/src/services/repair.service.ts +++ b/packages/backend/src/services/repair.service.ts @@ -30,15 +30,13 @@ async function generateUniqueNumber( db: PostgresJsDatabase, table: typeof repairTickets | typeof repairBatches, column: typeof repairTickets.ticketNumber | typeof repairBatches.batchNumber, - companyId: string, - companyIdColumn: Column, ): Promise { 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, companyId: string, input: RepairTicketCreateInput) { + async create(db: PostgresJsDatabase, 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, companyId: string, id: string) { + async getById(db: PostgresJsDatabase, 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, companyId: string, params: PaginationInput, filters?: { + async list(db: PostgresJsDatabase, 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 = { ticket_number: repairTickets.ticketNumber, @@ -151,8 +148,8 @@ export const RepairTicketService = { return paginatedResponse(data, total, params.page, params.limit) }, - async listByBatch(db: PostgresJsDatabase, companyId: string, batchId: string, params: PaginationInput) { - const baseWhere = and(eq(repairTickets.companyId, companyId), eq(repairTickets.repairBatchId, batchId)) + async listByBatch(db: PostgresJsDatabase, 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, companyId: string, id: string, input: RepairTicketUpdateInput) { + async update(db: PostgresJsDatabase, id: string, input: RepairTicketUpdateInput) { const values: Record = { ...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, companyId: string, id: string, status: string) { + async updateStatus(db: PostgresJsDatabase, id: string, status: string) { const updates: Record = { 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, companyId: string, id: string) { + async delete(db: PostgresJsDatabase, 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, companyId: string, lineItemId: string): Promise { - 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, companyId: string, id: string, input: RepairLineItemUpdateInput) { - if (!(await this.verifyOwnership(db, companyId, id))) return null - + async update(db: PostgresJsDatabase, id: string, input: RepairLineItemUpdateInput) { const values: Record = { ...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, companyId: string, id: string) { - if (!(await this.verifyOwnership(db, companyId, id))) return null - + async delete(db: PostgresJsDatabase, 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, companyId: string, input: RepairBatchCreateInput) { + async create(db: PostgresJsDatabase, 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, companyId: string, id: string) { + async getById(db: PostgresJsDatabase, 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, companyId: string, params: PaginationInput) { - const baseWhere = eq(repairBatches.companyId, companyId) + async list(db: PostgresJsDatabase, 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 = { 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, companyId: string, id: string, input: RepairBatchUpdateInput) { + async update(db: PostgresJsDatabase, id: string, input: RepairBatchUpdateInput) { const values: Record = { ...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, companyId: string, id: string, status: string) { + async updateStatus(db: PostgresJsDatabase, id: string, status: string) { const updates: Record = { 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, companyId: string, id: string, approvedBy: string) { + async approve(db: PostgresJsDatabase, 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, companyId: string, id: string) { + async reject(db: PostgresJsDatabase, 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, companyId: string, input: RepairServiceTemplateCreateInput) { + async create(db: PostgresJsDatabase, 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, companyId: string, params: PaginationInput) { - const baseWhere = and(eq(repairServiceTemplates.companyId, companyId), eq(repairServiceTemplates.isActive, true)) + async list(db: PostgresJsDatabase, 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, companyId: string, id: string, input: RepairServiceTemplateUpdateInput) { + async update(db: PostgresJsDatabase, id: string, input: RepairServiceTemplateUpdateInput) { const values: Record = { ...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, companyId: string, id: string) { + async delete(db: PostgresJsDatabase, 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, 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, id: string) { const [note] = await db .delete(repairNotes) .where(eq(repairNotes.id, id)) diff --git a/packages/backend/src/test/helpers.ts b/packages/backend/src/test/helpers.ts index ee166c4..7946093 100644 --- a/packages/backend/src/test/helpers.ts +++ b/packages/backend/src/test/helpers.ts @@ -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 { 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) {