Update template email
This commit is contained in:
parent
3c80b710af
commit
18003313dd
1
go.mod
1
go.mod
@ -80,6 +80,7 @@ require (
|
|||||||
require (
|
require (
|
||||||
github.com/aws/aws-sdk-go v1.50.0
|
github.com/aws/aws-sdk-go v1.50.0
|
||||||
github.com/getbrevo/brevo-go v1.0.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/veritrans/go-midtrans v0.0.0-20210616100512-16326c5eeb00
|
||||||
github.com/xuri/excelize/v2 v2.9.0
|
github.com/xuri/excelize/v2 v2.9.0
|
||||||
go.uber.org/zap v1.21.0
|
go.uber.org/zap v1.21.0
|
||||||
|
|||||||
@ -28,12 +28,12 @@ postgresql:
|
|||||||
debug: false
|
debug: false
|
||||||
|
|
||||||
oss:
|
oss:
|
||||||
access_key_id: e50b31e5eddf63c0ZKB2
|
access_key_id: cf9a475e18bc7626cbdbf09709d82a64
|
||||||
access_key_secret: GAyX9jiCWyTwgJMuqzun2x0zHS3kjQt26kyzY21S
|
access_key_secret: 91f3321294d3e23035427a0ecb893ada
|
||||||
endpoint: obs.eranyacloud.com
|
endpoint: sin1.contabostorage.com
|
||||||
bucket_name: enaklo-pos
|
bucket_name: enaklo
|
||||||
log_level: Error # type: LogOff, Debug, Error, Warn, Info
|
log_level: Error
|
||||||
host_url: https://obs.eranyacloud.com
|
host_url: 'https://sin1.contabostorage.com/fda98c2228f246f29a7e466b86b3b9e7:'
|
||||||
|
|
||||||
midtrans:
|
midtrans:
|
||||||
server_key: "SB-Mid-server-YOIvuaIlRw3In9SymCuFz-hB"
|
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"
|
callback_url: "https://enaklo-pos-be.app-dev.altru.id/api/v1/linkqu/callback"
|
||||||
|
|
||||||
brevo:
|
brevo:
|
||||||
api_key: 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
|
api_key: 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
|
||||||
|
|
||||||
email:
|
email:
|
||||||
sender: "enaklo-pos.official@gmail.com"
|
sender: "noreply@enaklo.co.id"
|
||||||
sender_customer: "enaklo-pos.official@gmail.com"
|
sender_customer: "enaklo-pos.official@gmail.com"
|
||||||
reset_password:
|
reset_password:
|
||||||
template_name: "reset_password"
|
template_name: "reset_password"
|
||||||
|
|||||||
@ -21,6 +21,7 @@ const (
|
|||||||
errInsufficientBalance ErrType = "Insufficient Balance"
|
errInsufficientBalance ErrType = "Insufficient Balance"
|
||||||
errInactivePartner ErrType = "Partner's license is invalid or has expired. Please contact Admin Support."
|
errInactivePartner ErrType = "Partner's license is invalid or has expired. Please contact Admin Support."
|
||||||
errTicketAlreadyUsed ErrType = "Ticket Already Used."
|
errTicketAlreadyUsed ErrType = "Ticket Already Used."
|
||||||
|
errProductIsRequired ErrType = "Product"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
package constants
|
package constants
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ContextRequestID string = "requestId"
|
ContextRequestID string = "requestId"
|
||||||
)
|
)
|
||||||
@ -9,3 +14,41 @@ type UserType string
|
|||||||
func (u UserType) toString() string {
|
func (u UserType) toString() string {
|
||||||
return string(u)
|
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
|
||||||
|
}
|
||||||
@ -16,6 +16,7 @@ type JWTAuthClaims struct {
|
|||||||
type JWTOrderClaims struct {
|
type JWTOrderClaims struct {
|
||||||
PartnerID int64 `json:"id"`
|
PartnerID int64 `json:"id"`
|
||||||
OrderID int64 `json:"order_id"`
|
OrderID int64 `json:"order_id"`
|
||||||
|
InquiryID string `json:"inquiry_id"`
|
||||||
jwt.StandardClaims
|
jwt.StandardClaims
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,19 +1,18 @@
|
|||||||
package entity
|
package entity
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"gorm.io/datatypes"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Order struct {
|
type Order struct {
|
||||||
ID int64 `gorm:"primaryKey;autoIncrement;column:id"`
|
ID int64 `gorm:"primaryKey;autoIncrement;column:id"`
|
||||||
RefID string `gorm:"type:varchar;column:ref_id"`
|
|
||||||
PartnerID int64 `gorm:"type:int;column:partner_id"`
|
PartnerID int64 `gorm:"type:int;column:partner_id"`
|
||||||
Status string `gorm:"type:varchar;column:status"`
|
Status string `gorm:"type:varchar;column:status"`
|
||||||
Amount float64 `gorm:"type:numeric;not null;column:amount"`
|
Amount float64 `gorm:"type:numeric;not null;column:amount"`
|
||||||
Total float64 `gorm:"type:numeric;not null;column:total"`
|
Total float64 `gorm:"type:numeric;not null;column:total"`
|
||||||
Fee float64 `gorm:"type:numeric;not null;column:fee"`
|
Fee float64 `gorm:"type:numeric;not null;column:fee"`
|
||||||
SiteID *int64 `gorm:"type:numeric;not null;column:site_id"`
|
CustomerID *int64
|
||||||
|
InquiryID *string
|
||||||
Site *Site `gorm:"foreignKey:SiteID;constraint:OnDelete:CASCADE;"`
|
Site *Site `gorm:"foreignKey:SiteID;constraint:OnDelete:CASCADE;"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime;column:created_at"`
|
CreatedAt time.Time `gorm:"autoCreateTime;column:created_at"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at"`
|
||||||
@ -24,9 +23,6 @@ type Order struct {
|
|||||||
Payment Payment `gorm:"foreignKey:OrderID;constraint:OnDelete:CASCADE;"`
|
Payment Payment `gorm:"foreignKey:OrderID;constraint:OnDelete:CASCADE;"`
|
||||||
User User `gorm:"foreignKey:CreatedBy;constraint:OnDelete:CASCADE;"`
|
User User `gorm:"foreignKey:CreatedBy;constraint:OnDelete:CASCADE;"`
|
||||||
Source string `gorm:"type:varchar;column:source"`
|
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"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type OrderDB struct {
|
type OrderDB struct {
|
||||||
@ -47,7 +43,6 @@ func (e *OrderDB) ToSumAmount() *Order {
|
|||||||
|
|
||||||
type OrderResponse struct {
|
type OrderResponse struct {
|
||||||
Order *Order
|
Order *Order
|
||||||
Token string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type CheckinResponse struct {
|
type CheckinResponse struct {
|
||||||
@ -80,7 +75,7 @@ type OrderItem struct {
|
|||||||
ItemID int64 `gorm:"type:int;column:item_id"`
|
ItemID int64 `gorm:"type:int;column:item_id"`
|
||||||
ItemType string `gorm:"type:varchar;column:item_type"`
|
ItemType string `gorm:"type:varchar;column:item_type"`
|
||||||
Price float64 `gorm:"type:numeric;not null;column:price"`
|
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"`
|
CreatedAt time.Time `gorm:"autoCreateTime;column:created_at"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at"`
|
||||||
CreatedBy int64 `gorm:"type:int;column:created_by"`
|
CreatedBy int64 `gorm:"type:int;column:created_by"`
|
||||||
@ -95,17 +90,18 @@ func (OrderItem) TableName() string {
|
|||||||
type OrderRequest struct {
|
type OrderRequest struct {
|
||||||
Source string
|
Source string
|
||||||
CreatedBy int64
|
CreatedBy int64
|
||||||
PartnerID int64 `json:"partner_id" validate:"required"`
|
PartnerID int64
|
||||||
PaymentMethod string `json:"payment_method" validate:"required"`
|
PaymentMethod string
|
||||||
OrderItems []OrderItemRequest `json:"order_items" validate:"required,dive"`
|
OrderItems []OrderItemRequest
|
||||||
VisitDate string `json:"visit_date"`
|
CustomerID *int64
|
||||||
BankCode string `json:"bank_code"`
|
CustomerName string
|
||||||
BankName string `json:"bank_name"`
|
CustomerEmail string
|
||||||
|
CustomerPhoneNumber string
|
||||||
}
|
}
|
||||||
|
|
||||||
type OrderItemRequest struct {
|
type OrderItemRequest struct {
|
||||||
ProductID int64 `json:"product_id" validate:"required"`
|
ProductID int64 `json:"product_id" validate:"required"`
|
||||||
Quantity int64 `json:"quantity" validate:"required"`
|
Quantity int `json:"quantity" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OrderExecuteRequest struct {
|
type OrderExecuteRequest struct {
|
||||||
@ -117,7 +113,6 @@ type OrderExecuteRequest struct {
|
|||||||
func (o *Order) SetExecutePaymentStatus() {
|
func (o *Order) SetExecutePaymentStatus() {
|
||||||
if o.PaymentType == "CASH" {
|
if o.PaymentType == "CASH" {
|
||||||
o.Status = "PAID"
|
o.Status = "PAID"
|
||||||
o.TicketStatus = "USED"
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
o.Status = "PENDING"
|
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 {
|
type Product struct {
|
||||||
ID int64 `gorm:"primaryKey;autoIncrement;column:id"`
|
ID int64 `gorm:"primaryKey;autoIncrement;column:id"`
|
||||||
PartnerID int64 `gorm:"type:int;column:partner_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"`
|
Name string `gorm:"type:varchar(255);not null;column:name"`
|
||||||
Type string `gorm:"type:varchar;column:type"`
|
Type string `gorm:"type:varchar;column:type"`
|
||||||
Price float64 `gorm:"type:decimal;column:price"`
|
Price float64 `gorm:"type:decimal;column:price"`
|
||||||
@ -19,7 +18,7 @@ type Product struct {
|
|||||||
DeletedAt *time.Time `gorm:"column:deleted_at"`
|
DeletedAt *time.Time `gorm:"column:deleted_at"`
|
||||||
CreatedBy int64 `gorm:"type:int;column:created_by"`
|
CreatedBy int64 `gorm:"type:int;column:created_by"`
|
||||||
UpdatedBy int64 `gorm:"type:int;column:updated_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 {
|
func (Product) TableName() string {
|
||||||
@ -71,7 +70,7 @@ func (e *ProductDB) ToProduct() *Product {
|
|||||||
DeletedAt: e.DeletedAt,
|
DeletedAt: e.DeletedAt,
|
||||||
CreatedBy: e.CreatedBy,
|
CreatedBy: e.CreatedBy,
|
||||||
UpdatedBy: e.UpdatedBy,
|
UpdatedBy: e.UpdatedBy,
|
||||||
SiteID: e.SiteID,
|
Image: e.Image,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,10 +78,8 @@ func (b *ProductList) ToProductList() []*Product {
|
|||||||
var Products []*Product
|
var Products []*Product
|
||||||
|
|
||||||
for _, p := range *b {
|
for _, p := range *b {
|
||||||
if p.Status == "Available" {
|
|
||||||
Products = append(Products, p.ToProduct())
|
Products = append(Products, p.ToProduct())
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return Products
|
return Products
|
||||||
}
|
}
|
||||||
@ -126,3 +123,8 @@ func (o *ProductDB) SetDeleted(updatedby int64) {
|
|||||||
o.DeletedAt = ¤tTime
|
o.DeletedAt = ¤tTime
|
||||||
o.UpdatedBy = updatedby
|
o.UpdatedBy = updatedby
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProductDetails struct {
|
||||||
|
Products map[int64]*Product // Map for quick lookups by ID
|
||||||
|
PartnerID int64 // Common site ID for all products
|
||||||
|
}
|
||||||
|
|||||||
@ -6,16 +6,16 @@ import (
|
|||||||
|
|
||||||
type Transaction struct {
|
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"`
|
PartnerID int64 `gorm:"not null"`
|
||||||
TransactionType string `gorm:"not null"`
|
TransactionType string `gorm:"not null"`
|
||||||
ReferenceID string `gorm:"size:255"`
|
|
||||||
Status string `gorm:"size:255"`
|
Status string `gorm:"size:255"`
|
||||||
CreatedBy int64 `gorm:"not null"`
|
CreatedBy int64 `gorm:"not null"`
|
||||||
UpdatedBy int64 `gorm:"not null"`
|
UpdatedBy int64 `gorm:"not null"`
|
||||||
Amount float64 `gorm:"not null"`
|
Amount float64 `gorm:"not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
SiteID *int64
|
PaymentMethod string `json:"payment_method"`
|
||||||
Fee float64
|
Fee float64
|
||||||
Total float64
|
Total float64
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,6 +34,8 @@ type Customer struct {
|
|||||||
Name string
|
Name string
|
||||||
Email string
|
Email string
|
||||||
Password string
|
Password string
|
||||||
|
Phone string
|
||||||
|
Points int
|
||||||
Status userstatus.UserStatus
|
Status userstatus.UserStatus
|
||||||
NIK string
|
NIK string
|
||||||
UserType string
|
UserType string
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import (
|
|||||||
"enaklo-pos-be/internal/handlers/request"
|
"enaklo-pos-be/internal/handlers/request"
|
||||||
"enaklo-pos-be/internal/handlers/response"
|
"enaklo-pos-be/internal/handlers/response"
|
||||||
"enaklo-pos-be/internal/services"
|
"enaklo-pos-be/internal/services"
|
||||||
"enaklo-pos-be/internal/utils"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
@ -22,7 +21,7 @@ type Handler struct {
|
|||||||
func (h *Handler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) {
|
func (h *Handler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) {
|
||||||
route := group.Group("/order")
|
route := group.Group("/order")
|
||||||
|
|
||||||
route.POST("/inquiry", jwt, h.Inquiry)
|
route.POST("/inquiry", h.Inquiry)
|
||||||
route.POST("/execute", jwt, h.Execute)
|
route.POST("/execute", jwt, h.Execute)
|
||||||
route.GET("/history", jwt, h.History)
|
route.GET("/history", jwt, h.History)
|
||||||
route.GET("/detail", jwt, h.Detail)
|
route.GET("/detail", jwt, h.Detail)
|
||||||
@ -104,20 +103,14 @@ func MapOrderToCreateOrderResponse(orderResponse *entity.OrderResponse, req requ
|
|||||||
|
|
||||||
return response.CreateOrderResponse{
|
return response.CreateOrderResponse{
|
||||||
ID: order.ID,
|
ID: order.ID,
|
||||||
RefID: order.RefID,
|
|
||||||
PartnerID: order.PartnerID,
|
PartnerID: order.PartnerID,
|
||||||
Status: order.Status,
|
Status: order.Status,
|
||||||
Amount: order.Amount,
|
Amount: order.Amount,
|
||||||
PaymentType: order.PaymentType,
|
PaymentType: order.PaymentType,
|
||||||
CreatedAt: order.CreatedAt,
|
CreatedAt: order.CreatedAt,
|
||||||
OrderItems: orderItems,
|
OrderItems: orderItems,
|
||||||
Token: orderResponse.Token,
|
|
||||||
Fee: order.Fee,
|
Fee: order.Fee,
|
||||||
Total: order.Total,
|
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{
|
return response.ExecuteOrderResponse{
|
||||||
ID: order.ID,
|
ID: order.ID,
|
||||||
RefID: order.RefID,
|
|
||||||
PartnerID: order.PartnerID,
|
PartnerID: order.PartnerID,
|
||||||
Status: order.Status,
|
Status: order.Status,
|
||||||
Amount: order.Amount,
|
Amount: order.Amount,
|
||||||
@ -239,10 +231,6 @@ func (h *Handler) toOrderDetail(order *entity.Order) *response.OrderDetail {
|
|||||||
|
|
||||||
qrCode := ""
|
qrCode := ""
|
||||||
|
|
||||||
if order.Status == "PAID" {
|
|
||||||
qrCode = order.RefID
|
|
||||||
}
|
|
||||||
|
|
||||||
var siteName string
|
var siteName string
|
||||||
|
|
||||||
if order.Site != nil {
|
if order.Site != nil {
|
||||||
|
|||||||
@ -214,7 +214,6 @@ func ConvertToProductResp(resp []*entity.Product) *response.SearchProductSiteRes
|
|||||||
productResp = append(productResp, response.SearchProductSiteByIDResponse{
|
productResp = append(productResp, response.SearchProductSiteByIDResponse{
|
||||||
ID: res.ID,
|
ID: res.ID,
|
||||||
Name: res.Name,
|
Name: res.Name,
|
||||||
SiteID: res.SiteID,
|
|
||||||
Price: res.Price,
|
Price: res.Price,
|
||||||
Description: res.Description,
|
Description: res.Description,
|
||||||
Type: res.Type,
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ctx.IsCasheer() {
|
|
||||||
response.ErrorWrapper(c, errors.ErrorBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// override the partner_id
|
|
||||||
req.PartnerID = *ctx.GetPartnerID()
|
|
||||||
|
|
||||||
validate := validator.New()
|
validate := validator.New()
|
||||||
if err := validate.Struct(req); err != nil {
|
if err := validate.Struct(req); err != nil {
|
||||||
response.ErrorWrapper(c, err)
|
response.ErrorWrapper(c, err)
|
||||||
return
|
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 {
|
if err != nil {
|
||||||
response.ErrorWrapper(c, err)
|
response.ErrorWrapper(c, err)
|
||||||
return
|
return
|
||||||
@ -206,7 +200,6 @@ func MapOrderToCreateOrderResponse(orderResponse *entity.OrderResponse) response
|
|||||||
|
|
||||||
return response.CreateOrderResponse{
|
return response.CreateOrderResponse{
|
||||||
ID: order.ID,
|
ID: order.ID,
|
||||||
RefID: order.RefID,
|
|
||||||
PartnerID: order.PartnerID,
|
PartnerID: order.PartnerID,
|
||||||
Status: order.Status,
|
Status: order.Status,
|
||||||
Amount: order.Amount,
|
Amount: order.Amount,
|
||||||
@ -215,7 +208,6 @@ func MapOrderToCreateOrderResponse(orderResponse *entity.OrderResponse) response
|
|||||||
PaymentType: order.PaymentType,
|
PaymentType: order.PaymentType,
|
||||||
CreatedAt: order.CreatedAt,
|
CreatedAt: order.CreatedAt,
|
||||||
OrderItems: orderItems,
|
OrderItems: orderItems,
|
||||||
Token: orderResponse.Token,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -234,7 +226,6 @@ func MapOrderToExecuteOrderResponse(orderResponse *entity.ExecuteOrderResponse)
|
|||||||
|
|
||||||
return response.ExecuteOrderResponse{
|
return response.ExecuteOrderResponse{
|
||||||
ID: order.ID,
|
ID: order.ID,
|
||||||
RefID: order.RefID,
|
|
||||||
PartnerID: order.PartnerID,
|
PartnerID: order.PartnerID,
|
||||||
Status: order.Status,
|
Status: order.Status,
|
||||||
Amount: order.Amount,
|
Amount: order.Amount,
|
||||||
@ -261,7 +252,6 @@ func MapOrderToExecuteCheckinResponse(order *entity.Order) response.ExecuteCheck
|
|||||||
|
|
||||||
return response.ExecuteCheckinResponse{
|
return response.ExecuteCheckinResponse{
|
||||||
ID: order.ID,
|
ID: order.ID,
|
||||||
RefID: order.RefID,
|
|
||||||
PartnerID: order.PartnerID,
|
PartnerID: order.PartnerID,
|
||||||
Status: order.Status,
|
Status: order.Status,
|
||||||
Amount: order.Amount,
|
Amount: order.Amount,
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import (
|
|||||||
|
|
||||||
const _oneMB = 1 << 20 // 1MB
|
const _oneMB = 1 << 20 // 1MB
|
||||||
const _maxUploadSizeMB = 2 * _oneMB
|
const _maxUploadSizeMB = 2 * _oneMB
|
||||||
const _folderName = "/file"
|
const _folderName = "/public"
|
||||||
|
|
||||||
type OssHandler struct {
|
type OssHandler struct {
|
||||||
ossService services.OSSService
|
ossService services.OSSService
|
||||||
@ -22,7 +22,7 @@ type OssHandler struct {
|
|||||||
func (h *OssHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) {
|
func (h *OssHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) {
|
||||||
route := group.Group("/file")
|
route := group.Group("/file")
|
||||||
|
|
||||||
route.POST("/upload", h.UploadFile, jwt)
|
route.POST("/upload", h.UploadFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOssHandler(ossService services.OSSService) *OssHandler {
|
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),
|
CreatedAt: resp.CreatedAt.Format(time.RFC3339),
|
||||||
UpdatedAt: resp.CreatedAt.Format(time.RFC3339),
|
UpdatedAt: resp.CreatedAt.Format(time.RFC3339),
|
||||||
PartnerID: resp.PartnerID,
|
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{
|
res = append(res, response.Product{
|
||||||
ID: product.ID,
|
ID: product.ID,
|
||||||
PartnerID: product.PartnerID,
|
PartnerID: product.PartnerID,
|
||||||
SiteID: product.SiteID,
|
|
||||||
Name: product.Name,
|
Name: product.Name,
|
||||||
Type: product.Type,
|
Type: product.Type,
|
||||||
Price: product.Price,
|
Price: product.Price,
|
||||||
|
|||||||
@ -4,13 +4,14 @@ import (
|
|||||||
"enaklo-pos-be/internal/common/mycontext"
|
"enaklo-pos-be/internal/common/mycontext"
|
||||||
"enaklo-pos-be/internal/constants/transaction"
|
"enaklo-pos-be/internal/constants/transaction"
|
||||||
"enaklo-pos-be/internal/entity"
|
"enaklo-pos-be/internal/entity"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Order struct {
|
type Order struct {
|
||||||
PartnerID int64 `json:"partner_id" validate:"required"`
|
CustomerName string `json:"customer_name"`
|
||||||
PaymentMethod transaction.PaymentMethod `json:"payment_method" validate:"required"`
|
CustomerPhone string `json:"customer_phone"`
|
||||||
OrderItems []OrderItem `json:"order_items" validate:"required"`
|
CustomerEmail string `json:"customer_email"`
|
||||||
|
PaymentMethod string `json:"payment_method"`
|
||||||
|
OrderItems []OrderItem `json:"order_items"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CustomerOrder struct {
|
type CustomerOrder struct {
|
||||||
@ -36,8 +37,6 @@ func (o *CustomerOrder) ToEntity(createdBy int64) *entity.OrderRequest {
|
|||||||
OrderItems: orderItems,
|
OrderItems: orderItems,
|
||||||
CreatedBy: createdBy,
|
CreatedBy: createdBy,
|
||||||
Source: "ONLINE",
|
Source: "ONLINE",
|
||||||
VisitDate: o.VisitDate,
|
|
||||||
BankCode: o.BankCode,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,10 +81,10 @@ func (o *OrderParam) ToOrderEntity(ctx mycontext.Context) entity.OrderSearch {
|
|||||||
|
|
||||||
type OrderItem struct {
|
type OrderItem struct {
|
||||||
ProductID int64 `json:"product_id" validate:"required"`
|
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))
|
orderItems := make([]entity.OrderItemRequest, len(o.OrderItems))
|
||||||
for i, item := range o.OrderItems {
|
for i, item := range o.OrderItems {
|
||||||
orderItems[i] = entity.OrderItemRequest{
|
orderItems[i] = entity.OrderItemRequest{
|
||||||
@ -95,12 +94,11 @@ func (o *Order) ToEntity(createdBy int64) *entity.OrderRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &entity.OrderRequest{
|
return &entity.OrderRequest{
|
||||||
PartnerID: o.PartnerID,
|
PartnerID: partnerID,
|
||||||
PaymentMethod: string(o.PaymentMethod),
|
PaymentMethod: o.PaymentMethod,
|
||||||
OrderItems: orderItems,
|
OrderItems: orderItems,
|
||||||
CreatedBy: createdBy,
|
CreatedBy: createdBy,
|
||||||
Source: "POS",
|
Source: "POS",
|
||||||
VisitDate: time.Now().Format("2006-01-02"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -37,7 +37,7 @@ type Product struct {
|
|||||||
IsWeekendTicket bool `json:"is_weekend_ticket"`
|
IsWeekendTicket bool `json:"is_weekend_ticket"`
|
||||||
IsSeasonTicket bool `json:"is_season_ticket"`
|
IsSeasonTicket bool `json:"is_season_ticket"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Description string `json:"description" validate:"required"`
|
Description string `json:"description"`
|
||||||
Stock int64 `json:"stock"`
|
Stock int64 `json:"stock"`
|
||||||
Image string `json:"image"`
|
Image string `json:"image"`
|
||||||
}
|
}
|
||||||
@ -50,7 +50,6 @@ func (e *Product) ToEntity() *entity.Product {
|
|||||||
Status: e.Status,
|
Status: e.Status,
|
||||||
Description: e.Description,
|
Description: e.Description,
|
||||||
PartnerID: e.PartnerID,
|
PartnerID: e.PartnerID,
|
||||||
SiteID: e.SiteID,
|
|
||||||
Image: e.Image,
|
Image: e.Image,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -67,7 +67,7 @@ type SearchSiteByIDResponse struct {
|
|||||||
|
|
||||||
type SearchProductSiteByIDResponse struct {
|
type SearchProductSiteByIDResponse struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
SiteID int64 `json:"site_id"`
|
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Price float64 `json:"price"`
|
Price float64 `json:"price"`
|
||||||
|
|||||||
@ -84,9 +84,6 @@ type OrderBranchRevenue struct {
|
|||||||
|
|
||||||
type CreateOrderResponse struct {
|
type CreateOrderResponse struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
SiteName string `json:"site_name"`
|
|
||||||
VisitDate string `json:"visit_date"`
|
|
||||||
RefID string `json:"ref_id"`
|
|
||||||
PartnerID int64 `json:"partner_id"`
|
PartnerID int64 `json:"partner_id"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Amount float64 `json:"amount"`
|
Amount float64 `json:"amount"`
|
||||||
@ -95,9 +92,6 @@ type CreateOrderResponse struct {
|
|||||||
PaymentType string `json:"payment_type"`
|
PaymentType string `json:"payment_type"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
OrderItems []CreateOrderItemResponse `json:"order_items"`
|
OrderItems []CreateOrderItemResponse `json:"order_items"`
|
||||||
Token string `json:"token"`
|
|
||||||
BankCode string `json:"bank_code"`
|
|
||||||
BankName string `json:"bank_name"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PrintDetailResponse struct {
|
type PrintDetailResponse struct {
|
||||||
@ -117,7 +111,6 @@ type PrintDetailResponse struct {
|
|||||||
|
|
||||||
type ExecuteOrderResponse struct {
|
type ExecuteOrderResponse struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
RefID string `json:"ref_id"`
|
|
||||||
PartnerID int64 `json:"partner_id"`
|
PartnerID int64 `json:"partner_id"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Amount float64 `json:"amount"`
|
Amount float64 `json:"amount"`
|
||||||
@ -134,7 +127,6 @@ type ExecuteOrderResponse struct {
|
|||||||
|
|
||||||
type ExecuteCheckinResponse struct {
|
type ExecuteCheckinResponse struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
RefID string `json:"ref_id"`
|
|
||||||
PartnerID int64 `json:"partner_id"`
|
PartnerID int64 `json:"partner_id"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Amount float64 `json:"amount"`
|
Amount float64 `json:"amount"`
|
||||||
@ -153,7 +145,7 @@ type CheckingInquiryResponse struct {
|
|||||||
type CreateOrderItemResponse struct {
|
type CreateOrderItemResponse struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
ItemID int64 `json:"item_id"`
|
ItemID int64 `json:"item_id"`
|
||||||
Quantity int64 `json:"quantity"`
|
Quantity int `json:"quantity"`
|
||||||
Price float64 `json:"price"`
|
Price float64 `json:"price"`
|
||||||
Name string `json:"name"`
|
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 {
|
type Product struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
PartnerID int64 `json:"partner_id"`
|
PartnerID int64 `json:"partner_id"`
|
||||||
SiteID int64 `json:"site_id"`
|
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Price float64 `json:"price"`
|
Price float64 `json:"price"`
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import (
|
|||||||
|
|
||||||
func AuthorizationMiddleware(cryp repository.Crypto) gin.HandlerFunc {
|
func AuthorizationMiddleware(cryp repository.Crypto) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
// Get the JWT token from the header
|
|
||||||
tokenString := c.GetHeader("Authorization")
|
tokenString := c.GetHeader("Authorization")
|
||||||
if tokenString == "" {
|
if tokenString == "" {
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"})
|
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
|
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 {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
return err
|
return err
|
||||||
@ -45,6 +69,7 @@ func (s ServiceImpl) sendEmail(ctx context.Context, tmpl *template.Template, par
|
|||||||
|
|
||||||
payload := brevo.SendSmtpEmail{
|
payload := brevo.SendSmtpEmail{
|
||||||
Sender: &brevo.SendSmtpEmailSender{
|
Sender: &brevo.SendSmtpEmailSender{
|
||||||
|
Name: "Enaklo",
|
||||||
Email: param.Sender,
|
Email: param.Sender,
|
||||||
},
|
},
|
||||||
To: []brevo.SendSmtpEmailTo{
|
To: []brevo.SendSmtpEmailTo{
|
||||||
|
|||||||
@ -149,6 +149,49 @@ func (c *CryptoImpl) GenerateJWTOrder(order *entity.Order) (string, error) {
|
|||||||
return token, nil
|
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) {
|
func (c *CryptoImpl) ValidateJWTOrder(tokenString string) (int64, int64, error) {
|
||||||
token, err := jwt.ParseWithClaims(tokenString, &entity.JWTOrderClaims{}, func(token *jwt.Token) (interface{}, error) {
|
token, err := jwt.ParseWithClaims(tokenString, &entity.JWTOrderClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
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 == "" {
|
if fileName == "" {
|
||||||
return ""
|
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)
|
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 {
|
if req.Limit > 0 {
|
||||||
query = query.Limit(req.Limit)
|
query = query.Limit(req.Limit)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,6 +51,11 @@ type RepoManagerImpl struct {
|
|||||||
Transaction TransactionRepository
|
Transaction TransactionRepository
|
||||||
PG PaymentGateway
|
PG PaymentGateway
|
||||||
LinkQu LinkQu
|
LinkQu LinkQu
|
||||||
|
|
||||||
|
OrderRepo OrderRepository
|
||||||
|
CustomerRepo CustomerRepo
|
||||||
|
ProductRepo ProductRepository
|
||||||
|
TransactionRepo TransactionRepo
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRepoManagerImpl(db *gorm.DB, cfg *config.Config) *RepoManagerImpl {
|
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),
|
Transaction: transactions.NewTransactionRepository(db),
|
||||||
PG: pg.NewPaymentGatewayRepo(&cfg.Midtrans, &cfg.LinkQu),
|
PG: pg.NewPaymentGatewayRepo(&cfg.Midtrans, &cfg.LinkQu),
|
||||||
LinkQu: linkqu.NewLinkQuService(&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)
|
GenerateJWT(user *entity.User) (string, error)
|
||||||
GenerateJWTReseetPassword(user *entity.User) (string, error)
|
GenerateJWTReseetPassword(user *entity.User) (string, error)
|
||||||
GenerateJWTOrder(order *entity.Order) (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)
|
ValidateJWTOrder(tokenString string) (int64, int64, error)
|
||||||
ValidateResetPassword(tokenString string) (int64, error)
|
ValidateResetPassword(tokenString string) (int64, error)
|
||||||
ParseAndValidateJWT(token string) (*entity.JWTAuthClaims, 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 {
|
if len(site.Products) > 0 {
|
||||||
for i := range site.Products {
|
for i := range site.Products {
|
||||||
site.Products[i].SiteID = site.ID
|
|
||||||
if site.Products[i].ID != 0 {
|
if site.Products[i].ID != 0 {
|
||||||
// Update existing product
|
|
||||||
if err := tx.Save(&site.Products[i]).Error; err != nil {
|
if err := tx.Save(&site.Products[i]).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Create new product
|
|
||||||
if err := tx.Create(&site.Products[i]).Error; err != nil {
|
if err := tx.Create(&site.Products[i]).Error; err != nil {
|
||||||
return err
|
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
|
package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
http2 "enaklo-pos-be/internal/handlers/http"
|
||||||
"enaklo-pos-be/internal/handlers/http/balance"
|
"enaklo-pos-be/internal/handlers/http/balance"
|
||||||
"enaklo-pos-be/internal/handlers/http/license"
|
"enaklo-pos-be/internal/handlers/http/license"
|
||||||
linkqu "enaklo-pos-be/internal/handlers/http/linqu"
|
linkqu "enaklo-pos-be/internal/handlers/http/linqu"
|
||||||
@ -68,3 +69,18 @@ func RegisterPrivateRoutes(app *app.Server, serviceManager *services.ServiceMana
|
|||||||
handler.Route(approute, authMiddleware)
|
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{
|
transaction := &entity.Transaction{
|
||||||
PartnerID: wallet.PartnerID,
|
PartnerID: wallet.PartnerID,
|
||||||
TransactionType: "WITHDRAW",
|
TransactionType: "WITHDRAW",
|
||||||
ReferenceID: "",
|
|
||||||
Status: "WAITING_APPROVAL",
|
Status: "WAITING_APPROVAL",
|
||||||
CreatedBy: ctx.RequestedBy(),
|
CreatedBy: ctx.RequestedBy(),
|
||||||
Amount: totalAmount,
|
Amount: totalAmount,
|
||||||
|
|||||||
@ -76,11 +76,9 @@ func (s *OrderService) CreateOrder(ctx mycontext.Context, req *entity.OrderReque
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var siteID int64
|
|
||||||
productMap := make(map[int64]*entity.ProductDB)
|
productMap := make(map[int64]*entity.ProductDB)
|
||||||
for _, product := range products {
|
for _, product := range products {
|
||||||
productMap[product.ID] = product
|
productMap[product.ID] = product
|
||||||
siteID = product.SiteID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
totalAmount := 0.0
|
totalAmount := 0.0
|
||||||
@ -93,32 +91,16 @@ func (s *OrderService) CreateOrder(ctx mycontext.Context, req *entity.OrderReque
|
|||||||
totalAmount += product.Price * float64(item.Quantity)
|
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{
|
order := &entity.Order{
|
||||||
PartnerID: req.PartnerID,
|
PartnerID: req.PartnerID,
|
||||||
RefID: generator.GenerateUUID(),
|
|
||||||
Status: order2.New.String(),
|
Status: order2.New.String(),
|
||||||
Amount: totalAmount,
|
Amount: totalAmount,
|
||||||
Total: totalAmount + s.cfg.GetOrderFee(req.Source),
|
Total: totalAmount + s.cfg.GetOrderFee(req.Source),
|
||||||
Fee: s.cfg.GetOrderFee(req.Source),
|
Fee: s.cfg.GetOrderFee(req.Source),
|
||||||
PaymentType: req.PaymentMethod,
|
PaymentType: req.PaymentMethod,
|
||||||
SiteID: &siteID,
|
|
||||||
CreatedBy: req.CreatedBy,
|
CreatedBy: req.CreatedBy,
|
||||||
OrderItems: []entity.OrderItem{},
|
OrderItems: []entity.OrderItem{},
|
||||||
Source: req.Source,
|
Source: req.Source,
|
||||||
VisitDate: parsedTime,
|
|
||||||
TicketStatus: "UNUSED",
|
|
||||||
Metadata: metadata,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, item := range req.OrderItems {
|
for _, item := range req.OrderItems {
|
||||||
@ -126,7 +108,7 @@ func (s *OrderService) CreateOrder(ctx mycontext.Context, req *entity.OrderReque
|
|||||||
ItemID: item.ProductID,
|
ItemID: item.ProductID,
|
||||||
ItemType: productMap[item.ProductID].Type,
|
ItemType: productMap[item.ProductID].Type,
|
||||||
Price: productMap[item.ProductID].Price,
|
Price: productMap[item.ProductID].Price,
|
||||||
Quantity: item.Quantity,
|
Quantity: int(item.Quantity),
|
||||||
CreatedBy: req.CreatedBy,
|
CreatedBy: req.CreatedBy,
|
||||||
Product: productMap[item.ProductID].ToProduct(),
|
Product: productMap[item.ProductID].ToProduct(),
|
||||||
})
|
})
|
||||||
@ -138,12 +120,6 @@ func (s *OrderService) CreateOrder(ctx mycontext.Context, req *entity.OrderReque
|
|||||||
return nil, err
|
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)
|
order, err = s.repo.FindByID(ctx, order.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ContextLogger(ctx).Error("error when creating order", zap.Error(err))
|
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{
|
return &entity.OrderResponse{
|
||||||
Order: order,
|
Order: order,
|
||||||
Token: token,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,26 +162,6 @@ func (s *OrderService) CheckInInquiry(ctx mycontext.Context, qrCode string, part
|
|||||||
return nil, errors2.ErrorInvalidRequest
|
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)
|
token, err := s.crypt.GenerateJWTOrder(order)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ContextLogger(ctx).Error("error when generate checkin token", zap.Error(err))
|
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,
|
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
|
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) {
|
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{
|
paymentRequest := entity.PaymentRequest{
|
||||||
PaymentReferenceID: generator.GenerateUUIDV4(),
|
PaymentReferenceID: generator.GenerateUUIDV4(),
|
||||||
TotalAmount: int64(order.Total),
|
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),
|
CustomerID: strconv.FormatInt(order.User.ID, 10),
|
||||||
CustomerName: order.User.Name,
|
CustomerName: order.User.Name,
|
||||||
CustomerEmail: order.User.Email,
|
CustomerEmail: order.User.Email,
|
||||||
BankCode: metadata["bank_code"],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
paymentResponse, err := s.pg.CreatePaymentVA(paymentRequest)
|
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{
|
transaction := &entity.Transaction{
|
||||||
PartnerID: existingPayment.PartnerID,
|
PartnerID: existingPayment.PartnerID,
|
||||||
TransactionType: "PAYMENT_RECEIVED",
|
TransactionType: "PAYMENT_RECEIVED",
|
||||||
ReferenceID: existingPayment.ReferenceID,
|
|
||||||
Status: "SUCCESS",
|
Status: "SUCCESS",
|
||||||
CreatedBy: 0,
|
CreatedBy: 0,
|
||||||
Amount: existingPayment.Amount,
|
Amount: existingPayment.Amount,
|
||||||
Fee: order.Fee,
|
Fee: order.Fee,
|
||||||
Total: order.Total,
|
Total: order.Total,
|
||||||
SiteID: order.SiteID,
|
|
||||||
}
|
}
|
||||||
if _, err = s.transaction.Create(ctx, tx, transaction); err != nil {
|
if _, err = s.transaction.Create(ctx, tx, transaction); err != nil {
|
||||||
return fmt.Errorf("failed to update transaction: %w", err)
|
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/studio"
|
||||||
"enaklo-pos-be/internal/services/transaction"
|
"enaklo-pos-be/internal/services/transaction"
|
||||||
"enaklo-pos-be/internal/services/users"
|
"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"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
@ -38,9 +41,17 @@ type ServiceManagerImpl struct {
|
|||||||
Transaction Transaction
|
Transaction Transaction
|
||||||
Balance Balance
|
Balance Balance
|
||||||
DiscoverService DiscoverService
|
DiscoverService DiscoverService
|
||||||
|
|
||||||
|
OrderV2Svc orderSvc.Service
|
||||||
|
CustomerV2Svc customerSvc.Service
|
||||||
|
ProductV2Svc productSvc.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl) *ServiceManagerImpl {
|
func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl) *ServiceManagerImpl {
|
||||||
|
|
||||||
|
custSvcV2 := customerSvc.New(repo.CustomerRepo)
|
||||||
|
productSvcV2 := productSvc.New(repo.ProductRepo)
|
||||||
|
|
||||||
return &ServiceManagerImpl{
|
return &ServiceManagerImpl{
|
||||||
AuthSvc: auth.New(repo.Auth, repo.Crypto, repo.User, repo.EmailService, cfg.Email, repo.Trx, repo.License),
|
AuthSvc: auth.New(repo.Auth, repo.Crypto, repo.User, repo.EmailService, cfg.Email, repo.Trx, repo.License),
|
||||||
EventSvc: event.NewEventService(repo.Event),
|
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),
|
Transaction: transaction.New(repo.Transaction, repo.Wallet, repo.Trx),
|
||||||
Balance: balance.NewBalanceService(repo.Wallet, repo.Trx, repo.Crypto, &cfg.Withdraw, repo.Transaction),
|
Balance: balance.NewBalanceService(repo.Wallet, repo.Trx, repo.Crypto, &cfg.Withdraw, repo.Transaction),
|
||||||
DiscoverService: discovery.NewDiscoveryService(repo.Site, cfg.Discovery, repo.Product),
|
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:
|
tls:
|
||||||
- hosts:
|
- hosts:
|
||||||
- "api-dev.enaklo.co.id"
|
- "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.RegisterPublicRoutes(server, service, repo)
|
||||||
routes.RegisterPrivateRoutes(server, service, repo)
|
routes.RegisterPrivateRoutes(server, service, repo)
|
||||||
|
routes.RegisterPrivateRoutesV2(server, service, repo)
|
||||||
routes.RegisterCustomerRoutes(server, service, repo)
|
routes.RegisterCustomerRoutes(server, service, repo)
|
||||||
|
|
||||||
server.StartScheduler()
|
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