feat(classes): optional required-form gate at signup (waiver-on-file)

Pure signupFormEligibility (manager override bypasses) + 4 tests.
signUpForClass now: classes.manage override + sign-up-on-behalf
(targetMemberId), and a hard form gate -- if the class has a
required_form_id the member must have a form_submissions row on file
(checked via service client, since a class manager need not hold
forms.manage). create/updateClass validate the form is in-space and
published. listUpcomingSessions exposes required_form {title,url,
satisfied}. Manage UI: published-form picker on create/edit + a 'requires
form' badge. Member UI: shows the requirement, links to the form, and
replaces Sign up with 'Complete required form' until it is on file.
Mirrors the equipment cert-gate pattern.
This commit is contained in:
Moheeb Zara
2026-05-16 21:41:01 -07:00
parent 92c5737a25
commit f3b05afe14
7 changed files with 259 additions and 16 deletions
+18
View File
@@ -4,6 +4,7 @@ import {
computeSignupStatus,
sessionTiming,
canSignUp,
signupFormEligibility,
pickPromotion,
} from '@/lib/classes-logic'
@@ -72,3 +73,20 @@ describe('pickPromotion', () => {
expect(pickPromotion([{ id: 'r1', status: 'registered', signed_up_at: 'x' }], 5)).toBeNull()
})
})
describe('signupFormEligibility', () => {
it('passes when no form is required', () => {
expect(signupFormEligibility({ requiresForm: false, memberHasForm: false, managerOverride: false }).ok).toBe(true)
})
it('passes when the form is on file', () => {
expect(signupFormEligibility({ requiresForm: true, memberHasForm: true, managerOverride: false }).ok).toBe(true)
})
it('blocks when required and not on file', () => {
const r = signupFormEligibility({ requiresForm: true, memberHasForm: false, managerOverride: false })
expect(r.ok).toBe(false)
if (!r.ok) expect(r.reason).toMatch(/requires a form/i)
})
it('a manager override bypasses the form requirement', () => {
expect(signupFormEligibility({ requiresForm: true, memberHasForm: false, managerOverride: true }).ok).toBe(true)
})
})