mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Merge branch 'feat/BE/Sprint-6' of https://gitlab.com/mbugroup/lti-api into fix/BE/US-282/adjustment-recording-egg
This commit is contained in:
@@ -4,12 +4,17 @@ go 1.23
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/MicahParks/keyfunc/v2 v2.1.0
|
github.com/MicahParks/keyfunc/v2 v2.1.0
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.40.0
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.32.2
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.2
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.92.1
|
||||||
github.com/bytedance/sonic v1.12.1
|
github.com/bytedance/sonic v1.12.1
|
||||||
github.com/glebarez/sqlite v1.11.0
|
github.com/glebarez/sqlite v1.11.0
|
||||||
github.com/go-playground/validator/v10 v10.27.0
|
github.com/go-playground/validator/v10 v10.27.0
|
||||||
github.com/gofiber/contrib/jwt v1.0.10
|
github.com/gofiber/contrib/jwt v1.0.10
|
||||||
github.com/gofiber/fiber/v2 v2.52.5
|
github.com/gofiber/fiber/v2 v2.52.5
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
github.com/jackc/pgconn v1.14.1
|
github.com/jackc/pgconn v1.14.1
|
||||||
github.com/redis/go-redis/v9 v9.14.0
|
github.com/redis/go-redis/v9 v9.14.0
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
@@ -21,6 +26,21 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.2 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.5 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.2 // indirect
|
||||||
|
github.com/aws/smithy-go v1.23.2 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.2.0 // indirect
|
github.com/bytedance/sonic/loader v0.2.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
@@ -33,7 +53,6 @@ require (
|
|||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/google/go-cmp v0.6.0 // indirect
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||||
github.com/jackc/pgio v1.0.0 // indirect
|
github.com/jackc/pgio v1.0.0 // indirect
|
||||||
|
|||||||
@@ -2,6 +2,44 @@ github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+G
|
|||||||
github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k=
|
github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k=
|
||||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrKlwc=
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
|
||||||
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 h1:DHctwEM8P8iTXFxC/QK0MRjwEpWQeM9yzidCRjldUz0=
|
||||||
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3/go.mod h1:xdCzcZEtnSTKVDOmUZs4l/j3pSV6rpo1WXl5ugNsL8Y=
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.32.2 h1:4liUsdEpUUPZs5WVapsJLx5NPmQhQdez7nYFcovrytk=
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.32.2/go.mod h1:l0hs06IFz1eCT+jTacU/qZtC33nvcnLADAPL/XyrkZI=
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.2 h1:qZry8VUyTK4VIo5aEdUcBjPZHL2v4FyQ3QEOaWcFLu4=
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.2/go.mod h1:YUqm5a1/kBnoK+/NY5WEiMocZihKSo15/tJdmdXnM5g=
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 h1:WZVR5DbDgxzA0BJeudId89Kmgy6DIU4ORpxwsVHz0qA=
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14/go.mod h1:Dadl9QO0kHgbrH1GRqGiZdYtW5w+IXXaBNCHTIaheM4=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14/go.mod h1:VymhrMJUWs69D8u0/lZ7jSB6WgaG/NqHi3gX0aYf6U0=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 h1:bOS19y6zlJwagBfHxs0ESzr1XCOU2KXJCWcq3E2vfjY=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14/go.mod h1:1ipeGBMAxZ0xcTm6y6paC2C/J6f6OO7LBODV9afuAyM=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14 h1:ITi7qiDSv/mSGDSWNpZ4k4Ve0DQR6Ug2SJQ8zEHoDXg=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14/go.mod h1:k1xtME53H1b6YpZt74YmwlONMWf4ecM+lut1WQLAF/U=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5 h1:Hjkh7kE6D81PgrHlE/m9gx+4TyyeLHuY8xJs7yXN5C4=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5/go.mod h1:nPRXgyCfAurhyaTMoBMwRBYBhaHI4lNPAnJmjM0Tslc=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 h1:FIouAnCE46kyYqyhs0XEBDFFSREtdnr8HQuLPQPLCrY=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14/go.mod h1:UTwDc5COa5+guonQU8qBikJo1ZJ4ln2r1MkF7Dqag1E=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14 h1:FzQE21lNtUor0Fb7QNgnEyiRCBlolLTX/Z1j65S7teM=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14/go.mod h1:s1ydyWG9pm3ZwmmYN21HKyG9WzAZhYVW85wMHs5FV6w=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.92.1 h1:OgQy/+0+Kc3khtqiEOk23xQAglXi3Tj0y5doOxbi5tg=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.92.1/go.mod h1:wYNqY3L02Z3IgRYxOBPH9I1zD9Cjh9hI5QOy/eOjQvw=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.2 h1:MxMBdKTYBjPQChlJhi4qlEueqB1p1KcbTEa7tD5aqPs=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.2/go.mod h1:iS6EPmNeqCsGo+xQmXv0jIMjyYtQfnwg36zl2FwEouk=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.5 h1:ksUT5KtgpZd3SAiFJNJ0AFEJVva3gjBmN7eXUZjzUwQ=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.5/go.mod h1:av+ArJpoYf3pgyrj6tcehSFW+y9/QvAY8kMooR9bZCw=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10 h1:GtsxyiF3Nd3JahRBJbxLCCdYW9ltGQYrFWg8XdkGDd8=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10/go.mod h1:/j67Z5XBVDx8nZVp9EuFM9/BS5dvBznbqILGuu73hug=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.2 h1:a5UTtD4mHBU3t0o6aHQZFJTNKVfxFWfPX7J0Lr7G+uY=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.2/go.mod h1:6TxbXoDSgBQ225Qd8Q+MbxUxUh6TtNKwbRt/EPS9xso=
|
||||||
|
github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
|
||||||
|
github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DocumentRepository interface {
|
||||||
|
BaseRepository[entity.Document]
|
||||||
|
ListByTarget(ctx context.Context, documentableType string, documentableID uint64, modifier func(*gorm.DB) *gorm.DB) ([]entity.Document, error)
|
||||||
|
DeleteByTarget(ctx context.Context, documentableType string, documentableID uint64, modifier func(*gorm.DB) *gorm.DB) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type documentRepositoryImpl struct {
|
||||||
|
*BaseRepositoryImpl[entity.Document]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDocumentRepository(db *gorm.DB) DocumentRepository {
|
||||||
|
return &documentRepositoryImpl{
|
||||||
|
BaseRepositoryImpl: NewBaseRepository[entity.Document](db),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *documentRepositoryImpl) ListByTarget(
|
||||||
|
ctx context.Context,
|
||||||
|
documentableType string,
|
||||||
|
documentableID uint64,
|
||||||
|
modifier func(*gorm.DB) *gorm.DB,
|
||||||
|
) ([]entity.Document, error) {
|
||||||
|
var documents []entity.Document
|
||||||
|
|
||||||
|
q := r.DB().WithContext(ctx).
|
||||||
|
Where("documentable_type = ? AND documentable_id = ?", documentableType, documentableID)
|
||||||
|
|
||||||
|
if modifier != nil {
|
||||||
|
q = modifier(q)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := q.Order("created_at ASC").Find(&documents).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return documents, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *documentRepositoryImpl) DeleteByTarget(
|
||||||
|
ctx context.Context,
|
||||||
|
documentableType string,
|
||||||
|
documentableID uint64,
|
||||||
|
modifier func(*gorm.DB) *gorm.DB,
|
||||||
|
) error {
|
||||||
|
q := r.DB().WithContext(ctx).
|
||||||
|
Where("documentable_type = ? AND documentable_id = ?", documentableType, documentableID)
|
||||||
|
|
||||||
|
if modifier != nil {
|
||||||
|
q = modifier(q)
|
||||||
|
}
|
||||||
|
|
||||||
|
return q.Delete(&entity.Document{}).Error
|
||||||
|
}
|
||||||
@@ -0,0 +1,411 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"mime"
|
||||||
|
"mime/multipart"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultDocumentPathLimit = 50
|
||||||
|
defaultDocumentKeyPrefix = "docs"
|
||||||
|
maxDocumentNameLength = 50
|
||||||
|
)
|
||||||
|
|
||||||
|
type DocumentService interface {
|
||||||
|
UploadDocuments(ctx context.Context, req DocumentUploadRequest) ([]DocumentUploadResult, error)
|
||||||
|
ListByTarget(ctx context.Context, documentableType string, documentableID uint64) ([]entity.Document, error)
|
||||||
|
DeleteDocuments(ctx context.Context, ids []uint, removeFromStorage bool) error
|
||||||
|
DeleteByTarget(ctx context.Context, documentableType string, documentableID uint64, removeFromStorage bool) error
|
||||||
|
PublicURL(document entity.Document) string
|
||||||
|
}
|
||||||
|
|
||||||
|
type DocumentUploadRequest struct {
|
||||||
|
DocumentableType string
|
||||||
|
DocumentableID uint64
|
||||||
|
CreatedBy *uint
|
||||||
|
Files []DocumentFile
|
||||||
|
}
|
||||||
|
|
||||||
|
type DocumentFile struct {
|
||||||
|
File *multipart.FileHeader
|
||||||
|
Type string
|
||||||
|
Index *int
|
||||||
|
}
|
||||||
|
|
||||||
|
type DocumentUploadResult struct {
|
||||||
|
Document entity.Document
|
||||||
|
URL string
|
||||||
|
Index *int
|
||||||
|
}
|
||||||
|
|
||||||
|
type DocumentServiceOption func(*documentService)
|
||||||
|
|
||||||
|
type documentService struct {
|
||||||
|
repo commonRepo.DocumentRepository
|
||||||
|
storage DocumentStorage
|
||||||
|
keyPrefix string
|
||||||
|
maxPathLength int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDocumentService(repo commonRepo.DocumentRepository, storage DocumentStorage, opts ...DocumentServiceOption) DocumentService {
|
||||||
|
svc := &documentService{
|
||||||
|
repo: repo,
|
||||||
|
storage: storage,
|
||||||
|
keyPrefix: defaultDocumentKeyPrefix,
|
||||||
|
maxPathLength: defaultDocumentPathLimit,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(svc)
|
||||||
|
}
|
||||||
|
|
||||||
|
return svc
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDocumentServiceFromConfig(ctx context.Context, repo commonRepo.DocumentRepository) (DocumentService, error) {
|
||||||
|
if repo == nil {
|
||||||
|
return nil, errors.New("document repository is required")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(config.S3Bucket) == "" {
|
||||||
|
return nil, errors.New("S3_BUCKET is not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
storage, err := NewS3DocumentStorage(ctx, S3DocumentStorageConfig{
|
||||||
|
Region: config.S3Region,
|
||||||
|
Bucket: config.S3Bucket,
|
||||||
|
AccessKey: config.S3AccessKey,
|
||||||
|
SecretKey: config.S3SecretKey,
|
||||||
|
Endpoint: config.S3Endpoint,
|
||||||
|
BaseURL: config.S3PublicBaseURL,
|
||||||
|
ForcePathStyle: config.S3ForcePathStyle,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix := config.S3DocumentKeyPrefix
|
||||||
|
if prefix == "" {
|
||||||
|
prefix = defaultDocumentKeyPrefix
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewDocumentService(
|
||||||
|
repo,
|
||||||
|
storage,
|
||||||
|
WithDocumentKeyPrefix(prefix),
|
||||||
|
WithDocumentPathLimit(defaultDocumentPathLimit),
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithDocumentKeyPrefix(prefix string) DocumentServiceOption {
|
||||||
|
return func(svc *documentService) {
|
||||||
|
prefix = strings.Trim(prefix, "/")
|
||||||
|
if prefix == "" {
|
||||||
|
prefix = defaultDocumentKeyPrefix
|
||||||
|
}
|
||||||
|
svc.keyPrefix = prefix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithDocumentPathLimit(limit int) DocumentServiceOption {
|
||||||
|
return func(svc *documentService) {
|
||||||
|
if limit > 0 {
|
||||||
|
svc.maxPathLength = limit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *documentService) UploadDocuments(ctx context.Context, req DocumentUploadRequest) ([]DocumentUploadResult, error) {
|
||||||
|
if s.repo == nil {
|
||||||
|
return nil, errors.New("document repository not configured")
|
||||||
|
}
|
||||||
|
if s.storage == nil {
|
||||||
|
return nil, errors.New("document storage not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
documentableType := strings.ToUpper(strings.TrimSpace(req.DocumentableType))
|
||||||
|
if documentableType == "" {
|
||||||
|
return nil, errors.New("documentable type is required")
|
||||||
|
}
|
||||||
|
if req.DocumentableID == 0 {
|
||||||
|
return nil, errors.New("documentable id is required")
|
||||||
|
}
|
||||||
|
if len(req.Files) == 0 {
|
||||||
|
return nil, errors.New("no files to upload")
|
||||||
|
}
|
||||||
|
|
||||||
|
var createdBy *uint
|
||||||
|
if req.CreatedBy != nil && *req.CreatedBy != 0 {
|
||||||
|
idCopy := *req.CreatedBy
|
||||||
|
createdBy = &idCopy
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]DocumentUploadResult, 0, len(req.Files))
|
||||||
|
createdDocs := make([]entity.Document, 0, len(req.Files))
|
||||||
|
|
||||||
|
for _, file := range req.Files {
|
||||||
|
if file.File == nil {
|
||||||
|
return nil, errors.New("file header is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
originalName := sanitizeDocumentName(file.File.Filename)
|
||||||
|
contentType := detectContentType(file.File, originalName)
|
||||||
|
ext := detectExtension(file.File.Filename, contentType)
|
||||||
|
key, err := s.generateObjectKey(ext)
|
||||||
|
if err != nil {
|
||||||
|
s.rollbackDocuments(ctx, createdDocs)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, err := file.File.Open()
|
||||||
|
if err != nil {
|
||||||
|
s.rollbackDocuments(ctx, createdDocs)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
uploadRes, err := s.storage.Upload(ctx, key, reader, file.File.Size, contentType)
|
||||||
|
_ = reader.Close()
|
||||||
|
if err != nil {
|
||||||
|
s.rollbackDocuments(ctx, createdDocs)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
docType := resolveDocumentType(file.Type, documentableType)
|
||||||
|
doc := entity.Document{
|
||||||
|
DocumentableType: documentableType,
|
||||||
|
DocumentableId: req.DocumentableID,
|
||||||
|
Type: docType,
|
||||||
|
Path: uploadRes.Key,
|
||||||
|
Name: originalName,
|
||||||
|
Ext: strings.TrimPrefix(ext, "."),
|
||||||
|
Size: float64(file.File.Size),
|
||||||
|
CreatedBy: createdBy,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.repo.CreateOne(ctx, &doc, nil); err != nil {
|
||||||
|
_ = s.storage.Delete(ctx, uploadRes.Key)
|
||||||
|
s.rollbackDocuments(ctx, createdDocs)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
createdDocs = append(createdDocs, doc)
|
||||||
|
results = append(results, DocumentUploadResult{
|
||||||
|
Document: doc,
|
||||||
|
URL: uploadRes.URL,
|
||||||
|
Index: cloneIndex(file.Index),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *documentService) ListByTarget(ctx context.Context, documentableType string, documentableID uint64) ([]entity.Document, error) {
|
||||||
|
if s.repo == nil {
|
||||||
|
return nil, errors.New("document repository not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
documentableType = strings.ToUpper(strings.TrimSpace(documentableType))
|
||||||
|
if documentableType == "" {
|
||||||
|
return nil, errors.New("documentable type is required")
|
||||||
|
}
|
||||||
|
if documentableID == 0 {
|
||||||
|
return nil, errors.New("documentable id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.repo.ListByTarget(ctx, documentableType, documentableID, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *documentService) DeleteDocuments(ctx context.Context, ids []uint, removeFromStorage bool) error {
|
||||||
|
if s.repo == nil {
|
||||||
|
return errors.New("document repository not configured")
|
||||||
|
}
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
docs, err := s.repo.GetByIDs(ctx, ids, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, doc := range docs {
|
||||||
|
if err := s.repo.DeleteOne(ctx, doc.Id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if removeFromStorage && s.storage != nil {
|
||||||
|
if err := s.storage.Delete(ctx, doc.Path); err != nil {
|
||||||
|
utils.Log.WithError(err).Warnf("failed to delete document object %s", doc.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *documentService) DeleteByTarget(ctx context.Context, documentableType string, documentableID uint64, removeFromStorage bool) error {
|
||||||
|
if s.repo == nil {
|
||||||
|
return errors.New("document repository not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
documentableType = strings.ToUpper(strings.TrimSpace(documentableType))
|
||||||
|
if documentableType == "" || documentableID == 0 {
|
||||||
|
return errors.New("documentable type and id are required")
|
||||||
|
}
|
||||||
|
|
||||||
|
var docs []entity.Document
|
||||||
|
if removeFromStorage && s.storage != nil {
|
||||||
|
var err error
|
||||||
|
docs, err = s.repo.ListByTarget(ctx, documentableType, documentableID, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.repo.DeleteByTarget(ctx, documentableType, documentableID, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if removeFromStorage && len(docs) > 0 {
|
||||||
|
for _, doc := range docs {
|
||||||
|
if err := s.storage.Delete(ctx, doc.Path); err != nil {
|
||||||
|
utils.Log.WithError(err).Warnf("failed to delete document object %s", doc.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *documentService) PublicURL(document entity.Document) string {
|
||||||
|
if s.storage == nil || strings.TrimSpace(document.Path) == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return s.storage.URL(document.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *documentService) generateObjectKey(ext string) (string, error) {
|
||||||
|
normalizedExt := strings.TrimSpace(ext)
|
||||||
|
if normalizedExt != "" && !strings.HasPrefix(normalizedExt, ".") {
|
||||||
|
normalizedExt = "." + normalizedExt
|
||||||
|
}
|
||||||
|
|
||||||
|
u := uuid.New().String()
|
||||||
|
key := fmt.Sprintf("%s/%s%s", strings.Trim(s.keyPrefix, "/"), u, normalizedExt)
|
||||||
|
if s.keyPrefix == "" {
|
||||||
|
key = fmt.Sprintf("%s%s", u, normalizedExt)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(key) > s.maxPathLength {
|
||||||
|
key = fmt.Sprintf("%s%s", u, normalizedExt)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(key) > s.maxPathLength {
|
||||||
|
return "", fmt.Errorf("object key exceeds maximum length (%d)", s.maxPathLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *documentService) rollbackDocuments(ctx context.Context, docs []entity.Document) {
|
||||||
|
if len(docs) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := len(docs) - 1; i >= 0; i-- {
|
||||||
|
doc := docs[i]
|
||||||
|
if s.repo != nil && doc.Id != 0 {
|
||||||
|
if err := s.repo.DeleteOne(ctx, doc.Id); err != nil {
|
||||||
|
utils.Log.WithError(err).Warnf("failed to rollback document #%d", doc.Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s.storage != nil && strings.TrimSpace(doc.Path) != "" {
|
||||||
|
if err := s.storage.Delete(ctx, doc.Path); err != nil {
|
||||||
|
utils.Log.WithError(err).Warnf("failed to rollback document object %s", doc.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeDocumentName(name string) string {
|
||||||
|
name = filepath.Base(strings.TrimSpace(name))
|
||||||
|
if name == "." || name == "" {
|
||||||
|
name = "document"
|
||||||
|
}
|
||||||
|
name = strings.Map(func(r rune) rune {
|
||||||
|
if r < 32 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
switch r {
|
||||||
|
case '\\', '/', ':', '*', '?', '"', '<', '>', '|':
|
||||||
|
return '-'
|
||||||
|
default:
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
}, name)
|
||||||
|
|
||||||
|
if len(name) > maxDocumentNameLength {
|
||||||
|
runes := []rune(name)
|
||||||
|
if len(runes) > maxDocumentNameLength {
|
||||||
|
name = string(runes[:maxDocumentNameLength])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectExtension(filename, contentType string) string {
|
||||||
|
ext := strings.ToLower(strings.TrimSpace(filepath.Ext(filename)))
|
||||||
|
if ext == "" && contentType != "" {
|
||||||
|
if exts, _ := mime.ExtensionsByType(contentType); len(exts) > 0 {
|
||||||
|
ext = exts[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ext == "" {
|
||||||
|
return ".bin"
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(ext, ".") {
|
||||||
|
ext = "." + ext
|
||||||
|
}
|
||||||
|
return ext
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectContentType(file *multipart.FileHeader, filename string) string {
|
||||||
|
if file == nil {
|
||||||
|
return "application/octet-stream"
|
||||||
|
}
|
||||||
|
contentType := strings.TrimSpace(file.Header.Get("Content-Type"))
|
||||||
|
if contentType != "" {
|
||||||
|
return contentType
|
||||||
|
}
|
||||||
|
if ext := filepath.Ext(filename); ext != "" {
|
||||||
|
if guess := mime.TypeByExtension(ext); guess != "" {
|
||||||
|
return guess
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "application/octet-stream"
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveDocumentType(fileType, fallback string) string {
|
||||||
|
value := strings.ToUpper(strings.TrimSpace(fileType))
|
||||||
|
if value == "" {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneIndex(index *int) *int {
|
||||||
|
if index == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
value := *index
|
||||||
|
return &value
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/database"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDocumentServiceUpload(t *testing.T) {
|
||||||
|
if strings.TrimSpace(config.S3Bucket) == "" {
|
||||||
|
t.Fatal("S3 bucket is not configured; set S3_* env vars to run this test")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
db := setupDocumentTestDB(t)
|
||||||
|
repo := commonRepo.NewDocumentRepository(db)
|
||||||
|
|
||||||
|
svc, err := NewDocumentServiceFromConfig(ctx, repo)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create document service from config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
file := newTestFileHeader(t, "integration-proof.txt", "text/plain", []byte("document integration test"))
|
||||||
|
userID := uint(100)
|
||||||
|
|
||||||
|
results, err := svc.UploadDocuments(ctx, DocumentUploadRequest{
|
||||||
|
DocumentableType: "INVENTORY_TRANSFER",
|
||||||
|
DocumentableID: 99,
|
||||||
|
CreatedBy: &userID,
|
||||||
|
Files: []DocumentFile{
|
||||||
|
{File: file, Type: "integration"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("upload to S3 failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(results) != 1 {
|
||||||
|
t.Fatalf("expected 1 uploaded document, got %d", len(results))
|
||||||
|
}
|
||||||
|
|
||||||
|
doc := results[0].Document
|
||||||
|
if doc.Path == "" {
|
||||||
|
t.Fatalf("expected non-empty storage path")
|
||||||
|
}
|
||||||
|
if results[0].URL == "" {
|
||||||
|
t.Fatalf("expected public URL for uploaded document")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("uploaded document #%d to %s (path=%s)", doc.Id, results[0].URL, doc.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupDocumentTestDB(t *testing.T) *gorm.DB {
|
||||||
|
t.Helper()
|
||||||
|
if strings.TrimSpace(config.DBHost) == "" || strings.TrimSpace(config.DBName) == "" {
|
||||||
|
t.Fatal("database configuration missing; ensure DB_HOST and DB_NAME are set")
|
||||||
|
}
|
||||||
|
db := database.Connect(config.DBHost, config.DBName)
|
||||||
|
if db == nil {
|
||||||
|
t.Fatal("failed to create database connection")
|
||||||
|
}
|
||||||
|
if err := db.AutoMigrate(&entity.Document{}); err != nil {
|
||||||
|
t.Fatalf("failed to migrate document table: %v", err)
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestFileHeader(t *testing.T, filename, contentType string, data []byte) *multipart.FileHeader {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
body := &bytes.Buffer{}
|
||||||
|
writer := multipart.NewWriter(body)
|
||||||
|
part, err := writer.CreateFormFile("documents", filename)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create form file: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := part.Write(data); err != nil {
|
||||||
|
t.Fatalf("failed to write file data: %v", err)
|
||||||
|
}
|
||||||
|
if err := writer.Close(); err != nil {
|
||||||
|
t.Fatalf("failed to close writer: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "http://example.com/upload", body)
|
||||||
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
|
||||||
|
_, fileHeader, err := req.FormFile("documents")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse form file: %v", err)
|
||||||
|
}
|
||||||
|
fileHeader.Header.Set("Content-Type", contentType)
|
||||||
|
return fileHeader
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
|
awsconfig "github.com/aws/aws-sdk-go-v2/config"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DocumentStorage interface {
|
||||||
|
Upload(ctx context.Context, key string, body io.Reader, size int64, contentType string) (DocumentStorageUploadResult, error)
|
||||||
|
Delete(ctx context.Context, key string) error
|
||||||
|
URL(key string) string
|
||||||
|
}
|
||||||
|
|
||||||
|
type DocumentStorageUploadResult struct {
|
||||||
|
Key string
|
||||||
|
URL string
|
||||||
|
ETag string
|
||||||
|
}
|
||||||
|
|
||||||
|
type S3DocumentStorageConfig struct {
|
||||||
|
Region string
|
||||||
|
Bucket string
|
||||||
|
AccessKey string
|
||||||
|
SecretKey string
|
||||||
|
Endpoint string
|
||||||
|
BaseURL string
|
||||||
|
ForcePathStyle bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type s3DocumentStorage struct {
|
||||||
|
client *s3.Client
|
||||||
|
bucket string
|
||||||
|
base string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewS3DocumentStorage(ctx context.Context, cfg S3DocumentStorageConfig) (DocumentStorage, error) {
|
||||||
|
bucket := strings.TrimSpace(cfg.Bucket)
|
||||||
|
if bucket == "" {
|
||||||
|
return nil, errors.New("s3 bucket is required")
|
||||||
|
}
|
||||||
|
region := strings.TrimSpace(cfg.Region)
|
||||||
|
if region == "" {
|
||||||
|
region = "us-east-1"
|
||||||
|
}
|
||||||
|
|
||||||
|
options := []func(*awsconfig.LoadOptions) error{
|
||||||
|
awsconfig.WithRegion(region),
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint := strings.TrimSpace(cfg.Endpoint)
|
||||||
|
if endpoint != "" {
|
||||||
|
resolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, _ ...interface{}) (aws.Endpoint, error) {
|
||||||
|
if service == s3.ServiceID {
|
||||||
|
return aws.Endpoint{
|
||||||
|
URL: endpoint,
|
||||||
|
SigningRegion: region,
|
||||||
|
HostnameImmutable: true,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return aws.Endpoint{}, &aws.EndpointNotFoundError{}
|
||||||
|
})
|
||||||
|
options = append(options, awsconfig.WithEndpointResolverWithOptions(resolver))
|
||||||
|
}
|
||||||
|
|
||||||
|
accessKey := strings.TrimSpace(cfg.AccessKey)
|
||||||
|
secretKey := strings.TrimSpace(cfg.SecretKey)
|
||||||
|
if accessKey != "" && secretKey != "" {
|
||||||
|
options = append(options, awsconfig.WithCredentialsProvider(
|
||||||
|
credentials.NewStaticCredentialsProvider(accessKey, secretKey, ""),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
awsCfg, err := awsconfig.LoadDefaultConfig(ctx, options...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
client := s3.NewFromConfig(awsCfg, func(o *s3.Options) {
|
||||||
|
o.UsePathStyle = cfg.ForcePathStyle
|
||||||
|
})
|
||||||
|
|
||||||
|
baseURL := strings.TrimSuffix(strings.TrimSpace(cfg.BaseURL), "/")
|
||||||
|
if baseURL == "" {
|
||||||
|
if endpoint != "" {
|
||||||
|
baseURL = fmt.Sprintf("%s/%s", strings.TrimSuffix(endpoint, "/"), bucket)
|
||||||
|
} else {
|
||||||
|
baseURL = fmt.Sprintf("https://%s.s3.%s.amazonaws.com", bucket, region)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &s3DocumentStorage{
|
||||||
|
client: client,
|
||||||
|
bucket: bucket,
|
||||||
|
base: baseURL,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *s3DocumentStorage) Upload(ctx context.Context, key string, body io.Reader, size int64, contentType string) (DocumentStorageUploadResult, error) {
|
||||||
|
if strings.TrimSpace(key) == "" {
|
||||||
|
return DocumentStorageUploadResult{}, errors.New("storage key is required")
|
||||||
|
}
|
||||||
|
if size < 0 {
|
||||||
|
size = 0
|
||||||
|
}
|
||||||
|
input := &s3.PutObjectInput{
|
||||||
|
Bucket: aws.String(s.bucket),
|
||||||
|
Key: aws.String(key),
|
||||||
|
Body: body,
|
||||||
|
}
|
||||||
|
input.ContentLength = aws.Int64(size)
|
||||||
|
if ct := strings.TrimSpace(contentType); ct != "" {
|
||||||
|
input.ContentType = aws.String(ct)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := s.client.PutObject(ctx, input)
|
||||||
|
if err != nil {
|
||||||
|
return DocumentStorageUploadResult{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var etag string
|
||||||
|
if out.ETag != nil {
|
||||||
|
etag = strings.Trim(*out.ETag, "\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
return DocumentStorageUploadResult{
|
||||||
|
Key: key,
|
||||||
|
URL: s.URL(key),
|
||||||
|
ETag: etag,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *s3DocumentStorage) Delete(ctx context.Context, key string) error {
|
||||||
|
if strings.TrimSpace(key) == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_, err := s.client.DeleteObject(ctx, &s3.DeleteObjectInput{
|
||||||
|
Bucket: aws.String(s.bucket),
|
||||||
|
Key: aws.String(key),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *s3DocumentStorage) URL(key string) string {
|
||||||
|
key = strings.TrimPrefix(strings.TrimSpace(key), "/")
|
||||||
|
if key == "" {
|
||||||
|
return s.base
|
||||||
|
}
|
||||||
|
if s.base == "" {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s/%s", s.base, key)
|
||||||
|
}
|
||||||
@@ -65,6 +65,14 @@ var (
|
|||||||
SSOUserSyncDrift time.Duration
|
SSOUserSyncDrift time.Duration
|
||||||
SSOUserSyncNonceTTL time.Duration
|
SSOUserSyncNonceTTL time.Duration
|
||||||
SSOUserSyncMaxBodyBytes int
|
SSOUserSyncMaxBodyBytes int
|
||||||
|
S3Endpoint string
|
||||||
|
S3Region string
|
||||||
|
S3Bucket string
|
||||||
|
S3AccessKey string
|
||||||
|
S3SecretKey string
|
||||||
|
S3ForcePathStyle bool
|
||||||
|
S3PublicBaseURL string
|
||||||
|
S3DocumentKeyPrefix string
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -106,6 +114,16 @@ func init() {
|
|||||||
// Redis
|
// Redis
|
||||||
RedisURL = viper.GetString("REDIS_URL")
|
RedisURL = viper.GetString("REDIS_URL")
|
||||||
|
|
||||||
|
// Object storage
|
||||||
|
S3Endpoint = strings.TrimSpace(viper.GetString("S3_ENDPOINT"))
|
||||||
|
S3Region = strings.TrimSpace(viper.GetString("S3_REGION"))
|
||||||
|
S3Bucket = strings.TrimSpace(viper.GetString("S3_BUCKET"))
|
||||||
|
S3AccessKey = strings.TrimSpace(viper.GetString("S3_ACCESS_KEY"))
|
||||||
|
S3SecretKey = strings.TrimSpace(viper.GetString("S3_SECRET_KEY"))
|
||||||
|
S3ForcePathStyle = viper.GetBool("S3_FORCE_PATH_STYLE")
|
||||||
|
S3PublicBaseURL = strings.TrimSuffix(strings.TrimSpace(viper.GetString("S3_PUBLIC_BASE_URL")), "/")
|
||||||
|
S3DocumentKeyPrefix = defaultString(strings.Trim(strings.TrimSpace(viper.GetString("S3_DOCUMENT_PREFIX")), "/"), "docs")
|
||||||
|
|
||||||
// SSO integration
|
// SSO integration
|
||||||
SSOIssuer = viper.GetString("SSO_ISSUER")
|
SSOIssuer = viper.GetString("SSO_ISSUER")
|
||||||
SSOJWKSURL = viper.GetString("SSO_JWKS_URL")
|
SSOJWKSURL = viper.GetString("SSO_JWKS_URL")
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE products
|
||||||
|
DROP COLUMN IF EXISTS is_visible;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE products
|
||||||
|
ADD COLUMN IF NOT EXISTS is_visible BOOLEAN NOT NULL DEFAULT TRUE;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
DROP INDEX IF EXISTS documents_documentable_polymorphic;
|
||||||
|
DROP TABLE IF EXISTS documents;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
CREATE TABLE documents (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
documentable_type VARCHAR(50) NOT NULL,
|
||||||
|
documentable_id BIGINT NOT NULL,
|
||||||
|
type VARCHAR(50) NOT NULL,
|
||||||
|
path VARCHAR(50) NOT NULL,
|
||||||
|
name VARCHAR(50) NOT NULL,
|
||||||
|
ext VARCHAR(50) NOT NULL,
|
||||||
|
size NUMERIC(15, 3) NOT NULL,
|
||||||
|
created_by BIGINT REFERENCES users (id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX documents_documentable_polymorphic ON documents (documentable_type, documentable_id);
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Drop new indexes and FK
|
||||||
|
DROP INDEX IF EXISTS idx_product_warehouses_project_flock_kandang_id;
|
||||||
|
DROP INDEX IF EXISTS idx_product_warehouses_unique;
|
||||||
|
|
||||||
|
ALTER TABLE product_warehouses
|
||||||
|
DROP CONSTRAINT IF EXISTS fk_product_warehouses_project_flock_kandang_id,
|
||||||
|
ALTER COLUMN project_flock_kandang_id DROP NOT NULL,
|
||||||
|
DROP COLUMN IF EXISTS project_flock_kandang_id;
|
||||||
|
|
||||||
|
-- Revert qty to integer quantity
|
||||||
|
ALTER TABLE product_warehouses
|
||||||
|
RENAME COLUMN qty TO quantity;
|
||||||
|
|
||||||
|
ALTER TABLE product_warehouses
|
||||||
|
ALTER COLUMN quantity TYPE INTEGER USING quantity::integer,
|
||||||
|
ALTER COLUMN quantity SET DEFAULT 0,
|
||||||
|
ALTER COLUMN quantity SET NOT NULL;
|
||||||
|
|
||||||
|
-- Restore audit/soft-delete columns
|
||||||
|
ALTER TABLE product_warehouses
|
||||||
|
ADD COLUMN IF NOT EXISTS created_by BIGINT NOT NULL REFERENCES users (id),
|
||||||
|
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
|
||||||
|
|
||||||
|
-- Recreate prior indexes
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_product_warehouses_deleted_at ON product_warehouses (deleted_at);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_product_warehouses_unique
|
||||||
|
ON product_warehouses (product_id, warehouse_id)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Drop indexes that depend on deleted_at or old uniqueness
|
||||||
|
DROP INDEX IF EXISTS idx_product_warehouses_deleted_at;
|
||||||
|
DROP INDEX IF EXISTS idx_product_warehouses_unique;
|
||||||
|
|
||||||
|
-- Add new relation and adjust quantity column
|
||||||
|
ALTER TABLE product_warehouses
|
||||||
|
ADD COLUMN IF NOT EXISTS project_flock_kandang_id BIGINT;
|
||||||
|
|
||||||
|
ALTER TABLE product_warehouses
|
||||||
|
RENAME COLUMN quantity TO qty;
|
||||||
|
|
||||||
|
-- Enforce numeric quantity with precision and default
|
||||||
|
ALTER TABLE product_warehouses
|
||||||
|
ALTER COLUMN qty TYPE NUMERIC(15, 3) USING qty::numeric(15, 3),
|
||||||
|
ALTER COLUMN qty SET DEFAULT 0,
|
||||||
|
ALTER COLUMN qty SET NOT NULL;
|
||||||
|
|
||||||
|
-- Remove audit/soft-delete columns no longer used
|
||||||
|
ALTER TABLE product_warehouses
|
||||||
|
DROP COLUMN IF EXISTS created_by,
|
||||||
|
DROP COLUMN IF EXISTS created_at,
|
||||||
|
DROP COLUMN IF EXISTS updated_at,
|
||||||
|
DROP COLUMN IF EXISTS deleted_at;
|
||||||
|
|
||||||
|
-- Enforce FK and not-null for project_flock_kandang_id
|
||||||
|
ALTER TABLE product_warehouses
|
||||||
|
ADD CONSTRAINT fk_product_warehouses_project_flock_kandang_id
|
||||||
|
FOREIGN KEY (project_flock_kandang_id)
|
||||||
|
REFERENCES project_flock_kandangs (id)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- New indexes
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_product_warehouses_project_flock_kandang_id
|
||||||
|
ON product_warehouses (project_flock_kandang_id);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_product_warehouses_unique
|
||||||
|
ON product_warehouses (product_id, warehouse_id, project_flock_kandang_id);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Drop new indexes
|
||||||
|
DROP INDEX IF EXISTS stock_logs_loggable_type_loggable_id_idx;
|
||||||
|
DROP INDEX IF EXISTS stock_logs_product_warehouse_id_idx;
|
||||||
|
DROP INDEX IF EXISTS stock_logs_created_by_idx;
|
||||||
|
DROP INDEX IF EXISTS stock_logs_created_at_idx;
|
||||||
|
|
||||||
|
-- Restore obsolete columns
|
||||||
|
ALTER TABLE stock_logs
|
||||||
|
ADD COLUMN IF NOT EXISTS transaction_type VARCHAR(20) DEFAULT '' NOT NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS quantity NUMERIC(15, 3) DEFAULT 0 NOT NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS before_quantity NUMERIC(15, 3) DEFAULT 0 NOT NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS after_quantity NUMERIC(15, 3) DEFAULT 0 NOT NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
|
||||||
|
|
||||||
|
-- Rename columns back
|
||||||
|
ALTER TABLE stock_logs
|
||||||
|
RENAME COLUMN loggable_type TO log_type;
|
||||||
|
|
||||||
|
ALTER TABLE stock_logs
|
||||||
|
RENAME COLUMN loggable_id TO log_id;
|
||||||
|
|
||||||
|
ALTER TABLE stock_logs
|
||||||
|
RENAME COLUMN notes TO note;
|
||||||
|
|
||||||
|
-- Drop new columns
|
||||||
|
ALTER TABLE stock_logs
|
||||||
|
DROP COLUMN IF EXISTS increase,
|
||||||
|
DROP COLUMN IF EXISTS decrease;
|
||||||
|
|
||||||
|
-- Restore indexes for old structure
|
||||||
|
CREATE INDEX IF NOT EXISTS stock_logs_product_warehouse_id_idx ON stock_logs (product_warehouse_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS stock_logs_log_type_log_id_idx ON stock_logs (log_type, log_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS stock_logs_created_by_idx ON stock_logs (created_by);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS stock_logs_created_at_idx ON stock_logs (created_at);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS stock_logs_deleted_at_idx ON stock_logs (deleted_at);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Drop old indexes tied to removed columns
|
||||||
|
DROP INDEX IF EXISTS stock_logs_log_type_log_id_idx;
|
||||||
|
DROP INDEX IF EXISTS stock_logs_deleted_at_idx;
|
||||||
|
|
||||||
|
-- Rename columns to new naming
|
||||||
|
ALTER TABLE stock_logs
|
||||||
|
RENAME COLUMN log_type TO loggable_type;
|
||||||
|
|
||||||
|
ALTER TABLE stock_logs
|
||||||
|
RENAME COLUMN log_id TO loggable_id;
|
||||||
|
|
||||||
|
ALTER TABLE stock_logs
|
||||||
|
RENAME COLUMN note TO notes;
|
||||||
|
|
||||||
|
-- Add new increase/decrease columns
|
||||||
|
ALTER TABLE stock_logs
|
||||||
|
ADD COLUMN IF NOT EXISTS increase NUMERIC(15, 3) DEFAULT 0,
|
||||||
|
ADD COLUMN IF NOT EXISTS decrease NUMERIC(15, 3) DEFAULT 0;
|
||||||
|
|
||||||
|
-- Adjust column definitions
|
||||||
|
ALTER TABLE stock_logs
|
||||||
|
ALTER COLUMN loggable_type TYPE VARCHAR(50),
|
||||||
|
ALTER COLUMN loggable_type SET NOT NULL,
|
||||||
|
ALTER COLUMN loggable_id SET NOT NULL,
|
||||||
|
ALTER COLUMN increase SET DEFAULT 0,
|
||||||
|
ALTER COLUMN increase SET NOT NULL,
|
||||||
|
ALTER COLUMN decrease SET DEFAULT 0,
|
||||||
|
ALTER COLUMN decrease SET NOT NULL;
|
||||||
|
|
||||||
|
-- Remove obsolete columns
|
||||||
|
ALTER TABLE stock_logs
|
||||||
|
DROP COLUMN IF EXISTS transaction_type,
|
||||||
|
DROP COLUMN IF EXISTS quantity,
|
||||||
|
DROP COLUMN IF EXISTS before_quantity,
|
||||||
|
DROP COLUMN IF EXISTS after_quantity,
|
||||||
|
DROP COLUMN IF EXISTS updated_at,
|
||||||
|
DROP COLUMN IF EXISTS deleted_at;
|
||||||
|
|
||||||
|
-- Recreate indexes for new structure
|
||||||
|
CREATE INDEX IF NOT EXISTS stock_logs_product_warehouse_id_idx ON stock_logs (product_warehouse_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS stock_logs_loggable_type_loggable_id_idx ON stock_logs (loggable_type, loggable_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS stock_logs_created_by_idx ON stock_logs (created_by);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS stock_logs_created_at_idx ON stock_logs (created_at);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -910,7 +910,7 @@ func seedProductWarehouse(tx *gorm.DB, createdBy uint) error {
|
|||||||
ProductId: product.Id,
|
ProductId: product.Id,
|
||||||
WarehouseId: warehouse.Id,
|
WarehouseId: warehouse.Id,
|
||||||
Quantity: seed.Quantity,
|
Quantity: seed.Quantity,
|
||||||
CreatedBy: createdBy,
|
// CreatedBy: createdBy,
|
||||||
}
|
}
|
||||||
if err := tx.Create(&productWarehouse).Error; err != nil {
|
if err := tx.Create(&productWarehouse).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Document struct {
|
||||||
|
Id uint `gorm:"primaryKey"`
|
||||||
|
DocumentableType string `gorm:"size:50;not null;index:documents_documentable_polymorphic,priority:1"`
|
||||||
|
DocumentableId uint64 `gorm:"not null;index:documents_documentable_polymorphic,priority:2"`
|
||||||
|
Type string `gorm:"size:50;not null"`
|
||||||
|
Path string `gorm:"size:50;not null"`
|
||||||
|
Name string `gorm:"size:50;not null"`
|
||||||
|
Ext string `gorm:"size:50;not null"`
|
||||||
|
Size float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
CreatedBy *uint `gorm:"index"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
|
||||||
|
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
|
}
|
||||||
@@ -21,10 +21,12 @@ type Product struct {
|
|||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
IsVisible bool `gorm:"column:is_visible;default:true"`
|
||||||
|
|
||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
Uom Uom `gorm:"foreignKey:UomId;references:Id"`
|
Uom Uom `gorm:"foreignKey:UomId;references:Id"`
|
||||||
ProductCategory ProductCategory `gorm:"foreignKey:ProductCategoryId;references:Id"`
|
ProductCategory ProductCategory `gorm:"foreignKey:ProductCategoryId;references:Id"`
|
||||||
ProductSuppliers []ProductSupplier `gorm:"foreignKey:ProductId;references:Id"`
|
ProductSuppliers []ProductSupplier `gorm:"foreignKey:ProductId;references:Id"`
|
||||||
Flags []Flag `gorm:"polymorphic:Flagable;polymorphicValue:products"`
|
Flags []Flag `gorm:"polymorphic:Flagable;polymorphicValue:products"`
|
||||||
|
ProductWarehouses []ProductWarehouse `gorm:"foreignKey:ProductId;references:Id"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,15 @@
|
|||||||
package entities
|
package entities
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ProductWarehouse struct {
|
type ProductWarehouse struct {
|
||||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
Id uint `gorm:"primaryKey;column:id"`
|
||||||
ProductId uint `gorm:"not null"`
|
ProductId uint `gorm:"column:product_id;not null"`
|
||||||
WarehouseId uint `gorm:"not null"`
|
WarehouseId uint `gorm:"column:warehouse_id;not null"`
|
||||||
Quantity float64 `gorm:"default:0"`
|
ProjectFlockKandangId *uint `gorm:"column:project_flock_kandang_id"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
Quantity float64 `gorm:"column:qty;type:numeric(15,3);default:0"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
|
||||||
CreatedBy uint `gorm:"not null"`
|
|
||||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
Product Product `gorm:"foreignKey:ProductId;references:Id"`
|
Product Product `gorm:"foreignKey:ProductId;references:Id"`
|
||||||
Warehouse Warehouse `gorm:"foreignKey:WarehouseId;references:Id"`
|
Warehouse Warehouse `gorm:"foreignKey:WarehouseId;references:Id"`
|
||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
||||||
|
StockLogs []StockLog `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ProjectBudget struct {
|
type ProjectBudget struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Qty float64 `gorm:"type:numeric(15,3);not null"`
|
ProjectFlockId uint `gorm:"not null"`
|
||||||
Price float64 `gorm:"type:numeric(15,3);not null"`
|
NonstockId uint `gorm:"not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
Qty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
Price float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
|
||||||
Nonstock *Nonstock `gorm:"foreignKey:Id;references:Id"`
|
Nonstock *Nonstock `gorm:"foreignKey:NonstockId;references:Id"`
|
||||||
ProjectFlock *ProjectFlock `gorm:"foreignKey:Id;references:Id"`
|
ProjectFlock *ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type ProjectFlock struct {
|
|||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
Kandangs []Kandang `gorm:"many2many:project_flock_kandangs;joinTableForeignKey:project_flock_id;joinTableReferences:kandang_id" json:"kandangs,omitempty"`
|
Kandangs []Kandang `gorm:"many2many:project_flock_kandangs;joinTableForeignKey:project_flock_id;joinTableReferences:kandang_id" json:"kandangs,omitempty"`
|
||||||
KandangHistory []ProjectFlockKandang `gorm:"foreignKey:ProjectFlockId;references:Id" json:"-"`
|
KandangHistory []ProjectFlockKandang `gorm:"foreignKey:ProjectFlockId;references:Id" json:"-"`
|
||||||
|
Budgets []ProjectBudget `gorm:"foreignKey:ProjectFlockId;references:Id" json:"-"`
|
||||||
|
|
||||||
LatestApproval *Approval `gorm:"-" json:"-"`
|
LatestApproval *Approval `gorm:"-" json:"-"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
package entities
|
package entities
|
||||||
|
|
||||||
import (
|
import "time"
|
||||||
"time"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
LogTypeAdjustment = "ADJUSTMENT"
|
LogTypeAdjustment = "ADJUSTMENT"
|
||||||
@@ -17,19 +13,18 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type StockLog struct {
|
type StockLog struct {
|
||||||
Id uint `gorm:"primaryKey;column:id"`
|
Id uint `gorm:"primaryKey;column:id"`
|
||||||
TransactionType string `gorm:"type:varchar(20);not null"`
|
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null;index"`
|
||||||
Quantity float64 `gorm:"type:numeric(15,3);not null"`
|
CreatedBy uint `gorm:"column:created_by;not null;index"`
|
||||||
BeforeQuantity float64 `gorm:"type:numeric(15,3);not null"`
|
|
||||||
AfterQuantity float64 `gorm:"type:numeric(15,3);not null"`
|
Increase float64 `gorm:"column:increase;type:numeric(15,3);default:0"`
|
||||||
LogType string `gorm:"type:varchar(50);not null;index:stock_logs_flaggable_lookup,priority:1"`
|
Decrease float64 `gorm:"column:decrease;type:numeric(15,3);default:0"`
|
||||||
LogId uint `gorm:"not null;index:stock_logs_flaggable_lookup,priority:2"`
|
|
||||||
Note string `gorm:"type:text"`
|
LoggableType string `gorm:"column:loggable_type;type:varchar(50);not null"`
|
||||||
ProductWarehouseId uint `gorm:"not null;index"`
|
LoggableId uint `gorm:"column:loggable_id;not null"`
|
||||||
CreatedBy uint `gorm:"index"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
Notes string `gorm:"column:notes;type:text"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"`
|
||||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
|
||||||
|
|
||||||
ProductWarehouse *ProductWarehouse `json:"product_warehouse,omitempty" gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
ProductWarehouse *ProductWarehouse `json:"product_warehouse,omitempty" gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
CreatedUser *User `json:"created_user,omitempty" gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser *User `json:"created_user,omitempty" gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
|
|||||||
@@ -57,11 +57,11 @@ func (u *ClosingController) GetOne(c *fiber.Ctx) error {
|
|||||||
param := c.Params("id")
|
param := c.Params("id")
|
||||||
|
|
||||||
id, err := strconv.Atoi(param)
|
id, err := strconv.Atoi(param)
|
||||||
if err != nil {
|
if err != nil || id <= 0 {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid id")
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := u.ClosingService.GetOne(c, uint(id))
|
result, err := u.ClosingService.GetProjectFlockByID(c, uint(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -70,7 +70,56 @@ func (u *ClosingController) GetOne(c *fiber.Ctx) error {
|
|||||||
JSON(response.Success{
|
JSON(response.Success{
|
||||||
Code: fiber.StatusOK,
|
Code: fiber.StatusOK,
|
||||||
Status: "success",
|
Status: "success",
|
||||||
Message: "Get closing successfully",
|
Message: "Retrieved closing information successfully",
|
||||||
Data: dto.ToClosingListDTO(*result),
|
Data: result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ClosingController) GetClosingSummary(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("projectFlockId")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil || id <= 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.ClosingService.GetClosingSummary(c, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Retrieved project information successfully",
|
||||||
|
Data: result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ClosingController) GetPenjualan(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("project_flock_id")
|
||||||
|
|
||||||
|
projectFlockID, err := strconv.Atoi(param)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
projectFlock, err := u.ClosingService.GetProjectFlockByID(c, uint(projectFlockID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.ClosingService.GetPenjualan(c, uint(projectFlockID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get closing penjualan successfully",
|
||||||
|
Data: dto.ToPenjualanRealisasiResponseDTO(projectFlock.Category, uint(projectFlockID), result),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
@@ -26,6 +27,67 @@ type ClosingDetailDTO struct {
|
|||||||
ClosingListDTO
|
ClosingListDTO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ClosingSummaryDTO struct {
|
||||||
|
LocationID uint `json:"location_id"`
|
||||||
|
Periode int `json:"periode"`
|
||||||
|
JenisProduk string `json:"jenis_produk"`
|
||||||
|
LabelPopulasi string `json:"label_populasi"`
|
||||||
|
JumlahPopulasi int `json:"jumlah_populasi"`
|
||||||
|
JumlahPopulasiFormatted string `json:"jumlah_populasi_formatted"`
|
||||||
|
JenisProject string `json:"jenis_project"`
|
||||||
|
KandangAktif int `json:"kandang_aktif"`
|
||||||
|
KandangAktifFormatted string `json:"kandang_aktif_formatted"`
|
||||||
|
StatusPembayaranPenjualan string `json:"status_pembayaran_penjualan"`
|
||||||
|
StatusPembayaranMitra string `json:"status_pembayaran_mitra"`
|
||||||
|
StatusProject string `json:"status_project"`
|
||||||
|
StatusClosing string `json:"status_closing"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToClosingSummaryDTO(project entity.ProjectFlock, statusProject, statusClosing string) ClosingSummaryDTO {
|
||||||
|
history := project.KandangHistory
|
||||||
|
|
||||||
|
period := maxPeriod(history)
|
||||||
|
kandangCount := len(history)
|
||||||
|
population := sumPopulation(history)
|
||||||
|
populationInt := int(population)
|
||||||
|
|
||||||
|
return ClosingSummaryDTO{
|
||||||
|
LocationID: project.LocationId,
|
||||||
|
Periode: period,
|
||||||
|
JenisProduk: project.Category,
|
||||||
|
LabelPopulasi: "",
|
||||||
|
JumlahPopulasi: populationInt,
|
||||||
|
JumlahPopulasiFormatted: fmt.Sprintf("%d Ekor", populationInt),
|
||||||
|
JenisProject: "",
|
||||||
|
KandangAktif: kandangCount,
|
||||||
|
KandangAktifFormatted: fmt.Sprintf("%d Kandang", kandangCount),
|
||||||
|
StatusPembayaranPenjualan: "Tempo",
|
||||||
|
StatusPembayaranMitra: "",
|
||||||
|
StatusProject: statusProject,
|
||||||
|
StatusClosing: statusClosing,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func maxPeriod(history []entity.ProjectFlockKandang) int {
|
||||||
|
max := 0
|
||||||
|
for _, h := range history {
|
||||||
|
if h.Period > max {
|
||||||
|
max = h.Period
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return max
|
||||||
|
}
|
||||||
|
|
||||||
|
func sumPopulation(history []entity.ProjectFlockKandang) float64 {
|
||||||
|
var total float64
|
||||||
|
for _, h := range history {
|
||||||
|
for _, chickin := range h.Chickins {
|
||||||
|
total += chickin.UsageQty + chickin.PendingUsageQty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
// === Mapper Functions ===
|
// === Mapper Functions ===
|
||||||
|
|
||||||
func ToClosingRelationDTO(e entity.ProjectFlock) ClosingRelationDTO {
|
func ToClosingRelationDTO(e entity.ProjectFlock) ClosingRelationDTO {
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
deliveryOrdersDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/dto"
|
||||||
|
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
|
||||||
|
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
|
||||||
|
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// === Response DTO ===
|
||||||
|
|
||||||
|
type SalesDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
RealizationDate time.Time `json:"realization_date"`
|
||||||
|
Age int `json:"age"`
|
||||||
|
DoNumber string `json:"do_number"`
|
||||||
|
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
|
||||||
|
Customer *customerDTO.CustomerRelationDTO `json:"customer,omitempty"`
|
||||||
|
Qty float64 `json:"qty"`
|
||||||
|
Weight float64 `json:"weight"`
|
||||||
|
AvgWeight float64 `json:"avg_weight"`
|
||||||
|
Price float64 `json:"price"`
|
||||||
|
TotalPrice float64 `json:"total_price"`
|
||||||
|
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
|
||||||
|
PaymentStatus string `json:"payment_status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PenjualanRealisasiResponseDTO struct {
|
||||||
|
ProjectType string `json:"project_type"`
|
||||||
|
FlockId uint `json:"flock_id"`
|
||||||
|
Period int `json:"period"`
|
||||||
|
Sales []SalesDTO `json:"sales"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Mapper Functions ===
|
||||||
|
|
||||||
|
func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO {
|
||||||
|
|
||||||
|
// todo: usia ayam masih dummy
|
||||||
|
age := 0
|
||||||
|
|
||||||
|
var product *productDTO.ProductRelationDTO
|
||||||
|
if e.MarketingProduct.ProductWarehouse.Product.Id != 0 {
|
||||||
|
mapped := productDTO.ToProductRelationDTO(e.MarketingProduct.ProductWarehouse.Product)
|
||||||
|
product = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
var customer *customerDTO.CustomerRelationDTO
|
||||||
|
if e.MarketingProduct.Marketing.Customer.Id != 0 {
|
||||||
|
mapped := customerDTO.ToCustomerRelationDTO(e.MarketingProduct.Marketing.Customer)
|
||||||
|
customer = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
var kandang *kandangDTO.KandangRelationDTO
|
||||||
|
if e.MarketingProduct.ProductWarehouse.ProjectFlockKandang != nil && e.MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang.Id != 0 {
|
||||||
|
mapped := kandangDTO.ToKandangRelationDTO(e.MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang)
|
||||||
|
kandang = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
doNumber := deliveryOrdersDTO.GenerateDeliveryOrderNumber(e.MarketingProduct.Marketing.SoNumber, e.DeliveryDate, e.MarketingProduct.ProductWarehouse.Warehouse.Id)
|
||||||
|
|
||||||
|
return SalesDTO{
|
||||||
|
Id: e.Id,
|
||||||
|
RealizationDate: *e.DeliveryDate,
|
||||||
|
Age: age,
|
||||||
|
DoNumber: doNumber,
|
||||||
|
Product: product,
|
||||||
|
Customer: customer,
|
||||||
|
Qty: e.Qty,
|
||||||
|
Weight: e.TotalWeight,
|
||||||
|
AvgWeight: e.AvgWeight,
|
||||||
|
Price: e.UnitPrice,
|
||||||
|
TotalPrice: e.TotalPrice,
|
||||||
|
Kandang: kandang,
|
||||||
|
PaymentStatus: "Paid",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToSalesDTOs(e []entity.MarketingDeliveryProduct) []SalesDTO {
|
||||||
|
result := make([]SalesDTO, len(e))
|
||||||
|
for i, r := range e {
|
||||||
|
result[i] = ToSalesDTO(r)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToPenjualanRealisasiResponseDTO(projectType string, projectFlockID uint, e []entity.MarketingDeliveryProduct) PenjualanRealisasiResponseDTO {
|
||||||
|
period := extractPeriodFromRealisasi(e)
|
||||||
|
return PenjualanRealisasiResponseDTO{
|
||||||
|
ProjectType: projectType,
|
||||||
|
FlockId: projectFlockID,
|
||||||
|
Period: period,
|
||||||
|
Sales: ToSalesDTOs(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractPeriodFromRealisasi(realisasi []entity.MarketingDeliveryProduct) int {
|
||||||
|
if len(realisasi) > 0 {
|
||||||
|
for _, item := range realisasi {
|
||||||
|
if item.MarketingProduct.ProductWarehouse.ProjectFlockKandang != nil {
|
||||||
|
return item.MarketingProduct.ProductWarehouse.ProjectFlockKandang.Period
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
@@ -5,8 +5,12 @@ import (
|
|||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
rClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
|
rClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
|
||||||
sClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
|
sClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
|
||||||
|
rMarketings "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
|
||||||
|
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
|
|
||||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -17,10 +21,14 @@ type ClosingModule struct{}
|
|||||||
func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||||
closingRepo := rClosing.NewClosingRepository(db)
|
closingRepo := rClosing.NewClosingRepository(db)
|
||||||
userRepo := rUser.NewUserRepository(db)
|
userRepo := rUser.NewUserRepository(db)
|
||||||
|
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
|
||||||
|
marketingRepo := rMarketings.NewMarketingRepository(db)
|
||||||
|
marketingDeliveryProductRepo := rMarketings.NewMarketingDeliveryProductRepository(db)
|
||||||
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
|
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||||
|
|
||||||
closingService := sClosing.NewClosingService(closingRepo, validate)
|
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, validate)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
ClosingRoutes(router, userService, closingService)
|
ClosingRoutes(router, userService, closingService)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService) {
|
func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService) {
|
||||||
ctrl := controller.NewClosingController(s)
|
ctrl := controller.NewClosingController(s)
|
||||||
|
|
||||||
route := v1.Group("/closings")
|
route := v1.Group("/closing")
|
||||||
|
|
||||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
||||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
||||||
@@ -21,5 +21,6 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService
|
|||||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
route.Get("/:id", ctrl.GetOne)
|
route.Get("/:project_flock_id/penjualan", ctrl.GetPenjualan)
|
||||||
|
route.Get("/:projectFlockId", ctrl.GetClosingSummary)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
|
||||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
|
||||||
|
marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
|
||||||
|
marketingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
|
||||||
|
projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
@@ -16,20 +23,30 @@ import (
|
|||||||
|
|
||||||
type ClosingService interface {
|
type ClosingService interface {
|
||||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error)
|
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error)
|
||||||
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error)
|
GetProjectFlockByID(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error)
|
||||||
|
GetPenjualan(ctx *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error)
|
||||||
|
GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type closingService struct {
|
type closingService struct {
|
||||||
Log *logrus.Logger
|
Log *logrus.Logger
|
||||||
Validate *validator.Validate
|
Validate *validator.Validate
|
||||||
Repository repository.ClosingRepository
|
Repository repository.ClosingRepository
|
||||||
|
ProjectFlockRepo projectflockRepository.ProjectflockRepository
|
||||||
|
MarketingRepo marketingRepository.MarketingRepository
|
||||||
|
MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository
|
||||||
|
ApprovalSvc commonSvc.ApprovalService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClosingService(repo repository.ClosingRepository, validate *validator.Validate) ClosingService {
|
func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, validate *validator.Validate) ClosingService {
|
||||||
return &closingService{
|
return &closingService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
Repository: repo,
|
Repository: repo,
|
||||||
|
ProjectFlockRepo: projectFlockRepo,
|
||||||
|
MarketingRepo: marketingRepo,
|
||||||
|
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
|
||||||
|
ApprovalSvc: approvalSvc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,6 +54,12 @@ func (s closingService) withRelations(db *gorm.DB) *gorm.DB {
|
|||||||
return db.Preload("CreatedUser")
|
return db.Preload("CreatedUser")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s closingService) withClosingRelations(db *gorm.DB) *gorm.DB {
|
||||||
|
return s.withRelations(db).
|
||||||
|
Preload("KandangHistory").
|
||||||
|
Preload("KandangHistory.Chickins")
|
||||||
|
}
|
||||||
|
|
||||||
func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) {
|
func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) {
|
||||||
if err := s.Validate.Struct(params); err != nil {
|
if err := s.Validate.Struct(params); err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
@@ -59,14 +82,109 @@ func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity
|
|||||||
return closings, total, nil
|
return closings, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s closingService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) {
|
func (s closingService) GetProjectFlockByID(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) {
|
||||||
closing, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
|
projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), id, s.withRelations)
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Closing not found")
|
return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock not found")
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed get closing by id: %+v", err)
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return closing, nil
|
return projectFlock, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error) {
|
||||||
|
|
||||||
|
realisasi, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.
|
||||||
|
Preload("MarketingProduct").
|
||||||
|
Preload("MarketingProduct.ProductWarehouse").
|
||||||
|
Preload("MarketingProduct.ProductWarehouse.Product").
|
||||||
|
Preload("MarketingProduct.ProductWarehouse.Product.ProductCategory").
|
||||||
|
Preload("MarketingProduct.ProductWarehouse.Product.Uom").
|
||||||
|
Preload("MarketingProduct.ProductWarehouse.Product.Flags").
|
||||||
|
Preload("MarketingProduct.ProductWarehouse.Warehouse").
|
||||||
|
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang").
|
||||||
|
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang").
|
||||||
|
Preload("MarketingProduct.Marketing").
|
||||||
|
Preload("MarketingProduct.Marketing.Customer").
|
||||||
|
Order("marketing_delivery_products.delivery_date DESC")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(realisasi) == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Penjualan realisasi not found")
|
||||||
|
}
|
||||||
|
return realisasi, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error) {
|
||||||
|
if projectFlockID == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
|
||||||
|
}
|
||||||
|
|
||||||
|
project, err := s.Repository.GetByID(c.Context(), projectFlockID, s.withClosingRelations)
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed get project flock %d for closing summary: %+v", projectFlockID, err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
|
||||||
|
}
|
||||||
|
|
||||||
|
statusProject, statusClosing, err := s.getApprovalStatuses(c.Context(), projectFlockID)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to retrieve approval statuses for project flock %d: %+v", projectFlockID, err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch approval status")
|
||||||
|
}
|
||||||
|
|
||||||
|
summary := dto.ToClosingSummaryDTO(*project, statusProject, statusClosing)
|
||||||
|
|
||||||
|
return &summary, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s closingService) getApprovalStatuses(ctx context.Context, projectFlockID uint) (string, string, error) {
|
||||||
|
if s.ApprovalSvc == nil {
|
||||||
|
return "", "Belum Selesai", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
records, _, err := s.ApprovalSvc.List(ctx, utils.ApprovalWorkflowProjectFlock.String(), &projectFlockID, 1, 1000, "")
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
minStep uint16
|
||||||
|
statusProject string
|
||||||
|
completed int
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, rec := range records {
|
||||||
|
if minStep == 0 || rec.StepNumber < minStep {
|
||||||
|
minStep = rec.StepNumber
|
||||||
|
statusProject = rec.StepName
|
||||||
|
}
|
||||||
|
if rec.StepNumber == uint16(utils.ProjectFlockStepSelesai) {
|
||||||
|
completed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if statusProject == "" && minStep > 0 {
|
||||||
|
if label, ok := approvalutils.ApprovalStepName(utils.ApprovalWorkflowProjectFlock, approvalutils.ApprovalStep(minStep)); ok {
|
||||||
|
statusProject = label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
statusClosing := "Belum Selesai"
|
||||||
|
switch {
|
||||||
|
case len(records) == 0 || completed == 0:
|
||||||
|
statusClosing = "Belum Selesai"
|
||||||
|
case completed < len(records):
|
||||||
|
statusClosing = "Sebagian"
|
||||||
|
default:
|
||||||
|
statusClosing = "Selesai"
|
||||||
|
}
|
||||||
|
|
||||||
|
return statusProject, statusClosing, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,12 +151,16 @@ func (u *ExpenseController) UpdateOne(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.Documents = form.File["documents"]
|
req.Documents = form.File["documents"]
|
||||||
if transactionDate := c.FormValue("transaction_date"); transactionDate != "" {
|
|
||||||
|
transactionDate := c.FormValue("transaction_date")
|
||||||
|
if transactionDate != "" {
|
||||||
req.TransactionDate = &transactionDate
|
req.TransactionDate = &transactionDate
|
||||||
}
|
}
|
||||||
|
|
||||||
categoryVal := c.FormValue("category")
|
categoryVal := c.FormValue("category")
|
||||||
req.Category = &categoryVal
|
if categoryVal != "" {
|
||||||
|
req.Category = &categoryVal
|
||||||
|
}
|
||||||
|
|
||||||
supplierIDVal := c.FormValue("supplier_id")
|
supplierIDVal := c.FormValue("supplier_id")
|
||||||
if supplierIDVal != "" {
|
if supplierIDVal != "" {
|
||||||
@@ -312,13 +316,18 @@ func (u *ExpenseController) UpdateRealization(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
req.Documents = form.File["documents"]
|
req.Documents = form.File["documents"]
|
||||||
|
|
||||||
req.RealizationDate = c.FormValue("realization_date")
|
realizationDate := c.FormValue("realization_date")
|
||||||
|
if realizationDate != "" {
|
||||||
|
req.RealizationDate = &realizationDate
|
||||||
|
}
|
||||||
|
|
||||||
realizationsJSON := c.FormValue("realizations")
|
realizationsJSON := c.FormValue("realizations")
|
||||||
if realizationsJSON != "" {
|
if realizationsJSON != "" {
|
||||||
if err := json.Unmarshal([]byte(realizationsJSON), &req.Realizations); err != nil {
|
var realizations []validation.RealizationItem
|
||||||
|
if err := json.Unmarshal([]byte(realizationsJSON), &realizations); err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid realizations JSON: %v", err))
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid realizations JSON: %v", err))
|
||||||
}
|
}
|
||||||
|
req.Realizations = &realizations
|
||||||
}
|
}
|
||||||
|
|
||||||
expense, err := u.ExpenseService.UpdateRealization(c, uint(id), &req)
|
expense, err := u.ExpenseService.UpdateRealization(c, uint(id), &req)
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ func ExpenseRoutes(v1 fiber.Router, u user.UserService, s expense.ExpenseService
|
|||||||
|
|
||||||
route := v1.Group("/expenses")
|
route := v1.Group("/expenses")
|
||||||
route.Use(m.Auth(u))
|
route.Use(m.Auth(u))
|
||||||
|
|
||||||
|
// route.Use(m.Auth(u))
|
||||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
||||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
||||||
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
||||||
|
|||||||
@@ -732,10 +732,10 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va
|
|||||||
expenseRepoTx := repository.NewExpenseRepository(tx)
|
expenseRepoTx := repository.NewExpenseRepository(tx)
|
||||||
|
|
||||||
// Check if only updating documents
|
// Check if only updating documents
|
||||||
updateDataOnly := len(req.Realizations) == 0 && len(req.Documents) > 0
|
updateDataOnly := req.Realizations == nil && len(req.Documents) > 0
|
||||||
|
|
||||||
if len(req.Realizations) > 0 {
|
if req.Realizations != nil {
|
||||||
for _, realizationItem := range req.Realizations {
|
for _, realizationItem := range *req.Realizations {
|
||||||
|
|
||||||
expenseNonstockID := realizationItem.ExpenseNonstockID
|
expenseNonstockID := realizationItem.ExpenseNonstockID
|
||||||
|
|
||||||
@@ -770,6 +770,12 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.RealizationDate != nil {
|
||||||
|
if err := expenseRepoTx.PatchOne(c.Context(), expenseID, map[string]interface{}{"realization_date": *req.RealizationDate}, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update realization date")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if len(req.Documents) > 0 {
|
if len(req.Documents) > 0 {
|
||||||
if err := s.processDocuments(c, expenseRepoTx, expenseID, req.Documents, true); err != nil {
|
if err := s.processDocuments(c, expenseRepoTx, expenseID, req.Documents, true); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Create struct {
|
type Create struct {
|
||||||
PoNumber string `form:"po_number" json:"po_number" validate:"omitempty,max=50"`
|
PoNumber string `form:"po_number" json:"po_number" validate:"omitempty,max=50"`
|
||||||
TransactionDate string `form:"transaction_date" json:"transaction_date" validate:"required,datetime=2006-01-02"`
|
TransactionDate string `form:"transaction_date" json:"transaction_date" validate:"required,datetime=2006-01-02"`
|
||||||
Category string `form:"category" json:"category" validate:"required,oneof=BOP NON-BOP"`
|
Category string `form:"category" json:"category" validate:"required,oneof=BOP NON-BOP"`
|
||||||
SupplierID uint64 `form:"supplier_id" json:"supplier_id" validate:"required,gt=0"`
|
SupplierID uint64 `form:"supplier_id" json:"supplier_id" validate:"required,gt=0"`
|
||||||
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
|
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
|
||||||
ExpenseNonstocks []ExpenseNonstock `form:"expense_nonstocks" json:"expense_nonstocks" validate:"required,min=1,dive"`
|
ExpenseNonstocks []ExpenseNonstock `form:"expense_nonstocks" json:"expense_nonstocks" validate:"required,min=1,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,11 +26,11 @@ type CostItem struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Update struct {
|
type Update struct {
|
||||||
TransactionDate *string `form:"transaction_date" json:"transaction_date" validate:"omitempty,datetime=2006-01-02"`
|
TransactionDate *string `form:"transaction_date" json:"transaction_date" validate:"omitempty,datetime=2006-01-02"`
|
||||||
Category *string `form:"category" json:"category" validate:"omitempty,oneof=BOP NON-BOP"`
|
Category *string `form:"category" json:"category" validate:"omitempty,oneof=BOP NON-BOP"`
|
||||||
SupplierID *uint64 `form:"supplier_id" json:"supplier_id" validate:"omitempty,gt=0"`
|
SupplierID *uint64 `form:"supplier_id" json:"supplier_id" validate:"omitempty,gt=0"`
|
||||||
|
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
|
||||||
ExpenseNonstocks *[]ExpenseNonstock `form:"expense_nonstocks" json:"expense_nonstocks" validate:"omitempty,min=1,dive"`
|
ExpenseNonstocks *[]ExpenseNonstock `form:"expense_nonstocks" json:"expense_nonstocks" validate:"omitempty,min=1,dive"`
|
||||||
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
@@ -46,9 +46,9 @@ type CreateRealization struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UpdateRealization struct {
|
type UpdateRealization struct {
|
||||||
RealizationDate string `form:"realization_date" json:"realization_date" validate:"omitempty,datetime=2006-01-02"`
|
RealizationDate *string `form:"realization_date" json:"realization_date" validate:"omitempty,datetime=2006-01-02"`
|
||||||
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
|
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
|
||||||
Realizations []RealizationItem `form:"realizations" json:"realizations" validate:"required,min=1,dive"`
|
Realizations *[]RealizationItem `form:"realizations" json:"realizations" validate:"omitempty,min=1,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RealizationItem struct {
|
type RealizationItem struct {
|
||||||
|
|||||||
@@ -104,12 +104,12 @@ func ToProductWarehouseDTO(e *entity.ProductWarehouse) *ProductWarehouseDTO {
|
|||||||
|
|
||||||
func ToAdjustmentRelationDTO(e *entity.StockLog) AdjustmentRelationDTO {
|
func ToAdjustmentRelationDTO(e *entity.StockLog) AdjustmentRelationDTO {
|
||||||
return AdjustmentRelationDTO{
|
return AdjustmentRelationDTO{
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
TransactionType: e.TransactionType,
|
// TransactionType: e.LoggableType,
|
||||||
Quantity: e.Quantity,
|
// Quantity: e.Q,
|
||||||
BeforeQuantity: e.BeforeQuantity,
|
// BeforeQuantity: e.BeforeQuantity,
|
||||||
AfterQuantity: e.AfterQuantity,
|
// AfterQuantity: e.AfterQuantity,
|
||||||
Note: e.Note,
|
Note: e.Notes,
|
||||||
ProductWarehouseId: e.ProductWarehouseId,
|
ProductWarehouseId: e.ProductWarehouseId,
|
||||||
ProductWarehouse: ToProductWarehouseDTO(e.ProductWarehouse),
|
ProductWarehouse: ToProductWarehouseDTO(e.ProductWarehouse),
|
||||||
}
|
}
|
||||||
@@ -136,6 +136,6 @@ func ToAdjustmentListDTO(e *entity.StockLog) AdjustmentListDTO {
|
|||||||
func ToAdjustmentDetailDTO(e *entity.StockLog) AdjustmentDetailDTO {
|
func ToAdjustmentDetailDTO(e *entity.StockLog) AdjustmentDetailDTO {
|
||||||
return AdjustmentDetailDTO{
|
return AdjustmentDetailDTO{
|
||||||
AdjustmentListDTO: ToAdjustmentListDTO(e),
|
AdjustmentListDTO: ToAdjustmentListDTO(e),
|
||||||
UpdatedAt: e.UpdatedAt,
|
// UpdatedAt: e.UpdatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ func (s *adjustmentService) GetOne(c *fiber.Ctx, id uint) (*entity.StockLog, err
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if stockLog.LogType != entity.LogTypeAdjustment {
|
if stockLog.LoggableType != entity.LogTypeAdjustment {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Adjustment not found")
|
return nil, fiber.NewError(fiber.StatusNotFound, "Adjustment not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
ProductId: uint(req.ProductID),
|
ProductId: uint(req.ProductID),
|
||||||
WarehouseId: uint(req.WarehouseID),
|
WarehouseId: uint(req.WarehouseID),
|
||||||
Quantity: 0,
|
Quantity: 0,
|
||||||
CreatedBy: actorID,
|
// CreatedBy: 1, // TODO: should Get from auth middleware
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.ProductWarehouseRepo.CreateOne(ctx, newPW, nil); err != nil {
|
if err := s.ProductWarehouseRepo.CreateOne(ctx, newPW, nil); err != nil {
|
||||||
@@ -128,25 +128,23 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
}
|
}
|
||||||
|
|
||||||
afterQuantity := productWarehouse.Quantity
|
afterQuantity := productWarehouse.Quantity
|
||||||
|
newLog := &entity.StockLog{
|
||||||
|
// TransactionType: transactionType,
|
||||||
|
LoggableType: entity.LogTypeAdjustment,
|
||||||
|
LoggableId: 0,
|
||||||
|
Notes: req.Note,
|
||||||
|
ProductWarehouseId: productWarehouse.Id,
|
||||||
|
CreatedBy: actorID, // TODO: should Get from auth middleware
|
||||||
|
}
|
||||||
if transactionType == entity.TransactionTypeIncrease {
|
if transactionType == entity.TransactionTypeIncrease {
|
||||||
afterQuantity += req.Quantity
|
afterQuantity += req.Quantity
|
||||||
|
newLog.Increase = afterQuantity
|
||||||
} else {
|
} else {
|
||||||
if productWarehouse.Quantity < req.Quantity {
|
if productWarehouse.Quantity < req.Quantity {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Insufficient stock for adjustment")
|
return fiber.NewError(fiber.StatusBadRequest, "Insufficient stock for adjustment")
|
||||||
}
|
}
|
||||||
afterQuantity -= req.Quantity
|
afterQuantity -= req.Quantity
|
||||||
}
|
newLog.Decrease = afterQuantity
|
||||||
|
|
||||||
newLog := &entity.StockLog{
|
|
||||||
TransactionType: transactionType,
|
|
||||||
Quantity: req.Quantity,
|
|
||||||
BeforeQuantity: productWarehouse.Quantity,
|
|
||||||
AfterQuantity: afterQuantity,
|
|
||||||
LogType: entity.LogTypeAdjustment,
|
|
||||||
LogId: 0,
|
|
||||||
Note: req.Note,
|
|
||||||
ProductWarehouseId: productWarehouse.Id,
|
|
||||||
CreatedBy: actorID,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.StockLogsRepository.WithTx(tx).CreateOne(ctx, newLog, nil); err != nil {
|
if err := s.StockLogsRepository.WithTx(tx).CreateOne(ctx, newLog, nil); err != nil {
|
||||||
@@ -200,7 +198,7 @@ func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Qu
|
|||||||
|
|
||||||
db = s.withRelations(db)
|
db = s.withRelations(db)
|
||||||
|
|
||||||
db = db.Where("log_type = ?", entity.LogTypeAdjustment)
|
db = db.Where("loggable_type = ?", entity.LogTypeAdjustment)
|
||||||
|
|
||||||
if query.TransactionType != "" {
|
if query.TransactionType != "" {
|
||||||
db = db.Where("transaction_type = ?", strings.ToUpper(query.TransactionType))
|
db = db.Where("transaction_type = ?", strings.ToUpper(query.TransactionType))
|
||||||
|
|||||||
-51
@@ -1,51 +0,0 @@
|
|||||||
package repository
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MarketingDeliveryProductRepository interface {
|
|
||||||
repository.BaseRepository[entity.MarketingDeliveryProduct]
|
|
||||||
GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error)
|
|
||||||
GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type MarketingDeliveryProductRepositoryImpl struct {
|
|
||||||
*repository.BaseRepositoryImpl[entity.MarketingDeliveryProduct]
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMarketingDeliveryProductRepository(db *gorm.DB) MarketingDeliveryProductRepository {
|
|
||||||
return &MarketingDeliveryProductRepositoryImpl{
|
|
||||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.MarketingDeliveryProduct](db),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error) {
|
|
||||||
var deliveryProduct entity.MarketingDeliveryProduct
|
|
||||||
if err := r.DB().WithContext(ctx).Where("marketing_product_id = ?", marketingProductID).First(&deliveryProduct).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &deliveryProduct, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error) {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
return deliveryProducts, nil
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/dto"
|
||||||
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/services"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/validations"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
// entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProductStockController struct {
|
||||||
|
ProductStockService service.ProductStockService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProductStockController(productStockService service.ProductStockService) *ProductStockController {
|
||||||
|
return &ProductStockController{
|
||||||
|
ProductStockService: productStockService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ProductStockController) 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.ProductStockService.GetAll(c, query)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.SuccessWithPaginate[dto.ProductStockListDTO]{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get all productStocks successfully",
|
||||||
|
Meta: response.Meta{
|
||||||
|
Page: query.Page,
|
||||||
|
Limit: query.Limit,
|
||||||
|
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||||
|
TotalResults: totalResults,
|
||||||
|
},
|
||||||
|
Data: dto.ToProductStockListDTOs(result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ProductStockController) GetOne(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("id")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := u.ProductStockService.GetOne(c, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Retrieved product successfully",
|
||||||
|
Data: dto.ToProductStockDetailDTO(*res),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
|
||||||
|
productCategoryDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/dto"
|
||||||
|
uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto"
|
||||||
|
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// === DTO Structs ===
|
||||||
|
|
||||||
|
type ProductStockRelationDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductStockListDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Brand string `json:"brand"`
|
||||||
|
Sku *string `json:"sku,omitempty"`
|
||||||
|
ProductPrice float64 `json:"product_price"`
|
||||||
|
SellingPrice *float64 `json:"selling_price,omitempty"`
|
||||||
|
Tax *float64 `json:"tax,omitempty"`
|
||||||
|
ExpiryPeriod *int `json:"expiry_period,omitempty"`
|
||||||
|
Flags []string `json:"flags"`
|
||||||
|
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
|
||||||
|
ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"`
|
||||||
|
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
Suppliers []SupplierDTO `json:"suppliers,omitempty"`
|
||||||
|
ProductWarehouses []ProductWarehouseDTO `json:"product_warehouses,omitempty"`
|
||||||
|
TotalStock float64 `json:"total_stock"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductStockDetailDTO struct {
|
||||||
|
ProductStockListDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
type SupplierDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Alias string `json:"alias"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductWarehouseDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
ProductId uint `json:"product_id"`
|
||||||
|
WarehouseId uint `json:"warehouse_id"`
|
||||||
|
WarehouseName string `json:"warehouse_name"`
|
||||||
|
Location *locationDTO.LocationRelationDTO `json:"location"`
|
||||||
|
CurrentStock float64 `json:"current_stock"`
|
||||||
|
StockLogs []StockLogDetailDTO `json:"stock_logs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StockLogDetailDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Increase float64 `json:"increase"`
|
||||||
|
Decrease float64 `json:"decrease"`
|
||||||
|
LoggableType string `json:"loggable_type"`
|
||||||
|
LoggableId uint `json:"loggable_id"`
|
||||||
|
Notes *string `json:"notes"`
|
||||||
|
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||||
|
CreatedBy uint `json:"created_by"`
|
||||||
|
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Mapper Functions ===
|
||||||
|
func ToProductStockListDTO(e entity.Product) ProductStockListDTO {
|
||||||
|
var createdUser *userDTO.UserRelationDTO
|
||||||
|
if e.CreatedUser.Id != 0 {
|
||||||
|
mapped := userDTO.ToUserRelationDTO(e.CreatedUser)
|
||||||
|
createdUser = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
var categoryRef *productCategoryDTO.ProductCategoryRelationDTO
|
||||||
|
if e.ProductCategory.Id != 0 {
|
||||||
|
mapped := productCategoryDTO.ToProductCategoryRelationDTO(e.ProductCategory)
|
||||||
|
categoryRef = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := make([]string, len(e.Flags))
|
||||||
|
for i, f := range e.Flags {
|
||||||
|
flags[i] = f.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
var uomRef *uomDTO.UomRelationDTO
|
||||||
|
if e.Uom.Id != 0 {
|
||||||
|
mapped := uomDTO.ToUomRelationDTO(e.Uom)
|
||||||
|
uomRef = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
return ProductStockListDTO{
|
||||||
|
Id: e.Id,
|
||||||
|
Name: e.Name,
|
||||||
|
Flags: flags,
|
||||||
|
Uom: uomRef,
|
||||||
|
Brand: e.Brand,
|
||||||
|
Sku: e.Sku,
|
||||||
|
ProductPrice: e.ProductPrice,
|
||||||
|
SellingPrice: e.SellingPrice,
|
||||||
|
Tax: e.Tax,
|
||||||
|
ExpiryPeriod: e.ExpiryPeriod,
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
UpdatedAt: e.UpdatedAt,
|
||||||
|
CreatedUser: createdUser,
|
||||||
|
ProductCategory: categoryRef,
|
||||||
|
Suppliers: mapSupplierDTOs(e.ProductSuppliers),
|
||||||
|
TotalStock: calculateTotalStock(e.ProductWarehouses),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToProductStockListDTOs(e []entity.Product) []ProductStockListDTO {
|
||||||
|
result := make([]ProductStockListDTO, len(e))
|
||||||
|
for i, r := range e {
|
||||||
|
result[i] = ToProductStockListDTO(r)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToProductStockDetailDTO(e entity.Product) ProductStockDetailDTO {
|
||||||
|
base := ToProductStockListDTO(e)
|
||||||
|
base.ProductWarehouses = mapProductWarehouseDTOs(e.ProductWarehouses)
|
||||||
|
|
||||||
|
return ProductStockDetailDTO{
|
||||||
|
ProductStockListDTO: base,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- helpers ---
|
||||||
|
|
||||||
|
func mapSupplierDTOs(src []entity.ProductSupplier) []SupplierDTO {
|
||||||
|
if len(src) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result := make([]SupplierDTO, 0, len(src))
|
||||||
|
for _, ps := range src {
|
||||||
|
if ps.Supplier.Id == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result = append(result, SupplierDTO{
|
||||||
|
Id: ps.Supplier.Id,
|
||||||
|
Name: ps.Supplier.Name,
|
||||||
|
Alias: ps.Supplier.Alias,
|
||||||
|
Category: ps.Supplier.Category,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapProductWarehouseDTOs(src []entity.ProductWarehouse) []ProductWarehouseDTO {
|
||||||
|
if len(src) == 0 {
|
||||||
|
return []ProductWarehouseDTO{}
|
||||||
|
}
|
||||||
|
result := make([]ProductWarehouseDTO, 0, len(src))
|
||||||
|
for _, pw := range src {
|
||||||
|
dto := ProductWarehouseDTO{
|
||||||
|
Id: pw.Id,
|
||||||
|
ProductId: pw.ProductId,
|
||||||
|
WarehouseId: pw.WarehouseId,
|
||||||
|
CurrentStock: pw.Quantity,
|
||||||
|
StockLogs: mapStockLogs(pw.StockLogs),
|
||||||
|
}
|
||||||
|
if pw.Warehouse.Id != 0 {
|
||||||
|
dto.WarehouseName = pw.Warehouse.Name
|
||||||
|
if pw.Warehouse.Location != nil {
|
||||||
|
mapped := locationDTO.ToLocationRelationDTO(*pw.Warehouse.Location)
|
||||||
|
dto.Location = &mapped
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = append(result, dto)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapStockLogs(src []entity.StockLog) []StockLogDetailDTO {
|
||||||
|
if len(src) == 0 {
|
||||||
|
return []StockLogDetailDTO{}
|
||||||
|
}
|
||||||
|
result := make([]StockLogDetailDTO, 0, len(src))
|
||||||
|
for _, log := range src {
|
||||||
|
var notes *string
|
||||||
|
if log.Notes != "" {
|
||||||
|
n := log.Notes
|
||||||
|
notes = &n
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, StockLogDetailDTO{
|
||||||
|
Id: log.Id,
|
||||||
|
Increase: log.Increase,
|
||||||
|
Decrease: log.Decrease,
|
||||||
|
LoggableType: log.LoggableType,
|
||||||
|
LoggableId: log.LoggableId,
|
||||||
|
Notes: notes,
|
||||||
|
ProductWarehouseId: log.ProductWarehouseId,
|
||||||
|
CreatedBy: log.CreatedBy,
|
||||||
|
CreatedUser: mapCreatedUser(log.CreatedUser),
|
||||||
|
CreatedAt: log.CreatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapCreatedUser(user *entity.User) *userDTO.UserRelationDTO {
|
||||||
|
if user == nil || user.Id == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
mapped := userDTO.ToUserRelationDTO(*user)
|
||||||
|
return &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculateTotalStock(productWarehouses []entity.ProductWarehouse) float64 {
|
||||||
|
var total float64
|
||||||
|
for _, pw := range productWarehouses {
|
||||||
|
total += pw.Quantity
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package productStocks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
sProductStock "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/services"
|
||||||
|
|
||||||
|
rProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
|
||||||
|
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
|
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProductStockModule struct{}
|
||||||
|
|
||||||
|
func (ProductStockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||||
|
productRepo := rProduct.NewProductRepository(db)
|
||||||
|
userRepo := rUser.NewUserRepository(db)
|
||||||
|
|
||||||
|
productStockService := sProductStock.NewProductStockService(productRepo, validate)
|
||||||
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
|
ProductStockRoutes(router, userService, productStockService)
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
// import (
|
||||||
|
// entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
// "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
// "gorm.io/gorm"
|
||||||
|
// )
|
||||||
|
|
||||||
|
// type ProductStockRepository interface {
|
||||||
|
// repository.BaseRepository[entity.ProductStock]
|
||||||
|
// }
|
||||||
|
|
||||||
|
// type ProductStockRepositoryImpl struct {
|
||||||
|
// *repository.BaseRepositoryImpl[entity.ProductStock]
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func NewProductStockRepository(db *gorm.DB) ProductStockRepository {
|
||||||
|
// return &ProductStockRepositoryImpl{
|
||||||
|
// BaseRepositoryImpl: repository.NewBaseRepository[entity.ProductStock](db),
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package productStocks
|
||||||
|
|
||||||
|
import (
|
||||||
|
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/controllers"
|
||||||
|
productStock "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/services"
|
||||||
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ProductStockRoutes(v1 fiber.Router, u user.UserService, s productStock.ProductStockService) {
|
||||||
|
ctrl := controller.NewProductStockController(s)
|
||||||
|
|
||||||
|
route := v1.Group("/product-stocks")
|
||||||
|
|
||||||
|
// 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.Get("/:id", ctrl.GetOne)
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/validations"
|
||||||
|
productRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProductStockService interface {
|
||||||
|
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Product, int64, error)
|
||||||
|
GetOne(ctx *fiber.Ctx, id uint) (*entity.Product, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type productStockService struct {
|
||||||
|
Log *logrus.Logger
|
||||||
|
Validate *validator.Validate
|
||||||
|
ProductRepository productRepository.ProductRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProductStockService(
|
||||||
|
productRepo productRepository.ProductRepository,
|
||||||
|
validate *validator.Validate,
|
||||||
|
) ProductStockService {
|
||||||
|
return &productStockService{
|
||||||
|
Log: utils.Log,
|
||||||
|
Validate: validate,
|
||||||
|
ProductRepository: productRepo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s productStockService) withRelations(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.
|
||||||
|
Preload("CreatedUser").
|
||||||
|
Preload("Uom").
|
||||||
|
Preload("ProductCategory").
|
||||||
|
Preload("Flags").
|
||||||
|
Preload("ProductWarehouses").
|
||||||
|
Preload("ProductWarehouses.Warehouse").
|
||||||
|
Preload("ProductWarehouses.Warehouse.Location").
|
||||||
|
Preload("ProductWarehouses.Warehouse.Location.Area").
|
||||||
|
Preload("ProductWarehouses.StockLogs", func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Order("created_at ASC")
|
||||||
|
}).
|
||||||
|
Preload("ProductWarehouses.StockLogs.CreatedUser").
|
||||||
|
Preload("ProductSuppliers").
|
||||||
|
Preload("ProductSuppliers.Supplier", func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Order("suppliers.name ASC")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s productStockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Product, int64, error) {
|
||||||
|
if err := s.Validate.Struct(params); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := (params.Page - 1) * params.Limit
|
||||||
|
|
||||||
|
productStocks, total, err := s.ProductRepository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
|
db = s.withRelations(db)
|
||||||
|
if params.Search != "" {
|
||||||
|
return db.Where("name ILIKE ?", "%"+params.Search+"%")
|
||||||
|
}
|
||||||
|
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to get productStocks: %+v", err)
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
return productStocks, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s productStockService) GetOne(c *fiber.Ctx, id uint) (*entity.Product, error) {
|
||||||
|
product, err := s.ProductRepository.GetByID(c.Context(), id, s.withRelations)
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Product not found")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed get product by id: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return product, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
type Create struct {
|
||||||
|
Name string `json:"name" validate:"required_strict,min=3"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Update struct {
|
||||||
|
Name *string `json:"name,omitempty" validate:"omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
@@ -98,8 +98,8 @@ func ToProductWarehouseNestedDTO(e entity.ProductWarehouse) ProductWarehousNeste
|
|||||||
func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDTO {
|
func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDTO {
|
||||||
dto := ProductWarehouseListDTO{
|
dto := ProductWarehouseListDTO{
|
||||||
ProductWarehouseRelationDTO: ToProductWarehouseRelationDTO(e),
|
ProductWarehouseRelationDTO: ToProductWarehouseRelationDTO(e),
|
||||||
CreatedAt: e.CreatedAt,
|
// CreatedAt: e.CreatedAt,
|
||||||
UpdatedAt: e.UpdatedAt,
|
// UpdatedAt: e.UpdatedAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map Product relation jika ada
|
// Map Product relation jika ada
|
||||||
@@ -140,13 +140,13 @@ func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDT
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Map CreatedUser relation jika ada
|
// Map CreatedUser relation jika ada
|
||||||
if e.CreatedUser.Id != 0 {
|
// if e.CreatedUser.Id != 0 {
|
||||||
user := UserRelationDTO{
|
// user := UserRelationDTO{
|
||||||
Id: e.CreatedUser.Id,
|
// Id: e.CreatedUser.Id,
|
||||||
Username: e.CreatedUser.Name,
|
// Username: e.CreatedUser.Name,
|
||||||
}
|
// }
|
||||||
dto.CreatedUser = &user
|
// dto.CreatedUser = &user
|
||||||
}
|
// }
|
||||||
|
|
||||||
return dto
|
return dto
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-7
@@ -151,7 +151,7 @@ func (r *ProductWarehouseRepositoryImpl) AdjustQuantities(ctx context.Context, d
|
|||||||
}
|
}
|
||||||
if err := base.Model(&entity.ProductWarehouse{}).
|
if err := base.Model(&entity.ProductWarehouse{}).
|
||||||
Where("id = ?", id).
|
Where("id = ?", id).
|
||||||
Update("quantity", gorm.Expr("COALESCE(quantity,0) + ?", delta)).Error; err != nil {
|
Update("qty", gorm.Expr("COALESCE(qty,0) + ?", delta)).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,7 +171,7 @@ func (r *ProductWarehouseRepositoryImpl) CleanupEmpty(ctx context.Context, affec
|
|||||||
var emptyIDs []uint
|
var emptyIDs []uint
|
||||||
if err := r.DB().WithContext(ctx).
|
if err := r.DB().WithContext(ctx).
|
||||||
Model(&entity.ProductWarehouse{}).
|
Model(&entity.ProductWarehouse{}).
|
||||||
Where("id IN ? AND COALESCE(quantity,0) <= 0", ids).
|
Where("id IN ? AND COALESCE(qty,0) <= 0", ids).
|
||||||
Pluck("id", &emptyIDs).Error; err != nil {
|
Pluck("id", &emptyIDs).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -213,11 +213,11 @@ func (r *ProductWarehouseRepositoryImpl) EnsureProductWarehouse(
|
|||||||
ProductId: productID,
|
ProductId: productID,
|
||||||
WarehouseId: warehouseID,
|
WarehouseId: warehouseID,
|
||||||
Quantity: 0,
|
Quantity: 0,
|
||||||
CreatedBy: uint(createdBy),
|
// CreatedBy: uint(createdBy),
|
||||||
}
|
|
||||||
if entity.CreatedBy == 0 {
|
|
||||||
entity.CreatedBy = 1
|
|
||||||
}
|
}
|
||||||
|
// if entity.CreatedBy == 0 {
|
||||||
|
// entity.CreatedBy = 1
|
||||||
|
// }
|
||||||
|
|
||||||
if err := r.CreateOne(ctx, entity, nil); err != nil {
|
if err := r.CreateOne(ctx, entity, nil); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
@@ -257,7 +257,7 @@ func (r *ProductWarehouseRepositoryImpl) GetByFlagAndWarehouseID(ctx context.Con
|
|||||||
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
||||||
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'").
|
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'").
|
||||||
Where("flags.name = ? AND product_warehouses.warehouse_id = ?", flagName, warehouseId).
|
Where("flags.name = ? AND product_warehouses.warehouse_id = ?", flagName, warehouseId).
|
||||||
Order("product_warehouses.created_at DESC").
|
Order("product_warehouses.id DESC").
|
||||||
Preload("Product").Preload("Warehouse").
|
Preload("Product").Preload("Warehouse").
|
||||||
Find(&productWarehouses).Error
|
Find(&productWarehouses).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func (s productWarehouseService) withRelations(db *gorm.DB) *gorm.DB {
|
|||||||
Preload("Warehouse.Location").
|
Preload("Warehouse.Location").
|
||||||
Preload("Warehouse.Area").
|
Preload("Warehouse.Area").
|
||||||
Preload("Warehouse.Kandang").
|
Preload("Warehouse.Kandang").
|
||||||
Preload("CreatedUser")
|
Preload("ProjectFlockKandang")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProductWarehouse, int64, error) {
|
func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProductWarehouse, int64, error) {
|
||||||
@@ -104,7 +104,7 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query)
|
|||||||
|
|
||||||
db = s.Repository.ApplyFlagsFilter(db, cleanFlags)
|
db = s.Repository.ApplyFlagsFilter(db, cleanFlags)
|
||||||
|
|
||||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
return db.Order("product_warehouses.id DESC")
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
adjustments "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments"
|
adjustments "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments"
|
||||||
|
productStocks "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks"
|
||||||
productWarehouses "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses"
|
productWarehouses "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses"
|
||||||
transfers "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers"
|
transfers "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers"
|
||||||
// MODULE IMPORTS
|
// MODULE IMPORTS
|
||||||
@@ -21,6 +22,7 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida
|
|||||||
|
|
||||||
adjustments.AdjustmentModule{},
|
adjustments.AdjustmentModule{},
|
||||||
transfers.TransferModule{},
|
transfers.TransferModule{},
|
||||||
|
productStocks.ProductStockModule{},
|
||||||
// MODULE REGISTRY
|
// MODULE REGISTRY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -271,15 +271,18 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
s.Log.Infof("Source product warehouse updated: %+v", sourcePW.Id)
|
s.Log.Infof("Source product warehouse updated: %+v", sourcePW.Id)
|
||||||
|
|
||||||
// create stock log for decrease (source)
|
// create stock log for decrease (source)
|
||||||
beforeQty := sourcePW.Quantity + product.ProductQty // sourcePW already decreased
|
// beforeQty := sourcePW.Quantity + product.ProductQty // sourcePW already decreased
|
||||||
decreaseLog := &entity.StockLog{
|
decreaseLog := &entity.StockLog{
|
||||||
TransactionType: entity.TransactionTypeDecrease,
|
// TransactionType: entity.TransactionTypeDecrease,
|
||||||
Quantity: product.ProductQty,
|
// Quantity: product.ProductQty,
|
||||||
BeforeQuantity: beforeQty,
|
// BeforeQuantity: beforeQty,
|
||||||
AfterQuantity: sourcePW.Quantity,
|
// AfterQuantity: sourcePW.Qty,
|
||||||
LogType: entity.LogTypeTransfer,
|
// LogType: entity.LogTypeTransfer,
|
||||||
LogId: uint(entityTransfer.Id),
|
// LogId: uint(entityTransfer.Id),
|
||||||
Note: "",
|
Decrease: product.ProductQty,
|
||||||
|
Notes: "",
|
||||||
|
LoggableType: entity.LogTypeTransfer,
|
||||||
|
LoggableId: uint(entityTransfer.Id),
|
||||||
ProductWarehouseId: sourcePW.Id,
|
ProductWarehouseId: sourcePW.Id,
|
||||||
CreatedBy: actorID,
|
CreatedBy: actorID,
|
||||||
}
|
}
|
||||||
@@ -302,7 +305,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
ProductId: uint(product.ProductID),
|
ProductId: uint(product.ProductID),
|
||||||
WarehouseId: uint(req.DestinationWarehouseID),
|
WarehouseId: uint(req.DestinationWarehouseID),
|
||||||
Quantity: 0,
|
Quantity: 0,
|
||||||
CreatedBy: actorID,
|
// CreatedBy: 1, // TODO: should Get from auth middleware
|
||||||
}
|
}
|
||||||
if err := s.ProductWarehouseRepo.WithTx(tx).CreateOne(c.Context(), destPW, nil); err != nil {
|
if err := s.ProductWarehouseRepo.WithTx(tx).CreateOne(c.Context(), destPW, nil); err != nil {
|
||||||
s.Log.Errorf("Failed to create destination product warehouse: %+v", err)
|
s.Log.Errorf("Failed to create destination product warehouse: %+v", err)
|
||||||
@@ -319,15 +322,16 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
s.Log.Infof("Destination product warehouse updated: %+v", destPW.Id)
|
s.Log.Infof("Destination product warehouse updated: %+v", destPW.Id)
|
||||||
|
|
||||||
// create stock log for increase (destination)
|
// create stock log for increase (destination)
|
||||||
beforeDestQty := destPW.Quantity - product.ProductQty
|
// beforeDestQty := destPW.Quantity - product.ProductQty
|
||||||
increaseLog := &entity.StockLog{
|
increaseLog := &entity.StockLog{
|
||||||
TransactionType: entity.TransactionTypeIncrease,
|
// TransactionType: entity.TransactionTypeIncrease,
|
||||||
Quantity: product.ProductQty,
|
// Quantity: product.ProductQty,
|
||||||
BeforeQuantity: beforeDestQty,
|
// BeforeQuantity: beforeDestQty,
|
||||||
AfterQuantity: destPW.Quantity,
|
// AfterQuantity: destPW.Qty,
|
||||||
LogType: entity.LogTypeTransfer,
|
Increase: product.ProductQty,
|
||||||
LogId: uint(entityTransfer.Id),
|
LoggableType: entity.LogTypeTransfer,
|
||||||
Note: "",
|
LoggableId: uint(entityTransfer.Id),
|
||||||
|
Notes: "",
|
||||||
ProductWarehouseId: destPW.Id,
|
ProductWarehouseId: destPW.Id,
|
||||||
CreatedBy: actorID,
|
CreatedBy: actorID,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -319,15 +319,20 @@ func groupDeliveryProducts(products []MarketingDeliveryProductDTO, soNumber stri
|
|||||||
})
|
})
|
||||||
|
|
||||||
for i := range groups {
|
for i := range groups {
|
||||||
if groups[i].DeliveryDate != nil {
|
groups[i].DoNumber = GenerateDeliveryOrderNumber(soNumber, groups[i].DeliveryDate, groups[i].Warehouse.Id)
|
||||||
dateStr := groups[i].DeliveryDate.Format("20060102")
|
|
||||||
groups[i].DoNumber = fmt.Sprintf("%s-%s-%d", soNumber, dateStr, groups[i].Warehouse.Id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return groups
|
return groups
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GenerateDeliveryOrderNumber(soNumber string, deliveryDate *time.Time, warehouseId uint) string {
|
||||||
|
dateStr := ""
|
||||||
|
if deliveryDate != nil {
|
||||||
|
dateStr = deliveryDate.Format("20060102")
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s-%s-%d", soNumber, dateStr, warehouseId)
|
||||||
|
}
|
||||||
|
|
||||||
func getVehicleNumber(e entity.MarketingProduct) string {
|
func getVehicleNumber(e entity.MarketingProduct) string {
|
||||||
if e.DeliveryProduct != nil && e.DeliveryProduct.VehicleNumber != "" {
|
if e.DeliveryProduct != nil && e.DeliveryProduct.VehicleNumber != "" {
|
||||||
return e.DeliveryProduct.VehicleNumber
|
return e.DeliveryProduct.VehicleNumber
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
|
|
||||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
rMarketingDeliveryProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/marketing-delivery-products/repositories"
|
|
||||||
sDeliveryOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/services"
|
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"
|
rMarketing "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
|
||||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
@@ -22,7 +21,7 @@ type DeliveryOrdersModule struct{}
|
|||||||
func (DeliveryOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
func (DeliveryOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||||
marketingRepo := rMarketing.NewMarketingRepository(db)
|
marketingRepo := rMarketing.NewMarketingRepository(db)
|
||||||
marketingProductRepo := rMarketing.NewMarketingProductRepository(db)
|
marketingProductRepo := rMarketing.NewMarketingProductRepository(db)
|
||||||
marketingDeliveryProductRepo := rMarketingDeliveryProduct.NewMarketingDeliveryProductRepository(db)
|
marketingDeliveryProductRepo := rMarketing.NewMarketingDeliveryProductRepository(db)
|
||||||
userRepo := rUser.NewUserRepository(db)
|
userRepo := rUser.NewUserRepository(db)
|
||||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
approvalSvc := commonSvc.NewApprovalService(approvalRepo)
|
approvalSvc := commonSvc.NewApprovalService(approvalRepo)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package delivery_orderss
|
package delivery_orderss
|
||||||
|
|
||||||
import (
|
import (
|
||||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/controllers"
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/controllers"
|
||||||
deliveryOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/services"
|
deliveryOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/services"
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -17,6 +17,7 @@ func DeliveryOrdersRoutes(v1 fiber.Router, u user.UserService, s deliveryOrders.
|
|||||||
|
|
||||||
// Sisanya di group /delivery-orders
|
// Sisanya di group /delivery-orders
|
||||||
route := v1.Group("/delivery-orders")
|
route := v1.Group("/delivery-orders")
|
||||||
|
route.Use(m.Auth(u))
|
||||||
|
|
||||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
||||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
||||||
|
|||||||
@@ -8,9 +8,8 @@ import (
|
|||||||
|
|
||||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
marketingDeliveryProductRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/marketing-delivery-products/repositories"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/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"
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/dto"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/validations"
|
||||||
@@ -35,14 +34,14 @@ type deliveryOrdersService struct {
|
|||||||
Validate *validator.Validate
|
Validate *validator.Validate
|
||||||
MarketingRepo marketingRepo.MarketingRepository
|
MarketingRepo marketingRepo.MarketingRepository
|
||||||
MarketingProductRepo marketingRepo.MarketingProductRepository
|
MarketingProductRepo marketingRepo.MarketingProductRepository
|
||||||
MarketingDeliveryProductRepo marketingDeliveryProductRepo.MarketingDeliveryProductRepository
|
MarketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository
|
||||||
ApprovalSvc commonSvc.ApprovalService
|
ApprovalSvc commonSvc.ApprovalService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDeliveryOrdersService(
|
func NewDeliveryOrdersService(
|
||||||
marketingRepo marketingRepo.MarketingRepository,
|
marketingRepo marketingRepo.MarketingRepository,
|
||||||
marketingProductRepo marketingRepo.MarketingProductRepository,
|
marketingProductRepo marketingRepo.MarketingProductRepository,
|
||||||
marketingDeliveryProductRepo marketingDeliveryProductRepo.MarketingDeliveryProductRepository,
|
marketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository,
|
||||||
approvalSvc commonSvc.ApprovalService,
|
approvalSvc commonSvc.ApprovalService,
|
||||||
validate *validator.Validate,
|
validate *validator.Validate,
|
||||||
) DeliveryOrdersService {
|
) DeliveryOrdersService {
|
||||||
@@ -200,7 +199,7 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
|||||||
err = s.MarketingRepo.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)
|
marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction)
|
||||||
marketingDeliveryProductRepositoryTx := marketingDeliveryProductRepo.NewMarketingDeliveryProductRepository(dbTransaction)
|
marketingDeliveryProductRepositoryTx := marketingRepo.NewMarketingDeliveryProductRepository(dbTransaction)
|
||||||
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||||
|
|
||||||
allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), req.MarketingId)
|
allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), req.MarketingId)
|
||||||
@@ -259,7 +258,6 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
approvalAction := entity.ApprovalActionApproved
|
approvalAction := entity.ApprovalActionApproved
|
||||||
if _, err := approvalSvcTx.CreateApproval(
|
if _, err := approvalSvcTx.CreateApproval(
|
||||||
c.Context(),
|
c.Context(),
|
||||||
@@ -301,7 +299,7 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, i
|
|||||||
err := s.MarketingRepo.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)
|
marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction)
|
||||||
marketingDeliveryProductRepositoryTx := marketingDeliveryProductRepo.NewMarketingDeliveryProductRepository(dbTransaction)
|
marketingDeliveryProductRepositoryTx := marketingRepo.NewMarketingDeliveryProductRepository(dbTransaction)
|
||||||
|
|
||||||
allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), id)
|
allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), id)
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
|||||||
+55
@@ -1,6 +1,8 @@
|
|||||||
package repository
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -8,6 +10,9 @@ import (
|
|||||||
|
|
||||||
type MarketingDeliveryProductRepository interface {
|
type MarketingDeliveryProductRepository interface {
|
||||||
repository.BaseRepository[entity.MarketingDeliveryProduct]
|
repository.BaseRepository[entity.MarketingDeliveryProduct]
|
||||||
|
GetDeliveryProductsByProjectFlockID(ctx context.Context, projectFlockID uint, callback func(*gorm.DB) *gorm.DB) ([]entity.MarketingDeliveryProduct, error)
|
||||||
|
GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error)
|
||||||
|
GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type MarketingDeliveryProductRepositoryImpl struct {
|
type MarketingDeliveryProductRepositoryImpl struct {
|
||||||
@@ -19,3 +24,53 @@ func NewMarketingDeliveryProductRepository(db *gorm.DB) MarketingDeliveryProduct
|
|||||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.MarketingDeliveryProduct](db),
|
BaseRepositoryImpl: repository.NewBaseRepository[entity.MarketingDeliveryProduct](db),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *MarketingDeliveryProductRepositoryImpl) GetDeliveryProductsByProjectFlockID(ctx context.Context, projectFlockID uint, callback func(*gorm.DB) *gorm.DB) ([]entity.MarketingDeliveryProduct, error) {
|
||||||
|
var deliveryProducts []entity.MarketingDeliveryProduct
|
||||||
|
|
||||||
|
// JOIN digunakan untuk filter WHERE clause ke ProjectFlockID yang berada 3 level relasi atas
|
||||||
|
// Entity relations digunakan di Preload (callback) untuk load data, bukan untuk filter
|
||||||
|
db := r.DB().WithContext(ctx).
|
||||||
|
Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id").
|
||||||
|
Joins("JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id").
|
||||||
|
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = product_warehouses.project_flock_kandang_id").
|
||||||
|
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
|
||||||
|
Distinct("marketing_delivery_products.*")
|
||||||
|
|
||||||
|
if callback != nil {
|
||||||
|
db = callback(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Find(&deliveryProducts).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return deliveryProducts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error) {
|
||||||
|
var deliveryProducts []entity.MarketingDeliveryProduct
|
||||||
|
|
||||||
|
// JOIN untuk filter by marketing_id yang ada di related table
|
||||||
|
db := r.DB().WithContext(ctx).
|
||||||
|
Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id").
|
||||||
|
Where("marketing_products.marketing_id = ?", marketingId)
|
||||||
|
|
||||||
|
if err := db.Find(&deliveryProducts).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return deliveryProducts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error) {
|
||||||
|
var deliveryProduct entity.MarketingDeliveryProduct
|
||||||
|
|
||||||
|
if err := r.DB().WithContext(ctx).
|
||||||
|
Where("marketing_product_id = ?", marketingProductID).
|
||||||
|
First(&deliveryProduct).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &deliveryProduct, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package sales_orders
|
package sales_orders
|
||||||
|
|
||||||
import (
|
import (
|
||||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/controllers"
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/controllers"
|
||||||
salesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services"
|
salesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services"
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -14,6 +14,7 @@ func SalesOrdersRoutes(v1 fiber.Router, u user.UserService, s salesOrders.SalesO
|
|||||||
|
|
||||||
v1.Delete("/:id", ctrl.DeleteOne)
|
v1.Delete("/:id", ctrl.DeleteOne)
|
||||||
route := v1.Group("/sales-orders")
|
route := v1.Group("/sales-orders")
|
||||||
|
route.Use(m.Auth(u))
|
||||||
|
|
||||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
||||||
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
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"
|
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"
|
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"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/validations"
|
||||||
@@ -125,7 +124,7 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
|
|
||||||
marketingRepoTx := repository.NewMarketingRepository(dbTransaction)
|
marketingRepoTx := repository.NewMarketingRepository(dbTransaction)
|
||||||
marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction)
|
marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction)
|
||||||
invDeliveryRepoTx := rInvMarketingDeliveryProduct.NewMarketingDeliveryProductRepository(dbTransaction)
|
invDeliveryRepoTx := repository.NewMarketingDeliveryProductRepository(dbTransaction)
|
||||||
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||||
|
|
||||||
marketing = &entity.Marketing{
|
marketing = &entity.Marketing{
|
||||||
@@ -220,7 +219,7 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
|
|||||||
marketingRepoTx := repository.NewMarketingRepository(dbTransaction)
|
marketingRepoTx := repository.NewMarketingRepository(dbTransaction)
|
||||||
marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction)
|
marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction)
|
||||||
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||||
invDeliveryRepoTx := rInvMarketingDeliveryProduct.NewMarketingDeliveryProductRepository(dbTransaction)
|
invDeliveryRepoTx := repository.NewMarketingDeliveryProductRepository(dbTransaction)
|
||||||
|
|
||||||
updateBody := make(map[string]any)
|
updateBody := make(map[string]any)
|
||||||
if req.CustomerId != 0 {
|
if req.CustomerId != 0 {
|
||||||
@@ -527,7 +526,7 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e
|
|||||||
return updated, nil
|
return updated, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Context, marketingId uint, rp validation.CreateMarketingProduct, marketingProductRepo repository.MarketingProductRepository, invDeliveryRepo rInvMarketingDeliveryProduct.MarketingDeliveryProductRepository) error {
|
func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Context, marketingId uint, rp validation.CreateMarketingProduct, marketingProductRepo repository.MarketingProductRepository, invDeliveryRepo repository.MarketingDeliveryProductRepository) error {
|
||||||
|
|
||||||
marketingProduct := &entity.MarketingProduct{
|
marketingProduct := &entity.MarketingProduct{
|
||||||
MarketingId: marketingId,
|
MarketingId: marketingId,
|
||||||
|
|||||||
@@ -557,7 +557,7 @@ func (s *chickinService) getOrCreateProductWarehouse(ctx *fiber.Ctx, warehouseId
|
|||||||
ProductId: product.Id,
|
ProductId: product.Id,
|
||||||
WarehouseId: warehouseId,
|
WarehouseId: warehouseId,
|
||||||
Quantity: 0,
|
Quantity: 0,
|
||||||
CreatedBy: actorID,
|
// CreatedBy: actorID,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.ProductWarehouseRepo.WithTx(dbTransaction).CreateOne(ctx.Context(), newPW, nil); err != nil {
|
if err := s.ProductWarehouseRepo.WithTx(dbTransaction).CreateOne(ctx.Context(), newPW, nil); err != nil {
|
||||||
|
|||||||
@@ -329,3 +329,29 @@ func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error {
|
|||||||
Message: "Get projectflock kandang successfully",
|
Message: "Get projectflock kandang successfully",
|
||||||
Data: dtoResult})
|
Data: dtoResult})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *ProjectflockController) Resubmit(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("id")
|
||||||
|
req := new(validation.Resubmit)
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.ProjectflockService.Resubmit(c, req, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Resubmit projectflock successfully",
|
||||||
|
Data: dto.ToProjectFlockListDTO(*result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
fcrDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto"
|
fcrDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto"
|
||||||
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
|
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
|
||||||
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
|
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
|
||||||
|
nonstockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/dto"
|
||||||
|
|
||||||
// pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
|
// pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
|
||||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||||
@@ -24,15 +25,16 @@ type ProjectFlockRelationDTO struct {
|
|||||||
|
|
||||||
type ProjectFlockListDTO struct {
|
type ProjectFlockListDTO struct {
|
||||||
ProjectFlockRelationDTO
|
ProjectFlockRelationDTO
|
||||||
Area *areaDTO.AreaRelationDTO `json:"area,omitempty"`
|
Area *areaDTO.AreaRelationDTO `json:"area,omitempty"`
|
||||||
Category string `json:"category"`
|
Category string `json:"category"`
|
||||||
Fcr *fcrDTO.FcrRelationDTO `json:"fcr,omitempty"`
|
Fcr *fcrDTO.FcrRelationDTO `json:"fcr,omitempty"`
|
||||||
Location *locationDTO.LocationRelationDTO `json:"location,omitempty"`
|
Location *locationDTO.LocationRelationDTO `json:"location,omitempty"`
|
||||||
Kandangs []KandangWithProjectFlockIdDTO `json:"kandangs,omitempty"`
|
Kandangs []KandangWithProjectFlockIdDTO `json:"kandangs,omitempty"`
|
||||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
ProjectBudgets []ProjectBudgetDTO `json:"project_budgets,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type KandangWithProjectFlockIdDTO struct {
|
type KandangWithProjectFlockIdDTO struct {
|
||||||
@@ -51,6 +53,13 @@ type KandangPeriodSummaryDTO struct {
|
|||||||
Period int `json:"period"`
|
Period int `json:"period"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProjectBudgetDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Qty float64 `json:"qty"`
|
||||||
|
Price float64 `json:"price"`
|
||||||
|
Nonstock *nonstockDTO.NonstockRelationDTO `json:"nonstock,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
func ToProjectFlockListDTOWithPeriod(e entity.ProjectFlock, period int) ProjectFlockListDTO {
|
func ToProjectFlockListDTOWithPeriod(e entity.ProjectFlock, period int) ProjectFlockListDTO {
|
||||||
var createdUser *userDTO.UserRelationDTO
|
var createdUser *userDTO.UserRelationDTO
|
||||||
if e.CreatedUser.Id != 0 {
|
if e.CreatedUser.Id != 0 {
|
||||||
@@ -110,6 +119,7 @@ func ToProjectFlockListDTOWithPeriod(e entity.ProjectFlock, period int) ProjectF
|
|||||||
ProjectFlockRelationDTO: createProjectFlockRelationDTO(e, period),
|
ProjectFlockRelationDTO: createProjectFlockRelationDTO(e, period),
|
||||||
Area: areaSummary,
|
Area: areaSummary,
|
||||||
Kandangs: kandangSummaries,
|
Kandangs: kandangSummaries,
|
||||||
|
ProjectBudgets: ToProjectBudgetDTOs(e.Budgets),
|
||||||
Category: e.Category,
|
Category: e.Category,
|
||||||
Fcr: fcrSummary,
|
Fcr: fcrSummary,
|
||||||
Location: locationSummary,
|
Location: locationSummary,
|
||||||
@@ -184,3 +194,26 @@ func createProjectFlockRelationDTO(e entity.ProjectFlock, period int) ProjectFlo
|
|||||||
FlockName: e.FlockName,
|
FlockName: e.FlockName,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ToProjectBudgetDTO(e entity.ProjectBudget) ProjectBudgetDTO {
|
||||||
|
var nonstockRef *nonstockDTO.NonstockRelationDTO
|
||||||
|
if e.Nonstock != nil && e.Nonstock.Id != 0 {
|
||||||
|
mapped := nonstockDTO.ToNonstockRelationDTO(*e.Nonstock)
|
||||||
|
nonstockRef = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
return ProjectBudgetDTO{
|
||||||
|
Id: e.Id,
|
||||||
|
Qty: e.Qty,
|
||||||
|
Price: e.Price,
|
||||||
|
Nonstock: nonstockRef,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToProjectBudgetDTOs(e []entity.ProjectBudget) []ProjectBudgetDTO {
|
||||||
|
result := make([]ProjectBudgetDTO, len(e))
|
||||||
|
for i, r := range e {
|
||||||
|
result[i] = ToProjectBudgetDTO(r)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ import (
|
|||||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
rFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories"
|
rFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories"
|
||||||
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
||||||
|
rNonstock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/repositories"
|
||||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
|
rProjectBudget "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
rProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
rProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
|
|
||||||
sProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services"
|
sProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services"
|
||||||
@@ -27,10 +29,12 @@ type ProjectflockModule struct{}
|
|||||||
func (ProjectflockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
func (ProjectflockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||||
flockRepo := rFlock.NewFlockRepository(db)
|
flockRepo := rFlock.NewFlockRepository(db)
|
||||||
kandangRepo := rKandang.NewKandangRepository(db)
|
kandangRepo := rKandang.NewKandangRepository(db)
|
||||||
|
nonstockRepo := rNonstock.NewNonstockRepository(db)
|
||||||
projectflockRepo := rProjectflock.NewProjectflockRepository(db)
|
projectflockRepo := rProjectflock.NewProjectflockRepository(db)
|
||||||
projectflockKandangRepo := rProjectflock.NewProjectFlockKandangRepository(db)
|
projectflockKandangRepo := rProjectflock.NewProjectFlockKandangRepository(db)
|
||||||
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||||
|
projectBudgetRepo := rProjectBudget.NewProjectBudgetRepository(db)
|
||||||
userRepo := rUser.NewUserRepository(db)
|
userRepo := rUser.NewUserRepository(db)
|
||||||
|
|
||||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
@@ -39,7 +43,7 @@ func (ProjectflockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, valid
|
|||||||
panic(fmt.Sprintf("failed to register project flock approval workflow: %v", err))
|
panic(fmt.Sprintf("failed to register project flock approval workflow: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
projectflockService := sProjectflock.NewProjectflockService(projectflockRepo, flockRepo, kandangRepo, projectflockKandangRepo, warehouseRepo, productWarehouseRepo, approvalService, validate)
|
projectflockService := sProjectflock.NewProjectflockService(projectflockRepo, flockRepo, kandangRepo, projectflockKandangRepo, warehouseRepo, productWarehouseRepo, projectBudgetRepo, nonstockRepo, approvalService, validate)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
ProjectflockRoutes(router, userService, projectflockService)
|
ProjectflockRoutes(router, userService, projectflockService)
|
||||||
|
|||||||
@@ -54,7 +54,10 @@ func (r *ProjectflockRepositoryImpl) WithDefaultRelations() func(*gorm.DB) *gorm
|
|||||||
Preload("Location").
|
Preload("Location").
|
||||||
Preload("Kandangs").
|
Preload("Kandangs").
|
||||||
Preload("KandangHistory").
|
Preload("KandangHistory").
|
||||||
Preload("KandangHistory.Kandang")
|
Preload("KandangHistory.Kandang").
|
||||||
|
Preload("Budgets").
|
||||||
|
Preload("Budgets.Nonstock").
|
||||||
|
Preload("Budgets.Nonstock.Uom")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,5 +23,6 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj
|
|||||||
route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang)
|
route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang)
|
||||||
route.Post("/approvals", ctrl.Approval)
|
route.Post("/approvals", ctrl.Approval)
|
||||||
route.Get("/locations/:location_id/periods", ctrl.GetPeriodSummary)
|
route.Get("/locations/:location_id/periods", ctrl.GetPeriodSummary)
|
||||||
|
route.Put("/:id/resubmit", ctrl.Resubmit)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ import (
|
|||||||
flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
|
flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
|
||||||
flockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories"
|
flockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories"
|
||||||
kandangRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
kandangRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
||||||
|
nonstockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/repositories"
|
||||||
warehouseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
warehouseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
|
projectBudgetRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
|
pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations"
|
||||||
@@ -39,6 +41,7 @@ type ProjectflockService interface {
|
|||||||
GetPeriodSummary(ctx *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error)
|
GetPeriodSummary(ctx *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error)
|
||||||
GetProjectPeriods(ctx *fiber.Ctx, projectIDs []uint) (map[uint]int, error)
|
GetProjectPeriods(ctx *fiber.Ctx, projectIDs []uint) (map[uint]int, error)
|
||||||
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error)
|
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error)
|
||||||
|
Resubmit(ctx *fiber.Ctx, req *validation.Resubmit, id uint) (*entity.ProjectFlock, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type projectflockService struct {
|
type projectflockService struct {
|
||||||
@@ -47,8 +50,10 @@ type projectflockService struct {
|
|||||||
Repository repository.ProjectflockRepository
|
Repository repository.ProjectflockRepository
|
||||||
FlockRepo flockRepository.FlockRepository
|
FlockRepo flockRepository.FlockRepository
|
||||||
KandangRepo kandangRepository.KandangRepository
|
KandangRepo kandangRepository.KandangRepository
|
||||||
|
NonstockRepo nonstockRepository.NonstockRepository
|
||||||
WarehouseRepo warehouseRepository.WarehouseRepository
|
WarehouseRepo warehouseRepository.WarehouseRepository
|
||||||
ProductWarehouseRepo productWarehouseRepository.ProductWarehouseRepository
|
ProductWarehouseRepo productWarehouseRepository.ProductWarehouseRepository
|
||||||
|
ProjectBudgetRepo projectBudgetRepository.ProjectBudgetRepository
|
||||||
PivotRepo repository.ProjectFlockKandangRepository
|
PivotRepo repository.ProjectFlockKandangRepository
|
||||||
ApprovalSvc commonSvc.ApprovalService
|
ApprovalSvc commonSvc.ApprovalService
|
||||||
approvalWorkflow approvalutils.ApprovalWorkflowKey
|
approvalWorkflow approvalutils.ApprovalWorkflowKey
|
||||||
@@ -67,8 +72,11 @@ func NewProjectflockService(
|
|||||||
pivotRepo repository.ProjectFlockKandangRepository,
|
pivotRepo repository.ProjectFlockKandangRepository,
|
||||||
warehouseRepo warehouseRepository.WarehouseRepository,
|
warehouseRepo warehouseRepository.WarehouseRepository,
|
||||||
productWarehouseRepo productWarehouseRepository.ProductWarehouseRepository,
|
productWarehouseRepo productWarehouseRepository.ProductWarehouseRepository,
|
||||||
|
projectBudgetRepo projectBudgetRepository.ProjectBudgetRepository,
|
||||||
|
nonstockRepo nonstockRepository.NonstockRepository,
|
||||||
approvalSvc commonSvc.ApprovalService,
|
approvalSvc commonSvc.ApprovalService,
|
||||||
validate *validator.Validate,
|
validate *validator.Validate,
|
||||||
|
|
||||||
) ProjectflockService {
|
) ProjectflockService {
|
||||||
return &projectflockService{
|
return &projectflockService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
@@ -76,6 +84,7 @@ func NewProjectflockService(
|
|||||||
Repository: repo,
|
Repository: repo,
|
||||||
FlockRepo: flockRepo,
|
FlockRepo: flockRepo,
|
||||||
KandangRepo: kandangRepo,
|
KandangRepo: kandangRepo,
|
||||||
|
NonstockRepo: nonstockRepo,
|
||||||
WarehouseRepo: warehouseRepo,
|
WarehouseRepo: warehouseRepo,
|
||||||
ProductWarehouseRepo: productWarehouseRepo,
|
ProductWarehouseRepo: productWarehouseRepo,
|
||||||
PivotRepo: pivotRepo,
|
PivotRepo: pivotRepo,
|
||||||
@@ -289,7 +298,6 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
|
|||||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||||
projectRepo := repository.NewProjectflockRepository(dbTransaction)
|
projectRepo := repository.NewProjectflockRepository(dbTransaction)
|
||||||
|
|
||||||
// Generate unique flock name (sequential per base name, starting from 1)
|
|
||||||
generatedName, _, err := s.generateSequentialFlockName(c.Context(), projectRepo, canonicalBase, 1, nil)
|
generatedName, _, err := s.generateSequentialFlockName(c.Context(), projectRepo, canonicalBase, 1, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -300,7 +308,6 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute period per kandang so every kandang maintains its own cycle history.
|
|
||||||
periods, err := projectRepo.GetNextPeriodsForKandangs(c.Context(), kandangIDs)
|
periods, err := projectRepo.GetNextPeriodsForKandangs(c.Context(), kandangIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -309,6 +316,10 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := s.UpsertProjectBudget(c.Context(), dbTransaction, createBody.Id, req.ProjectBudgets); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
action := entity.ApprovalActionCreated
|
action := entity.ApprovalActionCreated
|
||||||
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||||
_, err = approvalSvcTx.CreateApproval(
|
_, err = approvalSvcTx.CreateApproval(
|
||||||
@@ -1044,3 +1055,139 @@ func (s projectflockService) kandangRepoWithTx(tx *gorm.DB) kandangRepository.Ka
|
|||||||
}
|
}
|
||||||
return kandangRepository.NewKandangRepository(s.Repository.DB())
|
return kandangRepository.NewKandangRepository(s.Repository.DB())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s projectflockService) Resubmit(c *fiber.Ctx, req *validation.Resubmit, id uint) (*entity.ProjectFlock, error) {
|
||||||
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
actorID, err := m.ActorIDFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
existing, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock tidak ditemukan")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock")
|
||||||
|
}
|
||||||
|
|
||||||
|
kandangIDs := uniqueUintSlice(req.KandangIds)
|
||||||
|
kandangs, err := s.KandangRepo.GetByIDs(c.Context(), kandangIDs, nil)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Beberapa kandang tidak ditemukan")
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data kandang")
|
||||||
|
}
|
||||||
|
if len(kandangs) != len(kandangIDs) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Beberapa kandang tidak ditemukan")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pb := range req.ProjectBudgets {
|
||||||
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
|
commonSvc.RelationCheck{Name: "Nonstock", ID: &pb.NonstockId, Exists: s.NonstockRepo.IdExists},
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||||
|
|
||||||
|
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||||
|
|
||||||
|
var period int = 1
|
||||||
|
if len(existing.KandangHistory) > 0 {
|
||||||
|
period = existing.KandangHistory[0].Period
|
||||||
|
}
|
||||||
|
|
||||||
|
periods := make(map[uint]int, len(kandangIDs))
|
||||||
|
for _, kandangID := range kandangIDs {
|
||||||
|
periods[kandangID] = period
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.attachKandangs(c.Context(), dbTransaction, existing.Id, kandangIDs, periods); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.UpsertProjectBudget(c.Context(), dbTransaction, existing.Id, req.ProjectBudgets); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
action := entity.ApprovalActionUpdated
|
||||||
|
_, err = approvalSvc.CreateApproval(
|
||||||
|
c.Context(),
|
||||||
|
utils.ApprovalWorkflowProjectFlock,
|
||||||
|
existing.Id,
|
||||||
|
utils.ProjectFlockStepPengajuan,
|
||||||
|
&action,
|
||||||
|
actorID,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if fiberErr, ok := err.(*fiber.Error); ok {
|
||||||
|
return nil, fiberErr
|
||||||
|
}
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock tidak ditemukan")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengajukan ulang project flock")
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.getOneEntityOnly(c, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s projectflockService) UpsertProjectBudget(ctx context.Context, dbTransaction *gorm.DB, projectFlockID uint, budgets []validation.ProjectBudget) error {
|
||||||
|
|
||||||
|
if len(budgets) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
budgetRepo := projectBudgetRepository.NewProjectBudgetRepository(dbTransaction)
|
||||||
|
|
||||||
|
nonstockMap := make(map[uint]bool)
|
||||||
|
relationChecks := make([]commonSvc.RelationCheck, 0, len(budgets))
|
||||||
|
for _, b := range budgets {
|
||||||
|
if nonstockMap[b.NonstockId] {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate nonstock_id: %d", b.NonstockId))
|
||||||
|
}
|
||||||
|
nonstockMap[b.NonstockId] = true
|
||||||
|
nonstockID := b.NonstockId
|
||||||
|
relationChecks = append(relationChecks, commonSvc.RelationCheck{
|
||||||
|
Name: "Nonstock",
|
||||||
|
ID: &nonstockID,
|
||||||
|
Exists: s.NonstockRepo.IdExists,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := commonSvc.EnsureRelations(ctx, relationChecks...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := budgetRepo.DeleteMany(ctx, func(q *gorm.DB) *gorm.DB {
|
||||||
|
return q.Where("project_flock_id = ?", projectFlockID)
|
||||||
|
}); err != nil && err != gorm.ErrRecordNotFound {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
records := make([]*entity.ProjectBudget, 0, len(budgets))
|
||||||
|
for _, b := range budgets {
|
||||||
|
records = append(records, &entity.ProjectBudget{
|
||||||
|
ProjectFlockId: projectFlockID,
|
||||||
|
NonstockId: b.NonstockId,
|
||||||
|
Price: b.Price,
|
||||||
|
Qty: b.Qty,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := budgetRepo.CreateMany(ctx, records, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to save project budgets")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
package validation
|
package validation
|
||||||
|
|
||||||
type Create struct {
|
type Create struct {
|
||||||
FlockName string `json:"flock_name" validate:"required_strict"`
|
FlockName string `json:"flock_name" validate:"required_strict"`
|
||||||
AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"`
|
AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"`
|
||||||
Category string `json:"category" validate:"required_strict"`
|
Category string `json:"category" validate:"required_strict"`
|
||||||
FcrId uint `json:"fcr_id" validate:"required_strict,number,gt=0"`
|
FcrId uint `json:"fcr_id" validate:"required_strict,number,gt=0"`
|
||||||
LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"`
|
LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"`
|
||||||
KandangIds []uint `json:"kandang_ids" validate:"required,min=1,dive,gt=0"`
|
KandangIds []uint `json:"kandang_ids" validate:"required,min=1,dive,gt=0"`
|
||||||
|
ProjectBudgets []ProjectBudget `json:"project_budgets" validate:"required,min=1,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Update struct {
|
type Update struct {
|
||||||
@@ -36,3 +37,14 @@ type Approve struct {
|
|||||||
ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"`
|
ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"`
|
||||||
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProjectBudget struct {
|
||||||
|
NonstockId uint `json:"nonstock_id" validate:"required_strict,number,gt=0"`
|
||||||
|
Price float64 `json:"price" validate:"required_strict,number,gt=0"`
|
||||||
|
Qty float64 `json:"qty" validate:"required_strict,number,gt=0"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Resubmit struct {
|
||||||
|
KandangIds []uint `json:"kandang_ids" validate:"required_strict,min=1,dive,gt=0"`
|
||||||
|
ProjectBudgets []ProjectBudget `json:"project_budgets" validate:"required_strict,min=1,dive"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -778,7 +778,7 @@ func (s *transferLayingService) getOrCreateProductWarehouse(ctx context.Context,
|
|||||||
ProductId: productID,
|
ProductId: productID,
|
||||||
WarehouseId: warehouseID,
|
WarehouseId: warehouseID,
|
||||||
Quantity: quantity,
|
Quantity: quantity,
|
||||||
CreatedBy: actorID,
|
// CreatedBy: actorID,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := productWarehouseRepoTx.CreateOne(ctx, newWarehouse, nil); err != nil {
|
if err := productWarehouseRepoTx.CreateOne(ctx, newWarehouse, nil); err != nil {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ func (r *StockLogRepositoryImpl) GetByFlaggable(ctx context.Context, logType str
|
|||||||
var stockLogs []*entity.StockLog
|
var stockLogs []*entity.StockLog
|
||||||
|
|
||||||
err := r.DB().WithContext(ctx).
|
err := r.DB().WithContext(ctx).
|
||||||
Where("log_type = ? AND log_id = ?", logType, logId).
|
Where("loggable_type = ? AND log_id = ?", logType, logId).
|
||||||
Preload("ProductWarehouse").
|
Preload("ProductWarehouse").
|
||||||
Preload("ProductWarehouse.Product").
|
Preload("ProductWarehouse.Product").
|
||||||
Preload("ProductWarehouse.Warehouse").
|
Preload("ProductWarehouse.Warehouse").
|
||||||
|
|||||||
@@ -154,12 +154,14 @@ const (
|
|||||||
ApprovalWorkflowProjectFlock approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("PROJECT_FLOCKS")
|
ApprovalWorkflowProjectFlock approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("PROJECT_FLOCKS")
|
||||||
ProjectFlockStepPengajuan approvalutils.ApprovalStep = 1
|
ProjectFlockStepPengajuan approvalutils.ApprovalStep = 1
|
||||||
ProjectFlockStepAktif approvalutils.ApprovalStep = 2
|
ProjectFlockStepAktif approvalutils.ApprovalStep = 2
|
||||||
|
ProjectFlockStepSelesai approvalutils.ApprovalStep = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
// projectFlockApprovalSteps keeps the workflow step definitions for project flock approvals.
|
// projectFlockApprovalSteps keeps the workflow step definitions for project flock approvals.
|
||||||
var ProjectFlockApprovalSteps = map[approvalutils.ApprovalStep]string{
|
var ProjectFlockApprovalSteps = map[approvalutils.ApprovalStep]string{
|
||||||
ProjectFlockStepPengajuan: "Pengajuan",
|
ProjectFlockStepPengajuan: "Pengajuan",
|
||||||
ProjectFlockStepAktif: "Aktif",
|
ProjectFlockStepAktif: "Aktif",
|
||||||
|
ProjectFlockStepSelesai: "Selesai",
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
|
|||||||
@@ -263,7 +263,6 @@ func createProductWarehouseRow(t *testing.T, db *gorm.DB, qty float64) entity.Pr
|
|||||||
ProductId: 1,
|
ProductId: 1,
|
||||||
WarehouseId: 1,
|
WarehouseId: 1,
|
||||||
Quantity: qty,
|
Quantity: qty,
|
||||||
CreatedBy: 1,
|
|
||||||
}
|
}
|
||||||
if err := db.Create(&pw).Error; err != nil {
|
if err := db.Create(&pw).Error; err != nil {
|
||||||
t.Fatalf("create product warehouse: %v", err)
|
t.Fatalf("create product warehouse: %v", err)
|
||||||
|
|||||||
Reference in New Issue
Block a user