Rename Forte to LunarFront, generalize for any small business

Rebrand from Forte (music-store-specific) to LunarFront (any small business):
- Package namespace @forte/* → @lunarfront/*
- Database forte/forte_test → lunarfront/lunarfront_test
- Docker containers, volumes, connection strings
- UI branding, localStorage keys, test emails
- All documentation and planning docs

Generalize music-specific terminology:
- instrumentDescription → itemDescription
- instrumentCount → itemCount
- instrumentType → itemCategory (on service templates)
- New migration 0027_generalize_terminology for column renames
- Seed data updated with generic examples
- RBAC descriptions updated
This commit is contained in:
Ryan Moon
2026-03-30 08:51:54 -05:00
parent 535446696c
commit 9400828f62
84 changed files with 390 additions and 820 deletions

View File

@@ -7,9 +7,9 @@ import { createClient } from './lib/client.js'
// --- Config ---
const DB_HOST = process.env.DB_HOST ?? 'localhost'
const DB_PORT = Number(process.env.DB_PORT ?? '5432')
const DB_USER = process.env.DB_USER ?? 'forte'
const DB_PASS = process.env.DB_PASS ?? 'forte'
const TEST_DB = 'forte_api_test'
const DB_USER = process.env.DB_USER ?? 'lunarfront'
const DB_PASS = process.env.DB_PASS ?? 'lunarfront'
const TEST_DB = 'lunarfront_api_test'
const TEST_PORT = 8001
const BASE_URL = `http://localhost:${TEST_PORT}`
const COMPANY_ID = 'a0000000-0000-0000-0000-000000000001'
@@ -60,7 +60,7 @@ async function setupDatabase() {
`)
// 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 company (id, name, timezone) VALUES (${COMPANY_ID}, 'Test Store', 'America/Chicago')`
await testSql`INSERT INTO location (id, name) VALUES (${LOCATION_ID}, 'Test Location')`
// Seed lookup tables
@@ -96,8 +96,8 @@ async function setupDatabase() {
{ slug: 'inventory', name: 'Inventory', description: 'Product catalog, stock tracking, and unit management', enabled: true },
{ slug: 'pos', name: 'Point of Sale', description: 'Sales transactions, cash drawer, and receipts', enabled: true },
{ slug: 'repairs', name: 'Repairs', description: 'Repair ticket management, batches, and service templates', enabled: true },
{ slug: 'rentals', name: 'Rentals', description: 'Instrument rental agreements and billing', enabled: false },
{ slug: 'lessons', name: 'Lessons', description: 'Lesson scheduling, instructor management, and billing', enabled: false },
{ slug: 'rentals', name: 'Rentals', description: 'Rental agreements and billing', enabled: false },
{ slug: 'lessons', name: 'Lessons', description: 'Scheduling, staff management, and billing', enabled: false },
{ slug: 'files', name: 'Files', description: 'Shared file storage with folder organization', enabled: true },
{ slug: 'vault', name: 'Vault', description: 'Encrypted password and secret manager', enabled: true },
{ slug: 'email', name: 'Email', description: 'Email campaigns, templates, and sending', enabled: false },
@@ -134,7 +134,7 @@ async function startBackend(): Promise<Subprocess> {
HOST: '0.0.0.0',
NODE_ENV: 'development',
LOG_LEVEL: 'error',
STORAGE_LOCAL_PATH: '/tmp/forte-test-files',
STORAGE_LOCAL_PATH: '/tmp/lunarfront-test-files',
},
stdout: 'pipe',
stderr: 'pipe',
@@ -170,7 +170,7 @@ async function registerTestUser(): Promise<string> {
method: 'POST',
headers,
body: JSON.stringify({
email: 'test@forte.dev',
email: 'test@lunarfront.dev',
password: testPassword,
firstName: 'Test',
lastName: 'Runner',
@@ -193,7 +193,7 @@ async function registerTestUser(): Promise<string> {
const loginRes = await fetch(`${BASE_URL}/v1/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'test@forte.dev', password: testPassword }),
body: JSON.stringify({ email: 'test@lunarfront.dev', password: testPassword }),
})
const loginData = await loginRes.json() as { token?: string }
if (loginRes.status !== 200 || !loginData.token) throw new Error(`Auth failed: ${JSON.stringify(loginData)}`)

View File

@@ -304,7 +304,7 @@ suite('RBAC', { tags: ['rbac', 'permissions'] }, (t) => {
t.test('cannot disable yourself', { tags: ['users', 'status'] }, async () => {
// Get current user ID from the users list
const usersRes = await t.api.get('/v1/users')
const currentUser = usersRes.data.data.find((u: { email: string }) => u.email === 'test@forte.dev')
const currentUser = usersRes.data.data.find((u: { email: string }) => u.email === 'test@lunarfront.dev')
t.assert.ok(currentUser)
const res = await t.api.patch(`/v1/users/${currentUser.id}/status`, { isActive: false })

View File

@@ -7,8 +7,8 @@ suite('Repairs', { tags: ['repairs'] }, (t) => {
const res = await t.api.post('/v1/repair-tickets', {
customerName: 'Walk-In Customer',
customerPhone: '555-0100',
instrumentDescription: 'Yamaha Trumpet',
problemDescription: 'Stuck valve, needs cleaning',
itemDescription: 'Samsung Galaxy S24',
problemDescription: 'Cracked screen, touch not working',
conditionIn: 'fair',
})
t.assert.status(res, 201)
@@ -25,8 +25,8 @@ suite('Repairs', { tags: ['repairs'] }, (t) => {
const res = await t.api.post('/v1/repair-tickets', {
customerName: 'Repair Customer',
accountId: acct.data.id,
problemDescription: 'Broken bridge on violin',
instrumentDescription: 'Student Violin 4/4',
problemDescription: 'Screen flickering intermittently',
itemDescription: 'Dell XPS 15 Laptop',
conditionIn: 'poor',
})
t.assert.status(res, 201)
@@ -164,12 +164,12 @@ suite('Repairs', { tags: ['repairs'] }, (t) => {
t.assert.ok(res.data.data.some((t: { customerName: string }) => t.customerName === 'Searchable Trumpet Guy'))
})
t.test('searches tickets by instrument description', { tags: ['tickets', 'search'] }, async () => {
await t.api.post('/v1/repair-tickets', { customerName: 'Instrument Search', problemDescription: 'Test', instrumentDescription: 'Selmer Mark VI Saxophone' })
t.test('searches tickets by item description', { tags: ['tickets', 'search'] }, async () => {
await t.api.post('/v1/repair-tickets', { customerName: 'Item Search', problemDescription: 'Test', itemDescription: 'Samsung Galaxy S24 Ultra' })
const res = await t.api.get('/v1/repair-tickets', { q: 'Mark VI' })
const res = await t.api.get('/v1/repair-tickets', { q: 'Galaxy S24' })
t.assert.status(res, 200)
t.assert.ok(res.data.data.some((t: { instrumentDescription: string }) => t.instrumentDescription?.includes('Mark VI')))
t.assert.ok(res.data.data.some((t: { itemDescription: string }) => t.itemDescription?.includes('Galaxy S24')))
})
t.test('sorts tickets by customer name descending', { tags: ['tickets', 'sort'] }, async () => {
@@ -289,7 +289,7 @@ suite('Repairs', { tags: ['repairs'] }, (t) => {
const ticket = await t.api.post('/v1/repair-tickets', { customerName: 'Customer Note', problemDescription: 'Test' })
const res = await t.api.post(`/v1/repair-tickets/${ticket.data.id}/notes`, {
content: 'Your instrument is ready for pickup',
content: 'Your item is ready for pickup',
visibility: 'customer',
})
t.assert.status(res, 201)
@@ -341,17 +341,17 @@ suite('Repairs', { tags: ['repairs'] }, (t) => {
const res = await t.api.post('/v1/repair-batches', {
accountId: acct.data.id,
contactName: 'Band Director',
contactName: 'IT Director',
contactPhone: '555-0200',
instrumentCount: 15,
notes: 'Annual instrument checkup',
itemCount: 15,
notes: 'Annual equipment checkup',
})
t.assert.status(res, 201)
t.assert.ok(res.data.batchNumber)
t.assert.equal(res.data.batchNumber.length, 6)
t.assert.equal(res.data.status, 'intake')
t.assert.equal(res.data.approvalStatus, 'pending')
t.assert.equal(res.data.instrumentCount, 15)
t.assert.equal(res.data.itemCount, 15)
})
t.test('returns 404 for missing batch', { tags: ['batches', 'read'] }, async () => {
@@ -377,19 +377,19 @@ suite('Repairs', { tags: ['repairs'] }, (t) => {
t.test('updates a batch', { tags: ['batches', 'update'] }, async () => {
const acct = await t.api.post('/v1/accounts', { name: 'Update Batch School', billingMode: 'consolidated' })
const batch = await t.api.post('/v1/repair-batches', { accountId: acct.data.id, instrumentCount: 5 })
const batch = await t.api.post('/v1/repair-batches', { accountId: acct.data.id, itemCount: 5 })
const res = await t.api.patch(`/v1/repair-batches/${batch.data.id}`, { instrumentCount: 10, contactName: 'Updated Director' })
const res = await t.api.patch(`/v1/repair-batches/${batch.data.id}`, { itemCount: 10, contactName: 'Updated Director' })
t.assert.status(res, 200)
t.assert.equal(res.data.contactName, 'Updated Director')
})
t.test('adds tickets to a batch and lists them', { tags: ['batches', 'tickets'] }, async () => {
const acct = await t.api.post('/v1/accounts', { name: 'Batch Tickets School', billingMode: 'consolidated' })
const batch = await t.api.post('/v1/repair-batches', { accountId: acct.data.id, instrumentCount: 2 })
const batch = await t.api.post('/v1/repair-batches', { accountId: acct.data.id, itemCount: 2 })
await t.api.post('/v1/repair-tickets', { customerName: 'Batch Tickets School', repairBatchId: batch.data.id, problemDescription: 'Flute pads', instrumentDescription: 'Flute' })
await t.api.post('/v1/repair-tickets', { customerName: 'Batch Tickets School', repairBatchId: batch.data.id, problemDescription: 'Clarinet cork', instrumentDescription: 'Clarinet' })
await t.api.post('/v1/repair-tickets', { customerName: 'Batch Tickets School', repairBatchId: batch.data.id, problemDescription: 'Screen cracked', itemDescription: 'Chromebook #1' })
await t.api.post('/v1/repair-tickets', { customerName: 'Batch Tickets School', repairBatchId: batch.data.id, problemDescription: 'Battery dead', itemDescription: 'Chromebook #2' })
const tickets = await t.api.get(`/v1/repair-batches/${batch.data.id}/tickets`, { limit: 100 })
t.assert.status(tickets, 200)
@@ -437,36 +437,36 @@ suite('Repairs', { tags: ['repairs'] }, (t) => {
t.test('creates a service template', { tags: ['templates', 'create'] }, async () => {
const res = await t.api.post('/v1/repair-service-templates', {
name: 'Bow Rehair',
instrumentType: 'Violin',
size: '4/4',
name: 'Screen Repair',
itemCategory: 'Electronics',
size: 'Phone',
itemType: 'flat_rate',
defaultPrice: 65,
defaultCost: 15,
})
t.assert.status(res, 201)
t.assert.equal(res.data.name, 'Bow Rehair')
t.assert.equal(res.data.instrumentType, 'Violin')
t.assert.equal(res.data.size, '4/4')
t.assert.equal(res.data.name, 'Screen Repair')
t.assert.equal(res.data.itemCategory, 'Electronics')
t.assert.equal(res.data.size, 'Phone')
t.assert.equal(res.data.defaultPrice, '65.00')
t.assert.equal(res.data.defaultCost, '15.00')
})
t.test('lists service templates with search', { tags: ['templates', 'read'] }, async () => {
await t.api.post('/v1/repair-service-templates', { name: 'String Change', instrumentType: 'Guitar', defaultPrice: 25 })
await t.api.post('/v1/repair-service-templates', { name: 'Battery Replacement', itemCategory: 'Electronics', defaultPrice: 25 })
const res = await t.api.get('/v1/repair-service-templates', { q: 'String', limit: 100 })
const res = await t.api.get('/v1/repair-service-templates', { q: 'Battery', limit: 100 })
t.assert.status(res, 200)
t.assert.ok(res.data.data.some((t: { name: string }) => t.name === 'String Change'))
t.assert.ok(res.data.data.some((t: { name: string }) => t.name === 'Battery Replacement'))
t.assert.ok(res.data.pagination)
})
t.test('updates a service template', { tags: ['templates', 'update'] }, async () => {
const created = await t.api.post('/v1/repair-service-templates', { name: 'Pad Replace', defaultPrice: 30 })
const res = await t.api.patch(`/v1/repair-service-templates/${created.data.id}`, { defaultPrice: 35, instrumentType: 'Clarinet' })
const created = await t.api.post('/v1/repair-service-templates', { name: 'Tune-Up', defaultPrice: 30 })
const res = await t.api.patch(`/v1/repair-service-templates/${created.data.id}`, { defaultPrice: 35, itemCategory: 'Bicycles' })
t.assert.status(res, 200)
t.assert.equal(res.data.defaultPrice, '35.00')
t.assert.equal(res.data.instrumentType, 'Clarinet')
t.assert.equal(res.data.itemCategory, 'Bicycles')
})
t.test('soft-deletes a service template', { tags: ['templates', 'delete'] }, async () => {

View File

@@ -96,14 +96,14 @@ suite('Vault', { tags: ['vault'] }, (t) => {
const res = await t.api.post(`/v1/vault/categories/${catId}/entries`, {
name: 'Store WiFi',
username: 'ForteMusic',
username: 'DemoUser',
url: 'http://192.168.1.1',
notes: 'Router admin panel',
secret: 'supersecretpassword123',
})
t.assert.status(res, 201)
t.assert.equal(res.data.name, 'Store WiFi')
t.assert.equal(res.data.username, 'ForteMusic')
t.assert.equal(res.data.username, 'DemoUser')
t.assert.equal(res.data.hasSecret, true)
// Secret value should NOT be in the response
t.assert.falsy(res.data.encryptedValue)

View File

@@ -27,7 +27,7 @@ async function dav(
suite('WebDAV', { tags: ['webdav', 'storage'] }, (t) => {
// Use the same test user created by the test runner
const email = 'test@forte.dev'
const email = 'test@lunarfront.dev'
const password = 'testpassword1234'
const basicAuth = 'Basic ' + Buffer.from(`${email}:${password}`).toString('base64')
const badAuth = 'Basic ' + Buffer.from(`${email}:wrongpassword`).toString('base64')

View File

@@ -5,6 +5,6 @@ export default defineConfig({
out: './src/db/migrations',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL ?? 'postgresql://forte:forte@localhost:5432/forte',
url: process.env.DATABASE_URL ?? 'postgresql://lunarfront:lunarfront@localhost:5432/lunarfront',
},
})

View File

@@ -1,5 +1,5 @@
{
"name": "@forte/backend",
"name": "@lunarfront/backend",
"version": "0.0.1",
"private": true,
"type": "module",
@@ -13,6 +13,8 @@
"db:generate": "bunx drizzle-kit generate",
"db:migrate": "bunx drizzle-kit migrate",
"db:seed-dev": "bun run src/db/seeds/dev-seed.ts",
"db:seed-music": "bun run src/db/seeds/music-store-seed.ts",
"db:seed-reset-repairs": "bun run src/db/seeds/reset-repairs.ts",
"db:seed": "bun run src/db/seed.ts"
},
"dependencies": {
@@ -20,7 +22,7 @@
"@fastify/jwt": "^9",
"@fastify/multipart": "^9.4.0",
"@fastify/rate-limit": "^10.3.0",
"@forte/shared": "workspace:*",
"@lunarfront/shared": "workspace:*",
"bcrypt": "^6",
"drizzle-orm": "^0.38",
"fastify": "^5",

View File

@@ -0,0 +1,14 @@
-- Generalize music-specific column names to generic terminology
-- repair_ticket: instrument_description -> item_description
ALTER TABLE repair_ticket RENAME COLUMN instrument_description TO item_description;
-- repair_batch: instrument_count -> item_count
ALTER TABLE repair_batch RENAME COLUMN instrument_count TO item_count;
-- repair_service_template: instrument_type -> item_category
ALTER TABLE repair_service_template RENAME COLUMN instrument_type TO item_category;
-- Update module descriptions to be industry-agnostic
UPDATE module_config SET description = 'Rental agreements and billing' WHERE slug = 'rentals';
UPDATE module_config SET description = 'Scheduling, staff management, and billing' WHERE slug = 'lessons';

View File

@@ -190,6 +190,13 @@
"when": 1774860000000,
"tag": "0026_modules",
"breakpoints": true
},
{
"idx": 27,
"version": "7",
"when": 1774870000000,
"tag": "0027_generalize_terminology",
"breakpoints": true
}
]
}

View File

@@ -82,7 +82,7 @@ export const repairBatches = pgTable('repair_batch', {
dueDate: timestamp('due_date', { withTimezone: true }),
completedDate: timestamp('completed_date', { withTimezone: true }),
deliveredDate: timestamp('delivered_date', { withTimezone: true }),
instrumentCount: integer('instrument_count').notNull().default(0),
itemCount: integer('item_count').notNull().default(0),
receivedCount: integer('received_count').notNull().default(0),
estimatedTotal: numeric('estimated_total', { precision: 10, scale: 2 }),
actualTotal: numeric('actual_total', { precision: 10, scale: 2 }),
@@ -101,7 +101,7 @@ export const repairTickets = pgTable('repair_ticket', {
customerName: varchar('customer_name', { length: 255 }).notNull(),
customerPhone: varchar('customer_phone', { length: 50 }),
inventoryUnitId: uuid('inventory_unit_id').references(() => inventoryUnits.id),
instrumentDescription: text('instrument_description'),
itemDescription: text('item_description'),
serialNumber: varchar('serial_number', { length: 255 }),
conditionIn: repairConditionInEnum('condition_in'),
conditionInNotes: text('condition_in_notes'),
@@ -158,7 +158,7 @@ export type RepairNoteInsert = typeof repairNotes.$inferInsert
export const repairServiceTemplates = pgTable('repair_service_template', {
id: uuid('id').primaryKey().defaultRandom(),
name: varchar('name', { length: 255 }).notNull(),
instrumentType: varchar('instrument_type', { length: 100 }),
itemCategory: varchar('item_category', { length: 100 }),
size: varchar('size', { length: 50 }),
description: text('description'),
itemType: repairLineItemTypeEnum('item_type').notNull().default('flat_rate'),

View File

@@ -7,7 +7,7 @@ const DEV_LOCATION_ID = '00000000-0000-0000-0000-000000000010'
async function seed() {
const connectionString =
process.env.DATABASE_URL ?? 'postgresql://forte:forte@localhost:5432/forte'
process.env.DATABASE_URL ?? 'postgresql://lunarfront:lunarfront@localhost:5432/lunarfront'
const sql = postgres(connectionString)
const db = drizzle(sql)
@@ -17,7 +17,7 @@ async function seed() {
.insert(companies)
.values({
id: DEV_COMPANY_ID,
name: 'Dev Music Co.',
name: 'Dev Store',
timezone: 'America/Chicago',
})
.onConflictDoNothing()

View File

@@ -6,7 +6,7 @@
*/
import postgres from 'postgres'
const DB_URL = process.env.DATABASE_URL ?? 'postgresql://forte:forte@localhost:5432/forte'
const DB_URL = process.env.DATABASE_URL ?? 'postgresql://lunarfront:lunarfront@localhost:5432/lunarfront'
const COMPANY_ID = 'a0000000-0000-0000-0000-000000000001'
const sql = postgres(DB_URL)
@@ -17,7 +17,7 @@ async function seed() {
// Create company and location if they don't exist
const [company] = await sql`SELECT id FROM company WHERE id = ${COMPANY_ID}`
if (!company) {
await sql`INSERT INTO company (id, name, timezone) VALUES (${COMPANY_ID}, 'Forte Music Store', 'America/Chicago')`
await sql`INSERT INTO company (id, name, timezone) VALUES (${COMPANY_ID}, 'Demo Store', 'America/Chicago')`
await sql`INSERT INTO location (id, name) VALUES ('a0000000-0000-0000-0000-000000000002', 'Main Store')`
console.log(' Created company and location')
@@ -39,16 +39,16 @@ 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 FROM "user" WHERE email = 'admin@lunarfront.dev'`
if (!adminUser) {
const bcrypt = await import('bcrypt')
const hashedPw = await (bcrypt.default || bcrypt).hash('admin1234', 10)
const [user] = await sql`INSERT INTO "user" (email, password_hash, first_name, last_name, role) VALUES ('admin@forte.dev', ${hashedPw}, 'Admin', 'User', 'admin') RETURNING id`
const [user] = await sql`INSERT INTO "user" (email, password_hash, first_name, last_name, role) VALUES ('admin@lunarfront.dev', ${hashedPw}, 'Admin', 'User', 'admin') RETURNING id`
const [adminRole] = await sql`SELECT id FROM role WHERE slug = 'admin' LIMIT 1`
if (adminRole) {
await sql`INSERT INTO user_role_assignment (user_id, role_id) VALUES (${user.id}, ${adminRole.id}) ON CONFLICT DO NOTHING`
}
console.log(' Created admin user: admin@forte.dev / admin1234')
console.log(' Created admin user: admin@lunarfront.dev / admin1234')
} else {
const [adminRole] = await sql`SELECT id FROM role WHERE slug = 'admin' LIMIT 1`
if (adminRole) {
@@ -61,11 +61,11 @@ async function seed() {
const accounts = [
{ name: 'Smith Family', email: 'smith@example.com', phone: '555-0101' },
{ name: 'Johnson Family', email: 'johnson@example.com', phone: '555-0102' },
{ name: 'Lincoln High School', email: 'band@lincoln.edu', phone: '555-0200' },
{ name: 'Garcia Music Studio', email: 'garcia@studio.com', phone: '555-0103' },
{ name: 'Lincoln High School', email: 'office@lincoln.edu', phone: '555-0200' },
{ name: 'Garcia Workshop', email: 'garcia@studio.com', phone: '555-0103' },
{ name: 'Mike Thompson', email: 'mike.t@email.com', phone: '555-0104' },
{ name: 'Emily Chen', email: 'emily.chen@email.com', phone: '555-0105' },
{ name: 'Westside Church', email: 'music@westsidechurch.org', phone: '555-0300' },
{ name: 'Westside Church', email: 'admin@westsidechurch.org', phone: '555-0300' },
{ name: 'Oak Elementary', email: 'office@oakelementary.edu', phone: '555-0201' },
]
@@ -89,7 +89,7 @@ async function seed() {
{ accountName: 'Smith Family', firstName: 'Tommy', lastName: 'Smith', isMinor: true },
{ accountName: 'Johnson Family', firstName: 'Lisa', lastName: 'Johnson', email: 'lisa.j@example.com' },
{ accountName: 'Johnson Family', firstName: 'Jake', lastName: 'Johnson', isMinor: true },
{ accountName: 'Garcia Music Studio', firstName: 'Carlos', lastName: 'Garcia', email: 'carlos@studio.com' },
{ accountName: 'Garcia Workshop', firstName: 'Carlos', lastName: 'Garcia', email: 'carlos@studio.com' },
{ accountName: 'Mike Thompson', firstName: 'Mike', lastName: 'Thompson', email: 'mike.t@email.com' },
{ accountName: 'Emily Chen', firstName: 'Emily', lastName: 'Chen', email: 'emily.chen@email.com' },
]
@@ -105,39 +105,37 @@ async function seed() {
// --- Repair Service Templates ---
const templates = [
{ name: 'Bow Rehair', instrumentType: 'Violin', size: '4/4', itemType: 'flat_rate', price: '65.00', cost: '15.00' },
{ name: 'Bow Rehair', instrumentType: 'Violin', size: '3/4', itemType: 'flat_rate', price: '55.00', cost: '12.00' },
{ name: 'Bow Rehair', instrumentType: 'Cello', size: null, itemType: 'flat_rate', price: '80.00', cost: '20.00' },
{ name: 'Bow Rehair', instrumentType: 'Bass', size: null, itemType: 'flat_rate', price: '90.00', cost: '25.00' },
{ name: 'String Change', instrumentType: 'Guitar', size: 'Acoustic', itemType: 'flat_rate', price: '25.00', cost: '8.00' },
{ name: 'String Change', instrumentType: 'Guitar', size: 'Electric', itemType: 'flat_rate', price: '25.00', cost: '7.00' },
{ name: 'String Change', instrumentType: 'Violin', size: '4/4', itemType: 'flat_rate', price: '35.00', cost: '12.00' },
{ name: 'Valve Overhaul', instrumentType: 'Trumpet', size: null, itemType: 'labor', price: '85.00', cost: null },
{ name: 'Pad Replacement', instrumentType: 'Clarinet', size: null, itemType: 'flat_rate', price: '120.00', cost: '30.00' },
{ name: 'Pad Replacement', instrumentType: 'Flute', size: null, itemType: 'flat_rate', price: '110.00', cost: '25.00' },
{ name: 'Cork Replacement', instrumentType: 'Clarinet', size: null, itemType: 'flat_rate', price: '45.00', cost: '5.00' },
{ name: 'Slide Repair', instrumentType: 'Trombone', size: null, itemType: 'labor', price: '75.00', cost: null },
{ name: 'Bridge Setup', instrumentType: 'Violin', size: '4/4', itemType: 'flat_rate', price: '40.00', cost: '10.00' },
{ name: 'Guitar Setup', instrumentType: 'Guitar', size: null, itemType: 'flat_rate', price: '65.00', cost: '5.00' },
{ name: 'Dent Removal', instrumentType: 'Brass', size: null, itemType: 'labor', price: '50.00', cost: null },
{ name: 'General Cleaning', instrumentType: null, size: null, itemType: 'flat_rate', price: '30.00', cost: '5.00' },
{ name: 'Screen Repair', itemCategory: 'Electronics', size: 'Phone', itemType: 'flat_rate', price: '89.00', cost: '25.00' },
{ name: 'Screen Repair', itemCategory: 'Electronics', size: 'Tablet', itemType: 'flat_rate', price: '129.00', cost: '45.00' },
{ name: 'Battery Replacement', itemCategory: 'Electronics', size: 'Phone', itemType: 'flat_rate', price: '59.00', cost: '15.00' },
{ name: 'Battery Replacement', itemCategory: 'Electronics', size: 'Laptop', itemType: 'flat_rate', price: '99.00', cost: '35.00' },
{ name: 'Tune-Up', itemCategory: 'Bicycles', size: 'Standard', itemType: 'flat_rate', price: '65.00', cost: '10.00' },
{ name: 'Brake Adjustment', itemCategory: 'Bicycles', size: null, itemType: 'flat_rate', price: '35.00', cost: '5.00' },
{ name: 'Blade Sharpening', itemCategory: 'Tools', size: null, itemType: 'flat_rate', price: '15.00', cost: '3.00' },
{ name: 'Motor Repair', itemCategory: 'Appliances', size: null, itemType: 'labor', price: '85.00', cost: null },
{ name: 'Zipper Replacement', itemCategory: 'Clothing', size: null, itemType: 'flat_rate', price: '25.00', cost: '5.00' },
{ name: 'Sole Replacement', itemCategory: 'Footwear', size: null, itemType: 'flat_rate', price: '55.00', cost: '15.00' },
{ name: 'Watch Battery', itemCategory: 'Watches', size: null, itemType: 'flat_rate', price: '15.00', cost: '3.00' },
{ name: 'Furniture Refinishing', itemCategory: 'Furniture', size: null, itemType: 'labor', price: '150.00', cost: null },
{ name: 'Diagnostic Check', itemCategory: null, size: null, itemType: 'flat_rate', price: '30.00', cost: '5.00' },
{ name: 'General Cleaning', itemCategory: null, size: null, itemType: 'flat_rate', price: '30.00', cost: '5.00' },
]
for (const t of templates) {
const existing = await sql`SELECT id FROM repair_service_template WHERE name = ${t.name} AND COALESCE(instrument_type, '') = ${t.instrumentType ?? ''} AND COALESCE(size, '') = ${t.size ?? ''}`
const existing = await sql`SELECT id FROM repair_service_template WHERE name = ${t.name} AND COALESCE(item_category, '') = ${t.itemCategory ?? ''} AND COALESCE(size, '') = ${t.size ?? ''}`
if (existing.length > 0) continue
await sql`INSERT INTO repair_service_template (name, instrument_type, size, item_type, default_price, default_cost, is_active) VALUES (${t.name}, ${t.instrumentType}, ${t.size}, ${t.itemType}, ${t.price}, ${t.cost}, true)`
console.log(` Template: ${t.name} ${t.instrumentType ?? ''} ${t.size ?? ''}`)
await sql`INSERT INTO repair_service_template (name, item_category, size, item_type, default_price, default_cost, is_active) VALUES (${t.name}, ${t.itemCategory}, ${t.size}, ${t.itemType}, ${t.price}, ${t.cost}, true)`
console.log(` Template: ${t.name} ${t.itemCategory ?? ''} ${t.size ?? ''}`)
}
// --- Repair Tickets ---
const tickets = [
{ customer: 'Mike Thompson', instrument: 'Fender Stratocaster', serial: 'US22-045891', problem: 'Fret buzz on 3rd and 5th fret, needs setup', condition: 'good', status: 'in_progress', estimate: '65.00' },
{ customer: 'Emily Chen', instrument: 'Yamaha YTR-2330 Trumpet', serial: 'YTR-78432', problem: 'Stuck 2nd valve, sluggish action on all valves', condition: 'fair', status: 'pending_approval', estimate: '85.00' },
{ customer: 'David Smith', instrument: 'Stradivarius Copy Violin', serial: null, problem: 'Bow needs rehair, bridge slightly warped', condition: 'fair', status: 'ready', estimate: '105.00' },
{ customer: 'Carlos Garcia', instrument: 'Martin D-28 Acoustic Guitar', serial: 'M2284563', problem: 'Broken tuning peg, needs replacement', condition: 'good', status: 'new', estimate: null },
{ customer: 'Lisa Johnson', instrument: 'Yamaha YCL-255 Clarinet', serial: null, problem: 'Several pads worn, keys sticking', condition: 'poor', status: 'diagnosing', estimate: null },
{ customer: 'Walk-In Customer', instrument: 'Unknown Flute', serial: null, problem: 'Customer says it squeaks on high notes', condition: 'fair', status: 'intake', estimate: null },
{ customer: 'Mike Thompson', item: 'Samsung Galaxy S24', serial: 'IMEI-354789102', problem: 'Cracked screen, touch not responsive in bottom half', condition: 'good', status: 'in_progress', estimate: '89.00' },
{ customer: 'Emily Chen', item: 'HP LaserJet Pro M404', serial: 'HP-CNB3K12345', problem: 'Paper jam sensor error, won\'t feed from tray 2', condition: 'fair', status: 'pending_approval', estimate: '85.00' },
{ customer: 'David Smith', item: 'Trek Marlin 7 Mountain Bike', serial: null, problem: 'Rear derailleur bent, chain skipping gears', condition: 'fair', status: 'ready', estimate: '65.00' },
{ customer: 'Carlos Garcia', item: 'KitchenAid Stand Mixer KSM150', serial: 'W10807813', problem: 'Motor making grinding noise at low speeds', condition: 'good', status: 'new', estimate: null },
{ customer: 'Lisa Johnson', item: 'Apple MacBook Pro 14"', serial: null, problem: 'Battery draining rapidly, trackpad click intermittent', condition: 'poor', status: 'diagnosing', estimate: null },
{ customer: 'Walk-In Customer', item: 'Leather Work Boots', serial: null, problem: 'Sole separating from upper on both shoes', condition: 'fair', status: 'intake', estimate: null },
]
for (const t of tickets) {
@@ -145,8 +143,8 @@ async function seed() {
if (existing.length > 0) continue
const num = String(Math.floor(100000 + Math.random() * 900000))
const acctId = acctIds[t.customer] ?? null
await sql`INSERT INTO repair_ticket (ticket_number, customer_name, account_id, instrument_description, serial_number, problem_description, condition_in, status, estimated_cost) VALUES (${num}, ${t.customer}, ${acctId}, ${t.instrument}, ${t.serial}, ${t.problem}, ${t.condition}, ${t.status}, ${t.estimate})`
console.log(` Ticket: ${t.customer}${t.instrument} [${t.status}]`)
await sql`INSERT INTO repair_ticket (ticket_number, customer_name, account_id, item_description, serial_number, problem_description, condition_in, status, estimated_cost) VALUES (${num}, ${t.customer}, ${acctId}, ${t.item}, ${t.serial}, ${t.problem}, ${t.condition}, ${t.status}, ${t.estimate})`
console.log(` Ticket: ${t.customer}${t.item} [${t.status}]`)
}
// --- Repair Batch ---
@@ -154,22 +152,22 @@ async function seed() {
if (batchExists.length === 0) {
const batchNum = String(Math.floor(100000 + Math.random() * 900000))
const schoolId = acctIds['Lincoln High School']
const [batch] = await sql`INSERT INTO repair_batch (batch_number, account_id, contact_name, contact_phone, contact_email, instrument_count, notes, status) VALUES (${batchNum}, ${schoolId}, 'Mr. Williams', '555-0210', 'williams@lincoln.edu', 5, 'Annual band instrument checkup — 5 instruments', 'intake') RETURNING id`
const [batch] = await sql`INSERT INTO repair_batch (batch_number, account_id, contact_name, contact_phone, contact_email, item_count, notes, status) VALUES (${batchNum}, ${schoolId}, 'Mr. Williams', '555-0210', 'williams@lincoln.edu', 5, 'Annual equipment checkup — 5 items', 'intake') RETURNING id`
const batchTickets = [
{ instrument: 'Student Flute', problem: 'Pads worn, needs replacement check', condition: 'fair' },
{ instrument: 'Student Clarinet #1', problem: 'Keys sticking, cork dried out', condition: 'fair' },
{ instrument: 'Student Clarinet #2', problem: 'Barrel crack, needs assessment', condition: 'poor' },
{ instrument: 'Student Trumpet', problem: 'Valve oil needed, general checkup', condition: 'good' },
{ instrument: 'Student Trombone', problem: 'Slide dent, sluggish movement', condition: 'fair' },
{ item: 'Chromebook #101', problem: 'Screen flickering, hinge loose', condition: 'fair' },
{ item: 'Chromebook #102', problem: 'Keyboard unresponsive, several keys stuck', condition: 'fair' },
{ item: 'Projector — Epson EB-X51', problem: 'Lamp dim, color wheel noise', condition: 'poor' },
{ item: 'Label Printer — Dymo 450', problem: 'Feed mechanism jammed', condition: 'good' },
{ item: 'PA Speaker — JBL EON715', problem: 'Crackling at high volume', condition: 'fair' },
]
for (const bt of batchTickets) {
const num = String(Math.floor(100000 + Math.random() * 900000))
await sql`INSERT INTO repair_ticket (ticket_number, customer_name, account_id, repair_batch_id, instrument_description, problem_description, condition_in, status) VALUES (${num}, 'Lincoln High School', ${schoolId}, ${batch.id}, ${bt.instrument}, ${bt.problem}, ${bt.condition}, 'new')`
console.log(` Batch ticket: ${bt.instrument}`)
await sql`INSERT INTO repair_ticket (ticket_number, customer_name, account_id, repair_batch_id, item_description, problem_description, condition_in, status) VALUES (${num}, 'Lincoln High School', ${schoolId}, ${batch.id}, ${bt.item}, ${bt.problem}, ${bt.condition}, 'new')`
console.log(` Batch ticket: ${bt.item}`)
}
console.log(` Batch: Lincoln High School — 5 instruments`)
console.log(` Batch: Lincoln High School — 5 items`)
}
console.log('\nDev seed complete!')

View File

@@ -20,14 +20,14 @@ export const SYSTEM_PERMISSIONS = [
{ slug: 'pos.admin', domain: 'pos', action: 'admin', description: 'Void transactions, override prices, manage discounts' },
// Rentals
{ slug: 'rentals.view', domain: 'rentals', action: 'view', description: 'View rental contracts, fleet, billing' },
{ slug: 'rentals.edit', domain: 'rentals', action: 'edit', description: 'Create rentals, process returns, manage fleet' },
{ slug: 'rentals.view', domain: 'rentals', action: 'view', description: 'View rental contracts, inventory, billing' },
{ slug: 'rentals.edit', domain: 'rentals', action: 'edit', description: 'Create rentals, process returns, manage rental inventory' },
{ slug: 'rentals.admin', domain: 'rentals', action: 'admin', description: 'Override terms, adjust equity, cancel contracts' },
// Lessons
{ slug: 'lessons.view', domain: 'lessons', action: 'view', description: 'View lesson schedules, enrollments, attendance' },
// Lessons / Scheduling
{ slug: 'lessons.view', domain: 'lessons', action: 'view', description: 'View schedules, enrollments, attendance' },
{ slug: 'lessons.edit', domain: 'lessons', action: 'edit', description: 'Manage scheduling, enrollment, attendance' },
{ slug: 'lessons.admin', domain: 'lessons', action: 'admin', description: 'Configure lesson settings, manage instructors' },
{ slug: 'lessons.admin', domain: 'lessons', action: 'admin', description: 'Configure scheduling settings, manage staff' },
// Repairs
{ slug: 'repairs.view', domain: 'repairs', action: 'view', description: 'View repair tickets, parts inventory' },

View File

@@ -80,7 +80,7 @@ export function webdavBasicAuth(app: FastifyInstance) {
const authHeader = request.headers.authorization
if (!authHeader || !authHeader.startsWith('Basic ')) {
reply.header('WWW-Authenticate', 'Basic realm="Forte WebDAV"')
reply.header('WWW-Authenticate', 'Basic realm="LunarFront WebDAV"')
return reply.status(401).send('Authentication required')
}
@@ -88,7 +88,7 @@ export function webdavBasicAuth(app: FastifyInstance) {
const colonIndex = decoded.indexOf(':')
if (colonIndex === -1) {
recordFailure(ip)
reply.header('WWW-Authenticate', 'Basic realm="Forte WebDAV"')
reply.header('WWW-Authenticate', 'Basic realm="LunarFront WebDAV"')
return reply.status(401).send('Invalid credentials')
}
@@ -103,14 +103,14 @@ export function webdavBasicAuth(app: FastifyInstance) {
if (!user || !user.isActive) {
recordFailure(ip)
reply.header('WWW-Authenticate', 'Basic realm="Forte WebDAV"')
reply.header('WWW-Authenticate', 'Basic realm="LunarFront WebDAV"')
return reply.status(401).send('Invalid credentials')
}
const valid = await bcrypt.compare(password, user.passwordHash)
if (!valid) {
recordFailure(ip)
reply.header('WWW-Authenticate', 'Basic realm="Forte WebDAV"')
reply.header('WWW-Authenticate', 'Basic realm="LunarFront WebDAV"')
return reply.status(401).send('Invalid credentials')
}

View File

@@ -13,7 +13,7 @@ import {
TaxExemptionUpdateSchema,
MemberIdentifierCreateSchema,
MemberIdentifierUpdateSchema,
} from '@forte/shared/schemas'
} from '@lunarfront/shared/schemas'
import {
AccountService,
MemberService,

View File

@@ -1,7 +1,7 @@
import type { FastifyPluginAsync } from 'fastify'
import { eq } from 'drizzle-orm'
import bcrypt from 'bcrypt'
import { RegisterSchema, LoginSchema } from '@forte/shared/schemas'
import { RegisterSchema, LoginSchema } from '@lunarfront/shared/schemas'
import { users } from '../../db/schema/users.js'
const SALT_ROUNDS = 10

View File

@@ -5,7 +5,7 @@ import {
SupplierCreateSchema,
SupplierUpdateSchema,
PaginationSchema,
} from '@forte/shared/schemas'
} from '@lunarfront/shared/schemas'
import { CategoryService, SupplierService } from '../../services/inventory.service.js'
export const inventoryRoutes: FastifyPluginAsync = async (app) => {

View File

@@ -1,5 +1,5 @@
import type { FastifyPluginAsync } from 'fastify'
import { LookupCreateSchema, LookupUpdateSchema } from '@forte/shared/schemas'
import { LookupCreateSchema, LookupUpdateSchema } from '@lunarfront/shared/schemas'
import { UnitStatusService, ItemConditionService } from '../../services/lookup.service.js'
import { ConflictError, ValidationError } from '../../lib/errors.js'

View File

@@ -5,7 +5,7 @@ import {
InventoryUnitCreateSchema,
InventoryUnitUpdateSchema,
PaginationSchema,
} from '@forte/shared/schemas'
} from '@lunarfront/shared/schemas'
import { ProductService, InventoryUnitService } from '../../services/product.service.js'
export const productRoutes: FastifyPluginAsync = async (app) => {

View File

@@ -1,6 +1,6 @@
import type { FastifyPluginAsync } from 'fastify'
import { eq, count, sql, type Column } from 'drizzle-orm'
import { PaginationSchema } from '@forte/shared/schemas'
import { PaginationSchema } from '@lunarfront/shared/schemas'
import { RbacService } from '../../services/rbac.service.js'
import { ValidationError } from '../../lib/errors.js'
import { users } from '../../db/schema/users.js'

View File

@@ -12,7 +12,7 @@ import {
RepairNoteCreateSchema,
RepairServiceTemplateCreateSchema,
RepairServiceTemplateUpdateSchema,
} from '@forte/shared/schemas'
} from '@lunarfront/shared/schemas'
import { RepairTicketService, RepairLineItemService, RepairBatchService, RepairNoteService, RepairServiceTemplateService } from '../../services/repair.service.js'
export const repairRoutes: FastifyPluginAsync = async (app) => {

View File

@@ -1,6 +1,6 @@
import type { FastifyPluginAsync } from 'fastify'
import multipart from '@fastify/multipart'
import { PaginationSchema } from '@forte/shared/schemas'
import { PaginationSchema } from '@lunarfront/shared/schemas'
import { StorageFolderService, StorageFileService, StoragePermissionService } from '../../services/storage.service.js'
import { ValidationError } from '../../lib/errors.js'

View File

@@ -1,5 +1,5 @@
import type { FastifyPluginAsync } from 'fastify'
import { PaginationSchema } from '@forte/shared/schemas'
import { PaginationSchema } from '@lunarfront/shared/schemas'
import { VaultKeyService, VaultPermissionService, VaultCategoryService, VaultEntryService } from '../../services/vault.service.js'
import { ValidationError } from '../../lib/errors.js'

View File

@@ -20,8 +20,8 @@ import type {
TaxExemptionCreateInput,
TaxExemptionUpdateInput,
PaginationInput,
} from '@forte/shared/schemas'
import { isMinor, normalizeStateCode } from '@forte/shared/utils'
} from '@lunarfront/shared/schemas'
import { isMinor, normalizeStateCode } from '@lunarfront/shared/utils'
import {
withPagination,
withSort,

View File

@@ -7,7 +7,7 @@ import type {
SupplierCreateInput,
SupplierUpdateInput,
PaginationInput,
} from '@forte/shared/schemas'
} from '@lunarfront/shared/schemas'
import {
withPagination,
withSort,

View File

@@ -7,7 +7,7 @@ import {
SYSTEM_UNIT_STATUSES,
SYSTEM_ITEM_CONDITIONS,
} from '../db/schema/lookups.js'
import type { LookupCreateInput, LookupUpdateInput } from '@forte/shared/schemas'
import type { LookupCreateInput, LookupUpdateInput } from '@lunarfront/shared/schemas'
function createLookupService(
table: typeof inventoryUnitStatuses | typeof itemConditions,

View File

@@ -8,7 +8,7 @@ import type {
InventoryUnitCreateInput,
InventoryUnitUpdateInput,
PaginationInput,
} from '@forte/shared/schemas'
} from '@lunarfront/shared/schemas'
import {
withPagination,
withSort,

View File

@@ -1,6 +1,6 @@
import { eq, and, inArray, count, type Column } from 'drizzle-orm'
import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
import type { PaginationInput } from '@forte/shared/schemas'
import type { PaginationInput } from '@lunarfront/shared/schemas'
import { permissions, roles, rolePermissions, userRoles } from '../db/schema/rbac.js'
import { SYSTEM_PERMISSIONS, DEFAULT_ROLES } from '../db/seeds/rbac.js'
import { ForbiddenError } from '../lib/errors.js'

View File

@@ -18,7 +18,7 @@ import type {
RepairServiceTemplateCreateInput,
RepairServiceTemplateUpdateInput,
PaginationInput,
} from '@forte/shared/schemas'
} from '@lunarfront/shared/schemas'
import {
withPagination,
withSort,
@@ -59,7 +59,7 @@ export const RepairTicketService = {
locationId: input.locationId,
repairBatchId: input.repairBatchId,
inventoryUnitId: input.inventoryUnitId,
instrumentDescription: input.instrumentDescription,
itemDescription: input.itemDescription,
serialNumber: input.serialNumber,
conditionIn: input.conditionIn,
conditionInNotes: input.conditionInNotes,
@@ -101,7 +101,7 @@ export const RepairTicketService = {
repairTickets.ticketNumber,
repairTickets.customerName,
repairTickets.customerPhone,
repairTickets.instrumentDescription,
repairTickets.itemDescription,
repairTickets.serialNumber,
])
if (search) conditions.push(search)
@@ -151,7 +151,7 @@ export const RepairTicketService = {
async listByBatch(db: PostgresJsDatabase<any>, batchId: string, params: PaginationInput) {
const baseWhere = eq(repairTickets.repairBatchId, batchId)
const searchCondition = params.q
? buildSearchCondition(params.q, [repairTickets.ticketNumber, repairTickets.customerName, repairTickets.instrumentDescription])
? buildSearchCondition(params.q, [repairTickets.ticketNumber, repairTickets.customerName, repairTickets.itemDescription])
: undefined
const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere
@@ -292,7 +292,7 @@ export const RepairBatchService = {
contactEmail: input.contactEmail,
pickupDate: input.pickupDate ? new Date(input.pickupDate) : undefined,
dueDate: input.dueDate ? new Date(input.dueDate) : undefined,
instrumentCount: input.instrumentCount,
itemCount: input.itemCount,
notes: input.notes,
})
.returning()
@@ -391,7 +391,7 @@ export const RepairServiceTemplateService = {
.insert(repairServiceTemplates)
.values({
name: input.name,
instrumentType: input.instrumentType,
itemCategory: input.itemCategory,
size: input.size,
description: input.description,
itemType: input.itemType,
@@ -406,13 +406,13 @@ export const RepairServiceTemplateService = {
async list(db: PostgresJsDatabase<any>, params: PaginationInput) {
const baseWhere = eq(repairServiceTemplates.isActive, true)
const searchCondition = params.q
? buildSearchCondition(params.q, [repairServiceTemplates.name, repairServiceTemplates.instrumentType, repairServiceTemplates.size, repairServiceTemplates.description])
? buildSearchCondition(params.q, [repairServiceTemplates.name, repairServiceTemplates.itemCategory, repairServiceTemplates.size, repairServiceTemplates.description])
: undefined
const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere
const sortableColumns: Record<string, Column> = {
name: repairServiceTemplates.name,
instrument_type: repairServiceTemplates.instrumentType,
item_category: repairServiceTemplates.itemCategory,
default_price: repairServiceTemplates.defaultPrice,
sort_order: repairServiceTemplates.sortOrder,
created_at: repairServiceTemplates.createdAt,

View File

@@ -5,7 +5,7 @@ import { userRoles } from '../db/schema/rbac.js'
import type { StorageProvider } from '../storage/index.js'
import { randomUUID } from 'crypto'
import { withPagination, withSort, buildSearchCondition, paginatedResponse } from '../utils/pagination.js'
import type { PaginationInput } from '@forte/shared/schemas'
import type { PaginationInput } from '@lunarfront/shared/schemas'
const MAX_PARENT_DEPTH = 50

View File

@@ -3,7 +3,7 @@ import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
import { vaultConfig, vaultCategories, vaultCategoryPermissions, vaultEntries } from '../db/schema/vault.js'
import { userRoles } from '../db/schema/rbac.js'
import { withPagination, withSort, buildSearchCondition, paginatedResponse } from '../utils/pagination.js'
import type { PaginationInput } from '@forte/shared/schemas'
import type { PaginationInput } from '@lunarfront/shared/schemas'
import { randomBytes, createCipheriv, createDecipheriv, pbkdf2Sync } from 'crypto'
import bcrypt from 'bcrypt'

View File

@@ -1,6 +1,6 @@
import { sql, asc, desc, ilike, or, type SQL, type Column } from 'drizzle-orm'
import type { PgSelect } from 'drizzle-orm/pg-core'
import type { PaginationInput, PaginatedResponse } from '@forte/shared/schemas'
import type { PaginationInput, PaginatedResponse } from '@lunarfront/shared/schemas'
/**
* Apply pagination (offset + limit) to a Drizzle query.