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
514 lines
26 KiB
TypeScript
514 lines
26 KiB
TypeScript
import { suite } from '../lib/context.js'
|
|
|
|
suite('Repairs', { tags: ['repairs'] }, (t) => {
|
|
// ─── Repair Tickets: CRUD ───
|
|
|
|
t.test('creates a repair ticket (walk-in) with default status new', { tags: ['tickets', 'create'] }, async () => {
|
|
const res = await t.api.post('/v1/repair-tickets', {
|
|
customerName: 'Walk-In Customer',
|
|
customerPhone: '555-0100',
|
|
itemDescription: 'Samsung Galaxy S24',
|
|
problemDescription: 'Cracked screen, touch not working',
|
|
conditionIn: 'fair',
|
|
})
|
|
t.assert.status(res, 201)
|
|
t.assert.equal(res.data.customerName, 'Walk-In Customer')
|
|
t.assert.equal(res.data.status, 'new')
|
|
t.assert.ok(res.data.ticketNumber)
|
|
t.assert.equal(res.data.ticketNumber.length, 6)
|
|
t.assert.ok(res.data.id)
|
|
t.assert.ok(res.data.intakeDate)
|
|
})
|
|
|
|
t.test('creates a repair ticket with account link', { tags: ['tickets', 'create'] }, async () => {
|
|
const acct = await t.api.post('/v1/accounts', { name: 'Repair Customer', billingMode: 'consolidated' })
|
|
const res = await t.api.post('/v1/repair-tickets', {
|
|
customerName: 'Repair Customer',
|
|
accountId: acct.data.id,
|
|
problemDescription: 'Screen flickering intermittently',
|
|
itemDescription: 'Dell XPS 15 Laptop',
|
|
conditionIn: 'poor',
|
|
})
|
|
t.assert.status(res, 201)
|
|
t.assert.equal(res.data.accountId, acct.data.id)
|
|
})
|
|
|
|
t.test('rejects ticket creation without required fields', { tags: ['tickets', 'create', 'validation'] }, async () => {
|
|
const res = await t.api.post('/v1/repair-tickets', {})
|
|
t.assert.status(res, 400)
|
|
})
|
|
|
|
t.test('rejects ticket without problem description', { tags: ['tickets', 'create', 'validation'] }, async () => {
|
|
const res = await t.api.post('/v1/repair-tickets', { customerName: 'Test' })
|
|
t.assert.status(res, 400)
|
|
})
|
|
|
|
t.test('gets repair ticket by id', { tags: ['tickets', 'read'] }, async () => {
|
|
const created = await t.api.post('/v1/repair-tickets', { customerName: 'Get By ID', problemDescription: 'Test' })
|
|
const res = await t.api.get(`/v1/repair-tickets/${created.data.id}`)
|
|
t.assert.status(res, 200)
|
|
t.assert.equal(res.data.customerName, 'Get By ID')
|
|
})
|
|
|
|
t.test('returns 404 for missing repair ticket', { tags: ['tickets', 'read'] }, async () => {
|
|
const res = await t.api.get('/v1/repair-tickets/a0000000-0000-0000-0000-999999999999')
|
|
t.assert.status(res, 404)
|
|
})
|
|
|
|
t.test('updates a repair ticket', { tags: ['tickets', 'update'] }, async () => {
|
|
const created = await t.api.post('/v1/repair-tickets', { customerName: 'Before Update', problemDescription: 'Old issue' })
|
|
const res = await t.api.patch(`/v1/repair-tickets/${created.data.id}`, {
|
|
customerName: 'After Update',
|
|
estimatedCost: 150,
|
|
technicianNotes: 'Needs full overhaul',
|
|
})
|
|
t.assert.status(res, 200)
|
|
t.assert.equal(res.data.customerName, 'After Update')
|
|
t.assert.equal(res.data.estimatedCost, '150.00')
|
|
t.assert.equal(res.data.technicianNotes, 'Needs full overhaul')
|
|
})
|
|
|
|
t.test('soft-deletes (cancels) a repair ticket', { tags: ['tickets', 'delete'] }, async () => {
|
|
const created = await t.api.post('/v1/repair-tickets', { customerName: 'To Cancel', problemDescription: 'Cancel me' })
|
|
const res = await t.api.del(`/v1/repair-tickets/${created.data.id}`)
|
|
t.assert.status(res, 200)
|
|
t.assert.equal(res.data.status, 'cancelled')
|
|
})
|
|
|
|
// ─── Repair Tickets: Status Flow ───
|
|
|
|
t.test('full status lifecycle: new → intake → diagnosing → pending_approval → approved → in_progress → ready → picked_up', { tags: ['tickets', 'status'] }, async () => {
|
|
const created = await t.api.post('/v1/repair-tickets', { customerName: 'Full Flow', problemDescription: 'Lifecycle test' })
|
|
t.assert.equal(created.data.status, 'new')
|
|
|
|
const intake = await t.api.post(`/v1/repair-tickets/${created.data.id}/status`, { status: 'intake' })
|
|
t.assert.equal(intake.data.status, 'intake')
|
|
|
|
const diag = await t.api.post(`/v1/repair-tickets/${created.data.id}/status`, { status: 'diagnosing' })
|
|
t.assert.equal(diag.data.status, 'diagnosing')
|
|
|
|
const pending = await t.api.post(`/v1/repair-tickets/${created.data.id}/status`, { status: 'pending_approval' })
|
|
t.assert.equal(pending.data.status, 'pending_approval')
|
|
|
|
const approved = await t.api.post(`/v1/repair-tickets/${created.data.id}/status`, { status: 'approved' })
|
|
t.assert.equal(approved.data.status, 'approved')
|
|
|
|
const prog = await t.api.post(`/v1/repair-tickets/${created.data.id}/status`, { status: 'in_progress' })
|
|
t.assert.equal(prog.data.status, 'in_progress')
|
|
|
|
const ready = await t.api.post(`/v1/repair-tickets/${created.data.id}/status`, { status: 'ready' })
|
|
t.assert.equal(ready.data.status, 'ready')
|
|
t.assert.ok(ready.data.completedDate, 'should set completedDate on ready')
|
|
|
|
const picked = await t.api.post(`/v1/repair-tickets/${created.data.id}/status`, { status: 'picked_up' })
|
|
t.assert.equal(picked.data.status, 'picked_up')
|
|
})
|
|
|
|
t.test('in_transit branch: new → in_transit → intake', { tags: ['tickets', 'status'] }, async () => {
|
|
const created = await t.api.post('/v1/repair-tickets', { customerName: 'Transit Test', problemDescription: 'School pickup' })
|
|
t.assert.equal(created.data.status, 'new')
|
|
|
|
const transit = await t.api.post(`/v1/repair-tickets/${created.data.id}/status`, { status: 'in_transit' })
|
|
t.assert.equal(transit.data.status, 'in_transit')
|
|
|
|
const intake = await t.api.post(`/v1/repair-tickets/${created.data.id}/status`, { status: 'intake' })
|
|
t.assert.equal(intake.data.status, 'intake')
|
|
})
|
|
|
|
t.test('pending_parts branch: in_progress → pending_parts → in_progress', { tags: ['tickets', 'status'] }, async () => {
|
|
const created = await t.api.post('/v1/repair-tickets', { customerName: 'Parts Test', problemDescription: 'Needs parts' })
|
|
await t.api.post(`/v1/repair-tickets/${created.data.id}/status`, { status: 'intake' })
|
|
await t.api.post(`/v1/repair-tickets/${created.data.id}/status`, { status: 'in_progress' })
|
|
|
|
const pending = await t.api.post(`/v1/repair-tickets/${created.data.id}/status`, { status: 'pending_parts' })
|
|
t.assert.equal(pending.data.status, 'pending_parts')
|
|
|
|
const back = await t.api.post(`/v1/repair-tickets/${created.data.id}/status`, { status: 'in_progress' })
|
|
t.assert.equal(back.data.status, 'in_progress')
|
|
})
|
|
|
|
t.test('delivered as alternate ending', { tags: ['tickets', 'status'] }, async () => {
|
|
const created = await t.api.post('/v1/repair-tickets', { customerName: 'Delivery Test', problemDescription: 'School batch' })
|
|
await t.api.post(`/v1/repair-tickets/${created.data.id}/status`, { status: 'ready' })
|
|
|
|
const delivered = await t.api.post(`/v1/repair-tickets/${created.data.id}/status`, { status: 'delivered' })
|
|
t.assert.equal(delivered.data.status, 'delivered')
|
|
t.assert.ok(delivered.data.completedDate)
|
|
})
|
|
|
|
t.test('reopen cancelled ticket back to new', { tags: ['tickets', 'status'] }, async () => {
|
|
const created = await t.api.post('/v1/repair-tickets', { customerName: 'Reopen Test', problemDescription: 'Cancel then reopen' })
|
|
await t.api.post(`/v1/repair-tickets/${created.data.id}/status`, { status: 'cancelled' })
|
|
|
|
const reopened = await t.api.post(`/v1/repair-tickets/${created.data.id}/status`, { status: 'new' })
|
|
t.assert.equal(reopened.data.status, 'new')
|
|
})
|
|
|
|
// ─── Repair Tickets: List, Search, Sort, Filter ───
|
|
|
|
t.test('lists repair tickets with pagination', { tags: ['tickets', 'read', 'pagination'] }, async () => {
|
|
await t.api.post('/v1/repair-tickets', { customerName: 'Page Test A', problemDescription: 'A' })
|
|
await t.api.post('/v1/repair-tickets', { customerName: 'Page Test B', problemDescription: 'B' })
|
|
|
|
const res = await t.api.get('/v1/repair-tickets', { limit: 100 })
|
|
t.assert.status(res, 200)
|
|
t.assert.ok(res.data.data.length >= 2)
|
|
t.assert.ok(res.data.pagination.total >= 2)
|
|
})
|
|
|
|
t.test('searches tickets by customer name', { tags: ['tickets', 'search'] }, async () => {
|
|
await t.api.post('/v1/repair-tickets', { customerName: 'Searchable Trumpet Guy', problemDescription: 'Dent' })
|
|
|
|
const res = await t.api.get('/v1/repair-tickets', { q: 'Trumpet Guy' })
|
|
t.assert.status(res, 200)
|
|
t.assert.ok(res.data.data.some((t: { customerName: string }) => t.customerName === 'Searchable Trumpet Guy'))
|
|
})
|
|
|
|
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: 'Galaxy S24' })
|
|
t.assert.status(res, 200)
|
|
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 () => {
|
|
await t.api.post('/v1/repair-tickets', { customerName: 'AAA Sort First', problemDescription: 'Sort' })
|
|
await t.api.post('/v1/repair-tickets', { customerName: 'ZZZ Sort Last', problemDescription: 'Sort' })
|
|
|
|
const res = await t.api.get('/v1/repair-tickets', { sort: 'customer_name', order: 'desc', limit: 100 })
|
|
t.assert.status(res, 200)
|
|
const names = res.data.data.map((t: { customerName: string }) => t.customerName)
|
|
const zIdx = names.findIndex((n: string) => n.includes('ZZZ'))
|
|
const aIdx = names.findIndex((n: string) => n.includes('AAA'))
|
|
t.assert.ok(zIdx < aIdx, 'ZZZ should come before AAA in desc order')
|
|
})
|
|
|
|
t.test('filters tickets by status', { tags: ['tickets', 'filter'] }, async () => {
|
|
const ticket = await t.api.post('/v1/repair-tickets', { customerName: 'Filter Status', problemDescription: 'Test' })
|
|
await t.api.post(`/v1/repair-tickets/${ticket.data.id}/status`, { status: 'intake' })
|
|
|
|
const res = await t.api.get('/v1/repair-tickets', { status: 'intake', limit: 100 })
|
|
t.assert.status(res, 200)
|
|
t.assert.ok(res.data.data.every((t: { status: string }) => t.status === 'intake'))
|
|
})
|
|
|
|
t.test('filters tickets by isBatch', { tags: ['tickets', 'filter'] }, async () => {
|
|
const acct = await t.api.post('/v1/accounts', { name: 'Filter Batch School', billingMode: 'consolidated' })
|
|
const batch = await t.api.post('/v1/repair-batches', { accountId: acct.data.id })
|
|
await t.api.post('/v1/repair-tickets', { customerName: 'Batch Ticket', repairBatchId: batch.data.id, problemDescription: 'In batch' })
|
|
|
|
const batchOnly = await t.api.get('/v1/repair-tickets', { isBatch: 'true', limit: 100 })
|
|
t.assert.status(batchOnly, 200)
|
|
t.assert.ok(batchOnly.data.data.every((t: { repairBatchId: string | null }) => t.repairBatchId !== null))
|
|
|
|
const individualOnly = await t.api.get('/v1/repair-tickets', { isBatch: 'false', limit: 100 })
|
|
t.assert.status(individualOnly, 200)
|
|
t.assert.ok(individualOnly.data.data.every((t: { repairBatchId: string | null }) => t.repairBatchId === null))
|
|
})
|
|
|
|
// ─── Repair Line Items ───
|
|
|
|
t.test('creates line items (labor, part, flat_rate, misc)', { tags: ['line-items', 'create'] }, async () => {
|
|
const ticket = await t.api.post('/v1/repair-tickets', { customerName: 'Line Item Test', problemDescription: 'Needs work' })
|
|
|
|
const labor = await t.api.post(`/v1/repair-tickets/${ticket.data.id}/line-items`, {
|
|
itemType: 'labor', description: 'Mechanical overhaul', qty: 2.5, unitPrice: 65, totalPrice: 162.50,
|
|
})
|
|
t.assert.status(labor, 201)
|
|
t.assert.equal(labor.data.itemType, 'labor')
|
|
|
|
const part = await t.api.post(`/v1/repair-tickets/${ticket.data.id}/line-items`, {
|
|
itemType: 'part', description: 'Valve guide', qty: 3, unitPrice: 2.50, totalPrice: 7.50, cost: 1.25,
|
|
})
|
|
t.assert.status(part, 201)
|
|
t.assert.equal(part.data.cost, '1.25')
|
|
|
|
const flatRate = await t.api.post(`/v1/repair-tickets/${ticket.data.id}/line-items`, {
|
|
itemType: 'flat_rate', description: 'Bow Rehair', qty: 1, unitPrice: 50, totalPrice: 50,
|
|
})
|
|
t.assert.status(flatRate, 201)
|
|
|
|
const misc = await t.api.post(`/v1/repair-tickets/${ticket.data.id}/line-items`, {
|
|
itemType: 'misc', description: 'Expedite fee', qty: 1, unitPrice: 15, totalPrice: 15,
|
|
})
|
|
t.assert.status(misc, 201)
|
|
t.assert.equal(misc.data.itemType, 'misc')
|
|
})
|
|
|
|
t.test('lists line items for a ticket', { tags: ['line-items', 'read'] }, async () => {
|
|
const ticket = await t.api.post('/v1/repair-tickets', { customerName: 'List Items', problemDescription: 'Test' })
|
|
await t.api.post(`/v1/repair-tickets/${ticket.data.id}/line-items`, { itemType: 'labor', description: 'Work A', qty: 1, unitPrice: 50, totalPrice: 50 })
|
|
await t.api.post(`/v1/repair-tickets/${ticket.data.id}/line-items`, { itemType: 'part', description: 'Part B', qty: 2, unitPrice: 10, totalPrice: 20 })
|
|
|
|
const res = await t.api.get(`/v1/repair-tickets/${ticket.data.id}/line-items`, { limit: 100 })
|
|
t.assert.status(res, 200)
|
|
t.assert.equal(res.data.data.length, 2)
|
|
t.assert.ok(res.data.pagination)
|
|
})
|
|
|
|
t.test('updates a line item', { tags: ['line-items', 'update'] }, async () => {
|
|
const ticket = await t.api.post('/v1/repair-tickets', { customerName: 'Update Item', problemDescription: 'Test' })
|
|
const item = await t.api.post(`/v1/repair-tickets/${ticket.data.id}/line-items`, { itemType: 'labor', description: 'Old', qty: 1, unitPrice: 50, totalPrice: 50 })
|
|
|
|
const res = await t.api.patch(`/v1/repair-line-items/${item.data.id}`, { description: 'New', unitPrice: 75, totalPrice: 75 })
|
|
t.assert.status(res, 200)
|
|
t.assert.equal(res.data.description, 'New')
|
|
t.assert.equal(res.data.unitPrice, '75.00')
|
|
})
|
|
|
|
t.test('deletes a line item', { tags: ['line-items', 'delete'] }, async () => {
|
|
const ticket = await t.api.post('/v1/repair-tickets', { customerName: 'Delete Item', problemDescription: 'Test' })
|
|
const item = await t.api.post(`/v1/repair-tickets/${ticket.data.id}/line-items`, { itemType: 'misc', description: 'To delete', qty: 1, unitPrice: 10, totalPrice: 10 })
|
|
|
|
const res = await t.api.del(`/v1/repair-line-items/${item.data.id}`)
|
|
t.assert.status(res, 200)
|
|
|
|
const list = await t.api.get(`/v1/repair-tickets/${ticket.data.id}/line-items`, { limit: 100 })
|
|
t.assert.equal(list.data.data.length, 0)
|
|
})
|
|
|
|
// ─── Repair Notes ───
|
|
|
|
t.test('creates a note on a ticket', { tags: ['notes', 'create'] }, async () => {
|
|
const ticket = await t.api.post('/v1/repair-tickets', { customerName: 'Note Test', problemDescription: 'Test' })
|
|
|
|
const res = await t.api.post(`/v1/repair-tickets/${ticket.data.id}/notes`, {
|
|
content: 'Valve is corroded, needs replacement',
|
|
visibility: 'internal',
|
|
})
|
|
t.assert.status(res, 201)
|
|
t.assert.equal(res.data.content, 'Valve is corroded, needs replacement')
|
|
t.assert.equal(res.data.visibility, 'internal')
|
|
t.assert.ok(res.data.authorName)
|
|
t.assert.ok(res.data.authorId)
|
|
t.assert.equal(res.data.ticketStatus, 'new')
|
|
})
|
|
|
|
t.test('creates a customer-visible note', { tags: ['notes', 'create'] }, async () => {
|
|
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 item is ready for pickup',
|
|
visibility: 'customer',
|
|
})
|
|
t.assert.status(res, 201)
|
|
t.assert.equal(res.data.visibility, 'customer')
|
|
})
|
|
|
|
t.test('lists notes for a ticket with pagination', { tags: ['notes', 'read'] }, async () => {
|
|
const ticket = await t.api.post('/v1/repair-tickets', { customerName: 'List Notes', problemDescription: 'Test' })
|
|
await t.api.post(`/v1/repair-tickets/${ticket.data.id}/notes`, { content: 'First note' })
|
|
await t.api.post(`/v1/repair-tickets/${ticket.data.id}/notes`, { content: 'Second note' })
|
|
|
|
const res = await t.api.get(`/v1/repair-tickets/${ticket.data.id}/notes`, { limit: 100 })
|
|
t.assert.status(res, 200)
|
|
t.assert.equal(res.data.data.length, 2)
|
|
t.assert.ok(res.data.pagination)
|
|
t.assert.equal(res.data.pagination.total, 2)
|
|
t.assert.equal(res.data.data[0].content, 'First note')
|
|
t.assert.equal(res.data.data[1].content, 'Second note')
|
|
})
|
|
|
|
t.test('note captures ticket status at time of creation', { tags: ['notes', 'status'] }, async () => {
|
|
const ticket = await t.api.post('/v1/repair-tickets', { customerName: 'Status Note', problemDescription: 'Test' })
|
|
await t.api.post(`/v1/repair-tickets/${ticket.data.id}/status`, { status: 'in_progress' })
|
|
|
|
const note = await t.api.post(`/v1/repair-tickets/${ticket.data.id}/notes`, { content: 'Working on it' })
|
|
t.assert.equal(note.data.ticketStatus, 'in_progress')
|
|
})
|
|
|
|
t.test('deletes a note', { tags: ['notes', 'delete'] }, async () => {
|
|
const ticket = await t.api.post('/v1/repair-tickets', { customerName: 'Delete Note', problemDescription: 'Test' })
|
|
const note = await t.api.post(`/v1/repair-tickets/${ticket.data.id}/notes`, { content: 'To delete' })
|
|
|
|
const res = await t.api.del(`/v1/repair-notes/${note.data.id}`)
|
|
t.assert.status(res, 200)
|
|
|
|
const list = await t.api.get(`/v1/repair-tickets/${ticket.data.id}/notes`, { limit: 100 })
|
|
t.assert.equal(list.data.data.length, 0)
|
|
})
|
|
|
|
t.test('returns 404 when creating note for missing ticket', { tags: ['notes', 'validation'] }, async () => {
|
|
const res = await t.api.post('/v1/repair-tickets/a0000000-0000-0000-0000-999999999999/notes', { content: 'Test' })
|
|
t.assert.status(res, 404)
|
|
})
|
|
|
|
// ─── Repair Batches ───
|
|
|
|
t.test('creates a repair batch', { tags: ['batches', 'create'] }, async () => {
|
|
const acct = await t.api.post('/v1/accounts', { name: 'Lincoln High School', billingMode: 'consolidated' })
|
|
|
|
const res = await t.api.post('/v1/repair-batches', {
|
|
accountId: acct.data.id,
|
|
contactName: 'IT Director',
|
|
contactPhone: '555-0200',
|
|
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.itemCount, 15)
|
|
})
|
|
|
|
t.test('returns 404 for missing batch', { tags: ['batches', 'read'] }, async () => {
|
|
const res = await t.api.get('/v1/repair-batches/a0000000-0000-0000-0000-999999999999')
|
|
t.assert.status(res, 404)
|
|
})
|
|
|
|
t.test('lists repair batches with pagination', { tags: ['batches', 'read', 'pagination'] }, async () => {
|
|
const res = await t.api.get('/v1/repair-batches', { limit: 100 })
|
|
t.assert.status(res, 200)
|
|
t.assert.ok(res.data.data.length >= 1)
|
|
t.assert.ok(res.data.pagination)
|
|
})
|
|
|
|
t.test('searches batches by contact name', { tags: ['batches', 'search'] }, async () => {
|
|
const acct = await t.api.post('/v1/accounts', { name: 'Search Batch School', billingMode: 'consolidated' })
|
|
await t.api.post('/v1/repair-batches', { accountId: acct.data.id, contactName: 'Findable Director' })
|
|
|
|
const res = await t.api.get('/v1/repair-batches', { q: 'Findable', limit: 100 })
|
|
t.assert.status(res, 200)
|
|
t.assert.ok(res.data.data.some((b: { contactName: string }) => b.contactName === 'Findable Director'))
|
|
})
|
|
|
|
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, itemCount: 5 })
|
|
|
|
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, itemCount: 2 })
|
|
|
|
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)
|
|
t.assert.equal(tickets.data.data.length, 2)
|
|
t.assert.ok(tickets.data.pagination)
|
|
})
|
|
|
|
t.test('approves a batch', { tags: ['batches', 'approve'] }, async () => {
|
|
const acct = await t.api.post('/v1/accounts', { name: 'Approve Batch School', billingMode: 'consolidated' })
|
|
const batch = await t.api.post('/v1/repair-batches', { accountId: acct.data.id })
|
|
|
|
const res = await t.api.post(`/v1/repair-batches/${batch.data.id}/approve`, {})
|
|
t.assert.status(res, 200)
|
|
t.assert.equal(res.data.approvalStatus, 'approved')
|
|
t.assert.ok(res.data.approvedBy)
|
|
t.assert.ok(res.data.approvedAt)
|
|
})
|
|
|
|
t.test('rejects a batch', { tags: ['batches', 'reject'] }, async () => {
|
|
const acct = await t.api.post('/v1/accounts', { name: 'Reject Batch School', billingMode: 'consolidated' })
|
|
const batch = await t.api.post('/v1/repair-batches', { accountId: acct.data.id })
|
|
|
|
const res = await t.api.post(`/v1/repair-batches/${batch.data.id}/reject`, {})
|
|
t.assert.status(res, 200)
|
|
t.assert.equal(res.data.approvalStatus, 'rejected')
|
|
})
|
|
|
|
t.test('updates batch status with timestamps', { tags: ['batches', 'status'] }, async () => {
|
|
const acct = await t.api.post('/v1/accounts', { name: 'Status Batch School', billingMode: 'consolidated' })
|
|
const batch = await t.api.post('/v1/repair-batches', { accountId: acct.data.id })
|
|
|
|
const prog = await t.api.post(`/v1/repair-batches/${batch.data.id}/status`, { status: 'in_progress' })
|
|
t.assert.equal(prog.data.status, 'in_progress')
|
|
|
|
const done = await t.api.post(`/v1/repair-batches/${batch.data.id}/status`, { status: 'completed' })
|
|
t.assert.equal(done.data.status, 'completed')
|
|
t.assert.ok(done.data.completedDate)
|
|
|
|
const delivered = await t.api.post(`/v1/repair-batches/${batch.data.id}/status`, { status: 'delivered' })
|
|
t.assert.equal(delivered.data.status, 'delivered')
|
|
t.assert.ok(delivered.data.deliveredDate)
|
|
})
|
|
|
|
// ─── Service Templates ───
|
|
|
|
t.test('creates a service template', { tags: ['templates', 'create'] }, async () => {
|
|
const res = await t.api.post('/v1/repair-service-templates', {
|
|
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, '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: 'Battery Replacement', itemCategory: 'Electronics', defaultPrice: 25 })
|
|
|
|
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 === '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: '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.itemCategory, 'Bicycles')
|
|
})
|
|
|
|
t.test('soft-deletes a service template', { tags: ['templates', 'delete'] }, async () => {
|
|
const created = await t.api.post('/v1/repair-service-templates', { name: 'To Delete Template', defaultPrice: 10 })
|
|
const res = await t.api.del(`/v1/repair-service-templates/${created.data.id}`)
|
|
t.assert.status(res, 200)
|
|
t.assert.equal(res.data.isActive, false)
|
|
|
|
// Should not appear in list (list filters active only)
|
|
const list = await t.api.get('/v1/repair-service-templates', { q: 'To Delete Template', limit: 100 })
|
|
t.assert.equal(list.data.data.length, 0)
|
|
})
|
|
|
|
// ─── Signed URLs ───
|
|
|
|
t.test('generates a signed URL for a file', { tags: ['files', 'signed-url'] }, async () => {
|
|
// Upload a file first
|
|
const ticket = await t.api.post('/v1/repair-tickets', { customerName: 'Signed URL Test', problemDescription: 'Test' })
|
|
|
|
const formData = new FormData()
|
|
formData.append('entityType', 'repair_ticket')
|
|
formData.append('entityId', ticket.data.id)
|
|
formData.append('category', 'intake')
|
|
formData.append('file', new Blob(['test image data'], { type: 'image/jpeg' }), 'test.jpg')
|
|
|
|
const uploadRes = await fetch(`${t.baseUrl}/v1/files`, {
|
|
method: 'POST',
|
|
headers: { Authorization: `Bearer ${t.token}` },
|
|
body: formData,
|
|
})
|
|
t.assert.equal(uploadRes.status, 201)
|
|
const uploaded = await uploadRes.json() as { id: string }
|
|
|
|
// Get signed URL
|
|
const signedRes = await t.api.get(`/v1/files/signed-url/${uploaded.id}`)
|
|
t.assert.status(signedRes, 200)
|
|
t.assert.ok(signedRes.data.url)
|
|
t.assert.contains(signedRes.data.url, 'token=')
|
|
|
|
// Fetch via signed URL (no auth header)
|
|
const fileRes = await fetch(`${t.baseUrl}${signedRes.data.url}`)
|
|
t.assert.equal(fileRes.status, 200)
|
|
})
|
|
})
|