learn-platform-deploy/Backend/cmd/api/live_courses_payloads.go

487 lines
14 KiB
Go

package main
import (
"errors"
"strings"
"time"
)
func prepareLiveCoursePayload(payload Doc, partial bool) (Doc, error) {
if payload == nil {
payload = Doc{}
}
title, hasTitle, err := getOptionalString(payload, "title")
if err != nil {
return nil, errors.New("title must be a string")
}
if !partial && (!hasTitle || title == "") {
return nil, errors.New("title is required")
}
if hasTitle {
payload["title"] = title
}
slug, hasSlug, err := getOptionalString(payload, "slug")
if err != nil {
return nil, errors.New("slug must be a string")
}
if !hasSlug && !partial && hasTitle {
slug = slugify(title)
hasSlug = true
}
if hasSlug {
payload["slug"] = normalizeSlug(slug)
}
status, hasStatus, err := getOptionalString(payload, "status")
if err != nil {
return nil, errors.New("status must be a string")
}
if !partial && !hasStatus {
status = liveCourseStatusDraft
hasStatus = true
}
if hasStatus {
normalizedStatus := strings.ToLower(strings.TrimSpace(status))
if normalizedStatus != liveCourseStatusDraft && normalizedStatus != liveCourseStatusScheduled && normalizedStatus != liveCourseStatusPublished && normalizedStatus != liveCourseStatusArchived {
return nil, errors.New("status must be one of: draft, scheduled, published, archived")
}
payload["status"] = normalizedStatus
}
if visibility, has, err := getOptionalString(payload, "visibility"); err != nil {
return nil, errors.New("visibility must be a string")
} else if has {
normalized := strings.ToLower(strings.TrimSpace(visibility))
switch normalized {
case "public", "authenticated", "private", "organisation":
payload["visibility"] = normalized
default:
return nil, errors.New("visibility must be one of: public, authenticated, private, organisation")
}
}
if mode, has, err := getOptionalString(payload, "deliveryMode"); err != nil {
return nil, errors.New("deliveryMode must be a string")
} else if has {
normalized := strings.ToLower(strings.TrimSpace(mode))
switch normalized {
case "online", "in_person", "hybrid":
payload["deliveryMode"] = normalized
default:
return nil, errors.New("deliveryMode must be one of: online, in_person, hybrid")
}
}
if level, has, err := getOptionalString(payload, "level"); err != nil {
return nil, errors.New("level must be a string")
} else if has {
normalized := strings.ToLower(strings.TrimSpace(level))
switch normalized {
case "beginner", "intermediate", "advanced":
payload["level"] = normalized
default:
return nil, errors.New("level must be one of: beginner, intermediate, advanced")
}
}
if instructorIDs, has, err := normalizeStringSlice(payload["instructorIds"]); err != nil {
return nil, errors.New("instructorIds must be an array of strings")
} else if has {
payload["instructorIds"] = instructorIDs
}
if assetIDs, has, err := normalizeStringSlice(payload["assetIds"]); err != nil {
return nil, errors.New("assetIds must be an array of strings")
} else if has {
payload["assetIds"] = assetIDs
}
for _, field := range []string{"description", "durationSummary", "communityId", "entitlementKey"} {
value, has, err := getOptionalString(payload, field)
if err != nil {
return nil, errors.New(field + " must be a string")
}
if has {
payload[field] = value
}
}
return payload, nil
}
func prepareLiveCourseCohortPayload(payload Doc, partial bool) (Doc, error) {
if payload == nil {
payload = Doc{}
}
title, hasTitle, err := getOptionalString(payload, "title")
if err != nil {
return nil, errors.New("title must be a string")
}
if !partial && (!hasTitle || title == "") {
return nil, errors.New("title is required")
}
if hasTitle {
payload["title"] = title
}
status, hasStatus, err := getOptionalString(payload, "status")
if err != nil {
return nil, errors.New("status must be a string")
}
if !partial && !hasStatus {
status = "draft"
hasStatus = true
}
if hasStatus {
normalizedStatus := strings.ToLower(strings.TrimSpace(status))
switch normalizedStatus {
case "draft", "open", "closed", "in_progress", "completed", "cancelled":
payload["status"] = normalizedStatus
default:
return nil, errors.New("status must be one of: draft, open, closed, in_progress, completed, cancelled")
}
}
startsAt, hasStartsAt, err := parseOptionalTime(payload["startsAt"])
if err != nil {
return nil, errors.New("startsAt must be a unix milliseconds number or RFC3339 string")
}
if hasStartsAt {
payload["startsAt"] = startsAt.UnixMilli()
}
endsAt, hasEndsAt, err := parseOptionalTime(payload["endsAt"])
if err != nil {
return nil, errors.New("endsAt must be a unix milliseconds number or RFC3339 string")
}
if hasEndsAt {
payload["endsAt"] = endsAt.UnixMilli()
}
if hasStartsAt && hasEndsAt && endsAt.Before(startsAt) {
return nil, errors.New("endsAt must be greater than or equal to startsAt")
}
if timezone, has, err := getOptionalString(payload, "timezone"); err != nil {
return nil, errors.New("timezone must be a string")
} else if has {
if _, tzErr := time.LoadLocation(timezone); tzErr != nil {
return nil, errors.New("timezone must be a valid IANA timezone")
}
payload["timezone"] = timezone
}
if capacity, has, err := getOptionalInteger(payload, "capacity"); err != nil {
return nil, errors.New("capacity must be an integer")
} else if has {
if capacity < 0 {
return nil, errors.New("capacity must be greater than or equal to 0")
}
payload["capacity"] = capacity
}
if enrolledCount, has, err := getOptionalInteger(payload, "enrolledCount"); err != nil {
return nil, errors.New("enrolledCount must be an integer")
} else if has {
if enrolledCount < 0 {
return nil, errors.New("enrolledCount must be greater than or equal to 0")
}
payload["enrolledCount"] = enrolledCount
} else if !partial {
payload["enrolledCount"] = 0
}
if waitlistEnabled, has, err := getOptionalBool(payload, "waitlistEnabled"); err != nil {
return nil, errors.New("waitlistEnabled must be a boolean")
} else if has {
payload["waitlistEnabled"] = waitlistEnabled
}
for _, field := range []string{"priceId", "entitlementKey", "meetingProvider", "defaultMeetingUrl", "organisationId"} {
value, has, err := getOptionalString(payload, field)
if err != nil {
return nil, errors.New(field + " must be a string")
}
if has {
payload[field] = value
}
}
return payload, nil
}
func prepareLiveCourseSessionPayload(payload Doc, partial bool) (Doc, error) {
if payload == nil {
payload = Doc{}
}
title, hasTitle, err := getOptionalString(payload, "title")
if err != nil {
return nil, errors.New("title must be a string")
}
if !partial && (!hasTitle || title == "") {
return nil, errors.New("title is required")
}
if hasTitle {
payload["title"] = title
}
if description, has, err := getOptionalString(payload, "description"); err != nil {
return nil, errors.New("description must be a string")
} else if has {
payload["description"] = description
}
status, hasStatus, err := getOptionalString(payload, "status")
if err != nil {
return nil, errors.New("status must be a string")
}
if !partial && !hasStatus {
status = "scheduled"
hasStatus = true
}
if hasStatus {
normalizedStatus := strings.ToLower(strings.TrimSpace(status))
switch normalizedStatus {
case "scheduled", "live", "completed", "cancelled":
payload["status"] = normalizedStatus
default:
return nil, errors.New("status must be one of: scheduled, live, completed, cancelled")
}
}
startsAt, hasStartsAt, err := parseOptionalTime(payload["startsAt"])
if err != nil {
return nil, errors.New("startsAt must be a unix milliseconds number or RFC3339 string")
}
if hasStartsAt {
payload["startsAt"] = startsAt.UnixMilli()
}
endsAt, hasEndsAt, err := parseOptionalTime(payload["endsAt"])
if err != nil {
return nil, errors.New("endsAt must be a unix milliseconds number or RFC3339 string")
}
if hasEndsAt {
payload["endsAt"] = endsAt.UnixMilli()
}
if hasStartsAt && hasEndsAt && endsAt.Before(startsAt) {
return nil, errors.New("endsAt must be greater than or equal to startsAt")
}
if timezone, has, err := getOptionalString(payload, "timezone"); err != nil {
return nil, errors.New("timezone must be a string")
} else if has {
if _, tzErr := time.LoadLocation(timezone); tzErr != nil {
return nil, errors.New("timezone must be a valid IANA timezone")
}
payload["timezone"] = timezone
}
if instructorIDs, has, err := normalizeStringSlice(payload["instructorIds"]); err != nil {
return nil, errors.New("instructorIds must be an array of strings")
} else if has {
payload["instructorIds"] = instructorIDs
}
if assetIDs, has, err := normalizeStringSlice(payload["assetIds"]); err != nil {
return nil, errors.New("assetIds must be an array of strings")
} else if has {
payload["assetIds"] = assetIDs
}
for _, field := range []string{"meetingUrl", "meetingProvider", "recordingAssetId", "communityThreadId"} {
value, has, err := getOptionalString(payload, field)
if err != nil {
return nil, errors.New(field + " must be a string")
}
if has {
payload[field] = value
}
}
return payload, nil
}
func prepareLiveCourseEnrollmentPayload(payload Doc, partial bool) (Doc, error) {
if payload == nil {
payload = Doc{}
}
userID, hasUserID, err := getOptionalString(payload, "userId")
if err != nil {
return nil, errors.New("userId must be a string")
}
if !partial && (!hasUserID || userID == "") {
return nil, errors.New("userId is required")
}
if hasUserID {
payload["userId"] = userID
}
status, hasStatus, err := getOptionalString(payload, "status")
if err != nil {
return nil, errors.New("status must be a string")
}
if !partial && !hasStatus {
status = "enrolled"
hasStatus = true
}
if hasStatus {
normalizedStatus := strings.ToLower(strings.TrimSpace(status))
switch normalizedStatus {
case "invited", "enrolled", "waitlisted", "cancelled", "completed", "removed":
payload["status"] = normalizedStatus
default:
return nil, errors.New("status must be one of: invited, enrolled, waitlisted, cancelled, completed, removed")
}
}
source, hasSource, err := getOptionalString(payload, "source")
if err != nil {
return nil, errors.New("source must be a string")
}
if !partial && !hasSource {
source = "manual"
hasSource = true
}
if hasSource {
normalizedSource := strings.ToLower(strings.TrimSpace(source))
switch normalizedSource {
case "manual", "stripe", "subscription", "organisation", "invite":
payload["source"] = normalizedSource
default:
return nil, errors.New("source must be one of: manual, stripe, subscription, organisation, invite")
}
}
if entitlementKey, has, err := getOptionalString(payload, "entitlementKey"); err != nil {
return nil, errors.New("entitlementKey must be a string")
} else if has {
payload["entitlementKey"] = entitlementKey
}
enrolledAt, hasEnrolledAt, err := parseOptionalTime(payload["enrolledAt"])
if err != nil {
return nil, errors.New("enrolledAt must be a unix milliseconds number or RFC3339 string")
}
if hasEnrolledAt {
payload["enrolledAt"] = enrolledAt.UnixMilli()
}
completedAt, hasCompletedAt, err := parseOptionalTime(payload["completedAt"])
if err != nil {
return nil, errors.New("completedAt must be a unix milliseconds number or RFC3339 string")
}
if hasCompletedAt {
payload["completedAt"] = completedAt.UnixMilli()
}
if hasEnrolledAt && hasCompletedAt && completedAt.Before(enrolledAt) {
return nil, errors.New("completedAt must be greater than or equal to enrolledAt")
}
return payload, nil
}
func prepareLiveCourseAttendancePayload(payload Doc, partial bool) (Doc, error) {
if payload == nil {
payload = Doc{}
}
userID, hasUserID, err := getOptionalString(payload, "userId")
if err != nil {
return nil, errors.New("userId must be a string")
}
if !partial && (!hasUserID || userID == "") {
return nil, errors.New("userId is required")
}
if hasUserID {
payload["userId"] = userID
}
status, hasStatus, err := getOptionalString(payload, "status")
if err != nil {
return nil, errors.New("status must be a string")
}
if !partial && !hasStatus {
status = "present"
hasStatus = true
}
if hasStatus {
normalizedStatus := strings.ToLower(strings.TrimSpace(status))
switch normalizedStatus {
case "present", "late", "absent", "excused", "completed":
payload["status"] = normalizedStatus
default:
return nil, errors.New("status must be one of: present, late, absent, excused, completed")
}
}
joinedAt, hasJoinedAt, err := parseOptionalTime(payload["joinedAt"])
if err != nil {
return nil, errors.New("joinedAt must be a unix milliseconds number or RFC3339 string")
}
if hasJoinedAt {
payload["joinedAt"] = joinedAt.UnixMilli()
}
leftAt, hasLeftAt, err := parseOptionalTime(payload["leftAt"])
if err != nil {
return nil, errors.New("leftAt must be a unix milliseconds number or RFC3339 string")
}
if hasLeftAt {
payload["leftAt"] = leftAt.UnixMilli()
}
if hasJoinedAt && hasLeftAt {
if leftAt.Before(joinedAt) {
return nil, errors.New("leftAt must be greater than or equal to joinedAt")
}
payload["durationSeconds"] = int(leftAt.Sub(joinedAt).Seconds())
}
if durationSeconds, has, err := getOptionalInteger(payload, "durationSeconds"); err != nil {
return nil, errors.New("durationSeconds must be an integer")
} else if has {
if durationSeconds < 0 {
return nil, errors.New("durationSeconds must be greater than or equal to 0")
}
payload["durationSeconds"] = durationSeconds
}
if notes, has, err := getOptionalString(payload, "notes"); err != nil {
return nil, errors.New("notes must be a string")
} else if has {
payload["notes"] = notes
}
return payload, nil
}
func getOptionalInteger(payload map[string]any, field string) (int, bool, error) {
raw, ok := payload[field]
if !ok || raw == nil {
return 0, false, nil
}
switch value := raw.(type) {
case int:
return value, true, nil
case int32:
return int(value), true, nil
case int64:
return int(value), true, nil
case float64:
if value != float64(int(value)) {
return 0, false, errors.New("not an integer")
}
return int(value), true, nil
default:
return 0, false, errors.New("not an integer")
}
}