487 lines
14 KiB
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")
|
|
}
|
|
}
|