From f6a7b5911ab662d86e5d8be0d529ab3569664219 Mon Sep 17 00:00:00 2001 From: Habib Fatkhul Rohman Date: Thu, 27 Nov 2025 12:04:20 +0700 Subject: [PATCH] feat: implement sequence management with CRUD operations and integrate into vendor and UOM services --- database/entities/sequence_entity.go | 21 ++++ database/migration.go | 2 + modules/mvendor/dto/vendor_dto.go | 4 +- modules/mvendor/service/vendor_service.go | 34 +++++-- modules/product/service/product_service.go | 60 +++++++++--- modules/sequence/dto/sequence_dto.go | 7 ++ .../repository/sequence_repository.go | 49 ++++++++++ modules/sequence/service/sequence_service.go | 95 +++++++++++++++++++ modules/uom/service/uom_service.go | 22 +++-- pkg/dto/response.go | 6 ++ pkg/utils/utils.go | 27 ++++++ providers/core.go | 11 ++- 12 files changed, 306 insertions(+), 32 deletions(-) create mode 100644 database/entities/sequence_entity.go create mode 100644 modules/sequence/dto/sequence_dto.go create mode 100644 modules/sequence/repository/sequence_repository.go create mode 100644 modules/sequence/service/sequence_service.go create mode 100644 pkg/utils/utils.go diff --git a/database/entities/sequence_entity.go b/database/entities/sequence_entity.go new file mode 100644 index 0000000..6dddb66 --- /dev/null +++ b/database/entities/sequence_entity.go @@ -0,0 +1,21 @@ +package entities + +import ( + "github.com/google/uuid" +) + +type SequenceEntity struct { + ID uuid.UUID `gorm:"type:uuid;default:uuid_generate_v4()" json:"id"` + ClientID string `gorm:"column:client_id;index:idx_client_entity"` + EntityType string `gorm:"column:entity_type;index:idx_client_entity"` + Period string `gorm:"column:period"` + CurrentSeq int `gorm:"column:current_seq"` + + Client M_Client `gorm:"foreignKey:ClientID;references:ID;"` + + Timestamp +} + +func (SequenceEntity) TableName() string { + return "sequences" +} diff --git a/database/migration.go b/database/migration.go index ea2d7c4..57697ef 100644 --- a/database/migration.go +++ b/database/migration.go @@ -45,6 +45,7 @@ func Migrate(db *gorm.DB) error { &entities.TInventoryMovementLineEntity{}, &entities.TInventoryQuarantineEntity{}, &entities.TInventoryQuarantineLineEntity{}, + &entities.SequenceEntity{}, ); err != nil { return err } @@ -93,6 +94,7 @@ func MigrateFresh(db *gorm.DB) error { // &entities.TInventoryMovementLineEntity{}, &entities.TInventoryQuarantineEntity{}, &entities.TInventoryQuarantineLineEntity{}, + &entities.SequenceEntity{}, ); err != nil { return err } diff --git a/modules/mvendor/dto/vendor_dto.go b/modules/mvendor/dto/vendor_dto.go index a68e11e..d417ca4 100644 --- a/modules/mvendor/dto/vendor_dto.go +++ b/modules/mvendor/dto/vendor_dto.go @@ -26,7 +26,7 @@ var ( ) type VendorCreateRequest struct { - SearchKey string `json:"search_key"` + // SearchKey string `json:"search_key"` Name string `json:"name" binding:"required"` Address string `json:"address"` ContactPerson string `json:"contact_person"` @@ -35,7 +35,7 @@ type VendorCreateRequest struct { } type VendorUpdateRequest struct { - SearchKey string `json:"search_key"` + // SearchKey string `json:"search_key"` Name string `json:"name"` Address string `json:"address"` ContactPerson string `json:"contact_person"` diff --git a/modules/mvendor/service/vendor_service.go b/modules/mvendor/service/vendor_service.go index 323938e..93bb536 100644 --- a/modules/mvendor/service/vendor_service.go +++ b/modules/mvendor/service/vendor_service.go @@ -7,7 +7,9 @@ import ( "github.com/Caknoooo/go-gin-clean-starter/modules/mvendor/dto" "github.com/Caknoooo/go-gin-clean-starter/modules/mvendor/query" "github.com/Caknoooo/go-gin-clean-starter/modules/mvendor/repository" + sequenceservice "github.com/Caknoooo/go-gin-clean-starter/modules/sequence/service" pkgdto "github.com/Caknoooo/go-gin-clean-starter/pkg/dto" + "github.com/Caknoooo/go-gin-clean-starter/pkg/utils" "github.com/google/uuid" "gorm.io/gorm" ) @@ -21,14 +23,16 @@ type VendorService interface { } type vendorService struct { - db *gorm.DB - vendorRepo repository.VendorRepository + db *gorm.DB + vendorRepo repository.VendorRepository + sequenceService sequenceservice.SequenceService } -func NewVendorService(vendorRepo repository.VendorRepository, db *gorm.DB) VendorService { +func NewVendorService(vendorRepo repository.VendorRepository, sequenceService sequenceservice.SequenceService, db *gorm.DB) VendorService { return &vendorService{ - vendorRepo: vendorRepo, - db: db, + vendorRepo: vendorRepo, + sequenceService: sequenceService, + db: db, } } @@ -39,8 +43,23 @@ func (s *vendorService) Create(ctx context.Context, req dto.VendorCreateRequest) tx.Rollback() } }() + + suffix := utils.GetInitials(req.Name) + + seqConfig := pkgdto.SequenceConfig{ + EntityType: "vendor", + Prefix: "VND", + Period: "", + } + + searchKey, err := s.sequenceService.GenerateNumberWithSuffix(ctx, req.ClientID, seqConfig, suffix) + if err != nil { + tx.Rollback() + return dto.VendorResponse{}, err + } + vendor := entities.MVendorEntity{ - SearchKey: req.SearchKey, + SearchKey: searchKey, Name: req.Name, Address: req.Address, ContactPerson: req.ContactPerson, @@ -93,9 +112,6 @@ func (s *vendorService) Update(ctx context.Context, req dto.VendorUpdateRequest, tx.Rollback() return dto.VendorResponse{}, err } - if req.SearchKey != "" { - vendor.SearchKey = req.SearchKey - } if req.Name != "" { vendor.Name = req.Name } diff --git a/modules/product/service/product_service.go b/modules/product/service/product_service.go index 509be15..2f4d99b 100644 --- a/modules/product/service/product_service.go +++ b/modules/product/service/product_service.go @@ -4,11 +4,14 @@ import ( "context" "github.com/Caknoooo/go-gin-clean-starter/database/entities" + categoryrepo "github.com/Caknoooo/go-gin-clean-starter/modules/category/repository" invstoragerepo "github.com/Caknoooo/go-gin-clean-starter/modules/inventory_storage/repository" invtransactionrepo "github.com/Caknoooo/go-gin-clean-starter/modules/inventory_transaction/repository" "github.com/Caknoooo/go-gin-clean-starter/modules/product/dto" "github.com/Caknoooo/go-gin-clean-starter/modules/product/query" "github.com/Caknoooo/go-gin-clean-starter/modules/product/repository" + sequenceservice "github.com/Caknoooo/go-gin-clean-starter/modules/sequence/service" + pkgdto "github.com/Caknoooo/go-gin-clean-starter/pkg/dto" "github.com/google/uuid" "gorm.io/gorm" ) @@ -31,6 +34,9 @@ type productService struct { productRepo repository.ProductRepository inventoryTransactionRepo invtransactionrepo.InventoryTransactionRepository inventoryStorageRepo invstoragerepo.InventoryStorageRepository + categoryRepo categoryrepo.CategoryRepository + sequenceService sequenceservice.SequenceService // tambahkan ini + } // GetCrossReferencesByProductAndClient implements ProductService. @@ -112,12 +118,15 @@ func (s *productService) RemoveCrossReference(ctx context.Context, productId str return nil } -func NewProductService(productRepo repository.ProductRepository, db *gorm.DB, inventoryTransactionRepo invtransactionrepo.InventoryTransactionRepository, inventoryStorageRepo invstoragerepo.InventoryStorageRepository) ProductService { +func NewProductService(productRepo repository.ProductRepository, db *gorm.DB, inventoryTransactionRepo invtransactionrepo.InventoryTransactionRepository, + inventoryStorageRepo invstoragerepo.InventoryStorageRepository, categoryRepo categoryrepo.CategoryRepository, sequenceService sequenceservice.SequenceService) ProductService { return &productService{ productRepo: productRepo, db: db, inventoryTransactionRepo: inventoryTransactionRepo, inventoryStorageRepo: inventoryStorageRepo, + categoryRepo: categoryRepo, + sequenceService: sequenceService, } } @@ -128,7 +137,40 @@ func (s *productService) Create(ctx context.Context, req dto.ProductCreateReques tx.Rollback() } }() - refNumber, err := entities.GenerateRefNumberProduct(tx, req.ClientID, *req.CategoryID) + + // UUID fields + clientUUID, err := uuid.Parse(req.ClientID) + if err != nil { + tx.Rollback() + return dto.ProductResponse{}, err + } + + uomUUID, err := uuid.Parse(*req.UomID) + if err != nil { + tx.Rollback() + return dto.ProductResponse{}, err + } + + categoryUUID, err := uuid.Parse(*req.CategoryID) + if err != nil { + tx.Rollback() + return dto.ProductResponse{}, err + } + + category, err := s.categoryRepo.GetById(ctx, tx, categoryUUID.String()) + if err != nil { + tx.Rollback() + return dto.ProductResponse{}, err + } + + // Gunakan sequenceService untuk generate nomor referensi + seqConfig := pkgdto.SequenceConfig{ + EntityType: "product", + Prefix: "PRD", + Period: "", + } + + refNumber, err := s.sequenceService.GenerateNumberWithPeriod(ctx, req.ClientID, seqConfig, category.SearchKey) if err != nil { tx.Rollback() return dto.ProductResponse{}, err @@ -140,6 +182,9 @@ func (s *productService) Create(ctx context.Context, req dto.ProductCreateReques Description: req.Description, Status: req.Status, IsReturnable: req.IsReturnable, + CategoryID: &categoryUUID, + UomID: &uomUUID, + ClientID: clientUUID, // DimLength: req.DimLength, // DimWidth: req.DimWidth, // DimHeight: req.DimHeight, @@ -157,16 +202,7 @@ func (s *productService) Create(ctx context.Context, req dto.ProductCreateReques // MultiplyRate: req.MultiplyRate, // DivideRate: req.DivideRate, } - // UUID fields - product.ClientID = parseUUID(req.ClientID) - if req.CategoryID != nil { - id := parseUUID(*req.CategoryID) - product.CategoryID = &id - } - if req.UomID != nil { - id := parseUUID(*req.UomID) - product.UomID = &id - } + // product.DimUomID = parseUUID(req.DimUomID) // product.WeightUomID = parseUUID(req.WeightUomID) // product.VolumeUomID = parseUUID(req.VolumeUomID) diff --git a/modules/sequence/dto/sequence_dto.go b/modules/sequence/dto/sequence_dto.go new file mode 100644 index 0000000..08e3038 --- /dev/null +++ b/modules/sequence/dto/sequence_dto.go @@ -0,0 +1,7 @@ +package dto + +type SequenceConfig struct { + EntityType string + Prefix string + Period string +} diff --git a/modules/sequence/repository/sequence_repository.go b/modules/sequence/repository/sequence_repository.go new file mode 100644 index 0000000..785e34a --- /dev/null +++ b/modules/sequence/repository/sequence_repository.go @@ -0,0 +1,49 @@ +package repository + +import ( + "context" + + "github.com/Caknoooo/go-gin-clean-starter/database/entities" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +type SequenceRepository interface { + GetOrCreateSequence(ctx context.Context, tx *gorm.DB, clientId, entityType, period string) (*entities.SequenceEntity, error) + UpdateSequence(ctx context.Context, tx *gorm.DB, seq *entities.SequenceEntity) error +} + +type sequenceRepository struct { + db *gorm.DB +} + +func NewSequenceRepository(db *gorm.DB) SequenceRepository { + return &sequenceRepository{db: db} +} + +func (r *sequenceRepository) GetOrCreateSequence(ctx context.Context, tx *gorm.DB, clientId, entityType, period string) (*entities.SequenceEntity, error) { + if tx == nil { + tx = r.db + } + var seq entities.SequenceEntity + err := tx.WithContext(ctx). + Clauses(clause.Locking{Strength: "UPDATE"}). + Where("client_id = ? AND entity_type = ? AND period = ?", clientId, entityType, period). + FirstOrCreate(&seq, entities.SequenceEntity{ + ClientID: clientId, + EntityType: entityType, + Period: period, + CurrentSeq: 0, + }).Error + if err != nil { + return nil, err + } + return &seq, nil +} + +func (r *sequenceRepository) UpdateSequence(ctx context.Context, tx *gorm.DB, seq *entities.SequenceEntity) error { + if tx == nil { + tx = r.db + } + return tx.WithContext(ctx).Model(seq).Update("current_seq", seq.CurrentSeq).Error +} diff --git a/modules/sequence/service/sequence_service.go b/modules/sequence/service/sequence_service.go new file mode 100644 index 0000000..e4ed93b --- /dev/null +++ b/modules/sequence/service/sequence_service.go @@ -0,0 +1,95 @@ +package service + +import ( + "context" + "fmt" + "time" + + "github.com/Caknoooo/go-gin-clean-starter/modules/sequence/repository" + pkgdto "github.com/Caknoooo/go-gin-clean-starter/pkg/dto" + "gorm.io/gorm" +) + +type SequenceService interface { + GenerateNumber(ctx context.Context, clientId string, config pkgdto.SequenceConfig) (string, error) + GenerateNumberWithPeriod(ctx context.Context, clientId string, config pkgdto.SequenceConfig, suffix string) (string, error) + GenerateNumberWithSuffix(ctx context.Context, clientId string, config pkgdto.SequenceConfig, suffix string) (string, error) + GetNextSequence(ctx context.Context, clientId string, config pkgdto.SequenceConfig) (int, error) +} + +type sequenceService struct { + db *gorm.DB + sequenceRepo repository.SequenceRepository +} + +func NewSequenceService(sequenceRepo repository.SequenceRepository, db *gorm.DB) SequenceService { + return &sequenceService{ + sequenceRepo: sequenceRepo, + db: db, + } +} + +// GenerateNumberWithSuffix implements SequenceService. +func (s *sequenceService) GenerateNumberWithSuffix(ctx context.Context, clientId string, config pkgdto.SequenceConfig, suffix string) (string, error) { + period := config.Period + seq, err := s.sequenceRepo.GetOrCreateSequence(ctx, s.db, clientId, config.EntityType, period) + if err != nil { + return "", err + } + seq.CurrentSeq++ + if err := s.sequenceRepo.UpdateSequence(ctx, s.db, seq); err != nil { + return "", err + } + refNum := fmt.Sprintf("%s-%s-%04d", config.Prefix, suffix, seq.CurrentSeq) + return refNum, nil +} + +func (s *sequenceService) GenerateNumber(ctx context.Context, clientId string, config pkgdto.SequenceConfig) (string, error) { + period := config.Period + if period == "" { + period = "-" + } + seq, err := s.sequenceRepo.GetOrCreateSequence(ctx, s.db, clientId, config.EntityType, period) + if err != nil { + return "", err + } + seq.CurrentSeq++ + if err := s.sequenceRepo.UpdateSequence(ctx, s.db, seq); err != nil { + return "", err + } + refNum := fmt.Sprintf("%s-%04d", config.Prefix, seq.CurrentSeq) + return refNum, nil +} + +func (s *sequenceService) GenerateNumberWithPeriod(ctx context.Context, clientId string, config pkgdto.SequenceConfig, suffix string) (string, error) { + if config.Period == "" { + config.Period = time.Now().Format("0601") + } + period := config.Period + seq, err := s.sequenceRepo.GetOrCreateSequence(ctx, s.db, clientId, config.EntityType, period) + if err != nil { + return "", err + } + seq.CurrentSeq++ + if err := s.sequenceRepo.UpdateSequence(ctx, s.db, seq); err != nil { + return "", err + } + refNum := fmt.Sprintf("%s-%s-%s-%04d", config.Prefix, suffix, period, seq.CurrentSeq) + return refNum, nil +} + +func (s *sequenceService) GetNextSequence(ctx context.Context, clientId string, config pkgdto.SequenceConfig) (int, error) { + period := config.Period + if period == "" { + period = "-" + } + seq, err := s.sequenceRepo.GetOrCreateSequence(ctx, s.db, clientId, config.EntityType, period) + if err != nil { + return 0, err + } + seq.CurrentSeq++ + if err := s.sequenceRepo.UpdateSequence(ctx, s.db, seq); err != nil { + return 0, err + } + return seq.CurrentSeq, nil +} diff --git a/modules/uom/service/uom_service.go b/modules/uom/service/uom_service.go index 3c281ac..edce14e 100644 --- a/modules/uom/service/uom_service.go +++ b/modules/uom/service/uom_service.go @@ -4,6 +4,7 @@ import ( "context" "github.com/Caknoooo/go-gin-clean-starter/database/entities" + sequenceservice "github.com/Caknoooo/go-gin-clean-starter/modules/sequence/service" dtodomain "github.com/Caknoooo/go-gin-clean-starter/modules/uom/dto" "github.com/Caknoooo/go-gin-clean-starter/modules/uom/query" "github.com/Caknoooo/go-gin-clean-starter/modules/uom/repository" @@ -21,14 +22,16 @@ type UomService interface { } type uomService struct { - db *gorm.DB - uomRepo repository.UomRepository + db *gorm.DB + uomRepo repository.UomRepository + sequenceService sequenceservice.SequenceService } -func NewUomService(uomRepo repository.UomRepository, db *gorm.DB) UomService { +func NewUomService(uomRepo repository.UomRepository, sequenceService sequenceservice.SequenceService, db *gorm.DB) UomService { return &uomService{ - uomRepo: uomRepo, - db: db, + uomRepo: uomRepo, + sequenceService: sequenceService, + db: db, } } @@ -39,7 +42,14 @@ func (s *uomService) Create(ctx context.Context, req dtodomain.UomCreateRequest) tx.Rollback() } }() - code, err := entities.GenerateCodeUom(s.db, req.ClientID) + + seqConfig := pkgdto.SequenceConfig{ + EntityType: "UOM", + Prefix: "UOM", + Period: "", + } + + code, err := s.sequenceService.GenerateNumber(ctx, req.ClientID, seqConfig) if err != nil { tx.Rollback() return dtodomain.UomResponse{}, err diff --git a/pkg/dto/response.go b/pkg/dto/response.go index ad91519..b492041 100644 --- a/pkg/dto/response.go +++ b/pkg/dto/response.go @@ -35,4 +35,10 @@ type ( ID string `json:"id"` DocumentNumber string `json:"document_number"` } + + SequenceConfig struct { + EntityType string + Prefix string + Period string + } ) diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go new file mode 100644 index 0000000..df76987 --- /dev/null +++ b/pkg/utils/utils.go @@ -0,0 +1,27 @@ +package utils + +import "strings" + +func GetInitials(name string) string { + name = strings.TrimSpace(name) + words := strings.Fields(name) + if len(words) == 0 { + return "" + } + if len(words) == 1 { + // Jika hanya satu kata, ambil 3 huruf awal (atau kurang jika <3) + initial := words[0] + if len(initial) > 3 { + initial = initial[:3] + } + return strings.ToUpper(initial) + } + // Jika lebih dari satu kata, ambil huruf pertama tiap kata + var initials string + for _, w := range words { + if len(w) > 0 { + initials += string(w[0]) + } + } + return strings.ToUpper(initials) +} diff --git a/providers/core.go b/providers/core.go index 7ebfe75..f1ad156 100644 --- a/providers/core.go +++ b/providers/core.go @@ -101,6 +101,9 @@ import ( quarantineRepo "github.com/Caknoooo/go-gin-clean-starter/modules/quarantine/repository" quarantineService "github.com/Caknoooo/go-gin-clean-starter/modules/quarantine/service" + sequenceRepo "github.com/Caknoooo/go-gin-clean-starter/modules/sequence/repository" + sequenceService "github.com/Caknoooo/go-gin-clean-starter/modules/sequence/service" + "github.com/Caknoooo/go-gin-clean-starter/modules/user/controller" "github.com/Caknoooo/go-gin-clean-starter/modules/user/repository" userService "github.com/Caknoooo/go-gin-clean-starter/modules/user/service" @@ -166,18 +169,20 @@ func RegisterDependencies(injector *do.Injector) { inventoryTransactionRepository := inventoryTransactionRepo.NewInventoryTransactionRepository(db) quarantineRepository := quarantineRepo.NewQuarantineRepository(db) quarantineLineRepository := quarantineLineRepo.NewQuarantineLineRepository(db) + sequenceRepository := sequenceRepo.NewSequenceRepository(db) // Service + sequenceServ := sequenceService.NewSequenceService(sequenceRepository, db) userServ := userService.NewUserService(userRepository, roleRepository, warehouseRepository, clientRepository, refreshTokenRepository, jwtService, db) - productService := productService.NewProductService(productRepository, db, inventoryTransactionRepository, inventoryStorageRepository) + productService := productService.NewProductService(productRepository, db, inventoryTransactionRepository, inventoryStorageRepository, categoryRepository, sequenceServ) roleServ := roleService.NewRoleService(roleRepository, refreshTokenRepository, jwtService, userServ, db) menuSvc := menuService.NewMenuService(menuRepository, jwtService, db) maintenanceGroupServ := maintGroupService.NewMaintenanceGroupService(maintenanceGroupRepository, maintenanceGroupRoleRepository, maintenanceGroupRoleUserRepository, db) clientServ := clientService.NewClientService(clientRepository, db) permissionsServ := permissionsService.NewPermissionsService(permissionsRepository, db) categoryServ := categoryService.NewCategoryService(categoryRepository, db) - uomServ := uomService.NewUomService(uomRepository, db) - mvendorServ := mvendorService.NewVendorService(mvendorRepository, db) + uomServ := uomService.NewUomService(uomRepository, sequenceServ, db) + mvendorServ := mvendorService.NewVendorService(mvendorRepository, sequenceServ, db) warehouseServ := warehouseService.NewWarehouseService(warehouseRepository, db) zonaServ := zonaService.NewZonaService(zonaRepository, db) aisleServ := aisleService.NewAisleService(aisleRepository, db)