Update template email
This commit is contained in:
parent
3c80b710af
commit
18003313dd
1
go.mod
1
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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
8
internal/entity/cust.go
Normal file
8
internal/entity/cust.go
Normal file
@ -0,0 +1,8 @@
|
||||
package entity
|
||||
|
||||
type CustomerResolutionRequest struct {
|
||||
ID *int64
|
||||
Name string
|
||||
Email string
|
||||
PhoneNumber string
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
114
internal/entity/order_inquiry.go
Normal file
114
internal/entity/order_inquiry.go
Normal file
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -34,6 +34,8 @@ type Customer struct {
|
||||
Name string
|
||||
Email string
|
||||
Password string
|
||||
Phone string
|
||||
Points int
|
||||
Status userstatus.UserStatus
|
||||
NIK string
|
||||
UserType string
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
126
internal/handlers/http/order.go
Normal file
126
internal/handlers/http/order.go
Normal file
@ -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),
|
||||
})
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"`
|
||||
|
||||
@ -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"`
|
||||
}
|
||||
|
||||
119
internal/handlers/response/order_inquiry.go
Normal file
119
internal/handlers/response/order_inquiry.go
Normal file
@ -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
|
||||
}
|
||||
@ -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"`
|
||||
|
||||
@ -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"})
|
||||
|
||||
@ -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{
|
||||
|
||||
@ -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 {
|
||||
|
||||
128
internal/repository/customer_repo.go
Normal file
128
internal/repository/customer_repo.go
Normal file
@ -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,
|
||||
}
|
||||
}
|
||||
19
internal/repository/models/customer.go
Normal file
19
internal/repository/models/customer.go
Normal file
@ -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"
|
||||
}
|
||||
80
internal/repository/models/order.go
Normal file
80
internal/repository/models/order.go
Normal file
@ -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"
|
||||
}
|
||||
22
internal/repository/models/product.go
Normal file
22
internal/repository/models/product.go
Normal file
@ -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"
|
||||
}
|
||||
21
internal/repository/models/transaction.go
Normal file
21
internal/repository/models/transaction.go
Normal file
@ -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"
|
||||
}
|
||||
292
internal/repository/orde_repo.go
Normal file
292
internal/repository/orde_repo.go
Normal file
@ -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
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
82
internal/repository/product_repo.go
Normal file
82
internal/repository/product_repo.go
Normal file
@ -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,
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
76
internal/repository/transaction_repo.go
Normal file
76
internal/repository/transaction_repo.go
Normal file
@ -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,
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
117
internal/services/v2/customer/customer.go
Normal file
117
internal/services/v2/customer/customer.go
Normal file
@ -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
|
||||
}
|
||||
144
internal/services/v2/order/create_order_inquiry.go
Normal file
144
internal/services/v2/order/create_order_inquiry.go
Normal file
@ -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
|
||||
}
|
||||
173
internal/services/v2/order/execute_order.go
Normal file
173
internal/services/v2/order/execute_order.go
Normal file
@ -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
|
||||
}
|
||||
80
internal/services/v2/order/order.go
Normal file
80
internal/services/v2/order/order.go
Normal file
@ -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,
|
||||
}
|
||||
}
|
||||
33
internal/services/v2/product/get_product_by_id.go
Normal file
33
internal/services/v2/product/get_product_by_id.go
Normal file
@ -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
|
||||
}
|
||||
56
internal/services/v2/product/get_product_details.go
Normal file
56
internal/services/v2/product/get_product_details.go
Normal file
@ -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"
|
||||
}
|
||||
26
internal/services/v2/product/product.go
Normal file
26
internal/services/v2/product/product.go
Normal file
@ -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,
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
1
main.go
1
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()
|
||||
|
||||
159
templates/monthly_points.html
Normal file
159
templates/monthly_points.html
Normal file
@ -0,0 +1,159 @@
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<title>Laporan Keanggotaan Anda</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style type="text/css">
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
background-color: #f1f0f7;
|
||||
}
|
||||
|
||||
table,
|
||||
td {
|
||||
border-collapse: collapse;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
p {
|
||||
display: block;
|
||||
margin: 13px 0;
|
||||
}
|
||||
|
||||
.mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
background-color: #ffffff;
|
||||
margin: 0px auto;
|
||||
max-width: 600px;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header {
|
||||
font-family: Ubuntu, Helvetica, Arial, sans-serif;
|
||||
font-size: 24px;
|
||||
line-height: 28px;
|
||||
color: #f46f02;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-family: Ubuntu, Helvetica, Arial, sans-serif;
|
||||
font-size: 20px;
|
||||
line-height: 22px;
|
||||
color: #000000;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-family: Ubuntu, Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
color: #000000;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.points {
|
||||
display: block;
|
||||
width: fit-content;
|
||||
margin: 20px auto;
|
||||
background-color: #f5f5f5;
|
||||
color: #000000;
|
||||
text-align: center;
|
||||
padding: 15px 25px;
|
||||
border-radius: 5px;
|
||||
font-family: Ubuntu, Helvetica, Arial, sans-serif;
|
||||
font-size: 22px;
|
||||
line-height: 24px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
font-family: Ubuntu, Helvetica, Arial, sans-serif;
|
||||
font-size: 12px;
|
||||
line-height: 15px;
|
||||
text-align: center;
|
||||
color: #808080;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
border-top: solid 1px #808080;
|
||||
margin: 20px auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: block;
|
||||
margin: 0 auto 20px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.cta-button {
|
||||
display: block;
|
||||
width: fit-content;
|
||||
margin: 20px auto;
|
||||
padding: 12px 20px;
|
||||
background-color: #d90000;
|
||||
color: #ffffff !important;
|
||||
font-size: 16px;
|
||||
font-family: Ubuntu, Helvetica, Arial, sans-serif;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
border-radius: 5px;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div style="padding: 50px; background-color: #f1f0f7;">
|
||||
<div class="content">
|
||||
<img src="https://res.cloudinary.com/dl0wpumax/image/upload/c_thumb,w_200,g_face/v1741363977/61747686_5_vtz0n4.png" alt="Enaklo Logo" class="logo">
|
||||
<div class="title">Laporan Keanggotaan Anda</div>
|
||||
<div class="text">
|
||||
Hi {{ .UserName }},<br><br>
|
||||
Berikut adalah laporan keanggotaan Anda untuk bulan ini. Saldo <b>{{ .PointsName }}</b> Anda saat ini adalah:
|
||||
</div>
|
||||
<div class="points">{{ .PointsBalance }} {{ .PointsName }}</div>
|
||||
<div class="text">
|
||||
Gunakan {{ .PointsName }} Anda untuk menukarkan diskon spesial dan hadiah menarik! <br>
|
||||
Cek penawaran terbaru dan nikmati makanan favorit Anda.
|
||||
</div>
|
||||
<a href="{{ .RedeemLink }}" class="cta-button">Tukarkan Sekarang</a>
|
||||
<div class="divider"></div>
|
||||
<div class="footer">
|
||||
Email ini dikirim secara otomatis. Mohon jangan membalas email ini. <br>
|
||||
Butuh bantuan? Hubungi tim support kami di <a href="mailto:support@enaklo.com">support@enaklo.com</a>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
194
templates/transaction_receipt.html
Normal file
194
templates/transaction_receipt.html
Normal file
@ -0,0 +1,194 @@
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<title>Enaklo - Resi Pembelian</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style type="text/css">
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
background-color: #f1f0f7;
|
||||
}
|
||||
|
||||
table,
|
||||
td {
|
||||
border-collapse: collapse;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
p {
|
||||
display: block;
|
||||
margin: 13px 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
background-color: #ffffff;
|
||||
margin: 0px auto;
|
||||
max-width: 600px;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-family: Ubuntu, Helvetica, Arial, sans-serif;
|
||||
font-size: 20px;
|
||||
line-height: 22px;
|
||||
color: #000000;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-family: Ubuntu, Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
color: #000000;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.points {
|
||||
display: block;
|
||||
width: fit-content;
|
||||
margin: 20px auto;
|
||||
background-color: #f5f5f5;
|
||||
color: #000000;
|
||||
text-align: center;
|
||||
padding: 15px 25px;
|
||||
border-radius: 5px;
|
||||
font-family: Ubuntu, Helvetica, Arial, sans-serif;
|
||||
font-size: 22px;
|
||||
line-height: 24px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
font-family: Ubuntu, Helvetica, Arial, sans-serif;
|
||||
font-size: 12px;
|
||||
line-height: 15px;
|
||||
text-align: center;
|
||||
color: #808080;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
border-top: solid 1px #808080;
|
||||
margin: 20px auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: block;
|
||||
margin: 0 auto 20px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.cta-button {
|
||||
display: block;
|
||||
width: fit-content;
|
||||
margin: 20px auto;
|
||||
padding: 12px 20px;
|
||||
background-color: #d90000;
|
||||
color: #ffffff !important;
|
||||
font-size: 16px;
|
||||
font-family: Ubuntu, Helvetica, Arial, sans-serif;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
text-decoration: none !important;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
width: 100%;
|
||||
margin: 20px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.table-container table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.table-container th,
|
||||
.table-container td {
|
||||
border: 1px solid #dddddd;
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
font-family: Ubuntu, Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.table-container th {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div style="padding: 50px; background-color: #f1f0f7;">
|
||||
<div class="content">
|
||||
<img src="https://res.cloudinary.com/dl0wpumax/image/upload/c_thumb,w_200,g_face/v1741363977/61747686_5_vtz0n4.png" alt="Enaklo Logo" class="logo">
|
||||
<div class="title">Resi Pembelian Anda</div>
|
||||
|
||||
<div class="text">
|
||||
Hi {{ .UserName }}<br><br>
|
||||
Terima kasih telah bertransaksi di Enaklo {{ .BranchName }} <br>Berikut adalah rincian pembelian Anda:
|
||||
</div>
|
||||
|
||||
<div class="text">
|
||||
<strong>ID Transaksi:</strong> {{ .TransactionNumber }} <br>
|
||||
<strong>Tanggal:</strong> {{ .TransactionDate }} <br>
|
||||
<strong>Metode Pembayaran:</strong> {{ .PaymentMethod }}
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<tr>
|
||||
<th>Nama Item</th>
|
||||
<th>Jumlah</th>
|
||||
<th>Harga</th>
|
||||
</tr>
|
||||
{{ range .Items }}
|
||||
<tr>
|
||||
<td>{{ .ItemName }}</td>
|
||||
<td>{{ .Quantity }}</td>
|
||||
<td>{{ .Price }}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="text">
|
||||
<strong>Total Pembayaran:</strong> {{ .TotalPayment }}
|
||||
</div>
|
||||
|
||||
<a href="{{ .ViewTransactionLink }}" class="cta-button">Lihat Detail Pesanan</a>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="footer">
|
||||
Jika Anda memiliki pertanyaan atau membutuhkan bantuan, silakan hubungi kami di
|
||||
<a href="mailto:support@enaklo.com">support@enaklo.com</a>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user