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") } }