Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fa9fc5070 | ||
|
|
07e8be0521 | ||
|
|
4b7b225f58 |
@@ -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=
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
@@ -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 = ¬es
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
|||||||
@@ -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: ¬es},
|
{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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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{}).
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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})
|
||||||
|
|||||||
@@ -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;
|
|
||||||
Reference in New Issue
Block a user