feat: update profit sharing

This commit is contained in:
efrilm
2026-10-04 21:54:13 +07:00
parent b59b7b4ddd
commit 117e3529aa
23 changed files with 521 additions and 95 deletions
+4 -8
View File
@@ -1,10 +1,6 @@
package constants
// Budget allocation of revenue used by the parent category cut-off report.
// The three shares are expected to add up to 100. BudgetLimitOwnerPercent is only the
// default: a parent category can override it with categories.owner_fee_percent.
const (
BudgetLimitPurchasePercent = 60.0
BudgetLimitOwnerPercent = 20.0
BudgetLimitTeamPercent = 20.0
)
// Revenue split used by the profit sharing report: the owner takes its fee as SDL and
// the team takes the rest. BudgetLimitOwnerPercent is only the default: a parent
// category can override it with categories.owner_fee_percent.
const BudgetLimitOwnerPercent = 20.0
+8 -10
View File
@@ -372,19 +372,17 @@ type BudgetCutOff struct {
}
type BudgetPercentages struct {
Purchase float64 `json:"purchase"`
Owner float64 `json:"owner"`
Team float64 `json:"team"`
Owner float64 `json:"owner"`
Team float64 `json:"team"`
}
type BudgetPeriod struct {
PeriodStart time.Time `json:"period_start"`
PeriodEnd time.Time `json:"period_end"`
Revenue float64 `json:"revenue"`
OrderCount int64 `json:"order_count"`
LimitPurchase float64 `json:"limit_purchase"`
SDL float64 `json:"sdl"`
LimitTeam float64 `json:"limit_team"`
PeriodStart time.Time `json:"period_start"`
PeriodEnd time.Time `json:"period_end"`
Revenue float64 `json:"revenue"`
OrderCount int64 `json:"order_count"`
SDL float64 `json:"sdl"`
LimitTeam float64 `json:"limit_team"`
}
type BudgetMonthPeriod struct {
+3
View File
@@ -16,6 +16,7 @@ type CreateCategoryRequest struct {
ParentID *uuid.UUID `json:"parent_id,omitempty"`
Order *int `json:"order,omitempty"`
OwnerFeePercent *float64 `json:"owner_fee_percent,omitempty"`
IsTeam *bool `json:"is_team,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
@@ -27,6 +28,7 @@ type UpdateCategoryRequest struct {
ParentID *uuid.UUID `json:"parent_id,omitempty"`
Order *int `json:"order,omitempty"`
OwnerFeePercent *float64 `json:"owner_fee_percent,omitempty"`
IsTeam *bool `json:"is_team,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
// Set when the field is sent as null (or "" for parent_id), which asks for the
@@ -99,6 +101,7 @@ type CategoryResponse struct {
BusinessType string `json:"business_type"`
Order int `json:"order"`
OwnerFeePercent *float64 `json:"owner_fee_percent"`
IsTeam bool `json:"is_team"`
Metadata map[string]interface{} `json:"metadata"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
+7 -3
View File
@@ -42,9 +42,13 @@ type Category struct {
BusinessType string `gorm:"size:50;default:'restaurant'" json:"business_type"`
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
// OwnerFeePercent overrides the owner's share in the parent category budget report; nil uses the default
OwnerFeePercent *float64 `gorm:"type:numeric(5,2)" json:"owner_fee_percent"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
OwnerFeePercent *float64 `gorm:"type:numeric(5,2)" json:"owner_fee_percent"`
// IsTeam marks a parent category as a team in the revenue split. It has no gorm
// default on purpose: GORM would turn an explicit false into the default on insert,
// so callers creating a category set it themselves.
IsTeam bool `gorm:"not null" json:"is_team"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
Products []Product `gorm:"foreignKey:CategoryID" json:"products,omitempty"`
+12
View File
@@ -58,6 +58,12 @@ func CreateCategoryRequestToEntity(req *models.CreateCategoryRequest) *entities.
metadata["image_url"] = *req.ImageURL
}
// A category is a team unless the request says otherwise
isTeam := true
if req.IsTeam != nil {
isTeam = *req.IsTeam
}
return &entities.Category{
OrganizationID: req.OrganizationID,
OutletID: req.OutletID,
@@ -66,6 +72,7 @@ func CreateCategoryRequestToEntity(req *models.CreateCategoryRequest) *entities.
Description: req.Description,
Order: req.Order,
OwnerFeePercent: req.OwnerFeePercent,
IsTeam: isTeam,
BusinessType: "restaurant",
Metadata: metadata,
}
@@ -105,6 +112,7 @@ func CategoryEntityToResponse(entity *entities.Category) *models.CategoryRespons
ImageURL: imageURL,
Order: entity.Order,
OwnerFeePercent: entity.OwnerFeePercent,
IsTeam: entity.IsTeam,
IsActive: true,
CreatedAt: entity.CreatedAt,
UpdatedAt: entity.UpdatedAt,
@@ -142,6 +150,10 @@ func UpdateCategoryEntityFromRequest(entity *entities.Category, req *models.Upda
entity.OwnerFeePercent = req.OwnerFeePercent
}
if req.IsTeam != nil {
entity.IsTeam = *req.IsTeam
}
if req.OutletID != nil {
entity.OutletID = req.OutletID
}
+34
View File
@@ -0,0 +1,34 @@
package mappers
import (
"testing"
"apskel-pos-be/internal/models"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
)
func TestCreateCategoryRequestToEntityDefaultsToTeam(t *testing.T) {
notTeam := false
tests := []struct {
name string
isTeam *bool
want bool
}{
{name: "omitted is a team", isTeam: nil, want: true},
{name: "false is not a team", isTeam: &notTeam, want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
entity := CreateCategoryRequestToEntity(&models.CreateCategoryRequest{
OrganizationID: uuid.New(),
Name: "Merchandise",
IsTeam: tt.isTeam,
})
require.Equal(t, tt.want, entity.IsTeam)
})
}
}
+1 -1
View File
@@ -82,7 +82,7 @@ func (m *AuthMiddleware) RequireRole(allowedRoles ...string) gin.HandlerFunc {
}
func (m *AuthMiddleware) RequireAdminOrManager() gin.HandlerFunc {
return m.RequireRole("superadmin", "admin", "manager", "owner", "purchasing")
return m.RequireRole("superadmin", "admin", "manager", "owner", "purchasing")
}
// RequireLoyaltyManager guards what moves or prices EnakPoint and EnakCoin: loyalty
+8 -10
View File
@@ -412,19 +412,17 @@ type BudgetCutOff struct {
}
type BudgetPercentages struct {
Purchase float64 `json:"purchase"`
Owner float64 `json:"owner"`
Team float64 `json:"team"`
Owner float64 `json:"owner"`
Team float64 `json:"team"`
}
type BudgetPeriod struct {
PeriodStart time.Time `json:"period_start"`
PeriodEnd time.Time `json:"period_end"`
Revenue float64 `json:"revenue"`
OrderCount int64 `json:"order_count"`
LimitPurchase float64 `json:"limit_purchase"`
SDL float64 `json:"sdl"`
LimitTeam float64 `json:"limit_team"`
PeriodStart time.Time `json:"period_start"`
PeriodEnd time.Time `json:"period_end"`
Revenue float64 `json:"revenue"`
OrderCount int64 `json:"order_count"`
SDL float64 `json:"sdl"`
LimitTeam float64 `json:"limit_team"`
}
type BudgetMonthPeriod struct {
+4
View File
@@ -28,6 +28,8 @@ type CreateCategoryRequest struct {
ImageURL *string `validate:"omitempty,url"`
Order int `validate:"min=0"`
OwnerFeePercent *float64 `validate:"omitempty,min=0,max=100"`
// IsTeam defaults to true when left out
IsTeam *bool
}
type UpdateCategoryRequest struct {
@@ -38,6 +40,7 @@ type UpdateCategoryRequest struct {
ParentID *uuid.UUID
Order *int `validate:"omitempty,min=0"`
OwnerFeePercent *float64 `validate:"omitempty,min=0,max=100"`
IsTeam *bool
IsActive *bool
// Clear flags remove the value; a nil pointer above only means "leave unchanged"
@@ -56,6 +59,7 @@ type CategoryResponse struct {
ImageURL *string
Order int
OwnerFeePercent *float64
IsTeam bool
IsActive bool
CreatedAt time.Time
UpdatedAt time.Time
+12 -12
View File
@@ -545,6 +545,7 @@ func (p *AnalyticsProcessorImpl) GetParentCategoryAnalyticsDetail(ctx context.Co
// The block covers a single parent category, so it reports that category's own fee
if detail.Summary != nil {
budget.Percentages.Owner = detail.Summary.OwnerFeePercent
budget.Percentages.Team = 100 - detail.Summary.OwnerFeePercent
}
return &models.ParentCategoryAnalyticsDetailResponse{
@@ -597,17 +598,17 @@ func endOfWeek(t time.Time) time.Time {
return startOfWeek(t).AddDate(0, 0, 7).Add(-time.Nanosecond)
}
// newBudgetPeriod splits a period's revenue into the spending limits. The owner limit
// is passed in because a parent category can carry its own owner fee percent.
// newBudgetPeriod splits a period's revenue between the owner and the team. The owner
// share is passed in because a parent category can carry its own owner fee percent,
// and the team takes whatever the owner does not.
func newBudgetPeriod(start, end time.Time, revenue, sdl float64, orderCount int64) models.BudgetPeriod {
return models.BudgetPeriod{
PeriodStart: start,
PeriodEnd: end,
Revenue: revenue,
OrderCount: orderCount,
LimitPurchase: revenue * constants.BudgetLimitPurchasePercent / 100,
SDL: sdl,
LimitTeam: revenue * constants.BudgetLimitTeamPercent / 100,
PeriodStart: start,
PeriodEnd: end,
Revenue: revenue,
OrderCount: orderCount,
SDL: sdl,
LimitTeam: revenue - sdl,
}
}
@@ -621,9 +622,8 @@ func (p *AnalyticsProcessorImpl) buildBudgetCutOff(ctx context.Context, organiza
budget := models.BudgetCutOff{
Percentages: models.BudgetPercentages{
Purchase: constants.BudgetLimitPurchasePercent,
Owner: constants.BudgetLimitOwnerPercent,
Team: constants.BudgetLimitTeamPercent,
Owner: constants.BudgetLimitOwnerPercent,
Team: 100 - constants.BudgetLimitOwnerPercent,
},
CutOffFrom: cutOffFrom,
CutOffTo: cutOffTo,
@@ -607,7 +607,7 @@ func TestPaymentMethodAnalytics_EnakPointIsNotCashIn(t *testing.T) {
}
// A parent category with its own owner fee percent moves the owner limit away from the
// default share, while purchase and team stay on the default split of revenue.
// default share, and the team limit shrinks by the same amount.
func TestAnalyticsProcessorParentCategoryUsesOwnerFeePercent(t *testing.T) {
monday := time.Date(2026, 9, 28, 0, 0, 0, 0, time.UTC)
processor := NewAnalyticsProcessorImpl(&analyticsRepositoryStub{
@@ -636,8 +636,9 @@ func TestAnalyticsProcessorParentCategoryUsesOwnerFeePercent(t *testing.T) {
require.Len(t, result.Budget.Weekly, 1)
require.Equal(t, float64(900), result.Budget.Weekly[0].SDL)
require.Equal(t, float64(1800), result.Budget.Weekly[0].LimitPurchase)
require.Equal(t, float64(600), result.Budget.Weekly[0].LimitTeam)
require.Equal(t, float64(2100), result.Budget.Weekly[0].LimitTeam)
require.Equal(t, float64(20), result.Budget.Percentages.Owner)
require.Equal(t, float64(80), result.Budget.Percentages.Team)
require.Len(t, result.Budget.Monthly, 1)
require.Equal(t, float64(900), result.Budget.Monthly[0].SDL)
require.Equal(t, float64(900), result.Budget.Total.SDL)
+11 -11
View File
@@ -93,18 +93,17 @@ func TestBuildBudgetCutOffAppliesLimits(t *testing.T) {
require.NoError(t, err)
require.Len(t, budget.Weekly, 1)
// 60 / 20 of the week's revenue; the owner limit is whatever the repository
// summed from each parent category's own fee percent
// The owner limit is whatever the repository summed from each parent category's
// own fee percent, and the team takes the rest of the week's revenue
week := budget.Weekly[0]
require.Equal(t, float64(6_000_000), week.LimitPurchase)
require.Equal(t, float64(2_500_000), week.SDL)
require.Equal(t, float64(2_000_000), week.LimitTeam)
require.Equal(t, float64(7_500_000), week.LimitTeam)
require.Equal(t, int64(120), week.OrderCount)
// Totals mirror the single week
require.Equal(t, week.Revenue, budget.Total.Revenue)
require.Equal(t, week.LimitPurchase, budget.Total.LimitPurchase)
require.Equal(t, week.SDL, budget.Total.SDL)
require.Equal(t, week.LimitTeam, budget.Total.LimitTeam)
}
func TestBuildBudgetCutOffAccumulatesMonthlyFromWeeks(t *testing.T) {
@@ -112,8 +111,8 @@ func TestBuildBudgetCutOffAccumulatesMonthlyFromWeeks(t *testing.T) {
first := time.Date(2026, 8, 3, 0, 0, 0, 0, loc)
stub := &analyticsRepositoryStub{budgetCutOffWeeks: []*entities.BudgetCutOffWeek{
{WeekStart: first, Revenue: 10_000_000, OrderCount: 100},
{WeekStart: first.AddDate(0, 0, 7), Revenue: 5_000_000, OrderCount: 60},
{WeekStart: first, Revenue: 10_000_000, OrderCount: 100, SDL: 2_000_000},
{WeekStart: first.AddDate(0, 0, 7), Revenue: 5_000_000, OrderCount: 60, SDL: 1_000_000},
}}
processor := &AnalyticsProcessorImpl{analyticsRepo: stub}
@@ -127,9 +126,10 @@ func TestBuildBudgetCutOffAccumulatesMonthlyFromWeeks(t *testing.T) {
require.Equal(t, float64(15_000_000), month.Revenue)
require.Equal(t, int64(160), month.OrderCount)
// The month limit is the accumulation of its weeks
require.Equal(t, float64(9_000_000), month.LimitPurchase)
require.Equal(t, budget.Weekly[0].LimitPurchase+budget.Weekly[1].LimitPurchase, month.LimitPurchase)
// The month limits are the accumulation of its weeks
require.Equal(t, float64(3_000_000), month.SDL)
require.Equal(t, float64(12_000_000), month.LimitTeam)
require.Equal(t, budget.Weekly[0].LimitTeam+budget.Weekly[1].LimitTeam, month.LimitTeam)
}
func TestBuildBudgetCutOffEmitsWeeksWithoutSales(t *testing.T) {
@@ -147,7 +147,7 @@ func TestBuildBudgetCutOffEmitsWeeksWithoutSales(t *testing.T) {
require.Len(t, budget.Weekly, 3)
require.Zero(t, budget.Weekly[0].Revenue)
require.Zero(t, budget.Weekly[0].LimitPurchase)
require.Zero(t, budget.Weekly[0].LimitTeam)
require.Zero(t, budget.Weekly[1].Revenue)
require.Equal(t, float64(4_000_000), budget.Weekly[2].Revenue)
require.Equal(t, float64(4_000_000), budget.Total.Revenue)
+12 -5
View File
@@ -11,12 +11,13 @@ import (
"github.com/google/uuid"
)
// Teams are the parent product categories, plus Pusat for spending that belongs to
// no single team. Both purchase orders and cash advances are charged to one, so the rules
// for picking and storing a team live here rather than in either processor.
// Teams are the parent product categories flagged is_team, plus Pusat for spending
// that belongs to no single team. Both purchase orders and cash advances are charged to
// one, so the rules for picking and storing a team live here rather than in either
// processor.
// listTeams returns the teams money can be charged to: the parent categories of the
// outlet in scope, followed by Pusat. Pusat has no category row, so it is appended
// listTeams returns the teams money can be charged to: the team parent categories of
// the outlet in scope, followed by Pusat. Pusat has no category row, so it is appended
// here rather than read from the database.
func listTeams(ctx context.Context, categoryRepo CategoryRepository, organizationID uuid.UUID, outletID *uuid.UUID) (*models.ListPurchaseTeamsResponse, error) {
categories, err := categoryRepo.ListParentCategories(ctx, organizationID, outletID)
@@ -26,6 +27,9 @@ func listTeams(ctx context.Context, categoryRepo CategoryRepository, organizatio
teams := make([]models.PurchaseTeam, 0, len(categories)+1)
for _, category := range categories {
if !category.IsTeam {
continue
}
categoryID := category.ID
teams = append(teams, models.PurchaseTeam{
Scope: constants.PurchaseTeamScopeCategory,
@@ -75,6 +79,9 @@ func resolveTeamSelection(ctx context.Context, categoryRepo CategoryRepository,
if category.ParentID != nil {
return nil, nil, fmt.Errorf("team must be a parent category")
}
if !category.IsTeam {
return nil, nil, fmt.Errorf("category %s is not a team", category.Name)
}
// Categories without an outlet are shared, so only an outlet-specific
// category has to match the outlet the record is booked against.
if category.OutletID != nil && outletID != nil && *category.OutletID != *outletID {
+64
View File
@@ -0,0 +1,64 @@
package processor
import (
"context"
"errors"
"testing"
"apskel-pos-be/internal/constants"
"apskel-pos-be/internal/entities"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
)
// teamCategoryRepositoryStub serves the two lookups the team rules make. The embedded
// interface is left nil, so any other call panics and shows up in the test.
type teamCategoryRepositoryStub struct {
CategoryRepository
categories []*entities.Category
}
func (s *teamCategoryRepositoryStub) ListParentCategories(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) ([]*entities.Category, error) {
return s.categories, nil
}
func (s *teamCategoryRepositoryStub) GetByID(ctx context.Context, id uuid.UUID) (*entities.Category, error) {
for _, category := range s.categories {
if category.ID == id {
return category, nil
}
}
return nil, errors.New("record not found")
}
func TestListTeamsSkipsParentCategoriesThatAreNotTeams(t *testing.T) {
organizationID := uuid.New()
food := &entities.Category{ID: uuid.New(), OrganizationID: organizationID, Name: "Food", IsTeam: true}
merch := &entities.Category{ID: uuid.New(), OrganizationID: organizationID, Name: "Merchandise", IsTeam: false}
repo := &teamCategoryRepositoryStub{categories: []*entities.Category{food, merch}}
result, err := listTeams(context.Background(), repo, organizationID, nil)
require.NoError(t, err)
require.Len(t, result.Teams, 2)
require.Equal(t, "Food", result.Teams[0].Name)
require.Equal(t, &food.ID, result.Teams[0].CategoryID)
require.Equal(t, constants.PurchaseTeamCentralName, result.Teams[1].Name)
}
func TestResolveTeamSelectionRejectsParentCategoryThatIsNotATeam(t *testing.T) {
organizationID := uuid.New()
food := &entities.Category{ID: uuid.New(), OrganizationID: organizationID, Name: "Food", IsTeam: true}
merch := &entities.Category{ID: uuid.New(), OrganizationID: organizationID, Name: "Merchandise", IsTeam: false}
repo := &teamCategoryRepositoryStub{categories: []*entities.Category{food, merch}}
scope := constants.PurchaseTeamScopeCategory
resolvedScope, resolvedID, err := resolveTeamSelection(context.Background(), repo, organizationID, nil, &scope, &food.ID)
require.NoError(t, err)
require.Equal(t, constants.PurchaseTeamScopeCategory, *resolvedScope)
require.Equal(t, food.ID, *resolvedID)
_, _, err = resolveTeamSelection(context.Background(), repo, organizationID, nil, &scope, &merch.ID)
require.EqualError(t, err, "category Merchandise is not a team")
}
+12 -2
View File
@@ -614,6 +614,8 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalyticsPerParentCategory(ctx conte
Joins("JOIN categories c ON p.category_id = c.id").
// Categories without a parent roll up to themselves, so top-level categories still appear
Joins("JOIN categories pc ON pc.id = COALESCE(c.parent_id, c.id)").
// Profit sharing only covers the parent categories that are teams
Where("pc.is_team = ?", true).
Joins("JOIN orders o ON oi.order_id = o.id").
Joins("LEFT JOIN (SELECT pr.product_id, SUM(pr.quantity * (1 + COALESCE(pr.waste_percentage, 0)/100.0) * i.cost) as hpp_per_unit FROM product_recipes pr JOIN ingredients i ON pr.ingredient_id = i.id GROUP BY pr.product_id) shpp ON shpp.product_id = p.id").
Joins("LEFT JOIN (?) mahpp ON mahpp.product_id = p.id",
@@ -665,6 +667,7 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalyticsPerParentCategory(ctx conte
Joins("JOIN categories c ON p.category_id = c.id").
Joins("JOIN categories pc ON pc.id = COALESCE(c.parent_id, c.id)").
Joins("JOIN orders o ON oi.order_id = o.id").
Where("pc.is_team = ?", true).
Where("o.organization_id = ?", organizationID).
Where("o.is_void = ?", false).
Where("o.is_refund = ?", false).
@@ -739,11 +742,12 @@ func (r *AnalyticsRepositoryImpl) GetParentCategoryAnalyticsDetail(ctx context.C
var parent struct {
ID uuid.UUID
Name string
IsTeam bool
OwnerFeePercent float64
}
if err := r.db.WithContext(ctx).
Table("categories").
Select("id, name, COALESCE(owner_fee_percent, ?) as owner_fee_percent", constants.BudgetLimitOwnerPercent).
Select("id, name, is_team, COALESCE(owner_fee_percent, ?) as owner_fee_percent", constants.BudgetLimitOwnerPercent).
Where("id = ? AND organization_id = ?", parentCategoryID, organizationID).
Scan(&parent).Error; err != nil {
return nil, err
@@ -751,6 +755,9 @@ func (r *AnalyticsRepositoryImpl) GetParentCategoryAnalyticsDetail(ctx context.C
if parent.ID == uuid.Nil {
return nil, fmt.Errorf("category not found")
}
if !parent.IsTeam {
return nil, fmt.Errorf("category %s is not a team", parent.Name)
}
detail := &entities.ParentCategoryAnalyticsDetail{
ParentCategoryID: parent.ID,
@@ -849,7 +856,8 @@ func (r *AnalyticsRepositoryImpl) GetParentCategoryAnalyticsDetail(ctx context.C
// GetBudgetCutOffWeekly buckets revenue and cost of goods sold into Monday-to-Sunday
// weeks. DATE_TRUNC('week') is ISO, so the buckets start on Monday, and the connection
// runs with TimeZone=Asia/Jakarta so the boundaries land on local midnight.
// A nil parentCategoryID covers every category in scope.
// A nil parentCategoryID covers every team in scope; parent categories that are not a
// team stay out of the split.
func (r *AnalyticsRepositoryImpl) GetBudgetCutOffWeekly(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, parentCategoryID *uuid.UUID, cutOffFrom, cutOffTo time.Time) ([]*entities.BudgetCutOffWeek, error) {
var results []*entities.BudgetCutOffWeek
@@ -868,6 +876,8 @@ func (r *AnalyticsRepositoryImpl) GetBudgetCutOffWeekly(ctx context.Context, org
// The owner fee lives on the parent category; a category without a parent is its own group
Joins("JOIN categories pc ON pc.id = COALESCE(c.parent_id, c.id)").
Joins("JOIN orders o ON oi.order_id = o.id").
// Only teams take part in the revenue split
Where("pc.is_team = ?", true).
Where("o.organization_id = ?", organizationID).
Where("o.is_void = ?", false).
Where("o.is_refund = ?", false).
+3 -3
View File
@@ -63,7 +63,7 @@ type Router struct {
customerDeviceHandler *handler.CustomerDeviceHandler
customerOutletHandler *handler.CustomerOutletHandler
customerOrderHandler *handler.CustomerOrderHandler
authMiddleware *middleware.AuthMiddleware
authMiddleware *middleware.AuthMiddleware
customerAuthMiddleware *middleware.CustomerAuthMiddleware
redisClient *redis.Client
}
@@ -374,8 +374,8 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
analytics.GET("/purchasing", r.analyticsHandler.GetPurchasingAnalytics)
analytics.GET("/products", r.analyticsHandler.GetProductAnalytics)
analytics.GET("/categories", r.analyticsHandler.GetProductAnalyticsPerCategory)
analytics.GET("/parent-categories", r.analyticsHandler.GetProductAnalyticsPerParentCategory)
analytics.GET("/parent-categories/:parent_category_id", r.analyticsHandler.GetParentCategoryAnalyticsDetail)
analytics.GET("/profit-sharing", r.analyticsHandler.GetProductAnalyticsPerParentCategory)
analytics.GET("/profit-sharing/:parent_category_id", r.analyticsHandler.GetParentCategoryAnalyticsDetail)
analytics.GET("/dashboard", r.analyticsHandler.GetDashboardAnalytics)
analytics.GET("/profit-loss", r.analyticsHandler.GetProfitLossAnalytics)
analytics.GET("/exclusive-summary/period", r.analyticsHandler.GetExclusiveSummaryPeriod)
+16 -16
View File
@@ -224,23 +224,23 @@ func getPLPctByID(rows []models.ProfitLossSummaryRow, id string) float64 {
// profitLossReportData holds data for the profit/loss PDF template
type profitLossReportData struct {
OrganizationName string
MonthName string
ReportDate string
ReportDateUpper string
TotalPenjualan string
TotalBiaya string
LabaRugi string
LabaRugiClass string
LabaRugiValueClass string
LabaRugiMtd string
LabaRugiMtdClass string
OrganizationName string
MonthName string
ReportDate string
ReportDateUpper string
TotalPenjualan string
TotalBiaya string
LabaRugi string
LabaRugiClass string
LabaRugiValueClass string
LabaRugiMtd string
LabaRugiMtdClass string
LabaRugiMtdValueClass string
MainSummary []profitLossSummaryRowView
PurchasingItems []profitLossPurchasingItem
PurchasingTotal string
GeneratedBy string
PrintTime string
MainSummary []profitLossSummaryRowView
PurchasingItems []profitLossPurchasingItem
PurchasingTotal string
GeneratedBy string
PrintTime string
}
type profitLossSummaryRowView struct {
+8 -10
View File
@@ -446,13 +446,12 @@ func parentCategoryTopProductModelToContract(product *models.ParentCategoryTopPr
// budgetPeriodModelToContract converts one budget period to contract
func budgetPeriodModelToContract(period models.BudgetPeriod) contract.BudgetPeriod {
return contract.BudgetPeriod{
PeriodStart: period.PeriodStart,
PeriodEnd: period.PeriodEnd,
Revenue: period.Revenue,
OrderCount: period.OrderCount,
LimitPurchase: period.LimitPurchase,
SDL: period.SDL,
LimitTeam: period.LimitTeam,
PeriodStart: period.PeriodStart,
PeriodEnd: period.PeriodEnd,
Revenue: period.Revenue,
OrderCount: period.OrderCount,
SDL: period.SDL,
LimitTeam: period.LimitTeam,
}
}
@@ -474,9 +473,8 @@ func BudgetCutOffModelToContract(budget models.BudgetCutOff) contract.BudgetCutO
return contract.BudgetCutOff{
Percentages: contract.BudgetPercentages{
Purchase: budget.Percentages.Purchase,
Owner: budget.Percentages.Owner,
Team: budget.Percentages.Team,
Owner: budget.Percentages.Owner,
Team: budget.Percentages.Team,
},
CutOffFrom: budget.CutOffFrom,
CutOffTo: budget.CutOffTo,
@@ -20,6 +20,7 @@ func CreateCategoryRequestToModel(apctx *appcontext.ContextInfo, req *contract.C
ImageURL: nil,
Order: order,
OwnerFeePercent: req.OwnerFeePercent,
IsTeam: req.IsTeam,
}
}
@@ -32,6 +33,7 @@ func UpdateCategoryRequestToModel(req *contract.UpdateCategoryRequest) *models.U
ParentID: req.ParentID,
Order: req.Order,
OwnerFeePercent: req.OwnerFeePercent,
IsTeam: req.IsTeam,
IsActive: nil,
ClearParentID: req.ClearParentID,
@@ -55,6 +57,7 @@ func CategoryModelResponseToResponse(cat *models.CategoryResponse) *contract.Cat
BusinessType: "restaurant",
Order: cat.Order,
OwnerFeePercent: cat.OwnerFeePercent,
IsTeam: cat.IsTeam,
Metadata: map[string]interface{}{},
CreatedAt: cat.CreatedAt,
UpdatedAt: cat.UpdatedAt,
+1 -1
View File
@@ -63,7 +63,7 @@ func (v *CategoryValidatorImpl) ValidateUpdateCategoryRequest(req *contract.Upda
}
// At least one field should be provided for update
if req.Name == nil && req.Description == nil && req.BusinessType == nil && req.ParentID == nil && req.Metadata == nil && req.OwnerFeePercent == nil && req.Order == nil && req.OutletID == nil && !req.ClearParentID && !req.ClearOwnerFeePercent {
if req.Name == nil && req.Description == nil && req.BusinessType == nil && req.ParentID == nil && req.Metadata == nil && req.OwnerFeePercent == nil && req.IsTeam == nil && req.Order == nil && req.OutletID == nil && !req.ClearParentID && !req.ClearOwnerFeePercent {
return errors.New("at least one field must be provided for update"), constants.MissingFieldErrorCode
}