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 uint, items []*entity.PurchaseItem) error UpdatePricing(ctx context.Context, purchaseID uint, updates []PurchasePricingUpdate) error UpdateReceivingDetails(ctx context.Context, purchaseID uint, updates []PurchaseReceivingUpdate) error DeleteItems(ctx context.Context, purchaseID uint, itemIDs []uint) error NextPrNumber(ctx context.Context, tx *gorm.DB) (string, error) NextPoNumber(ctx context.Context, tx *gorm.DB) (string, error) BackfillProjectFlockKandang(ctx context.Context, purchaseID uint) error } type PurchaseRepositoryImpl struct { *repository.BaseRepositoryImpl[entity.Purchase] } func NewPurchaseRepository(db *gorm.DB) PurchaseRepository { return &PurchaseRepositoryImpl{ BaseRepositoryImpl: repository.NewBaseRepository[entity.Purchase](db), } } func (r *PurchaseRepositoryImpl) CreateWithItems(ctx context.Context, purchase *entity.Purchase, items []*entity.PurchaseItem) error { db := r.DB().WithContext(ctx) //ambil dari base repository 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) BackfillProjectFlockKandang(ctx context.Context, purchaseID uint) error { if purchaseID == 0 { return nil } query := ` WITH latest_pfk AS ( SELECT pfk.id, pfk.kandang_id FROM project_flock_kandangs pfk JOIN ( SELECT DISTINCT ON (approvable_id) approvable_id, step_name, action_at FROM approvals WHERE approvable_type = 'PROJECT_FLOCKS' ORDER BY approvable_id, action_at DESC ) latest_approval ON latest_approval.approvable_id = pfk.project_flock_id WHERE LOWER(latest_approval.step_name) = LOWER('Aktif') ) UPDATE purchase_items pi SET project_flock_kandang_id = lp.id FROM warehouses w JOIN latest_pfk lp ON lp.kandang_id = w.kandang_id WHERE pi.purchase_id = ? AND pi.project_flock_kandang_id IS NULL AND pi.warehouse_id = w.id; ` return r.DB().WithContext(ctx).Exec(query, purchaseID).Error } func (r *PurchaseRepositoryImpl) CreateItems(ctx context.Context, purchaseID uint, 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 } type PurchasePricingUpdate struct { ItemID uint ProductID *uint Price float64 TotalPrice float64 Quantity *float64 TotalQty *float64 } type PurchaseReceivingUpdate struct { ItemID uint 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 uint, updates []PurchasePricingUpdate, ) 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 } } return nil } func (r *PurchaseRepositoryImpl) UpdateReceivingDetails( ctx context.Context, purchaseID uint, 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) DeleteItems(ctx context.Context, purchaseID uint, itemIDs []uint) 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 }