Add Checkin
This commit is contained in:
parent
4c8999a3cf
commit
e544ef8f71
@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@ -19,6 +19,7 @@ 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 (
|
||||
@ -36,6 +37,7 @@ var (
|
||||
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
|
||||
}
|
||||
|
||||
@ -22,6 +22,8 @@ type Order struct {
|
||||
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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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:"-"`
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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,6 +224,30 @@ 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,
|
||||
|
||||
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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"`
|
||||
|
||||
@ -18,6 +18,7 @@ type Site struct {
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
Products []Product `json:"products"`
|
||||
LatLong string `json:"lat_long"`
|
||||
}
|
||||
|
||||
type SiteName struct {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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+"%")
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user