package calendar import ( "context" "errors" "time" "github.com/jackc/pgx/v5" ) var ErrCustomDateNotFound = errors.New("custom date not found") type CustomDate struct { ID string `json:"id"` ClientID *string `json:"client_id"` Name string `json:"name"` Description string `json:"description"` Date string `json:"date"` RecursAnnually bool `json:"recurs_annually"` Visibility string `json:"visibility"` Category string `json:"category"` CreatedBy *string `json:"created_by"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } func (s Store) ListCustomDatesByYear(ctx context.Context, year int, clientID string, viewerUserID string) ([]CustomDate, error) { rows, err := s.db.Query(ctx, ` SELECT id::text, client_id::text, name, description, CASE WHEN recurs_annually THEN make_date( $1, EXTRACT(MONTH FROM date)::int, LEAST( EXTRACT(DAY FROM date)::int, EXTRACT(DAY FROM (date_trunc('month', make_date($1, EXTRACT(MONTH FROM date)::int, 1)) + interval '1 month - 1 day'))::int ) )::text ELSE date::text END, recurs_annually, visibility, category, created_by::text, created_at, updated_at FROM commemorative_dates WHERE ( (date >= make_date($1, 1, 1) AND date < make_date($1 + 1, 1, 1)) OR (recurs_annually AND date < make_date($1 + 1, 1, 1)) ) AND ($2 = '' OR client_id IS NULL OR client_id::text = $2) AND ( $3 = '' OR ( visibility = 'client' AND ( client_id IS NULL OR EXISTS ( SELECT 1 FROM client_users WHERE client_users.client_id = commemorative_dates.client_id AND client_users.user_id = $3::uuid ) ) ) ) ORDER BY date ASC, name ASC `, year, clientID, viewerUserID) if err != nil { return nil, err } defer rows.Close() dates := []CustomDate{} for rows.Next() { date, err := scanCustomDate(rows) if err != nil { return nil, err } dates = append(dates, date) } return dates, rows.Err() } func (s Store) CreateCustomDate(ctx context.Context, input CreateCustomDateInput) (CustomDate, error) { row := s.db.QueryRow(ctx, ` INSERT INTO commemorative_dates ( client_id, name, description, date, recurs_annually, visibility, category, created_by ) VALUES (NULLIF($1, '')::uuid, $2, $3, $4, $5, $6, $7, NULLIF($8, '')::uuid) RETURNING id::text, client_id::text, name, description, date::text, recurs_annually, visibility, category, created_by::text, created_at, updated_at `, input.ClientID, input.Name, input.Description, input.Date, input.RecursAnnually, input.Visibility, input.Category, input.CreatedBy, ) return scanCustomDate(row) } type UpdateCustomDateInput struct { ID string ClientID string Name string Description string Date string RecursAnnually bool Visibility string } func (s Store) UpdateCustomDate(ctx context.Context, input UpdateCustomDateInput) (CustomDate, error) { row := s.db.QueryRow(ctx, ` UPDATE commemorative_dates SET client_id = NULLIF($2, '')::uuid, name = $3, description = $4, date = $5::date, recurs_annually = $6, visibility = $7, updated_at = now() WHERE id = $1::uuid AND category != 'feriados-brasil' RETURNING id::text, client_id::text, name, description, date::text, recurs_annually, visibility, category, created_by::text, created_at, updated_at `, input.ID, input.ClientID, input.Name, input.Description, input.Date, input.RecursAnnually, input.Visibility) return scanCustomDate(row) } func (s Store) DeleteCustomDate(ctx context.Context, id string) error { command, err := s.db.Exec(ctx, ` DELETE FROM commemorative_dates WHERE id = $1::uuid AND category != 'feriados-brasil' `, id) if err != nil { return err } if command.RowsAffected() == 0 { return ErrCustomDateNotFound } return nil } func (s Store) UpsertExternalCommemorativeDates(ctx context.Context, dates []ExternalCommemorativeDate) error { for _, date := range dates { if date.Name == "" || date.Date == "" { continue } if _, err := s.db.Exec(ctx, ` INSERT INTO commemorative_dates ( client_id, name, description, date, recurs_annually, visibility, category ) VALUES (NULL, $1, $2, $3::date, false, 'client', 'feriados-brasil') ON CONFLICT DO NOTHING `, date.Name, date.Description, date.Date); err != nil { return err } } return nil } type CreateCustomDateInput struct { ClientID string Name string Description string Date string RecursAnnually bool Visibility string Category string CreatedBy string } type customDateScanner interface { Scan(dest ...any) error } func scanCustomDate(row customDateScanner) (CustomDate, error) { var date CustomDate err := row.Scan( &date.ID, &date.ClientID, &date.Name, &date.Description, &date.Date, &date.RecursAnnually, &date.Visibility, &date.Category, &date.CreatedBy, &date.CreatedAt, &date.UpdatedAt, ) if errors.Is(err, pgx.ErrNoRows) { return CustomDate{}, ErrCustomDateNotFound } if err != nil { return CustomDate{}, err } return date, nil }