Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55119b3e91 | ||
|
|
c1d859ebdd | ||
|
|
2ad9e2f85f | ||
|
|
8816e4addc | ||
|
|
2921631ac3 | ||
|
|
6c19876a47 | ||
|
|
b2db56f855 | ||
|
|
8c4d9c69d0 | ||
|
|
657a201fc0 |
@@ -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,6 +339,13 @@ 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" validate:"required"`
|
VendorID *uuid.UUID `json:"vendor_id,omitempty" validate:"omitempty"`
|
||||||
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,7 +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"`
|
||||||
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 {
|
||||||
|
|||||||
@@ -11,7 +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"`
|
||||||
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"`
|
||||||
|
|||||||
@@ -266,3 +266,31 @@ 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")
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,6 +349,13 @@ 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,7 +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"`
|
||||||
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"`
|
||||||
@@ -44,7 +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"`
|
||||||
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"`
|
||||||
@@ -84,7 +84,7 @@ type PurchaseOrderAttachmentResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreatePurchaseOrderRequest struct {
|
type CreatePurchaseOrderRequest struct {
|
||||||
VendorID uuid.UUID `json:"vendor_id"`
|
VendorID *uuid.UUID `json:"vendor_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"`
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ 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 {
|
||||||
@@ -735,6 +736,18 @@ func (p *AnalyticsProcessorImpl) GetExclusiveSummaryMonthly(ctx context.Context,
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *AnalyticsProcessorImpl) GetExclusiveSummaryMTD(ctx context.Context, req *models.ExclusiveSummaryMTDRequest) (*models.ExclusiveSummaryPeriodResponse, error) {
|
||||||
|
mtdStart := time.Date(req.DateTo.Year(), req.DateTo.Month(), 1, 0, 0, 0, 0, req.DateTo.Location())
|
||||||
|
|
||||||
|
return p.buildExclusiveSummaryPeriod(ctx, &models.ExclusiveSummaryPeriodRequest{
|
||||||
|
OrganizationID: req.OrganizationID,
|
||||||
|
OutletID: req.OutletID,
|
||||||
|
DateFrom: mtdStart,
|
||||||
|
DateTo: req.DateTo,
|
||||||
|
ExcludeGajiStaffFromReimburse: req.ExcludeGajiStaffFromReimburse,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
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) {
|
||||||
result, err := p.analyticsRepo.GetExclusiveSummaryAnalytics(ctx, req.OrganizationID, req.OutletID, req.DateFrom, req.DateTo)
|
result, err := p.analyticsRepo.GetExclusiveSummaryAnalytics(ctx, req.OrganizationID, req.OutletID, req.DateFrom, req.DateTo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ type analyticsRepositoryStub struct {
|
|||||||
bankBalances []entities.ExclusiveSummaryBankBalance
|
bankBalances []entities.ExclusiveSummaryBankBalance
|
||||||
profitLossGroup string
|
profitLossGroup string
|
||||||
exclusiveSummaryCalls int
|
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) {
|
||||||
@@ -50,7 +52,9 @@ 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, time.Time, time.Time) (*entities.ExclusiveSummaryAnalytics, error) {
|
func (s *analyticsRepositoryStub) GetExclusiveSummaryAnalytics(_ context.Context, _ uuid.UUID, _ *uuid.UUID, dateFrom, dateTo time.Time) (*entities.ExclusiveSummaryAnalytics, error) {
|
||||||
|
s.exclusiveSummaryFrom = append(s.exclusiveSummaryFrom, dateFrom)
|
||||||
|
s.exclusiveSummaryTo = append(s.exclusiveSummaryTo, dateTo)
|
||||||
if s.exclusiveSummaryCalls < len(s.exclusiveSummaryResults) {
|
if s.exclusiveSummaryCalls < len(s.exclusiveSummaryResults) {
|
||||||
result := s.exclusiveSummaryResults[s.exclusiveSummaryCalls]
|
result := s.exclusiveSummaryResults[s.exclusiveSummaryCalls]
|
||||||
s.exclusiveSummaryCalls++
|
s.exclusiveSummaryCalls++
|
||||||
@@ -393,3 +397,52 @@ func TestAnalyticsProcessorGetExclusiveSummaryMonthlyBuildsSummaryAndBuckets(t *
|
|||||||
require.Equal(t, notes, *result.BankBalance[0].Notes)
|
require.Equal(t, notes, *result.BankBalance[0].Notes)
|
||||||
require.Equal(t, 6, stub.exclusiveSummaryCalls)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -55,10 +55,12 @@ func NewPurchaseOrderProcessorImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *PurchaseOrderProcessorImpl) CreatePurchaseOrder(ctx context.Context, organizationID 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
|
// Check if vendor exists and belongs to organization when provided.
|
||||||
_, err := p.vendorRepo.GetByIDAndOrganizationID(ctx, req.VendorID, organizationID)
|
if req.VendorID != nil {
|
||||||
if err != nil {
|
_, err := p.vendorRepo.GetByIDAndOrganizationID(ctx, *req.VendorID, organizationID)
|
||||||
return nil, fmt.Errorf("vendor not found: %w", err)
|
if err != nil {
|
||||||
|
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
|
||||||
@@ -107,7 +109,7 @@ 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 += item.Amount
|
totalAmount += calculatePurchaseOrderItemTotal(item.Quantity, item.Amount)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create purchase order entity
|
// Create purchase order entity
|
||||||
@@ -186,7 +188,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)
|
||||||
@@ -277,7 +279,7 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrder(ctx context.Context, id
|
|||||||
UnitID: unitID,
|
UnitID: unitID,
|
||||||
Amount: amount,
|
Amount: amount,
|
||||||
}
|
}
|
||||||
totalAmount += amount
|
totalAmount += calculatePurchaseOrderItemTotal(quantity, amount)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete and recreate only after all replacement items are valid.
|
// Delete and recreate only after all replacement items are valid.
|
||||||
@@ -447,7 +449,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 = item.Amount / quantityToAdd
|
unitCost = calculatePurchaseOrderItemTotal(item.Quantity, item.Amount) / quantityToAdd
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create inventory movement for ingredient purchase
|
// Create inventory movement for ingredient purchase
|
||||||
@@ -506,3 +508,11 @@ 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx contex
|
|||||||
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 po.vendor_id) as total_vendors
|
COUNT(DISTINCT COALESCE(po.vendor_id::text, 'no-vendor')) 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 ingredients i ON poi.ingredient_id = i.id").
|
Joins("LEFT JOIN ingredients i ON poi.ingredient_id = i.id").
|
||||||
@@ -197,7 +197,7 @@ func (r *AnalyticsRepositoryImpl) getPurchaseOrderPurchasingAnalytics(ctx contex
|
|||||||
COUNT(DISTINCT po.id) as purchase_orders,
|
COUNT(DISTINCT po.id) as purchase_orders,
|
||||||
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 po.vendor_id) as vendors
|
COUNT(DISTINCT COALESCE(po.vendor_id::text, 'no-vendor')) 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 ingredients i ON poi.ingredient_id = i.id").
|
Joins("LEFT JOIN ingredients i ON poi.ingredient_id = i.id").
|
||||||
@@ -247,20 +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,
|
||||||
v.name as vendor_name,
|
COALESCE(v.name, 'No Vendor') as vendor_name,
|
||||||
COALESCE(SUM(poi.amount), 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("JOIN vendors v ON po.vendor_id = v.id").
|
Joins("LEFT 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 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, v.name").
|
Group("v.id, COALESCE(v.name, 'No Vendor')").
|
||||||
Order("total_cost DESC")
|
Order("total_cost DESC")
|
||||||
vendorQuery = r.applyPurchaseOrderItemOutletFilter(vendorQuery, outletID)
|
vendorQuery = r.applyPurchaseOrderItemOutletFilter(vendorQuery, outletID)
|
||||||
|
|
||||||
|
|||||||
@@ -339,6 +339,7 @@ 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,6 +20,7 @@ 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 {
|
||||||
@@ -349,6 +350,19 @@ 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")
|
||||||
@@ -373,6 +387,22 @@ 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,6 +49,10 @@ 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)
|
||||||
@@ -254,3 +258,44 @@ func TestAnalyticsServiceGetExclusiveSummaryMonthlyValidation(t *testing.T) {
|
|||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.Contains(t, err.Error(), "month is required")
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -604,6 +604,27 @@ 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
|
||||||
@@ -772,6 +793,22 @@ 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 {
|
||||||
|
|||||||
@@ -12,12 +12,13 @@ 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: uuid.New(),
|
VendorID: &vendorID,
|
||||||
PONumber: "PO-001",
|
PONumber: "PO-001",
|
||||||
TransactionDate: "2026-05-29",
|
TransactionDate: "2026-05-29",
|
||||||
Items: []contract.CreatePurchaseOrderItemRequest{
|
Items: []contract.CreatePurchaseOrderItemRequest{
|
||||||
@@ -35,9 +36,10 @@ 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: uuid.New(),
|
VendorID: &vendorID,
|
||||||
PONumber: "PO-001",
|
PONumber: "PO-001",
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -45,3 +47,19 @@ 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 == uuid.Nil {
|
if req.VendorID != nil && *req.VendorID == uuid.Nil {
|
||||||
return errors.New("vendor_id is required"), constants.MissingFieldErrorCode
|
return errors.New("vendor_id cannot be empty"), constants.MalformedFieldErrorCode
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.TrimSpace(req.PONumber) == "" {
|
if strings.TrimSpace(req.PONumber) == "" {
|
||||||
|
|||||||
@@ -11,12 +11,13 @@ 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: uuid.New(),
|
VendorID: &vendorID,
|
||||||
PONumber: "PO-001",
|
PONumber: "PO-001",
|
||||||
TransactionDate: "2026-05-29",
|
TransactionDate: "2026-05-29",
|
||||||
Items: []contract.CreatePurchaseOrderItemRequest{
|
Items: []contract.CreatePurchaseOrderItemRequest{
|
||||||
@@ -31,6 +32,30 @@ 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()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE purchase_orders
|
||||||
|
ALTER COLUMN vendor_id SET NOT NULL;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE purchase_orders
|
||||||
|
ALTER COLUMN vendor_id DROP NOT NULL;
|
||||||
Reference in New Issue
Block a user