From 18003313dd273b806d152255d36776188ee54668 Mon Sep 17 00:00:00 2001 From: "aditya.siregar" Date: Sat, 8 Mar 2025 00:35:23 +0700 Subject: [PATCH] Update template email --- go.mod | 1 + infra/enaklopos.development.yaml | 16 +- internal/common/errors/errors.go | 1 + internal/constants/constants.go | 43 +++ internal/entity/cust.go | 8 + internal/entity/jwt.go | 5 +- internal/entity/order.go | 63 ++-- internal/entity/order_inquiry.go | 114 +++++++ internal/entity/product.go | 14 +- internal/entity/transaction.go | 6 +- internal/entity/user.go | 2 + internal/handlers/http/customerorder/order.go | 14 +- internal/handlers/http/discovery/discover.go | 1 - internal/handlers/http/order.go | 126 ++++++++ internal/handlers/http/order/order.go | 16 +- internal/handlers/http/oss/oss.go | 4 +- internal/handlers/http/product/product.go | 2 +- internal/handlers/http/sites/sites.go | 1 - internal/handlers/request/order.go | 20 +- internal/handlers/request/product.go | 3 +- internal/handlers/response/discovery.go | 4 +- internal/handlers/response/order.go | 10 +- internal/handlers/response/order_inquiry.go | 119 +++++++ internal/handlers/response/product.go | 1 - internal/middlewares/auth.go | 1 - internal/repository/brevo/init.go | 27 +- internal/repository/crypto/init.go | 43 +++ internal/repository/customer_repo.go | 128 ++++++++ internal/repository/models/customer.go | 19 ++ internal/repository/models/order.go | 80 +++++ internal/repository/models/product.go | 22 ++ internal/repository/models/transaction.go | 21 ++ internal/repository/orde_repo.go | 292 ++++++++++++++++++ internal/repository/oss/oss.go | 2 +- internal/repository/product_repo.go | 82 +++++ internal/repository/products/product.go | 8 - internal/repository/repository.go | 12 + internal/repository/sites/sites.go | 3 - internal/repository/transaction_repo.go | 76 +++++ internal/routes/routes.go | 16 + internal/services/balance/balance.go | 1 - internal/services/order/order.go | 84 +---- internal/services/service.go | 12 + internal/services/v2/customer/customer.go | 117 +++++++ .../services/v2/order/create_order_inquiry.go | 144 +++++++++ internal/services/v2/order/execute_order.go | 173 +++++++++++ internal/services/v2/order/order.go | 80 +++++ .../services/v2/product/get_product_by_id.go | 33 ++ .../v2/product/get_product_details.go | 56 ++++ internal/services/v2/product/product.go | 26 ++ k8s/staging/ingress.yaml | 2 +- main.go | 1 + templates/monthly_points.html | 159 ++++++++++ templates/transaction_receipt.html | 194 ++++++++++++ 54 files changed, 2309 insertions(+), 199 deletions(-) create mode 100644 internal/entity/cust.go create mode 100644 internal/entity/order_inquiry.go create mode 100644 internal/handlers/http/order.go create mode 100644 internal/handlers/response/order_inquiry.go create mode 100644 internal/repository/customer_repo.go create mode 100644 internal/repository/models/customer.go create mode 100644 internal/repository/models/order.go create mode 100644 internal/repository/models/product.go create mode 100644 internal/repository/models/transaction.go create mode 100644 internal/repository/orde_repo.go create mode 100644 internal/repository/product_repo.go create mode 100644 internal/repository/transaction_repo.go create mode 100644 internal/services/v2/customer/customer.go create mode 100644 internal/services/v2/order/create_order_inquiry.go create mode 100644 internal/services/v2/order/execute_order.go create mode 100644 internal/services/v2/order/order.go create mode 100644 internal/services/v2/product/get_product_by_id.go create mode 100644 internal/services/v2/product/get_product_details.go create mode 100644 internal/services/v2/product/product.go create mode 100644 templates/monthly_points.html create mode 100644 templates/transaction_receipt.html diff --git a/go.mod b/go.mod index ff8f9fe..5db9d96 100644 --- a/go.mod +++ b/go.mod @@ -80,6 +80,7 @@ require ( require ( github.com/aws/aws-sdk-go v1.50.0 github.com/getbrevo/brevo-go v1.0.0 + github.com/pkg/errors v0.9.1 github.com/veritrans/go-midtrans v0.0.0-20210616100512-16326c5eeb00 github.com/xuri/excelize/v2 v2.9.0 go.uber.org/zap v1.21.0 diff --git a/infra/enaklopos.development.yaml b/infra/enaklopos.development.yaml index 81a67a9..06849d5 100644 --- a/infra/enaklopos.development.yaml +++ b/infra/enaklopos.development.yaml @@ -28,12 +28,12 @@ postgresql: debug: false oss: - access_key_id: e50b31e5eddf63c0ZKB2 - access_key_secret: GAyX9jiCWyTwgJMuqzun2x0zHS3kjQt26kyzY21S - endpoint: obs.eranyacloud.com - bucket_name: enaklo-pos - log_level: Error # type: LogOff, Debug, Error, Warn, Info - host_url: https://obs.eranyacloud.com + access_key_id: cf9a475e18bc7626cbdbf09709d82a64 + access_key_secret: 91f3321294d3e23035427a0ecb893ada + endpoint: sin1.contabostorage.com + bucket_name: enaklo + log_level: Error + host_url: 'https://sin1.contabostorage.com/fda98c2228f246f29a7e466b86b3b9e7:' midtrans: server_key: "SB-Mid-server-YOIvuaIlRw3In9SymCuFz-hB" @@ -50,10 +50,10 @@ linkqu: callback_url: "https://enaklo-pos-be.app-dev.altru.id/api/v1/linkqu/callback" brevo: - api_key: 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 + api_key: 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 email: - sender: "enaklo-pos.official@gmail.com" + sender: "noreply@enaklo.co.id" sender_customer: "enaklo-pos.official@gmail.com" reset_password: template_name: "reset_password" diff --git a/internal/common/errors/errors.go b/internal/common/errors/errors.go index 9bea836..f3dba47 100644 --- a/internal/common/errors/errors.go +++ b/internal/common/errors/errors.go @@ -21,6 +21,7 @@ const ( errInsufficientBalance ErrType = "Insufficient Balance" errInactivePartner ErrType = "Partner's license is invalid or has expired. Please contact Admin Support." errTicketAlreadyUsed ErrType = "Ticket Already Used." + errProductIsRequired ErrType = "Product" ) var ( diff --git a/internal/constants/constants.go b/internal/constants/constants.go index 3564685..0e543ef 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -1,5 +1,10 @@ package constants +import ( + "github.com/google/uuid" + "time" +) + const ( ContextRequestID string = "requestId" ) @@ -9,3 +14,41 @@ type UserType string func (u UserType) toString() string { return string(u) } + +const ( + StatusPending = "PENDING" + StatusPaid = "PAID" + StatusCanceled = "CANCELED" + StatusExpired = "EXPIRED" + StatusExecuted = "EXECUTED" +) + +const ( + PaymentCash = "CASH" + PaymentCreditCard = "CREDIT_CARD" + PaymentDebitCard = "DEBIT_CARD" + PaymentEWallet = "E_WALLET" +) + +const ( + SourcePOS = "POS" + SourceMobile = "MOBILE" + SourceWeb = "WEB" +) + +const ( + DefaultInquiryExpiryDuration = 30 * time.Minute +) + +func GenerateUUID() string { + return uuid.New().String() +} + +func GenerateRefID() string { + now := time.Now() + return now.Format("20060102") + "-" + uuid.New().String()[:8] +} + +var TimeNow = func() time.Time { + return time.Now() +} diff --git a/internal/entity/cust.go b/internal/entity/cust.go new file mode 100644 index 0000000..680f43b --- /dev/null +++ b/internal/entity/cust.go @@ -0,0 +1,8 @@ +package entity + +type CustomerResolutionRequest struct { + ID *int64 + Name string + Email string + PhoneNumber string +} diff --git a/internal/entity/jwt.go b/internal/entity/jwt.go index ccf7855..adb8b2b 100644 --- a/internal/entity/jwt.go +++ b/internal/entity/jwt.go @@ -14,8 +14,9 @@ type JWTAuthClaims struct { } type JWTOrderClaims struct { - PartnerID int64 `json:"id"` - OrderID int64 `json:"order_id"` + PartnerID int64 `json:"id"` + OrderID int64 `json:"order_id"` + InquiryID string `json:"inquiry_id"` jwt.StandardClaims } diff --git a/internal/entity/order.go b/internal/entity/order.go index b847730..6b57f43 100644 --- a/internal/entity/order.go +++ b/internal/entity/order.go @@ -1,32 +1,28 @@ package entity import ( - "gorm.io/datatypes" "time" ) 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"` - Site *Site `gorm:"foreignKey:SiteID;constraint:OnDelete:CASCADE;"` - 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"` - Metadata datatypes.JSON `gorm:"type:json;not null;column:metadata"` + ID int64 `gorm:"primaryKey;autoIncrement;column: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"` + CustomerID *int64 + InquiryID *string + Site *Site `gorm:"foreignKey:SiteID;constraint:OnDelete:CASCADE;"` + 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"` } type OrderDB struct { @@ -47,7 +43,6 @@ func (e *OrderDB) ToSumAmount() *Order { type OrderResponse struct { Order *Order - Token string } type CheckinResponse struct { @@ -80,7 +75,7 @@ type OrderItem struct { ItemID int64 `gorm:"type:int;column:item_id"` ItemType string `gorm:"type:varchar;column:item_type"` Price float64 `gorm:"type:numeric;not null;column:price"` - Quantity int64 `gorm:"type:int;column:qty"` + Quantity int `gorm:"type:int;column:qty"` CreatedAt time.Time `gorm:"autoCreateTime;column:created_at"` UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at"` CreatedBy int64 `gorm:"type:int;column:created_by"` @@ -93,19 +88,20 @@ func (OrderItem) TableName() string { } type OrderRequest struct { - Source string - CreatedBy int64 - PartnerID int64 `json:"partner_id" validate:"required"` - PaymentMethod string `json:"payment_method" validate:"required"` - OrderItems []OrderItemRequest `json:"order_items" validate:"required,dive"` - VisitDate string `json:"visit_date"` - BankCode string `json:"bank_code"` - BankName string `json:"bank_name"` + Source string + CreatedBy int64 + PartnerID int64 + PaymentMethod string + OrderItems []OrderItemRequest + CustomerID *int64 + CustomerName string + CustomerEmail string + CustomerPhoneNumber string } type OrderItemRequest struct { ProductID int64 `json:"product_id" validate:"required"` - Quantity int64 `json:"quantity" validate:"required"` + Quantity int `json:"quantity" validate:"required"` } type OrderExecuteRequest struct { @@ -117,7 +113,6 @@ type OrderExecuteRequest struct { func (o *Order) SetExecutePaymentStatus() { if o.PaymentType == "CASH" { o.Status = "PAID" - o.TicketStatus = "USED" return } o.Status = "PENDING" diff --git a/internal/entity/order_inquiry.go b/internal/entity/order_inquiry.go new file mode 100644 index 0000000..35da00f --- /dev/null +++ b/internal/entity/order_inquiry.go @@ -0,0 +1,114 @@ +package entity + +import ( + "enaklo-pos-be/internal/constants" + "time" +) + +type OrderInquiry struct { + ID string `json:"id"` + PartnerID int64 `json:"partner_id"` + CustomerID int64 `json:"customer_id,omitempty"` + CustomerName string `json:"customer_name"` + CustomerPhoneNumber string `json:"customer_phone_number"` + CustomerEmail string `json:"customer_email"` + Status string `json:"status"` + Amount float64 `json:"amount"` + Fee float64 `json:"fee"` + Total float64 `json:"total"` + PaymentType string `json:"payment_type"` + Source string `json:"source"` + CreatedBy int64 `json:"created_by"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + ExpiresAt time.Time `json:"expires_at"` + OrderItems []OrderItem `json:"order_items"` +} + +type OrderCalculation struct { + Subtotal float64 `json:"subtotal"` + Fee float64 `json:"fee"` + Total float64 `json:"total"` +} + +type OrderInquiryResponse struct { + OrderInquiry *OrderInquiry `json:"order_inquiry"` + Token string `json:"token"` +} + +func NewOrderInquiry( + partnerID int64, + customerID int64, + amount float64, + fee float64, + total float64, + paymentType string, + source string, + createdBy int64, + customerName string, + customerPhoneNumber string, + customerEmail string, +) *OrderInquiry { + return &OrderInquiry{ + ID: constants.GenerateUUID(), + PartnerID: partnerID, + Status: "PENDING", + Amount: amount, + Fee: fee, + Total: total, + PaymentType: paymentType, + CustomerID: customerID, + Source: source, + CreatedBy: createdBy, + CreatedAt: time.Now(), + ExpiresAt: time.Now().Add(2 * time.Minute), + OrderItems: []OrderItem{}, + CustomerName: customerName, + CustomerEmail: customerEmail, + CustomerPhoneNumber: customerPhoneNumber, + } +} + +func (oi *OrderInquiry) AddOrderItem(item OrderItemRequest, product *Product) { + oi.OrderItems = append(oi.OrderItems, OrderItem{ + ItemID: item.ProductID, + ItemType: product.Type, + Price: product.Price, + Quantity: item.Quantity, + CreatedBy: oi.CreatedBy, + Product: product, + }) +} + +func (i *OrderInquiry) ToOrder(paymentMethod string) *Order { + now := time.Now() + + order := &Order{ + PartnerID: i.PartnerID, + CustomerID: &i.CustomerID, + InquiryID: &i.ID, + Status: constants.StatusPaid, + Amount: i.Amount, + Fee: i.Fee, + Total: i.Total, + PaymentType: paymentMethod, + Source: i.Source, + CreatedBy: i.CreatedBy, + CreatedAt: now, + OrderItems: make([]OrderItem, len(i.OrderItems)), + } + + for idx, item := range i.OrderItems { + order.OrderItems[idx] = OrderItem{ + ItemID: item.ItemID, + ItemType: item.ItemType, + Price: item.Price, + Quantity: item.Quantity, + CreatedBy: i.CreatedBy, + CreatedAt: now, + Product: item.Product, + } + } + + return order +} diff --git a/internal/entity/product.go b/internal/entity/product.go index b896b46..6febc12 100644 --- a/internal/entity/product.go +++ b/internal/entity/product.go @@ -8,7 +8,6 @@ import ( type Product struct { ID int64 `gorm:"primaryKey;autoIncrement;column:id"` PartnerID int64 `gorm:"type:int;column:partner_id"` - SiteID int64 `gorm:"type:int;column:site_id"` Name string `gorm:"type:varchar(255);not null;column:name"` Type string `gorm:"type:varchar;column:type"` Price float64 `gorm:"type:decimal;column:price"` @@ -19,7 +18,7 @@ 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"` - Image string `gorm:"type:varchar;column:type"` + Image string `gorm:"type:varchar;column:image"` } func (Product) TableName() string { @@ -71,7 +70,7 @@ func (e *ProductDB) ToProduct() *Product { DeletedAt: e.DeletedAt, CreatedBy: e.CreatedBy, UpdatedBy: e.UpdatedBy, - SiteID: e.SiteID, + Image: e.Image, } } @@ -79,9 +78,7 @@ func (b *ProductList) ToProductList() []*Product { var Products []*Product for _, p := range *b { - if p.Status == "Available" { - Products = append(Products, p.ToProduct()) - } + Products = append(Products, p.ToProduct()) } return Products @@ -126,3 +123,8 @@ func (o *ProductDB) SetDeleted(updatedby int64) { o.DeletedAt = ¤tTime o.UpdatedBy = updatedby } + +type ProductDetails struct { + Products map[int64]*Product // Map for quick lookups by ID + PartnerID int64 // Common site ID for all products +} diff --git a/internal/entity/transaction.go b/internal/entity/transaction.go index 998349e..00f2f7e 100644 --- a/internal/entity/transaction.go +++ b/internal/entity/transaction.go @@ -5,17 +5,17 @@ import ( ) type Transaction struct { - ID string `gorm:"type:uuid;primaryKey;default:uuid_generate_v4()"` + ID string `gorm:"type:uuid;primaryKey;default:uuid_generate_v4()"` + OrderID int64 PartnerID int64 `gorm:"not null"` TransactionType string `gorm:"not null"` - ReferenceID string `gorm:"size:255"` Status string `gorm:"size:255"` CreatedBy int64 `gorm:"not null"` UpdatedBy int64 `gorm:"not null"` Amount float64 `gorm:"not null"` CreatedAt time.Time `gorm:"autoCreateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"` - SiteID *int64 + PaymentMethod string `json:"payment_method"` Fee float64 Total float64 } diff --git a/internal/entity/user.go b/internal/entity/user.go index 1d44fdf..9d4e425 100644 --- a/internal/entity/user.go +++ b/internal/entity/user.go @@ -34,6 +34,8 @@ type Customer struct { Name string Email string Password string + Phone string + Points int Status userstatus.UserStatus NIK string UserType string diff --git a/internal/handlers/http/customerorder/order.go b/internal/handlers/http/customerorder/order.go index 98e0ce8..46d4826 100644 --- a/internal/handlers/http/customerorder/order.go +++ b/internal/handlers/http/customerorder/order.go @@ -6,7 +6,6 @@ import ( "enaklo-pos-be/internal/handlers/request" "enaklo-pos-be/internal/handlers/response" "enaklo-pos-be/internal/services" - "enaklo-pos-be/internal/utils" "encoding/json" "net/http" "time" @@ -22,7 +21,7 @@ type Handler struct { func (h *Handler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) { route := group.Group("/order") - route.POST("/inquiry", jwt, h.Inquiry) + route.POST("/inquiry", h.Inquiry) route.POST("/execute", jwt, h.Execute) route.GET("/history", jwt, h.History) route.GET("/detail", jwt, h.Detail) @@ -104,20 +103,14 @@ func MapOrderToCreateOrderResponse(orderResponse *entity.OrderResponse, req requ return response.CreateOrderResponse{ ID: order.ID, - RefID: order.RefID, PartnerID: order.PartnerID, Status: order.Status, Amount: order.Amount, PaymentType: order.PaymentType, CreatedAt: order.CreatedAt, OrderItems: orderItems, - Token: orderResponse.Token, Fee: order.Fee, Total: order.Total, - VisitDate: order.VisitDate.Format("2006-01-02"), - SiteName: order.Site.Name, - BankCode: req.BankCode, - BankName: utils.BankName(req.BankCode), } } @@ -136,7 +129,6 @@ func MapOrderToExecuteOrderResponse(orderResponse *entity.ExecuteOrderResponse) return response.ExecuteOrderResponse{ ID: order.ID, - RefID: order.RefID, PartnerID: order.PartnerID, Status: order.Status, Amount: order.Amount, @@ -239,10 +231,6 @@ func (h *Handler) toOrderDetail(order *entity.Order) *response.OrderDetail { qrCode := "" - if order.Status == "PAID" { - qrCode = order.RefID - } - var siteName string if order.Site != nil { diff --git a/internal/handlers/http/discovery/discover.go b/internal/handlers/http/discovery/discover.go index 1634b9a..f751bf8 100644 --- a/internal/handlers/http/discovery/discover.go +++ b/internal/handlers/http/discovery/discover.go @@ -214,7 +214,6 @@ func ConvertToProductResp(resp []*entity.Product) *response.SearchProductSiteRes productResp = append(productResp, response.SearchProductSiteByIDResponse{ ID: res.ID, Name: res.Name, - SiteID: res.SiteID, Price: res.Price, Description: res.Description, Type: res.Type, diff --git a/internal/handlers/http/order.go b/internal/handlers/http/order.go new file mode 100644 index 0000000..4a2512d --- /dev/null +++ b/internal/handlers/http/order.go @@ -0,0 +1,126 @@ +package http + +import ( + "enaklo-pos-be/internal/common/errors" + "enaklo-pos-be/internal/entity" + "enaklo-pos-be/internal/handlers/request" + "enaklo-pos-be/internal/handlers/response" + "enaklo-pos-be/internal/services/v2/order" + "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" + "net/http" +) + +type Handler struct { + service order.Service +} + +func NewOrderHandler(service order.Service) *Handler { + return &Handler{ + service: service, + } +} + +func (h *Handler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) { + route := group.Group("/order") + + route.POST("/inquiry", jwt, h.Inquiry) + route.POST("/execute", jwt, h.Execute) +} + +type InquiryRequest struct { + CustomerID *int64 `json:"customer_id"` + CustomerName string `json:"customer_name" validate:"required_without=CustomerID"` + CustomerEmail string `json:"customer_email"` + CustomerPhoneNumber string `json:"customer_phone_number"` + PaymentMethod string `json:"payment_method" validate:"required"` + OrderItems []OrderItemRequest `json:"order_items" validate:"required,min=1,dive"` +} + +type OrderItemRequest struct { + ProductID int64 `json:"product_id" validate:"required"` + Quantity int `json:"quantity" validate:"required,min=1"` +} + +type ExecuteRequest struct { + PaymentMethod string `json:"payment_method" validate:"required"` + Token string `json:"token"` +} + +func (h *Handler) Inquiry(c *gin.Context) { + ctx := request.GetMyContext(c) + userID := ctx.RequestedBy() + partnerID := ctx.GetPartnerID() + + var req InquiryRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.ErrorWrapper(c, errors.ErrorBadRequest) + return + } + + validate := validator.New() + if err := validate.Struct(req); err != nil { + response.ErrorWrapper(c, err) + return + } + + orderItems := make([]entity.OrderItemRequest, len(req.OrderItems)) + for i, item := range req.OrderItems { + orderItems[i] = entity.OrderItemRequest{ + ProductID: item.ProductID, + Quantity: item.Quantity, + } + } + + orderReq := &entity.OrderRequest{ + Source: "POS", + CreatedBy: userID, + PartnerID: *partnerID, + PaymentMethod: req.PaymentMethod, + OrderItems: orderItems, + CustomerID: req.CustomerID, + CustomerName: req.CustomerName, + CustomerEmail: req.CustomerEmail, + CustomerPhoneNumber: req.CustomerPhoneNumber, + } + + result, err := h.service.CreateOrderInquiry(ctx, orderReq) + if err != nil { + response.ErrorWrapper(c, err) + return + } + + c.JSON(http.StatusOK, response.BaseResponse{ + Success: true, + Status: http.StatusOK, + Data: response.MapToInquiryResponse(result), + }) +} + +func (h *Handler) Execute(c *gin.Context) { + ctx := request.GetMyContext(c) + + var req ExecuteRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.ErrorWrapper(c, errors.ErrorBadRequest) + return + } + + validate := validator.New() + if err := validate.Struct(req); err != nil { + response.ErrorWrapper(c, err) + return + } + + result, err := h.service.ExecuteOrderInquiry(ctx, req.Token, req.PaymentMethod) + if err != nil { + response.ErrorWrapper(c, err) + return + } + + c.JSON(http.StatusOK, response.BaseResponse{ + Success: true, + Status: http.StatusOK, + Data: response.MapToOrderResponse(result), + }) +} diff --git a/internal/handlers/http/order/order.go b/internal/handlers/http/order/order.go index 3a35a29..f966e4d 100644 --- a/internal/handlers/http/order/order.go +++ b/internal/handlers/http/order/order.go @@ -47,21 +47,15 @@ func (h *Handler) Inquiry(c *gin.Context) { return } - if !ctx.IsCasheer() { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - // override the partner_id - req.PartnerID = *ctx.GetPartnerID() - validate := validator.New() if err := validate.Struct(req); err != nil { response.ErrorWrapper(c, err) return } - order, err := h.service.CreateOrder(ctx, req.ToEntity(ctx.RequestedBy())) + orderRequest := req.ToEntity(*ctx.GetPartnerID(), ctx.RequestedBy()) + + order, err := h.service.CreateOrder(ctx, orderRequest) if err != nil { response.ErrorWrapper(c, err) return @@ -206,7 +200,6 @@ func MapOrderToCreateOrderResponse(orderResponse *entity.OrderResponse) response return response.CreateOrderResponse{ ID: order.ID, - RefID: order.RefID, PartnerID: order.PartnerID, Status: order.Status, Amount: order.Amount, @@ -215,7 +208,6 @@ func MapOrderToCreateOrderResponse(orderResponse *entity.OrderResponse) response PaymentType: order.PaymentType, CreatedAt: order.CreatedAt, OrderItems: orderItems, - Token: orderResponse.Token, } } @@ -234,7 +226,6 @@ func MapOrderToExecuteOrderResponse(orderResponse *entity.ExecuteOrderResponse) return response.ExecuteOrderResponse{ ID: order.ID, - RefID: order.RefID, PartnerID: order.PartnerID, Status: order.Status, Amount: order.Amount, @@ -261,7 +252,6 @@ func MapOrderToExecuteCheckinResponse(order *entity.Order) response.ExecuteCheck return response.ExecuteCheckinResponse{ ID: order.ID, - RefID: order.RefID, PartnerID: order.PartnerID, Status: order.Status, Amount: order.Amount, diff --git a/internal/handlers/http/oss/oss.go b/internal/handlers/http/oss/oss.go index 2472c33..1559f7e 100644 --- a/internal/handlers/http/oss/oss.go +++ b/internal/handlers/http/oss/oss.go @@ -13,7 +13,7 @@ import ( const _oneMB = 1 << 20 // 1MB const _maxUploadSizeMB = 2 * _oneMB -const _folderName = "/file" +const _folderName = "/public" type OssHandler struct { ossService services.OSSService @@ -22,7 +22,7 @@ type OssHandler struct { func (h *OssHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) { route := group.Group("/file") - route.POST("/upload", h.UploadFile, jwt) + route.POST("/upload", h.UploadFile) } func NewOssHandler(ossService services.OSSService) *OssHandler { diff --git a/internal/handlers/http/product/product.go b/internal/handlers/http/product/product.go index a65c4f9..0b7ed88 100644 --- a/internal/handlers/http/product/product.go +++ b/internal/handlers/http/product/product.go @@ -278,7 +278,7 @@ func (h *Handler) toProductResponse(resp *entity.Product) response.Product { CreatedAt: resp.CreatedAt.Format(time.RFC3339), UpdatedAt: resp.CreatedAt.Format(time.RFC3339), PartnerID: resp.PartnerID, - SiteID: resp.SiteID, + Image: resp.Image, } } diff --git a/internal/handlers/http/sites/sites.go b/internal/handlers/http/sites/sites.go index f29949e..81ed864 100644 --- a/internal/handlers/http/sites/sites.go +++ b/internal/handlers/http/sites/sites.go @@ -314,7 +314,6 @@ func (h *Handler) toProductResponseList(products []entity.Product) []response.Pr res = append(res, response.Product{ ID: product.ID, PartnerID: product.PartnerID, - SiteID: product.SiteID, Name: product.Name, Type: product.Type, Price: product.Price, diff --git a/internal/handlers/request/order.go b/internal/handlers/request/order.go index c4e3b85..c9123e8 100644 --- a/internal/handlers/request/order.go +++ b/internal/handlers/request/order.go @@ -4,13 +4,14 @@ import ( "enaklo-pos-be/internal/common/mycontext" "enaklo-pos-be/internal/constants/transaction" "enaklo-pos-be/internal/entity" - "time" ) type Order struct { - PartnerID int64 `json:"partner_id" validate:"required"` - PaymentMethod transaction.PaymentMethod `json:"payment_method" validate:"required"` - OrderItems []OrderItem `json:"order_items" validate:"required"` + CustomerName string `json:"customer_name"` + CustomerPhone string `json:"customer_phone"` + CustomerEmail string `json:"customer_email"` + PaymentMethod string `json:"payment_method"` + OrderItems []OrderItem `json:"order_items"` } type CustomerOrder struct { @@ -36,8 +37,6 @@ func (o *CustomerOrder) ToEntity(createdBy int64) *entity.OrderRequest { OrderItems: orderItems, CreatedBy: createdBy, Source: "ONLINE", - VisitDate: o.VisitDate, - BankCode: o.BankCode, } } @@ -82,10 +81,10 @@ func (o *OrderParam) ToOrderEntity(ctx mycontext.Context) entity.OrderSearch { type OrderItem struct { ProductID int64 `json:"product_id" validate:"required"` - Quantity int64 `json:"quantity" validate:"required"` + Quantity int `json:"quantity" validate:"required"` } -func (o *Order) ToEntity(createdBy int64) *entity.OrderRequest { +func (o *Order) ToEntity(partnerID, createdBy int64) *entity.OrderRequest { orderItems := make([]entity.OrderItemRequest, len(o.OrderItems)) for i, item := range o.OrderItems { orderItems[i] = entity.OrderItemRequest{ @@ -95,12 +94,11 @@ func (o *Order) ToEntity(createdBy int64) *entity.OrderRequest { } return &entity.OrderRequest{ - PartnerID: o.PartnerID, - PaymentMethod: string(o.PaymentMethod), + PartnerID: partnerID, + PaymentMethod: o.PaymentMethod, OrderItems: orderItems, CreatedBy: createdBy, Source: "POS", - VisitDate: time.Now().Format("2006-01-02"), } } diff --git a/internal/handlers/request/product.go b/internal/handlers/request/product.go index 3ec2b49..c6272d2 100644 --- a/internal/handlers/request/product.go +++ b/internal/handlers/request/product.go @@ -37,7 +37,7 @@ type Product struct { IsWeekendTicket bool `json:"is_weekend_ticket"` IsSeasonTicket bool `json:"is_season_ticket"` Status string `json:"status"` - Description string `json:"description" validate:"required"` + Description string `json:"description"` Stock int64 `json:"stock"` Image string `json:"image"` } @@ -50,7 +50,6 @@ func (e *Product) ToEntity() *entity.Product { Status: e.Status, Description: e.Description, PartnerID: e.PartnerID, - SiteID: e.SiteID, Image: e.Image, } } diff --git a/internal/handlers/response/discovery.go b/internal/handlers/response/discovery.go index a0ad607..636b8a4 100644 --- a/internal/handlers/response/discovery.go +++ b/internal/handlers/response/discovery.go @@ -66,8 +66,8 @@ type SearchSiteByIDResponse struct { } type SearchProductSiteByIDResponse struct { - ID int64 `json:"id"` - SiteID int64 `json:"site_id"` + ID int64 `json:"id"` + Name string `json:"name"` Type string `json:"type"` Price float64 `json:"price"` diff --git a/internal/handlers/response/order.go b/internal/handlers/response/order.go index 8277de0..05c89e2 100644 --- a/internal/handlers/response/order.go +++ b/internal/handlers/response/order.go @@ -84,9 +84,6 @@ type OrderBranchRevenue struct { type CreateOrderResponse struct { ID int64 `json:"id"` - SiteName string `json:"site_name"` - VisitDate string `json:"visit_date"` - RefID string `json:"ref_id"` PartnerID int64 `json:"partner_id"` Status string `json:"status"` Amount float64 `json:"amount"` @@ -95,9 +92,6 @@ type CreateOrderResponse struct { PaymentType string `json:"payment_type"` CreatedAt time.Time `json:"created_at"` OrderItems []CreateOrderItemResponse `json:"order_items"` - Token string `json:"token"` - BankCode string `json:"bank_code"` - BankName string `json:"bank_name"` } type PrintDetailResponse struct { @@ -117,7 +111,6 @@ type PrintDetailResponse struct { type ExecuteOrderResponse struct { ID int64 `json:"id"` - RefID string `json:"ref_id"` PartnerID int64 `json:"partner_id"` Status string `json:"status"` Amount float64 `json:"amount"` @@ -134,7 +127,6 @@ type ExecuteOrderResponse struct { 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"` @@ -153,7 +145,7 @@ type CheckingInquiryResponse struct { type CreateOrderItemResponse struct { ID int64 `json:"id"` ItemID int64 `json:"item_id"` - Quantity int64 `json:"quantity"` + Quantity int `json:"quantity"` Price float64 `json:"price"` Name string `json:"name"` } diff --git a/internal/handlers/response/order_inquiry.go b/internal/handlers/response/order_inquiry.go new file mode 100644 index 0000000..d12bacd --- /dev/null +++ b/internal/handlers/response/order_inquiry.go @@ -0,0 +1,119 @@ +package response + +import ( + "enaklo-pos-be/internal/entity" + "time" +) + +type OrderInquiryResponse struct { + ID string `json:"id"` + Status string `json:"status"` + Amount float64 `json:"amount"` + Fee float64 `json:"fee"` + Total float64 `json:"total"` + CustomerID int64 `json:"customer_id"` + PaymentType string `json:"payment_type"` + CustomerName string `json:"customer_name"` + CustomerPhoneNumber string `json:"customer_phone_number"` + CustomerEmail string `json:"customer_email"` + ExpiresAt time.Time `json:"expires_at"` + CreatedAt time.Time `json:"created_at"` + Items []OrderItemResponse `json:"items"` + Token string `json:"token"` +} + +type OrderItemResponse struct { + ProductID int64 `json:"product_id"` + ProductName string `json:"product_name"` + Price float64 `json:"price"` + Quantity int `json:"quantity"` + Subtotal float64 `json:"subtotal"` +} + +func mapToOrderItemResponses(items []entity.OrderItem) []OrderItemResponse { + result := make([]OrderItemResponse, 0, len(items)) + for _, item := range items { + productName := "" + if item.Product != nil { + productName = item.Product.Name + } + + result = append(result, OrderItemResponse{ + ProductID: item.ItemID, + ProductName: productName, + Price: item.Price, + Quantity: item.Quantity, + Subtotal: item.Price * float64(item.Quantity), + }) + } + return result +} + +func MapToInquiryResponse(result *entity.OrderInquiryResponse) OrderInquiryResponse { + resp := OrderInquiryResponse{ + ID: result.OrderInquiry.ID, + Status: result.OrderInquiry.Status, + Amount: result.OrderInquiry.Amount, + Fee: result.OrderInquiry.Fee, + Total: result.OrderInquiry.Total, + CustomerID: result.OrderInquiry.CustomerID, + PaymentType: result.OrderInquiry.PaymentType, + ExpiresAt: result.OrderInquiry.ExpiresAt, + CreatedAt: result.OrderInquiry.CreatedAt, + Items: mapToOrderItemResponses(result.OrderInquiry.OrderItems), + Token: result.Token, + CustomerName: result.OrderInquiry.CustomerName, + CustomerEmail: result.OrderInquiry.CustomerEmail, + CustomerPhoneNumber: result.OrderInquiry.CustomerPhoneNumber, + } + + return resp +} + +type OrderResponse struct { + ID int64 `json:"id"` + Status string `json:"status"` + Amount float64 `json:"amount"` + Fee float64 `json:"fee"` + Total float64 `json:"total"` + CustomerName string `json:"customer_name,omitempty"` + PaymentType string `json:"payment_type"` + Source string `json:"source"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at,omitempty"` + Items []OrderItemResponse `json:"items"` +} + +func MapToOrderResponse(result *entity.OrderResponse) OrderResponse { + resp := OrderResponse{ + ID: result.Order.ID, + Status: result.Order.Status, + Amount: result.Order.Amount, + Fee: result.Order.Fee, + Total: result.Order.Total, + PaymentType: result.Order.PaymentType, + CreatedAt: result.Order.CreatedAt, + Items: MapToOrderItemResponses(result.Order.OrderItems), + } + + return resp +} + +func MapToOrderItemResponses(items []entity.OrderItem) []OrderItemResponse { + result := make([]OrderItemResponse, 0, len(items)) + for _, item := range items { + productName := "" + if item.Product != nil { + productName = item.Product.Name + } + + result = append(result, OrderItemResponse{ + ProductID: item.ItemID, + ProductName: productName, + Price: item.Price, + Quantity: item.Quantity, + Subtotal: item.Price * float64(item.Quantity), + }) + } + return result +} diff --git a/internal/handlers/response/product.go b/internal/handlers/response/product.go index 57bd004..83c002f 100644 --- a/internal/handlers/response/product.go +++ b/internal/handlers/response/product.go @@ -3,7 +3,6 @@ package response type Product struct { ID int64 `json:"id"` PartnerID int64 `json:"partner_id"` - SiteID int64 `json:"site_id"` Name string `json:"name"` Type string `json:"type"` Price float64 `json:"price"` diff --git a/internal/middlewares/auth.go b/internal/middlewares/auth.go index 751e25c..8ee9f97 100644 --- a/internal/middlewares/auth.go +++ b/internal/middlewares/auth.go @@ -12,7 +12,6 @@ import ( func AuthorizationMiddleware(cryp repository.Crypto) gin.HandlerFunc { return func(c *gin.Context) { - // Get the JWT token from the header tokenString := c.GetHeader("Authorization") if tokenString == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"}) diff --git a/internal/repository/brevo/init.go b/internal/repository/brevo/init.go index 12fb6c9..c6bd457 100644 --- a/internal/repository/brevo/init.go +++ b/internal/repository/brevo/init.go @@ -26,7 +26,31 @@ func (s ServiceImpl) SendEmailTransactional(ctx context.Context, param entity.Se return err } - renderedTemplate, err := template.New(param.TemplateName).Parse(string(templateFile)) + tmpl := template.New(param.TemplateName).Funcs(template.FuncMap{ + "range": func(args ...interface{}) []interface{} { + if len(args) == 0 { + return nil + } + + switch items := args[0].(type) { + case []map[string]string: + result := make([]interface{}, len(items)) + for i, item := range items { + result[i] = item + } + return result + case []interface{}: + return items + default: + if slice, ok := args[0].([]interface{}); ok { + return slice + } + return nil + } + }, + }) + + renderedTemplate, err := tmpl.Parse(string(templateFile)) if err != nil { log.Println(err) return err @@ -45,6 +69,7 @@ func (s ServiceImpl) sendEmail(ctx context.Context, tmpl *template.Template, par payload := brevo.SendSmtpEmail{ Sender: &brevo.SendSmtpEmailSender{ + Name: "Enaklo", Email: param.Sender, }, To: []brevo.SendSmtpEmailTo{ diff --git a/internal/repository/crypto/init.go b/internal/repository/crypto/init.go index 75d9be0..6215aeb 100644 --- a/internal/repository/crypto/init.go +++ b/internal/repository/crypto/init.go @@ -149,6 +149,49 @@ func (c *CryptoImpl) GenerateJWTOrder(order *entity.Order) (string, error) { return token, nil } +func (c *CryptoImpl) GenerateJWTOrderInquiry(inquiry *entity.OrderInquiry) (string, error) { + claims := &entity.JWTOrderClaims{ + StandardClaims: jwt.StandardClaims{ + Subject: inquiry.ID, + ExpiresAt: c.Config.AccessTokenOrderExpiresDate().Unix(), + IssuedAt: time.Now().Unix(), + NotBefore: time.Now().Unix(), + }, + PartnerID: inquiry.PartnerID, + InquiryID: inquiry.ID, + } + + token, err := jwt. + NewWithClaims(jwt.SigningMethodHS256, claims). + SignedString([]byte(c.Config.AccessTokenOrderSecret())) + + if err != nil { + return "", err + } + + return token, nil +} + +func (c *CryptoImpl) ValidateJWTOrderInquiry(tokenString string) (int64, string, error) { + token, err := jwt.ParseWithClaims(tokenString, &entity.JWTOrderClaims{}, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(c.Config.AccessTokenOrderSecret()), nil + }) + + if err != nil { + return 0, "", err + } + + claims, ok := token.Claims.(*entity.JWTOrderClaims) + if !ok || !token.Valid { + return 0, "", fmt.Errorf("invalid token %v", token.Header["alg"]) + } + + return claims.PartnerID, claims.InquiryID, nil +} + func (c *CryptoImpl) ValidateJWTOrder(tokenString string) (int64, int64, error) { token, err := jwt.ParseWithClaims(tokenString, &entity.JWTOrderClaims{}, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { diff --git a/internal/repository/customer_repo.go b/internal/repository/customer_repo.go new file mode 100644 index 0000000..1d9bc53 --- /dev/null +++ b/internal/repository/customer_repo.go @@ -0,0 +1,128 @@ +package repository + +import ( + "enaklo-pos-be/internal/common/mycontext" + "enaklo-pos-be/internal/entity" + "enaklo-pos-be/internal/repository/models" + "github.com/pkg/errors" + "gorm.io/gorm" + "time" +) + +type CustomerRepo interface { + Create(ctx mycontext.Context, customer *entity.Customer) (*entity.Customer, error) + FindByID(ctx mycontext.Context, id int64) (*entity.Customer, error) + FindByPhone(ctx mycontext.Context, phone string) (*entity.Customer, error) + FindByEmail(ctx mycontext.Context, email string) (*entity.Customer, error) + AddPoints(ctx mycontext.Context, id int64, points int) error +} + +type customerRepository struct { + db *gorm.DB +} + +func NewCustomerRepository(db *gorm.DB) *customerRepository { + return &customerRepository{db: db} +} + +func (r *customerRepository) Create(ctx mycontext.Context, customer *entity.Customer) (*entity.Customer, error) { + customerDB := r.toCustomerDBModel(customer) + + if err := r.db.Create(&customerDB).Error; err != nil { + return nil, errors.Wrap(err, "failed to insert customer") + } + + customer.ID = customerDB.ID + + return customer, nil +} + +func (r *customerRepository) FindByID(ctx mycontext.Context, id int64) (*entity.Customer, error) { + var customerDB models.CustomerDB + + if err := r.db.First(&customerDB, id).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("customer not found") + } + return nil, errors.Wrap(err, "failed to find customer") + } + + customer := r.toDomainCustomerModel(&customerDB) + + return customer, nil +} + +func (r *customerRepository) FindByPhone(ctx mycontext.Context, phone string) (*entity.Customer, error) { + var customerDB models.CustomerDB + + if err := r.db.Where("phone = ?", phone).First(&customerDB).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("customer not found") + } + return nil, errors.Wrap(err, "failed to find customer by phone") + } + + customer := r.toDomainCustomerModel(&customerDB) + + return customer, nil +} + +func (r *customerRepository) FindByEmail(ctx mycontext.Context, email string) (*entity.Customer, error) { + var customerDB models.CustomerDB + + if err := r.db.Where("email = ?", email).First(&customerDB).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("customer not found") + } + return nil, errors.Wrap(err, "failed to find customer by email") + } + + customer := r.toDomainCustomerModel(&customerDB) + + return customer, nil +} + +func (r *customerRepository) AddPoints(ctx mycontext.Context, id int64, points int) error { + now := time.Now() + + result := r.db.Model(&models.CustomerDB{}). + Where("id = ?", id). + Updates(map[string]interface{}{ + "points": gorm.Expr("points + ?", points), + "updated_at": now, + }) + + if result.Error != nil { + return errors.Wrap(result.Error, "failed to add points to customer") + } + + if result.RowsAffected == 0 { + return errors.New("customer not found") + } + + return nil +} + +func (r *customerRepository) toCustomerDBModel(customer *entity.Customer) models.CustomerDB { + return models.CustomerDB{ + ID: customer.ID, + Name: customer.Name, + Email: customer.Email, + Phone: customer.Phone, + Points: customer.Points, + CreatedAt: customer.CreatedAt, + UpdatedAt: customer.UpdatedAt, + } +} + +func (r *customerRepository) toDomainCustomerModel(dbModel *models.CustomerDB) *entity.Customer { + return &entity.Customer{ + ID: dbModel.ID, + Name: dbModel.Name, + Email: dbModel.Email, + Phone: dbModel.Phone, + Points: dbModel.Points, + CreatedAt: dbModel.CreatedAt, + UpdatedAt: dbModel.UpdatedAt, + } +} diff --git a/internal/repository/models/customer.go b/internal/repository/models/customer.go new file mode 100644 index 0000000..0ea80c4 --- /dev/null +++ b/internal/repository/models/customer.go @@ -0,0 +1,19 @@ +package models + +import ( + "time" +) + +type CustomerDB struct { + ID int64 `gorm:"primaryKey;column:id"` + Name string `gorm:"column:name"` + Email string `gorm:"column:email"` + Phone string `gorm:"column:phone"` + Points int `gorm:"column:points"` + CreatedAt time.Time `gorm:"column:created_at"` + UpdatedAt time.Time `gorm:"column:updated_at"` +} + +func (CustomerDB) TableName() string { + return "customers" +} diff --git a/internal/repository/models/order.go b/internal/repository/models/order.go new file mode 100644 index 0000000..738faa0 --- /dev/null +++ b/internal/repository/models/order.go @@ -0,0 +1,80 @@ +package models + +import ( + "time" +) + +type OrderDB struct { + ID int64 `gorm:"primaryKey;column:id"` + PartnerID int64 `gorm:"column:partner_id"` + CustomerID *int64 `gorm:"column:customer_id"` + InquiryID *string `gorm:"column:inquiry_id"` + Status string `gorm:"column:status"` + Amount float64 `gorm:"column:amount"` + Fee float64 `gorm:"column:fee"` + Total float64 `gorm:"column:total"` + PaymentType string `gorm:"column:payment_type"` + Source string `gorm:"column:source"` + CreatedBy int64 `gorm:"column:created_by"` + CreatedAt time.Time `gorm:"column:created_at"` + UpdatedAt time.Time `gorm:"column:updated_at"` + OrderItems []OrderItemDB `gorm:"foreignKey:OrderID"` +} + +func (OrderDB) TableName() string { + return "orders" +} + +type OrderItemDB struct { + ID int64 `gorm:"primaryKey;column:order_item_id"` + OrderID int64 `gorm:"column:order_id"` + ItemID int64 `gorm:"column:item_id"` + ItemType string `gorm:"column:item_type"` + Price float64 `gorm:"column:price"` + Quantity int `gorm:"column:quantity"` + CreatedBy int64 `gorm:"column:created_by"` + CreatedAt time.Time `gorm:"column:created_at"` +} + +func (OrderItemDB) TableName() string { + return "order_items" +} + +type OrderInquiryDB struct { + ID string `gorm:"primaryKey;column:id"` + PartnerID int64 `gorm:"column:partner_id"` + CustomerID *int64 `gorm:"column:customer_id"` + CustomerName string `gorm:"column:customer_name"` + CustomerEmail string `gorm:"column:customer_email"` + CustomerPhoneNumber string `gorm:"column:customer_phone_number"` + Status string `gorm:"column:status"` + Amount float64 `gorm:"column:amount"` + Fee float64 `gorm:"column:fee"` + Total float64 `gorm:"column:total"` + PaymentType string `gorm:"column:payment_type"` + Source string `gorm:"column:source"` + CreatedBy int64 `gorm:"column:created_by"` + CreatedAt time.Time `gorm:"column:created_at"` + UpdatedAt time.Time `gorm:"column:updated_at"` + ExpiresAt time.Time `gorm:"column:expires_at"` + InquiryItems []InquiryItemDB `gorm:"foreignKey:InquiryID"` +} + +func (OrderInquiryDB) TableName() string { + return "order_inquiries" +} + +type InquiryItemDB struct { + ID int64 `gorm:"primaryKey;column:id"` + InquiryID string `gorm:"column:inquiry_id"` + ItemID int64 `gorm:"column:item_id"` + ItemType string `gorm:"column:item_type"` + Price float64 `gorm:"column:price"` + Quantity int `gorm:"column:quantity"` + CreatedBy int64 `gorm:"column:created_by"` + CreatedAt time.Time `gorm:"column:created_at"` +} + +func (InquiryItemDB) TableName() string { + return "inquiry_items" +} diff --git a/internal/repository/models/product.go b/internal/repository/models/product.go new file mode 100644 index 0000000..eed9b8f --- /dev/null +++ b/internal/repository/models/product.go @@ -0,0 +1,22 @@ +package models + +import ( + "time" +) + +type ProductDB struct { + ID int64 `gorm:"primaryKey;column:id"` + SiteID int64 `gorm:"column:site_id"` + PartnerID int64 `gorm:"column:partner_id"` + Name string `gorm:"column:name"` + Description string `gorm:"column:description"` + Price float64 `gorm:"column:price"` + Type string `gorm:"column:type"` + Status string `gorm:"column:status"` + CreatedAt time.Time `gorm:"column:created_at"` + UpdatedAt time.Time `gorm:"column:updated_at"` +} + +func (ProductDB) TableName() string { + return "products" +} diff --git a/internal/repository/models/transaction.go b/internal/repository/models/transaction.go new file mode 100644 index 0000000..dc812a9 --- /dev/null +++ b/internal/repository/models/transaction.go @@ -0,0 +1,21 @@ +package models + +import ( + "time" +) + +type TransactionDB struct { + ID string `gorm:"primaryKey;column:id"` + OrderID int64 `gorm:"column:order_id"` + Amount float64 `gorm:"column:amount"` + PaymentMethod string `gorm:"column:payment_method"` + Status string `gorm:"column:status"` + CreatedAt time.Time `gorm:"column:created_at"` + UpdatedAt time.Time `gorm:"column:updated_at"` + PartnerID int64 `gorm:"column:partner_id"` + TransactionType string `gorm:"column:transaction_type"` +} + +func (TransactionDB) TableName() string { + return "transactions" +} diff --git a/internal/repository/orde_repo.go b/internal/repository/orde_repo.go new file mode 100644 index 0000000..84cbe23 --- /dev/null +++ b/internal/repository/orde_repo.go @@ -0,0 +1,292 @@ +package repository + +import ( + "enaklo-pos-be/internal/common/logger" + "enaklo-pos-be/internal/common/mycontext" + "enaklo-pos-be/internal/entity" + "enaklo-pos-be/internal/repository/models" + "github.com/pkg/errors" + "go.uber.org/zap" + "gorm.io/gorm" + "time" +) + +type OrderRepository interface { + Create(ctx mycontext.Context, order *entity.Order) (*entity.Order, error) + FindByID(ctx mycontext.Context, id int64) (*entity.Order, error) + CreateInquiry(ctx mycontext.Context, inquiry *entity.OrderInquiry) (*entity.OrderInquiry, error) + FindInquiryByID(ctx mycontext.Context, id string) (*entity.OrderInquiry, error) + UpdateInquiryStatus(ctx mycontext.Context, id string, status string) error +} + +type orderRepository struct { + db *gorm.DB +} + +func NeworderRepository(db *gorm.DB) *orderRepository { + return &orderRepository{db: db} +} + +func (r *orderRepository) Create(ctx mycontext.Context, order *entity.Order) (*entity.Order, error) { + orderDB := r.toOrderDBModel(order) + + tx := r.db.Begin() + if tx.Error != nil { + return nil, errors.Wrap(tx.Error, "failed to begin transaction") + } + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + if err := tx.Create(&orderDB).Error; err != nil { + tx.Rollback() + return nil, errors.Wrap(err, "failed to insert order") + } + + order.ID = orderDB.ID + + for i := range order.OrderItems { + item := &order.OrderItems[i] + item.OrderID = orderDB.ID + + itemDB := r.toOrderItemDBModel(item) + + if err := tx.Create(&itemDB).Error; err != nil { + tx.Rollback() + return nil, errors.Wrap(err, "failed to insert order item") + } + + item.ID = itemDB.ID + } + + if err := tx.Commit().Error; err != nil { + return nil, errors.Wrap(err, "failed to commit transaction") + } + + return order, nil +} + +func (r *orderRepository) FindByID(ctx mycontext.Context, id int64) (*entity.Order, error) { + var orderDB models.OrderDB + + if err := r.db.Preload("OrderItems").First(&orderDB, id).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("order not found") + } + return nil, errors.Wrap(err, "failed to find order") + } + + order := r.toDomainOrderModel(&orderDB) + + for _, itemDB := range orderDB.OrderItems { + item := r.toDomainOrderItemModel(&itemDB) + order.OrderItems = append(order.OrderItems, *item) + } + + return order, nil +} + +func (r *orderRepository) CreateInquiry(ctx mycontext.Context, inquiry *entity.OrderInquiry) (*entity.OrderInquiry, error) { + inquiryDB := r.toOrderInquiryDBModel(inquiry) + inquiryItems := make([]models.InquiryItemDB, 0, len(inquiry.OrderItems)) + + for _, item := range inquiry.OrderItems { + inquiryItems = append(inquiryItems, models.InquiryItemDB{ + InquiryID: inquiryDB.ID, + ItemID: item.ItemID, + ItemType: item.ItemType, + Price: item.Price, + Quantity: item.Quantity, + CreatedBy: item.CreatedBy, + CreatedAt: time.Now(), + }) + } + + tx := r.db.Begin() + if tx.Error != nil { + return nil, errors.Wrap(tx.Error, "failed to begin transaction") + } + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + if err := tx.Create(&inquiryDB).Error; err != nil { + tx.Rollback() + return nil, errors.Wrap(err, "failed to insert order inquiry") + } + + if len(inquiryItems) > 0 { + if err := tx.CreateInBatches(inquiryItems, 100).Error; err != nil { + tx.Rollback() + return nil, errors.Wrap(err, "failed to insert inquiry items") + } + } + + if err := tx.Commit().Error; err != nil { + return nil, errors.Wrap(err, "failed to commit transaction") + } + + return inquiry, nil +} + +func (r *orderRepository) FindInquiryByID(ctx mycontext.Context, id string) (*entity.OrderInquiry, error) { + var inquiryDB models.OrderInquiryDB + + if err := r.db.Preload("InquiryItems").First(&inquiryDB, "id = ?", id).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("inquiry not found") + } + return nil, errors.Wrap(err, "failed to find inquiry") + } + + inquiry := r.toDomainOrderInquiryModel(&inquiryDB) + + orderItems := make([]entity.OrderItem, 0, len(inquiryDB.InquiryItems)) + for _, itemDB := range inquiryDB.InquiryItems { + orderItems = append(orderItems, entity.OrderItem{ + ItemID: itemDB.ItemID, + ItemType: itemDB.ItemType, + Price: itemDB.Price, + Quantity: itemDB.Quantity, + CreatedBy: itemDB.CreatedBy, + CreatedAt: itemDB.CreatedAt, + }) + } + inquiry.OrderItems = orderItems + + return inquiry, nil +} + +func (r *orderRepository) UpdateInquiryStatus(ctx mycontext.Context, id string, status string) error { + now := time.Now() + + result := r.db.Model(&models.OrderInquiryDB{}). + Where("id = ?", id). + Updates(map[string]interface{}{ + "status": status, + "updated_at": now, + }) + + if result.Error != nil { + return errors.Wrap(result.Error, "failed to update inquiry status") + } + + if result.RowsAffected == 0 { + logger.ContextLogger(ctx).Warn("no inquiry updated", zap.String("id", id)) + } + + return nil +} + +func (r *orderRepository) toOrderDBModel(order *entity.Order) models.OrderDB { + return models.OrderDB{ + ID: order.ID, + PartnerID: order.PartnerID, + CustomerID: order.CustomerID, + InquiryID: order.InquiryID, + Status: order.Status, + Amount: order.Amount, + Fee: order.Fee, + Total: order.Total, + PaymentType: order.PaymentType, + Source: order.Source, + CreatedBy: order.CreatedBy, + CreatedAt: order.CreatedAt, + UpdatedAt: order.UpdatedAt, + } +} + +func (r *orderRepository) toDomainOrderModel(dbModel *models.OrderDB) *entity.Order { + return &entity.Order{ + ID: dbModel.ID, + PartnerID: dbModel.PartnerID, + CustomerID: dbModel.CustomerID, + InquiryID: dbModel.InquiryID, + Status: dbModel.Status, + Amount: dbModel.Amount, + Fee: dbModel.Fee, + Total: dbModel.Total, + PaymentType: dbModel.PaymentType, + Source: dbModel.Source, + CreatedBy: dbModel.CreatedBy, + CreatedAt: dbModel.CreatedAt, + UpdatedAt: dbModel.UpdatedAt, + OrderItems: []entity.OrderItem{}, + } +} + +func (r *orderRepository) toOrderItemDBModel(item *entity.OrderItem) models.OrderItemDB { + return models.OrderItemDB{ + ID: item.ID, + OrderID: item.OrderID, + ItemID: item.ItemID, + ItemType: item.ItemType, + Price: item.Price, + Quantity: item.Quantity, + CreatedBy: item.CreatedBy, + CreatedAt: item.CreatedAt, + } +} + +func (r *orderRepository) toDomainOrderItemModel(dbModel *models.OrderItemDB) *entity.OrderItem { + return &entity.OrderItem{ + ID: dbModel.ID, + OrderID: dbModel.OrderID, + ItemID: dbModel.ItemID, + ItemType: dbModel.ItemType, + Price: dbModel.Price, + Quantity: dbModel.Quantity, + CreatedBy: dbModel.CreatedBy, + CreatedAt: dbModel.CreatedAt, + } +} + +func (r *orderRepository) toOrderInquiryDBModel(inquiry *entity.OrderInquiry) models.OrderInquiryDB { + return models.OrderInquiryDB{ + ID: inquiry.ID, + PartnerID: inquiry.PartnerID, + CustomerID: &inquiry.CustomerID, + Status: inquiry.Status, + Amount: inquiry.Amount, + Fee: inquiry.Fee, + Total: inquiry.Total, + PaymentType: inquiry.PaymentType, + Source: inquiry.Source, + CreatedBy: inquiry.CreatedBy, + CreatedAt: inquiry.CreatedAt, + UpdatedAt: inquiry.UpdatedAt, + ExpiresAt: inquiry.ExpiresAt, + CustomerName: inquiry.CustomerName, + CustomerPhoneNumber: inquiry.CustomerPhoneNumber, + CustomerEmail: inquiry.CustomerEmail, + } +} + +func (r *orderRepository) toDomainOrderInquiryModel(dbModel *models.OrderInquiryDB) *entity.OrderInquiry { + inquiry := &entity.OrderInquiry{ + ID: dbModel.ID, + PartnerID: dbModel.PartnerID, + Status: dbModel.Status, + Amount: dbModel.Amount, + Fee: dbModel.Fee, + Total: dbModel.Total, + PaymentType: dbModel.PaymentType, + Source: dbModel.Source, + CreatedBy: dbModel.CreatedBy, + CreatedAt: dbModel.CreatedAt, + ExpiresAt: dbModel.ExpiresAt, + OrderItems: []entity.OrderItem{}, + } + + if dbModel.CustomerID != nil { + inquiry.CustomerID = *dbModel.CustomerID + } + + inquiry.UpdatedAt = dbModel.UpdatedAt + + return inquiry +} diff --git a/internal/repository/oss/oss.go b/internal/repository/oss/oss.go index c57c7c1..e82d39f 100644 --- a/internal/repository/oss/oss.go +++ b/internal/repository/oss/oss.go @@ -63,5 +63,5 @@ func (r *OssRepositoryImpl) GetPublicURL(fileName string) string { if fileName == "" { return "" } - return fmt.Sprintf("%s/%s%s", r.cfg.GetHostURL(), r.cfg.GetBucketName(), fileName) + return fmt.Sprintf("%s%s%s", r.cfg.GetHostURL(), r.cfg.GetBucketName(), fileName) } diff --git a/internal/repository/product_repo.go b/internal/repository/product_repo.go new file mode 100644 index 0000000..a40c89f --- /dev/null +++ b/internal/repository/product_repo.go @@ -0,0 +1,82 @@ +package repository + +import ( + "enaklo-pos-be/internal/common/mycontext" + "enaklo-pos-be/internal/entity" + "enaklo-pos-be/internal/repository/models" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type ProductRepository interface { + GetProductsByIDs(ctx mycontext.Context, ids []int64, partnerID int64) ([]*entity.Product, error) + GetProductDetails(ctx mycontext.Context, productIDs []int64, partnerID int64) (*entity.ProductDetails, error) +} + +type productRepository struct { + db *gorm.DB +} + +func NewproductRepository(db *gorm.DB) *productRepository { + return &productRepository{db: db} +} + +func (r *productRepository) GetProductsByIDs(ctx mycontext.Context, ids []int64, partnerID int64) ([]*entity.Product, error) { + if len(ids) == 0 { + return []*entity.Product{}, nil + } + + var productsDB []models.ProductDB + + if err := r.db.Where("id IN ? AND partner_id = ?", ids, partnerID).Find(&productsDB).Error; err != nil { + return nil, errors.Wrap(err, "failed to find products") + } + + products := make([]*entity.Product, 0, len(productsDB)) + for i := range productsDB { + product := r.toDomainProductModel(&productsDB[i]) + products = append(products, product) + } + + return products, nil +} + +func (r *productRepository) GetProductDetails(ctx mycontext.Context, productIDs []int64, partnerID int64) (*entity.ProductDetails, error) { + if len(productIDs) == 0 { + return &entity.ProductDetails{ + Products: make(map[int64]*entity.Product), + }, nil + } + + var productsDB []models.ProductDB + + if err := r.db.Where("id IN ? AND partner_id = ?", productIDs, partnerID).Find(&productsDB).Error; err != nil { + return nil, errors.Wrap(err, "failed to find products") + } + + productMap := make(map[int64]*entity.Product, len(productsDB)) + + for i := range productsDB { + product := r.toDomainProductModel(&productsDB[i]) + productMap[product.ID] = product + } + + return &entity.ProductDetails{ + Products: productMap, + PartnerID: partnerID, + }, nil +} + +func (r *productRepository) toDomainProductModel(dbModel *models.ProductDB) *entity.Product { + return &entity.Product{ + ID: dbModel.ID, + PartnerID: dbModel.PartnerID, + Name: dbModel.Name, + Description: dbModel.Description, + Price: dbModel.Price, + Type: dbModel.Type, + Status: dbModel.Status, + CreatedAt: dbModel.CreatedAt, + UpdatedAt: dbModel.UpdatedAt, + } +} diff --git a/internal/repository/products/product.go b/internal/repository/products/product.go index 84c7828..2b8c631 100644 --- a/internal/repository/products/product.go +++ b/internal/repository/products/product.go @@ -88,14 +88,6 @@ func (b *ProductRepository) GetAllProducts(ctx context.Context, req entity.Produ query = query.Where("branch_id = ? ", req.BranchID) } - if req.Available != "" { - if req.Available.IsAvailable() { - query = query.Where("stock_qty > 0 ") - } else if req.Available.IsUnavailable() { - query = query.Where("stock_qty < 1 ") - } - } - if req.Limit > 0 { query = query.Limit(req.Limit) } diff --git a/internal/repository/repository.go b/internal/repository/repository.go index de0507a..f8b387c 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -51,6 +51,11 @@ type RepoManagerImpl struct { Transaction TransactionRepository PG PaymentGateway LinkQu LinkQu + + OrderRepo OrderRepository + CustomerRepo CustomerRepo + ProductRepo ProductRepository + TransactionRepo TransactionRepo } func NewRepoManagerImpl(db *gorm.DB, cfg *config.Config) *RepoManagerImpl { @@ -74,6 +79,11 @@ func NewRepoManagerImpl(db *gorm.DB, cfg *config.Config) *RepoManagerImpl { Transaction: transactions.NewTransactionRepository(db), PG: pg.NewPaymentGatewayRepo(&cfg.Midtrans, &cfg.LinkQu), LinkQu: linkqu.NewLinkQuService(&cfg.LinkQu), + + OrderRepo: NeworderRepository(db), + CustomerRepo: NewCustomerRepository(db), + ProductRepo: NewproductRepository(db), + TransactionRepo: NewTransactionRepository(db), } } @@ -97,6 +107,8 @@ type Crypto interface { GenerateJWT(user *entity.User) (string, error) GenerateJWTReseetPassword(user *entity.User) (string, error) GenerateJWTOrder(order *entity.Order) (string, error) + GenerateJWTOrderInquiry(inquiry *entity.OrderInquiry) (string, error) + ValidateJWTOrderInquiry(tokenString string) (int64, string, error) ValidateJWTOrder(tokenString string) (int64, int64, error) ValidateResetPassword(tokenString string) (int64, error) ParseAndValidateJWT(token string) (*entity.JWTAuthClaims, error) diff --git a/internal/repository/sites/sites.go b/internal/repository/sites/sites.go index 85b7aed..1573396 100644 --- a/internal/repository/sites/sites.go +++ b/internal/repository/sites/sites.go @@ -36,14 +36,11 @@ func (r *SiteRepository) Upsert(ctx context.Context, site *entity.Site) (*entity if len(site.Products) > 0 { for i := range site.Products { - site.Products[i].SiteID = site.ID if site.Products[i].ID != 0 { - // Update existing product if err := tx.Save(&site.Products[i]).Error; err != nil { return err } } else { - // Create new product if err := tx.Create(&site.Products[i]).Error; err != nil { return err } diff --git a/internal/repository/transaction_repo.go b/internal/repository/transaction_repo.go new file mode 100644 index 0000000..399a459 --- /dev/null +++ b/internal/repository/transaction_repo.go @@ -0,0 +1,76 @@ +package repository + +import ( + "enaklo-pos-be/internal/common/mycontext" + "enaklo-pos-be/internal/entity" + "enaklo-pos-be/internal/repository/models" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type TransactionRepo interface { + Create(ctx mycontext.Context, transaction *entity.Transaction) (*entity.Transaction, error) + FindByOrderID(ctx mycontext.Context, orderID int64) ([]*entity.Transaction, error) +} + +type transactionRepository struct { + db *gorm.DB +} + +func NewTransactionRepository(db *gorm.DB) *transactionRepository { + return &transactionRepository{ + db: db, + } +} + +func (r *transactionRepository) Create(ctx mycontext.Context, transaction *entity.Transaction) (*entity.Transaction, error) { + transactionDB := r.toTransactionDBModel(transaction) + + if err := r.db.Create(&transactionDB).Error; err != nil { + return nil, errors.Wrap(err, "failed to insert transaction") + } + + return transaction, nil +} + +func (r *transactionRepository) FindByOrderID(ctx mycontext.Context, orderID int64) ([]*entity.Transaction, error) { + var transactionsDB []models.TransactionDB + + if err := r.db.Where("order_id = ?", orderID).Find(&transactionsDB).Error; err != nil { + return nil, errors.Wrap(err, "failed to find transactions for order") + } + + transactions := make([]*entity.Transaction, 0, len(transactionsDB)) + for i := range transactionsDB { + transaction := r.toDomainTransactionModel(&transactionsDB[i]) + transactions = append(transactions, transaction) + } + + return transactions, nil +} + +func (r *transactionRepository) toTransactionDBModel(transaction *entity.Transaction) models.TransactionDB { + return models.TransactionDB{ + ID: transaction.ID, + OrderID: transaction.OrderID, + Amount: transaction.Amount, + PaymentMethod: transaction.PaymentMethod, + Status: transaction.Status, + CreatedAt: transaction.CreatedAt, + UpdatedAt: transaction.UpdatedAt, + TransactionType: transaction.TransactionType, + PartnerID: transaction.PartnerID, + } +} + +func (r *transactionRepository) toDomainTransactionModel(dbModel *models.TransactionDB) *entity.Transaction { + return &entity.Transaction{ + ID: dbModel.ID, + OrderID: dbModel.OrderID, + Amount: dbModel.Amount, + PaymentMethod: dbModel.PaymentMethod, + Status: dbModel.Status, + CreatedAt: dbModel.CreatedAt, + UpdatedAt: dbModel.UpdatedAt, + } +} diff --git a/internal/routes/routes.go b/internal/routes/routes.go index d85daed..6f1c269 100644 --- a/internal/routes/routes.go +++ b/internal/routes/routes.go @@ -1,6 +1,7 @@ package routes import ( + http2 "enaklo-pos-be/internal/handlers/http" "enaklo-pos-be/internal/handlers/http/balance" "enaklo-pos-be/internal/handlers/http/license" linkqu "enaklo-pos-be/internal/handlers/http/linqu" @@ -68,3 +69,18 @@ func RegisterPrivateRoutes(app *app.Server, serviceManager *services.ServiceMana handler.Route(approute, authMiddleware) } } + +func RegisterPrivateRoutesV2(app *app.Server, serviceManager *services.ServiceManagerImpl, + repoManager *repository.RepoManagerImpl) { + approute := app.Group("/api/v2") + + authMiddleware := middlewares.AuthorizationMiddleware(repoManager.Crypto) + + serverRoutes := []HTTPHandlerRoutes{ + http2.NewOrderHandler(serviceManager.OrderV2Svc), + } + + for _, handler := range serverRoutes { + handler.Route(approute, authMiddleware) + } +} diff --git a/internal/services/balance/balance.go b/internal/services/balance/balance.go index 2ea1f78..6d569fe 100644 --- a/internal/services/balance/balance.go +++ b/internal/services/balance/balance.go @@ -112,7 +112,6 @@ func (s *BalanceService) WithdrawExecute(ctx mycontext.Context, req *entity.Wall transaction := &entity.Transaction{ PartnerID: wallet.PartnerID, TransactionType: "WITHDRAW", - ReferenceID: "", Status: "WAITING_APPROVAL", CreatedBy: ctx.RequestedBy(), Amount: totalAmount, diff --git a/internal/services/order/order.go b/internal/services/order/order.go index 440c29e..cc66e45 100644 --- a/internal/services/order/order.go +++ b/internal/services/order/order.go @@ -76,11 +76,9 @@ func (s *OrderService) CreateOrder(ctx mycontext.Context, req *entity.OrderReque return nil, err } - var siteID int64 productMap := make(map[int64]*entity.ProductDB) for _, product := range products { productMap[product.ID] = product - siteID = product.SiteID } totalAmount := 0.0 @@ -93,32 +91,16 @@ func (s *OrderService) CreateOrder(ctx mycontext.Context, req *entity.OrderReque totalAmount += product.Price * float64(item.Quantity) } - parsedTime, err := time.Parse("2006-01-02", req.VisitDate) - if err != nil { - fmt.Println("Error parsing date:", err) - return nil, errors.New("visit date not defined") - } - - metadata, err := json.Marshal(map[string]string{ - "bank_code": req.BankCode, - "bank_name": req.BankName, - }) - order := &entity.Order{ - PartnerID: req.PartnerID, - RefID: generator.GenerateUUID(), - Status: order2.New.String(), - Amount: totalAmount, - Total: totalAmount + s.cfg.GetOrderFee(req.Source), - Fee: s.cfg.GetOrderFee(req.Source), - PaymentType: req.PaymentMethod, - SiteID: &siteID, - CreatedBy: req.CreatedBy, - OrderItems: []entity.OrderItem{}, - Source: req.Source, - VisitDate: parsedTime, - TicketStatus: "UNUSED", - Metadata: metadata, + PartnerID: req.PartnerID, + Status: order2.New.String(), + Amount: totalAmount, + Total: totalAmount + s.cfg.GetOrderFee(req.Source), + Fee: s.cfg.GetOrderFee(req.Source), + PaymentType: req.PaymentMethod, + CreatedBy: req.CreatedBy, + OrderItems: []entity.OrderItem{}, + Source: req.Source, } for _, item := range req.OrderItems { @@ -126,7 +108,7 @@ func (s *OrderService) CreateOrder(ctx mycontext.Context, req *entity.OrderReque ItemID: item.ProductID, ItemType: productMap[item.ProductID].Type, Price: productMap[item.ProductID].Price, - Quantity: item.Quantity, + Quantity: int(item.Quantity), CreatedBy: req.CreatedBy, Product: productMap[item.ProductID].ToProduct(), }) @@ -138,12 +120,6 @@ func (s *OrderService) CreateOrder(ctx mycontext.Context, req *entity.OrderReque return nil, err } - token, err := s.crypt.GenerateJWTOrder(order) - if err != nil { - logger.ContextLogger(ctx).Error("error when create token", zap.Error(err)) - return nil, err - } - order, err = s.repo.FindByID(ctx, order.ID) if err != nil { logger.ContextLogger(ctx).Error("error when creating order", zap.Error(err)) @@ -152,7 +128,6 @@ func (s *OrderService) CreateOrder(ctx mycontext.Context, req *entity.OrderReque return &entity.OrderResponse{ Order: order, - Token: token, }, nil } @@ -187,26 +162,6 @@ func (s *OrderService) CheckInInquiry(ctx mycontext.Context, qrCode string, part return nil, errors2.ErrorInvalidRequest } - location, _ := time.LoadLocation("Asia/Jakarta") - today := time.Now().In(location).Format("2006-01-02") - visitDate := time.Date( - order.VisitDate.Year(), - order.VisitDate.Month(), - order.VisitDate.Day(), - 0, 0, 0, 0, - location, - ).Format("2006-01-02") - - if order.TicketStatus == "USED" || visitDate < today { - return nil, errors2.NewErrorMessage(errors2.ErrorTicketInvalidOrAlreadyUsed, - "Maaf! Tiket ini tidak valid karena sudah terpakai atau sudah kadaluwarsa") - } - - if visitDate != today { - return nil, errors2.NewErrorMessage(errors2.ErrorTicketInvalidOrAlreadyUsed, - "Maaf Tiket ini tidak valid karena tidak sesuai dengan tanggal tiket") - } - token, err := s.crypt.GenerateJWTOrder(order) if err != nil { logger.ContextLogger(ctx).Error("error when generate checkin token", zap.Error(err)) @@ -242,17 +197,6 @@ func (s *OrderService) CheckInExecute(ctx mycontext.Context, Order: order, } - if order.TicketStatus != "UNUSED" { - return resp, nil - } - - order.TicketStatus = "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 } @@ -446,11 +390,6 @@ func (s *OrderService) processQRPayment(ctx mycontext.Context, order *entity.Ord } func (s *OrderService) processVAPayment(ctx mycontext.Context, order *entity.Order, partnerID, createdBy int64) (*entity.PaymentResponse, error) { - metadata := map[string]string{} - if err := json.Unmarshal(order.Metadata, &metadata); err != nil { - return nil, err - } - paymentRequest := entity.PaymentRequest{ PaymentReferenceID: generator.GenerateUUIDV4(), TotalAmount: int64(order.Total), @@ -458,7 +397,6 @@ func (s *OrderService) processVAPayment(ctx mycontext.Context, order *entity.Ord CustomerID: strconv.FormatInt(order.User.ID, 10), CustomerName: order.User.Name, CustomerEmail: order.User.Email, - BankCode: metadata["bank_code"], } paymentResponse, err := s.pg.CreatePaymentVA(paymentRequest) @@ -544,13 +482,11 @@ func (s *OrderService) processPayment(ctx context.Context, tx *gorm.DB, req *ent transaction := &entity.Transaction{ PartnerID: existingPayment.PartnerID, TransactionType: "PAYMENT_RECEIVED", - ReferenceID: existingPayment.ReferenceID, Status: "SUCCESS", CreatedBy: 0, Amount: existingPayment.Amount, Fee: order.Fee, Total: order.Total, - SiteID: order.SiteID, } if _, err = s.transaction.Create(ctx, tx, transaction); err != nil { return fmt.Errorf("failed to update transaction: %w", err) diff --git a/internal/services/service.go b/internal/services/service.go index 85f2af9..39e5b82 100644 --- a/internal/services/service.go +++ b/internal/services/service.go @@ -14,6 +14,9 @@ import ( "enaklo-pos-be/internal/services/studio" "enaklo-pos-be/internal/services/transaction" "enaklo-pos-be/internal/services/users" + customerSvc "enaklo-pos-be/internal/services/v2/customer" + orderSvc "enaklo-pos-be/internal/services/v2/order" + productSvc "enaklo-pos-be/internal/services/v2/product" "gorm.io/gorm" @@ -38,9 +41,17 @@ type ServiceManagerImpl struct { Transaction Transaction Balance Balance DiscoverService DiscoverService + + OrderV2Svc orderSvc.Service + CustomerV2Svc customerSvc.Service + ProductV2Svc productSvc.Service } func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl) *ServiceManagerImpl { + + custSvcV2 := customerSvc.New(repo.CustomerRepo) + productSvcV2 := productSvc.New(repo.ProductRepo) + return &ServiceManagerImpl{ AuthSvc: auth.New(repo.Auth, repo.Crypto, repo.User, repo.EmailService, cfg.Email, repo.Trx, repo.License), EventSvc: event.NewEventService(repo.Event), @@ -56,6 +67,7 @@ func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl) Transaction: transaction.New(repo.Transaction, repo.Wallet, repo.Trx), Balance: balance.NewBalanceService(repo.Wallet, repo.Trx, repo.Crypto, &cfg.Withdraw, repo.Transaction), DiscoverService: discovery.NewDiscoveryService(repo.Site, cfg.Discovery, repo.Product), + OrderV2Svc: orderSvc.New(repo.OrderRepo, productSvcV2, custSvcV2, repo.TransactionRepo, repo.Crypto, &cfg.Order, repo.EmailService), } } diff --git a/internal/services/v2/customer/customer.go b/internal/services/v2/customer/customer.go new file mode 100644 index 0000000..ea45ad3 --- /dev/null +++ b/internal/services/v2/customer/customer.go @@ -0,0 +1,117 @@ +package customer + +import ( + "enaklo-pos-be/internal/common/logger" + "enaklo-pos-be/internal/common/mycontext" + "enaklo-pos-be/internal/constants" + "enaklo-pos-be/internal/entity" + "github.com/pkg/errors" + "go.uber.org/zap" + "strings" +) + +type Repository interface { + Create(ctx mycontext.Context, customer *entity.Customer) (*entity.Customer, error) + FindByID(ctx mycontext.Context, id int64) (*entity.Customer, error) + FindByPhone(ctx mycontext.Context, phone string) (*entity.Customer, error) + FindByEmail(ctx mycontext.Context, email string) (*entity.Customer, error) + AddPoints(ctx mycontext.Context, id int64, points int) error +} + +type Service interface { + ResolveCustomer(ctx mycontext.Context, req *entity.CustomerResolutionRequest) (int64, error) + AddPoints(ctx mycontext.Context, customerID int64, points int) error + GetCustomer(ctx mycontext.Context, id int64) (*entity.Customer, error) +} + +type customerSvc struct { + repo Repository +} + +func New(repo Repository) Service { + return &customerSvc{ + repo: repo, + } +} + +func (s *customerSvc) ResolveCustomer(ctx mycontext.Context, req *entity.CustomerResolutionRequest) (int64, error) { + if req.Email == "" && req.PhoneNumber == "" { + return 0, nil + } + + if req.ID != nil && *req.ID > 0 { + customer, err := s.repo.FindByID(ctx, *req.ID) + if err != nil { + if !strings.Contains(err.Error(), "not found") { + return 0, errors.Wrap(err, "failed to find customer by ID") + } + } else { + return customer.ID, nil + } + } + + if req.PhoneNumber != "" { + customer, err := s.repo.FindByPhone(ctx, req.PhoneNumber) + if err != nil { + if !strings.Contains(err.Error(), "not found") { + return 0, errors.Wrap(err, "failed to find customer by phone") + } + } else { + return customer.ID, nil + } + } + + if req.Email != "" { + customer, err := s.repo.FindByEmail(ctx, req.Email) + if err != nil { + if !strings.Contains(err.Error(), "not found") { + return 0, errors.Wrap(err, "failed to find customer by email") + } + } else { + return customer.ID, nil + } + } + + if req.Name == "" { + return 0, errors.New("customer name is required to create a new customer") + } + + newCustomer := &entity.Customer{ + Name: req.Name, + Email: req.Email, + Phone: req.PhoneNumber, + Points: 0, + CreatedAt: constants.TimeNow(), + UpdatedAt: constants.TimeNow(), + } + + customer, err := s.repo.Create(ctx, newCustomer) + if err != nil { + logger.ContextLogger(ctx).Error("failed to create customer", zap.Error(err)) + return 0, errors.Wrap(err, "failed to create customer") + } + + return customer.ID, nil +} + +func (s *customerSvc) AddPoints(ctx mycontext.Context, customerID int64, points int) error { + if points <= 0 { + return nil + } + + err := s.repo.AddPoints(ctx, customerID, points) + if err != nil { + return errors.Wrap(err, "failed to add points to customer") + } + + return nil +} + +func (s *customerSvc) GetCustomer(ctx mycontext.Context, id int64) (*entity.Customer, error) { + customer, err := s.repo.FindByID(ctx, id) + if err != nil { + return nil, errors.Wrap(err, "failed to get customer") + } + + return customer, nil +} diff --git a/internal/services/v2/order/create_order_inquiry.go b/internal/services/v2/order/create_order_inquiry.go new file mode 100644 index 0000000..e3022a2 --- /dev/null +++ b/internal/services/v2/order/create_order_inquiry.go @@ -0,0 +1,144 @@ +package order + +import ( + "enaklo-pos-be/internal/common/errors" + "enaklo-pos-be/internal/common/logger" + "enaklo-pos-be/internal/common/mycontext" + "enaklo-pos-be/internal/constants" + "enaklo-pos-be/internal/entity" + "go.uber.org/zap" +) + +func (s *orderSvc) CreateOrderInquiry(ctx mycontext.Context, + req *entity.OrderRequest) (*entity.OrderInquiryResponse, error) { + productIDs, filteredItems, err := s.validateOrderItems(ctx, req.OrderItems) + if err != nil { + return nil, err + } + req.OrderItems = filteredItems + + productDetails, err := s.product.GetProductDetails(ctx, productIDs, req.PartnerID) + if err != nil { + logger.ContextLogger(ctx).Error("failed to get product details", zap.Error(err)) + return nil, err + } + + orderCalculation, err := s.calculateOrderTotals(ctx, req.OrderItems, productDetails, req.Source) + if err != nil { + return nil, err + } + + customerID, err := s.customer.ResolveCustomer(ctx, &entity.CustomerResolutionRequest{ + ID: req.CustomerID, + Name: req.CustomerName, + Email: req.CustomerEmail, + PhoneNumber: req.CustomerPhoneNumber, + }) + if err != nil { + logger.ContextLogger(ctx).Error("failed to resolve customer", zap.Error(err)) + return nil, err + } + + inquiry := entity.NewOrderInquiry( + req.PartnerID, + customerID, + orderCalculation.Subtotal, + orderCalculation.Fee, + orderCalculation.Total, + req.PaymentMethod, + req.Source, + req.CreatedBy, + req.CustomerName, + req.CustomerPhoneNumber, + req.CustomerEmail, + ) + + for _, item := range req.OrderItems { + product := productDetails.Products[item.ProductID] + inquiry.AddOrderItem(item, product) + } + + savedInquiry, err := s.repo.CreateInquiry(ctx, inquiry) + if err != nil { + logger.ContextLogger(ctx).Error("failed to create order inquiry", zap.Error(err)) + return nil, err + } + + token, err := s.crypt.GenerateJWTOrderInquiry(savedInquiry) + if err != nil { + logger.ContextLogger(ctx).Error("failed to generate token", zap.Error(err)) + return nil, err + } + + return &entity.OrderInquiryResponse{ + OrderInquiry: savedInquiry, + Token: token, + }, nil +} + +func (s *orderSvc) validateOrderItems(ctx mycontext.Context, items []entity.OrderItemRequest) ([]int64, []entity.OrderItemRequest, error) { + var productIDs []int64 + var filteredItems []entity.OrderItemRequest + + for _, item := range items { + if item.Quantity <= 0 { + continue + } + productIDs = append(productIDs, item.ProductID) + filteredItems = append(filteredItems, item) + } + + if len(productIDs) == 0 { + return nil, nil, errors.ErrorBadRequest + } + + return productIDs, filteredItems, nil +} + +func (s *orderSvc) calculateOrderTotals( + ctx mycontext.Context, + items []entity.OrderItemRequest, + productDetails *entity.ProductDetails, + source string, +) (*entity.OrderCalculation, error) { + subtotal := 0.0 + + for _, item := range items { + product, ok := productDetails.Products[item.ProductID] + if !ok { + return nil, errors.NewError(errors.ErrorInvalidRequest.ErrorType(), "product not found") + } + subtotal += product.Price * float64(item.Quantity) + } + + fee := s.cfg.GetOrderFee(source) + + return &entity.OrderCalculation{ + Subtotal: subtotal, + Fee: fee, + Total: subtotal + fee, + }, nil +} + +func (s *orderSvc) validateInquiry(ctx mycontext.Context, token string) (*entity.OrderInquiry, error) { + partnerID, inquiryID, err := s.crypt.ValidateJWTOrderInquiry(token) + if err != nil { + return nil, errors.NewError(errors.ErrorInvalidRequest.ErrorType(), "inquiry is not valid or expired") + } + + if partnerID != *ctx.GetPartnerID() { + return nil, errors.NewError(errors.ErrorInvalidRequest.ErrorType(), "invalid request") + } + + inquiry, err := s.repo.FindInquiryByID(ctx, inquiryID) + if err != nil { + logger.ContextLogger(ctx).Error("error when finding inquiry", zap.Error(err)) + return nil, err + } + + if inquiry.Status != constants.StatusPending { + return nil, errors.NewError(errors.ErrorInvalidRequest.ErrorType(), "inquiry is no longer pending") + } + + return inquiry, nil +} diff --git a/internal/services/v2/order/execute_order.go b/internal/services/v2/order/execute_order.go new file mode 100644 index 0000000..d8a43fa --- /dev/null +++ b/internal/services/v2/order/execute_order.go @@ -0,0 +1,173 @@ +package order + +import ( + "enaklo-pos-be/internal/common/logger" + "enaklo-pos-be/internal/common/mycontext" + "enaklo-pos-be/internal/constants" + "enaklo-pos-be/internal/entity" + "fmt" + "go.uber.org/zap" +) + +func (s *orderSvc) ExecuteOrderInquiry(ctx mycontext.Context, + token string, paymentMethod string) (*entity.OrderResponse, error) { + inquiry, err := s.validateInquiry(ctx, token) + if err != nil { + return nil, err + } + + order := inquiry.ToOrder(paymentMethod) + + savedOrder, err := s.repo.Create(ctx, order) + if err != nil { + logger.ContextLogger(ctx).Error("failed to create order", zap.Error(err)) + return nil, err + } + + err = s.processPostOrderActions(ctx, savedOrder, inquiry.ID, paymentMethod) + if err != nil { + logger.ContextLogger(ctx).Warn("some post-order actions failed", zap.Error(err)) + } + + return &entity.OrderResponse{ + Order: savedOrder, + }, nil +} + +func (s *orderSvc) processPostOrderActions( + ctx mycontext.Context, + order *entity.Order, + inquiryID string, + paymentMethod string, +) error { + err := s.repo.UpdateInquiryStatus(ctx, inquiryID, constants.StatusExecuted) + if err != nil { + logger.ContextLogger(ctx).Error("error when updating inquiry status", zap.Error(err)) + } + + trx, err := s.createTransaction(ctx, order, paymentMethod) + if err != nil { + logger.ContextLogger(ctx).Error("error when creating transaction", zap.Error(err)) + } + + if order.CustomerID != nil && *order.CustomerID > 0 { + err = s.addCustomerPoints(ctx, *order.CustomerID, int(order.Total)) + if err != nil { + logger.ContextLogger(ctx).Error("error when adding points", zap.Error(err)) + } + } + + s.sendTransactionReceipt(ctx, order, trx, "CASH") + return nil +} + +func (s *orderSvc) createTransaction(ctx mycontext.Context, order *entity.Order, paymentMethod string) (*entity.Transaction, error) { + transaction := &entity.Transaction{ + ID: constants.GenerateUUID(), + OrderID: order.ID, + Amount: order.Total, + PaymentMethod: paymentMethod, + Status: "SUCCESS", + CreatedAt: constants.TimeNow(), + PartnerID: order.PartnerID, + TransactionType: "TRANSACTION", + } + + _, err := s.transaction.Create(ctx, transaction) + + return transaction, err +} + +func (s *orderSvc) addCustomerPoints(ctx mycontext.Context, customerID int64, points int) error { + return s.customer.AddPoints(ctx, customerID, points) +} + +func (s *orderSvc) sendTransactionReceipt(ctx mycontext.Context, order *entity.Order, transaction *entity.Transaction, paymentMethod string) error { + if order.CustomerID == nil || *order.CustomerID == 0 { + return nil + } + + customer, err := s.customer.GetCustomer(ctx, *order.CustomerID) + if err != nil { + logger.ContextLogger(ctx).Error("error getting customer details", zap.Error(err)) + return err + } + + branchName := "Bakso 343 Rawamangun" + + var productIDs []int64 + productIDMap := make(map[int64]bool) + for _, item := range order.OrderItems { + if item.ItemID > 0 && !productIDMap[item.ItemID] { + productIDs = append(productIDs, item.ItemID) + productIDMap[item.ItemID] = true + } + } + + productMap := make(map[int64]*entity.Product) + if len(productIDs) > 0 { + products, err := s.product.GetProductsByIDs(ctx, productIDs, order.PartnerID) + if err != nil { + logger.ContextLogger(ctx).Error("error fetching products", zap.Error(err)) + } else { + for _, product := range products { + productMap[product.ID] = product + } + } + } + + var itemsData []map[string]string + for _, item := range order.OrderItems { + itemName := "Item" + + if product, exists := productMap[item.ItemID]; exists { + itemName = product.Name + } + + itemsData = append(itemsData, map[string]string{ + "ItemName": itemName, + "Quantity": fmt.Sprintf("%d", item.Quantity), + "Price": fmt.Sprintf("Rp %s", formatCurrency(item.Price)), + }) + } + + transactionDate := transaction.CreatedAt.Format("02 January 2006 15:04") + viewTransactionLink := fmt.Sprintf("https://enaklo.co.id/transaction/%s", transaction.ID) + + emailData := map[string]interface{}{ + "UserName": customer.Name, + "BranchName": branchName, + "TransactionNumber": order.ID, + "TransactionDate": transactionDate, + "PaymentMethod": formatPaymentMethod(paymentMethod), + "Items": itemsData, + "TotalPayment": fmt.Sprintf("Rp %s", formatCurrency(order.Total)), + "ViewTransactionLink": viewTransactionLink, + } + + return s.notification.SendEmailTransactional(ctx, entity.SendEmailNotificationParam{ + Sender: "noreply@enaklo.co.id", + Recipient: customer.Email, + Subject: "Enaklo - Resi Pembelian", + TemplateName: "transaction_receipt", + TemplatePath: "templates/transaction_receipt.html", + Data: emailData, + }) +} + +func formatCurrency(amount float64) string { + return fmt.Sprintf("%.2f", amount) +} + +func formatPaymentMethod(method string) string { + methodMap := map[string]string{ + "CASH": "Tunai", + "QRIS": "QRIS", + "CARD": "Kartu Kredit/Debit", + } + + if displayName, exists := methodMap[method]; exists { + return displayName + } + return method +} diff --git a/internal/services/v2/order/order.go b/internal/services/v2/order/order.go new file mode 100644 index 0000000..6a66502 --- /dev/null +++ b/internal/services/v2/order/order.go @@ -0,0 +1,80 @@ +package order + +import ( + "context" + "enaklo-pos-be/internal/common/mycontext" + "enaklo-pos-be/internal/entity" +) + +type Repository interface { + Create(ctx mycontext.Context, order *entity.Order) (*entity.Order, error) + FindByID(ctx mycontext.Context, id int64) (*entity.Order, error) + CreateInquiry(ctx mycontext.Context, inquiry *entity.OrderInquiry) (*entity.OrderInquiry, error) + FindInquiryByID(ctx mycontext.Context, id string) (*entity.OrderInquiry, error) + UpdateInquiryStatus(ctx mycontext.Context, id string, status string) error +} + +type ProductService interface { + GetProductDetails(ctx mycontext.Context, productIDs []int64, partnerID int64) (*entity.ProductDetails, error) + GetProductsByIDs(ctx mycontext.Context, ids []int64, partnerID int64) ([]*entity.Product, error) +} + +type CustomerService interface { + ResolveCustomer(ctx mycontext.Context, req *entity.CustomerResolutionRequest) (int64, error) + AddPoints(ctx mycontext.Context, customerID int64, points int) error + GetCustomer(ctx mycontext.Context, id int64) (*entity.Customer, error) +} + +type TransactionService interface { + Create(ctx mycontext.Context, transaction *entity.Transaction) (*entity.Transaction, error) +} + +type CryptService interface { + GenerateJWTOrderInquiry(inquiry *entity.OrderInquiry) (string, error) + ValidateJWTOrderInquiry(tokenString string) (int64, string, error) +} + +type NotificationService interface { + SendEmailTransactional(ctx context.Context, param entity.SendEmailNotificationParam) error +} + +type Service interface { + CreateOrderInquiry(ctx mycontext.Context, + req *entity.OrderRequest) (*entity.OrderInquiryResponse, error) + ExecuteOrderInquiry(ctx mycontext.Context, + token string, paymentMethod string) (*entity.OrderResponse, error) +} + +type Config interface { + GetOrderFee(source string) float64 +} + +type orderSvc struct { + repo Repository + product ProductService + customer CustomerService + transaction TransactionService + crypt CryptService + cfg Config + notification NotificationService +} + +func New( + repo Repository, + product ProductService, + customer CustomerService, + transaction TransactionService, + crypt CryptService, + cfg Config, + notification NotificationService, +) Service { + return &orderSvc{ + repo: repo, + product: product, + customer: customer, + transaction: transaction, + crypt: crypt, + cfg: cfg, + notification: notification, + } +} diff --git a/internal/services/v2/product/get_product_by_id.go b/internal/services/v2/product/get_product_by_id.go new file mode 100644 index 0000000..49074fc --- /dev/null +++ b/internal/services/v2/product/get_product_by_id.go @@ -0,0 +1,33 @@ +package product + +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" +) + +func (s *productSvc) GetProductsByIDs(ctx mycontext.Context, ids []int64, partnerID int64) ([]*entity.Product, error) { + if len(ids) == 0 { + return []*entity.Product{}, nil + } + + products, err := s.repo.GetProductsByIDs(ctx, ids, partnerID) + if err != nil { + logger.ContextLogger(ctx).Error("failed to get products by IDs", + zap.Int64s("productIDs", ids), + zap.Int64("partnerID", partnerID), + zap.Error(err)) + return nil, errors.Wrap(err, "failed to get products by IDs") + } + + // Validate that we found all requested products + if len(products) != len(ids) { + logger.ContextLogger(ctx).Warn("some products not found", + zap.Int("requestedCount", len(ids)), + zap.Int("foundCount", len(products))) + } + + return products, nil +} diff --git a/internal/services/v2/product/get_product_details.go b/internal/services/v2/product/get_product_details.go new file mode 100644 index 0000000..961b9f6 --- /dev/null +++ b/internal/services/v2/product/get_product_details.go @@ -0,0 +1,56 @@ +package product + +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" +) + +func (s *productSvc) GetProductDetails(ctx mycontext.Context, productIDs []int64, partnerID int64) (*entity.ProductDetails, error) { + if len(productIDs) == 0 { + return &entity.ProductDetails{ + Products: make(map[int64]*entity.Product), + }, nil + } + + productDetails, err := s.repo.GetProductDetails(ctx, productIDs, partnerID) + if err != nil { + logger.ContextLogger(ctx).Error("failed to get product details", + zap.Int64s("productIDs", productIDs), + zap.Int64("partnerID", partnerID), + zap.Error(err)) + return nil, errors.Wrap(err, "failed to get product details") + } + + if len(productDetails.Products) != len(productIDs) { + missingIDs := findMissingProductIDs(productIDs, productDetails.Products) + logger.ContextLogger(ctx).Warn("some products not found", + zap.Int("requestedCount", len(productIDs)), + zap.Int("foundCount", len(productDetails.Products)), + zap.Int64s("missingIDs", missingIDs)) + + if len(productDetails.Products) == 0 { + return nil, errors.New("no products found") + } + } + + return productDetails, nil +} + +func findMissingProductIDs(requestedIDs []int64, foundProducts map[int64]*entity.Product) []int64 { + var missingIDs []int64 + + for _, id := range requestedIDs { + if _, exists := foundProducts[id]; !exists { + missingIDs = append(missingIDs, id) + } + } + + return missingIDs +} + +func (s *productSvc) IsProductAvailable(product *entity.Product) bool { + return product.Status == "ACTIVE" +} diff --git a/internal/services/v2/product/product.go b/internal/services/v2/product/product.go new file mode 100644 index 0000000..8f7125d --- /dev/null +++ b/internal/services/v2/product/product.go @@ -0,0 +1,26 @@ +package product + +import ( + "enaklo-pos-be/internal/common/mycontext" + "enaklo-pos-be/internal/entity" +) + +type Repository interface { + GetProductsByIDs(ctx mycontext.Context, ids []int64, partnerID int64) ([]*entity.Product, error) + GetProductDetails(ctx mycontext.Context, productIDs []int64, partnerID int64) (*entity.ProductDetails, error) +} + +type Service interface { + GetProductsByIDs(ctx mycontext.Context, ids []int64, partnerID int64) ([]*entity.Product, error) + GetProductDetails(ctx mycontext.Context, productIDs []int64, partnerID int64) (*entity.ProductDetails, error) +} + +type productSvc struct { + repo Repository +} + +func New(repo Repository) Service { + return &productSvc{ + repo: repo, + } +} diff --git a/k8s/staging/ingress.yaml b/k8s/staging/ingress.yaml index d4bda78..a43eb23 100644 --- a/k8s/staging/ingress.yaml +++ b/k8s/staging/ingress.yaml @@ -22,4 +22,4 @@ spec: tls: - hosts: - "api-dev.enaklo.co.id" - secretName: enaklo-pos-backend-app-dev-biz-id-tls + secretName: enaklo-pos-dev-app-dev-biz-id-tls diff --git a/main.go b/main.go index ead25bb..a349d69 100644 --- a/main.go +++ b/main.go @@ -32,6 +32,7 @@ func main() { routes.RegisterPublicRoutes(server, service, repo) routes.RegisterPrivateRoutes(server, service, repo) + routes.RegisterPrivateRoutesV2(server, service, repo) routes.RegisterCustomerRoutes(server, service, repo) server.StartScheduler() diff --git a/templates/monthly_points.html b/templates/monthly_points.html new file mode 100644 index 0000000..359d2f9 --- /dev/null +++ b/templates/monthly_points.html @@ -0,0 +1,159 @@ + + + + + Laporan Keanggotaan Anda + + + + + + + +
+
+ +
Laporan Keanggotaan Anda
+
+ Hi {{ .UserName }},

+ Berikut adalah laporan keanggotaan Anda untuk bulan ini. Saldo {{ .PointsName }} Anda saat ini adalah: +
+
{{ .PointsBalance }} {{ .PointsName }}
+
+ Gunakan {{ .PointsName }} Anda untuk menukarkan diskon spesial dan hadiah menarik!
+ Cek penawaran terbaru dan nikmati makanan favorit Anda. +
+ Tukarkan Sekarang +
+ +
+
+ + + diff --git a/templates/transaction_receipt.html b/templates/transaction_receipt.html new file mode 100644 index 0000000..ce85c95 --- /dev/null +++ b/templates/transaction_receipt.html @@ -0,0 +1,194 @@ + + + + + Enaklo - Resi Pembelian + + + + + + + +
+
+ +
Resi Pembelian Anda
+ +
+ Hi {{ .UserName }}

+ Terima kasih telah bertransaksi di Enaklo {{ .BranchName }}
Berikut adalah rincian pembelian Anda: +
+ +
+ ID Transaksi: {{ .TransactionNumber }}
+ Tanggal: {{ .TransactionDate }}
+ Metode Pembayaran: {{ .PaymentMethod }} +
+ +
+ + + + + + + {{ range .Items }} + + + + + + {{ end }} +
Nama ItemJumlahHarga
{{ .ItemName }}{{ .Quantity }}{{ .Price }}
+
+ +
+ Total Pembayaran: {{ .TotalPayment }} +
+ + Lihat Detail Pesanan + +
+ + +
+
+ + +