Update payment
This commit is contained in:
parent
451697b783
commit
ccb0458189
@ -142,10 +142,11 @@ type repositories struct {
|
|||||||
fileRepo *repository.FileRepositoryImpl
|
fileRepo *repository.FileRepositoryImpl
|
||||||
customerRepo *repository.CustomerRepository
|
customerRepo *repository.CustomerRepository
|
||||||
analyticsRepo *repository.AnalyticsRepositoryImpl
|
analyticsRepo *repository.AnalyticsRepositoryImpl
|
||||||
tableRepo repository.TableRepositoryInterface
|
tableRepo *repository.TableRepository
|
||||||
unitRepo *repository.UnitRepository
|
unitRepo *repository.UnitRepository
|
||||||
ingredientRepo *repository.IngredientRepository
|
ingredientRepo *repository.IngredientRepository
|
||||||
productRecipeRepo *repository.ProductRecipeRepository
|
productRecipeRepo *repository.ProductRecipeRepository
|
||||||
|
txManager *repository.TxManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) initRepositories() *repositories {
|
func (a *App) initRepositories() *repositories {
|
||||||
@ -171,6 +172,7 @@ func (a *App) initRepositories() *repositories {
|
|||||||
unitRepo: repository.NewUnitRepository(a.db),
|
unitRepo: repository.NewUnitRepository(a.db),
|
||||||
ingredientRepo: repository.NewIngredientRepository(a.db),
|
ingredientRepo: repository.NewIngredientRepository(a.db),
|
||||||
productRecipeRepo: repository.NewProductRecipeRepository(a.db),
|
productRecipeRepo: repository.NewProductRecipeRepository(a.db),
|
||||||
|
txManager: repository.NewTxManager(a.db),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,10 +195,12 @@ type processors struct {
|
|||||||
ingredientProcessor *processor.IngredientProcessorImpl
|
ingredientProcessor *processor.IngredientProcessorImpl
|
||||||
productRecipeProcessor *processor.ProductRecipeProcessorImpl
|
productRecipeProcessor *processor.ProductRecipeProcessorImpl
|
||||||
fileClient processor.FileClient
|
fileClient processor.FileClient
|
||||||
|
inventoryMovementService service.InventoryMovementService
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processors {
|
func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processors {
|
||||||
fileClient := client.NewFileClient(cfg.S3Config)
|
fileClient := client.NewFileClient(cfg.S3Config)
|
||||||
|
inventoryMovementService := service.NewInventoryMovementService(repos.inventoryMovementRepo, repos.ingredientRepo)
|
||||||
|
|
||||||
return &processors{
|
return &processors{
|
||||||
userProcessor: processor.NewUserProcessor(repos.userRepo, repos.organizationRepo, repos.outletRepo),
|
userProcessor: processor.NewUserProcessor(repos.userRepo, repos.organizationRepo, repos.outletRepo),
|
||||||
@ -207,7 +211,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
|||||||
productProcessor: processor.NewProductProcessorImpl(repos.productRepo, repos.categoryRepo, repos.productVariantRepo, repos.inventoryRepo, repos.outletRepo),
|
productProcessor: processor.NewProductProcessorImpl(repos.productRepo, repos.categoryRepo, repos.productVariantRepo, repos.inventoryRepo, repos.outletRepo),
|
||||||
productVariantProcessor: processor.NewProductVariantProcessorImpl(repos.productVariantRepo, repos.productRepo),
|
productVariantProcessor: processor.NewProductVariantProcessorImpl(repos.productVariantRepo, repos.productRepo),
|
||||||
inventoryProcessor: processor.NewInventoryProcessorImpl(repos.inventoryRepo, repos.productRepo, repos.outletRepo),
|
inventoryProcessor: processor.NewInventoryProcessorImpl(repos.inventoryRepo, repos.productRepo, repos.outletRepo),
|
||||||
orderProcessor: processor.NewOrderProcessorImpl(repos.orderRepo, repos.orderItemRepo, repos.paymentRepo, repos.paymentOrderItemRepo, repos.productRepo, repos.paymentMethodRepo, repos.inventoryRepo, repos.inventoryMovementRepo, repos.productVariantRepo, repos.outletRepo, repos.customerRepo),
|
orderProcessor: processor.NewOrderProcessorImpl(repos.orderRepo, repos.orderItemRepo, repos.paymentRepo, repos.paymentOrderItemRepo, repos.productRepo, repos.paymentMethodRepo, repos.inventoryRepo, repos.inventoryMovementRepo, repos.productVariantRepo, repos.outletRepo, repos.customerRepo, repos.txManager, repos.productRecipeRepo, repos.ingredientRepo, inventoryMovementService),
|
||||||
paymentMethodProcessor: processor.NewPaymentMethodProcessorImpl(repos.paymentMethodRepo),
|
paymentMethodProcessor: processor.NewPaymentMethodProcessorImpl(repos.paymentMethodRepo),
|
||||||
fileProcessor: processor.NewFileProcessorImpl(repos.fileRepo, fileClient),
|
fileProcessor: processor.NewFileProcessorImpl(repos.fileRepo, fileClient),
|
||||||
customerProcessor: processor.NewCustomerProcessor(repos.customerRepo),
|
customerProcessor: processor.NewCustomerProcessor(repos.customerRepo),
|
||||||
@ -217,6 +221,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
|||||||
ingredientProcessor: processor.NewIngredientProcessor(repos.ingredientRepo, repos.unitRepo),
|
ingredientProcessor: processor.NewIngredientProcessor(repos.ingredientRepo, repos.unitRepo),
|
||||||
productRecipeProcessor: processor.NewProductRecipeProcessor(repos.productRecipeRepo, repos.productRepo, repos.ingredientRepo),
|
productRecipeProcessor: processor.NewProductRecipeProcessor(repos.productRecipeRepo, repos.productRepo, repos.ingredientRepo),
|
||||||
fileClient: fileClient,
|
fileClient: fileClient,
|
||||||
|
inventoryMovementService: inventoryMovementService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,6 +20,7 @@ const (
|
|||||||
InventoryMovementTypeTransferOut InventoryMovementType = "transfer_out"
|
InventoryMovementTypeTransferOut InventoryMovementType = "transfer_out"
|
||||||
InventoryMovementTypeDamage InventoryMovementType = "damage"
|
InventoryMovementTypeDamage InventoryMovementType = "damage"
|
||||||
InventoryMovementTypeExpiry InventoryMovementType = "expiry"
|
InventoryMovementTypeExpiry InventoryMovementType = "expiry"
|
||||||
|
InventoryMovementTypeIngredient InventoryMovementType = "ingredient"
|
||||||
)
|
)
|
||||||
|
|
||||||
type InventoryMovementReferenceType string
|
type InventoryMovementReferenceType string
|
||||||
|
|||||||
@ -19,6 +19,7 @@ const (
|
|||||||
InventoryMovementTypeTransferOut InventoryMovementType = "transfer_out"
|
InventoryMovementTypeTransferOut InventoryMovementType = "transfer_out"
|
||||||
InventoryMovementTypeDamage InventoryMovementType = "damage"
|
InventoryMovementTypeDamage InventoryMovementType = "damage"
|
||||||
InventoryMovementTypeExpiry InventoryMovementType = "expiry"
|
InventoryMovementTypeExpiry InventoryMovementType = "expiry"
|
||||||
|
InventoryMovementTypeIngredient InventoryMovementType = "ingredient"
|
||||||
)
|
)
|
||||||
|
|
||||||
type InventoryMovementReferenceType string
|
type InventoryMovementReferenceType string
|
||||||
|
|||||||
@ -1,18 +1,18 @@
|
|||||||
package processor
|
package processor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"apskel-pos-be/internal/constants"
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
|
|
||||||
"apskel-pos-be/internal/constants"
|
|
||||||
"apskel-pos-be/internal/entities"
|
"apskel-pos-be/internal/entities"
|
||||||
"apskel-pos-be/internal/mappers"
|
"apskel-pos-be/internal/mappers"
|
||||||
"apskel-pos-be/internal/models"
|
"apskel-pos-be/internal/models"
|
||||||
"apskel-pos-be/internal/repository"
|
"apskel-pos-be/internal/repository"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type OrderProcessor interface {
|
type OrderProcessor interface {
|
||||||
@ -66,8 +66,6 @@ type PaymentRepository interface {
|
|||||||
RefundPayment(ctx context.Context, id uuid.UUID, refundAmount float64, reason string, refundedBy uuid.UUID) error
|
RefundPayment(ctx context.Context, id uuid.UUID, refundAmount float64, reason string, refundedBy uuid.UUID) error
|
||||||
UpdateStatus(ctx context.Context, id uuid.UUID, status entities.PaymentTransactionStatus) error
|
UpdateStatus(ctx context.Context, id uuid.UUID, status entities.PaymentTransactionStatus) error
|
||||||
GetTotalPaidByOrderID(ctx context.Context, orderID uuid.UUID) (float64, error)
|
GetTotalPaidByOrderID(ctx context.Context, orderID uuid.UUID) (float64, error)
|
||||||
CreatePaymentWithInventoryMovement(ctx context.Context, req *models.CreatePaymentRequest, order *entities.Order, totalPaid float64) (*entities.Payment, error)
|
|
||||||
RefundPaymentWithInventoryMovement(ctx context.Context, paymentID uuid.UUID, refundAmount float64, reason string, refundedBy uuid.UUID, order *entities.Payment) error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PaymentOrderItemRepository interface {
|
type PaymentOrderItemRepository interface {
|
||||||
@ -87,16 +85,9 @@ type CustomerRepository interface {
|
|||||||
GetByIDAndOrganization(ctx context.Context, id, organizationID uuid.UUID) (*entities.Customer, error)
|
GetByIDAndOrganization(ctx context.Context, id, organizationID uuid.UUID) (*entities.Customer, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type SimplePaymentMethodRepository struct{}
|
type InventoryMovementService interface {
|
||||||
|
CreateIngredientMovement(ctx context.Context, ingredientID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error
|
||||||
func (r *SimplePaymentMethodRepository) GetByID(ctx context.Context, id uuid.UUID) (*entities.PaymentMethod, error) {
|
CreateProductMovement(ctx context.Context, productID, organizationID, outletID, userID uuid.UUID, movementType entities.InventoryMovementType, quantity float64, unitCost float64, reason string, referenceType *entities.InventoryMovementReferenceType, referenceID *uuid.UUID) error
|
||||||
// TODO: Implement proper payment method repository
|
|
||||||
// For now, return a mock payment method
|
|
||||||
return &entities.PaymentMethod{
|
|
||||||
ID: id,
|
|
||||||
Name: "Cash",
|
|
||||||
Type: entities.PaymentMethodTypeCash,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type OrderProcessorImpl struct {
|
type OrderProcessorImpl struct {
|
||||||
@ -112,6 +103,10 @@ type OrderProcessorImpl struct {
|
|||||||
outletRepo OutletRepository
|
outletRepo OutletRepository
|
||||||
customerRepo CustomerRepository
|
customerRepo CustomerRepository
|
||||||
splitBillProcessor SplitBillProcessor
|
splitBillProcessor SplitBillProcessor
|
||||||
|
txManager *repository.TxManager
|
||||||
|
productRecipeRepo *repository.ProductRecipeRepository
|
||||||
|
ingredientRepo IngredientRepository
|
||||||
|
inventoryMovementService InventoryMovementService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOrderProcessorImpl(
|
func NewOrderProcessorImpl(
|
||||||
@ -126,6 +121,10 @@ func NewOrderProcessorImpl(
|
|||||||
productVariantRepo repository.ProductVariantRepository,
|
productVariantRepo repository.ProductVariantRepository,
|
||||||
outletRepo OutletRepository,
|
outletRepo OutletRepository,
|
||||||
customerRepo CustomerRepository,
|
customerRepo CustomerRepository,
|
||||||
|
txManager *repository.TxManager,
|
||||||
|
productRecipeRepo *repository.ProductRecipeRepository,
|
||||||
|
ingredientRepo IngredientRepository,
|
||||||
|
inventoryMovementService InventoryMovementService,
|
||||||
) *OrderProcessorImpl {
|
) *OrderProcessorImpl {
|
||||||
return &OrderProcessorImpl{
|
return &OrderProcessorImpl{
|
||||||
orderRepo: orderRepo,
|
orderRepo: orderRepo,
|
||||||
@ -140,6 +139,10 @@ func NewOrderProcessorImpl(
|
|||||||
outletRepo: outletRepo,
|
outletRepo: outletRepo,
|
||||||
customerRepo: customerRepo,
|
customerRepo: customerRepo,
|
||||||
splitBillProcessor: NewSplitBillProcessorImpl(orderRepo, orderItemRepo, paymentRepo, paymentOrderItemRepo, outletRepo),
|
splitBillProcessor: NewSplitBillProcessorImpl(orderRepo, orderItemRepo, paymentRepo, paymentOrderItemRepo, outletRepo),
|
||||||
|
txManager: txManager,
|
||||||
|
productRecipeRepo: productRecipeRepo,
|
||||||
|
ingredientRepo: ingredientRepo,
|
||||||
|
inventoryMovementService: inventoryMovementService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -769,31 +772,11 @@ func (p *OrderProcessorImpl) CreatePayment(ctx context.Context, req *models.Crea
|
|||||||
return nil, fmt.Errorf("failed to get total paid: %w", err)
|
return nil, fmt.Errorf("failed to get total paid: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
remainingAmount := order.TotalAmount - totalPaid
|
payment, err := p.CreatePaymentWithInventoryMovement(ctx, req, order, totalPaid)
|
||||||
if req.Amount > remainingAmount {
|
|
||||||
return nil, fmt.Errorf("payment amount exceeds remaining balance")
|
|
||||||
}
|
|
||||||
|
|
||||||
payment, err := p.paymentRepo.CreatePaymentWithInventoryMovement(ctx, req, order, totalPaid)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update order payment status and remaining amount in processor layer
|
|
||||||
newTotalPaid := totalPaid + req.Amount
|
|
||||||
order.RemainingAmount = order.TotalAmount - newTotalPaid
|
|
||||||
|
|
||||||
if newTotalPaid >= order.TotalAmount {
|
|
||||||
order.PaymentStatus = entities.PaymentStatusCompleted
|
|
||||||
order.RemainingAmount = 0
|
|
||||||
} else {
|
|
||||||
order.PaymentStatus = entities.PaymentStatusPartial
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.orderRepo.Update(ctx, order); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to update order payment status: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
paymentWithRelations, err := p.paymentRepo.GetByID(ctx, payment.ID)
|
paymentWithRelations, err := p.paymentRepo.GetByID(ctx, payment.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to retrieve created payment: %w", err)
|
return nil, fmt.Errorf("failed to retrieve created payment: %w", err)
|
||||||
@ -817,7 +800,271 @@ func (p *OrderProcessorImpl) RefundPayment(ctx context.Context, paymentID uuid.U
|
|||||||
return fmt.Errorf("refund amount cannot exceed payment amount")
|
return fmt.Errorf("refund amount cannot exceed payment amount")
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.paymentRepo.RefundPaymentWithInventoryMovement(ctx, paymentID, refundAmount, reason, refundedBy, payment)
|
return p.RefundPaymentWithInventoryMovement(ctx, paymentID, refundAmount, reason, refundedBy, payment)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *OrderProcessorImpl) CreatePaymentWithInventoryMovement(ctx context.Context, req *models.CreatePaymentRequest, order *entities.Order, totalPaid float64) (*entities.Payment, error) {
|
||||||
|
var payment *entities.Payment
|
||||||
|
|
||||||
|
err := p.txManager.WithTransaction(ctx, func(ctx context.Context) error {
|
||||||
|
var err error
|
||||||
|
payment, err = p.createPayment(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create payment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.updateOrderStatus(ctx, req.OrderID); err != nil {
|
||||||
|
return fmt.Errorf("failed to update order status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.processInventoryAdjustments(ctx, req.OrderID, order, payment); err != nil {
|
||||||
|
return fmt.Errorf("failed to process inventory adjustments: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return payment, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *OrderProcessorImpl) createPayment(ctx context.Context, req *models.CreatePaymentRequest) (*entities.Payment, error) {
|
||||||
|
payment := &entities.Payment{
|
||||||
|
OrderID: req.OrderID,
|
||||||
|
PaymentMethodID: req.PaymentMethodID,
|
||||||
|
Amount: req.Amount,
|
||||||
|
Status: entities.PaymentTransactionStatusCompleted,
|
||||||
|
TransactionID: req.TransactionID,
|
||||||
|
SplitNumber: req.SplitNumber,
|
||||||
|
SplitTotal: req.SplitTotal,
|
||||||
|
SplitDescription: req.SplitDescription,
|
||||||
|
Metadata: entities.Metadata(req.Metadata),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.paymentRepo.Create(ctx, payment); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create payment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return payment, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *OrderProcessorImpl) updateOrderStatus(ctx context.Context, orderID uuid.UUID) error {
|
||||||
|
orderUpdate := &entities.Order{
|
||||||
|
ID: orderID,
|
||||||
|
Status: entities.OrderStatusCompleted,
|
||||||
|
PaymentStatus: entities.PaymentStatusCompleted,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.orderRepo.Update(ctx, orderUpdate); err != nil {
|
||||||
|
return fmt.Errorf("failed to update order status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *OrderProcessorImpl) processInventoryAdjustments(ctx context.Context, orderID uuid.UUID, order *entities.Order, payment *entities.Payment) error {
|
||||||
|
orderItems, err := p.orderItemRepo.GetByOrderID(ctx, orderID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get order items for inventory adjustment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var inventoryMovements []*entities.InventoryMovement
|
||||||
|
var inventoryUpdates []*entities.Inventory
|
||||||
|
var ingredientUpdates []*entities.Ingredient
|
||||||
|
var ingredientMovements []*entities.InventoryMovement
|
||||||
|
|
||||||
|
for _, item := range orderItems {
|
||||||
|
updatedInventory, err := p.prepareProductInventoryUpdate(ctx, item, order.OutletID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to prepare product inventory update for product %s: %w", item.ProductID, err)
|
||||||
|
}
|
||||||
|
inventoryUpdates = append(inventoryUpdates, updatedInventory)
|
||||||
|
|
||||||
|
productMovement := p.prepareProductInventoryMovement(item, order, payment, updatedInventory)
|
||||||
|
inventoryMovements = append(inventoryMovements, productMovement)
|
||||||
|
|
||||||
|
ingredientData, err := p.prepareIngredientRecipeData(ctx, item, order, payment)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to prepare ingredient recipe data for product %s: %w", item.ProductID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ingredientUpdates = append(ingredientUpdates, ingredientData.ingredientUpdates...)
|
||||||
|
ingredientMovements = append(ingredientMovements, ingredientData.movements...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(inventoryUpdates) > 0 {
|
||||||
|
if err := p.bulkUpdateInventory(ctx, inventoryUpdates); err != nil {
|
||||||
|
return fmt.Errorf("failed to bulk update product inventory: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allMovements := append(inventoryMovements, ingredientMovements...)
|
||||||
|
if len(allMovements) > 0 {
|
||||||
|
if err := p.bulkCreateInventoryMovements(ctx, allMovements); err != nil {
|
||||||
|
return fmt.Errorf("failed to bulk create inventory movements: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// adjustIngredientInventoryWithTransaction adjusts ingredient inventory within a transaction
|
||||||
|
func (p *OrderProcessorImpl) adjustIngredientInventoryWithTransaction(ctx context.Context, ingredientID, outletID uuid.UUID, delta float64) (*entities.Inventory, error) {
|
||||||
|
var inventory entities.Inventory
|
||||||
|
|
||||||
|
// Try to get existing ingredient inventory
|
||||||
|
existingInventory, err := p.inventoryRepo.GetByProductAndOutlet(ctx, ingredientID, outletID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
// Create new ingredient inventory record if it doesn't exist
|
||||||
|
inventory = entities.Inventory{
|
||||||
|
ProductID: ingredientID,
|
||||||
|
OutletID: outletID,
|
||||||
|
Quantity: 0,
|
||||||
|
ReorderLevel: 0,
|
||||||
|
}
|
||||||
|
if err := p.inventoryRepo.Create(ctx, &inventory); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create ingredient inventory record: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Use existing ingredient inventory
|
||||||
|
inventory = *existingInventory
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update quantity (note: ingredients use float64 quantities, but inventory uses int)
|
||||||
|
// Convert delta to int for inventory system
|
||||||
|
deltaInt := int(delta)
|
||||||
|
inventory.Quantity += deltaInt
|
||||||
|
if inventory.Quantity < 0 {
|
||||||
|
inventory.Quantity = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.inventoryRepo.Update(ctx, &inventory); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &inventory, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createIngredientInventoryMovement creates an inventory movement record for ingredient usage
|
||||||
|
func (p *OrderProcessorImpl) createIngredientInventoryMovement(ctx context.Context, recipe *entities.ProductRecipe, item *entities.OrderItem, order *entities.Order, payment *entities.Payment, updatedInventory *entities.Inventory, totalIngredientQuantity float64) error {
|
||||||
|
ingredient, err := p.getIngredientDetails(ctx, recipe.IngredientID, order.OrganizationID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get ingredient details: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
movement := &entities.InventoryMovement{
|
||||||
|
OrganizationID: order.OrganizationID,
|
||||||
|
OutletID: order.OutletID,
|
||||||
|
ItemID: recipe.IngredientID,
|
||||||
|
ItemType: "INGREDIENT",
|
||||||
|
MovementType: entities.InventoryMovementTypeIngredient,
|
||||||
|
Quantity: -totalIngredientQuantity, // Negative because we're consuming ingredients
|
||||||
|
PreviousQuantity: float64(updatedInventory.Quantity + int(totalIngredientQuantity)), // Add back the quantity that was subtracted
|
||||||
|
NewQuantity: float64(updatedInventory.Quantity),
|
||||||
|
UnitCost: ingredient.Cost, // Use ingredient cost
|
||||||
|
TotalCost: totalIngredientQuantity * ingredient.Cost,
|
||||||
|
ReferenceType: func() *entities.InventoryMovementReferenceType {
|
||||||
|
t := entities.InventoryMovementReferenceTypePayment
|
||||||
|
return &t
|
||||||
|
}(),
|
||||||
|
ReferenceID: &payment.ID,
|
||||||
|
OrderID: &order.ID,
|
||||||
|
PaymentID: &payment.ID,
|
||||||
|
UserID: order.UserID,
|
||||||
|
Reason: stringPtr("Ingredient consumption from order payment"),
|
||||||
|
Notes: stringPtr(fmt.Sprintf("Order: %s, Payment: %s, Product: %s, Ingredient: %s", order.OrderNumber, payment.ID, item.Product.Name, ingredient.Name)),
|
||||||
|
Metadata: entities.Metadata{"order_item_id": item.ID, "product_id": item.ProductID, "ingredient_id": recipe.IngredientID, "recipe_quantity": recipe.Quantity, "order_quantity": item.Quantity},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.inventoryMovementRepo.Create(ctx, movement); err != nil {
|
||||||
|
return fmt.Errorf("failed to create ingredient inventory movement: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getIngredientDetails retrieves ingredient details for cost calculation
|
||||||
|
func (p *OrderProcessorImpl) getIngredientDetails(ctx context.Context, ingredientID uuid.UUID, organizationID uuid.UUID) (*entities.Ingredient, error) {
|
||||||
|
ingredient, err := p.ingredientRepo.GetByID(ctx, ingredientID, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get ingredient details: %w", err)
|
||||||
|
}
|
||||||
|
return ingredient, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createInventoryMovement creates an inventory movement record for audit trail
|
||||||
|
func (p *OrderProcessorImpl) createInventoryMovement(ctx context.Context, item *entities.OrderItem, order *entities.Order, payment *entities.Payment, updatedInventory *entities.Inventory) error {
|
||||||
|
movement := &entities.InventoryMovement{
|
||||||
|
OrganizationID: order.OrganizationID,
|
||||||
|
OutletID: order.OutletID,
|
||||||
|
ItemID: item.ProductID,
|
||||||
|
ItemType: "PRODUCT",
|
||||||
|
MovementType: entities.InventoryMovementTypeSale,
|
||||||
|
Quantity: float64(-item.Quantity),
|
||||||
|
PreviousQuantity: float64(updatedInventory.Quantity + item.Quantity), // Add back the quantity that was subtracted
|
||||||
|
NewQuantity: float64(updatedInventory.Quantity),
|
||||||
|
UnitCost: item.UnitCost,
|
||||||
|
TotalCost: float64(item.Quantity) * item.UnitCost,
|
||||||
|
ReferenceType: func() *entities.InventoryMovementReferenceType {
|
||||||
|
t := entities.InventoryMovementReferenceTypePayment
|
||||||
|
return &t
|
||||||
|
}(),
|
||||||
|
ReferenceID: &payment.ID,
|
||||||
|
OrderID: &order.ID,
|
||||||
|
PaymentID: &payment.ID,
|
||||||
|
UserID: order.UserID,
|
||||||
|
Reason: stringPtr("Sale from order payment"),
|
||||||
|
Notes: stringPtr(fmt.Sprintf("Order: %s, Payment: %s", order.OrderNumber, payment.ID)),
|
||||||
|
Metadata: entities.Metadata{"order_item_id": item.ID},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.inventoryMovementRepo.Create(ctx, movement); err != nil {
|
||||||
|
return fmt.Errorf("failed to create inventory movement: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *OrderProcessorImpl) RefundPaymentWithInventoryMovement(ctx context.Context, paymentID uuid.UUID, refundAmount float64, reason string, refundedBy uuid.UUID, payment *entities.Payment) error {
|
||||||
|
return p.txManager.WithTransaction(ctx, func(ctx context.Context) error {
|
||||||
|
if err := p.processRefund(ctx, paymentID, refundAmount, reason, refundedBy); err != nil {
|
||||||
|
return fmt.Errorf("failed to process refund: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.updateOrderRefundAmount(ctx, payment.OrderID, refundAmount); err != nil {
|
||||||
|
return fmt.Errorf("failed to update order refund amount: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *OrderProcessorImpl) processRefund(ctx context.Context, paymentID uuid.UUID, refundAmount float64, reason string, refundedBy uuid.UUID) error {
|
||||||
|
if err := p.paymentRepo.RefundPayment(ctx, paymentID, refundAmount, reason, refundedBy); err != nil {
|
||||||
|
return fmt.Errorf("failed to refund payment: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *OrderProcessorImpl) updateOrderRefundAmount(ctx context.Context, orderID uuid.UUID, refundAmount float64) error {
|
||||||
|
order, err := p.orderRepo.GetByID(ctx, orderID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get order: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
order.RefundAmount += refundAmount
|
||||||
|
if err := p.orderRepo.Update(ctx, order); err != nil {
|
||||||
|
return fmt.Errorf("failed to update order refund amount: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *OrderProcessorImpl) SetOrderCustomer(ctx context.Context, orderID uuid.UUID, req *models.SetOrderCustomerRequest, organizationID uuid.UUID) (*models.SetOrderCustomerResponse, error) {
|
func (p *OrderProcessorImpl) SetOrderCustomer(ctx context.Context, orderID uuid.UUID, req *models.SetOrderCustomerRequest, organizationID uuid.UUID) (*models.SetOrderCustomerResponse, error) {
|
||||||
@ -920,3 +1167,438 @@ func (p *OrderProcessorImpl) SplitBill(ctx context.Context, req *models.SplitBil
|
|||||||
}
|
}
|
||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper method to adjust inventory within a transaction
|
||||||
|
func (p *OrderProcessorImpl) adjustInventoryWithTransaction(ctx context.Context, productID, outletID uuid.UUID, delta int) (*entities.Inventory, error) {
|
||||||
|
var inventory entities.Inventory
|
||||||
|
|
||||||
|
// Try to get existing inventory
|
||||||
|
existingInventory, err := p.inventoryRepo.GetByProductAndOutlet(ctx, productID, outletID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
// Create new inventory record if it doesn't exist
|
||||||
|
inventory = entities.Inventory{
|
||||||
|
ProductID: productID,
|
||||||
|
OutletID: outletID,
|
||||||
|
Quantity: 0,
|
||||||
|
ReorderLevel: 0,
|
||||||
|
}
|
||||||
|
if err := p.inventoryRepo.Create(ctx, &inventory); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create inventory record: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Use existing inventory
|
||||||
|
inventory = *existingInventory
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update quantity
|
||||||
|
inventory.UpdateQuantity(delta)
|
||||||
|
if err := p.inventoryRepo.Update(ctx, &inventory); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &inventory, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *OrderProcessorImpl) createIngredientProductMovement(ctx context.Context, recipe *entities.ProductRecipe, item *entities.OrderItem, order *entities.Order, payment *entities.Payment, totalIngredientQuantity float64) error {
|
||||||
|
ingredient, err := p.getIngredientDetails(ctx, recipe.IngredientID, order.OrganizationID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get ingredient details: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = p.inventoryMovementService.CreateProductMovement(
|
||||||
|
ctx,
|
||||||
|
recipe.IngredientID, // ingredientID as productID
|
||||||
|
order.OrganizationID,
|
||||||
|
order.OutletID,
|
||||||
|
order.UserID,
|
||||||
|
entities.InventoryMovementTypeSale, // Movement Type "Sales"
|
||||||
|
-totalIngredientQuantity, // Negative quantity for consumption
|
||||||
|
ingredient.Cost, // Unit cost from ingredient
|
||||||
|
"Ingredient consumption from order payment",
|
||||||
|
func() *entities.InventoryMovementReferenceType {
|
||||||
|
t := entities.InventoryMovementReferenceTypePayment
|
||||||
|
return &t
|
||||||
|
}(),
|
||||||
|
&payment.ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create ingredient product movement: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createRefundIngredientProductMovement creates a product movement record for ingredient with Movement Type "Refund" and ItemType "INGREDIENT"
|
||||||
|
func (p *OrderProcessorImpl) createRefundIngredientProductMovement(ctx context.Context, recipe *entities.ProductRecipe, item *entities.OrderItem, order *entities.Order, payment *entities.Payment, totalIngredientQuantity float64, refundRatio float64, reason string, refundedBy uuid.UUID, refundAmount float64) error {
|
||||||
|
// Get ingredient details for cost calculation
|
||||||
|
ingredient, err := p.getIngredientDetails(ctx, recipe.IngredientID, order.OrganizationID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get ingredient details: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create product movement using the inventory movement service
|
||||||
|
err = p.inventoryMovementService.CreateProductMovement(
|
||||||
|
ctx,
|
||||||
|
recipe.IngredientID, // ingredientID as productID
|
||||||
|
order.OrganizationID,
|
||||||
|
order.OutletID,
|
||||||
|
order.UserID,
|
||||||
|
entities.InventoryMovementTypeRefund, // Movement Type "Refund"
|
||||||
|
totalIngredientQuantity, // Positive quantity for restoration
|
||||||
|
ingredient.Cost, // Unit cost from ingredient
|
||||||
|
fmt.Sprintf("Ingredient restoration from order refund: %s", reason),
|
||||||
|
func() *entities.InventoryMovementReferenceType {
|
||||||
|
t := entities.InventoryMovementReferenceTypeRefund
|
||||||
|
return &t
|
||||||
|
}(),
|
||||||
|
&payment.ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create refund ingredient product movement: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepareProductInventoryUpdate prepares inventory update data without making database calls
|
||||||
|
func (p *OrderProcessorImpl) prepareProductInventoryUpdate(ctx context.Context, item *entities.OrderItem, outletID uuid.UUID) (*entities.Inventory, error) {
|
||||||
|
// Get current inventory
|
||||||
|
currentInventory, err := p.inventoryRepo.GetByProductAndOutlet(ctx, item.ProductID, outletID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
// Create new inventory record if it doesn't exist
|
||||||
|
currentInventory = &entities.Inventory{
|
||||||
|
ProductID: item.ProductID,
|
||||||
|
OutletID: outletID,
|
||||||
|
Quantity: 0,
|
||||||
|
ReorderLevel: 0,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate new quantity
|
||||||
|
newQuantity := currentInventory.Quantity - item.Quantity
|
||||||
|
if newQuantity < 0 {
|
||||||
|
newQuantity = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the inventory object (don't save to DB yet)
|
||||||
|
currentInventory.Quantity = newQuantity
|
||||||
|
return currentInventory, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepareProductInventoryMovement prepares inventory movement data without making database calls
|
||||||
|
func (p *OrderProcessorImpl) prepareProductInventoryMovement(item *entities.OrderItem, order *entities.Order, payment *entities.Payment, updatedInventory *entities.Inventory) *entities.InventoryMovement {
|
||||||
|
previousQuantity := updatedInventory.Quantity + item.Quantity
|
||||||
|
|
||||||
|
return &entities.InventoryMovement{
|
||||||
|
OrganizationID: order.OrganizationID,
|
||||||
|
OutletID: order.OutletID,
|
||||||
|
ItemID: item.ProductID,
|
||||||
|
ItemType: "PRODUCT",
|
||||||
|
MovementType: entities.InventoryMovementTypeSale,
|
||||||
|
Quantity: float64(-item.Quantity),
|
||||||
|
PreviousQuantity: float64(previousQuantity),
|
||||||
|
NewQuantity: float64(updatedInventory.Quantity),
|
||||||
|
UnitCost: item.UnitCost,
|
||||||
|
TotalCost: float64(item.Quantity) * item.UnitCost,
|
||||||
|
ReferenceType: func() *entities.InventoryMovementReferenceType {
|
||||||
|
t := entities.InventoryMovementReferenceTypePayment
|
||||||
|
return &t
|
||||||
|
}(),
|
||||||
|
ReferenceID: &payment.ID,
|
||||||
|
OrderID: &order.ID,
|
||||||
|
PaymentID: &payment.ID,
|
||||||
|
UserID: order.UserID,
|
||||||
|
Reason: stringPtr("Sale from order payment"),
|
||||||
|
Notes: stringPtr(fmt.Sprintf("Order: %s, Payment: %s", order.OrderNumber, payment.ID)),
|
||||||
|
Metadata: entities.Metadata{"order_item_id": item.ID},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ingredientRecipeData holds the collected data for ingredient recipes
|
||||||
|
type ingredientRecipeData struct {
|
||||||
|
ingredientUpdates []*entities.Ingredient
|
||||||
|
movements []*entities.InventoryMovement
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepareIngredientRecipeData prepares ingredient recipe data without making database calls
|
||||||
|
func (p *OrderProcessorImpl) prepareIngredientRecipeData(ctx context.Context, item *entities.OrderItem, order *entities.Order, payment *entities.Payment) (*ingredientRecipeData, error) {
|
||||||
|
// Check if the product has ingredients
|
||||||
|
product, err := p.productRepo.GetByID(ctx, item.ProductID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get product: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !product.HasIngredients {
|
||||||
|
return &ingredientRecipeData{}, nil // Product doesn't have ingredients
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get product recipes based on variant (if any)
|
||||||
|
var recipes []*entities.ProductRecipe
|
||||||
|
if item.ProductVariantID != nil {
|
||||||
|
recipes, err = p.productRecipeRepo.GetByProductAndVariantID(ctx, item.ProductID, item.ProductVariantID, order.OrganizationID)
|
||||||
|
} else {
|
||||||
|
recipes, err = p.productRecipeRepo.GetByProductAndVariantID(ctx, item.ProductID, nil, order.OrganizationID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get product recipes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(recipes) == 0 {
|
||||||
|
return &ingredientRecipeData{}, nil // No recipes found
|
||||||
|
}
|
||||||
|
|
||||||
|
var ingredientUpdates []*entities.Ingredient
|
||||||
|
var movements []*entities.InventoryMovement
|
||||||
|
|
||||||
|
// Process each ingredient in the recipe
|
||||||
|
for _, recipe := range recipes {
|
||||||
|
ingredientData, err := p.prepareIngredientRecipeItem(ctx, recipe, item, order, payment)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to prepare ingredient recipe item: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ingredientUpdates = append(ingredientUpdates, ingredientData.ingredient)
|
||||||
|
movements = append(movements, ingredientData.movement)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ingredientRecipeData{
|
||||||
|
ingredientUpdates: ingredientUpdates,
|
||||||
|
movements: movements,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ingredientRecipeItem holds data for a single ingredient recipe item
|
||||||
|
type ingredientRecipeItem struct {
|
||||||
|
ingredient *entities.Ingredient
|
||||||
|
movement *entities.InventoryMovement
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepareIngredientRecipeItem prepares data for a single ingredient recipe without making database calls
|
||||||
|
func (p *OrderProcessorImpl) prepareIngredientRecipeItem(ctx context.Context, recipe *entities.ProductRecipe, item *entities.OrderItem, order *entities.Order, payment *entities.Payment) (*ingredientRecipeItem, error) {
|
||||||
|
// Calculate total ingredient quantity needed
|
||||||
|
totalIngredientQuantity := recipe.Quantity * float64(item.Quantity)
|
||||||
|
|
||||||
|
// Get current ingredient details
|
||||||
|
currentIngredient, err := p.ingredientRepo.GetByID(ctx, recipe.IngredientID, order.OrganizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get ingredient: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For ingredients, we typically don't track quantity in the ingredient entity itself
|
||||||
|
// Instead, we create inventory movement records to track consumption
|
||||||
|
// The ingredient entity remains unchanged, but we track the movement
|
||||||
|
|
||||||
|
// Prepare movement record
|
||||||
|
movement := &entities.InventoryMovement{
|
||||||
|
OrganizationID: order.OrganizationID,
|
||||||
|
OutletID: order.OutletID,
|
||||||
|
ItemID: recipe.IngredientID,
|
||||||
|
ItemType: "INGREDIENT",
|
||||||
|
MovementType: entities.InventoryMovementTypeIngredient,
|
||||||
|
Quantity: -totalIngredientQuantity,
|
||||||
|
PreviousQuantity: 0, // We don't track current quantity in ingredient entity
|
||||||
|
NewQuantity: 0, // We don't track current quantity in ingredient entity
|
||||||
|
UnitCost: currentIngredient.Cost,
|
||||||
|
TotalCost: totalIngredientQuantity * currentIngredient.Cost,
|
||||||
|
ReferenceType: func() *entities.InventoryMovementReferenceType {
|
||||||
|
t := entities.InventoryMovementReferenceTypePayment
|
||||||
|
return &t
|
||||||
|
}(),
|
||||||
|
ReferenceID: &payment.ID,
|
||||||
|
OrderID: &order.ID,
|
||||||
|
PaymentID: &payment.ID,
|
||||||
|
UserID: order.UserID,
|
||||||
|
Reason: stringPtr("Ingredient consumption from order payment"),
|
||||||
|
Notes: stringPtr(fmt.Sprintf("Order: %s, Payment: %s, Product: %s", order.OrderNumber, payment.ID, item.Product.Name)),
|
||||||
|
Metadata: entities.Metadata{"order_item_id": item.ID, "product_id": item.ProductID, "ingredient_id": recipe.IngredientID, "recipe_quantity": recipe.Quantity, "order_quantity": item.Quantity},
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ingredientRecipeItem{
|
||||||
|
ingredient: currentIngredient, // Return unchanged ingredient
|
||||||
|
movement: movement,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// bulkUpdateInventory performs bulk update of inventory records
|
||||||
|
func (p *OrderProcessorImpl) bulkUpdateInventory(ctx context.Context, inventories []*entities.Inventory) error {
|
||||||
|
if len(inventories) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the repository's bulk update method
|
||||||
|
return p.inventoryRepo.BulkUpdate(ctx, inventories)
|
||||||
|
}
|
||||||
|
|
||||||
|
// bulkCreateInventoryMovements performs bulk creation of inventory movement records
|
||||||
|
func (p *OrderProcessorImpl) bulkCreateInventoryMovements(ctx context.Context, movements []*entities.InventoryMovement) error {
|
||||||
|
if len(movements) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use GORM's CreateInBatches for true bulk creation
|
||||||
|
// Convert to interface slice for GORM
|
||||||
|
movementInterfaces := make([]interface{}, len(movements))
|
||||||
|
for i, movement := range movements {
|
||||||
|
movementInterfaces[i] = movement
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the inventory movement repository's bulk create method
|
||||||
|
// Note: This assumes the repository has a bulk create method
|
||||||
|
// If not, we can implement it here using GORM's CreateInBatches
|
||||||
|
return p.inventoryMovementRepo.CreateInBatches(ctx, movements, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepareRefundProductInventoryUpdate prepares product inventory restoration data without making database calls
|
||||||
|
func (p *OrderProcessorImpl) prepareRefundProductInventoryUpdate(ctx context.Context, item *entities.OrderItem, outletID uuid.UUID, refundedQuantity int) (*entities.Inventory, error) {
|
||||||
|
// Get current inventory
|
||||||
|
currentInventory, err := p.inventoryRepo.GetByProductAndOutlet(ctx, item.ProductID, outletID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get product inventory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate new quantity (restore the refunded quantity)
|
||||||
|
newQuantity := currentInventory.Quantity + refundedQuantity
|
||||||
|
|
||||||
|
// Update the inventory object (don't save to DB yet)
|
||||||
|
currentInventory.Quantity = newQuantity
|
||||||
|
return currentInventory, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepareRefundProductInventoryMovement prepares product inventory movement data for refunds
|
||||||
|
func (p *OrderProcessorImpl) prepareRefundProductInventoryMovement(item *entities.OrderItem, order *entities.Order, payment *entities.Payment, updatedInventory *entities.Inventory, refundedQuantity int, refundRatio float64, reason string, refundedBy uuid.UUID, refundAmount float64) *entities.InventoryMovement {
|
||||||
|
previousQuantity := updatedInventory.Quantity - refundedQuantity
|
||||||
|
|
||||||
|
return &entities.InventoryMovement{
|
||||||
|
OrganizationID: order.OrganizationID,
|
||||||
|
OutletID: order.OutletID,
|
||||||
|
ItemID: item.ProductID,
|
||||||
|
ItemType: "PRODUCT",
|
||||||
|
MovementType: entities.InventoryMovementTypeRefund,
|
||||||
|
Quantity: float64(refundedQuantity),
|
||||||
|
PreviousQuantity: float64(previousQuantity),
|
||||||
|
NewQuantity: float64(updatedInventory.Quantity),
|
||||||
|
UnitCost: item.UnitCost,
|
||||||
|
TotalCost: float64(refundedQuantity) * item.UnitCost,
|
||||||
|
ReferenceType: func() *entities.InventoryMovementReferenceType {
|
||||||
|
t := entities.InventoryMovementReferenceTypeRefund
|
||||||
|
return &t
|
||||||
|
}(),
|
||||||
|
ReferenceID: &payment.ID,
|
||||||
|
OrderID: &order.ID,
|
||||||
|
PaymentID: &payment.ID,
|
||||||
|
UserID: refundedBy,
|
||||||
|
Reason: stringPtr(fmt.Sprintf("Refund: %s", reason)),
|
||||||
|
Notes: stringPtr(fmt.Sprintf("Order: %s, Payment: %s, Refund Amount: %.2f", order.OrderNumber, payment.ID, refundAmount)),
|
||||||
|
Metadata: entities.Metadata{"order_item_id": item.ID, "refund_ratio": refundRatio},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepareRefundedIngredientRecipeData prepares ingredient recipe restoration data for refunds
|
||||||
|
func (p *OrderProcessorImpl) prepareRefundedIngredientRecipeData(ctx context.Context, item *entities.OrderItem, order *entities.Order, payment *entities.Payment, refundRatio float64, reason string, refundedBy uuid.UUID, refundAmount float64) (*ingredientRecipeData, error) {
|
||||||
|
// Check if the product has ingredients
|
||||||
|
product, err := p.productRepo.GetByID(ctx, item.ProductID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get product: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !product.HasIngredients {
|
||||||
|
return &ingredientRecipeData{}, nil // Product doesn't have ingredients
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get product recipes based on variant (if any)
|
||||||
|
var recipes []*entities.ProductRecipe
|
||||||
|
if item.ProductVariantID != nil {
|
||||||
|
recipes, err = p.productRecipeRepo.GetByProductAndVariantID(ctx, item.ProductID, item.ProductVariantID, order.OrganizationID)
|
||||||
|
} else {
|
||||||
|
recipes, err = p.productRecipeRepo.GetByProductAndVariantID(ctx, item.ProductID, nil, order.OrganizationID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get product recipes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(recipes) == 0 {
|
||||||
|
return &ingredientRecipeData{}, nil // No recipes found
|
||||||
|
}
|
||||||
|
|
||||||
|
var ingredientUpdates []*entities.Ingredient
|
||||||
|
var movements []*entities.InventoryMovement
|
||||||
|
|
||||||
|
// Process each ingredient in the recipe
|
||||||
|
for _, recipe := range recipes {
|
||||||
|
ingredientData, err := p.prepareRefundedIngredientRecipeItem(ctx, recipe, item, order, payment, refundRatio, reason, refundedBy, refundAmount)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to prepare ingredient recipe restoration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ingredientUpdates = append(ingredientUpdates, ingredientData.ingredient)
|
||||||
|
movements = append(movements, ingredientData.movement)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ingredientRecipeData{
|
||||||
|
ingredientUpdates: ingredientUpdates,
|
||||||
|
movements: movements,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepareRefundedIngredientRecipeItem prepares data for a single ingredient recipe restoration
|
||||||
|
func (p *OrderProcessorImpl) prepareRefundedIngredientRecipeItem(ctx context.Context, recipe *entities.ProductRecipe, item *entities.OrderItem, order *entities.Order, payment *entities.Payment, refundRatio float64, reason string, refundedBy uuid.UUID, refundAmount float64) (*ingredientRecipeItem, error) {
|
||||||
|
// Calculate total ingredient quantity needed based on order item quantity
|
||||||
|
totalIngredientQuantity := recipe.Quantity * float64(item.Quantity)
|
||||||
|
|
||||||
|
// Get current ingredient details
|
||||||
|
currentIngredient, err := p.ingredientRepo.GetByID(ctx, recipe.IngredientID, order.OrganizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get ingredient: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For ingredients, we typically don't track quantity in the ingredient entity itself
|
||||||
|
// Instead, we create inventory movement records to track restoration
|
||||||
|
// The ingredient entity remains unchanged, but we track the movement
|
||||||
|
|
||||||
|
// Prepare movement record
|
||||||
|
movement := &entities.InventoryMovement{
|
||||||
|
OrganizationID: order.OrganizationID,
|
||||||
|
OutletID: order.OutletID,
|
||||||
|
ItemID: recipe.IngredientID,
|
||||||
|
ItemType: "INGREDIENT",
|
||||||
|
MovementType: entities.InventoryMovementTypeRefund,
|
||||||
|
Quantity: totalIngredientQuantity,
|
||||||
|
PreviousQuantity: 0, // We don't track current quantity in ingredient entity
|
||||||
|
NewQuantity: 0, // We don't track current quantity in ingredient entity
|
||||||
|
UnitCost: currentIngredient.Cost,
|
||||||
|
TotalCost: totalIngredientQuantity * currentIngredient.Cost,
|
||||||
|
ReferenceType: func() *entities.InventoryMovementReferenceType {
|
||||||
|
t := entities.InventoryMovementReferenceTypeRefund
|
||||||
|
return &t
|
||||||
|
}(),
|
||||||
|
ReferenceID: &payment.ID,
|
||||||
|
OrderID: &order.ID,
|
||||||
|
PaymentID: &payment.ID,
|
||||||
|
UserID: refundedBy,
|
||||||
|
Reason: stringPtr(fmt.Sprintf("Refund: %s", reason)),
|
||||||
|
Notes: stringPtr(fmt.Sprintf("Order: %s, Payment: %s, Refund Amount: %.2f", order.OrderNumber, payment.ID, refundAmount)),
|
||||||
|
Metadata: entities.Metadata{"order_item_id": item.ID, "refund_ratio": refundRatio},
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ingredientRecipeItem{
|
||||||
|
ingredient: currentIngredient, // Return unchanged ingredient
|
||||||
|
movement: movement,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to create string pointer
|
||||||
|
func stringPtr(s string) *string {
|
||||||
|
return &s
|
||||||
|
}
|
||||||
|
|||||||
@ -22,9 +22,7 @@ const (
|
|||||||
MetadataKeyLastSplitQuantities = "last_split_quantities"
|
MetadataKeyLastSplitQuantities = "last_split_quantities"
|
||||||
)
|
)
|
||||||
|
|
||||||
func stringPtr(s string) *string {
|
|
||||||
return &s
|
|
||||||
}
|
|
||||||
|
|
||||||
type SplitBillValidation struct {
|
type SplitBillValidation struct {
|
||||||
OrderItems map[uuid.UUID]*entities.OrderItem
|
OrderItems map[uuid.UUID]*entities.OrderItem
|
||||||
|
|||||||
@ -19,6 +19,7 @@ type InventoryMovementRepository interface {
|
|||||||
GetByPaymentID(ctx context.Context, paymentID uuid.UUID) ([]*entities.InventoryMovement, error)
|
GetByPaymentID(ctx context.Context, paymentID uuid.UUID) ([]*entities.InventoryMovement, error)
|
||||||
Count(ctx context.Context, filters map[string]interface{}) (int64, error)
|
Count(ctx context.Context, filters map[string]interface{}) (int64, error)
|
||||||
CreateWithTransaction(tx *gorm.DB, movement *entities.InventoryMovement) error
|
CreateWithTransaction(tx *gorm.DB, movement *entities.InventoryMovement) error
|
||||||
|
CreateInBatches(ctx context.Context, movements []*entities.InventoryMovement, batchSize int) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type InventoryMovementRepositoryImpl struct {
|
type InventoryMovementRepositoryImpl struct {
|
||||||
@ -39,6 +40,13 @@ func (r *InventoryMovementRepositoryImpl) CreateWithTransaction(tx *gorm.DB, mov
|
|||||||
return tx.Create(movement).Error
|
return tx.Create(movement).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *InventoryMovementRepositoryImpl) CreateInBatches(ctx context.Context, movements []*entities.InventoryMovement, batchSize int) error {
|
||||||
|
if len(movements) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return r.db.WithContext(ctx).CreateInBatches(movements, batchSize).Error
|
||||||
|
}
|
||||||
|
|
||||||
func (r *InventoryMovementRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.InventoryMovement, error) {
|
func (r *InventoryMovementRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.InventoryMovement, error) {
|
||||||
var movement entities.InventoryMovement
|
var movement entities.InventoryMovement
|
||||||
err := r.db.WithContext(ctx).First(&movement, "id = ?", id).Error
|
err := r.db.WithContext(ctx).First(&movement, "id = ?", id).Error
|
||||||
|
|||||||
@ -28,6 +28,7 @@ type InventoryRepository interface {
|
|||||||
SetQuantity(ctx context.Context, productID, outletID uuid.UUID, quantity int) (*entities.Inventory, error)
|
SetQuantity(ctx context.Context, productID, outletID uuid.UUID, quantity int) (*entities.Inventory, error)
|
||||||
UpdateReorderLevel(ctx context.Context, id uuid.UUID, reorderLevel int) error
|
UpdateReorderLevel(ctx context.Context, id uuid.UUID, reorderLevel int) error
|
||||||
BulkCreate(ctx context.Context, inventoryItems []*entities.Inventory) error
|
BulkCreate(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)
|
||||||
}
|
}
|
||||||
@ -276,6 +277,22 @@ func (r *InventoryRepositoryImpl) BulkCreate(ctx context.Context, inventoryItems
|
|||||||
return r.db.WithContext(ctx).CreateInBatches(inventoryItems, 100).Error
|
return r.db.WithContext(ctx).CreateInBatches(inventoryItems, 100).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *InventoryRepositoryImpl) BulkUpdate(ctx context.Context, inventoryItems []*entities.Inventory) error {
|
||||||
|
if len(inventoryItems) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use GORM's transaction for bulk updates
|
||||||
|
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
for _, inventory := range inventoryItems {
|
||||||
|
if err := tx.Save(inventory).Error; err != nil {
|
||||||
|
return fmt.Errorf("failed to update inventory for product %s: %w", inventory.ProductID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (r *InventoryRepositoryImpl) BulkAdjustQuantity(ctx context.Context, adjustments map[uuid.UUID]int, outletID uuid.UUID) error {
|
func (r *InventoryRepositoryImpl) BulkAdjustQuantity(ctx context.Context, adjustments map[uuid.UUID]int, outletID uuid.UUID) error {
|
||||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
for productID, delta := range adjustments {
|
for productID, delta := range adjustments {
|
||||||
|
|||||||
@ -2,12 +2,9 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
"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"
|
||||||
@ -95,207 +92,3 @@ func (r *PaymentRepositoryImpl) GetTotalPaidByOrderID(ctx context.Context, order
|
|||||||
Scan(&total).Error
|
Scan(&total).Error
|
||||||
return total, err
|
return total, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *PaymentRepositoryImpl) CreatePaymentWithInventoryMovement(ctx context.Context, req *models.CreatePaymentRequest, order *entities.Order, totalPaid float64) (*entities.Payment, error) {
|
|
||||||
var payment *entities.Payment
|
|
||||||
var orderJustCompleted bool
|
|
||||||
|
|
||||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
||||||
payment = &entities.Payment{
|
|
||||||
OrderID: req.OrderID,
|
|
||||||
PaymentMethodID: req.PaymentMethodID,
|
|
||||||
Amount: req.Amount,
|
|
||||||
Status: entities.PaymentTransactionStatusCompleted,
|
|
||||||
TransactionID: req.TransactionID,
|
|
||||||
SplitNumber: req.SplitNumber,
|
|
||||||
SplitTotal: req.SplitTotal,
|
|
||||||
SplitDescription: req.SplitDescription,
|
|
||||||
Metadata: entities.Metadata(req.Metadata),
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.Create(payment).Error; err != nil {
|
|
||||||
return fmt.Errorf("failed to create payment: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
newTotalPaid := totalPaid + req.Amount
|
|
||||||
if newTotalPaid >= order.TotalAmount {
|
|
||||||
if order.PaymentStatus != entities.PaymentStatusCompleted {
|
|
||||||
orderJustCompleted = true
|
|
||||||
}
|
|
||||||
if err := tx.Model(&entities.Order{}).Where("id = ?", req.OrderID).Update("status", entities.OrderStatusCompleted).Error; err != nil {
|
|
||||||
return fmt.Errorf("failed to update order status: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if orderJustCompleted {
|
|
||||||
orderItems, err := r.getOrderItemsWithTransaction(tx, req.OrderID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get order items for inventory adjustment: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, item := range orderItems {
|
|
||||||
updatedInventory, err := r.adjustInventoryWithTransaction(tx, item.ProductID, order.OutletID, -item.Quantity)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to adjust inventory for product %s: %w", item.ProductID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
movement := &entities.InventoryMovement{
|
|
||||||
OrganizationID: order.OrganizationID,
|
|
||||||
OutletID: order.OutletID,
|
|
||||||
ItemID: item.ProductID,
|
|
||||||
ItemType: "PRODUCT",
|
|
||||||
MovementType: entities.InventoryMovementTypeSale,
|
|
||||||
Quantity: float64(-item.Quantity),
|
|
||||||
PreviousQuantity: float64(updatedInventory.Quantity + item.Quantity), // Add back the quantity that was subtracted
|
|
||||||
NewQuantity: float64(updatedInventory.Quantity),
|
|
||||||
UnitCost: item.UnitCost,
|
|
||||||
TotalCost: float64(item.Quantity) * item.UnitCost,
|
|
||||||
ReferenceType: func() *entities.InventoryMovementReferenceType {
|
|
||||||
t := entities.InventoryMovementReferenceTypePayment
|
|
||||||
return &t
|
|
||||||
}(),
|
|
||||||
ReferenceID: &payment.ID,
|
|
||||||
OrderID: &order.ID,
|
|
||||||
PaymentID: &payment.ID,
|
|
||||||
UserID: order.UserID,
|
|
||||||
Reason: stringPtr("Sale from order payment"),
|
|
||||||
Notes: stringPtr(fmt.Sprintf("Order: %s, Payment: %s", order.OrderNumber, payment.ID)),
|
|
||||||
Metadata: entities.Metadata{"order_item_id": item.ID},
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := r.createInventoryMovementWithTransaction(tx, movement); err != nil {
|
|
||||||
return fmt.Errorf("failed to create inventory movement for product %s: %w", item.ProductID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return payment, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PaymentRepositoryImpl) RefundPaymentWithInventoryMovement(ctx context.Context, paymentID uuid.UUID, refundAmount float64, reason string, refundedBy uuid.UUID, payment *entities.Payment) error {
|
|
||||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
||||||
if err := r.RefundPayment(ctx, paymentID, refundAmount, reason, refundedBy); err != nil {
|
|
||||||
return fmt.Errorf("failed to refund payment: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get order for inventory management
|
|
||||||
order, err := r.getOrderWithTransaction(tx, payment.OrderID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get order: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update order refund amount
|
|
||||||
order.RefundAmount += refundAmount
|
|
||||||
if err := tx.Model(&entities.Order{}).Where("id = ?", order.ID).Update("refund_amount", order.RefundAmount).Error; err != nil {
|
|
||||||
return fmt.Errorf("failed to update order refund amount: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
refundRatio := refundAmount / payment.Amount
|
|
||||||
|
|
||||||
orderItems, err := r.getOrderItemsWithTransaction(tx, order.ID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get order items for inventory adjustment: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, item := range orderItems {
|
|
||||||
refundedQuantity := int(float64(item.Quantity) * refundRatio)
|
|
||||||
if refundedQuantity > 0 {
|
|
||||||
updatedInventory, err := r.adjustInventoryWithTransaction(tx, item.ProductID, order.OutletID, refundedQuantity)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to restore inventory for product %s: %w", item.ProductID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
movement := &entities.InventoryMovement{
|
|
||||||
OrganizationID: order.OrganizationID,
|
|
||||||
OutletID: order.OutletID,
|
|
||||||
ItemID: item.ProductID,
|
|
||||||
ItemType: "PRODUCT",
|
|
||||||
MovementType: entities.InventoryMovementTypeRefund,
|
|
||||||
Quantity: float64(refundedQuantity),
|
|
||||||
PreviousQuantity: float64(updatedInventory.Quantity - refundedQuantity), // Subtract the quantity that was added
|
|
||||||
NewQuantity: float64(updatedInventory.Quantity),
|
|
||||||
UnitCost: item.UnitCost,
|
|
||||||
TotalCost: float64(refundedQuantity) * item.UnitCost,
|
|
||||||
ReferenceType: func() *entities.InventoryMovementReferenceType {
|
|
||||||
t := entities.InventoryMovementReferenceTypeRefund
|
|
||||||
return &t
|
|
||||||
}(),
|
|
||||||
ReferenceID: &paymentID,
|
|
||||||
OrderID: &order.ID,
|
|
||||||
PaymentID: &paymentID,
|
|
||||||
UserID: refundedBy,
|
|
||||||
Reason: stringPtr(fmt.Sprintf("Refund: %s", reason)),
|
|
||||||
Notes: stringPtr(fmt.Sprintf("Order: %s, Payment: %s, Refund Amount: %.2f", order.OrderNumber, paymentID, refundAmount)),
|
|
||||||
Metadata: entities.Metadata{"order_item_id": item.ID, "refund_ratio": refundRatio},
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := r.createInventoryMovementWithTransaction(tx, movement); err != nil {
|
|
||||||
return fmt.Errorf("failed to create inventory movement for refund product %s: %w", item.ProductID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper methods for transaction operations
|
|
||||||
func (r *PaymentRepositoryImpl) getOrderWithTransaction(tx *gorm.DB, orderID uuid.UUID) (*entities.Order, error) {
|
|
||||||
var order entities.Order
|
|
||||||
err := tx.First(&order, "id = ?", orderID).Error
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &order, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PaymentRepositoryImpl) getOrderItemsWithTransaction(tx *gorm.DB, orderID uuid.UUID) ([]*entities.OrderItem, error) {
|
|
||||||
var orderItems []*entities.OrderItem
|
|
||||||
err := tx.Where("order_id = ?", orderID).Find(&orderItems).Error
|
|
||||||
return orderItems, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PaymentRepositoryImpl) adjustInventoryWithTransaction(tx *gorm.DB, productID, outletID uuid.UUID, delta int) (*entities.Inventory, error) {
|
|
||||||
var inventory entities.Inventory
|
|
||||||
|
|
||||||
// Try to find existing inventory
|
|
||||||
if err := tx.Where("product_id = ? AND outlet_id = ?", productID, outletID).First(&inventory).Error; err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
// Inventory doesn't exist, create it with initial quantity
|
|
||||||
inventory = entities.Inventory{
|
|
||||||
ProductID: productID,
|
|
||||||
OutletID: outletID,
|
|
||||||
Quantity: 0,
|
|
||||||
ReorderLevel: 0,
|
|
||||||
}
|
|
||||||
if err := tx.Create(&inventory).Error; err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create inventory record: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inventory.UpdateQuantity(delta)
|
|
||||||
if err := tx.Save(&inventory).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &inventory, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PaymentRepositoryImpl) createInventoryMovementWithTransaction(tx *gorm.DB, movement *entities.InventoryMovement) error {
|
|
||||||
return tx.Create(movement).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to create string pointer
|
|
||||||
func stringPtr(s string) *string {
|
|
||||||
return &s
|
|
||||||
}
|
|
||||||
|
|||||||
35
internal/repository/tx_manager.go
Normal file
35
internal/repository/tx_manager.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type txKeyType struct{}
|
||||||
|
|
||||||
|
var txKey = txKeyType{}
|
||||||
|
|
||||||
|
// DBFromContext returns the transactional *gorm.DB from context if present; otherwise returns base.
|
||||||
|
func DBFromContext(ctx context.Context, base *gorm.DB) *gorm.DB {
|
||||||
|
if v := ctx.Value(txKey); v != nil {
|
||||||
|
if tx, ok := v.(*gorm.DB); ok && tx != nil {
|
||||||
|
return tx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
|
||||||
|
type TxManager struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTxManager(db *gorm.DB) *TxManager { return &TxManager{db: db} }
|
||||||
|
|
||||||
|
// WithTransaction runs fn inside a DB transaction, injecting the *gorm.DB tx into ctx.
|
||||||
|
func (m *TxManager) WithTransaction(ctx context.Context, fn func(ctx context.Context) error) error {
|
||||||
|
return m.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
ctxTx := context.WithValue(ctx, txKey, tx)
|
||||||
|
return fn(ctxTx)
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -105,6 +105,7 @@ func (r *Router) Init() *gin.Engine {
|
|||||||
middleware.Recover(),
|
middleware.Recover(),
|
||||||
middleware.HTTPStatLogger(),
|
middleware.HTTPStatLogger(),
|
||||||
middleware.PopulateContext(),
|
middleware.PopulateContext(),
|
||||||
|
middleware.CORS(),
|
||||||
)
|
)
|
||||||
|
|
||||||
r.addAppRoutes(engine)
|
r.addAppRoutes(engine)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user