add total out

This commit is contained in:
Aditya Siregar
2025-08-14 01:35:19 +07:00
parent 07b3eda263
commit 13d8c75be7
7 changed files with 320 additions and 77 deletions
+178 -6
View File
@@ -1,10 +1,13 @@
package processor
import (
"apskel-pos-be/internal/appcontext"
"context"
"fmt"
"time"
"apskel-pos-be/internal/contract"
"apskel-pos-be/internal/entities"
"apskel-pos-be/internal/mappers"
"apskel-pos-be/internal/models"
"apskel-pos-be/internal/repository"
@@ -26,25 +29,32 @@ type InventoryProcessor interface {
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
RestockInventory(ctx context.Context, outletID uuid.UUID, items []contract.RestockItem, reason string) (*contract.RestockInventoryResponse, error)
GetInventoryReportSummary(ctx context.Context, outletID, organizationID uuid.UUID, dateFrom, dateTo *time.Time) (*models.InventoryReportSummary, error)
GetInventoryReportDetails(ctx context.Context, filter *models.InventoryReportFilter, organizationID uuid.UUID) (*models.InventoryReportDetail, error)
}
type InventoryProcessorImpl struct {
inventoryRepo repository.InventoryRepository
productRepo ProductRepository
outletRepo OutletRepository
inventoryRepo repository.InventoryRepository
productRepo ProductRepository
outletRepo OutletRepository
ingredientRepo IngredientRepository
inventoryMovementRepo repository.InventoryMovementRepository
}
func NewInventoryProcessorImpl(
inventoryRepo repository.InventoryRepository,
productRepo ProductRepository,
outletRepo OutletRepository,
ingredientRepo IngredientRepository,
inventoryMovementRepo repository.InventoryMovementRepository,
) *InventoryProcessorImpl {
return &InventoryProcessorImpl{
inventoryRepo: inventoryRepo,
productRepo: productRepo,
outletRepo: outletRepo,
inventoryRepo: inventoryRepo,
productRepo: productRepo,
outletRepo: outletRepo,
ingredientRepo: ingredientRepo,
inventoryMovementRepo: inventoryMovementRepo,
}
}
@@ -401,3 +411,165 @@ func (p *InventoryProcessorImpl) GetZeroStockItems(ctx context.Context, outletID
return responses, nil
}
func (p *InventoryProcessorImpl) RestockInventory(ctx context.Context, outletID uuid.UUID, items []contract.RestockItem, reason string) (*contract.RestockInventoryResponse, error) {
outlet, err := p.outletRepo.GetByID(ctx, outletID)
if err != nil {
return nil, fmt.Errorf("invalid outlet: %w", err)
}
if len(items) == 0 {
return nil, fmt.Errorf("no items provided for restocking")
}
restockResults := make([]contract.RestockItemResult, 0, len(items))
restockedAt := time.Now()
for _, item := range items {
result := contract.RestockItemResult{
ItemID: item.ItemID,
ItemType: item.ItemType,
AddedQty: item.Quantity,
Success: false,
}
switch item.ItemType {
case "PRODUCT":
if err := p.restockProduct(ctx, outletID, item.ItemID, item.Quantity, reason, outlet.OrganizationID); err != nil {
result.Error = err.Error()
}
case "INGREDIENT":
if err := p.restockIngredient(ctx, outletID, item.ItemID, item.Quantity, reason, outlet.OrganizationID); err != nil {
result.Error = err.Error()
}
default:
result.Error = fmt.Sprintf("unsupported item type: %s", item.ItemType)
}
restockResults = append(restockResults, result)
}
return &contract.RestockInventoryResponse{
OutletID: outletID,
Items: restockResults,
Reason: reason,
RestockedAt: restockedAt,
}, nil
}
func (p *InventoryProcessorImpl) restockProduct(ctx context.Context, outletID, productID uuid.UUID, quantity int, reason string, organizationID uuid.UUID) error {
contextInfo := appcontext.FromGinContext(ctx)
var previousQuantity int
var newQuantity int
inventory, err := p.inventoryRepo.GetByProductAndOutlet(ctx, productID, outletID)
if err != nil {
createReq := &models.CreateInventoryRequest{
OutletID: outletID,
ProductID: productID,
Quantity: quantity,
ReorderLevel: 0,
}
inventoryEntity := mappers.CreateInventoryRequestToEntity(createReq)
if err := p.inventoryRepo.Create(ctx, inventoryEntity); err != nil {
return fmt.Errorf("failed to create inventory for product %s: %w", productID, err)
}
previousQuantity = 0
newQuantity = quantity
} else {
previousQuantity = inventory.Quantity
newQuantity = inventory.Quantity + quantity
updateReq := &models.UpdateInventoryRequest{
Quantity: &newQuantity,
}
mappers.UpdateInventoryEntityFromRequest(inventory, updateReq)
if err := p.inventoryRepo.Update(ctx, inventory); err != nil {
return fmt.Errorf("failed to update inventory for product %s: %w", productID, err)
}
}
referenceType := entities.InventoryMovementReferenceTypeManual
movement := &entities.InventoryMovement{
OrganizationID: organizationID,
OutletID: outletID,
ItemID: productID,
ItemType: "PRODUCT",
MovementType: entities.InventoryMovementTypePurchase,
Quantity: float64(quantity),
PreviousQuantity: float64(previousQuantity),
NewQuantity: float64(newQuantity),
ReferenceType: &referenceType,
Reason: &reason,
UserID: contextInfo.UserID,
}
if err := p.inventoryMovementRepo.Create(ctx, movement); err != nil {
return fmt.Errorf("failed to create inventory movement record: %w", err)
}
return nil
}
func (p *InventoryProcessorImpl) restockIngredient(ctx context.Context, outletID, ingredientID uuid.UUID, quantity int, reason string, organizationID uuid.UUID) error {
contextInfo := appcontext.FromGinContext(ctx)
ingredient, err := p.getIngredientByID(ctx, ingredientID, organizationID)
if err != nil {
return fmt.Errorf("failed to get ingredient %s: %w", ingredientID, err)
}
previousStock := ingredient.Stock
newStock := ingredient.Stock + float64(quantity)
if err := p.updateIngredientStock(ctx, ingredientID, float64(quantity), organizationID); err != nil {
return fmt.Errorf("failed to update ingredient stock: %w", err)
}
referenceType := entities.InventoryMovementReferenceTypeManual
movement := &entities.InventoryMovement{
OrganizationID: organizationID,
OutletID: outletID,
ItemID: ingredientID,
ItemType: "INGREDIENT",
MovementType: entities.InventoryMovementTypePurchase,
Quantity: float64(quantity),
PreviousQuantity: previousStock,
NewQuantity: newStock,
ReferenceType: &referenceType,
Reason: &reason,
UserID: contextInfo.UserID,
}
if err := p.inventoryMovementRepo.Create(ctx, movement); err != nil {
return fmt.Errorf("failed to create inventory movement record: %w", err)
}
return nil
}
func (p *InventoryProcessorImpl) getIngredientByID(ctx context.Context, ingredientID, organizationID uuid.UUID) (*models.Ingredient, error) {
ingredient, err := p.ingredientRepo.GetByID(ctx, ingredientID, organizationID)
if err != nil {
return nil, fmt.Errorf("failed to get ingredient %s: %w", ingredientID, err)
}
return &models.Ingredient{
ID: ingredient.ID,
Name: ingredient.Name,
Stock: ingredient.Stock,
UnitID: ingredient.UnitID,
OrganizationID: ingredient.OrganizationID,
OutletID: ingredient.OutletID,
IsActive: ingredient.IsActive,
CreatedAt: ingredient.CreatedAt,
UpdatedAt: ingredient.UpdatedAt,
}, nil
}
func (p *InventoryProcessorImpl) updateIngredientStock(ctx context.Context, ingredientID uuid.UUID, quantityToAdd float64, organizationID uuid.UUID) error {
if err := p.ingredientRepo.UpdateStock(ctx, ingredientID, quantityToAdd, organizationID); err != nil {
return fmt.Errorf("failed to update ingredient stock: %w", err)
}
return nil
}