This commit is contained in:
aditya.siregar 2025-04-05 11:28:06 +08:00
parent 118ec58521
commit c642c5c61b
35 changed files with 1194 additions and 212 deletions

View File

@ -23,6 +23,8 @@ const (
errTicketAlreadyUsed ErrType = "Ticket Already Used."
errProductIsRequired ErrType = "Product"
errEmailAndPhoneNumberRequired ErrType = "Email or Phone is required"
errEmailAlreadyRegistered ErrType = "Email is already registered"
errPhoneNumberAlreadyRegistered ErrType = "Phone is already registered"
)
var (
@ -43,6 +45,8 @@ var (
ErrorInvalidLicense = NewServiceException(errInactivePartner)
ErrorTicketInvalidOrAlreadyUsed = NewServiceException(errTicketAlreadyUsed)
ErrorPhoneNumberEmailIsRequired = NewServiceException(errEmailAndPhoneNumberRequired)
ErrorPhoneNumberIsAlreadyRegistered = NewServiceException(errPhoneNumberAlreadyRegistered)
ErrorEmailIsAlreadyRegistered = NewServiceException(errEmailAlreadyRegistered)
)
type Error interface {

View File

@ -0,0 +1,2 @@
package entity

View File

@ -0,0 +1,29 @@
package entity
import "time"
type InProgressOrder struct {
ID string
PartnerID int64
CustomerID *int64
CustomerName string
CreatedBy int64
PaymentType string
PaymentProvider string
OrderItems []InProgressOrderItem
Payment Payment
User User
Source string
OrderType string
TableNumber string
CreatedAt time.Time
UpdatedAt time.Time
}
type InProgressOrderItem struct {
ID int64
InProgressOrderID int64
ItemID int64
Quantity int
Product *Product
}

View File

@ -12,17 +12,22 @@ type Order struct {
Total float64 `gorm:"type:numeric;not null;column:total"`
Fee float64 `gorm:"type:numeric;not null;column:fee"`
CustomerID *int64
CustomerName string
InquiryID *string
Site *Site `gorm:"foreignKey:SiteID;constraint:OnDelete:CASCADE;"`
CreatedAt time.Time `gorm:"autoCreateTime;column:created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at"`
CreatedBy int64 `gorm:"type:int;column:created_by"`
PaymentType string `gorm:"type:varchar;column:payment_type"`
PaymentProvider string `gorm:"type:varchar;column:payment_provider"`
UpdatedBy int64 `gorm:"type:int;column:updated_by"`
OrderItems []OrderItem `gorm:"foreignKey:OrderID;constraint:OnDelete:CASCADE;"`
Payment Payment `gorm:"foreignKey:OrderID;constraint:OnDelete:CASCADE;"`
User User `gorm:"foreignKey:CreatedBy;constraint:OnDelete:CASCADE;"`
Source string `gorm:"type:varchar;column:source"`
OrderType string `gorm:"type:varchar;column:order_type"`
TableNumber string
InProgressOrderID string
}
type OrderDB struct {
@ -97,6 +102,9 @@ type OrderRequest struct {
CustomerName string
CustomerEmail string
CustomerPhoneNumber string
TableNumber string
PaymentProvider string
OrderType string
}
type OrderItemRequest struct {
@ -111,11 +119,7 @@ type OrderExecuteRequest struct {
}
func (o *Order) SetExecutePaymentStatus() {
if o.PaymentType == "CASH" {
o.Status = "PAID"
return
}
o.Status = "PENDING"
}
type CallbackRequest struct {

View File

@ -23,6 +23,9 @@ type OrderInquiry struct {
UpdatedAt time.Time `json:"updated_at"`
ExpiresAt time.Time `json:"expires_at"`
OrderItems []OrderItem `json:"order_items"`
PaymentProvider string `json:"payment_provider"`
TableNumber string `json:"table_number"`
OrderType string `json:"order_type"`
}
type OrderCalculation struct {
@ -48,6 +51,9 @@ func NewOrderInquiry(
customerName string,
customerPhoneNumber string,
customerEmail string,
paymentProvider string,
tableNumber string,
orderType string,
) *OrderInquiry {
return &OrderInquiry{
ID: constants.GenerateUUID(),
@ -66,6 +72,9 @@ func NewOrderInquiry(
CustomerName: customerName,
CustomerEmail: customerEmail,
CustomerPhoneNumber: customerPhoneNumber,
PaymentProvider: paymentProvider,
TableNumber: tableNumber,
OrderType: orderType,
}
}
@ -80,7 +89,7 @@ func (oi *OrderInquiry) AddOrderItem(item OrderItemRequest, product *Product) {
})
}
func (i *OrderInquiry) ToOrder(paymentMethod string) *Order {
func (i *OrderInquiry) ToOrder(paymentMethod, paymentProvider string) *Order {
now := time.Now()
order := &Order{
@ -92,10 +101,12 @@ func (i *OrderInquiry) ToOrder(paymentMethod string) *Order {
Fee: i.Fee,
Total: i.Total,
PaymentType: paymentMethod,
PaymentProvider: paymentProvider,
Source: i.Source,
CreatedBy: i.CreatedBy,
CreatedAt: now,
OrderItems: make([]OrderItem, len(i.OrderItems)),
OrderType: i.OrderType,
}
for idx, item := range i.OrderItems {

View File

@ -51,6 +51,8 @@ type Customer struct {
ResetPassword bool
CustomerID string
BirthDate time.Time
VerificationID string
OTP string
}
type AuthenticateUser struct {
@ -117,3 +119,9 @@ func (u User) HashedPassword(password string) (string, error) {
return string(hashedPassword), nil
}
func (c Customer) HashedPassword() string {
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(c.Password), bcrypt.DefaultCost)
return string(hashedPassword)
}

View File

@ -1,6 +1,8 @@
package customerauth
import (
auth2 "enaklo-pos-be/internal/handlers/request"
"enaklo-pos-be/internal/services/v2/customer"
"fmt"
"net/http"
"strings"
@ -8,7 +10,6 @@ import (
"github.com/gin-gonic/gin"
"enaklo-pos-be/internal/common/errors"
auth2 "enaklo-pos-be/internal/handlers/request"
"enaklo-pos-be/internal/handlers/response"
"enaklo-pos-be/internal/services"
)
@ -16,6 +17,7 @@ import (
type AuthHandler struct {
service services.Auth
userService services.User
customerSvc customer.Service
}
func (a *AuthHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) {
@ -24,12 +26,14 @@ func (a *AuthHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) {
authRoute.POST("/forgot-password", a.ForgotPassword)
authRoute.POST("/reset-password", jwt, a.ResetPassword)
authRoute.POST("/register", a.Register)
authRoute.POST("/verify", a.VerifyRegistration)
}
func NewAuthHandler(service services.Auth, userService services.User) *AuthHandler {
func NewAuthHandler(service services.Auth, userService services.User, customerSvc customer.Service) *AuthHandler {
return &AuthHandler{
service: service,
userService: userService,
customerSvc: customerSvc,
}
}
@ -147,31 +151,47 @@ func (h *AuthHandler) ResetPassword(c *gin.Context) {
}
func (h *AuthHandler) Register(c *gin.Context) {
var req auth2.UserRegister
var req auth2.CustomerRegister
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWrapper(c, errors.ErrorBadRequest)
return
}
ctx := auth2.GetMyContext(c)
res, err := h.userService.Create(ctx, req.ToEntity())
customer, err := h.customerSvc.RegistrationMember(ctx, req.ToEntity())
if err != nil {
response.ErrorWrapper(c, err)
return
}
resp := response.UserRegister{
ID: res.ID,
Name: res.Name,
Email: res.Email,
Status: string(res.Status),
CreatedAt: res.CreatedAt,
UpdatedAt: res.UpdatedAt,
c.JSON(http.StatusOK, response.BaseResponse{
Success: true,
Status: http.StatusOK,
Data: response.CustomerRegistrationResp{
EmailVerificationRequired: true,
PhoneVerificationRequired: false,
VerificationID: customer.VerificationID,
},
})
}
func (h *AuthHandler) VerifyRegistration(c *gin.Context) {
var req auth2.VerifyEmailRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWrapper(c, errors.ErrorBadRequest)
return
}
ctx := auth2.GetMyContext(c)
err := h.customerSvc.VerifyOTP(ctx, req.VerificationID, req.OTPCode)
if err != nil {
response.ErrorWrapper(c, err)
return
}
c.JSON(http.StatusOK, response.BaseResponse{
Success: true,
Status: http.StatusOK,
Data: resp,
Message: "Email verification successful",
})
}

View File

@ -0,0 +1,173 @@
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/inprogress_order"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"net/http"
"strconv"
)
type InProgressOrderHandler struct {
service inprogress_order.InProgressOrderService
}
func NewInProgressOrderHandler(service inprogress_order.InProgressOrderService) *InProgressOrderHandler {
return &InProgressOrderHandler{
service: service,
}
}
func (h *InProgressOrderHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) {
route := group.Group("/inprogress-order")
route.POST("/save", jwt, h.Save)
route.GET("/list", jwt, h.GetByPartnerID)
}
type CreateInProgressOrderRequest 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"`
OrderItems []InProgressOrderItemRequest `json:"order_items" validate:"required,min=1,dive"`
OrderType string `json:"order_type"`
PaymentProvider string `json:"payment_provider"`
TableNumber string `json:"table_number"`
InProgressOrderID string `json:"in_progress_order_id"`
}
type InProgressOrderItemRequest struct {
ProductID int64 `json:"product_id" validate:"required"`
Quantity int `json:"quantity" validate:"required,min=1"`
}
type UpdateInProgressOrderRequest struct {
Status string `json:"status" validate:"required"`
Amount float64 `json:"amount" validate:"required,min=0"`
Fee float64 `json:"fee" validate:"min=0"`
Total float64 `json:"total" validate:"required,min=0"`
PaymentType string `json:"payment_type" validate:"required"`
OrderItems []InProgressOrderItemRequest `json:"order_items" validate:"required,min=1,dive"`
}
func (h *InProgressOrderHandler) Save(c *gin.Context) {
ctx := request.GetMyContext(c)
userID := ctx.RequestedBy()
partnerID := ctx.GetPartnerID()
var req CreateInProgressOrderRequest
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.InProgressOrderItem, len(req.OrderItems))
for i, item := range req.OrderItems {
orderItems[i] = entity.InProgressOrderItem{
ItemID: item.ProductID,
Quantity: item.Quantity,
}
}
order := &entity.InProgressOrder{
PartnerID: *partnerID,
CustomerID: req.CustomerID,
CustomerName: req.CustomerName,
CreatedBy: userID,
OrderItems: orderItems,
TableNumber: req.TableNumber,
OrderType: req.OrderType,
ID: req.InProgressOrderID,
}
_, err := h.service.Save(ctx, order)
if err != nil {
response.ErrorWrapper(c, err)
return
}
c.JSON(http.StatusCreated, response.BaseResponse{
Success: true,
Status: http.StatusCreated,
})
}
func mapToInProgressOrderResponse(order *entity.InProgressOrder) map[string]interface{} {
orderItems := make([]map[string]interface{}, len(order.OrderItems))
for i, item := range order.OrderItems {
orderItems[i] = map[string]interface{}{
"id": item.ID,
"item_id": item.ItemID,
"quantity": item.Quantity,
"name": item.Product.Name,
"price": item.Product.Price,
"image": item.Product.Image,
}
}
return map[string]interface{}{
"id": order.ID,
"partner_id": order.PartnerID,
"customer_id": order.CustomerID,
"customer_name": order.CustomerName,
"payment_type": order.PaymentType,
"source": order.Source,
"created_by": order.CreatedBy,
"created_at": order.CreatedAt,
"updated_at": order.UpdatedAt,
"order_items": orderItems,
"table_number": order.TableNumber,
}
}
func (h *InProgressOrderHandler) GetByPartnerID(c *gin.Context) {
ctx := request.GetMyContext(c)
limitStr := c.DefaultQuery("limit", "10")
offsetStr := c.DefaultQuery("offset", "0")
limit, err := strconv.Atoi(limitStr)
if err != nil || limit < 0 {
limit = 10
}
offset, err := strconv.Atoi(offsetStr)
if err != nil || offset < 0 {
offset = 0
}
orders, err := h.service.GetOrdersByPartnerID(ctx, *ctx.GetPartnerID(), limit, offset)
if err != nil {
response.ErrorWrapper(c, err)
return
}
orderResponses := make([]map[string]interface{}, len(orders))
for i, order := range orders {
orderResponses[i] = mapToInProgressOrderResponse(order)
}
c.JSON(http.StatusOK, response.BaseResponse{
Success: true,
Status: http.StatusOK,
Data: map[string]interface{}{
"orders": orderResponses,
"pagination": map[string]interface{}{
"limit": limit,
"offset": offset,
"count": len(orders),
},
},
})
}

View File

@ -35,6 +35,17 @@ type InquiryRequest struct {
CustomerPhoneNumber string `json:"customer_phone_number"`
PaymentMethod string `json:"payment_method" validate:"required"`
OrderItems []OrderItemRequest `json:"order_items" validate:"required,min=1,dive"`
OrderType string `json:"order_type"`
PaymentProvider string `json:"payment_provider"`
TableNumber string `json:"table_number"`
}
func (o *InquiryRequest) GetPaymentProvider() string {
if o.PaymentMethod == "CASH" {
return "CASH"
}
return o.PaymentProvider
}
type OrderItemRequest struct {
@ -44,6 +55,8 @@ type OrderItemRequest struct {
type ExecuteRequest struct {
PaymentMethod string `json:"payment_method" validate:"required"`
PaymentProvider string `json:"payment_provider"`
InProgressOrderID string `json:"in_progress_order_id"`
Token string `json:"token"`
}
@ -82,6 +95,9 @@ func (h *Handler) Inquiry(c *gin.Context) {
CustomerName: req.CustomerName,
CustomerEmail: req.CustomerEmail,
CustomerPhoneNumber: req.CustomerPhoneNumber,
OrderType: req.OrderType,
PaymentProvider: req.GetPaymentProvider(),
TableNumber: req.TableNumber,
}
result, err := h.service.CreateOrderInquiry(ctx, orderReq)
@ -112,7 +128,7 @@ func (h *Handler) Execute(c *gin.Context) {
return
}
result, err := h.service.ExecuteOrderInquiry(ctx, req.Token, req.PaymentMethod)
result, err := h.service.ExecuteOrderInquiry(ctx, req.Token, req.PaymentMethod, req.PaymentProvider, req.InProgressOrderID)
if err != nil {
response.ErrorWrapper(c, err)
return

View File

@ -0,0 +1,49 @@
package request
import (
"enaklo-pos-be/internal/entity"
"github.com/go-playground/validator/v10"
"strings"
"time"
)
type CustomerRegister struct {
Name string `json:"name" validate:"required" binding:"required"`
Email string `json:"email" validate:"required" binding:"required"`
PhoneNumber string `json:"phone_number" validate:"required" binding:"required"`
BirthDate string `json:"birth_date" validate:"required" binding:"required"`
Password string `json:"password" validate:"required" binding:"required"`
}
func (c *CustomerRegister) Validate() error {
validate := validator.New()
if err := validate.Struct(c); err != nil {
return err
}
return nil
}
func (c *CustomerRegister) GetBirthdate() (time.Time, error) {
parsedDate, err := time.Parse("02-01-2006", c.BirthDate)
if err != nil {
return time.Time{}, err
}
return parsedDate, nil
}
func (c *CustomerRegister) ToEntity() *entity.Customer {
birthdate, _ := c.GetBirthdate()
return &entity.Customer{
Name: c.Name,
Email: strings.ToLower(c.Email),
PhoneNumber: c.PhoneNumber,
Password: c.Password,
BirthDate: birthdate,
}
}
type VerifyEmailRequest struct {
VerificationID string `json:"verification_id" binding:"required"`
OTPCode string `json:"otp_code" binding:"required"`
}

View File

@ -11,6 +11,8 @@ type Order struct {
CustomerPhone string `json:"customer_phone"`
CustomerEmail string `json:"customer_email"`
PaymentMethod string `json:"payment_method"`
PaymentProvider string `json:"payment_provider"`
TableNumber string `json:"table_number"`
OrderItems []OrderItem `json:"order_items"`
}

View File

@ -174,7 +174,5 @@ func (u *UserRegister) ToEntity() *entity.User {
Email: strings.ToLower(u.Email),
PhoneNumber: u.PhoneNumber,
Password: u.Password,
RoleID: role.Customer,
UserType: "CUSTOMER",
}
}

View File

@ -33,3 +33,9 @@ func MapToCustomerListResponse(customers *entity.MemberList) []CustomerResponse
return responseList
}
type CustomerRegistrationResp struct {
EmailVerificationRequired bool `json:"email_verification_required"`
PhoneVerificationRequired bool `json:"phone_verification_required"`
VerificationID string `json:"verification_id"`
}

View File

@ -49,7 +49,7 @@ type UserRegister struct {
ID int64 `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Status string `json:"status"`
PhoneNumber string `json:"phone_number"`
CreatedAt time.Time `json:"created_at,omitempty"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
}

View File

@ -0,0 +1,267 @@
package repository
import (
"enaklo-pos-be/internal/common/mycontext"
"enaklo-pos-be/internal/constants"
"enaklo-pos-be/internal/entity"
"enaklo-pos-be/internal/repository/models"
"github.com/pkg/errors"
"gorm.io/gorm"
time2 "time"
)
type InProgressOrderRepository interface {
CreateOrUpdate(ctx mycontext.Context, order *entity.InProgressOrder) (*entity.InProgressOrder, error)
GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.InProgressOrder, error)
}
type inprogressOrderRepository struct {
db *gorm.DB
}
func NewInProgressOrderRepository(db *gorm.DB) *inprogressOrderRepository {
return &inprogressOrderRepository{db: db}
}
func (r *inprogressOrderRepository) CreateOrUpdate(ctx mycontext.Context, order *entity.InProgressOrder) (*entity.InProgressOrder, error) {
isUpdate := order.ID != ""
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()
}
}()
orderDB := r.toInProgressOrderDBModel(order)
if isUpdate {
var existingOrder models.InProgressOrderDB
if err := tx.First(&existingOrder, order.ID).Error; err != nil {
tx.Rollback()
return nil, errors.Wrap(err, "order not found for update")
}
if err := tx.Model(&orderDB).Updates(orderDB).Error; err != nil {
tx.Rollback()
return nil, errors.Wrap(err, "failed to update order")
}
if err := tx.Where("in_progress_order_id = ?", order.ID).Delete(&models.InProgressOrderItemDB{}).Error; err != nil {
tx.Rollback()
return nil, errors.Wrap(err, "failed to delete existing order items")
}
} else {
if err := tx.Create(&orderDB).Error; err != nil {
tx.Rollback()
return nil, errors.Wrap(err, "failed to insert order")
}
order.ID = orderDB.ID
}
var itemIDs []int64
for i := range order.OrderItems {
itemIDs = append(itemIDs, order.OrderItems[i].ItemID)
}
var products []models.ProductDB
if len(itemIDs) > 0 {
if err := tx.Where("id IN ?", itemIDs).Find(&products).Error; err != nil {
tx.Rollback()
return nil, errors.Wrap(err, "failed to fetch products")
}
}
productMap := make(map[int64]models.ProductDB)
for _, product := range products {
productMap[product.ID] = product
}
for i := range order.OrderItems {
item := &order.OrderItems[i]
itemDB := r.toOrderItemDBModel(item, orderDB.ID)
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 product, exists := productMap[item.ItemID]; exists {
item.Product = r.toDomainProductModel(&product)
}
}
if err := tx.Commit().Error; err != nil {
return nil, errors.Wrap(err, "failed to commit transaction")
}
return order, nil
}
func (r *inprogressOrderRepository) GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.InProgressOrder, error) {
var ordersDB []models.InProgressOrderDB
query := r.db.Where("partner_id = ?", partnerID).Order("created_at DESC")
if limit > 0 {
query = query.Limit(limit)
}
if offset > 0 {
query = query.Offset(offset)
}
if err := query.Preload("OrderItems.Product").Find(&ordersDB).Error; err != nil {
return nil, errors.Wrap(err, "failed to find orders by partner ID")
}
orders := make([]*entity.InProgressOrder, 0, len(ordersDB))
for _, orderDB := range ordersDB {
order := r.toDomainOrderModel(&orderDB)
order.OrderItems = make([]entity.InProgressOrderItem, 0, len(orderDB.OrderItems))
for _, itemDB := range orderDB.OrderItems {
item := r.toDomainOrderItemModel(&itemDB)
orderItem := entity.InProgressOrderItem{
ID: item.ID,
ItemID: item.ItemID,
Quantity: item.Quantity,
}
if itemDB.Product.ID > 0 {
productDomain := r.toDomainProductModel(&itemDB.Product)
orderItem.Product = productDomain
}
order.OrderItems = append(order.OrderItems, orderItem)
}
orders = append(orders, order)
}
return orders, nil
}
func (r *inprogressOrderRepository) toInProgressOrderDBModel(order *entity.InProgressOrder) models.InProgressOrderDB {
now := time2.Now()
return models.InProgressOrderDB{
ID: constants.GenerateUUID(),
PartnerID: order.PartnerID,
CustomerID: order.CustomerID,
CustomerName: order.CustomerName,
PaymentType: order.PaymentType,
CreatedBy: order.CreatedBy,
CreatedAt: now,
UpdatedAt: now,
TableNumber: order.TableNumber,
OrderType: order.OrderType,
}
}
func (r *inprogressOrderRepository) toDomainOrderModel(dbModel *models.InProgressOrderDB) *entity.InProgressOrder {
return &entity.InProgressOrder{
ID: dbModel.ID,
PartnerID: dbModel.PartnerID,
CustomerID: dbModel.CustomerID,
CustomerName: dbModel.CustomerName,
PaymentType: dbModel.PaymentType,
CreatedBy: dbModel.CreatedBy,
OrderItems: []entity.InProgressOrderItem{},
TableNumber: dbModel.TableNumber,
OrderType: dbModel.OrderType,
CreatedAt: dbModel.CreatedAt,
UpdatedAt: dbModel.UpdatedAt,
}
}
func (r *inprogressOrderRepository) toOrderItemDBModel(item *entity.InProgressOrderItem, inprogressOrderID string) models.InProgressOrderItemDB {
return models.InProgressOrderItemDB{
ID: item.ID,
InProgressOrderIO: inprogressOrderID,
ItemID: item.ItemID,
Quantity: item.Quantity,
}
}
func (r *inprogressOrderRepository) toDomainOrderItemModel(dbModel *models.InProgressOrderItemDB) *entity.OrderItem {
return &entity.OrderItem{
ID: dbModel.ID,
ItemID: dbModel.ItemID,
Quantity: dbModel.Quantity,
CreatedBy: dbModel.CreatedBy,
CreatedAt: dbModel.CreatedAt,
}
}
func (r *inprogressOrderRepository) 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,
PaymentProvider: inquiry.PaymentProvider,
OrderType: inquiry.OrderType,
TableNumber: inquiry.TableNumber,
}
}
func (r *inprogressOrderRepository) 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
}
func (r *inprogressOrderRepository) toDomainProductModel(productDB *models.ProductDB) *entity.Product {
if productDB == nil {
return nil
}
return &entity.Product{
ID: productDB.ID,
Name: productDB.Name,
Description: productDB.Description,
Price: productDB.Price,
CreatedAt: productDB.CreatedAt,
UpdatedAt: productDB.UpdatedAt,
Type: productDB.Type,
Image: productDB.Image,
}
}

View File

@ -4,8 +4,11 @@ import (
"enaklo-pos-be/internal/common/mycontext"
"enaklo-pos-be/internal/entity"
"enaklo-pos-be/internal/repository/models"
"fmt"
"github.com/google/uuid"
"github.com/pkg/errors"
"gorm.io/gorm"
"math/rand"
"time"
)
@ -14,9 +17,10 @@ type CustomerRepo interface {
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
AddPoints(ctx mycontext.Context, id int64, points int, reference string) error
FindSequence(ctx mycontext.Context, partnerID int64) (int64, error)
GetAllCustomers(ctx mycontext.Context, req entity.MemberSearch) (entity.MemberList, int, error)
VerifyOTP(ctx mycontext.Context, verificationHash string, otpCode string) (int64, error)
}
type customerRepository struct {
@ -28,13 +32,53 @@ func NewCustomerRepository(db *gorm.DB) *customerRepository {
}
func (r *customerRepository) Create(ctx mycontext.Context, customer *entity.Customer) (*entity.Customer, error) {
customerDB := r.toCustomerDBModel(customer)
tx := r.db.Begin()
if tx.Error != nil {
return nil, errors.Wrap(tx.Error, "failed to begin transaction")
}
if err := r.db.Create(&customerDB).Error; err != nil {
customerDB := r.toCustomerDBModel(customer)
if err := tx.Omit("CustomerID").Create(&customerDB).Error; err != nil {
tx.Rollback()
return nil, errors.Wrap(err, "failed to insert customer")
}
customerPoints := models.CustomerPointsDB{
CustomerID: uint64(customerDB.ID),
TotalPoints: 0,
AvailablePoints: 0,
LastUpdated: time.Now(),
}
if err := tx.Create(&customerPoints).Error; err != nil {
tx.Rollback()
return nil, errors.Wrap(err, "failed to create initial customer points")
}
otpCode := r.generateOTPCode()
expiresAt := time.Now().Add(15 * time.Minute)
verificationCode := models.CustomerVerificationCodeDB{
CustomerID: uint64(customerDB.ID),
Code: otpCode,
Type: "EMAIL",
ExpiresAt: expiresAt,
IsUsed: false,
VerificationID: uuid.New(),
}
if err := tx.Create(&verificationCode).Error; err != nil {
tx.Rollback()
return nil, errors.Wrap(err, "failed to create verification code")
}
if err := tx.Commit().Error; err != nil {
return nil, errors.Wrap(err, "failed to commit transaction")
}
customer.ID = customerDB.ID
customer.VerificationID = verificationCode.VerificationID.String()
customer.OTP = otpCode
return customer, nil
}
@ -84,22 +128,45 @@ func (r *customerRepository) FindByEmail(ctx mycontext.Context, email string) (*
return customer, nil
}
func (r *customerRepository) AddPoints(ctx mycontext.Context, id int64, points int) error {
now := time.Now()
func (r *customerRepository) AddPoints(ctx mycontext.Context, customerID int64, points int, reference string) error {
tx := r.db.Begin()
if tx.Error != nil {
return errors.Wrap(tx.Error, "failed to begin transaction")
}
result := r.db.Model(&models.CustomerDB{}).
Where("id = ?", id).
result := tx.Model(&models.CustomerPointsDB{}).
Where("customer_id = ?", customerID).
Updates(map[string]interface{}{
"points": gorm.Expr("points + ?", points),
"updated_at": now,
"total_points": gorm.Expr("total_points + ?", points),
"available_points": gorm.Expr("available_points + ?", points),
"last_updated": time.Now(),
})
if result.Error != nil {
return errors.Wrap(result.Error, "failed to add points to customer")
tx.Rollback()
return errors.Wrap(result.Error, "failed to update customer points")
}
if result.RowsAffected == 0 {
return errors.New("customer not found")
tx.Rollback()
return errors.New("customer points record not found")
}
pointTransaction := models.CustomerPointTransactionDB{
CustomerID: customerID,
Reference: reference,
PointsEarned: points,
TransactionDate: time.Now(),
Status: "SUCCESS",
}
if err := tx.Create(&pointTransaction).Error; err != nil {
tx.Rollback()
return errors.Wrap(err, "failed to create point transaction record")
}
if err := tx.Commit().Error; err != nil {
return errors.Wrap(err, "failed to commit transaction")
}
return nil
@ -114,8 +181,8 @@ func (r *customerRepository) toCustomerDBModel(customer *entity.Customer) models
Points: customer.Points,
CreatedAt: customer.CreatedAt,
UpdatedAt: customer.UpdatedAt,
CustomerID: customer.CustomerID,
BirthDate: customer.BirthDate,
Password: customer.Password,
}
}
@ -232,3 +299,59 @@ func (r *customerRepository) GetAllCustomers(ctx mycontext.Context, req entity.M
return customers, int(totalCount), nil
}
func (r *customerRepository) generateOTPCode() string {
rand.Seed(time.Now().UnixNano())
otpCode := fmt.Sprintf("%06d", rand.Intn(1000000))
return otpCode
}
func (r *customerRepository) VerifyOTP(ctx mycontext.Context, verificationHash string, otpCode string) (int64, error) {
var verificationCode models.CustomerVerificationCodeDB
if err := r.db.Where("verification_id = ? AND is_used = false", verificationHash).First(&verificationCode).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return 0, errors.New("invalid or expired verification code")
}
return 0, errors.Wrap(err, "failed to find verification code")
}
if time.Now().After(verificationCode.ExpiresAt) {
return 0, errors.New("verification code has expired")
}
if verificationCode.Code != otpCode {
return 0, errors.New("invalid verification code")
}
tx := r.db.Begin()
if tx.Error != nil {
return 0, errors.Wrap(tx.Error, "failed to begin transaction")
}
if err := tx.Model(&verificationCode).Updates(map[string]interface{}{
"is_used": true,
}).Error; err != nil {
tx.Rollback()
return 0, errors.Wrap(err, "failed to mark verification code as used")
}
if verificationCode.Type == "EMAIL" {
if err := tx.Model(&models.CustomerDB{}).Where("id = ?", verificationCode.CustomerID).
Update("is_email_verified", true).Error; err != nil {
tx.Rollback()
return 0, errors.Wrap(err, "failed to update customer verification status")
}
} else if verificationCode.Type == "PHONE" {
if err := tx.Model(&models.CustomerDB{}).Where("id = ?", verificationCode.CustomerID).
Update("is_phone_verified", true).Error; err != nil {
tx.Rollback()
return 0, errors.Wrap(err, "failed to update customer verification status")
}
}
if err := tx.Commit().Error; err != nil {
return 0, errors.Wrap(err, "failed to commit transaction")
}
return int64(verificationCode.CustomerID), nil
}

View File

@ -1,6 +1,7 @@
package models
import (
"github.com/google/uuid"
"time"
)
@ -14,6 +15,9 @@ type CustomerDB struct {
UpdatedAt time.Time `gorm:"column:updated_at"`
CustomerID string `gorm:"column:customer_id"`
BirthDate time.Time `gorm:"column:birth_date"`
Password string `gorm:"column:password"`
IsEmailVerified bool `gorm:"column:is_email_verified"`
IsPhoneVerified bool `gorm:"column:is_phone_verified"`
}
func (CustomerDB) TableName() string {
@ -30,3 +34,45 @@ type PartnerMemberSequence struct {
func (PartnerMemberSequence) TableName() string {
return "partner_member_sequences"
}
type CustomerPointsDB struct {
ID uint64 `gorm:"column:id;primaryKey;autoIncrement"`
CustomerID uint64 `gorm:"column:customer_id;not null"`
TotalPoints int `gorm:"column:total_points;not null;default:0"`
AvailablePoints int `gorm:"column:available_points;not null;default:0"`
LastUpdated time.Time `gorm:"column:last_updated;default:CURRENT_TIMESTAMP"`
}
func (CustomerPointsDB) TableName() string {
return "customer_points"
}
type CustomerPointTransactionDB struct {
ID uint64 `gorm:"column:id;primaryKey;autoIncrement"`
CustomerID int64 `gorm:"column:customer_id;not null"`
Reference string `gorm:"column:transaction_id"`
PointsEarned int `gorm:"column:points_earned;not null"`
TransactionDate time.Time `gorm:"column:transaction_date;not null"`
ExpirationDate *time.Time `gorm:"column:expiration_date"`
Status string `gorm:"column:status;default:active"`
CreatedAt time.Time `gorm:"column:created_at;default:CURRENT_TIMESTAMP"`
}
func (CustomerPointTransactionDB) TableName() string {
return "customer_point_transactions"
}
type CustomerVerificationCodeDB struct {
ID uint64 `gorm:"column:id;primaryKey;autoIncrement"`
CustomerID uint64 `gorm:"column:customer_id;not null"`
Code string `gorm:"column:code;not null"`
Type string `gorm:"column:type;not null"`
ExpiresAt time.Time `gorm:"column:expires_at;not null"`
IsUsed bool `gorm:"column:is_used;default:false"`
CreatedAt time.Time `gorm:"column:created_at;default:CURRENT_TIMESTAMP"`
VerificationID uuid.UUID `gorm:"column:verification_id;type:uuid;default:uuid_generate_v4()"`
}
func (CustomerVerificationCodeDB) TableName() string {
return "customer_verification_codes"
}

View File

@ -0,0 +1,35 @@
package models
import "time"
type InProgressOrderDB 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"`
PaymentType string `gorm:"column:payment_type"`
CreatedBy int64 `gorm:"column:created_by"`
CreatedAt time.Time `gorm:"column:created_at"`
UpdatedAt time.Time `gorm:"column:updated_at"`
TableNumber string `gorm:"column:table_number"`
OrderItems []InProgressOrderItemDB `gorm:"foreignKey:InProgressOrderIO"`
OrderType string `gorm:"column:order_type"`
}
type InProgressOrderItemDB struct {
ID int64 `gorm:"primaryKey;column:id"`
InProgressOrderIO string `gorm:"column:in_progress_order_id"`
ItemID int64 `gorm:"column:item_id"`
Quantity int `gorm:"column:quantity"`
CreatedBy int64 `gorm:"column:created_by"`
CreatedAt time.Time `gorm:"column:created_at"`
Product ProductDB `gorm:"foreignKey:ItemID;references:ID"`
}
func (InProgressOrderItemDB) TableName() string {
return "in_progress_order_items"
}
func (InProgressOrderDB) TableName() string {
return "in_progress_order"
}

View File

@ -58,6 +58,9 @@ type OrderInquiryDB struct {
UpdatedAt time.Time `gorm:"column:updated_at"`
ExpiresAt time.Time `gorm:"column:expires_at"`
InquiryItems []InquiryItemDB `gorm:"foreignKey:InquiryID"`
PaymentProvider string `gorm:"column:payment_provider"`
TableNumber string `gorm:"column:table_number"`
OrderType string `gorm:"column:order_type"`
}
func (OrderInquiryDB) TableName() string {

View File

@ -15,6 +15,7 @@ type ProductDB struct {
Status string `gorm:"column:status"`
CreatedAt time.Time `gorm:"column:created_at"`
UpdatedAt time.Time `gorm:"column:updated_at"`
Image string `gorm:"column:image"`
}
func (ProductDB) TableName() string {

View File

@ -61,6 +61,18 @@ func (r *orderRepository) Create(ctx mycontext.Context, order *entity.Order) (*e
item.ID = itemDB.ID
}
if order.InProgressOrderID != "" {
if err := tx.Where("in_progress_order_id = ?", order.InProgressOrderID).Delete(&models.InProgressOrderItemDB{}).Error; err != nil {
tx.Rollback()
return nil, errors.Wrap(err, "failed to delete in-progress order items")
}
if err := tx.Where("id = ?", order.InProgressOrderID).Delete(&models.InProgressOrderDB{}).Error; err != nil {
tx.Rollback()
return nil, errors.Wrap(err, "failed to delete in-progress order")
}
}
if err := tx.Commit().Error; err != nil {
return nil, errors.Wrap(err, "failed to commit transaction")
}
@ -263,6 +275,9 @@ func (r *orderRepository) toOrderInquiryDBModel(inquiry *entity.OrderInquiry) mo
CustomerName: inquiry.CustomerName,
CustomerPhoneNumber: inquiry.CustomerPhoneNumber,
CustomerEmail: inquiry.CustomerEmail,
PaymentProvider: inquiry.PaymentProvider,
OrderType: inquiry.OrderType,
TableNumber: inquiry.TableNumber,
}
}

View File

@ -141,9 +141,8 @@ func (b *OrderRepository) GetAllHystoryOrders(ctx context.Context, req entity.Or
query := b.db.Table("orders").
Select("orders.id as id, users.name as employee, sites.name as site, orders.created_at as timestamp, orders.created_at as booking_time, STRING_AGG(ticket_summary.name || ' x' || ticket_summary.total_qty, ', ') AS tickets, orders.payment_type as payment_type, orders.status as status, orders.amount as amount, orders.visit_date as visit_date, orders.ticket_status as ticket_status, orders.source as source").
Joins("left join (SELECT items.order_id, products.name, SUM(items.qty) AS total_qty FROM order_items items LEFT JOIN products ON items.item_id = products.id GROUP BY items.order_id, products.name) AS ticket_summary ON orders.id = ticket_summary.order_id").
Joins("left join (SELECT items.order_id, products.name, SUM(items.quantity) AS total_qty FROM order_items items LEFT JOIN products ON items.item_id = products.id GROUP BY items.order_id, products.name) AS ticket_summary ON orders.id = ticket_summary.order_id").
Joins("left join users on orders.created_by = users.id").
Joins("left join sites on orders.site_id = sites.id").
Where("orders.payment_type != ?", "NEW")
if req.PaymentType != "" {
@ -176,7 +175,7 @@ func (b *OrderRepository) GetAllHystoryOrders(ctx context.Context, req entity.Or
}
if req.SiteID != nil {
query = query.Where("orders.site_id = ?", req.SiteID)
query = query.Where("orders.partner_id = ?", req.SiteID)
}
if req.Source != "" {
@ -253,10 +252,6 @@ func (r *OrderRepository) SumAmount(ctx mycontext.Context, req entity.OrderSearc
query = query.Where("orders.partner_id = ?", req.PartnerID)
}
if req.SiteID != nil {
query = query.Where("orders.site_id = ?", req.SiteID)
}
if err := query.Scan(&amount).Error; err != nil {
logger.ContextLogger(ctx).Error("error when get cash amount", zap.Error(err))
return nil, err

View File

@ -53,6 +53,7 @@ type RepoManagerImpl struct {
LinkQu LinkQu
OrderRepo OrderRepository
InProgressOrderRepo InProgressOrderRepository
CustomerRepo CustomerRepo
ProductRepo ProductRepository
TransactionRepo TransactionRepo
@ -86,6 +87,7 @@ func NewRepoManagerImpl(db *gorm.DB, cfg *config.Config) *RepoManagerImpl {
ProductRepo: NewproductRepository(db),
TransactionRepo: NewTransactionRepository(db),
MemberRepository: NewMemberRepository(db),
InProgressOrderRepo: NewInProgressOrderRepository(db),
}
}

View File

@ -19,7 +19,7 @@ func RegisterCustomerRoutes(app *app.Server, serviceManager *services.ServiceMan
serverRoutes := []HTTPHandlerRoutes{
discovery.NewHandler(serviceManager.DiscoverService),
customerauth.NewAuthHandler(serviceManager.AuthSvc, serviceManager.UserSvc),
customerauth.NewAuthHandler(serviceManager.AuthSvc, serviceManager.UserSvc, serviceManager.CustomerV2Svc),
customerorder.NewHandler(serviceManager.OrderSvc),
}

View File

@ -80,6 +80,7 @@ func RegisterPrivateRoutesV2(app *app.Server, serviceManager *services.ServiceMa
http2.NewOrderHandler(serviceManager.OrderV2Svc),
http2.NewMemberRegistrationHandler(serviceManager.MemberRegistrationSvc),
http2.NewCustomerHandler(serviceManager.CustomerV2Svc),
http2.NewInProgressOrderHandler(serviceManager.InProgressSvc),
}
for _, handler := range serverRoutes {

View File

@ -83,7 +83,6 @@ func (u *AuthServiceImpl) AuthenticateUser(ctx context.Context, email, password
}
func (u *AuthServiceImpl) SendPasswordResetLink(ctx context.Context, email string) error {
// Check if the user exists
user, err := u.authRepo.CheckExistsUserAccount(ctx, email)
if err != nil {
logger.ContextLogger(ctx).Error("error when getting user", zap.Error(err))

View File

@ -162,7 +162,6 @@ func (s *memberSvc) ResendOTP(
) (*entity.ResendOTPResponse, error) {
logger.ContextLogger(ctx).Info("resending OTP", zap.String("token", token))
// Get registration by token
registration, err := s.repo.GetRegistrationByToken(ctx, token)
if err != nil {
logger.ContextLogger(ctx).Error("failed to get registration", zap.Error(err))
@ -211,7 +210,7 @@ func (s *memberSvc) sendRegistrationOTP(
Recipient: registration.Email,
Subject: "Enaklo - Registration Verification Code",
TemplateName: "member_registration_otp",
TemplatePath: "/templates/member_registration_otp.html",
TemplatePath: "templates/member_registration_otp.html",
Data: emailData,
})

View File

@ -240,32 +240,6 @@ func (s *OrderService) Execute(ctx mycontext.Context, req *entity.OrderExecuteRe
Order: order,
}
if order.PaymentType != "CASH" {
if order.PaymentType == "VA" {
paymentResponse, err := s.processVAPayment(ctx, order, partnerID, req.CreatedBy)
if err != nil {
return nil, err
}
resp.VirtualAccount = paymentResponse.VirtualAccountNumber
resp.BankName = paymentResponse.BankName
resp.BankCode = paymentResponse.BankCode
}
if order.PaymentType == "QRIS" {
paymentResponse, err := s.processQRPayment(ctx, order, partnerID, req.CreatedBy)
if err != nil {
return nil, err
}
resp.QRCode = paymentResponse.QRCodeURL
} else {
paymentResponse, err := s.processNonCashPayment(ctx, order, partnerID, req.CreatedBy)
if err != nil {
return nil, err
}
resp.PaymentToken = paymentResponse.Token
resp.RedirectURL = paymentResponse.RedirectURL
}
}
order.SetExecutePaymentStatus()
order, err = s.repo.Update(ctx, order)
if err != nil {

View File

@ -16,6 +16,7 @@ import (
"enaklo-pos-be/internal/services/transaction"
"enaklo-pos-be/internal/services/users"
customerSvc "enaklo-pos-be/internal/services/v2/customer"
"enaklo-pos-be/internal/services/v2/inprogress_order"
orderSvc "enaklo-pos-be/internal/services/v2/order"
productSvc "enaklo-pos-be/internal/services/v2/product"
@ -47,12 +48,14 @@ type ServiceManagerImpl struct {
CustomerV2Svc customerSvc.Service
ProductV2Svc productSvc.Service
MemberRegistrationSvc member.RegistrationService
InProgressSvc inprogress_order.InProgressOrderService
}
func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl) *ServiceManagerImpl {
custSvcV2 := customerSvc.New(repo.CustomerRepo)
custSvcV2 := customerSvc.New(repo.CustomerRepo, repo.EmailService)
productSvcV2 := productSvc.New(repo.ProductRepo)
inprogressOrder := inprogress_order.NewInProgressOrderService(repo.InProgressOrderRepo)
return &ServiceManagerImpl{
AuthSvc: auth.New(repo.Auth, repo.Crypto, repo.User, repo.EmailService, cfg.Email, repo.Trx, repo.License),
@ -72,6 +75,7 @@ func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl)
OrderV2Svc: orderSvc.New(repo.OrderRepo, productSvcV2, custSvcV2, repo.TransactionRepo, repo.Crypto, &cfg.Order, repo.EmailService),
MemberRegistrationSvc: member.NewMemberRegistrationService(repo.MemberRepository, repo.EmailService, custSvcV2),
CustomerV2Svc: custSvcV2,
InProgressSvc: inprogressOrder,
}
}

View File

@ -1,6 +1,8 @@
package customer
import (
"context"
errors2 "enaklo-pos-be/internal/common/errors"
"enaklo-pos-be/internal/common/logger"
"enaklo-pos-be/internal/common/mycontext"
"enaklo-pos-be/internal/constants"
@ -8,7 +10,9 @@ import (
"enaklo-pos-be/internal/utils"
"github.com/pkg/errors"
"go.uber.org/zap"
"log"
"strings"
"time"
)
type Repository interface {
@ -16,26 +20,35 @@ type Repository interface {
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
AddPoints(ctx mycontext.Context, id int64, points int, reference string) error
FindSequence(ctx mycontext.Context, partnerID int64) (int64, error)
GetAllCustomers(ctx mycontext.Context, req entity.MemberSearch) (entity.MemberList, int, error)
VerifyOTP(ctx mycontext.Context, verificationHash string, otpCode string) (int64, error)
}
type Service interface {
ResolveCustomer(ctx mycontext.Context, req *entity.CustomerResolutionRequest) (int64, error)
AddPoints(ctx mycontext.Context, customerID int64, points int) error
AddPoints(ctx mycontext.Context, customerID int64, points int, reference string) error
GetCustomer(ctx mycontext.Context, id int64) (*entity.Customer, error)
CustomerCheck(ctx mycontext.Context, req *entity.CustomerResolutionRequest) (*entity.CustomerCheckResponse, error)
GetAllCustomers(ctx mycontext.Context, req *entity.MemberSearch) (*entity.MemberList, int, error)
RegistrationMember(ctx mycontext.Context, req *entity.Customer) (*entity.Customer, error)
VerifyOTP(ctx mycontext.Context, verificationID, otpCode string) error
}
type EmailService interface {
SendEmailTransactional(ctx context.Context, param entity.SendEmailNotificationParam) error
}
type customerSvc struct {
repo Repository
notification EmailService
}
func New(repo Repository) Service {
func New(repo Repository, notification EmailService) Service {
return &customerSvc{
repo: repo,
notification: notification,
}
}
@ -106,12 +119,68 @@ func (s *customerSvc) ResolveCustomer(ctx mycontext.Context, req *entity.Custome
return customer.ID, nil
}
func (s *customerSvc) AddPoints(ctx mycontext.Context, customerID int64, points int) error {
func (s *customerSvc) RegistrationMember(ctx mycontext.Context, req *entity.Customer) (*entity.Customer, error) {
if req.Email == "" && req.PhoneNumber == "" {
return nil, errors2.ErrorPhoneNumberEmailIsRequired
}
if req.PhoneNumber != "" {
customer, err := s.repo.FindByPhone(ctx, req.PhoneNumber)
if err != nil && !strings.Contains(err.Error(), "not found") {
return nil, errors2.ErrorInternalServer
}
if customer != nil {
return nil, errors2.ErrorPhoneNumberIsAlreadyRegistered
}
}
if req.Email != "" {
customer, err := s.repo.FindByEmail(ctx, req.Email)
if err != nil && !strings.Contains(err.Error(), "not found") {
return nil, errors2.ErrorInternalServer
}
if customer != nil {
return nil, errors2.ErrorEmailIsAlreadyRegistered
}
}
newCustomer := &entity.Customer{
Name: req.Name,
Email: req.Email,
Phone: req.PhoneNumber,
CreatedAt: constants.TimeNow(),
UpdatedAt: constants.TimeNow(),
BirthDate: req.BirthDate,
Password: req.HashedPassword(),
}
customer, err := s.repo.Create(ctx, newCustomer)
if err != nil {
logger.ContextLogger(ctx).Error("failed to create customer", zap.Error(err))
return nil, errors2.ErrorInternalServer
}
errs := s.sendRegistrationOTP(ctx, &entity.MemberRegistration{
Name: customer.Name,
Email: customer.Email,
OTP: customer.OTP,
})
if err != nil {
logger.ContextLogger(ctx).Error("failed to send OTP", zap.Error(errs))
}
return customer, nil
}
func (s *customerSvc) AddPoints(ctx mycontext.Context, customerID int64, points int, reference string) error {
if points <= 0 {
return nil
}
err := s.repo.AddPoints(ctx, customerID, points)
err := s.repo.AddPoints(ctx, customerID, points, reference)
if err != nil {
return errors.Wrap(err, "failed to add points to customer")
}
@ -202,3 +271,75 @@ func (s *customerSvc) GetAllCustomers(ctx mycontext.Context, req *entity.MemberS
return &customers, totalCount, nil
}
func (s *customerSvc) sendRegistrationOTP(
ctx mycontext.Context,
registration *entity.MemberRegistration,
) error {
emailData := map[string]interface{}{
"UserName": registration.Name,
"OTPCode": registration.OTP,
}
err := s.notification.SendEmailTransactional(ctx, entity.SendEmailNotificationParam{
Sender: "noreply@enaklo.co.id",
Recipient: registration.Email,
Subject: "Enaklo - Registration Verification Code",
TemplateName: "member_registration_otp",
TemplatePath: "templates/member_registration_otp.html",
Data: emailData,
})
if err != nil {
return err
}
return nil
}
func (s *customerSvc) VerifyOTP(ctx mycontext.Context, verificationID, otpCode string) error {
customerID, err := s.repo.VerifyOTP(ctx, verificationID, otpCode)
if err != nil {
return errors.Wrap(err, "verification failed")
}
customer, _ := s.repo.FindByID(ctx, customerID)
go func(customer *entity.Customer) {
newCtx := context.Background()
defer func() {
if r := recover(); r != nil {
log.Printf("Recovered from panic in sendWelcomeEmail: %v", r)
}
}()
s.sendWelcomeEmail(newCtx, customer)
}(customer)
return nil
}
func (s *customerSvc) sendWelcomeEmail(
ctx context.Context,
customer *entity.Customer,
) error {
welcomeData := map[string]interface{}{
"UserName": customer.Name,
"MemberID": customer.CustomerID,
"PointsName": "EnakPoint",
"PointsBalance": customer.Points,
"RedeemLink": "https://enaklo.co.id/redeem",
"CurrentDate": time.Now().Format("01-2006"),
}
return s.notification.SendEmailTransactional(ctx, entity.SendEmailNotificationParam{
Sender: "noreply@enaklo.co.id",
Recipient: customer.Email,
Subject: "Welcome to Enaklo Membership Program",
TemplateName: "welcome_member",
TemplatePath: "templates/welcome_member.html",
Data: welcomeData,
})
}

View File

@ -0,0 +1,51 @@
package inprogress_order
import (
"enaklo-pos-be/internal/common/logger"
"enaklo-pos-be/internal/common/mycontext"
"enaklo-pos-be/internal/entity"
"enaklo-pos-be/internal/repository"
"github.com/pkg/errors"
"go.uber.org/zap"
)
type InProgressOrderService interface {
Save(ctx mycontext.Context, order *entity.InProgressOrder) (*entity.InProgressOrder, error)
GetOrdersByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.InProgressOrder, error)
}
type inProgressOrderSvc struct {
repo repository.InProgressOrderRepository
}
func NewInProgressOrderService(repo repository.InProgressOrderRepository) InProgressOrderService {
return &inProgressOrderSvc{
repo: repo,
}
}
func (s *inProgressOrderSvc) Save(ctx mycontext.Context, order *entity.InProgressOrder) (*entity.InProgressOrder, error) {
createdOrder, err := s.repo.CreateOrUpdate(ctx, order)
if err != nil {
logger.ContextLogger(ctx).Error("failed to create in-progress order",
zap.Error(err),
zap.Int64("partnerID", order.PartnerID))
return nil, errors.Wrap(err, "failed to create in-progress order")
}
return createdOrder, nil
}
func (s *inProgressOrderSvc) GetOrdersByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.InProgressOrder, error) {
orders, err := s.repo.GetListByPartnerID(ctx, partnerID, limit, offset)
if err != nil {
logger.ContextLogger(ctx).Error("failed to get in-progress orders by partner ID",
zap.Error(err),
zap.Int64("partnerID", partnerID),
zap.Int("limit", limit),
zap.Int("offset", offset))
return nil, errors.Wrap(err, "failed to get in-progress orders")
}
return orders, nil
}

View File

@ -51,6 +51,9 @@ func (s *orderSvc) CreateOrderInquiry(ctx mycontext.Context,
req.CustomerName,
req.CustomerPhoneNumber,
req.CustomerEmail,
req.PaymentProvider,
req.TableNumber,
req.OrderType,
)
for _, item := range req.OrderItems {

View File

@ -10,13 +10,14 @@ import (
)
func (s *orderSvc) ExecuteOrderInquiry(ctx mycontext.Context,
token string, paymentMethod string) (*entity.OrderResponse, error) {
token string, paymentMethod, paymentProvider, inprogressOrderID string) (*entity.OrderResponse, error) {
inquiry, err := s.validateInquiry(ctx, token)
if err != nil {
return nil, err
}
order := inquiry.ToOrder(paymentMethod)
order := inquiry.ToOrder(paymentMethod, paymentProvider)
order.InProgressOrderID = inprogressOrderID
savedOrder, err := s.repo.Create(ctx, order)
if err != nil {
@ -51,7 +52,7 @@ func (s *orderSvc) processPostOrderActions(
}
if order.CustomerID != nil && *order.CustomerID > 0 {
err = s.addCustomerPoints(ctx, *order.CustomerID, int(order.Total/1000))
err = s.addCustomerPoints(ctx, *order.CustomerID, int(order.Total/1000), fmt.Sprintf("TRX #%s", trx.ID))
if err != nil {
logger.ContextLogger(ctx).Error("error when adding points", zap.Error(err))
}
@ -78,8 +79,8 @@ func (s *orderSvc) createTransaction(ctx mycontext.Context, order *entity.Order,
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) addCustomerPoints(ctx mycontext.Context, customerID int64, points int, reference string) error {
return s.customer.AddPoints(ctx, customerID, points, reference)
}
func (s *orderSvc) sendTransactionReceipt(ctx mycontext.Context, order *entity.Order, transaction *entity.Transaction, paymentMethod string) error {

View File

@ -21,7 +21,7 @@ type ProductService interface {
type CustomerService interface {
ResolveCustomer(ctx mycontext.Context, req *entity.CustomerResolutionRequest) (int64, error)
AddPoints(ctx mycontext.Context, customerID int64, points int) error
AddPoints(ctx mycontext.Context, customerID int64, points int, reference string) error
GetCustomer(ctx mycontext.Context, id int64) (*entity.Customer, error)
}
@ -42,7 +42,7 @@ type Service interface {
CreateOrderInquiry(ctx mycontext.Context,
req *entity.OrderRequest) (*entity.OrderInquiryResponse, error)
ExecuteOrderInquiry(ctx mycontext.Context,
token string, paymentMethod string) (*entity.OrderResponse, error)
token string, paymentMethod, paymentProvider, inProgressOrderID string) (*entity.OrderResponse, error)
}
type Config interface {

View File

@ -168,7 +168,7 @@
<div class="title">Kode Verifikasi Pendaftaran Member</div>
<div class="text">
Hai {{ .UserName }},<br><br>
Terima kasih telah mendaftar sebagai member Enaklo. Berikan kode verifikasi berikut kepada staf kasir kami untuk menyelesaikan pendaftaran Anda:
Terima kasih telah mendaftar sebagai member Enaklo. Masukan kode verifikasi berikut untuk menyelesaikan pendaftaran Anda:
</div>
<div class="otp-code">{{ .OTPCode }}</div>
<div class="expiry">Kode ini berlaku selama 10 menit</div>