feat: implement sequence management with CRUD operations and integrate into vendor and UOM services
Deploy Application / deploy (push) Successful in 20s Details

This commit is contained in:
Habib Fatkhul Rohman 2025-11-27 12:04:20 +07:00
parent fbb73a77d0
commit f6a7b5911a
12 changed files with 306 additions and 32 deletions

View File

@ -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"
}

View File

@ -45,6 +45,7 @@ func Migrate(db *gorm.DB) error {
&entities.TInventoryMovementLineEntity{}, &entities.TInventoryMovementLineEntity{},
&entities.TInventoryQuarantineEntity{}, &entities.TInventoryQuarantineEntity{},
&entities.TInventoryQuarantineLineEntity{}, &entities.TInventoryQuarantineLineEntity{},
&entities.SequenceEntity{},
); err != nil { ); err != nil {
return err return err
} }
@ -93,6 +94,7 @@ func MigrateFresh(db *gorm.DB) error {
// &entities.TInventoryMovementLineEntity{}, // &entities.TInventoryMovementLineEntity{},
&entities.TInventoryQuarantineEntity{}, &entities.TInventoryQuarantineEntity{},
&entities.TInventoryQuarantineLineEntity{}, &entities.TInventoryQuarantineLineEntity{},
&entities.SequenceEntity{},
); err != nil { ); err != nil {
return err return err
} }

View File

@ -26,7 +26,7 @@ var (
) )
type VendorCreateRequest struct { type VendorCreateRequest struct {
SearchKey string `json:"search_key"` // SearchKey string `json:"search_key"`
Name string `json:"name" binding:"required"` Name string `json:"name" binding:"required"`
Address string `json:"address"` Address string `json:"address"`
ContactPerson string `json:"contact_person"` ContactPerson string `json:"contact_person"`
@ -35,7 +35,7 @@ type VendorCreateRequest struct {
} }
type VendorUpdateRequest struct { type VendorUpdateRequest struct {
SearchKey string `json:"search_key"` // SearchKey string `json:"search_key"`
Name string `json:"name"` Name string `json:"name"`
Address string `json:"address"` Address string `json:"address"`
ContactPerson string `json:"contact_person"` ContactPerson string `json:"contact_person"`

View File

@ -7,7 +7,9 @@ import (
"github.com/Caknoooo/go-gin-clean-starter/modules/mvendor/dto" "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/query"
"github.com/Caknoooo/go-gin-clean-starter/modules/mvendor/repository" "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" pkgdto "github.com/Caknoooo/go-gin-clean-starter/pkg/dto"
"github.com/Caknoooo/go-gin-clean-starter/pkg/utils"
"github.com/google/uuid" "github.com/google/uuid"
"gorm.io/gorm" "gorm.io/gorm"
) )
@ -21,14 +23,16 @@ type VendorService interface {
} }
type vendorService struct { type vendorService struct {
db *gorm.DB db *gorm.DB
vendorRepo repository.VendorRepository 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{ return &vendorService{
vendorRepo: vendorRepo, vendorRepo: vendorRepo,
db: db, sequenceService: sequenceService,
db: db,
} }
} }
@ -39,8 +43,23 @@ func (s *vendorService) Create(ctx context.Context, req dto.VendorCreateRequest)
tx.Rollback() 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{ vendor := entities.MVendorEntity{
SearchKey: req.SearchKey, SearchKey: searchKey,
Name: req.Name, Name: req.Name,
Address: req.Address, Address: req.Address,
ContactPerson: req.ContactPerson, ContactPerson: req.ContactPerson,
@ -93,9 +112,6 @@ func (s *vendorService) Update(ctx context.Context, req dto.VendorUpdateRequest,
tx.Rollback() tx.Rollback()
return dto.VendorResponse{}, err return dto.VendorResponse{}, err
} }
if req.SearchKey != "" {
vendor.SearchKey = req.SearchKey
}
if req.Name != "" { if req.Name != "" {
vendor.Name = req.Name vendor.Name = req.Name
} }

View File

@ -4,11 +4,14 @@ import (
"context" "context"
"github.com/Caknoooo/go-gin-clean-starter/database/entities" "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" invstoragerepo "github.com/Caknoooo/go-gin-clean-starter/modules/inventory_storage/repository"
invtransactionrepo "github.com/Caknoooo/go-gin-clean-starter/modules/inventory_transaction/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/dto"
"github.com/Caknoooo/go-gin-clean-starter/modules/product/query" "github.com/Caknoooo/go-gin-clean-starter/modules/product/query"
"github.com/Caknoooo/go-gin-clean-starter/modules/product/repository" "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" "github.com/google/uuid"
"gorm.io/gorm" "gorm.io/gorm"
) )
@ -31,6 +34,9 @@ type productService struct {
productRepo repository.ProductRepository productRepo repository.ProductRepository
inventoryTransactionRepo invtransactionrepo.InventoryTransactionRepository inventoryTransactionRepo invtransactionrepo.InventoryTransactionRepository
inventoryStorageRepo invstoragerepo.InventoryStorageRepository inventoryStorageRepo invstoragerepo.InventoryStorageRepository
categoryRepo categoryrepo.CategoryRepository
sequenceService sequenceservice.SequenceService // tambahkan ini
} }
// GetCrossReferencesByProductAndClient implements ProductService. // GetCrossReferencesByProductAndClient implements ProductService.
@ -112,12 +118,15 @@ func (s *productService) RemoveCrossReference(ctx context.Context, productId str
return nil 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{ return &productService{
productRepo: productRepo, productRepo: productRepo,
db: db, db: db,
inventoryTransactionRepo: inventoryTransactionRepo, inventoryTransactionRepo: inventoryTransactionRepo,
inventoryStorageRepo: inventoryStorageRepo, inventoryStorageRepo: inventoryStorageRepo,
categoryRepo: categoryRepo,
sequenceService: sequenceService,
} }
} }
@ -128,7 +137,40 @@ func (s *productService) Create(ctx context.Context, req dto.ProductCreateReques
tx.Rollback() 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 { if err != nil {
tx.Rollback() tx.Rollback()
return dto.ProductResponse{}, err return dto.ProductResponse{}, err
@ -140,6 +182,9 @@ func (s *productService) Create(ctx context.Context, req dto.ProductCreateReques
Description: req.Description, Description: req.Description,
Status: req.Status, Status: req.Status,
IsReturnable: req.IsReturnable, IsReturnable: req.IsReturnable,
CategoryID: &categoryUUID,
UomID: &uomUUID,
ClientID: clientUUID,
// DimLength: req.DimLength, // DimLength: req.DimLength,
// DimWidth: req.DimWidth, // DimWidth: req.DimWidth,
// DimHeight: req.DimHeight, // DimHeight: req.DimHeight,
@ -157,16 +202,7 @@ func (s *productService) Create(ctx context.Context, req dto.ProductCreateReques
// MultiplyRate: req.MultiplyRate, // MultiplyRate: req.MultiplyRate,
// DivideRate: req.DivideRate, // 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.DimUomID = parseUUID(req.DimUomID)
// product.WeightUomID = parseUUID(req.WeightUomID) // product.WeightUomID = parseUUID(req.WeightUomID)
// product.VolumeUomID = parseUUID(req.VolumeUomID) // product.VolumeUomID = parseUUID(req.VolumeUomID)

View File

@ -0,0 +1,7 @@
package dto
type SequenceConfig struct {
EntityType string
Prefix string
Period string
}

View File

@ -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
}

View File

@ -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
}

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"github.com/Caknoooo/go-gin-clean-starter/database/entities" "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" 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/query"
"github.com/Caknoooo/go-gin-clean-starter/modules/uom/repository" "github.com/Caknoooo/go-gin-clean-starter/modules/uom/repository"
@ -21,14 +22,16 @@ type UomService interface {
} }
type uomService struct { type uomService struct {
db *gorm.DB db *gorm.DB
uomRepo repository.UomRepository 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{ return &uomService{
uomRepo: uomRepo, uomRepo: uomRepo,
db: db, sequenceService: sequenceService,
db: db,
} }
} }
@ -39,7 +42,14 @@ func (s *uomService) Create(ctx context.Context, req dtodomain.UomCreateRequest)
tx.Rollback() 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 { if err != nil {
tx.Rollback() tx.Rollback()
return dtodomain.UomResponse{}, err return dtodomain.UomResponse{}, err

View File

@ -35,4 +35,10 @@ type (
ID string `json:"id"` ID string `json:"id"`
DocumentNumber string `json:"document_number"` DocumentNumber string `json:"document_number"`
} }
SequenceConfig struct {
EntityType string
Prefix string
Period string
}
) )

27
pkg/utils/utils.go Normal file
View File

@ -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)
}

View File

@ -101,6 +101,9 @@ import (
quarantineRepo "github.com/Caknoooo/go-gin-clean-starter/modules/quarantine/repository" quarantineRepo "github.com/Caknoooo/go-gin-clean-starter/modules/quarantine/repository"
quarantineService "github.com/Caknoooo/go-gin-clean-starter/modules/quarantine/service" 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/controller"
"github.com/Caknoooo/go-gin-clean-starter/modules/user/repository" "github.com/Caknoooo/go-gin-clean-starter/modules/user/repository"
userService "github.com/Caknoooo/go-gin-clean-starter/modules/user/service" userService "github.com/Caknoooo/go-gin-clean-starter/modules/user/service"
@ -166,18 +169,20 @@ func RegisterDependencies(injector *do.Injector) {
inventoryTransactionRepository := inventoryTransactionRepo.NewInventoryTransactionRepository(db) inventoryTransactionRepository := inventoryTransactionRepo.NewInventoryTransactionRepository(db)
quarantineRepository := quarantineRepo.NewQuarantineRepository(db) quarantineRepository := quarantineRepo.NewQuarantineRepository(db)
quarantineLineRepository := quarantineLineRepo.NewQuarantineLineRepository(db) quarantineLineRepository := quarantineLineRepo.NewQuarantineLineRepository(db)
sequenceRepository := sequenceRepo.NewSequenceRepository(db)
// Service // Service
sequenceServ := sequenceService.NewSequenceService(sequenceRepository, db)
userServ := userService.NewUserService(userRepository, roleRepository, warehouseRepository, clientRepository, refreshTokenRepository, jwtService, 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) roleServ := roleService.NewRoleService(roleRepository, refreshTokenRepository, jwtService, userServ, db)
menuSvc := menuService.NewMenuService(menuRepository, jwtService, db) menuSvc := menuService.NewMenuService(menuRepository, jwtService, db)
maintenanceGroupServ := maintGroupService.NewMaintenanceGroupService(maintenanceGroupRepository, maintenanceGroupRoleRepository, maintenanceGroupRoleUserRepository, db) maintenanceGroupServ := maintGroupService.NewMaintenanceGroupService(maintenanceGroupRepository, maintenanceGroupRoleRepository, maintenanceGroupRoleUserRepository, db)
clientServ := clientService.NewClientService(clientRepository, db) clientServ := clientService.NewClientService(clientRepository, db)
permissionsServ := permissionsService.NewPermissionsService(permissionsRepository, db) permissionsServ := permissionsService.NewPermissionsService(permissionsRepository, db)
categoryServ := categoryService.NewCategoryService(categoryRepository, db) categoryServ := categoryService.NewCategoryService(categoryRepository, db)
uomServ := uomService.NewUomService(uomRepository, db) uomServ := uomService.NewUomService(uomRepository, sequenceServ, db)
mvendorServ := mvendorService.NewVendorService(mvendorRepository, db) mvendorServ := mvendorService.NewVendorService(mvendorRepository, sequenceServ, db)
warehouseServ := warehouseService.NewWarehouseService(warehouseRepository, db) warehouseServ := warehouseService.NewWarehouseService(warehouseRepository, db)
zonaServ := zonaService.NewZonaService(zonaRepository, db) zonaServ := zonaService.NewZonaService(zonaRepository, db)
aisleServ := aisleService.NewAisleService(aisleRepository, db) aisleServ := aisleService.NewAisleService(aisleRepository, db)