310 lines
11 KiB
Go
310 lines
11 KiB
Go
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
|
|
}
|