feat(certifications): pure expiry/status logic + Zod schemas

certifications-logic.ts: computeExpiry (month math with end-of-month
clamping and year rollover), isCertificationActive, certificationStatus
(revoked > expired > expiring_soon > active). Dependency-free and
unit-tested (16 cases incl. Jan-31 and leap-year boundaries).

validations.ts: create/update/grant/revoke/renew + id/list schemas.
grant's expires_at is an optional override (empty -> null); validity is
a positive integer count of months.
This commit is contained in:
Moheeb Zara
2026-05-16 18:52:53 -07:00
parent e439b1130f
commit ddf6717d1b
3 changed files with 232 additions and 0 deletions
+63
View File
@@ -678,6 +678,69 @@ export const getPublicFormSchema = z.object({
slug: formSlug,
})
// ─── Certifications ──────────────────────────────────────────────────────────
const certificationName = z
.string()
.min(1, 'Name is required')
.max(200, 'Name must be 200 characters or fewer')
// validity_months: null/omitted = never expires. When set it must be a
// positive integer (months added to grant time to compute expiry).
const validityMonths = z
.number()
.int('Validity must be a whole number of months')
.positive('Validity must be at least 1 month')
.max(1200, 'Validity is unreasonably large')
.optional()
.nullable()
export const createCertificationSchema = z.object({
name: certificationName,
description: z.string().max(2000).optional().nullable(),
validity_months: validityMonths,
})
export const updateCertificationSchema = z.object({
certificationId: z.string().uuid('Invalid certification ID'),
name: certificationName.optional(),
description: z.string().max(2000).optional().nullable(),
validity_months: validityMonths,
is_active: z.boolean().optional(),
})
export const certificationIdSchema = z.object({
certificationId: z.string().uuid('Invalid certification ID'),
})
// Award a cert to a member. expires_at is optional: when omitted the action
// computes it from the cert's validity_months; when provided it overrides
// (e.g. an externally dated certificate). Empty string -> null (no expiry).
export const grantCertificationSchema = z.object({
memberId: z.string().uuid('Invalid member ID'),
certificationId: z.string().uuid('Invalid certification ID'),
note: z.string().max(2000).optional().nullable(),
expires_at: z
.string()
.optional()
.nullable()
.transform(v => (v && v.trim() !== '' ? v : null)),
})
export const revokeCertificationSchema = z.object({
memberCertificationId: z.string().uuid('Invalid grant ID'),
reason: z.string().max(500).optional().nullable(),
})
export const renewCertificationSchema = z.object({
memberCertificationId: z.string().uuid('Invalid grant ID'),
note: z.string().max(2000).optional().nullable(),
})
export const listMemberCertificationsSchema = z.object({
memberId: z.string().uuid('Invalid member ID'),
})
// ─── Generic ID schemas ──────────────────────────────────────────────────────
export const uuidSchema = z.string().uuid('Invalid ID format')