Add lessons module, rate cycles, EC2 deploy scripts, and help content
- Lessons module: lesson types, instructors, schedule slots, enrollments, sessions (list + week grid view), lesson plans, grading scales, templates - Rate cycles: replace monthly_rate with billing_interval + billing_unit on enrollments; add weekly/monthly/quarterly rate presets to lesson types and schedule slots with auto-fill on enrollment form - Member detail page: tabbed layout for details, identity documents, enrollments - Sessions week view: custom 7-column grid replacing react-big-calendar - Music store seed: instructors, lesson types, slots, enrollments, sessions, grading scale, lesson plan template - Scrollbar styling: themed to match sidebar/app palette - deploy/: EC2 setup and redeploy scripts, nginx config, systemd service - Help: add Lessons category (overview, types, instructors, slots, enrollments, sessions, plans/grading); collapsible sidebar with independent scroll; remove POS/accounting references from docs
This commit is contained in:
@@ -112,14 +112,14 @@ suite('Lessons', { tags: ['lessons'] }, (t) => {
|
||||
instrument: 'Piano',
|
||||
durationMinutes: 30,
|
||||
lessonFormat: 'private',
|
||||
baseRateMonthly: 120,
|
||||
rateMonthly: 120,
|
||||
})
|
||||
t.assert.status(res, 201)
|
||||
t.assert.equal(res.data.name, '30-min Private Piano')
|
||||
t.assert.equal(res.data.instrument, 'Piano')
|
||||
t.assert.equal(res.data.durationMinutes, 30)
|
||||
t.assert.equal(res.data.lessonFormat, 'private')
|
||||
t.assert.equal(res.data.baseRateMonthly, '120')
|
||||
t.assert.equal(res.data.rateMonthly, '120.00')
|
||||
t.assert.ok(res.data.id)
|
||||
})
|
||||
|
||||
@@ -129,12 +129,26 @@ suite('Lessons', { tags: ['lessons'] }, (t) => {
|
||||
instrument: 'Guitar',
|
||||
durationMinutes: 60,
|
||||
lessonFormat: 'group',
|
||||
baseRateMonthly: 80,
|
||||
rateMonthly: 80,
|
||||
})
|
||||
t.assert.status(res, 201)
|
||||
t.assert.equal(res.data.lessonFormat, 'group')
|
||||
})
|
||||
|
||||
t.test('creates a lesson type with all three rate presets', { tags: ['lesson-types', 'create'] }, async () => {
|
||||
const res = await t.api.post('/v1/lesson-types', {
|
||||
name: 'Multi-Rate Type',
|
||||
durationMinutes: 30,
|
||||
rateWeekly: 35,
|
||||
rateMonthly: 120,
|
||||
rateQuarterly: 330,
|
||||
})
|
||||
t.assert.status(res, 201)
|
||||
t.assert.equal(res.data.rateWeekly, '35.00')
|
||||
t.assert.equal(res.data.rateMonthly, '120.00')
|
||||
t.assert.equal(res.data.rateQuarterly, '330.00')
|
||||
})
|
||||
|
||||
t.test('rejects lesson type without required fields', { tags: ['lesson-types', 'create', 'validation'] }, async () => {
|
||||
const res = await t.api.post('/v1/lesson-types', {})
|
||||
t.assert.status(res, 400)
|
||||
@@ -162,12 +176,12 @@ suite('Lessons', { tags: ['lessons'] }, (t) => {
|
||||
const res = await t.api.patch(`/v1/lesson-types/${created.data.id}`, {
|
||||
name: 'After Update Type',
|
||||
durationMinutes: 45,
|
||||
baseRateMonthly: 150,
|
||||
rateMonthly: 150,
|
||||
})
|
||||
t.assert.status(res, 200)
|
||||
t.assert.equal(res.data.name, 'After Update Type')
|
||||
t.assert.equal(res.data.durationMinutes, 45)
|
||||
t.assert.equal(res.data.baseRateMonthly, '150')
|
||||
t.assert.equal(res.data.rateMonthly, '150.00')
|
||||
})
|
||||
|
||||
t.test('soft-deletes a lesson type', { tags: ['lesson-types', 'delete'] }, async () => {
|
||||
@@ -370,6 +384,45 @@ suite('Lessons', { tags: ['lessons'] }, (t) => {
|
||||
t.assert.equal(res.data.maxStudents, 3)
|
||||
})
|
||||
|
||||
t.test('creates a schedule slot with instructor rate overrides', { tags: ['schedule-slots', 'create'] }, async () => {
|
||||
const instructor = await t.api.post('/v1/instructors', { displayName: 'Rates Instructor' })
|
||||
const lessonType = await t.api.post('/v1/lesson-types', { name: 'Rates Slot Type', durationMinutes: 30, rateMonthly: 100 })
|
||||
|
||||
const res = await t.api.post('/v1/schedule-slots', {
|
||||
instructorId: instructor.data.id,
|
||||
lessonTypeId: lessonType.data.id,
|
||||
dayOfWeek: 1,
|
||||
startTime: '13:00',
|
||||
rateWeekly: 40,
|
||||
rateMonthly: 150,
|
||||
rateQuarterly: 400,
|
||||
})
|
||||
t.assert.status(res, 201)
|
||||
t.assert.equal(res.data.rateWeekly, '40.00')
|
||||
t.assert.equal(res.data.rateMonthly, '150.00')
|
||||
t.assert.equal(res.data.rateQuarterly, '400.00')
|
||||
})
|
||||
|
||||
t.test('updates schedule slot rates', { tags: ['schedule-slots', 'update'] }, async () => {
|
||||
const instructor = await t.api.post('/v1/instructors', { displayName: 'Update Rates Instructor' })
|
||||
const lessonType = await t.api.post('/v1/lesson-types', { name: 'Update Rates Type', durationMinutes: 30 })
|
||||
const created = await t.api.post('/v1/schedule-slots', {
|
||||
instructorId: instructor.data.id,
|
||||
lessonTypeId: lessonType.data.id,
|
||||
dayOfWeek: 3,
|
||||
startTime: '09:00',
|
||||
})
|
||||
|
||||
const res = await t.api.patch(`/v1/schedule-slots/${created.data.id}`, {
|
||||
rateWeekly: 35,
|
||||
rateMonthly: 120,
|
||||
})
|
||||
t.assert.status(res, 200)
|
||||
t.assert.equal(res.data.rateWeekly, '35.00')
|
||||
t.assert.equal(res.data.rateMonthly, '120.00')
|
||||
t.assert.equal(res.data.rateQuarterly, null)
|
||||
})
|
||||
|
||||
t.test('update detects conflict when changing time', { tags: ['schedule-slots', 'update', 'conflict'] }, async () => {
|
||||
const instructor = await t.api.post('/v1/instructors', { displayName: 'Update Conflict Instructor' })
|
||||
const lessonType = await t.api.post('/v1/lesson-types', { name: 'Update Conflict Type', durationMinutes: 30 })
|
||||
@@ -505,13 +558,17 @@ suite('Lessons', { tags: ['lessons'] }, (t) => {
|
||||
scheduleSlotId: slot.data.id,
|
||||
instructorId: instructor.data.id,
|
||||
startDate: '2026-01-15',
|
||||
monthlyRate: 120,
|
||||
rate: 120,
|
||||
billingInterval: 1,
|
||||
billingUnit: 'month',
|
||||
notes: 'Beginner piano student',
|
||||
})
|
||||
t.assert.status(res, 201)
|
||||
t.assert.equal(res.data.status, 'active')
|
||||
t.assert.equal(res.data.memberId, member.data.id)
|
||||
t.assert.equal(res.data.monthlyRate, '120.00')
|
||||
t.assert.equal(res.data.rate, '120.00')
|
||||
t.assert.equal(res.data.billingInterval, 1)
|
||||
t.assert.equal(res.data.billingUnit, 'month')
|
||||
t.assert.equal(res.data.startDate, '2026-01-15')
|
||||
t.assert.equal(res.data.makeupCredits, 0)
|
||||
})
|
||||
@@ -618,12 +675,16 @@ suite('Lessons', { tags: ['lessons'] }, (t) => {
|
||||
})
|
||||
|
||||
const res = await t.api.patch(`/v1/enrollments/${created.data.id}`, {
|
||||
monthlyRate: 150,
|
||||
rate: 150,
|
||||
billingInterval: 2,
|
||||
billingUnit: 'week',
|
||||
notes: 'Updated rate',
|
||||
endDate: '2026-06-30',
|
||||
})
|
||||
t.assert.status(res, 200)
|
||||
t.assert.equal(res.data.monthlyRate, '150.00')
|
||||
t.assert.equal(res.data.rate, '150.00')
|
||||
t.assert.equal(res.data.billingInterval, 2)
|
||||
t.assert.equal(res.data.billingUnit, 'week')
|
||||
t.assert.equal(res.data.notes, 'Updated rate')
|
||||
t.assert.equal(res.data.endDate, '2026-06-30')
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user