From e544ef8f71cb3324e6375a859e91bae759121517 Mon Sep 17 00:00:00 2001 From: "aditya.siregar" Date: Tue, 13 Aug 2024 23:09:05 +0700 Subject: [PATCH] Add Checkin --- internal/common/errors/code.go | 3 + internal/common/errors/errors.go | 33 +++--- internal/entity/order.go | 98 +++++++++-------- internal/entity/product.go | 2 + internal/entity/sites.go | 2 + internal/handlers/http/discovery/discover.go | 2 + internal/handlers/http/order/order.go | 108 +++++++++++++++++-- internal/handlers/http/sites/sites.go | 2 + internal/handlers/request/order.go | 8 ++ internal/handlers/request/site.go | 21 ++++ internal/handlers/response/discovery.go | 2 + internal/handlers/response/order.go | 42 +++++--- internal/handlers/response/site.go | 33 +++--- internal/repository/orders/order.go | 22 +++- internal/repository/repository.go | 1 + internal/repository/sites/sites.go | 1 + internal/services/order/order.go | 80 +++++++++++++- internal/services/service.go | 3 + 18 files changed, 362 insertions(+), 101 deletions(-) diff --git a/internal/common/errors/code.go b/internal/common/errors/code.go index 060cb3e..8e3ae9a 100644 --- a/internal/common/errors/code.go +++ b/internal/common/errors/code.go @@ -8,6 +8,7 @@ const ( BadRequest Code = "40000" InvalidRequest Code = "40001" Unauthorized Code = "40100" + CheckinInvalid Code = "40002" Forbidden Code = "40300" Timeout Code = "50400" ) @@ -23,6 +24,7 @@ var ( ServerError: "Internal Server Error", Forbidden: "Forbidden", InvalidRequest: "Invalid Request", + CheckinInvalid: "Ticket Already Used or Expired", } codeHTTPMap = map[Code]int{ @@ -33,6 +35,7 @@ var ( ServerError: http.StatusInternalServerError, Forbidden: http.StatusForbidden, InvalidRequest: http.StatusUnprocessableEntity, + CheckinInvalid: http.StatusBadRequest, } ) diff --git a/internal/common/errors/errors.go b/internal/common/errors/errors.go index 49c47d6..6e58919 100644 --- a/internal/common/errors/errors.go +++ b/internal/common/errors/errors.go @@ -19,23 +19,25 @@ const ( errUnauthorized ErrType = "Unauthorized" errInsufficientBalance ErrType = "Insufficient Balance" errInactivePartner ErrType = "Partner's license is invalid or has expired. Please contact Admin Support." + errTicketAlreadyUsed ErrType = "Ticket Already Used." ) var ( - ErrorBadRequest = NewServiceException(errBadRequest) - ErrorInvalidRequest = NewServiceException(errInvalidRequest) - ErrorUnauthorized = NewServiceException(errUnauthorized) - ErrorOrderNotFound = NewServiceException(errOrderNotFound) - ErrorClientIDNotDefined = NewServiceException(errCheckoutIDNotDefined) - ErrorRequestTimeout = NewServiceException(errRequestTimeOut) - ErrorExternalCall = NewServiceException(errExternalCall) - ErrorFailedExternalCall = NewServiceException(errFailedExternalCall) - ErrorConnectionTimeOut = NewServiceException(errConnectTimeOut) - ErrorInternalServer = NewServiceException(errInternalServer) - ErrorUserIsNotFound = NewServiceException(errUserIsNotFound) - ErrorUserInvalidLogin = NewServiceException(errInvalidLogin) - ErrorInsufficientBalance = NewServiceException(errInsufficientBalance) - ErrorInvalidLicense = NewServiceException(errInactivePartner) + ErrorBadRequest = NewServiceException(errBadRequest) + ErrorInvalidRequest = NewServiceException(errInvalidRequest) + ErrorUnauthorized = NewServiceException(errUnauthorized) + ErrorOrderNotFound = NewServiceException(errOrderNotFound) + ErrorClientIDNotDefined = NewServiceException(errCheckoutIDNotDefined) + ErrorRequestTimeout = NewServiceException(errRequestTimeOut) + ErrorExternalCall = NewServiceException(errExternalCall) + ErrorFailedExternalCall = NewServiceException(errFailedExternalCall) + ErrorConnectionTimeOut = NewServiceException(errConnectTimeOut) + ErrorInternalServer = NewServiceException(errInternalServer) + ErrorUserIsNotFound = NewServiceException(errUserIsNotFound) + ErrorUserInvalidLogin = NewServiceException(errInvalidLogin) + ErrorInsufficientBalance = NewServiceException(errInsufficientBalance) + ErrorInvalidLicense = NewServiceException(errInactivePartner) + ErrorTicketInvalidOrAlreadyUsed = NewServiceException(errTicketAlreadyUsed) ) type Error interface { @@ -115,6 +117,9 @@ func (s *ServiceException) MapErrorsToCode() Code { case errInactivePartner: return BadRequest + case errTicketAlreadyUsed: + return CheckinInvalid + default: return BadRequest } diff --git a/internal/entity/order.go b/internal/entity/order.go index 87910b8..195a99e 100644 --- a/internal/entity/order.go +++ b/internal/entity/order.go @@ -5,23 +5,25 @@ import ( ) type Order struct { - ID int64 `gorm:"primaryKey;autoIncrement;column:id"` - RefID string `gorm:"type:varchar;column:ref_id"` - PartnerID int64 `gorm:"type:int;column:partner_id"` - Status string `gorm:"type:varchar;column:status"` - Amount float64 `gorm:"type:numeric;not null;column:amount"` - Total float64 `gorm:"type:numeric;not null;column:total"` - Fee float64 `gorm:"type:numeric;not null;column:fee"` - SiteID *int64 `gorm:"type:numeric;not null;column:site_id"` - CreatedAt time.Time `gorm:"autoCreateTime;column:created_at"` - UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at"` - CreatedBy int64 `gorm:"type:int;column:created_by"` - PaymentType string `gorm:"type:varchar;column:payment_type"` - UpdatedBy int64 `gorm:"type:int;column:updated_by"` - OrderItems []OrderItem `gorm:"foreignKey:OrderID;constraint:OnDelete:CASCADE;"` - Payment Payment `gorm:"foreignKey:OrderID;constraint:OnDelete:CASCADE;"` - User User `gorm:"foreignKey:CreatedBy;constraint:OnDelete:CASCADE;"` - Source string `gorm:"type:varchar;column:source"` + ID int64 `gorm:"primaryKey;autoIncrement;column:id"` + RefID string `gorm:"type:varchar;column:ref_id"` + PartnerID int64 `gorm:"type:int;column:partner_id"` + Status string `gorm:"type:varchar;column:status"` + Amount float64 `gorm:"type:numeric;not null;column:amount"` + Total float64 `gorm:"type:numeric;not null;column:total"` + Fee float64 `gorm:"type:numeric;not null;column:fee"` + SiteID *int64 `gorm:"type:numeric;not null;column:site_id"` + CreatedAt time.Time `gorm:"autoCreateTime;column:created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at"` + CreatedBy int64 `gorm:"type:int;column:created_by"` + PaymentType string `gorm:"type:varchar;column:payment_type"` + UpdatedBy int64 `gorm:"type:int;column:updated_by"` + OrderItems []OrderItem `gorm:"foreignKey:OrderID;constraint:OnDelete:CASCADE;"` + Payment Payment `gorm:"foreignKey:OrderID;constraint:OnDelete:CASCADE;"` + User User `gorm:"foreignKey:CreatedBy;constraint:OnDelete:CASCADE;"` + Source string `gorm:"type:varchar;column:source"` + TicketStatus string `gorm:"type:varchar;column:ticket_status"` + VisitDate time.Time `gorm:"type:date;column:visit_date"` } type OrderDB struct { @@ -45,6 +47,16 @@ type OrderResponse struct { Token string } +type CheckinResponse struct { + Order *Order + Token string +} + +type CheckinExecute struct { + Order *Order + Token string +} + type ExecuteOrderResponse struct { Order *Order QRCode string @@ -107,19 +119,19 @@ type CallbackRequest struct { } type HistoryOrder struct { - ID int64 `gorm:"primaryKey;autoIncrement;column:id"` - Employee string `gorm:"type:varchar;column:employee"` - Site string `gorm:"type:varchar;column:site"` - Timestamp time.Time `gorm:"autoCreateTime;column:timestamp"` - BookingTime time.Time `gorm:"autoCreateTime;column:booking_time"` - Tickets []string `gorm:"-"` - RawTickets string `gorm:"type:text;column:tickets"` - PaymentType string `gorm:"type:varchar;column:payment_type"` - Status string `gorm:"type:varchar;column:status"` - Amount float64 `gorm:"type:numeric;column:amount"` - VisitDate time.Time `gorm:"type:date;column:visit_date"` - TicketStatus string `gorm:"type:varchar;column:ticket_status"` - Source string `gorm:"type:numeric;column:source"` + ID int64 `gorm:"primaryKey;autoIncrement;column:id"` + Employee string `gorm:"type:varchar;column:employee"` + Site string `gorm:"type:varchar;column:site"` + Timestamp time.Time `gorm:"autoCreateTime;column:timestamp"` + BookingTime time.Time `gorm:"autoCreateTime;column:booking_time"` + Tickets []string `gorm:"-"` + RawTickets string `gorm:"type:text;column:tickets"` + PaymentType string `gorm:"type:varchar;column:payment_type"` + Status string `gorm:"type:varchar;column:status"` + Amount float64 `gorm:"type:numeric;column:amount"` + VisitDate time.Time `gorm:"type:date;column:visit_date"` + TicketStatus string `gorm:"type:varchar;column:ticket_status"` + Source string `gorm:"type:numeric;column:source"` } type HistoryOrderDB struct { @@ -139,7 +151,7 @@ type OrderSearch struct { EndDate string Period string IsCustomer bool - Source string + Source string } type HistoryOrderList []*HistoryOrderDB @@ -152,19 +164,19 @@ func (b *HistoryOrder) ToHistoryOrderDB() *HistoryOrderDB { func (e *HistoryOrderDB) ToHistoryOrder() *HistoryOrder { return &HistoryOrder{ - ID: e.ID, - Employee: e.Employee, - Site: e.Site, - Timestamp: e.Timestamp, - BookingTime: e.BookingTime, - Tickets: e.Tickets, - RawTickets: e.RawTickets, - PaymentType: e.PaymentType, - Status: e.Status, - Amount: e.Amount, - VisitDate: e.VisitDate, + ID: e.ID, + Employee: e.Employee, + Site: e.Site, + Timestamp: e.Timestamp, + BookingTime: e.BookingTime, + Tickets: e.Tickets, + RawTickets: e.RawTickets, + PaymentType: e.PaymentType, + Status: e.Status, + Amount: e.Amount, + VisitDate: e.VisitDate, TicketStatus: e.TicketStatus, - Source: e.Source, + Source: e.Source, } } diff --git a/internal/entity/product.go b/internal/entity/product.go index bf93711..08046f6 100644 --- a/internal/entity/product.go +++ b/internal/entity/product.go @@ -21,6 +21,8 @@ type Product struct { DeletedAt *time.Time `gorm:"column:deleted_at"` CreatedBy int64 `gorm:"type:int;column:created_by"` UpdatedBy int64 `gorm:"type:int;column:updated_by"` + Region string `gorm:"type:varchar;column:region"` + Regency string `gorm:"type:varchar;column:regency"` } func (Product) TableName() string { diff --git a/internal/entity/sites.go b/internal/entity/sites.go index cc8edb3..247ede4 100644 --- a/internal/entity/sites.go +++ b/internal/entity/sites.go @@ -29,6 +29,8 @@ type Site struct { Longitude *float64 `json:"longitude"` Region string `json:"region"` Regency string `json:"regency"` + Lat float64 `json:"lat"` + Long float64 `json:"long"` Distance float64 `gorm:"-"` } diff --git a/internal/handlers/http/discovery/discover.go b/internal/handlers/http/discovery/discover.go index 2036379..05cff71 100644 --- a/internal/handlers/http/discovery/discover.go +++ b/internal/handlers/http/discovery/discover.go @@ -197,6 +197,8 @@ func ConvertEntityToGetByIDResp(resp *entity.Site) *response.SearchSiteByIDRespo TnC: resp.TnC, AdditionalInfo: resp.AdditionalInfo, PartnerID: resp.PartnerID, + Regency: resp.Regency, + Region: resp.Region, } } diff --git a/internal/handlers/http/order/order.go b/internal/handlers/http/order/order.go index 7b68550..4dee42c 100644 --- a/internal/handlers/http/order/order.go +++ b/internal/handlers/http/order/order.go @@ -24,6 +24,8 @@ func (h *Handler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) { route.POST("/execute", jwt, h.Execute) route.GET("/history", jwt, h.GetAllHistoryOrders) route.GET("/ticket-sold", jwt, h.CountSoldOfTicket) + route.POST("/checkin/inquiry", jwt, h.CheckInInquiry) + route.POST("/checkin/execute", jwt, h.CheckInExecute) route.GET("/sum-amount", jwt, h.SumAmount) route.GET("/daily-sales", jwt, h.GetDailySalesTicket) route.GET("/payment-distribution", jwt, h.GetPaymentDistributionChart) @@ -106,6 +108,66 @@ func (h *Handler) Execute(c *gin.Context) { }) } +func (h *Handler) CheckInInquiry(c *gin.Context) { + ctx := request.GetMyContext(c) + + var req request.Checkin + if err := c.ShouldBindJSON(&req); err != nil { + response.ErrorWrapper(c, errors.ErrorBadRequest) + return + } + + if !ctx.IsCasheer() || req.QRCode == "" { + response.ErrorWrapper(c, errors.ErrorBadRequest) + return + } + + partnerID := ctx.GetPartnerID() + + resp, err := h.service.CheckInInquiry(ctx, req.QRCode, partnerID) + if err != nil { + response.ErrorWrapper(c, err) + return + } + + c.JSON(http.StatusOK, response.BaseResponse{ + Success: true, + Status: http.StatusOK, + Data: response.CheckingInquiryResponse{ + Token: resp.Token, + }, + }) +} + +func (h *Handler) CheckInExecute(c *gin.Context) { + ctx := request.GetMyContext(c) + + var req request.CheckinExecute + if err := c.ShouldBindJSON(&req); err != nil { + response.ErrorWrapper(c, errors.ErrorBadRequest) + return + } + + if !ctx.IsCasheer() || req.Token == "" { + response.ErrorWrapper(c, errors.ErrorBadRequest) + return + } + + partnerID := ctx.GetPartnerID() + + resp, err := h.service.CheckInExecute(ctx, req.Token, partnerID) + if err != nil { + response.ErrorWrapper(c, err) + return + } + + c.JSON(http.StatusOK, response.BaseResponse{ + Success: true, + Status: http.StatusOK, + Data: MapOrderToExecuteCheckinResponse(resp.Order), + }) +} + func MapOrderToCreateOrderResponse(orderResponse *entity.OrderResponse) response.CreateOrderResponse { order := orderResponse.Order orderItems := make([]response.CreateOrderItemResponse, len(order.OrderItems)) @@ -162,20 +224,44 @@ func MapOrderToExecuteOrderResponse(orderResponse *entity.ExecuteOrderResponse) } } +func MapOrderToExecuteCheckinResponse(order *entity.Order) response.ExecuteCheckinResponse { + orderItems := make([]response.CreateOrderItemResponse, len(order.OrderItems)) + for i, item := range order.OrderItems { + orderItems[i] = response.CreateOrderItemResponse{ + ID: item.ID, + ItemID: item.ItemID, + Quantity: item.Quantity, + Price: item.Price, + Name: item.Product.Name, + } + } + + return response.ExecuteCheckinResponse{ + ID: order.ID, + RefID: order.RefID, + PartnerID: order.PartnerID, + Status: order.Status, + Amount: order.Amount, + PaymentType: order.PaymentType, + CreatedAt: order.CreatedAt, + OrderItems: orderItems, + } +} + func (h *Handler) toHistoryOrderResponse(resp *entity.HistoryOrder) response.HistoryOrder { return response.HistoryOrder{ - ID: resp.ID, - Employee: resp.Employee, - Site: resp.Site, - Timestamp: resp.Timestamp.Format(time.RFC3339), - BookingTime: resp.BookingTime.Format(time.RFC3339), - Tickets: resp.Tickets, - PaymentType: resp.PaymentType, - Status: resp.Status, - Amount: resp.Amount, - VisitDate: resp.VisitDate.Format(time.RFC3339), + ID: resp.ID, + Employee: resp.Employee, + Site: resp.Site, + Timestamp: resp.Timestamp.Format(time.RFC3339), + BookingTime: resp.BookingTime.Format(time.RFC3339), + Tickets: resp.Tickets, + PaymentType: resp.PaymentType, + Status: resp.Status, + Amount: resp.Amount, + VisitDate: resp.VisitDate.Format(time.RFC3339), TicketStatus: resp.TicketStatus, - Source: resp.Source, + Source: resp.Source, } } diff --git a/internal/handlers/http/sites/sites.go b/internal/handlers/http/sites/sites.go index 53e1c86..67b4f51 100644 --- a/internal/handlers/http/sites/sites.go +++ b/internal/handlers/http/sites/sites.go @@ -1,6 +1,7 @@ package site import ( + "fmt" "furtuna-be/internal/common/errors" "furtuna-be/internal/entity" "furtuna-be/internal/handlers/request" @@ -287,6 +288,7 @@ func (h *Handler) toSiteResponse(resp *entity.Site) response.Site { CreatedAt: resp.CreatedAt.Format(time.RFC3339), UpdatedAt: resp.UpdatedAt.Format(time.RFC3339), Products: h.toProductResponseList(resp.Products), + LatLong: fmt.Sprintf("%f,%f", resp.Lat, resp.Long), } } diff --git a/internal/handlers/request/order.go b/internal/handlers/request/order.go index 65769c2..0c1add4 100644 --- a/internal/handlers/request/order.go +++ b/internal/handlers/request/order.go @@ -47,6 +47,14 @@ type OrderParam struct { Source string `form:"source" json:"source" example:"tes"` } +type Checkin struct { + QRCode string `json:"qr_code" validate:"required"` +} + +type CheckinExecute struct { + Token string `json:"token" validate:"required"` +} + func (o *OrderParam) ToOrderEntity(ctx mycontext.Context) entity.OrderSearch { return entity.OrderSearch{ PartnerID: ctx.GetPartnerID(), diff --git a/internal/handlers/request/site.go b/internal/handlers/request/site.go index ebf0b8f..1611f77 100644 --- a/internal/handlers/request/site.go +++ b/internal/handlers/request/site.go @@ -3,6 +3,9 @@ package request import ( "furtuna-be/internal/common/mycontext" "furtuna-be/internal/entity" + "log" + "strconv" + "strings" ) type Site struct { @@ -21,6 +24,9 @@ type Site struct { IsSeasonTicket bool `json:"is_season_ticket"` IsDiscountActive bool `json:"is_discount_active"` Products []Product `json:"products"` + Region string `json:"region"` + Regency string `json:"regency"` + LatLong string `json:"lat_long"` } func (r *Site) ToEntity(createdBy int64) *entity.Site { @@ -39,6 +45,17 @@ func (r *Site) ToEntity(createdBy int64) *entity.Site { CreatedBy: createdBy, }) } + latLong := strings.Split(r.LatLong, ".") + + lat, err := strconv.ParseFloat(latLong[0], 64) + if err != nil { + log.Fatalf("Error converting latitude: %v", err) + } + + long, err := strconv.ParseFloat(latLong[1], 64) + if err != nil { + log.Fatalf("Error converting longitude: %v", err) + } return &entity.Site{ ID: r.ID, @@ -56,6 +73,10 @@ func (r *Site) ToEntity(createdBy int64) *entity.Site { IsSeasonTicket: r.IsSeasonTicket, IsDiscountActive: r.IsDiscountActive, Products: products, + Region: r.Region, + Regency: r.Regency, + Lat: lat, + Long: long, } } diff --git a/internal/handlers/response/discovery.go b/internal/handlers/response/discovery.go index c53a161..a0ad607 100644 --- a/internal/handlers/response/discovery.go +++ b/internal/handlers/response/discovery.go @@ -61,6 +61,8 @@ type SearchSiteByIDResponse struct { TnC string `json:"tn_c"` AdditionalInfo string `json:"additional_info"` Status string `json:"status"` + Region string `json:"region"` + Regency string `json:"regency"` } type SearchProductSiteByIDResponse struct { diff --git a/internal/handlers/response/order.go b/internal/handlers/response/order.go index c4c6019..2c501ba 100644 --- a/internal/handlers/response/order.go +++ b/internal/handlers/response/order.go @@ -26,18 +26,18 @@ type OrderAmount struct { } type HistoryOrder struct { - ID int64 `json:"id"` - Employee string `json:"employee"` - Site string `json:"site"` - Timestamp string `json:"timestamp"` - BookingTime string `json:"booking_time"` - Tickets []string `json:"tickets"` - PaymentType string `json:"payment_type"` - Status string `json:"status"` - Amount float64 `json:"amount"` - VisitDate string `json:"visit_date"` - TicketStatus string `json:"ticket_status"` - Source string `json:"source"` + ID int64 `json:"id"` + Employee string `json:"employee"` + Site string `json:"site"` + Timestamp string `json:"timestamp"` + BookingTime string `json:"booking_time"` + Tickets []string `json:"tickets"` + PaymentType string `json:"payment_type"` + Status string `json:"status"` + Amount float64 `json:"amount"` + VisitDate string `json:"visit_date"` + TicketStatus string `json:"ticket_status"` + Source string `json:"source"` } type OrderItem struct { @@ -110,6 +110,24 @@ type ExecuteOrderResponse struct { QRcode string `json:"qr_code"` } +type ExecuteCheckinResponse struct { + ID int64 `json:"id"` + RefID string `json:"ref_id"` + PartnerID int64 `json:"partner_id"` + Status string `json:"status"` + Amount float64 `json:"amount"` + PaymentType string `json:"payment_type"` + CreatedAt time.Time `json:"created_at"` + OrderItems []CreateOrderItemResponse `json:"order_items"` + PaymentToken string `json:"payment_token"` + RedirectURL string `json:"redirect_url"` + QRcode string `json:"qr_code"` +} + +type CheckingInquiryResponse struct { + Token string `json:"token"` +} + type CreateOrderItemResponse struct { ID int64 `json:"id"` ItemID int64 `json:"item_id"` diff --git a/internal/handlers/response/site.go b/internal/handlers/response/site.go index 6c12b77..c0d0d35 100644 --- a/internal/handlers/response/site.go +++ b/internal/handlers/response/site.go @@ -1,23 +1,24 @@ package response type Site struct { - ID *int64 `json:"id"` - Name string `json:"name"` - PartnerID int64 `json:"partner_id"` - Image string `json:"image"` - Address string `json:"address"` - LocationLink string `json:"location_link"` - Description string `json:"description"` - Highlight string `json:"highlight"` - ContactPerson string `json:"contact_person"` - TnC string `json:"tnc"` - AdditionalInfo string `json:"additional_info"` - Status string `json:"status"` - IsSeasonTicket bool `json:"is_season_ticket"` - IsDiscountActive bool `json:"is_discount_active"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` + ID *int64 `json:"id"` + Name string `json:"name"` + PartnerID int64 `json:"partner_id"` + Image string `json:"image"` + Address string `json:"address"` + LocationLink string `json:"location_link"` + Description string `json:"description"` + Highlight string `json:"highlight"` + ContactPerson string `json:"contact_person"` + TnC string `json:"tnc"` + AdditionalInfo string `json:"additional_info"` + Status string `json:"status"` + IsSeasonTicket bool `json:"is_season_ticket"` + IsDiscountActive bool `json:"is_discount_active"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` Products []Product `json:"products"` + LatLong string `json:"lat_long"` } type SiteName struct { diff --git a/internal/repository/orders/order.go b/internal/repository/orders/order.go index e30ebc6..3a79976 100644 --- a/internal/repository/orders/order.go +++ b/internal/repository/orders/order.go @@ -64,6 +64,26 @@ func (r *OrderRepository) FindByID(ctx context.Context, id int64) (*entity.Order return &order, nil } +func (r *OrderRepository) FindByQRCode(ctx context.Context, refID string) (*entity.Order, error) { + var order entity.Order + + err := r.db.WithContext(ctx). + Preload("OrderItems", func(db *gorm.DB) *gorm.DB { + return db.Preload("Product") + }). + Preload("User"). + Preload("Payment"). + Where("ref_id = ?", refID). + First(&order).Error + + if err != nil { + logger.ContextLogger(ctx).Error("error when finding order by refID", zap.Error(err)) + return nil, err + } + + return &order, nil +} + func (r *OrderRepository) SetOrderStatus(ctx context.Context, db *gorm.DB, orderID int64, status string) error { var order entity.Order if err := db.WithContext(ctx).Preload("OrderItems").First(&order, orderID).Error; err != nil { @@ -100,7 +120,7 @@ func (b *OrderRepository) GetAllHystoryOrders(ctx context.Context, req entity.Or Joins("left join sites on orders.site_id = sites.id"). Where("orders.status != ?", "NEW") - if req.PaymentType != "" { + if req.PaymentType != "" { query = query.Where("orders.payment_type = ?", req.PaymentType) } diff --git a/internal/repository/repository.go b/internal/repository/repository.go index fa31ad4..6b782ae 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -141,6 +141,7 @@ type Product interface { type Order interface { Create(ctx context.Context, order *entity.Order) (*entity.Order, error) FindByID(ctx context.Context, id int64) (*entity.Order, error) + FindByQRCode(ctx context.Context, refID string) (*entity.Order, error) Update(ctx context.Context, order *entity.Order) (*entity.Order, error) SetOrderStatus(ctx context.Context, db *gorm.DB, orderID int64, status string) error GetAllHystoryOrders(ctx context.Context, req entity.OrderSearch) (entity.HistoryOrderList, int, error) diff --git a/internal/repository/sites/sites.go b/internal/repository/sites/sites.go index 4639ff0..791dbc3 100644 --- a/internal/repository/sites/sites.go +++ b/internal/repository/sites/sites.go @@ -93,6 +93,7 @@ func (r *SiteRepository) GetAll(ctx context.Context, req entity.SiteSearch) (ent query := r.db query = query.Where("deleted_at IS NULL") + query = query.Where("status is ?", "Active") if req.Search != "" { query = query.Where("name ILIKE ?", "%"+req.Search+"%") diff --git a/internal/services/order/order.go b/internal/services/order/order.go index 4c372c2..d81fa8c 100644 --- a/internal/services/order/order.go +++ b/internal/services/order/order.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + errors2 "furtuna-be/internal/common/errors" "furtuna-be/internal/common/logger" "furtuna-be/internal/common/mycontext" order2 "furtuna-be/internal/constants/order" @@ -121,6 +122,77 @@ func (s *OrderService) CreateOrder(ctx mycontext.Context, req *entity.OrderReque }, nil } +func (s *OrderService) CheckInInquiry(ctx mycontext.Context, qrCode string, partnerID *int64) (*entity.CheckinResponse, error) { + order, err := s.repo.FindByQRCode(ctx, qrCode) + if err != nil { + logger.ContextLogger(ctx).Error("error when getting order by QR code", zap.Error(err)) + return nil, err + } + + if order.PartnerID != *partnerID { + return nil, errors2.ErrorBadRequest + } + + if order.TicketStatus == "USED" { + return nil, errors2.ErrorTicketInvalidOrAlreadyUsed + } + + today := time.Now().Format("2006-01-02") + visitDate := order.VisitDate.Format("2006-01-02") + + if visitDate != today { + return nil, errors2.ErrorTicketInvalidOrAlreadyUsed + } + + token, err := s.crypt.GenerateJWTOrder(order) + if err != nil { + logger.ContextLogger(ctx).Error("error when generate checkin token", zap.Error(err)) + return nil, err + } + + orderResponse := &entity.CheckinResponse{ + Token: token, + } + + return orderResponse, nil +} + +func (s *OrderService) CheckInExecute(ctx mycontext.Context, + token string, partnerID *int64) (*entity.CheckinExecute, error) { + pID, orderID, err := s.crypt.ValidateJWTOrder(token) + if err != nil { + logger.ContextLogger(ctx).Error("error when validating JWT order", zap.Error(err)) + return nil, err + } + + if pID != *partnerID { + return nil, errors2.ErrorBadRequest + } + + order, err := s.repo.FindByID(ctx, orderID) + if err != nil { + logger.ContextLogger(ctx).Error("error when getting order by ID", zap.Error(err)) + return nil, err + } + + resp := &entity.CheckinExecute{ + Order: order, + } + + if order.Status != "UNUSED" { + return resp, nil + } + + order.Status = "USED" + order, err = s.repo.Update(ctx, order) + if err != nil { + logger.ContextLogger(ctx).Error("error when updating order status", zap.Error(err)) + return nil, err + } + + return resp, nil +} + func (s *OrderService) Execute(ctx context.Context, req *entity.OrderExecuteRequest) (*entity.ExecuteOrderResponse, error) { partnerID, orderID, err := s.crypt.ValidateJWTOrder(req.Token) if err != nil { @@ -376,7 +448,7 @@ func (s *OrderService) GetAllHistoryOrders(ctx mycontext.Context, req entity.Ord return data, total, nil } -func (s OrderService) CountSoldOfTicket(ctx mycontext.Context, req entity.OrderSearch) (*entity.TicketSold, error) { +func (s *OrderService) CountSoldOfTicket(ctx mycontext.Context, req entity.OrderSearch) (*entity.TicketSold, error) { ticket, err := s.repo.CountSoldOfTicket(ctx, req) if err != nil { @@ -389,7 +461,7 @@ func (s OrderService) CountSoldOfTicket(ctx mycontext.Context, req entity.OrderS return data, nil } -func (s OrderService) GetDailySales(ctx mycontext.Context, req entity.OrderSearch) ([]entity.ProductDailySales, error) { +func (s *OrderService) GetDailySales(ctx mycontext.Context, req entity.OrderSearch) ([]entity.ProductDailySales, error) { dailySales, err := s.repo.GetDailySalesMetrics(ctx, req) if err != nil { @@ -400,7 +472,7 @@ func (s OrderService) GetDailySales(ctx mycontext.Context, req entity.OrderSearc return dailySales, nil } -func (s OrderService) GetPaymentDistribution(ctx mycontext.Context, req entity.OrderSearch) ([]entity.PaymentTypeDistribution, error) { +func (s *OrderService) GetPaymentDistribution(ctx mycontext.Context, req entity.OrderSearch) ([]entity.PaymentTypeDistribution, error) { paymentDistribution, err := s.repo.GetPaymentTypeDistribution(ctx, req) if err != nil { @@ -411,7 +483,7 @@ func (s OrderService) GetPaymentDistribution(ctx mycontext.Context, req entity.O return paymentDistribution, nil } -func (s OrderService) SumAmount(ctx mycontext.Context, req entity.OrderSearch) (*entity.Order, error) { +func (s *OrderService) SumAmount(ctx mycontext.Context, req entity.OrderSearch) (*entity.Order, error) { amount, err := s.repo.SumAmount(ctx, req) if err != nil { diff --git a/internal/services/service.go b/internal/services/service.go index 54af676..619ac31 100644 --- a/internal/services/service.go +++ b/internal/services/service.go @@ -112,6 +112,9 @@ type Product interface { type Order interface { CreateOrder(ctx mycontext.Context, req *entity.OrderRequest) (*entity.OrderResponse, error) + CheckInInquiry(ctx mycontext.Context, qrCode string, partnerID *int64) (*entity.CheckinResponse, error) + CheckInExecute(ctx mycontext.Context, + token string, partnerID *int64) (*entity.CheckinExecute, error) Execute(ctx context.Context, req *entity.OrderExecuteRequest) (*entity.ExecuteOrderResponse, error) ProcessCallback(ctx context.Context, req *entity.CallbackRequest) error GetAllHistoryOrders(ctx mycontext.Context, req entity.OrderSearch) ([]*entity.HistoryOrder, int, error)