package repository import ( "context" "errors" "fmt" "time" "apskel-pos-be/internal/entities" "apskel-pos-be/internal/models" "github.com/google/uuid" "gorm.io/gorm" ) type PaymentRepository interface { Create(ctx context.Context, payment *entities.Payment) error GetByID(ctx context.Context, id uuid.UUID) (*entities.Payment, error) GetByOrderID(ctx context.Context, orderID uuid.UUID) ([]*entities.Payment, error) Update(ctx context.Context, payment *entities.Payment) error Delete(ctx context.Context, id 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 GetTotalPaidByOrderID(ctx context.Context, orderID uuid.UUID) (float64, error) } type PaymentRepositoryImpl struct { db *gorm.DB } func NewPaymentRepositoryImpl(db *gorm.DB) *PaymentRepositoryImpl { return &PaymentRepositoryImpl{ db: db, } } func (r *PaymentRepositoryImpl) Create(ctx context.Context, payment *entities.Payment) error { return r.db.WithContext(ctx).Create(payment).Error } func (r *PaymentRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.Payment, error) { var payment entities.Payment err := r.db.WithContext(ctx). Preload("PaymentMethod"). Preload("PaymentOrderItems"). First(&payment, "id = ?", id).Error if err != nil { return nil, err } return &payment, nil } func (r *PaymentRepositoryImpl) GetByOrderID(ctx context.Context, orderID uuid.UUID) ([]*entities.Payment, error) { var payments []*entities.Payment err := r.db.WithContext(ctx). Preload("PaymentMethod"). Preload("PaymentOrderItems"). Where("order_id = ?", orderID). Find(&payments).Error return payments, err } func (r *PaymentRepositoryImpl) Update(ctx context.Context, payment *entities.Payment) error { return r.db.WithContext(ctx).Save(payment).Error } func (r *PaymentRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error { return r.db.WithContext(ctx).Delete(&entities.Payment{}, "id = ?", id).Error } func (r *PaymentRepositoryImpl) RefundPayment(ctx context.Context, id uuid.UUID, refundAmount float64, reason string, refundedBy uuid.UUID) error { now := time.Now() return r.db.WithContext(ctx).Model(&entities.Payment{}). Where("id = ?", id). Updates(map[string]interface{}{ "refund_amount": refundAmount, "refund_reason": reason, "refunded_at": now, "refunded_by": refundedBy, "status": entities.PaymentTransactionStatusRefunded, }).Error } func (r *PaymentRepositoryImpl) UpdateStatus(ctx context.Context, id uuid.UUID, status entities.PaymentTransactionStatus) error { return r.db.WithContext(ctx).Model(&entities.Payment{}). Where("id = ?", id). Update("status", status).Error } func (r *PaymentRepositoryImpl) GetTotalPaidByOrderID(ctx context.Context, orderID uuid.UUID) (float64, error) { var total float64 err := r.db.WithContext(ctx).Model(&entities.Payment{}). Where("order_id = ? AND status = ?", orderID, entities.PaymentTransactionStatusCompleted). Select("COALESCE(SUM(amount), 0)"). Scan(&total).Error 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("payment_status", entities.PaymentStatusCompleted).Error; err != nil { return fmt.Errorf("failed to update order payment status: %w", err) } 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) } } else { if err := tx.Model(&entities.Order{}).Where("id = ?", req.OrderID).Update("payment_status", entities.PaymentStatusPartiallyRefunded).Error; err != nil { return fmt.Errorf("failed to update order payment 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 }