package data import ( "context" "errors" "fmt" "math/rand" "strings" "time" "github.com/google/uuid" "github.com/spf13/viper" "gorm.io/gorm" ) type Attendance struct { Id uint `json:"id" gorm:"primarykey;autoIncrement"` UUID uuid.UUID `json:"uuid" gorm:"type:uuid;uniqueIndex;not null"` AttendanceId uuid.UUID `json:"attendance_id" gorm:"type:uuid;uniqueIndex;not null"` EventId uuid.UUID `json:"event_id" gorm:"type:uuid;uniqueIndex:unique_event_user;not null"` UserId uuid.UUID `json:"user_id" gorm:"type:uuid;uniqueIndex:unique_event_user;not null"` KycId uuid.UUID `json:"kyc_id" gorm:"type:uuid;uniqueIndex:unique_event_user;not null"` Role string `json:"role" gorm:"type:varchar(255);not null"` State string `json:"state" gorm:"type:varchar(255);not null"` // suspended | out_of_limit | success CheckinAt time.Time `json:"checkin_at"` } type AttendanceSearchDoc struct { AttendanceId string `json:"attendance_id"` EventId string `json:"event_id"` UserId string `json:"user_id"` Role string `json:"role"` CheckinAt time.Time `json:"checkin_at"` } func (self *Attendance) SetEventId(s uuid.UUID) *Attendance { self.EventId = s return self } func (self *Attendance) SetUserId(s uuid.UUID) *Attendance { self.UserId = s return self } func (self *Attendance) SetKycId(s uuid.UUID) *Attendance { self.KycId = s return self } func (self *Attendance) SetRole(s string) *Attendance { self.Role = s return self } func (self *Attendance) SetState(s string) *Attendance { self.State = s return self } func (self *Attendance) GetAttendance(ctx context.Context, userId, eventId uuid.UUID) (*Attendance, error) { var checkin Attendance err := Database.WithContext(ctx). Where("user_id = ? AND event_id = ?", userId, eventId). First(&checkin).Error if err != nil { if err == gorm.ErrRecordNotFound { return nil, nil } return nil, err } return &checkin, err } type AttendanceUsers struct { UserId uuid.UUID `json:"user_id"` Role string `json:"role"` CheckinAt time.Time `json:"checkin_at"` } func (self *Attendance) GetUsersByEventID(ctx context.Context, eventID uuid.UUID) (*[]AttendanceUsers, error) { var result []AttendanceUsers err := Database.WithContext(ctx). Model(&Attendance{}). Select("user_id, checkin_at"). Where("event_id = ?", eventID). Order("checkin_at ASC"). Scan(&result).Error return &result, err } type AttendanceEvent struct { EventId uuid.UUID `json:"event_id"` CheckinAt time.Time `json:"checkin_at"` } func (self *Attendance) GetEventsByUserID(ctx context.Context, userID uuid.UUID) (*[]AttendanceEvent, error) { var result []AttendanceEvent err := Database.WithContext(ctx). Model(&Attendance{}). Select("event_id, checkin_at"). Where("user_id = ?", userID). Order("checkin_at ASC"). Scan(&result).Error return &result, err } func (self *Attendance) GetAttendanceByEventIdAndUserId(ctx context.Context, eventId, userId uuid.UUID) (*Attendance, error) { var attendance Attendance err := Database.WithContext(ctx). Where("event_id = ? AND user_id = ?", eventId, userId). First(&attendance).Error if err != nil { if err == gorm.ErrRecordNotFound { return nil, nil } return nil, err } return &attendance, nil } func (self *Attendance) Create(ctx context.Context) error { self.UUID = uuid.New() self.AttendanceId = uuid.New() // DB transaction for strong consistency err := Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Create(&self).Error; err != nil { return err } return nil }) if err != nil { return err } return nil } func (self *Attendance) Update(ctx context.Context, attendanceId uuid.UUID) (*Attendance, error) { var attendance Attendance err := Database.WithContext(ctx).Transaction(func(tx *gorm.DB) error { // Lock the row for update if err := tx. Where("attendance_id = ?", attendanceId). First(&attendance).Error; err != nil { return err } if err := tx. Model(&attendance). Updates(self).Error; err != nil { return err } // Reload to ensure struct is up to date return tx. Where("attendance_id = ?", attendanceId). First(&attendance).Error }) if err != nil { return nil, err } return &attendance, nil } func (self *Attendance) GenCheckinCode(ctx context.Context, eventId uuid.UUID) (*string, error) { ttl := viper.GetDuration("ttl.checkin_code_ttl") rng := rand.New(rand.NewSource(time.Now().UnixNano())) for { code := fmt.Sprintf("%06d", rng.Intn(900000)+100000) ok, err := Redis.SetNX( ctx, "checkin_code:"+code, "user_id:"+self.UserId.String()+":event_id:"+eventId.String(), ttl, ).Result() if err != nil { return nil, err } if ok { return &code, nil } } } func (self *Attendance) VerifyCheckinCode(ctx context.Context, checkinCode string) error { val, err := Redis.Get(ctx, "checkin_code:"+checkinCode).Result() if err != nil { return errors.New("[Attendance Data] invalid or expired checkin code") } // Expected format: user_id::event_id: parts := strings.Split(val, ":") if len(parts) != 4 { return errors.New("[Attendance Data] invalid checkin code format") } userIdStr := parts[1] eventIdStr := parts[3] userId, err := uuid.Parse(userIdStr) if err != nil { return err } eventId, err := uuid.Parse(eventIdStr) if err != nil { return err } attendanceData, err := self.GetAttendance(ctx, userId, eventId) if err != nil { return err } self.CheckinAt = time.Now() _, err = self.Update(ctx, attendanceData.AttendanceId) if err != nil { return err } return nil }