220 lines
8.1 KiB
Go
220 lines
8.1 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"apskel-pos-be/internal/constants"
|
|
"apskel-pos-be/internal/entities"
|
|
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
// cashAdvanceSettledAmountExpr sums the spending charged to an advance straight from
|
|
// the purchase orders and expenses that point at it. Keeping it as an expression
|
|
// rather than a column means an advance can never disagree with the purchases behind it,
|
|
// whichever screen edited them. Cancelled spending never accounted for anything.
|
|
const cashAdvanceSettledAmountExpr = `(
|
|
COALESCE((SELECT SUM(po.total_amount) FROM purchase_orders po
|
|
WHERE po.cash_advance_id = cash_advances.id AND po.status <> 'cancelled'), 0)
|
|
+ COALESCE((SELECT SUM(e.total) FROM expenses e
|
|
WHERE e.cash_advance_id = cash_advances.id AND e.status <> 'cancel'), 0)
|
|
)`
|
|
|
|
// Money is stored to two decimals, so half a cent is the smallest gap that means
|
|
// anything. The filters use it for the same reason the mapper does.
|
|
const cashAdvanceAmountEpsilon = 0.005
|
|
|
|
type CashAdvanceRepositoryImpl struct {
|
|
db *gorm.DB
|
|
}
|
|
|
|
func NewCashAdvanceRepositoryImpl(db *gorm.DB) *CashAdvanceRepositoryImpl {
|
|
return &CashAdvanceRepositoryImpl{db: db}
|
|
}
|
|
|
|
func (r *CashAdvanceRepositoryImpl) Create(ctx context.Context, cashAdvance *entities.CashAdvance) error {
|
|
return r.db.WithContext(ctx).Create(cashAdvance).Error
|
|
}
|
|
|
|
func (r *CashAdvanceRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.CashAdvance, error) {
|
|
var cashAdvance entities.CashAdvance
|
|
err := r.db.WithContext(ctx).
|
|
Model(&entities.CashAdvance{}).
|
|
Select("cash_advances.*, "+cashAdvanceSettledAmountExpr+" AS settled_amount").
|
|
Preload("Outlet").
|
|
Preload("TeamCategory").
|
|
Where("cash_advances.id = ?", id).
|
|
First(&cashAdvance).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &cashAdvance, nil
|
|
}
|
|
|
|
func (r *CashAdvanceRepositoryImpl) GetByIDAndOrganizationID(ctx context.Context, id, organizationID uuid.UUID) (*entities.CashAdvance, error) {
|
|
var cashAdvance entities.CashAdvance
|
|
err := r.db.WithContext(ctx).
|
|
Model(&entities.CashAdvance{}).
|
|
Select("cash_advances.*, "+cashAdvanceSettledAmountExpr+" AS settled_amount").
|
|
Preload("Outlet").
|
|
Preload("TeamCategory").
|
|
Where("cash_advances.id = ? AND cash_advances.organization_id = ?", id, organizationID).
|
|
First(&cashAdvance).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &cashAdvance, nil
|
|
}
|
|
|
|
func (r *CashAdvanceRepositoryImpl) GetByCodeNumber(ctx context.Context, codeNumber string, organizationID uuid.UUID) (*entities.CashAdvance, error) {
|
|
var cashAdvance entities.CashAdvance
|
|
err := r.db.WithContext(ctx).
|
|
Where("code_number = ? AND organization_id = ?", codeNumber, organizationID).
|
|
First(&cashAdvance).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &cashAdvance, nil
|
|
}
|
|
|
|
func (r *CashAdvanceRepositoryImpl) Update(ctx context.Context, cashAdvance *entities.CashAdvance) error {
|
|
// Omit associations so a preloaded TeamCategory or Outlet is not written back
|
|
// over the row it came from.
|
|
return r.db.WithContext(ctx).Omit(clause.Associations).Save(cashAdvance).Error
|
|
}
|
|
|
|
func (r *CashAdvanceRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error {
|
|
return r.db.WithContext(ctx).Delete(&entities.CashAdvance{}, "id = ?", id).Error
|
|
}
|
|
|
|
func (r *CashAdvanceRepositoryImpl) List(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, limit, offset int) ([]*entities.CashAdvance, int64, error) {
|
|
var cashAdvances []*entities.CashAdvance
|
|
var total int64
|
|
|
|
// Count on its own query: the select list carries a correlated subquery, which
|
|
// GORM would otherwise drag into the COUNT.
|
|
countQuery := applyCashAdvanceFilters(r.db.WithContext(ctx).Model(&entities.CashAdvance{}).Where("cash_advances.organization_id = ?", organizationID), filters)
|
|
if err := countQuery.Count(&total).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
query := applyCashAdvanceFilters(r.db.WithContext(ctx).Model(&entities.CashAdvance{}).Where("cash_advances.organization_id = ?", organizationID), filters)
|
|
err := query.
|
|
Select("cash_advances.*, " + cashAdvanceSettledAmountExpr + " AS settled_amount").
|
|
Preload("Outlet").
|
|
Preload("TeamCategory").
|
|
Order("cash_advances.issued_date DESC, cash_advances.created_at DESC").
|
|
Limit(limit).
|
|
Offset(offset).
|
|
Find(&cashAdvances).Error
|
|
|
|
return cashAdvances, total, err
|
|
}
|
|
|
|
func applyCashAdvanceFilters(query *gorm.DB, filters map[string]interface{}) *gorm.DB {
|
|
for key, value := range filters {
|
|
switch key {
|
|
case "search":
|
|
if search, ok := value.(string); ok && search != "" {
|
|
pattern := "%" + strings.ToLower(search) + "%"
|
|
query = query.Where("LOWER(cash_advances.code_number) LIKE ? OR LOWER(cash_advances.description) LIKE ?", pattern, pattern)
|
|
}
|
|
case "status":
|
|
if status, ok := value.(string); ok && status != "" {
|
|
query = query.Where("cash_advances.status = ?", status)
|
|
}
|
|
case "outlet_id":
|
|
if outletID, ok := value.(uuid.UUID); ok {
|
|
query = query.Where("cash_advances.outlet_id = ?", outletID)
|
|
}
|
|
case "team_scope":
|
|
if teamScope, ok := value.(string); ok && teamScope != "" {
|
|
query = query.Where("cash_advances.team_scope = ?", teamScope)
|
|
}
|
|
case "team_category_id":
|
|
if teamCategoryID, ok := value.(uuid.UUID); ok {
|
|
query = query.Where("cash_advances.team_category_id = ?", teamCategoryID)
|
|
}
|
|
case "settlement_status":
|
|
query = applyCashAdvanceSettlementFilter(query, value)
|
|
case "start_date":
|
|
if startDate, ok := value.(time.Time); ok {
|
|
query = query.Where("cash_advances.issued_date >= ?", startDate)
|
|
}
|
|
case "end_date":
|
|
if endDate, ok := value.(time.Time); ok {
|
|
query = query.Where("cash_advances.issued_date <= ?", endDate)
|
|
}
|
|
}
|
|
}
|
|
|
|
return query
|
|
}
|
|
|
|
// applyCashAdvanceSettlementFilter reproduces in SQL what the mapper computes in Go:
|
|
// how much of the advance has been accounted for, by spending plus cash returned.
|
|
func applyCashAdvanceSettlementFilter(query *gorm.DB, value interface{}) *gorm.DB {
|
|
status, ok := value.(string)
|
|
if !ok || status == "" {
|
|
return query
|
|
}
|
|
|
|
accounted := cashAdvanceSettledAmountExpr + " + cash_advances.returned_amount"
|
|
|
|
switch status {
|
|
case constants.CashAdvanceSettlementOpen:
|
|
return query.Where(accounted+" <= ?", cashAdvanceAmountEpsilon)
|
|
case constants.CashAdvanceSettlementPartial:
|
|
return query.
|
|
Where(accounted+" > ?", cashAdvanceAmountEpsilon).
|
|
Where("cash_advances.amount - ("+accounted+") > ?", cashAdvanceAmountEpsilon)
|
|
case constants.CashAdvanceSettlementSettled:
|
|
return query.
|
|
Where(accounted+" > ?", cashAdvanceAmountEpsilon).
|
|
Where("cash_advances.amount - ("+accounted+") <= ?", cashAdvanceAmountEpsilon)
|
|
}
|
|
|
|
return query
|
|
}
|
|
|
|
// ListSettlements returns the spending charged to an advance, newest first. Purchase
|
|
// orders and expenses are two tables recording the same thing here, so they are
|
|
// read as one list.
|
|
func (r *CashAdvanceRepositoryImpl) ListSettlements(ctx context.Context, cashAdvanceID uuid.UUID) ([]*entities.CashAdvanceSettlement, error) {
|
|
query := fmt.Sprintf(`
|
|
SELECT '%s' AS type, po.id AS id, po.po_number AS number,
|
|
po.transaction_date AS date, po.total_amount AS amount, po.status AS status
|
|
FROM purchase_orders po
|
|
WHERE po.cash_advance_id = ?
|
|
UNION ALL
|
|
SELECT '%s' AS type, e.id AS id, e.code_number AS number,
|
|
e.transaction_date AS date, e.total AS amount, e.status AS status
|
|
FROM expenses e
|
|
WHERE e.cash_advance_id = ?
|
|
ORDER BY date DESC`,
|
|
constants.CashAdvanceSettlementTypePurchaseOrder,
|
|
constants.CashAdvanceSettlementTypeExpense,
|
|
)
|
|
|
|
var settlements []*entities.CashAdvanceSettlement
|
|
err := r.db.WithContext(ctx).Raw(query, cashAdvanceID, cashAdvanceID).Scan(&settlements).Error
|
|
return settlements, err
|
|
}
|
|
|
|
// CountSettlements is what stops an advance being deleted once spending has been
|
|
// charged to it; the foreign keys would refuse anyway, but not with a readable error.
|
|
func (r *CashAdvanceRepositoryImpl) CountSettlements(ctx context.Context, cashAdvanceID uuid.UUID) (int64, error) {
|
|
var count int64
|
|
err := r.db.WithContext(ctx).Raw(`
|
|
SELECT (SELECT COUNT(*) FROM purchase_orders WHERE cash_advance_id = ?)
|
|
+ (SELECT COUNT(*) FROM expenses WHERE cash_advance_id = ?)`,
|
|
cashAdvanceID, cashAdvanceID).Scan(&count).Error
|
|
return count, err
|
|
}
|