Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 51 additions & 21 deletions core/calendar.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,22 +39,53 @@ func InsertOOOEvents(ctx context.Context, jwtCfg jwt.Config, r ClockifyRequest,
return nil, fmt.Errorf("req=%s user=%s: bad period.end: %w", r.ID, r.UserEmail, err)
}

// Normalize to local dates
startLocal := startUTC.In(loc)
endLocal := endUTC.In(loc)
var eventStart, eventEnd time.Time

y1, m1, d1 := startLocal.Date()
y2, m2, d2 := endLocal.Date()
if r.TimeOffPeriod.HalfDay {
eventStart, eventEnd, err = handleHalfDay(
r.TimeOffPeriod.HalfDayHours,
loc,
)
if err != nil {
return nil, fmt.Errorf(
"req=%s user=%s: %w",
r.ID,
r.UserEmail,
err,
)
}
} else {
eventStart, eventEnd, err = handleAllDay(startUTC, endUTC, loc)
if err != nil {
return nil, fmt.Errorf(
"req=%s user=%s: %w",
r.ID,
r.UserEmail,
err,
)
}

}

// All-day local time window used for Events.List (TimeMin / TimeMax).
allDayStart := time.Date(y1, m1, d1, 0, 0, 0, 0, loc)
// Clockify is inclusive; GCal all-day is [start, end) exclusive.
// So cover the last OOO day by adding +1 local day to the end date.
allDayEndExclusive := time.Date(y2, m2, d2, 0, 0, 0, 0, loc).AddDate(0, 0, 1)
var calendarStart, calendarEnd *calendar.EventDateTime

// YYYY-MM-DD string format is used for the Insert event payload.
startDate := allDayStart.Format("2006-01-02")
endDate := allDayEndExclusive.Format("2006-01-02")
// Google Calendar uses RFC3339 DateTime values for timed events
// and YYYY-MM-DD Date values for all-day events.
if r.TimeOffPeriod.HalfDay {
calendarStart = &calendar.EventDateTime{
DateTime: eventStart.Format(time.RFC3339),
}
calendarEnd = &calendar.EventDateTime{
DateTime: eventEnd.Format(time.RFC3339),
}
} else {
calendarStart = &calendar.EventDateTime{
Date: eventStart.Format("2006-01-02"),
}
calendarEnd = &calendar.EventDateTime{
Date: eventEnd.Format("2006-01-02"),
}
}

cfg := jwtCfg
cfg.Subject = r.UserEmail
Expand All @@ -73,10 +104,9 @@ func InsertOOOEvents(ctx context.Context, jwtCfg jwt.Config, r ClockifyRequest,
ev := &calendar.Event{
Summary: summary,
Description: fmt.Sprintf("Clockify request: %s\nCreatedAt: %s", r.ID, r.CreatedAt),
Start: &calendar.EventDateTime{Date: startDate},
End: &calendar.EventDateTime{Date: endDate}, // exclusive
Start: calendarStart,
End: calendarEnd,
// Attaching the Clockify request ID as a private extended property.
// TODO: Before inserting, check for an existing event with this key and insert event/skip:
ExtendedProperties: &calendar.EventExtendedProperties{
Private: map[string]string{
"clockifyRequestId": r.ID,
Expand All @@ -87,8 +117,8 @@ func InsertOOOEvents(ctx context.Context, jwtCfg jwt.Config, r ClockifyRequest,
// Insert into calendars
for _, calID := range calendarIDs {
existing, err := findClockifyEvents(
ctx, srv, calID, r.ID,
allDayStart, allDayEndExclusive,
srv, calID, r.ID,
eventStart, eventEnd,
)
if err != nil {
log.Printf("lookup %s (user=%s cal=%s) failed: %v",
Expand All @@ -110,8 +140,8 @@ func InsertOOOEvents(ctx context.Context, jwtCfg jwt.Config, r ClockifyRequest,
r.UserEmail,
calID,
e.Id,
e.Start.Date,
e.End.Date,
calendarEventTimeValue(e.Start),
calendarEventTimeValue(e.End),
)
}

Expand All @@ -134,7 +164,7 @@ func InsertOOOEvents(ctx context.Context, jwtCfg jwt.Config, r ClockifyRequest,

log.Printf(
"Inserted OOO for req=%s user=%s cal=%s (%s → %s)\n",
r.ID, r.UserEmail, calID, startDate, endDate,
r.ID, r.UserEmail, calID, calendarEventTimeValue(calendarStart), calendarEventTimeValue(calendarEnd),
)
}

Expand Down
81 changes: 81 additions & 0 deletions core/calendar_utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package core

import (
"fmt"
"time"

"google.golang.org/api/calendar/v3"
)

func handleAllDay(startUTC time.Time,
endUTC time.Time,
loc *time.Location) (time.Time, time.Time, error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verify that loc is not null here. (It shouldn't be with the way it's currently called, but just for future-proofing.)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I added a nil check for loc to both calendar helpers and added test coverage. I initially thought the caller’s validation was enough, but agree that validating within the helpers makes them safer for any future call sites.

if loc == nil {
return time.Time{}, time.Time{},
fmt.Errorf("location is required")
}
// Normalize to local dates
startLocal := startUTC.In(loc)
endLocal := endUTC.In(loc)

y1, m1, d1 := startLocal.Date()
y2, m2, d2 := endLocal.Date()

// All-day local time window used for Events.List (TimeMin / TimeMax).
allDayStart := time.Date(y1, m1, d1, 0, 0, 0, 0, loc)
// Clockify is inclusive; GCal all-day is [start, end) exclusive.
// So cover the last OOO day by adding +1 local day to the end date.
allDayEndExclusive := time.Date(y2, m2, d2, 0, 0, 0, 0, loc).AddDate(0, 0, 1)

return allDayStart, allDayEndExclusive, nil
}

func handleHalfDay(
halfDayHours *ClockifyTimePeriod,
loc *time.Location,
) (time.Time, time.Time, error) {
if loc == nil {
return time.Time{}, time.Time{},
fmt.Errorf("location is required")
}

if halfDayHours == nil {
return time.Time{}, time.Time{},
fmt.Errorf("half-day request is missing halfDayHours")
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verify that loc is not null here.

startUTC, err := ParseTimeAny(halfDayHours.Start)
if err != nil {
return time.Time{}, time.Time{},
fmt.Errorf("bad halfDayHours.start: %w", err)
}

endUTC, err := ParseTimeAny(halfDayHours.End)
if err != nil {
return time.Time{}, time.Time{},
fmt.Errorf("bad halfDayHours.end: %w", err)
}

startLocal := startUTC.In(loc)
endLocal := endUTC.In(loc)

if !endLocal.After(startLocal) {
return time.Time{}, time.Time{},
fmt.Errorf("half-day end must be after start")
}

return startLocal, endLocal, nil
}

func calendarEventTimeValue(eventTime *calendar.EventDateTime) string {
if eventTime == nil {
return ""
}

if eventTime.DateTime != "" {
return eventTime.DateTime
}

return eventTime.Date
}
Loading
Loading