add order and payment

This commit is contained in:
aditya.siregar
2024-06-04 02:59:31 +07:00
parent 8a23e72230
commit 4ae9079ff9
34 changed files with 810 additions and 1172 deletions
@@ -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
+4
View File
@@ -24,6 +24,10 @@ func (i *OrderStatus) IsNew() bool {
return false
}
func (i OrderStatus) String() string {
return string(i)
}
type ItemType string
const (
+6
View File
@@ -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
}
+12
View File
@@ -0,0 +1,12 @@
package entity
type MidtransResponse struct {
Token string
RedirectURL string
}
type MidtransRequest struct {
PaymentReferenceID string
TotalAmount int64
OrderItems []OrderItem
}
+53 -142
View File
@@ -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"
}
+26
View File
@@ -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"
}
+1 -1
View File
@@ -7,7 +7,7 @@ import (
type Transaction struct {
ID int64
BranchID int64
PartnerID int64
Status transaction.PaymentStatus
Amount float64
OrderID int64
+65 -323
View File
@@ -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,
}
}
+6 -6
View File
@@ -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)
+23 -92
View File
@@ -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,
}
}
+33
View File
@@ -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"`
}
+45
View File
@@ -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
}
+81
View File
@@ -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
}
+22 -266
View File
@@ -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
}
+72
View File
@@ -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
}
+9
View File
@@ -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
}
+48 -33
View File
@@ -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
View File
@@ -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
}
+3 -8
View File
@@ -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()
}