Compare commits

..
Author SHA1 Message Date
ryan 5fa9fc5070 Merge branch 'feature/exclusive-summary-revise' into feature/exclusive-summary 2026-06-17 18:32:11 +07:00
ryan 07e8be0521 Add account balance 2026-06-17 17:53:01 +07:00
ryan 4b7b225f58 Add exclusive summary endpoint 2026-06-17 17:35:37 +07:00
29 changed files with 229 additions and 925 deletions
+1 -2
View File
@@ -351,8 +351,6 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@@ -382,6 +380,7 @@ go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+6 -13
View File
@@ -140,12 +140,12 @@ type PurchasingIngredientData struct {
} }
type PurchasingVendorData struct { type PurchasingVendorData struct {
VendorID *uuid.UUID `json:"vendor_id"` VendorID uuid.UUID `json:"vendor_id"`
VendorName string `json:"vendor_name"` VendorName string `json:"vendor_name"`
TotalCost float64 `json:"total_cost"` TotalCost float64 `json:"total_cost"`
PurchaseOrderCount int64 `json:"purchase_order_count"` PurchaseOrderCount int64 `json:"purchase_order_count"`
IngredientCount int64 `json:"ingredient_count"` IngredientCount int64 `json:"ingredient_count"`
Quantity float64 `json:"quantity"` Quantity float64 `json:"quantity"`
} }
// ProductAnalyticsRequest represents the request for product analytics // ProductAnalyticsRequest represents the request for product analytics
@@ -339,13 +339,6 @@ type ExclusiveSummaryMonthlyRequest struct {
Month string `form:"month" validate:"required"` Month string `form:"month" validate:"required"`
} }
type ExclusiveSummaryMTDRequest struct {
OrganizationID uuid.UUID
OutletID *string `form:"outlet_id,omitempty"`
DateTo string `form:"date_to" validate:"required"`
ExcludeGajiStaffFromReimburse bool `form:"exclude_gaji_staff_from_reimburse"`
}
type ExclusiveSummaryPeriodResponse struct { type ExclusiveSummaryPeriodResponse struct {
OrganizationID uuid.UUID `json:"organization_id"` OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id,omitempty"` OutletID *uuid.UUID `json:"outlet_id,omitempty"`
+2 -3
View File
@@ -7,7 +7,7 @@ import (
) )
type CreatePurchaseOrderRequest struct { type CreatePurchaseOrderRequest struct {
VendorID *uuid.UUID `json:"vendor_id,omitempty" validate:"omitempty"` VendorID uuid.UUID `json:"vendor_id" validate:"required"`
PONumber string `json:"po_number" validate:"required,min=1,max=50"` PONumber string `json:"po_number" validate:"required,min=1,max=50"`
TransactionDate string `json:"transaction_date" validate:"required"` // Format: YYYY-MM-DD TransactionDate string `json:"transaction_date" validate:"required"` // Format: YYYY-MM-DD
DueDate *string `json:"due_date,omitempty" validate:"omitempty"` // Format: YYYY-MM-DD DueDate *string `json:"due_date,omitempty" validate:"omitempty"` // Format: YYYY-MM-DD
@@ -52,8 +52,7 @@ type UpdatePurchaseOrderItemRequest struct {
type PurchaseOrderResponse struct { type PurchaseOrderResponse 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"` VendorID uuid.UUID `json:"vendor_id"`
VendorID *uuid.UUID `json:"vendor_id"`
PONumber string `json:"po_number"` PONumber string `json:"po_number"`
TransactionDate time.Time `json:"transaction_date"` TransactionDate time.Time `json:"transaction_date"`
DueDate *time.Time `json:"due_date"` DueDate *time.Time `json:"due_date"`
+11 -12
View File
@@ -72,12 +72,12 @@ type PurchasingIngredientData struct {
} }
type PurchasingVendorData struct { type PurchasingVendorData struct {
VendorID *uuid.UUID `json:"vendor_id"` VendorID uuid.UUID `json:"vendor_id"`
VendorName string `json:"vendor_name"` VendorName string `json:"vendor_name"`
TotalCost float64 `json:"total_cost"` TotalCost float64 `json:"total_cost"`
PurchaseOrderCount int64 `json:"purchase_order_count"` PurchaseOrderCount int64 `json:"purchase_order_count"`
IngredientCount int64 `json:"ingredient_count"` IngredientCount int64 `json:"ingredient_count"`
Quantity float64 `json:"quantity"` Quantity float64 `json:"quantity"`
} }
type ProductAnalytics struct { type ProductAnalytics struct {
@@ -218,10 +218,9 @@ type ExclusiveSummaryDailyTransaction struct {
} }
type ExclusiveSummaryBankBalance struct { type ExclusiveSummaryBankBalance struct {
Bank string Bank string
OpeningBalance *float64 AccountType string
IncomingMutation *float64 OpeningBalance float64
OutgoingMutation *float64 ClosingBalance float64
ClosingBalance *float64 Description *string
Notes *string
} }
+1 -3
View File
@@ -11,8 +11,7 @@ import (
type PurchaseOrder struct { type PurchaseOrder 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" json:"organization_id" validate:"required"` OrganizationID uuid.UUID `gorm:"type:uuid;not null" json:"organization_id" validate:"required"`
OutletID *uuid.UUID `gorm:"type:uuid;index" json:"outlet_id" validate:"omitempty"` VendorID uuid.UUID `gorm:"type:uuid;not null" json:"vendor_id" validate:"required"`
VendorID *uuid.UUID `gorm:"type:uuid" json:"vendor_id" validate:"omitempty"`
PONumber string `gorm:"not null;size:50" json:"po_number" validate:"required,min=1,max=50"` PONumber string `gorm:"not null;size:50" json:"po_number" validate:"required,min=1,max=50"`
TransactionDate time.Time `gorm:"type:date;not null" json:"transaction_date" validate:"required"` TransactionDate time.Time `gorm:"type:date;not null" json:"transaction_date" validate:"required"`
DueDate *time.Time `gorm:"type:date" json:"due_date" validate:"omitempty"` DueDate *time.Time `gorm:"type:date" json:"due_date" validate:"omitempty"`
@@ -24,7 +23,6 @@ type PurchaseOrder 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"`
Vendor *Vendor `gorm:"foreignKey:VendorID" json:"vendor,omitempty"` Vendor *Vendor `gorm:"foreignKey:VendorID" json:"vendor,omitempty"`
Items []PurchaseOrderItem `gorm:"foreignKey:PurchaseOrderID" json:"items,omitempty"` Items []PurchaseOrderItem `gorm:"foreignKey:PurchaseOrderID" json:"items,omitempty"`
Attachments []PurchaseOrderAttachment `gorm:"foreignKey:PurchaseOrderID" json:"attachments,omitempty"` Attachments []PurchaseOrderAttachment `gorm:"foreignKey:PurchaseOrderID" json:"attachments,omitempty"`
-28
View File
@@ -266,31 +266,3 @@ func (h *AnalyticsHandler) GetExclusiveSummaryMonthly(c *gin.Context) {
contractResp := transformer.ExclusiveSummaryMonthlyModelToContract(response) contractResp := transformer.ExclusiveSummaryMonthlyModelToContract(response)
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "AnalyticsHandler::GetExclusiveSummaryMonthly") util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "AnalyticsHandler::GetExclusiveSummaryMonthly")
} }
func (h *AnalyticsHandler) GetExclusiveSummaryMTD(c *gin.Context) {
ctx := c.Request.Context()
contextInfo := appcontext.FromGinContext(ctx)
var req contract.ExclusiveSummaryMTDRequest
if err := c.ShouldBindQuery(&req); err != nil {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "AnalyticsHandler::GetExclusiveSummaryMTD", err.Error())}), "AnalyticsHandler::GetExclusiveSummaryMTD")
return
}
req.OrganizationID = contextInfo.OrganizationID
req.OutletID = h.resolveOutletID(c, contextInfo.OutletID)
modelReq, err := transformer.ExclusiveSummaryMTDContractToModel(&req)
if err != nil {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "AnalyticsHandler::GetExclusiveSummaryMTD", err.Error())}), "AnalyticsHandler::GetExclusiveSummaryMTD")
return
}
response, err := h.analyticsService.GetExclusiveSummaryMTD(ctx, modelReq)
if err != nil {
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "AnalyticsHandler::GetExclusiveSummaryMTD", err.Error())}), "AnalyticsHandler::GetExclusiveSummaryMTD")
return
}
contractResp := transformer.ExclusiveSummaryPeriodModelToContract(response)
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "AnalyticsHandler::GetExclusiveSummaryMTD")
}
@@ -13,7 +13,6 @@ func PurchaseOrderEntityToModel(entity *entities.PurchaseOrder) *models.Purchase
return &models.PurchaseOrder{ return &models.PurchaseOrder{
ID: entity.ID, ID: entity.ID,
OrganizationID: entity.OrganizationID, OrganizationID: entity.OrganizationID,
OutletID: entity.OutletID,
VendorID: entity.VendorID, VendorID: entity.VendorID,
PONumber: entity.PONumber, PONumber: entity.PONumber,
TransactionDate: entity.TransactionDate, TransactionDate: entity.TransactionDate,
@@ -35,7 +34,6 @@ func PurchaseOrderModelToEntity(model *models.PurchaseOrder) *entities.PurchaseO
return &entities.PurchaseOrder{ return &entities.PurchaseOrder{
ID: model.ID, ID: model.ID,
OrganizationID: model.OrganizationID, OrganizationID: model.OrganizationID,
OutletID: model.OutletID,
VendorID: model.VendorID, VendorID: model.VendorID,
PONumber: model.PONumber, PONumber: model.PONumber,
TransactionDate: model.TransactionDate, TransactionDate: model.TransactionDate,
@@ -57,7 +55,6 @@ func PurchaseOrderEntityToResponse(entity *entities.PurchaseOrder) *models.Purch
response := &models.PurchaseOrderResponse{ response := &models.PurchaseOrderResponse{
ID: entity.ID, ID: entity.ID,
OrganizationID: entity.OrganizationID, OrganizationID: entity.OrganizationID,
OutletID: entity.OutletID,
VendorID: entity.VendorID, VendorID: entity.VendorID,
PONumber: entity.PONumber, PONumber: entity.PONumber,
TransactionDate: entity.TransactionDate, TransactionDate: entity.TransactionDate,
+6 -13
View File
@@ -150,12 +150,12 @@ type PurchasingIngredientData struct {
// PurchasingVendorData represents purchasing analytics for a vendor // PurchasingVendorData represents purchasing analytics for a vendor
type PurchasingVendorData struct { type PurchasingVendorData struct {
VendorID *uuid.UUID `json:"vendor_id"` VendorID uuid.UUID `json:"vendor_id"`
VendorName string `json:"vendor_name"` VendorName string `json:"vendor_name"`
TotalCost float64 `json:"total_cost"` TotalCost float64 `json:"total_cost"`
PurchaseOrderCount int64 `json:"purchase_order_count"` PurchaseOrderCount int64 `json:"purchase_order_count"`
IngredientCount int64 `json:"ingredient_count"` IngredientCount int64 `json:"ingredient_count"`
Quantity float64 `json:"quantity"` Quantity float64 `json:"quantity"`
} }
// ProductAnalyticsRequest represents the request for product analytics // ProductAnalyticsRequest represents the request for product analytics
@@ -349,13 +349,6 @@ type ExclusiveSummaryMonthlyRequest struct {
Month time.Time `validate:"required"` Month time.Time `validate:"required"`
} }
type ExclusiveSummaryMTDRequest struct {
OrganizationID uuid.UUID `validate:"required"`
OutletID *uuid.UUID `validate:"omitempty"`
DateTo time.Time `validate:"required"`
ExcludeGajiStaffFromReimburse bool `validate:"omitempty"`
}
type ExclusiveSummaryPeriodResponse struct { type ExclusiveSummaryPeriodResponse struct {
OrganizationID uuid.UUID `json:"organization_id"` OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id,omitempty"` OutletID *uuid.UUID `json:"outlet_id,omitempty"`
+3 -6
View File
@@ -9,8 +9,7 @@ import (
type PurchaseOrder struct { type PurchaseOrder 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"` VendorID uuid.UUID `json:"vendor_id"`
VendorID *uuid.UUID `json:"vendor_id"`
PONumber string `json:"po_number"` PONumber string `json:"po_number"`
TransactionDate time.Time `json:"transaction_date"` TransactionDate time.Time `json:"transaction_date"`
DueDate *time.Time `json:"due_date"` DueDate *time.Time `json:"due_date"`
@@ -45,8 +44,7 @@ type PurchaseOrderAttachment struct {
type PurchaseOrderResponse struct { type PurchaseOrderResponse 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"` VendorID uuid.UUID `json:"vendor_id"`
VendorID *uuid.UUID `json:"vendor_id"`
PONumber string `json:"po_number"` PONumber string `json:"po_number"`
TransactionDate time.Time `json:"transaction_date"` TransactionDate time.Time `json:"transaction_date"`
DueDate *time.Time `json:"due_date"` DueDate *time.Time `json:"due_date"`
@@ -86,8 +84,7 @@ type PurchaseOrderAttachmentResponse struct {
} }
type CreatePurchaseOrderRequest struct { type CreatePurchaseOrderRequest struct {
VendorID *uuid.UUID `json:"vendor_id,omitempty"` VendorID uuid.UUID `json:"vendor_id"`
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
PONumber string `json:"po_number"` PONumber string `json:"po_number"`
TransactionDate time.Time `json:"transaction_date"` TransactionDate time.Time `json:"transaction_date"`
DueDate *time.Time `json:"due_date,omitempty"` DueDate *time.Time `json:"due_date,omitempty"`
+35 -29
View File
@@ -9,6 +9,8 @@ import (
"apskel-pos-be/internal/entities" "apskel-pos-be/internal/entities"
"apskel-pos-be/internal/models" "apskel-pos-be/internal/models"
"apskel-pos-be/internal/repository" "apskel-pos-be/internal/repository"
"github.com/google/uuid"
) )
type AnalyticsProcessor interface { type AnalyticsProcessor interface {
@@ -21,7 +23,6 @@ type AnalyticsProcessor interface {
GetProfitLossAnalytics(ctx context.Context, req *models.ProfitLossAnalyticsRequest) (*models.ProfitLossAnalyticsResponse, error) GetProfitLossAnalytics(ctx context.Context, req *models.ProfitLossAnalyticsRequest) (*models.ProfitLossAnalyticsResponse, error)
GetExclusiveSummaryPeriod(ctx context.Context, req *models.ExclusiveSummaryPeriodRequest) (*models.ExclusiveSummaryPeriodResponse, error) GetExclusiveSummaryPeriod(ctx context.Context, req *models.ExclusiveSummaryPeriodRequest) (*models.ExclusiveSummaryPeriodResponse, error)
GetExclusiveSummaryMonthly(ctx context.Context, req *models.ExclusiveSummaryMonthlyRequest) (*models.ExclusiveSummaryMonthlyResponse, error) GetExclusiveSummaryMonthly(ctx context.Context, req *models.ExclusiveSummaryMonthlyRequest) (*models.ExclusiveSummaryMonthlyResponse, error)
GetExclusiveSummaryMTD(ctx context.Context, req *models.ExclusiveSummaryMTDRequest) (*models.ExclusiveSummaryPeriodResponse, error)
} }
type AnalyticsProcessorImpl struct { type AnalyticsProcessorImpl struct {
@@ -678,8 +679,14 @@ func (p *AnalyticsProcessorImpl) GetExclusiveSummaryMonthly(ctx context.Context,
return nil, err return nil, err
} }
periods := make([]models.ExclusiveSummaryMonthlyPeriod, 0) bankBalance, err := p.buildExclusiveSummaryBankBalances(ctx, req.OrganizationID, req.OutletID)
for _, bucket := range buildExclusiveSummaryMonthlyBuckets(monthStart) { if err != nil {
return nil, err
}
buckets := buildExclusiveSummaryMonthlyBuckets(monthStart)
periods := make([]models.ExclusiveSummaryMonthlyPeriod, 0, len(buckets))
for _, bucket := range buckets {
period, err := p.buildExclusiveSummaryPeriod(ctx, &models.ExclusiveSummaryPeriodRequest{ period, err := p.buildExclusiveSummaryPeriod(ctx, &models.ExclusiveSummaryPeriodRequest{
OrganizationID: req.OrganizationID, OrganizationID: req.OrganizationID,
OutletID: req.OutletID, OutletID: req.OutletID,
@@ -701,23 +708,6 @@ func (p *AnalyticsProcessorImpl) GetExclusiveSummaryMonthly(ctx context.Context,
}) })
} }
bankBalances, err := p.analyticsRepo.GetExclusiveSummaryBankBalances(ctx, req.OrganizationID, req.OutletID)
if err != nil {
return nil, fmt.Errorf("failed to get exclusive summary bank balances: %w", err)
}
bankBalance := make([]models.ExclusiveSummaryBankBalance, len(bankBalances))
for i, item := range bankBalances {
bankBalance[i] = models.ExclusiveSummaryBankBalance{
Bank: item.Bank,
OpeningBalance: item.OpeningBalance,
IncomingMutation: item.IncomingMutation,
OutgoingMutation: item.OutgoingMutation,
ClosingBalance: item.ClosingBalance,
Notes: item.Notes,
}
}
return &models.ExclusiveSummaryMonthlyResponse{ return &models.ExclusiveSummaryMonthlyResponse{
OrganizationID: req.OrganizationID, OrganizationID: req.OrganizationID,
OutletID: req.OutletID, OutletID: req.OutletID,
@@ -736,16 +726,32 @@ func (p *AnalyticsProcessorImpl) GetExclusiveSummaryMonthly(ctx context.Context,
}, nil }, nil
} }
func (p *AnalyticsProcessorImpl) GetExclusiveSummaryMTD(ctx context.Context, req *models.ExclusiveSummaryMTDRequest) (*models.ExclusiveSummaryPeriodResponse, error) { func (p *AnalyticsProcessorImpl) buildExclusiveSummaryBankBalances(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) ([]models.ExclusiveSummaryBankBalance, error) {
mtdStart := time.Date(req.DateTo.Year(), req.DateTo.Month(), 1, 0, 0, 0, 0, req.DateTo.Location()) balances, err := p.analyticsRepo.GetExclusiveSummaryBankBalances(ctx, organizationID, outletID)
if err != nil {
return nil, fmt.Errorf("failed to get exclusive summary bank balances: %w", err)
}
return p.buildExclusiveSummaryPeriod(ctx, &models.ExclusiveSummaryPeriodRequest{ result := make([]models.ExclusiveSummaryBankBalance, len(balances))
OrganizationID: req.OrganizationID, for i, balance := range balances {
OutletID: req.OutletID, openingBalance := balance.OpeningBalance
DateFrom: mtdStart, closingBalance := balance.ClosingBalance
DateTo: req.DateTo, notes := strings.TrimSpace(balance.AccountType)
ExcludeGajiStaffFromReimburse: req.ExcludeGajiStaffFromReimburse, if balance.Description != nil && strings.TrimSpace(*balance.Description) != "" {
}) notes = strings.TrimSpace(*balance.Description)
}
result[i] = models.ExclusiveSummaryBankBalance{
Bank: balance.Bank,
OpeningBalance: &openingBalance,
ClosingBalance: &closingBalance,
}
if notes != "" {
result[i].Notes = &notes
}
}
return result, nil
} }
func (p *AnalyticsProcessorImpl) buildExclusiveSummaryPeriod(ctx context.Context, req *models.ExclusiveSummaryPeriodRequest) (*models.ExclusiveSummaryPeriodResponse, error) { func (p *AnalyticsProcessorImpl) buildExclusiveSummaryPeriod(ctx context.Context, req *models.ExclusiveSummaryPeriodRequest) (*models.ExclusiveSummaryPeriodResponse, error) {
+57 -132
View File
@@ -13,14 +13,11 @@ import (
) )
type analyticsRepositoryStub struct { type analyticsRepositoryStub struct {
purchasingResult *entities.PurchasingAnalytics purchasingResult *entities.PurchasingAnalytics
profitLossResult *entities.ProfitLossAnalytics profitLossResult *entities.ProfitLossAnalytics
exclusiveSummaryResults []*entities.ExclusiveSummaryAnalytics exclusiveResult *entities.ExclusiveSummaryAnalytics
bankBalances []entities.ExclusiveSummaryBankBalance bankBalances []entities.ExclusiveSummaryBankBalance
profitLossGroup string profitLossGroup string
exclusiveSummaryCalls int
exclusiveSummaryFrom []time.Time
exclusiveSummaryTo []time.Time
} }
func (analyticsRepositoryStub) GetPaymentMethodAnalytics(context.Context, uuid.UUID, *uuid.UUID, time.Time, time.Time) ([]*entities.PaymentMethodAnalytics, error) { func (analyticsRepositoryStub) GetPaymentMethodAnalytics(context.Context, uuid.UUID, *uuid.UUID, time.Time, time.Time) ([]*entities.PaymentMethodAnalytics, error) {
@@ -52,19 +49,11 @@ func (s analyticsRepositoryStub) GetProfitLossAnalytics(_ context.Context, _ uui
return s.profitLossResult, nil return s.profitLossResult, nil
} }
func (s *analyticsRepositoryStub) GetExclusiveSummaryAnalytics(_ context.Context, _ uuid.UUID, _ *uuid.UUID, dateFrom, dateTo time.Time) (*entities.ExclusiveSummaryAnalytics, error) { func (s analyticsRepositoryStub) GetExclusiveSummaryAnalytics(context.Context, uuid.UUID, *uuid.UUID, time.Time, time.Time) (*entities.ExclusiveSummaryAnalytics, error) {
s.exclusiveSummaryFrom = append(s.exclusiveSummaryFrom, dateFrom) return s.exclusiveResult, nil
s.exclusiveSummaryTo = append(s.exclusiveSummaryTo, dateTo)
if s.exclusiveSummaryCalls < len(s.exclusiveSummaryResults) {
result := s.exclusiveSummaryResults[s.exclusiveSummaryCalls]
s.exclusiveSummaryCalls++
return result, nil
}
s.exclusiveSummaryCalls++
return &entities.ExclusiveSummaryAnalytics{}, nil
} }
func (s *analyticsRepositoryStub) GetExclusiveSummaryBankBalances(context.Context, uuid.UUID, *uuid.UUID) ([]entities.ExclusiveSummaryBankBalance, error) { func (s analyticsRepositoryStub) GetExclusiveSummaryBankBalances(context.Context, uuid.UUID, *uuid.UUID) ([]entities.ExclusiveSummaryBankBalance, error) {
return s.bankBalances, nil return s.bankBalances, nil
} }
@@ -92,7 +81,7 @@ func TestAnalyticsProcessorGetPurchasingAnalyticsPassesOutletName(t *testing.T)
outletID := uuid.New() outletID := uuid.New()
outletName := "Main Outlet" outletName := "Main Outlet"
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC) now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
processor := NewAnalyticsProcessorImpl(&analyticsRepositoryStub{ processor := NewAnalyticsProcessorImpl(analyticsRepositoryStub{
purchasingResult: &entities.PurchasingAnalytics{ purchasingResult: &entities.PurchasingAnalytics{
OutletName: &outletName, OutletName: &outletName,
Summary: entities.PurchasingSummary{ Summary: entities.PurchasingSummary{
@@ -145,7 +134,7 @@ func TestAnalyticsProcessorGetProfitLossAnalyticsMapsOverviewAndReportFields(t *
productID := uuid.New() productID := uuid.New()
categoryID := uuid.New() categoryID := uuid.New()
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC) now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
processor := NewAnalyticsProcessorImpl(&analyticsRepositoryStub{ processor := NewAnalyticsProcessorImpl(analyticsRepositoryStub{
profitLossResult: &entities.ProfitLossAnalytics{ profitLossResult: &entities.ProfitLossAnalytics{
Summary: entities.ProfitLossSummary{ Summary: entities.ProfitLossSummary{
TotalRevenue: 1000, TotalRevenue: 1000,
@@ -217,7 +206,7 @@ func TestAnalyticsProcessorGetProfitLossAnalyticsMapsOverviewAndReportFields(t *
func TestAnalyticsProcessorGetProfitLossAnalyticsDynamicExpenseCategories(t *testing.T) { func TestAnalyticsProcessorGetProfitLossAnalyticsDynamicExpenseCategories(t *testing.T) {
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC) now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
processor := NewAnalyticsProcessorImpl(&analyticsRepositoryStub{ processor := NewAnalyticsProcessorImpl(analyticsRepositoryStub{
profitLossResult: &entities.ProfitLossAnalytics{ profitLossResult: &entities.ProfitLossAnalytics{
Summary: entities.ProfitLossSummary{ Summary: entities.ProfitLossSummary{
TotalRevenue: 10000, TotalRevenue: 10000,
@@ -295,28 +284,22 @@ func TestAnalyticsProcessorGetProfitLossAnalyticsDynamicExpenseCategories(t *tes
require.True(t, result.MainSummary[6].IsBold) require.True(t, result.MainSummary[6].IsBold)
} }
func TestAnalyticsProcessorGetExclusiveSummaryPeriodCalculatesTotalsAndReimburse(t *testing.T) { func TestAnalyticsProcessorGetExclusiveSummaryPeriodCalculatesSummaryAndReimburse(t *testing.T) {
now := time.Date(2026, 5, 26, 0, 0, 0, 0, time.UTC) now := time.Date(2026, 5, 26, 0, 0, 0, 0, time.UTC)
processor := NewAnalyticsProcessorImpl(&analyticsRepositoryStub{ processor := NewAnalyticsProcessorImpl(analyticsRepositoryStub{
exclusiveSummaryResults: []*entities.ExclusiveSummaryAnalytics{ exclusiveResult: &entities.ExclusiveSummaryAnalytics{
{ SalesTotal: 35619000,
SalesTotal: 1000, HPPBreakdown: []entities.ExclusiveSummaryCategoryTotal{
HPPBreakdown: []entities.ExclusiveSummaryCategoryTotal{ {CategoryCode: "hpp_nusantara", CategoryName: "Nusantara", Amount: 19010552},
{CategoryCode: "RAW", CategoryName: "Raw", Amount: 400}, },
}, OperationalExpenseBreakdown: []entities.ExclusiveSummaryCategoryTotal{
OperationalExpenseBreakdown: []entities.ExclusiveSummaryCategoryTotal{ {CategoryCode: "biaya_gaji_staff", CategoryName: "Gaji Staff", Amount: 48203333},
{CategoryCode: "GAJI", CategoryName: "Gaji", Amount: 250}, {CategoryCode: "biaya_gaji_dw", CategoryName: "Gaji DW", Amount: 3555000},
{CategoryCode: "OPS", CategoryName: "Operasional", Amount: 100}, {CategoryCode: "biaya_lain", CategoryName: "Biaya Lain-lain", Amount: 1608605},
}, },
DailySummary: []entities.ExclusiveSummaryDailySummary{ DailyTransactions: []entities.ExclusiveSummaryDailyTransaction{
{Date: now, TransactionCount: 3, TotalCost: 750}, {Date: now, CategoryCode: "biaya_gaji_staff", CategoryName: "Gaji Staff", Description: "gaji kary", Amount: 48203333, Source: "purchase_order"},
}, {Date: now, CategoryCode: "biaya_gaji_dw", CategoryName: "Gaji DW", Description: "DW", Amount: 3555000, Source: "purchase_order"},
DailyTransactions: []entities.ExclusiveSummaryDailyTransaction{
{Date: now, CategoryCode: "RAW", CategoryName: "Raw", Description: "beras", Amount: 400, Source: "purchase_order"},
{Date: now, CategoryCode: "GAJI", CategoryName: "Gaji", Description: "gaji karyawan", Amount: 200, Source: "purchase_order"},
{Date: now, CategoryCode: "GAJI", CategoryName: "Gaji", Description: "DW", Amount: 50, Source: "purchase_order"},
{Date: now, CategoryCode: "OPS", CategoryName: "Operasional", Description: "atk", Amount: 100, Source: "purchase_order"},
},
}, },
}, },
}, expenseRepositoryStub{}) }, expenseRepositoryStub{})
@@ -330,45 +313,38 @@ func TestAnalyticsProcessorGetExclusiveSummaryPeriodCalculatesTotalsAndReimburse
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, result) require.NotNil(t, result)
require.Equal(t, float64(1000), result.Summary.Sales) require.Equal(t, float64(35619000), result.Summary.Sales)
require.Equal(t, float64(400), result.Summary.HPP) require.Equal(t, float64(19010552), result.Summary.HPP)
require.Equal(t, float64(600), result.Summary.GrossProfit) require.Equal(t, float64(16608448), result.Summary.GrossProfit)
require.Equal(t, float64(350), result.Summary.OperationalExpensesTotal) require.Equal(t, float64(51758333), result.Summary.SalaryTotal)
require.Equal(t, float64(750), result.Summary.TotalCost) require.Equal(t, float64(3555000), result.Summary.SalaryDW)
require.Equal(t, float64(250), result.Summary.NetProfit) require.Equal(t, float64(48203333), result.Summary.SalaryStaff)
require.Equal(t, float64(250), result.Summary.SalaryTotal) require.Equal(t, float64(53366938), result.Summary.OperationalExpensesTotal)
require.Equal(t, float64(50), result.Summary.SalaryDW) require.Equal(t, float64(72377490), result.Summary.TotalCost)
require.Equal(t, float64(200), result.Summary.SalaryStaff) require.Equal(t, float64(-36758490), result.Summary.NetProfit)
require.Equal(t, float64(100), result.Summary.OtherOperationalExpenses) require.Equal(t, float64(48203333), result.Reimburse.ExcludedSalaryStaff)
require.Equal(t, float64(200), result.Reimburse.ExcludedSalaryStaff) require.Equal(t, float64(24174157), result.Reimburse.TotalReimburse)
require.Equal(t, float64(550), result.Reimburse.TotalReimburse)
require.Len(t, result.HPPBreakdown, 1)
require.Equal(t, float64(100), result.HPPBreakdown[0].Percentage)
require.Len(t, result.DailySummary, 1)
require.Len(t, result.DailyTransactions, 4)
} }
func TestAnalyticsProcessorGetExclusiveSummaryMonthlyBuildsSummaryAndBuckets(t *testing.T) { func TestAnalyticsProcessorGetExclusiveSummaryMonthlyBuildsCalendarBucketsAndBankTemplate(t *testing.T) {
location, err := time.LoadLocation("Asia/Jakarta") location, err := time.LoadLocation("Asia/Jakarta")
require.NoError(t, err) require.NoError(t, err)
month := time.Date(2026, 5, 1, 0, 0, 0, 0, location) month := time.Date(2026, 5, 1, 0, 0, 0, 0, location)
openingBalance := 5000000.0 processor := NewAnalyticsProcessorImpl(analyticsRepositoryStub{
closingBalance := 5000000.0 exclusiveResult: &entities.ExclusiveSummaryAnalytics{
notes := "Main cash account for daily transactions" SalesTotal: 1000,
stub := &analyticsRepositoryStub{ HPPBreakdown: []entities.ExclusiveSummaryCategoryTotal{
exclusiveSummaryResults: []*entities.ExclusiveSummaryAnalytics{ {CategoryCode: "hpp", CategoryName: "HPP", Amount: 400},
{SalesTotal: 1000, HPPBreakdown: []entities.ExclusiveSummaryCategoryTotal{{Amount: 400}}, OperationalExpenseBreakdown: []entities.ExclusiveSummaryCategoryTotal{{Amount: 100}}}, },
{SalesTotal: 100, HPPBreakdown: []entities.ExclusiveSummaryCategoryTotal{{Amount: 40}}}, OperationalExpenseBreakdown: []entities.ExclusiveSummaryCategoryTotal{
{SalesTotal: 200, HPPBreakdown: []entities.ExclusiveSummaryCategoryTotal{{Amount: 80}}}, {CategoryCode: "ops", CategoryName: "OPS", Amount: 100},
{SalesTotal: 300, HPPBreakdown: []entities.ExclusiveSummaryCategoryTotal{{Amount: 120}}}, },
{SalesTotal: 400, HPPBreakdown: []entities.ExclusiveSummaryCategoryTotal{{Amount: 160}}},
{SalesTotal: 500, HPPBreakdown: []entities.ExclusiveSummaryCategoryTotal{{Amount: 200}}},
}, },
bankBalances: []entities.ExclusiveSummaryBankBalance{ bankBalances: []entities.ExclusiveSummaryBankBalance{
{Bank: "Cash and Bank", OpeningBalance: &openingBalance, ClosingBalance: &closingBalance, Notes: &notes}, {Bank: "Rekening Bank", AccountType: "wallet", OpeningBalance: 1000, ClosingBalance: 2500},
{Bank: "Kas Utama", AccountType: "cash", OpeningBalance: 3000, ClosingBalance: 3500},
}, },
} }, expenseRepositoryStub{})
processor := NewAnalyticsProcessorImpl(stub, expenseRepositoryStub{})
result, err := processor.GetExclusiveSummaryMonthly(context.Background(), &models.ExclusiveSummaryMonthlyRequest{ result, err := processor.GetExclusiveSummaryMonthly(context.Background(), &models.ExclusiveSummaryMonthlyRequest{
OrganizationID: uuid.New(), OrganizationID: uuid.New(),
@@ -379,70 +355,19 @@ func TestAnalyticsProcessorGetExclusiveSummaryMonthlyBuildsSummaryAndBuckets(t *
require.NotNil(t, result) require.NotNil(t, result)
require.Equal(t, "2026-05", result.Month) require.Equal(t, "2026-05", result.Month)
require.Equal(t, float64(1000), result.Summary.TotalSales) require.Equal(t, float64(1000), result.Summary.TotalSales)
require.Equal(t, float64(400), result.Summary.HPP)
require.Equal(t, float64(500), result.Summary.NetProfit) require.Equal(t, float64(500), result.Summary.NetProfit)
require.InDelta(t, float64(50), result.Summary.NetProfitMargin, 0.0001)
require.Len(t, result.Periods, 5) require.Len(t, result.Periods, 5)
require.Equal(t, "1 - 3 Mei", result.Periods[0].Label) require.Equal(t, "1 - 3 Mei", result.Periods[0].Label)
require.Equal(t, "25 - 31 Mei", result.Periods[4].Label) require.Equal(t, "25 - 31 Mei", result.Periods[4].Label)
require.Len(t, result.BankBalance, 1) require.Len(t, result.BankBalance, 2)
require.Equal(t, "Cash and Bank", result.BankBalance[0].Bank) require.Equal(t, "Rekening Bank", result.BankBalance[0].Bank)
require.NotNil(t, result.BankBalance[0].OpeningBalance) require.NotNil(t, result.BankBalance[0].OpeningBalance)
require.Equal(t, openingBalance, *result.BankBalance[0].OpeningBalance) require.Equal(t, float64(1000), *result.BankBalance[0].OpeningBalance)
require.NotNil(t, result.BankBalance[0].ClosingBalance) require.NotNil(t, result.BankBalance[0].ClosingBalance)
require.Equal(t, closingBalance, *result.BankBalance[0].ClosingBalance) require.Equal(t, float64(2500), *result.BankBalance[0].ClosingBalance)
require.NotNil(t, result.BankBalance[0].Notes)
require.Equal(t, "wallet", *result.BankBalance[0].Notes)
require.Nil(t, result.BankBalance[0].IncomingMutation) require.Nil(t, result.BankBalance[0].IncomingMutation)
require.Nil(t, result.BankBalance[0].OutgoingMutation) require.Nil(t, result.BankBalance[0].OutgoingMutation)
require.NotNil(t, result.BankBalance[0].Notes) require.Equal(t, "Kas Utama", result.BankBalance[1].Bank)
require.Equal(t, notes, *result.BankBalance[0].Notes)
require.Equal(t, 6, stub.exclusiveSummaryCalls)
}
func TestAnalyticsProcessorGetExclusiveSummaryMTDBuildsMonthToDateBreakdown(t *testing.T) {
location, err := time.LoadLocation("Asia/Jakarta")
require.NoError(t, err)
dateTo := time.Date(2026, 6, 18, 23, 59, 59, int(time.Second-time.Nanosecond), location)
stub := &analyticsRepositoryStub{
exclusiveSummaryResults: []*entities.ExclusiveSummaryAnalytics{
{
SalesTotal: 1000,
HPPBreakdown: []entities.ExclusiveSummaryCategoryTotal{
{CategoryCode: "RAW", CategoryName: "Raw Material", Amount: 400},
},
OperationalExpenseBreakdown: []entities.ExclusiveSummaryCategoryTotal{
{CategoryCode: "OPS", CategoryName: "Operational", Amount: 100},
},
DailySummary: []entities.ExclusiveSummaryDailySummary{
{Date: dateTo, TransactionCount: 2, TotalCost: 500},
},
DailyTransactions: []entities.ExclusiveSummaryDailyTransaction{
{Date: dateTo, CategoryCode: "RAW", CategoryName: "Raw Material", Description: "beras", Amount: 400, Source: "purchase_order"},
{Date: dateTo, CategoryCode: "OPS", CategoryName: "Operational", Description: "atk", Amount: 100, Source: "expense"},
},
},
},
}
processor := NewAnalyticsProcessorImpl(stub, expenseRepositoryStub{})
result, err := processor.GetExclusiveSummaryMTD(context.Background(), &models.ExclusiveSummaryMTDRequest{
OrganizationID: uuid.New(),
DateTo: dateTo,
})
require.NoError(t, err)
require.NotNil(t, result)
require.Len(t, stub.exclusiveSummaryFrom, 1)
require.Equal(t, time.Date(2026, 6, 1, 0, 0, 0, 0, location), stub.exclusiveSummaryFrom[0])
require.Equal(t, dateTo, stub.exclusiveSummaryTo[0])
require.Equal(t, stub.exclusiveSummaryFrom[0], result.Period.DateFrom)
require.Equal(t, dateTo, result.Period.DateTo)
require.Equal(t, float64(1000), result.Summary.Sales)
require.Equal(t, float64(400), result.Summary.HPP)
require.Equal(t, float64(500), result.Summary.TotalCost)
require.Equal(t, float64(500), result.Summary.NetProfit)
require.Len(t, result.HPPBreakdown, 1)
require.Equal(t, float64(100), result.HPPBreakdown[0].Percentage)
require.Len(t, result.OperationalExpenseBreakdown, 1)
require.Len(t, result.DailySummary, 1)
require.Len(t, result.DailyTransactions, 2)
} }
+13 -32
View File
@@ -11,8 +11,8 @@ import (
) )
type PurchaseOrderProcessor interface { type PurchaseOrderProcessor interface {
CreatePurchaseOrder(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, req *models.CreatePurchaseOrderRequest) (*models.PurchaseOrderResponse, error) CreatePurchaseOrder(ctx context.Context, organizationID uuid.UUID, req *models.CreatePurchaseOrderRequest) (*models.PurchaseOrderResponse, error)
UpdatePurchaseOrder(ctx context.Context, id, organizationID uuid.UUID, outletID *uuid.UUID, req *models.UpdatePurchaseOrderRequest) (*models.PurchaseOrderResponse, error) UpdatePurchaseOrder(ctx context.Context, id, organizationID uuid.UUID, req *models.UpdatePurchaseOrderRequest) (*models.PurchaseOrderResponse, error)
DeletePurchaseOrder(ctx context.Context, id, organizationID uuid.UUID) error DeletePurchaseOrder(ctx context.Context, id, organizationID uuid.UUID) error
GetPurchaseOrderByID(ctx context.Context, id, organizationID uuid.UUID) (*models.PurchaseOrderResponse, error) GetPurchaseOrderByID(ctx context.Context, id, organizationID uuid.UUID) (*models.PurchaseOrderResponse, error)
ListPurchaseOrders(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, page, limit int) ([]*models.PurchaseOrderResponse, int, error) ListPurchaseOrders(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, page, limit int) ([]*models.PurchaseOrderResponse, int, error)
@@ -54,13 +54,11 @@ func NewPurchaseOrderProcessorImpl(
} }
} }
func (p *PurchaseOrderProcessorImpl) CreatePurchaseOrder(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, req *models.CreatePurchaseOrderRequest) (*models.PurchaseOrderResponse, error) { func (p *PurchaseOrderProcessorImpl) CreatePurchaseOrder(ctx context.Context, organizationID uuid.UUID, req *models.CreatePurchaseOrderRequest) (*models.PurchaseOrderResponse, error) {
// Check if vendor exists and belongs to organization when provided. // Check if vendor exists and belongs to organization
if req.VendorID != nil { _, err := p.vendorRepo.GetByIDAndOrganizationID(ctx, req.VendorID, organizationID)
_, err := p.vendorRepo.GetByIDAndOrganizationID(ctx, *req.VendorID, organizationID) if err != nil {
if err != nil { return nil, fmt.Errorf("vendor not found: %w", err)
return nil, fmt.Errorf("vendor not found: %w", err)
}
} }
// Check if PO number already exists in organization // Check if PO number already exists in organization
@@ -109,13 +107,12 @@ func (p *PurchaseOrderProcessorImpl) CreatePurchaseOrder(ctx context.Context, or
// Calculate total amount // Calculate total amount
totalAmount := 0.0 totalAmount := 0.0
for _, item := range req.Items { for _, item := range req.Items {
totalAmount += calculatePurchaseOrderItemTotal(item.Quantity, item.Amount) totalAmount += item.Amount
} }
// Create purchase order entity // Create purchase order entity
poEntity := &entities.PurchaseOrder{ poEntity := &entities.PurchaseOrder{
OrganizationID: organizationID, OrganizationID: organizationID,
OutletID: outletID,
VendorID: req.VendorID, VendorID: req.VendorID,
PONumber: req.PONumber, PONumber: req.PONumber,
TransactionDate: req.TransactionDate, TransactionDate: req.TransactionDate,
@@ -176,15 +173,12 @@ func (p *PurchaseOrderProcessorImpl) CreatePurchaseOrder(ctx context.Context, or
return mappers.PurchaseOrderEntityToResponse(createdPO), nil return mappers.PurchaseOrderEntityToResponse(createdPO), nil
} }
func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrder(ctx context.Context, id, organizationID uuid.UUID, outletID *uuid.UUID, req *models.UpdatePurchaseOrderRequest) (*models.PurchaseOrderResponse, error) { func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrder(ctx context.Context, id, organizationID uuid.UUID, req *models.UpdatePurchaseOrderRequest) (*models.PurchaseOrderResponse, error) {
// Get existing purchase order // Get existing purchase order
poEntity, err := p.purchaseOrderRepo.GetByIDAndOrganizationID(ctx, id, organizationID) poEntity, err := p.purchaseOrderRepo.GetByIDAndOrganizationID(ctx, id, organizationID)
if err != nil { if err != nil {
return nil, fmt.Errorf("purchase order not found: %w", err) return nil, fmt.Errorf("purchase order not found: %w", err)
} }
if poEntity.OutletID == nil && outletID != nil {
poEntity.OutletID = outletID
}
// Check if vendor exists and belongs to organization (if vendor is being updated) // Check if vendor exists and belongs to organization (if vendor is being updated)
if req.VendorID != nil { if req.VendorID != nil {
@@ -192,7 +186,7 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrder(ctx context.Context, id
if err != nil { if err != nil {
return nil, fmt.Errorf("vendor not found: %w", err) return nil, fmt.Errorf("vendor not found: %w", err)
} }
poEntity.VendorID = req.VendorID poEntity.VendorID = *req.VendorID
} }
// Check if PO number already exists (if PO number is being updated) // Check if PO number already exists (if PO number is being updated)
@@ -283,7 +277,7 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrder(ctx context.Context, id
UnitID: unitID, UnitID: unitID,
Amount: amount, Amount: amount,
} }
totalAmount += calculatePurchaseOrderItemTotal(quantity, amount) totalAmount += amount
} }
// Delete and recreate only after all replacement items are valid. // Delete and recreate only after all replacement items are valid.
@@ -453,7 +447,7 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrderStatus(ctx context.Conte
// Calculate unit cost in ingredient's base unit // Calculate unit cost in ingredient's base unit
unitCost := 0.0 unitCost := 0.0
if quantityToAdd > 0 { if quantityToAdd > 0 {
unitCost = calculatePurchaseOrderItemTotal(item.Quantity, item.Amount) / quantityToAdd unitCost = item.Amount / quantityToAdd
} }
// Create inventory movement for ingredient purchase // Create inventory movement for ingredient purchase
@@ -482,12 +476,7 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrderStatus(ctx context.Conte
} }
// Update the purchase order status // Update the purchase order status
statusOutletID := po.OutletID err = p.purchaseOrderRepo.UpdateStatus(ctx, id, status)
if statusOutletID == nil && outletID != uuid.Nil {
statusOutletID = &outletID
}
err = p.purchaseOrderRepo.UpdateStatusAndOutlet(ctx, id, status, statusOutletID)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to update purchase order status: %w", err) return nil, fmt.Errorf("failed to update purchase order status: %w", err)
} }
@@ -517,11 +506,3 @@ func (p *PurchaseOrderProcessorImpl) validatePurchaseCategory(ctx context.Contex
return category, nil return category, nil
} }
func calculatePurchaseOrderItemTotal(quantity *float64, amount float64) float64 {
if quantity == nil {
return amount
}
return *quantity * amount
}
@@ -19,7 +19,6 @@ type PurchaseOrderRepository interface {
GetByStatus(ctx context.Context, organizationID uuid.UUID, status string) ([]*entities.PurchaseOrder, error) GetByStatus(ctx context.Context, organizationID uuid.UUID, status string) ([]*entities.PurchaseOrder, error)
GetOverdue(ctx context.Context, organizationID uuid.UUID) ([]*entities.PurchaseOrder, error) GetOverdue(ctx context.Context, organizationID uuid.UUID) ([]*entities.PurchaseOrder, error)
UpdateStatus(ctx context.Context, id uuid.UUID, status string) error UpdateStatus(ctx context.Context, id uuid.UUID, status string) error
UpdateStatusAndOutlet(ctx context.Context, id uuid.UUID, status string, outletID *uuid.UUID) error
UpdateTotalAmount(ctx context.Context, id uuid.UUID, totalAmount float64) error UpdateTotalAmount(ctx context.Context, id uuid.UUID, totalAmount float64) error
CreateItem(ctx context.Context, item *entities.PurchaseOrderItem) error CreateItem(ctx context.Context, item *entities.PurchaseOrderItem) error
UpdateItem(ctx context.Context, item *entities.PurchaseOrderItem) error UpdateItem(ctx context.Context, item *entities.PurchaseOrderItem) error
+46 -325
View File
@@ -2,7 +2,6 @@ package repository
import ( import (
"context" "context"
"sort"
"time" "time"
"apskel-pos-be/internal/entities" "apskel-pos-be/internal/entities"
@@ -40,18 +39,6 @@ func (r *AnalyticsRepositoryImpl) resolveOutletID(query *gorm.DB, outletID *uuid
return query return query
} }
func purchaseOrderItemTotalAmountSQL() string {
return "CASE WHEN pc.type = '" + string(entities.PurchaseCategoryTypeRawMaterial) + "' THEN COALESCE(poi.quantity, 0) * poi.amount ELSE poi.amount END"
}
func purchaseOrderRawMaterialAmountSQL() string {
return "CASE WHEN pc.type = '" + string(entities.PurchaseCategoryTypeRawMaterial) + "' THEN " + purchaseOrderItemTotalAmountSQL() + " ELSE 0 END"
}
func purchaseOrderExpenseAmountSQL() string {
return "CASE WHEN pc.type = '" + string(entities.PurchaseCategoryTypeExpense) + "' THEN " + purchaseOrderItemTotalAmountSQL() + " ELSE 0 END"
}
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
@@ -166,23 +153,18 @@ func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx contex
summaryQuery := r.db.WithContext(ctx). summaryQuery := r.db.WithContext(ctx).
Table("purchase_orders po"). Table("purchase_orders po").
Select(` Select(`
COALESCE(SUM(`+purchaseOrderItemTotalAmountSQL()+`), 0) as total_purchases, COALESCE(SUM(poi.amount), 0) as total_purchases,
COALESCE(SUM(`+purchaseOrderRawMaterialAmountSQL()+`), 0) as raw_material_purchases,
COALESCE(SUM(`+purchaseOrderExpenseAmountSQL()+`), 0) as expense_purchases,
COUNT(DISTINCT po.id) as total_purchase_orders, COUNT(DISTINCT po.id) as total_purchase_orders,
COUNT(DISTINCT CASE WHEN pc.type = '`+string(entities.PurchaseCategoryTypeRawMaterial)+`' THEN po.id END) as raw_material_purchase_orders,
COUNT(CASE WHEN pc.type = '`+string(entities.PurchaseCategoryTypeExpense)+`' THEN poi.id END) as expense_count,
COALESCE(SUM(poi.quantity), 0) as total_quantity, COALESCE(SUM(poi.quantity), 0) as total_quantity,
CASE CASE
WHEN COUNT(DISTINCT po.id) > 0 WHEN COUNT(DISTINCT po.id) > 0
THEN COALESCE(SUM(`+purchaseOrderItemTotalAmountSQL()+`), 0) / COUNT(DISTINCT po.id) THEN COALESCE(SUM(poi.amount), 0) / COUNT(DISTINCT po.id)
ELSE 0 ELSE 0
END as average_purchase_order_value, END as average_purchase_order_value,
COUNT(DISTINCT i.id) as total_ingredients, COUNT(DISTINCT i.id) as total_ingredients,
COUNT(DISTINCT COALESCE(po.vendor_id::text, 'no-vendor')) as total_vendors COUNT(DISTINCT po.vendor_id) as total_vendors
`). `).
Joins("LEFT JOIN purchase_order_items poi ON poi.purchase_order_id = po.id"). Joins("LEFT JOIN purchase_order_items poi ON poi.purchase_order_id = po.id").
Joins("LEFT JOIN purchase_categories pc ON poi.purchase_category_id = pc.id").
Joins("LEFT JOIN ingredients i ON poi.ingredient_id = i.id"). Joins("LEFT JOIN ingredients i ON poi.ingredient_id = i.id").
Joins("LEFT JOIN units u ON poi.unit_id = u.id"). Joins("LEFT JOIN units u ON poi.unit_id = u.id").
Where("po.organization_id = ?", organizationID). Where("po.organization_id = ?", organizationID).
@@ -211,23 +193,17 @@ func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx contex
Table("purchase_orders po"). Table("purchase_orders po").
Select(` Select(`
`+dateFormat+` as date, `+dateFormat+` as date,
COALESCE(SUM(`+purchaseOrderItemTotalAmountSQL()+`), 0) as purchases, COALESCE(SUM(poi.amount), 0) as purchases,
COALESCE(SUM(`+purchaseOrderRawMaterialAmountSQL()+`), 0) as raw_material_purchases,
COALESCE(SUM(`+purchaseOrderExpenseAmountSQL()+`), 0) as expense_purchases,
COUNT(DISTINCT po.id) as purchase_orders, COUNT(DISTINCT po.id) as purchase_orders,
COUNT(DISTINCT CASE WHEN pc.type = '`+string(entities.PurchaseCategoryTypeRawMaterial)+`' THEN po.id END) as raw_material_purchase_orders,
COUNT(CASE WHEN pc.type = '`+string(entities.PurchaseCategoryTypeExpense)+`' THEN poi.id END) as expense_count,
COALESCE(SUM(poi.quantity), 0) as quantity, COALESCE(SUM(poi.quantity), 0) as quantity,
COUNT(DISTINCT i.id) as ingredients, COUNT(DISTINCT i.id) as ingredients,
COUNT(DISTINCT COALESCE(po.vendor_id::text, 'no-vendor')) as vendors COUNT(DISTINCT po.vendor_id) as vendors
`). `).
Joins("LEFT JOIN purchase_order_items poi ON poi.purchase_order_id = po.id"). Joins("LEFT JOIN purchase_order_items poi ON poi.purchase_order_id = po.id").
Joins("LEFT JOIN purchase_categories pc ON poi.purchase_category_id = pc.id").
Joins("LEFT JOIN ingredients i ON poi.ingredient_id = i.id"). Joins("LEFT JOIN ingredients i ON poi.ingredient_id = i.id").
Joins("LEFT JOIN units u ON poi.unit_id = u.id"). Joins("LEFT JOIN units u ON poi.unit_id = u.id").
Where("po.organization_id = ?", organizationID). Where("po.organization_id = ?", organizationID).
Where("po.status != ?", "cancelled"). Where("po.status != ?", "cancelled").
Where("pc.type = ?", entities.PurchaseCategoryTypeRawMaterial).
Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo). Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo).
Group(dateFormat). Group(dateFormat).
Order(dateFormat) Order(dateFormat)
@@ -244,16 +220,15 @@ func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx contex
i.id as ingredient_id, i.id as ingredient_id,
i.name as ingredient_name, i.name as ingredient_name,
COALESCE(SUM(poi.quantity), 0) as quantity, COALESCE(SUM(poi.quantity), 0) as quantity,
COALESCE(SUM(`+purchaseOrderItemTotalAmountSQL()+`), 0) as total_cost, COALESCE(SUM(poi.amount), 0) as total_cost,
CASE CASE
WHEN SUM(poi.quantity) > 0 WHEN SUM(poi.quantity) > 0
THEN COALESCE(SUM(`+purchaseOrderItemTotalAmountSQL()+`), 0) / SUM(poi.quantity) THEN COALESCE(SUM(poi.amount), 0) / SUM(poi.quantity)
ELSE 0 ELSE 0
END as average_unit_cost, END as average_unit_cost,
COUNT(DISTINCT po.id) as purchase_order_count COUNT(DISTINCT po.id) as purchase_order_count
`). `).
Joins("JOIN purchase_orders po ON poi.purchase_order_id = po.id"). Joins("JOIN purchase_orders po ON poi.purchase_order_id = po.id").
Joins("JOIN purchase_categories pc ON poi.purchase_category_id = pc.id").
Joins("JOIN ingredients i ON poi.ingredient_id = i.id"). Joins("JOIN ingredients i ON poi.ingredient_id = i.id").
Joins("LEFT JOIN units u ON poi.unit_id = u.id"). Joins("LEFT JOIN units u ON poi.unit_id = u.id").
Where("po.organization_id = ?", organizationID). Where("po.organization_id = ?", organizationID).
@@ -272,21 +247,20 @@ func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx contex
Table("purchase_orders po"). Table("purchase_orders po").
Select(` Select(`
v.id as vendor_id, v.id as vendor_id,
COALESCE(v.name, 'No Vendor') as vendor_name, v.name as vendor_name,
COALESCE(SUM(`+purchaseOrderItemTotalAmountSQL()+`), 0) as total_cost, COALESCE(SUM(poi.amount), 0) as total_cost,
COUNT(DISTINCT po.id) as purchase_order_count, COUNT(DISTINCT po.id) as purchase_order_count,
COUNT(DISTINCT i.id) as ingredient_count, COUNT(DISTINCT i.id) as ingredient_count,
COALESCE(SUM(poi.quantity), 0) as quantity COALESCE(SUM(poi.quantity), 0) as quantity
`). `).
Joins("LEFT JOIN vendors v ON po.vendor_id = v.id"). Joins("JOIN vendors v ON po.vendor_id = v.id").
Joins("LEFT JOIN purchase_order_items poi ON poi.purchase_order_id = po.id"). Joins("LEFT JOIN purchase_order_items poi ON poi.purchase_order_id = po.id").
Joins("LEFT JOIN purchase_categories pc ON poi.purchase_category_id = pc.id").
Joins("LEFT JOIN ingredients i ON poi.ingredient_id = i.id"). Joins("LEFT JOIN ingredients i ON poi.ingredient_id = i.id").
Joins("LEFT JOIN units u ON poi.unit_id = u.id"). Joins("LEFT JOIN units u ON poi.unit_id = u.id").
Where("po.organization_id = ?", organizationID). Where("po.organization_id = ?", organizationID).
Where("po.status != ?", "cancelled"). Where("po.status != ?", "cancelled").
Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo). Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo).
Group("v.id, COALESCE(v.name, 'No Vendor')"). Group("v.id, v.name").
Order("total_cost DESC") Order("total_cost DESC")
vendorQuery = r.applyPurchaseOrderItemOutletFilter(vendorQuery, outletID) vendorQuery = r.applyPurchaseOrderItemOutletFilter(vendorQuery, outletID)
@@ -307,7 +281,7 @@ func (r *AnalyticsRepositoryImpl) applyPurchaseOrderItemOutletFilter(query *gorm
if outletID == nil { if outletID == nil {
return query return query
} }
return query.Where("po.outlet_id = ?", *outletID) return query.Where("(i.outlet_id = ? OR u.outlet_id = ?)", *outletID, *outletID)
} }
func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, limit int) ([]*entities.ProductAnalytics, error) { func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, limit int) ([]*entities.ProductAnalytics, error) {
@@ -318,13 +292,6 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organ
Select(` Select(`
p.id as product_id, p.id as product_id,
p.name as product_name, p.name as product_name,
p.sku as product_sku,
COALESCE(
NULLIF(pop.price, 0),
(SELECT price FROM product_outlet_prices WHERE product_id = p.id ORDER BY updated_at DESC LIMIT 1),
NULLIF(p.price, 0),
0
) as product_price,
c.id as category_id, c.id as category_id,
c.name as category_name, c.name as category_name,
c.order as category_order, c.order as category_order,
@@ -358,7 +325,6 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organ
Joins("JOIN products p ON oi.product_id = p.id"). Joins("JOIN products p ON oi.product_id = p.id").
Joins("JOIN categories c ON p.category_id = c.id"). Joins("JOIN categories c ON p.category_id = c.id").
Joins("JOIN orders o ON oi.order_id = o.id"). Joins("JOIN orders o ON oi.order_id = o.id").
Joins("LEFT JOIN product_outlet_prices pop ON pop.product_id = p.id AND pop.outlet_id = o.outlet_id").
Joins("LEFT JOIN (?) mahpp ON mahpp.product_id = p.id", Joins("LEFT JOIN (?) mahpp ON mahpp.product_id = p.id",
r.db.Table("product_recipes pr2"). r.db.Table("product_recipes pr2").
Select("pr2.product_id, SUM(pr2.quantity * (1 + COALESCE(pr2.waste_percentage, 0)/100.0) * COALESCE(ma.moving_avg_cost, ing.cost)) as hpp_per_unit"). Select("pr2.product_id, SUM(pr2.quantity * (1 + COALESCE(pr2.waste_percentage, 0)/100.0) * COALESCE(ma.moving_avg_cost, ing.cost)) as hpp_per_unit").
@@ -384,7 +350,7 @@ func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organ
query = r.resolveOutletID(query, outletID, "o.outlet_id") query = r.resolveOutletID(query, outletID, "o.outlet_id")
err := query. err := query.
Group("p.id, p.name, p.sku, p.price, p.cost, pop.price, 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").
Order("revenue DESC"). Order("revenue DESC").
Limit(limit). Limit(limit).
Scan(&results).Error Scan(&results).Error
@@ -520,11 +486,6 @@ func (r *AnalyticsRepositoryImpl) GetProfitLossAnalytics(ctx context.Context, or
if err := summaryQuery.Scan(&summary).Error; err != nil { if err := summaryQuery.Scan(&summary).Error; err != nil {
return nil, err return nil, err
} }
periodHPP, err := r.getPurchaseOrderRawMaterialTotal(ctx, organizationID, outletID, dateFrom, dateTo)
if err != nil {
return nil, err
}
applyProfitLossSummaryCost(&summary, periodHPP)
var timeFormat string var timeFormat string
switch groupBy { switch groupBy {
@@ -572,11 +533,6 @@ func (r *AnalyticsRepositoryImpl) GetProfitLossAnalytics(ctx context.Context, or
if err := dataQuery.Scan(&data).Error; err != nil { if err := dataQuery.Scan(&data).Error; err != nil {
return nil, err return nil, err
} }
poCostData, err := r.getPurchaseOrderRawMaterialCostByPeriod(ctx, organizationID, outletID, dateFrom, dateTo, groupBy)
if err != nil {
return nil, err
}
data = mergeProfitLossDataWithPurchaseOrderCost(data, poCostData)
var productData []entities.ProductProfitData var productData []entities.ProductProfitData
productQuery := r.db.WithContext(ctx). productQuery := r.db.WithContext(ctx).
@@ -637,11 +593,6 @@ func (r *AnalyticsRepositoryImpl) GetProfitLossAnalytics(ctx context.Context, or
if err := todayQuery.Scan(&todayRC).Error; err != nil { if err := todayQuery.Scan(&todayRC).Error; err != nil {
return nil, err return nil, err
} }
todayHPP, err := r.getPurchaseOrderRawMaterialTotal(ctx, organizationID, outletID, todayStart, todayEnd)
if err != nil {
return nil, err
}
todayRC.Cost = todayHPP
var mtdRC revenueCostResult var mtdRC revenueCostResult
mtdQuery := r.db.WithContext(ctx). mtdQuery := r.db.WithContext(ctx).
@@ -659,41 +610,21 @@ func (r *AnalyticsRepositoryImpl) GetProfitLossAnalytics(ctx context.Context, or
if err := mtdQuery.Scan(&mtdRC).Error; err != nil { if err := mtdQuery.Scan(&mtdRC).Error; err != nil {
return nil, err return nil, err
} }
mtdHPP, err := r.getPurchaseOrderRawMaterialTotal(ctx, organizationID, outletID, mtdStart, todayEnd)
if err != nil {
return nil, err
}
mtdRC.Cost = mtdHPP
todayExpenseByCategory, err := r.getExpenseByCategory(ctx, organizationID, outletID, todayStart, todayEnd) todayExpenseByCategory, err := r.getExpenseByCategory(ctx, organizationID, outletID, todayStart, todayEnd)
if err != nil { if err != nil {
return nil, err return nil, err
} }
todayPOExpenseByCategory, err := r.getPurchaseOrderExpenseByCategory(ctx, organizationID, outletID, todayStart, todayEnd)
if err != nil {
return nil, err
}
todayExpenseByCategory = mergeExpenseCategoryTotals(todayExpenseByCategory, todayPOExpenseByCategory)
mtdExpenseByCategory, err := r.getExpenseByCategory(ctx, organizationID, outletID, mtdStart, todayEnd) mtdExpenseByCategory, err := r.getExpenseByCategory(ctx, organizationID, outletID, mtdStart, todayEnd)
if err != nil { if err != nil {
return nil, err return nil, err
} }
mtdPOExpenseByCategory, err := r.getPurchaseOrderExpenseByCategory(ctx, organizationID, outletID, mtdStart, todayEnd)
if err != nil {
return nil, err
}
mtdExpenseByCategory = mergeExpenseCategoryTotals(mtdExpenseByCategory, mtdPOExpenseByCategory)
opsItems, err := r.getOperationalExpenseItems(ctx, organizationID, outletID, mtdStart, todayEnd) opsItems, err := r.getOperationalExpenseItems(ctx, organizationID, outletID, mtdStart, todayEnd)
if err != nil { if err != nil {
return nil, err return nil, err
} }
poOpsItems, err := r.getPurchaseOrderExpenseItems(ctx, organizationID, outletID, mtdStart, todayEnd)
if err != nil {
return nil, err
}
opsItems = mergeOperationalExpenseItems(opsItems, poOpsItems)
return &entities.ProfitLossAnalytics{ return &entities.ProfitLossAnalytics{
Summary: summary, Summary: summary,
@@ -709,200 +640,6 @@ func (r *AnalyticsRepositoryImpl) GetProfitLossAnalytics(ctx context.Context, or
}, nil }, nil
} }
func (r *AnalyticsRepositoryImpl) getPurchaseOrderRawMaterialTotal(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) (float64, error) {
type totalResult struct {
Total float64
}
var result totalResult
query := r.db.WithContext(ctx).
Table("purchase_order_items poi").
Select(`COALESCE(SUM(`+purchaseOrderItemTotalAmountSQL()+`), 0) as total`).
Joins("JOIN purchase_orders po ON poi.purchase_order_id = po.id").
Joins("JOIN purchase_categories pc ON poi.purchase_category_id = pc.id").
Where("po.organization_id = ?", organizationID).
Where("po.status = ?", "received").
Where("pc.type = ?", entities.PurchaseCategoryTypeRawMaterial).
Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo)
query = r.applyPurchaseOrderItemOutletFilter(query, outletID)
if err := query.Scan(&result).Error; err != nil {
return 0, err
}
return result.Total, nil
}
func (r *AnalyticsRepositoryImpl) getPurchaseOrderRawMaterialCostByPeriod(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, groupBy string) ([]entities.ProfitLossData, error) {
var dateFormat string
switch groupBy {
case "hour":
dateFormat = "DATE_TRUNC('hour', po.transaction_date::timestamp)"
case "week":
dateFormat = "DATE_TRUNC('week', po.transaction_date::timestamp)"
case "month":
dateFormat = "DATE_TRUNC('month', po.transaction_date::timestamp)"
default:
dateFormat = "DATE_TRUNC('day', po.transaction_date::timestamp)"
}
var results []entities.ProfitLossData
query := r.db.WithContext(ctx).
Table("purchase_order_items poi").
Select(`
`+dateFormat+` as date,
COALESCE(SUM(`+purchaseOrderItemTotalAmountSQL()+`), 0) as cost
`).
Joins("JOIN purchase_orders po ON poi.purchase_order_id = po.id").
Joins("JOIN purchase_categories pc ON poi.purchase_category_id = pc.id").
Where("po.organization_id = ?", organizationID).
Where("po.status = ?", "received").
Where("pc.type = ?", entities.PurchaseCategoryTypeRawMaterial).
Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo).
Group(dateFormat).
Order(dateFormat)
query = r.applyPurchaseOrderItemOutletFilter(query, outletID)
if err := query.Scan(&results).Error; err != nil {
return nil, err
}
return results, nil
}
func applyProfitLossSummaryCost(summary *entities.ProfitLossSummary, cost float64) {
summary.TotalCost = cost
summary.GrossProfit = summary.TotalRevenue - cost
summary.GrossProfitMargin = ratio(summary.GrossProfit, summary.TotalRevenue)
summary.NetProfit = summary.TotalRevenue - cost - summary.TotalDiscount
summary.NetProfitMargin = ratio(summary.NetProfit, summary.TotalRevenue)
if summary.TotalOrders > 0 {
summary.AverageProfit = summary.NetProfit / float64(summary.TotalOrders)
} else {
summary.AverageProfit = 0
}
summary.ProfitabilityRatio = ratio(summary.GrossProfit, cost)
}
func mergeProfitLossDataWithPurchaseOrderCost(data, costs []entities.ProfitLossData) []entities.ProfitLossData {
indexByDate := make(map[time.Time]int, len(data))
for i, item := range data {
data[i].Cost = 0
indexByDate[item.Date] = i
}
for _, cost := range costs {
if i, ok := indexByDate[cost.Date]; ok {
data[i].Cost = cost.Cost
continue
}
indexByDate[cost.Date] = len(data)
data = append(data, entities.ProfitLossData{Date: cost.Date, Cost: cost.Cost})
}
for i := range data {
data[i].GrossProfit = data[i].Revenue - data[i].Cost
data[i].GrossProfitMargin = ratio(data[i].GrossProfit, data[i].Revenue)
data[i].NetProfit = data[i].Revenue - data[i].Cost - data[i].Discount
data[i].NetProfitMargin = ratio(data[i].NetProfit, data[i].Revenue)
}
sort.Slice(data, func(i, j int) bool {
return data[i].Date.Before(data[j].Date)
})
return data
}
func ratio(numerator, denominator float64) float64 {
if denominator == 0 {
return 0
}
return (numerator / denominator) * 100
}
func (r *AnalyticsRepositoryImpl) getPurchaseOrderExpenseByCategory(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) ([]entities.ExpenseCategoryTotal, error) {
var results []entities.ExpenseCategoryTotal
query := r.db.WithContext(ctx).
Table("purchase_order_items poi").
Select(`
pc.name as category_name,
COALESCE(SUM(`+purchaseOrderItemTotalAmountSQL()+`), 0) as amount
`).
Joins("JOIN purchase_orders po ON poi.purchase_order_id = po.id").
Joins("JOIN purchase_categories pc ON poi.purchase_category_id = pc.id").
Where("po.organization_id = ?", organizationID).
Where("po.status = ?", "received").
Where("pc.type = ?", entities.PurchaseCategoryTypeExpense).
Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo).
Group("pc.id, pc.name, pc.sort_order").
Order("pc.sort_order ASC, pc.name ASC")
query = r.applyPurchaseOrderItemOutletFilter(query, outletID)
if err := query.Scan(&results).Error; err != nil {
return nil, err
}
return results, nil
}
func (r *AnalyticsRepositoryImpl) getPurchaseOrderExpenseItems(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) ([]entities.OperationalExpenseItem, error) {
var results []entities.OperationalExpenseItem
query := r.db.WithContext(ctx).
Table("purchase_order_items poi").
Select(`
COALESCE(NULLIF(poi.description, ''), pc.name) as item,
COALESCE(SUM(`+purchaseOrderItemTotalAmountSQL()+`), 0) as amount
`).
Joins("JOIN purchase_orders po ON poi.purchase_order_id = po.id").
Joins("JOIN purchase_categories pc ON poi.purchase_category_id = pc.id").
Where("po.organization_id = ?", organizationID).
Where("po.status = ?", "received").
Where("pc.type = ?", entities.PurchaseCategoryTypeExpense).
Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo).
Group("COALESCE(NULLIF(poi.description, ''), pc.name)").
Order("amount DESC")
query = r.applyPurchaseOrderItemOutletFilter(query, outletID)
if err := query.Scan(&results).Error; err != nil {
return nil, err
}
return results, nil
}
func mergeExpenseCategoryTotals(base, extra []entities.ExpenseCategoryTotal) []entities.ExpenseCategoryTotal {
indexByName := make(map[string]int, len(base))
for i, item := range base {
indexByName[item.CategoryName] = i
}
for _, item := range extra {
if i, ok := indexByName[item.CategoryName]; ok {
base[i].Amount += item.Amount
continue
}
indexByName[item.CategoryName] = len(base)
base = append(base, item)
}
return base
}
func mergeOperationalExpenseItems(base, extra []entities.OperationalExpenseItem) []entities.OperationalExpenseItem {
indexByName := make(map[string]int, len(base))
for i, item := range base {
indexByName[item.Item] = i
}
for _, item := range extra {
if i, ok := indexByName[item.Item]; ok {
base[i].Amount += item.Amount
continue
}
indexByName[item.Item] = len(base)
base = append(base, item)
}
return base
}
func (r *AnalyticsRepositoryImpl) getExpenseByCategory(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) ([]entities.ExpenseCategoryTotal, error) { func (r *AnalyticsRepositoryImpl) getExpenseByCategory(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) ([]entities.ExpenseCategoryTotal, error) {
var results []entities.ExpenseCategoryTotal var results []entities.ExpenseCategoryTotal
@@ -980,7 +717,7 @@ func (r *AnalyticsRepositoryImpl) GetExclusiveSummaryAnalytics(ctx context.Conte
return nil, err return nil, err
} }
operationalExpenseBreakdown, err := r.getExclusiveSummaryOperationalExpenseBreakdown(ctx, organizationID, outletID, dateFrom, dateTo) operationalExpenseBreakdown, err := r.getExclusiveSummaryOperationalExpenseBreakdown(ctx, organizationID, dateFrom, dateTo)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1013,7 +750,7 @@ func (r *AnalyticsRepositoryImpl) getExclusiveSummaryHPPBreakdown(ctx context.Co
Select(` Select(`
pc.code as category_code, pc.code as category_code,
pc.name as category_name, pc.name as category_name,
COALESCE(SUM(`+purchaseOrderItemTotalAmountSQL()+`), 0) as amount COALESCE(SUM(poi.amount), 0) as amount
`). `).
Joins("JOIN purchase_orders po ON poi.purchase_order_id = po.id"). Joins("JOIN purchase_orders po ON poi.purchase_order_id = po.id").
Joins("JOIN purchase_categories pc ON poi.purchase_category_id = pc.id"). Joins("JOIN purchase_categories pc ON poi.purchase_category_id = pc.id").
@@ -1023,7 +760,9 @@ func (r *AnalyticsRepositoryImpl) getExclusiveSummaryHPPBreakdown(ctx context.Co
Where("pc.type = ?", entities.PurchaseCategoryTypeRawMaterial). Where("pc.type = ?", entities.PurchaseCategoryTypeRawMaterial).
Where("po.status = ?", "received"). Where("po.status = ?", "received").
Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo) Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo)
query = r.applyPurchaseOrderItemOutletFilter(query, outletID) if outletID != nil {
query = query.Where("(i.outlet_id = ? OR u.outlet_id = ?)", *outletID, *outletID)
}
err := query. err := query.
Group("pc.id, pc.code, pc.name, pc.sort_order"). Group("pc.id, pc.code, pc.name, pc.sort_order").
@@ -1033,25 +772,22 @@ func (r *AnalyticsRepositoryImpl) getExclusiveSummaryHPPBreakdown(ctx context.Co
return results, err return results, err
} }
func (r *AnalyticsRepositoryImpl) getExclusiveSummaryOperationalExpenseBreakdown(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) ([]entities.ExclusiveSummaryCategoryTotal, error) { func (r *AnalyticsRepositoryImpl) getExclusiveSummaryOperationalExpenseBreakdown(ctx context.Context, organizationID uuid.UUID, dateFrom, dateTo time.Time) ([]entities.ExclusiveSummaryCategoryTotal, error) {
var results []entities.ExclusiveSummaryCategoryTotal var results []entities.ExclusiveSummaryCategoryTotal
query := r.db.WithContext(ctx). err := r.db.WithContext(ctx).
Table("purchase_order_items poi"). Table("purchase_order_items poi").
Select(` Select(`
pc.code as category_code, pc.code as category_code,
pc.name as category_name, pc.name as category_name,
COALESCE(SUM(`+purchaseOrderItemTotalAmountSQL()+`), 0) as amount COALESCE(SUM(poi.amount), 0) as amount
`). `).
Joins("JOIN purchase_orders po ON poi.purchase_order_id = po.id"). Joins("JOIN purchase_orders po ON poi.purchase_order_id = po.id").
Joins("JOIN purchase_categories pc ON poi.purchase_category_id = pc.id"). Joins("JOIN purchase_categories pc ON poi.purchase_category_id = pc.id").
Where("po.organization_id = ?", organizationID). Where("po.organization_id = ?", organizationID).
Where("pc.type = ?", entities.PurchaseCategoryTypeExpense). Where("pc.type = ?", entities.PurchaseCategoryTypeNonInventory).
Where("po.status = ?", "received"). Where("po.status = ?", "received").
Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo) Where("po.transaction_date >= ? AND po.transaction_date <= ?", dateFrom, dateTo).
query = r.applyPurchaseOrderItemOutletFilter(query, outletID)
err := query.
Group("pc.id, pc.code, pc.name, pc.sort_order"). Group("pc.id, pc.code, pc.name, pc.sort_order").
Order("pc.sort_order ASC, pc.name ASC"). Order("pc.sort_order ASC, pc.name ASC").
Scan(&results).Error Scan(&results).Error
@@ -1061,7 +797,7 @@ func (r *AnalyticsRepositoryImpl) getExclusiveSummaryOperationalExpenseBreakdown
func (r *AnalyticsRepositoryImpl) getExclusiveSummaryDailySummary(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) ([]entities.ExclusiveSummaryDailySummary, error) { func (r *AnalyticsRepositoryImpl) getExclusiveSummaryDailySummary(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) ([]entities.ExclusiveSummaryDailySummary, error) {
var results []entities.ExclusiveSummaryDailySummary var results []entities.ExclusiveSummaryDailySummary
rawQuery, args := r.exclusiveSummaryPurchaseOrderItemQuery(organizationID, outletID, dateFrom, dateTo) rawQuery, args := r.exclusiveSummaryPurchaseOrderItemsQuery(organizationID, outletID, dateFrom, dateTo)
err := r.db.WithContext(ctx).Raw(` err := r.db.WithContext(ctx).Raw(`
SELECT date, COUNT(*) as transaction_count, COALESCE(SUM(amount), 0) as total_cost SELECT date, COUNT(*) as transaction_count, COALESCE(SUM(amount), 0) as total_cost
@@ -1075,7 +811,7 @@ func (r *AnalyticsRepositoryImpl) getExclusiveSummaryDailySummary(ctx context.Co
func (r *AnalyticsRepositoryImpl) getExclusiveSummaryDailyTransactions(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) ([]entities.ExclusiveSummaryDailyTransaction, error) { func (r *AnalyticsRepositoryImpl) getExclusiveSummaryDailyTransactions(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) ([]entities.ExclusiveSummaryDailyTransaction, error) {
var results []entities.ExclusiveSummaryDailyTransaction var results []entities.ExclusiveSummaryDailyTransaction
rawQuery, args := r.exclusiveSummaryPurchaseOrderItemQuery(organizationID, outletID, dateFrom, dateTo) rawQuery, args := r.exclusiveSummaryPurchaseOrderItemsQuery(organizationID, outletID, dateFrom, dateTo)
err := r.db.WithContext(ctx).Raw(` err := r.db.WithContext(ctx).Raw(`
SELECT date, category_code, category_name, description, amount, source SELECT date, category_code, category_name, description, amount, source
@@ -1086,18 +822,19 @@ func (r *AnalyticsRepositoryImpl) getExclusiveSummaryDailyTransactions(ctx conte
return results, err return results, err
} }
func (r *AnalyticsRepositoryImpl) exclusiveSummaryPurchaseOrderItemQuery(organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) (string, []interface{}) { func (r *AnalyticsRepositoryImpl) exclusiveSummaryPurchaseOrderItemsQuery(organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) (string, []interface{}) {
outletFilter := ""
args := []interface{}{ args := []interface{}{
organizationID, organizationID,
entities.PurchaseCategoryTypeRawMaterial,
entities.PurchaseCategoryTypeNonInventory,
"received", "received",
dateFrom, dateFrom,
dateTo, dateTo,
} }
outletFilter := ""
if outletID != nil { if outletID != nil {
outletFilter = "AND po.outlet_id = ?" outletFilter = "AND (pc.type = ? OR i.outlet_id = ? OR u.outlet_id = ?)"
args = append(args, *outletID) args = append(args, entities.PurchaseCategoryTypeNonInventory, *outletID, *outletID)
} }
query := ` query := `
@@ -1106,7 +843,7 @@ func (r *AnalyticsRepositoryImpl) exclusiveSummaryPurchaseOrderItemQuery(organiz
pc.code as category_code, pc.code as category_code,
pc.name as category_name, pc.name as category_name,
COALESCE(NULLIF(poi.description, ''), i.name, pc.name) as description, COALESCE(NULLIF(poi.description, ''), i.name, pc.name) as description,
` + purchaseOrderItemTotalAmountSQL() + ` as amount, poi.amount as amount,
'purchase_order' as source 'purchase_order' as source
FROM purchase_order_items poi FROM purchase_order_items poi
JOIN purchase_orders po ON poi.purchase_order_id = po.id JOIN purchase_orders po ON poi.purchase_order_id = po.id
@@ -1114,6 +851,7 @@ func (r *AnalyticsRepositoryImpl) exclusiveSummaryPurchaseOrderItemQuery(organiz
LEFT JOIN ingredients i ON poi.ingredient_id = i.id LEFT JOIN ingredients i ON poi.ingredient_id = i.id
LEFT JOIN units u ON poi.unit_id = u.id LEFT JOIN units u ON poi.unit_id = u.id
WHERE po.organization_id = ? WHERE po.organization_id = ?
AND pc.type IN (?, ?)
AND po.status = ? AND po.status = ?
AND po.transaction_date >= ? AND po.transaction_date <= ? AND po.transaction_date >= ? AND po.transaction_date <= ?
` + outletFilter + ` ` + outletFilter + `
@@ -1123,45 +861,28 @@ func (r *AnalyticsRepositoryImpl) exclusiveSummaryPurchaseOrderItemQuery(organiz
} }
func (r *AnalyticsRepositoryImpl) GetExclusiveSummaryBankBalances(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) ([]entities.ExclusiveSummaryBankBalance, error) { func (r *AnalyticsRepositoryImpl) GetExclusiveSummaryBankBalances(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) ([]entities.ExclusiveSummaryBankBalance, error) {
type accountBalance struct { var results []entities.ExclusiveSummaryBankBalance
Name string
OpeningBalance float64
CurrentBalance float64
Description *string
}
var accounts []accountBalance
query := r.db.WithContext(ctx). query := r.db.WithContext(ctx).
Table("accounts"). Table("accounts").
Select("name, opening_balance, current_balance, description"). Select(`
name as bank,
account_type,
opening_balance,
current_balance as closing_balance,
description
`).
Where("organization_id = ?", organizationID). Where("organization_id = ?", organizationID).
Where("account_type IN ?", []entities.AccountType{entities.AccountTypeCash, entities.AccountTypeWallet, entities.AccountTypeBank}). Where("is_active = ?", true).
Where("is_active = ?", true) Where("account_type IN ?", []entities.AccountType{entities.AccountTypeBank, entities.AccountTypeWallet, entities.AccountTypeCash})
if outletID != nil { if outletID != nil {
query = query.Where("outlet_id = ? OR outlet_id IS NULL", *outletID) query = query.Where("outlet_id = ? OR outlet_id IS NULL", *outletID)
} else {
query = query.Where("outlet_id IS NULL")
} }
err := query. err := query.
Order("number ASC, name ASC"). Order("CASE account_type WHEN 'bank' THEN 1 WHEN 'wallet' THEN 2 WHEN 'cash' THEN 3 ELSE 4 END, number ASC, name ASC").
Scan(&accounts).Error Scan(&results).Error
if err != nil {
return nil, err
}
balances := make([]entities.ExclusiveSummaryBankBalance, len(accounts)) return results, err
for i, account := range accounts {
openingBalance := account.OpeningBalance
closingBalance := account.CurrentBalance
balances[i] = entities.ExclusiveSummaryBankBalance{
Bank: account.Name,
OpeningBalance: &openingBalance,
ClosingBalance: &closingBalance,
Notes: account.Description,
}
}
return balances, nil
} }
@@ -196,18 +196,6 @@ func (r *PurchaseOrderRepositoryImpl) UpdateStatus(ctx context.Context, id uuid.
Update("status", status).Error Update("status", status).Error
} }
func (r *PurchaseOrderRepositoryImpl) UpdateStatusAndOutlet(ctx context.Context, id uuid.UUID, status string, outletID *uuid.UUID) error {
updates := map[string]interface{}{"status": status}
if outletID != nil {
updates["outlet_id"] = *outletID
}
return r.db.WithContext(ctx).
Model(&entities.PurchaseOrder{}).
Where("id = ?", id).
Updates(updates).Error
}
func (r *PurchaseOrderRepositoryImpl) UpdateTotalAmount(ctx context.Context, id uuid.UUID, totalAmount float64) error { func (r *PurchaseOrderRepositoryImpl) UpdateTotalAmount(ctx context.Context, id uuid.UUID, totalAmount float64) error {
return r.db.WithContext(ctx). return r.db.WithContext(ctx).
Model(&entities.PurchaseOrder{}). Model(&entities.PurchaseOrder{}).
-1
View File
@@ -339,7 +339,6 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
analytics.GET("/profit-loss", r.analyticsHandler.GetProfitLossAnalytics) analytics.GET("/profit-loss", r.analyticsHandler.GetProfitLossAnalytics)
analytics.GET("/exclusive-summary/period", r.analyticsHandler.GetExclusiveSummaryPeriod) analytics.GET("/exclusive-summary/period", r.analyticsHandler.GetExclusiveSummaryPeriod)
analytics.GET("/exclusive-summary/monthly", r.analyticsHandler.GetExclusiveSummaryMonthly) analytics.GET("/exclusive-summary/monthly", r.analyticsHandler.GetExclusiveSummaryMonthly)
analytics.GET("/exclusive-summary/mtd", r.analyticsHandler.GetExclusiveSummaryMTD)
} }
tables := protected.Group("/tables") tables := protected.Group("/tables")
-30
View File
@@ -20,7 +20,6 @@ type AnalyticsService interface {
GetProfitLossAnalytics(ctx context.Context, req *models.ProfitLossAnalyticsRequest) (*models.ProfitLossAnalyticsResponse, error) GetProfitLossAnalytics(ctx context.Context, req *models.ProfitLossAnalyticsRequest) (*models.ProfitLossAnalyticsResponse, error)
GetExclusiveSummaryPeriod(ctx context.Context, req *models.ExclusiveSummaryPeriodRequest) (*models.ExclusiveSummaryPeriodResponse, error) GetExclusiveSummaryPeriod(ctx context.Context, req *models.ExclusiveSummaryPeriodRequest) (*models.ExclusiveSummaryPeriodResponse, error)
GetExclusiveSummaryMonthly(ctx context.Context, req *models.ExclusiveSummaryMonthlyRequest) (*models.ExclusiveSummaryMonthlyResponse, error) GetExclusiveSummaryMonthly(ctx context.Context, req *models.ExclusiveSummaryMonthlyRequest) (*models.ExclusiveSummaryMonthlyResponse, error)
GetExclusiveSummaryMTD(ctx context.Context, req *models.ExclusiveSummaryMTDRequest) (*models.ExclusiveSummaryPeriodResponse, error)
} }
type AnalyticsServiceImpl struct { type AnalyticsServiceImpl struct {
@@ -350,19 +349,6 @@ func (s *AnalyticsServiceImpl) GetExclusiveSummaryMonthly(ctx context.Context, r
return response, nil return response, nil
} }
func (s *AnalyticsServiceImpl) GetExclusiveSummaryMTD(ctx context.Context, req *models.ExclusiveSummaryMTDRequest) (*models.ExclusiveSummaryPeriodResponse, error) {
if err := s.validateExclusiveSummaryMTDRequest(req); err != nil {
return nil, fmt.Errorf("validation error: %w", err)
}
response, err := s.analyticsProcessor.GetExclusiveSummaryMTD(ctx, req)
if err != nil {
return nil, fmt.Errorf("failed to get exclusive summary mtd: %w", err)
}
return response, nil
}
func (s *AnalyticsServiceImpl) validateExclusiveSummaryPeriodRequest(req *models.ExclusiveSummaryPeriodRequest) error { func (s *AnalyticsServiceImpl) validateExclusiveSummaryPeriodRequest(req *models.ExclusiveSummaryPeriodRequest) error {
if req == nil { if req == nil {
return fmt.Errorf("request cannot be nil") return fmt.Errorf("request cannot be nil")
@@ -387,22 +373,6 @@ func (s *AnalyticsServiceImpl) validateExclusiveSummaryPeriodRequest(req *models
return nil return nil
} }
func (s *AnalyticsServiceImpl) validateExclusiveSummaryMTDRequest(req *models.ExclusiveSummaryMTDRequest) error {
if req == nil {
return fmt.Errorf("request cannot be nil")
}
if req.OrganizationID == uuid.Nil {
return fmt.Errorf("organization_id is required")
}
if req.DateTo.IsZero() {
return fmt.Errorf("date_to is required")
}
return nil
}
func (s *AnalyticsServiceImpl) validateExclusiveSummaryMonthlyRequest(req *models.ExclusiveSummaryMonthlyRequest) error { func (s *AnalyticsServiceImpl) validateExclusiveSummaryMonthlyRequest(req *models.ExclusiveSummaryMonthlyRequest) error {
if req == nil { if req == nil {
return fmt.Errorf("request cannot be nil") return fmt.Errorf("request cannot be nil")
-101
View File
@@ -49,10 +49,6 @@ func (analyticsProcessorStub) GetExclusiveSummaryMonthly(context.Context, *model
return &models.ExclusiveSummaryMonthlyResponse{}, nil return &models.ExclusiveSummaryMonthlyResponse{}, nil
} }
func (analyticsProcessorStub) GetExclusiveSummaryMTD(context.Context, *models.ExclusiveSummaryMTDRequest) (*models.ExclusiveSummaryPeriodResponse, error) {
return &models.ExclusiveSummaryPeriodResponse{}, nil
}
func TestAnalyticsServiceGetPurchasingAnalyticsValidation(t *testing.T) { func TestAnalyticsServiceGetPurchasingAnalyticsValidation(t *testing.T) {
service := NewAnalyticsServiceImpl(analyticsProcessorStub{}) service := NewAnalyticsServiceImpl(analyticsProcessorStub{})
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC) now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
@@ -202,100 +198,3 @@ func TestAnalyticsServiceGetProfitLossAnalyticsAllowsEmptyGroupBy(t *testing.T)
require.NoError(t, err) require.NoError(t, err)
require.Nil(t, resp) require.Nil(t, resp)
} }
func TestAnalyticsServiceGetExclusiveSummaryPeriodValidation(t *testing.T) {
service := NewAnalyticsServiceImpl(analyticsProcessorStub{})
now := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
tests := []struct {
name string
req *models.ExclusiveSummaryPeriodRequest
wantErr string
}{
{
name: "nil request",
req: nil,
wantErr: "request cannot be nil",
},
{
name: "missing organization",
req: &models.ExclusiveSummaryPeriodRequest{
DateFrom: now,
DateTo: now,
},
wantErr: "organization_id is required",
},
{
name: "reversed dates",
req: &models.ExclusiveSummaryPeriodRequest{
OrganizationID: uuid.New(),
DateFrom: now.AddDate(0, 0, 1),
DateTo: now,
},
wantErr: "date_from cannot be after date_to",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resp, err := service.GetExclusiveSummaryPeriod(context.Background(), tt.req)
require.Nil(t, resp)
require.Error(t, err)
require.Contains(t, err.Error(), tt.wantErr)
})
}
}
func TestAnalyticsServiceGetExclusiveSummaryMonthlyValidation(t *testing.T) {
service := NewAnalyticsServiceImpl(analyticsProcessorStub{})
resp, err := service.GetExclusiveSummaryMonthly(context.Background(), &models.ExclusiveSummaryMonthlyRequest{
OrganizationID: uuid.New(),
})
require.Nil(t, resp)
require.Error(t, err)
require.Contains(t, err.Error(), "month is required")
}
func TestAnalyticsServiceGetExclusiveSummaryMTDValidation(t *testing.T) {
service := NewAnalyticsServiceImpl(analyticsProcessorStub{})
now := time.Date(2026, 6, 18, 23, 59, 59, 0, time.UTC)
tests := []struct {
name string
req *models.ExclusiveSummaryMTDRequest
wantErr string
}{
{
name: "nil request",
req: nil,
wantErr: "request cannot be nil",
},
{
name: "missing organization",
req: &models.ExclusiveSummaryMTDRequest{
DateTo: now,
},
wantErr: "organization_id is required",
},
{
name: "missing date_to",
req: &models.ExclusiveSummaryMTDRequest{
OrganizationID: uuid.New(),
},
wantErr: "date_to is required",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resp, err := service.GetExclusiveSummaryMTD(context.Background(), tt.req)
require.Nil(t, resp)
require.Error(t, err)
require.Contains(t, err.Error(), tt.wantErr)
})
}
}
+2 -12
View File
@@ -40,12 +40,7 @@ func (s *PurchaseOrderServiceImpl) CreatePurchaseOrder(ctx context.Context, apct
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
} }
var outletID *uuid.UUID poResponse, err := s.purchaseOrderProcessor.CreatePurchaseOrder(ctx, apctx.OrganizationID, modelReq)
if apctx.OutletID != uuid.Nil {
outletID = &apctx.OutletID
}
poResponse, err := s.purchaseOrderProcessor.CreatePurchaseOrder(ctx, apctx.OrganizationID, outletID, modelReq)
if err != nil { if err != nil {
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.PurchaseOrderServiceEntity, err.Error()) errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.PurchaseOrderServiceEntity, err.Error())
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
@@ -62,12 +57,7 @@ func (s *PurchaseOrderServiceImpl) UpdatePurchaseOrder(ctx context.Context, apct
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
} }
var outletID *uuid.UUID poResponse, err := s.purchaseOrderProcessor.UpdatePurchaseOrder(ctx, id, apctx.OrganizationID, modelReq)
if apctx.OutletID != uuid.Nil {
outletID = &apctx.OutletID
}
poResponse, err := s.purchaseOrderProcessor.UpdatePurchaseOrder(ctx, id, apctx.OrganizationID, outletID, modelReq)
if err != nil { if err != nil {
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.PurchaseOrderServiceEntity, err.Error()) errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.PurchaseOrderServiceEntity, err.Error())
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
+6 -43
View File
@@ -604,27 +604,6 @@ func ExclusiveSummaryMonthlyContractToModel(req *contract.ExclusiveSummaryMonthl
}, nil }, nil
} }
func ExclusiveSummaryMTDContractToModel(req *contract.ExclusiveSummaryMTDRequest) (*models.ExclusiveSummaryMTDRequest, error) {
if req == nil {
return nil, fmt.Errorf("request cannot be nil")
}
dateTo, err := parseFlexibleDateToJakartaTime(req.DateTo, true)
if err != nil {
return nil, fmt.Errorf("invalid date_to: %w", err)
}
if dateTo == nil {
return nil, fmt.Errorf("date_to is required")
}
return &models.ExclusiveSummaryMTDRequest{
OrganizationID: req.OrganizationID,
OutletID: parseOutletID(req.OutletID),
DateTo: *dateTo,
ExcludeGajiStaffFromReimburse: req.ExcludeGajiStaffFromReimburse,
}, nil
}
func ExclusiveSummaryPeriodModelToContract(resp *models.ExclusiveSummaryPeriodResponse) *contract.ExclusiveSummaryPeriodResponse { func ExclusiveSummaryPeriodModelToContract(resp *models.ExclusiveSummaryPeriodResponse) *contract.ExclusiveSummaryPeriodResponse {
if resp == nil { if resp == nil {
return nil return nil
@@ -774,18 +753,18 @@ func parseISODateToJakartaTime(dateStr string, endOfDay bool) (*time.Time, error
return nil, nil return nil, nil
} }
date, err := time.Parse("2006-01-02", dateStr)
if err != nil {
return nil, err
}
location, err := time.LoadLocation("Asia/Jakarta") location, err := time.LoadLocation("Asia/Jakarta")
if err != nil { if err != nil {
return nil, err return nil, err
} }
date, err := time.ParseInLocation("2006-01-02", dateStr, location)
if err != nil {
return nil, err
}
if endOfDay { if endOfDay {
result := time.Date(date.Year(), date.Month(), date.Day(), 23, 59, 59, 999999999, location) result := time.Date(date.Year(), date.Month(), date.Day(), 23, 59, 59, int(time.Second-time.Nanosecond), location)
return &result, nil return &result, nil
} }
@@ -793,22 +772,6 @@ func parseISODateToJakartaTime(dateStr string, endOfDay bool) (*time.Time, error
return &result, nil return &result, nil
} }
func parseFlexibleDateToJakartaTime(dateStr string, endOfDay bool) (*time.Time, error) {
if dateStr == "" {
return nil, nil
}
fromTime, toTime, err := util.ParseDateRangeToJakartaTime(dateStr, dateStr)
if err == nil {
if endOfDay {
return toTime, nil
}
return fromTime, nil
}
return parseISODateToJakartaTime(dateStr, endOfDay)
}
func parseMonthToJakartaTime(month string) (time.Time, error) { func parseMonthToJakartaTime(month string) (time.Time, error) {
location, err := time.LoadLocation("Asia/Jakarta") location, err := time.LoadLocation("Asia/Jakarta")
if err != nil { if err != nil {
@@ -183,7 +183,7 @@ func TestProfitLossAnalyticsModelToContractCopiesDateRange(t *testing.T) {
require.Equal(t, "total_omset", result.MainSummary[0].ID) require.Equal(t, "total_omset", result.MainSummary[0].ID)
} }
func TestExclusiveSummaryPeriodContractToModelParsesFlexibleDates(t *testing.T) { func TestExclusiveSummaryPeriodContractToModelParsesISODateRange(t *testing.T) {
orgID := uuid.New() orgID := uuid.New()
outletID := uuid.New().String() outletID := uuid.New().String()
@@ -209,16 +209,50 @@ func TestExclusiveSummaryPeriodContractToModelParsesFlexibleDates(t *testing.T)
func TestExclusiveSummaryMonthlyContractToModelParsesMonth(t *testing.T) { func TestExclusiveSummaryMonthlyContractToModelParsesMonth(t *testing.T) {
orgID := uuid.New() orgID := uuid.New()
outletID := uuid.New().String()
result, err := ExclusiveSummaryMonthlyContractToModel(&contract.ExclusiveSummaryMonthlyRequest{ result, err := ExclusiveSummaryMonthlyContractToModel(&contract.ExclusiveSummaryMonthlyRequest{
OrganizationID: orgID, OrganizationID: orgID,
OutletID: &outletID,
Month: "2026-05", Month: "2026-05",
}) })
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, orgID, result.OrganizationID) require.Equal(t, orgID, result.OrganizationID)
require.NotNil(t, result.OutletID)
require.Equal(t, outletID, result.OutletID.String())
location, err := time.LoadLocation("Asia/Jakarta") location, err := time.LoadLocation("Asia/Jakarta")
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, time.Date(2026, 5, 1, 0, 0, 0, 0, location), result.Month) require.Equal(t, time.Date(2026, 5, 1, 0, 0, 0, 0, location), result.Month)
} }
func TestExclusiveSummaryPeriodModelToContractCopiesBreakdowns(t *testing.T) {
dateFrom := time.Date(2026, 5, 26, 0, 0, 0, 0, time.UTC)
dateTo := time.Date(2026, 5, 31, 23, 59, 59, int(time.Second-time.Nanosecond), time.UTC)
result := ExclusiveSummaryPeriodModelToContract(&models.ExclusiveSummaryPeriodResponse{
OrganizationID: uuid.New(),
Period: models.ExclusiveSummaryPeriodRange{
DateFrom: dateFrom,
DateTo: dateTo,
},
Summary: models.ExclusiveSummaryPeriodSummary{
Sales: 1000,
HPP: 400,
TotalCost: 550,
NetProfit: 450,
SalaryStaff: 100,
},
HPPBreakdown: []models.ExclusiveSummaryCategoryBreakdown{
{CategoryCode: "hpp", CategoryName: "HPP", Amount: 400, Percentage: 100},
},
})
require.NotNil(t, result)
require.Equal(t, dateFrom, result.Period.DateFrom)
require.Equal(t, dateTo, result.Period.DateTo)
require.Equal(t, float64(1000), result.Summary.Sales)
require.Len(t, result.HPPBreakdown, 1)
require.Equal(t, "hpp", result.HPPBreakdown[0].CategoryCode)
}
@@ -120,7 +120,6 @@ func PurchaseOrderModelResponseToResponse(po *models.PurchaseOrderResponse) *con
response := &contract.PurchaseOrderResponse{ response := &contract.PurchaseOrderResponse{
ID: po.ID, ID: po.ID,
OrganizationID: po.OrganizationID, OrganizationID: po.OrganizationID,
OutletID: po.OutletID,
VendorID: po.VendorID, VendorID: po.VendorID,
PONumber: po.PONumber, PONumber: po.PONumber,
TransactionDate: po.TransactionDate, TransactionDate: po.TransactionDate,
@@ -12,13 +12,12 @@ import (
) )
func TestCreatePurchaseOrderRequestToModelAllowsMissingDueDate(t *testing.T) { func TestCreatePurchaseOrderRequestToModelAllowsMissingDueDate(t *testing.T) {
vendorID := uuid.New()
ingredientID := uuid.New() ingredientID := uuid.New()
quantity := 1.0 quantity := 1.0
unitID := uuid.New() unitID := uuid.New()
result, err := CreatePurchaseOrderRequestToModel(&contract.CreatePurchaseOrderRequest{ result, err := CreatePurchaseOrderRequestToModel(&contract.CreatePurchaseOrderRequest{
VendorID: &vendorID, VendorID: uuid.New(),
PONumber: "PO-001", PONumber: "PO-001",
TransactionDate: "2026-05-29", TransactionDate: "2026-05-29",
Items: []contract.CreatePurchaseOrderItemRequest{ Items: []contract.CreatePurchaseOrderItemRequest{
@@ -36,10 +35,9 @@ func TestCreatePurchaseOrderRequestToModelAllowsMissingDueDate(t *testing.T) {
} }
func TestPurchaseOrderModelResponseToResponseIncludesNullDueDate(t *testing.T) { func TestPurchaseOrderModelResponseToResponseIncludesNullDueDate(t *testing.T) {
vendorID := uuid.New()
result := PurchaseOrderModelResponseToResponse(&models.PurchaseOrderResponse{ result := PurchaseOrderModelResponseToResponse(&models.PurchaseOrderResponse{
ID: uuid.New(), ID: uuid.New(),
VendorID: &vendorID, VendorID: uuid.New(),
PONumber: "PO-001", PONumber: "PO-001",
}) })
@@ -47,19 +45,3 @@ func TestPurchaseOrderModelResponseToResponseIncludesNullDueDate(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Contains(t, string(payload), `"due_date":null`) require.Contains(t, string(payload), `"due_date":null`)
} }
func TestCreatePurchaseOrderRequestToModelAllowsMissingVendor(t *testing.T) {
result, err := CreatePurchaseOrderRequestToModel(&contract.CreatePurchaseOrderRequest{
PONumber: "PO-001",
TransactionDate: "2026-05-29",
Items: []contract.CreatePurchaseOrderItemRequest{
{
PurchaseCategoryID: uuid.New(),
Amount: 1000,
},
},
})
require.NoError(t, err)
require.Nil(t, result.VendorID)
}
@@ -29,8 +29,8 @@ func (v *PurchaseOrderValidatorImpl) ValidateCreatePurchaseOrderRequest(req *con
return errors.New("request body is required"), constants.MissingFieldErrorCode return errors.New("request body is required"), constants.MissingFieldErrorCode
} }
if req.VendorID != nil && *req.VendorID == uuid.Nil { if req.VendorID == uuid.Nil {
return errors.New("vendor_id cannot be empty"), constants.MalformedFieldErrorCode return errors.New("vendor_id is required"), constants.MissingFieldErrorCode
} }
if strings.TrimSpace(req.PONumber) == "" { if strings.TrimSpace(req.PONumber) == "" {
@@ -11,13 +11,12 @@ import (
) )
func validCreatePurchaseOrderRequest() *contract.CreatePurchaseOrderRequest { func validCreatePurchaseOrderRequest() *contract.CreatePurchaseOrderRequest {
vendorID := uuid.New()
ingredientID := uuid.New() ingredientID := uuid.New()
quantity := 1.0 quantity := 1.0
unitID := uuid.New() unitID := uuid.New()
return &contract.CreatePurchaseOrderRequest{ return &contract.CreatePurchaseOrderRequest{
VendorID: &vendorID, VendorID: uuid.New(),
PONumber: "PO-001", PONumber: "PO-001",
TransactionDate: "2026-05-29", TransactionDate: "2026-05-29",
Items: []contract.CreatePurchaseOrderItemRequest{ Items: []contract.CreatePurchaseOrderItemRequest{
@@ -32,30 +31,6 @@ func validCreatePurchaseOrderRequest() *contract.CreatePurchaseOrderRequest {
} }
} }
func TestPurchaseOrderValidatorCreateAllowsMissingVendor(t *testing.T) {
validator := NewPurchaseOrderValidator()
req := validCreatePurchaseOrderRequest()
req.VendorID = nil
err, code := validator.ValidateCreatePurchaseOrderRequest(req)
require.NoError(t, err)
require.Empty(t, code)
}
func TestPurchaseOrderValidatorCreateRejectsEmptyVendor(t *testing.T) {
validator := NewPurchaseOrderValidator()
req := validCreatePurchaseOrderRequest()
vendorID := uuid.Nil
req.VendorID = &vendorID
err, code := validator.ValidateCreatePurchaseOrderRequest(req)
require.Error(t, err)
require.Equal(t, constants.MalformedFieldErrorCode, code)
require.Contains(t, err.Error(), "vendor_id cannot be empty")
}
func TestPurchaseOrderValidatorCreateAllowsMissingDueDate(t *testing.T) { func TestPurchaseOrderValidatorCreateAllowsMissingDueDate(t *testing.T) {
validator := NewPurchaseOrderValidator() validator := NewPurchaseOrderValidator()
@@ -1,2 +0,0 @@
ALTER TABLE purchase_orders
ALTER COLUMN vendor_id SET NOT NULL;
@@ -1,2 +0,0 @@
ALTER TABLE purchase_orders
ALTER COLUMN vendor_id DROP NOT NULL;
@@ -1,4 +0,0 @@
DROP INDEX IF EXISTS idx_purchase_orders_outlet_id;
ALTER TABLE purchase_orders
DROP COLUMN IF EXISTS outlet_id;
@@ -1,66 +0,0 @@
ALTER TABLE purchase_orders
ADD COLUMN IF NOT EXISTS outlet_id UUID REFERENCES outlets(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS idx_purchase_orders_outlet_id
ON purchase_orders(outlet_id);
WITH movement_outlets AS (
SELECT
poi.purchase_order_id,
MIN(im.outlet_id) AS outlet_id
FROM inventory_movements im
JOIN purchase_order_items poi ON im.purchase_order_item_id = poi.id
WHERE im.outlet_id IS NOT NULL
AND im.purchase_order_item_id IS NOT NULL
GROUP BY poi.purchase_order_id
HAVING COUNT(DISTINCT im.outlet_id) = 1
)
UPDATE purchase_orders po
SET outlet_id = movement_outlets.outlet_id
FROM movement_outlets
WHERE po.id = movement_outlets.purchase_order_id
AND po.outlet_id IS NULL;
WITH candidate_item_outlets AS (
SELECT
poi.purchase_order_id,
i.outlet_id
FROM purchase_order_items poi
JOIN ingredients i ON poi.ingredient_id = i.id
WHERE i.outlet_id IS NOT NULL
UNION ALL
SELECT
poi.purchase_order_id,
u.outlet_id
FROM purchase_order_items poi
JOIN units u ON poi.unit_id = u.id
WHERE u.outlet_id IS NOT NULL
), item_outlets AS (
SELECT
purchase_order_id,
MIN(outlet_id) AS outlet_id
FROM candidate_item_outlets
GROUP BY purchase_order_id
HAVING COUNT(DISTINCT outlet_id) = 1
)
UPDATE purchase_orders po
SET outlet_id = item_outlets.outlet_id
FROM item_outlets
WHERE po.id = item_outlets.purchase_order_id
AND po.outlet_id IS NULL;
WITH single_outlet_organizations AS (
SELECT
organization_id,
MIN(id) AS outlet_id
FROM outlets
GROUP BY organization_id
HAVING COUNT(*) = 1
)
UPDATE purchase_orders po
SET outlet_id = single_outlet_organizations.outlet_id
FROM single_outlet_organizations
WHERE po.organization_id = single_outlet_organizations.organization_id
AND po.outlet_id IS NULL;