Merge branch 'feat/BE/US-35/stock-transfer' into 'development-before-sso'

(BE-58,,59): extend db schema & build stock transfer api

See merge request mbugroup/lti-api!19
This commit is contained in:
Hafizh A. Y.
2025-10-17 03:35:43 +00:00
29 changed files with 1243 additions and 10 deletions
@@ -316,7 +316,7 @@ CREATE TABLE stock_logs (
before_quantity NUMERIC(15, 3) NOT NULL,
after_quantity NUMERIC(15, 3) NOT NULL,
log_type VARCHAR(50) NOT NULL,
log_id BIGINT ,
log_id BIGINT,
note TEXT,
product_warehouse_id BIGINT NOT NULL REFERENCES product_warehouses (id) ON DELETE CASCADE ON UPDATE CASCADE,
created_by BIGINT NOT NULL REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
@@ -0,0 +1,4 @@
-- DROP TABLE: STOCK_TRANSFERS DAN SEQUENCE-NYA
DROP TABLE IF EXISTS stock_transfers CASCADE;
DROP SEQUENCE IF EXISTS stock_transfer_seq CASCADE;
@@ -0,0 +1,57 @@
-- ===============================================================
-- STOCK TRANSFERS (HEADER)
-- ===============================================================
CREATE SEQUENCE IF NOT EXISTS stock_transfer_seq START 1;
CREATE TABLE IF NOT EXISTS stock_transfers (
id BIGSERIAL PRIMARY KEY,
movement_number VARCHAR(50) UNIQUE NOT NULL,
from_warehouse_id BIGINT NOT NULL,
to_warehouse_id BIGINT NOT NULL,
area_id BIGINT,
reason TEXT,
transfer_date DATE NOT NULL,
created_by BIGINT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'warehouses') THEN
ALTER TABLE stock_transfers
ADD CONSTRAINT fk_stock_transfers_from_warehouse
FOREIGN KEY (from_warehouse_id)
REFERENCES warehouses(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE stock_transfers
ADD CONSTRAINT fk_stock_transfers_to_warehouse
FOREIGN KEY (to_warehouse_id)
REFERENCES warehouses(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'areas') THEN
ALTER TABLE stock_transfers
ADD CONSTRAINT fk_stock_transfers_area
FOREIGN KEY (area_id)
REFERENCES areas(id)
ON DELETE SET NULL ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
ALTER TABLE stock_transfers
ADD CONSTRAINT fk_stock_transfers_created_by
FOREIGN KEY (created_by)
REFERENCES users(id)
ON DELETE SET NULL ON UPDATE CASCADE;
END IF;
END $$;
-- INDEXES
CREATE INDEX IF NOT EXISTS idx_stock_transfers_from_warehouse_id ON stock_transfers(from_warehouse_id);
CREATE INDEX IF NOT EXISTS idx_stock_transfers_to_warehouse_id ON stock_transfers(to_warehouse_id);
CREATE INDEX IF NOT EXISTS idx_stock_transfers_transfer_date ON stock_transfers(transfer_date);
@@ -0,0 +1,2 @@
-- DROP TABLE: STOCK_TRANSFER_DETAILS
DROP TABLE IF EXISTS stock_transfer_details CASCADE;
@@ -0,0 +1,48 @@
-- ===============================================================
-- STOCK TRANSFER DETAILS (PRODUK)
-- ===============================================================
CREATE TABLE IF NOT EXISTS stock_transfer_details (
id BIGSERIAL PRIMARY KEY,
stock_transfer_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
quantity NUMERIC(15, 3) NOT NULL CHECK (quantity > 0),
before_quantity NUMERIC(15, 3),
after_quantity NUMERIC(15, 3),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
-- ===============================================================
-- FOREIGN KEYS (dengan pengecekan tabel agar anti gagal)
-- ===============================================================
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'stock_transfers') THEN
EXECUTE
'ALTER TABLE stock_transfer_details
ADD CONSTRAINT fk_stock_transfer_details_transfer
FOREIGN KEY (stock_transfer_id)
REFERENCES stock_transfers(id)
ON DELETE CASCADE ON UPDATE CASCADE';
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'products') THEN
EXECUTE
'ALTER TABLE stock_transfer_details
ADD CONSTRAINT fk_stock_transfer_details_product
FOREIGN KEY (product_id)
REFERENCES products(id)
ON DELETE RESTRICT ON UPDATE CASCADE';
END IF;
END $$;
-- ===============================================================
-- INDEXES
-- ===============================================================
CREATE INDEX IF NOT EXISTS idx_stock_transfer_details_transfer_id ON stock_transfer_details (stock_transfer_id);
CREATE INDEX IF NOT EXISTS idx_stock_transfer_details_product_id ON stock_transfer_details (product_id);
@@ -0,0 +1,2 @@
-- DROP TABLE: STOCK_TRANSFER_DELIVERIES
DROP TABLE IF EXISTS stock_transfer_deliveries CASCADE;
@@ -0,0 +1,42 @@
-- ===============================================================
-- STOCK TRANSFER DELIVERIES (EKSPEDISI)
-- ===============================================================
CREATE TABLE IF NOT EXISTS stock_transfer_deliveries (
id BIGSERIAL PRIMARY KEY,
stock_transfer_id BIGINT NOT NULL,
supplier_id BIGINT,
vehicle_plate VARCHAR(20),
driver_name VARCHAR(100),
document_number VARCHAR(50),
document_path TEXT,
shipping_cost_item NUMERIC(15,3),
shipping_cost_total NUMERIC(15,3),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
-- FOREIGN KEYS
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'stock_transfers') THEN
ALTER TABLE stock_transfer_deliveries
ADD CONSTRAINT fk_stock_transfer_deliveries_transfer
FOREIGN KEY (stock_transfer_id)
REFERENCES stock_transfers(id)
ON DELETE CASCADE ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'suppliers') THEN
ALTER TABLE stock_transfer_deliveries
ADD CONSTRAINT fk_stock_transfer_deliveries_supplier
FOREIGN KEY (supplier_id)
REFERENCES suppliers(id)
ON DELETE SET NULL ON UPDATE CASCADE;
END IF;
END $$;
-- INDEXES
CREATE INDEX IF NOT EXISTS idx_stock_transfer_deliveries_transfer_id ON stock_transfer_deliveries(stock_transfer_id);
CREATE INDEX IF NOT EXISTS idx_stock_transfer_deliveries_supplier_id ON stock_transfer_deliveries(supplier_id);
@@ -0,0 +1,2 @@
-- DROP PIVOT TABLE: STOCK_TRANSFER_DELIVERY_ITEMS
DROP TABLE IF EXISTS stock_transfer_delivery_items CASCADE;
@@ -0,0 +1,35 @@
-- ===============================================================
-- STOCK TRANSFER DELIVERY ITEMS (PIVOT)
-- ===============================================================
CREATE TABLE IF NOT EXISTS stock_transfer_delivery_items (
id BIGSERIAL PRIMARY KEY,
stock_transfer_delivery_id BIGINT NOT NULL,
stock_transfer_detail_id BIGINT NOT NULL,
quantity NUMERIC(15, 3) NOT NULL CHECK (quantity > 0)
);
-- FOREIGN KEYS
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'stock_transfer_deliveries') THEN
ALTER TABLE stock_transfer_delivery_items
ADD CONSTRAINT fk_delivery_items_delivery
FOREIGN KEY (stock_transfer_delivery_id)
REFERENCES stock_transfer_deliveries(id)
ON DELETE CASCADE ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'stock_transfer_details') THEN
ALTER TABLE stock_transfer_delivery_items
ADD CONSTRAINT fk_delivery_items_detail
FOREIGN KEY (stock_transfer_detail_id)
REFERENCES stock_transfer_details(id)
ON DELETE CASCADE ON UPDATE CASCADE;
END IF;
END $$;
-- INDEXES
CREATE INDEX IF NOT EXISTS idx_stock_transfer_delivery_items_delivery_id ON stock_transfer_delivery_items (stock_transfer_delivery_id);
CREATE INDEX IF NOT EXISTS idx_stock_transfer_delivery_items_detail_id ON stock_transfer_delivery_items (stock_transfer_detail_id);
+83 -1
View File
@@ -89,6 +89,10 @@ func Run(db *gorm.DB) error {
return err
}
if err := seedTransferStock(tx, adminID); err != nil {
return err
}
fmt.Println("✅ Master data seeding completed")
return nil
})
@@ -936,7 +940,7 @@ func seedProductWarehouse(tx *gorm.DB, createdBy uint) error {
}{
{ProductID: 1, WarehouseID: 1, Quantity: 100},
{ProductID: 2, WarehouseID: 2, Quantity: 200},
{ProductID: 1, WarehouseID: 1, Quantity: 300},
{ProductID: 2, WarehouseID: 1, Quantity: 300},
}
for _, seed := range seeds {
@@ -960,6 +964,84 @@ func seedProductWarehouse(tx *gorm.DB, createdBy uint) error {
return nil
}
func seedTransferStock(tx *gorm.DB, createdBy uint) error {
// Seeder Transfer Stock
// 1. Insert StockTransfer (header)
transfer := entity.StockTransfer{
FromWarehouseId: 1,
ToWarehouseId: 2,
Reason: "Seed transfer stock",
TransferDate: time.Now(),
MovementNumber: "SEED-TRF-00001",
CreatedBy: 1,
}
if err := tx.Create(&transfer).Error; err != nil {
return err
}
// 2. Insert StockTransferDetail (detail)
details := []entity.StockTransferDetail{
{
StockTransferId: transfer.Id,
ProductId: 1,
Quantity: 10,
},
{
StockTransferId: transfer.Id,
ProductId: 2,
Quantity: 5,
},
}
for i := range details {
if err := tx.Create(&details[i]).Error; err != nil {
return err
}
}
// 3. Insert StockTransferDelivery (delivery)
deliveries := []entity.StockTransferDelivery{
{
StockTransferId: transfer.Id,
SupplierId: 1,
VehiclePlate: "B 1234 XYZ",
DriverName: "Driver Seed",
DocumentPath: "seed.pdf",
ShippingCostItem: 1000,
ShippingCostTotal: 2000,
},
}
for i := range deliveries {
if err := tx.Create(&deliveries[i]).Error; err != nil {
return err
}
}
detailMap := make(map[uint64]uint64)
for _, d := range details {
detailMap[d.ProductId] = d.Id
}
deliveryItems := []entity.StockTransferDeliveryItem{
{
StockTransferDeliveryId: deliveries[0].Id,
StockTransferDetailId: detailMap[1],
Quantity: 50,
},
{
StockTransferDeliveryId: deliveries[0].Id,
StockTransferDetailId: detailMap[2],
Quantity: 30,
},
}
for i := range deliveryItems {
if err := tx.Create(&deliveryItems[i]).Error; err != nil {
return err
}
}
return nil
}
func ptr[T any](v T) *T {
return &v
}
+23
View File
@@ -0,0 +1,23 @@
package entities
import "time"
// HEADER
type StockTransfer struct {
Id uint64 `gorm:"primaryKey;autoIncrement"`
MovementNumber string `gorm:"uniqueIndex;not null"`
FromWarehouseId uint64
ToWarehouseId uint64
TransferDate time.Time
Reason string
CreatedBy uint64
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time `gorm:"index"`
// Relations
FromWarehouse *Warehouse `gorm:"foreignKey:FromWarehouseId"`
ToWarehouse *Warehouse `gorm:"foreignKey:ToWarehouseId"`
Details []StockTransferDetail `gorm:"foreignKey:StockTransferId"`
Deliveries []StockTransferDelivery `gorm:"foreignKey:StockTransferId"`
CreatedUser *User `gorm:"foreignKey:CreatedBy"`
}
@@ -0,0 +1,23 @@
package entities
import "time"
// DETAIL EKSPEDISI
type StockTransferDelivery struct {
Id uint64 `gorm:"primaryKey;autoIncrement"`
StockTransferId uint64
SupplierId uint64
VehiclePlate string
DriverName string
DocumentNumber string
DocumentPath string
ShippingCostItem float64
ShippingCostTotal float64
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time `gorm:"index"`
// Relations
StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"`
Supplier *Supplier `gorm:"foreignKey:SupplierId"`
Items []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDeliveryId"`
}
@@ -0,0 +1,12 @@
package entities
// PIVOT TABLE TRANSFER
type StockTransferDeliveryItem struct {
Id uint64 `gorm:"primaryKey;autoIncrement"`
StockTransferDeliveryId uint64
StockTransferDetailId uint64
Quantity float64
// Relations
StockTransferDelivery *StockTransferDelivery `gorm:"foreignKey:StockTransferDeliveryId"`
StockTransferDetail *StockTransferDetail `gorm:"foreignKey:StockTransferDetailId"`
}
@@ -0,0 +1,18 @@
package entities
import "time"
// DETAIL PRODUK
type StockTransferDetail struct {
Id uint64 `gorm:"primaryKey;autoIncrement"`
StockTransferId uint64
ProductId uint64
Quantity float64
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time `gorm:"index"`
// Relations
StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"`
Product *Product `gorm:"foreignKey:ProductId"`
DeliveryItems []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDetailId"`
}
@@ -10,10 +10,10 @@ import (
// === DTO Structs ===
type ProductWarehouseBaseDTO struct {
Id uint `json:"id"`
ProductId uint `json:"product_id"`
WarehouseId uint `json:"warehouse_id"`
Quantity float64 `json:"quantity"`
Id uint `json:"id"`
ProductId uint `json:"product_id"`
WarehouseId uint `json:"warehouse_id"`
Quantity float64 `json:"quantity"`
}
type ProductWarehouseListDTO struct {
@@ -31,9 +31,10 @@ type ProductWarehouseDetailDTO struct {
// Nested DTOs for relations
type ProductBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Sku string `json:"sku"`
Id uint `json:"id"`
Name string `json:"name"`
Sku string `json:"sku"`
Flags []string `json:"flags"`
}
type WarehouseBaseDTO struct {
@@ -68,6 +69,12 @@ func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDT
if e.Product.Sku != nil {
product.Sku = *e.Product.Sku
}
// Map flags from Product relation
if len(e.Product.Flags) > 0 {
for _, f := range e.Product.Flags {
product.Flags = append(product.Flags, f.Name)
}
}
dto.Product = &product
}
@@ -34,7 +34,7 @@ func NewProductWarehouseService(repo repository.ProductWarehouseRepository, vali
}
func (s productWarehouseService) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("Product").Preload("Warehouse").Preload("CreatedUser")
return db.Preload("Product.Flags").Preload("Product").Preload("Warehouse").Preload("CreatedUser")
}
func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProductWarehouse, int64, error) {
+2
View File
@@ -9,6 +9,7 @@ import (
productWarehouses "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses"
adjustments "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments"
transfers "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers"
// MODULE IMPORTS
)
@@ -19,6 +20,7 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida
productWarehouses.ProductWarehouseModule{},
adjustments.AdjustmentModule{},
transfers.TransferModule{},
// MODULE REGISTRY
}
@@ -0,0 +1,103 @@
package controller
import (
"encoding/json"
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type TransferController struct {
TransferService service.TransferService
}
func NewTransferController(transferService service.TransferService) *TransferController {
return &TransferController{
TransferService: transferService,
}
}
func (u *TransferController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
}
result, totalResults, err := u.TransferService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.TransferListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all transfers successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToTransferListDTOs(result),
})
}
func (u *TransferController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
result, err := u.TransferService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get transfer successfully",
Data: dto.ToTransferListDTO(*result),
})
}
func (u *TransferController) CreateOne(c *fiber.Ctx) error {
data := c.FormValue("data")
var req validation.TransferRequest
if err := json.Unmarshal([]byte(data), &req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
// ambil file
form, err := c.MultipartForm()
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form")
}
_ = form.File["documents"]
// todo: tunggu ada aws baru proses
result, err := u.TransferService.CreateOne(c, &req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create transfer successfully",
Data: dto.ToTransferListDTO(*result),
})
}
@@ -0,0 +1,225 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type TransferBaseDTO struct {
Id uint64 `json:"id"`
TransferReason string `json:"transfer_reason"`
TransferDate string `json:"transfer_date"`
SourceWarehouse *WarehouseDetailDTO `json:"source_warehouse,omitempty"`
DestinationWarehouse *WarehouseDetailDTO `json:"destination_warehouse,omitempty"`
}
// Only id and name for warehouse simple view
type WarehouseSimpleDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type AreaDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type LocationDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type WarehouseDetailDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Location *LocationDTO `json:"location"`
Area *AreaDTO `json:"area"`
}
type TransferListDTO struct {
TransferBaseDTO
CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Details []TransferDetailItemDTO `json:"details"`
Deliveries []TransferDeliveryDTO `json:"deliveries"`
}
type TransferDetailDTO struct {
TransferListDTO
Details []TransferDetailItemDTO `json:"details"`
Deliveries []TransferDeliveryDTO `json:"deliveries"`
}
// Detail produk
type TransferDetailItemDTO struct {
Id uint64 `json:"id"`
ProductId uint64 `json:"product_id"`
Quantity float64 `json:"quantity"`
BeforeQuantity float64 `json:"before_quantity"`
AfterQuantity float64 `json:"after_quantity"`
}
// Delivery ekspedisi
type TransferDeliveryDTO struct {
Id uint64 `json:"id"`
SupplierId uint64 `json:"supplier_id"`
VehiclePlate string `json:"vehicle_plate"`
DriverName string `json:"driver_name"`
DocumentNumber string `json:"document_number"`
DocumentPath string `json:"document_path"`
ShippingCostItem float64 `json:"shipping_cost_item"`
ShippingCostTotal float64 `json:"shipping_cost_total"`
Items []TransferDeliveryItemDTO `json:"items"`
}
type TransferDeliveryItemDTO struct {
Id uint64 `json:"id"`
StockTransferDetailId uint64 `json:"stock_transfer_detail_id"`
Quantity float64 `json:"quantity"`
}
// === Mapper Functions ===
func ToTransferBaseDTO(e entity.StockTransfer) TransferBaseDTO {
var sourceWarehouse *WarehouseDetailDTO
if e.FromWarehouse != nil && e.FromWarehouse.Id != 0 {
sourceWarehouse = toWarehouseDetailDTO(e.FromWarehouse)
}
var destinationWarehouse *WarehouseDetailDTO
if e.ToWarehouse != nil && e.ToWarehouse.Id != 0 {
destinationWarehouse = toWarehouseDetailDTO(e.ToWarehouse)
}
return TransferBaseDTO{
Id: e.Id,
TransferReason: e.Reason,
TransferDate: e.CreatedAt.Format("2006-01-02"),
SourceWarehouse: sourceWarehouse,
DestinationWarehouse: destinationWarehouse,
}
}
func toAreaDTO(a *entity.Area) *AreaDTO {
if a == nil {
return nil
}
return &AreaDTO{
Id: a.Id,
Name: a.Name,
}
}
func toLocationDTO(l *entity.Location) *LocationDTO {
if l == nil {
return nil
}
return &LocationDTO{
Id: l.Id,
Name: l.Name,
}
}
func toWarehouseDetailDTO(w *entity.Warehouse) *WarehouseDetailDTO {
if w == nil {
return nil
}
return &WarehouseDetailDTO{
Id: w.Id,
Name: w.Name,
Location: toLocationDTO(w.Location),
Area: toAreaDTO(&w.Area), // Ambil area langsung dari warehouse (area_id)
}
}
func ToTransferListDTO(e entity.StockTransfer) TransferListDTO {
var createdUser *userDTO.UserBaseDTO
if e.CreatedUser != nil {
mapped := userDTO.ToUserBaseDTO(*e.CreatedUser)
createdUser = &mapped
}
// Map details
var details []TransferDetailItemDTO
for _, d := range e.Details {
details = append(details, TransferDetailItemDTO{
Id: d.Id,
ProductId: d.ProductId,
Quantity: d.Quantity,
})
}
// Map deliveries
var deliveries []TransferDeliveryDTO
for _, del := range e.Deliveries {
// Map delivery items
var items []TransferDeliveryItemDTO
for _, item := range del.Items {
items = append(items, TransferDeliveryItemDTO{
Id: item.Id,
StockTransferDetailId: item.StockTransferDetailId,
Quantity: item.Quantity,
})
}
deliveries = append(deliveries, TransferDeliveryDTO{
Id: del.Id,
SupplierId: del.SupplierId,
VehiclePlate: del.VehiclePlate,
DriverName: del.DriverName,
DocumentNumber: del.DocumentNumber,
DocumentPath: del.DocumentPath,
ShippingCostItem: del.ShippingCostItem,
ShippingCostTotal: del.ShippingCostTotal,
Items: items,
})
}
return TransferListDTO{
TransferBaseDTO: ToTransferBaseDTO(e),
CreatedUser: createdUser,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
Details: details,
Deliveries: deliveries,
}
}
func ToTransferListDTOs(e []entity.StockTransfer) []TransferListDTO {
result := make([]TransferListDTO, len(e))
for i, r := range e {
result[i] = ToTransferListDTO(r)
}
return result
}
func ToTransferDetailDTO(e entity.StockTransfer) TransferDetailDTO {
// Map details
var details []TransferDetailItemDTO
for _, d := range e.Details {
details = append(details, TransferDetailItemDTO{
Id: d.Id,
ProductId: d.ProductId,
Quantity: d.Quantity,
})
}
// Map deliveries
var deliveries []TransferDeliveryDTO
for _, del := range e.Deliveries {
deliveries = append(deliveries, TransferDeliveryDTO{
Id: del.Id,
SupplierId: del.SupplierId,
VehiclePlate: del.VehiclePlate,
DriverName: del.DriverName,
DocumentNumber: del.DocumentNumber,
DocumentPath: del.DocumentPath,
ShippingCostItem: del.ShippingCostItem,
ShippingCostTotal: del.ShippingCostTotal,
})
}
return TransferDetailDTO{
TransferListDTO: ToTransferListDTO(e),
Details: details,
Deliveries: deliveries,
}
}
@@ -0,0 +1,33 @@
package transfers
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories"
sTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services"
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/stock-logs/repositories"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type TransferModule struct{}
func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
stockTransferRepo := rStockTransfer.NewStockTransferRepository(db)
stockTransferDetailRepo := rStockTransfer.NewStockTransferDetailRepository(db)
stockTransferDeliveryRepo := rStockTransfer.NewStockTransferDeliveryRepository(db)
StockTransferDeliveryItemRepo := rStockTransfer.NewStockTransferDeliveryItemRepository(db)
stockLogsRepo := rStockLogs.NewStockLogRepository(db)
supplierRepo := rSupplier.NewSupplierRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
userRepo := rUser.NewUserRepository(db)
transferService := sTransfer.NewTransferService(validate, stockTransferRepo, stockTransferDetailRepo, stockTransferDeliveryRepo, StockTransferDeliveryItemRepo, stockLogsRepo, productWarehouseRepo, supplierRepo)
userService := sUser.NewUserService(userRepo, validate)
TransferRoutes(router, userService, transferService)
}
@@ -0,0 +1,34 @@
package repositories
import (
"context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type StockTransferRepository interface {
repository.BaseRepository[entity.StockTransfer]
// get sequence for movement number
GetNextMovementNumber(ctx context.Context) (int64, error)
}
type StockTransferRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.StockTransfer]
}
func NewStockTransferRepository(db *gorm.DB) StockTransferRepository {
return &StockTransferRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.StockTransfer](db),
}
}
func (r *StockTransferRepositoryImpl) GetNextMovementNumber(ctx context.Context) (int64, error) {
var seq int64
err := r.DB().WithContext(ctx).Raw("SELECT nextval('stock_transfer_seq')").Scan(&seq).Error
if err != nil {
return 0, err
}
return seq, nil
}
@@ -0,0 +1,22 @@
package repositories
import (
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type StockTransferDeliveryRepository interface {
repository.BaseRepository[entity.StockTransferDelivery]
// Tambahkan custom method jika perlu
}
type StockTransferDeliveryRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.StockTransferDelivery]
}
func NewStockTransferDeliveryRepository(db *gorm.DB) StockTransferDeliveryRepository {
return &StockTransferDeliveryRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.StockTransferDelivery](db),
}
}
@@ -0,0 +1,22 @@
package repositories
import (
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type StockTransferDeliveryItemRepository interface {
repository.BaseRepository[entity.StockTransferDeliveryItem]
// Tambahkan custom method jika perlu
}
type StockTransferDeliveryItemRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.StockTransferDeliveryItem]
}
func NewStockTransferDeliveryItemRepository(db *gorm.DB) StockTransferDeliveryItemRepository {
return &StockTransferDeliveryItemRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.StockTransferDeliveryItem](db),
}
}
@@ -0,0 +1,29 @@
// Find all details by StockTransferId
package repositories
import (
"context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type StockTransferDetailRepository interface {
repository.BaseRepository[entity.StockTransferDetail]
FindByTransferId(ctx context.Context, transferId uint64, out *[]entity.StockTransferDetail) error
}
type StockTransferDetailRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.StockTransferDetail]
}
func NewStockTransferDetailRepository(db *gorm.DB) StockTransferDetailRepository {
return &StockTransferDetailRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.StockTransferDetail](db),
}
}
func (r *StockTransferDetailRepositoryImpl) FindByTransferId(ctx context.Context, transferId uint64, out *[]entity.StockTransferDetail) error {
return r.DB().WithContext(ctx).Where("stock_transfer_id = ?", transferId).Find(out).Error
}
@@ -0,0 +1,27 @@
package transfers
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/controllers"
transfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func TransferRoutes(v1 fiber.Router, u user.UserService, s transfer.TransferService) {
ctrl := controller.NewTransferController(s)
route := v1.Group("/transfers")
// route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
}
@@ -0,0 +1,313 @@
package service
import (
"errors"
"fmt"
"strings"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/validations"
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/stock-logs/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type TransferService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.StockTransfer, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.StockTransfer, error)
CreateOne(ctx *fiber.Ctx, req *validation.TransferRequest) (*entity.StockTransfer, error)
}
type transferService struct {
Log *logrus.Logger
Validate *validator.Validate
StockTransferRepo rStockTransfer.StockTransferRepository
StockTransferDetailRepo rStockTransfer.StockTransferDetailRepository
StockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository
StockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository
StockLogsRepository rStockLogs.StockLogRepository
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
SupplierRepo rSupplier.SupplierRepository
}
func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, supplierRepo rSupplier.SupplierRepository) TransferService {
return &transferService{
Log: utils.Log,
Validate: validate,
StockTransferRepo: stockTransferRepo,
StockTransferDetailRepo: stockTransferDetailRepo,
StockTransferDeliveryRepo: stockTransferDeliveryRepo,
StockTransferDeliveryItemRepo: stockTransferDeliveryItemRepo,
StockLogsRepository: stockLogsRepo,
ProductWarehouseRepo: productWarehouseRepo,
SupplierRepo: supplierRepo,
}
}
func (s transferService) withRelations(db *gorm.DB) *gorm.DB {
return db.
Preload("CreatedUser").
Preload("FromWarehouse").
Preload("FromWarehouse.Location").
Preload("FromWarehouse.Area").
Preload("ToWarehouse").
Preload("ToWarehouse.Location").
Preload("ToWarehouse.Area").
Preload("Details").
Preload("Deliveries.Items")
}
func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.StockTransfer, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
transfers, total, err := s.StockTransferRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
db = db.Where("movement_number LIKE ?", "%"+strings.TrimSpace(params.Search)+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
if err != nil {
return nil, 0, err
}
s.Log.Infof("Retrieved %d transfers", len(transfers))
return transfers, total, nil
}
func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, error) {
var transfer entity.StockTransfer
// gunakan repo secara langsung
transferPtr, err := s.StockTransferRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
return s.withRelations(db)
})
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Transfer not found")
}
s.Log.Errorf("Failed to get transfer by ID: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer")
}
s.Log.Infof("Retrieved transfer: %+v", transfer)
return transferPtr, nil
}
func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferRequest) (*entity.StockTransfer, error) {
// Validasi stok di gudang asal harus exist dan mencukupi
for _, product := range req.Products {
sourcePW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(
c.Context(), uint(product.ProductID), uint(req.SourceWarehouseID),
)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d tidak tersedia di gudang asal", product.ProductID))
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal cek stok produk di gudang asal")
}
if sourcePW.Quantity < product.ProductQty {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d di gudang asal tidak cukup", product.ProductID))
}
}
// validasi total qty harus lebih besar dari atau sama dengan total qty di delivery compare berdasarkan productid
deliveryQtyMap := make(map[uint]float64)
for _, delivery := range req.Deliveries {
for _, prod := range delivery.Products {
deliveryQtyMap[prod.ProductID] += prod.ProductQty
}
}
// Cek: qty delivery tidak boleh melebihi qty di root
for _, product := range req.Products {
if deliveryQtyMap[product.ProductID] > product.ProductQty {
return nil, fiber.NewError(fiber.StatusBadRequest,
fmt.Sprintf("Total qty delivery untuk produk %d (%v) melebihi qty transfer (%v)", product.ProductID, deliveryQtyMap[product.ProductID], product.ProductQty))
}
}
// cek suplier id caegory BOP cek by id
for _, delivery := range req.Deliveries {
supplier, err := s.SupplierRepo.GetByID(c.Context(), uint(delivery.SupplierID), nil)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d tidak ditemukan", delivery.SupplierID))
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal cek data supplier")
}
if supplier.Category != "BOP" {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d bukan kategori BOP", delivery.SupplierID))
}
}
// Generate movement number
// Format: PND-MBU-00001
seqNum, err := s.StockTransferRepo.GetNextMovementNumber(c.Context())
if err != nil {
s.Log.Errorf("Failed to get next movement number: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate movement number")
}
movementNumber := fmt.Sprintf("PND-MBU-%05d", seqNum)
transferDate, _ := utils.ParseDateString(req.TransferDate)
entityTransfer := &entity.StockTransfer{
FromWarehouseId: uint64(req.SourceWarehouseID),
ToWarehouseId: uint64(req.DestinationWarehouseID),
Reason: req.TransferReason,
TransferDate: transferDate,
MovementNumber: movementNumber,
CreatedBy: 1, //todo: get from token
}
// Save the transfer entity to the database
err = s.StockTransferRepo.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
// Insert header
if err := s.StockTransferRepo.WithTx(tx).CreateOne(c.Context(), entityTransfer, nil); err != nil {
s.Log.Errorf("Failed to create stock transfer: %+v", err)
return err
}
s.Log.Infof("Stock transfer created: %+v", entityTransfer.Id)
// insert ke details
var details []*entity.StockTransferDetail
for _, product := range req.Products {
details = append(details, &entity.StockTransferDetail{
StockTransferId: entityTransfer.Id,
ProductId: uint64(product.ProductID),
Quantity: product.ProductQty,
})
}
if err := s.StockTransferDetailRepo.WithTx(tx).CreateMany(c.Context(), details, nil); err != nil {
s.Log.Errorf("Failed to create stock transfer details: %+v", err)
return err
}
s.Log.Infof("Stock transfer details created for transfer ID: %+v", entityTransfer.Id)
// Tambahkan proses insert delivery
var deliveries []*entity.StockTransferDelivery
for _, delivery := range req.Deliveries {
deliveries = append(deliveries, &entity.StockTransferDelivery{
StockTransferId: entityTransfer.Id,
SupplierId: uint64(delivery.SupplierID),
VehiclePlate: delivery.VehiclePlate,
DriverName: delivery.DriverName,
DocumentPath: "dummy duls", // todo: tunggu ada aws baru proses
ShippingCostItem: delivery.DeliveryCostPerItem,
ShippingCostTotal: delivery.DeliveryCost,
})
}
if err := s.StockTransferDeliveryRepo.WithTx(tx).CreateMany(c.Context(), deliveries, nil); err != nil {
s.Log.Errorf("Failed to create stock transfer deliveries: %+v", err)
return err
}
// tambahkan insert ke delivery items sebagai pivot
detailMap := make(map[uint64]uint64)
for _, d := range details {
detailMap[d.ProductId] = d.Id
}
var deliveryItems []*entity.StockTransferDeliveryItem
for i, delivery := range deliveries {
item := req.Deliveries[i]
for _, prod := range item.Products {
detailID, ok := detailMap[uint64(prod.ProductID)]
if !ok {
return fmt.Errorf("produk %d tidak ditemukan di detail", prod.ProductID)
}
deliveryItems = append(deliveryItems, &entity.StockTransferDeliveryItem{
StockTransferDeliveryId: delivery.Id,
StockTransferDetailId: detailID,
Quantity: prod.ProductQty,
})
}
}
if err := s.StockTransferDeliveryItemRepo.WithTx(tx).CreateMany(c.Context(), deliveryItems, nil); err != nil {
s.Log.Errorf("Failed to create stock transfer delivery items: %+v", err)
return err
}
s.Log.Infof("Stock transfer delivery items created for transfer ID: %+v", entityTransfer.Id)
// Proses pengurangan stok di gudang asal dan penambahan stok di gudang tujuan
for _, product := range req.Products {
// Kurangi stok di gudang asal
sourcePW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(c.Context(), uint(product.ProductID), uint(req.SourceWarehouseID))
if err != nil {
s.Log.Errorf("Failed to get source product warehouse: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get source product warehouse")
}
if sourcePW.Quantity < product.ProductQty {
s.Log.Errorf("Insufficient stock in source warehouse for product ID: %+v", product.ProductID)
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock in source warehouse for product ID: %d", product.ProductID))
}
sourcePW.Quantity -= product.ProductQty
if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(c.Context(), sourcePW.Id, sourcePW, nil); err != nil {
s.Log.Errorf("Failed to update source product warehouse: %+v", err)
return err
}
s.Log.Infof("Source product warehouse updated: %+v", sourcePW.Id)
// Tambah stok di gudang tujuan
destPW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(
c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID),
)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
s.Log.Errorf("Failed to get destination product warehouse: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get destination product warehouse")
}
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
// Jika belum ada record untuk produk di gudang tujuan, buat baru
destPW = &entity.ProductWarehouse{
ProductId: uint(product.ProductID),
WarehouseId: uint(req.DestinationWarehouseID),
Quantity: 0,
CreatedBy: 1, // TODO: should Get from auth middleware
}
if err := s.ProductWarehouseRepo.WithTx(tx).CreateOne(c.Context(), destPW, nil); err != nil {
s.Log.Errorf("Failed to create destination product warehouse: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create destination product warehouse")
}
s.Log.Infof("Destination product warehouse created: %+v", destPW.Id)
}
// Update stok di gudang tujuan
destPW.Quantity += product.ProductQty
if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(c.Context(), destPW.Id, destPW, nil); err != nil {
s.Log.Errorf("Failed to update destination product warehouse: %+v", err)
return err
}
s.Log.Infof("Destination product warehouse updated: %+v", destPW.Id)
}
return nil
})
if err != nil {
s.Log.Errorf("Transaction failed in CreateOne: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to process transfer transaction")
}
// Ambil data lengkap hasil create dengan GetOne (agar preload relasi sama dengan GetOne)
result, err := s.GetOne(c, uint(entityTransfer.Id))
if err != nil {
return nil, err
}
return result, nil
}
@@ -0,0 +1,40 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
Search string `query:"search" validate:"omitempty,max=50"`
}
type TransferProduct struct {
ProductID uint `json:"product_id" validate:"required"`
ProductQty float64 `json:"product_qty" validate:"required,gt=0"`
}
type TransferDeliveryProduct struct {
ProductID uint `json:"product_id" validate:"required"`
ProductQty float64 `json:"product_qty" validate:"required,gt=0"`
}
type TransferDelivery struct {
DeliveryCost float64 `json:"delivery_cost" validate:"required"`
DeliveryCostPerItem float64 `json:"delivery_cost_per_item" validate:"required"`
DocumentIndex int `json:"document_index" validate:"min=0"`
DriverName string `json:"driver_name" validate:"required"`
VehiclePlate string `json:"vehicle_plate" validate:"required"`
SupplierID uint `json:"supplier_id" validate:"required"`
Products []TransferDeliveryProduct `json:"products" validate:"required,dive"`
}
type TransferRequest struct {
TransferReason string `json:"transfer_reason" validate:"required"`
TransferDate string `json:"transfer_date" validate:"required,datetime=2006-01-02"`
SourceWarehouseID uint `json:"source_warehouse_id" validate:"required"`
DestinationWarehouseID uint `json:"destination_warehouse_id" validate:"required"`
Products []TransferProduct `json:"products" validate:"required,dive"`
Deliveries []TransferDelivery `json:"deliveries" validate:"required,dive"`
}
@@ -11,6 +11,7 @@ import (
type SupplierRepository interface {
repository.BaseRepository[entity.Supplier]
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
}
type SupplierRepositoryImpl struct {
+25
View File
@@ -0,0 +1,25 @@
package utils
import (
"time"
"errors"
)
// ParseDateString mengubah string "YYYY-MM-DD" menjadi time.Time
func ParseDateString(dateStr string) (time.Time, error) {
if dateStr == "" {
return time.Time{}, errors.New("date string is empty")
}
parsed, err := time.Parse("2006-01-02", dateStr)
if err != nil {
return time.Time{}, errors.New("invalid date format, expected YYYY-MM-DD")
}
return parsed, nil
}
// FormatDate mengubah time.Time menjadi string "YYYY-MM-DD"
func FormatDate(t time.Time) string {
return t.Format("2006-01-02")
}