diff --git a/internal/contract/purchase_order_contract.go b/internal/contract/purchase_order_contract.go index b5ae3bb..7038b4b 100644 --- a/internal/contract/purchase_order_contract.go +++ b/internal/contract/purchase_order_contract.go @@ -52,6 +52,7 @@ type UpdatePurchaseOrderItemRequest struct { type PurchaseOrderResponse struct { ID uuid.UUID `json:"id"` OrganizationID uuid.UUID `json:"organization_id"` + OutletID *uuid.UUID `json:"outlet_id"` VendorID *uuid.UUID `json:"vendor_id"` PONumber string `json:"po_number"` TransactionDate time.Time `json:"transaction_date"` diff --git a/internal/entities/purchase_order.go b/internal/entities/purchase_order.go index 7146fb6..ba5209e 100644 --- a/internal/entities/purchase_order.go +++ b/internal/entities/purchase_order.go @@ -11,6 +11,7 @@ import ( type PurchaseOrder struct { 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"` + OutletID *uuid.UUID `gorm:"type:uuid;index" json:"outlet_id" validate:"omitempty"` 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"` TransactionDate time.Time `gorm:"type:date;not null" json:"transaction_date" validate:"required"` @@ -23,6 +24,7 @@ type PurchaseOrder struct { UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` Organization *Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"` + Outlet *Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"` Vendor *Vendor `gorm:"foreignKey:VendorID" json:"vendor,omitempty"` Items []PurchaseOrderItem `gorm:"foreignKey:PurchaseOrderID" json:"items,omitempty"` Attachments []PurchaseOrderAttachment `gorm:"foreignKey:PurchaseOrderID" json:"attachments,omitempty"` diff --git a/internal/mappers/purchase_order_mapper.go b/internal/mappers/purchase_order_mapper.go index b08327f..5be72d2 100644 --- a/internal/mappers/purchase_order_mapper.go +++ b/internal/mappers/purchase_order_mapper.go @@ -13,6 +13,7 @@ func PurchaseOrderEntityToModel(entity *entities.PurchaseOrder) *models.Purchase return &models.PurchaseOrder{ ID: entity.ID, OrganizationID: entity.OrganizationID, + OutletID: entity.OutletID, VendorID: entity.VendorID, PONumber: entity.PONumber, TransactionDate: entity.TransactionDate, @@ -34,6 +35,7 @@ func PurchaseOrderModelToEntity(model *models.PurchaseOrder) *entities.PurchaseO return &entities.PurchaseOrder{ ID: model.ID, OrganizationID: model.OrganizationID, + OutletID: model.OutletID, VendorID: model.VendorID, PONumber: model.PONumber, TransactionDate: model.TransactionDate, @@ -55,6 +57,7 @@ func PurchaseOrderEntityToResponse(entity *entities.PurchaseOrder) *models.Purch response := &models.PurchaseOrderResponse{ ID: entity.ID, OrganizationID: entity.OrganizationID, + OutletID: entity.OutletID, VendorID: entity.VendorID, PONumber: entity.PONumber, TransactionDate: entity.TransactionDate, diff --git a/internal/models/purchase_order.go b/internal/models/purchase_order.go index d3428e2..7122ed8 100644 --- a/internal/models/purchase_order.go +++ b/internal/models/purchase_order.go @@ -9,6 +9,7 @@ import ( type PurchaseOrder struct { ID uuid.UUID `json:"id"` OrganizationID uuid.UUID `json:"organization_id"` + OutletID *uuid.UUID `json:"outlet_id"` VendorID *uuid.UUID `json:"vendor_id"` PONumber string `json:"po_number"` TransactionDate time.Time `json:"transaction_date"` @@ -44,6 +45,7 @@ type PurchaseOrderAttachment struct { type PurchaseOrderResponse struct { ID uuid.UUID `json:"id"` OrganizationID uuid.UUID `json:"organization_id"` + OutletID *uuid.UUID `json:"outlet_id"` VendorID *uuid.UUID `json:"vendor_id"` PONumber string `json:"po_number"` TransactionDate time.Time `json:"transaction_date"` @@ -85,6 +87,7 @@ type PurchaseOrderAttachmentResponse struct { type CreatePurchaseOrderRequest struct { VendorID *uuid.UUID `json:"vendor_id,omitempty"` + OutletID *uuid.UUID `json:"outlet_id,omitempty"` PONumber string `json:"po_number"` TransactionDate time.Time `json:"transaction_date"` DueDate *time.Time `json:"due_date,omitempty"` diff --git a/internal/processor/purchase_order_processor.go b/internal/processor/purchase_order_processor.go index 8782d00..fe8de6e 100644 --- a/internal/processor/purchase_order_processor.go +++ b/internal/processor/purchase_order_processor.go @@ -11,8 +11,8 @@ import ( ) type PurchaseOrderProcessor interface { - CreatePurchaseOrder(ctx context.Context, organizationID uuid.UUID, req *models.CreatePurchaseOrderRequest) (*models.PurchaseOrderResponse, error) - UpdatePurchaseOrder(ctx context.Context, id, organizationID uuid.UUID, req *models.UpdatePurchaseOrderRequest) (*models.PurchaseOrderResponse, error) + CreatePurchaseOrder(ctx context.Context, organizationID uuid.UUID, outletID *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) DeletePurchaseOrder(ctx context.Context, id, organizationID uuid.UUID) 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) @@ -54,7 +54,7 @@ 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, outletID *uuid.UUID, req *models.CreatePurchaseOrderRequest) (*models.PurchaseOrderResponse, error) { // Check if vendor exists and belongs to organization when provided. if req.VendorID != nil { _, err := p.vendorRepo.GetByIDAndOrganizationID(ctx, *req.VendorID, organizationID) @@ -115,6 +115,7 @@ func (p *PurchaseOrderProcessorImpl) CreatePurchaseOrder(ctx context.Context, or // Create purchase order entity poEntity := &entities.PurchaseOrder{ OrganizationID: organizationID, + OutletID: outletID, VendorID: req.VendorID, PONumber: req.PONumber, TransactionDate: req.TransactionDate, @@ -175,12 +176,15 @@ func (p *PurchaseOrderProcessorImpl) CreatePurchaseOrder(ctx context.Context, or return mappers.PurchaseOrderEntityToResponse(createdPO), nil } -func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrder(ctx context.Context, id, organizationID uuid.UUID, req *models.UpdatePurchaseOrderRequest) (*models.PurchaseOrderResponse, error) { +func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrder(ctx context.Context, id, organizationID uuid.UUID, outletID *uuid.UUID, req *models.UpdatePurchaseOrderRequest) (*models.PurchaseOrderResponse, error) { // Get existing purchase order poEntity, err := p.purchaseOrderRepo.GetByIDAndOrganizationID(ctx, id, organizationID) if err != nil { 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) if req.VendorID != nil { @@ -478,7 +482,12 @@ func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrderStatus(ctx context.Conte } // Update the purchase order status - err = p.purchaseOrderRepo.UpdateStatus(ctx, id, status) + statusOutletID := po.OutletID + if statusOutletID == nil && outletID != uuid.Nil { + statusOutletID = &outletID + } + + err = p.purchaseOrderRepo.UpdateStatusAndOutlet(ctx, id, status, statusOutletID) if err != nil { return nil, fmt.Errorf("failed to update purchase order status: %w", err) } diff --git a/internal/processor/purchase_order_repository.go b/internal/processor/purchase_order_repository.go index 98fa3d4..776e627 100644 --- a/internal/processor/purchase_order_repository.go +++ b/internal/processor/purchase_order_repository.go @@ -19,6 +19,7 @@ type PurchaseOrderRepository interface { GetByStatus(ctx context.Context, organizationID uuid.UUID, status string) ([]*entities.PurchaseOrder, error) GetOverdue(ctx context.Context, organizationID uuid.UUID) ([]*entities.PurchaseOrder, 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 CreateItem(ctx context.Context, item *entities.PurchaseOrderItem) error UpdateItem(ctx context.Context, item *entities.PurchaseOrderItem) error diff --git a/internal/repository/analytics_repository.go b/internal/repository/analytics_repository.go index b51f0d3..8387181 100644 --- a/internal/repository/analytics_repository.go +++ b/internal/repository/analytics_repository.go @@ -281,7 +281,7 @@ func (r *AnalyticsRepositoryImpl) applyPurchaseOrderItemOutletFilter(query *gorm if outletID == nil { return query } - return query.Where("(i.outlet_id = ? OR u.outlet_id = ?)", *outletID, *outletID) + return query.Where("po.outlet_id = ?", *outletID) } func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, limit int) ([]*entities.ProductAnalytics, error) { @@ -717,7 +717,7 @@ func (r *AnalyticsRepositoryImpl) GetExclusiveSummaryAnalytics(ctx context.Conte return nil, err } - operationalExpenseBreakdown, err := r.getExclusiveSummaryOperationalExpenseBreakdown(ctx, organizationID, dateFrom, dateTo) + operationalExpenseBreakdown, err := r.getExclusiveSummaryOperationalExpenseBreakdown(ctx, organizationID, outletID, dateFrom, dateTo) if err != nil { return nil, err } @@ -770,10 +770,10 @@ func (r *AnalyticsRepositoryImpl) getExclusiveSummaryHPPBreakdown(ctx context.Co return results, err } -func (r *AnalyticsRepositoryImpl) getExclusiveSummaryOperationalExpenseBreakdown(ctx context.Context, organizationID uuid.UUID, dateFrom, dateTo time.Time) ([]entities.ExclusiveSummaryCategoryTotal, error) { +func (r *AnalyticsRepositoryImpl) getExclusiveSummaryOperationalExpenseBreakdown(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) ([]entities.ExclusiveSummaryCategoryTotal, error) { var results []entities.ExclusiveSummaryCategoryTotal - err := r.db.WithContext(ctx). + query := r.db.WithContext(ctx). Table("purchase_order_items poi"). Select(` pc.code as category_code, @@ -785,7 +785,10 @@ func (r *AnalyticsRepositoryImpl) getExclusiveSummaryOperationalExpenseBreakdown Where("po.organization_id = ?", organizationID). Where("pc.type = ?", entities.PurchaseCategoryTypeExpense). 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"). Order("pc.sort_order ASC, pc.name ASC"). Scan(&results).Error @@ -830,8 +833,8 @@ func (r *AnalyticsRepositoryImpl) exclusiveSummaryPurchaseOrderItemQuery(organiz } if outletID != nil { - outletFilter = "AND (pc.type = ? OR i.outlet_id = ? OR u.outlet_id = ?)" - args = append(args, entities.PurchaseCategoryTypeExpense, *outletID, *outletID) + outletFilter = "AND po.outlet_id = ?" + args = append(args, *outletID) } query := ` diff --git a/internal/repository/purchase_order_repository.go b/internal/repository/purchase_order_repository.go index 8383eec..d65cd6b 100644 --- a/internal/repository/purchase_order_repository.go +++ b/internal/repository/purchase_order_repository.go @@ -196,6 +196,18 @@ func (r *PurchaseOrderRepositoryImpl) UpdateStatus(ctx context.Context, id uuid. 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 { return r.db.WithContext(ctx). Model(&entities.PurchaseOrder{}). diff --git a/internal/service/purchase_order_service.go b/internal/service/purchase_order_service.go index 4c9df39..27fbcb7 100644 --- a/internal/service/purchase_order_service.go +++ b/internal/service/purchase_order_service.go @@ -40,7 +40,12 @@ func (s *PurchaseOrderServiceImpl) CreatePurchaseOrder(ctx context.Context, apct return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) } - poResponse, err := s.purchaseOrderProcessor.CreatePurchaseOrder(ctx, apctx.OrganizationID, modelReq) + var outletID *uuid.UUID + if apctx.OutletID != uuid.Nil { + outletID = &apctx.OutletID + } + + poResponse, err := s.purchaseOrderProcessor.CreatePurchaseOrder(ctx, apctx.OrganizationID, outletID, modelReq) if err != nil { errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.PurchaseOrderServiceEntity, err.Error()) return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) @@ -57,7 +62,12 @@ func (s *PurchaseOrderServiceImpl) UpdatePurchaseOrder(ctx context.Context, apct return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) } - poResponse, err := s.purchaseOrderProcessor.UpdatePurchaseOrder(ctx, id, apctx.OrganizationID, modelReq) + var outletID *uuid.UUID + if apctx.OutletID != uuid.Nil { + outletID = &apctx.OutletID + } + + poResponse, err := s.purchaseOrderProcessor.UpdatePurchaseOrder(ctx, id, apctx.OrganizationID, outletID, modelReq) if err != nil { errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.PurchaseOrderServiceEntity, err.Error()) return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) diff --git a/internal/transformer/purchase_order_transformer.go b/internal/transformer/purchase_order_transformer.go index b61e2ef..814b8c4 100644 --- a/internal/transformer/purchase_order_transformer.go +++ b/internal/transformer/purchase_order_transformer.go @@ -120,6 +120,7 @@ func PurchaseOrderModelResponseToResponse(po *models.PurchaseOrderResponse) *con response := &contract.PurchaseOrderResponse{ ID: po.ID, OrganizationID: po.OrganizationID, + OutletID: po.OutletID, VendorID: po.VendorID, PONumber: po.PONumber, TransactionDate: po.TransactionDate, diff --git a/migrations/000082_add_outlet_id_to_purchase_orders.down.sql b/migrations/000082_add_outlet_id_to_purchase_orders.down.sql new file mode 100644 index 0000000..5a1a97f --- /dev/null +++ b/migrations/000082_add_outlet_id_to_purchase_orders.down.sql @@ -0,0 +1,4 @@ +DROP INDEX IF EXISTS idx_purchase_orders_outlet_id; + +ALTER TABLE purchase_orders +DROP COLUMN IF EXISTS outlet_id; diff --git a/migrations/000082_add_outlet_id_to_purchase_orders.up.sql b/migrations/000082_add_outlet_id_to_purchase_orders.up.sql new file mode 100644 index 0000000..a9dea93 --- /dev/null +++ b/migrations/000082_add_outlet_id_to_purchase_orders.up.sql @@ -0,0 +1,52 @@ +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;