Files
lti-api/internal/modules/purchases/repositories/purchase.repository.go
T

424 lines
11 KiB
Go

package repositories
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"time"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type PurchaseRepository interface {
repository.BaseRepository[entity.Purchase]
CreateWithItems(ctx context.Context, purchase *entity.Purchase, items []*entity.PurchaseItem) error
CreateItems(ctx context.Context, purchaseID uint64, items []*entity.PurchaseItem) error
GetByIDWithRelations(ctx context.Context, id uint64) (*entity.Purchase, error)
GetAllWithFilters(ctx context.Context, offset, limit int, filter *PurchaseListFilter) ([]entity.Purchase, int64, error)
UpdatePricing(ctx context.Context, purchaseID uint64, updates []PurchasePricingUpdate, grandTotal float64) error
UpdateReceivingDetails(ctx context.Context, purchaseID uint64, updates []PurchaseReceivingUpdate) error
DeleteItems(ctx context.Context, purchaseID uint64, itemIDs []uint64) error
WithListRelations() func(*gorm.DB) *gorm.DB
UpdateGrandTotal(ctx context.Context, purchaseID uint64, grandTotal float64) error
NextPrNumber(ctx context.Context, tx *gorm.DB) (string, error)
NextPoNumber(ctx context.Context, tx *gorm.DB) (string, error)
}
type PurchaseRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Purchase]
}
func NewPurchaseRepository(db *gorm.DB) PurchaseRepository {
return &PurchaseRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Purchase](db),
}
}
type PurchaseListFilter struct {
SupplierID uint
Search string
PrNumber string
CreatedFrom *time.Time
CreatedTo *time.Time
Status *entity.ApprovalAction
CompletedOnly bool
}
func (r *PurchaseRepositoryImpl) CreateWithItems(ctx context.Context, purchase *entity.Purchase, items []*entity.PurchaseItem) error {
db := r.DB().WithContext(ctx)
if err := db.Create(purchase).Error; err != nil {
return err
}
if len(items) == 0 {
return nil
}
for _, item := range items {
item.PurchaseId = purchase.Id
}
if err := db.Create(&items).Error; err != nil {
return err
}
return nil
}
func (r *PurchaseRepositoryImpl) CreateItems(ctx context.Context, purchaseID uint64, items []*entity.PurchaseItem) error {
if len(items) == 0 {
return nil
}
for _, item := range items {
if item == nil {
continue
}
item.PurchaseId = purchaseID
}
return r.DB().WithContext(ctx).Create(&items).Error
}
func (r *PurchaseRepositoryImpl) GetByIDWithRelations(ctx context.Context, id uint64) (*entity.Purchase, error) {
var purchase entity.Purchase
err := r.DB().WithContext(ctx).
Scopes(r.withDetailRelations).
First(&purchase, id).Error
if err != nil {
return nil, err
}
return &purchase, nil
}
func (r *PurchaseRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, filter *PurchaseListFilter) ([]entity.Purchase, int64, error) {
return r.GetAll(ctx, offset, limit, func(db *gorm.DB) *gorm.DB {
db = r.withListRelations(db)
return r.applyListFilters(db, filter)
})
}
func (r *PurchaseRepositoryImpl) WithListRelations() func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return r.withListRelations(db)
}
}
func (r *PurchaseRepositoryImpl) withDetailRelations(db *gorm.DB) *gorm.DB {
return db.
Preload("Supplier").
Preload("Items", func(db *gorm.DB) *gorm.DB {
return db.Order("id ASC")
}).
Preload("Items.Product").
Preload("Items.Warehouse").
Preload("Items.Warehouse.Area").
Preload("Items.Warehouse.Location").
Preload("Items.ProductWarehouse")
}
func (r *PurchaseRepositoryImpl) WithDetailRelations() func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return r.withDetailRelations(db)
}
}
type PurchasePricingUpdate struct {
ItemID uint64
ProductID *uint64
Price float64
TotalPrice float64
Quantity *float64
TotalQty *float64
}
type PurchaseReceivingUpdate struct {
ItemID uint64
ReceivedDate *time.Time
TravelNumber *string
TravelDocumentPath *string
VehicleNumber *string
ReceivedQty *float64
WarehouseID *uint
ProductWarehouseID *uint
ClearProductWarehouse bool
}
func (r *PurchaseRepositoryImpl) UpdatePricing(
ctx context.Context,
purchaseID uint64,
updates []PurchasePricingUpdate,
grandTotal float64,
) error {
if len(updates) == 0 {
return errors.New("pricing updates cannot be empty")
}
db := r.DB().WithContext(ctx)
for _, upd := range updates {
data := map[string]interface{}{
"price": upd.Price,
"total_price": upd.TotalPrice,
}
if upd.ProductID != nil {
data["product_id"] = *upd.ProductID
}
if upd.Quantity != nil {
data["sub_qty"] = *upd.Quantity
}
if upd.TotalQty != nil {
data["total_qty"] = *upd.TotalQty
}
result := db.Model(&entity.PurchaseItem{}).
Where("purchase_id = ? AND id = ?", purchaseID, upd.ItemID).
Updates(data)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
}
if err := db.Model(&entity.Purchase{}).
Where("id = ?", purchaseID).
Updates(map[string]interface{}{
"grand_total": grandTotal,
"updated_at": gorm.Expr("NOW()"),
}).Error; err != nil {
return err
}
return nil
}
func (r *PurchaseRepositoryImpl) UpdateReceivingDetails(
ctx context.Context,
purchaseID uint64,
updates []PurchaseReceivingUpdate,
) error {
if len(updates) == 0 {
return errors.New("receiving updates cannot be empty")
}
db := r.DB().WithContext(ctx)
for _, upd := range updates {
data := map[string]interface{}{}
if upd.ReceivedDate != nil {
data["received_date"] = upd.ReceivedDate
}
if upd.TravelNumber != nil {
data["travel_number"] = upd.TravelNumber
}
if upd.TravelDocumentPath != nil {
data["travel_number_docs"] = upd.TravelDocumentPath
}
if upd.VehicleNumber != nil {
data["vehicle_number"] = upd.VehicleNumber
}
if upd.ReceivedQty != nil {
data["total_qty"] = upd.ReceivedQty
}
if upd.WarehouseID != nil && *upd.WarehouseID != 0 {
data["warehouse_id"] = upd.WarehouseID
}
if upd.ProductWarehouseID != nil {
data["product_warehouse_id"] = *upd.ProductWarehouseID
} else if upd.ClearProductWarehouse {
data["product_warehouse_id"] = gorm.Expr("NULL")
}
if len(data) == 0 {
continue
}
result := db.Model(&entity.PurchaseItem{}).
Where("purchase_id = ? AND id = ?", purchaseID, upd.ItemID).
Updates(data)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
}
return nil
}
func (r *PurchaseRepositoryImpl) UpdateGrandTotal(
ctx context.Context,
purchaseID uint64,
grandTotal float64,
) error {
return r.DB().WithContext(ctx).
Model(&entity.Purchase{}).
Where("id = ?", purchaseID).
Updates(map[string]interface{}{
"grand_total": grandTotal,
"updated_at": gorm.Expr("NOW()"),
}).Error
}
func (r *PurchaseRepositoryImpl) DeleteItems(ctx context.Context, purchaseID uint64, itemIDs []uint64) error {
if len(itemIDs) == 0 {
return errors.New("itemIDs cannot be empty")
}
return r.DB().WithContext(ctx).
Where("purchase_id = ? AND id IN ?", purchaseID, itemIDs).
Delete(&entity.PurchaseItem{}).Error
}
func (r *PurchaseRepositoryImpl) NextPrNumber(ctx context.Context, tx *gorm.DB) (string, error) {
return r.generateSequentialNumber(ctx, tx, "pr_number", utils.PurchasePRNumberPrefix, utils.PurchaseNumberPadding)
}
func (r *PurchaseRepositoryImpl) NextPoNumber(ctx context.Context, tx *gorm.DB) (string, error) {
return r.generateSequentialNumber(ctx, tx, "po_number", utils.PurchasePONumberPrefix, utils.PurchaseNumberPadding)
}
func (r *PurchaseRepositoryImpl) generateSequentialNumber(ctx context.Context, tx *gorm.DB, column, prefix string, padding int) (string, error) {
db := tx
if db == nil {
db = r.DB()
}
var values []string
err := db.WithContext(ctx).
Model(&entity.Purchase{}).
Where(fmt.Sprintf("%s LIKE ?", column), prefix+"%").
Select(column).
Order(fmt.Sprintf("%s DESC", column)).
Limit(20).
Clauses(clause.Locking{Strength: "UPDATE"}).
Pluck(column, &values).Error
if err != nil {
return "", err
}
next := 1
for _, value := range values {
if number, ok := parseNumericSuffix(value, prefix); ok {
next = number + 1
break
}
}
const maxAttempts = 20
for attempt := 0; attempt < maxAttempts; attempt++ {
candidate := fmt.Sprintf("%s%0*d", prefix, padding, next)
exists, err := r.numberExists(ctx, db, column, candidate)
if err != nil {
return "", err
}
if !exists {
return candidate, nil
}
next++
}
return "", fmt.Errorf("unable to generate unique %s", column)
}
func (r *PurchaseRepositoryImpl) numberExists(ctx context.Context, db *gorm.DB, column, value string) (bool, error) {
var count int64
if err := db.WithContext(ctx).
Model(&entity.Purchase{}).
Where(fmt.Sprintf("%s = ?", column), value).
Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
func parseNumericSuffix(value, prefix string) (int, bool) {
if !strings.HasPrefix(value, prefix) {
return 0, false
}
suffix := strings.TrimPrefix(value, prefix)
if suffix == "" {
return 0, false
}
trimmed := strings.TrimLeft(suffix, "0")
if trimmed == "" {
trimmed = "0"
}
number, err := strconv.Atoi(trimmed)
if err != nil {
return 0, false
}
return number, true
}
func (r *PurchaseRepositoryImpl) withListRelations(db *gorm.DB) *gorm.DB {
return db.Preload("Supplier")
}
func (r *PurchaseRepositoryImpl) applyListFilters(db *gorm.DB, filter *PurchaseListFilter) *gorm.DB {
if filter == nil {
return db
}
if filter.SupplierID > 0 {
db = db.Where("purchases.supplier_id = ?", filter.SupplierID)
}
if search := strings.ToLower(strings.TrimSpace(filter.Search)); search != "" {
like := "%" + search + "%"
db = db.Where("(LOWER(purchases.pr_number) LIKE ? OR LOWER(COALESCE(purchases.notes, '')) LIKE ?)", like, like)
}
if pr := strings.TrimSpace(filter.PrNumber); pr != "" {
db = db.Where("purchases.pr_number ILIKE ?", "%"+pr+"%")
}
if filter.CreatedFrom != nil {
db = db.Where("purchases.created_at >= ?", *filter.CreatedFrom)
}
if filter.CreatedTo != nil {
db = db.Where("purchases.created_at < ?", *filter.CreatedTo)
}
if filter.CompletedOnly {
step := uint16(utils.PurchaseStepCompleted)
db = r.applyLatestApprovalFilter(db, entity.ApprovalActionApproved, &step)
} else if filter.Status != nil {
db = r.applyLatestApprovalFilter(db, *filter.Status, nil)
}
return db.Order("purchases.created_at DESC").Order("purchases.id DESC")
}
func (r *PurchaseRepositoryImpl) applyLatestApprovalFilter(db *gorm.DB, action entity.ApprovalAction, minStep *uint16) *gorm.DB {
latestSub := r.DB().
Model(&entity.Approval{}).
Select("approvable_id, MAX(action_at) AS latest_action_at").
Where("approvable_type = ?", utils.ApprovalWorkflowPurchase.String()).
Group("approvable_id")
db = db.
Joins("LEFT JOIN (?) AS latest_purchase_approvals ON latest_purchase_approvals.approvable_id = purchases.id", latestSub).
Joins(
"LEFT JOIN approvals ON approvals.approvable_id = purchases.id AND approvals.approvable_type = ? AND approvals.action_at = latest_purchase_approvals.latest_action_at",
utils.ApprovalWorkflowPurchase.String(),
).
Where("approvals.action = ?", string(action))
if minStep != nil {
db = db.Where("approvals.step_number >= ?", *minStep)
}
return db
}