Files
apskel-pos-backend/internal/repository/point_payment_repository.go
T

179 lines
6.4 KiB
Go
Raw Normal View History

package repository
import (
"context"
"errors"
"fmt"
"github.com/google/uuid"
"gorm.io/gorm"
"apskel-pos-be/internal/constants"
"apskel-pos-be/internal/entities"
)
// ErrPointPaymentOrderNotFound means the order does not exist.
var ErrPointPaymentOrderNotFound = errors.New("point payment: order not found")
// PointPaymentOrder is what paying with EnakPoint needs to know about an order.
type PointPaymentOrder struct {
ID uuid.UUID
OrganizationID uuid.UUID
OutletID uuid.UUID
OrderNumber string
OutletName string
CustomerID *uuid.UUID
TotalAmount float64
PaymentStatus string
IsVoid bool
CustomerIsDefault *bool
CustomerIsActive *bool
}
// PointPaymentRepository reads and writes what paying an order with EnakPoint touches
// (docs/prd-point-coin.md F9). Unlike the order and payment repositories, every method
// joins the caller's transaction, since the payment row, the balance and the order
// must change together.
type PointPaymentRepository interface {
// GetOrder reads the order; with lock it also locks the order row for the rest of
// the transaction, so two payments of the same order queue up.
GetOrder(ctx context.Context, orderID uuid.UUID, lock bool) (*PointPaymentOrder, error)
// GetPaymentMethod returns a method's organization and type.
GetPaymentMethod(ctx context.Context, methodID uuid.UUID) (organizationID uuid.UUID, methodType string, err error)
// TotalPaid sums the order's completed payments, as the rest of the order flow does.
TotalPaid(ctx context.Context, orderID uuid.UUID) (float64, error)
// PaidWithPoints sums the rupiah of the order's completed EnakPoint payments.
PaidWithPoints(ctx context.Context, orderID uuid.UUID) (float64, error)
InsertPayment(ctx context.Context, payment *entities.Payment) error
// UpdateOrderAfterPayment stores what is left to pay and marks the order paid when
// nothing is.
UpdateOrderAfterPayment(ctx context.Context, orderID uuid.UUID, remaining float64, completed bool) error
}
type pointPaymentRepository struct {
db *gorm.DB
}
func NewPointPaymentRepository(db *gorm.DB) PointPaymentRepository {
return &pointPaymentRepository{db: db}
}
func (r *pointPaymentRepository) GetOrder(ctx context.Context, orderID uuid.UUID, lock bool) (*PointPaymentOrder, error) {
lockClause := ""
if lock {
lockClause = "FOR UPDATE OF o"
}
var rows []struct {
ID string
OrganizationID string
OutletID string
OrderNumber string
OutletName string
CustomerID *string
TotalAmount float64
PaymentStatus string
IsVoid bool
CustomerIsDefault *bool
CustomerIsActive *bool
}
err := DBFromContext(ctx, r.db).WithContext(ctx).Raw(`
SELECT o.id::text AS id, o.organization_id::text AS organization_id, o.outlet_id::text AS outlet_id,
o.order_number, COALESCE(ou.name, '') AS outlet_name, o.customer_id::text AS customer_id,
o.total_amount, o.payment_status, COALESCE(o.is_void, false) AS is_void,
c.is_default AS customer_is_default, c.is_active AS customer_is_active
FROM orders o
LEFT JOIN outlets ou ON ou.id = o.outlet_id
LEFT JOIN customers c ON c.id = o.customer_id
WHERE o.id = ?
`+lockClause, orderID).Scan(&rows).Error
if err != nil {
return nil, fmt.Errorf("failed to read order: %w", err)
}
if len(rows) == 0 {
return nil, ErrPointPaymentOrderNotFound
}
row := rows[0]
order := &PointPaymentOrder{
OrderNumber: row.OrderNumber,
OutletName: row.OutletName,
TotalAmount: row.TotalAmount,
PaymentStatus: row.PaymentStatus,
IsVoid: row.IsVoid,
CustomerIsDefault: row.CustomerIsDefault,
CustomerIsActive: row.CustomerIsActive,
}
order.ID, _ = uuid.Parse(row.ID)
order.OrganizationID, _ = uuid.Parse(row.OrganizationID)
order.OutletID, _ = uuid.Parse(row.OutletID)
if row.CustomerID != nil {
if id, err := uuid.Parse(*row.CustomerID); err == nil {
order.CustomerID = &id
}
}
return order, nil
}
func (r *pointPaymentRepository) GetPaymentMethod(ctx context.Context, methodID uuid.UUID) (uuid.UUID, string, error) {
var rows []struct {
OrganizationID string
Type string
}
err := DBFromContext(ctx, r.db).WithContext(ctx).Raw(`
SELECT organization_id::text AS organization_id, type FROM payment_methods WHERE id = ?`, methodID).Scan(&rows).Error
if err != nil {
return uuid.Nil, "", fmt.Errorf("failed to read payment method: %w", err)
}
if len(rows) == 0 {
return uuid.Nil, "", fmt.Errorf("payment method not found")
}
org, _ := uuid.Parse(rows[0].OrganizationID)
return org, rows[0].Type, nil
}
func (r *pointPaymentRepository) TotalPaid(ctx context.Context, orderID uuid.UUID) (float64, error) {
var total float64
err := DBFromContext(ctx, r.db).WithContext(ctx).Raw(`
SELECT COALESCE(SUM(amount), 0) FROM payments WHERE order_id = ? AND status = ?`,
orderID, entities.PaymentTransactionStatusCompleted).Scan(&total).Error
if err != nil {
return 0, fmt.Errorf("failed to sum payments: %w", err)
}
return total, nil
}
func (r *pointPaymentRepository) PaidWithPoints(ctx context.Context, orderID uuid.UUID) (float64, error) {
var total float64
err := DBFromContext(ctx, r.db).WithContext(ctx).Raw(`
SELECT COALESCE(SUM(p.amount), 0) FROM payments p
JOIN payment_methods pm ON pm.id = p.payment_method_id
WHERE p.order_id = ? AND p.status = ? AND pm.type = ?`,
orderID, entities.PaymentTransactionStatusCompleted, constants.PaymentMethodTypePoint).Scan(&total).Error
if err != nil {
return 0, fmt.Errorf("failed to sum EnakPoint payments: %w", err)
}
return total, nil
}
func (r *pointPaymentRepository) InsertPayment(ctx context.Context, payment *entities.Payment) error {
if err := DBFromContext(ctx, r.db).WithContext(ctx).Create(payment).Error; err != nil {
return fmt.Errorf("failed to create payment: %w", err)
}
return nil
}
func (r *pointPaymentRepository) UpdateOrderAfterPayment(ctx context.Context, orderID uuid.UUID, remaining float64, completed bool) error {
paymentStatus := entities.PaymentStatusPartial
if completed {
paymentStatus = entities.PaymentStatusCompleted
}
err := DBFromContext(ctx, r.db).WithContext(ctx).Exec(`
UPDATE orders SET remaining_amount = ?, payment_status = ?,
status = CASE WHEN ? THEN ? ELSE status END, updated_at = NOW()
WHERE id = ?`,
remaining, paymentStatus, completed, entities.OrderStatusCompleted, orderID).Error
if err != nil {
return fmt.Errorf("failed to update order after payment: %w", err)
}
return nil
}