feat: add quarantine module with CRUD operations
Deploy Application / deploy (push) Successful in 43s Details

- Implemented TInventoryQuarantineEntity and TInventoryQuarantineLineEntity for database representation.
- Created quarantine service, repository, and controller for handling business logic and HTTP requests.
- Added DTOs for quarantine requests and responses.
- Implemented query filters for retrieving quarantines.
- Registered quarantine routes in the main application.
- Integrated quarantine functionality into the core application dependencies.
This commit is contained in:
Habib Fatkhul Rohman 2025-11-26 16:03:24 +07:00
parent b2de10c414
commit fbb73a77d0
12 changed files with 899 additions and 36 deletions

View File

@ -27,6 +27,7 @@ import (
"github.com/Caknoooo/go-gin-clean-starter/modules/mvendor"
"github.com/Caknoooo/go-gin-clean-starter/modules/permissions"
"github.com/Caknoooo/go-gin-clean-starter/modules/product"
"github.com/Caknoooo/go-gin-clean-starter/modules/quarantine"
"github.com/Caknoooo/go-gin-clean-starter/modules/role"
"github.com/Caknoooo/go-gin-clean-starter/modules/uom"
"github.com/Caknoooo/go-gin-clean-starter/modules/warehouse"
@ -177,6 +178,7 @@ func main() {
inventorymovement.RegisterRoutes(server, injector)
inventorystorage.RegisterRoutes(server, injector)
inventorytransaction.RegisterRoutes(server, injector)
quarantine.RegisterRoutes(server, injector)
// register swagger route
server.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))

View File

@ -0,0 +1,74 @@
package entities
import (
"fmt"
"strings"
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
type TInventoryQuarantineEntity struct {
ID uuid.UUID `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()" json:"id"`
DocumentNumber string `gorm:"type:varchar(100);" json:"document_number"`
DocumentDate time.Time `gorm:"type:timestamp;" json:"document_date"`
Status string `gorm:"type:varchar(50);default:'draft'" json:"status"`
WarehouseID uuid.UUID `gorm:"type:uuid;index;" json:"warehouse_id"`
ZonaID uuid.UUID `gorm:"type:uuid;index;" json:"zona_id"`
ClientID uuid.UUID `gorm:"type:uuid;index;" json:"client_id"`
Warehouse MWarehouseEntity `gorm:"foreignKey:WarehouseID;references:ID"`
Zona MZonaEntity `gorm:"foreignKey:ZonaID;references:ID"`
Client M_Client `gorm:"foreignKey:ClientID;references:ID"`
QuarantineLines []TInventoryQuarantineLineEntity `gorm:"foreignKey:QuarantineID;references:ID"`
Assignment TAssignmentEntity `gorm:"-"`
FullAuditTrail
}
func (TInventoryQuarantineEntity) TableName() string {
return "t_inventory_quarantines"
}
// GenerateDocumentNumber generates a new document number for a client
func GenerateDocumentNumberInQuarantine(db *gorm.DB, clientId string) (string, error) {
prefix := "QRTN"
// Ambil nama client berdasarkan clientId
var client struct {
Name string
}
if err := db.Table("m_clients").Select("name").Where("id = ?", clientId).First(&client).Error; err != nil {
return "", fmt.Errorf("client not found")
}
if client.Name == "" {
return "", fmt.Errorf("client name is empty")
}
words := strings.Fields(client.Name)
initials := ""
for _, w := range words {
if len(w) > 0 {
initials += strings.ToUpper(string(w[0]))
}
}
// Cari document number terakhir untuk client ini
var lastReceipt TInventoryIssueEntity
err := db.
Where("client_id = ?", clientId).
Order("document_number DESC").
First(&lastReceipt).Error
seq := 1
if err == nil && lastReceipt.DocumentNumber != "" {
parts := strings.Split(lastReceipt.DocumentNumber, "-")
if len(parts) == 3 {
fmt.Sscanf(parts[2], "%d", &seq)
seq++
}
}
docNum := fmt.Sprintf("%s-%s-%04d", prefix, initials, seq)
return docNum, nil
}

View File

@ -0,0 +1,25 @@
package entities
import (
"github.com/google/uuid"
)
type TInventoryQuarantineLineEntity struct {
ID uuid.UUID `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()" json:"id"`
QuarantineID uuid.UUID `gorm:"type:uuid;index;" json:"quarantine_id"`
StorageID uuid.UUID `gorm:"type:uuid;index;" json:"storage_id"`
ProductID uuid.UUID `gorm:"type:uuid;index;" json:"product_id"`
ClientID uuid.UUID `gorm:"type:uuid;index;" json:"client_id"`
Product MProductEntity `gorm:"foreignKey:ProductID;references:ID"`
Storage InventoryStorageEntity `gorm:"foreignKey:StorageID;references:ID"`
Quarantine TInventoryQuarantineEntity `gorm:"foreignKey:QuarantineID;references:ID"`
Client M_Client `gorm:"foreignKey:ClientID;references:ID"`
FullAuditTrail
}
func (TInventoryQuarantineLineEntity) TableName() string {
return "t_inventory_quarantine_lines"
}

View File

@ -43,6 +43,8 @@ func Migrate(db *gorm.DB) error {
&entities.InventoryStorageEntity{},
&entities.TInventoryMovementEntity{},
&entities.TInventoryMovementLineEntity{},
&entities.TInventoryQuarantineEntity{},
&entities.TInventoryQuarantineLineEntity{},
); err != nil {
return err
}
@ -53,42 +55,44 @@ func Migrate(db *gorm.DB) error {
func MigrateFresh(db *gorm.DB) error {
// Drop tables
if err := db.Migrator().DropTable(
// &entities.M_Client{},
// &entities.M_User{},
// &entities.RefreshToken{},
// &entities.M_Menu{},
// &entities.M_Role{},
// &entities.M_Permissions{},
// &entities.M_User_Role{},
// &entities.M_Role_Menu{},
// &entities.M_Menu_Client{},
// &entities.M_Role_Permission{},
// &entities.M_MaintenanceGroup{},
// &entities.M_MaintenanceGroupRole{},
// &entities.M_MaintenanceGroupRoleUser{},
// &entities.MCategoryEntity{},
// &entities.MProductEntity{},
// &entities.MUomEntity{},
// &entities.MVendorEntity{},
// &entities.MCrossReferenceEntity{},
// &entities.MWarehouseEntity{},
// &entities.MZonaEntity{},
// &entities.MAisleEntity{},
// &entities.MUserWarehouseEntity{},
// &entities.TAssignmentEntity{},
// &entities.TAssignmentUserEntity{},
// &entities.TInventoryReceiptEntity{},
// &entities.TInventoryReceiptLineEntity{},
// &entities.TInventoryRequestEntity{},
// &entities.TInventoryRequestLineEntity{},
// &entities.TInventoryIssueEntity{},
// &entities.TInventoryIssueLineEntity{},
// &entities.TInventoryReturnEntity{},
// &entities.TInventoryReturnLineEntity{},
// &entities.InventoryTransactionEntity{},
// &entities.InventoryStorageEntity{},
// &entities.TInventoryMovementEntity{},
// &entities.TInventoryMovementLineEntity{},
// &entities.M_Client{},
// &entities.M_User{},
// &entities.RefreshToken{},
// &entities.M_Menu{},
// &entities.M_Role{},
// &entities.M_Permissions{},
// &entities.M_User_Role{},
// &entities.M_Role_Menu{},
// &entities.M_Menu_Client{},
// &entities.M_Role_Permission{},
// &entities.M_MaintenanceGroup{},
// &entities.M_MaintenanceGroupRole{},
// &entities.M_MaintenanceGroupRoleUser{},
// &entities.MCategoryEntity{},
// &entities.MProductEntity{},
// &entities.MUomEntity{},
// &entities.MVendorEntity{},
// &entities.MCrossReferenceEntity{},
// &entities.MWarehouseEntity{},
// &entities.MZonaEntity{},
// &entities.MAisleEntity{},
// &entities.MUserWarehouseEntity{},
// &entities.TAssignmentEntity{},
// &entities.TAssignmentUserEntity{},
// &entities.TInventoryReceiptEntity{},
// &entities.TInventoryReceiptLineEntity{},
// &entities.TInventoryRequestEntity{},
// &entities.TInventoryRequestLineEntity{},
// &entities.TInventoryIssueEntity{},
// &entities.TInventoryIssueLineEntity{},
// &entities.TInventoryReturnEntity{},
// &entities.TInventoryReturnLineEntity{},
// &entities.InventoryTransactionEntity{},
// &entities.InventoryStorageEntity{},
// &entities.TInventoryMovementEntity{},
// &entities.TInventoryMovementLineEntity{},
&entities.TInventoryQuarantineEntity{},
&entities.TInventoryQuarantineLineEntity{},
); err != nil {
return err
}

View File

@ -0,0 +1,189 @@
package controller
import (
"github.com/Caknoooo/go-gin-clean-starter/modules/quarantine/dto"
"github.com/Caknoooo/go-gin-clean-starter/modules/quarantine/query"
"github.com/Caknoooo/go-gin-clean-starter/modules/quarantine/service"
"github.com/Caknoooo/go-gin-clean-starter/pkg/constants"
"github.com/Caknoooo/go-gin-clean-starter/pkg/utils"
"github.com/gin-gonic/gin"
"github.com/samber/do"
"gorm.io/gorm"
)
type QuarantineController interface {
Create(ctx *gin.Context)
Update(ctx *gin.Context)
Delete(ctx *gin.Context)
GetById(ctx *gin.Context)
GetAll(ctx *gin.Context)
CreateLine(ctx *gin.Context)
UpdateLine(ctx *gin.Context)
DeleteLine(ctx *gin.Context)
OnComplete(ctx *gin.Context)
}
type quarantineController struct {
quarantineService service.QuarantineService
db *gorm.DB
}
func NewQuarantineController(i *do.Injector, quarantineService service.QuarantineService) QuarantineController {
return &quarantineController{
quarantineService: quarantineService,
db: do.MustInvokeNamed[*gorm.DB](i, constants.DB),
}
}
func (c *quarantineController) Create(ctx *gin.Context) {
var req dto.QuarantineCreateRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_DATA_FROM_BODY, err.Error(), nil)
ctx.JSON(400, res)
return
}
created, err := c.quarantineService.Create(ctx, req)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_CREATE_QUARANTINE, err.Error(), nil)
ctx.JSON(500, res)
return
}
res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_CREATE_QUARANTINE, created)
ctx.JSON(200, res)
}
func (c *quarantineController) GetById(ctx *gin.Context) {
id := ctx.Param("id")
quarantine, err := c.quarantineService.GetById(ctx, id)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_QUARANTINE, err.Error(), nil)
ctx.JSON(404, res)
return
}
res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_GET_QUARANTINE, quarantine)
ctx.JSON(200, res)
}
func (c *quarantineController) GetAll(ctx *gin.Context) {
clientId := ctx.DefaultQuery("client_id", "")
var filter query.QuarantineFilter
filter.ClientID = clientId
if err := ctx.ShouldBindQuery(&filter); err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_QUARANTINE, err.Error(), nil)
ctx.JSON(400, res)
return
}
getAll := ctx.Query("get_all")
if getAll != "" {
quarantines, _, err := c.quarantineService.GetAll(ctx, filter)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_QUARANTINE, err.Error(), nil)
ctx.JSON(400, res)
return
}
response := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_GET_QUARANTINE, quarantines)
ctx.JSON(200, response)
return
}
perPage := utils.ParseInt(ctx.DefaultQuery("per_page", "10"))
page := utils.ParseInt(ctx.DefaultQuery("page", "1"))
filter.PerPage = perPage
filter.Page = (page - 1) * perPage
quarantines, total, err := c.quarantineService.GetAll(ctx, filter)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_QUARANTINE, err.Error(), nil)
ctx.JSON(500, res)
return
}
paginationResponse := utils.BuildPaginationResponse(perPage, page, total)
res := utils.BuildResponseSuccessWithPagination(200, dto.MESSAGE_SUCCESS_GET_QUARANTINE, quarantines, paginationResponse)
ctx.JSON(200, res)
}
func (c *quarantineController) Update(ctx *gin.Context) {
id := ctx.Param("id")
var req dto.QuarantineUpdateRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_DATA_FROM_BODY, err.Error(), nil)
ctx.JSON(400, res)
return
}
updated, err := c.quarantineService.Update(ctx, req, id)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_UPDATE_QUARANTINE, err.Error(), nil)
ctx.JSON(500, res)
return
}
res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_UPDATE_QUARANTINE, updated)
ctx.JSON(200, res)
}
func (c *quarantineController) Delete(ctx *gin.Context) {
id := ctx.Param("id")
if err := c.quarantineService.Delete(ctx, id); err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_DELETE_QUARANTINE, err.Error(), nil)
ctx.JSON(500, res)
return
}
res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_DELETE_QUARANTINE, nil)
ctx.JSON(200, res)
}
func (c *quarantineController) CreateLine(ctx *gin.Context) {
id := ctx.Param("id")
var req dto.QuarantineLineCreateRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_DATA_FROM_BODY, err.Error(), nil)
ctx.JSON(400, res)
return
}
created, err := c.quarantineService.CreateLine(ctx, id, req)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_CREATE_QUARANTINE_LINE, err.Error(), nil)
ctx.JSON(500, res)
return
}
res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_CREATE_QUARANTINE_LINE, created)
ctx.JSON(200, res)
}
func (c *quarantineController) UpdateLine(ctx *gin.Context) {
id := ctx.Param("id")
var req dto.QuarantineLineUpdateRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_DATA_FROM_BODY, err.Error(), nil)
ctx.JSON(400, res)
return
}
updated, err := c.quarantineService.UpdateLine(ctx, id, req)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_UPDATE_QUARANTINE_LINE, err.Error(), nil)
ctx.JSON(500, res)
return
}
res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_UPDATE_QUARANTINE_LINE, updated)
ctx.JSON(200, res)
}
func (c *quarantineController) DeleteLine(ctx *gin.Context) {
id := ctx.Param("id")
if err := c.quarantineService.DeleteLine(ctx, id); err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_DELETE_QUARANTINE_LINE, err.Error(), nil)
ctx.JSON(500, res)
return
}
res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_DELETE_QUARANTINE_LINE, nil)
ctx.JSON(200, res)
}
func (c *quarantineController) OnComplete(ctx *gin.Context) {
id := ctx.Param("id")
updated, err := c.quarantineService.OnComplete(ctx, id)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_COMPLETE_QUARANTINE, err.Error(), nil)
ctx.JSON(500, res)
return
}
res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_COMPLETE_QUARANTINE, updated)
ctx.JSON(200, res)
}

View File

@ -0,0 +1,87 @@
package dto
const (
MESSAGE_FAILED_CREATE_QUARANTINE = "failed create quarantine"
MESSAGE_FAILED_CREATE_QUARANTINE_LINE = "failed create quarantine line"
MESSAGE_SUCCESS_CREATE_QUARANTINE = "success create quarantine"
MESSAGE_SUCCESS_CREATE_QUARANTINE_LINE = "success create quarantine line"
MESSAGE_FAILED_GET_QUARANTINE = "failed get quarantine"
MESSAGE_SUCCESS_GET_QUARANTINE = "success get quarantine"
MESSAGE_FAILED_UPDATE_QUARANTINE = "failed update quarantine"
MESSAGE_FAILED_UPDATE_QUARANTINE_LINE = "failed update quarantine line"
MESSAGE_SUCCESS_UPDATE_QUARANTINE = "success update quarantine"
MESSAGE_SUCCESS_UPDATE_QUARANTINE_LINE = "success update quarantine line"
MESSAGE_FAILED_DELETE_QUARANTINE = "failed delete quarantine"
MESSAGE_FAILED_DELETE_QUARANTINE_LINE = "failed delete quarantine line"
MESSAGE_SUCCESS_DELETE_QUARANTINE = "success delete quarantine"
MESSAGE_SUCCESS_DELETE_QUARANTINE_LINE = "success delete quarantine line"
MESSAGE_FAILED_GET_DATA_FROM_BODY = "failed get data from body"
MESSAGE_SUCCESS_COMPLETE_QUARANTINE = "success complete quarantine"
MESSAGE_FAILED_COMPLETE_QUARANTINE = "failed complete quarantine"
)
type QuarantineCreateRequest struct {
ReferenceNumber string `json:"reference_number"`
DocumentDate string `json:"document_date"`
Source string `json:"source"`
QrCodeFile string `json:"qr_code_file"`
ClientID string `json:"client_id" binding:"required"`
Status string `json:"status"`
QuarantineLines []QuarantineLineCreateRequest `json:"quarantine_lines,omitempty" binding:"dive"`
}
type QuarantineLineCreateRequest struct {
Quantity float64 `json:"quantity"`
BatchNumber string `json:"batch_number"`
RepackingSuggestion string `json:"repacking_suggestion"`
RepackUomID string `json:"repack_uom_id"`
RepackUomCode string `json:"repack_uom_code"`
ProductID string `json:"product_id"`
ProductCode string `json:"product_code"`
ClientID string `json:"client_id"`
}
type QuarantineUpdateRequest struct {
ReferenceNumber string `json:"reference_number"`
DocumentDate string `json:"document_date"`
Source string `json:"source"`
QrCodeFile string `json:"qr_code_file"`
Status string `json:"status"`
}
type QuarantineLineUpdateRequest struct {
Quantity float64 `json:"quantity"`
BatchNumber string `json:"batch_number"`
RepackingSuggestion string `json:"repacking_suggestion"`
RepackUomID string `json:"repack_uom_id"`
RepackUomCode string `json:"repack_uom_code"`
ProductID string `json:"product_id"`
ProductCode string `json:"product_code"`
ClientID string `json:"client_id"`
}
type QuarantineResponse struct {
ID string `json:"id"`
DocumentNumber string `json:"document_number"`
DocumentDate string `json:"document_date"`
Status string `json:"status"`
WarehouseID string `json:"warehouse_id"`
ZonaID string `json:"zona_id"`
ClientID string `json:"client_id"`
Warehouse string `json:"warehouse"`
Zona string `json:"zona"`
Client string `json:"client"`
QuarantineLines []QuarantineLineResponse `json:"quarantine_lines,omitempty"`
}
type QuarantineLineResponse struct {
ID string `json:"id"`
QuarantineID string `json:"quarantine_id"`
StorageID string `json:"storage_id"`
ProductID string `json:"product_id"`
ClientID string `json:"client_id"`
Product string `json:"product"`
Storage string `json:"storage"`
Quarantine string `json:"quarantine"`
Client string `json:"client"`
}

View File

@ -0,0 +1,22 @@
package query
import (
"gorm.io/gorm"
)
type QuarantineFilter struct {
ClientID string `form:"client_id"`
Source string `form:"source"`
PerPage int `form:"per_page"`
Page int `form:"page"`
}
func ApplyQuarantineFilters(db *gorm.DB, filter QuarantineFilter) *gorm.DB {
if filter.ClientID != "" {
db = db.Where("client_id = ?", filter.ClientID)
}
if filter.Source != "" {
db = db.Where("source ILIKE ?", "%"+filter.Source+"%")
}
return db
}

View File

@ -0,0 +1,89 @@
package repository
import (
"context"
"github.com/Caknoooo/go-gin-clean-starter/database/entities"
"gorm.io/gorm"
)
type QuarantineLineRepository interface {
Create(ctx context.Context, tx *gorm.DB, line entities.TInventoryQuarantineLineEntity) (entities.TInventoryQuarantineLineEntity, error)
GetById(ctx context.Context, tx *gorm.DB, id string) (entities.TInventoryQuarantineLineEntity, error)
GetAllByQuarantineId(ctx context.Context, quarantineId string) ([]entities.TInventoryQuarantineLineEntity, error)
Update(ctx context.Context, tx *gorm.DB, line entities.TInventoryQuarantineLineEntity) (entities.TInventoryQuarantineLineEntity, error)
Delete(ctx context.Context, tx *gorm.DB, id string) error
BulkCreate(ctx context.Context, tx *gorm.DB, lines []entities.TInventoryQuarantineLineEntity) error
DeleteByQuarantineId(ctx context.Context, tx *gorm.DB, quarantineId string) error
}
type quarantineLineRepository struct {
db *gorm.DB
}
func NewQuarantineLineRepository(db *gorm.DB) QuarantineLineRepository {
return &quarantineLineRepository{db: db}
}
func (r *quarantineLineRepository) Create(ctx context.Context, tx *gorm.DB, line entities.TInventoryQuarantineLineEntity) (entities.TInventoryQuarantineLineEntity, error) {
if tx == nil {
tx = r.db
}
if err := tx.WithContext(ctx).Create(&line).Error; err != nil {
return line, err
}
return line, nil
}
func (r *quarantineLineRepository) GetById(ctx context.Context, tx *gorm.DB, id string) (entities.TInventoryQuarantineLineEntity, error) {
if tx == nil {
tx = r.db
}
var line entities.TInventoryQuarantineLineEntity
if err := tx.WithContext(ctx).Preload("Product").Preload("Storage").Preload("Quarantine").Preload("Client").First(&line, "id = ?", id).Error; err != nil {
return line, err
}
return line, nil
}
func (r *quarantineLineRepository) GetAllByQuarantineId(ctx context.Context, quarantineId string) ([]entities.TInventoryQuarantineLineEntity, error) {
var lines []entities.TInventoryQuarantineLineEntity
if err := r.db.WithContext(ctx).Where("quarantine_id = ?", quarantineId).Preload("Product").Preload("Storage").Preload("Quarantine").Preload("Client").Find(&lines).Error; err != nil {
return lines, err
}
return lines, nil
}
func (r *quarantineLineRepository) Update(ctx context.Context, tx *gorm.DB, line entities.TInventoryQuarantineLineEntity) (entities.TInventoryQuarantineLineEntity, error) {
if tx == nil {
tx = r.db
}
if err := tx.WithContext(ctx).Model(&entities.TInventoryQuarantineLineEntity{}).Where("id = ?", line.ID).Select("*").Updates(&line).Error; err != nil {
return line, err
}
return line, nil
}
func (r *quarantineLineRepository) Delete(ctx context.Context, tx *gorm.DB, id string) error {
if tx == nil {
tx = r.db
}
if err := tx.WithContext(ctx).Delete(&entities.TInventoryQuarantineLineEntity{}, "id = ?", id).Error; err != nil {
return err
}
return nil
}
func (r *quarantineLineRepository) BulkCreate(ctx context.Context, tx *gorm.DB, lines []entities.TInventoryQuarantineLineEntity) error {
if tx == nil {
tx = r.db
}
return tx.WithContext(ctx).Create(&lines).Error
}
func (r *quarantineLineRepository) DeleteByQuarantineId(ctx context.Context, tx *gorm.DB, quarantineId string) error {
if tx == nil {
tx = r.db
}
return tx.WithContext(ctx).Where("quarantine_id = ?", quarantineId).Delete(&entities.TInventoryQuarantineLineEntity{}).Error
}

View File

@ -0,0 +1,94 @@
package repository
import (
"context"
"github.com/Caknoooo/go-gin-clean-starter/database/entities"
"github.com/Caknoooo/go-gin-clean-starter/modules/quarantine/query"
"gorm.io/gorm"
)
type QuarantineRepository interface {
Create(ctx context.Context, tx *gorm.DB, quarantine entities.TInventoryQuarantineEntity) (entities.TInventoryQuarantineEntity, error)
GetById(ctx context.Context, tx *gorm.DB, id string) (entities.TInventoryQuarantineEntity, error)
GetAll(ctx context.Context, filter query.QuarantineFilter) ([]entities.TInventoryQuarantineEntity, int64, error)
Update(ctx context.Context, tx *gorm.DB, quarantine entities.TInventoryQuarantineEntity) (entities.TInventoryQuarantineEntity, error)
Delete(ctx context.Context, tx *gorm.DB, id string) error
}
type quarantineRepository struct {
db *gorm.DB
}
func NewQuarantineRepository(db *gorm.DB) QuarantineRepository {
return &quarantineRepository{db: db}
}
func (r *quarantineRepository) Create(ctx context.Context, tx *gorm.DB, quarantine entities.TInventoryQuarantineEntity) (entities.TInventoryQuarantineEntity, error) {
if tx == nil {
tx = r.db
}
if err := tx.WithContext(ctx).Create(&quarantine).Error; err != nil {
return quarantine, err
}
return quarantine, nil
}
func (r *quarantineRepository) GetById(ctx context.Context, tx *gorm.DB, id string) (entities.TInventoryQuarantineEntity, error) {
if tx == nil {
tx = r.db
}
var quarantine entities.TInventoryQuarantineEntity
if err := tx.WithContext(ctx).
Preload("Client").
Preload("QuarantineLines").
Preload("QuarantineLines.Product").
Preload("QuarantineLines.Storage").
First(&quarantine, "id = ?", id).Error; err != nil {
return quarantine, err
}
// Assignment manual jika ada
var assignment entities.TAssignmentEntity
if err := tx.WithContext(ctx).
Preload("AssignmentUsers").
Preload("AssignmentUsers.User").
Preload("AssignmentUsers.Role").
First(&assignment, "document_id = ? AND document_type = ?", quarantine.ID, "Quarantine").Error; err == nil {
quarantine.Assignment = assignment
}
return quarantine, nil
}
func (r *quarantineRepository) GetAll(ctx context.Context, filter query.QuarantineFilter) ([]entities.TInventoryQuarantineEntity, int64, error) {
var quarantines []entities.TInventoryQuarantineEntity
var total int64
db := query.ApplyQuarantineFilters(r.db, filter)
db.Model(&entities.TInventoryQuarantineEntity{}).Count(&total)
if filter.PerPage > 0 && filter.Page > 0 {
db = db.Offset((filter.Page - 1) * filter.PerPage).Limit(filter.PerPage)
}
if err := db.Preload("Client").Find(&quarantines).Error; err != nil {
return quarantines, total, err
}
return quarantines, total, nil
}
func (r *quarantineRepository) Update(ctx context.Context, tx *gorm.DB, quarantine entities.TInventoryQuarantineEntity) (entities.TInventoryQuarantineEntity, error) {
if tx == nil {
tx = r.db
}
if err := tx.WithContext(ctx).Model(&entities.TInventoryQuarantineEntity{}).Where("id = ?", quarantine.ID).Select("*").Updates(&quarantine).Error; err != nil {
return quarantine, err
}
return quarantine, nil
}
func (r *quarantineRepository) Delete(ctx context.Context, tx *gorm.DB, id string) error {
if tx == nil {
tx = r.db
}
if err := tx.WithContext(ctx).Delete(&entities.TInventoryQuarantineEntity{}, "id = ?", id).Error; err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,28 @@
package quarantine
import (
"github.com/Caknoooo/go-gin-clean-starter/middlewares"
"github.com/Caknoooo/go-gin-clean-starter/modules/auth/service"
"github.com/Caknoooo/go-gin-clean-starter/modules/quarantine/controller"
"github.com/Caknoooo/go-gin-clean-starter/pkg/constants"
"github.com/gin-gonic/gin"
"github.com/samber/do"
)
func RegisterRoutes(server *gin.Engine, injector *do.Injector) {
quarantineController := do.MustInvoke[controller.QuarantineController](injector)
jwtService := do.MustInvokeNamed[service.JWTService](injector, constants.JWTService)
quarantineRoutes := server.Group("/api/v1/quarantines")
{
quarantineRoutes.POST("", middlewares.Authenticate(jwtService), quarantineController.Create)
quarantineRoutes.GET(":id", middlewares.Authenticate(jwtService), quarantineController.GetById)
quarantineRoutes.PUT(":id", middlewares.Authenticate(jwtService), quarantineController.Update)
quarantineRoutes.DELETE(":id", middlewares.Authenticate(jwtService), quarantineController.Delete)
quarantineRoutes.GET("", middlewares.Authenticate(jwtService), quarantineController.GetAll)
quarantineRoutes.POST(":id/lines", middlewares.Authenticate(jwtService), quarantineController.CreateLine)
quarantineRoutes.PUT("lines/:id", middlewares.Authenticate(jwtService), quarantineController.UpdateLine)
quarantineRoutes.DELETE("lines/:id", middlewares.Authenticate(jwtService), quarantineController.DeleteLine)
quarantineRoutes.POST(":id/complete", middlewares.Authenticate(jwtService), quarantineController.OnComplete)
}
}

View File

@ -0,0 +1,236 @@
package service
import (
"context"
"time"
"github.com/Caknoooo/go-gin-clean-starter/database/entities"
invstoragerepository "github.com/Caknoooo/go-gin-clean-starter/modules/inventory_storage/repository"
productrepository "github.com/Caknoooo/go-gin-clean-starter/modules/product/repository"
dtodomain "github.com/Caknoooo/go-gin-clean-starter/modules/quarantine/dto"
"github.com/Caknoooo/go-gin-clean-starter/modules/quarantine/query"
"github.com/Caknoooo/go-gin-clean-starter/modules/quarantine/repository"
uomrepository "github.com/Caknoooo/go-gin-clean-starter/modules/uom/repository"
"github.com/google/uuid"
"gorm.io/gorm"
)
type QuarantineService interface {
Create(ctx context.Context, req dtodomain.QuarantineCreateRequest) (dtodomain.QuarantineResponse, error)
GetById(ctx context.Context, id string) (dtodomain.QuarantineResponse, error)
GetAll(ctx context.Context, filter query.QuarantineFilter) ([]dtodomain.QuarantineResponse, int64, error)
Update(ctx context.Context, req dtodomain.QuarantineUpdateRequest, id string) (dtodomain.QuarantineResponse, error)
Delete(ctx context.Context, id string) error
CreateLine(ctx context.Context, quarantineId string, req dtodomain.QuarantineLineCreateRequest) (dtodomain.QuarantineLineResponse, error)
UpdateLine(ctx context.Context, lineId string, req dtodomain.QuarantineLineUpdateRequest) (dtodomain.QuarantineLineResponse, error)
DeleteLine(ctx context.Context, lineId string) error
OnComplete(ctx context.Context, id string) (dtodomain.QuarantineResponse, error)
}
type quarantineService struct {
db *gorm.DB
quarantineRepo repository.QuarantineRepository
quarantineLineRepo repository.QuarantineLineRepository
productRepo productrepository.ProductRepository
}
func NewQuarantineService(db *gorm.DB, quarantineRepo repository.QuarantineRepository, quarantineLineRepo repository.QuarantineLineRepository, productRepo productrepository.ProductRepository, uomRepo uomrepository.UomRepository, invStorageRepository invstoragerepository.InventoryStorageRepository) QuarantineService {
return &quarantineService{
db: db,
quarantineRepo: quarantineRepo,
quarantineLineRepo: quarantineLineRepo,
productRepo: productRepo,
}
}
func (s *quarantineService) Create(ctx context.Context, req dtodomain.QuarantineCreateRequest) (dtodomain.QuarantineResponse, error) {
tx := s.db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
entity := entities.TInventoryQuarantineEntity{
DocumentNumber: req.ReferenceNumber,
DocumentDate: parseDate(req.DocumentDate),
Status: req.Status,
ClientID: parseUUID(req.ClientID),
}
created, err := s.quarantineRepo.Create(ctx, tx, entity)
if err != nil {
tx.Rollback()
return dtodomain.QuarantineResponse{}, err
}
var lines []entities.TInventoryQuarantineLineEntity
for _, lineReq := range req.QuarantineLines {
line := entities.TInventoryQuarantineLineEntity{
QuarantineID: created.ID,
ProductID: parseUUID(lineReq.ProductID),
ClientID: parseUUID(lineReq.ClientID),
}
lines = append(lines, line)
}
if len(lines) > 0 {
err = s.quarantineLineRepo.BulkCreate(ctx, tx, lines)
if err != nil {
tx.Rollback()
return dtodomain.QuarantineResponse{}, err
}
}
tx.Commit()
return mapQuarantineToResponse(created, lines), nil
}
func (s *quarantineService) GetById(ctx context.Context, id string) (dtodomain.QuarantineResponse, error) {
entity, err := s.quarantineRepo.GetById(ctx, nil, id)
if err != nil {
return dtodomain.QuarantineResponse{}, err
}
lines, _ := s.quarantineLineRepo.GetAllByQuarantineId(ctx, id)
return mapQuarantineToResponse(entity, lines), nil
}
func (s *quarantineService) GetAll(ctx context.Context, filter query.QuarantineFilter) ([]dtodomain.QuarantineResponse, int64, error) {
entities, total, err := s.quarantineRepo.GetAll(ctx, filter)
if err != nil {
return nil, 0, err
}
var responses []dtodomain.QuarantineResponse
for _, entity := range entities {
lines, _ := s.quarantineLineRepo.GetAllByQuarantineId(ctx, entity.ID.String())
responses = append(responses, mapQuarantineToResponse(entity, lines))
}
return responses, total, nil
}
func (s *quarantineService) Update(ctx context.Context, req dtodomain.QuarantineUpdateRequest, id string) (dtodomain.QuarantineResponse, error) {
tx := s.db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
entity, err := s.quarantineRepo.GetById(ctx, tx, id)
if err != nil {
tx.Rollback()
return dtodomain.QuarantineResponse{}, err
}
entity.DocumentNumber = req.ReferenceNumber
entity.DocumentDate = parseDate(req.DocumentDate)
entity.Status = req.Status
updated, err := s.quarantineRepo.Update(ctx, tx, entity)
if err != nil {
tx.Rollback()
return dtodomain.QuarantineResponse{}, err
}
tx.Commit()
lines, _ := s.quarantineLineRepo.GetAllByQuarantineId(ctx, id)
return mapQuarantineToResponse(updated, lines), nil
}
func (s *quarantineService) Delete(ctx context.Context, id string) error {
tx := s.db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
if err := s.quarantineLineRepo.DeleteByQuarantineId(ctx, tx, id); err != nil {
tx.Rollback()
return err
}
if err := s.quarantineRepo.Delete(ctx, tx, id); err != nil {
tx.Rollback()
return err
}
tx.Commit()
return nil
}
func (s *quarantineService) CreateLine(ctx context.Context, quarantineId string, req dtodomain.QuarantineLineCreateRequest) (dtodomain.QuarantineLineResponse, error) {
line := entities.TInventoryQuarantineLineEntity{
QuarantineID: parseUUID(quarantineId),
ProductID: parseUUID(req.ProductID),
ClientID: parseUUID(req.ClientID),
}
created, err := s.quarantineLineRepo.Create(ctx, nil, line)
if err != nil {
return dtodomain.QuarantineLineResponse{}, err
}
return mapQuarantineLineToResponse(created), nil
}
func (s *quarantineService) UpdateLine(ctx context.Context, lineId string, req dtodomain.QuarantineLineUpdateRequest) (dtodomain.QuarantineLineResponse, error) {
line, err := s.quarantineLineRepo.GetById(ctx, nil, lineId)
if err != nil {
return dtodomain.QuarantineLineResponse{}, err
}
line.ProductID = parseUUID(req.ProductID)
line.ClientID = parseUUID(req.ClientID)
updated, err := s.quarantineLineRepo.Update(ctx, nil, line)
if err != nil {
return dtodomain.QuarantineLineResponse{}, err
}
return mapQuarantineLineToResponse(updated), nil
}
func (s *quarantineService) DeleteLine(ctx context.Context, lineId string) error {
return s.quarantineLineRepo.Delete(ctx, nil, lineId)
}
func (s *quarantineService) OnComplete(ctx context.Context, id string) (dtodomain.QuarantineResponse, error) {
entity, err := s.quarantineRepo.GetById(ctx, nil, id)
if err != nil {
return dtodomain.QuarantineResponse{}, err
}
entity.Status = "completed"
updated, err := s.quarantineRepo.Update(ctx, nil, entity)
if err != nil {
return dtodomain.QuarantineResponse{}, err
}
lines, _ := s.quarantineLineRepo.GetAllByQuarantineId(ctx, id)
return mapQuarantineToResponse(updated, lines), nil
}
func parseUUID(id string) uuid.UUID {
uid, _ := uuid.Parse(id)
return uid
}
func parseDate(dateStr string) time.Time {
t, _ := time.Parse("2006-01-02", dateStr)
return t
}
func mapQuarantineToResponse(entity entities.TInventoryQuarantineEntity, lines []entities.TInventoryQuarantineLineEntity) dtodomain.QuarantineResponse {
resp := dtodomain.QuarantineResponse{
ID: entity.ID.String(),
DocumentNumber: entity.DocumentNumber,
DocumentDate: entity.DocumentDate.Format("2006-01-02"),
Status: entity.Status,
WarehouseID: entity.WarehouseID.String(),
ZonaID: entity.ZonaID.String(),
ClientID: entity.ClientID.String(),
Warehouse: entity.Warehouse.Name,
Zona: entity.Zona.Name,
Client: entity.Client.Name,
}
for _, line := range lines {
resp.QuarantineLines = append(resp.QuarantineLines, mapQuarantineLineToResponse(line))
}
return resp
}
func mapQuarantineLineToResponse(line entities.TInventoryQuarantineLineEntity) dtodomain.QuarantineLineResponse {
return dtodomain.QuarantineLineResponse{
ID: line.ID.String(),
QuarantineID: line.QuarantineID.String(),
StorageID: line.StorageID.String(),
ProductID: line.ProductID.String(),
ClientID: line.ClientID.String(),
Product: line.Product.Name,
Storage: line.StorageID.String(), // gunakan StorageID, karena field Name tidak ada
Quarantine: line.Quarantine.DocumentNumber,
Client: line.Client.Name,
}
}

View File

@ -96,6 +96,11 @@ import (
inventoryTransactionRepo "github.com/Caknoooo/go-gin-clean-starter/modules/inventory_transaction/repository"
inventoryTransactionService "github.com/Caknoooo/go-gin-clean-starter/modules/inventory_transaction/service"
quarantineController "github.com/Caknoooo/go-gin-clean-starter/modules/quarantine/controller"
quarantineLineRepo "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"
"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"
@ -159,6 +164,8 @@ func RegisterDependencies(injector *do.Injector) {
inventoryMovementLineRepository := inventoryMovementLineRepo.NewInventoryMovementLineRepository(db)
inventoryStorageRepository := inventoryStorageRepo.NewInventoryStorageRepository(db)
inventoryTransactionRepository := inventoryTransactionRepo.NewInventoryTransactionRepository(db)
quarantineRepository := quarantineRepo.NewQuarantineRepository(db)
quarantineLineRepository := quarantineLineRepo.NewQuarantineLineRepository(db)
// Service
userServ := userService.NewUserService(userRepository, roleRepository, warehouseRepository, clientRepository, refreshTokenRepository, jwtService, db)
@ -182,6 +189,7 @@ func RegisterDependencies(injector *do.Injector) {
inventoryMovementServ := inventoryMovementService.NewInventoryMovementService(db, inventoryMovementRepository, inventoryMovementLineRepository)
inventoryStorageService := inventoryStorageService.NewInventoryStorageService(db, inventoryStorageRepository)
inventoryTransactionServ := inventoryTransactionService.NewInventoryTransactionService(db, inventoryTransactionRepository)
quarantineServ := quarantineService.NewQuarantineService(db, quarantineRepository, quarantineLineRepository, productRepository, uomRepository, inventoryStorageRepository)
// Controller
do.Provide(
@ -299,4 +307,9 @@ func RegisterDependencies(injector *do.Injector) {
return inventoryTransactionController.NewInventoryTransactionController(i, inventoryTransactionServ), nil
},
)
do.Provide(
injector, func(i *do.Injector) (quarantineController.QuarantineController, error) {
return quarantineController.NewQuarantineController(i, quarantineServ), nil
},
)
}