diff --git a/internal/entities/marketing_delivery_product.go b/internal/entities/marketing_delivery_product.go index 85b4591a..253c00b2 100644 --- a/internal/entities/marketing_delivery_product.go +++ b/internal/entities/marketing_delivery_product.go @@ -9,13 +9,13 @@ import ( type MarketingDeliveryProduct struct { Id uint `gorm:"primaryKey;autoIncrement"` MarketingProductId uint `gorm:"uniqueIndex;not null"` - Qty float64 `gorm:"type:numeric(15,3);not null"` - UnitPrice float64 `gorm:"type:numeric(15,3);not null"` - TotalWeight float64 `gorm:"type:numeric(15,3);not null"` - AvgWeight float64 `gorm:"type:numeric(15,3);not null"` - TotalPrice float64 `gorm:"type:numeric(15,3);not null"` + Qty float64 `gorm:"type:numeric(15,3)"` + UnitPrice float64 `gorm:"type:numeric(15,3)"` + TotalWeight float64 `gorm:"type:numeric(15,3)"` + AvgWeight float64 `gorm:"type:numeric(15,3)"` + TotalPrice float64 `gorm:"type:numeric(15,3)"` DeliveryDate *time.Time `gorm:"type:timestamptz"` - VehicleNumber string `gorm:"type:varchar(50)"` + VehicleNumber string `gorm:"type:varchar(50)"` CreatedAt time.Time `gorm:"autoCreateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` diff --git a/internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go b/internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go index 96590990..512a5786 100644 --- a/internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go +++ b/internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go @@ -36,10 +36,12 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingId(ctx context.Co var deliveryProducts []entity.MarketingDeliveryProduct // Raw query untuk mengambil delivery products berdasarkan marketing ID dengan preload MarketingProduct + // Filter: hanya ambil yang sudah memiliki delivery_date (delivery date tidak null) if err := r.DB().WithContext(ctx). Preload("MarketingProduct"). Joins("INNER JOIN marketing_products mp ON marketing_delivery_products.marketing_product_id = mp.id"). Where("mp.marketing_id = ?", marketingId). + Where("marketing_delivery_products.delivery_date IS NOT NULL"). Order("marketing_delivery_products.id ASC"). Find(&deliveryProducts).Error; err != nil { return nil, err diff --git a/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go b/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go index 8c9f3846..c6ac5931 100644 --- a/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go +++ b/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go @@ -4,6 +4,7 @@ import ( "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto" ) // === DTO Structs === @@ -15,13 +16,19 @@ type ProductWarehouseBaseDTO struct { Quantity float64 `json:"quantity"` } +type ProductWarehousNestedDTO struct { + Id uint `json:"id"` + Product *productDTO.ProductBaseDTO `json:"product,omitempty"` + Warehouse *WarehouseBaseDTO `json:"warehouse,omitempty"` +} + type ProductWarehouseListDTO struct { ProductWarehouseBaseDTO - Product *ProductBaseDTO `json:"product,omitempty"` - Warehouse *WarehouseBaseDTO `json:"warehouse,omitempty"` - CreatedUser *UserBaseDTO `json:"created_user,omitempty"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + Product *productDTO.ProductBaseDTO `json:"product,omitempty"` + Warehouse *WarehouseBaseDTO `json:"warehouse,omitempty"` + CreatedUser *UserBaseDTO `json:"created_user,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type UserBaseDTO struct { @@ -75,6 +82,19 @@ func ToProductWarehouseBaseDTO(e entity.ProductWarehouse) ProductWarehouseBaseDT } } +func ToProductWarehouseNestedDTO(e entity.ProductWarehouse) ProductWarehousNestedDTO { + product := productDTO.ToProductBaseDTO(e.Product) + + return ProductWarehousNestedDTO{ + Id: e.Id, + Product: &product, + Warehouse: &WarehouseBaseDTO{ + Id: e.Warehouse.Id, + Name: e.Warehouse.Name, + }, + } +} + func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDTO { dto := ProductWarehouseListDTO{ ProductWarehouseBaseDTO: ToProductWarehouseBaseDTO(e), @@ -84,18 +104,7 @@ func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDT // Map Product relation jika ada if e.Product.Id != 0 { - product := ProductBaseDTO{ - Id: e.Product.Id, - Name: e.Product.Name, - } - if e.Product.Sku != nil { - product.Sku = *e.Product.Sku - } - if len(e.Product.Flags) > 0 { - for _, f := range e.Product.Flags { - product.Flags = append(product.Flags, f.Name) - } - } + product := productDTO.ToProductBaseDTO(e.Product) dto.Product = &product } diff --git a/internal/modules/marketing/delivery-orderss/controllers/delivery-orders.controller.go b/internal/modules/marketing/delivery-orderss/controllers/delivery-orders.controller.go index 8ca51dc5..292381d0 100644 --- a/internal/modules/marketing/delivery-orderss/controllers/delivery-orders.controller.go +++ b/internal/modules/marketing/delivery-orderss/controllers/delivery-orders.controller.go @@ -39,7 +39,7 @@ func (u *DeliveryOrdersController) GetAll(c *fiber.Ctx) error { } return c.Status(fiber.StatusOK). - JSON(response.SuccessWithPaginate[dto.DeliveryOrdersListDTO]{ + JSON(response.SuccessWithPaginate[dto.MarketingListDTO]{ Code: fiber.StatusOK, Status: "success", Message: "Get all deliveryOrderss successfully", @@ -122,44 +122,3 @@ func (u *DeliveryOrdersController) UpdateOne(c *fiber.Ctx) error { Data: result, }) } - -func (u *DeliveryOrdersController) DeleteOne(c *fiber.Ctx) error { - param := c.Params("id") - - id, err := strconv.Atoi(param) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") - } - - if err := u.DeliveryOrdersService.DeleteOne(c, uint(id)); err != nil { - return err - } - - return c.Status(fiber.StatusOK). - JSON(response.Common{ - Code: fiber.StatusOK, - Status: "success", - Message: "Delete deliveryOrders successfully", - }) -} - -func (u *DeliveryOrdersController) Approval(c *fiber.Ctx) error { - req := new(validation.Approve) - - if err := c.BodyParser(req); err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") - } - - results, err := u.DeliveryOrdersService.Approval(c, req) - if err != nil { - return err - } - - return c.Status(fiber.StatusOK). - JSON(response.Success{ - Code: fiber.StatusOK, - Status: "success", - Message: "Submit delivery order approval successfully", - Data: results, - }) -} diff --git a/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go b/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go index 6008269d..be662412 100644 --- a/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go +++ b/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go @@ -7,83 +7,100 @@ import ( entity "gitlab.com/mbugroup/lti-api.git/internal/entities" approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" + productwarehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/dto" + customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" ) -// === DTO Structs === +type MarketingBaseDTO struct { + Id uint `json:"id"` + SoNumber string `json:"so_number"` + SoDate time.Time `json:"so_date"` + Notes string `json:"notes,omitempty"` +} +type MarketingListDTO struct { + MarketingBaseDTO + Customer *customerDTO.CustomerBaseDTO `json:"customer,omitempty"` + SalesPerson *userDTO.UserBaseDTO `json:"sales_person,omitempty"` + SoDocs string `json:"so_docs,omitempty"` + CreatedUser *userDTO.UserBaseDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + LatestApproval *approvalDTO.ApprovalBaseDTO `json:"latest_approval,omitempty"` +} + +type MarketingDetailDTO struct { + MarketingBaseDTO + Customer *customerDTO.CustomerBaseDTO `json:"customer,omitempty"` + SalesPerson *userDTO.UserBaseDTO `json:"sales_person,omitempty"` + SoDocs string `json:"so_docs,omitempty"` + SalesOrder []MarketingProductDTO `json:"sales_order,omitempty"` + DeliveryOrder []DeliveryGroupDTO `json:"delivery_order,omitempty"` + CreatedUser *userDTO.UserBaseDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + LatestApproval *approvalDTO.ApprovalBaseDTO `json:"latest_approval,omitempty"` +} type MarketingDeliveryProductDTO struct { - Id uint `json:"id"` - MarketingProductId uint `json:"marketing_product_id"` - Qty float64 `json:"qty"` - UnitPrice float64 `json:"unit_price"` - TotalWeight float64 `json:"total_weight"` - AvgWeight float64 `json:"avg_weight"` - TotalPrice float64 `json:"total_price"` - DeliveryDate *time.Time `json:"delivery_date"` - VehicleNumber string `json:"vehicle_number"` + Id uint `json:"id"` + MarketingProductId uint `json:"marketing_product_id"` + Qty float64 `json:"qty"` + UnitPrice float64 `json:"unit_price"` + TotalWeight float64 `json:"total_weight"` + AvgWeight float64 `json:"avg_weight"` + TotalPrice float64 `json:"total_price"` + DeliveryDate *time.Time `json:"delivery_date"` + VehicleNumber string `json:"vehicle_number"` + ProductWarehouse *productwarehouseDTO.ProductWarehousNestedDTO `json:"product_warehouse,omitempty"` +} + +type DeliveryItemDTO struct { + ProductWarehouse *productwarehouseDTO.ProductWarehousNestedDTO `json:"product_warehouse"` + Qty float64 `json:"qty"` + UnitPrice float64 `json:"unit_price"` + TotalWeight float64 `json:"total_weight"` + AvgWeight float64 `json:"avg_weight"` + TotalPrice float64 `json:"total_price"` + VehicleNumber string `json:"vehicle_number"` } -// DTO untuk grouping delivery products berdasarkan warehouse dan tanggal type DeliveryGroupDTO struct { - WarehouseId uint `json:"warehouse_id"` - WarehouseName string `json:"warehouse_name"` - DeliveryDate *time.Time `json:"delivery_date"` - VehicleNumber string `json:"vehicle_number"` - TotalQty float64 `json:"total_qty"` - TotalWeight float64 `json:"total_weight"` - TotalPrice float64 `json:"total_price"` -} - -// DTO untuk Delivery Order (DO) - berisi data delivery yang sudah digroup -type DeliveryOrderDTO struct { - DeliveryGroups []DeliveryGroupDTO `json:"delivery_groups"` -} - -type DeliveryOrdersBaseDTO struct { - Id uint `json:"id,omitempty"` - DeliveryNumber *string `json:"delivery_number,omitempty"` - DeliveryDate *time.Time `json:"delivery_date,omitempty"` - MarketingId uint `json:"marketing_id"` - Notes string `json:"notes,omitempty"` + DoNumber string `json:"do_number"` + DeliveryDate *time.Time `json:"delivery_date"` + Warehouse *productwarehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"` + Deliveries []DeliveryItemDTO `json:"deliveries"` } type MarketingProductDTO struct { - Id uint `json:"id"` - MarketingId uint `json:"marketing_id"` - ProductWarehouseId uint `json:"product_warehouse_id"` - Qty float64 `json:"qty"` - UnitPrice float64 `json:"unit_price"` - AvgWeight float64 `json:"avg_weight"` - TotalWeight float64 `json:"total_weight"` - TotalPrice float64 `json:"total_price"` - // Add product relation if needed + Id uint `json:"id"` + MarketingId uint `json:"marketing_id"` + ProductWarehouseId uint `json:"product_warehouse_id"` + Qty float64 `json:"qty"` + UnitPrice float64 `json:"unit_price"` + AvgWeight float64 `json:"avg_weight"` + TotalWeight float64 `json:"total_weight"` + TotalPrice float64 `json:"total_price"` + ProductWarehouse *productwarehouseDTO.ProductWarehousNestedDTO `json:"product_warehouse,omitempty"` + VehicleNumber string `json:"vehicle_number,omitempty"` } -type MarketingBaseDTO struct { - Id uint `json:"id"` - SoNumber string `json:"so_number"` - SoDate time.Time `json:"so_date"` - Products []MarketingProductDTO `json:"products,omitempty"` +func ToMarketingBaseDTO(marketing *entity.Marketing) MarketingBaseDTO { + return MarketingBaseDTO{ + Id: marketing.Id, + SoNumber: marketing.SoNumber, + SoDate: marketing.SoDate, + Notes: marketing.Notes, + } } -type DeliveryOrdersListDTO struct { - DeliveryOrdersBaseDTO - SalesOrder *MarketingBaseDTO `json:"sales_order,omitempty"` // SO - Sales Order data - DeliveryOrder *DeliveryOrderDTO `json:"delivery_order,omitempty"` // DO - Delivery Order data (grouped) - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"` -} - -type DeliveryOrdersDetailDTO struct { - DeliveryOrdersListDTO -} - -// === Mapper Functions === - func ToMarketingProductDTO(e entity.MarketingProduct) MarketingProductDTO { + var productWarehouse *productwarehouseDTO.ProductWarehousNestedDTO + if e.ProductWarehouse.Id != 0 { + mapped := productwarehouseDTO.ToProductWarehouseNestedDTO(e.ProductWarehouse) + productWarehouse = &mapped + } + return MarketingProductDTO{ Id: e.Id, MarketingId: e.MarketingId, @@ -93,6 +110,8 @@ func ToMarketingProductDTO(e entity.MarketingProduct) MarketingProductDTO { AvgWeight: e.AvgWeight, TotalWeight: e.TotalWeight, TotalPrice: e.TotalPrice, + ProductWarehouse: productWarehouse, + VehicleNumber: getVehicleNumber(e), } } @@ -110,92 +129,67 @@ func ToMarketingDeliveryProductDTO(e entity.MarketingDeliveryProduct) MarketingD } } -func ToDeliveryOrdersBaseDTO(e entity.DeliveryOrders) DeliveryOrdersBaseDTO { - var deliveryNumber *string - if e.DeliveryNumber != "" { - deliveryNumber = &e.DeliveryNumber - } - - return DeliveryOrdersBaseDTO{ - Id: e.Id, - DeliveryNumber: deliveryNumber, - DeliveryDate: &e.DeliveryDate, - MarketingId: e.MarketingId, - Notes: e.Notes, - } -} - -func ToDeliveryOrdersListDTO(e entity.DeliveryOrders) DeliveryOrdersListDTO { +func ToMarketingListDTO(marketing *entity.Marketing, deliveryProducts []entity.MarketingDeliveryProduct) MarketingListDTO { var createdUser *userDTO.UserBaseDTO - if e.CreatedUser != nil && e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(*e.CreatedUser) + if marketing.CreatedUser.Id != 0 { + mapped := userDTO.ToUserBaseDTO(marketing.CreatedUser) createdUser = &mapped } - var marketing *MarketingBaseDTO - if e.Marketing != nil && e.Marketing.Id != 0 { - var marketingProducts []MarketingProductDTO - if len(e.Marketing.Products) > 0 { - marketingProducts = make([]MarketingProductDTO, len(e.Marketing.Products)) - for i, product := range e.Marketing.Products { - marketingProducts[i] = ToMarketingProductDTO(product) - } - } - - marketing = &MarketingBaseDTO{ - Id: e.Marketing.Id, - SoNumber: e.Marketing.SoNumber, - SoDate: e.Marketing.SoDate, - Products: marketingProducts, - } + var customer *customerDTO.CustomerBaseDTO + if marketing.Customer.Id != 0 { + mapped := customerDTO.ToCustomerBaseDTO(marketing.Customer) + customer = &mapped } - var deliveryProductsDTOs []MarketingDeliveryProductDTO - if len(e.DeliveryProducts) > 0 { - deliveryProductsDTOs = make([]MarketingDeliveryProductDTO, len(e.DeliveryProducts)) - for i, dp := range e.DeliveryProducts { - deliveryProductsDTOs[i] = ToMarketingDeliveryProductDTO(dp) - } + var salesPerson *userDTO.UserBaseDTO + if marketing.SalesPerson.Id != 0 { + mapped := userDTO.ToUserBaseDTO(marketing.SalesPerson) + salesPerson = &mapped } - // Group delivery products by warehouse and delivery date - deliveryGroups := groupDeliveryProducts(deliveryProductsDTOs) + var latestApproval *approvalDTO.ApprovalBaseDTO + if marketing.LatestApproval != nil { + mapped := approvalDTO.ToApprovalDTO(*marketing.LatestApproval) + latestApproval = &mapped + } - // Create delivery order DTO with summary - deliveryOrder := createDeliveryOrderDTO(deliveryGroups) - - return DeliveryOrdersListDTO{ - DeliveryOrdersBaseDTO: ToDeliveryOrdersBaseDTO(e), - SalesOrder: marketing, - DeliveryOrder: deliveryOrder, - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, + return MarketingListDTO{ + MarketingBaseDTO: ToMarketingBaseDTO(marketing), + Customer: customer, + SalesPerson: salesPerson, + SoDocs: marketing.SoDocs, + CreatedUser: createdUser, + CreatedAt: marketing.CreatedAt, + UpdatedAt: marketing.UpdatedAt, + LatestApproval: latestApproval, } } -func ToDeliveryOrdersListDTOWithProducts(e entity.DeliveryOrders, deliveryProducts []entity.MarketingDeliveryProduct) DeliveryOrdersListDTO { +func ToMarketingDetailDTO(marketing *entity.Marketing, deliveryProducts []entity.MarketingDeliveryProduct) MarketingDetailDTO { var createdUser *userDTO.UserBaseDTO - if e.CreatedUser != nil && e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(*e.CreatedUser) + if marketing.CreatedUser.Id != 0 { + mapped := userDTO.ToUserBaseDTO(marketing.CreatedUser) createdUser = &mapped } - var marketing *MarketingBaseDTO - if e.Marketing != nil && e.Marketing.Id != 0 { - var marketingProducts []MarketingProductDTO - if len(e.Marketing.Products) > 0 { - marketingProducts = make([]MarketingProductDTO, len(e.Marketing.Products)) - for i, product := range e.Marketing.Products { - marketingProducts[i] = ToMarketingProductDTO(product) - } - } + var customer *customerDTO.CustomerBaseDTO + if marketing.Customer.Id != 0 { + mapped := customerDTO.ToCustomerBaseDTO(marketing.Customer) + customer = &mapped + } - marketing = &MarketingBaseDTO{ - Id: e.Marketing.Id, - SoNumber: e.Marketing.SoNumber, - SoDate: e.Marketing.SoDate, - Products: marketingProducts, + var salesPerson *userDTO.UserBaseDTO + if marketing.SalesPerson.Id != 0 { + mapped := userDTO.ToUserBaseDTO(marketing.SalesPerson) + salesPerson = &mapped + } + + var salesOrderProducts []MarketingProductDTO + if len(marketing.Products) > 0 { + salesOrderProducts = make([]MarketingProductDTO, len(marketing.Products)) + for i, product := range marketing.Products { + salesOrderProducts[i] = ToMarketingProductDTO(product) } } @@ -205,87 +199,108 @@ func ToDeliveryOrdersListDTOWithProducts(e entity.DeliveryOrders, deliveryProduc for i, dp := range deliveryProducts { deliveryProductsDTOs[i] = ToMarketingDeliveryProductDTO(dp) } + deliveryProductsDTOs = enrichDeliveryProductDTOsWithWarehouse(deliveryProductsDTOs, marketing) } - // Group delivery products by warehouse and delivery date - deliveryGroups := groupDeliveryProducts(deliveryProductsDTOs) + deliveryGroups := groupDeliveryProducts(deliveryProductsDTOs, marketing.SoNumber) - // Create delivery order DTO with summary - deliveryOrder := createDeliveryOrderDTO(deliveryGroups) + var latestApproval *approvalDTO.ApprovalBaseDTO + if marketing.LatestApproval != nil { + mapped := approvalDTO.ToApprovalDTO(*marketing.LatestApproval) + latestApproval = &mapped + } - return DeliveryOrdersListDTO{ - DeliveryOrdersBaseDTO: ToDeliveryOrdersBaseDTO(e), - SalesOrder: marketing, - DeliveryOrder: deliveryOrder, - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, + return MarketingDetailDTO{ + MarketingBaseDTO: ToMarketingBaseDTO(marketing), + SoDocs: marketing.SoDocs, + Customer: customer, + SalesPerson: salesPerson, + SalesOrder: salesOrderProducts, + DeliveryOrder: deliveryGroups, + CreatedUser: createdUser, + CreatedAt: marketing.CreatedAt, + UpdatedAt: marketing.UpdatedAt, + LatestApproval: latestApproval, } } -func ToDeliveryOrdersListDTOs(e []entity.DeliveryOrders) []DeliveryOrdersListDTO { - result := make([]DeliveryOrdersListDTO, len(e)) - for i, r := range e { - result[i] = ToDeliveryOrdersListDTO(r) +func ToMarketingListDTOs(marketings []entity.Marketing) []MarketingListDTO { + result := make([]MarketingListDTO, len(marketings)) + for i, m := range marketings { + result[i] = ToMarketingListDTO(&m, []entity.MarketingDeliveryProduct{}) } return result } -func ToDeliveryOrdersDetailDTO(e entity.DeliveryOrders) DeliveryOrdersDetailDTO { - return DeliveryOrdersDetailDTO{ - DeliveryOrdersListDTO: ToDeliveryOrdersListDTO(e), +func enrichDeliveryProductDTOsWithWarehouse(deliveryProductDTOs []MarketingDeliveryProductDTO, marketing *entity.Marketing) []MarketingDeliveryProductDTO { + if len(deliveryProductDTOs) == 0 || marketing == nil || len(marketing.Products) == 0 { + return deliveryProductDTOs } + + productMap := make(map[uint]*entity.MarketingProduct) + for i := range marketing.Products { + productMap[marketing.Products[i].Id] = &marketing.Products[i] + } + + for i := range deliveryProductDTOs { + if product, exists := productMap[deliveryProductDTOs[i].MarketingProductId]; exists { + if product.ProductWarehouse.Id != 0 { + mapped := productwarehouseDTO.ToProductWarehouseNestedDTO(product.ProductWarehouse) + deliveryProductDTOs[i].ProductWarehouse = &mapped + } + } + } + + return deliveryProductDTOs } -// groupDeliveryProducts groups delivery products by warehouse and delivery date -func groupDeliveryProducts(products []MarketingDeliveryProductDTO) []DeliveryGroupDTO { - // Create a map to group products +func groupDeliveryProducts(products []MarketingDeliveryProductDTO, soNumber string) []DeliveryGroupDTO { groupMap := make(map[string]*DeliveryGroupDTO) for _, product := range products { - // Create unique key for grouping (warehouse_id + delivery_date) - // Since we're working with DTO, we need to handle the warehouse id differently - warehouseId := uint(0) - warehouseName := "" - - // Extract warehouse info from product (assuming it might be available in future) - // For now, we'll use a simple grouping by delivery date and vehicle number - var key string - if product.DeliveryDate != nil { - key = fmt.Sprintf("%s_%s", product.DeliveryDate.Format("2006-01-02"), product.VehicleNumber) - } else { - key = fmt.Sprintf("no_date_%s", product.VehicleNumber) + if product.DeliveryDate == nil { + continue } - // Get or create group + var warehouseId uint + var warehouseName string + if product.ProductWarehouse != nil { + warehouseId = product.ProductWarehouse.Warehouse.Id + warehouseName = product.ProductWarehouse.Warehouse.Name + } + + key := fmt.Sprintf("%d_%s", warehouseId, product.DeliveryDate.Format("2006-01-02")) + group, exists := groupMap[key] if !exists { group = &DeliveryGroupDTO{ - WarehouseId: warehouseId, - WarehouseName: warehouseName, - DeliveryDate: product.DeliveryDate, - VehicleNumber: product.VehicleNumber, - TotalQty: 0, - TotalWeight: 0, - TotalPrice: 0, + DeliveryDate: product.DeliveryDate, + Warehouse: &productwarehouseDTO.WarehouseBaseDTO{ + Id: warehouseId, + Name: warehouseName, + }, + Deliveries: []DeliveryItemDTO{}, } - groupMap[key] = group } - // Update totals - group.TotalQty += product.Qty - group.TotalWeight += product.TotalWeight - group.TotalPrice += product.TotalPrice + deliveryItem := DeliveryItemDTO{ + ProductWarehouse: product.ProductWarehouse, + Qty: product.Qty, + UnitPrice: product.UnitPrice, + TotalWeight: product.TotalWeight, + AvgWeight: product.AvgWeight, + TotalPrice: product.TotalPrice, + VehicleNumber: product.VehicleNumber, + } + group.Deliveries = append(group.Deliveries, deliveryItem) } - // Convert map to slice var groups []DeliveryGroupDTO for _, group := range groupMap { groups = append(groups, *group) } - // Sort groups by delivery date sort.Slice(groups, func(i, j int) bool { if groups[i].DeliveryDate == nil || groups[j].DeliveryDate == nil { return false @@ -293,16 +308,20 @@ func groupDeliveryProducts(products []MarketingDeliveryProductDTO) []DeliveryGro return groups[i].DeliveryDate.Before(*groups[j].DeliveryDate) }) + for i := range groups { + if groups[i].DeliveryDate != nil { + dateStr := groups[i].DeliveryDate.Format("20060102") + groups[i].DoNumber = fmt.Sprintf("%s-%s-%d", soNumber, dateStr, groups[i].Warehouse.Id) + } + } + return groups } -// createDeliveryOrderDTO creates delivery order DTO -func createDeliveryOrderDTO(deliveryGroups []DeliveryGroupDTO) *DeliveryOrderDTO { - if len(deliveryGroups) == 0 { - return nil - } - - return &DeliveryOrderDTO{ - DeliveryGroups: deliveryGroups, +// getVehicleNumber mengambil vehicle number dari DeliveryProduct jika ada +func getVehicleNumber(e entity.MarketingProduct) string { + if e.DeliveryProduct != nil && e.DeliveryProduct.VehicleNumber != "" { + return e.DeliveryProduct.VehicleNumber } + return "" } diff --git a/internal/modules/marketing/delivery-orderss/module.go b/internal/modules/marketing/delivery-orderss/module.go index 7fcc8ccc..99bd8396 100644 --- a/internal/modules/marketing/delivery-orderss/module.go +++ b/internal/modules/marketing/delivery-orderss/module.go @@ -1,6 +1,8 @@ package delivery_orderss import ( + "fmt" + "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "gorm.io/gorm" @@ -8,26 +10,29 @@ import ( commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" rMarketingDeliveryProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/marketing-delivery-products/repositories" - rDeliveryOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/repositories" sDeliveryOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/services" rMarketing "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" - rMarketingProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + "gitlab.com/mbugroup/lti-api.git/internal/utils" ) type DeliveryOrdersModule struct{} func (DeliveryOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { - deliveryOrdersRepo := rDeliveryOrders.NewDeliveryOrdersRepository(db) marketingRepo := rMarketing.NewMarketingRepository(db) - marketingProductRepo := rMarketingProduct.NewMarketingProductRepository(db) + marketingProductRepo := rMarketing.NewMarketingProductRepository(db) marketingDeliveryProductRepo := rMarketingDeliveryProduct.NewMarketingDeliveryProductRepository(db) userRepo := rUser.NewUserRepository(db) approvalRepo := commonRepo.NewApprovalRepository(db) approvalSvc := commonSvc.NewApprovalService(approvalRepo) - deliveryOrdersService := sDeliveryOrders.NewDeliveryOrdersService(deliveryOrdersRepo, marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, approvalSvc, validate) + // Register workflow steps for MARKETINGS approval + if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowMarketing, utils.MarketingApprovalSteps); err != nil { + panic(fmt.Sprintf("failed to register marketing approval workflow: %v", err)) + } + + deliveryOrdersService := sDeliveryOrders.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, approvalSvc, validate) userService := sUser.NewUserService(userRepo, validate) DeliveryOrdersRoutes(router, userService, deliveryOrdersService) diff --git a/internal/modules/marketing/delivery-orderss/route.go b/internal/modules/marketing/delivery-orderss/route.go index 8d58b70e..09e48f29 100644 --- a/internal/modules/marketing/delivery-orderss/route.go +++ b/internal/modules/marketing/delivery-orderss/route.go @@ -12,6 +12,10 @@ import ( func DeliveryOrdersRoutes(v1 fiber.Router, u user.UserService, s deliveryOrders.DeliveryOrdersService) { ctrl := controller.NewDeliveryOrdersController(s) + v1.Get("/", ctrl.GetAll) + v1.Get("/:id", ctrl.GetOne) + + // Sisanya di group /delivery-orders route := v1.Group("/delivery-orders") // route.Get("/", m.Auth(u), ctrl.GetAll) @@ -20,10 +24,7 @@ func DeliveryOrdersRoutes(v1 fiber.Router, u user.UserService, s deliveryOrders. // 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) route.Patch("/:id", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) - route.Post("/approvals", ctrl.Approval) + } diff --git a/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go b/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go index f090ac01..45b11d6d 100644 --- a/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go +++ b/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go @@ -1,6 +1,7 @@ package service import ( + "context" "errors" "fmt" "time" @@ -9,8 +10,8 @@ import ( commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" marketingDeliveryProductRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/marketing-delivery-products/repositories" + productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/dto" - repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/validations" marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" @@ -22,18 +23,15 @@ import ( ) type DeliveryOrdersService interface { - GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.DeliveryOrdersListDTO, int64, error) - GetOne(ctx *fiber.Ctx, id uint) (*dto.DeliveryOrdersListDTO, error) - CreateOne(ctx *fiber.Ctx, req *validation.Create) (*dto.DeliveryOrdersListDTO, error) - UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*dto.DeliveryOrdersListDTO, error) - DeleteOne(ctx *fiber.Ctx, id uint) error - Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.DeliveryOrders, error) + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.MarketingListDTO, int64, error) + GetOne(ctx *fiber.Ctx, id uint) (*dto.MarketingDetailDTO, error) + CreateOne(ctx *fiber.Ctx, req *validation.Create) (*dto.MarketingDetailDTO, error) + UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*dto.MarketingDetailDTO, error) } type deliveryOrdersService struct { Log *logrus.Logger Validate *validator.Validate - Repository repository.DeliveryOrdersRepository MarketingRepo marketingRepo.MarketingRepository MarketingProductRepo marketingRepo.MarketingProductRepository MarketingDeliveryProductRepo marketingDeliveryProductRepo.MarketingDeliveryProductRepository @@ -41,7 +39,6 @@ type deliveryOrdersService struct { } func NewDeliveryOrdersService( - repo repository.DeliveryOrdersRepository, marketingRepo marketingRepo.MarketingRepository, marketingProductRepo marketingRepo.MarketingProductRepository, marketingDeliveryProductRepo marketingDeliveryProductRepo.MarketingDeliveryProductRepository, @@ -51,7 +48,6 @@ func NewDeliveryOrdersService( return &deliveryOrdersService{ Log: utils.Log, Validate: validate, - Repository: repo, MarketingRepo: marketingRepo, MarketingProductRepo: marketingProductRepo, MarketingDeliveryProductRepo: marketingDeliveryProductRepo, @@ -60,20 +56,45 @@ func NewDeliveryOrdersService( } func (s deliveryOrdersService) withRelations(db *gorm.DB) *gorm.DB { - return db.Preload("CreatedUser"). - Preload("Marketing") + return db. + Preload("CreatedUser"). + Preload("Customer"). + Preload("SalesPerson"). + Preload("Products.ProductWarehouse.Product"). + Preload("Products.ProductWarehouse.Warehouse"). + Preload("Products.DeliveryProduct") } -func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.DeliveryOrdersListDTO, int64, error) { +func (s deliveryOrdersService) getMarketingWithDeliveries(c *fiber.Ctx, marketingId uint) (*dto.MarketingDetailDTO, error) { + marketing, err := s.MarketingRepo.GetByID(c.Context(), marketingId, s.withRelations) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing") + } + + latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, marketingId, nil) + if err != nil { + } + marketing.LatestApproval = latestApproval + + allDeliveryProducts, err := s.MarketingDeliveryProductRepo.GetByMarketingId(c.Context(), marketingId) + if err != nil { + allDeliveryProducts = []entity.MarketingDeliveryProduct{} + } + + responseDTO := dto.ToMarketingDetailDTO(marketing, allDeliveryProducts) + return &responseDTO, nil +} + +func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.MarketingListDTO, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err } offset := (params.Page - 1) * params.Limit - // Fetch dari Marketing, bukan DeliveryOrders marketings, total, err := s.MarketingRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { - db = db.Preload("CreatedUser"). + db = db. + Preload("CreatedUser"). Preload("Customer"). Preload("SalesPerson"). Preload("Products.ProductWarehouse") @@ -87,101 +108,58 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([ s.Log.Errorf("Failed to get marketings: %+v", err) return nil, 0, err } - - // Load delivery products untuk setiap marketing - result := make([]dto.DeliveryOrdersListDTO, len(marketings)) - for i, marketing := range marketings { - // Get marketing delivery products menggunakan repository method - allDeliveryProducts, err := s.MarketingDeliveryProductRepo.GetByMarketingId(c.Context(), marketing.Id) + for i := range marketings { + latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, marketings[i].Id, nil) if err != nil { - s.Log.Errorf("Failed to load delivery products for marketing %d: %+v", marketing.Id, err) - allDeliveryProducts = []entity.MarketingDeliveryProduct{} // Set empty slice jika gagal + s.Log.Warnf("Failed to load approval for marketing %d: %+v", marketings[i].Id, err) } + marketings[i].LatestApproval = latestApproval + } - // Build response DTO - deliveryOrderResponse := &entity.DeliveryOrders{ - MarketingId: marketing.Id, - CreatedUser: &marketing.CreatedUser, - Marketing: &marketing, - DeliveryProducts: allDeliveryProducts, - } - - result[i] = dto.ToDeliveryOrdersListDTOWithProducts(*deliveryOrderResponse, allDeliveryProducts) + result := make([]dto.MarketingListDTO, len(marketings)) + for i, marketing := range marketings { + result[i] = dto.ToMarketingListDTO(&marketing, []entity.MarketingDeliveryProduct{}) } return result, total, nil } -func (s deliveryOrdersService) GetOne(c *fiber.Ctx, id uint) (*dto.DeliveryOrdersListDTO, error) { - // Fetch Marketing by ID, bukan DeliveryOrders - marketing, err := s.MarketingRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { - return db.Preload("CreatedUser"). - Preload("Customer"). - Preload("SalesPerson"). - Preload("Products.ProductWarehouse.Product"). - Preload("Products.ProductWarehouse.Warehouse") - }) +func (s deliveryOrdersService) GetOne(c *fiber.Ctx, id uint) (*dto.MarketingDetailDTO, error) { + + marketing, err := s.MarketingRepo.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Marketing not found") } if err != nil { - s.Log.Errorf("Failed get marketing by id: %+v", err) return nil, err } - // Get marketing delivery products menggunakan repository method allDeliveryProducts, err := s.MarketingDeliveryProductRepo.GetByMarketingId(c.Context(), id) if err != nil { - s.Log.Errorf("Failed to load delivery products for marketing %d: %+v", id, err) - allDeliveryProducts = []entity.MarketingDeliveryProduct{} // Set empty slice jika gagal + allDeliveryProducts = []entity.MarketingDeliveryProduct{} } - // Debug: Log jumlah delivery products - s.Log.Infof("Found %d delivery products for marketing %d", len(allDeliveryProducts), id) + if s.ApprovalSvc != nil { + approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), utils.ApprovalWorkflowMarketing, marketing.Id, func(db *gorm.DB) *gorm.DB { + return db.Preload("ActionUser") + }) + if err != nil { - // Jika tidak ada delivery products, buat dummy data untuk testing - if len(allDeliveryProducts) == 0 && len(marketing.Products) > 0 { - s.Log.Infof("Creating dummy delivery products for testing") - for i, product := range marketing.Products { - deliveryDate := marketing.SoDate.AddDate(0, 0, i+7) // 7 hari setelah SO - dummyDeliveryProduct := entity.MarketingDeliveryProduct{ - Id: uint(i + 1), - MarketingProductId: product.Id, - Qty: product.Qty / 2, // Setengah dari qty asli - UnitPrice: product.UnitPrice, - TotalWeight: product.TotalWeight / 2, - AvgWeight: product.AvgWeight, - TotalPrice: (product.Qty / 2) * product.UnitPrice, - DeliveryDate: &deliveryDate, - VehicleNumber: fmt.Sprintf("B%04d%s", (i+1)*1000, "ABC"), + } else if len(approvals) > 0 { + if marketing.LatestApproval == nil { + latest := approvals[len(approvals)-1] + marketing.LatestApproval = &latest } - allDeliveryProducts = append(allDeliveryProducts, dummyDeliveryProduct) + } else { + marketing.LatestApproval = nil } - s.Log.Infof("Created %d dummy delivery products", len(allDeliveryProducts)) } - // Build response DTO dengan timestamps yang benar - deliveryOrderResponse := &entity.DeliveryOrders{ - MarketingId: marketing.Id, - CreatedUser: &marketing.CreatedUser, - Marketing: marketing, - DeliveryProducts: allDeliveryProducts, - CreatedAt: marketing.CreatedAt, - UpdatedAt: marketing.UpdatedAt, - } - - // Set delivery_date dari delivery products atau fallback ke marketing date - if len(allDeliveryProducts) > 0 && allDeliveryProducts[0].DeliveryDate != nil { - deliveryOrderResponse.DeliveryDate = *allDeliveryProducts[0].DeliveryDate - } else { - deliveryOrderResponse.DeliveryDate = marketing.SoDate - } - - responseDTO := dto.ToDeliveryOrdersListDTOWithProducts(*deliveryOrderResponse, allDeliveryProducts) + responseDTO := dto.ToMarketingDetailDTO(marketing, allDeliveryProducts) return &responseDTO, nil } -func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*dto.DeliveryOrdersListDTO, error) { +func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*dto.MarketingDetailDTO, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } @@ -192,14 +170,8 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) return nil, err } - var relationChecks []commonSvc.RelationCheck - for _, requestedProduct := range req.DeliveryProducts { - relationChecks = append(relationChecks, commonSvc.RelationCheck{ - Name: "MarketingProduct", ID: &requestedProduct.MarketingProductId, Exists: s.MarketingProductRepo.IdExists, - }) - } - approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.MarketingRepo.DB())) + latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, req.MarketingId, nil) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status") @@ -210,25 +182,28 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) if latestApproval.StepNumber < uint16(utils.MarketingStepSalesOrder) { return nil, fiber.NewError(fiber.StatusBadRequest, "Marketing must be approved to Sales Order step before creating delivery order") } + if latestApproval.StepNumber >= uint16(utils.MarketingDeliveryOrder) { + return nil, fiber.NewError(fiber.StatusBadRequest, "Delivery order already exists for this marketing") + } if latestApproval.Action == nil || *latestApproval.Action != entity.ApprovalActionApproved { - return nil, fiber.NewError(fiber.StatusBadRequest, "Marketing is not approved for delivery") + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Marketing is not approved - current status: %v", *latestApproval.Action)) } - err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction) marketingDeliveryProductRepositoryTx := marketingDeliveryProductRepo.NewMarketingDeliveryProductRepository(dbTransaction) + approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) + + allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), req.MarketingId) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("No marketing products found for marketing %d", req.MarketingId)) + } + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing products") + } for _, requestedProduct := range req.DeliveryProducts { - allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), req.MarketingId) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("No marketing products found for marketing %d", req.MarketingId)) - } - s.Log.Errorf("Failed to fetch marketing products: %+v", err) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing products") - } - var foundMarketingProduct *entity.MarketingProduct for i := range allMarketingProducts { if allMarketingProducts[i].Id == requestedProduct.MarketingProductId { @@ -248,15 +223,13 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery product") } - var itemDeliveryDate time.Time + var itemDeliveryDate *time.Time if requestedProduct.DeliveryDate != "" { parsedDate, err := utils.ParseDateString(requestedProduct.DeliveryDate) if err != nil { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid delivery date format for product %d: %v", requestedProduct.MarketingProductId, err)) } - itemDeliveryDate = parsedDate - } else { - itemDeliveryDate = time.Now() + itemDeliveryDate = &parsedDate } deliveryProduct.Qty = requestedProduct.Qty @@ -264,24 +237,22 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) deliveryProduct.AvgWeight = requestedProduct.AvgWeight deliveryProduct.TotalWeight = requestedProduct.TotalWeight deliveryProduct.TotalPrice = requestedProduct.TotalPrice - deliveryProduct.DeliveryDate = &itemDeliveryDate + deliveryProduct.DeliveryDate = itemDeliveryDate deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber + if requestedProduct.Qty > 0 { + if err := s.validateAndReduceProductWarehouse(c.Context(), dbTransaction, foundMarketingProduct, requestedProduct.Qty); err != nil { + return err + } + } if err := marketingDeliveryProductRepositoryTx.UpdateOne(c.Context(), deliveryProduct.Id, deliveryProduct, nil); err != nil { - s.Log.Errorf("Failed to update marketing delivery product: %+v", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product") } - s.Log.Infof("Updated delivery product %d: qty=%v, unitPrice=%v, totalPrice=%v", deliveryProduct.Id, requestedProduct.Qty, requestedProduct.UnitPrice, requestedProduct.TotalPrice) } - approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) actorID := uint(1) // TODO: ambil dari auth context - approvalAction := entity.ApprovalActionCreated - var notes *string - if req.Notes != "" { - notes = &req.Notes - } + approvalAction := entity.ApprovalActionApproved if _, err := approvalSvcTx.CreateApproval( c.Context(), utils.ApprovalWorkflowMarketing, @@ -289,9 +260,8 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) utils.MarketingDeliveryOrder, &approvalAction, actorID, - notes); err != nil { + nil); err != nil { if !errors.Is(err, gorm.ErrDuplicatedKey) { - s.Log.Errorf("Failed to create delivery order approval: %+v", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to create delivery order approval") } } @@ -300,76 +270,38 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) }) if err != nil { - return nil, err + if fiberErr, ok := err.(*fiber.Error); ok { + return nil, fiberErr + } + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create delivery order") } - marketing, err := s.MarketingRepo.GetByID(c.Context(), req.MarketingId, func(db *gorm.DB) *gorm.DB { - return db.Preload("CreatedUser").Preload("Products.DeliveryProduct") - }) - if err != nil { - s.Log.Errorf("Failed to fetch marketing after update: %+v", err) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch updated marketing") - } - - // Get marketing delivery products menggunakan repository method - allDeliveryProducts, err := s.MarketingDeliveryProductRepo.GetByMarketingId(c.Context(), req.MarketingId) - if err != nil { - s.Log.Errorf("Failed to load delivery products: %+v", err) - allDeliveryProducts = []entity.MarketingDeliveryProduct{} // Set empty slice jika gagal - } - - // Build response DTO - deliveryOrderResponse := &entity.DeliveryOrders{ - MarketingId: req.MarketingId, - Notes: req.Notes, - CreatedUser: &marketing.CreatedUser, - Marketing: marketing, - DeliveryProducts: allDeliveryProducts, - } - - responseDTO := dto.ToDeliveryOrdersListDTOWithProducts(*deliveryOrderResponse, allDeliveryProducts) - return &responseDTO, nil + return s.getMarketingWithDeliveries(c, req.MarketingId) } -func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*dto.DeliveryOrdersListDTO, error) { +func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*dto.MarketingDetailDTO, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } - // Validate bahwa marketing ID yang di-update ada (id parameter adalah marketing_id untuk delivery orders) if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Marketing", ID: &id, Exists: s.MarketingRepo.IdExists}, ); err != nil { return nil, err } - // Validate delivery products jika ada - if len(req.DeliveryProducts) > 0 { - for _, requestedProduct := range req.DeliveryProducts { - if err := commonSvc.EnsureRelations(c.Context(), - commonSvc.RelationCheck{Name: "MarketingProduct", ID: &requestedProduct.MarketingProductId, Exists: s.MarketingProductRepo.IdExists}, - ); err != nil { - return nil, err - } - } - } + err := s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { - err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction) marketingDeliveryProductRepositoryTx := marketingDeliveryProductRepo.NewMarketingDeliveryProductRepository(dbTransaction) - // Update delivery products jika ada dalam request + allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing products") + } + if len(req.DeliveryProducts) > 0 { for _, requestedProduct := range req.DeliveryProducts { - // Validate bahwa marketing product ada untuk marketing ini - allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), id) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("No marketing products found for marketing %d", id)) - } - s.Log.Errorf("Failed to fetch marketing products: %+v", err) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing products") - } var foundMarketingProduct *entity.MarketingProduct for i := range allMarketingProducts { @@ -382,7 +314,6 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, i return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing product %d not found for this marketing", requestedProduct.MarketingProductId)) } - // Get existing delivery product deliveryProduct, err := marketingDeliveryProductRepositoryTx.GetByMarketingProductID(c.Context(), foundMarketingProduct.Id) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { @@ -391,157 +322,101 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, i return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery product") } - // Parse delivery date - var itemDeliveryDate time.Time + var itemDeliveryDate *time.Time if requestedProduct.DeliveryDate != "" { parsedDate, err := utils.ParseDateString(requestedProduct.DeliveryDate) if err != nil { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid delivery date format for product %d: %v", requestedProduct.MarketingProductId, err)) } - itemDeliveryDate = parsedDate + itemDeliveryDate = &parsedDate } else if deliveryProduct.DeliveryDate != nil { - itemDeliveryDate = *deliveryProduct.DeliveryDate - } else { - itemDeliveryDate = time.Now() + itemDeliveryDate = deliveryProduct.DeliveryDate } - // Update delivery product + oldQty := deliveryProduct.Qty deliveryProduct.Qty = requestedProduct.Qty deliveryProduct.UnitPrice = requestedProduct.UnitPrice deliveryProduct.AvgWeight = requestedProduct.AvgWeight deliveryProduct.TotalWeight = requestedProduct.TotalWeight deliveryProduct.TotalPrice = requestedProduct.TotalPrice - deliveryProduct.DeliveryDate = &itemDeliveryDate + deliveryProduct.DeliveryDate = itemDeliveryDate deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber - if err := marketingDeliveryProductRepositoryTx.UpdateOne(c.Context(), deliveryProduct.Id, deliveryProduct, nil); err != nil { - s.Log.Errorf("Failed to update marketing delivery product: %+v", err) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product") + qtyChange := requestedProduct.Qty - oldQty + if qtyChange > 0 { + if err := s.validateAndReduceProductWarehouse(c.Context(), dbTransaction, foundMarketingProduct, qtyChange); err != nil { + return err + } + } else if qtyChange < 0 { + if err := s.restoreProductWarehouseStock(c.Context(), dbTransaction, foundMarketingProduct, -qtyChange); err != nil { + return err + } } - s.Log.Infof("Updated delivery product %d: qty=%v, unitPrice=%v, totalPrice=%v", deliveryProduct.Id, requestedProduct.Qty, requestedProduct.UnitPrice, requestedProduct.TotalPrice) + if err := marketingDeliveryProductRepositoryTx.UpdateOne(c.Context(), deliveryProduct.Id, deliveryProduct, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product") + } } } return nil }) - if err != nil { - return nil, err - } - - // Fetch updated marketing with delivery products - marketing, err := s.MarketingRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { - return db.Preload("CreatedUser").Preload("Products.DeliveryProduct") - }) - if err != nil { - s.Log.Errorf("Failed to fetch marketing after update: %+v", err) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch updated marketing") - } - - // Get marketing delivery products menggunakan repository method - allDeliveryProducts, err := s.MarketingDeliveryProductRepo.GetByMarketingId(c.Context(), id) - if err != nil { - s.Log.Errorf("Failed to load delivery products: %+v", err) - allDeliveryProducts = []entity.MarketingDeliveryProduct{} // Set empty slice jika gagal - } - - // Build response DTO - deliveryOrderResponse := &entity.DeliveryOrders{ - MarketingId: id, - Notes: req.Notes, - CreatedUser: &marketing.CreatedUser, - Marketing: marketing, - DeliveryProducts: allDeliveryProducts, - } - - responseDTO := dto.ToDeliveryOrdersListDTOWithProducts(*deliveryOrderResponse, allDeliveryProducts) - return &responseDTO, nil -} - -func (s deliveryOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.DeliveryOrders, error) { - if err := s.Validate.Struct(req); err != nil { - return nil, err - } - - var action entity.ApprovalAction - switch req.Action { - case "APPROVED": - action = entity.ApprovalActionApproved - case "REJECTED": - action = entity.ApprovalActionRejected - default: - return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED") - } - - approvableIDs := utils.UniqueUintSlice(req.ApprovableIds) - if len(approvableIDs) == 0 { - return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id") - } - - // Validate semua delivery order ada - for _, id := range approvableIDs { - _, err := s.Repository.GetByID(c.Context(), id, nil) - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Delivery order %d not found", id)) - } - if err != nil { - s.Log.Errorf("Failed to get delivery order %d: %+v", id, err) - return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get delivery order %d", id)) - } - } - - err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTx *gorm.DB) error { - approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTx)) - - for _, approvableID := range approvableIDs { - actorID := uint(1) // TODO: get from auth context - if _, err := approvalSvc.CreateApproval( - c.Context(), - utils.ApprovalWorkflowMarketing, - approvableID, - utils.MarketingDeliveryOrder, - &action, - actorID, - req.Notes, - ); err != nil { - s.Log.Errorf("Failed to create approval for %d: %+v", approvableID, err) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval") - } - } - return nil - }) - if err != nil { if fiberErr, ok := err.(*fiber.Error); ok { return nil, fiberErr } - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval") + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery order") } - updated := make([]entity.DeliveryOrders, 0, len(approvableIDs)) - for _, id := range approvableIDs { - deliveryOrder, err := s.Repository.GetByID(c.Context(), id, s.withRelations) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Delivery order %d not found", id)) - } - s.Log.Errorf("Failed to get delivery order %d: %+v", id, err) - return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get delivery order %d", id)) - } - updated = append(updated, *deliveryOrder) - } - - return updated, nil + return s.getMarketingWithDeliveries(c, id) } -func (s deliveryOrdersService) DeleteOne(c *fiber.Ctx, id uint) error { - if err := s.Repository.DeleteOne(c.Context(), id); err != nil { +func (s deliveryOrdersService) validateAndReduceProductWarehouse(ctx context.Context, tx *gorm.DB, marketingProduct *entity.MarketingProduct, qtyDeliver float64) error { + if marketingProduct == nil || marketingProduct.ProductWarehouseId == 0 { + return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found") + } + + pwRepo := productWarehouseRepo.NewProductWarehouseRepository(tx) + + pw, err := pwRepo.GetByID(ctx, marketingProduct.ProductWarehouseId, nil) + if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, "DeliveryOrders not found") + return fiber.NewError(fiber.StatusNotFound, "Product warehouse not found") } - s.Log.Errorf("Failed to delete deliveryOrders: %+v", err) - return err + return fiber.NewError(fiber.StatusInternalServerError, "Failed to check stock") + } + + if pw.Quantity < qtyDeliver { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock for warehouse - available: %.2f, requested: %.2f", pw.Quantity, qtyDeliver)) + } + + pw.Quantity = pw.Quantity - qtyDeliver + if err := pwRepo.UpdateOne(ctx, pw.Id, pw, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update stock") } return nil } + +func (s deliveryOrdersService) restoreProductWarehouseStock(ctx context.Context, tx *gorm.DB, marketingProduct *entity.MarketingProduct, qtyRestore float64) error { + if marketingProduct == nil || marketingProduct.ProductWarehouseId == 0 { + return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found") + } + + pwRepo := productWarehouseRepo.NewProductWarehouseRepository(tx) + pw, err := pwRepo.GetByID(ctx, marketingProduct.ProductWarehouseId, nil) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Product warehouse not found") + } + + return fiber.NewError(fiber.StatusInternalServerError, "Failed to check stock") + } + + pw.Quantity = pw.Quantity + qtyRestore + if err := pwRepo.UpdateOne(ctx, pw.Id, pw, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update stock") + } + + return nil +} diff --git a/internal/modules/marketing/delivery-orderss/validations/delivery-orders.validation.go b/internal/modules/marketing/delivery-orderss/validations/delivery-orders.validation.go index a80ad8d5..3317e952 100644 --- a/internal/modules/marketing/delivery-orderss/validations/delivery-orders.validation.go +++ b/internal/modules/marketing/delivery-orderss/validations/delivery-orders.validation.go @@ -2,26 +2,22 @@ package validation type DeliveryProduct struct { MarketingProductId uint `json:"marketing_product_id" validate:"required,gt=0"` - Qty float64 `json:"qty" validate:"required,gt=0"` - UnitPrice float64 `json:"unit_price" validate:"required,gt=0"` - AvgWeight float64 `json:"avg_weight" validate:"required,gt=0"` - TotalWeight float64 `json:"total_weight" validate:"required,gt=0"` - TotalPrice float64 `json:"total_price" validate:"required,gt=0"` + Qty float64 `json:"qty" validate:"omitempty,gte=0"` + UnitPrice float64 `json:"unit_price" validate:"omitempty,gte=0"` + AvgWeight float64 `json:"avg_weight" validate:"omitempty,gte=0"` + TotalWeight float64 `json:"total_weight" validate:"omitempty,gte=0"` + TotalPrice float64 `json:"total_price" validate:"omitempty,gte=0"` DeliveryDate string `json:"delivery_date" validate:"omitempty,datetime=2006-01-02"` VehicleNumber string `json:"vehicle_number" validate:"omitempty,max=50"` } type Create struct { MarketingId uint `json:"marketing_id" validate:"required,gt=0"` - DeliveryDate string `json:"delivery_date" validate:"omitempty,datetime=2006-01-02"` DeliveryProducts []DeliveryProduct `json:"delivery_products" validate:"required,min=1,dive"` - Notes string `json:"notes" validate:"omitempty,max=500"` } type Update struct { - DeliveryDate string `json:"delivery_date" validate:"omitempty,datetime=2006-01-02"` DeliveryProducts []DeliveryProduct `json:"delivery_products" validate:"omitempty,min=1,dive"` - Notes string `json:"notes" validate:"omitempty,max=500"` } type Query struct { diff --git a/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go b/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go index a5365787..16d3b5be 100644 --- a/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go +++ b/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go @@ -1,7 +1,6 @@ package controller import ( - "math" "strconv" "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/dto" @@ -22,65 +21,6 @@ func NewSalesOrdersController(salesOrdersService service.SalesOrdersService) *Sa } } -func (u *SalesOrdersController) GetAll(c *fiber.Ctx) error { - query := &validation.Query{ - Page: c.QueryInt("page", 1), - Limit: c.QueryInt("limit", 10), - Search: c.Query("search", ""), - } - - if query.Page < 1 || query.Limit < 1 { - return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") - } - - result, totalResults, err := u.SalesOrdersService.GetAll(c, query) - if err != nil { - return err - } - - // Convert marketing data to sales orders DTOs with products - salesOrdersDTOs := make([]dto.SalesOrdersListDTO, len(result)) - for i, marketing := range result { - salesOrdersDTOs[i] = dto.ToSalesOrdersListDTOFromMarketing(marketing) - } - - return c.Status(fiber.StatusOK). - JSON(response.SuccessWithPaginate[dto.SalesOrdersListDTO]{ - Code: fiber.StatusOK, - Status: "success", - Message: "Get all salesOrderss successfully", - Meta: response.Meta{ - Page: query.Page, - Limit: query.Limit, - TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), - TotalResults: totalResults, - }, - Data: salesOrdersDTOs, - }) -} - -func (u *SalesOrdersController) 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.SalesOrdersService.GetOne(c, uint(id)) - if err != nil { - return err - } - - return c.Status(fiber.StatusOK). - JSON(response.Success{ - Code: fiber.StatusOK, - Status: "success", - Message: "Get salesOrders successfully", - Data: dto.ToSalesOrdersListDTOFromMarketing(*result), - }) -} - func (u *SalesOrdersController) CreateOne(c *fiber.Ctx) error { req := new(validation.Create) @@ -115,13 +55,7 @@ func (u *SalesOrdersController) UpdateOne(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } - _, err = u.SalesOrdersService.UpdateOne(c, req, uint(id)) - if err != nil { - return err - } - - // Fetch full updated data for response - result, err := u.SalesOrdersService.GetOne(c, uint(id)) + result, err := u.SalesOrdersService.UpdateOne(c, req, uint(id)) if err != nil { return err } diff --git a/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go b/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go index 79bc6453..03a0d59a 100644 --- a/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go +++ b/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go @@ -4,111 +4,37 @@ import ( "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" - approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" - customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto" - productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto" - warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" - userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" - "gitlab.com/mbugroup/lti-api.git/internal/utils" - approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" + productWarehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/dto" ) // === DTO Structs === -type SalesOrdersBaseDTO struct { - Id uint `json:"id"` - Name string `json:"name"` -} - type MarketingProductDTO struct { - Id uint `json:"id"` - Qty float64 `json:"qty"` - UnitPrice float64 `json:"unit_price"` - AvgWeight float64 `json:"avg_weight"` - TotalWeight float64 `json:"total_weight"` - TotalPrice float64 `json:"total_price"` - ProductWarehouse *struct { - Id uint `json:"id"` - Product *productDTO.ProductBaseDTO `json:"product,omitempty"` - Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"` - } `json:"product_warehouse,omitempty"` - DeliveryProduct *MarketingDeliveryProductDTO `json:"delivery_product,omitempty"` -} - -type MarketingDeliveryProductDTO struct { - Id uint `json:"id"` - MarketingProductId uint `json:"marketing_product_id"` - Qty float64 `json:"qty"` - UnitPrice float64 `json:"unit_price"` - TotalWeight float64 `json:"total_weight"` - AvgWeight float64 `json:"avg_weight"` - TotalPrice float64 `json:"total_price"` - DeliveryDate *time.Time `json:"delivery_date"` - VehicleNumber string `json:"vehicle_number"` + Id uint `json:"id"` + Qty float64 `json:"qty"` + UnitPrice float64 `json:"unit_price"` + AvgWeight float64 `json:"avg_weight"` + TotalWeight float64 `json:"total_weight"` + TotalPrice float64 `json:"total_price"` + ProductWarehouse *productWarehouseDTO.ProductWarehousNestedDTO `json:"product_warehouse,omitempty"` } type SalesOrdersListDTO struct { - SalesOrdersBaseDTO - CustomerId uint `json:"customer_id,omitempty"` - Customer *customerDTO.CustomerBaseDTO `json:"customer,omitempty"` - SoDate *time.Time `json:"so_date,omitempty"` - SalesPersonId uint `json:"sales_person_id,omitempty"` - Notes string `json:"notes,omitempty"` - MarketingProducts []MarketingProductDTO `json:"marketing_products,omitempty"` - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Approval approvalDTO.ApprovalBaseDTO `json:"approval"` -} - -type SalesOrdersDetailDTO struct { - SalesOrdersListDTO + Id uint `json:"id"` + SoNumber string `json:"so_number"` + SoDate time.Time `json:"so_date"` + Notes string `json:"notes,omitempty"` + SalesOrder []MarketingProductDTO `json:"sales_order,omitempty"` } // === Mapper Functions === -func ToSalesOrdersBaseDTO(e entity.SalesOrders) SalesOrdersBaseDTO { - return SalesOrdersBaseDTO{ - Id: e.Id, - Name: e.Name, - } -} - func ToMarketingProductDTO(e entity.MarketingProduct) MarketingProductDTO { - var productWarehouse *struct { - Id uint `json:"id"` - Product *productDTO.ProductBaseDTO `json:"product,omitempty"` - Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"` - } + var productWarehouse *productWarehouseDTO.ProductWarehousNestedDTO if e.ProductWarehouse.Id != 0 { - product := (*productDTO.ProductBaseDTO)(nil) - if e.ProductWarehouse.Product.Id != 0 { - mapped := productDTO.ToProductBaseDTO(e.ProductWarehouse.Product) - product = &mapped - } - - warehouse := (*warehouseDTO.WarehouseBaseDTO)(nil) - if e.ProductWarehouse.Warehouse.Id != 0 { - mapped := warehouseDTO.ToWarehouseBaseDTO(e.ProductWarehouse.Warehouse) - warehouse = &mapped - } - - productWarehouse = &struct { - Id uint `json:"id"` - Product *productDTO.ProductBaseDTO `json:"product,omitempty"` - Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"` - }{ - Id: e.ProductWarehouse.Id, - Product: product, - Warehouse: warehouse, - } - } - - var deliveryProduct *MarketingDeliveryProductDTO - if e.DeliveryProduct != nil && e.DeliveryProduct.Id != 0 { - mapped := ToMarketingDeliveryProductDTO(*e.DeliveryProduct) - deliveryProduct = &mapped + mapped := productWarehouseDTO.ToProductWarehouseNestedDTO(e.ProductWarehouse) + productWarehouse = &mapped } return MarketingProductDTO{ @@ -119,139 +45,38 @@ func ToMarketingProductDTO(e entity.MarketingProduct) MarketingProductDTO { TotalWeight: e.TotalWeight, TotalPrice: e.TotalPrice, ProductWarehouse: productWarehouse, - DeliveryProduct: deliveryProduct, } } -func ToMarketingDeliveryProductDTO(e entity.MarketingDeliveryProduct) MarketingDeliveryProductDTO { - return MarketingDeliveryProductDTO{ - Id: e.Id, - MarketingProductId: e.MarketingProductId, - Qty: e.Qty, - UnitPrice: e.UnitPrice, - TotalWeight: e.TotalWeight, - AvgWeight: e.AvgWeight, - TotalPrice: e.TotalPrice, - DeliveryDate: e.DeliveryDate, - VehicleNumber: e.VehicleNumber, - } -} - -func defaultSalesOrdersLatestApproval(e entity.SalesOrders) approvalDTO.ApprovalBaseDTO { - result := approvalDTO.ApprovalBaseDTO{} - - step := utils.MarketingStepPengajuan - if step > 0 { - result.StepNumber = uint16(step) - if label, ok := approvalutils.ApprovalStepName(utils.ApprovalWorkflowMarketing, step); ok { - result.StepName = label - } else if label, ok := utils.MarketingApprovalSteps[step]; ok { - result.StepName = label - } - } - - if e.CreatedUser.Id != 0 { - result.ActionBy = userDTO.ToUserBaseDTO(e.CreatedUser) - } else if e.CreatedBy != 0 { - result.ActionBy = userDTO.UserBaseDTO{ - Id: e.CreatedBy, - IdUser: int64(e.CreatedBy), - } - } - - return result -} - func ToSalesOrdersListDTO(e entity.SalesOrders) SalesOrdersListDTO { - var createdUser *userDTO.UserBaseDTO - if e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(e.CreatedUser) - createdUser = &mapped - } - - approval := defaultSalesOrdersLatestApproval(e) - if e.LatestApproval != nil { - approval = approvalDTO.ToApprovalDTO(*e.LatestApproval) - } return SalesOrdersListDTO{ - SalesOrdersBaseDTO: ToSalesOrdersBaseDTO(e), - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, - Approval: approval, + Id: e.Id, + SoNumber: e.Name, + SoDate: time.Time{}, + Notes: "", + SalesOrder: []MarketingProductDTO{}, } } func ToSalesOrdersListDTOFromMarketing(e entity.Marketing) SalesOrdersListDTO { - var createdUser *userDTO.UserBaseDTO - if e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(e.CreatedUser) - createdUser = &mapped - } - - var marketingProducts []MarketingProductDTO + var salesOrder []MarketingProductDTO if len(e.Products) > 0 { - marketingProducts = make([]MarketingProductDTO, len(e.Products)) + salesOrder = make([]MarketingProductDTO, len(e.Products)) for i, product := range e.Products { - marketingProducts[i] = ToMarketingProductDTO(product) + salesOrder[i] = ToMarketingProductDTO(product) } } - var customerSummary *customerDTO.CustomerBaseDTO - if e.Customer.Id != 0 { - mapped := customerDTO.ToCustomerBaseDTO(e.Customer) - customerSummary = &mapped - } - - approval := defaultSalesOrdersLatestApprovalFromMarketing(e) - if e.LatestApproval != nil { - approval = approvalDTO.ToApprovalDTO(*e.LatestApproval) - } - return SalesOrdersListDTO{ - SalesOrdersBaseDTO: SalesOrdersBaseDTO{ - Id: e.Id, - Name: e.SoNumber, - }, - CustomerId: e.Customer.Id, - Customer: customerSummary, - SoDate: &e.SoDate, - SalesPersonId: e.SalesPersonId, - Notes: e.Notes, - MarketingProducts: marketingProducts, - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, - Approval: approval, + Id: e.Id, + SoNumber: e.SoNumber, + SoDate: e.SoDate, + Notes: e.Notes, + SalesOrder: salesOrder, } } -func defaultSalesOrdersLatestApprovalFromMarketing(e entity.Marketing) approvalDTO.ApprovalBaseDTO { - result := approvalDTO.ApprovalBaseDTO{} - - step := utils.MarketingStepPengajuan - if step > 0 { - result.StepNumber = uint16(step) - if label, ok := approvalutils.ApprovalStepName(utils.ApprovalWorkflowMarketing, step); ok { - result.StepName = label - } else if label, ok := utils.MarketingApprovalSteps[step]; ok { - result.StepName = label - } - } - - if e.CreatedUser.Id != 0 { - result.ActionBy = userDTO.ToUserBaseDTO(e.CreatedUser) - } else if e.CreatedBy != 0 { - result.ActionBy = userDTO.UserBaseDTO{ - Id: e.CreatedBy, - IdUser: int64(e.CreatedBy), - } - } - - return result -} - func ToSalesOrdersListDTOsFromMarketing(e []entity.Marketing) []SalesOrdersListDTO { result := make([]SalesOrdersListDTO, len(e)) for i, r := range e { @@ -259,17 +84,3 @@ func ToSalesOrdersListDTOsFromMarketing(e []entity.Marketing) []SalesOrdersListD } return result } - -func ToSalesOrdersListDTOs(e []entity.SalesOrders) []SalesOrdersListDTO { - result := make([]SalesOrdersListDTO, len(e)) - for i, r := range e { - result[i] = ToSalesOrdersListDTO(r) - } - return result -} - -func ToSalesOrdersDetailDTO(e entity.SalesOrders) SalesOrdersDetailDTO { - return SalesOrdersDetailDTO{ - SalesOrdersListDTO: ToSalesOrdersListDTO(e), - } -} diff --git a/internal/modules/marketing/sales-orders/module.go b/internal/modules/marketing/sales-orders/module.go index 35d002fc..6d1963af 100644 --- a/internal/modules/marketing/sales-orders/module.go +++ b/internal/modules/marketing/sales-orders/module.go @@ -1,6 +1,8 @@ package sales_orders import ( + "fmt" + "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "gorm.io/gorm" @@ -11,10 +13,9 @@ import ( rSalesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" sSalesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services" rCustomer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories" - rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" - rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + "gitlab.com/mbugroup/lti-api.git/internal/utils" ) type SalesOrdersModule struct{} @@ -23,12 +24,16 @@ func (SalesOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, valida salesOrdersRepo := rSalesOrders.NewSalesOrdersRepository(db) userRepo := rUser.NewUserRepository(db) customerRepo := rCustomer.NewCustomerRepository(db) - kandangRepo := rKandang.NewKandangRepository(db) productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) marketingRepo := rSalesOrders.NewMarketingRepository(db) approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(db)) - salesOrdersService := sSalesOrders.NewSalesOrdersService(salesOrdersRepo, customerRepo, kandangRepo, productWarehouseRepo, marketingRepo, userRepo, approvalSvc, validate) + + if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowMarketing, utils.MarketingApprovalSteps); err != nil { + panic(fmt.Sprintf("failed to register marketing approval workflow: %v", err)) + } + + salesOrdersService := sSalesOrders.NewSalesOrdersService(salesOrdersRepo, customerRepo, productWarehouseRepo, marketingRepo, userRepo, approvalSvc, validate) userService := sUser.NewUserService(userRepo, validate) SalesOrdersRoutes(router, userService, salesOrdersService) diff --git a/internal/modules/marketing/sales-orders/repositories/marketings.repository.go b/internal/modules/marketing/sales-orders/repositories/marketings.repository.go index f06bf401..dd0f99ab 100644 --- a/internal/modules/marketing/sales-orders/repositories/marketings.repository.go +++ b/internal/modules/marketing/sales-orders/repositories/marketings.repository.go @@ -11,6 +11,7 @@ import ( type MarketingRepository interface { repository.BaseRepository[entity.Marketing] IdExists(ctx context.Context, id uint) (bool, error) + GetNextSequence(ctx context.Context) (uint, error) } type MarketingRepositoryImpl struct { @@ -26,3 +27,11 @@ func NewMarketingRepository(db *gorm.DB) MarketingRepository { func (r *MarketingRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) { return repository.Exists[entity.Marketing](ctx, r.DB(), id) } + +func (r *MarketingRepositoryImpl) GetNextSequence(ctx context.Context) (uint, error) { + var maxID uint + if err := r.DB().WithContext(ctx).Model(&entity.Marketing{}).Select("COALESCE(MAX(id), 0)").Scan(&maxID).Error; err != nil { + return 0, err + } + return maxID + 1, nil +} diff --git a/internal/modules/marketing/sales-orders/route.go b/internal/modules/marketing/sales-orders/route.go index c48ae2a7..ae6d7a81 100644 --- a/internal/modules/marketing/sales-orders/route.go +++ b/internal/modules/marketing/sales-orders/route.go @@ -12,18 +12,15 @@ import ( func SalesOrdersRoutes(v1 fiber.Router, u user.UserService, s salesOrders.SalesOrdersService) { ctrl := controller.NewSalesOrdersController(s) + v1.Delete("/:id", ctrl.DeleteOne) route := v1.Group("/sales-orders") - // 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) route.Patch("/:id", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) + route.Post("/approvals", ctrl.Approval) } diff --git a/internal/modules/marketing/sales-orders/services/sales-orders.service.go b/internal/modules/marketing/sales-orders/services/sales-orders.service.go index 74244496..0dc47a2c 100644 --- a/internal/modules/marketing/sales-orders/services/sales-orders.service.go +++ b/internal/modules/marketing/sales-orders/services/sales-orders.service.go @@ -1,19 +1,19 @@ package service import ( + "context" "errors" "fmt" "strings" - "time" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + rInvMarketingDeliveryProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/marketing-delivery-products/repositories" productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/validations" customerRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories" - kandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" userRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" @@ -25,8 +25,6 @@ import ( ) type SalesOrdersService interface { - GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Marketing, int64, error) - GetOne(ctx *fiber.Ctx, id uint) (*entity.Marketing, error) CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Marketing, error) UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Marketing, error) DeleteOne(ctx *fiber.Ctx, id uint) error @@ -38,20 +36,18 @@ type salesOrdersService struct { Validate *validator.Validate Repository repository.SalesOrdersRepository CustomerRepo customerRepo.CustomerRepository - KandangRepo kandangRepo.KandangRepository ProductWarehouseRepo productWarehouseRepo.ProductWarehouseRepository MarketingRepo repository.MarketingRepository UserRepo userRepo.UserRepository ApprovalSvc commonSvc.ApprovalService } -func NewSalesOrdersService(repo repository.SalesOrdersRepository, customerRepo customerRepo.CustomerRepository, kandangRepo kandangRepo.KandangRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, marketingRepo repository.MarketingRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, validate *validator.Validate) SalesOrdersService { +func NewSalesOrdersService(repo repository.SalesOrdersRepository, customerRepo customerRepo.CustomerRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, marketingRepo repository.MarketingRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, validate *validator.Validate) SalesOrdersService { return &salesOrdersService{ Log: utils.Log, Validate: validate, Repository: repo, CustomerRepo: customerRepo, - KandangRepo: kandangRepo, ProductWarehouseRepo: productWarehouseRepo, MarketingRepo: marketingRepo, UserRepo: userRepo, @@ -60,58 +56,15 @@ func NewSalesOrdersService(repo repository.SalesOrdersRepository, customerRepo c } func (s salesOrdersService) withRelations(db *gorm.DB) *gorm.DB { - return db.Preload("CreatedUser"). + return db. + Preload("CreatedUser"). Preload("Customer"). Preload("SalesPerson"). Preload("Products.ProductWarehouse.Product"). Preload("Products.ProductWarehouse.Warehouse") } -func (s salesOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Marketing, int64, error) { - if err := s.Validate.Struct(params); err != nil { - return nil, 0, err - } - - offset := (params.Page - 1) * params.Limit - - marketings, total, err := s.MarketingRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { - db = s.withRelations(db) - if params.Search != "" { - return db.Where("so_number LIKE ?", "%"+params.Search+"%") - } - return db.Order("created_at DESC").Order("updated_at DESC") - }) - - if err != nil { - s.Log.Errorf("Failed to get marketings: %+v", err) - return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales orders") - } - - if s.ApprovalSvc != nil && len(marketings) > 0 { - ids := make([]uint, len(marketings)) - for i, item := range marketings { - ids[i] = item.Id - } - - latestMap, err := s.ApprovalSvc.LatestByTargets(c.Context(), utils.ApprovalWorkflowMarketing, ids, func(db *gorm.DB) *gorm.DB { - return db.Preload("ActionUser") - }) - if err != nil { - s.Log.Warnf("Unable to load latest approvals for marketings: %+v", err) - } else if len(latestMap) > 0 { - for i := range marketings { - if approval, ok := latestMap[marketings[i].Id]; ok { - marketings[i].LatestApproval = approval - } - } - } - } - - return marketings, total, nil -} - -func (s salesOrdersService) GetOne(c *fiber.Ctx, id uint) (*entity.Marketing, error) { - +func (s salesOrdersService) getOne(c *fiber.Ctx, id uint) (*entity.Marketing, error) { marketing, err := s.MarketingRepo.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "SalesOrders not found") @@ -125,15 +78,9 @@ func (s salesOrdersService) GetOne(c *fiber.Ctx, id uint) (*entity.Marketing, er approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), utils.ApprovalWorkflowMarketing, id, func(db *gorm.DB) *gorm.DB { return db.Preload("ActionUser") }) - if err != nil { - s.Log.Warnf("Unable to load approvals for marketing %d: %+v", id, err) - } else if len(approvals) > 0 { - if marketing.LatestApproval == nil { - latest := approvals[len(approvals)-1] - marketing.LatestApproval = &latest - } - } else { - marketing.LatestApproval = nil + if err == nil && len(approvals) > 0 { + latest := approvals[len(approvals)-1] + marketing.LatestApproval = &latest } } @@ -153,7 +100,6 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e for _, item := range req.MarketingProducts { if err := commonSvc.EnsureRelations(c.Context(), - commonSvc.RelationCheck{Name: "Kandang", ID: &item.KandangId, Exists: s.KandangRepo.IdExists}, commonSvc.RelationCheck{Name: "ProductWarehouse", ID: &item.ProductWarehouseId, Exists: s.ProductWarehouseRepo.IdExists}, ); err != nil { return nil, err @@ -165,14 +111,18 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid date format") } - soNumber := "SO-" + time.Now().Format("20060102150405") + nextSeq, err := s.MarketingRepo.GetNextSequence(c.Context()) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate SO number") + } + soNumber := fmt.Sprintf("SO-%05d", nextSeq) var marketing *entity.Marketing err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { marketingRepoTx := repository.NewMarketingRepository(dbTransaction) marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction) - marketingDeliveryProductRepoTx := repository.NewMarketingDeliveryProductRepository(dbTransaction) + invDeliveryRepoTx := rInvMarketingDeliveryProduct.NewMarketingDeliveryProductRepository(dbTransaction) approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) marketing = &entity.Marketing{ @@ -184,52 +134,18 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e CreatedBy: 1, } if err := marketingRepoTx.CreateOne(c.Context(), marketing, nil); err != nil { - s.Log.Errorf("Failed to create marketing: %+v", err) - return err + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create salesOrders") } if len(req.MarketingProducts) > 0 { for _, product := range req.MarketingProducts { - marketingProduct := &entity.MarketingProduct{ - MarketingId: marketing.Id, - ProductWarehouseId: product.ProductWarehouseId, - Qty: product.Qty, - UnitPrice: product.UnitPrice, - AvgWeight: product.AvgWeight, - TotalWeight: product.TotalWeight, - TotalPrice: product.TotalPrice, - } - if err := marketingProductRepoTx.CreateOne(c.Context(), marketingProduct, nil); err != nil { - s.Log.Errorf("Failed to create marketing product: %+v", err) - return err - } - - marketingDeliveryProduct := &entity.MarketingDeliveryProduct{ - MarketingProductId: marketingProduct.Id, - Qty: 0, - UnitPrice: 0, - TotalWeight: 0, - AvgWeight: 0, - TotalPrice: 0, - DeliveryDate: nil, - VehicleNumber: product.VehicleNumber, - } - if err := marketingDeliveryProductRepoTx.CreateOne(c.Context(), marketingDeliveryProduct, nil); err != nil { - s.Log.Errorf("Failed to create marketing delivery product: %+v", err) - return err + if err := s.createMarketingProductWithDelivery(c.Context(), marketing.Id, product, marketingProductRepoTx, invDeliveryRepoTx); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing product") } } } actorID := uint(1) // TODO: ambil dari auth context - if err := approvalSvcTx.RegisterWorkflowSteps( - utils.ApprovalWorkflowMarketing, - utils.MarketingApprovalSteps, - ); err != nil { - s.Log.Errorf("Failed to register workflow steps: %+v", err) - return err - } - approvalAction := entity.ApprovalActionCreated if _, err := approvalSvcTx.CreateApproval( c.Context(), @@ -240,8 +156,7 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e actorID, nil); err != nil { if !errors.Is(err, gorm.ErrDuplicatedKey) { - s.Log.Errorf("Failed to create approval: %+v", err) - return err + fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval") } } @@ -256,7 +171,6 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e marketing, err = s.MarketingRepo.GetByID(c.Context(), marketing.Id, s.withRelations) if err != nil { - s.Log.Errorf("Failed to fetch created marketing: %+v", err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch created sales order") } @@ -278,7 +192,6 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, id, nil) if err != nil { - s.Log.Errorf("Failed to check approval status: %+v", err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status") } if latestApproval != nil && latestApproval.StepNumber >= 3 { @@ -299,8 +212,8 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u marketingRepoTx := repository.NewMarketingRepository(dbTransaction) marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction) - marketingDeliveryProductRepoTx := repository.NewMarketingDeliveryProductRepository(dbTransaction) approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) + invDeliveryRepoTx := rInvMarketingDeliveryProduct.NewMarketingDeliveryProductRepository(dbTransaction) updateBody := make(map[string]any) if req.CustomerId != 0 { @@ -330,53 +243,85 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u oldProducts, err := marketingProductRepoTx.GetByMarketingID(c.Context(), id) if err != nil && err != gorm.ErrRecordNotFound { - s.Log.Errorf("Failed to fetch old marketing products: %+v", err) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to update products") + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch existing products") } - for _, oldProduct := range oldProducts { - if err := marketingDeliveryProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB { - return db.Where("marketing_product_id = ?", oldProduct.Id) - }); err != nil && err != gorm.ErrRecordNotFound { - s.Log.Errorf("Failed to delete delivery products: %+v", err) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to update products") + oldByPW := make(map[uint]*entity.MarketingProduct) + for i := range oldProducts { + p := oldProducts[i] + oldByPW[p.ProductWarehouseId] = &p + } + + reqByPW := make(map[uint]validation.CreateMarketingProduct) + for _, rp := range req.MarketingProducts { + reqByPW[rp.ProductWarehouseId] = rp + } + + for _, rp := range req.MarketingProducts { + if old, ok := oldByPW[rp.ProductWarehouseId]; ok { + + updateBody := map[string]any{ + "product_warehouse_id": rp.ProductWarehouseId, + "qty": rp.Qty, + "unit_price": rp.UnitPrice, + "avg_weight": rp.AvgWeight, + "total_weight": rp.TotalWeight, + "total_price": rp.TotalPrice, + } + if err := marketingProductRepoTx.PatchOne(c.Context(), old.Id, updateBody, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update marketing product") + } + + // Ensure delivery product exists; if not, create default + if _, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + mdp := &entity.MarketingDeliveryProduct{ + MarketingProductId: old.Id, + Qty: 0, + UnitPrice: 0, + TotalWeight: 0, + AvgWeight: 0, + TotalPrice: 0, + DeliveryDate: nil, + VehicleNumber: rp.VehicleNumber, + } + if err := invDeliveryRepoTx.CreateOne(c.Context(), mdp, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing delivery product") + } + } else { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to check delivery product") + } + } + } else { + // Create new marketing product (use helper) + if err := s.createMarketingProductWithDelivery(c.Context(), id, rp, marketingProductRepoTx, invDeliveryRepoTx); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing product") + } } } - if err := marketingProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB { - return db.Where("marketing_id = ?", id) - }); err != nil && err != gorm.ErrRecordNotFound { - s.Log.Errorf("Failed to delete marketing products: %+v", err) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to update products") - } - - for _, product := range req.MarketingProducts { - marketingProduct := &entity.MarketingProduct{ - MarketingId: id, - ProductWarehouseId: product.ProductWarehouseId, - Qty: product.Qty, - UnitPrice: product.UnitPrice, - AvgWeight: product.AvgWeight, - TotalWeight: product.TotalWeight, - TotalPrice: product.TotalPrice, - } - if err := marketingProductRepoTx.CreateOne(c.Context(), marketingProduct, nil); err != nil { - - return err - } - - marketingDeliveryProduct := &entity.MarketingDeliveryProduct{ - MarketingProductId: marketingProduct.Id, - Qty: 0, - UnitPrice: 0, - TotalWeight: 0, - AvgWeight: 0, - TotalPrice: 0, - DeliveryDate: nil, - VehicleNumber: product.VehicleNumber, - } - if err := marketingDeliveryProductRepoTx.CreateOne(c.Context(), marketingDeliveryProduct, nil); err != nil { - return err + // 2) Delete missing old products (prevent deletion if deliveries exist) + for _, old := range oldProducts { + if _, ok := reqByPW[old.ProductWarehouseId]; !ok { + // Check delivery product for this marketing product + deliveryProduct, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing delivery product") + } + if err == nil { + // If delivery exists (delivery_date not nil or qty > 0), prevent deletion + if deliveryProduct.DeliveryDate != nil || deliveryProduct.Qty > 0 { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete marketing product %d because it has delivery records", old.Id)) + } + // safe to delete delivery product record + if err := invDeliveryRepoTx.DeleteOne(c.Context(), deliveryProduct.Id); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete marketing delivery product") + } + } + // Delete marketing product + if err := marketingProductRepoTx.DeleteOne(c.Context(), old.Id); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete marketing product") + } } } } @@ -394,7 +339,6 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u actorID, &resetNote) if err != nil { - s.Log.Errorf("Failed to create reset approval: %+v", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset approval status") } } @@ -409,16 +353,16 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update sales order") } - return s.GetOne(c, id) + return s.getOne(c, id) } func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error { marketing, err := s.MarketingRepo.GetByID(c.Context(), id, s.withRelations) + if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "SalesOrders not found") } if err != nil { - s.Log.Errorf("Failed to fetch marketing %d before delete: %+v", id, err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales order") } @@ -494,7 +438,6 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status") } - if latestApproval == nil { return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No approval found for Marketing %d - sales orders must be created first", id)) } @@ -511,13 +454,12 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e } err := s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) for _, approvableID := range approvableIDs { - latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, approvableID, nil) if err != nil { - s.Log.Errorf("Failed to get latest approval for %d: %+v", approvableID, err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to check current approval step") } @@ -568,7 +510,7 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e updated := make([]entity.Marketing, 0, len(approvableIDs)) for _, id := range approvableIDs { - marketing, err := s.GetOne(c, id) + marketing, err := s.getOne(c, id) if err != nil { return nil, err } @@ -577,3 +519,35 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e return updated, nil } + +func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Context, marketingId uint, rp validation.CreateMarketingProduct, marketingProductRepo repository.MarketingProductRepository, invDeliveryRepo rInvMarketingDeliveryProduct.MarketingDeliveryProductRepository) error { + + marketingProduct := &entity.MarketingProduct{ + MarketingId: marketingId, + ProductWarehouseId: rp.ProductWarehouseId, + Qty: rp.Qty, + UnitPrice: rp.UnitPrice, + AvgWeight: rp.AvgWeight, + TotalWeight: rp.TotalWeight, + TotalPrice: rp.TotalPrice, + } + if err := marketingProductRepo.CreateOne(ctx, marketingProduct, nil); err != nil { + return err + } + + marketingDeliveryProduct := &entity.MarketingDeliveryProduct{ + MarketingProductId: marketingProduct.Id, + Qty: 0, + UnitPrice: 0, + TotalWeight: 0, + AvgWeight: 0, + TotalPrice: 0, + DeliveryDate: nil, + VehicleNumber: rp.VehicleNumber, + } + if err := invDeliveryRepo.CreateOne(ctx, marketingDeliveryProduct, nil); err != nil { + return err + } + + return nil +} diff --git a/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go b/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go index 01b3af9d..47d2e616 100644 --- a/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go +++ b/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go @@ -10,7 +10,6 @@ type Create struct { type CreateMarketingProduct struct { VehicleNumber string `json:"vehicle_number" validate:"required,min=1,max=50"` - KandangId uint `json:"kandang_id" validate:"required,gt=0"` ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,gt=0"` UnitPrice float64 `json:"unit_price" validate:"required,gt=0"` TotalWeight float64 `json:"total_weight" validate:"required,gt=0"` @@ -27,12 +26,6 @@ type Update struct { MarketingProducts []CreateMarketingProduct `json:"marketing_products" validate:"omitempty,min=1,dive"` } -type Query struct { - Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` - Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` - Search string `query:"search" validate:"omitempty,max=50"` -} - type Approve struct { Action string `json:"action" validate:"required_strict"` ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"`