upodate system

This commit is contained in:
aditya.siregar
2025-06-27 13:01:39 +07:00
parent 1201b2e45b
commit f31f83e485
36 changed files with 2400 additions and 689 deletions
+48 -2
View File
@@ -5,9 +5,10 @@ import (
"enaklo-pos-be/internal/handlers/request"
"enaklo-pos-be/internal/handlers/response"
"enaklo-pos-be/internal/services/v2/cashier_session"
"github.com/gin-gonic/gin"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
type CashierSessionHandler struct {
@@ -15,7 +16,9 @@ type CashierSessionHandler struct {
}
func NewCashierSession(service cashier_session.Service) *CashierSessionHandler {
return &CashierSessionHandler{service: service}
return &CashierSessionHandler{
service: service,
}
}
func (h *CashierSessionHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) {
@@ -26,6 +29,7 @@ func (h *CashierSessionHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFun
route.POST("/close/:id", h.CloseSession)
route.GET("/open", h.GetOpenSession)
route.GET("/report/:id", h.GetSessionReport)
route.GET("/history", h.GetSessionHistory)
}
func (h *CashierSessionHandler) OpenSession(c *gin.Context) {
@@ -121,3 +125,45 @@ func (h *CashierSessionHandler) GetSessionReport(c *gin.Context) {
Data: response.MapToCashierSessionReport(report),
})
}
func (h *CashierSessionHandler) GetSessionHistory(c *gin.Context) {
ctx := request.GetMyContext(c)
partnerID := ctx.GetPartnerID()
// Parse query parameters
limitStr := c.DefaultQuery("limit", "10")
offsetStr := c.DefaultQuery("offset", "0")
limit, err := strconv.Atoi(limitStr)
if err != nil || limit < 0 {
limit = 10
}
if limit > 50 {
limit = 50
}
offset, err := strconv.Atoi(offsetStr)
if err != nil || offset < 0 {
offset = 0
}
sessions, total, err := h.service.GetSessionHistory(ctx, *partnerID, limit, offset)
if err != nil {
response.ErrorWrapper(c, err)
return
}
responseData := make([]*response.CashierSessionResponse, len(sessions))
for i, session := range sessions {
responseData[i] = response.MapToCashierSessionResponse(session)
}
pagingMeta := response.NewPaginationHelper().BuildPagingMeta(offset, limit, total)
c.JSON(http.StatusOK, response.BaseResponse{
Success: true,
Status: http.StatusOK,
Data: responseData,
PagingMeta: pagingMeta,
})
}
+1 -1
View File
@@ -27,7 +27,6 @@ func (h *CustomerOrderHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc
route.GET("/history", jwt, h.GetOrderHistory)
route.GET("/detail/:id", jwt, h.GetOrderID)
}
func (h *CustomerOrderHandler) GetOrderHistory(c *gin.Context) {
@@ -99,6 +98,7 @@ func (h *CustomerOrderHandler) GetOrderHistory(c *gin.Context) {
Price: item.Price,
Quantity: item.Quantity,
Subtotal: item.Price * float64(item.Quantity),
Status: item.Status,
})
}
+54 -119
View File
@@ -7,20 +7,23 @@ import (
"enaklo-pos-be/internal/handlers/request"
"enaklo-pos-be/internal/handlers/response"
"enaklo-pos-be/internal/services/v2/order"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
)
type Handler struct {
service order.Service
service order.Service
queryParser *request.QueryParser
}
func NewOrderHandler(service order.Service) *Handler {
return &Handler{
service: service,
service: service,
queryParser: request.NewQueryParser(),
}
}
@@ -39,6 +42,7 @@ func (h *Handler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) {
route.GET("/revenue-overview", jwt, h.GetRevenueOverview)
route.GET("/sales-by-category", jwt, h.GetSalesByCategory)
route.GET("/popular-products", jwt, h.GetPopularProducts)
route.GET("/detail/:id", jwt, h.GetByID)
}
type InquiryRequest struct {
@@ -104,12 +108,10 @@ type VoidItemRequest struct {
}
type SplitBillRequest struct {
OrderID int64 `json:"order_id" validate:"required"`
Type string `json:"type" validate:"required,oneof=ITEM AMOUNT"`
PaymentMethod string `json:"payment_method" validate:"required"`
PaymentProvider string `json:"payment_provider"`
Items []SplitBillItemRequest `json:"items,omitempty" validate:"required_if=Type ITEM,dive"`
Amount float64 `json:"amount,omitempty" validate:"required_if=Type AMOUNT,min=0"`
OrderID int64 `json:"order_id" validate:"required"`
Type string `json:"type" validate:"required,oneof=ITEM AMOUNT"`
Items []SplitBillItemRequest `json:"items,omitempty" validate:"required_if=Type ITEM,dive"`
Amount float64 `json:"amount,omitempty" validate:"required_if=Type AMOUNT,min=0"`
}
type SplitBillItemRequest struct {
@@ -318,7 +320,7 @@ func (h *Handler) Refund(c *gin.Context) {
Reason: req.Reason,
RefundedAt: order.UpdatedAt.Format("2006-01-02T15:04:05Z"),
CustomerName: order.CustomerName,
PaymentType: h.formatPayment(order.PaymentType, order.PaymentProvider),
PaymentType: response.NewPaymentFormatter().Format(order.PaymentType, order.PaymentProvider),
}
c.JSON(http.StatusOK, response.BaseResponse{
@@ -330,116 +332,30 @@ func (h *Handler) Refund(c *gin.Context) {
func (h *Handler) GetOrderHistory(c *gin.Context) {
ctx := request.GetMyContext(c)
partnerID := ctx.GetPartnerID()
limitStr := c.Query("limit")
offsetStr := c.Query("offset")
status := c.Query("status")
startDateStr := c.Query("start_date")
endDateStr := c.Query("end_date")
searchReq := entity.SearchRequest{}
if status != "" {
searchReq.Status = status
}
limit := 20
if limitStr != "" {
parsedLimit, err := strconv.Atoi(limitStr)
if err == nil && parsedLimit > 0 {
limit = parsedLimit
}
}
if limit > 100 {
limit = 100
}
searchReq.Limit = limit
offset := 0
if offsetStr != "" {
parsedOffset, err := strconv.Atoi(offsetStr)
if err == nil && parsedOffset >= 0 {
offset = parsedOffset
}
}
searchReq.Offset = offset
if startDateStr != "" {
startDate, err := time.Parse(time.RFC3339, startDateStr)
if err == nil {
searchReq.Start = startDate
}
}
// Parse end date if provided
if endDateStr != "" {
endDate, err := time.Parse(time.RFC3339, endDateStr)
if err == nil {
searchReq.End = endDate
}
}
orders, total, err := h.service.GetOrderHistory(ctx, *partnerID, searchReq)
searchReq, err := h.queryParser.ParseSearchRequest(c)
if err != nil {
response.ErrorWrapper(c, err)
return
}
responseData := []response.OrderHistoryResponse{}
for _, order := range orders {
var orderItems []response.OrderItemResponse
for _, item := range order.OrderItems {
orderItems = append(orderItems, response.OrderItemResponse{
ProductID: item.ItemID,
ProductName: item.ItemName,
Price: item.Price,
Quantity: item.Quantity,
Subtotal: item.Price * float64(item.Quantity),
})
}
responseData = append(responseData, response.OrderHistoryResponse{
ID: order.ID,
CustomerName: order.CustomerName,
CustomerID: order.CustomerID,
IsMember: order.IsMemberOrder(),
Status: order.Status,
Amount: order.Amount,
Total: order.Total,
PaymentType: h.formatPayment(order.PaymentType, order.PaymentProvider),
TableNumber: order.TableNumber,
OrderType: order.OrderType,
OrderItems: orderItems,
CreatedAt: order.CreatedAt.Format("2006-01-02T15:04:05Z"),
Tax: order.Tax,
})
orders, total, err := h.service.GetOrderHistory(ctx, *searchReq)
if err != nil {
response.ErrorWrapper(c, err)
return
}
responseData := response.MapOrderHistoryResponse(orders)
pagingMeta := response.NewPaginationHelper().BuildPagingMeta(searchReq.Offset, searchReq.Limit, total)
c.JSON(http.StatusOK, response.BaseResponse{
Success: true,
Status: http.StatusOK,
Data: responseData,
PagingMeta: &response.PagingMeta{
Page: offset + 1,
Total: int64(total),
Limit: limit,
},
Success: true,
Status: http.StatusOK,
Data: responseData,
PagingMeta: pagingMeta,
})
}
func (h *Handler) formatPayment(payment, provider string) string {
if payment == "CASH" {
return payment
}
return payment + " " + provider
}
func (h *Handler) GetPaymentMethodAnalysis(c *gin.Context) {
ctx := request.GetMyContext(c)
partnerID := ctx.GetPartnerID()
@@ -501,7 +417,7 @@ func (h *Handler) GetPaymentMethodAnalysis(c *gin.Context) {
paymentBreakdown := make([]PaymentMethodBreakdown, len(paymentAnalysis.PaymentMethodBreakdown))
for i, bd := range paymentAnalysis.PaymentMethodBreakdown {
paymentBreakdown[i] = PaymentMethodBreakdown{
PaymentMethod: h.formatPayment(bd.PaymentType, bd.PaymentProvider),
PaymentMethod: response.NewPaymentFormatter().Format(bd.PaymentType, bd.PaymentProvider),
TotalTransactions: bd.TotalTransactions,
TotalAmount: bd.TotalAmount,
}
@@ -632,7 +548,6 @@ func (h *Handler) GetPopularProducts(c *gin.Context) {
func (h *Handler) GetRefundHistory(c *gin.Context) {
ctx := request.GetMyContext(c)
partnerID := ctx.GetPartnerID()
limitStr := c.Query("limit")
offsetStr := c.Query("offset")
@@ -664,8 +579,6 @@ func (h *Handler) GetRefundHistory(c *gin.Context) {
}
searchReq.Offset = offset
// Set status to REFUNDED to get only refunded orders
searchReq.Status = "REFUNDED"
if startDateStr != "" {
@@ -682,7 +595,7 @@ func (h *Handler) GetRefundHistory(c *gin.Context) {
}
}
orders, total, err := h.service.GetOrderHistory(ctx, *partnerID, searchReq)
orders, total, err := h.service.GetOrderHistory(ctx, searchReq)
if err != nil {
response.ErrorWrapper(c, err)
return
@@ -698,7 +611,7 @@ func (h *Handler) GetRefundHistory(c *gin.Context) {
Status: order.Status,
Amount: order.Amount,
Total: order.Total,
PaymentType: h.formatPayment(order.PaymentType, order.PaymentProvider),
PaymentType: response.NewPaymentFormatter().Format(order.PaymentType, order.PaymentProvider),
TableNumber: order.TableNumber,
OrderType: order.OrderType,
CreatedAt: order.CreatedAt.Format("2006-01-02T15:04:05Z"),
@@ -759,7 +672,6 @@ func (h *Handler) PartialRefund(c *gin.Context) {
return
}
// Calculate refunded amount
refundedAmount := 0.0
var refundedItems []RefundedItemResponse
@@ -789,7 +701,7 @@ func (h *Handler) PartialRefund(c *gin.Context) {
Reason: req.Reason,
RefundedAt: order.UpdatedAt.Format("2006-01-02T15:04:05Z"),
CustomerName: order.CustomerName,
PaymentType: h.formatPayment(order.PaymentType, order.PaymentProvider),
PaymentType: response.NewPaymentFormatter().Format(order.PaymentType, order.PaymentProvider),
RefundedItems: refundedItems,
}
@@ -815,7 +727,6 @@ func (h *Handler) VoidOrder(c *gin.Context) {
return
}
// Convert request items to entity items
var items []entity.VoidItem
if req.Type == "ITEM" {
items = make([]entity.VoidItem, len(req.Items))
@@ -906,7 +817,8 @@ func (h *Handler) SplitBill(c *gin.Context) {
}
}
splitOrder, err := h.service.SplitBillRequest(ctx, *ctx.GetPartnerID(), req.OrderID, req.Type, req.PaymentMethod, req.PaymentProvider, items, req.Amount)
splitOrder, err := h.service.SplitBillRequest(ctx,
*ctx.GetPartnerID(), req.OrderID, req.Type, items, req.Amount)
if err != nil {
response.ErrorWrapper(c, err)
return
@@ -918,3 +830,26 @@ func (h *Handler) SplitBill(c *gin.Context) {
Data: response.MapToOrderResponse(&entity.OrderResponse{Order: splitOrder}),
})
}
func (h *Handler) GetByID(c *gin.Context) {
ctx := request.GetMyContext(c)
partnerID := ctx.GetPartnerID()
orderID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.ErrorWrapper(c, errors.ErrorBadRequest)
return
}
order, err := h.service.GetOrderByIDAndPartnerID(ctx, orderID, *partnerID)
if err != nil {
response.ErrorWrapper(c, err)
return
}
c.JSON(http.StatusOK, response.BaseResponse{
Success: true,
Status: http.StatusOK,
Data: response.MapToOrderResponse(&entity.OrderResponse{Order: order}),
})
}
+2
View File
@@ -3,6 +3,7 @@ package request
import "enaklo-pos-be/internal/entity"
type OpenCashierSessionRequest struct {
PartnerID int64 `json:"partner_id" validate:"required"`
OpeningAmount float64 `json:"opening_amount" validate:"required,gt=0"`
}
@@ -12,6 +13,7 @@ type CloseCashierSessionRequest struct {
func (o *OpenCashierSessionRequest) ToEntity(cashierID int64) *entity.CashierSession {
return &entity.CashierSession{
PartnerID: o.PartnerID,
CashierID: cashierID,
OpeningAmount: o.OpeningAmount,
}
+11 -13
View File
@@ -30,19 +30,17 @@ func (p *ProductParam) ToEntity(partnerID int64) entity.ProductSearch {
}
type Product struct {
ID int64 `json:"id,omitempty"`
PartnerID int64 `json:"partner_id"`
SiteID int64 `json:"site_id"`
Name string `json:"name" validate:"required"`
Type string `json:"type"`
Price float64 `json:"price" validate:"required"`
IsWeekendTicket bool `json:"is_weekend_ticket"`
IsSeasonTicket bool `json:"is_season_ticket"`
Status string `json:"status"`
Description string `json:"description"`
Stock int64 `json:"stock"`
Image string `json:"image"`
CategoryID int64 `json:"category_id"`
ID int64 `json:"id,omitempty"`
PartnerID int64 `json:"partner_id"`
SiteID int64 `json:"site_id"`
Name string `json:"name" validate:"required"`
Type string `json:"type"`
Price float64 `json:"price" validate:"required"`
Status string `json:"status"`
Description string `json:"description"`
Stock int64 `json:"stock"`
Image string `json:"image"`
CategoryID int64 `json:"category_id"`
}
func (e *Product) ToEntity() *entity.Product {
+126
View File
@@ -0,0 +1,126 @@
package request
import (
"enaklo-pos-be/internal/entity"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
"strconv"
"time"
)
type QueryParser struct {
defaultLimit int
maxLimit int
}
func NewQueryParser() *QueryParser {
return &QueryParser{
defaultLimit: 20,
maxLimit: 100,
}
}
func (p *QueryParser) ParseSearchRequest(c *gin.Context) (*entity.SearchRequest, error) {
req := &entity.SearchRequest{}
if status := c.Query("status"); status != "" {
req.Status = status
}
limit, err := p.parseLimit(c.Query("limit"))
if err != nil {
return nil, errors.Wrap(err, "invalid limit parameter")
}
req.Limit = limit
offset, err := p.parseOffset(c.Query("offset"))
if err != nil {
return nil, errors.Wrap(err, "invalid offset parameter")
}
req.Offset = offset
if err := p.parseDateRange(c, req); err != nil {
return nil, errors.Wrap(err, "invalid date parameters")
}
return req, nil
}
func (p *QueryParser) parseLimit(limitStr string) (int, error) {
if limitStr == "" {
return p.defaultLimit, nil
}
limit, err := strconv.Atoi(limitStr)
if err != nil {
return 0, errors.New("limit must be a valid integer")
}
if limit <= 0 {
return p.defaultLimit, nil
}
if limit > p.maxLimit {
limit = p.maxLimit
}
return limit, nil
}
func (p *QueryParser) parseOffset(offsetStr string) (int, error) {
if offsetStr == "" {
return 0, nil
}
offset, err := strconv.Atoi(offsetStr)
if err != nil {
return 0, errors.New("offset must be a valid integer")
}
if offset < 0 {
return 0, nil
}
return offset, nil
}
func (p *QueryParser) parseDateRange(c *gin.Context, req *entity.SearchRequest) error {
if startDateStr := c.Query("start_date"); startDateStr != "" {
startDate, err := p.parseDate(startDateStr)
if err != nil {
return errors.Wrap(err, "invalid start_date format")
}
req.Start = startDate
}
if endDateStr := c.Query("end_date"); endDateStr != "" {
endDate, err := p.parseDate(endDateStr)
if err != nil {
return errors.Wrap(err, "invalid end_date format")
}
req.End = endDate
}
if !req.Start.IsZero() && !req.End.IsZero() && req.Start.After(req.End) {
return errors.New("start_date cannot be after end_date")
}
return nil
}
func (p *QueryParser) parseDate(dateStr string) (time.Time, error) {
formats := []string{
time.RFC3339,
"2006-01-02T15:04:05",
"2006-01-02",
"2006-01-02 15:04:05",
}
for _, format := range formats {
if date, err := time.Parse(format, dateStr); err == nil {
return date, nil
}
}
return time.Time{}, errors.New("unsupported date format")
}
@@ -0,0 +1,43 @@
package validator
import (
"enaklo-pos-be/internal/entity"
"github.com/pkg/errors"
"time"
)
type RequestValidator struct{}
func NewRequestValidator() *RequestValidator {
return &RequestValidator{}
}
func (v *RequestValidator) ValidateSearchRequest(req *entity.SearchRequest) error {
if req.Status != "" {
validStatuses := []string{"pending", "confirmed", "processing", "completed", "cancelled"}
if !v.isValidStatus(req.Status, validStatuses) {
return errors.New("invalid status value")
}
}
if !req.Start.IsZero() && !req.End.IsZero() {
if req.Start.After(req.End) {
return errors.New("start date cannot be after end date")
}
if req.End.Sub(req.Start) > 365*24*time.Hour {
return errors.New("date range cannot exceed 1 year")
}
}
return nil
}
func (v *RequestValidator) isValidStatus(status string, validStatuses []string) bool {
for _, validStatus := range validStatuses {
if status == validStatus {
return true
}
}
return false
}
+2
View File
@@ -7,6 +7,7 @@ import (
type CashierSessionResponse struct {
ID int64 `json:"id"`
PartnerID int64 `json:"partner_id"`
CashierID int64 `json:"cashier_id"`
OpenedAt time.Time `json:"opened_at"`
ClosedAt *time.Time `json:"closed_at,omitempty"`
@@ -35,6 +36,7 @@ func MapToCashierSessionResponse(e *entity.CashierSession) *CashierSessionRespon
return &CashierSessionResponse{
ID: e.ID,
PartnerID: e.PartnerID,
CashierID: e.CashierID,
OpenedAt: e.OpenedAt,
ClosedAt: e.ClosedAt,
+50
View File
@@ -3,6 +3,7 @@ package response
import (
"enaklo-pos-be/internal/constants/order"
"enaklo-pos-be/internal/constants/transaction"
"enaklo-pos-be/internal/entity"
"time"
)
@@ -204,3 +205,52 @@ type OrderHistoryResponse struct {
Tax float64 `json:"tax"`
RestaurantName string `json:"restaurant_name"`
}
func MapOrderHistoryResponse(orders []*entity.Order) []OrderHistoryResponse {
responseData := make([]OrderHistoryResponse, 0, len(orders))
for _, order := range orders {
orderResponse := mapOrderToResponse(order)
responseData = append(responseData, orderResponse)
}
return responseData
}
func mapOrderToResponse(order *entity.Order) OrderHistoryResponse {
paymentFormatter := NewPaymentFormatter()
return OrderHistoryResponse{
ID: order.ID,
CustomerName: order.CustomerName,
CustomerID: order.CustomerID,
IsMember: order.IsMemberOrder(),
Status: order.Status,
Amount: order.Amount,
Total: order.Total,
PaymentType: paymentFormatter.Format(order.PaymentType, order.PaymentProvider),
TableNumber: order.TableNumber,
OrderType: order.OrderType,
OrderItems: mapOrderItems(order.OrderItems),
CreatedAt: order.CreatedAt.Format(time.RFC3339),
Tax: order.Tax,
}
}
func mapOrderItems(items []entity.OrderItem) []OrderItemResponse {
orderItems := make([]OrderItemResponse, 0, len(items))
for _, item := range items {
orderItems = append(orderItems, OrderItemResponse{
OrderItemID: item.ID,
ProductID: item.ItemID,
ProductName: item.ItemName,
Price: item.Price,
Quantity: item.Quantity,
Subtotal: item.Price * float64(item.Quantity),
Notes: item.Notes,
Status: item.Status,
})
}
return orderItems
}
@@ -23,12 +23,14 @@ type OrderInquiryResponse struct {
}
type OrderItemResponse struct {
OrderItemID int64 `json:"order_item_id"`
ProductID int64 `json:"product_id"`
ProductName string `json:"product_name"`
Price float64 `json:"price"`
Quantity int `json:"quantity"`
Subtotal float64 `json:"subtotal"`
Notes string `json:"notes"`
Status string `json:"status"`
}
func mapToOrderItemResponses(items []entity.OrderItem) []OrderItemResponse {
@@ -109,12 +111,14 @@ func MapToOrderItemResponses(items []entity.OrderItem) []OrderItemResponse {
result := make([]OrderItemResponse, 0, len(items))
for _, item := range items {
result = append(result, OrderItemResponse{
OrderItemID: item.ID,
ProductID: item.ItemID,
ProductName: item.ItemName,
Price: item.Price,
Quantity: item.Quantity,
Subtotal: item.Price * float64(item.Quantity),
Notes: item.Notes,
Status: item.Status,
})
}
return result
@@ -0,0 +1,34 @@
package response
type PaginationHelper struct{}
func NewPaginationHelper() *PaginationHelper {
return &PaginationHelper{}
}
func (p *PaginationHelper) BuildPagingMeta(offset, limit int, total int64) *PagingMeta {
page := 1
if limit > 0 {
page = (offset / limit) + 1
}
return &PagingMeta{
Page: page,
Total: total,
Limit: limit,
}
}
func (p *PaginationHelper) calculateTotalPages(total int64, limit int) int {
if limit <= 0 {
return 1
}
return int((total + int64(limit) - 1) / int64(limit))
}
func (p *PaginationHelper) hasNextPage(offset, limit int, total int64) bool {
if limit <= 0 {
return false
}
return int64(offset+limit) < total
}
@@ -0,0 +1,20 @@
package response
import "fmt"
type PaymentFormatter interface {
Format(paymentType, paymentProvider string) string
}
type paymentFormatter struct{}
func NewPaymentFormatter() PaymentFormatter {
return &paymentFormatter{}
}
func (f *paymentFormatter) Format(paymentType, paymentProvider string) string {
if paymentProvider != "" {
return fmt.Sprintf("%s (%s)", paymentType, paymentProvider)
}
return paymentType
}