Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa037b4d2a | ||
|
|
d38a770ec5 | ||
|
|
015292e830 | ||
|
|
c573b23d76 | ||
|
|
f73a5d533c | ||
|
|
4ea8e32a8e | ||
|
|
06d79046d0 | ||
|
|
8eb19c57ba | ||
|
|
f123de7233 |
@@ -1,7 +1,7 @@
|
||||
server:
|
||||
base-url:
|
||||
local-url:
|
||||
self-order-url: http://localhost:5174
|
||||
self-order-url: http://localhost:5173
|
||||
port: 4000
|
||||
|
||||
jwt:
|
||||
@@ -58,4 +58,4 @@ fonnte:
|
||||
|
||||
fcm:
|
||||
credentials_file: "infra/firebase-service-account.json"
|
||||
project_id: "your-firebase-project-id"
|
||||
project_id: "apskel-pos-v2"
|
||||
+24
-7
@@ -25,11 +25,12 @@ import (
|
||||
)
|
||||
|
||||
type App struct {
|
||||
server *http.Server
|
||||
db *gorm.DB
|
||||
redisClient *redis.Client
|
||||
router *router.Router
|
||||
shutdown chan os.Signal
|
||||
server *http.Server
|
||||
db *gorm.DB
|
||||
redisClient *redis.Client
|
||||
router *router.Router
|
||||
shutdown chan os.Signal
|
||||
omsetScheduler *service.OmsetMilestoneScheduler
|
||||
}
|
||||
|
||||
func NewApp(db *gorm.DB, redisClient *redis.Client) *App {
|
||||
@@ -43,6 +44,14 @@ func NewApp(db *gorm.DB, redisClient *redis.Client) *App {
|
||||
func (a *App) Initialize(cfg *config.Config) error {
|
||||
repos := a.initRepositories()
|
||||
processors := a.initProcessors(cfg, repos)
|
||||
|
||||
// Initialize omset milestone scheduler
|
||||
a.omsetScheduler = service.NewOmsetMilestoneScheduler(
|
||||
repos.organizationRepo,
|
||||
repos.userRepo,
|
||||
processors.notificationProcessor,
|
||||
)
|
||||
|
||||
services := a.initServices(processors, repos, cfg)
|
||||
validators := a.initValidators()
|
||||
middleware := a.initMiddleware(services, cfg)
|
||||
@@ -129,6 +138,11 @@ func (a *App) Initialize(cfg *config.Config) error {
|
||||
}
|
||||
|
||||
func (a *App) Start(port string) error {
|
||||
// Start the omset milestone scheduler (checks every hour)
|
||||
if a.omsetScheduler != nil {
|
||||
a.omsetScheduler.Start(1 * time.Hour)
|
||||
}
|
||||
|
||||
engine := a.router.Init()
|
||||
|
||||
a.server = &http.Server{
|
||||
@@ -164,6 +178,9 @@ func (a *App) Start(port string) error {
|
||||
}
|
||||
|
||||
func (a *App) Shutdown() {
|
||||
if a.omsetScheduler != nil {
|
||||
a.omsetScheduler.Stop()
|
||||
}
|
||||
close(a.shutdown)
|
||||
}
|
||||
|
||||
@@ -409,7 +426,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
||||
productService := service.NewProductService(processors.productProcessor)
|
||||
productVariantService := service.NewProductVariantService(processors.productVariantProcessor)
|
||||
inventoryService := service.NewInventoryService(processors.inventoryProcessor)
|
||||
orderService := service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, nil, processors.orderIngredientTransactionProcessor, *repos.productRecipeRepo, repos.txManager, repos.sessionRepo) // Will be updated after orderIngredientTransactionService is created
|
||||
orderService := service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, nil, processors.orderIngredientTransactionProcessor, *repos.productRecipeRepo, repos.txManager, repos.sessionRepo, processors.notificationProcessor, repos.userRepo) // Will be updated after orderIngredientTransactionService is created
|
||||
paymentMethodService := service.NewPaymentMethodService(processors.paymentMethodProcessor)
|
||||
fileService := service.NewFileServiceImpl(processors.fileProcessor)
|
||||
var customerService service.CustomerService = service.NewCustomerService(processors.customerProcessor)
|
||||
@@ -436,7 +453,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
||||
notificationService := service.NewNotificationService(processors.notificationProcessor)
|
||||
|
||||
// Update order service with order ingredient transaction service
|
||||
orderService = service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, orderIngredientTransactionService, processors.orderIngredientTransactionProcessor, *repos.productRecipeRepo, repos.txManager, repos.sessionRepo)
|
||||
orderService = service.NewOrderServiceImpl(processors.orderProcessor, repos.tableRepo, orderIngredientTransactionService, processors.orderIngredientTransactionProcessor, *repos.productRecipeRepo, repos.txManager, repos.sessionRepo, processors.notificationProcessor, repos.userRepo)
|
||||
|
||||
return &services{
|
||||
userService: service.NewUserService(processors.userProcessor),
|
||||
|
||||
@@ -7,6 +7,7 @@ const (
|
||||
RoleManager UserRole = "manager"
|
||||
RoleCashier UserRole = "cashier"
|
||||
RoleWaiter UserRole = "waiter"
|
||||
RoleOwner UserRole = "owner"
|
||||
)
|
||||
|
||||
func GetAllUserRoles() []UserRole {
|
||||
@@ -15,6 +16,7 @@ func GetAllUserRoles() []UserRole {
|
||||
RoleManager,
|
||||
RoleCashier,
|
||||
RoleWaiter,
|
||||
RoleOwner,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@ import (
|
||||
)
|
||||
|
||||
type PaymentMethodAnalyticsRequest struct {
|
||||
OrganizationID uuid.UUID `form:"organization_id"`
|
||||
OutletID *uuid.UUID `form:"outlet_id,omitempty"`
|
||||
DateFrom string `form:"date_from" validate:"required"`
|
||||
DateTo string `form:"date_to" validate:"required"`
|
||||
GroupBy string `form:"group_by,default=day" validate:"omitempty,oneof=day hour week month"`
|
||||
OrganizationID uuid.UUID `form:"organization_id"`
|
||||
OutletID *string `form:"outlet_id,omitempty"`
|
||||
DateFrom string `form:"date_from" validate:"required"`
|
||||
DateTo string `form:"date_to" validate:"required"`
|
||||
GroupBy string `form:"group_by,default=day" validate:"omitempty,oneof=day hour week month"`
|
||||
}
|
||||
|
||||
// PaymentMethodAnalyticsResponse represents the response for payment method analytics
|
||||
@@ -45,10 +45,10 @@ type PaymentMethodAnalyticsData struct {
|
||||
|
||||
type SalesAnalyticsRequest struct {
|
||||
OrganizationID uuid.UUID
|
||||
OutletID *uuid.UUID `form:"outlet_id,omitempty"`
|
||||
DateFrom string `form:"date_from" validate:"required"`
|
||||
DateTo string `form:"date_to" validate:"required"`
|
||||
GroupBy string `form:"group_by,default=day" validate:"omitempty,oneof=day hour week month"`
|
||||
OutletID *string `form:"outlet_id,omitempty"`
|
||||
DateFrom string `form:"date_from" validate:"required"`
|
||||
DateTo string `form:"date_to" validate:"required"`
|
||||
GroupBy string `form:"group_by,default=day" validate:"omitempty,oneof=day hour week month"`
|
||||
}
|
||||
|
||||
type SalesAnalyticsResponse struct {
|
||||
@@ -86,10 +86,10 @@ type SalesAnalyticsData struct {
|
||||
// ProductAnalyticsRequest represents the request for product analytics
|
||||
type ProductAnalyticsRequest struct {
|
||||
OrganizationID uuid.UUID
|
||||
OutletID *uuid.UUID `form:"outlet_id,omitempty"`
|
||||
DateFrom string `form:"date_from" validate:"required"`
|
||||
DateTo string `form:"date_to" validate:"required"`
|
||||
Limit int `form:"limit,default=1000" validate:"min=1,max=1000"`
|
||||
OutletID *string `form:"outlet_id,omitempty"`
|
||||
DateFrom string `form:"date_from" validate:"required"`
|
||||
DateTo string `form:"date_to" validate:"required"`
|
||||
Limit int `form:"limit,default=1000" validate:"min=1,max=1000"`
|
||||
}
|
||||
|
||||
// ProductAnalyticsResponse represents the response for product analytics
|
||||
@@ -123,9 +123,9 @@ type ProductAnalyticsData struct {
|
||||
// ProductAnalyticsPerCategoryRequest represents the request for product analytics per category
|
||||
type ProductAnalyticsPerCategoryRequest struct {
|
||||
OrganizationID uuid.UUID
|
||||
OutletID *uuid.UUID `form:"outlet_id,omitempty"`
|
||||
DateFrom string `form:"date_from" validate:"required"`
|
||||
DateTo string `form:"date_to" validate:"required"`
|
||||
OutletID *string `form:"outlet_id,omitempty"`
|
||||
DateFrom string `form:"date_from" validate:"required"`
|
||||
DateTo string `form:"date_to" validate:"required"`
|
||||
}
|
||||
|
||||
// ProductAnalyticsPerCategoryResponse represents the response for product analytics per category
|
||||
@@ -152,9 +152,9 @@ type ProductAnalyticsPerCategoryData struct {
|
||||
// DashboardAnalyticsRequest represents the request for dashboard analytics
|
||||
type DashboardAnalyticsRequest struct {
|
||||
OrganizationID uuid.UUID
|
||||
OutletID *uuid.UUID `form:"outlet_id,omitempty"`
|
||||
DateFrom string `form:"date_from" validate:"required"`
|
||||
DateTo string `form:"date_to" validate:"required"`
|
||||
OutletID *string `form:"outlet_id,omitempty"`
|
||||
DateFrom string `form:"date_from" validate:"required"`
|
||||
DateTo string `form:"date_to" validate:"required"`
|
||||
}
|
||||
|
||||
// DashboardAnalyticsResponse represents the response for dashboard analytics
|
||||
@@ -182,10 +182,10 @@ type DashboardOverview struct {
|
||||
// ProfitLossAnalyticsRequest represents the request for profit and loss analytics
|
||||
type ProfitLossAnalyticsRequest struct {
|
||||
OrganizationID uuid.UUID
|
||||
OutletID *uuid.UUID `form:"outlet_id,omitempty"`
|
||||
DateFrom string `form:"date_from" validate:"required"`
|
||||
DateTo string `form:"date_to" validate:"required"`
|
||||
GroupBy string `form:"group_by,default=day" validate:"omitempty,oneof=day hour week month"`
|
||||
OutletID *string `form:"outlet_id,omitempty"`
|
||||
DateFrom string `form:"date_from" validate:"required"`
|
||||
DateTo string `form:"date_to" validate:"required"`
|
||||
GroupBy string `form:"group_by,default=day" validate:"omitempty,oneof=day hour week month"`
|
||||
}
|
||||
|
||||
// ProfitLossAnalyticsResponse represents the response for profit and loss analytics
|
||||
|
||||
@@ -80,5 +80,3 @@ type SelfOrderListCategoriesResponse struct {
|
||||
type SelfOrderListOrdersResponse struct {
|
||||
Orders []OrderResponse `json:"orders"`
|
||||
}
|
||||
|
||||
type SelfOrderOrderLineItem = OrderItemResponse
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"apskel-pos-be/internal/util"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type AnalyticsHandler struct {
|
||||
@@ -25,6 +26,17 @@ func NewAnalyticsHandler(
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AnalyticsHandler) resolveOutletID(c *gin.Context, contextOutletID uuid.UUID) *string {
|
||||
if outletIDStr := c.Query("outlet_id"); outletIDStr != "" {
|
||||
return &outletIDStr
|
||||
}
|
||||
if contextOutletID != uuid.Nil {
|
||||
s := contextOutletID.String()
|
||||
return &s
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *AnalyticsHandler) GetPaymentMethodAnalytics(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
@@ -36,7 +48,7 @@ func (h *AnalyticsHandler) GetPaymentMethodAnalytics(c *gin.Context) {
|
||||
}
|
||||
|
||||
req.OrganizationID = contextInfo.OrganizationID
|
||||
req.OutletID = &contextInfo.OutletID
|
||||
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||
modelReq := transformer.PaymentMethodAnalyticsContractToModel(&req)
|
||||
|
||||
response, err := h.analyticsService.GetPaymentMethodAnalytics(ctx, modelReq)
|
||||
@@ -60,7 +72,7 @@ func (h *AnalyticsHandler) GetSalesAnalytics(c *gin.Context) {
|
||||
}
|
||||
|
||||
req.OrganizationID = contextInfo.OrganizationID
|
||||
req.OutletID = &contextInfo.OutletID
|
||||
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||
modelReq := transformer.SalesAnalyticsContractToModel(&req)
|
||||
|
||||
response, err := h.analyticsService.GetSalesAnalytics(ctx, modelReq)
|
||||
@@ -84,7 +96,7 @@ func (h *AnalyticsHandler) GetProductAnalytics(c *gin.Context) {
|
||||
}
|
||||
|
||||
req.OrganizationID = contextInfo.OrganizationID
|
||||
req.OutletID = &contextInfo.OutletID
|
||||
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||
modelReq := transformer.ProductAnalyticsContractToModel(&req)
|
||||
|
||||
response, err := h.analyticsService.GetProductAnalytics(ctx, modelReq)
|
||||
@@ -108,7 +120,7 @@ func (h *AnalyticsHandler) GetProductAnalyticsPerCategory(c *gin.Context) {
|
||||
}
|
||||
|
||||
req.OrganizationID = contextInfo.OrganizationID
|
||||
req.OutletID = &contextInfo.OutletID
|
||||
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||
modelReq := transformer.ProductAnalyticsPerCategoryContractToModel(&req)
|
||||
|
||||
response, err := h.analyticsService.GetProductAnalyticsPerCategory(ctx, modelReq)
|
||||
@@ -132,7 +144,7 @@ func (h *AnalyticsHandler) GetDashboardAnalytics(c *gin.Context) {
|
||||
}
|
||||
|
||||
req.OrganizationID = contextInfo.OrganizationID
|
||||
req.OutletID = &contextInfo.OutletID
|
||||
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||
modelReq := transformer.DashboardAnalyticsContractToModel(&req)
|
||||
|
||||
response, err := h.analyticsService.GetDashboardAnalytics(ctx, modelReq)
|
||||
@@ -156,6 +168,7 @@ func (h *AnalyticsHandler) GetProfitLossAnalytics(c *gin.Context) {
|
||||
}
|
||||
|
||||
req.OrganizationID = contextInfo.OrganizationID
|
||||
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||
modelReq, err := transformer.ProfitLossAnalyticsContractToModel(&req)
|
||||
if err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "AnalyticsHandler::GetProfitLossAnalytics", err.Error())}), "AnalyticsHandler::GetProfitLossAnalytics")
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ReportHandler struct {
|
||||
@@ -19,11 +20,26 @@ func NewReportHandler(reportService service.ReportService, userService UserServi
|
||||
return &ReportHandler{reportService: reportService, userService: userService}
|
||||
}
|
||||
|
||||
func (h *ReportHandler) resolveOutletID(c *gin.Context, contextOutletID uuid.UUID) string {
|
||||
if outletIDStr := c.Query("outlet_id"); outletIDStr != "" {
|
||||
if _, err := uuid.Parse(outletIDStr); err == nil {
|
||||
return outletIDStr
|
||||
}
|
||||
}
|
||||
if pathOutletID := c.Param("outlet_id"); pathOutletID != "" {
|
||||
return pathOutletID
|
||||
}
|
||||
if contextOutletID != uuid.Nil {
|
||||
return contextOutletID.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (h *ReportHandler) GetDailyTransactionReportPDF(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
ci := appcontext.FromGinContext(ctx)
|
||||
|
||||
outletID := c.Param("outlet_id")
|
||||
outletID := h.resolveOutletID(c, ci.OutletID)
|
||||
var dayPtr *time.Time
|
||||
if d := c.Query("date"); d != "" {
|
||||
if t, err := time.Parse("2006-01-02", d); err == nil {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"apskel-pos-be/internal/contract"
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/logger"
|
||||
"apskel-pos-be/internal/mappers"
|
||||
"apskel-pos-be/internal/models"
|
||||
"apskel-pos-be/internal/pkg/tabletoken"
|
||||
"apskel-pos-be/internal/processor"
|
||||
@@ -391,14 +392,15 @@ func (h *SelfOrderHandler) GetOrdersBySession(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
modelOrders := mappers.OrderEntitiesToResponses(orders)
|
||||
contractOrders := make([]contract.OrderResponse, len(modelOrders))
|
||||
for i := range modelOrders {
|
||||
contractOrders[i] = *transformer.OrderModelToContract(&modelOrders[i])
|
||||
}
|
||||
|
||||
resp := &contract.SelfOrderListOrdersResponse{
|
||||
Orders: make([]contract.OrderResponse, 0, len(orders)),
|
||||
Orders: contractOrders,
|
||||
}
|
||||
|
||||
for _, o := range orders {
|
||||
resp.Orders = append(resp.Orders, *transformer.OrderEntityToContract(o))
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(resp), "SelfOrderHandler::GetOrdersBySession")
|
||||
}
|
||||
|
||||
@@ -407,6 +409,7 @@ func (h *SelfOrderHandler) validateCreateOrderRequest(req *contract.SelfOrderCre
|
||||
return fmt.Errorf("session_id is required")
|
||||
}
|
||||
if len(req.OrderItems) == 0 {
|
||||
|
||||
return fmt.Errorf("at least one order item is required")
|
||||
}
|
||||
for i, item := range req.OrderItems {
|
||||
|
||||
@@ -29,6 +29,13 @@ func NewAnalyticsRepositoryImpl(db *gorm.DB) *AnalyticsRepositoryImpl {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *AnalyticsRepositoryImpl) resolveOutletID(query *gorm.DB, outletID *uuid.UUID, column string) *gorm.DB {
|
||||
if outletID != nil {
|
||||
return query.Where(column+" = ?", *outletID)
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
func (r *AnalyticsRepositoryImpl) GetPaymentMethodAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) ([]*entities.PaymentMethodAnalytics, error) {
|
||||
var results []*entities.PaymentMethodAnalytics
|
||||
|
||||
@@ -50,9 +57,7 @@ func (r *AnalyticsRepositoryImpl) GetPaymentMethodAnalytics(ctx context.Context,
|
||||
Where("p.status = ?", entities.PaymentTransactionStatusCompleted).
|
||||
Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo)
|
||||
|
||||
if outletID != nil {
|
||||
query = query.Where("o.outlet_id = ?", *outletID)
|
||||
}
|
||||
query = r.resolveOutletID(query, outletID, "o.outlet_id")
|
||||
|
||||
err := query.
|
||||
Group("pm.id, pm.name, pm.type").
|
||||
@@ -180,9 +185,7 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organ
|
||||
Where("oi.status != ?", entities.OrderItemStatusCancelled).
|
||||
Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo)
|
||||
|
||||
if outletID != nil {
|
||||
query = query.Where("o.outlet_id = ?", *outletID)
|
||||
}
|
||||
query = r.resolveOutletID(query, outletID, "o.outlet_id")
|
||||
|
||||
err := query.
|
||||
Group("p.id, p.name, p.cost, c.id, c.name, c.order, mahpp.hpp_per_unit").
|
||||
@@ -235,9 +238,7 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalyticsPerCategory(ctx context.Con
|
||||
Where("oi.status != ?", entities.OrderItemStatusCancelled).
|
||||
Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo)
|
||||
|
||||
if outletID != nil {
|
||||
query = query.Where("o.outlet_id = ?", *outletID)
|
||||
}
|
||||
query = r.resolveOutletID(query, outletID, "o.outlet_id")
|
||||
|
||||
err := query.
|
||||
Group("c.id, c.name").
|
||||
@@ -267,9 +268,7 @@ func (r *AnalyticsRepositoryImpl) GetDashboardOverview(ctx context.Context, orga
|
||||
Where("o.organization_id = ?", organizationID).
|
||||
Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo)
|
||||
|
||||
if outletID != nil {
|
||||
query = query.Where("o.outlet_id = ?", *outletID)
|
||||
}
|
||||
query = r.resolveOutletID(query, outletID, "o.outlet_id")
|
||||
|
||||
err := query.Scan(&result).Error
|
||||
if err != nil {
|
||||
@@ -320,9 +319,7 @@ func (r *AnalyticsRepositoryImpl) GetProfitLossAnalytics(ctx context.Context, or
|
||||
Where("o.is_void = false AND o.is_refund = false").
|
||||
Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo)
|
||||
|
||||
if outletID != nil {
|
||||
summaryQuery = summaryQuery.Where("o.outlet_id = ?", *outletID)
|
||||
}
|
||||
summaryQuery = r.resolveOutletID(summaryQuery, outletID, "o.outlet_id")
|
||||
|
||||
err := summaryQuery.Scan(&summary).Error
|
||||
if err != nil {
|
||||
@@ -374,9 +371,7 @@ func (r *AnalyticsRepositoryImpl) GetProfitLossAnalytics(ctx context.Context, or
|
||||
Group(timeFormat).
|
||||
Order(timeFormat)
|
||||
|
||||
if outletID != nil {
|
||||
dataQuery = dataQuery.Where("o.outlet_id = ?", *outletID)
|
||||
}
|
||||
dataQuery = r.resolveOutletID(dataQuery, outletID, "o.outlet_id")
|
||||
|
||||
err = dataQuery.Scan(&data).Error
|
||||
if err != nil {
|
||||
@@ -419,9 +414,7 @@ func (r *AnalyticsRepositoryImpl) GetProfitLossAnalytics(ctx context.Context, or
|
||||
Order("p.name ASC").
|
||||
Limit(1000)
|
||||
|
||||
if outletID != nil {
|
||||
productQuery = productQuery.Where("o.outlet_id = ?", *outletID)
|
||||
}
|
||||
productQuery = r.resolveOutletID(productQuery, outletID, "o.outlet_id")
|
||||
|
||||
err = productQuery.Scan(&productData).Error
|
||||
if err != nil {
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type InventoryRepository interface {
|
||||
@@ -278,7 +279,12 @@ func (r *InventoryRepositoryImpl) UpdateReorderLevel(ctx context.Context, id uui
|
||||
}
|
||||
|
||||
func (r *InventoryRepositoryImpl) BulkCreate(ctx context.Context, inventoryItems []*entities.Inventory) error {
|
||||
return r.db.WithContext(ctx).CreateInBatches(inventoryItems, 100).Error
|
||||
return r.db.WithContext(ctx).
|
||||
Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "outlet_id"}, {Name: "product_id"}},
|
||||
DoNothing: true,
|
||||
}).
|
||||
CreateInBatches(inventoryItems, 100).Error
|
||||
}
|
||||
|
||||
func (r *InventoryRepositoryImpl) BulkUpdate(ctx context.Context, inventoryItems []*entities.Inventory) error {
|
||||
@@ -301,21 +307,25 @@ func (r *InventoryRepositoryImpl) BulkAdjustQuantity(ctx context.Context, adjust
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
for productID, delta := range adjustments {
|
||||
var inventory entities.Inventory
|
||||
if err := tx.Where("product_id = ? AND outlet_id = ?", productID, outletID).First(&inventory).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// Inventory doesn't exist, create it with initial quantity
|
||||
inventory = entities.Inventory{
|
||||
ProductID: productID,
|
||||
OutletID: outletID,
|
||||
Quantity: 0,
|
||||
ReorderLevel: 0,
|
||||
}
|
||||
if err := tx.Create(&inventory).Error; err != nil {
|
||||
return fmt.Errorf("failed to create inventory record for product %s: %w", productID, err)
|
||||
}
|
||||
} else {
|
||||
err := tx.Set("gorm:query_option", "FOR UPDATE").
|
||||
Where("product_id = ? AND outlet_id = ?", productID, outletID).
|
||||
First(&inventory).Error
|
||||
if err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
// Use FirstOrCreate to handle race conditions — avoids duplicate key
|
||||
// if another transaction already inserted this row concurrently.
|
||||
inventory = entities.Inventory{
|
||||
ProductID: productID,
|
||||
OutletID: outletID,
|
||||
Quantity: 0,
|
||||
ReorderLevel: 0,
|
||||
}
|
||||
if err := tx.Where(entities.Inventory{ProductID: productID, OutletID: outletID}).
|
||||
FirstOrCreate(&inventory).Error; err != nil {
|
||||
return fmt.Errorf("failed to create inventory record for product %s: %w", productID, err)
|
||||
}
|
||||
}
|
||||
|
||||
inventory.UpdateQuantity(delta)
|
||||
|
||||
@@ -99,3 +99,14 @@ func (r *OrganizationRepositoryImpl) GetByEmail(ctx context.Context, email strin
|
||||
}
|
||||
return &org, nil
|
||||
}
|
||||
|
||||
// GetTotalOmset returns the total revenue from completed orders for an organization.
|
||||
func (r *OrganizationRepositoryImpl) GetTotalOmset(ctx context.Context, organizationID uuid.UUID) (float64, error) {
|
||||
var total float64
|
||||
err := r.db.WithContext(ctx).
|
||||
Table("orders").
|
||||
Where("organization_id = ? AND payment_status = ?", organizationID, "completed").
|
||||
Select("COALESCE(SUM(total_amount), 0)").
|
||||
Scan(&total).Error
|
||||
return total, err
|
||||
}
|
||||
|
||||
@@ -61,6 +61,17 @@ func (r *UserRepositoryImpl) GetActiveUsers(ctx context.Context, organizationID
|
||||
return users, err
|
||||
}
|
||||
|
||||
func (r *UserRepositoryImpl) GetActiveByOutletID(ctx context.Context, organizationID, outletID uuid.UUID) ([]*entities.User, error) {
|
||||
var users []*entities.User
|
||||
err := r.db.WithContext(ctx).
|
||||
Where(
|
||||
"organization_id = ? AND is_active = ? AND (outlet_id = ? OR role IN ?)",
|
||||
organizationID, true, outletID, []string{"admin", "manager"},
|
||||
).
|
||||
Find(&users).Error
|
||||
return users, err
|
||||
}
|
||||
|
||||
func (r *UserRepositoryImpl) Update(ctx context.Context, user *entities.User) error {
|
||||
return r.db.WithContext(ctx).Save(user).Error
|
||||
}
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"apskel-pos-be/internal/constants"
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/models"
|
||||
"apskel-pos-be/internal/processor"
|
||||
"apskel-pos-be/internal/repository"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultCheckInterval = 1 * time.Hour
|
||||
OmsetMillionRupiah = 1_000_000.0
|
||||
)
|
||||
|
||||
// OmsetMilestoneScheduler periodically checks each organization's total omset
|
||||
// and sends a notification to owner/admin users when a milestone is reached.
|
||||
//
|
||||
// NOTE: Milestone tracking is in-memory; notifications may re-trigger after a restart.
|
||||
// For persistent tracking, persist the notified state in the database.
|
||||
type OmsetMilestoneScheduler struct {
|
||||
orgRepo *repository.OrganizationRepositoryImpl
|
||||
userRepo *repository.UserRepositoryImpl
|
||||
notificationProc processor.NotificationProcessor
|
||||
|
||||
mu sync.Mutex
|
||||
notified map[string]bool // "orgID:milestone" -> already notified
|
||||
stopCh chan struct{}
|
||||
}
|
||||
|
||||
func NewOmsetMilestoneScheduler(
|
||||
orgRepo *repository.OrganizationRepositoryImpl,
|
||||
userRepo *repository.UserRepositoryImpl,
|
||||
notificationProc processor.NotificationProcessor,
|
||||
) *OmsetMilestoneScheduler {
|
||||
return &OmsetMilestoneScheduler{
|
||||
orgRepo: orgRepo,
|
||||
userRepo: userRepo,
|
||||
notificationProc: notificationProc,
|
||||
notified: make(map[string]bool),
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Start begins the periodic milestone check in a background goroutine.
|
||||
func (s *OmsetMilestoneScheduler) Start(interval time.Duration) {
|
||||
if interval <= 0 {
|
||||
interval = defaultCheckInterval
|
||||
}
|
||||
|
||||
go func() {
|
||||
// Perform an initial check immediately.
|
||||
s.checkAllOrganizations()
|
||||
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
s.checkAllOrganizations()
|
||||
case <-s.stopCh:
|
||||
log.Println("Omset milestone scheduler stopped")
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
log.Println("Omset milestone scheduler started")
|
||||
}
|
||||
|
||||
// Stop signals the scheduler to stop.
|
||||
func (s *OmsetMilestoneScheduler) Stop() {
|
||||
close(s.stopCh)
|
||||
}
|
||||
|
||||
func (s *OmsetMilestoneScheduler) checkAllOrganizations() {
|
||||
ctx := context.Background()
|
||||
|
||||
orgs, _, err := s.orgRepo.List(ctx, nil, 1000, 0)
|
||||
if err != nil {
|
||||
log.Printf("OmsetMilestoneScheduler: failed to list organizations: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, org := range orgs {
|
||||
s.checkOrganization(ctx, org)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *OmsetMilestoneScheduler) checkOrganization(ctx context.Context, org *entities.Organization) {
|
||||
totalOmset, err := s.orgRepo.GetTotalOmset(ctx, org.ID)
|
||||
if err != nil {
|
||||
log.Printf("OmsetMilestoneScheduler: failed to get total omset for org %s: %v", org.ID, err)
|
||||
return
|
||||
}
|
||||
|
||||
milestones := []float64{OmsetMillionRupiah}
|
||||
|
||||
for _, milestone := range milestones {
|
||||
if totalOmset < milestone {
|
||||
continue
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%s:%.0f", org.ID.String(), milestone)
|
||||
|
||||
s.mu.Lock()
|
||||
if s.notified[key] {
|
||||
s.mu.Unlock()
|
||||
continue
|
||||
}
|
||||
s.notified[key] = true
|
||||
s.mu.Unlock()
|
||||
|
||||
s.sendMilestoneNotification(ctx, org, totalOmset, milestone)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *OmsetMilestoneScheduler) sendMilestoneNotification(ctx context.Context, org *entities.Organization, totalOmset float64, milestone float64) {
|
||||
users, err := s.userRepo.GetByOrganizationID(ctx, org.ID)
|
||||
if err != nil {
|
||||
log.Printf("OmsetMilestoneScheduler: failed to get users for org %s: %v", org.ID, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Notify owner and admin users.
|
||||
var receiverIDs []uuid.UUID
|
||||
for _, user := range users {
|
||||
roleStr := string(user.Role)
|
||||
if roleStr == string(constants.RoleOwner) || roleStr == string(constants.RoleAdmin) {
|
||||
receiverIDs = append(receiverIDs, user.ID)
|
||||
}
|
||||
}
|
||||
|
||||
if len(receiverIDs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
orgID := org.ID
|
||||
title := "🎉 Selamat! Omset Telah Mencapai 1 Juta Rupiah"
|
||||
body := fmt.Sprintf("Organisasi %s telah mencapai omset Rp %.0f. Terus tingkatkan prestasinya!", org.Name, totalOmset)
|
||||
|
||||
notifReq := &models.SendNotificationRequest{
|
||||
Title: title,
|
||||
Body: body,
|
||||
Type: "milestone",
|
||||
Category: "omset_milestone",
|
||||
NotifiableType: "organization",
|
||||
NotifiableID: &orgID,
|
||||
ReceiverIDs: receiverIDs,
|
||||
Data: map[string]interface{}{
|
||||
"organization_id": org.ID.String(),
|
||||
"total_omset": totalOmset,
|
||||
"milestone": milestone,
|
||||
},
|
||||
}
|
||||
|
||||
if _, err := s.notificationProc.Send(ctx, notifReq); err != nil {
|
||||
log.Printf("OmsetMilestoneScheduler: failed to send notification for org %s: %v", org.ID, err)
|
||||
} else {
|
||||
log.Printf("OmsetMilestoneScheduler: sent milestone notification to org %s (omset: %.0f)", org.ID, totalOmset)
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,11 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// orderUserRepository is a minimal interface to fetch users by organization for notification purposes.
|
||||
type orderUserRepository interface {
|
||||
GetActiveByOutletID(ctx context.Context, organizationID, outletID uuid.UUID) ([]*entities.User, error)
|
||||
}
|
||||
|
||||
type OrderService interface {
|
||||
CreateOrder(ctx context.Context, req *models.CreateOrderRequest, organizationID uuid.UUID) (*models.OrderResponse, error)
|
||||
AddToOrder(ctx context.Context, orderID uuid.UUID, req *models.AddToOrderRequest) (*models.AddToOrderResponse, error)
|
||||
@@ -38,9 +43,11 @@ type OrderServiceImpl struct {
|
||||
productRecipeRepo repository.ProductRecipeRepository
|
||||
txManager *repository.TxManager
|
||||
sessionRepo repository.SessionRepository
|
||||
notificationProcessor processor.NotificationProcessor
|
||||
userRepo orderUserRepository
|
||||
}
|
||||
|
||||
func NewOrderServiceImpl(orderProcessor processor.OrderProcessor, tableRepo repository.TableRepositoryInterface, orderIngredientTransactionService *OrderIngredientTransactionService, orderIngredientTransactionProcessor processor.OrderIngredientTransactionProcessor, productRecipeRepo repository.ProductRecipeRepository, txManager *repository.TxManager, sessionRepo repository.SessionRepository) *OrderServiceImpl {
|
||||
func NewOrderServiceImpl(orderProcessor processor.OrderProcessor, tableRepo repository.TableRepositoryInterface, orderIngredientTransactionService *OrderIngredientTransactionService, orderIngredientTransactionProcessor processor.OrderIngredientTransactionProcessor, productRecipeRepo repository.ProductRecipeRepository, txManager *repository.TxManager, sessionRepo repository.SessionRepository, notificationProcessor processor.NotificationProcessor, userRepo orderUserRepository) *OrderServiceImpl {
|
||||
return &OrderServiceImpl{
|
||||
orderProcessor: orderProcessor,
|
||||
tableRepo: tableRepo,
|
||||
@@ -49,6 +56,8 @@ func NewOrderServiceImpl(orderProcessor processor.OrderProcessor, tableRepo repo
|
||||
productRecipeRepo: productRecipeRepo,
|
||||
txManager: txManager,
|
||||
sessionRepo: sessionRepo,
|
||||
notificationProcessor: notificationProcessor,
|
||||
userRepo: userRepo,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,10 +113,73 @@ func (s *OrderServiceImpl) CreateOrder(ctx context.Context, req *models.CreateOr
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Send notification to all org users if this is a self-order
|
||||
if isSelfOrder(req.Metadata) {
|
||||
go s.sendSelfOrderNotification(context.Background(), response, organizationID)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// createIngredientTransactions creates ingredient transactions for order items efficiently
|
||||
// isSelfOrder checks if the order metadata indicates a self-order.
|
||||
func isSelfOrder(metadata map[string]interface{}) bool {
|
||||
if metadata == nil {
|
||||
return false
|
||||
}
|
||||
v, ok := metadata["self_order"]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
b, ok := v.(bool)
|
||||
return ok && b
|
||||
}
|
||||
|
||||
// sendSelfOrderNotification sends a new-order notification to all active users
|
||||
// that can access the outlet where the self-order was placed.
|
||||
func (s *OrderServiceImpl) sendSelfOrderNotification(ctx context.Context, order *models.OrderResponse, organizationID uuid.UUID) {
|
||||
if s.notificationProcessor == nil || s.userRepo == nil {
|
||||
return
|
||||
}
|
||||
|
||||
users, err := s.userRepo.GetActiveByOutletID(ctx, organizationID, order.OutletID)
|
||||
if err != nil || len(users) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
receiverIDs := make([]uuid.UUID, 0, len(users))
|
||||
for _, u := range users {
|
||||
receiverIDs = append(receiverIDs, u.ID)
|
||||
}
|
||||
|
||||
tableName := ""
|
||||
if order.TableNumber != nil {
|
||||
tableName = *order.TableNumber
|
||||
}
|
||||
|
||||
title := "Pesanan Baru Masuk"
|
||||
body := fmt.Sprintf("Ada pesanan baru dari meja %s", tableName)
|
||||
if tableName == "" {
|
||||
body = "Ada pesanan baru masuk"
|
||||
}
|
||||
|
||||
orderID := order.ID
|
||||
notifReq := &models.SendNotificationRequest{
|
||||
Title: title,
|
||||
Body: body,
|
||||
Type: "order",
|
||||
Category: "self_order",
|
||||
NotifiableType: "order",
|
||||
NotifiableID: &orderID,
|
||||
ReceiverIDs: receiverIDs,
|
||||
Data: map[string]interface{}{
|
||||
"order_id": order.ID.String(),
|
||||
"order_number": order.OrderNumber,
|
||||
"table_name": tableName,
|
||||
},
|
||||
}
|
||||
|
||||
_, _ = s.notificationProcessor.Send(ctx, notifReq)
|
||||
}
|
||||
func (s *OrderServiceImpl) createIngredientTransactions(ctx context.Context, orderID uuid.UUID, orderItems []models.OrderItemResponse) ([]*contract.CreateOrderIngredientTransactionRequest, error) {
|
||||
appCtx := appcontext.FromGinContext(ctx)
|
||||
organizationID := appCtx.OrganizationID
|
||||
|
||||
@@ -6,8 +6,22 @@ import (
|
||||
"apskel-pos-be/internal/util"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// parseOutletID converts a *string outlet ID to *uuid.UUID, returning nil for invalid/empty values.
|
||||
func parseOutletID(s *string) *uuid.UUID {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
id, err := uuid.Parse(*s)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return &id
|
||||
}
|
||||
|
||||
// PaymentMethodAnalyticsContractToModel converts contract request to model
|
||||
func PaymentMethodAnalyticsContractToModel(req *contract.PaymentMethodAnalyticsRequest) *models.PaymentMethodAnalyticsRequest {
|
||||
var dateFrom, dateTo time.Time
|
||||
@@ -23,7 +37,7 @@ func PaymentMethodAnalyticsContractToModel(req *contract.PaymentMethodAnalyticsR
|
||||
|
||||
return &models.PaymentMethodAnalyticsRequest{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
OutletID: parseOutletID(req.OutletID),
|
||||
DateFrom: dateFrom,
|
||||
DateTo: dateTo,
|
||||
GroupBy: req.GroupBy,
|
||||
@@ -79,7 +93,7 @@ func SalesAnalyticsContractToModel(req *contract.SalesAnalyticsRequest) *models.
|
||||
|
||||
return &models.SalesAnalyticsRequest{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
OutletID: parseOutletID(req.OutletID),
|
||||
DateFrom: dateFrom,
|
||||
DateTo: dateTo,
|
||||
GroupBy: req.GroupBy,
|
||||
@@ -139,7 +153,7 @@ func ProductAnalyticsContractToModel(req *contract.ProductAnalyticsRequest) *mod
|
||||
|
||||
return &models.ProductAnalyticsRequest{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
OutletID: parseOutletID(req.OutletID),
|
||||
DateFrom: dateFrom,
|
||||
DateTo: dateTo,
|
||||
Limit: req.Limit,
|
||||
@@ -199,7 +213,7 @@ func ProductAnalyticsPerCategoryContractToModel(req *contract.ProductAnalyticsPe
|
||||
|
||||
return &models.ProductAnalyticsPerCategoryRequest{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
OutletID: parseOutletID(req.OutletID),
|
||||
DateFrom: dateFrom,
|
||||
DateTo: dateTo,
|
||||
}
|
||||
@@ -251,7 +265,7 @@ func DashboardAnalyticsContractToModel(req *contract.DashboardAnalyticsRequest)
|
||||
|
||||
return &models.DashboardAnalyticsRequest{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
OutletID: parseOutletID(req.OutletID),
|
||||
DateFrom: dateFrom,
|
||||
DateTo: dateTo,
|
||||
}
|
||||
@@ -346,7 +360,7 @@ func ProfitLossAnalyticsContractToModel(req *contract.ProfitLossAnalyticsRequest
|
||||
|
||||
return &models.ProfitLossAnalyticsRequest{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
OutletID: parseOutletID(req.OutletID),
|
||||
DateFrom: *dateFrom,
|
||||
DateTo: *dateTo,
|
||||
GroupBy: req.GroupBy,
|
||||
|
||||
@@ -3,7 +3,6 @@ package transformer
|
||||
import (
|
||||
"apskel-pos-be/internal/constants"
|
||||
"apskel-pos-be/internal/contract"
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/models"
|
||||
"apskel-pos-be/internal/util"
|
||||
"strconv"
|
||||
@@ -417,122 +416,3 @@ func SplitBillModelToContract(resp *models.SplitBillResponse) *contract.SplitBil
|
||||
Message: resp.Message,
|
||||
}
|
||||
}
|
||||
|
||||
func OrderEntityToContract(o *entities.Order) *contract.OrderResponse {
|
||||
if o == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
orderItems := make([]contract.OrderItemResponse, 0, len(o.OrderItems))
|
||||
for _, oi := range o.OrderItems {
|
||||
lineItem := contract.OrderItemResponse{
|
||||
ID: oi.ID,
|
||||
OrderID: oi.OrderID,
|
||||
ProductID: oi.ProductID,
|
||||
ProductVariantID: oi.ProductVariantID,
|
||||
Quantity: oi.Quantity,
|
||||
UnitPrice: oi.UnitPrice,
|
||||
TotalPrice: oi.TotalPrice,
|
||||
Modifiers: oi.Modifiers,
|
||||
Notes: oi.Notes,
|
||||
Metadata: oi.Metadata,
|
||||
Status: string(oi.Status),
|
||||
CreatedAt: oi.CreatedAt,
|
||||
UpdatedAt: oi.UpdatedAt,
|
||||
}
|
||||
if oi.Product.ID != uuid.Nil {
|
||||
lineItem.ProductName = oi.Product.Name
|
||||
}
|
||||
if oi.ProductVariant != nil {
|
||||
lineItem.ProductVariantName = &oi.ProductVariant.Name
|
||||
}
|
||||
orderItems = append(orderItems, lineItem)
|
||||
}
|
||||
|
||||
payments := make([]contract.PaymentResponse, 0, len(o.Payments))
|
||||
for i := range o.Payments {
|
||||
if p := PaymentEntityToContract(&o.Payments[i]); p != nil {
|
||||
payments = append(payments, *p)
|
||||
}
|
||||
}
|
||||
|
||||
return &contract.OrderResponse{
|
||||
ID: o.ID,
|
||||
OrderNumber: o.OrderNumber,
|
||||
OutletID: o.OutletID,
|
||||
UserID: o.UserID,
|
||||
TableNumber: o.TableNumber,
|
||||
OrderType: string(o.OrderType),
|
||||
Status: string(o.Status),
|
||||
Subtotal: o.Subtotal,
|
||||
TaxAmount: o.TaxAmount,
|
||||
DiscountAmount: o.DiscountAmount,
|
||||
TotalAmount: o.TotalAmount,
|
||||
TotalCost: o.TotalCost,
|
||||
RemainingAmount: o.RemainingAmount,
|
||||
PaymentStatus: string(o.PaymentStatus),
|
||||
RefundAmount: o.RefundAmount,
|
||||
IsVoid: o.IsVoid,
|
||||
IsRefund: o.IsRefund,
|
||||
VoidReason: o.VoidReason,
|
||||
VoidedAt: o.VoidedAt,
|
||||
VoidedBy: o.VoidedBy,
|
||||
RefundReason: o.RefundReason,
|
||||
RefundedAt: o.RefundedAt,
|
||||
RefundedBy: o.RefundedBy,
|
||||
Metadata: o.Metadata,
|
||||
CreatedAt: o.CreatedAt,
|
||||
UpdatedAt: o.UpdatedAt,
|
||||
OrderItems: orderItems,
|
||||
Payments: payments,
|
||||
TotalPaid: o.GetTotalPaid(),
|
||||
PaymentCount: len(o.Payments),
|
||||
}
|
||||
}
|
||||
|
||||
func PaymentEntityToContract(p *entities.Payment) *contract.PaymentResponse {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var splitType *string
|
||||
if p.SplitType != nil {
|
||||
s := string(*p.SplitType)
|
||||
splitType = &s
|
||||
}
|
||||
|
||||
paymentOrderItems := make([]contract.PaymentOrderItemResponse, 0, len(p.PaymentOrderItems))
|
||||
for _, poi := range p.PaymentOrderItems {
|
||||
paymentOrderItems = append(paymentOrderItems, contract.PaymentOrderItemResponse{
|
||||
ID: poi.ID,
|
||||
PaymentID: poi.PaymentID,
|
||||
OrderItemID: poi.OrderItemID,
|
||||
Amount: poi.Amount,
|
||||
CreatedAt: poi.CreatedAt,
|
||||
UpdatedAt: poi.UpdatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
return &contract.PaymentResponse{
|
||||
ID: p.ID,
|
||||
OrderID: p.OrderID,
|
||||
PaymentMethodID: p.PaymentMethodID,
|
||||
PaymentMethodName: p.PaymentMethod.Name,
|
||||
PaymentMethodType: string(p.PaymentMethod.Type),
|
||||
Amount: p.Amount,
|
||||
Status: string(p.Status),
|
||||
TransactionID: p.TransactionID,
|
||||
SplitNumber: p.SplitNumber,
|
||||
SplitTotal: p.SplitTotal,
|
||||
SplitType: splitType,
|
||||
SplitDescription: p.SplitDescription,
|
||||
RefundAmount: p.RefundAmount,
|
||||
RefundReason: p.RefundReason,
|
||||
RefundedAt: p.RefundedAt,
|
||||
RefundedBy: p.RefundedBy,
|
||||
Metadata: p.Metadata,
|
||||
CreatedAt: p.CreatedAt,
|
||||
UpdatedAt: p.UpdatedAt,
|
||||
PaymentOrderItems: paymentOrderItems,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user