11package event
22
3+ import (
4+ "fmt"
5+ "strings"
6+ )
7+
8+ const (
9+ StatusDraft = "draft"
10+ StatusPublished = "published"
11+ StatusClosed = "closed"
12+ )
13+
14+ const (
15+ VisibilityPublic = "public"
16+ VisibilityPrivate = "private"
17+ )
18+
19+ const (
20+ RoleMember = "member"
21+ RoleOfficer = "officer"
22+ RoleAdmin = "admin"
23+ )
24+
325type AnswerDetails struct {
426 MaxChars int `bson:"max_chars,omitempty" json:"max_chars,omitempty"`
527}
@@ -29,6 +51,8 @@ type Event struct {
2951 MaxAttendees int `bson:"max_attendees" json:"max_attendees"`
3052 CreatedAt string `bson:"created_at" json:"created_at"`
3153 Status string `bson:"status" json:"status"` // draft, published, closed
54+ Visibility string `bson:"visibility" json:"visibility"` // public, private
55+ MinimumVisibleRole string `bson:"minimum_visible_role,omitempty" json:"minimum_visible_role,omitempty"`
3256}
3357
3458// IsAdmin checks if the given userID is in the event's Admins list.
@@ -41,6 +65,210 @@ func (e *Event) IsAdmin(userID string) bool {
4165 return false
4266}
4367
68+ // ApplyDefaults sets safe defaults for newly created events.
69+ func (e * Event ) ApplyDefaults () {
70+ if strings .TrimSpace (e .Status ) == "" {
71+ e .Status = StatusDraft
72+ }
73+
74+ if strings .TrimSpace (e .Visibility ) == "" {
75+ e .Visibility = VisibilityPublic
76+ }
77+
78+ // public events should not carry a minimum visible role
79+ if e .Visibility == VisibilityPublic {
80+ e .MinimumVisibleRole = ""
81+ }
82+ }
83+
84+ // Validate validates the event shape for create/replace scenarios.
85+ func (e * Event ) Validate () error {
86+ if strings .TrimSpace (e .Name ) == "" {
87+ return fmt .Errorf ("name is required" )
88+ }
89+
90+ if strings .TrimSpace (e .Date ) == "" {
91+ return fmt .Errorf ("date is required" )
92+ }
93+
94+ if strings .TrimSpace (e .Time ) == "" {
95+ return fmt .Errorf ("time is required" )
96+ }
97+
98+ if strings .TrimSpace (e .Location ) == "" {
99+ return fmt .Errorf ("location is required" )
100+ }
101+
102+ if err := ValidateStatus (e .Status ); err != nil {
103+ return err
104+ }
105+
106+ if err := ValidateVisibility (e .Visibility ); err != nil {
107+ return err
108+ }
109+
110+ if err := ValidateMinimumVisibleRole (e .Visibility , e .MinimumVisibleRole ); err != nil {
111+ return err
112+ }
113+
114+ if e .MaxAttendees < 0 {
115+ return fmt .Errorf ("max_attendees cannot be negative" )
116+ }
117+
118+ return nil
119+ }
120+
121+ func ValidateStatus (status string ) error {
122+ switch status {
123+ case StatusDraft , StatusPublished , StatusClosed :
124+ return nil
125+ default :
126+ return fmt .Errorf ("invalid status: must be one of draft, published, closed" )
127+ }
128+ }
129+
130+ func ValidateVisibility (visibility string ) error {
131+ switch visibility {
132+ case VisibilityPublic , VisibilityPrivate :
133+ return nil
134+ default :
135+ return fmt .Errorf ("invalid visibility: must be one of public, private" )
136+ }
137+ }
138+
139+ func ValidateRole (role string ) error {
140+ switch role {
141+ case RoleMember , RoleOfficer , RoleAdmin :
142+ return nil
143+ default :
144+ return fmt .Errorf ("invalid minimum_visible_role: must be one of member, officer, admin" )
145+ }
146+ }
147+
148+ func ValidateMinimumVisibleRole (visibility string , role string ) error {
149+ switch visibility {
150+ case VisibilityPublic :
151+ if strings .TrimSpace (role ) != "" {
152+ return fmt .Errorf ("minimum_visible_role must be empty when visibility is public" )
153+ }
154+ return nil
155+
156+ case VisibilityPrivate :
157+ if strings .TrimSpace (role ) == "" {
158+ return fmt .Errorf ("minimum_visible_role is required when visibility is private" )
159+ }
160+ return ValidateRole (role )
161+
162+ default :
163+ return fmt .Errorf ("invalid visibility: must be one of public, private" )
164+ }
165+ }
166+
167+ // ValidateUpdateFields validates only the fields present in a PATCH payload.
168+ func ValidateUpdateFields (fields map [string ]interface {}) error {
169+ var (
170+ statusProvided bool
171+ visibilityProvided bool
172+ roleProvided bool
173+
174+ status string
175+ visibility string
176+ role string
177+ )
178+
179+ for key , value := range fields {
180+ switch key {
181+ case "status" :
182+ s , ok := value .(string )
183+ if ! ok {
184+ return fmt .Errorf ("status must be a string" )
185+ }
186+ statusProvided = true
187+ status = strings .TrimSpace (s )
188+ if err := ValidateStatus (status ); err != nil {
189+ return err
190+ }
191+
192+ case "visibility" :
193+ s , ok := value .(string )
194+ if ! ok {
195+ return fmt .Errorf ("visibility must be a string" )
196+ }
197+ visibilityProvided = true
198+ visibility = strings .TrimSpace (s )
199+ if err := ValidateVisibility (visibility ); err != nil {
200+ return err
201+ }
202+
203+ case "minimum_visible_role" :
204+ s , ok := value .(string )
205+ if ! ok {
206+ return fmt .Errorf ("minimum_visible_role must be a string" )
207+ }
208+ roleProvided = true
209+ role = strings .TrimSpace (s )
210+
211+ case "max_attendees" :
212+ // JSON numbers come in as float64 when bound into map[string]interface{}
213+ n , ok := value .(float64 )
214+ if ! ok {
215+ return fmt .Errorf ("max_attendees must be a number" )
216+ }
217+ if n < 0 {
218+ return fmt .Errorf ("max_attendees cannot be negative" )
219+ }
220+
221+ case "name" , "date" , "time" , "location" , "description" :
222+ s , ok := value .(string )
223+ if ! ok {
224+ return fmt .Errorf ("%s must be a string" , key )
225+ }
226+ if key != "description" && strings .TrimSpace (s ) == "" {
227+ return fmt .Errorf ("%s cannot be empty" , key )
228+ }
229+ }
230+ }
231+
232+ // Cross-field validation only when one of these fields is being updated.
233+ if visibilityProvided || roleProvided {
234+ if ! visibilityProvided {
235+ vRaw , ok := fields ["visibility" ]
236+ if ok {
237+ v , ok := vRaw .(string )
238+ if ! ok {
239+ return fmt .Errorf ("visibility must be a string" )
240+ }
241+ visibility = strings .TrimSpace (v )
242+ }
243+ }
244+
245+ if ! roleProvided {
246+ rRaw , ok := fields ["minimum_visible_role" ]
247+ if ok {
248+ r , ok := rRaw .(string )
249+ if ! ok {
250+ return fmt .Errorf ("minimum_visible_role must be a string" )
251+ }
252+ role = strings .TrimSpace (r )
253+ }
254+ }
255+
256+ // We only fully validate the relationship if visibility is present in the PATCH.
257+ // This avoids breaking partial updates where only unrelated fields are sent.
258+ if visibilityProvided {
259+ if err := ValidateMinimumVisibleRole (visibility , role ); err != nil {
260+ return err
261+ }
262+ }
263+ }
264+
265+ if statusProvided && status == "" {
266+ return fmt .Errorf ("status cannot be empty" )
267+ }
268+
269+ return nil
270+ }
271+
44272// SanitizeUpdateFields removes immutable fields from an update map
45273// to prevent them from being overwritten.
46274func SanitizeUpdateFields (fields map [string ]interface {}) {
0 commit comments