Update template email

This commit is contained in:
aditya.siregar 2025-03-08 00:35:23 +07:00
parent 3c80b710af
commit 18003313dd
54 changed files with 2309 additions and 199 deletions

1
go.mod
View File

@ -80,6 +80,7 @@ require (
require (
github.com/aws/aws-sdk-go v1.50.0
github.com/getbrevo/brevo-go v1.0.0
github.com/pkg/errors v0.9.1
github.com/veritrans/go-midtrans v0.0.0-20210616100512-16326c5eeb00
github.com/xuri/excelize/v2 v2.9.0
go.uber.org/zap v1.21.0

View File

@ -28,12 +28,12 @@ postgresql:
debug: false
oss:
access_key_id: e50b31e5eddf63c0ZKB2
access_key_secret: GAyX9jiCWyTwgJMuqzun2x0zHS3kjQt26kyzY21S
endpoint: obs.eranyacloud.com
bucket_name: enaklo-pos
log_level: Error # type: LogOff, Debug, Error, Warn, Info
host_url: https://obs.eranyacloud.com
access_key_id: cf9a475e18bc7626cbdbf09709d82a64
access_key_secret: 91f3321294d3e23035427a0ecb893ada
endpoint: sin1.contabostorage.com
bucket_name: enaklo
log_level: Error
host_url: 'https://sin1.contabostorage.com/fda98c2228f246f29a7e466b86b3b9e7:'
midtrans:
server_key: "SB-Mid-server-YOIvuaIlRw3In9SymCuFz-hB"
@ -50,10 +50,10 @@ linkqu:
callback_url: "https://enaklo-pos-be.app-dev.altru.id/api/v1/linkqu/callback"
brevo:
api_key: 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
api_key: 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
email:
sender: "enaklo-pos.official@gmail.com"
sender: "noreply@enaklo.co.id"
sender_customer: "enaklo-pos.official@gmail.com"
reset_password:
template_name: "reset_password"

View File

@ -21,6 +21,7 @@ const (
errInsufficientBalance ErrType = "Insufficient Balance"
errInactivePartner ErrType = "Partner's license is invalid or has expired. Please contact Admin Support."
errTicketAlreadyUsed ErrType = "Ticket Already Used."
errProductIsRequired ErrType = "Product"
)
var (

View File

@ -1,5 +1,10 @@
package constants
import (
"github.com/google/uuid"
"time"
)
const (
ContextRequestID string = "requestId"
)
@ -9,3 +14,41 @@ type UserType string
func (u UserType) toString() string {
return string(u)
}
const (
StatusPending = "PENDING"
StatusPaid = "PAID"
StatusCanceled = "CANCELED"
StatusExpired = "EXPIRED"
StatusExecuted = "EXECUTED"
)
const (
PaymentCash = "CASH"
PaymentCreditCard = "CREDIT_CARD"
PaymentDebitCard = "DEBIT_CARD"
PaymentEWallet = "E_WALLET"
)
const (
SourcePOS = "POS"
SourceMobile = "MOBILE"
SourceWeb = "WEB"
)
const (
DefaultInquiryExpiryDuration = 30 * time.Minute
)
func GenerateUUID() string {
return uuid.New().String()
}
func GenerateRefID() string {
now := time.Now()
return now.Format("20060102") + "-" + uuid.New().String()[:8]
}
var TimeNow = func() time.Time {
return time.Now()
}

8
internal/entity/cust.go Normal file
View File

@ -0,0 +1,8 @@
package entity
type CustomerResolutionRequest struct {
ID *int64
Name string
Email string
PhoneNumber string
}

View File

@ -16,6 +16,7 @@ type JWTAuthClaims struct {
type JWTOrderClaims struct {
PartnerID int64 `json:"id"`
OrderID int64 `json:"order_id"`
InquiryID string `json:"inquiry_id"`
jwt.StandardClaims
}

View File

@ -1,19 +1,18 @@
package entity
import (
"gorm.io/datatypes"
"time"
)
type Order struct {
ID int64 `gorm:"primaryKey;autoIncrement;column:id"`
RefID string `gorm:"type:varchar;column:ref_id"`
PartnerID int64 `gorm:"type:int;column:partner_id"`
Status string `gorm:"type:varchar;column:status"`
Amount float64 `gorm:"type:numeric;not null;column:amount"`
Total float64 `gorm:"type:numeric;not null;column:total"`
Fee float64 `gorm:"type:numeric;not null;column:fee"`
SiteID *int64 `gorm:"type:numeric;not null;column:site_id"`
CustomerID *int64
InquiryID *string
Site *Site `gorm:"foreignKey:SiteID;constraint:OnDelete:CASCADE;"`
CreatedAt time.Time `gorm:"autoCreateTime;column:created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at"`
@ -24,9 +23,6 @@ type Order struct {
Payment Payment `gorm:"foreignKey:OrderID;constraint:OnDelete:CASCADE;"`
User User `gorm:"foreignKey:CreatedBy;constraint:OnDelete:CASCADE;"`
Source string `gorm:"type:varchar;column:source"`
TicketStatus string `gorm:"type:varchar;column:ticket_status"`
VisitDate time.Time `gorm:"type:date;column:visit_date"`
Metadata datatypes.JSON `gorm:"type:json;not null;column:metadata"`
}
type OrderDB struct {
@ -47,7 +43,6 @@ func (e *OrderDB) ToSumAmount() *Order {
type OrderResponse struct {
Order *Order
Token string
}
type CheckinResponse struct {
@ -80,7 +75,7 @@ type OrderItem struct {
ItemID int64 `gorm:"type:int;column:item_id"`
ItemType string `gorm:"type:varchar;column:item_type"`
Price float64 `gorm:"type:numeric;not null;column:price"`
Quantity int64 `gorm:"type:int;column:qty"`
Quantity int `gorm:"type:int;column:qty"`
CreatedAt time.Time `gorm:"autoCreateTime;column:created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at"`
CreatedBy int64 `gorm:"type:int;column:created_by"`
@ -95,17 +90,18 @@ func (OrderItem) TableName() string {
type OrderRequest struct {
Source string
CreatedBy int64
PartnerID int64 `json:"partner_id" validate:"required"`
PaymentMethod string `json:"payment_method" validate:"required"`
OrderItems []OrderItemRequest `json:"order_items" validate:"required,dive"`
VisitDate string `json:"visit_date"`
BankCode string `json:"bank_code"`
BankName string `json:"bank_name"`
PartnerID int64
PaymentMethod string
OrderItems []OrderItemRequest
CustomerID *int64
CustomerName string
CustomerEmail string
CustomerPhoneNumber string
}
type OrderItemRequest struct {
ProductID int64 `json:"product_id" validate:"required"`
Quantity int64 `json:"quantity" validate:"required"`
Quantity int `json:"quantity" validate:"required"`
}
type OrderExecuteRequest struct {
@ -117,7 +113,6 @@ type OrderExecuteRequest struct {
func (o *Order) SetExecutePaymentStatus() {
if o.PaymentType == "CASH" {
o.Status = "PAID"
o.TicketStatus = "USED"
return
}
o.Status = "PENDING"

View 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
}

View File

@ -8,7 +8,6 @@ import (
type Product struct {
ID int64 `gorm:"primaryKey;autoIncrement;column:id"`
PartnerID int64 `gorm:"type:int;column:partner_id"`
SiteID int64 `gorm:"type:int;column:site_id"`
Name string `gorm:"type:varchar(255);not null;column:name"`
Type string `gorm:"type:varchar;column:type"`
Price float64 `gorm:"type:decimal;column:price"`
@ -19,7 +18,7 @@ type Product struct {
DeletedAt *time.Time `gorm:"column:deleted_at"`
CreatedBy int64 `gorm:"type:int;column:created_by"`
UpdatedBy int64 `gorm:"type:int;column:updated_by"`
Image string `gorm:"type:varchar;column:type"`
Image string `gorm:"type:varchar;column:image"`
}
func (Product) TableName() string {
@ -71,7 +70,7 @@ func (e *ProductDB) ToProduct() *Product {
DeletedAt: e.DeletedAt,
CreatedBy: e.CreatedBy,
UpdatedBy: e.UpdatedBy,
SiteID: e.SiteID,
Image: e.Image,
}
}
@ -79,10 +78,8 @@ func (b *ProductList) ToProductList() []*Product {
var Products []*Product
for _, p := range *b {
if p.Status == "Available" {
Products = append(Products, p.ToProduct())
}
}
return Products
}
@ -126,3 +123,8 @@ func (o *ProductDB) SetDeleted(updatedby int64) {
o.DeletedAt = &currentTime
o.UpdatedBy = updatedby
}
type ProductDetails struct {
Products map[int64]*Product // Map for quick lookups by ID
PartnerID int64 // Common site ID for all products
}

View File

@ -6,16 +6,16 @@ import (
type Transaction struct {
ID string `gorm:"type:uuid;primaryKey;default:uuid_generate_v4()"`
OrderID int64
PartnerID int64 `gorm:"not null"`
TransactionType string `gorm:"not null"`
ReferenceID string `gorm:"size:255"`
Status string `gorm:"size:255"`
CreatedBy int64 `gorm:"not null"`
UpdatedBy int64 `gorm:"not null"`
Amount float64 `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
SiteID *int64
PaymentMethod string `json:"payment_method"`
Fee float64
Total float64
}

View File

@ -34,6 +34,8 @@ type Customer struct {
Name string
Email string
Password string
Phone string
Points int
Status userstatus.UserStatus
NIK string
UserType string

View File

@ -6,7 +6,6 @@ import (
"enaklo-pos-be/internal/handlers/request"
"enaklo-pos-be/internal/handlers/response"
"enaklo-pos-be/internal/services"
"enaklo-pos-be/internal/utils"
"encoding/json"
"net/http"
"time"
@ -22,7 +21,7 @@ type Handler struct {
func (h *Handler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) {
route := group.Group("/order")
route.POST("/inquiry", jwt, h.Inquiry)
route.POST("/inquiry", h.Inquiry)
route.POST("/execute", jwt, h.Execute)
route.GET("/history", jwt, h.History)
route.GET("/detail", jwt, h.Detail)
@ -104,20 +103,14 @@ func MapOrderToCreateOrderResponse(orderResponse *entity.OrderResponse, req requ
return response.CreateOrderResponse{
ID: order.ID,
RefID: order.RefID,
PartnerID: order.PartnerID,
Status: order.Status,
Amount: order.Amount,
PaymentType: order.PaymentType,
CreatedAt: order.CreatedAt,
OrderItems: orderItems,
Token: orderResponse.Token,
Fee: order.Fee,
Total: order.Total,
VisitDate: order.VisitDate.Format("2006-01-02"),
SiteName: order.Site.Name,
BankCode: req.BankCode,
BankName: utils.BankName(req.BankCode),
}
}
@ -136,7 +129,6 @@ func MapOrderToExecuteOrderResponse(orderResponse *entity.ExecuteOrderResponse)
return response.ExecuteOrderResponse{
ID: order.ID,
RefID: order.RefID,
PartnerID: order.PartnerID,
Status: order.Status,
Amount: order.Amount,
@ -239,10 +231,6 @@ func (h *Handler) toOrderDetail(order *entity.Order) *response.OrderDetail {
qrCode := ""
if order.Status == "PAID" {
qrCode = order.RefID
}
var siteName string
if order.Site != nil {

View File

@ -214,7 +214,6 @@ func ConvertToProductResp(resp []*entity.Product) *response.SearchProductSiteRes
productResp = append(productResp, response.SearchProductSiteByIDResponse{
ID: res.ID,
Name: res.Name,
SiteID: res.SiteID,
Price: res.Price,
Description: res.Description,
Type: res.Type,

View 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),
})
}

View File

@ -47,21 +47,15 @@ func (h *Handler) Inquiry(c *gin.Context) {
return
}
if !ctx.IsCasheer() {
response.ErrorWrapper(c, errors.ErrorBadRequest)
return
}
// override the partner_id
req.PartnerID = *ctx.GetPartnerID()
validate := validator.New()
if err := validate.Struct(req); err != nil {
response.ErrorWrapper(c, err)
return
}
order, err := h.service.CreateOrder(ctx, req.ToEntity(ctx.RequestedBy()))
orderRequest := req.ToEntity(*ctx.GetPartnerID(), ctx.RequestedBy())
order, err := h.service.CreateOrder(ctx, orderRequest)
if err != nil {
response.ErrorWrapper(c, err)
return
@ -206,7 +200,6 @@ func MapOrderToCreateOrderResponse(orderResponse *entity.OrderResponse) response
return response.CreateOrderResponse{
ID: order.ID,
RefID: order.RefID,
PartnerID: order.PartnerID,
Status: order.Status,
Amount: order.Amount,
@ -215,7 +208,6 @@ func MapOrderToCreateOrderResponse(orderResponse *entity.OrderResponse) response
PaymentType: order.PaymentType,
CreatedAt: order.CreatedAt,
OrderItems: orderItems,
Token: orderResponse.Token,
}
}
@ -234,7 +226,6 @@ func MapOrderToExecuteOrderResponse(orderResponse *entity.ExecuteOrderResponse)
return response.ExecuteOrderResponse{
ID: order.ID,
RefID: order.RefID,
PartnerID: order.PartnerID,
Status: order.Status,
Amount: order.Amount,
@ -261,7 +252,6 @@ func MapOrderToExecuteCheckinResponse(order *entity.Order) response.ExecuteCheck
return response.ExecuteCheckinResponse{
ID: order.ID,
RefID: order.RefID,
PartnerID: order.PartnerID,
Status: order.Status,
Amount: order.Amount,

View File

@ -13,7 +13,7 @@ import (
const _oneMB = 1 << 20 // 1MB
const _maxUploadSizeMB = 2 * _oneMB
const _folderName = "/file"
const _folderName = "/public"
type OssHandler struct {
ossService services.OSSService
@ -22,7 +22,7 @@ type OssHandler struct {
func (h *OssHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) {
route := group.Group("/file")
route.POST("/upload", h.UploadFile, jwt)
route.POST("/upload", h.UploadFile)
}
func NewOssHandler(ossService services.OSSService) *OssHandler {

View File

@ -278,7 +278,7 @@ func (h *Handler) toProductResponse(resp *entity.Product) response.Product {
CreatedAt: resp.CreatedAt.Format(time.RFC3339),
UpdatedAt: resp.CreatedAt.Format(time.RFC3339),
PartnerID: resp.PartnerID,
SiteID: resp.SiteID,
Image: resp.Image,
}
}

View File

@ -314,7 +314,6 @@ func (h *Handler) toProductResponseList(products []entity.Product) []response.Pr
res = append(res, response.Product{
ID: product.ID,
PartnerID: product.PartnerID,
SiteID: product.SiteID,
Name: product.Name,
Type: product.Type,
Price: product.Price,

View File

@ -4,13 +4,14 @@ import (
"enaklo-pos-be/internal/common/mycontext"
"enaklo-pos-be/internal/constants/transaction"
"enaklo-pos-be/internal/entity"
"time"
)
type Order struct {
PartnerID int64 `json:"partner_id" validate:"required"`
PaymentMethod transaction.PaymentMethod `json:"payment_method" validate:"required"`
OrderItems []OrderItem `json:"order_items" validate:"required"`
CustomerName string `json:"customer_name"`
CustomerPhone string `json:"customer_phone"`
CustomerEmail string `json:"customer_email"`
PaymentMethod string `json:"payment_method"`
OrderItems []OrderItem `json:"order_items"`
}
type CustomerOrder struct {
@ -36,8 +37,6 @@ func (o *CustomerOrder) ToEntity(createdBy int64) *entity.OrderRequest {
OrderItems: orderItems,
CreatedBy: createdBy,
Source: "ONLINE",
VisitDate: o.VisitDate,
BankCode: o.BankCode,
}
}
@ -82,10 +81,10 @@ func (o *OrderParam) ToOrderEntity(ctx mycontext.Context) entity.OrderSearch {
type OrderItem struct {
ProductID int64 `json:"product_id" validate:"required"`
Quantity int64 `json:"quantity" validate:"required"`
Quantity int `json:"quantity" validate:"required"`
}
func (o *Order) ToEntity(createdBy int64) *entity.OrderRequest {
func (o *Order) ToEntity(partnerID, createdBy int64) *entity.OrderRequest {
orderItems := make([]entity.OrderItemRequest, len(o.OrderItems))
for i, item := range o.OrderItems {
orderItems[i] = entity.OrderItemRequest{
@ -95,12 +94,11 @@ func (o *Order) ToEntity(createdBy int64) *entity.OrderRequest {
}
return &entity.OrderRequest{
PartnerID: o.PartnerID,
PaymentMethod: string(o.PaymentMethod),
PartnerID: partnerID,
PaymentMethod: o.PaymentMethod,
OrderItems: orderItems,
CreatedBy: createdBy,
Source: "POS",
VisitDate: time.Now().Format("2006-01-02"),
}
}

View File

@ -37,7 +37,7 @@ type Product struct {
IsWeekendTicket bool `json:"is_weekend_ticket"`
IsSeasonTicket bool `json:"is_season_ticket"`
Status string `json:"status"`
Description string `json:"description" validate:"required"`
Description string `json:"description"`
Stock int64 `json:"stock"`
Image string `json:"image"`
}
@ -50,7 +50,6 @@ func (e *Product) ToEntity() *entity.Product {
Status: e.Status,
Description: e.Description,
PartnerID: e.PartnerID,
SiteID: e.SiteID,
Image: e.Image,
}
}

View File

@ -67,7 +67,7 @@ type SearchSiteByIDResponse struct {
type SearchProductSiteByIDResponse struct {
ID int64 `json:"id"`
SiteID int64 `json:"site_id"`
Name string `json:"name"`
Type string `json:"type"`
Price float64 `json:"price"`

View File

@ -84,9 +84,6 @@ type OrderBranchRevenue struct {
type CreateOrderResponse struct {
ID int64 `json:"id"`
SiteName string `json:"site_name"`
VisitDate string `json:"visit_date"`
RefID string `json:"ref_id"`
PartnerID int64 `json:"partner_id"`
Status string `json:"status"`
Amount float64 `json:"amount"`
@ -95,9 +92,6 @@ type CreateOrderResponse struct {
PaymentType string `json:"payment_type"`
CreatedAt time.Time `json:"created_at"`
OrderItems []CreateOrderItemResponse `json:"order_items"`
Token string `json:"token"`
BankCode string `json:"bank_code"`
BankName string `json:"bank_name"`
}
type PrintDetailResponse struct {
@ -117,7 +111,6 @@ type PrintDetailResponse struct {
type ExecuteOrderResponse struct {
ID int64 `json:"id"`
RefID string `json:"ref_id"`
PartnerID int64 `json:"partner_id"`
Status string `json:"status"`
Amount float64 `json:"amount"`
@ -134,7 +127,6 @@ type ExecuteOrderResponse struct {
type ExecuteCheckinResponse struct {
ID int64 `json:"id"`
RefID string `json:"ref_id"`
PartnerID int64 `json:"partner_id"`
Status string `json:"status"`
Amount float64 `json:"amount"`
@ -153,7 +145,7 @@ type CheckingInquiryResponse struct {
type CreateOrderItemResponse struct {
ID int64 `json:"id"`
ItemID int64 `json:"item_id"`
Quantity int64 `json:"quantity"`
Quantity int `json:"quantity"`
Price float64 `json:"price"`
Name string `json:"name"`
}

View 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
}

View File

@ -3,7 +3,6 @@ package response
type Product struct {
ID int64 `json:"id"`
PartnerID int64 `json:"partner_id"`
SiteID int64 `json:"site_id"`
Name string `json:"name"`
Type string `json:"type"`
Price float64 `json:"price"`

View File

@ -12,7 +12,6 @@ import (
func AuthorizationMiddleware(cryp repository.Crypto) gin.HandlerFunc {
return func(c *gin.Context) {
// Get the JWT token from the header
tokenString := c.GetHeader("Authorization")
if tokenString == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"})

View File

@ -26,7 +26,31 @@ func (s ServiceImpl) SendEmailTransactional(ctx context.Context, param entity.Se
return err
}
renderedTemplate, err := template.New(param.TemplateName).Parse(string(templateFile))
tmpl := template.New(param.TemplateName).Funcs(template.FuncMap{
"range": func(args ...interface{}) []interface{} {
if len(args) == 0 {
return nil
}
switch items := args[0].(type) {
case []map[string]string:
result := make([]interface{}, len(items))
for i, item := range items {
result[i] = item
}
return result
case []interface{}:
return items
default:
if slice, ok := args[0].([]interface{}); ok {
return slice
}
return nil
}
},
})
renderedTemplate, err := tmpl.Parse(string(templateFile))
if err != nil {
log.Println(err)
return err
@ -45,6 +69,7 @@ func (s ServiceImpl) sendEmail(ctx context.Context, tmpl *template.Template, par
payload := brevo.SendSmtpEmail{
Sender: &brevo.SendSmtpEmailSender{
Name: "Enaklo",
Email: param.Sender,
},
To: []brevo.SendSmtpEmailTo{

View File

@ -149,6 +149,49 @@ func (c *CryptoImpl) GenerateJWTOrder(order *entity.Order) (string, error) {
return token, nil
}
func (c *CryptoImpl) GenerateJWTOrderInquiry(inquiry *entity.OrderInquiry) (string, error) {
claims := &entity.JWTOrderClaims{
StandardClaims: jwt.StandardClaims{
Subject: inquiry.ID,
ExpiresAt: c.Config.AccessTokenOrderExpiresDate().Unix(),
IssuedAt: time.Now().Unix(),
NotBefore: time.Now().Unix(),
},
PartnerID: inquiry.PartnerID,
InquiryID: inquiry.ID,
}
token, err := jwt.
NewWithClaims(jwt.SigningMethodHS256, claims).
SignedString([]byte(c.Config.AccessTokenOrderSecret()))
if err != nil {
return "", err
}
return token, nil
}
func (c *CryptoImpl) ValidateJWTOrderInquiry(tokenString string) (int64, string, error) {
token, err := jwt.ParseWithClaims(tokenString, &entity.JWTOrderClaims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(c.Config.AccessTokenOrderSecret()), nil
})
if err != nil {
return 0, "", err
}
claims, ok := token.Claims.(*entity.JWTOrderClaims)
if !ok || !token.Valid {
return 0, "", fmt.Errorf("invalid token %v", token.Header["alg"])
}
return claims.PartnerID, claims.InquiryID, nil
}
func (c *CryptoImpl) ValidateJWTOrder(tokenString string) (int64, int64, error) {
token, err := jwt.ParseWithClaims(tokenString, &entity.JWTOrderClaims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {

View 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,
}
}

View 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"
}

View 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"
}

View 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"
}

View 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"
}

View 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
}

View File

@ -63,5 +63,5 @@ func (r *OssRepositoryImpl) GetPublicURL(fileName string) string {
if fileName == "" {
return ""
}
return fmt.Sprintf("%s/%s%s", r.cfg.GetHostURL(), r.cfg.GetBucketName(), fileName)
return fmt.Sprintf("%s%s%s", r.cfg.GetHostURL(), r.cfg.GetBucketName(), fileName)
}

View 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,
}
}

View File

@ -88,14 +88,6 @@ func (b *ProductRepository) GetAllProducts(ctx context.Context, req entity.Produ
query = query.Where("branch_id = ? ", req.BranchID)
}
if req.Available != "" {
if req.Available.IsAvailable() {
query = query.Where("stock_qty > 0 ")
} else if req.Available.IsUnavailable() {
query = query.Where("stock_qty < 1 ")
}
}
if req.Limit > 0 {
query = query.Limit(req.Limit)
}

View File

@ -51,6 +51,11 @@ type RepoManagerImpl struct {
Transaction TransactionRepository
PG PaymentGateway
LinkQu LinkQu
OrderRepo OrderRepository
CustomerRepo CustomerRepo
ProductRepo ProductRepository
TransactionRepo TransactionRepo
}
func NewRepoManagerImpl(db *gorm.DB, cfg *config.Config) *RepoManagerImpl {
@ -74,6 +79,11 @@ func NewRepoManagerImpl(db *gorm.DB, cfg *config.Config) *RepoManagerImpl {
Transaction: transactions.NewTransactionRepository(db),
PG: pg.NewPaymentGatewayRepo(&cfg.Midtrans, &cfg.LinkQu),
LinkQu: linkqu.NewLinkQuService(&cfg.LinkQu),
OrderRepo: NeworderRepository(db),
CustomerRepo: NewCustomerRepository(db),
ProductRepo: NewproductRepository(db),
TransactionRepo: NewTransactionRepository(db),
}
}
@ -97,6 +107,8 @@ type Crypto interface {
GenerateJWT(user *entity.User) (string, error)
GenerateJWTReseetPassword(user *entity.User) (string, error)
GenerateJWTOrder(order *entity.Order) (string, error)
GenerateJWTOrderInquiry(inquiry *entity.OrderInquiry) (string, error)
ValidateJWTOrderInquiry(tokenString string) (int64, string, error)
ValidateJWTOrder(tokenString string) (int64, int64, error)
ValidateResetPassword(tokenString string) (int64, error)
ParseAndValidateJWT(token string) (*entity.JWTAuthClaims, error)

View File

@ -36,14 +36,11 @@ func (r *SiteRepository) Upsert(ctx context.Context, site *entity.Site) (*entity
if len(site.Products) > 0 {
for i := range site.Products {
site.Products[i].SiteID = site.ID
if site.Products[i].ID != 0 {
// Update existing product
if err := tx.Save(&site.Products[i]).Error; err != nil {
return err
}
} else {
// Create new product
if err := tx.Create(&site.Products[i]).Error; err != nil {
return err
}

View 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,
}
}

View File

@ -1,6 +1,7 @@
package routes
import (
http2 "enaklo-pos-be/internal/handlers/http"
"enaklo-pos-be/internal/handlers/http/balance"
"enaklo-pos-be/internal/handlers/http/license"
linkqu "enaklo-pos-be/internal/handlers/http/linqu"
@ -68,3 +69,18 @@ func RegisterPrivateRoutes(app *app.Server, serviceManager *services.ServiceMana
handler.Route(approute, authMiddleware)
}
}
func RegisterPrivateRoutesV2(app *app.Server, serviceManager *services.ServiceManagerImpl,
repoManager *repository.RepoManagerImpl) {
approute := app.Group("/api/v2")
authMiddleware := middlewares.AuthorizationMiddleware(repoManager.Crypto)
serverRoutes := []HTTPHandlerRoutes{
http2.NewOrderHandler(serviceManager.OrderV2Svc),
}
for _, handler := range serverRoutes {
handler.Route(approute, authMiddleware)
}
}

View File

@ -112,7 +112,6 @@ func (s *BalanceService) WithdrawExecute(ctx mycontext.Context, req *entity.Wall
transaction := &entity.Transaction{
PartnerID: wallet.PartnerID,
TransactionType: "WITHDRAW",
ReferenceID: "",
Status: "WAITING_APPROVAL",
CreatedBy: ctx.RequestedBy(),
Amount: totalAmount,

View File

@ -76,11 +76,9 @@ func (s *OrderService) CreateOrder(ctx mycontext.Context, req *entity.OrderReque
return nil, err
}
var siteID int64
productMap := make(map[int64]*entity.ProductDB)
for _, product := range products {
productMap[product.ID] = product
siteID = product.SiteID
}
totalAmount := 0.0
@ -93,32 +91,16 @@ func (s *OrderService) CreateOrder(ctx mycontext.Context, req *entity.OrderReque
totalAmount += product.Price * float64(item.Quantity)
}
parsedTime, err := time.Parse("2006-01-02", req.VisitDate)
if err != nil {
fmt.Println("Error parsing date:", err)
return nil, errors.New("visit date not defined")
}
metadata, err := json.Marshal(map[string]string{
"bank_code": req.BankCode,
"bank_name": req.BankName,
})
order := &entity.Order{
PartnerID: req.PartnerID,
RefID: generator.GenerateUUID(),
Status: order2.New.String(),
Amount: totalAmount,
Total: totalAmount + s.cfg.GetOrderFee(req.Source),
Fee: s.cfg.GetOrderFee(req.Source),
PaymentType: req.PaymentMethod,
SiteID: &siteID,
CreatedBy: req.CreatedBy,
OrderItems: []entity.OrderItem{},
Source: req.Source,
VisitDate: parsedTime,
TicketStatus: "UNUSED",
Metadata: metadata,
}
for _, item := range req.OrderItems {
@ -126,7 +108,7 @@ func (s *OrderService) CreateOrder(ctx mycontext.Context, req *entity.OrderReque
ItemID: item.ProductID,
ItemType: productMap[item.ProductID].Type,
Price: productMap[item.ProductID].Price,
Quantity: item.Quantity,
Quantity: int(item.Quantity),
CreatedBy: req.CreatedBy,
Product: productMap[item.ProductID].ToProduct(),
})
@ -138,12 +120,6 @@ func (s *OrderService) CreateOrder(ctx mycontext.Context, req *entity.OrderReque
return nil, err
}
token, err := s.crypt.GenerateJWTOrder(order)
if err != nil {
logger.ContextLogger(ctx).Error("error when create token", zap.Error(err))
return nil, err
}
order, err = s.repo.FindByID(ctx, order.ID)
if err != nil {
logger.ContextLogger(ctx).Error("error when creating order", zap.Error(err))
@ -152,7 +128,6 @@ func (s *OrderService) CreateOrder(ctx mycontext.Context, req *entity.OrderReque
return &entity.OrderResponse{
Order: order,
Token: token,
}, nil
}
@ -187,26 +162,6 @@ func (s *OrderService) CheckInInquiry(ctx mycontext.Context, qrCode string, part
return nil, errors2.ErrorInvalidRequest
}
location, _ := time.LoadLocation("Asia/Jakarta")
today := time.Now().In(location).Format("2006-01-02")
visitDate := time.Date(
order.VisitDate.Year(),
order.VisitDate.Month(),
order.VisitDate.Day(),
0, 0, 0, 0,
location,
).Format("2006-01-02")
if order.TicketStatus == "USED" || visitDate < today {
return nil, errors2.NewErrorMessage(errors2.ErrorTicketInvalidOrAlreadyUsed,
"Maaf! Tiket ini tidak valid karena sudah terpakai atau sudah kadaluwarsa")
}
if visitDate != today {
return nil, errors2.NewErrorMessage(errors2.ErrorTicketInvalidOrAlreadyUsed,
"Maaf Tiket ini tidak valid karena tidak sesuai dengan tanggal tiket")
}
token, err := s.crypt.GenerateJWTOrder(order)
if err != nil {
logger.ContextLogger(ctx).Error("error when generate checkin token", zap.Error(err))
@ -242,17 +197,6 @@ func (s *OrderService) CheckInExecute(ctx mycontext.Context,
Order: order,
}
if order.TicketStatus != "UNUSED" {
return resp, nil
}
order.TicketStatus = "USED"
order, err = s.repo.Update(ctx, order)
if err != nil {
logger.ContextLogger(ctx).Error("error when updating order status", zap.Error(err))
return nil, err
}
return resp, nil
}
@ -446,11 +390,6 @@ func (s *OrderService) processQRPayment(ctx mycontext.Context, order *entity.Ord
}
func (s *OrderService) processVAPayment(ctx mycontext.Context, order *entity.Order, partnerID, createdBy int64) (*entity.PaymentResponse, error) {
metadata := map[string]string{}
if err := json.Unmarshal(order.Metadata, &metadata); err != nil {
return nil, err
}
paymentRequest := entity.PaymentRequest{
PaymentReferenceID: generator.GenerateUUIDV4(),
TotalAmount: int64(order.Total),
@ -458,7 +397,6 @@ func (s *OrderService) processVAPayment(ctx mycontext.Context, order *entity.Ord
CustomerID: strconv.FormatInt(order.User.ID, 10),
CustomerName: order.User.Name,
CustomerEmail: order.User.Email,
BankCode: metadata["bank_code"],
}
paymentResponse, err := s.pg.CreatePaymentVA(paymentRequest)
@ -544,13 +482,11 @@ func (s *OrderService) processPayment(ctx context.Context, tx *gorm.DB, req *ent
transaction := &entity.Transaction{
PartnerID: existingPayment.PartnerID,
TransactionType: "PAYMENT_RECEIVED",
ReferenceID: existingPayment.ReferenceID,
Status: "SUCCESS",
CreatedBy: 0,
Amount: existingPayment.Amount,
Fee: order.Fee,
Total: order.Total,
SiteID: order.SiteID,
}
if _, err = s.transaction.Create(ctx, tx, transaction); err != nil {
return fmt.Errorf("failed to update transaction: %w", err)

View File

@ -14,6 +14,9 @@ import (
"enaklo-pos-be/internal/services/studio"
"enaklo-pos-be/internal/services/transaction"
"enaklo-pos-be/internal/services/users"
customerSvc "enaklo-pos-be/internal/services/v2/customer"
orderSvc "enaklo-pos-be/internal/services/v2/order"
productSvc "enaklo-pos-be/internal/services/v2/product"
"gorm.io/gorm"
@ -38,9 +41,17 @@ type ServiceManagerImpl struct {
Transaction Transaction
Balance Balance
DiscoverService DiscoverService
OrderV2Svc orderSvc.Service
CustomerV2Svc customerSvc.Service
ProductV2Svc productSvc.Service
}
func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl) *ServiceManagerImpl {
custSvcV2 := customerSvc.New(repo.CustomerRepo)
productSvcV2 := productSvc.New(repo.ProductRepo)
return &ServiceManagerImpl{
AuthSvc: auth.New(repo.Auth, repo.Crypto, repo.User, repo.EmailService, cfg.Email, repo.Trx, repo.License),
EventSvc: event.NewEventService(repo.Event),
@ -56,6 +67,7 @@ func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl)
Transaction: transaction.New(repo.Transaction, repo.Wallet, repo.Trx),
Balance: balance.NewBalanceService(repo.Wallet, repo.Trx, repo.Crypto, &cfg.Withdraw, repo.Transaction),
DiscoverService: discovery.NewDiscoveryService(repo.Site, cfg.Discovery, repo.Product),
OrderV2Svc: orderSvc.New(repo.OrderRepo, productSvcV2, custSvcV2, repo.TransactionRepo, repo.Crypto, &cfg.Order, repo.EmailService),
}
}

View 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
}

View 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
}

View 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
}

View 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,
}
}

View 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
}

View 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"
}

View 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,
}
}

View File

@ -22,4 +22,4 @@ spec:
tls:
- hosts:
- "api-dev.enaklo.co.id"
secretName: enaklo-pos-backend-app-dev-biz-id-tls
secretName: enaklo-pos-dev-app-dev-biz-id-tls

View File

@ -32,6 +32,7 @@ func main() {
routes.RegisterPublicRoutes(server, service, repo)
routes.RegisterPrivateRoutes(server, service, repo)
routes.RegisterPrivateRoutesV2(server, service, repo)
routes.RegisterCustomerRoutes(server, service, repo)
server.StartScheduler()

View 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>

View 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>