mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-23 06:45:43 +00:00
424 lines
11 KiB
Go
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
|
|
}
|