Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e92c487815 |
+5
-22
@@ -25,12 +25,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
server *http.Server
|
server *http.Server
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
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 {
|
||||||
@@ -44,14 +43,6 @@ 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)
|
||||||
@@ -138,11 +129,6 @@ 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{
|
||||||
@@ -178,9 +164,6 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ 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 {
|
||||||
@@ -16,7 +15,6 @@ func GetAllUserRoles() []UserRole {
|
|||||||
RoleManager,
|
RoleManager,
|
||||||
RoleCashier,
|
RoleCashier,
|
||||||
RoleWaiter,
|
RoleWaiter,
|
||||||
RoleOwner,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type PaymentMethodAnalyticsRequest struct {
|
type PaymentMethodAnalyticsRequest struct {
|
||||||
OrganizationID uuid.UUID `form:"organization_id"`
|
OrganizationID uuid.UUID `form:"organization_id"`
|
||||||
OutletID *string `form:"outlet_id,omitempty"`
|
OutletID *uuid.UUID `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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PaymentMethodAnalyticsResponse represents the response for payment method analytics
|
// PaymentMethodAnalyticsResponse represents the response for payment method analytics
|
||||||
@@ -45,10 +45,10 @@ type PaymentMethodAnalyticsData struct {
|
|||||||
|
|
||||||
type SalesAnalyticsRequest struct {
|
type SalesAnalyticsRequest struct {
|
||||||
OrganizationID uuid.UUID
|
OrganizationID uuid.UUID
|
||||||
OutletID *string `form:"outlet_id,omitempty"`
|
OutletID *uuid.UUID `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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SalesAnalyticsResponse struct {
|
type SalesAnalyticsResponse struct {
|
||||||
@@ -86,10 +86,10 @@ 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 *string `form:"outlet_id,omitempty"`
|
OutletID *uuid.UUID `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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProductAnalyticsResponse represents the response for product analytics
|
// ProductAnalyticsResponse represents the response for product analytics
|
||||||
@@ -123,9 +123,9 @@ 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 *string `form:"outlet_id,omitempty"`
|
OutletID *uuid.UUID `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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProductAnalyticsPerCategoryResponse represents the response for product analytics per category
|
// ProductAnalyticsPerCategoryResponse represents the response for product analytics per category
|
||||||
@@ -152,9 +152,9 @@ 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 *string `form:"outlet_id,omitempty"`
|
OutletID *uuid.UUID `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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DashboardAnalyticsResponse represents the response for dashboard analytics
|
// DashboardAnalyticsResponse represents the response for dashboard analytics
|
||||||
@@ -182,10 +182,10 @@ 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 *string `form:"outlet_id,omitempty"`
|
OutletID *uuid.UUID `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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProfitLossAnalyticsResponse represents the response for profit and loss analytics
|
// ProfitLossAnalyticsResponse represents the response for profit and loss analytics
|
||||||
|
|||||||
@@ -8,25 +8,22 @@ import (
|
|||||||
|
|
||||||
type CreateCategoryRequest struct {
|
type CreateCategoryRequest struct {
|
||||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
BusinessType *string `json:"business_type,omitempty"`
|
BusinessType *string `json:"business_type,omitempty"`
|
||||||
Order *int `json:"order,omitempty"`
|
Order *int `json:"order,omitempty"`
|
||||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateCategoryRequest struct {
|
type UpdateCategoryRequest struct {
|
||||||
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
|
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
BusinessType *string `json:"business_type,omitempty"`
|
BusinessType *string `json:"business_type,omitempty"`
|
||||||
Order *int `json:"order,omitempty"`
|
Order *int `json:"order,omitempty"`
|
||||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListCategoriesRequest struct {
|
type ListCategoriesRequest struct {
|
||||||
OrganizationID *uuid.UUID `json:"organization_id,omitempty"`
|
OrganizationID *uuid.UUID `json:"organization_id,omitempty"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
|
||||||
BusinessType string `json:"business_type,omitempty"`
|
BusinessType string `json:"business_type,omitempty"`
|
||||||
Search string `json:"search,omitempty"`
|
Search string `json:"search,omitempty"`
|
||||||
Page int `json:"page" validate:"required,min=1"`
|
Page int `json:"page" validate:"required,min=1"`
|
||||||
@@ -37,11 +34,10 @@ type ListCategoriesRequest struct {
|
|||||||
type CategoryResponse struct {
|
type CategoryResponse struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
BusinessType string `json:"business_type"`
|
BusinessType string `json:"business_type"`
|
||||||
Order int `json:"order"`
|
Order int `json:"order"`
|
||||||
Metadata map[string]interface{} `json:"metadata"`
|
Metadata map[string]interface{} `json:"metadata"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type CreateProductRequest struct {
|
type CreateProductRequest struct {
|
||||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
|
||||||
CategoryID uuid.UUID `json:"category_id" validate:"required"`
|
CategoryID uuid.UUID `json:"category_id" validate:"required"`
|
||||||
SKU *string `json:"sku,omitempty"`
|
SKU *string `json:"sku,omitempty"`
|
||||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||||
@@ -26,7 +25,6 @@ type CreateProductRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UpdateProductRequest struct {
|
type UpdateProductRequest struct {
|
||||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
|
||||||
CategoryID *uuid.UUID `json:"category_id,omitempty"`
|
CategoryID *uuid.UUID `json:"category_id,omitempty"`
|
||||||
SKU *string `json:"sku,omitempty"`
|
SKU *string `json:"sku,omitempty"`
|
||||||
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
|
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
|
||||||
@@ -60,7 +58,6 @@ type UpdateProductVariantRequest struct {
|
|||||||
type ProductResponse struct {
|
type ProductResponse struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
OrganizationID uuid.UUID `json:"organization_id"`
|
OrganizationID uuid.UUID `json:"organization_id"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id"`
|
|
||||||
CategoryID uuid.UUID `json:"category_id"`
|
CategoryID uuid.UUID `json:"category_id"`
|
||||||
CategoryName string `json:"category_name"`
|
CategoryName string `json:"category_name"`
|
||||||
SKU *string `json:"sku"`
|
SKU *string `json:"sku"`
|
||||||
@@ -92,7 +89,6 @@ type ProductVariantResponse struct {
|
|||||||
|
|
||||||
type ListProductsRequest struct {
|
type ListProductsRequest struct {
|
||||||
OrganizationID *uuid.UUID `json:"organization_id,omitempty"`
|
OrganizationID *uuid.UUID `json:"organization_id,omitempty"`
|
||||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
|
||||||
CategoryID *uuid.UUID `json:"category_id,omitempty"`
|
CategoryID *uuid.UUID `json:"category_id,omitempty"`
|
||||||
BusinessType string `json:"business_type,omitempty"`
|
BusinessType string `json:"business_type,omitempty"`
|
||||||
IsActive *bool `json:"is_active,omitempty"`
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
|
|||||||
@@ -31,19 +31,17 @@ func (m *Metadata) Scan(value interface{}) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Category struct {
|
type Category struct {
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id" validate:"required"`
|
OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id" validate:"required"`
|
||||||
OutletID *uuid.UUID `gorm:"type:uuid;index" json:"outlet_id,omitempty"`
|
Name string `gorm:"not null;size:255" json:"name" validate:"required,min=1,max=255"`
|
||||||
Name string `gorm:"not null;size:255" json:"name" validate:"required,min=1,max=255"`
|
Description *string `gorm:"type:text" json:"description"`
|
||||||
Description *string `gorm:"type:text" json:"description"`
|
Order int `gorm:"default:0" json:"order"`
|
||||||
Order int `gorm:"default:0" json:"order"`
|
BusinessType string `gorm:"size:50;default:'restaurant'" json:"business_type"`
|
||||||
BusinessType string `gorm:"size:50;default:'restaurant'" json:"business_type"`
|
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
|
||||||
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
|
||||||
|
|
||||||
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
||||||
Outlet Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
|
|
||||||
Products []Product `gorm:"foreignKey:CategoryID" json:"products,omitempty"`
|
Products []Product `gorm:"foreignKey:CategoryID" json:"products,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
type Product struct {
|
type Product struct {
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id" validate:"required"`
|
OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id" validate:"required"`
|
||||||
OutletID *uuid.UUID `gorm:"type:uuid;index" json:"outlet_id"`
|
|
||||||
CategoryID uuid.UUID `gorm:"type:uuid;not null;index" json:"category_id" validate:"required"`
|
CategoryID uuid.UUID `gorm:"type:uuid;not null;index" json:"category_id" validate:"required"`
|
||||||
SKU *string `gorm:"size:100;index" json:"sku"`
|
SKU *string `gorm:"size:100;index" json:"sku"`
|
||||||
Name string `gorm:"not null;size:255" json:"name" validate:"required,min=1,max=255"`
|
Name string `gorm:"not null;size:255" json:"name" validate:"required,min=1,max=255"`
|
||||||
@@ -28,7 +27,6 @@ type Product struct {
|
|||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
|
||||||
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
|
||||||
Outlet *Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
|
|
||||||
Category Category `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
|
Category Category `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
|
||||||
Unit *Unit `gorm:"foreignKey:UnitID" json:"unit,omitempty"`
|
Unit *Unit `gorm:"foreignKey:UnitID" json:"unit,omitempty"`
|
||||||
ProductVariants []ProductVariant `gorm:"foreignKey:ProductID" json:"variants,omitempty"`
|
ProductVariants []ProductVariant `gorm:"foreignKey:ProductID" json:"variants,omitempty"`
|
||||||
|
|||||||
@@ -26,13 +26,14 @@ func NewAnalyticsHandler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *AnalyticsHandler) resolveOutletID(c *gin.Context, contextOutletID uuid.UUID) *string {
|
func (h *AnalyticsHandler) resolveOutletID(c *gin.Context, contextOutletID uuid.UUID) *uuid.UUID {
|
||||||
if outletIDStr := c.Query("outlet_id"); outletIDStr != "" {
|
if outletIDStr := c.Query("outlet_id"); outletIDStr != "" {
|
||||||
return &outletIDStr
|
if parsedID, err := uuid.Parse(outletIDStr); err == nil {
|
||||||
|
return &parsedID
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if contextOutletID != uuid.Nil {
|
if contextOutletID != uuid.Nil {
|
||||||
s := contextOutletID.String()
|
return &contextOutletID
|
||||||
return &s
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,12 +170,6 @@ func (h *CategoryHandler) ListCategories(c *gin.Context) {
|
|||||||
req.BusinessType = businessType
|
req.BusinessType = businessType
|
||||||
}
|
}
|
||||||
|
|
||||||
if outletIDStr := c.Query("outlet_id"); outletIDStr != "" {
|
|
||||||
if outletID, err := uuid.Parse(outletIDStr); err == nil {
|
|
||||||
req.OutletID = &outletID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if organizationIDStr := c.Query("organization_id"); organizationIDStr != "" {
|
if organizationIDStr := c.Query("organization_id"); organizationIDStr != "" {
|
||||||
if organizationID, err := uuid.Parse(organizationIDStr); err == nil {
|
if organizationID, err := uuid.Parse(organizationIDStr); err == nil {
|
||||||
req.OrganizationID = &organizationID
|
req.OrganizationID = &organizationID
|
||||||
|
|||||||
@@ -172,12 +172,6 @@ func (h *ProductHandler) ListProducts(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if outletIDStr := c.Query("outlet_id"); outletIDStr != "" {
|
|
||||||
if outletID, err := uuid.Parse(outletIDStr); err == nil {
|
|
||||||
req.OutletID = &outletID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if categoryIDStr := c.Query("category_id"); categoryIDStr != "" {
|
if categoryIDStr := c.Query("category_id"); categoryIDStr != "" {
|
||||||
if categoryID, err := uuid.Parse(categoryIDStr); err == nil {
|
if categoryID, err := uuid.Parse(categoryIDStr); err == nil {
|
||||||
req.CategoryID = &categoryID
|
req.CategoryID = &categoryID
|
||||||
|
|||||||
+10
-14
@@ -9,11 +9,10 @@ import (
|
|||||||
type Category struct {
|
type Category struct {
|
||||||
ID uuid.UUID
|
ID uuid.UUID
|
||||||
OrganizationID uuid.UUID
|
OrganizationID uuid.UUID
|
||||||
OutletID *uuid.UUID
|
|
||||||
Name string
|
Name string
|
||||||
Description *string
|
Description *string
|
||||||
ImageURL *string
|
ImageURL *string
|
||||||
Order int
|
Order int
|
||||||
IsActive bool
|
IsActive bool
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
@@ -21,30 +20,27 @@ type Category struct {
|
|||||||
|
|
||||||
type CreateCategoryRequest struct {
|
type CreateCategoryRequest struct {
|
||||||
OrganizationID uuid.UUID `validate:"required"`
|
OrganizationID uuid.UUID `validate:"required"`
|
||||||
OutletID *uuid.UUID
|
Name string `validate:"required,min=1,max=255"`
|
||||||
Name string `validate:"required,min=1,max=255"`
|
Description *string `validate:"omitempty,max=1000"`
|
||||||
Description *string `validate:"omitempty,max=1000"`
|
ImageURL *string `validate:"omitempty,url"`
|
||||||
ImageURL *string `validate:"omitempty,url"`
|
Order int `validate:"min=0"`
|
||||||
Order int `validate:"min=0"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateCategoryRequest struct {
|
type UpdateCategoryRequest struct {
|
||||||
OutletID *uuid.UUID `validate:"omitempty,required"`
|
Name *string `validate:"omitempty,min=1,max=255"`
|
||||||
Name *string `validate:"omitempty,min=1,max=255"`
|
Description *string `validate:"omitempty,max=1000"`
|
||||||
Description *string `validate:"omitempty,max=1000"`
|
ImageURL *string `validate:"omitempty,url"`
|
||||||
ImageURL *string `validate:"omitempty,url"`
|
Order *int `validate:"omitempty,min=0"`
|
||||||
Order *int `validate:"omitempty,min=0"`
|
|
||||||
IsActive *bool
|
IsActive *bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type CategoryResponse struct {
|
type CategoryResponse struct {
|
||||||
ID uuid.UUID
|
ID uuid.UUID
|
||||||
OrganizationID uuid.UUID
|
OrganizationID uuid.UUID
|
||||||
OutletID *uuid.UUID
|
|
||||||
Name string
|
Name string
|
||||||
Description *string
|
Description *string
|
||||||
ImageURL *string
|
ImageURL *string
|
||||||
Order int
|
Order int
|
||||||
IsActive bool
|
IsActive bool
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
type Product struct {
|
type Product struct {
|
||||||
ID uuid.UUID
|
ID uuid.UUID
|
||||||
OrganizationID uuid.UUID
|
OrganizationID uuid.UUID
|
||||||
OutletID *uuid.UUID
|
|
||||||
CategoryID uuid.UUID
|
CategoryID uuid.UUID
|
||||||
SKU *string
|
SKU *string
|
||||||
Name string
|
Name string
|
||||||
@@ -41,7 +40,6 @@ type ProductVariant struct {
|
|||||||
|
|
||||||
type CreateProductRequest struct {
|
type CreateProductRequest struct {
|
||||||
OrganizationID uuid.UUID `validate:"required"`
|
OrganizationID uuid.UUID `validate:"required"`
|
||||||
OutletID *uuid.UUID `validate:"omitempty"`
|
|
||||||
CategoryID uuid.UUID `validate:"required"`
|
CategoryID uuid.UUID `validate:"required"`
|
||||||
SKU *string `validate:"omitempty,max=100"`
|
SKU *string `validate:"omitempty,max=100"`
|
||||||
Name string `validate:"required,min=1,max=255"`
|
Name string `validate:"required,min=1,max=255"`
|
||||||
@@ -62,7 +60,6 @@ type CreateProductRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UpdateProductRequest struct {
|
type UpdateProductRequest struct {
|
||||||
OutletID *uuid.UUID `validate:"omitempty"`
|
|
||||||
CategoryID *uuid.UUID `validate:"omitempty"`
|
CategoryID *uuid.UUID `validate:"omitempty"`
|
||||||
SKU *string `validate:"omitempty,max=100"`
|
SKU *string `validate:"omitempty,max=100"`
|
||||||
Name *string `validate:"omitempty,min=1,max=255"`
|
Name *string `validate:"omitempty,min=1,max=255"`
|
||||||
@@ -97,7 +94,6 @@ type UpdateProductVariantRequest struct {
|
|||||||
type ProductResponse struct {
|
type ProductResponse struct {
|
||||||
ID uuid.UUID
|
ID uuid.UUID
|
||||||
OrganizationID uuid.UUID
|
OrganizationID uuid.UUID
|
||||||
OutletID *uuid.UUID
|
|
||||||
CategoryID uuid.UUID
|
CategoryID uuid.UUID
|
||||||
CategoryName string
|
CategoryName string
|
||||||
SKU *string
|
SKU *string
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ func (p *CategoryProcessorImpl) CreateCategory(ctx context.Context, req *models.
|
|||||||
|
|
||||||
// Map request to entity
|
// Map request to entity
|
||||||
categoryEntity := mappers.CreateCategoryRequestToEntity(req)
|
categoryEntity := mappers.CreateCategoryRequestToEntity(req)
|
||||||
categoryEntity.OutletID = req.OutletID
|
|
||||||
|
|
||||||
// Create category
|
// Create category
|
||||||
if err := p.categoryRepo.Create(ctx, categoryEntity); err != nil {
|
if err := p.categoryRepo.Create(ctx, categoryEntity); err != nil {
|
||||||
@@ -87,9 +86,6 @@ func (p *CategoryProcessorImpl) UpdateCategory(ctx context.Context, id uuid.UUID
|
|||||||
|
|
||||||
// Apply updates to entity
|
// Apply updates to entity
|
||||||
mappers.UpdateCategoryEntityFromRequest(existingCategory, req)
|
mappers.UpdateCategoryEntityFromRequest(existingCategory, req)
|
||||||
if req.OutletID != nil {
|
|
||||||
existingCategory.OutletID = req.OutletID
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update category
|
// Update category
|
||||||
if err := p.categoryRepo.Update(ctx, existingCategory); err != nil {
|
if err := p.categoryRepo.Update(ctx, existingCategory); err != nil {
|
||||||
|
|||||||
@@ -29,13 +29,6 @@ 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
|
||||||
|
|
||||||
@@ -57,7 +50,9 @@ 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)
|
||||||
|
|
||||||
query = r.resolveOutletID(query, outletID, "o.outlet_id")
|
if outletID != nil {
|
||||||
|
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").
|
||||||
@@ -185,7 +180,9 @@ 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)
|
||||||
|
|
||||||
query = r.resolveOutletID(query, outletID, "o.outlet_id")
|
if outletID != nil {
|
||||||
|
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").
|
||||||
@@ -238,7 +235,9 @@ 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)
|
||||||
|
|
||||||
query = r.resolveOutletID(query, outletID, "o.outlet_id")
|
if outletID != nil {
|
||||||
|
query = query.Where("o.outlet_id = ?", *outletID)
|
||||||
|
}
|
||||||
|
|
||||||
err := query.
|
err := query.
|
||||||
Group("c.id, c.name").
|
Group("c.id, c.name").
|
||||||
@@ -268,7 +267,9 @@ 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)
|
||||||
|
|
||||||
query = r.resolveOutletID(query, outletID, "o.outlet_id")
|
if outletID != nil {
|
||||||
|
query = query.Where("o.outlet_id = ?", *outletID)
|
||||||
|
}
|
||||||
|
|
||||||
err := query.Scan(&result).Error
|
err := query.Scan(&result).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -319,7 +320,9 @@ 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)
|
||||||
|
|
||||||
summaryQuery = r.resolveOutletID(summaryQuery, outletID, "o.outlet_id")
|
if outletID != nil {
|
||||||
|
summaryQuery = summaryQuery.Where("o.outlet_id = ?", *outletID)
|
||||||
|
}
|
||||||
|
|
||||||
err := summaryQuery.Scan(&summary).Error
|
err := summaryQuery.Scan(&summary).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -371,7 +374,9 @@ func (r *AnalyticsRepositoryImpl) GetProfitLossAnalytics(ctx context.Context, or
|
|||||||
Group(timeFormat).
|
Group(timeFormat).
|
||||||
Order(timeFormat)
|
Order(timeFormat)
|
||||||
|
|
||||||
dataQuery = r.resolveOutletID(dataQuery, outletID, "o.outlet_id")
|
if outletID != nil {
|
||||||
|
dataQuery = dataQuery.Where("o.outlet_id = ?", *outletID)
|
||||||
|
}
|
||||||
|
|
||||||
err = dataQuery.Scan(&data).Error
|
err = dataQuery.Scan(&data).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -414,7 +419,9 @@ func (r *AnalyticsRepositoryImpl) GetProfitLossAnalytics(ctx context.Context, or
|
|||||||
Order("p.name ASC").
|
Order("p.name ASC").
|
||||||
Limit(1000)
|
Limit(1000)
|
||||||
|
|
||||||
productQuery = r.resolveOutletID(productQuery, outletID, "o.outlet_id")
|
if outletID != nil {
|
||||||
|
productQuery = productQuery.Where("o.outlet_id = ?", *outletID)
|
||||||
|
}
|
||||||
|
|
||||||
err = productQuery.Scan(&productData).Error
|
err = productQuery.Scan(&productData).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func (r *CategoryRepositoryImpl) Create(ctx context.Context, category *entities.
|
|||||||
|
|
||||||
func (r *CategoryRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.Category, error) {
|
func (r *CategoryRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.Category, error) {
|
||||||
var category entities.Category
|
var category entities.Category
|
||||||
err := r.db.WithContext(ctx).Preload("Outlet").First(&category, "id = ?", id).Error
|
err := r.db.WithContext(ctx).First(&category, "id = ?", id).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -34,7 +34,7 @@ func (r *CategoryRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*en
|
|||||||
|
|
||||||
func (r *CategoryRepositoryImpl) GetWithProducts(ctx context.Context, id uuid.UUID) (*entities.Category, error) {
|
func (r *CategoryRepositoryImpl) GetWithProducts(ctx context.Context, id uuid.UUID) (*entities.Category, error) {
|
||||||
var category entities.Category
|
var category entities.Category
|
||||||
err := r.db.WithContext(ctx).Preload("Products").Preload("Outlet").First(&category, "id = ?", id).Error
|
err := r.db.WithContext(ctx).Preload("Products").First(&category, "id = ?", id).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -81,7 +81,7 @@ func (r *CategoryRepositoryImpl) List(ctx context.Context, filters map[string]in
|
|||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err := query.Preload("Outlet").Order("\"order\" ASC").Limit(limit).Offset(offset).Find(&categories).Error
|
err := query.Order("\"order\" ASC").Limit(limit).Offset(offset).Find(&categories).Error
|
||||||
return categories, total, err
|
return categories, total, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -99,14 +99,3 @@ 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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -88,9 +88,6 @@ func (s *CategoryServiceImpl) ListCategories(ctx context.Context, req *contract.
|
|||||||
if req.BusinessType != "" {
|
if req.BusinessType != "" {
|
||||||
filters["business_type"] = req.BusinessType
|
filters["business_type"] = req.BusinessType
|
||||||
}
|
}
|
||||||
if req.OutletID != nil {
|
|
||||||
filters["outlet_id"] = *req.OutletID
|
|
||||||
}
|
|
||||||
if req.Search != "" {
|
if req.Search != "" {
|
||||||
filters["search"] = req.Search
|
filters["search"] = req.Search
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,171 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -85,9 +85,6 @@ func (s *ProductServiceImpl) ListProducts(ctx context.Context, req *contract.Lis
|
|||||||
if req.OrganizationID != nil {
|
if req.OrganizationID != nil {
|
||||||
filters["organization_id"] = *req.OrganizationID
|
filters["organization_id"] = *req.OrganizationID
|
||||||
}
|
}
|
||||||
if req.OutletID != nil {
|
|
||||||
filters["outlet_id"] = *req.OutletID
|
|
||||||
}
|
|
||||||
if req.CategoryID != nil {
|
if req.CategoryID != nil {
|
||||||
filters["category_id"] = *req.CategoryID
|
filters["category_id"] = *req.CategoryID
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,22 +6,8 @@ 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
|
||||||
@@ -37,7 +23,7 @@ func PaymentMethodAnalyticsContractToModel(req *contract.PaymentMethodAnalyticsR
|
|||||||
|
|
||||||
return &models.PaymentMethodAnalyticsRequest{
|
return &models.PaymentMethodAnalyticsRequest{
|
||||||
OrganizationID: req.OrganizationID,
|
OrganizationID: req.OrganizationID,
|
||||||
OutletID: parseOutletID(req.OutletID),
|
OutletID: req.OutletID,
|
||||||
DateFrom: dateFrom,
|
DateFrom: dateFrom,
|
||||||
DateTo: dateTo,
|
DateTo: dateTo,
|
||||||
GroupBy: req.GroupBy,
|
GroupBy: req.GroupBy,
|
||||||
@@ -93,7 +79,7 @@ func SalesAnalyticsContractToModel(req *contract.SalesAnalyticsRequest) *models.
|
|||||||
|
|
||||||
return &models.SalesAnalyticsRequest{
|
return &models.SalesAnalyticsRequest{
|
||||||
OrganizationID: req.OrganizationID,
|
OrganizationID: req.OrganizationID,
|
||||||
OutletID: parseOutletID(req.OutletID),
|
OutletID: req.OutletID,
|
||||||
DateFrom: dateFrom,
|
DateFrom: dateFrom,
|
||||||
DateTo: dateTo,
|
DateTo: dateTo,
|
||||||
GroupBy: req.GroupBy,
|
GroupBy: req.GroupBy,
|
||||||
@@ -153,7 +139,7 @@ func ProductAnalyticsContractToModel(req *contract.ProductAnalyticsRequest) *mod
|
|||||||
|
|
||||||
return &models.ProductAnalyticsRequest{
|
return &models.ProductAnalyticsRequest{
|
||||||
OrganizationID: req.OrganizationID,
|
OrganizationID: req.OrganizationID,
|
||||||
OutletID: parseOutletID(req.OutletID),
|
OutletID: req.OutletID,
|
||||||
DateFrom: dateFrom,
|
DateFrom: dateFrom,
|
||||||
DateTo: dateTo,
|
DateTo: dateTo,
|
||||||
Limit: req.Limit,
|
Limit: req.Limit,
|
||||||
@@ -213,7 +199,7 @@ func ProductAnalyticsPerCategoryContractToModel(req *contract.ProductAnalyticsPe
|
|||||||
|
|
||||||
return &models.ProductAnalyticsPerCategoryRequest{
|
return &models.ProductAnalyticsPerCategoryRequest{
|
||||||
OrganizationID: req.OrganizationID,
|
OrganizationID: req.OrganizationID,
|
||||||
OutletID: parseOutletID(req.OutletID),
|
OutletID: req.OutletID,
|
||||||
DateFrom: dateFrom,
|
DateFrom: dateFrom,
|
||||||
DateTo: dateTo,
|
DateTo: dateTo,
|
||||||
}
|
}
|
||||||
@@ -265,7 +251,7 @@ func DashboardAnalyticsContractToModel(req *contract.DashboardAnalyticsRequest)
|
|||||||
|
|
||||||
return &models.DashboardAnalyticsRequest{
|
return &models.DashboardAnalyticsRequest{
|
||||||
OrganizationID: req.OrganizationID,
|
OrganizationID: req.OrganizationID,
|
||||||
OutletID: parseOutletID(req.OutletID),
|
OutletID: req.OutletID,
|
||||||
DateFrom: dateFrom,
|
DateFrom: dateFrom,
|
||||||
DateTo: dateTo,
|
DateTo: dateTo,
|
||||||
}
|
}
|
||||||
@@ -360,7 +346,7 @@ func ProfitLossAnalyticsContractToModel(req *contract.ProfitLossAnalyticsRequest
|
|||||||
|
|
||||||
return &models.ProfitLossAnalyticsRequest{
|
return &models.ProfitLossAnalyticsRequest{
|
||||||
OrganizationID: req.OrganizationID,
|
OrganizationID: req.OrganizationID,
|
||||||
OutletID: parseOutletID(req.OutletID),
|
OutletID: req.OutletID,
|
||||||
DateFrom: *dateFrom,
|
DateFrom: *dateFrom,
|
||||||
DateTo: *dateTo,
|
DateTo: *dateTo,
|
||||||
GroupBy: req.GroupBy,
|
GroupBy: req.GroupBy,
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
func CreateCategoryRequestToModel(apctx *appcontext.ContextInfo, req *contract.CreateCategoryRequest) *models.CreateCategoryRequest {
|
func CreateCategoryRequestToModel(apctx *appcontext.ContextInfo, req *contract.CreateCategoryRequest) *models.CreateCategoryRequest {
|
||||||
return &models.CreateCategoryRequest{
|
return &models.CreateCategoryRequest{
|
||||||
OrganizationID: apctx.OrganizationID,
|
OrganizationID: apctx.OrganizationID,
|
||||||
OutletID: req.OutletID,
|
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
Description: req.Description,
|
Description: req.Description,
|
||||||
ImageURL: nil,
|
ImageURL: nil,
|
||||||
@@ -19,7 +18,6 @@ func CreateCategoryRequestToModel(apctx *appcontext.ContextInfo, req *contract.C
|
|||||||
|
|
||||||
func UpdateCategoryRequestToModel(req *contract.UpdateCategoryRequest) *models.UpdateCategoryRequest {
|
func UpdateCategoryRequestToModel(req *contract.UpdateCategoryRequest) *models.UpdateCategoryRequest {
|
||||||
return &models.UpdateCategoryRequest{
|
return &models.UpdateCategoryRequest{
|
||||||
OutletID: req.OutletID,
|
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
Description: req.Description,
|
Description: req.Description,
|
||||||
ImageURL: nil,
|
ImageURL: nil,
|
||||||
@@ -36,7 +34,6 @@ func CategoryModelResponseToResponse(cat *models.CategoryResponse) *contract.Cat
|
|||||||
return &contract.CategoryResponse{
|
return &contract.CategoryResponse{
|
||||||
ID: cat.ID,
|
ID: cat.ID,
|
||||||
OrganizationID: cat.OrganizationID,
|
OrganizationID: cat.OrganizationID,
|
||||||
OutletID: cat.OutletID,
|
|
||||||
Name: cat.Name,
|
Name: cat.Name,
|
||||||
Description: cat.Description,
|
Description: cat.Description,
|
||||||
BusinessType: "restaurant", // Default business type
|
BusinessType: "restaurant", // Default business type
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ func CreateProductRequestToModel(apctx *appcontext.ContextInfo, req *contract.Cr
|
|||||||
|
|
||||||
return &models.CreateProductRequest{
|
return &models.CreateProductRequest{
|
||||||
OrganizationID: apctx.OrganizationID,
|
OrganizationID: apctx.OrganizationID,
|
||||||
OutletID: req.OutletID,
|
|
||||||
CategoryID: req.CategoryID,
|
CategoryID: req.CategoryID,
|
||||||
SKU: req.SKU,
|
SKU: req.SKU,
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
@@ -61,8 +60,7 @@ func UpdateProductRequestToModel(req *contract.UpdateProductRequest) *models.Upd
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &models.UpdateProductRequest{
|
return &models.UpdateProductRequest{
|
||||||
OutletID: req.OutletID,
|
CategoryID: req.CategoryID,
|
||||||
CategoryID: req.CategoryID,
|
|
||||||
SKU: req.SKU,
|
SKU: req.SKU,
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
Description: req.Description,
|
Description: req.Description,
|
||||||
@@ -102,7 +100,6 @@ func ProductModelResponseToResponse(prod *models.ProductResponse) *contract.Prod
|
|||||||
return &contract.ProductResponse{
|
return &contract.ProductResponse{
|
||||||
ID: prod.ID,
|
ID: prod.ID,
|
||||||
OrganizationID: prod.OrganizationID,
|
OrganizationID: prod.OrganizationID,
|
||||||
OutletID: prod.OutletID,
|
|
||||||
CategoryID: prod.CategoryID,
|
CategoryID: prod.CategoryID,
|
||||||
CategoryName: prod.CategoryName,
|
CategoryName: prod.CategoryName,
|
||||||
SKU: prod.SKU,
|
SKU: prod.SKU,
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ func (v *CategoryValidatorImpl) ValidateUpdateCategoryRequest(req *contract.Upda
|
|||||||
}
|
}
|
||||||
|
|
||||||
// At least one field should be provided for update
|
// At least one field should be provided for update
|
||||||
if req.Name == nil && req.Description == nil && req.BusinessType == nil && req.Metadata == nil && req.OutletID == nil {
|
if req.Name == nil && req.Description == nil && req.BusinessType == nil && req.Metadata == nil {
|
||||||
return errors.New("at least one field must be provided for update"), constants.MissingFieldErrorCode
|
return errors.New("at least one field must be provided for update"), constants.MissingFieldErrorCode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
DROP INDEX IF EXISTS idx_categories_outlet_id;
|
|
||||||
ALTER TABLE categories DROP CONSTRAINT IF EXISTS fk_categories_outlet;
|
|
||||||
ALTER TABLE categories DROP COLUMN IF EXISTS outlet_id;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
ALTER TABLE categories ADD COLUMN outlet_id UUID;
|
|
||||||
ALTER TABLE categories ADD CONSTRAINT fk_categories_outlet FOREIGN KEY (outlet_id) REFERENCES outlets(id) ON DELETE SET NULL;
|
|
||||||
CREATE INDEX idx_categories_outlet_id ON categories(outlet_id);
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
-- Remove foreign key constraint
|
|
||||||
ALTER TABLE products DROP CONSTRAINT IF EXISTS fk_products_outlet;
|
|
||||||
|
|
||||||
-- Remove index
|
|
||||||
DROP INDEX IF EXISTS idx_products_outlet_id;
|
|
||||||
|
|
||||||
-- Remove outlet_id column
|
|
||||||
ALTER TABLE products DROP COLUMN IF EXISTS outlet_id;
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
-- Add nullable outlet_id column to products table
|
|
||||||
ALTER TABLE products ADD COLUMN outlet_id UUID;
|
|
||||||
|
|
||||||
-- Create index on outlet_id for faster queries
|
|
||||||
CREATE INDEX idx_products_outlet_id ON products (outlet_id);
|
|
||||||
|
|
||||||
-- Add foreign key constraint
|
|
||||||
ALTER TABLE products ADD CONSTRAINT fk_products_outlet FOREIGN KEY (outlet_id) REFERENCES outlets(id) ON DELETE SET NULL;
|
|
||||||
Reference in New Issue
Block a user