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:
@@ -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)}`)
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user