update order status

This commit is contained in:
Aditya Siregar 2025-08-13 23:36:31 +07:00
parent ccb0458189
commit ee7d0e529b
9 changed files with 846 additions and 35 deletions

View File

@ -67,3 +67,51 @@ type InventoryAdjustmentResponse struct {
Reason string `json:"reason"` Reason string `json:"reason"`
AdjustedAt time.Time `json:"adjusted_at"` AdjustedAt time.Time `json:"adjusted_at"`
} }
// Inventory Report Contracts
type InventoryReportSummaryResponse struct {
TotalProducts int `json:"total_products"`
TotalIngredients int `json:"total_ingredients"`
TotalValue float64 `json:"total_value"`
LowStockProducts int `json:"low_stock_products"`
LowStockIngredients int `json:"low_stock_ingredients"`
ZeroStockProducts int `json:"zero_stock_products"`
ZeroStockIngredients int `json:"zero_stock_ingredients"`
OutletID string `json:"outlet_id"`
OutletName string `json:"outlet_name"`
GeneratedAt string `json:"generated_at"`
}
type InventoryReportDetailResponse struct {
Summary *InventoryReportSummaryResponse `json:"summary"`
Products []*InventoryProductDetailResponse `json:"products"`
Ingredients []*InventoryIngredientDetailResponse `json:"ingredients"`
}
type InventoryProductDetailResponse struct {
ID string `json:"id"`
ProductID string `json:"product_id"`
ProductName string `json:"product_name"`
CategoryName string `json:"category_name"`
Quantity int `json:"quantity"`
ReorderLevel int `json:"reorder_level"`
UnitCost float64 `json:"unit_cost"`
TotalValue float64 `json:"total_value"`
IsLowStock bool `json:"is_low_stock"`
IsZeroStock bool `json:"is_zero_stock"`
UpdatedAt string `json:"updated_at"`
}
type InventoryIngredientDetailResponse struct {
ID string `json:"id"`
IngredientID string `json:"ingredient_id"`
IngredientName string `json:"ingredient_name"`
UnitName string `json:"unit_name"`
Quantity int `json:"quantity"`
ReorderLevel int `json:"reorder_level"`
UnitCost float64 `json:"unit_cost"`
TotalValue float64 `json:"total_value"`
IsLowStock bool `json:"is_low_stock"`
IsZeroStock bool `json:"is_zero_stock"`
UpdatedAt string `json:"updated_at"`
}

View File

@ -7,6 +7,7 @@ import (
"apskel-pos-be/internal/constants" "apskel-pos-be/internal/constants"
"apskel-pos-be/internal/contract" "apskel-pos-be/internal/contract"
"apskel-pos-be/internal/logger" "apskel-pos-be/internal/logger"
"apskel-pos-be/internal/models"
"apskel-pos-be/internal/service" "apskel-pos-be/internal/service"
"apskel-pos-be/internal/util" "apskel-pos-be/internal/util"
"apskel-pos-be/internal/validator" "apskel-pos-be/internal/validator"
@ -277,3 +278,111 @@ func (h *InventoryHandler) GetZeroStockItems(c *gin.Context) {
util.HandleResponse(c.Writer, c.Request, inventoryResponse, "InventoryHandler::GetZeroStockItems") util.HandleResponse(c.Writer, c.Request, inventoryResponse, "InventoryHandler::GetZeroStockItems")
} }
// GetInventoryReportSummary returns summary statistics for inventory report
func (h *InventoryHandler) GetInventoryReportSummary(c *gin.Context) {
ctx := c.Request.Context()
contextInfo := appcontext.FromGinContext(ctx)
outletIDStr := c.Param("outlet_id")
outletID, err := uuid.Parse(outletIDStr)
if err != nil {
logger.FromContext(ctx).WithError(err).Error("InventoryHandler::GetInventoryReportSummary -> Invalid outlet ID")
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid outlet ID")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "InventoryHandler::GetInventoryReportSummary")
return
}
summary, err := h.inventoryService.GetInventoryReportSummary(ctx, outletID, contextInfo.OrganizationID)
if err != nil {
logger.FromContext(ctx).WithError(err).Error("InventoryHandler::GetInventoryReportSummary -> Failed to get inventory report summary from service")
responseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error())
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{responseError}), "InventoryHandler::GetInventoryReportSummary")
return
}
response := contract.BuildSuccessResponse(summary)
util.HandleResponse(c.Writer, c.Request, response, "InventoryHandler::GetInventoryReportSummary")
}
// GetInventoryReportDetails returns detailed inventory report with products and ingredients
func (h *InventoryHandler) GetInventoryReportDetails(c *gin.Context) {
ctx := c.Request.Context()
contextInfo := appcontext.FromGinContext(ctx)
// Parse query parameters
filter := &models.InventoryReportFilter{}
// Parse outlet_id (required)
if outletIDStr := c.Query("outlet_id"); outletIDStr != "" {
outletID, err := uuid.Parse(outletIDStr)
if err != nil {
logger.FromContext(ctx).WithError(err).Error("InventoryHandler::GetInventoryReportDetails -> Invalid outlet ID")
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid outlet ID")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "InventoryHandler::GetInventoryReportDetails")
return
}
filter.OutletID = &outletID
} else {
logger.FromContext(ctx).Error("InventoryHandler::GetInventoryReportDetails -> Missing outlet_id parameter")
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "outlet_id is required")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "InventoryHandler::GetInventoryReportDetails")
return
}
// Parse category_id (optional)
if categoryIDStr := c.Query("category_id"); categoryIDStr != "" {
categoryID, err := uuid.Parse(categoryIDStr)
if err != nil {
logger.FromContext(ctx).WithError(err).Error("InventoryHandler::GetInventoryReportDetails -> Invalid category ID")
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid category ID")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "InventoryHandler::GetInventoryReportDetails")
return
}
filter.CategoryID = &categoryID
}
// Parse show_low_stock (optional)
if showLowStockStr := c.Query("show_low_stock"); showLowStockStr != "" {
if showLowStock, err := strconv.ParseBool(showLowStockStr); err == nil {
filter.ShowLowStock = &showLowStock
}
}
// Parse show_zero_stock (optional)
if showZeroStockStr := c.Query("show_zero_stock"); showZeroStockStr != "" {
if showZeroStock, err := strconv.ParseBool(showZeroStockStr); err == nil {
filter.ShowZeroStock = &showZeroStock
}
}
// Parse search (optional)
if search := c.Query("search"); search != "" {
filter.Search = &search
}
// Parse limit (optional)
if limitStr := c.Query("limit"); limitStr != "" {
if limit, err := strconv.Atoi(limitStr); err == nil && limit > 0 {
filter.Limit = &limit
}
}
// Parse offset (optional)
if offsetStr := c.Query("offset"); offsetStr != "" {
if offset, err := strconv.Atoi(offsetStr); err == nil && offset >= 0 {
filter.Offset = &offset
}
}
report, err := h.inventoryService.GetInventoryReportDetails(ctx, filter, contextInfo.OrganizationID)
if err != nil {
logger.FromContext(ctx).WithError(err).Error("InventoryHandler::GetInventoryReportDetails -> Failed to get inventory report details from service")
responseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error())
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{responseError}), "InventoryHandler::GetInventoryReportDetails")
return
}
response := contract.BuildSuccessResponse(report)
util.HandleResponse(c.Writer, c.Request, response, "InventoryHandler::GetInventoryReportDetails")
}

View File

@ -28,8 +28,10 @@ type UpdateInventoryRequest struct {
} }
type InventoryAdjustmentRequest struct { type InventoryAdjustmentRequest struct {
Delta int ProductID uuid.UUID
Reason string OutletID uuid.UUID
Delta int
Reason string
} }
type InventoryResponse struct { type InventoryResponse struct {
@ -58,3 +60,61 @@ func (i *Inventory) AdjustQuantity(delta int) int {
} }
return newQuantity return newQuantity
} }
// Inventory Report Models
type InventoryReportSummary struct {
TotalProducts int `json:"total_products"`
TotalIngredients int `json:"total_ingredients"`
TotalValue float64 `json:"total_value"`
LowStockProducts int `json:"low_stock_products"`
LowStockIngredients int `json:"low_stock_ingredients"`
ZeroStockProducts int `json:"zero_stock_products"`
ZeroStockIngredients int `json:"zero_stock_ingredients"`
OutletID uuid.UUID `json:"outlet_id"`
OutletName string `json:"outlet_name"`
GeneratedAt time.Time `json:"generated_at"`
}
type InventoryReportDetail struct {
Summary *InventoryReportSummary `json:"summary"`
Products []*InventoryProductDetail `json:"products"`
Ingredients []*InventoryIngredientDetail `json:"ingredients"`
}
type InventoryProductDetail struct {
ID uuid.UUID `json:"id"`
ProductID uuid.UUID `json:"product_id"`
ProductName string `json:"product_name"`
CategoryName string `json:"category_name"`
Quantity int `json:"quantity"`
ReorderLevel int `json:"reorder_level"`
UnitCost float64 `json:"unit_cost"`
TotalValue float64 `json:"total_value"`
IsLowStock bool `json:"is_low_stock"`
IsZeroStock bool `json:"is_zero_stock"`
UpdatedAt time.Time `json:"updated_at"`
}
type InventoryIngredientDetail struct {
ID uuid.UUID `json:"id"`
IngredientID uuid.UUID `json:"ingredient_id"`
IngredientName string `json:"ingredient_name"`
UnitName string `json:"unit_name"`
Quantity int `json:"quantity"`
ReorderLevel int `json:"reorder_level"`
UnitCost float64 `json:"unit_cost"`
TotalValue float64 `json:"total_value"`
IsLowStock bool `json:"is_low_stock"`
IsZeroStock bool `json:"is_zero_stock"`
UpdatedAt time.Time `json:"updated_at"`
}
type InventoryReportFilter struct {
OutletID *uuid.UUID `json:"outlet_id"`
CategoryID *uuid.UUID `json:"category_id"`
ShowLowStock *bool `json:"show_low_stock"`
ShowZeroStock *bool `json:"show_zero_stock"`
Search *string `json:"search"`
Limit *int `json:"limit"`
Offset *int `json:"offset"`
}

View File

@ -12,14 +12,21 @@ import (
) )
type InventoryProcessor interface { type InventoryProcessor interface {
CreateInventory(ctx context.Context, req *models.CreateInventoryRequest) (*models.InventoryResponse, error) Create(ctx context.Context, req *models.CreateInventoryRequest, organizationID uuid.UUID) (*models.InventoryResponse, error)
UpdateInventory(ctx context.Context, id uuid.UUID, req *models.UpdateInventoryRequest) (*models.InventoryResponse, error) GetByID(ctx context.Context, id, organizationID uuid.UUID) (*models.InventoryResponse, error)
DeleteInventory(ctx context.Context, id uuid.UUID) error GetByProductAndOutlet(ctx context.Context, productID, outletID, organizationID uuid.UUID) (*models.InventoryResponse, error)
GetInventoryByID(ctx context.Context, id uuid.UUID) (*models.InventoryResponse, error) GetByOutlet(ctx context.Context, outletID, organizationID uuid.UUID) ([]*models.InventoryResponse, error)
ListInventory(ctx context.Context, filters map[string]interface{}, page, limit int) ([]models.InventoryResponse, int, error) GetByProduct(ctx context.Context, productID, organizationID uuid.UUID) ([]*models.InventoryResponse, error)
AdjustInventory(ctx context.Context, productID, outletID uuid.UUID, req *models.InventoryAdjustmentRequest) (*models.InventoryResponse, error) GetLowStock(ctx context.Context, outletID, organizationID uuid.UUID) ([]*models.InventoryResponse, error)
GetLowStockItems(ctx context.Context, outletID uuid.UUID) ([]models.InventoryResponse, error) GetZeroStock(ctx context.Context, outletID, organizationID uuid.UUID) ([]*models.InventoryResponse, error)
GetZeroStockItems(ctx context.Context, outletID uuid.UUID) ([]models.InventoryResponse, error) Update(ctx context.Context, id uuid.UUID, req *models.UpdateInventoryRequest, organizationID uuid.UUID) (*models.InventoryResponse, error)
Delete(ctx context.Context, id, organizationID uuid.UUID) error
List(ctx context.Context, filters map[string]interface{}, limit, offset int, organizationID uuid.UUID) ([]*models.InventoryResponse, int64, error)
AdjustQuantity(ctx context.Context, productID, outletID, organizationID uuid.UUID, delta int) (*models.InventoryResponse, error)
SetQuantity(ctx context.Context, productID, outletID, organizationID uuid.UUID, quantity int) (*models.InventoryResponse, error)
UpdateReorderLevel(ctx context.Context, id uuid.UUID, reorderLevel int, organizationID uuid.UUID) error
GetInventoryReportSummary(ctx context.Context, outletID, organizationID uuid.UUID) (*models.InventoryReportSummary, error)
GetInventoryReportDetails(ctx context.Context, filter *models.InventoryReportFilter, organizationID uuid.UUID) (*models.InventoryReportDetail, error)
} }
type InventoryProcessorImpl struct { type InventoryProcessorImpl struct {
@ -40,7 +47,8 @@ func NewInventoryProcessorImpl(
} }
} }
func (p *InventoryProcessorImpl) CreateInventory(ctx context.Context, req *models.CreateInventoryRequest) (*models.InventoryResponse, error) { // Create creates a new inventory record
func (p *InventoryProcessorImpl) Create(ctx context.Context, req *models.CreateInventoryRequest, organizationID uuid.UUID) (*models.InventoryResponse, error) {
_, err := p.productRepo.GetByID(ctx, req.ProductID) _, err := p.productRepo.GetByID(ctx, req.ProductID)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid product: %w", err) return nil, fmt.Errorf("invalid product: %w", err)
@ -77,7 +85,8 @@ func (p *InventoryProcessorImpl) CreateInventory(ctx context.Context, req *model
return response, nil return response, nil
} }
func (p *InventoryProcessorImpl) UpdateInventory(ctx context.Context, id uuid.UUID, req *models.UpdateInventoryRequest) (*models.InventoryResponse, error) { // Update updates an existing inventory record
func (p *InventoryProcessorImpl) Update(ctx context.Context, id uuid.UUID, req *models.UpdateInventoryRequest, organizationID uuid.UUID) (*models.InventoryResponse, error) {
// Get existing inventory // Get existing inventory
existingInventory, err := p.inventoryRepo.GetByID(ctx, id) existingInventory, err := p.inventoryRepo.GetByID(ctx, id)
if err != nil { if err != nil {
@ -103,7 +112,8 @@ func (p *InventoryProcessorImpl) UpdateInventory(ctx context.Context, id uuid.UU
return response, nil return response, nil
} }
func (p *InventoryProcessorImpl) DeleteInventory(ctx context.Context, id uuid.UUID) error { // Delete deletes an inventory record
func (p *InventoryProcessorImpl) Delete(ctx context.Context, id uuid.UUID, organizationID uuid.UUID) error {
// Check if inventory exists // Check if inventory exists
_, err := p.inventoryRepo.GetByID(ctx, id) _, err := p.inventoryRepo.GetByID(ctx, id)
if err != nil { if err != nil {
@ -118,6 +128,177 @@ func (p *InventoryProcessorImpl) DeleteInventory(ctx context.Context, id uuid.UU
return nil return nil
} }
// GetByID retrieves an inventory record by ID
func (p *InventoryProcessorImpl) GetByID(ctx context.Context, id, organizationID uuid.UUID) (*models.InventoryResponse, error) {
inventory, err := p.inventoryRepo.GetWithRelations(ctx, id)
if err != nil {
return nil, fmt.Errorf("inventory not found: %w", err)
}
response := mappers.InventoryEntityToResponse(inventory)
return response, nil
}
// GetByProductAndOutlet retrieves inventory by product and outlet
func (p *InventoryProcessorImpl) GetByProductAndOutlet(ctx context.Context, productID, outletID, organizationID uuid.UUID) (*models.InventoryResponse, error) {
inventory, err := p.inventoryRepo.GetByProductAndOutlet(ctx, productID, outletID)
if err != nil {
return nil, fmt.Errorf("inventory not found: %w", err)
}
response := mappers.InventoryEntityToResponse(inventory)
return response, nil
}
// GetByOutlet retrieves all inventory records for a specific outlet
func (p *InventoryProcessorImpl) GetByOutlet(ctx context.Context, outletID, organizationID uuid.UUID) ([]*models.InventoryResponse, error) {
inventories, err := p.inventoryRepo.GetByOutlet(ctx, outletID)
if err != nil {
return nil, fmt.Errorf("failed to get inventory by outlet: %w", err)
}
var responses []*models.InventoryResponse
for _, inventory := range inventories {
response := mappers.InventoryEntityToResponse(inventory)
responses = append(responses, response)
}
return responses, nil
}
// GetByProduct retrieves all inventory records for a specific product
func (p *InventoryProcessorImpl) GetByProduct(ctx context.Context, productID, organizationID uuid.UUID) ([]*models.InventoryResponse, error) {
inventories, err := p.inventoryRepo.GetByProduct(ctx, productID)
if err != nil {
return nil, fmt.Errorf("failed to get inventory by product: %w", err)
}
var responses []*models.InventoryResponse
for _, inventory := range inventories {
response := mappers.InventoryEntityToResponse(inventory)
responses = append(responses, response)
}
return responses, nil
}
// List retrieves inventory records with filtering and pagination
func (p *InventoryProcessorImpl) List(ctx context.Context, filters map[string]interface{}, limit, offset int, organizationID uuid.UUID) ([]*models.InventoryResponse, int64, error) {
inventories, totalCount, err := p.inventoryRepo.List(ctx, filters, limit, offset)
if err != nil {
return nil, 0, fmt.Errorf("failed to list inventory: %w", err)
}
var responses []*models.InventoryResponse
for _, inventory := range inventories {
response := mappers.InventoryEntityToResponse(inventory)
responses = append(responses, response)
}
return responses, totalCount, nil
}
// AdjustQuantity adjusts the quantity of an inventory item
func (p *InventoryProcessorImpl) AdjustQuantity(ctx context.Context, productID, outletID, organizationID uuid.UUID, delta int) (*models.InventoryResponse, error) {
inventory, err := p.inventoryRepo.AdjustQuantity(ctx, productID, outletID, delta)
if err != nil {
return nil, fmt.Errorf("failed to adjust inventory quantity: %w", err)
}
response := mappers.InventoryEntityToResponse(inventory)
return response, nil
}
// SetQuantity sets the quantity of an inventory item
func (p *InventoryProcessorImpl) SetQuantity(ctx context.Context, productID, outletID, organizationID uuid.UUID, quantity int) (*models.InventoryResponse, error) {
inventory, err := p.inventoryRepo.SetQuantity(ctx, productID, outletID, quantity)
if err != nil {
return nil, fmt.Errorf("failed to set inventory quantity: %w", err)
}
response := mappers.InventoryEntityToResponse(inventory)
return response, nil
}
// UpdateReorderLevel updates the reorder level of an inventory item
func (p *InventoryProcessorImpl) UpdateReorderLevel(ctx context.Context, id uuid.UUID, reorderLevel int, organizationID uuid.UUID) error {
return p.inventoryRepo.UpdateReorderLevel(ctx, id, reorderLevel)
}
// GetLowStock retrieves low stock inventory items for a specific outlet
func (p *InventoryProcessorImpl) GetLowStock(ctx context.Context, outletID, organizationID uuid.UUID) ([]*models.InventoryResponse, error) {
inventories, err := p.inventoryRepo.GetLowStock(ctx, outletID)
if err != nil {
return nil, fmt.Errorf("failed to get low stock inventory: %w", err)
}
var responses []*models.InventoryResponse
for _, inventory := range inventories {
response := mappers.InventoryEntityToResponse(inventory)
responses = append(responses, response)
}
return responses, nil
}
// GetZeroStock retrieves zero stock inventory items for a specific outlet
func (p *InventoryProcessorImpl) GetZeroStock(ctx context.Context, outletID, organizationID uuid.UUID) ([]*models.InventoryResponse, error) {
inventories, err := p.inventoryRepo.GetZeroStock(ctx, outletID)
if err != nil {
return nil, fmt.Errorf("failed to get zero stock inventory: %w", err)
}
var responses []*models.InventoryResponse
for _, inventory := range inventories {
response := mappers.InventoryEntityToResponse(inventory)
responses = append(responses, response)
}
return responses, nil
}
// GetInventoryReportSummary returns summary statistics for inventory report
func (p *InventoryProcessorImpl) GetInventoryReportSummary(ctx context.Context, outletID, organizationID uuid.UUID) (*models.InventoryReportSummary, error) {
// Verify outlet belongs to organization
outlet, err := p.outletRepo.GetByID(ctx, outletID)
if err != nil {
return nil, fmt.Errorf("outlet not found: %w", err)
}
if outlet.OrganizationID != organizationID {
return nil, fmt.Errorf("outlet does not belong to the organization")
}
summary, err := p.inventoryRepo.GetInventoryReportSummary(ctx, outletID)
if err != nil {
return nil, fmt.Errorf("failed to get inventory report summary: %w", err)
}
return summary, nil
}
// GetInventoryReportDetails returns detailed inventory report with products and ingredients
func (p *InventoryProcessorImpl) GetInventoryReportDetails(ctx context.Context, filter *models.InventoryReportFilter, organizationID uuid.UUID) (*models.InventoryReportDetail, error) {
if filter.OutletID == nil {
return nil, fmt.Errorf("outlet_id is required for inventory report")
}
// Verify outlet belongs to organization
outlet, err := p.outletRepo.GetByID(ctx, *filter.OutletID)
if err != nil {
return nil, fmt.Errorf("outlet not found: %w", err)
}
if outlet.OrganizationID != organizationID {
return nil, fmt.Errorf("outlet does not belong to the organization")
}
report, err := p.inventoryRepo.GetInventoryReportDetails(ctx, filter)
if err != nil {
return nil, fmt.Errorf("failed to get inventory report details: %w", err)
}
return report, nil
}
func (p *InventoryProcessorImpl) GetInventoryByID(ctx context.Context, id uuid.UUID) (*models.InventoryResponse, error) { func (p *InventoryProcessorImpl) GetInventoryByID(ctx context.Context, id uuid.UUID) (*models.InventoryResponse, error) {
inventoryEntity, err := p.inventoryRepo.GetWithRelations(ctx, id) inventoryEntity, err := p.inventoryRepo.GetWithRelations(ctx, id)
if err != nil { if err != nil {

View File

@ -34,6 +34,7 @@ type OrderRepository interface {
GetByID(ctx context.Context, id uuid.UUID) (*entities.Order, error) GetByID(ctx context.Context, id uuid.UUID) (*entities.Order, error)
GetWithRelations(ctx context.Context, id uuid.UUID) (*entities.Order, error) GetWithRelations(ctx context.Context, id uuid.UUID) (*entities.Order, error)
Update(ctx context.Context, order *entities.Order) error Update(ctx context.Context, order *entities.Order) error
UpdateStatusSuccess(ctx context.Context, id uuid.UUID, orderStatus entities.OrderStatus, paymentStatus entities.PaymentStatus) error
Delete(ctx context.Context, id uuid.UUID) error Delete(ctx context.Context, id uuid.UUID) error
List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.Order, int64, error) List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.Order, int64, error)
GetByOrderNumber(ctx context.Context, orderNumber string) (*entities.Order, error) GetByOrderNumber(ctx context.Context, orderNumber string) (*entities.Order, error)
@ -458,12 +459,13 @@ func (p *OrderProcessorImpl) UpdateOrder(ctx context.Context, id uuid.UUID, req
} }
} }
// Update order order.Status = entities.OrderStatusCompleted
if err := p.orderRepo.Update(ctx, order); err != nil { order.PaymentStatus = entities.PaymentStatusCompleted
if err := p.orderRepo.UpdateStatusSuccess(ctx, order.ID, order.Status, order.PaymentStatus); err != nil {
return nil, fmt.Errorf("failed to update order: %w", err) return nil, fmt.Errorf("failed to update order: %w", err)
} }
// Get updated order with relations
orderWithRelations, err := p.orderRepo.GetWithRelations(ctx, id) orderWithRelations, err := p.orderRepo.GetWithRelations(ctx, id)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to retrieve updated order: %w", err) return nil, fmt.Errorf("failed to retrieve updated order: %w", err)
@ -858,7 +860,7 @@ func (p *OrderProcessorImpl) updateOrderStatus(ctx context.Context, orderID uuid
PaymentStatus: entities.PaymentStatusCompleted, PaymentStatus: entities.PaymentStatusCompleted,
} }
if err := p.orderRepo.Update(ctx, orderUpdate); err != nil { if err := p.orderRepo.UpdateStatusSuccess(ctx, orderID, orderUpdate.Status, orderUpdate.PaymentStatus); err != nil {
return fmt.Errorf("failed to update order status: %w", err) return fmt.Errorf("failed to update order status: %w", err)
} }

View File

@ -4,8 +4,10 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"time"
"apskel-pos-be/internal/entities" "apskel-pos-be/internal/entities"
"apskel-pos-be/internal/models"
"github.com/google/uuid" "github.com/google/uuid"
"gorm.io/gorm" "gorm.io/gorm"
@ -31,6 +33,8 @@ type InventoryRepository interface {
BulkUpdate(ctx context.Context, inventoryItems []*entities.Inventory) error BulkUpdate(ctx context.Context, inventoryItems []*entities.Inventory) error
BulkAdjustQuantity(ctx context.Context, adjustments map[uuid.UUID]int, outletID uuid.UUID) error BulkAdjustQuantity(ctx context.Context, adjustments map[uuid.UUID]int, outletID uuid.UUID) error
GetTotalValueByOutlet(ctx context.Context, outletID uuid.UUID) (float64, error) GetTotalValueByOutlet(ctx context.Context, outletID uuid.UUID) (float64, error)
GetInventoryReportSummary(ctx context.Context, outletID uuid.UUID) (*models.InventoryReportSummary, error)
GetInventoryReportDetails(ctx context.Context, filter *models.InventoryReportFilter) (*models.InventoryReportDetail, error)
} }
type InventoryRepositoryImpl struct { type InventoryRepositoryImpl struct {
@ -326,12 +330,295 @@ func (r *InventoryRepositoryImpl) BulkAdjustQuantity(ctx context.Context, adjust
func (r *InventoryRepositoryImpl) GetTotalValueByOutlet(ctx context.Context, outletID uuid.UUID) (float64, error) { func (r *InventoryRepositoryImpl) GetTotalValueByOutlet(ctx context.Context, outletID uuid.UUID) (float64, error) {
var totalValue float64 var totalValue float64
err := r.db.WithContext(ctx). if err := r.db.WithContext(ctx).
Table("inventory"). Table("inventory").
Select("SUM(inventory.quantity * products.cost)"). Select("SUM(inventory.quantity * products.cost)").
Joins("JOIN products ON inventory.product_id = products.id"). Joins("JOIN products ON inventory.product_id = products.id").
Where("inventory.outlet_id = ?", outletID). Where("inventory.outlet_id = ?", outletID).
Scan(&totalValue).Error Scan(&totalValue).Error; err != nil {
return 0, fmt.Errorf("failed to get total value: %w", err)
}
return totalValue, err return totalValue, nil
}
// GetInventoryReportSummary returns summary statistics for inventory report
func (r *InventoryRepositoryImpl) GetInventoryReportSummary(ctx context.Context, outletID uuid.UUID) (*models.InventoryReportSummary, error) {
var summary models.InventoryReportSummary
summary.OutletID = outletID
summary.GeneratedAt = time.Now()
// Get outlet name
var outlet entities.Outlet
if err := r.db.WithContext(ctx).Select("name").First(&outlet, "id = ?", outletID).Error; err != nil {
return nil, fmt.Errorf("failed to get outlet name: %w", err)
}
summary.OutletName = outlet.Name
// Get total products count
var totalProducts int64
if err := r.db.WithContext(ctx).Model(&entities.Inventory{}).
Joins("JOIN products ON inventory.product_id = products.id").
Where("inventory.outlet_id = ? AND products.has_ingredients = false", outletID).
Count(&totalProducts).Error; err != nil {
return nil, fmt.Errorf("failed to count total products: %w", err)
}
summary.TotalProducts = int(totalProducts)
// Get total ingredients count
var totalIngredients int64
if err := r.db.WithContext(ctx).Model(&entities.Inventory{}).
Joins("JOIN ingredients ON inventory.product_id = ingredients.id").
Where("inventory.outlet_id = ?", outletID).
Count(&totalIngredients).Error; err != nil {
return nil, fmt.Errorf("failed to count total ingredients: %w", err)
}
summary.TotalIngredients = int(totalIngredients)
// Get low stock products count
var lowStockProducts int64
if err := r.db.WithContext(ctx).Model(&entities.Inventory{}).
Joins("JOIN products ON inventory.product_id = products.id").
Where("inventory.outlet_id = ? AND products.has_ingredients = false AND inventory.quantity <= inventory.reorder_level AND inventory.quantity > 0", outletID).
Count(&lowStockProducts).Error; err != nil {
return nil, fmt.Errorf("failed to count low stock products: %w", err)
}
summary.LowStockProducts = int(lowStockProducts)
// Get low stock ingredients count
var lowStockIngredients int64
if err := r.db.WithContext(ctx).Model(&entities.Inventory{}).
Joins("JOIN ingredients ON inventory.product_id = ingredients.id").
Where("inventory.outlet_id = ? AND inventory.quantity <= inventory.reorder_level AND inventory.quantity > 0", outletID).
Count(&lowStockIngredients).Error; err != nil {
return nil, fmt.Errorf("failed to count low stock ingredients: %w", err)
}
summary.LowStockIngredients = int(lowStockIngredients)
// Get zero stock products count
var zeroStockProducts int64
if err := r.db.WithContext(ctx).Model(&entities.Inventory{}).
Joins("JOIN products ON inventory.product_id = products.id").
Where("inventory.outlet_id = ? AND products.has_ingredients = false AND inventory.quantity = 0", outletID).
Count(&zeroStockProducts).Error; err != nil {
return nil, fmt.Errorf("failed to count zero stock products: %w", err)
}
summary.ZeroStockProducts = int(zeroStockProducts)
// Get zero stock ingredients count
var zeroStockIngredients int64
if err := r.db.WithContext(ctx).Model(&entities.Inventory{}).
Joins("JOIN ingredients ON inventory.product_id = ingredients.id").
Where("inventory.outlet_id = ? AND inventory.quantity = 0", outletID).
Count(&zeroStockIngredients).Error; err != nil {
return nil, fmt.Errorf("failed to count zero stock ingredients: %w", err)
}
summary.ZeroStockIngredients = int(zeroStockIngredients)
// Get total value
totalValue, err := r.GetTotalValueByOutlet(ctx, outletID)
if err != nil {
return nil, fmt.Errorf("failed to get total value: %w", err)
}
summary.TotalValue = totalValue
return &summary, nil
}
// GetInventoryReportDetails returns detailed inventory report with products and ingredients
func (r *InventoryRepositoryImpl) GetInventoryReportDetails(ctx context.Context, filter *models.InventoryReportFilter) (*models.InventoryReportDetail, error) {
report := &models.InventoryReportDetail{}
// Get summary
if filter.OutletID != nil {
summary, err := r.GetInventoryReportSummary(ctx, *filter.OutletID)
if err != nil {
return nil, fmt.Errorf("failed to get report summary: %w", err)
}
report.Summary = summary
}
// Get products details
products, err := r.getInventoryProductsDetails(ctx, filter)
if err != nil {
return nil, fmt.Errorf("failed to get products details: %w", err)
}
report.Products = products
// Get ingredients details
ingredients, err := r.getInventoryIngredientsDetails(ctx, filter)
if err != nil {
return nil, fmt.Errorf("failed to get ingredients details: %w", err)
}
report.Ingredients = ingredients
return report, nil
}
// getInventoryProductsDetails retrieves detailed product inventory information
func (r *InventoryRepositoryImpl) getInventoryProductsDetails(ctx context.Context, filter *models.InventoryReportFilter) ([]*models.InventoryProductDetail, error) {
query := r.db.WithContext(ctx).Table("inventory").
Select(`
inventory.id,
inventory.product_id,
products.name as product_name,
categories.name as category_name,
inventory.quantity,
inventory.reorder_level,
COALESCE(product_variants.cost, products.cost) as unit_cost,
(COALESCE(product_variants.cost, products.cost) * inventory.quantity) as total_value,
inventory.updated_at
`).
Joins("JOIN products ON inventory.product_id = products.id").
Joins("LEFT JOIN categories ON products.category_id = categories.id").
Joins("LEFT JOIN product_variants ON products.id = product_variants.product_id").
Where("inventory.outlet_id = ? AND products.has_ingredients = false", filter.OutletID)
// Apply filters
if filter.CategoryID != nil {
query = query.Where("products.category_id = ?", *filter.CategoryID)
}
if filter.ShowLowStock != nil && *filter.ShowLowStock {
query = query.Where("inventory.quantity <= inventory.reorder_level AND inventory.quantity > 0")
}
if filter.ShowZeroStock != nil && *filter.ShowZeroStock {
query = query.Where("inventory.quantity = 0")
}
if filter.Search != nil && *filter.Search != "" {
searchTerm := "%" + *filter.Search + "%"
query = query.Where("products.name ILIKE ? OR categories.name ILIKE ?", searchTerm, searchTerm)
}
// Apply pagination
if filter.Limit != nil {
query = query.Limit(*filter.Limit)
}
if filter.Offset != nil {
query = query.Offset(*filter.Offset)
}
query = query.Order("products.name ASC")
var results []struct {
ID uuid.UUID
ProductID uuid.UUID
ProductName string
CategoryName *string
Quantity int
ReorderLevel int
UnitCost float64
TotalValue float64
UpdatedAt time.Time
}
if err := query.Find(&results).Error; err != nil {
return nil, err
}
var products []*models.InventoryProductDetail
for _, result := range results {
categoryName := ""
if result.CategoryName != nil {
categoryName = *result.CategoryName
}
product := &models.InventoryProductDetail{
ID: result.ID,
ProductID: result.ProductID,
ProductName: result.ProductName,
CategoryName: categoryName,
Quantity: result.Quantity,
ReorderLevel: result.ReorderLevel,
UnitCost: result.UnitCost,
TotalValue: result.TotalValue,
IsLowStock: result.Quantity <= result.ReorderLevel && result.Quantity > 0,
IsZeroStock: result.Quantity == 0,
UpdatedAt: result.UpdatedAt,
}
products = append(products, product)
}
return products, nil
}
// getInventoryIngredientsDetails retrieves detailed ingredient inventory information
func (r *InventoryRepositoryImpl) getInventoryIngredientsDetails(ctx context.Context, filter *models.InventoryReportFilter) ([]*models.InventoryIngredientDetail, error) {
query := r.db.WithContext(ctx).Table("inventory").
Select(`
inventory.id,
inventory.product_id as ingredient_id,
ingredients.name as ingredient_name,
units.name as unit_name,
inventory.quantity,
inventory.reorder_level,
ingredients.cost as unit_cost,
(ingredients.cost * inventory.quantity) as total_value,
inventory.updated_at
`).
Joins("JOIN ingredients ON inventory.product_id = ingredients.id").
Joins("LEFT JOIN units ON ingredients.unit_id = units.id").
Where("inventory.outlet_id = ?", filter.OutletID)
// Apply filters
if filter.ShowLowStock != nil && *filter.ShowLowStock {
query = query.Where("inventory.quantity <= inventory.reorder_level AND inventory.quantity > 0")
}
if filter.ShowZeroStock != nil && *filter.ShowZeroStock {
query = query.Where("inventory.quantity = 0")
}
if filter.Search != nil && *filter.Search != "" {
searchTerm := "%" + *filter.Search + "%"
query = query.Where("ingredients.name ILIKE ? OR units.name ILIKE ?", searchTerm, searchTerm)
}
// Apply pagination
if filter.Limit != nil {
query = query.Limit(*filter.Limit)
}
if filter.Offset != nil {
query = query.Offset(*filter.Offset)
}
query = query.Order("ingredients.name ASC")
var results []struct {
ID uuid.UUID
IngredientID uuid.UUID
IngredientName string
UnitName *string
Quantity int
ReorderLevel int
UnitCost float64
TotalValue float64
UpdatedAt time.Time
}
if err := query.Find(&results).Error; err != nil {
return nil, err
}
var ingredients []*models.InventoryIngredientDetail
for _, result := range results {
unitName := ""
if result.UnitName != nil {
unitName = *result.UnitName
}
ingredient := &models.InventoryIngredientDetail{
ID: result.ID,
IngredientID: result.IngredientID,
IngredientName: result.IngredientName,
UnitName: unitName,
Quantity: result.Quantity,
ReorderLevel: result.ReorderLevel,
UnitCost: result.UnitCost,
TotalValue: result.TotalValue,
IsLowStock: result.Quantity <= result.ReorderLevel && result.Quantity > 0,
IsZeroStock: result.Quantity == 0,
UpdatedAt: result.UpdatedAt,
}
ingredients = append(ingredients, ingredient)
}
return ingredients, nil
} }

View File

@ -74,6 +74,21 @@ func (r *OrderRepositoryImpl) Update(ctx context.Context, order *entities.Order)
return r.db.WithContext(ctx).Save(order).Error return r.db.WithContext(ctx).Save(order).Error
} }
func (r *OrderRepositoryImpl) UpdateStatusSuccess(
ctx context.Context,
id uuid.UUID,
orderStatus entities.OrderStatus,
paymentStatus entities.PaymentStatus,
) error {
return r.db.WithContext(ctx).
Model(&entities.Order{}).
Where("id = ?", id).
Updates(map[string]interface{}{
"order_status": orderStatus,
"payment_status": paymentStatus,
}).Error
}
func (r *OrderRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error { func (r *OrderRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error {
return r.db.WithContext(ctx).Delete(&entities.Order{}, "id = ?", id).Error return r.db.WithContext(ctx).Delete(&entities.Order{}, "id = ?", id).Error
} }

View File

@ -205,6 +205,8 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
inventory.POST("/adjust", r.inventoryHandler.AdjustInventory) inventory.POST("/adjust", r.inventoryHandler.AdjustInventory)
inventory.GET("/low-stock/:outlet_id", r.inventoryHandler.GetLowStockItems) inventory.GET("/low-stock/:outlet_id", r.inventoryHandler.GetLowStockItems)
inventory.GET("/zero-stock/:outlet_id", r.inventoryHandler.GetZeroStockItems) inventory.GET("/zero-stock/:outlet_id", r.inventoryHandler.GetZeroStockItems)
inventory.GET("/report/summary/:outlet_id", r.inventoryHandler.GetInventoryReportSummary)
inventory.GET("/report/details", r.inventoryHandler.GetInventoryReportDetails)
} }
orders := protected.Group("/orders") orders := protected.Group("/orders")

View File

@ -2,10 +2,12 @@ package service
import ( import (
"context" "context"
"time"
"apskel-pos-be/internal/appcontext" "apskel-pos-be/internal/appcontext"
"apskel-pos-be/internal/constants" "apskel-pos-be/internal/constants"
"apskel-pos-be/internal/contract" "apskel-pos-be/internal/contract"
"apskel-pos-be/internal/models"
"apskel-pos-be/internal/processor" "apskel-pos-be/internal/processor"
"apskel-pos-be/internal/transformer" "apskel-pos-be/internal/transformer"
@ -21,6 +23,8 @@ type InventoryService interface {
AdjustInventory(ctx context.Context, req *contract.AdjustInventoryRequest) *contract.Response AdjustInventory(ctx context.Context, req *contract.AdjustInventoryRequest) *contract.Response
GetLowStockItems(ctx context.Context, outletID uuid.UUID) *contract.Response GetLowStockItems(ctx context.Context, outletID uuid.UUID) *contract.Response
GetZeroStockItems(ctx context.Context, outletID uuid.UUID) *contract.Response GetZeroStockItems(ctx context.Context, outletID uuid.UUID) *contract.Response
GetInventoryReportSummary(ctx context.Context, outletID, organizationID uuid.UUID) (*contract.InventoryReportSummaryResponse, error)
GetInventoryReportDetails(ctx context.Context, filter *models.InventoryReportFilter, organizationID uuid.UUID) (*contract.InventoryReportDetailResponse, error)
} }
type InventoryServiceImpl struct { type InventoryServiceImpl struct {
@ -36,7 +40,7 @@ func NewInventoryService(inventoryProcessor processor.InventoryProcessor) *Inven
func (s *InventoryServiceImpl) CreateInventory(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.CreateInventoryRequest) *contract.Response { func (s *InventoryServiceImpl) CreateInventory(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.CreateInventoryRequest) *contract.Response {
modelReq := transformer.CreateInventoryRequestToModel(req) modelReq := transformer.CreateInventoryRequestToModel(req)
inventoryResponse, err := s.inventoryProcessor.CreateInventory(ctx, modelReq) inventoryResponse, err := s.inventoryProcessor.Create(ctx, modelReq, apctx.OrganizationID)
if err != nil { if err != nil {
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error()) errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error())
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
@ -49,7 +53,7 @@ func (s *InventoryServiceImpl) CreateInventory(ctx context.Context, apctx *appco
func (s *InventoryServiceImpl) UpdateInventory(ctx context.Context, id uuid.UUID, req *contract.UpdateInventoryRequest) *contract.Response { func (s *InventoryServiceImpl) UpdateInventory(ctx context.Context, id uuid.UUID, req *contract.UpdateInventoryRequest) *contract.Response {
modelReq := transformer.UpdateInventoryRequestToModel(req) modelReq := transformer.UpdateInventoryRequestToModel(req)
inventoryResponse, err := s.inventoryProcessor.UpdateInventory(ctx, id, modelReq) inventoryResponse, err := s.inventoryProcessor.Update(ctx, id, modelReq, uuid.Nil) // TODO: Get organizationID from context
if err != nil { if err != nil {
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error()) errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error())
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
@ -60,7 +64,7 @@ func (s *InventoryServiceImpl) UpdateInventory(ctx context.Context, id uuid.UUID
} }
func (s *InventoryServiceImpl) DeleteInventory(ctx context.Context, id uuid.UUID) *contract.Response { func (s *InventoryServiceImpl) DeleteInventory(ctx context.Context, id uuid.UUID) *contract.Response {
err := s.inventoryProcessor.DeleteInventory(ctx, id) err := s.inventoryProcessor.Delete(ctx, id, uuid.Nil) // TODO: Get organizationID from context
if err != nil { if err != nil {
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error()) errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error())
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
@ -72,7 +76,7 @@ func (s *InventoryServiceImpl) DeleteInventory(ctx context.Context, id uuid.UUID
} }
func (s *InventoryServiceImpl) GetInventoryByID(ctx context.Context, id uuid.UUID) *contract.Response { func (s *InventoryServiceImpl) GetInventoryByID(ctx context.Context, id uuid.UUID) *contract.Response {
inventoryResponse, err := s.inventoryProcessor.GetInventoryByID(ctx, id) inventoryResponse, err := s.inventoryProcessor.GetByID(ctx, id, uuid.Nil) // TODO: Get organizationID from context
if err != nil { if err != nil {
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error()) errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error())
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
@ -104,24 +108,30 @@ func (s *InventoryServiceImpl) ListInventory(ctx context.Context, req *contract.
filters["search"] = req.Search filters["search"] = req.Search
} }
inventory, totalCount, err := s.inventoryProcessor.ListInventory(ctx, filters, req.Page, req.Limit) inventory, totalCount, err := s.inventoryProcessor.List(ctx, filters, req.Limit, req.Page, uuid.Nil) // TODO: Get organizationID from context
if err != nil { if err != nil {
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error()) errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error())
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
} }
// Convert from []*models.InventoryResponse to []models.InventoryResponse
var inventoryResponses []models.InventoryResponse
for _, inv := range inventory {
inventoryResponses = append(inventoryResponses, *inv)
}
// Convert to contract responses // Convert to contract responses
contractResponses := transformer.InventoryToResponses(inventory) contractResponses := transformer.InventoryToResponses(inventoryResponses)
// Calculate total pages // Calculate total pages
totalPages := totalCount / req.Limit totalPages := int(totalCount) / req.Limit
if totalCount%req.Limit > 0 { if int(totalCount)%req.Limit > 0 {
totalPages++ totalPages++
} }
listResponse := &contract.ListInventoryResponse{ listResponse := &contract.ListInventoryResponse{
Inventory: contractResponses, Inventory: contractResponses,
TotalCount: totalCount, TotalCount: int(totalCount),
Page: req.Page, Page: req.Page,
Limit: req.Limit, Limit: req.Limit,
TotalPages: totalPages, TotalPages: totalPages,
@ -133,7 +143,7 @@ func (s *InventoryServiceImpl) ListInventory(ctx context.Context, req *contract.
func (s *InventoryServiceImpl) AdjustInventory(ctx context.Context, req *contract.AdjustInventoryRequest) *contract.Response { func (s *InventoryServiceImpl) AdjustInventory(ctx context.Context, req *contract.AdjustInventoryRequest) *contract.Response {
modelReq := transformer.AdjustInventoryRequestToModel(req) modelReq := transformer.AdjustInventoryRequestToModel(req)
inventoryResponse, err := s.inventoryProcessor.AdjustInventory(ctx, req.ProductID, req.OutletID, modelReq) inventoryResponse, err := s.inventoryProcessor.AdjustQuantity(ctx, modelReq.ProductID, modelReq.OutletID, uuid.Nil, modelReq.Delta) // TODO: Get organizationID from context
if err != nil { if err != nil {
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error()) errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error())
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
@ -144,23 +154,120 @@ func (s *InventoryServiceImpl) AdjustInventory(ctx context.Context, req *contrac
} }
func (s *InventoryServiceImpl) GetLowStockItems(ctx context.Context, outletID uuid.UUID) *contract.Response { func (s *InventoryServiceImpl) GetLowStockItems(ctx context.Context, outletID uuid.UUID) *contract.Response {
inventory, err := s.inventoryProcessor.GetLowStockItems(ctx, outletID) inventory, err := s.inventoryProcessor.GetLowStock(ctx, outletID, uuid.Nil) // TODO: Get organizationID from context
if err != nil { if err != nil {
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error()) errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error())
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
} }
contractResponses := transformer.InventoryToResponses(inventory) // Convert from []*models.InventoryResponse to []models.InventoryResponse
var inventoryResponses []models.InventoryResponse
for _, inv := range inventory {
inventoryResponses = append(inventoryResponses, *inv)
}
contractResponses := transformer.InventoryToResponses(inventoryResponses)
return contract.BuildSuccessResponse(contractResponses) return contract.BuildSuccessResponse(contractResponses)
} }
func (s *InventoryServiceImpl) GetZeroStockItems(ctx context.Context, outletID uuid.UUID) *contract.Response { func (s *InventoryServiceImpl) GetZeroStockItems(ctx context.Context, outletID uuid.UUID) *contract.Response {
inventory, err := s.inventoryProcessor.GetZeroStockItems(ctx, outletID) inventory, err := s.inventoryProcessor.GetZeroStock(ctx, outletID, uuid.Nil) // TODO: Get organizationID from context
if err != nil { if err != nil {
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error()) errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error())
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
} }
contractResponses := transformer.InventoryToResponses(inventory) // Convert from []*models.InventoryResponse to []models.InventoryResponse
var inventoryResponses []models.InventoryResponse
for _, inv := range inventory {
inventoryResponses = append(inventoryResponses, *inv)
}
contractResponses := transformer.InventoryToResponses(inventoryResponses)
return contract.BuildSuccessResponse(contractResponses) return contract.BuildSuccessResponse(contractResponses)
} }
// GetInventoryReportSummary returns summary statistics for inventory report
func (s *InventoryServiceImpl) GetInventoryReportSummary(ctx context.Context, outletID, organizationID uuid.UUID) (*contract.InventoryReportSummaryResponse, error) {
summary, err := s.inventoryProcessor.GetInventoryReportSummary(ctx, outletID, organizationID)
if err != nil {
return nil, err
}
return &contract.InventoryReportSummaryResponse{
TotalProducts: summary.TotalProducts,
TotalIngredients: summary.TotalIngredients,
TotalValue: summary.TotalValue,
LowStockProducts: summary.LowStockProducts,
LowStockIngredients: summary.LowStockIngredients,
ZeroStockProducts: summary.ZeroStockProducts,
ZeroStockIngredients: summary.ZeroStockIngredients,
OutletID: summary.OutletID.String(),
OutletName: summary.OutletName,
GeneratedAt: summary.GeneratedAt.Format(time.RFC3339),
}, nil
}
// GetInventoryReportDetails returns detailed inventory report with products and ingredients
func (s *InventoryServiceImpl) GetInventoryReportDetails(ctx context.Context, filter *models.InventoryReportFilter, organizationID uuid.UUID) (*contract.InventoryReportDetailResponse, error) {
report, err := s.inventoryProcessor.GetInventoryReportDetails(ctx, filter, organizationID)
if err != nil {
return nil, err
}
response := &contract.InventoryReportDetailResponse{}
// Transform summary
if report.Summary != nil {
response.Summary = &contract.InventoryReportSummaryResponse{
TotalProducts: report.Summary.TotalProducts,
TotalIngredients: report.Summary.TotalIngredients,
TotalValue: report.Summary.TotalValue,
LowStockProducts: report.Summary.LowStockProducts,
LowStockIngredients: report.Summary.LowStockIngredients,
ZeroStockProducts: report.Summary.ZeroStockProducts,
ZeroStockIngredients: report.Summary.ZeroStockIngredients,
OutletID: report.Summary.OutletID.String(),
OutletName: report.Summary.OutletName,
GeneratedAt: report.Summary.GeneratedAt.Format(time.RFC3339),
}
}
// Transform products
response.Products = make([]*contract.InventoryProductDetailResponse, len(report.Products))
for i, product := range report.Products {
response.Products[i] = &contract.InventoryProductDetailResponse{
ID: product.ID.String(),
ProductID: product.ProductID.String(),
ProductName: product.ProductName,
CategoryName: product.CategoryName,
Quantity: product.Quantity,
ReorderLevel: product.ReorderLevel,
UnitCost: product.UnitCost,
TotalValue: product.TotalValue,
IsLowStock: product.IsLowStock,
IsZeroStock: product.IsZeroStock,
UpdatedAt: product.UpdatedAt.Format(time.RFC3339),
}
}
// Transform ingredients
response.Ingredients = make([]*contract.InventoryIngredientDetailResponse, len(report.Ingredients))
for i, ingredient := range report.Ingredients {
response.Ingredients[i] = &contract.InventoryIngredientDetailResponse{
ID: ingredient.ID.String(),
IngredientID: ingredient.IngredientID.String(),
IngredientName: ingredient.IngredientName,
UnitName: ingredient.UnitName,
Quantity: ingredient.Quantity,
ReorderLevel: ingredient.ReorderLevel,
UnitCost: ingredient.UnitCost,
TotalValue: ingredient.TotalValue,
IsLowStock: ingredient.IsLowStock,
IsZeroStock: ingredient.IsZeroStock,
UpdatedAt: ingredient.UpdatedAt.Format(time.RFC3339),
}
}
return response, nil
}