diff --git a/internal/entity/undian.go b/internal/entity/undian.go index 84d52fb..6139192 100644 --- a/internal/entity/undian.go +++ b/internal/entity/undian.go @@ -1,56 +1,44 @@ package entity -import ( - "time" -) - -// ============================================= -// DATABASE ENTITIES -// ============================================= +import "time" type UndianEventDB struct { - ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` - Title string `gorm:"size:255;not null" json:"title"` - Description *string `gorm:"type:text" json:"description"` - ImageURL *string `gorm:"size:500" json:"image_url"` - Status string `gorm:"size:20;not null;default:upcoming" json:"status"` // upcoming, active, completed, cancelled - - // Event timing - StartDate time.Time `gorm:"not null" json:"start_date"` - EndDate time.Time `gorm:"not null" json:"end_date"` - DrawDate time.Time `gorm:"not null" json:"draw_date"` - - // Configuration - MinimumPurchase float64 `gorm:"type:decimal(10,2);default:50000" json:"minimum_purchase"` - - // Draw status - DrawCompleted bool `gorm:"default:false" json:"draw_completed"` - DrawCompletedAt *time.Time `json:"draw_completed_at"` - - // Metadata - TermsAndConditions *string `gorm:"type:text" json:"terms_and_conditions"` - CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` - UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` + Title string `gorm:"size:255;not null" json:"title"` + Description *string `gorm:"type:text" json:"description"` + ImageURL *string `gorm:"size:500" json:"image_url"` + Status string `gorm:"size:20;not null;default:upcoming" json:"status"` + StartDate time.Time `gorm:"not null" json:"start_date"` + EndDate time.Time `gorm:"not null" json:"end_date"` + DrawDate time.Time `gorm:"not null" json:"draw_date"` + MinimumPurchase float64 `gorm:"type:numeric(10,2);default:50000" json:"minimum_purchase"` + DrawCompleted bool `gorm:"default:false" json:"draw_completed"` + DrawCompletedAt *time.Time `json:"draw_completed_at"` + TermsAndConditions *string `gorm:"column:terms_and_conditions;type:text" json:"terms_and_conditions"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + Prefix *string `json:"prefix"` + Prizes []UndianPrizeDB `gorm:"foreignKey:UndianEventID" json:"prizes,omitempty"` + Vouchers []UndianVoucherDB `gorm:"foreignKey:UndianEventID" json:"vouchers,omitempty"` } func (UndianEventDB) TableName() string { return "undian_events" } +// UndianPrizeDB represents the undian_prizes table type UndianPrizeDB struct { ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` UndianEventID int64 `gorm:"not null" json:"undian_event_id"` Rank int `gorm:"not null" json:"rank"` PrizeName string `gorm:"size:255;not null" json:"prize_name"` - PrizeValue *float64 `gorm:"type:decimal(15,2)" json:"prize_value"` + PrizeValue *float64 `gorm:"type:numeric(15,2)" json:"prize_value"` PrizeDescription *string `gorm:"type:text" json:"prize_description"` - PrizeType string `gorm:"size:50;default:voucher" json:"prize_type"` // gold, voucher, cash, product, service + PrizeType string `gorm:"size:50;default:voucher" json:"prize_type"` PrizeImageURL *string `gorm:"size:500" json:"prize_image_url"` - - // Winner information (filled after draw) - WinningVoucherID *int64 `json:"winning_voucher_id"` - WinnerUserID *int64 `json:"winner_user_id"` - + WinningVoucherID *int64 `json:"winning_voucher_id"` + WinnerUserID *int64 `json:"winner_user_id"` + Amount *int64 `json:"amount"` // Relations UndianEvent UndianEventDB `gorm:"foreignKey:UndianEventID" json:"undian_event,omitempty"` } @@ -59,22 +47,18 @@ func (UndianPrizeDB) TableName() string { return "undian_prizes" } +// UndianVoucherDB represents the undian_vouchers table type UndianVoucherDB struct { - ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` - UndianEventID int64 `gorm:"not null" json:"undian_event_id"` - CustomerID int64 `gorm:"not null" json:"customer_id"` - OrderID *int64 `json:"order_id"` - - // Voucher identification - VoucherCode string `gorm:"size:50;not null;uniqueIndex" json:"voucher_code"` - VoucherNumber *int `json:"voucher_number"` - - // Winner status - IsWinner bool `gorm:"default:false" json:"is_winner"` - PrizeRank *int `json:"prize_rank"` - WonAt *time.Time `json:"won_at"` - - CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` + UndianEventID int64 `gorm:"not null" json:"undian_event_id"` + CustomerID int64 `gorm:"not null" json:"customer_id"` + OrderID *int64 `json:"order_id"` + VoucherCode string `gorm:"size:50;not null;uniqueIndex" json:"voucher_code"` + VoucherNumber *int `json:"voucher_number"` + IsWinner bool `gorm:"default:false" json:"is_winner"` + PrizeRank *int `json:"prize_rank"` + WonAt *time.Time `json:"won_at"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` // Relations UndianEvent UndianEventDB `gorm:"foreignKey:UndianEventID" json:"undian_event,omitempty"` @@ -84,368 +68,51 @@ func (UndianVoucherDB) TableName() string { return "undian_vouchers" } -// ============================================= -// REQUEST/RESPONSE ENTITIES -// ============================================= - -type UndianEventSearch struct { - Status string `json:"status"` - IsActive bool `json:"is_active"` - IsCompleted bool `json:"is_completed"` - Search string `json:"search"` - StartDateFrom time.Time `json:"start_date_from"` - StartDateTo time.Time `json:"start_date_to"` - Limit int `json:"limit"` - Offset int `json:"offset"` -} - -type CreateUndianEventRequest struct { - Title string `json:"title" validate:"required,min=3,max=255"` - Description *string `json:"description"` - ImageURL *string `json:"image_url"` - StartDate time.Time `json:"start_date" validate:"required"` - EndDate time.Time `json:"end_date" validate:"required"` - DrawDate time.Time `json:"draw_date" validate:"required"` - MinimumPurchase float64 `json:"minimum_purchase" validate:"min=0"` - TermsAndConditions *string `json:"terms_and_conditions"` - Prizes []CreateUndianPrizeRequest `json:"prizes" validate:"required,min=1"` -} - -type UpdateUndianEventRequest struct { - ID int64 `json:"id" validate:"required"` - Title string `json:"title" validate:"required,min=3,max=255"` - Description *string `json:"description"` - ImageURL *string `json:"image_url"` - StartDate time.Time `json:"start_date" validate:"required"` - EndDate time.Time `json:"end_date" validate:"required"` - DrawDate time.Time `json:"draw_date" validate:"required"` - MinimumPurchase float64 `json:"minimum_purchase" validate:"min=0"` - TermsAndConditions *string `json:"terms_and_conditions"` - Status string `json:"status" validate:"oneof=upcoming active completed cancelled"` -} - -type CreateUndianPrizeRequest struct { - Rank int `json:"rank" validate:"required,min=1"` - PrizeName string `json:"prize_name" validate:"required,min=3,max=255"` - PrizeValue *float64 `json:"prize_value"` - PrizeDescription *string `json:"prize_description"` - PrizeType string `json:"prize_type" validate:"oneof=gold voucher cash product service"` - PrizeImageURL *string `json:"prize_image_url"` +// Response Models +type UndianListResponse struct { + Events []*UndianEventResponse `json:"events"` } type UndianEventResponse struct { - ID int64 `json:"id"` - Title string `json:"title"` - Description *string `json:"description"` - ImageURL *string `json:"image_url"` - Status string `json:"status"` - StartDate time.Time `json:"start_date"` - EndDate time.Time `json:"end_date"` - DrawDate time.Time `json:"draw_date"` - MinimumPurchase float64 `json:"minimum_purchase"` - DrawCompleted bool `json:"draw_completed"` - DrawCompletedAt *time.Time `json:"draw_completed_at"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - - // Statistics (populated when needed) - TotalVouchers int64 `json:"total_vouchers,omitempty"` - TotalParticipants int64 `json:"total_participants,omitempty"` - TotalPrizes int64 `json:"total_prizes,omitempty"` - - // Relations (populated when needed) - Prizes []UndianPrizeResponse `json:"prizes,omitempty"` -} - -type UndianPrizeResponse struct { - ID int64 `json:"id"` - UndianEventID int64 `json:"undian_event_id"` - Rank int `json:"rank"` - PrizeName string `json:"prize_name"` - PrizeValue *float64 `json:"prize_value"` - PrizeDescription *string `json:"prize_description"` - PrizeType string `json:"prize_type"` - PrizeImageURL *string `json:"prize_image_url"` - WinningVoucherID *int64 `json:"winning_voucher_id,omitempty"` - WinnerUserID *int64 `json:"winner_user_id,omitempty"` + ID int64 `json:"id"` + Title string `json:"title"` + Description *string `json:"description"` + ImageURL *string `json:"image_url"` + Status string `json:"status"` + StartDate time.Time `json:"start_date"` + EndDate time.Time `json:"end_date"` + DrawDate time.Time `json:"draw_date"` + MinimumPurchase float64 `json:"minimum_purchase"` + DrawCompleted bool `json:"draw_completed"` + DrawCompletedAt *time.Time `json:"draw_completed_at"` + TermsConditions *string `json:"terms_and_conditions"` + Prefix *string `json:"prefix"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + VoucherCount int `json:"voucher_count"` + Vouchers []*UndianVoucherResponse `json:"vouchers"` + Prizes []*UndianPrizeResponse `json:"prizes"` } type UndianVoucherResponse struct { ID int64 `json:"id"` - UndianEventID int64 `json:"undian_event_id"` - CustomerID int64 `json:"customer_id"` - OrderID *int64 `json:"order_id"` VoucherCode string `json:"voucher_code"` VoucherNumber *int `json:"voucher_number"` IsWinner bool `json:"is_winner"` PrizeRank *int `json:"prize_rank"` WonAt *time.Time `json:"won_at"` CreatedAt time.Time `json:"created_at"` - - // Relations (populated when needed) - UndianEvent *UndianEventResponse `json:"undian_event,omitempty"` - Prize *UndianPrizeResponse `json:"prize,omitempty"` } -// ============================================= -// COMPOSITE ENTITIES -// ============================================= - -type UndianEventWithStats struct { - Event UndianEventDB `json:"event"` - TotalVouchers int64 `json:"total_vouchers"` - TotalParticipants int64 `json:"total_participants"` - TotalPrizes int64 `json:"total_prizes"` +type UndianPrizeResponse struct { + ID int64 `json:"id"` + Rank int `json:"rank"` + PrizeName string `json:"prize_name"` + PrizeValue *float64 `json:"prize_value"` + PrizeDescription *string `json:"prize_description"` + PrizeType string `json:"prize_type"` + PrizeImageURL *string `json:"prize_image_url"` + WinningVoucherID *int64 `json:"winning_voucher_id"` + WinnerUserID *int64 `json:"winner_user_id"` + Amount *int64 `json:"amount"` } - -type UndianEventWithPrizes struct { - Event UndianEventDB `json:"event"` - Prizes []*UndianPrizeDB `json:"prizes"` -} - -type CustomerUndianSummary struct { - CustomerID int64 `json:"customer_id"` - EventID int64 `json:"event_id"` - TotalVouchers int64 `json:"total_vouchers"` - WinningVouchers int64 `json:"winning_vouchers"` - IsParticipating bool `json:"is_participating"` -} - -// ============================================= -// API RESPONSE ENTITIES -// ============================================= - -type UndianEventListResponse struct { - Events []UndianEventResponse `json:"events"` - Total int `json:"total"` - Page int `json:"page"` - Limit int `json:"limit"` -} - -type CustomerUndianListResponse struct { - Events []CustomerUndianEventResponse `json:"events"` - Total int `json:"total"` -} - -type CustomerUndianEventResponse struct { - Event UndianEventResponse `json:"event"` - UserVouchers []UndianVoucherResponse `json:"user_vouchers"` - TotalVouchers int `json:"total_vouchers"` - WinningVouchers int `json:"winning_vouchers"` - IsParticipating bool `json:"is_participating"` -} - -type UndianEventDetailResponse struct { - Event UndianEventResponse `json:"event"` - Prizes []UndianPrizeResponse `json:"prizes"` - UserVouchers []UndianVoucherResponse `json:"user_vouchers,omitempty"` - TotalVouchers int64 `json:"total_vouchers"` - TotalParticipants int64 `json:"total_participants"` - UserParticipating bool `json:"user_participating"` - UserVoucherCount int `json:"user_voucher_count"` -} - -type DrawResultResponse struct { - EventID int64 `json:"event_id"` - EventTitle string `json:"event_title"` - DrawDate time.Time `json:"draw_date"` - DrawCompleted bool `json:"draw_completed"` - Winners []DrawWinnerResponse `json:"winners"` -} - -type DrawWinnerResponse struct { - Rank int `json:"rank"` - PrizeName string `json:"prize_name"` - PrizeValue *float64 `json:"prize_value"` - VoucherCode string `json:"voucher_code"` - WinnerUserID *int64 `json:"winner_user_id,omitempty"` -} - -// ============================================= -// ERROR RESPONSES -// ============================================= - -type UndianError struct { - Code string `json:"code"` - Message string `json:"message"` - Details string `json:"details,omitempty"` -} - -func (e UndianError) Error() string { - return e.Message -} - -// Common error codes -var ( - ErrUndianEventNotFound = UndianError{ - Code: "UNDIAN_EVENT_NOT_FOUND", - Message: "Undian event not found", - } - - ErrUndianEventNotActive = UndianError{ - Code: "UNDIAN_EVENT_NOT_ACTIVE", - Message: "Undian event is not active", - } - - ErrDrawAlreadyCompleted = UndianError{ - Code: "DRAW_ALREADY_COMPLETED", - Message: "Draw has already been completed for this event", - } - - ErrDrawDateNotReached = UndianError{ - Code: "DRAW_DATE_NOT_REACHED", - Message: "Draw date has not been reached yet", - } - - ErrInsufficientOrderAmount = UndianError{ - Code: "INSUFFICIENT_ORDER_AMOUNT", - Message: "Order amount does not meet minimum purchase requirement", - } - - ErrVoucherNotFound = UndianError{ - Code: "VOUCHER_NOT_FOUND", - Message: "Voucher not found", - } - - ErrVoucherAlreadyExists = UndianError{ - Code: "VOUCHER_ALREADY_EXISTS", - Message: "Voucher with this code already exists", - } - - ErrInvalidDateRange = UndianError{ - Code: "INVALID_DATE_RANGE", - Message: "Invalid date range: start_date must be before end_date, and end_date must be before draw_date", - } -) - -// ============================================= -// CONVERSION METHODS -// ============================================= - -func (e *UndianEventDB) ToResponse() UndianEventResponse { - return UndianEventResponse{ - ID: e.ID, - Title: e.Title, - Description: e.Description, - ImageURL: e.ImageURL, - Status: e.Status, - StartDate: e.StartDate, - EndDate: e.EndDate, - DrawDate: e.DrawDate, - MinimumPurchase: e.MinimumPurchase, - DrawCompleted: e.DrawCompleted, - DrawCompletedAt: e.DrawCompletedAt, - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - } -} - -func (p *UndianPrizeDB) ToResponse() UndianPrizeResponse { - return UndianPrizeResponse{ - ID: p.ID, - UndianEventID: p.UndianEventID, - Rank: p.Rank, - PrizeName: p.PrizeName, - PrizeValue: p.PrizeValue, - PrizeDescription: p.PrizeDescription, - PrizeType: p.PrizeType, - PrizeImageURL: p.PrizeImageURL, - WinningVoucherID: p.WinningVoucherID, - WinnerUserID: p.WinnerUserID, - } -} - -func (v *UndianVoucherDB) ToResponse() UndianVoucherResponse { - return UndianVoucherResponse{ - ID: v.ID, - UndianEventID: v.UndianEventID, - CustomerID: v.CustomerID, - OrderID: v.OrderID, - VoucherCode: v.VoucherCode, - VoucherNumber: v.VoucherNumber, - IsWinner: v.IsWinner, - PrizeRank: v.PrizeRank, - WonAt: v.WonAt, - CreatedAt: v.CreatedAt, - } -} - -// Helper method to convert slice of DB entities to response entities -func UndianEventsToResponse(events []*UndianEventDB) []UndianEventResponse { - responses := make([]UndianEventResponse, len(events)) - for i, event := range events { - responses[i] = event.ToResponse() - } - return responses -} - -func UndianPrizesToResponse(prizes []*UndianPrizeDB) []UndianPrizeResponse { - responses := make([]UndianPrizeResponse, len(prizes)) - for i, prize := range prizes { - responses[i] = prize.ToResponse() - } - return responses -} - -func UndianVouchersToResponse(vouchers []*UndianVoucherDB) []UndianVoucherResponse { - responses := make([]UndianVoucherResponse, len(vouchers)) - for i, voucher := range vouchers { - responses[i] = voucher.ToResponse() - } - return responses -} - -// ============================================= -// VALIDATION HELPERS -// ============================================= - -func (r *CreateUndianEventRequest) Validate() error { - if r.StartDate.After(r.EndDate) { - return ErrInvalidDateRange - } - if r.EndDate.After(r.DrawDate) { - return ErrInvalidDateRange - } - if r.MinimumPurchase < 0 { - return UndianError{ - Code: "INVALID_MINIMUM_PURCHASE", - Message: "Minimum purchase must be greater than or equal to 0", - } - } - return nil -} - -func (r *UpdateUndianEventRequest) Validate() error { - if r.StartDate.After(r.EndDate) { - return ErrInvalidDateRange - } - if r.EndDate.After(r.DrawDate) { - return ErrInvalidDateRange - } - if r.MinimumPurchase < 0 { - return UndianError{ - Code: "INVALID_MINIMUM_PURCHASE", - Message: "Minimum purchase must be greater than or equal to 0", - } - } - return nil -} - -// ============================================= -// STATUS CONSTANTS -// ============================================= - -const ( - UndianStatusUpcoming = "upcoming" - UndianStatusActive = "active" - UndianStatusCompleted = "completed" - UndianStatusCancelled = "cancelled" -) - -const ( - PrizeTypeGold = "gold" - PrizeTypeVoucher = "voucher" - PrizeTypeCash = "cash" - PrizeTypeProduct = "product" - PrizeTypeService = "service" -) diff --git a/internal/handlers/http/customer_undian.go b/internal/handlers/http/customer_undian.go new file mode 100644 index 0000000..d58c83d --- /dev/null +++ b/internal/handlers/http/customer_undian.go @@ -0,0 +1,164 @@ +package http + +import ( + "enaklo-pos-be/internal/entity" + "enaklo-pos-be/internal/handlers/request" + "enaklo-pos-be/internal/handlers/response" + "enaklo-pos-be/internal/services/v2/undian" + "github.com/gin-gonic/gin" + "net/http" + "time" +) + +type CustomerUndianHandler struct { + undianService undian.Service +} + +func NewCustomerUndianHandler(undianService undian.Service) *CustomerUndianHandler { + return &CustomerUndianHandler{ + undianService: undianService, + } +} + +func (h *CustomerUndianHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) { + route := group.Group("/undian") + + route.GET("/list", jwt, h.GetUndianList) + route.GET("/events", jwt, h.GetActiveEvents) +} + +func (h *CustomerUndianHandler) GetUndianList(c *gin.Context) { + ctx := request.GetMyContext(c) + userID := ctx.RequestedBy() + + undianResponse, err := h.undianService.GetUndianList(ctx, userID) + if err != nil { + response.ErrorWrapper(c, err) + return + } + + responseData := h.mapToUndianListResponse(undianResponse) + + c.JSON(http.StatusOK, response.BaseResponse{ + Success: true, + Status: http.StatusOK, + Data: responseData, + }) +} + +func (h *CustomerUndianHandler) GetActiveEvents(c *gin.Context) { + ctx := request.GetMyContext(c) + + events, err := h.undianService.GetActiveUndianEvents(ctx) + if err != nil { + response.ErrorWrapper(c, err) + return + } + + responseData := h.mapToActiveEventsResponse(events) + + c.JSON(http.StatusOK, response.BaseResponse{ + Success: true, + Status: http.StatusOK, + Data: responseData, + }) +} + +func (h *CustomerUndianHandler) mapToUndianListResponse(undianResponse *entity.UndianListResponse) response.UndianListResponse { + events := make([]response.UndianEventResponse, 0, len(undianResponse.Events)) + + for _, event := range undianResponse.Events { + vouchers := make([]response.UndianVoucherResponse, 0, len(event.Vouchers)) + for _, voucher := range event.Vouchers { + vouchers = append(vouchers, response.UndianVoucherResponse{ + ID: voucher.ID, + VoucherCode: voucher.VoucherCode, + VoucherNumber: voucher.VoucherNumber, + IsWinner: voucher.IsWinner, + PrizeRank: voucher.PrizeRank, + WonAt: h.formatTimePointer(voucher.WonAt), + CreatedAt: voucher.CreatedAt.Format("2006-01-02T15:04:05Z"), + }) + } + + prizes := make([]response.UndianPrizeResponse, 0, len(event.Prizes)) + for _, prize := range event.Prizes { + prizes = append(prizes, response.UndianPrizeResponse{ + ID: prize.ID, + Rank: prize.Rank, + PrizeName: prize.PrizeName, + PrizeValue: prize.PrizeValue, + PrizeDescription: prize.PrizeDescription, + PrizeType: prize.PrizeType, + PrizeImageURL: prize.PrizeImageURL, + WinningVoucherID: prize.WinningVoucherID, + WinnerUserID: prize.WinnerUserID, + IsWon: prize.WinningVoucherID != nil, + Amount: prize.Amount, + }) + } + + events = append(events, response.UndianEventResponse{ + ID: event.ID, + Title: event.Title, + Description: event.Description, + ImageURL: event.ImageURL, + Status: event.Status, + StartDate: event.StartDate.Format("2006-01-02T15:04:05Z"), + EndDate: event.EndDate.Format("2006-01-02T15:04:05Z"), + DrawDate: event.DrawDate.Format("2006-01-02T15:04:05Z"), + MinimumPurchase: event.MinimumPurchase, + DrawCompleted: event.DrawCompleted, + DrawCompletedAt: h.formatTimePointer(event.DrawCompletedAt), + TermsConditions: event.TermsConditions, + Prefix: event.Prefix, + CreatedAt: event.CreatedAt.Format("2006-01-02T15:04:05Z"), + UpdatedAt: event.UpdatedAt.Format("2006-01-02T15:04:05Z"), + VoucherCount: event.VoucherCount, + Vouchers: vouchers, + Prizes: prizes, + TotalPrizes: len(prizes), + }) + } + + return response.UndianListResponse{ + Events: events, + } +} + +func (h *CustomerUndianHandler) mapToActiveEventsResponse(events []*entity.UndianEventDB) response.ActiveEventsResponse { + eventResponses := make([]response.ActiveEventResponse, 0, len(events)) + + for _, event := range events { + eventResponses = append(eventResponses, response.ActiveEventResponse{ + ID: event.ID, + Title: event.Title, + Description: event.Description, + ImageURL: event.ImageURL, + Status: event.Status, + StartDate: event.StartDate.Format("2006-01-02T15:04:05Z"), + EndDate: event.EndDate.Format("2006-01-02T15:04:05Z"), + DrawDate: event.DrawDate.Format("2006-01-02T15:04:05Z"), + MinimumPurchase: event.MinimumPurchase, + DrawCompleted: event.DrawCompleted, + DrawCompletedAt: h.formatTimePointer(event.DrawCompletedAt), + TermsConditions: event.TermsAndConditions, + Prefix: event.Prefix, + CreatedAt: event.CreatedAt.Format("2006-01-02T15:04:05Z"), + UpdatedAt: event.UpdatedAt.Format("2006-01-02T15:04:05Z"), + }) + } + + return response.ActiveEventsResponse{ + Events: eventResponses, + } +} + +// formatTimePointer formats time pointer to string +func (h *CustomerUndianHandler) formatTimePointer(t *time.Time) *string { + if t == nil { + return nil + } + formatted := t.Format("2006-01-02T15:04:05Z") + return &formatted +} diff --git a/internal/handlers/response/undian.go b/internal/handlers/response/undian.go new file mode 100644 index 0000000..1bef236 --- /dev/null +++ b/internal/handlers/response/undian.go @@ -0,0 +1,79 @@ +package response + +// UndianListResponse represents the response for undian list API +type UndianListResponse struct { + Events []UndianEventResponse `json:"events"` +} + +// UndianEventResponse represents an undian event with customer's vouchers and prizes +type UndianEventResponse struct { + ID int64 `json:"id"` + Title string `json:"title"` + Description *string `json:"description"` + ImageURL *string `json:"image_url"` + Status string `json:"status"` + StartDate string `json:"start_date"` + EndDate string `json:"end_date"` + DrawDate string `json:"draw_date"` + MinimumPurchase float64 `json:"minimum_purchase"` + DrawCompleted bool `json:"draw_completed"` + DrawCompletedAt *string `json:"draw_completed_at"` + TermsConditions *string `json:"terms_and_conditions"` + Prefix *string `json:"prefix"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + VoucherCount int `json:"voucher_count"` + TotalPrizes int `json:"total_prizes"` + Vouchers []UndianVoucherResponse `json:"vouchers"` + Prizes []UndianPrizeResponse `json:"prizes"` +} + +// UndianVoucherResponse represents a customer's voucher +type UndianVoucherResponse struct { + ID int64 `json:"id"` + VoucherCode string `json:"voucher_code"` + VoucherNumber *int `json:"voucher_number"` + IsWinner bool `json:"is_winner"` + PrizeRank *int `json:"prize_rank"` + WonAt *string `json:"won_at"` + CreatedAt string `json:"created_at"` +} + +// UndianPrizeResponse represents a prize in the undian event +type UndianPrizeResponse struct { + ID int64 `json:"id"` + Rank int `json:"rank"` + PrizeName string `json:"prize_name"` + PrizeValue *float64 `json:"prize_value"` + PrizeDescription *string `json:"prize_description"` + PrizeType string `json:"prize_type"` + PrizeImageURL *string `json:"prize_image_url"` + WinningVoucherID *int64 `json:"winning_voucher_id,omitempty"` + WinnerUserID *int64 `json:"winner_user_id,omitempty"` + IsWon bool `json:"is_won"` + Amount *int64 `json:"amount"` +} + +// ActiveEventsResponse represents the response for active events API (without customer data) +type ActiveEventsResponse struct { + Events []ActiveEventResponse `json:"events"` +} + +// ActiveEventResponse represents an active undian event (without customer-specific data) +type ActiveEventResponse struct { + ID int64 `json:"id"` + Title string `json:"title"` + Description *string `json:"description"` + ImageURL *string `json:"image_url"` + Status string `json:"status"` + StartDate string `json:"start_date"` + EndDate string `json:"end_date"` + DrawDate string `json:"draw_date"` + MinimumPurchase float64 `json:"minimum_purchase"` + DrawCompleted bool `json:"draw_completed"` + DrawCompletedAt *string `json:"draw_completed_at"` + TermsConditions *string `json:"terms_and_conditions"` + Prefix *string `json:"prefix"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} diff --git a/internal/middlewares/auth.go b/internal/middlewares/auth.go index 21e633e..c1f19b5 100644 --- a/internal/middlewares/auth.go +++ b/internal/middlewares/auth.go @@ -72,6 +72,34 @@ func CustomerAuthorizationMiddleware(cryp repository.Crypto) gin.HandlerFunc { } } +func OptionalCustomerAuthorizationMiddleware(cryp repository.Crypto) gin.HandlerFunc { + return func(c *gin.Context) { + tokenString := c.GetHeader("Authorization") + if tokenString == "" { + c.Next() + return + } + + tokenString = strings.TrimPrefix(tokenString, "Bearer ") + + claims, err := cryp.ParseAndValidateJWTCustomer(tokenString) + if err != nil { + c.Next() + return + } + + customCtx, err := mycontext.NewMyContextCustomer(c.Request.Context(), claims) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "error initialize context"}) + c.Next() + return + } + + c.Set("myCtx", customCtx) + c.Next() + } +} + func SuperAdminMiddleware() gin.HandlerFunc { return func(c *gin.Context) { ctx, exists := c.Get("myCtx") diff --git a/internal/repository/undian_repo.go b/internal/repository/undian_repo.go index 178e8f3..0b0a5eb 100644 --- a/internal/repository/undian_repo.go +++ b/internal/repository/undian_repo.go @@ -1,7 +1,6 @@ package repository import ( - "context" "enaklo-pos-be/internal/common/logger" "enaklo-pos-be/internal/common/mycontext" "enaklo-pos-be/internal/entity" @@ -13,9 +12,11 @@ import ( ) type UndianRepo interface { - GetUndianEventByID(ctx context.Context, id int64) (*entity.UndianEventDB, error) - GetActiveUndianEvents(ctx context.Context) ([]*entity.UndianEventDB, error) - CreateUndianVouchers(ctx context.Context, vouchers []*entity.UndianVoucherDB) error + GetUndianEventByID(ctx mycontext.Context, id int64) (*entity.UndianEventDB, error) + GetActiveUndianEvents(ctx mycontext.Context) ([]*entity.UndianEventDB, error) + GetActiveUndianEventsWithPrizes(ctx mycontext.Context, customerID int64) ([]*entity.UndianEventDB, error) + GetCustomerVouchersByEventIDs(ctx mycontext.Context, customerID int64, eventIDs []int64) ([]*entity.UndianVoucherDB, error) + CreateUndianVouchers(ctx mycontext.Context, vouchers []*entity.UndianVoucherDB) error GetNextVoucherSequence(ctx mycontext.Context) (int64, error) GetNextVoucherSequenceBatch(ctx mycontext.Context, count int) (int64, error) } @@ -24,13 +25,13 @@ type undianRepository struct { db *gorm.DB } -func NewUndianRepository(db *gorm.DB) *undianRepository { +func NewUndianRepository(db *gorm.DB) UndianRepo { return &undianRepository{ db: db, } } -func (r *undianRepository) GetUndianEventByID(ctx context.Context, id int64) (*entity.UndianEventDB, error) { +func (r *undianRepository) GetUndianEventByID(ctx mycontext.Context, id int64) (*entity.UndianEventDB, error) { event := new(entity.UndianEventDB) if err := r.db.WithContext(ctx).First(event, id).Error; err != nil { logger.ContextLogger(ctx).Error("error when get undian event by id", zap.Error(err)) @@ -39,12 +40,13 @@ func (r *undianRepository) GetUndianEventByID(ctx context.Context, id int64) (*e return event, nil } -func (r *undianRepository) GetActiveUndianEvents(ctx context.Context) ([]*entity.UndianEventDB, error) { +func (r *undianRepository) GetActiveUndianEvents(ctx mycontext.Context) ([]*entity.UndianEventDB, error) { var events []*entity.UndianEventDB now := time.Now() if err := r.db.WithContext(ctx). Where("status = 'active' AND start_date <= ? AND end_date >= ?", now, now). + Order("created_at DESC"). Find(&events).Error; err != nil { logger.ContextLogger(ctx).Error("error when get active undian events", zap.Error(err)) return nil, err @@ -53,7 +55,57 @@ func (r *undianRepository) GetActiveUndianEvents(ctx context.Context) ([]*entity return events, nil } -func (r *undianRepository) CreateUndianVouchers(ctx context.Context, vouchers []*entity.UndianVoucherDB) error { +func (r *undianRepository) GetActiveUndianEventsWithPrizes(ctx mycontext.Context, customerID int64) ([]*entity.UndianEventDB, error) { + var events []*entity.UndianEventDB + now := time.Now() + + query := r.db.WithContext(ctx). + Preload("Prizes", func(db *gorm.DB) *gorm.DB { + return db.Order("rank ASC") + }). + Where("status = 'active' AND start_date <= ? AND end_date >= ?", now, now). + Order("created_at DESC") + + // If customer ID is provided, preload only that customer's vouchers + if customerID > 0 { + query = query.Preload("Vouchers", func(db *gorm.DB) *gorm.DB { + return db.Where("customer_id = ?", customerID).Order("created_at ASC") + }) + } else { + // If no customer ID, don't load any vouchers (or load all if needed) + query = query.Preload("Vouchers", func(db *gorm.DB) *gorm.DB { + return db.Where("1 = 0") // This loads no vouchers + }) + } + + if err := query.Find(&events).Error; err != nil { + logger.ContextLogger(ctx).Error("error when get active undian events with prizes", + zap.Int64("customerID", customerID), + zap.Error(err)) + return nil, err + } + + return events, nil +} + +func (r *undianRepository) GetCustomerVouchersByEventIDs(ctx mycontext.Context, customerID int64, eventIDs []int64) ([]*entity.UndianVoucherDB, error) { + var vouchers []*entity.UndianVoucherDB + + if err := r.db.WithContext(ctx). + Where("customer_id = ? AND undian_event_id IN ?", customerID, eventIDs). + Order("undian_event_id ASC, created_at ASC"). + Find(&vouchers).Error; err != nil { + logger.ContextLogger(ctx).Error("error when get customer vouchers by event IDs", + zap.Int64("customerID", customerID), + zap.Int64s("eventIDs", eventIDs), + zap.Error(err)) + return nil, err + } + + return vouchers, nil +} + +func (r *undianRepository) CreateUndianVouchers(ctx mycontext.Context, vouchers []*entity.UndianVoucherDB) error { err := r.db.WithContext(ctx).Create(&vouchers).Error if err != nil { logger.ContextLogger(ctx).Error("error when create undian vouchers", zap.Error(err)) @@ -85,4 +137,4 @@ func (r *undianRepository) GetNextVoucherSequenceBatch(ctx mycontext.Context, co } return startSequence, nil -} \ No newline at end of file +} diff --git a/internal/routes/customer_routes.go b/internal/routes/customer_routes.go index 35efa88..7ba563a 100644 --- a/internal/routes/customer_routes.go +++ b/internal/routes/customer_routes.go @@ -16,6 +16,7 @@ func RegisterCustomerRoutes(app *app.Server, serviceManager *services.ServiceMan approute := app.Group("/api/v1/customer") authMiddleware := middlewares.CustomerAuthorizationMiddleware(repoManager.Crypto) + optionlMiddleWare := middlewares.OptionalCustomerAuthorizationMiddleware(repoManager.Crypto) serverRoutes := []HTTPHandlerRoutes{ discovery.NewHandler(serviceManager.DiscoverService), @@ -27,4 +28,6 @@ func RegisterCustomerRoutes(app *app.Server, serviceManager *services.ServiceMan for _, handler := range serverRoutes { handler.Route(approute, authMiddleware) } + + http.NewCustomerUndianHandler(serviceManager.UndianSvc).Route(approute, optionlMiddleWare) } diff --git a/internal/routes/routes.go b/internal/routes/routes.go index caff080..12c9c52 100644 --- a/internal/routes/routes.go +++ b/internal/routes/routes.go @@ -44,7 +44,6 @@ func RegisterPrivateRoutes(app *app.Server, serviceManager *services.ServiceMana approute := app.Group("/api/v1") authMiddleware := middlewares.AuthorizationMiddleware(repoManager.Crypto) - serverRoutes := []HTTPHandlerRoutes{ auth.NewAuthHandler(serviceManager.AuthSvc), event.NewHandler(serviceManager.EventSvc), diff --git a/internal/services/service.go b/internal/services/service.go index e01d35c..d313b31 100644 --- a/internal/services/service.go +++ b/internal/services/service.go @@ -18,6 +18,7 @@ import ( customerSvc "enaklo-pos-be/internal/services/v2/customer" "enaklo-pos-be/internal/services/v2/inprogress_order" orderSvc "enaklo-pos-be/internal/services/v2/order" + "enaklo-pos-be/internal/services/v2/undian" "enaklo-pos-be/internal/services/v2/partner_settings" productSvc "enaklo-pos-be/internal/services/v2/product" @@ -51,6 +52,7 @@ type ServiceManagerImpl struct { MemberRegistrationSvc member.RegistrationService InProgressSvc inprogress_order.InProgressOrderService AuthV2Svc authSvc.Service + UndianSvc undian.Service } func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl) *ServiceManagerImpl { @@ -81,6 +83,7 @@ func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl) InProgressSvc: inprogressOrder, ProductV2Svc: productSvcV2, AuthV2Svc: authSvc.New(repo.CustomerRepo, repo.Crypto), + UndianSvc: undian.New(repo.UndianRepository), } } diff --git a/internal/services/v2/order/order.go b/internal/services/v2/order/order.go index 047d025..f991e3c 100644 --- a/internal/services/v2/order/order.go +++ b/internal/services/v2/order/order.go @@ -117,10 +117,9 @@ type InProgressOrderRepository interface { } type VoucherUndianRepo interface { - GetActiveUndianEvents(ctx context.Context) ([]*entity.UndianEventDB, error) - CreateUndianVouchers(ctx context.Context, vouchers []*entity.UndianVoucherDB) error - GetNextVoucherSequence(ctx mycontext.Context) (int64, error) + GetActiveUndianEvents(ctx mycontext.Context) ([]*entity.UndianEventDB, error) GetNextVoucherSequenceBatch(ctx mycontext.Context, count int) (int64, error) + CreateUndianVouchers(ctx mycontext.Context, vouchers []*entity.UndianVoucherDB) error } type orderSvc struct { diff --git a/internal/services/v2/undian/undian.go b/internal/services/v2/undian/undian.go new file mode 100644 index 0000000..2f1fd1e --- /dev/null +++ b/internal/services/v2/undian/undian.go @@ -0,0 +1,123 @@ +package undian + +import ( + "enaklo-pos-be/internal/common/logger" + "enaklo-pos-be/internal/common/mycontext" + "enaklo-pos-be/internal/entity" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +type Service interface { + GetUndianList(ctx mycontext.Context, customerID int64) (*entity.UndianListResponse, error) + GetActiveUndianEvents(ctx mycontext.Context) ([]*entity.UndianEventDB, error) +} + +type Repository interface { + GetUndianEventByID(ctx mycontext.Context, id int64) (*entity.UndianEventDB, error) + GetActiveUndianEvents(ctx mycontext.Context) ([]*entity.UndianEventDB, error) + GetActiveUndianEventsWithPrizes(ctx mycontext.Context, customerID int64) ([]*entity.UndianEventDB, error) + GetCustomerVouchersByEventIDs(ctx mycontext.Context, customerID int64, eventIDs []int64) ([]*entity.UndianVoucherDB, error) + CreateUndianVouchers(ctx mycontext.Context, vouchers []*entity.UndianVoucherDB) error + GetNextVoucherSequence(ctx mycontext.Context) (int64, error) + GetNextVoucherSequenceBatch(ctx mycontext.Context, count int) (int64, error) +} + +type undianSvc struct { + repo Repository +} + +func New(repo Repository) Service { + return &undianSvc{ + repo: repo, + } +} + +func (s *undianSvc) GetUndianList(ctx mycontext.Context, customerID int64) (*entity.UndianListResponse, error) { + events, err := s.repo.GetActiveUndianEventsWithPrizes(ctx, customerID) + if err != nil { + logger.ContextLogger(ctx).Error("failed to get active undian events with prizes", + zap.Int64("customerID", customerID), + zap.Error(err)) + return nil, errors.Wrap(err, "failed to get active undian events with prizes") + } + + if len(events) == 0 { + return &entity.UndianListResponse{ + Events: []*entity.UndianEventResponse{}, + }, nil + } + + // Build response + eventResponses := make([]*entity.UndianEventResponse, 0, len(events)) + for _, event := range events { + voucherResponses := make([]*entity.UndianVoucherResponse, 0, len(event.Vouchers)) + for _, voucher := range event.Vouchers { + voucherResponse := &entity.UndianVoucherResponse{ + ID: voucher.ID, + VoucherCode: voucher.VoucherCode, + VoucherNumber: voucher.VoucherNumber, + IsWinner: voucher.IsWinner, + PrizeRank: voucher.PrizeRank, + WonAt: voucher.WonAt, + CreatedAt: voucher.CreatedAt, + } + voucherResponses = append(voucherResponses, voucherResponse) + } + + // Convert prizes to response format + prizeResponses := make([]*entity.UndianPrizeResponse, 0, len(event.Prizes)) + for _, prize := range event.Prizes { + prizeResponse := &entity.UndianPrizeResponse{ + ID: prize.ID, + Rank: prize.Rank, + PrizeName: prize.PrizeName, + PrizeValue: prize.PrizeValue, + PrizeDescription: prize.PrizeDescription, + PrizeType: prize.PrizeType, + PrizeImageURL: prize.PrizeImageURL, + WinningVoucherID: prize.WinningVoucherID, + WinnerUserID: prize.WinnerUserID, + Amount: prize.Amount, + } + prizeResponses = append(prizeResponses, prizeResponse) + } + + eventResponse := &entity.UndianEventResponse{ + ID: event.ID, + Title: event.Title, + Description: event.Description, + ImageURL: event.ImageURL, + Status: event.Status, + StartDate: event.StartDate, + EndDate: event.EndDate, + DrawDate: event.DrawDate, + MinimumPurchase: event.MinimumPurchase, + DrawCompleted: event.DrawCompleted, + DrawCompletedAt: event.DrawCompletedAt, + TermsConditions: event.TermsAndConditions, + Prefix: event.Prefix, + CreatedAt: event.CreatedAt, + UpdatedAt: event.UpdatedAt, + VoucherCount: len(voucherResponses), // Fixed: use voucherResponses instead of eventVouchers + Vouchers: voucherResponses, // Fixed: use voucherResponses instead of eventVouchers + Prizes: prizeResponses, + } + + eventResponses = append(eventResponses, eventResponse) + } + + return &entity.UndianListResponse{ + Events: eventResponses, + }, nil +} + +func (s *undianSvc) GetActiveUndianEvents(ctx mycontext.Context) ([]*entity.UndianEventDB, error) { + events, err := s.repo.GetActiveUndianEvents(ctx) + if err != nil { + logger.ContextLogger(ctx).Error("failed to get active undian events", zap.Error(err)) + return nil, errors.Wrap(err, "failed to get active undian events") + } + + return events, nil +}