add order and payment
This commit is contained in:
@@ -13,6 +13,7 @@ type Context interface {
|
||||
|
||||
RequestedBy() int64
|
||||
IsSuperAdmin() bool
|
||||
IsCasheer() bool
|
||||
GetPartnerID() *int64
|
||||
}
|
||||
|
||||
@@ -33,6 +34,10 @@ func (m *MyContextImpl) IsSuperAdmin() bool {
|
||||
return m.roleID == int(role.SuperAdmin)
|
||||
}
|
||||
|
||||
func (m *MyContextImpl) IsCasheer() bool {
|
||||
return m.roleID == int(role.Casheer)
|
||||
}
|
||||
|
||||
func (m *MyContextImpl) GetPartnerID() *int64 {
|
||||
if m.partnerID != 0 {
|
||||
return &m.partnerID
|
||||
|
||||
@@ -24,6 +24,10 @@ func (i *OrderStatus) IsNew() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (i OrderStatus) String() string {
|
||||
return string(i)
|
||||
}
|
||||
|
||||
type ItemType string
|
||||
|
||||
const (
|
||||
|
||||
@@ -10,3 +10,9 @@ type JWTAuthClaims struct {
|
||||
PartnerID int64 `json:"partner_id"`
|
||||
jwt.StandardClaims
|
||||
}
|
||||
|
||||
type JWTOrderClaims struct {
|
||||
PartnerID int64 `json:"id"`
|
||||
OrderID int64 `json:"order_id"`
|
||||
jwt.StandardClaims
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package entity
|
||||
|
||||
type MidtransResponse struct {
|
||||
Token string
|
||||
RedirectURL string
|
||||
}
|
||||
|
||||
type MidtransRequest struct {
|
||||
PaymentReferenceID string
|
||||
TotalAmount int64
|
||||
OrderItems []OrderItem
|
||||
}
|
||||
+53
-142
@@ -1,166 +1,77 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"furtuna-be/internal/constants/order"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Order struct {
|
||||
ID int64
|
||||
BranchID int64
|
||||
BranchName string
|
||||
Status order.OrderStatus
|
||||
Amount float64
|
||||
CustomerName string `gorm:"column:customer_name" json:"customer_name"`
|
||||
CustomerPhone string `gorm:"column:customer_phone" json:"customer_phone"`
|
||||
Pax int `gorm:"column:pax" json:"pax"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
CreatedBy int64
|
||||
UpdatedBy int64
|
||||
Transaction Transaction
|
||||
OrderItem []OrderItem
|
||||
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"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime;column:created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at"`
|
||||
CreatedBy int64 `gorm:"type:int;column:created_by"`
|
||||
PaymentType string `gorm:"type:varchar;column:payment_type"`
|
||||
UpdatedBy int64 `gorm:"type:int;column:updated_by"`
|
||||
OrderItems []OrderItem `gorm:"foreignKey:OrderID;constraint:OnDelete:CASCADE;"`
|
||||
}
|
||||
|
||||
type OrderSearch struct {
|
||||
Search string
|
||||
StatusActive order.OrderSearchStatus
|
||||
Status order.OrderStatus
|
||||
BranchID int64
|
||||
Limit int
|
||||
Offset int
|
||||
type OrderResponse struct {
|
||||
Order *Order
|
||||
Token string
|
||||
}
|
||||
|
||||
type OrderTotalRevenueSearch struct {
|
||||
Year int
|
||||
Month int
|
||||
BranchID int64
|
||||
DateStart *time.Time
|
||||
DateEnd *time.Time
|
||||
type ExecuteOrderResponse struct {
|
||||
Order *Order
|
||||
PaymentToken string
|
||||
RedirectURL string
|
||||
}
|
||||
|
||||
type OrderYearlyRevenueList []OrderYearlyRevenue
|
||||
type OrderYearlyRevenue struct {
|
||||
ItemType string `gorm:"column:item_type"`
|
||||
Month int `gorm:"column:month_number"`
|
||||
Amount float64 `gorm:"column:total_amount"`
|
||||
}
|
||||
|
||||
type OrderBranchRevenueSearch struct {
|
||||
DateStart *time.Time
|
||||
DateEnd *time.Time
|
||||
}
|
||||
|
||||
type OrderBranchRevenueList []OrderBranchRevenue
|
||||
type OrderBranchRevenue struct {
|
||||
BranchID string `gorm:"column:branch_id"`
|
||||
BranchName string `gorm:"column:name"`
|
||||
BranchLocation string `gorm:"column:location"`
|
||||
TotalTransaction int `gorm:"column:total_trans"`
|
||||
TotalAmount float64 `gorm:"column:total_amount"`
|
||||
}
|
||||
|
||||
type OrderList []*OrderDB
|
||||
|
||||
type OrderDB struct {
|
||||
Order
|
||||
}
|
||||
|
||||
func (b *Order) ToOrderDB() *OrderDB {
|
||||
var amount float64
|
||||
for _, i := range b.OrderItem {
|
||||
amount = amount + (i.Price * float64(i.Qty))
|
||||
}
|
||||
b.Amount = amount
|
||||
b.Transaction.Amount = amount
|
||||
|
||||
return &OrderDB{
|
||||
Order: *b,
|
||||
}
|
||||
}
|
||||
|
||||
func (OrderDB) TableName() string {
|
||||
func (Order) TableName() string {
|
||||
return "orders"
|
||||
}
|
||||
|
||||
func (e *OrderDB) ToOrder() *Order {
|
||||
return &Order{
|
||||
ID: e.ID,
|
||||
Status: e.Status,
|
||||
BranchID: e.BranchID,
|
||||
BranchName: e.BranchName,
|
||||
Amount: e.Amount,
|
||||
CustomerName: e.CustomerName,
|
||||
CustomerPhone: e.CustomerPhone,
|
||||
Pax: e.Pax,
|
||||
Transaction: e.Transaction,
|
||||
OrderItem: e.OrderItem,
|
||||
CreatedAt: e.CreatedAt,
|
||||
UpdatedAt: e.UpdatedAt,
|
||||
CreatedBy: e.CreatedBy,
|
||||
UpdatedBy: e.UpdatedBy,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *OrderList) ToOrderList() []*Order {
|
||||
var Orders []*Order
|
||||
for _, order := range *b {
|
||||
Orders = append(Orders, order.ToOrder())
|
||||
}
|
||||
return Orders
|
||||
}
|
||||
|
||||
func (o *OrderDB) ToUpdatedOrder(updatedby int64, req Order) {
|
||||
o.UpdatedBy = updatedby
|
||||
|
||||
if req.Amount > 0 {
|
||||
o.Amount = req.Amount
|
||||
}
|
||||
|
||||
if req.Status != "" {
|
||||
o.Status = req.Status
|
||||
}
|
||||
}
|
||||
|
||||
type OrderItem struct {
|
||||
OrderItemID int64
|
||||
OrderID int64
|
||||
ItemID int64
|
||||
ItemType order.ItemType
|
||||
ItemName string
|
||||
Price float64
|
||||
Qty int64
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
CreatedBy int64
|
||||
UpdatedBy int64
|
||||
ID int64 `gorm:"primaryKey;autoIncrement;column:order_item_id"`
|
||||
OrderID int64 `gorm:"type:int;column:order_id"`
|
||||
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"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime;column:created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at"`
|
||||
CreatedBy int64 `gorm:"type:int;column:created_by"`
|
||||
UpdatedBy int64 `gorm:"type:int;column:updated_by"`
|
||||
}
|
||||
|
||||
type OrderItemDB struct {
|
||||
OrderItem
|
||||
}
|
||||
|
||||
func (b *OrderItem) ToOrderItemDB() *OrderItemDB {
|
||||
return &OrderItemDB{
|
||||
OrderItem: *b,
|
||||
}
|
||||
}
|
||||
|
||||
func (OrderItemDB) TableName() string {
|
||||
func (OrderItem) TableName() string {
|
||||
return "order_items"
|
||||
}
|
||||
|
||||
func (e *OrderItemDB) ToOrderItem() *OrderItem {
|
||||
return &OrderItem{
|
||||
OrderItemID: e.OrderItemID,
|
||||
OrderID: e.OrderID,
|
||||
ItemID: e.ItemID,
|
||||
ItemType: e.ItemType,
|
||||
Price: e.Price,
|
||||
Qty: e.Qty,
|
||||
CreatedAt: e.CreatedAt,
|
||||
UpdatedAt: e.UpdatedAt,
|
||||
CreatedBy: e.CreatedBy,
|
||||
UpdatedBy: e.UpdatedBy,
|
||||
type OrderRequest struct {
|
||||
CreatedBy int64
|
||||
PartnerID int64 `json:"partner_id" validate:"required"`
|
||||
PaymentMethod string `json:"payment_method" validate:"required"`
|
||||
OrderItems []OrderItemRequest `json:"order_items" validate:"required,dive"`
|
||||
}
|
||||
|
||||
type OrderItemRequest struct {
|
||||
ProductID int64 `json:"product_id" validate:"required"`
|
||||
Quantity int64 `json:"quantity" validate:"required"`
|
||||
}
|
||||
|
||||
type OrderExecuteRequest struct {
|
||||
CreatedBy int64
|
||||
PartnerID int64
|
||||
Token string
|
||||
}
|
||||
|
||||
func (o *Order) SetExecutePaymentStatus() {
|
||||
if o.PaymentType == "CASH" {
|
||||
o.Status = "PAID"
|
||||
return
|
||||
}
|
||||
o.Status = "PENDING"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/datatypes"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Payment struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;default:uuid_generate_v4();primaryKey;column:id"`
|
||||
PartnerID string `gorm:"type:varchar;not null;column:partner_id"`
|
||||
OrderID string `gorm:"type:varchar;not null;column:order_id"`
|
||||
ReferenceID string `gorm:"type:varchar;not null;column:reference_id"`
|
||||
Channel string `gorm:"type:varchar;not null;column:channel"`
|
||||
PaymentType string `gorm:"type:varchar;not null;column:payment_type"`
|
||||
Amount float64 `gorm:"type:numeric;not null;column:amount"`
|
||||
State string `gorm:"type:varchar;not null;column:state"`
|
||||
RequestMetadata datatypes.JSON `gorm:"type:json;not null;column:request_metadata"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime;column:created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at"`
|
||||
FinishedAt time.Time `gorm:"column:finished_at"`
|
||||
}
|
||||
|
||||
func (Payment) TableName() string {
|
||||
return "payments"
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
type Transaction struct {
|
||||
ID int64
|
||||
BranchID int64
|
||||
PartnerID int64
|
||||
Status transaction.PaymentStatus
|
||||
Amount float64
|
||||
OrderID int64
|
||||
|
||||
@@ -6,12 +6,9 @@ import (
|
||||
"furtuna-be/internal/handlers/request"
|
||||
"furtuna-be/internal/handlers/response"
|
||||
"furtuna-be/internal/services"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
@@ -21,13 +18,8 @@ type Handler struct {
|
||||
func (h *Handler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) {
|
||||
route := group.Group("/order")
|
||||
|
||||
route.POST("/", jwt, h.Create)
|
||||
route.GET("/list", jwt, h.GetAll)
|
||||
route.GET("/:id", jwt, h.GetByID)
|
||||
route.GET("/total-revenue", jwt, h.GetTotalRevenue)
|
||||
route.GET("/yearly-revenue/:year", jwt, h.GetYearlyRevenue)
|
||||
route.GET("/branch-revenue", jwt, h.GetBranchRevenue)
|
||||
route.PUT("/update-status/:id", jwt, h.UpdateStatus)
|
||||
route.POST("/inquiry", jwt, h.Inquiry)
|
||||
route.POST("/execute", jwt, h.Execute)
|
||||
}
|
||||
|
||||
func NewHandler(service services.Order) *Handler {
|
||||
@@ -36,19 +28,7 @@ func NewHandler(service services.Order) *Handler {
|
||||
}
|
||||
}
|
||||
|
||||
// Create handles the creation of a new order.
|
||||
// @Summary Create a new order
|
||||
// @Description Create a new order with the provided details.
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param Authorization header string true "JWT token"
|
||||
// @Param order body request.Order true "Order details"
|
||||
// @Success 200 {object} response.BaseResponse "Order created successfully"
|
||||
// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request"
|
||||
// @Failure 401 {object} response.BaseResponse{data=errors.Error} "Unauthorized"
|
||||
// @Router /api/v1/order [post]
|
||||
// @Tag Order APIs
|
||||
func (h *Handler) Create(c *gin.Context) {
|
||||
func (h *Handler) Inquiry(c *gin.Context) {
|
||||
ctx := request.GetMyContext(c)
|
||||
|
||||
var req request.Order
|
||||
@@ -57,14 +37,21 @@ func (h *Handler) Create(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
|
||||
}
|
||||
|
||||
err := h.service.Create(ctx, req.ToEntity())
|
||||
|
||||
order, err := h.service.CreateOrder(ctx, req.ToEntity(ctx.RequestedBy()))
|
||||
if err != nil {
|
||||
response.ErrorWrapper(c, err)
|
||||
return
|
||||
@@ -73,122 +60,33 @@ func (h *Handler) Create(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, response.BaseResponse{
|
||||
Success: true,
|
||||
Status: http.StatusOK,
|
||||
Data: MapOrderToCreateOrderResponse(order),
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateStatus handles the update of the order status.
|
||||
// @Summary Update the status of an order
|
||||
// @Description Update the status of the specified order with the provided details.
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param Authorization header string true "JWT token"
|
||||
// @Param id path string true "Order ID"
|
||||
// @Param status body request.UpdateStatus true "Status details"
|
||||
// @Success 200 {object} response.BaseResponse{data=response.Order} "Order status updated successfully"
|
||||
// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request"
|
||||
// @Failure 401 {object} response.BaseResponse{data=errors.Error} "Unauthorized"
|
||||
// @Failure 500 {object} response.BaseResponse "Internal server error"
|
||||
// @Router /api/v1/order/update-status/{id} [put]
|
||||
// @Tag Order APIs
|
||||
func (h *Handler) UpdateStatus(c *gin.Context) {
|
||||
func (h *Handler) Execute(c *gin.Context) {
|
||||
ctx := request.GetMyContext(c)
|
||||
|
||||
var req request.UpdateStatus
|
||||
var req request.Execute
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.ErrorWrapper(c, errors.ErrorBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
id := c.Param("id")
|
||||
|
||||
// Parse the ID into a uint
|
||||
orderID, err := strconv.ParseInt(id, 10, 64)
|
||||
if err != nil {
|
||||
if !ctx.IsCasheer() {
|
||||
response.ErrorWrapper(c, errors.ErrorBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
res, err := h.service.UpdateStatus(ctx, orderID, req.ToEntity())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, response.BaseResponse{
|
||||
Success: false,
|
||||
Status: http.StatusInternalServerError,
|
||||
Message: err.Error(),
|
||||
Data: nil,
|
||||
})
|
||||
req.PartnerID = *ctx.GetPartnerID()
|
||||
|
||||
validate := validator.New()
|
||||
if err := validate.Struct(req); err != nil {
|
||||
response.ErrorWrapper(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response.BaseResponse{
|
||||
Success: true,
|
||||
Status: http.StatusOK,
|
||||
Data: h.toOrderResponse(res),
|
||||
})
|
||||
}
|
||||
|
||||
// GetByID retrieves the details of a specific order by ID.
|
||||
// @Summary Get details of an order by ID
|
||||
// @Description Retrieve the details of the specified order by ID.
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param Authorization header string true "JWT token"
|
||||
// @Param id path string true "Order ID"
|
||||
// @Success 200 {object} response.BaseResponse{data=response.Order} "Order details retrieved successfully"
|
||||
// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request"
|
||||
// @Failure 401 {object} response.BaseResponse{data=errors.Error} "Unauthorized"
|
||||
// @Failure 500 {object} response.BaseResponse "Internal server error"
|
||||
// @Router /api/v1/order/{id} [get]
|
||||
// @Tag Order APIs
|
||||
func (h *Handler) GetByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
// Parse the ID into a uint
|
||||
orderID, err := strconv.ParseInt(id, 10, 64)
|
||||
if err != nil {
|
||||
response.ErrorWrapper(c, errors.ErrorBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
res, err := h.service.GetByID(c.Request.Context(), orderID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, response.BaseResponse{
|
||||
Success: false,
|
||||
Status: http.StatusInternalServerError,
|
||||
Message: err.Error(),
|
||||
Data: nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response.BaseResponse{
|
||||
Success: true,
|
||||
Status: http.StatusOK,
|
||||
Data: h.toOrderResponse(res),
|
||||
})
|
||||
}
|
||||
|
||||
// GetAll retrieves a list of orders based on the specified parameters.
|
||||
// @Summary Get a list of orders
|
||||
// @Description Retrieve a list of orders based on the specified parameters.
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param Authorization header string true "JWT token"
|
||||
// @Param limit query int false "Number of items to retrieve (default: 10)"
|
||||
// @Param offset query int false "Number of items to skip (default: 0)"
|
||||
// @Success 200 {object} response.BaseResponse{data=response.OrderList} "List of orders retrieved successfully"
|
||||
// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request"
|
||||
// @Failure 401 {object} response.BaseResponse{data=errors.Error} "Unauthorized"
|
||||
// @Failure 500 {object} response.BaseResponse "Internal server error"
|
||||
// @Router /api/v1/order/list [get]
|
||||
// @Tag Order APIs
|
||||
func (h *Handler) GetAll(c *gin.Context) {
|
||||
var req request.OrderParam
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
response.ErrorWrapper(c, errors.ErrorBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
orders, total, err := h.service.GetAll(c.Request.Context(), req.ToEntity())
|
||||
order, err := h.service.Execute(ctx, req.ToOrderExecuteRequest(ctx.RequestedBy()))
|
||||
if err != nil {
|
||||
response.ErrorWrapper(c, err)
|
||||
return
|
||||
@@ -197,213 +95,57 @@ func (h *Handler) GetAll(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, response.BaseResponse{
|
||||
Success: true,
|
||||
Status: http.StatusOK,
|
||||
Data: h.toOrderResponseList(orders, int64(total), req),
|
||||
Data: MapOrderToExecuteOrderResponse(order),
|
||||
})
|
||||
}
|
||||
|
||||
// GetTotalRevenue retrieves the total revenue and number of transactions for orders.
|
||||
// @Summary Get total revenue and number of transactions for orders
|
||||
// @Description Retrieve the total revenue and number of transactions for orders based on the specified parameters.
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param Authorization header string true "JWT token"
|
||||
// @Param start_date query string false "Start date for filtering (format: 'YYYY-MM-DD')"
|
||||
// @Param end_date query string false "End date for filtering (format: 'YYYY-MM-DD')"
|
||||
// @Success 200 {object} response.BaseResponse{data=response.OrderMonthlyRevenue} "Total revenue and transactions retrieved successfully"
|
||||
// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request"
|
||||
// @Failure 401 {object} response.BaseResponse{data=errors.Error} "Unauthorized"
|
||||
// @Failure 500 {object} response.BaseResponse "Internal server error"
|
||||
// @Router /api/v1/order/total-revenue [get]
|
||||
// @Tag Order APIs
|
||||
func (h *Handler) GetTotalRevenue(c *gin.Context) {
|
||||
var req request.OrderTotalRevenueParam
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
response.ErrorWrapper(c, errors.ErrorBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
rev, trans, err := h.service.GetTotalRevenue(c.Request.Context(), req.ToEntity())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, response.BaseResponse{
|
||||
Success: false,
|
||||
Status: http.StatusInternalServerError,
|
||||
Message: err.Error(),
|
||||
Data: nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response.BaseResponse{
|
||||
Success: true,
|
||||
Status: http.StatusOK,
|
||||
Data: h.toOrderTotalRevenueResponse(rev, trans),
|
||||
})
|
||||
}
|
||||
|
||||
// GetYearlyRevenue retrieves the yearly revenue for orders.
|
||||
// @Summary Get yearly revenue for orders
|
||||
// @Description Retrieve the yearly revenue for orders based on the specified year.
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param Authorization header string true "JWT token"
|
||||
// @Param year path int true "Year for filtering"
|
||||
// @Success 200 {object} response.BaseResponse{data=map[int]map[string]float64} "Yearly revenue retrieved successfully"
|
||||
// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request"
|
||||
// @Failure 401 {object} response.BaseResponse{data=errors.Error} "Unauthorized"
|
||||
// @Failure 500 {object} response.BaseResponse "Internal server error"
|
||||
// @Router /api/v1/order/yearly-revenue/{year} [get]
|
||||
// @Tag Order APIs
|
||||
func (h *Handler) GetYearlyRevenue(c *gin.Context) {
|
||||
yearParam := c.Param("year")
|
||||
|
||||
// Parse the ID into a uint
|
||||
year, err := strconv.Atoi(yearParam)
|
||||
if err != nil {
|
||||
response.ErrorWrapper(c, errors.ErrorBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
rev, err := h.service.GetYearlyRevenue(c.Request.Context(), year)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, response.BaseResponse{
|
||||
Success: false,
|
||||
Status: http.StatusInternalServerError,
|
||||
Message: err.Error(),
|
||||
Data: nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response.BaseResponse{
|
||||
Success: true,
|
||||
Status: http.StatusOK,
|
||||
Data: h.toOrderYearlyRevenueResponse(rev),
|
||||
})
|
||||
}
|
||||
|
||||
// GetBranchRevenue retrieves the branch-wise revenue for orders.
|
||||
// @Summary Get branch-wise revenue for orders
|
||||
// @Description Retrieve the branch-wise revenue for orders based on the specified parameters.
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param Authorization header string true "JWT token"
|
||||
// @Param branch_id query int false "Branch ID for filtering"
|
||||
// @Param start_date query string false "Start date for filtering (format: 'YYYY-MM-DD')"
|
||||
// @Param end_date query string false "End date for filtering (format: 'YYYY-MM-DD')"
|
||||
// @Success 200 {object} response.BaseResponse{data=[]response.OrderBranchRevenue} "Branch-wise revenue retrieved successfully"
|
||||
// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request"
|
||||
// @Failure 401 {object} response.BaseResponse{data=errors.Error} "Unauthorized"
|
||||
// @Failure 500 {object} response.BaseResponse "Internal server error"
|
||||
// @Router /api/v1/order/branch-revenue [get]
|
||||
// @Tag Order APIs
|
||||
func (h *Handler) GetBranchRevenue(c *gin.Context) {
|
||||
var req request.OrderBranchRevenueParam
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
response.ErrorWrapper(c, errors.ErrorBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
rev, err := h.service.GetBranchRevenue(c.Request.Context(), req.ToEntity())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, response.BaseResponse{
|
||||
Success: false,
|
||||
Status: http.StatusInternalServerError,
|
||||
Message: err.Error(),
|
||||
Data: nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response.BaseResponse{
|
||||
Success: true,
|
||||
Status: http.StatusOK,
|
||||
Data: h.toOrderBranchRevenueResponse(rev),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) toOrderResponse(resp *entity.Order) response.Order {
|
||||
orderItems := []response.OrderItem{}
|
||||
for _, i := range resp.OrderItem {
|
||||
orderItems = append(orderItems, response.OrderItem{
|
||||
OrderItemID: i.OrderItemID,
|
||||
ItemID: i.ItemID,
|
||||
ItemType: i.ItemType,
|
||||
ItemName: i.ItemName,
|
||||
Price: i.Price,
|
||||
Qty: i.Qty,
|
||||
CreatedAt: i.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: i.CreatedAt.Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
return response.Order{
|
||||
ID: resp.ID,
|
||||
BranchID: resp.BranchID,
|
||||
BranchName: resp.BranchName,
|
||||
Amount: resp.Amount,
|
||||
OrderItem: orderItems,
|
||||
Status: resp.Status,
|
||||
CustomerName: resp.CustomerName,
|
||||
CustomerPhone: resp.CustomerPhone,
|
||||
Pax: resp.Pax,
|
||||
PaymentMethod: resp.Transaction.PaymentMethod,
|
||||
CreatedAt: resp.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: resp.CreatedAt.Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) toOrderResponseList(resp []*entity.Order, total int64, req request.OrderParam) response.OrderList {
|
||||
var orders []response.Order
|
||||
for _, b := range resp {
|
||||
orders = append(orders, h.toOrderResponse(b))
|
||||
}
|
||||
|
||||
return response.OrderList{
|
||||
Orders: orders,
|
||||
Total: total,
|
||||
Limit: req.Limit,
|
||||
Offset: req.Offset,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) toOrderTotalRevenueResponse(rev float64, trans int64) response.OrderMonthlyRevenue {
|
||||
return response.OrderMonthlyRevenue{
|
||||
TotalRevenue: rev,
|
||||
TotalTransaction: trans,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) toOrderYearlyRevenueResponse(data entity.OrderYearlyRevenueList) map[int]map[string]float64 {
|
||||
result := make(map[int]map[string]float64)
|
||||
|
||||
// Initialize result map with 0 values for all months and item types
|
||||
for i := 1; i <= 12; i++ {
|
||||
result[i] = map[string]float64{
|
||||
"PRODUCT": 0,
|
||||
"STUDIO": 0,
|
||||
func MapOrderToCreateOrderResponse(orderResponse *entity.OrderResponse) response.CreateOrderResponse {
|
||||
order := orderResponse.Order
|
||||
orderItems := make([]response.CreateOrderItemResponse, len(order.OrderItems))
|
||||
for i, item := range order.OrderItems {
|
||||
orderItems[i] = response.CreateOrderItemResponse{
|
||||
ID: item.ID,
|
||||
ItemID: item.ItemID,
|
||||
Quantity: item.Quantity,
|
||||
Price: item.Price,
|
||||
}
|
||||
}
|
||||
|
||||
// Populate result map with actual data
|
||||
for _, v := range data {
|
||||
result[v.Month][v.ItemType] = v.Amount
|
||||
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,
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (h *Handler) toOrderBranchRevenueResponse(data entity.OrderBranchRevenueList) []response.OrderBranchRevenue {
|
||||
var resp []response.OrderBranchRevenue
|
||||
|
||||
for _, v := range data {
|
||||
resp = append(resp, response.OrderBranchRevenue{
|
||||
BranchID: v.BranchID,
|
||||
BranchName: v.BranchName,
|
||||
BranchLocation: v.BranchLocation,
|
||||
TotalTransaction: v.TotalTransaction,
|
||||
TotalAmount: v.TotalAmount,
|
||||
})
|
||||
func MapOrderToExecuteOrderResponse(orderResponse *entity.ExecuteOrderResponse) response.ExecuteOrderResponse {
|
||||
order := orderResponse.Order
|
||||
orderItems := make([]response.CreateOrderItemResponse, len(order.OrderItems))
|
||||
for i, item := range order.OrderItems {
|
||||
orderItems[i] = response.CreateOrderItemResponse{
|
||||
ID: item.ID,
|
||||
ItemID: item.ItemID,
|
||||
Quantity: item.Quantity,
|
||||
Price: item.Price,
|
||||
}
|
||||
}
|
||||
|
||||
return resp
|
||||
return response.ExecuteOrderResponse{
|
||||
ID: order.ID,
|
||||
RefID: order.RefID,
|
||||
PartnerID: order.PartnerID,
|
||||
Status: order.Status,
|
||||
Amount: order.Amount,
|
||||
PaymentType: order.PaymentType,
|
||||
CreatedAt: order.CreatedAt,
|
||||
OrderItems: orderItems,
|
||||
PaymentToken: orderResponse.PaymentToken,
|
||||
RedirectURL: orderResponse.RedirectURL,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,14 +56,14 @@ func (h *Handler) Create(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
req.PartnerID = ctx.GetPartnerID()
|
||||
if err := req.Validate(); err != nil {
|
||||
response.ErrorWrapper(c, errors.ErrorInvalidRequest)
|
||||
return
|
||||
if !ctx.IsSuperAdmin() {
|
||||
req.PartnerID = ctx.GetPartnerID()
|
||||
if err := req.Validate(); err != nil {
|
||||
response.ErrorWrapper(c, errors.ErrorInvalidRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.IsSuperAdmin()
|
||||
|
||||
res, err := h.service.Create(ctx, req.ToEntity())
|
||||
if err != nil {
|
||||
response.ErrorWrapper(c, err)
|
||||
|
||||
@@ -1,116 +1,47 @@
|
||||
package request
|
||||
|
||||
import (
|
||||
"furtuna-be/internal/constants/order"
|
||||
"furtuna-be/internal/constants/transaction"
|
||||
"furtuna-be/internal/entity"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Order struct {
|
||||
BranchID int64 `json:"branch_id" validate:"required"`
|
||||
Amount float64 `json:"amount" validate:"required"`
|
||||
CustomerName string `json:"customer_name" validate:"required"`
|
||||
CustomerPhone string `json:"customer_phone" validate:"required"`
|
||||
Pax int `json:"pax" validate:"required"`
|
||||
PartnerID int64 `json:"partner_id" validate:"required"`
|
||||
PaymentMethod transaction.PaymentMethod `json:"payment_method" validate:"required"`
|
||||
OrderItem []OrderItem `json:"order_items" validate:"required"`
|
||||
OrderItems []OrderItem `json:"order_items" validate:"required"`
|
||||
}
|
||||
|
||||
type OrderItem struct {
|
||||
ItemID int64 `json:"item_id" validate:"required"`
|
||||
ItemType order.ItemType `json:"item_type" validate:"required"`
|
||||
Price float64 `json:"price" validate:"required"`
|
||||
Qty int64 `json:"qty" validate:"required"`
|
||||
ProductID int64 `json:"product_id" validate:"required"`
|
||||
Quantity int64 `json:"quantity" validate:"required"`
|
||||
}
|
||||
|
||||
type OrderParam struct {
|
||||
Search string `form:"search" json:"search" example:"name,branch_name,item_name"`
|
||||
StatusActive order.OrderSearchStatus `form:"status_active" json:"status_active" example:"active,inactive"`
|
||||
Status order.OrderStatus `form:"status" json:"status" example:"NEW,PAID,CANCEL"`
|
||||
BranchID int64 `form:"branch_id" json:"branch_id" example:"1"`
|
||||
Limit int `form:"limit" json:"limit" example:"10"`
|
||||
Offset int `form:"offset" json:"offset" example:"0"`
|
||||
}
|
||||
|
||||
func (p *OrderParam) ToEntity() entity.OrderSearch {
|
||||
return entity.OrderSearch{
|
||||
Search: p.Search,
|
||||
StatusActive: p.StatusActive,
|
||||
Status: p.Status,
|
||||
BranchID: p.BranchID,
|
||||
Limit: p.Limit,
|
||||
Offset: p.Offset,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Order) ToEntity() *entity.Order {
|
||||
var ordItems []entity.OrderItem
|
||||
if len(o.OrderItem) > 0 {
|
||||
for _, i := range o.OrderItem {
|
||||
ordItems = append(ordItems, entity.OrderItem{
|
||||
ItemID: i.ItemID,
|
||||
ItemType: i.ItemType,
|
||||
Price: i.Price,
|
||||
Qty: i.Qty,
|
||||
})
|
||||
func (o *Order) ToEntity(createdBy int64) *entity.OrderRequest {
|
||||
orderItems := make([]entity.OrderItemRequest, len(o.OrderItems))
|
||||
for i, item := range o.OrderItems {
|
||||
orderItems[i] = entity.OrderItemRequest{
|
||||
ProductID: item.ProductID,
|
||||
Quantity: item.Quantity,
|
||||
}
|
||||
}
|
||||
|
||||
transaction := entity.Transaction{
|
||||
BranchID: o.BranchID,
|
||||
CustomerName: o.CustomerName,
|
||||
CustomerPhone: o.CustomerPhone,
|
||||
PaymentMethod: o.PaymentMethod,
|
||||
}
|
||||
|
||||
return &entity.Order{
|
||||
BranchID: o.BranchID,
|
||||
Amount: o.Amount,
|
||||
CustomerName: o.CustomerName,
|
||||
CustomerPhone: o.CustomerPhone,
|
||||
Pax: o.Pax,
|
||||
Transaction: transaction,
|
||||
OrderItem: ordItems,
|
||||
return &entity.OrderRequest{
|
||||
PartnerID: o.PartnerID,
|
||||
PaymentMethod: string(o.PaymentMethod),
|
||||
OrderItems: orderItems,
|
||||
CreatedBy: createdBy,
|
||||
}
|
||||
}
|
||||
|
||||
type OrderTotalRevenueParam struct {
|
||||
Year int `form:"year" json:"year" example:"1,2,3"`
|
||||
Month int `form:"month" json:"month" example:"1,2,3"`
|
||||
BranchID int64 `form:"branch_id" json:"branch_id" example:"1"`
|
||||
DateStart *time.Time `form:"date_start" json:"date_start" example:"2024-01-01" time_format:"2006-1-2"`
|
||||
DateEnd *time.Time `form:"date_end" json:"date_end" example:"2024-01-01" time_format:"2006-1-2"`
|
||||
type Execute struct {
|
||||
PartnerID int64 `json:"partner_id" validate:"required"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
func (p *OrderTotalRevenueParam) ToEntity() entity.OrderTotalRevenueSearch {
|
||||
return entity.OrderTotalRevenueSearch{
|
||||
Year: p.Year,
|
||||
Month: p.Month,
|
||||
BranchID: p.BranchID,
|
||||
DateStart: p.DateStart,
|
||||
DateEnd: p.DateEnd,
|
||||
}
|
||||
}
|
||||
|
||||
type OrderBranchRevenueParam struct {
|
||||
DateStart *time.Time `form:"date_start" json:"date_start" example:"2024-01-01" time_format:"2006-1-2"`
|
||||
DateEnd *time.Time `form:"date_end" json:"date_end" example:"2024-01-01" time_format:"2006-1-2"`
|
||||
}
|
||||
|
||||
func (p *OrderBranchRevenueParam) ToEntity() entity.OrderBranchRevenueSearch {
|
||||
return entity.OrderBranchRevenueSearch{
|
||||
DateStart: p.DateStart,
|
||||
DateEnd: p.DateEnd,
|
||||
}
|
||||
}
|
||||
|
||||
type UpdateStatus struct {
|
||||
Status order.OrderStatus `form:"status" json:"status" example:"NEW,PAID,CANCEL"`
|
||||
}
|
||||
|
||||
func (o *UpdateStatus) ToEntity() *entity.Order {
|
||||
return &entity.Order{
|
||||
Status: o.Status,
|
||||
func (e Execute) ToOrderExecuteRequest(createdBy int64) *entity.OrderExecuteRequest {
|
||||
return &entity.OrderExecuteRequest{
|
||||
CreatedBy: createdBy,
|
||||
PartnerID: e.PartnerID,
|
||||
Token: e.Token,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package response
|
||||
import (
|
||||
"furtuna-be/internal/constants/order"
|
||||
"furtuna-be/internal/constants/transaction"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Order struct {
|
||||
@@ -50,3 +51,35 @@ type OrderBranchRevenue struct {
|
||||
TotalTransaction int `json:"total_trans"`
|
||||
TotalAmount float64 `json:"total_amount"`
|
||||
}
|
||||
|
||||
type CreateOrderResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
RefID string `json:"ref_id"`
|
||||
PartnerID int64 `json:"partner_id"`
|
||||
Status string `json:"status"`
|
||||
Amount float64 `json:"amount"`
|
||||
PaymentType string `json:"payment_type"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
OrderItems []CreateOrderItemResponse `json:"order_items"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
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"`
|
||||
PaymentType string `json:"payment_type"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
OrderItems []CreateOrderItemResponse `json:"order_items"`
|
||||
PaymentToken string `json:"payment_token"`
|
||||
RedirectURL string `json:"redirect_url"`
|
||||
}
|
||||
|
||||
type CreateOrderItemResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
ItemID int64 `json:"item_id"`
|
||||
Quantity int64 `json:"quantity"`
|
||||
Price float64 `json:"price"`
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ func NewCrypto(config CryptoConfig) *CryptoImpl {
|
||||
|
||||
type CryptoConfig interface {
|
||||
AccessTokenSecret() string
|
||||
AccessTokenOrderSecret() string
|
||||
AccessTokenOrderExpiresDate() time.Time
|
||||
AccessTokenExpiresDate() time.Time
|
||||
}
|
||||
|
||||
@@ -88,3 +90,46 @@ func (c *CryptoImpl) ParseAndValidateJWT(tokenString string) (*entity.JWTAuthCla
|
||||
return nil, errors.ErrorUnauthorized
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CryptoImpl) GenerateJWTOrder(order *entity.Order) (string, error) {
|
||||
claims := &entity.JWTOrderClaims{
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
Subject: strconv.FormatInt(order.ID, 10),
|
||||
ExpiresAt: c.Config.AccessTokenOrderExpiresDate().Unix(),
|
||||
IssuedAt: time.Now().Unix(),
|
||||
NotBefore: time.Now().Unix(),
|
||||
},
|
||||
PartnerID: order.PartnerID,
|
||||
OrderID: order.ID,
|
||||
}
|
||||
|
||||
token, err := jwt.
|
||||
NewWithClaims(jwt.SigningMethodHS256, claims).
|
||||
SignedString([]byte(c.Config.AccessTokenOrderSecret()))
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return token, 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 {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return []byte(c.Config.AccessTokenOrderSecret()), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(*entity.JWTOrderClaims)
|
||||
if !ok || !token.Valid {
|
||||
return 0, 0, fmt.Errorf("invalid token %v", token.Header["alg"])
|
||||
}
|
||||
|
||||
return claims.PartnerID, claims.OrderID, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package mdtrns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"furtuna-be/internal/common/logger"
|
||||
"furtuna-be/internal/entity"
|
||||
"log"
|
||||
"strconv"
|
||||
|
||||
"github.com/veritrans/go-midtrans"
|
||||
)
|
||||
|
||||
type MidtransConfig interface {
|
||||
MidtransServerKey() string
|
||||
MidtransClientKey() string
|
||||
MidtranEnvType() int
|
||||
}
|
||||
|
||||
type ClientService struct {
|
||||
client midtrans.Client
|
||||
midtransConfig MidtransConfig
|
||||
}
|
||||
|
||||
func New(midtransConfig MidtransConfig) *ClientService {
|
||||
midclient := midtrans.NewClient()
|
||||
midclient.ServerKey = midtransConfig.MidtransServerKey()
|
||||
midclient.ClientKey = midtransConfig.MidtransClientKey()
|
||||
midclient.APIEnvType = midtrans.EnvironmentType(midtransConfig.MidtranEnvType())
|
||||
|
||||
return &ClientService{
|
||||
client: midclient,
|
||||
midtransConfig: midtransConfig,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ClientService) CreatePayment(order entity.MidtransRequest) (*entity.MidtransResponse, error) {
|
||||
var snapGateway midtrans.SnapGateway
|
||||
snapGateway = midtrans.SnapGateway{
|
||||
Client: c.client,
|
||||
}
|
||||
|
||||
snapReq := &midtrans.SnapReq{
|
||||
EnabledPayments: []midtrans.PaymentType{
|
||||
midtrans.SourceGopay,
|
||||
},
|
||||
TransactionDetails: midtrans.TransactionDetails{
|
||||
OrderID: order.PaymentReferenceID,
|
||||
GrossAmt: order.TotalAmount,
|
||||
},
|
||||
}
|
||||
|
||||
log.Println("GetToken:")
|
||||
snapTokenResp, err := snapGateway.GetToken(snapReq)
|
||||
|
||||
if err != nil {
|
||||
logger.GetLogger().Error(fmt.Sprintf("error when create midtrans payment %v", err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &entity.MidtransResponse{
|
||||
Token: snapTokenResp.Token,
|
||||
RedirectURL: snapTokenResp.RedirectURL,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func (c ClientService) getProductItems(products []entity.OrderItem) []midtrans.ItemDetail {
|
||||
var items []midtrans.ItemDetail
|
||||
|
||||
for _, product := range products {
|
||||
item := midtrans.ItemDetail{
|
||||
ID: strconv.FormatInt(product.ID, 10),
|
||||
Name: product.ItemType,
|
||||
Qty: int32(product.Quantity),
|
||||
Price: int64(product.Price),
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
@@ -2,10 +2,8 @@ package orders
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"furtuna-be/internal/common/logger"
|
||||
"furtuna-be/internal/entity"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -20,284 +18,42 @@ func NewOrderRepository(db *gorm.DB) *OrderRepository {
|
||||
}
|
||||
}
|
||||
|
||||
func (o *OrderRepository) CreateOrder(ctx context.Context, order *entity.OrderDB) (*entity.OrderDB, error) {
|
||||
tx := o.db.Begin()
|
||||
|
||||
if err := tx.Select("branch_id", "status", "customer_name", "customer_phone", "pax", "amount", "created_by").Create(order).Error; err != nil {
|
||||
tx.Rollback()
|
||||
logError(ctx, "creating order", err)
|
||||
func (r *OrderRepository) Create(ctx context.Context, order *entity.Order) (*entity.Order, error) {
|
||||
err := r.db.WithContext(ctx).Create(order).Error
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when creating order", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i, orditem := range order.OrderItem {
|
||||
orderItem := orditem.ToOrderItemDB()
|
||||
orderItem.OrderID = order.ID
|
||||
|
||||
err := tx.Select("order_id", "item_id", "item_type", "price", "qty", "created_by").Create(orderItem).Error
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when create order item", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
order.OrderItem[i] = *orderItem.ToOrderItem()
|
||||
}
|
||||
|
||||
//insert transaction
|
||||
transaction := order.Transaction.ToTransactionDB()
|
||||
transaction.OrderID = order.ID
|
||||
|
||||
if err := tx.Select("branch_id", "status", "amount", "order_id", "payment_method", "customer_name", "customer_phone", "created_by").Create(transaction).Error; err != nil {
|
||||
tx.Rollback()
|
||||
logError(ctx, "creating transaction", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
tx.Rollback()
|
||||
logError(ctx, "committing transaction", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return order, nil
|
||||
}
|
||||
|
||||
func (b *OrderRepository) UpdateOrder(ctx context.Context, order *entity.OrderDB) (*entity.OrderDB, error) {
|
||||
|
||||
if err := b.db.Select("status", "updated_at", "updated_by").Save(order).Error; err != nil {
|
||||
logError(ctx, "update order", err)
|
||||
func (r *OrderRepository) UpdateStatus(ctx context.Context, orderID int64, status string) (*entity.Order, error) {
|
||||
order := new(entity.Order)
|
||||
if err := r.db.WithContext(ctx).First(order, orderID).Error; err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when finding order", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
order.Status = status
|
||||
if err := r.db.WithContext(ctx).Save(order).Error; err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when updating order status", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return order, nil
|
||||
}
|
||||
|
||||
func (b *OrderRepository) GetAllOrders(ctx context.Context, req entity.OrderSearch) (entity.OrderList, int, error) {
|
||||
var orders []*entity.OrderDB
|
||||
var total int64
|
||||
|
||||
query := b.db.Table("orders").
|
||||
Select("orders.id, orders.branch_id, b.name as branch_name, orders.status, orders.amount, orders.created_at, orders.updated_at, oi.order_item_id, oi.order_id, oi.item_id, oi.item_type, COALESCE(p.name, s.name, '') as item_name, oi.price, oi.qty, oi.created_at, oi.updated_at, COALESCE(t.payment_method, ''), COALESCE(orders.customer_name, ''), COALESCE(orders.customer_phone, ''), COALESCE(orders.pax, 0)").
|
||||
Joins("LEFT JOIN order_items oi ON orders.id = oi.order_id").
|
||||
Joins("LEFT JOIN transactions t ON orders.id = t.order_id").
|
||||
Joins("LEFT JOIN products p ON oi.item_id = p.id AND oi.item_type ='PRODUCT' ").
|
||||
Joins("LEFT JOIN studios s ON oi.item_id = s.id AND oi.item_type ='STUDIO' ").
|
||||
Joins("LEFT JOIN branches b ON orders.branch_id = b.id")
|
||||
|
||||
if req.Search != "" {
|
||||
query = query.Where("b.name ILIKE ? or orders.status ILIKE ? or oi.item_type ILIKE ? or p.name ILIKE ? or orders.customer_name ILIKE ? ", "%"+req.Search+"%", "%"+req.Search+"%", "%"+req.Search+"%", "%"+req.Search+"%", "%"+req.Search+"%")
|
||||
}
|
||||
|
||||
if req.Status != "" {
|
||||
query = query.Where("orders.status = ?", req.Status)
|
||||
}
|
||||
|
||||
if req.BranchID > 0 {
|
||||
query = query.Where("orders.branch_id = ?", req.BranchID)
|
||||
}
|
||||
|
||||
if req.StatusActive.IsActive() {
|
||||
query = query.Joins("INNER JOIN (SELECT o.id, oi.qty, o.created_at FROM orders o INNER JOIN order_items oi ON o.id = oi.order_id AND oi.item_type = 'STUDIO' where o.status != 'CANCEL' and CURRENT_TIMESTAMP > o.created_at AND CURRENT_TIMESTAMP < (o.created_at + (oi.qty || ' hours')::interval)) order_active on order_active.id=orders.id")
|
||||
}
|
||||
|
||||
if req.Limit > 0 {
|
||||
query = query.Limit(req.Limit)
|
||||
}
|
||||
|
||||
if req.Offset > 0 {
|
||||
query = query.Offset(req.Offset)
|
||||
}
|
||||
|
||||
query.Order("orders.created_at DESC")
|
||||
|
||||
rows, err := query.Rows()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
ordersMap := make(map[int64]*entity.OrderDB) // Map to store orders by ID
|
||||
for rows.Next() {
|
||||
var ordr entity.OrderDB
|
||||
var oi entity.OrderItem
|
||||
|
||||
err := rows.Scan(&ordr.ID, &ordr.BranchID, &ordr.BranchName, &ordr.Status, &ordr.Amount, &ordr.CreatedAt, &ordr.UpdatedAt,
|
||||
&oi.OrderItemID, &oi.OrderID, &oi.ItemID, &oi.ItemType, &oi.ItemName, &oi.Price, &oi.Qty, &oi.CreatedAt, &oi.UpdatedAt,
|
||||
&ordr.Transaction.PaymentMethod, &ordr.CustomerName, &ordr.CustomerPhone, &ordr.Pax)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("error scanning rows", zap.Error(err))
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if order, ok := ordersMap[ordr.ID]; ok {
|
||||
// Order already exists in map, append OrderItem to existing order
|
||||
order.OrderItem = append(order.OrderItem, oi)
|
||||
} else {
|
||||
// Order doesn't exist in map, create a new OrderDB
|
||||
newOrder := ordr
|
||||
newOrder.OrderItem = []entity.OrderItem{oi}
|
||||
ordersMap[ordr.ID] = &newOrder
|
||||
|
||||
orders = append(orders, &ordr)
|
||||
}
|
||||
}
|
||||
|
||||
// assign value order item
|
||||
for _, v := range orders {
|
||||
v.OrderItem = ordersMap[v.ID].OrderItem
|
||||
}
|
||||
|
||||
//reset limit for count total data
|
||||
query = query.Offset(-1).Limit(-1)
|
||||
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when count orders", zap.Error(err))
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return orders, int(total), nil
|
||||
}
|
||||
|
||||
func (b *OrderRepository) GetOrderByID(ctx context.Context, id int64) (*entity.OrderDB, error) {
|
||||
var orders *entity.OrderDB
|
||||
|
||||
query := b.db.Table("orders").
|
||||
Select("orders.id, orders.branch_id, b.name as branch_name, orders.status, orders.amount, orders.created_at, orders.updated_at, oi.order_item_id, oi.order_id, oi.item_id, oi.item_type, COALESCE(p.name, s.name, '') as item_name, oi.price, oi.qty, oi.created_at, oi.updated_at, t.payment_method, orders.customer_name, orders.customer_phone, orders.pax").
|
||||
Joins("LEFT JOIN order_items oi ON orders.id = oi.order_id").
|
||||
Joins("LEFT JOIN transactions t ON orders.id = t.order_id").
|
||||
Joins("LEFT JOIN products p ON oi.item_id = p.id AND oi.item_type ='PRODUCT' ").
|
||||
Joins("LEFT JOIN studios s ON oi.item_id = s.id AND oi.item_type ='STUDIO' ").
|
||||
Joins("LEFT JOIN branches b ON orders.branch_id = b.id").
|
||||
Where("orders.id = ?", id)
|
||||
|
||||
rows, err := query.Rows()
|
||||
if err != nil {
|
||||
func (r *OrderRepository) FindByID(ctx context.Context, id int64) (*entity.Order, error) {
|
||||
var order entity.Order
|
||||
if err := r.db.WithContext(ctx).Preload("OrderItems").First(&order, id).Error; err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when finding order by ID", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
var ordr entity.OrderDB // Map to store orders by ID
|
||||
for rows.Next() {
|
||||
var oi entity.OrderItem
|
||||
|
||||
err := rows.Scan(&ordr.ID, &ordr.BranchID, &ordr.BranchName, &ordr.Status, &ordr.Amount, &ordr.CreatedAt, &ordr.UpdatedAt,
|
||||
&oi.OrderItemID, &oi.OrderID, &oi.ItemID, &oi.ItemType, &oi.ItemName, &oi.Price, &oi.Qty, &oi.CreatedAt, &oi.UpdatedAt,
|
||||
&ordr.Transaction.PaymentMethod, &ordr.CustomerName, &ordr.CustomerPhone, &ordr.Pax)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("error scanning rows", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ordr.OrderItem = append(ordr.OrderItem, oi)
|
||||
}
|
||||
|
||||
orders = &ordr
|
||||
|
||||
if orders == nil {
|
||||
return nil, fmt.Errorf("order not found")
|
||||
}
|
||||
|
||||
return orders, nil
|
||||
return &order, nil
|
||||
}
|
||||
|
||||
func (b *OrderRepository) GetTotalRevenue(ctx context.Context, req entity.OrderTotalRevenueSearch) (float64, int64, error) {
|
||||
var (
|
||||
totalmonthlyRevenue float64
|
||||
totalmonthlyTrans int64
|
||||
)
|
||||
|
||||
query := b.db.Table("orders").
|
||||
Select("COALESCE(sum(amount),0) as total_amount, COALESCE(count(id),0) as total_transaction").
|
||||
Where("status in ('NEW','PAID') ")
|
||||
|
||||
if req.BranchID > 0 {
|
||||
query = query.Where("branch_id = ?", req.BranchID)
|
||||
}
|
||||
|
||||
if req.Month > 0 {
|
||||
query = query.Where("EXTRACT(MONTH FROM created_at) = ? ", req.Month)
|
||||
}
|
||||
|
||||
if req.Year > 0 {
|
||||
query = query.Where("EXTRACT(YEAR FROM created_at) = ? ", req.Year)
|
||||
}
|
||||
|
||||
if req.DateStart != nil {
|
||||
query = query.Where("created_at >= ? ", req.DateStart)
|
||||
}
|
||||
|
||||
if req.DateEnd != nil {
|
||||
query = query.Where("created_at <= ? ", req.DateEnd)
|
||||
}
|
||||
|
||||
rows, err := query.Rows()
|
||||
if err != nil {
|
||||
return totalmonthlyRevenue, totalmonthlyTrans, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
err := rows.Scan(&totalmonthlyRevenue, &totalmonthlyTrans)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("error scanning rows", zap.Error(err))
|
||||
return totalmonthlyRevenue, totalmonthlyTrans, err
|
||||
}
|
||||
}
|
||||
|
||||
return totalmonthlyRevenue, totalmonthlyTrans, nil
|
||||
}
|
||||
|
||||
func (b *OrderRepository) GetYearlyRevenue(ctx context.Context, year int) (entity.OrderYearlyRevenueList, error) {
|
||||
var result entity.OrderYearlyRevenueList
|
||||
|
||||
err := b.db.Raw(` SELECT
|
||||
oi.item_type,
|
||||
EXTRACT(MONTH FROM o.created_at) AS month_number,
|
||||
SUM(oi.price ) AS total_amount
|
||||
FROM
|
||||
orders o
|
||||
JOIN
|
||||
order_items oi ON o.id = oi.order_id
|
||||
WHERE
|
||||
EXTRACT(YEAR FROM o.created_at) = ?
|
||||
AND o.status IN ('NEW', 'PAID')
|
||||
GROUP BY
|
||||
EXTRACT(MONTH FROM o.created_at),
|
||||
oi.item_type
|
||||
ORDER BY
|
||||
month_number,
|
||||
oi.item_type`, year).Scan(&result).Error
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (b *OrderRepository) GetBranchRevenue(ctx context.Context, req entity.OrderBranchRevenueSearch) (entity.OrderBranchRevenueList, error) {
|
||||
var result entity.OrderBranchRevenueList
|
||||
|
||||
query := b.db.Table("orders o").
|
||||
Joins("JOIN branches ON branches.id = o.branch_id").
|
||||
Select("o.branch_id, branches.name, branches.location, SUM(o.amount) as total_amount, COUNT(o.id) as total_trans").
|
||||
Where("o.status IN ('NEW', 'PAID')").
|
||||
Group("o.branch_id, branches.name, branches.location").
|
||||
Order("total_amount DESC, total_trans DESC")
|
||||
|
||||
if req.DateStart != nil {
|
||||
query = query.Where("o.created_at >= ? ", req.DateStart)
|
||||
}
|
||||
|
||||
if req.DateEnd != nil {
|
||||
query = query.Where("o.created_at <= ? ", req.DateEnd)
|
||||
}
|
||||
|
||||
if err := query.Find(&result).Error; err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when GetBranchRevenue", zap.Error(err))
|
||||
func (r *OrderRepository) Update(ctx context.Context, order *entity.Order) (*entity.Order, error) {
|
||||
if err := r.db.WithContext(ctx).Save(order).Error; err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when updating order", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func logError(ctx context.Context, s string, err error) {
|
||||
panic("unimplemented")
|
||||
return order, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"furtuna-be/internal/common/logger"
|
||||
"furtuna-be/internal/entity"
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type PaymentRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewPaymentRepository(db *gorm.DB) *PaymentRepository {
|
||||
return &PaymentRepository{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *PaymentRepository) Create(ctx context.Context, payment *entity.Payment) (*entity.Payment, error) {
|
||||
payment.ID = uuid.New()
|
||||
if err := r.db.WithContext(ctx).Create(payment).Error; err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when creating payment", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
return payment, nil
|
||||
}
|
||||
|
||||
// Update updates an existing payment record in the database
|
||||
func (r *PaymentRepository) Update(ctx context.Context, payment *entity.Payment) (*entity.Payment, error) {
|
||||
if err := r.db.WithContext(ctx).Save(payment).Error; err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when updating payment", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
return payment, nil
|
||||
}
|
||||
|
||||
// FindByID retrieves a payment record by its ID
|
||||
func (r *PaymentRepository) FindByID(ctx context.Context, id uuid.UUID) (*entity.Payment, error) {
|
||||
payment := new(entity.Payment)
|
||||
if err := r.db.WithContext(ctx).First(payment, id).Error; err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when finding payment by ID", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
return payment, nil
|
||||
}
|
||||
|
||||
func (r *PaymentRepository) FindByOrderAndPartnerID(ctx context.Context, orderID, partnerID int64) (*entity.Payment, error) {
|
||||
payment := new(entity.Payment)
|
||||
orderIDStr := strconv.FormatInt(orderID, 10)
|
||||
partnerIDStr := strconv.FormatInt(partnerID, 10)
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("order_id = ? AND partner_id = ?", orderIDStr, partnerIDStr).
|
||||
First(payment).Error; err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when finding payment by order ID and partner ID", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
return payment, nil
|
||||
}
|
||||
|
||||
// FindByReferenceID retrieves a payment record by its reference ID
|
||||
func (r *PaymentRepository) FindByReferenceID(ctx context.Context, referenceID string) (*entity.Payment, error) {
|
||||
payment := new(entity.Payment)
|
||||
if err := r.db.WithContext(ctx).Where("reference_id = ?", referenceID).First(payment).Error; err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when finding payment by reference ID", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
return payment, nil
|
||||
}
|
||||
@@ -105,3 +105,12 @@ func (b *ProductRepository) DeleteProduct(ctx context.Context, id int64) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *ProductRepository) GetProductsByIDs(ctx context.Context, ids []int64, partnerID int64) ([]*entity.ProductDB, error) {
|
||||
var products []*entity.ProductDB
|
||||
if err := b.db.Where("partner_id = ? AND id IN ?", partnerID, ids).Find(&products).Error; err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when getting products by IDs and partner ID", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
return products, nil
|
||||
}
|
||||
|
||||
@@ -4,9 +4,11 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"furtuna-be/internal/repository/branches"
|
||||
mdtrns "furtuna-be/internal/repository/midtrans"
|
||||
"furtuna-be/internal/repository/orders"
|
||||
"furtuna-be/internal/repository/oss"
|
||||
"furtuna-be/internal/repository/partners"
|
||||
"furtuna-be/internal/repository/payment"
|
||||
"furtuna-be/internal/repository/products"
|
||||
"furtuna-be/internal/repository/sites"
|
||||
"furtuna-be/internal/repository/studios"
|
||||
@@ -24,36 +26,40 @@ import (
|
||||
)
|
||||
|
||||
type RepoManagerImpl struct {
|
||||
Crypto Crypto
|
||||
Auth Auth
|
||||
Event Event
|
||||
User User
|
||||
Branch Branch
|
||||
Studio Studio
|
||||
Product Product
|
||||
Order Order
|
||||
OSS OSSRepository
|
||||
Partner PartnerRepository
|
||||
Site SiteRepository
|
||||
Trx TransactionManager
|
||||
Wallet WalletRepository
|
||||
Crypto Crypto
|
||||
Auth Auth
|
||||
Event Event
|
||||
User User
|
||||
Branch Branch
|
||||
Studio Studio
|
||||
Product Product
|
||||
Order Order
|
||||
OSS OSSRepository
|
||||
Partner PartnerRepository
|
||||
Site SiteRepository
|
||||
Trx TransactionManager
|
||||
Wallet WalletRepository
|
||||
Midtrans Midtrans
|
||||
Payment Payment
|
||||
}
|
||||
|
||||
func NewRepoManagerImpl(db *gorm.DB, cfg *config.Config) *RepoManagerImpl {
|
||||
return &RepoManagerImpl{
|
||||
Crypto: crypto.NewCrypto(cfg.Auth()),
|
||||
Auth: auth.NewAuthRepository(db),
|
||||
Event: event.NewEventRepo(db),
|
||||
User: users.NewUserRepository(db),
|
||||
Branch: branches.NewBranchRepository(db),
|
||||
Studio: studios.NewStudioRepository(db),
|
||||
Product: products.NewProductRepository(db),
|
||||
Order: orders.NewOrderRepository(db),
|
||||
OSS: oss.NewOssRepositoryImpl(cfg.OSSConfig),
|
||||
Partner: partners.NewPartnerRepository(db),
|
||||
Site: sites.NewSiteRepository(db),
|
||||
Trx: trx.NewGormTransactionManager(db),
|
||||
Wallet: repository.NewWalletRepository(db),
|
||||
Crypto: crypto.NewCrypto(cfg.Auth()),
|
||||
Auth: auth.NewAuthRepository(db),
|
||||
Event: event.NewEventRepo(db),
|
||||
User: users.NewUserRepository(db),
|
||||
Branch: branches.NewBranchRepository(db),
|
||||
Studio: studios.NewStudioRepository(db),
|
||||
Product: products.NewProductRepository(db),
|
||||
Order: orders.NewOrderRepository(db),
|
||||
OSS: oss.NewOssRepositoryImpl(cfg.OSSConfig),
|
||||
Partner: partners.NewPartnerRepository(db),
|
||||
Site: sites.NewSiteRepository(db),
|
||||
Trx: trx.NewGormTransactionManager(db),
|
||||
Wallet: repository.NewWalletRepository(db),
|
||||
Midtrans: mdtrns.New(&cfg.Midtrans),
|
||||
Payment: payment.NewPaymentRepository(db),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +79,8 @@ type Crypto interface {
|
||||
CompareHashAndPassword(hash string, password string) bool
|
||||
ValidateWT(tokenString string) (*jwt.Token, error)
|
||||
GenerateJWT(user *entity.User) (string, error)
|
||||
GenerateJWTOrder(order *entity.Order) (string, error)
|
||||
ValidateJWTOrder(tokenString string) (int64, int64, error)
|
||||
ParseAndValidateJWT(token string) (*entity.JWTAuthClaims, error)
|
||||
}
|
||||
|
||||
@@ -106,16 +114,13 @@ type Product interface {
|
||||
GetProductByID(ctx context.Context, id int64) (*entity.ProductDB, error)
|
||||
GetAllProducts(ctx context.Context, req entity.ProductSearch) (entity.ProductList, int, error)
|
||||
DeleteProduct(ctx context.Context, id int64) error
|
||||
GetProductsByIDs(ctx context.Context, ids []int64, partnerID int64) ([]*entity.ProductDB, error)
|
||||
}
|
||||
|
||||
type Order interface {
|
||||
CreateOrder(ctx context.Context, order *entity.OrderDB) (*entity.OrderDB, error)
|
||||
UpdateOrder(ctx context.Context, order *entity.OrderDB) (*entity.OrderDB, error)
|
||||
GetOrderByID(ctx context.Context, id int64) (*entity.OrderDB, error)
|
||||
GetAllOrders(ctx context.Context, req entity.OrderSearch) (entity.OrderList, int, error)
|
||||
GetTotalRevenue(ctx context.Context, req entity.OrderTotalRevenueSearch) (float64, int64, error)
|
||||
GetYearlyRevenue(ctx context.Context, year int) (entity.OrderYearlyRevenueList, error)
|
||||
GetBranchRevenue(ctx context.Context, req entity.OrderBranchRevenueSearch) (entity.OrderBranchRevenueList, error)
|
||||
Create(ctx context.Context, order *entity.Order) (*entity.Order, error)
|
||||
FindByID(ctx context.Context, id int64) (*entity.Order, error)
|
||||
Update(ctx context.Context, order *entity.Order) (*entity.Order, error)
|
||||
}
|
||||
|
||||
type OSSRepository interface {
|
||||
@@ -150,3 +155,13 @@ type TransactionManager interface {
|
||||
type WalletRepository interface {
|
||||
Create(ctx context.Context, tx *gorm.DB, wallet *entity.Wallet) (*entity.Wallet, error)
|
||||
}
|
||||
|
||||
type Midtrans interface {
|
||||
CreatePayment(order entity.MidtransRequest) (*entity.MidtransResponse, error)
|
||||
}
|
||||
|
||||
type Payment interface {
|
||||
Create(ctx context.Context, payment *entity.Payment) (*entity.Payment, error)
|
||||
Update(ctx context.Context, payment *entity.Payment) (*entity.Payment, error)
|
||||
FindByOrderAndPartnerID(ctx context.Context, orderID, partnerID int64) (*entity.Payment, error)
|
||||
}
|
||||
|
||||
+174
-100
@@ -1,147 +1,221 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"furtuna-be/internal/common/logger"
|
||||
"furtuna-be/internal/common/mycontext"
|
||||
"furtuna-be/internal/constants/order"
|
||||
"furtuna-be/internal/constants/transaction"
|
||||
order2 "furtuna-be/internal/constants/order"
|
||||
"furtuna-be/internal/entity"
|
||||
"furtuna-be/internal/repository"
|
||||
|
||||
"furtuna-be/internal/utils/generator"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/net/context"
|
||||
"gorm.io/gorm"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type OrderService struct {
|
||||
repo repository.Order
|
||||
prodRepo repository.Product
|
||||
studioRepo repository.Studio
|
||||
repo repository.Order
|
||||
crypt repository.Crypto
|
||||
product repository.Product
|
||||
midtrans repository.Midtrans
|
||||
payment repository.Payment
|
||||
}
|
||||
|
||||
func NewOrderService(repo repository.Order, prodRepo repository.Product, studioRepo repository.Studio) *OrderService {
|
||||
func NewOrderService(repo repository.Order, product repository.Product, crypt repository.Crypto,
|
||||
midtrans repository.Midtrans, payment repository.Payment) *OrderService {
|
||||
return &OrderService{
|
||||
repo: repo,
|
||||
prodRepo: prodRepo,
|
||||
studioRepo: studioRepo,
|
||||
repo: repo,
|
||||
product: product,
|
||||
crypt: crypt,
|
||||
midtrans: midtrans,
|
||||
payment: payment,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *OrderService) Create(ctx mycontext.Context, req *entity.Order) error {
|
||||
func (s *OrderService) CreateOrder(ctx context.Context, req *entity.OrderRequest) (*entity.OrderResponse, error) {
|
||||
productIDs := make([]int64, len(req.OrderItems))
|
||||
for i, item := range req.OrderItems {
|
||||
productIDs[i] = item.ProductID
|
||||
}
|
||||
|
||||
for i, oi := range req.OrderItem {
|
||||
if oi.ItemType.IsProduct() {
|
||||
item, err := o.prodRepo.GetProductByID(ctx, oi.ItemID)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when get product item", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
products, err := s.product.GetProductsByIDs(ctx, productIDs, req.PartnerID)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when getting products by IDs", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.OrderItem[i].Price = item.Price
|
||||
req.OrderItem[i].CreatedBy = ctx.RequestedBy()
|
||||
} else if oi.ItemType.IsStudio() {
|
||||
//get data studio
|
||||
item, err := o.studioRepo.GetStudioByID(ctx, oi.ItemID)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when get studio item", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
productMap := make(map[int64]*entity.ProductDB)
|
||||
for _, product := range products {
|
||||
productMap[product.ID] = product
|
||||
}
|
||||
|
||||
req.OrderItem[i].Price = item.Price
|
||||
req.OrderItem[i].CreatedBy = ctx.RequestedBy()
|
||||
totalAmount := 0.0
|
||||
for _, item := range req.OrderItems {
|
||||
product, ok := productMap[item.ProductID]
|
||||
if !ok {
|
||||
logger.ContextLogger(ctx).Error("product not found", zap.Int64("productID", item.ProductID))
|
||||
return nil, errors.New("product not found")
|
||||
}
|
||||
|
||||
totalAmount += product.Price * float64(item.Quantity)
|
||||
}
|
||||
|
||||
orderDB := req.ToOrderDB()
|
||||
orderDB.CreatedBy = ctx.RequestedBy()
|
||||
orderDB.Transaction.CreatedBy = ctx.RequestedBy()
|
||||
|
||||
//set status
|
||||
orderDB.Status = order.Paid
|
||||
orderDB.Transaction.Status = transaction.Paid
|
||||
|
||||
orderDB, err := o.repo.CreateOrder(ctx, orderDB)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when create order", zap.Error(err))
|
||||
return err
|
||||
order := &entity.Order{
|
||||
PartnerID: req.PartnerID,
|
||||
RefID: generator.GenerateUUID(),
|
||||
Status: order2.New.String(),
|
||||
Amount: totalAmount,
|
||||
PaymentType: req.PaymentMethod,
|
||||
CreatedBy: req.CreatedBy,
|
||||
OrderItems: []entity.OrderItem{},
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
for _, item := range req.OrderItems {
|
||||
order.OrderItems = append(order.OrderItems, entity.OrderItem{
|
||||
ItemID: item.ProductID,
|
||||
ItemType: productMap[item.ProductID].Type,
|
||||
Price: productMap[item.ProductID].Price,
|
||||
Quantity: item.Quantity,
|
||||
CreatedBy: req.CreatedBy,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *OrderService) GetByID(ctx context.Context, id int64) (*entity.Order, error) {
|
||||
orderDB, err := s.repo.GetOrderByID(ctx, id)
|
||||
order, err = s.repo.Create(ctx, order)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when get order by id", zap.Error(err))
|
||||
logger.ContextLogger(ctx).Error("error when creating order", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return orderDB.ToOrder(), nil
|
||||
}
|
||||
|
||||
func (s *OrderService) GetAll(ctx context.Context, search entity.OrderSearch) ([]*entity.Order, int, error) {
|
||||
orders, total, err := s.repo.GetAllOrders(ctx, search)
|
||||
token, err := s.crypt.GenerateJWTOrder(order)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when get all orders", zap.Error(err))
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return orders.ToOrderList(), total, nil
|
||||
}
|
||||
|
||||
func (s *OrderService) GetTotalRevenue(ctx context.Context, search entity.OrderTotalRevenueSearch) (float64, int64, error) {
|
||||
rev, trans, err := s.repo.GetTotalRevenue(ctx, search)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when get total revenue orders", zap.Error(err))
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
return rev, trans, nil
|
||||
}
|
||||
|
||||
func (s *OrderService) GetYearlyRevenue(ctx context.Context, year int) (entity.OrderYearlyRevenueList, error) {
|
||||
result, err := s.repo.GetYearlyRevenue(ctx, year)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when get yearly revenue orders", zap.Error(err))
|
||||
logger.ContextLogger(ctx).Error("error when create token", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
return &entity.OrderResponse{
|
||||
Order: order,
|
||||
Token: token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *OrderService) GetBranchRevenue(ctx context.Context, req entity.OrderBranchRevenueSearch) (entity.OrderBranchRevenueList, error) {
|
||||
result, err := s.repo.GetBranchRevenue(ctx, req)
|
||||
func (s *OrderService) Execute(ctx context.Context, req *entity.OrderExecuteRequest) (*entity.ExecuteOrderResponse, error) {
|
||||
partnerID, orderID, err := s.crypt.ValidateJWTOrder(req.Token)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when get branch revenue orders", zap.Error(err))
|
||||
logger.ContextLogger(ctx).Error("error when validating JWT order", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *OrderService) UpdateStatus(ctx mycontext.Context, id int64, orderReq *entity.Order) (*entity.Order, error) {
|
||||
existingOrder, err := s.repo.GetOrderByID(ctx, id)
|
||||
order, err := s.repo.FindByID(ctx, orderID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
logger.ContextLogger(ctx).Error("order not found", zap.Int64("orderID", orderID))
|
||||
return nil, errors.New("order not found")
|
||||
}
|
||||
logger.ContextLogger(ctx).Error("error when finding order by ID", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//validate state
|
||||
if !existingOrder.Status.IsNew() {
|
||||
return nil, fmt.Errorf("Invalid order status")
|
||||
}
|
||||
|
||||
if existingOrder.Status == orderReq.Status {
|
||||
return nil, fmt.Errorf("Order status already %s", existingOrder.Status)
|
||||
}
|
||||
|
||||
existingOrder.ToUpdatedOrder(ctx.RequestedBy(), *orderReq)
|
||||
|
||||
_, err = s.repo.UpdateOrder(ctx, existingOrder)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when update user", zap.Error(err))
|
||||
// Check for existing payment to handle idempotency
|
||||
payment, err := s.payment.FindByOrderAndPartnerID(ctx, orderID, partnerID)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
logger.ContextLogger(ctx).Error("error getting payment data from db", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return existingOrder.ToOrder(), nil
|
||||
if payment != nil {
|
||||
return s.createExecuteOrderResponse(order, payment), nil
|
||||
}
|
||||
|
||||
if order.PartnerID != partnerID {
|
||||
logger.ContextLogger(ctx).Error("partner ID mismatch", zap.Int64("orderID", orderID), zap.Int64("tokenPartnerID", partnerID), zap.Int64("orderPartnerID", order.PartnerID))
|
||||
return nil, errors.New("partner ID mismatch")
|
||||
}
|
||||
|
||||
if order.Status != "NEW" {
|
||||
return nil, errors.New("invalid state")
|
||||
}
|
||||
|
||||
resp := &entity.ExecuteOrderResponse{
|
||||
Order: order,
|
||||
}
|
||||
|
||||
if order.PaymentType != "CASH" {
|
||||
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 {
|
||||
logger.ContextLogger(ctx).Error("error when updating order status", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *OrderService) createExecuteOrderResponse(order *entity.Order, payment *entity.Payment) *entity.ExecuteOrderResponse {
|
||||
var metadata map[string]string
|
||||
if err := json.Unmarshal(payment.RequestMetadata, &metadata); err != nil {
|
||||
logger.ContextLogger(context.Background()).Error("error unmarshaling request metadata", zap.Error(err))
|
||||
return &entity.ExecuteOrderResponse{
|
||||
Order: order,
|
||||
}
|
||||
}
|
||||
return &entity.ExecuteOrderResponse{
|
||||
Order: order,
|
||||
PaymentToken: metadata["payment_token"],
|
||||
RedirectURL: metadata["payment_redirect_url"],
|
||||
}
|
||||
}
|
||||
|
||||
func (s *OrderService) processNonCashPayment(ctx context.Context, order *entity.Order, partnerID, createdBy int64) (*entity.MidtransResponse, error) {
|
||||
paymentRequest := entity.MidtransRequest{
|
||||
PaymentReferenceID: generator.GenerateUUIDV4(),
|
||||
TotalAmount: int64(order.Amount),
|
||||
OrderItems: order.OrderItems,
|
||||
}
|
||||
|
||||
paymentResponse, err := s.midtrans.CreatePayment(paymentRequest)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when creating payment", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
requestMetadata, err := json.Marshal(map[string]string{
|
||||
"partner_id": strconv.FormatInt(partnerID, 10),
|
||||
"created_by": strconv.FormatInt(createdBy, 10),
|
||||
"payment_token": paymentResponse.Token,
|
||||
"payment_redirect_url": paymentResponse.RedirectURL,
|
||||
})
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when marshaling request metadata", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
payment := &entity.Payment{
|
||||
PartnerID: strconv.FormatInt(partnerID, 10),
|
||||
OrderID: strconv.FormatInt(order.ID, 10),
|
||||
ReferenceID: paymentRequest.PaymentReferenceID,
|
||||
Channel: "xendit",
|
||||
PaymentType: order.PaymentType,
|
||||
Amount: order.Amount,
|
||||
State: "pending",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
RequestMetadata: requestMetadata,
|
||||
}
|
||||
|
||||
_, err = s.payment.Create(ctx, payment)
|
||||
if err != nil {
|
||||
logger.ContextLogger(ctx).Error("error when creating payment record", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return paymentResponse, nil
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl)
|
||||
BranchSvc: branch.NewBranchService(repo.Branch),
|
||||
StudioSvc: studio.NewStudioService(repo.Studio),
|
||||
ProductSvc: product.NewProductService(repo.Product),
|
||||
OrderSvc: order.NewOrderService(repo.Order, repo.Product, repo.Studio),
|
||||
OrderSvc: order.NewOrderService(repo.Order, repo.Product, repo.Crypto, repo.Midtrans, repo.Payment),
|
||||
OSSSvc: oss.NewOSSService(repo.OSS),
|
||||
PartnerSvc: partner.NewPartnerService(
|
||||
repo.Partner, users.NewUserService(repo.User, repo.Branch), repo.Trx, repo.Wallet),
|
||||
@@ -94,13 +94,8 @@ type Product interface {
|
||||
}
|
||||
|
||||
type Order interface {
|
||||
Create(ctx mycontext.Context, req *entity.Order) error
|
||||
UpdateStatus(ctx mycontext.Context, id int64, orderReq *entity.Order) (*entity.Order, error)
|
||||
GetByID(ctx context.Context, id int64) (*entity.Order, error)
|
||||
GetAll(ctx context.Context, search entity.OrderSearch) ([]*entity.Order, int, error)
|
||||
GetTotalRevenue(ctx context.Context, search entity.OrderTotalRevenueSearch) (float64, int64, error)
|
||||
GetYearlyRevenue(ctx context.Context, year int) (entity.OrderYearlyRevenueList, error)
|
||||
GetBranchRevenue(ctx context.Context, req entity.OrderBranchRevenueSearch) (entity.OrderBranchRevenueList, error)
|
||||
CreateOrder(ctx context.Context, req *entity.OrderRequest) (*entity.OrderResponse, error)
|
||||
Execute(ctx context.Context, req *entity.OrderExecuteRequest) (*entity.ExecuteOrderResponse, error)
|
||||
}
|
||||
|
||||
type OSSService interface {
|
||||
|
||||
@@ -2,6 +2,7 @@ package generator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
uuid2 "github.com/gofrs/uuid"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -34,3 +35,8 @@ func GenerateUUID() string {
|
||||
id := uuid.New()
|
||||
return id.String()
|
||||
}
|
||||
|
||||
func GenerateUUIDV4() string {
|
||||
id, _ := uuid2.NewV4()
|
||||
return id.String()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user