Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa037b4d2a | ||
|
|
d38a770ec5 | ||
|
|
015292e830 | ||
|
|
c573b23d76 | ||
|
|
f73a5d533c | ||
|
|
4ea8e32a8e | ||
|
|
06d79046d0 | ||
|
|
8eb19c57ba | ||
|
|
f123de7233 | ||
|
|
7ba776555e | ||
|
|
bccf02b5f7 | ||
|
|
c24a8a8c13 | ||
|
|
6064ef8fde | ||
|
|
1834dd0b19 |
@@ -4,4 +4,5 @@ type Server struct {
|
|||||||
Port string `mapstructure:"port"`
|
Port string `mapstructure:"port"`
|
||||||
BaseUrl string `mapstructure:"common-url"`
|
BaseUrl string `mapstructure:"common-url"`
|
||||||
LocalUrl string `mapstructure:"local-url"`
|
LocalUrl string `mapstructure:"local-url"`
|
||||||
|
SelfOrderUrl string `mapstructure:"self-order-url"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
server:
|
server:
|
||||||
base-url:
|
base-url:
|
||||||
local-url:
|
local-url:
|
||||||
|
self-order-url: http://localhost:5173
|
||||||
port: 4000
|
port: 4000
|
||||||
|
|
||||||
jwt:
|
jwt:
|
||||||
@@ -57,4 +58,4 @@ fonnte:
|
|||||||
|
|
||||||
fcm:
|
fcm:
|
||||||
credentials_file: "infra/firebase-service-account.json"
|
credentials_file: "infra/firebase-service-account.json"
|
||||||
project_id: "your-firebase-project-id"
|
project_id: "apskel-pos-v2"
|
||||||
+19
-2
@@ -30,6 +30,7 @@ type App struct {
|
|||||||
redisClient *redis.Client
|
redisClient *redis.Client
|
||||||
router *router.Router
|
router *router.Router
|
||||||
shutdown chan os.Signal
|
shutdown chan os.Signal
|
||||||
|
omsetScheduler *service.OmsetMilestoneScheduler
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApp(db *gorm.DB, redisClient *redis.Client) *App {
|
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 {
|
func (a *App) Initialize(cfg *config.Config) error {
|
||||||
repos := a.initRepositories()
|
repos := a.initRepositories()
|
||||||
processors := a.initProcessors(cfg, repos)
|
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)
|
services := a.initServices(processors, repos, cfg)
|
||||||
validators := a.initValidators()
|
validators := a.initValidators()
|
||||||
middleware := a.initMiddleware(services, cfg)
|
middleware := a.initMiddleware(services, cfg)
|
||||||
@@ -129,6 +138,11 @@ func (a *App) Initialize(cfg *config.Config) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) Start(port string) 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()
|
engine := a.router.Init()
|
||||||
|
|
||||||
a.server = &http.Server{
|
a.server = &http.Server{
|
||||||
@@ -164,6 +178,9 @@ func (a *App) Start(port string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) Shutdown() {
|
func (a *App) Shutdown() {
|
||||||
|
if a.omsetScheduler != nil {
|
||||||
|
a.omsetScheduler.Stop()
|
||||||
|
}
|
||||||
close(a.shutdown)
|
close(a.shutdown)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,7 +426,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
|||||||
productService := service.NewProductService(processors.productProcessor)
|
productService := service.NewProductService(processors.productProcessor)
|
||||||
productVariantService := service.NewProductVariantService(processors.productVariantProcessor)
|
productVariantService := service.NewProductVariantService(processors.productVariantProcessor)
|
||||||
inventoryService := service.NewInventoryService(processors.inventoryProcessor)
|
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)
|
paymentMethodService := service.NewPaymentMethodService(processors.paymentMethodProcessor)
|
||||||
fileService := service.NewFileServiceImpl(processors.fileProcessor)
|
fileService := service.NewFileServiceImpl(processors.fileProcessor)
|
||||||
var customerService service.CustomerService = service.NewCustomerService(processors.customerProcessor)
|
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)
|
notificationService := service.NewNotificationService(processors.notificationProcessor)
|
||||||
|
|
||||||
// Update order service with order ingredient transaction service
|
// 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{
|
return &services{
|
||||||
userService: service.NewUserService(processors.userProcessor),
|
userService: service.NewUserService(processors.userProcessor),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const (
|
|||||||
RoleManager UserRole = "manager"
|
RoleManager UserRole = "manager"
|
||||||
RoleCashier UserRole = "cashier"
|
RoleCashier UserRole = "cashier"
|
||||||
RoleWaiter UserRole = "waiter"
|
RoleWaiter UserRole = "waiter"
|
||||||
|
RoleOwner UserRole = "owner"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetAllUserRoles() []UserRole {
|
func GetAllUserRoles() []UserRole {
|
||||||
@@ -15,6 +16,7 @@ func GetAllUserRoles() []UserRole {
|
|||||||
RoleManager,
|
RoleManager,
|
||||||
RoleCashier,
|
RoleCashier,
|
||||||
RoleWaiter,
|
RoleWaiter,
|
||||||
|
RoleOwner,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
type PaymentMethodAnalyticsRequest struct {
|
type PaymentMethodAnalyticsRequest struct {
|
||||||
OrganizationID uuid.UUID `form:"organization_id"`
|
OrganizationID uuid.UUID `form:"organization_id"`
|
||||||
OutletID *uuid.UUID `form:"outlet_id,omitempty"`
|
OutletID *string `form:"outlet_id,omitempty"`
|
||||||
DateFrom string `form:"date_from" validate:"required"`
|
DateFrom string `form:"date_from" validate:"required"`
|
||||||
DateTo string `form:"date_to" validate:"required"`
|
DateTo string `form:"date_to" validate:"required"`
|
||||||
GroupBy string `form:"group_by,default=day" validate:"omitempty,oneof=day hour week month"`
|
GroupBy string `form:"group_by,default=day" validate:"omitempty,oneof=day hour week month"`
|
||||||
@@ -45,7 +45,7 @@ type PaymentMethodAnalyticsData struct {
|
|||||||
|
|
||||||
type SalesAnalyticsRequest struct {
|
type SalesAnalyticsRequest struct {
|
||||||
OrganizationID uuid.UUID
|
OrganizationID uuid.UUID
|
||||||
OutletID *uuid.UUID `form:"outlet_id,omitempty"`
|
OutletID *string `form:"outlet_id,omitempty"`
|
||||||
DateFrom string `form:"date_from" validate:"required"`
|
DateFrom string `form:"date_from" validate:"required"`
|
||||||
DateTo string `form:"date_to" validate:"required"`
|
DateTo string `form:"date_to" validate:"required"`
|
||||||
GroupBy string `form:"group_by,default=day" validate:"omitempty,oneof=day hour week month"`
|
GroupBy string `form:"group_by,default=day" validate:"omitempty,oneof=day hour week month"`
|
||||||
@@ -86,7 +86,7 @@ type SalesAnalyticsData struct {
|
|||||||
// ProductAnalyticsRequest represents the request for product analytics
|
// ProductAnalyticsRequest represents the request for product analytics
|
||||||
type ProductAnalyticsRequest struct {
|
type ProductAnalyticsRequest struct {
|
||||||
OrganizationID uuid.UUID
|
OrganizationID uuid.UUID
|
||||||
OutletID *uuid.UUID `form:"outlet_id,omitempty"`
|
OutletID *string `form:"outlet_id,omitempty"`
|
||||||
DateFrom string `form:"date_from" validate:"required"`
|
DateFrom string `form:"date_from" validate:"required"`
|
||||||
DateTo string `form:"date_to" validate:"required"`
|
DateTo string `form:"date_to" validate:"required"`
|
||||||
Limit int `form:"limit,default=1000" validate:"min=1,max=1000"`
|
Limit int `form:"limit,default=1000" validate:"min=1,max=1000"`
|
||||||
@@ -123,7 +123,7 @@ type ProductAnalyticsData struct {
|
|||||||
// ProductAnalyticsPerCategoryRequest represents the request for product analytics per category
|
// ProductAnalyticsPerCategoryRequest represents the request for product analytics per category
|
||||||
type ProductAnalyticsPerCategoryRequest struct {
|
type ProductAnalyticsPerCategoryRequest struct {
|
||||||
OrganizationID uuid.UUID
|
OrganizationID uuid.UUID
|
||||||
OutletID *uuid.UUID `form:"outlet_id,omitempty"`
|
OutletID *string `form:"outlet_id,omitempty"`
|
||||||
DateFrom string `form:"date_from" validate:"required"`
|
DateFrom string `form:"date_from" validate:"required"`
|
||||||
DateTo string `form:"date_to" validate:"required"`
|
DateTo string `form:"date_to" validate:"required"`
|
||||||
}
|
}
|
||||||
@@ -152,7 +152,7 @@ type ProductAnalyticsPerCategoryData struct {
|
|||||||
// DashboardAnalyticsRequest represents the request for dashboard analytics
|
// DashboardAnalyticsRequest represents the request for dashboard analytics
|
||||||
type DashboardAnalyticsRequest struct {
|
type DashboardAnalyticsRequest struct {
|
||||||
OrganizationID uuid.UUID
|
OrganizationID uuid.UUID
|
||||||
OutletID *uuid.UUID `form:"outlet_id,omitempty"`
|
OutletID *string `form:"outlet_id,omitempty"`
|
||||||
DateFrom string `form:"date_from" validate:"required"`
|
DateFrom string `form:"date_from" validate:"required"`
|
||||||
DateTo string `form:"date_to" validate:"required"`
|
DateTo string `form:"date_to" validate:"required"`
|
||||||
}
|
}
|
||||||
@@ -182,7 +182,7 @@ type DashboardOverview struct {
|
|||||||
// ProfitLossAnalyticsRequest represents the request for profit and loss analytics
|
// ProfitLossAnalyticsRequest represents the request for profit and loss analytics
|
||||||
type ProfitLossAnalyticsRequest struct {
|
type ProfitLossAnalyticsRequest struct {
|
||||||
OrganizationID uuid.UUID
|
OrganizationID uuid.UUID
|
||||||
OutletID *uuid.UUID `form:"outlet_id,omitempty"`
|
OutletID *string `form:"outlet_id,omitempty"`
|
||||||
DateFrom string `form:"date_from" validate:"required"`
|
DateFrom string `form:"date_from" validate:"required"`
|
||||||
DateTo string `form:"date_to" validate:"required"`
|
DateTo string `form:"date_to" validate:"required"`
|
||||||
GroupBy string `form:"group_by,default=day" validate:"omitempty,oneof=day hour week month"`
|
GroupBy string `form:"group_by,default=day" validate:"omitempty,oneof=day hour week month"`
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package contract
|
package contract
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -51,6 +49,8 @@ type SelfOrderMenuVariant struct {
|
|||||||
|
|
||||||
type SelfOrderCreateOrderRequest struct {
|
type SelfOrderCreateOrderRequest struct {
|
||||||
SessionID string `json:"session_id" validate:"required"`
|
SessionID string `json:"session_id" validate:"required"`
|
||||||
|
CustomerName string `json:"customer_name" validate:"required"`
|
||||||
|
OrderType string `json:"order_type" validate:"required,oneof=dine_in takeaway delivery"`
|
||||||
OrderItems []SelfOrderCreateOrderItem `json:"order_items" validate:"required,min=1,dive"`
|
OrderItems []SelfOrderCreateOrderItem `json:"order_items" validate:"required,min=1,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ type SelfOrderCreateOrderItem struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SelfOrderListCategoriesRequest struct {
|
type SelfOrderListCategoriesRequest struct {
|
||||||
OrganizationID string `form:"organisasi_id" validate:"required"`
|
OrganizationID string `form:"organization_id" validate:"required"`
|
||||||
OutletID string `form:"outlet_id" validate:"required"`
|
OutletID string `form:"outlet_id" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,35 +78,5 @@ type SelfOrderListCategoriesResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SelfOrderListOrdersResponse struct {
|
type SelfOrderListOrdersResponse struct {
|
||||||
Orders []SelfOrderOrderItem `json:"orders"`
|
Orders []OrderResponse `json:"orders"`
|
||||||
}
|
|
||||||
|
|
||||||
type SelfOrderOrderItem struct {
|
|
||||||
ID uuid.UUID `json:"id"`
|
|
||||||
OrderNumber string `json:"order_number"`
|
|
||||||
TableNumber *string `json:"table_number,omitempty"`
|
|
||||||
OrderType string `json:"order_type"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Subtotal float64 `json:"subtotal"`
|
|
||||||
TaxAmount float64 `json:"tax_amount"`
|
|
||||||
DiscountAmount float64 `json:"discount_amount"`
|
|
||||||
TotalAmount float64 `json:"total_amount"`
|
|
||||||
RemainingAmount float64 `json:"remaining_amount"`
|
|
||||||
PaymentStatus string `json:"payment_status"`
|
|
||||||
IsVoid bool `json:"is_void"`
|
|
||||||
IsRefund bool `json:"is_refund"`
|
|
||||||
Items []SelfOrderOrderLineItem `json:"items,omitempty"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SelfOrderOrderLineItem struct {
|
|
||||||
ProductID uuid.UUID `json:"product_id"`
|
|
||||||
ProductName string `json:"product_name"`
|
|
||||||
ProductVariantID *uuid.UUID `json:"product_variant_id,omitempty"`
|
|
||||||
ProductVariantNam *string `json:"product_variant_name,omitempty"`
|
|
||||||
Quantity int `json:"quantity"`
|
|
||||||
UnitPrice float64 `json:"unit_price"`
|
|
||||||
TotalPrice float64 `json:"total_price"`
|
|
||||||
Notes *string `json:"notes,omitempty"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"apskel-pos-be/internal/util"
|
"apskel-pos-be/internal/util"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AnalyticsHandler struct {
|
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) {
|
func (h *AnalyticsHandler) GetPaymentMethodAnalytics(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
contextInfo := appcontext.FromGinContext(ctx)
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
@@ -36,7 +48,7 @@ func (h *AnalyticsHandler) GetPaymentMethodAnalytics(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.OrganizationID = contextInfo.OrganizationID
|
req.OrganizationID = contextInfo.OrganizationID
|
||||||
req.OutletID = &contextInfo.OutletID
|
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||||
modelReq := transformer.PaymentMethodAnalyticsContractToModel(&req)
|
modelReq := transformer.PaymentMethodAnalyticsContractToModel(&req)
|
||||||
|
|
||||||
response, err := h.analyticsService.GetPaymentMethodAnalytics(ctx, modelReq)
|
response, err := h.analyticsService.GetPaymentMethodAnalytics(ctx, modelReq)
|
||||||
@@ -60,7 +72,7 @@ func (h *AnalyticsHandler) GetSalesAnalytics(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.OrganizationID = contextInfo.OrganizationID
|
req.OrganizationID = contextInfo.OrganizationID
|
||||||
req.OutletID = &contextInfo.OutletID
|
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||||
modelReq := transformer.SalesAnalyticsContractToModel(&req)
|
modelReq := transformer.SalesAnalyticsContractToModel(&req)
|
||||||
|
|
||||||
response, err := h.analyticsService.GetSalesAnalytics(ctx, modelReq)
|
response, err := h.analyticsService.GetSalesAnalytics(ctx, modelReq)
|
||||||
@@ -84,7 +96,7 @@ func (h *AnalyticsHandler) GetProductAnalytics(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.OrganizationID = contextInfo.OrganizationID
|
req.OrganizationID = contextInfo.OrganizationID
|
||||||
req.OutletID = &contextInfo.OutletID
|
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||||
modelReq := transformer.ProductAnalyticsContractToModel(&req)
|
modelReq := transformer.ProductAnalyticsContractToModel(&req)
|
||||||
|
|
||||||
response, err := h.analyticsService.GetProductAnalytics(ctx, modelReq)
|
response, err := h.analyticsService.GetProductAnalytics(ctx, modelReq)
|
||||||
@@ -108,7 +120,7 @@ func (h *AnalyticsHandler) GetProductAnalyticsPerCategory(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.OrganizationID = contextInfo.OrganizationID
|
req.OrganizationID = contextInfo.OrganizationID
|
||||||
req.OutletID = &contextInfo.OutletID
|
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||||
modelReq := transformer.ProductAnalyticsPerCategoryContractToModel(&req)
|
modelReq := transformer.ProductAnalyticsPerCategoryContractToModel(&req)
|
||||||
|
|
||||||
response, err := h.analyticsService.GetProductAnalyticsPerCategory(ctx, modelReq)
|
response, err := h.analyticsService.GetProductAnalyticsPerCategory(ctx, modelReq)
|
||||||
@@ -132,7 +144,7 @@ func (h *AnalyticsHandler) GetDashboardAnalytics(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.OrganizationID = contextInfo.OrganizationID
|
req.OrganizationID = contextInfo.OrganizationID
|
||||||
req.OutletID = &contextInfo.OutletID
|
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||||
modelReq := transformer.DashboardAnalyticsContractToModel(&req)
|
modelReq := transformer.DashboardAnalyticsContractToModel(&req)
|
||||||
|
|
||||||
response, err := h.analyticsService.GetDashboardAnalytics(ctx, modelReq)
|
response, err := h.analyticsService.GetDashboardAnalytics(ctx, modelReq)
|
||||||
@@ -156,6 +168,7 @@ func (h *AnalyticsHandler) GetProfitLossAnalytics(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.OrganizationID = contextInfo.OrganizationID
|
req.OrganizationID = contextInfo.OrganizationID
|
||||||
|
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
|
||||||
modelReq, err := transformer.ProfitLossAnalyticsContractToModel(&req)
|
modelReq, err := transformer.ProfitLossAnalyticsContractToModel(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "AnalyticsHandler::GetProfitLossAnalytics", err.Error())}), "AnalyticsHandler::GetProfitLossAnalytics")
|
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"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ReportHandler struct {
|
type ReportHandler struct {
|
||||||
@@ -19,11 +20,26 @@ func NewReportHandler(reportService service.ReportService, userService UserServi
|
|||||||
return &ReportHandler{reportService: reportService, userService: userService}
|
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) {
|
func (h *ReportHandler) GetDailyTransactionReportPDF(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
ci := appcontext.FromGinContext(ctx)
|
ci := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
outletID := c.Param("outlet_id")
|
outletID := h.resolveOutletID(c, ci.OutletID)
|
||||||
var dayPtr *time.Time
|
var dayPtr *time.Time
|
||||||
if d := c.Query("date"); d != "" {
|
if d := c.Query("date"); d != "" {
|
||||||
if t, err := time.Parse("2006-01-02", d); err == nil {
|
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/contract"
|
||||||
"apskel-pos-be/internal/entities"
|
"apskel-pos-be/internal/entities"
|
||||||
"apskel-pos-be/internal/logger"
|
"apskel-pos-be/internal/logger"
|
||||||
|
"apskel-pos-be/internal/mappers"
|
||||||
"apskel-pos-be/internal/models"
|
"apskel-pos-be/internal/models"
|
||||||
"apskel-pos-be/internal/pkg/tabletoken"
|
"apskel-pos-be/internal/pkg/tabletoken"
|
||||||
"apskel-pos-be/internal/processor"
|
"apskel-pos-be/internal/processor"
|
||||||
@@ -330,6 +331,7 @@ func (h *SelfOrderHandler) CreateOrder(c *gin.Context) {
|
|||||||
metadata := make(map[string]interface{})
|
metadata := make(map[string]interface{})
|
||||||
metadata["self_order"] = true
|
metadata["self_order"] = true
|
||||||
metadata["session_id"] = session.ID
|
metadata["session_id"] = session.ID
|
||||||
|
metadata["customer_name"] = req.CustomerName
|
||||||
|
|
||||||
tableID := table.ID
|
tableID := table.ID
|
||||||
modelReq := &models.CreateOrderRequest{
|
modelReq := &models.CreateOrderRequest{
|
||||||
@@ -337,7 +339,7 @@ func (h *SelfOrderHandler) CreateOrder(c *gin.Context) {
|
|||||||
UserID: userID,
|
UserID: userID,
|
||||||
TableID: &tableID,
|
TableID: &tableID,
|
||||||
TableNumber: &table.TableName,
|
TableNumber: &table.TableName,
|
||||||
OrderType: constants.OrderTypeDineIn,
|
OrderType: constants.OrderType(req.OrderType),
|
||||||
OrderItems: orderItems,
|
OrderItems: orderItems,
|
||||||
Metadata: metadata,
|
Metadata: metadata,
|
||||||
}
|
}
|
||||||
@@ -357,7 +359,7 @@ func (h *SelfOrderHandler) CreateOrder(c *gin.Context) {
|
|||||||
|
|
||||||
func (h *SelfOrderHandler) GetOrdersBySession(c *gin.Context) {
|
func (h *SelfOrderHandler) GetOrdersBySession(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
sessionID := c.Param("sessionId")
|
sessionID := c.Param("session_id")
|
||||||
|
|
||||||
if sessionID == "" {
|
if sessionID == "" {
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
@@ -390,47 +392,15 @@ func (h *SelfOrderHandler) GetOrdersBySession(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := &contract.SelfOrderListOrdersResponse{
|
modelOrders := mappers.OrderEntitiesToResponses(orders)
|
||||||
Orders: make([]contract.SelfOrderOrderItem, 0, len(orders)),
|
contractOrders := make([]contract.OrderResponse, len(modelOrders))
|
||||||
}
|
for i := range modelOrders {
|
||||||
for _, o := range orders {
|
contractOrders[i] = *transformer.OrderModelToContract(&modelOrders[i])
|
||||||
item := contract.SelfOrderOrderItem{
|
|
||||||
ID: o.ID,
|
|
||||||
OrderNumber: o.OrderNumber,
|
|
||||||
TableNumber: o.TableNumber,
|
|
||||||
OrderType: string(o.OrderType),
|
|
||||||
Status: string(o.Status),
|
|
||||||
Subtotal: o.Subtotal,
|
|
||||||
TaxAmount: o.TaxAmount,
|
|
||||||
DiscountAmount: o.DiscountAmount,
|
|
||||||
TotalAmount: o.TotalAmount,
|
|
||||||
RemainingAmount: o.RemainingAmount,
|
|
||||||
PaymentStatus: string(o.PaymentStatus),
|
|
||||||
IsVoid: o.IsVoid,
|
|
||||||
IsRefund: o.IsRefund,
|
|
||||||
CreatedAt: o.CreatedAt,
|
|
||||||
}
|
|
||||||
for _, oi := range o.OrderItems {
|
|
||||||
lineItem := contract.SelfOrderOrderLineItem{
|
|
||||||
ProductID: oi.ProductID,
|
|
||||||
Quantity: oi.Quantity,
|
|
||||||
UnitPrice: oi.UnitPrice,
|
|
||||||
TotalPrice: oi.TotalPrice,
|
|
||||||
Notes: oi.Notes,
|
|
||||||
Status: string(oi.Status),
|
|
||||||
ProductVariantID: oi.ProductVariantID,
|
|
||||||
}
|
|
||||||
if oi.Product.ID != uuid.Nil {
|
|
||||||
lineItem.ProductName = oi.Product.Name
|
|
||||||
}
|
|
||||||
if oi.ProductVariant != nil {
|
|
||||||
lineItem.ProductVariantNam = &oi.ProductVariant.Name
|
|
||||||
}
|
|
||||||
item.Items = append(item.Items, lineItem)
|
|
||||||
}
|
|
||||||
resp.Orders = append(resp.Orders, item)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resp := &contract.SelfOrderListOrdersResponse{
|
||||||
|
Orders: contractOrders,
|
||||||
|
}
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(resp), "SelfOrderHandler::GetOrdersBySession")
|
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(resp), "SelfOrderHandler::GetOrdersBySession")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,6 +409,7 @@ func (h *SelfOrderHandler) validateCreateOrderRequest(req *contract.SelfOrderCre
|
|||||||
return fmt.Errorf("session_id is required")
|
return fmt.Errorf("session_id is required")
|
||||||
}
|
}
|
||||||
if len(req.OrderItems) == 0 {
|
if len(req.OrderItems) == 0 {
|
||||||
|
|
||||||
return fmt.Errorf("at least one order item is required")
|
return fmt.Errorf("at least one order item is required")
|
||||||
}
|
}
|
||||||
for i, item := range req.OrderItems {
|
for i, item := range req.OrderItems {
|
||||||
@@ -466,7 +437,7 @@ func (h *SelfOrderHandler) ListCategories(c *gin.Context) {
|
|||||||
|
|
||||||
if req.OrganizationID == "" {
|
if req.OrganizationID == "" {
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "organisasi_id is required"),
|
contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "organization_id is required"),
|
||||||
}), "SelfOrderHandler::ListCategories")
|
}), "SelfOrderHandler::ListCategories")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -481,7 +452,7 @@ func (h *SelfOrderHandler) ListCategories(c *gin.Context) {
|
|||||||
orgID, err := uuid.Parse(req.OrganizationID)
|
orgID, err := uuid.Parse(req.OrganizationID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||||
contract.NewResponseError(constants.ValidationErrorCode, constants.RequestEntity, "invalid organisasi_id format"),
|
contract.NewResponseError(constants.ValidationErrorCode, constants.RequestEntity, "invalid organization_id format"),
|
||||||
}), "SelfOrderHandler::ListCategories")
|
}), "SelfOrderHandler::ListCategories")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,14 +19,14 @@ import (
|
|||||||
type TableHandler struct {
|
type TableHandler struct {
|
||||||
tableService TableService
|
tableService TableService
|
||||||
tableValidator *validator.TableValidator
|
tableValidator *validator.TableValidator
|
||||||
baseURL string
|
selfOrderURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTableHandler(tableService TableService, tableValidator *validator.TableValidator, baseURL string) *TableHandler {
|
func NewTableHandler(tableService TableService, tableValidator *validator.TableValidator, selfOrderURL string) *TableHandler {
|
||||||
return &TableHandler{
|
return &TableHandler{
|
||||||
tableService: tableService,
|
tableService: tableService,
|
||||||
tableValidator: tableValidator,
|
tableValidator: tableValidator,
|
||||||
baseURL: baseURL,
|
selfOrderURL: selfOrderURL,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,7 +312,7 @@ func (h *TableHandler) GenerateQRCode(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
selfOrderURL := fmt.Sprintf("%s/api/v1/self-order/table/%s", h.baseURL, token)
|
selfOrderURLResult := fmt.Sprintf("%s/menu?token=%s", h.selfOrderURL, token)
|
||||||
|
|
||||||
size := 256
|
size := 256
|
||||||
if sizeStr := c.Query("size"); sizeStr != "" {
|
if sizeStr := c.Query("size"); sizeStr != "" {
|
||||||
@@ -321,7 +321,7 @@ func (h *TableHandler) GenerateQRCode(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pngBytes, err := qrcode.GeneratePNG(selfOrderURL, size)
|
pngBytes, err := qrcode.GeneratePNG(selfOrderURLResult, size)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.FromContext(ctx).WithError(err).Error("TableHandler::GenerateQRCode -> QR generation failed")
|
logger.FromContext(ctx).WithError(err).Error("TableHandler::GenerateQRCode -> QR generation failed")
|
||||||
validationResponseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.TableEntity, "Failed to generate QR code")
|
validationResponseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.TableEntity, "Failed to generate QR code")
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"apskel-pos-be/internal/constants"
|
"apskel-pos-be/internal/constants"
|
||||||
"apskel-pos-be/internal/entities"
|
"apskel-pos-be/internal/entities"
|
||||||
"apskel-pos-be/internal/models"
|
"apskel-pos-be/internal/models"
|
||||||
|
"apskel-pos-be/internal/pkg/tabletoken"
|
||||||
"apskel-pos-be/internal/repository"
|
"apskel-pos-be/internal/repository"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -212,6 +213,15 @@ func (p *TableProcessor) GetTokenByID(ctx context.Context, id uuid.UUID) (string
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, _, _, err := tabletoken.Decode(table.Token); err != nil {
|
||||||
|
newToken := tabletoken.Encode(table.ID, table.OrganizationID, table.OutletID)
|
||||||
|
if updateErr := p.tableRepo.UpdateToken(ctx, table.ID, newToken); updateErr != nil {
|
||||||
|
return "", updateErr
|
||||||
|
}
|
||||||
|
return newToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
return table.Token, nil
|
return table.Token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
func (r *AnalyticsRepositoryImpl) GetPaymentMethodAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) ([]*entities.PaymentMethodAnalytics, error) {
|
||||||
var results []*entities.PaymentMethodAnalytics
|
var results []*entities.PaymentMethodAnalytics
|
||||||
|
|
||||||
@@ -50,9 +57,7 @@ func (r *AnalyticsRepositoryImpl) GetPaymentMethodAnalytics(ctx context.Context,
|
|||||||
Where("p.status = ?", entities.PaymentTransactionStatusCompleted).
|
Where("p.status = ?", entities.PaymentTransactionStatusCompleted).
|
||||||
Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo)
|
Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo)
|
||||||
|
|
||||||
if outletID != nil {
|
query = r.resolveOutletID(query, outletID, "o.outlet_id")
|
||||||
query = query.Where("o.outlet_id = ?", *outletID)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := query.
|
err := query.
|
||||||
Group("pm.id, pm.name, pm.type").
|
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("oi.status != ?", entities.OrderItemStatusCancelled).
|
||||||
Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo)
|
Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo)
|
||||||
|
|
||||||
if outletID != nil {
|
query = r.resolveOutletID(query, outletID, "o.outlet_id")
|
||||||
query = query.Where("o.outlet_id = ?", *outletID)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := query.
|
err := query.
|
||||||
Group("p.id, p.name, p.cost, c.id, c.name, c.order, mahpp.hpp_per_unit").
|
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("oi.status != ?", entities.OrderItemStatusCancelled).
|
||||||
Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo)
|
Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo)
|
||||||
|
|
||||||
if outletID != nil {
|
query = r.resolveOutletID(query, outletID, "o.outlet_id")
|
||||||
query = query.Where("o.outlet_id = ?", *outletID)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := query.
|
err := query.
|
||||||
Group("c.id, c.name").
|
Group("c.id, c.name").
|
||||||
@@ -267,9 +268,7 @@ func (r *AnalyticsRepositoryImpl) GetDashboardOverview(ctx context.Context, orga
|
|||||||
Where("o.organization_id = ?", organizationID).
|
Where("o.organization_id = ?", organizationID).
|
||||||
Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo)
|
Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo)
|
||||||
|
|
||||||
if outletID != nil {
|
query = r.resolveOutletID(query, outletID, "o.outlet_id")
|
||||||
query = query.Where("o.outlet_id = ?", *outletID)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := query.Scan(&result).Error
|
err := query.Scan(&result).Error
|
||||||
if err != nil {
|
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.is_void = false AND o.is_refund = false").
|
||||||
Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo)
|
Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo)
|
||||||
|
|
||||||
if outletID != nil {
|
summaryQuery = r.resolveOutletID(summaryQuery, outletID, "o.outlet_id")
|
||||||
summaryQuery = summaryQuery.Where("o.outlet_id = ?", *outletID)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := summaryQuery.Scan(&summary).Error
|
err := summaryQuery.Scan(&summary).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -374,9 +371,7 @@ func (r *AnalyticsRepositoryImpl) GetProfitLossAnalytics(ctx context.Context, or
|
|||||||
Group(timeFormat).
|
Group(timeFormat).
|
||||||
Order(timeFormat)
|
Order(timeFormat)
|
||||||
|
|
||||||
if outletID != nil {
|
dataQuery = r.resolveOutletID(dataQuery, outletID, "o.outlet_id")
|
||||||
dataQuery = dataQuery.Where("o.outlet_id = ?", *outletID)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = dataQuery.Scan(&data).Error
|
err = dataQuery.Scan(&data).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -419,9 +414,7 @@ func (r *AnalyticsRepositoryImpl) GetProfitLossAnalytics(ctx context.Context, or
|
|||||||
Order("p.name ASC").
|
Order("p.name ASC").
|
||||||
Limit(1000)
|
Limit(1000)
|
||||||
|
|
||||||
if outletID != nil {
|
productQuery = r.resolveOutletID(productQuery, outletID, "o.outlet_id")
|
||||||
productQuery = productQuery.Where("o.outlet_id = ?", *outletID)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = productQuery.Scan(&productData).Error
|
err = productQuery.Scan(&productData).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
type InventoryRepository interface {
|
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 {
|
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 {
|
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 {
|
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
for productID, delta := range adjustments {
|
for productID, delta := range adjustments {
|
||||||
var inventory entities.Inventory
|
var inventory entities.Inventory
|
||||||
if err := tx.Where("product_id = ? AND outlet_id = ?", productID, outletID).First(&inventory).Error; err != nil {
|
err := tx.Set("gorm:query_option", "FOR UPDATE").
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
Where("product_id = ? AND outlet_id = ?", productID, outletID).
|
||||||
// Inventory doesn't exist, create it with initial quantity
|
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{
|
inventory = entities.Inventory{
|
||||||
ProductID: productID,
|
ProductID: productID,
|
||||||
OutletID: outletID,
|
OutletID: outletID,
|
||||||
Quantity: 0,
|
Quantity: 0,
|
||||||
ReorderLevel: 0,
|
ReorderLevel: 0,
|
||||||
}
|
}
|
||||||
if err := tx.Create(&inventory).Error; err != nil {
|
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)
|
return fmt.Errorf("failed to create inventory record for product %s: %w", productID, err)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inventory.UpdateQuantity(delta)
|
inventory.UpdateQuantity(delta)
|
||||||
|
|||||||
@@ -99,3 +99,14 @@ func (r *OrganizationRepositoryImpl) GetByEmail(ctx context.Context, email strin
|
|||||||
}
|
}
|
||||||
return &org, nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -171,6 +171,13 @@ func (r *TableRepository) ReleaseTable(ctx context.Context, tableID uuid.UUID, p
|
|||||||
}).Error
|
}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *TableRepository) UpdateToken(ctx context.Context, tableID uuid.UUID, token string) error {
|
||||||
|
return r.db.WithContext(ctx).
|
||||||
|
Model(&entities.Table{}).
|
||||||
|
Where("id = ?", tableID).
|
||||||
|
Update("token", token).Error
|
||||||
|
}
|
||||||
|
|
||||||
func (r *TableRepository) GetByOrderID(ctx context.Context, orderID uuid.UUID) (*entities.Table, error) {
|
func (r *TableRepository) GetByOrderID(ctx context.Context, orderID uuid.UUID) (*entities.Table, error) {
|
||||||
var table entities.Table
|
var table entities.Table
|
||||||
err := r.db.WithContext(ctx).
|
err := r.db.WithContext(ctx).
|
||||||
|
|||||||
@@ -24,4 +24,5 @@ type TableRepositoryInterface interface {
|
|||||||
OccupyTable(ctx context.Context, tableID, orderID uuid.UUID, startTime *time.Time) error
|
OccupyTable(ctx context.Context, tableID, orderID uuid.UUID, startTime *time.Time) error
|
||||||
ReleaseTable(ctx context.Context, tableID uuid.UUID, paymentAmount float64) error
|
ReleaseTable(ctx context.Context, tableID uuid.UUID, paymentAmount float64) error
|
||||||
GetByOrderID(ctx context.Context, orderID uuid.UUID) (*entities.Table, error)
|
GetByOrderID(ctx context.Context, orderID uuid.UUID) (*entities.Table, error)
|
||||||
|
UpdateToken(ctx context.Context, tableID uuid.UUID, token string) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,17 @@ func (r *UserRepositoryImpl) GetActiveUsers(ctx context.Context, organizationID
|
|||||||
return users, err
|
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 {
|
func (r *UserRepositoryImpl) Update(ctx context.Context, user *entities.User) error {
|
||||||
return r.db.WithContext(ctx).Save(user).Error
|
return r.db.WithContext(ctx).Save(user).Error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ func NewRouter(cfg *config.Config, healthHandler *handler.HealthHandler, authSer
|
|||||||
paymentMethodHandler: handler.NewPaymentMethodHandler(paymentMethodService, paymentMethodValidator),
|
paymentMethodHandler: handler.NewPaymentMethodHandler(paymentMethodService, paymentMethodValidator),
|
||||||
analyticsHandler: handler.NewAnalyticsHandler(analyticsService, transformer.NewTransformer()),
|
analyticsHandler: handler.NewAnalyticsHandler(analyticsService, transformer.NewTransformer()),
|
||||||
reportHandler: handler.NewReportHandler(reportService, userService),
|
reportHandler: handler.NewReportHandler(reportService, userService),
|
||||||
tableHandler: handler.NewTableHandler(tableService, tableValidator, cfg.Server.BaseUrl),
|
tableHandler: handler.NewTableHandler(tableService, tableValidator, cfg.Server.SelfOrderUrl),
|
||||||
unitHandler: handler.NewUnitHandler(unitService),
|
unitHandler: handler.NewUnitHandler(unitService),
|
||||||
ingredientHandler: handler.NewIngredientHandler(ingredientService),
|
ingredientHandler: handler.NewIngredientHandler(ingredientService),
|
||||||
productRecipeHandler: handler.NewProductRecipeHandler(productRecipeService),
|
productRecipeHandler: handler.NewProductRecipeHandler(productRecipeService),
|
||||||
@@ -157,7 +157,7 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
|||||||
selfOrder.GET("/categories", r.selfOrderHandler.ListCategories)
|
selfOrder.GET("/categories", r.selfOrderHandler.ListCategories)
|
||||||
selfOrder.GET("/menu", r.selfOrderHandler.GetMenu)
|
selfOrder.GET("/menu", r.selfOrderHandler.GetMenu)
|
||||||
selfOrder.POST("/orders", r.selfOrderHandler.CreateOrder)
|
selfOrder.POST("/orders", r.selfOrderHandler.CreateOrder)
|
||||||
selfOrder.GET("/orders/:sessionId", r.selfOrderHandler.GetOrdersBySession)
|
selfOrder.GET("/orders/:session_id", r.selfOrderHandler.GetOrdersBySession)
|
||||||
}
|
}
|
||||||
|
|
||||||
organizations := v1.Group("/organizations")
|
organizations := v1.Group("/organizations")
|
||||||
|
|||||||
@@ -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"
|
"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 {
|
type OrderService interface {
|
||||||
CreateOrder(ctx context.Context, req *models.CreateOrderRequest, organizationID uuid.UUID) (*models.OrderResponse, error)
|
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)
|
AddToOrder(ctx context.Context, orderID uuid.UUID, req *models.AddToOrderRequest) (*models.AddToOrderResponse, error)
|
||||||
@@ -38,9 +43,11 @@ type OrderServiceImpl struct {
|
|||||||
productRecipeRepo repository.ProductRecipeRepository
|
productRecipeRepo repository.ProductRecipeRepository
|
||||||
txManager *repository.TxManager
|
txManager *repository.TxManager
|
||||||
sessionRepo repository.SessionRepository
|
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{
|
return &OrderServiceImpl{
|
||||||
orderProcessor: orderProcessor,
|
orderProcessor: orderProcessor,
|
||||||
tableRepo: tableRepo,
|
tableRepo: tableRepo,
|
||||||
@@ -49,6 +56,8 @@ func NewOrderServiceImpl(orderProcessor processor.OrderProcessor, tableRepo repo
|
|||||||
productRecipeRepo: productRecipeRepo,
|
productRecipeRepo: productRecipeRepo,
|
||||||
txManager: txManager,
|
txManager: txManager,
|
||||||
sessionRepo: sessionRepo,
|
sessionRepo: sessionRepo,
|
||||||
|
notificationProcessor: notificationProcessor,
|
||||||
|
userRepo: userRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,10 +113,73 @@ func (s *OrderServiceImpl) CreateOrder(ctx context.Context, req *models.CreateOr
|
|||||||
return nil, err
|
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
|
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) {
|
func (s *OrderServiceImpl) createIngredientTransactions(ctx context.Context, orderID uuid.UUID, orderItems []models.OrderItemResponse) ([]*contract.CreateOrderIngredientTransactionRequest, error) {
|
||||||
appCtx := appcontext.FromGinContext(ctx)
|
appCtx := appcontext.FromGinContext(ctx)
|
||||||
organizationID := appCtx.OrganizationID
|
organizationID := appCtx.OrganizationID
|
||||||
|
|||||||
@@ -6,8 +6,22 @@ import (
|
|||||||
"apskel-pos-be/internal/util"
|
"apskel-pos-be/internal/util"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"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
|
// PaymentMethodAnalyticsContractToModel converts contract request to model
|
||||||
func PaymentMethodAnalyticsContractToModel(req *contract.PaymentMethodAnalyticsRequest) *models.PaymentMethodAnalyticsRequest {
|
func PaymentMethodAnalyticsContractToModel(req *contract.PaymentMethodAnalyticsRequest) *models.PaymentMethodAnalyticsRequest {
|
||||||
var dateFrom, dateTo time.Time
|
var dateFrom, dateTo time.Time
|
||||||
@@ -23,7 +37,7 @@ func PaymentMethodAnalyticsContractToModel(req *contract.PaymentMethodAnalyticsR
|
|||||||
|
|
||||||
return &models.PaymentMethodAnalyticsRequest{
|
return &models.PaymentMethodAnalyticsRequest{
|
||||||
OrganizationID: req.OrganizationID,
|
OrganizationID: req.OrganizationID,
|
||||||
OutletID: req.OutletID,
|
OutletID: parseOutletID(req.OutletID),
|
||||||
DateFrom: dateFrom,
|
DateFrom: dateFrom,
|
||||||
DateTo: dateTo,
|
DateTo: dateTo,
|
||||||
GroupBy: req.GroupBy,
|
GroupBy: req.GroupBy,
|
||||||
@@ -79,7 +93,7 @@ func SalesAnalyticsContractToModel(req *contract.SalesAnalyticsRequest) *models.
|
|||||||
|
|
||||||
return &models.SalesAnalyticsRequest{
|
return &models.SalesAnalyticsRequest{
|
||||||
OrganizationID: req.OrganizationID,
|
OrganizationID: req.OrganizationID,
|
||||||
OutletID: req.OutletID,
|
OutletID: parseOutletID(req.OutletID),
|
||||||
DateFrom: dateFrom,
|
DateFrom: dateFrom,
|
||||||
DateTo: dateTo,
|
DateTo: dateTo,
|
||||||
GroupBy: req.GroupBy,
|
GroupBy: req.GroupBy,
|
||||||
@@ -139,7 +153,7 @@ func ProductAnalyticsContractToModel(req *contract.ProductAnalyticsRequest) *mod
|
|||||||
|
|
||||||
return &models.ProductAnalyticsRequest{
|
return &models.ProductAnalyticsRequest{
|
||||||
OrganizationID: req.OrganizationID,
|
OrganizationID: req.OrganizationID,
|
||||||
OutletID: req.OutletID,
|
OutletID: parseOutletID(req.OutletID),
|
||||||
DateFrom: dateFrom,
|
DateFrom: dateFrom,
|
||||||
DateTo: dateTo,
|
DateTo: dateTo,
|
||||||
Limit: req.Limit,
|
Limit: req.Limit,
|
||||||
@@ -199,7 +213,7 @@ func ProductAnalyticsPerCategoryContractToModel(req *contract.ProductAnalyticsPe
|
|||||||
|
|
||||||
return &models.ProductAnalyticsPerCategoryRequest{
|
return &models.ProductAnalyticsPerCategoryRequest{
|
||||||
OrganizationID: req.OrganizationID,
|
OrganizationID: req.OrganizationID,
|
||||||
OutletID: req.OutletID,
|
OutletID: parseOutletID(req.OutletID),
|
||||||
DateFrom: dateFrom,
|
DateFrom: dateFrom,
|
||||||
DateTo: dateTo,
|
DateTo: dateTo,
|
||||||
}
|
}
|
||||||
@@ -251,7 +265,7 @@ func DashboardAnalyticsContractToModel(req *contract.DashboardAnalyticsRequest)
|
|||||||
|
|
||||||
return &models.DashboardAnalyticsRequest{
|
return &models.DashboardAnalyticsRequest{
|
||||||
OrganizationID: req.OrganizationID,
|
OrganizationID: req.OrganizationID,
|
||||||
OutletID: req.OutletID,
|
OutletID: parseOutletID(req.OutletID),
|
||||||
DateFrom: dateFrom,
|
DateFrom: dateFrom,
|
||||||
DateTo: dateTo,
|
DateTo: dateTo,
|
||||||
}
|
}
|
||||||
@@ -346,7 +360,7 @@ func ProfitLossAnalyticsContractToModel(req *contract.ProfitLossAnalyticsRequest
|
|||||||
|
|
||||||
return &models.ProfitLossAnalyticsRequest{
|
return &models.ProfitLossAnalyticsRequest{
|
||||||
OrganizationID: req.OrganizationID,
|
OrganizationID: req.OrganizationID,
|
||||||
OutletID: req.OutletID,
|
OutletID: parseOutletID(req.OutletID),
|
||||||
DateFrom: *dateFrom,
|
DateFrom: *dateFrom,
|
||||||
DateTo: *dateTo,
|
DateTo: *dateTo,
|
||||||
GroupBy: req.GroupBy,
|
GroupBy: req.GroupBy,
|
||||||
|
|||||||
Reference in New Issue
Block a user