Compare commits

...

116 Commits

Author SHA1 Message Date
kris 7a6b3121f6 Update .gitlab-ci.yml file 2025-12-01 04:37:25 +00:00
kris 92901bc60c Update .gitlab-ci.yml file 2025-12-01 04:35:16 +00:00
GitLab Deploy Bot 2ea2e1ddf3 Merge remote-tracking branch 'origin/development' into devops-ec2 2025-12-01 11:26:27 +07:00
Adnan Zahir 2e2aed67b8 Merge branch 'feat/BE/Sprint-5' into 'development'
[FEAT/BE][US#159,160,161,162,163,164,255,256] : Purchase Request, Purchase Order, Sales Order, Delivery Order, Expense Submission, Expense Realization

See merge request mbugroup/lti-api!71
2025-11-24 09:47:23 +07:00
Hafizh A. Y. a801081a99 Merge branch 'dev/teguh' into 'feat/BE/Sprint-5'
[FIX/BE][US#116,164/TASK#260,261,262,263,264,265,266,267,268] : Creating BOP Migration And All API Needed on bop an bop realization module

See merge request mbugroup/lti-api!70
2025-11-21 04:30:51 +00:00
aguhh18 b0dfa717d5 FIX[BE-261]: expense list dto ganti dari hardcoded ke ambil dari expensebasedto 2025-11-21 11:16:34 +07:00
aguhh18 16d562e024 Merge branch 'feat/BE/Sprint-5' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2025-11-21 11:05:33 +07:00
Hafizh A. Y. 8881be2a22 Merge branch 'fix/merge-request-project-flock' into 'feat/BE/Sprint-5'
fix merging

See merge request mbugroup/lti-api!69
2025-11-21 03:55:34 +00:00
ragilap 3fc330d8f7 fix merging 2025-11-21 10:50:30 +07:00
aguhh18 af147f4f2b Merge branch 'feat/BE/Sprint-5' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2025-11-21 10:46:50 +07:00
aguhh18 6768092e3b Fix[BE-261]: fixing location not preloaded on get one non-bop API 2025-11-21 10:20:07 +07:00
Hafizh A. Y 53b226f243 fix(BE): adjust dto and project flock, master data, and marketing 2025-11-21 09:53:33 +07:00
Hafizh A. Y. cd752f19f4 Merge branch 'fix/merge-request-project-flock' into 'feat/BE/Sprint-5'
fix merging

See merge request mbugroup/lti-api!68
2025-11-21 02:04:30 +00:00
aguhh18 5a73ad0164 Fix[BE-261]: delete timestampz on expense nonstock anda expense reaalizations table 2025-11-21 01:06:48 +07:00
aguhh18 b8d1268dfa Feat[BE-261]: creating multiple Approval API, Update API, Delete API and some logic Adjustment 2025-11-21 00:55:02 +07:00
ragilap da10861fd2 fix merging 2025-11-20 21:17:04 +07:00
Hafizh A. Y 228aedc215 fix(BE-273): add object nonstock and supplier in response get one and fix name base to relation in dto 2025-11-20 14:59:50 +07:00
Hafizh A. Y. b4b860b9d4 Merge branch 'feat/BE/US-159,160/marketing' into 'feat/BE/Sprint-5'
Feat/be/us 159,160/marketing

See merge request mbugroup/lti-api!66
2025-11-20 02:16:25 +00:00
Hafizh A. Y. 3080a6f8ef Merge branch 'feat/BE/Sprint-5' into 'feat/BE/US-159,160/marketing'
# Conflicts:
#   internal/modules/production/project_flocks/controllers/projectflock.controller.go
#   internal/modules/production/project_flocks/dto/projectflock.dto.go
#   internal/modules/production/project_flocks/route.go
#   internal/modules/production/transfer_layings/dto/transfer_laying.dto.go
2025-11-20 02:16:12 +00:00
aguhh18 b502751b4e Fix[BE-261]: change realization json to not using prefix Realization 2025-11-20 08:50:47 +07:00
aguhh18 4c7e5b0731 Feat[BE-261,265]: add category request body on create on 2025-11-20 08:27:02 +07:00
aguhh18 105b20c333 Feat[BE-261,265]: createing BOP and BOP realization(Unfinished) 2025-11-19 16:05:11 +07:00
Hafizh A. Y. f5b7fd60ad Merge branch 'feat/BE/US-161,162/purchase' into 'feat/BE/Sprint-5'
Merge branch 'dev/ragil-before-sso' into 'feat/BE/US-161,162/purchase'

See merge request mbugroup/lti-api!65
2025-11-19 04:28:33 +00:00
Hafizh A. Y. ced27e23a0 Merge branch 'fix/BE/ISSUE-270/project-flock-period' into 'feat/BE/Sprint-5'
feat[BE]: Refactor Chickin create and approvals support chickin growing and...

See merge request mbugroup/lti-api!64
2025-11-19 04:27:51 +00:00
Hafizh A. Y. 242ccc9230 Merge branch 'dev/ragil-before-sso' into 'feat/BE/US-161,162/purchase'
[FEAT/BE][US#161,162/TASK#229,234,235,230,231,232,233] : purchase request and purchase order and fix master data dto

See merge request mbugroup/lti-api!60
2025-11-19 04:26:51 +00:00
Hafizh A. Y. 1e52c51987 Merge branch 'dev/ragil-before-sso' into 'fix/BE/ISSUE-270/project-flock-period'
[Fix/BE][US#74,Task#270] : Fix period project flock

See merge request mbugroup/lti-api!63
2025-11-19 04:26:36 +00:00
Hafizh A. Y. bf8519df3f Merge branch 'dev/teguh' into 'feat/BE/US-159,160/marketing'
[FIX/BE][US#159/TASK#222] :  fixing approval status when updated and delete timestamz on children of marketing table

See merge request mbugroup/lti-api!62
2025-11-18 06:54:40 +00:00
aguhh18 a57ef82ebb Fix{BE-222] fixing approval status when updated and delete timestamz on children of marketing table 2025-11-18 12:44:19 +07:00
ragilap c2b60c1aff feat(BE-#270): Project flock period change to project_flock_kandangs 2025-11-18 12:26:54 +07:00
aguhh18 320f5e65c6 Fix[BE]: fix wrong approval step when SO uppdated 2025-11-18 11:32:41 +07:00
aguhh18 28c81aac25 Merge branch 'dev/ragil-before-sso' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2025-11-18 08:23:13 +07:00
aguhh18 1dac74e25b Feat[BE-261]: creating Entity and repository for each table expenses 2025-11-18 08:18:37 +07:00
Hafizh A. Y. 9ca9dfc2be Merge branch 'dev/teguh' into 'feat/BE/US-159,160/marketing'
[FIX/BE] : delete unused delivery order repository

See merge request mbugroup/lti-api!61
2025-11-17 09:08:49 +00:00
ragilap 02cc082d67 feat(BE-229,234,235,230,231,232,233): purchase request and purchase order and fix master data dto 2025-11-17 15:17:25 +07:00
aguhh18 5c25c84f7f Fix[BE]: fixing delivery order delet unused repository 2025-11-17 15:15:52 +07:00
Hafizh A. Y. aaf129622b Merge branch 'dev/teguh' into 'feat/BE/US-159,160/marketing'
[FEAT/BE][US#159/TASK#221,222] create migration and API SO DO

See merge request mbugroup/lti-api!58
2025-11-17 08:04:23 +00:00
ragilap 69469edb62 PR 2025-11-17 14:48:39 +07:00
aguhh18 09d503f5be Feat[BE-261] : inisiate expense module 2025-11-17 14:46:21 +07:00
aguhh18 d528096d56 Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2025-11-17 13:52:13 +07:00
ragilap 0708628b78 feat(BE-229,234,235,230,231,232,233): purchase request and purchase order and fix master data dto 2025-11-17 11:53:27 +07:00
aguhh18 cb1df12b7e Feat[BE-260]: create BOP migration 2025-11-17 11:03:15 +07:00
ragilap 11f2389ec5 feat(BE-229,234,235,230,231,232,233): purchase request and purchase order and fix master data dto 2025-11-17 09:39:30 +07:00
aguhh18 60757237c0 Feat[BE-222]: add marketing product to get all marketing for Frontend needs 2025-11-17 09:28:30 +07:00
aguhh18 7905bdb0d7 Feat[BE-222]: Completed SO and DO API 2025-11-17 07:16:07 +07:00
Adnan Zahir 26f9196876 Merge branch 'fix/master-kandang-capacity' into 'development'
[FIX/BE][ISSUE#259] kandang capacity and fix err response

See merge request mbugroup/lti-api!59
2025-11-14 16:56:27 +07:00
Hafizh A. Y 17d3042586 fix(BE): kandang capacity and fix err response 2025-11-14 16:43:01 +07:00
aguhh18 903b114315 fix[BE]: fixing null project flock ikandang id on lookup 2025-11-13 20:27:49 +07:00
Asep Teguh Hidayat 2f5fab9f80 Feat[BE-222]: creating update DO(Unfinished) 2025-11-13 19:28:50 +07:00
aguhh18 74ec25db5b Feat[BE-222.223.224]: creating create one delivery order and getone delivery order[Unfinished] 2025-11-13 09:50:34 +07:00
aguhh18 0a0c3f869b Feat[BE-222,223,224]: creating So create delete patch update get getall approval API 2025-11-12 11:28:18 +07:00
kris 8f4548971e Update .gitlab-ci.yml file 2025-11-11 09:23:32 +00:00
kris d940580152 Update .gitlab-ci.yml file 2025-11-11 09:18:02 +00:00
kris c8052f4cb5 Update .gitlab-ci.yml file 2025-11-11 09:09:50 +00:00
kris b8dca3c25e Update .gitlab-ci.yml file 2025-11-11 09:07:45 +00:00
kris c885fba4ef Update .gitlab-ci.yml file 2025-11-11 08:53:59 +00:00
kris 4837ed4255 Update .gitlab-ci.yml file 2025-11-11 08:45:25 +00:00
kris d740a3e26e Update .gitlab-ci.yml file 2025-11-11 08:36:36 +00:00
kris 0766cfeeb2 Update .gitlab-ci.yml file 2025-11-11 08:28:54 +00:00
kris 8e89f9fad0 Update .gitlab-ci.yml file 2025-11-11 08:26:37 +00:00
aguhh18 762dfa9fb9 feat[BE-127] add source and target project flock to transfer laying API 2025-11-11 14:32:55 +07:00
aguhh18 6b5d27ae8e feat[BE]: add flock response to project flock and projectflockkandang getone and getall API 2025-11-11 12:16:39 +07:00
aguhh18 fd0943dfaf feat[BE-222]: create migration create template for SO API and kandang id param on product warehouse 2025-11-10 14:49:46 +07:00
kris 80c84210b8 Delete .gitlab-ci.yml.old 2025-11-09 04:25:49 +00:00
kris 05ec64b456 Merge branch 'chroot/update-endpoint' into 'development'
update endpoint callback

See merge request mbugroup/lti-api!57
2025-11-08 18:13:13 +00:00
GitLab Deploy Bot 9e97b3951c update endpoint callback 2025-11-09 01:12:17 +07:00
aguhh18 b2ed58c734 Fix[BE]: availableqty not appeared 2025-11-07 16:41:52 +07:00
aguhh18 3785d52925 FIX[BE]: fix get projectflock kandang periods not found 2025-11-07 15:26:32 +07:00
aguhh18 4c279baad7 FIX[BE} change json response on avaibility transfer laying 2025-11-07 13:32:03 +07:00
aguhh18 6e69e97d26 feat[BE-127]: create available qty API and inisiate sales order and delivery order 2025-11-07 13:24:48 +07:00
aguhh18 ba12320d12 Feat[BE-221]: create So DO migration 2025-11-07 09:01:37 +07:00
aguhh18 d21aaead7b Fix[BE]: use new request body for frontend requrement on chickin create API 2025-11-07 07:58:00 +07:00
aguhh18 954cccd564 Fix[BE]: make projectflock kandang API and dto clean 2025-11-06 21:25:15 +07:00
aguhh18 663d5129bb Feat[BE-127] Creating project flock kandang get all with soma query param 2025-11-06 17:57:10 +07:00
Adnan Zahir e54b2157c7 Merge branch 'chore/replace-configuration' into 'development'
replace-configuration

See merge request mbugroup/lti-api!56
2025-11-06 14:49:15 +07:00
GitLab Deploy Bot 95dad52cea eplace-configuration 2025-11-06 14:47:58 +07:00
Adnan Zahir 28dcae5865 Merge branch 'chore/ignore-and-delete-configurations' into 'development'
chore(CI): ignore and delete configurations

See merge request mbugroup/lti-api!55
2025-11-06 14:36:55 +07:00
GitLab Deploy Bot 4129c36f9e chore(CI): ignore and delete configurations 2025-11-06 14:33:42 +07:00
aguhh18 d587a793fe Merge branch 'dev/ragil-before-sso' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2025-11-06 14:05:58 +07:00
aguhh18 a587584156 Feat[BE]: change response flock name on project flock kandang 2025-11-06 14:04:20 +07:00
aguhh18 4b69afe4fa Feat[BE-127] create get all with param on project flock kandang 2025-11-06 14:03:47 +07:00
aguhh18 5cfa97dd03 Feat[BE]: adding project flock kandang id into project flock get one projecct flock api 2025-11-06 13:41:16 +07:00
aguhh18 028d5f6f91 FEAT[BE-127]: add flockName to getone projectflockkandang API 2025-11-06 11:13:56 +07:00
aguhh18 60fe553f63 FIX[BE]: getting available qty from Flags instead of Product.Category 2025-11-06 10:51:32 +07:00
aguhh18 1c99093ff8 feat[BE-127]; creating correct logic update and delete transfer laying 2025-11-06 10:31:35 +07:00
Adnan Zahir 54cb1cf3da Merge branch 'development-after-sso' into 'development'
[FEAT/BE] merge feat recording, refactor chickin and implement auth middleware

See merge request mbugroup/lti-api!54
2025-11-05 21:51:05 +07:00
Hafizh A. Y a0569302c8 fix(BE): project_flock route 2025-11-05 19:49:48 +07:00
ragilap 8f74391f1e unfinished purchase 2025-11-05 18:58:06 +07:00
Hafizh A. Y 5a2f99196f chore(BE): makefile local and dev 2025-11-05 18:08:05 +07:00
Hafizh A. Y 91fbbf5dd9 chore(BE): gitignore 2025-11-05 17:15:03 +07:00
kris ca168928c7 Update .gitlab-ci.yml file 2025-11-05 16:51:01 +07:00
GitLab Deploy Bot 4d2a9bd7b4 update secure DB setup and env isolation for LTI API 2025-11-05 16:51:01 +07:00
GitLab Deploy Bot 4c4be2ef41 Actived .gitlab-ci.yml 2025-11-05 16:51:01 +07:00
GitLab Deploy Bot a22c615ac1 Actived .gitlab-ci.yml 2025-11-05 16:51:01 +07:00
Hafizh A. Y. 4aed480662 Merge branch 'dev/ragil-before-sso' into 'development-before-sso'
fix(BE-78)change typedata in recording dto and validation for create and update

See merge request mbugroup/lti-api!51
2025-11-05 08:58:45 +00:00
Hafizh A. Y. e5b91161a9 Merge branch 'feat/BE/US-75/chick-in-doc' into 'development-before-sso'
feat[BE]: Refactor Chickin create and approvals support chickin growing and...

See merge request mbugroup/lti-api!52
2025-11-05 08:58:07 +00:00
Hafizh A. Y. a38491fef1 Merge branch 'dev/teguh' into 'feat/BE/US-75/chick-in-doc'
[CHORE/BE] resolve conflicts development-before-sso ito chickin

See merge request mbugroup/lti-api!53
2025-11-05 08:57:49 +00:00
aguhh18 b234778634 Merge branch 'development-before-sso' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2025-11-05 14:03:45 +07:00
Hafizh A. Y. 59e71856ac Merge branch 'dev/teguh' into 'feat/BE/US-75/chick-in-doc'
[FIX/BE][US#75/TASK#119] : Refactor chickin support chickin Growing and LAYING also have approval flow

See merge request mbugroup/lti-api!50
2025-11-05 05:26:42 +00:00
aguhh18 1ee97b91a5 feat[BE-127]: Createing transfer laying create one, approvals, get one, get all, update, delete, but Still unfinished 2025-11-05 08:56:18 +07:00
aguhh18 3a5c49c511 fix[BE]: fix naming on project_flock_kandang dto to standarized project 2025-11-05 08:40:27 +07:00
aguhh18 48730e1b74 FIX[BE]: fix error handling on chickin service to better handler 2025-11-04 16:34:36 +07:00
ragilap f97d404121 fix(BE-78)change typedata in recording dto and validation for create and update 2025-11-04 12:05:04 +07:00
kris 3ecf39814e Update .gitlab-ci.yml file 2025-11-04 03:32:25 +00:00
aguhh18 8220e34302 FIX[BE]: fix logic on Chickin Laying not convert to layer but still Pullet, and inisiate laying transfer migration and base basic API 2025-11-04 08:24:38 +07:00
aguhh18 c72db5bd18 FIX[BE]: delete redudant kandang response on projectflockkandang getone API 2025-11-03 09:29:00 +07:00
aguhh18 86f37a89c1 Feat[BE]: add multilpple type of chickin growing and laying, make convertion product when chickin approved, add projectflockkandangid on projectflock api 2025-11-03 09:16:29 +07:00
aguhh18 20f1be2ef8 feat[BE]: Refactor Chickin create and approvals support chickin growing and chickin laying, and create get one project flock kandang API 2025-11-02 21:06:03 +07:00
Hafizh A. Y. 672c76d26d Merge branch 'dev/teguh' into 'feat/BE/US-75/chick-in-doc'
[FIX/BE][US#75] Adjust "chickin delete one" code to match backend standard

See merge request mbugroup/lti-api!49
2025-10-31 09:48:27 +00:00
aguhh18 219a6a39ed Feat[BE]: refactored Chickin createone and implement approvals and add more needed constant 2025-10-31 15:33:31 +07:00
aguhh18 c91d84b652 feat[BE-127]: inisiate transfer laying for base template API 2025-10-31 14:30:45 +07:00
aguhh18 bf14ab7865 fix(BE): Change migration chickin and project flock population to refactored one 2025-10-31 14:27:08 +07:00
GitLab Deploy Bot b459245c5c update secure DB setup and env isolation for LTI API 2025-10-31 10:32:05 +07:00
aguhh18 31bb28f7da Feat(BE-127): create migration for transfer to laying and inisiate module 2025-10-30 09:06:21 +07:00
aguhh18 a390d1d23a FIX[BE]: Fix Delete one on chickin match with BE standard 2025-10-29 14:19:08 +07:00
GitLab Deploy Bot c4448594e2 Actived .gitlab-ci.yml 2025-10-27 16:25:55 +07:00
GitLab Deploy Bot fb831208f4 Actived .gitlab-ci.yml 2025-10-27 16:22:20 +07:00
208 changed files with 13029 additions and 4529 deletions
+3 -7
View File
@@ -3,13 +3,9 @@ root = "."
tmp_dir = "tmp"
[build]
# Build binary utama
cmd = "go build -o /lti-api/tmp/main ./cmd/api"
# Lokasi binary hasil build
bin = "/lti-api/tmp/main"
# Jalankan binary langsung dengan environment dev
full_bin = "APP_ENV=dev /lti-api/tmp/main"
# File yang dipantau oleh Air
cmd = "go build -o ./tmp/main ./cmd/api"
bin = "tmp/main"
full_bin = "APP_ENV=dev ./tmp/main"
include_ext = ["go", "tpl", "tmpl", "html"]
exclude_dir = ["vendor", "tmp"]
-56
View File
@@ -1,56 +0,0 @@
# server configuration
# Env value : prod || dev
VERSION=0.0.1
APP_ENV=dev
APP_HOST=0.0.0.0
APP_PORT=8081
APP_URL=http://localhost:8081
# database configuration
DB_HOST=postgresdb
DB_USER=postgres
DB_PASSWORD=changeme
DB_NAME=db_lti_erp
DB_PORT=5432
DB_PORT_HOST=5542
# JWT
JWT_SECRET=changeme
JWT_ACCESS_EXP_MINUTES=30
JWT_REFRESH_EXP_DAYS=30
JWT_RESET_PASSWORD_EXP_MINUTES=10
JWT_VERIFY_EMAIL_EXP_MINUTES=10
# CORS
CORS_ALLOW_ORIGINS=changeme
CORS_ALLOW_METHODS=GET,POST,PUT,PATCH,DELETE,OPTIONS
CORS_ALLOW_HEADERS=Authorization,Content-Type,X-Requested-With
CORS_EXPOSE_HEADERS=Link,Location
CORS_ALLOW_CREDENTIALS=true
CORS_MAX_AGE=600
# Redis
REDIS_URL=redis://redis:6379/0
REDIS_PORT_HOST=6381
# SSO Integration
SSO_ISSUER=http://localhost:8080/api
# SSO_JWKS_URL=http://localhost:8080/api/.well-known/jwks.json
SSO_JWKS_URL=http://host.docker.internal:8080/api/.well-known/jwks.json
SSO_ALLOWED_AUDIENCES=client:lti-api
SSO_AUTHORIZE_URL=http://localhost:8080/sso/authorize
SSO_TOKEN_URL=http://localhost:8080/sso/token
SSO_GETME_URL=http://localhost:8080/api/auth/get-me
SSO_ACCESS_COOKIE_NAME=sso_access
SSO_REFRESH_COOKIE_NAME=sso_refresh
SSO_COOKIE_DOMAIN=
SSO_COOKIE_SECURE=false
SSO_COOKIE_SAMESITE=Lax
SSO_TOKEN_BLACKLIST_PREFIX=sso:blacklist
SSO_PKCE_TTL_SECONDS=300
# Security window and payload limits for SSO user sync webhook
SSO_USER_SYNC_SIGNATURE_DRIFT_SECONDS=120
SSO_USER_SYNC_NONCE_TTL_SECONDS=600
SSO_USER_SYNC_MAX_BODY_BYTES=32768
# Example JSON (single-line) of client configs (each client requires a unique sync_secret)
SSO_CLIENTS={"LTI":{"public_id":"Lumbung-Telur-Indonesia","redirect_uri":"http://localhost:8081/api/sso/callback","scope":"openid profile","default_return_uri":"http://localhost:3000","allowed_return_origins":["http://localhost:3000"],"sync_secret":"onUyfODIMHOh4TgGLgyWLmsNeVNxFRHqoLJFLPjr"}}
-58
View File
@@ -1,58 +0,0 @@
# .env.lti-api (Development Server with Domain)
# =============================================
# Server configuration
VERSION=0.0.1
APP_ENV=dev
APP_HOST=0.0.0.0
APP_PORT=8081
APP_URL=https://dev-api-lti.mbugroup.id
# Database configuration (pakai PostgreSQL milik SSO)
DB_HOST=sso-postgres
DB_USER=postgres
DB_PASSWORD=postgres
DB_NAME=db_lti_erp
DB_PORT=5432
# JWT configuration
JWT_SECRET=changeme
JWT_ACCESS_EXP_MINUTES=30
JWT_REFRESH_EXP_DAYS=30
JWT_RESET_PASSWORD_EXP_MINUTES=10
JWT_VERIFY_EMAIL_EXP_MINUTES=10
# Redis (pakai Redis milik SSO)
REDIS_URL=redis://sso-redis:6379/0
# CORS configuration
CORS_ALLOW_ORIGINS=https://dev-api-sso.mbugroup.id,https://dev-lti.mbugroup.id,https://dev-api-lti.mbugroup.id,http://localhost:3000
CORS_ALLOW_METHODS=GET,POST,PUT,PATCH,DELETE,OPTIONS
CORS_ALLOW_HEADERS=Authorization,Content-Type,X-Requested-With
CORS_EXPOSE_HEADERS=Link,Location
CORS_ALLOW_CREDENTIALS=true
CORS_MAX_AGE=600
# SSO Integration (Gunakan domain backend SSO)
SSO_ISSUER=https://dev-api-sso.mbugroup.id
SSO_JWKS_URL=https://dev-api-sso.mbugroup.id/api/.well-known/jwks.json
SSO_ALLOWED_AUDIENCES=
SSO_AUTHORIZE_URL=https://dev-api-sso.mbugroup.id/api/sso/authorize
SSO_TOKEN_URL=https://dev-api-sso.mbugroup.id/api/sso/token
SSO_GETME_URL=https://dev-api-sso.mbugroup.id/api/auth/get-me
# Cookie & session configuration
SSO_ACCESS_COOKIE_NAME=sso_access
SSO_REFRESH_COOKIE_NAME=sso_refresh
SSO_COOKIE_DOMAIN=.mbugroup.id
SSO_COOKIE_SECURE=true
SSO_COOKIE_SAMESITE=Lax
SSO_PKCE_TTL_SECONDS=300
# SSO webhook / user sync settings
SSO_USER_SYNC_SIGNATURE_DRIFT_SECONDS=120
SSO_USER_SYNC_NONCE_TTL_SECONDS=600
SSO_USER_SYNC_MAX_BODY_BYTES=32768
# Client registration for SSO
SSO_CLIENTS={"Lumbung-Telur-Indonesia":{"public_id":"Lumbung-Telur-Indonesia","redirect_uri":"https://dev-api-lti.mbugroup.id/api/sso/callback","scope":"openid profile","default_return_uri":"https://dev-lti.mbugroup.id","allowed_return_origins":["https://dev-lti.mbugroup.id","http://localhost:3000"],"sync_secret":"onUyfODIMHOh4TgGLgyWLmsNeVNxFRHqoLJFLPjr"}}
+1 -1
View File
@@ -16,7 +16,7 @@ docker-compose.yaml
Dockerfile.local
# Go build cache
.gocache/
vendor/
vendor
# Logs & reports
*.log
+91
View File
@@ -0,0 +1,91 @@
stages:
- deploy
deploy-dev:
stage: deploy
image: alpine:3.20
variables:
DEPLOY_APP: "LTI-MBUGROUP"
# Opsional: kalau pakai submodule, ini bikin clone submodule pakai SSH juga
GIT_SUBMODULE_STRATEGY: recursive
GIT_DEPTH: "1"
before_script:
- echo "🧰 Installing dependencies..."
- apk update && apk add --no-cache openssh git curl bash
# Setup SSH di runner
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- eval "$(ssh-agent -s)"
- ssh-add ~/.ssh/id_rsa
# Trust host keys (server + gitlab) biar SSH gak nanya interaktif
- ssh-keyscan -H "$SERVER_IP" >> ~/.ssh/known_hosts
- ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
script:
- echo "🚀 Deploying latest code to $SERVER_USER@$SERVER_IP"
- >
if ssh -o StrictHostKeyChecking=no "$SERVER_USER@$SERVER_IP" "
set -e
cd /home/devops/docker/deployment/development/lti-api
# Pastikan remote origin SSH (antisipasi kalau pernah ke-set HTTPS)
git remote set-url origin git@gitlab.com:mbugroup/lti-api.git
# Pastikan server percaya gitlab.com juga (untuk git fetch via SSH)
mkdir -p ~/.ssh
ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
# Fetch/reset pakai SSH
GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no' git fetch origin development
git reset --hard origin/development
docker compose restart dev-api-lti || docker compose up -d dev-api-lti
"; then
STATUS='success';
else
STATUS='failed';
fi;
RUN_URL="${CI_PROJECT_URL}/-/pipelines/${CI_PIPELINE_ID}";
if [ "$STATUS" = "success" ]; then
COLOR=3066993;
TITLE="✅ Deployment API Succeeded";
DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` completed successfully.";
else
COLOR=15158332;
TITLE="❌ Deployment API Failed Gaes";
DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` failed.";
fi;
echo "{
\"username\": \"CI Bot\",
\"embeds\": [{
\"title\": \"$TITLE\",
\"description\": \"$DESC\",
\"color\": $COLOR,
\"fields\": [
{\"name\": \"Repository\", \"value\": \"${CI_PROJECT_PATH}\", \"inline\": true},
{\"name\": \"Actor\", \"value\": \"${GITLAB_USER_LOGIN}\", \"inline\": true},
{\"name\": \"Commit\", \"value\": \"${CI_COMMIT_SHA}\", \"inline\": false},
{\"name\": \"Pipeline\", \"value\": \"[Open run](${RUN_URL})\", \"inline\": false}
]
}]
}" > payload.json;
echo "📡 Sending notification to Discord...";
curl -sS -H "Content-Type: application/json" \
-d @payload.json "$DISCORD_WEBHOOK_URL";
only:
- development
- devops-ec2
environment:
name: development
-78
View File
@@ -1,78 +0,0 @@
stages:
- build
- deploy
- cleanup
# ==============================
# 🏗️ BUILD IMAGE (Overwrite :dev)
# ==============================
build_image:
stage: build
image: docker:latest
services:
- docker:dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
script:
- echo "🔧 Building Docker image for :dev..."
- docker login -u gitlab-ci-token -p "$CI_JOB_TOKEN" "$CI_REGISTRY"
- docker build -f Dockerfile.local -t registry.gitlab.com/mbugroup/sso-mbugroup/lti-api:dev .
- docker push registry.gitlab.com/mbugroup/sso-mbugroup/lti-api:dev
only:
- development
# ==============================
# 🚀 DEPLOY TO DEV SERVER
# ==============================
deploy_lti:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache openssh-client bash curl
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- ssh-keyscan -H "$SERVER_IP" >> ~/.ssh/known_hosts
script:
- echo "🚀 Deploy ke ${SERVER_USER}@${SERVER_IP} menggunakan image :dev"
- |
ssh -o StrictHostKeyChecking=no ${SERVER_USER}@${SERVER_IP} bash -s <<REMOTE
set -e
APP_NAME="lti-api"
DOCKER_IMAGE="registry.gitlab.com/mbugroup/sso-mbugroup/lti-api:dev"
NETWORK_NAME="lti-network"
ENV_PATH="/home/devops/code/api/lti-api/.env.lti-api"
PORT=8081
echo "🔑 Login ke GitLab Registry..."
echo "${GITLAB_TOKEN}" | docker login -u "${GITLAB_USER}" --password-stdin registry.gitlab.com
echo "🛑 Stop & remove old container..."
docker stop "\${APP_NAME}" >/dev/null 2>&1 || true
docker rm -f "\${APP_NAME}" >/dev/null 2>&1 || true
echo "🧹 Membersihkan container zombie di port \${PORT}..."
OLD_ID=\$(docker ps -aq --filter "publish=\${PORT}")
if [ -n "\${OLD_ID}" ]; then
echo "⚠️ Container lain masih pakai port \${PORT}, hapus..."
docker stop \${OLD_ID} >/dev/null 2>&1 || true
docker rm -f \${OLD_ID} >/dev/null 2>&1 || true
fi
echo "🐳 Pull image baru..."
docker pull "\${DOCKER_IMAGE}"
echo "🚀 Run container baru..."
docker run -d --name "\${APP_NAME}" --restart always \
--env-file "\${ENV_PATH}" \
-p \${PORT}:8081 \
--network "\${NETWORK_NAME}" \
"\${DOCKER_IMAGE}"
echo "✅ Deployment selesai di port \${PORT}"
REMOTE
only:
- development
View File
-59
View File
@@ -1,59 +0,0 @@
# ===============================
# LTI-API Makefile (Docker Setup)
# ===============================
APP_NAME := lti-api
COMPOSE := docker compose -f docker-compose.yaml
NETWORK := lti-network
ENV_FILE := .env.lti-api
include $(ENV_FILE)
export $(shell sed 's/=.*//' $(ENV_FILE))
MIGRATIONS_DIR := ./migrations
MIGRATE_IMAGE := migrate/migrate:v4.15.2
DB_URL := postgres://$(DB_USER):$(DB_PASSWORD)@lti-postgres:5432/$(DB_NAME)?sslmode=disable
# --- Docker ---
docker-local:
@echo "🚀 Starting $(APP_NAME) with local PostgreSQL & Redis..."
@$(COMPOSE) up --build -d
docker-down:
@$(COMPOSE) down --remove-orphans
docker-nuke:
@echo "💣 Removing all containers, images, and volumes..."
@$(COMPOSE) down --rmi all --volumes --remove-orphans
# --- Database / Migration ---
wait-db:
@echo "⏳ Waiting for database lti-postgres to be ready (inside Docker network)..."
@$(COMPOSE) run --rm app sh -c 'until nc -z lti-postgres 5432; do echo "Waiting for DB..."; sleep 2; done; echo "✅ Database is ready!"'
migrate-up: wait-db
@echo "⬆️ Running migrations..."
@docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" up
migrate-down: wait-db
@echo "⬇️ Rolling back all migrations..."
@docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" down -all
seed:
@echo "🌱 Running seed script..."
@$(COMPOSE) run --rm app go run cmd/seed/main.go
psql:
@docker exec -it lti-postgres psql -U $(DB_USER) -d $(DB_NAME)
logs:
@$(COMPOSE) logs -f app
restart:
@$(COMPOSE) restart
status:
@$(COMPOSE) ps
+3
View File
@@ -0,0 +1,3 @@
POSTGRES_USER=postgres
POSTGRES_PASSWORD=Postgres@Secure2025!
POSTGRES_DB=db_lti_erp
+47
View File
@@ -0,0 +1,47 @@
-- ============================================================
-- 🧩 INIT SCRIPT: CREATE LIMITED APP USER FOR LTI API
-- ============================================================
-- Buat user aplikasi jika belum ada
DO
$$
BEGIN
IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'app_lti_user') THEN
CREATE ROLE app_lti_user WITH LOGIN PASSWORD 'AppLti@Secure2025!' NOINHERIT NOCREATEROLE NOCREATEDB NOSUPERUSER;
RAISE NOTICE '✅ Role app_lti_user created successfully.';
ELSE
RAISE NOTICE '️ Role app_lti_user already exists.';
END IF;
END
$$;
-- Buat database jika belum ada
DO
$$
BEGIN
IF NOT EXISTS (SELECT FROM pg_database WHERE datname = 'db_lti_erp') THEN
CREATE DATABASE db_lti_erp OWNER app_lti_user;
RAISE NOTICE '✅ Database db_lti_erp created and owned by app_lti_user.';
ELSE
RAISE NOTICE '️ Database db_lti_erp already exists.';
END IF;
END
$$;
\connect db_lti_erp
-- Beri hak CRUD untuk app_lti_user
GRANT CONNECT ON DATABASE db_lti_erp TO app_lti_user;
GRANT USAGE ON SCHEMA public TO app_lti_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_lti_user;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_lti_user;
-- Set default privileges agar tabel baru juga bisa diakses
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_lti_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT USAGE, SELECT ON SEQUENCES TO app_lti_user;
-- Tampilkan hasil
\du app_lti_user
+2
View File
@@ -41,6 +41,8 @@ services:
working_dir: /lti-api
volumes:
- .:/lti-api
- ./internal/config/jwtRS256.key:/run/keys/jwtRS256.key
- ./internal/config/jwtRS256.key.pub:/run/keys/jwtRS256.key.pub
command: air -c .air.toml
env_file:
- .env
+43 -22
View File
@@ -1,30 +1,28 @@
version: "3.9"
services:
dev-lti-api:
container_name: dev-lti-api
dev-api-lti:
build:
context: .
dockerfile: Dockerfile.local
image: dev-lti-api:latest
dockerfile: Dockerfile
container_name: dev-api-lti
working_dir: /lti-api
command: air -c .air.toml
command: ["/bin/sh", "scripts/entrypoint.sh"]
ports:
- "8081:8081"
env_file:
- .env.lti-api
- .env
environment:
# override agar koneksi ke container internal
DB_HOST: dev-lti-postgres
DB_HOST: dev-postgres-lti
DB_PORT: 5432
REDIS_URL: redis://dev-lti-redis:6379/0
REDIS_URL: redis://dev-redis-lti:6379/0
volumes:
- .:/lti-api
- ./.air.toml:/lti-api/.air.toml:ro
- ./internal/config/jwtRS256.key:/run/keys/jwtRS256.key
- ./internal/config/jwtRS256.key.pub:/run/keys/jwtRS256.key.pub
depends_on:
- dev-lti-postgres
- dev-lti-redis
- dev-postgres-lti
- dev-redis-lti
networks:
- lti-network
healthcheck:
@@ -33,19 +31,26 @@ services:
timeout: 3s
retries: 10
start_period: 10s
deploy:
resources:
limits:
cpus: "2.0"
memory: 2G
reservations:
cpus: "1.0"
memory: 512M
dev-lti-postgres:
dev-postgres-lti:
image: postgres:15-alpine
container_name: dev-lti-postgres
container_name: dev-postgres-lti
restart: always
environment:
POSTGRES_USER: ${DB_USER:-postgres}
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
POSTGRES_DB: ${DB_NAME:-db_lti_erp}
env_file:
- credential/.env.db
ports:
- "5433:5432"
volumes:
- dev-lti-postgres-data:/var/lib/postgresql/data
- dev-postgres-lti-data:/var/lib/postgresql/data
- ./credential:/docker-entrypoint-initdb.d:ro
networks:
- lti-network
healthcheck:
@@ -54,10 +59,18 @@ services:
timeout: 5s
retries: 5
start_period: 5s
deploy:
resources:
limits:
cpus: "1.0"
memory: 2G
reservations:
cpus: "0.5"
memory: 512M
dev-lti-redis:
dev-redis-lti:
image: redis:7-alpine
container_name: dev-lti-redis
container_name: dev-redis-lti
restart: always
ports:
- "6380:6379"
@@ -68,10 +81,18 @@ services:
interval: 10s
timeout: 3s
retries: 10
deploy:
resources:
limits:
cpus: "0.5"
memory: 512M
reservations:
cpus: "0.2"
memory: 256M
networks:
lti-network:
driver: bridge
volumes:
dev-lti-postgres-data:
dev-postgres-lti-data:
-8
View File
@@ -5,7 +5,6 @@ go 1.23
require (
github.com/MicahParks/keyfunc/v2 v2.1.0
github.com/bytedance/sonic v1.12.1
github.com/glebarez/sqlite v1.11.0
github.com/go-playground/validator/v10 v10.27.0
github.com/gofiber/contrib/jwt v1.0.10
github.com/gofiber/fiber/v2 v2.52.5
@@ -26,10 +25,8 @@ require (
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/google/go-cmp v0.6.0 // indirect
@@ -54,7 +51,6 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/philhofer/fwd v1.1.2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
@@ -79,8 +75,4 @@ require (
golang.org/x/text v0.22.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/sqlite v1.23.1 // indirect
)
-19
View File
@@ -27,18 +27,12 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -56,8 +50,6 @@ github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17w
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
@@ -154,9 +146,6 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE=
github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@@ -317,12 +306,4 @@ gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
@@ -13,6 +13,7 @@ type ApprovalRepository interface {
FindByTarget(ctx context.Context, workflow string, approvableID uint, modifier func(*gorm.DB) *gorm.DB) ([]entity.Approval, error)
LatestByTarget(ctx context.Context, workflow string, approvableID uint, modifier func(*gorm.DB) *gorm.DB) (*entity.Approval, error)
LatestByTargets(ctx context.Context, workflow string, approvableIDs []uint, modifier func(*gorm.DB) *gorm.DB) (map[uint]entity.Approval, error)
DeleteByTarget(ctx context.Context, workflow string, approvableID uint) error
}
type approvalRepositoryImpl struct {
@@ -104,3 +105,13 @@ func (r *approvalRepositoryImpl) LatestByTargets(
return result, nil
}
func (r *approvalRepositoryImpl) DeleteByTarget(
ctx context.Context,
workflow string,
approvableID uint,
) error {
return r.DB().WithContext(ctx).
Where("approvable_type = ? AND approvable_id = ?", workflow, approvableID).
Delete(&entity.Approval{}).Error
}
+40 -8
View File
@@ -3,6 +3,8 @@ package validation
import (
"errors"
"fmt"
"reflect"
"strings"
"github.com/go-playground/validator/v10"
)
@@ -21,34 +23,41 @@ var customMessages = map[string]string{
"alphanum": "Field %s must contain only alphanumeric characters",
"oneof": "Invalid value for field %s",
"password": "Field %s must be at least 8 characters, contain uppercase, lowercase, number, and special character",
"gt": "Invalid %s, must be greater than %s",
}
func CustomErrorMessages(err error) map[string]string {
func CustomErrorMessages(err error) (string, map[string]string) {
var validationErrors validator.ValidationErrors
if errors.As(err, &validationErrors) {
return generateErrorMessages(validationErrors)
}
return nil
return "", nil
}
func generateErrorMessages(validationErrors validator.ValidationErrors) map[string]string {
func generateErrorMessages(validationErrors validator.ValidationErrors) (string, map[string]string) {
errorsMap := make(map[string]string)
for _, err := range validationErrors {
var firstMessage string
for i, err := range validationErrors {
fieldName := err.StructNamespace()
tag := err.Tag()
customMessage := customMessages[tag]
var msg string
if customMessage != "" {
errorsMap[fieldName] = formatErrorMessage(customMessage, err, tag)
msg = formatErrorMessage(customMessage, err, tag)
} else {
errorsMap[fieldName] = defaultErrorMessage(err)
msg = defaultErrorMessage(err)
}
errorsMap[fieldName] = msg
if i == 0 {
firstMessage = msg
}
}
return errorsMap
return firstMessage, errorsMap
}
func formatErrorMessage(customMessage string, err validator.FieldError, tag string) string {
if tag == "min" || tag == "max" || tag == "len" {
if tag == "min" || tag == "max" || tag == "len" || tag == "gt" {
return fmt.Sprintf(customMessage, err.Field(), err.Param())
}
return fmt.Sprintf(customMessage, err.Field())
@@ -61,6 +70,16 @@ func defaultErrorMessage(err validator.FieldError) string {
func Validator() *validator.Validate {
validate := validator.New()
validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
if jsonTag := getTagName(fld, "json"); jsonTag != "" {
return jsonTag
}
if queryTag := getTagName(fld, "query"); queryTag != "" {
return queryTag
}
return fld.Name
})
if err := validate.RegisterValidation("password", Password); err != nil {
return nil
}
@@ -72,3 +91,16 @@ func Validator() *validator.Validate {
}
return validate
}
func getTagName(fld reflect.StructField, tag string) string {
value, ok := fld.Tag.Lookup(tag)
if !ok || value == "-" {
return ""
}
name := strings.Split(value, ",")[0]
if name == "" || name == "-" {
return ""
}
return name
}
BIN
View File
Binary file not shown.
@@ -1,36 +0,0 @@
CREATE TABLE IF NOT EXISTS project_chickins (
id BIGSERIAL PRIMARY KEY,
project_flock_kandang_id BIGINT NOT NULL,
chick_in_date DATE NOT NULL,
quantity NUMERIC(15, 3) NOT NULL,
note TEXT,
created_by BIGINT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
deleted_at TIMESTAMPTZ
);
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
ALTER TABLE project_chickins
ADD CONSTRAINT fk_project_flock_kandang_id
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
ALTER TABLE project_chickins
ADD CONSTRAINT fk_created_by
FOREIGN KEY (created_by)
REFERENCES users(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
END $$;
-- INDEXES
CREATE INDEX IF NOT EXISTS idx_project_chickins_project_flock_kandang_id ON project_chickins (project_flock_kandang_id);
CREATE INDEX IF NOT EXISTS idx_project_chickins_created_by ON project_chickins (created_by);
@@ -1,36 +0,0 @@
CREATE TABLE IF NOT EXISTS project_flock_populations (
id BIGSERIAL PRIMARY KEY,
project_flock_kandang_id BIGINT NOT NULL,
initial_quantity NUMERIC(15, 3) NOT NULL,
current_quantity NUMERIC(15, 3) NOT NULL,
reserved_quantity NUMERIC(15, 3),
created_by BIGINT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
deleted_at TIMESTAMPTZ
);
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
ALTER TABLE project_flock_populations
ADD CONSTRAINT fk_project_flock_kandang_id
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
ALTER TABLE project_flock_populations
ADD CONSTRAINT fk_created_by
FOREIGN KEY (created_by)
REFERENCES users(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
END $$;
-- INDEXES
CREATE INDEX IF NOT EXISTS idx_project_flock_populations_project_flock_kandang_id ON project_flock_populations (project_flock_kandang_id);
CREATE INDEX IF NOT EXISTS idx_project_flock_populations_created_by ON project_flock_populations (created_by);
@@ -9,7 +9,7 @@ CREATE TABLE IF NOT EXISTS project_chickin_details (
deleted_at TIMESTAMPTZ
);
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_chickins') THEN
@@ -1,22 +1,30 @@
ALTER TABLE kandangs
DROP CONSTRAINT IF EXISTS kandangs_project_flock_id_fkey;
DROP CONSTRAINT IF EXISTS kandangs_project_flock_id_fkey;
ALTER TABLE kandangs
DROP COLUMN IF EXISTS project_flock_id;
ALTER TABLE kandangs DROP COLUMN IF EXISTS project_flock_id;
ALTER TABLE project_chickins
DROP CONSTRAINT fk_project_flock_kandang_id,
ADD CONSTRAINT fk_project_flock_kandang_id
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs(id)
ON UPDATE CASCADE
ON DELETE CASCADE;
-- Only alter if tables exist
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_chickins') THEN
ALTER TABLE project_chickins
DROP CONSTRAINT IF EXISTS fk_project_flock_kandang_id;
ALTER TABLE project_chickins
ADD CONSTRAINT fk_project_flock_kandang_id
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs(id)
ON UPDATE CASCADE
ON DELETE CASCADE;
END IF;
ALTER TABLE project_flock_populations
DROP CONSTRAINT fk_project_flock_kandang_id,
ADD CONSTRAINT fk_project_flock_kandang_id
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs(id)
ON UPDATE CASCADE
ON DELETE CASCADE;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_populations') THEN
ALTER TABLE project_flock_populations
DROP CONSTRAINT IF EXISTS fk_project_flock_kandang_id;
ALTER TABLE project_flock_populations
ADD CONSTRAINT fk_project_flock_kandang_id
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs(id)
ON UPDATE CASCADE
ON DELETE CASCADE;
END IF;
END $$;
@@ -0,0 +1 @@
DROP TABLE IF EXISTS laying_transfers CASCADE;
@@ -0,0 +1,52 @@
CREATE TABLE IF NOT EXISTS laying_transfers (
id BIGSERIAL PRIMARY KEY,
transfer_number VARCHAR(50) UNIQUE NOT NULL,
from_project_flock_id BIGINT NOT NULL,
to_project_flock_id BIGINT NOT NULL,
transfer_date DATE NOT NULL,
pending_usage_qty NUMERIC(15, 3),
usage_qty NUMERIC(15, 3),
notes TEXT,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
deleted_at TIMESTAMPTZ,
created_by BIGINT NOT NULL
);
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flocks') THEN
ALTER TABLE laying_transfers
ADD CONSTRAINT fk_laying_from_project_flock
FOREIGN KEY (from_project_flock_id)
REFERENCES project_flocks(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE laying_transfers
ADD CONSTRAINT fk_laying_to_project_flock
FOREIGN KEY (to_project_flock_id)
REFERENCES project_flocks(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
ALTER TABLE laying_transfers
ADD CONSTRAINT fk_laying_created_by
FOREIGN KEY (created_by)
REFERENCES users(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
END $$;
-- INDEXES
CREATE UNIQUE INDEX IF NOT EXISTS idx_laying_transfers_transfer_number ON laying_transfers (transfer_number)
WHERE
deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_laying_transfers_from_project_flock_id ON laying_transfers (from_project_flock_id);
CREATE INDEX IF NOT EXISTS idx_laying_transfers_to_project_flock_id ON laying_transfers (to_project_flock_id);
CREATE INDEX IF NOT EXISTS idx_laying_transfers_created_by ON laying_transfers (created_by);
CREATE INDEX IF NOT EXISTS idx_laying_transfers_deleted_at ON laying_transfers (deleted_at);
@@ -0,0 +1,58 @@
-- ============================================
-- MIGRATION: project_chickins
-- ============================================
-- STEP 1: Hapus tabel jika sudah ada
DROP TABLE IF EXISTS project_chickins;
-- STEP 2: Buat tabel project_chickins
CREATE TABLE IF NOT EXISTS project_chickins (
id BIGSERIAL PRIMARY KEY,
project_flock_kandang_id BIGINT NOT NULL,
product_warehouse_id BIGINT NOT NULL,
chick_in_date DATE NOT NULL,
usage_qty NUMERIC(15, 3) NOT NULL,
pending_usage_qty NUMERIC(15, 3) DEFAULT 0,
notes TEXT,
created_by BIGINT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
deleted_at TIMESTAMPTZ
);
-- STEP 3: FOREIGN KEYS
BEGIN;
-- Relasi ke project_flock_kandangs
ALTER TABLE project_chickins
ADD CONSTRAINT fk_project_chickins_kandang FOREIGN KEY (project_flock_kandang_id) REFERENCES project_flock_kandangs (id) ON DELETE RESTRICT ON UPDATE CASCADE;
-- Relasi ke product_warehouses
ALTER TABLE project_chickins
ADD CONSTRAINT fk_project_chickins_warehouse FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses (id) ON DELETE RESTRICT ON UPDATE CASCADE;
-- Relasi ke users
ALTER TABLE project_chickins
ADD CONSTRAINT fk_project_chickins_created_by FOREIGN KEY (created_by) REFERENCES users (id) ON DELETE RESTRICT ON UPDATE CASCADE;
COMMIT;
-- STEP 4: INDEXES
CREATE INDEX IF NOT EXISTS idx_chickins_kandang_id ON project_chickins (project_flock_kandang_id)
WHERE
deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_chickins_warehouse_id ON project_chickins (product_warehouse_id)
WHERE
deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_chickins_created_by ON project_chickins (created_by);
-- Composite index for common queries
CREATE INDEX IF NOT EXISTS idx_chickins_kandang_deleted ON project_chickins (
project_flock_kandang_id,
deleted_at
);
-- Index for soft delete queries
CREATE INDEX IF NOT EXISTS idx_chickins_deleted_at ON project_chickins (deleted_at);
@@ -0,0 +1,62 @@
-- ============================================
-- MIGRATION: project_flock_populations
-- ============================================
-- STEP 1: Hapus tabel jika sudah ada
DROP TABLE IF EXISTS project_flock_populations;
-- STEP 2: Buat tabel project_flock_populations
CREATE TABLE IF NOT EXISTS project_flock_populations (
id BIGSERIAL PRIMARY KEY,
project_chickin_id BIGINT NOT NULL,
product_warehouse_id BIGINT NOT NULL,
total_qty NUMERIC(15, 3) NOT NULL,
total_used_qty NUMERIC(15, 3) DEFAULT 0,
notes TEXT,
created_by BIGINT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
deleted_at TIMESTAMPTZ
);
-- STEP 3: FOREIGN KEYS
BEGIN;
-- Relasi ke project_chickins
ALTER TABLE project_flock_populations
ADD CONSTRAINT fk_project_flock_populations_chickin FOREIGN KEY (project_chickin_id) REFERENCES project_chickins (id) ON DELETE RESTRICT ON UPDATE CASCADE;
-- Relasi ke product_warehouses
ALTER TABLE project_flock_populations
ADD CONSTRAINT fk_project_flock_populations_warehouse FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses (id) ON DELETE RESTRICT ON UPDATE CASCADE;
-- Relasi ke users
ALTER TABLE project_flock_populations
ADD CONSTRAINT fk_project_flock_populations_created_by FOREIGN KEY (created_by) REFERENCES users (id) ON DELETE RESTRICT ON UPDATE CASCADE;
COMMIT;
-- STEP 4: INDEXES
CREATE INDEX IF NOT EXISTS idx_populations_chickin_id ON project_flock_populations (project_chickin_id)
WHERE
deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_populations_warehouse_id ON project_flock_populations (product_warehouse_id)
WHERE
deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_populations_created_by ON project_flock_populations (created_by);
-- Composite index for common queries
CREATE INDEX IF NOT EXISTS idx_populations_chickin_deleted ON project_flock_populations (
project_chickin_id,
deleted_at
);
-- Index for soft delete queries
CREATE INDEX IF NOT EXISTS idx_populations_deleted_at ON project_flock_populations (deleted_at);
-- Unique constraint: one population per chickin
CREATE UNIQUE INDEX IF NOT EXISTS idx_populations_chickin_unique ON project_flock_populations (project_chickin_id)
WHERE
deleted_at IS NULL;
@@ -0,0 +1,5 @@
-- Rollback laying_transfer_sources dan laying_transfer_targets tables
DROP TABLE IF EXISTS laying_transfer_targets CASCADE;
DROP TABLE IF EXISTS laying_transfer_sources CASCADE;
@@ -0,0 +1,93 @@
-- Create laying_transfer_sources dan laying_transfer_targets tables
-- 1. Create laying_transfer_sources table (detail sumber - kandang asal growing)
CREATE TABLE laying_transfer_sources (
id BIGSERIAL PRIMARY KEY,
laying_transfer_id BIGINT NOT NULL,
source_project_flock_kandang_id BIGINT NOT NULL,
product_warehouse_id BIGINT,
qty NUMERIC(15, 3) NOT NULL,
note TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
-- Add foreign keys untuk laying_transfer_sources
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'laying_transfers') THEN
ALTER TABLE laying_transfer_sources
ADD CONSTRAINT fk_laying_transfer_sources_laying_transfer_id
FOREIGN KEY (laying_transfer_id) REFERENCES laying_transfers(id) ON DELETE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
ALTER TABLE laying_transfer_sources
ADD CONSTRAINT fk_laying_transfer_sources_project_flock_kandang_id
FOREIGN KEY (source_project_flock_kandang_id) REFERENCES project_flock_kandangs(id) ON DELETE RESTRICT;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
ALTER TABLE laying_transfer_sources
ADD CONSTRAINT fk_laying_transfer_sources_product_warehouse_id
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id) ON DELETE SET NULL;
END IF;
END $$;
-- 2. Create laying_transfer_targets table (detail tujuan - kandang laying)
CREATE TABLE laying_transfer_targets (
id BIGSERIAL PRIMARY KEY,
laying_transfer_id BIGINT NOT NULL,
target_project_flock_kandang_id BIGINT NOT NULL,
qty NUMERIC(15, 3) NOT NULL,
product_warehouse_id BIGINT,
note TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
-- Add foreign keys untuk laying_transfer_targets
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'laying_transfers') THEN
ALTER TABLE laying_transfer_targets
ADD CONSTRAINT fk_laying_transfer_targets_laying_transfer_id
FOREIGN KEY (laying_transfer_id) REFERENCES laying_transfers(id) ON DELETE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
ALTER TABLE laying_transfer_targets
ADD CONSTRAINT fk_laying_transfer_targets_project_flock_kandang_id
FOREIGN KEY (target_project_flock_kandang_id) REFERENCES project_flock_kandangs(id) ON DELETE RESTRICT;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
ALTER TABLE laying_transfer_targets
ADD CONSTRAINT fk_laying_transfer_targets_product_warehouse_id
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id) ON DELETE SET NULL;
END IF;
END $$;
-- 3. Create indexes untuk laying_transfer_sources
CREATE INDEX idx_laying_transfer_sources_laying_transfer_id ON laying_transfer_sources (laying_transfer_id);
CREATE INDEX idx_laying_transfer_sources_source_kandang_id ON laying_transfer_sources (
source_project_flock_kandang_id
);
CREATE INDEX idx_laying_transfer_sources_product_warehouse_id ON laying_transfer_sources (product_warehouse_id);
CREATE INDEX idx_laying_transfer_sources_deleted_at ON laying_transfer_sources (deleted_at);
-- 4. Create indexes untuk laying_transfer_targets
CREATE INDEX idx_laying_transfer_targets_laying_transfer_id ON laying_transfer_targets (laying_transfer_id);
CREATE INDEX idx_laying_transfer_targets_target_kandang_id ON laying_transfer_targets (
target_project_flock_kandang_id
);
CREATE INDEX idx_laying_transfer_targets_product_warehouse_id ON laying_transfer_targets (product_warehouse_id);
CREATE INDEX idx_laying_transfer_targets_deleted_at ON laying_transfer_targets (deleted_at);
@@ -0,0 +1 @@
DROP TABLE IF EXISTS purchase_items;
@@ -0,0 +1,54 @@
CREATE TABLE IF NOT EXISTS purchase_items (
id BIGSERIAL PRIMARY KEY,
purchase_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
warehouse_id BIGINT NOT NULL,
product_warehouse_id BIGINT,
received_date TIMESTAMPTZ,
travel_number VARCHAR,
travel_number_docs VARCHAR,
vehicle_number VARCHAR,
sub_qty NUMERIC(15, 3) NOT NULL,
total_qty NUMERIC(15, 3) NOT NULL DEFAULT 0,
total_used NUMERIC(15, 3) NOT NULL DEFAULT 0,
price NUMERIC(15, 3) NOT NULL DEFAULT 0,
total_price NUMERIC(15, 3) NOT NULL DEFAULT 0,
CONSTRAINT uq_purchase_items_purchase_product_warehouse
UNIQUE (purchase_id, product_id, warehouse_id)
);
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'products') THEN
EXECUTE
'ALTER TABLE purchase_items
ADD CONSTRAINT fk_purchase_items_product
FOREIGN KEY (product_id)
REFERENCES products(id)
ON DELETE RESTRICT ON UPDATE CASCADE';
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'warehouses') THEN
EXECUTE
'ALTER TABLE purchase_items
ADD CONSTRAINT fk_purchase_items_warehouse
FOREIGN KEY (warehouse_id)
REFERENCES warehouses(id)
ON DELETE RESTRICT ON UPDATE CASCADE';
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
EXECUTE
'ALTER TABLE purchase_items
ADD CONSTRAINT fk_purchase_items_product_warehouse
FOREIGN KEY (product_warehouse_id)
REFERENCES product_warehouses(id)
ON DELETE SET NULL ON UPDATE CASCADE';
END IF;
END $$;
CREATE INDEX IF NOT EXISTS idx_purchase_items_product_id ON purchase_items (product_id);
CREATE INDEX IF NOT EXISTS idx_purchase_items_warehouse_id ON purchase_items (warehouse_id);
CREATE INDEX IF NOT EXISTS idx_purchase_items_product_warehouse_id ON purchase_items (product_warehouse_id);
CREATE INDEX IF NOT EXISTS idx_purchase_items_purchase_id ON purchase_items (purchase_id);
@@ -0,0 +1,14 @@
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'fk_purchase_items_purchase'
AND conrelid = 'purchase_items'::regclass
) THEN
ALTER TABLE purchase_items
DROP CONSTRAINT fk_purchase_items_purchase;
END IF;
END $$;
DROP TABLE IF EXISTS purchases;
@@ -0,0 +1,58 @@
CREATE TABLE IF NOT EXISTS purchases (
id BIGSERIAL PRIMARY KEY,
pr_number VARCHAR NOT NULL,
po_number VARCHAR NULL,
po_date TIMESTAMPTZ NULL,
supplier_id BIGINT NOT NULL,
credit_term INT NOT NULL,
due_date TIMESTAMPTZ,
grand_total NUMERIC(15, 3) NOT NULL,
notes TEXT,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL,
deleted_at TIMESTAMPTZ,
created_by BIGINT NOT NULL,
CONSTRAINT uq_purchases_pr_number UNIQUE (pr_number),
CONSTRAINT uq_purchases_po_number UNIQUE (po_number)
);
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'suppliers') THEN
EXECUTE
'ALTER TABLE purchases
ADD CONSTRAINT fk_purchases_supplier
FOREIGN KEY (supplier_id)
REFERENCES suppliers(id)
ON DELETE RESTRICT ON UPDATE CASCADE';
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
EXECUTE
'ALTER TABLE purchases
ADD CONSTRAINT fk_purchases_created_by
FOREIGN KEY (created_by)
REFERENCES users(id)
ON DELETE RESTRICT ON UPDATE CASCADE';
END IF;
IF EXISTS (
SELECT 1 FROM pg_tables WHERE tablename = 'purchase_items'
) AND NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'fk_purchase_items_purchase'
) THEN
EXECUTE
'ALTER TABLE purchase_items
ADD CONSTRAINT fk_purchase_items_purchase
FOREIGN KEY (purchase_id)
REFERENCES purchases(id)
ON DELETE CASCADE ON UPDATE CASCADE';
END IF;
END $$;
CREATE INDEX IF NOT EXISTS idx_purchases_supplier_id ON purchases (supplier_id);
CREATE INDEX IF NOT EXISTS idx_purchases_created_by ON purchases (created_by);
CREATE INDEX IF NOT EXISTS idx_purchases_po_date ON purchases (po_date);
CREATE INDEX IF NOT EXISTS idx_purchases_deleted_at ON purchases (deleted_at);
@@ -0,0 +1,5 @@
DROP TABLE IF EXISTS marketing_delivery_products CASCADE;
DROP TABLE IF EXISTS marketing_products CASCADE;
DROP TABLE IF EXISTS marketings CASCADE;
@@ -0,0 +1,44 @@
CREATE TABLE marketings (
id BIGSERIAL PRIMARY KEY,
so_number VARCHAR(255) UNIQUE NOT NULL,
customer_id BIGINT NOT NULL,
so_docs VARCHAR(20),
so_date DATE NOT NULL,
sales_person_id BIGINT NOT NULL,
notes TEXT,
created_by BIGINT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'customers') THEN
ALTER TABLE marketings
ADD CONSTRAINT fk_marketings_customer_id
FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE RESTRICT;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
ALTER TABLE marketings
ADD CONSTRAINT fk_marketings_sales_person_id
FOREIGN KEY (sales_person_id) REFERENCES users(id) ON DELETE RESTRICT;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
ALTER TABLE marketings
ADD CONSTRAINT fk_marketings_created_by
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE RESTRICT;
END IF;
END $$;
CREATE INDEX idx_marketings_customer_id ON marketings (customer_id);
CREATE INDEX idx_marketings_sales_person_id ON marketings (sales_person_id);
CREATE INDEX idx_marketings_created_by ON marketings (created_by);
CREATE INDEX idx_marketings_so_date ON marketings (so_date);
CREATE INDEX idx_marketings_deleted_at ON marketings (deleted_at);
@@ -0,0 +1 @@
DROP TABLE IF EXISTS marketing_products CASCADE;
@@ -0,0 +1,34 @@
CREATE TABLE marketing_products (
id BIGSERIAL PRIMARY KEY,
marketing_id BIGINT NOT NULL,
product_warehouse_id BIGINT NOT NULL,
qty NUMERIC(15, 3) NOT NULL,
unit_price NUMERIC(15, 3) NOT NULL,
avg_weight NUMERIC(15, 3) NOT NULL,
total_weight NUMERIC(15, 3) NOT NULL,
total_price NUMERIC(15, 3) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'marketings') THEN
ALTER TABLE marketing_products
ADD CONSTRAINT fk_marketing_products_marketing_id
FOREIGN KEY (marketing_id) REFERENCES marketings(id) ON DELETE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
ALTER TABLE marketing_products
ADD CONSTRAINT fk_marketing_products_product_warehouse_id
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id) ON DELETE RESTRICT;
END IF;
END $$;
CREATE INDEX idx_marketing_products_marketing_id ON marketing_products (marketing_id);
CREATE INDEX idx_marketing_products_product_warehouse_id ON marketing_products (product_warehouse_id);
CREATE INDEX idx_marketing_products_deleted_at ON marketing_products (deleted_at);
@@ -0,0 +1,2 @@
DROP TABLE IF EXISTS marketing_delivery_products CASCADE;
@@ -0,0 +1,29 @@
CREATE TABLE marketing_delivery_products (
id BIGSERIAL PRIMARY KEY,
marketing_product_id BIGINT UNIQUE NOT NULL,
qty NUMERIC(15, 3) NOT NULL,
unit_price NUMERIC(15, 3) NOT NULL,
total_weight NUMERIC(15, 3) NOT NULL,
avg_weight NUMERIC(15, 3) NOT NULL,
total_price NUMERIC(15, 3) NOT NULL,
delivery_date DATE,
vehicle_number VARCHAR(50),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'marketing_products') THEN
ALTER TABLE marketing_delivery_products
ADD CONSTRAINT fk_marketing_delivery_products_marketing_product_id
FOREIGN KEY (marketing_product_id) REFERENCES marketing_products(id) ON DELETE CASCADE;
END IF;
END $$;
CREATE INDEX idx_marketing_delivery_products_marketing_product_id ON marketing_delivery_products (marketing_product_id);
CREATE INDEX idx_marketing_delivery_products_delivery_date ON marketing_delivery_products (delivery_date);
CREATE INDEX idx_marketing_delivery_products_deleted_at ON marketing_delivery_products (deleted_at);
@@ -0,0 +1,2 @@
ALTER TABLE kandangs
DROP COLUMN IF EXISTS capacity;
@@ -0,0 +1,2 @@
ALTER TABLE kandangs
ADD COLUMN capacity NUMERIC(15,3) NOT NULL;
@@ -0,0 +1 @@
DROP TABLE IF EXISTS expenses;
@@ -0,0 +1,50 @@
CREATE TABLE expenses (
id BIGSERIAL PRIMARY KEY,
reference_number VARCHAR(50) UNIQUE NOT NULL,
supplier_id BIGINT NULL,
category VARCHAR(50) NOT NULL CHECK (
category IN ('BOP', 'NON-BOP')
),
po_number VARCHAR(50) NULL,
document_path JSON,
realization_document_path JSON,
expense_date DATE NOT NULL,
realization_date DATE,
grand_total NUMERIC(15, 3) DEFAULT 0,
note TEXT,
created_by BIGINT,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
deleted_at TIMESTAMPTZ
);
CREATE SEQUENCE expenses_ref_seq INCREMENT BY 1 START WITH 1;
-- Tambahkan Foreign Key ke suppliers
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'suppliers') THEN
ALTER TABLE expenses
ADD CONSTRAINT fk_expenses_supplier_id
FOREIGN KEY (supplier_id) REFERENCES suppliers(id);
END IF;
END $$;
-- Tambahkan Foreign Key ke users (created_by)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
ALTER TABLE expenses
ADD CONSTRAINT fk_expenses_created_by
FOREIGN KEY (created_by) REFERENCES users(id);
END IF;
END $$;
-- Index
CREATE INDEX idx_expenses_supplier_id ON expenses (supplier_id);
CREATE INDEX idx_expenses_expense_date ON expenses (expense_date);
CREATE INDEX idx_expenses_deleted_at ON expenses (deleted_at);
@@ -0,0 +1,3 @@
DROP TABLE IF EXISTS expense_nonstocks;
DROP SEQUENCE expenses_ref_seq;
@@ -0,0 +1,56 @@
CREATE TABLE expense_nonstocks (
id BIGSERIAL PRIMARY KEY,
expense_id BIGINT NOT NULL,
project_flock_kandang_id BIGINT NULL,
kandang_id BIGINT NULL,
nonstock_id BIGINT,
qty NUMERIC(15, 3) NOT NULL,
unit_price NUMERIC(15, 3) NOT NULL,
total_price NUMERIC(15, 3) NOT NULL,
note TEXT NULL
);
-- Tambahkan Foreign Key ke expenses
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'expenses') THEN
ALTER TABLE expense_nonstocks
ADD CONSTRAINT fk_expense_nonstocks_expense_id
FOREIGN KEY (expense_id) REFERENCES expenses(id);
END IF;
END $$;
-- Tambahkan Foreign Key ke project_flock_kandangs
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
ALTER TABLE expense_nonstocks
ADD CONSTRAINT fk_expense_nonstocks_kandang_id
FOREIGN KEY (project_flock_kandang_id) REFERENCES project_flock_kandangs(id);
END IF;
END $$;
-- Tambahkan Foreign key ke kandang_id
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'kandangs') THEN
ALTER TABLE expense_nonstocks
ADD CONSTRAINT fk_expense_nonstocks_kandang_id_2
FOREIGN KEY (kandang_id) REFERENCES kandangs(id);
END IF;
END $$;
-- Tambahkan Foreign Key ke nonstocks
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'nonstocks') THEN
ALTER TABLE expense_nonstocks
ADD CONSTRAINT fk_expense_nonstocks_nonstock_id
FOREIGN KEY (nonstock_id) REFERENCES nonstocks(id);
END IF;
END $$;
-- Index
CREATE INDEX idx_expense_nonstocks_expense_id ON expense_nonstocks (expense_id);
CREATE INDEX idx_expense_nonstocks_nonstock_id ON expense_nonstocks (nonstock_id);
@@ -0,0 +1 @@
DROP TABLE IF EXISTS expense_realizations;
@@ -0,0 +1,35 @@
CREATE TABLE expense_realizations (
id BIGSERIAL PRIMARY KEY,
expense_nonstock_id BIGINT UNIQUE,
realization_qty NUMERIC(15, 3) NOT NULL,
realization_unit_price NUMERIC(15, 3) NOT NULL,
realization_total_price NUMERIC(15, 3) NOT NULL,
realization_date DATE NOT NULL,
note TEXT,
created_by BIGINT
);
-- Tambahkan Foreign Key ke expense_nonstocks
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'expense_nonstocks') THEN
ALTER TABLE expense_realizations
ADD CONSTRAINT fk_expense_realizations_nonstock_id
FOREIGN KEY (expense_nonstock_id) REFERENCES expense_nonstocks(id);
END IF;
END $$;
-- Tambahkan Foreign Key ke users (created_by)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
ALTER TABLE expense_realizations
ADD CONSTRAINT fk_expense_realizations_created_by
FOREIGN KEY (created_by) REFERENCES users(id);
END IF;
END $$;
-- Index
CREATE INDEX idx_expense_realizations_nonstock_id ON expense_realizations (expense_nonstock_id);
CREATE INDEX idx_expense_realizations_date ON expense_realizations (realization_date);
@@ -0,0 +1,16 @@
BEGIN;
ALTER TABLE project_flock_kandangs
DROP COLUMN IF EXISTS period;
ALTER TABLE project_flocks
ADD COLUMN IF NOT EXISTS period INT NOT NULL DEFAULT 0;
CREATE UNIQUE INDEX IF NOT EXISTS project_flocks_base_period_unique
ON project_flocks (
LOWER(TRIM(regexp_replace(flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g'))),
period
)
WHERE deleted_at IS NULL;
COMMIT;
@@ -0,0 +1,29 @@
BEGIN;
ALTER TABLE project_flock_kandangs
ADD COLUMN IF NOT EXISTS period INT;
UPDATE project_flock_kandangs pfk
SET period = pf.period
FROM project_flocks pf
WHERE pfk.project_flock_id = pf.id
AND (pfk.period IS NULL OR pfk.period = 0)
AND pf.period IS NOT NULL;
ALTER TABLE project_flock_kandangs
ALTER COLUMN period SET DEFAULT 0;
UPDATE project_flock_kandangs
SET period = 0
WHERE period IS NULL;
ALTER TABLE project_flock_kandangs
ALTER COLUMN period SET NOT NULL;
-- Drop period from project_flocks as the source of truth
DROP INDEX IF EXISTS project_flocks_base_period_unique;
ALTER TABLE project_flocks
DROP COLUMN IF EXISTS period;
COMMIT;
@@ -0,0 +1,11 @@
-- Add back timestamp columns to marketing_products table
ALTER TABLE marketing_products
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;
-- Add back timestamp columns to marketing_delivery_products table
ALTER TABLE marketing_delivery_products
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;
@@ -0,0 +1,27 @@
-- Drop timestamp columns from marketing_products table if it exists
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'marketing_products' AND column_name = 'created_at') THEN
ALTER TABLE marketing_products DROP COLUMN created_at;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'marketing_products' AND column_name = 'updated_at') THEN
ALTER TABLE marketing_products DROP COLUMN updated_at;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'marketing_products' AND column_name = 'deleted_at') THEN
ALTER TABLE marketing_products DROP COLUMN deleted_at;
END IF;
END $$;
-- Drop timestamp columns from marketing_delivery_products table if it exists
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'marketing_delivery_products' AND column_name = 'created_at') THEN
ALTER TABLE marketing_delivery_products DROP COLUMN created_at;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'marketing_delivery_products' AND column_name = 'updated_at') THEN
ALTER TABLE marketing_delivery_products DROP COLUMN updated_at;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'marketing_delivery_products' AND column_name = 'deleted_at') THEN
ALTER TABLE marketing_delivery_products DROP COLUMN deleted_at;
END IF;
END $$;
+48 -45
View File
@@ -82,7 +82,7 @@ func Run(db *gorm.DB) error {
return err
}
if err := seedTransferStock(tx, adminID); err != nil {
if err := seedTransferStock(tx); err != nil {
return err
}
fmt.Println("✅ Master data seeding completed")
@@ -235,13 +235,14 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
seeds := []struct {
Name string
Status utils.KandangStatus
Capacity float64
Location string
PicKey string
}{
{Name: "Singaparna 1", Status: utils.KandangStatusNonActive, Location: "Singaparna", PicKey: "admin"},
{Name: "Singaparna 2", Status: utils.KandangStatusNonActive, Location: "Singaparna", PicKey: "admin"},
{Name: "Cikaum 1", Status: utils.KandangStatusNonActive, Location: "Cikaum", PicKey: "admin"},
{Name: "Cikaum 2", Status: utils.KandangStatusNonActive, Location: "Cikaum", PicKey: "admin"},
{Name: "Singaparna 1", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Singaparna", PicKey: "admin"},
{Name: "Singaparna 2", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Singaparna", PicKey: "admin"},
{Name: "Cikaum 1", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Cikaum", PicKey: "admin"},
{Name: "Cikaum 2", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Cikaum", PicKey: "admin"},
}
result := make(map[string]uint, len(seeds))
@@ -571,52 +572,54 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
Flags: []utils.FlagType{utils.FlagDOC},
},
{
Name: "Ayam Afkir",
Brand: "-",
Sku: "1",
Name: "Ayam Pullet",
Brand: "MBU Pullet",
Sku: "PLT0001",
Uom: "Ekor",
Category: "Day Old Chick",
Price: 1,
Category: "Pullet",
Price: 15000,
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
Flags: []utils.FlagType{utils.FlagPullet},
},
{
Name: "Ayam Mati",
Brand: "-",
Sku: "2",
Uom: "Ekor",
Category: "Day Old Chick",
Price: 1,
Name: "Ayam Afkir",
Brand: "-",
Sku: "1",
Uom: "Ekor",
Category: "Day Old Chick",
Price: 1,
},
{
Name: "Ayam Culling",
Brand: "-",
Sku: "3",
Uom: "Ekor",
Category: "Day Old Chick",
Price: 1,
Name: "Ayam Mati",
Brand: "-",
Sku: "2",
Uom: "Ekor",
Category: "Day Old Chick",
Price: 1,
},
{
Name: "Telur Konsumsi Baik",
Brand: "-",
Sku: "4",
Uom: "Unit",
Category: "Telur",
Price: 1,
Name: "Ayam Culling",
Brand: "-",
Sku: "3",
Uom: "Ekor",
Category: "Day Old Chick",
Price: 1,
},
{
Name: "Telur Pecah",
Brand: "-",
Sku: "5",
Uom: "Unit",
Category: "Telur",
Price: 1,
Name: "Telur Konsumsi Baik",
Brand: "-",
Sku: "4",
Uom: "Unit",
Category: "Telur",
Price: 1,
},
{
Name: "Telur Pecah",
Brand: "-",
Sku: "5",
Uom: "Unit",
Category: "Telur",
Price: 1,
},
{
Name: "281 SPECIAL STARTER",
@@ -689,7 +692,7 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
var existing entity.ProductSupplier
err := tx.Where("product_id = ? AND supplier_id = ?", product.Id, supplierID).First(&existing).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
link := entity.ProductSupplier{ProductID: product.Id, SupplierID: supplierID}
link := entity.ProductSupplier{ProductId: product.Id, SupplierId: supplierID}
if err := tx.Create(&link).Error; err != nil {
return err
}
@@ -762,7 +765,7 @@ func seedNonstocks(tx *gorm.DB, createdBy uint, uoms map[string]uint, suppliers
var existing entity.NonstockSupplier
err := tx.Where("nonstock_id = ? AND supplier_id = ?", nonstock.Id, supplierID).First(&existing).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
link := entity.NonstockSupplier{NonstockID: nonstock.Id, SupplierID: supplierID}
link := entity.NonstockSupplier{NonstockId: nonstock.Id, SupplierId: supplierID}
if err := tx.Create(&link).Error; err != nil {
return err
}
@@ -926,7 +929,7 @@ func seedProductWarehouse(tx *gorm.DB, createdBy uint) error {
return nil
}
func seedTransferStock(tx *gorm.DB, createdBy uint) error {
func seedTransferStock(tx *gorm.DB) error {
transfer := entity.StockTransfer{
FromWarehouseId: 1,
+32
View File
@@ -0,0 +1,32 @@
package entities
import (
"database/sql"
"time"
"gorm.io/gorm"
)
type Expense struct {
Id uint64 `gorm:"primaryKey;autoIncrement"`
ReferenceNumber string `gorm:"type:varchar(50);uniqueIndex"`
SupplierId uint64 `gorm:""`
Category string `gorm:"type:varchar(50);not null"`
PoNumber string `gorm:"type:varchar(50)"`
DocumentPath sql.NullString `gorm:"type:json"` // Dokumen pengajuan
RealizationDocumentPath sql.NullString `gorm:"type:json;column:realization_document_path"` // Dokumen realisasi
RealizationDate time.Time `gorm:"type:date;column:realization_date"` // Tanggal realisasi
ExpenseDate time.Time `gorm:"type:date;not null"`
GrandTotal float64 `gorm:"type:numeric(15,3);default:0"`
Note string `gorm:"type:text"`
CreatedBy uint64 `gorm:""`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
// Relations
Supplier *Supplier `gorm:"foreignKey:SupplierId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
Nonstocks []ExpenseNonstock `gorm:"foreignKey:ExpenseId;references:Id"`
LatestApproval *Approval `gorm:"-" json:"latest_approval,omitempty"`
}
+20
View File
@@ -0,0 +1,20 @@
package entities
type ExpenseNonstock struct {
Id uint64 `gorm:"primaryKey;autoIncrement"`
ExpenseId *uint64 `gorm:""`
ProjectFlockKandangId *uint64 `gorm:""`
KandangId *uint64 `gorm:""`
NonstockId *uint64 `gorm:""`
Qty float64 `gorm:"type:numeric(15,3);not null"`
UnitPrice float64 `gorm:"type:numeric(15,3);not null"`
TotalPrice float64 `gorm:"type:numeric(15,3);not null"`
Note string `gorm:"type:text"`
// Relations
Expense *Expense `gorm:"foreignKey:ExpenseId;references:Id"`
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
Kandang *Kandang `gorm:"foreignKey:KandangId;references:Id"`
Nonstock *Nonstock `gorm:"foreignKey:NonstockId;references:Id"`
Realization *ExpenseRealization `gorm:"foreignKey:ExpenseNonstockId;references:Id"`
}
+20
View File
@@ -0,0 +1,20 @@
package entities
import (
"time"
)
type ExpenseRealization struct {
Id uint64 `gorm:"primaryKey;autoIncrement"`
ExpenseNonstockId *uint64 `gorm:""`
RealizationQty float64 `gorm:"type:numeric(15,3);not null"`
RealizationUnitPrice float64 `gorm:"type:numeric(15,3);not null"`
RealizationTotalPrice float64 `gorm:"type:numeric(15,3);not null"`
RealizationDate time.Time `gorm:"type:date;not null"`
Note *string `gorm:"type:text"`
CreatedBy *uint64 `gorm:""`
// Relations
ExpenseNonstock *ExpenseNonstock `gorm:"foreignKey:ExpenseNonstockId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
}
+13 -12
View File
@@ -7,17 +7,18 @@ import (
)
type Kandang struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:kandangs_name_unique,where:deleted_at IS NULL"`
Status string `gorm:"type:varchar(50);not null"`
LocationId uint `gorm:"not null"`
PicId uint `gorm:"not null"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Location Location `gorm:"foreignKey:LocationId;references:Id"`
Pic User `gorm:"foreignKey:PicId;references:Id"`
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:kandangs_name_unique,where:deleted_at IS NULL"`
Status string `gorm:"type:varchar(50);not null"`
LocationId uint `gorm:"not null"`
Capacity float64 `gorm:"not null"`
PicId uint `gorm:"not null"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Location Location `gorm:"foreignKey:LocationId;references:Id"`
Pic User `gorm:"foreignKey:PicId;references:Id"`
ProjectFlockKandangs []ProjectFlockKandang `gorm:"foreignKey:KandangId;references:Id" json:"-"`
}
@@ -0,0 +1,22 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type LayingKandangTransfer struct {
Id uint `gorm:"primaryKey"`
KandangId uint
ProductWarehouseId uint
Qty float64 `gorm:"type:numeric(15,3)"`
LayingTransferId uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"`
Kandang *Kandang `gorm:"foreignKey:KandangId;references:Id"`
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
LayingTransfer *LayingTransfer `gorm:"foreignKey:LayingTransferId;references:Id"`
}
+29
View File
@@ -0,0 +1,29 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type LayingTransfer struct {
Id uint `gorm:"primaryKey"`
TransferNumber string `gorm:"uniqueIndex;not null"`
FromProjectFlockId uint `gorm:"not null"`
ToProjectFlockId uint `gorm:"not null"`
TransferDate time.Time `gorm:"type:date;not null"`
PendingUsageQty *float64 `gorm:"type:numeric(15,3)"`
UsageQty *float64 `gorm:"type:numeric(15,3)"`
Notes string `gorm:"type:text"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"`
FromProjectFlock *ProjectFlock `gorm:"foreignKey:FromProjectFlockId;references:Id"`
ToProjectFlock *ProjectFlock `gorm:"foreignKey:ToProjectFlockId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
Sources []LayingTransferSource `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
Targets []LayingTransferTarget `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
LatestApproval *Approval `gorm:"-" json:"-"`
}
@@ -0,0 +1,23 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type LayingTransferSource struct {
Id uint `gorm:"primaryKey"`
LayingTransferId uint `gorm:"index;not null"`
SourceProjectFlockKandangId uint `gorm:"not null"`
ProductWarehouseId *uint `gorm:""`
Qty float64 `gorm:"type:numeric(15,3);not null"`
Note string `gorm:"type:text"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"`
LayingTransfer *LayingTransfer `gorm:"foreignKey:LayingTransferId;references:Id"`
SourceProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:SourceProjectFlockKandangId;references:Id"`
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
}
@@ -0,0 +1,24 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type LayingTransferTarget struct {
Id uint `gorm:"primaryKey"`
LayingTransferId uint `gorm:"index;not null"`
TargetProjectFlockKandangId uint `gorm:"not null"`
Qty float64 `gorm:"type:numeric(15,3);not null"`
ProductWarehouseId *uint `gorm:""`
Note string `gorm:"type:text"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"`
LayingTransfer *LayingTransfer `gorm:"foreignKey:LayingTransferId;references:Id"`
TargetProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:TargetProjectFlockKandangId;references:Id"`
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
}
+3 -2
View File
@@ -16,6 +16,7 @@ type Location struct {
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Area Area `gorm:"foreignKey:AreaId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Area Area `gorm:"foreignKey:AreaId;references:Id"`
Kandangs []Kandang `gorm:"foreignKey:LocationId;references:Id"`
}
+27
View File
@@ -0,0 +1,27 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Marketing struct {
Id uint `gorm:"primaryKey;autoIncrement"`
SoNumber string `gorm:"uniqueIndex;not null"`
CustomerId uint `gorm:"not null"`
SoDocs string `gorm:"type:varchar(20)"`
SoDate time.Time `gorm:"type:date;not null"`
SalesPersonId uint `gorm:"not null"`
Notes string `gorm:"type:text"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Customer Customer `gorm:"foreignKey:CustomerId;references:Id"`
SalesPerson User `gorm:"foreignKey:SalesPersonId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Products []MarketingProduct `gorm:"foreignKey:MarketingId;references:Id"`
LatestApproval *Approval `gorm:"-" json:"latest_approval,omitempty"`
}
@@ -0,0 +1,19 @@
package entities
import (
"time"
)
type MarketingDeliveryProduct struct {
Id uint `gorm:"primaryKey;autoIncrement"`
MarketingProductId uint `gorm:"uniqueIndex;not null"`
Qty float64 `gorm:"type:numeric(15,3)"`
UnitPrice float64 `gorm:"type:numeric(15,3)"`
TotalWeight float64 `gorm:"type:numeric(15,3)"`
AvgWeight float64 `gorm:"type:numeric(15,3)"`
TotalPrice float64 `gorm:"type:numeric(15,3)"`
DeliveryDate *time.Time `gorm:"type:timestamptz"`
VehicleNumber string `gorm:"type:varchar(50)"`
MarketingProduct MarketingProduct `gorm:"foreignKey:MarketingProductId;references:Id"`
}
+16
View File
@@ -0,0 +1,16 @@
package entities
type MarketingProduct struct {
Id uint `gorm:"primaryKey;autoIncrement"`
MarketingId uint `gorm:"not null"`
ProductWarehouseId uint `gorm:"not null"`
Qty float64 `gorm:"type:numeric(15,3);not null"`
UnitPrice float64 `gorm:"type:numeric(15,3);not null"`
AvgWeight float64 `gorm:"type:numeric(15,3);not null"`
TotalWeight float64 `gorm:"type:numeric(15,3);not null"`
TotalPrice float64 `gorm:"type:numeric(15,3);not null"`
Marketing Marketing `gorm:"foreignKey:MarketingId;references:Id"`
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
DeliveryProduct *MarketingDeliveryProduct `gorm:"foreignKey:MarketingProductId;references:Id"`
}
+4 -4
View File
@@ -15,8 +15,8 @@ type Nonstock struct {
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Uom Uom `gorm:"foreignKey:UomId;references:Id"`
Suppliers []Supplier `gorm:"many2many:nonstock_suppliers;joinForeignKey:NonstockID;joinReferences:SupplierID"`
Flags []Flag `gorm:"polymorphic:Flagable;polymorphicValue:nonstocks"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Uom Uom `gorm:"foreignKey:UomId;references:Id"`
NonstockSuppliers []NonstockSupplier `gorm:"foreignKey:NonstockId;references:Id"`
Flags []Flag `gorm:"polymorphic:Flagable;polymorphicValue:nonstocks"`
}
+5 -2
View File
@@ -3,7 +3,10 @@ package entities
import "time"
type NonstockSupplier struct {
NonstockID uint `gorm:"primaryKey"`
SupplierID uint `gorm:"primaryKey"`
NonstockId uint `gorm:"not null"`
SupplierId uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
Nonstock Nonstock `gorm:"foreignKey:NonstockId;references:Id"`
Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"`
}
+5 -5
View File
@@ -22,9 +22,9 @@ type Product struct {
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Uom Uom `gorm:"foreignKey:UomId;references:Id"`
ProductCategory ProductCategory `gorm:"foreignKey:ProductCategoryId;references:Id"`
Suppliers []Supplier `gorm:"many2many:product_suppliers;joinForeignKey:ProductID;joinReferences:SupplierID"`
Flags []Flag `gorm:"polymorphic:Flagable;polymorphicValue:products"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Uom Uom `gorm:"foreignKey:UomId;references:Id"`
ProductCategory ProductCategory `gorm:"foreignKey:ProductCategoryId;references:Id"`
ProductSuppliers []ProductSupplier `gorm:"foreignKey:ProductId;references:Id"`
Flags []Flag `gorm:"polymorphic:Flagable;polymorphicValue:products"`
}
+5 -2
View File
@@ -3,7 +3,10 @@ package entities
import "time"
type ProductSupplier struct {
ProductID uint `gorm:"primaryKey"`
SupplierID uint `gorm:"primaryKey"`
ProductId uint `gorm:"not null"`
SupplierId uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
Product Product `gorm:"foreignKey:ProductId;references:Id"`
Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"`
}
+7 -4
View File
@@ -12,13 +12,16 @@ type ProjectChickin struct {
Id uint `gorm:"primaryKey"`
ProjectFlockKandangId uint `gorm:"not null;index;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
ChickInDate time.Time `gorm:"not null"`
Quantity float64 `gorm:"not null"`
Note string `gorm:"type:text"`
ProductWarehouseId uint `gorm:"not null"`
UsageQty float64 `gorm:"type:numeric(15,3);not null"`
PendingUsageQty float64 `gorm:"type:numeric(15,3);default:0"`
Notes string `gorm:"type:text"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
ProjectFlockKandang ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
}
+13 -12
View File
@@ -7,17 +7,18 @@ import (
)
type ProjectFlockPopulation struct {
Id uint `gorm:"primaryKey"`
ProjectFlockKandangId uint `gorm:"not null;index;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
InitialQuantity float64 `gorm:"type:numeric(15,3);not null"`
CurrentQuantity float64 `gorm:"type:numeric(15,3);not null"`
ReservedQuantity float64 `gorm:"type:numeric(15,3)"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"`
Id uint `gorm:"primaryKey"`
ProjectChickinId uint `gorm:"not null"`
ProductWarehouseId uint `gorm:"not null"`
TotalQty float64 `gorm:"type:numeric(15,3);not null"`
TotalUsedQty float64 `gorm:"type:numeric(15,3);not null"`
Notes string `gorm:"type:text"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"`
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
ProjectChickin *ProjectChickin `gorm:"foreignKey:ProjectChickinId;references:Id"`
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
}
-1
View File
@@ -13,7 +13,6 @@ type ProjectFlock struct {
Category string `gorm:"type:varchar(20);not null"`
FcrId uint `gorm:"not null"`
LocationId uint `gorm:"not null"`
Period int `gorm:"not null"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
+9 -8
View File
@@ -3,13 +3,14 @@ package entities
import "time"
type ProjectFlockKandang struct {
Id uint `gorm:"primaryKey"`
ProjectFlockId uint `gorm:"not null;index:idx_project_flock_kandangs_project;uniqueIndex:idx_project_flock_kandangs_unique"`
KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_unique"`
CreatedAt time.Time `gorm:"autoCreateTime"`
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
Id uint `gorm:"primaryKey"`
ProjectFlockId uint `gorm:"not null;index:idx_project_flock_kandangs_project;uniqueIndex:idx_project_flock_kandangs_unique"`
KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_unique"`
Period int `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
Chickins []ProjectChickin `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
LatestApproval *Approval `gorm:"-" json:"-"`
}
+27
View File
@@ -0,0 +1,27 @@
package entities
import (
"time"
)
type Purchase struct {
Id uint64 `gorm:"primaryKey;autoIncrement"`
PrNumber string `gorm:"not null"`
PoNumber *string
PoDate *time.Time
SupplierId uint64 `gorm:"not null"`
CreditTerm *int
DueDate *time.Time
GrandTotal float64 `gorm:"type:numeric(15,3);default:0"`
Notes *string
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt *time.Time `gorm:"index"`
CreatedBy uint64 `gorm:"not null"`
// Relations
Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"`
Items []PurchaseItem `gorm:"foreignKey:PurchaseId"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
LatestApproval *Approval `gorm:"-" json:"-"`
}
+28
View File
@@ -0,0 +1,28 @@
package entities
import (
"time"
)
type PurchaseItem struct {
Id uint64 `gorm:"primaryKey;autoIncrement"`
PurchaseId uint64 `gorm:"not null"`
ProductId uint64 `gorm:"not null"`
WarehouseId uint64 `gorm:"not null"`
ProductWarehouseId *uint64
ReceivedDate *time.Time
TravelNumber *string
TravelNumberDocs *string
VehicleNumber *string
SubQty float64 `gorm:"type:numeric(15,3);not null"`
TotalQty float64 `gorm:"type:numeric(15,3);default:0"`
TotalUsed float64 `gorm:"type:numeric(15,3);default:0"`
Price float64 `gorm:"type:numeric(15,3);default:0"`
TotalPrice float64 `gorm:"type:numeric(15,3);default:0"`
// Relations
Purchase *Purchase `gorm:"foreignKey:PurchaseId;references:Id"`
Product *Product `gorm:"foreignKey:ProductId;references:Id"`
Warehouse *Warehouse `gorm:"foreignKey:WarehouseId;references:Id"`
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
}
+3 -1
View File
@@ -26,5 +26,7 @@ type Supplier struct {
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
ProductSuppliers []ProductSupplier `gorm:"foreignKey:SupplierId;references:Id"`
NonstockSuppliers []NonstockSupplier `gorm:"foreignKey:SupplierId;references:Id"`
}
@@ -85,7 +85,7 @@ func (u *ApprovalController) GetAll(c *fiber.Ctx) error {
flat := dto.ToApprovalDTOs(records)
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.ApprovalBaseDTO]{
JSON(response.SuccessWithPaginate[dto.ApprovalRelationDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get All approvals successfully",
+19 -17
View File
@@ -10,23 +10,25 @@ import (
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
)
type ApprovalBaseDTO struct {
StepNumber uint16 `json:"step_number"`
StepName string `json:"step_name"`
Action *string `json:"action"`
Notes *string `json:"notes"`
ActionBy userDTO.UserBaseDTO `json:"action_by"`
ActionAt time.Time `json:"action_at"`
type ApprovalRelationDTO struct {
Id uint `json:"id"`
StepNumber uint16 `json:"step_number"`
StepName string `json:"step_name"`
Action *string `json:"action"`
Notes *string `json:"notes"`
ActionBy userDTO.UserRelationDTO `json:"action_by"`
ActionAt time.Time `json:"action_at"`
}
type ApprovalGroupDTO struct {
StepNumber uint16 `json:"step_number"`
StepName string `json:"step_name"`
Approvals []ApprovalBaseDTO `json:"approvals"`
StepNumber uint16 `json:"step_number"`
StepName string `json:"step_name"`
Approvals []ApprovalRelationDTO `json:"approvals"`
}
func ToApprovalDTO(e entity.Approval) ApprovalBaseDTO {
dto := ApprovalBaseDTO{
func ToApprovalDTO(e entity.Approval) ApprovalRelationDTO {
dto := ApprovalRelationDTO{
Id: e.Id,
Notes: e.Notes,
}
@@ -52,10 +54,10 @@ func ToApprovalDTO(e entity.Approval) ApprovalBaseDTO {
}
if e.ActionUser != nil && e.ActionUser.Id != 0 {
user := userDTO.ToUserBaseDTO(*e.ActionUser)
user := userDTO.ToUserRelationDTO(*e.ActionUser)
dto.ActionBy = user
} else if e.ActionBy != nil && *e.ActionBy != 0 {
dto.ActionBy = userDTO.UserBaseDTO{
dto.ActionBy = userDTO.UserRelationDTO{
Id: *e.ActionBy,
IdUser: int64(*e.ActionBy),
}
@@ -69,8 +71,8 @@ func ToApprovalDTO(e entity.Approval) ApprovalBaseDTO {
return dto
}
func ToApprovalDTOs(items []entity.Approval) []ApprovalBaseDTO {
result := make([]ApprovalBaseDTO, len(items))
func ToApprovalDTOs(items []entity.Approval) []ApprovalRelationDTO {
result := make([]ApprovalRelationDTO, len(items))
for i, item := range items {
result[i] = ToApprovalDTO(item)
}
@@ -84,7 +86,7 @@ func ToApprovalGroupDTOs(items []entity.Approval) []ApprovalGroupDTO {
type groupAccumulator struct {
StepName string
Approvals []ApprovalBaseDTO
Approvals []ApprovalRelationDTO
}
groups := make(map[uint16]*groupAccumulator)
@@ -82,6 +82,10 @@ func (r *ConstantRepositoryImpl) GetConstants() map[string]interface{} {
"LOKASI",
"KANDANG",
},
"stock_log": map[string][]string{
"log_types": []string{"TRANSFER", "ADJUSTMENT"},
"transaction_types": []string{"INCREASE", "DECREASE"},
},
"supplier_categories": []string{
"BOP",
"SAPRONAK",
@@ -0,0 +1,391 @@
package controller
import (
"encoding/json"
"fmt"
"math"
"strconv"
"strings"
"gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type ExpenseController struct {
ExpenseService service.ExpenseService
}
func NewExpenseController(expenseService service.ExpenseService) *ExpenseController {
return &ExpenseController{
ExpenseService: expenseService,
}
}
func (u *ExpenseController) 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.ExpenseService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.ExpenseListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all expenses successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: result,
})
}
func (u *ExpenseController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
result, err := u.ExpenseService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get expense successfully",
Data: result,
})
}
func (u *ExpenseController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create)
req.TransactionDate = c.FormValue("transaction_date")
req.Category = c.FormValue("category")
supplierID, err := strconv.ParseUint(c.FormValue("supplier_id"), 10, 64)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid supplier_id format")
}
req.SupplierID = supplierID
form, err := c.MultipartForm()
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form")
}
req.Documents = form.File["documents"]
costPerKandangJSON := c.FormValue("cost_per_kandangs")
if costPerKandangJSON != "" {
if err := json.Unmarshal([]byte(costPerKandangJSON), &req.CostPerKandangs); err != nil {
var singleCostPerKandang validation.CostPerKandang
if err := json.Unmarshal([]byte(costPerKandangJSON), &singleCostPerKandang); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid cost_per_kandangs JSON: %v", err))
}
if singleCostPerKandang.KandangID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Field KandangID is required")
}
req.CostPerKandangs = []validation.CostPerKandang{singleCostPerKandang}
} else {
for i, costPerKandang := range req.CostPerKandangs {
if costPerKandang.KandangID == 0 {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Field KandangID is required for cost_per_kandangs[%d]", i))
}
}
}
} else {
return fiber.NewError(fiber.StatusBadRequest, "Field cost_per_kandangs is required")
}
result, err := u.ExpenseService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create expense successfully",
Data: result,
})
}
func (u *ExpenseController) UpdateOne(c *fiber.Ctx) error {
req := new(validation.Update)
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
form, err := c.MultipartForm()
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form")
}
req.Documents = form.File["documents"]
if transactionDate := c.FormValue("transaction_date"); transactionDate != "" {
req.TransactionDate = &transactionDate
}
costPerKandangJSON := c.FormValue("cost_per_kandang")
if costPerKandangJSON != "" {
var costPerKandang []validation.CostPerKandang
if err := json.Unmarshal([]byte(costPerKandangJSON), &costPerKandang); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid cost_per_kandang JSON: %v", err))
}
for i, costPerKandang := range costPerKandang {
if costPerKandang.KandangID == 0 {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Field KandangID is required for cost_per_kandang[%d]", i))
}
}
req.CostPerKandang = &costPerKandang
}
result, err := u.ExpenseService.UpdateOne(c, req, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Update expense successfully",
Data: result,
})
}
func (u *ExpenseController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.ExpenseService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete expense successfully",
})
}
func (u *ExpenseController) Approval(c *fiber.Ctx) error {
req := new(validation.ApprovalRequest)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
path := c.Path()
approvalType := ""
if strings.Contains(path, "/approvals/manager") {
approvalType = "manager"
} else if strings.Contains(path, "/approvals/finance") {
approvalType = "finance"
} else {
return fiber.NewError(fiber.StatusBadRequest, "Invalid approval path")
}
results, err := u.ExpenseService.Approval(c, req, approvalType)
if err != nil {
return err
}
var (
data interface{}
message = "Submit expense approval successfully"
)
if len(results) == 1 {
data = results[0]
} else {
message = "Submit expense approvals successfully"
data = results
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: message,
Data: data,
})
}
func (u *ExpenseController) CreateRealization(c *fiber.Ctx) error {
expenseID := c.Params("id")
id, err := strconv.Atoi(expenseID)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid expense ID")
}
req := new(validation.CreateRealization)
form, err := c.MultipartForm()
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form")
}
req.Documents = form.File["documents"]
req.RealizationDate = c.FormValue("realization_date")
realizationsJSON := c.FormValue("realizations")
if realizationsJSON != "" {
if err := json.Unmarshal([]byte(realizationsJSON), &req.Realizations); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid realizations JSON: %v", err))
}
}
expense, err := u.ExpenseService.CreateRealization(c, uint(id), req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create realization successfully",
Data: expense,
})
}
func (u *ExpenseController) UpdateRealization(c *fiber.Ctx) error {
expenseID := c.Params("id")
id, err := strconv.Atoi(expenseID)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid expense ID")
}
var req validation.UpdateRealization
form, err := c.MultipartForm()
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form")
}
req.Documents = form.File["documents"]
req.RealizationDate = c.FormValue("realization_date")
realizationsJSON := c.FormValue("realizations")
if realizationsJSON != "" {
if err := json.Unmarshal([]byte(realizationsJSON), &req.Realizations); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid realizations JSON: %v", err))
}
}
expense, err := u.ExpenseService.UpdateRealization(c, uint(id), &req)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Update realization successfully",
Data: expense,
})
}
func (u *ExpenseController) CompleteExpense(c *fiber.Ctx) error {
expenseID := c.Params("id")
id, err := strconv.Atoi(expenseID)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid expense ID")
}
expense, err := u.ExpenseService.CompleteExpense(c, uint(id), nil)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Complete expense successfully",
Data: expense,
})
}
func (u *ExpenseController) DeleteDocument(c *fiber.Ctx) error {
expenseID, err := strconv.Atoi(c.Params("id"))
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid expense ID")
}
documentID, err := strconv.ParseUint(c.Params("documentId"), 10, 64)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid document ID")
}
if err := u.ExpenseService.DeleteDocument(c, uint(expenseID), documentID, false); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete document successfully",
})
}
func (u *ExpenseController) DeleteRealizationDocument(c *fiber.Ctx) error {
expenseID, err := strconv.Atoi(c.Params("id"))
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid expense ID")
}
documentID, err := strconv.ParseUint(c.Params("documentId"), 10, 64)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid document ID")
}
if err := u.ExpenseService.DeleteDocument(c, uint(expenseID), documentID, true); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete realization document successfully",
})
}
@@ -0,0 +1,321 @@
package dto
import (
"encoding/json"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/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"
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type ExpenseRelationDTO struct {
Id uint64 `json:"id"`
PoNumber string `json:"po_number"`
ExpenseDate time.Time `json:"expense_date"`
GrandTotal float64 `json:"grand_total"`
}
type ExpenseBaseDTO struct {
Id uint64 `json:"id"`
ReferenceNumber string `json:"reference_number"`
PoNumber string `json:"po_number"`
Category string `json:"category"`
Supplier *supplierDTO.SupplierRelationDTO `json:"supplier,omitempty"`
RealizationDate *time.Time `json:"realization_date,omitempty"`
ExpenseDate time.Time `json:"expense_date"`
GrandTotal float64 `json:"grand_total"`
Location *locationDTO.LocationRelationDTO `json:"location,omitempty"`
}
type ExpenseListDTO struct {
ExpenseBaseDTO
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval,omitempty"`
}
type ExpenseDetailDTO struct {
ExpenseBaseDTO
Documents []DocumentDTO `json:"documents,omitempty"`
RealizationDocs []DocumentDTO `json:"realization_docs,omitempty"`
Kandangs []KandangGroupDTO `json:"kandangs,omitempty"`
TotalPengajuan float64 `json:"total_pengajuan"`
TotalRealisasi float64 `json:"total_realisasi"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval,omitempty"`
}
type ExpenseNonstockDTO struct {
Id uint64 `json:"id"`
Qty float64 `json:"qty"`
UnitPrice float64 `json:"unit_price"`
TotalPrice float64 `json:"total_price"`
Note *string `json:"note,omitempty"`
Nonstock *nonstockDTO.NonstockRelationDTO `json:"nonstock,omitempty"`
}
type ExpenseRealizationDTO struct {
Id uint64 `json:"id"`
Qty float64 `json:"qty"`
UnitPrice float64 `json:"unit_price"`
TotalPrice float64 `json:"total_price"`
Note *string `json:"note,omitempty"`
Nonstock *nonstockDTO.NonstockRelationDTO `json:"nonstock,omitempty"`
}
type KandangGroupDTO struct {
Id uint64 `json:"id"`
KandangId uint64 `json:"kandang_id"`
Name string `json:"name,omitempty"`
Pengajuans []ExpenseNonstockDTO `json:"pengajuans,omitempty"`
Realisasi []ExpenseRealizationDTO `json:"realisasi,omitempty"`
}
type DocumentDTO struct {
ID uint64 `json:"id"`
Path string `json:"path"`
}
// === MAPPERS ===
func ToExpenseRelationDTO(e entity.Expense) ExpenseRelationDTO {
return ExpenseRelationDTO{
Id: e.Id,
PoNumber: e.PoNumber,
ExpenseDate: e.ExpenseDate,
GrandTotal: e.GrandTotal,
}
}
func ToExpenseBaseDTO(e *entity.Expense) ExpenseBaseDTO {
var location *locationDTO.LocationRelationDTO
var supplier *supplierDTO.SupplierRelationDTO
var realizationDate *time.Time
if !e.RealizationDate.IsZero() {
realizationDate = &e.RealizationDate
}
if len(e.Nonstocks) > 0 && e.Nonstocks[0].Kandang != nil {
if e.Nonstocks[0].Kandang.Location.Id != 0 {
mapped := locationDTO.ToLocationRelationDTO(e.Nonstocks[0].Kandang.Location)
location = &mapped
}
}
if e.Supplier != nil && e.Supplier.Id != 0 {
mapped := supplierDTO.ToSupplierRelationDTO(*e.Supplier)
supplier = &mapped
}
return ExpenseBaseDTO{
Id: e.Id,
ReferenceNumber: e.ReferenceNumber,
PoNumber: e.PoNumber,
Category: e.Category,
Supplier: supplier,
RealizationDate: realizationDate,
ExpenseDate: e.ExpenseDate,
GrandTotal: e.GrandTotal,
Location: location,
}
}
func ToExpenseListDTO(e entity.Expense) ExpenseListDTO {
var createdUser *userDTO.UserRelationDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserRelationDTO(*e.CreatedUser)
createdUser = &mapped
}
var latestApproval *approvalDTO.ApprovalRelationDTO
if e.LatestApproval != nil {
mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval)
latestApproval = &mapped
}
return ExpenseListDTO{
ExpenseBaseDTO: ToExpenseBaseDTO(&e),
CreatedUser: createdUser,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
LatestApproval: latestApproval,
}
}
func ToExpenseListDTOs(expenses []entity.Expense) []ExpenseListDTO {
result := make([]ExpenseListDTO, len(expenses))
for i, expense := range expenses {
result[i] = ToExpenseListDTO(expense)
}
return result
}
func ToExpenseDetailDTO(e *entity.Expense) ExpenseDetailDTO {
var documents []DocumentDTO
var realizationDocs []DocumentDTO
var createdUser *userDTO.UserRelationDTO
if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserRelationDTO(*e.CreatedUser)
createdUser = &mapped
}
var latestApproval *approvalDTO.ApprovalRelationDTO
if e.LatestApproval != nil {
mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval)
latestApproval = &mapped
}
var pengajuans []ExpenseNonstockDTO
var realisasi []ExpenseRealizationDTO
if e.DocumentPath.Valid && e.DocumentPath.String != "" {
json.Unmarshal([]byte(e.DocumentPath.String), &documents)
}
if e.RealizationDocumentPath.Valid && e.RealizationDocumentPath.String != "" {
json.Unmarshal([]byte(e.RealizationDocumentPath.String), &realizationDocs)
}
if len(e.Nonstocks) > 0 {
pengajuans = make([]ExpenseNonstockDTO, 0)
realisasi = make([]ExpenseRealizationDTO, 0)
for _, ns := range e.Nonstocks {
pengajuanDTO := ToExpenseNonstockDTO(ns)
pengajuans = append(pengajuans, pengajuanDTO)
if ns.Realization != nil && ns.Realization.Id != 0 {
var nonstock *nonstockDTO.NonstockRelationDTO
if ns.Nonstock != nil && ns.Nonstock.Id != 0 {
mapped := nonstockDTO.ToNonstockRelationDTO(*ns.Nonstock)
nonstock = &mapped
}
realisasiDTO := ExpenseRealizationDTO{
Id: ns.Realization.Id,
Qty: ns.Realization.RealizationQty,
UnitPrice: ns.Realization.RealizationUnitPrice,
TotalPrice: ns.Realization.RealizationTotalPrice,
Note: ns.Realization.Note,
Nonstock: nonstock,
}
realisasi = append(realisasi, realisasiDTO)
}
}
}
var totalPengajuan float64
for _, p := range pengajuans {
totalPengajuan += p.TotalPrice
}
var totalRealisasi float64
for _, r := range realisasi {
totalRealisasi += r.TotalPrice
}
kandangs := ToKandangGroupDTO(pengajuans, realisasi, e.Nonstocks)
return ExpenseDetailDTO{
ExpenseBaseDTO: ToExpenseBaseDTO(e),
Documents: documents,
RealizationDocs: realizationDocs,
CreatedUser: createdUser,
Kandangs: kandangs,
TotalPengajuan: totalPengajuan,
TotalRealisasi: totalRealisasi,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
LatestApproval: latestApproval,
}
}
func ToExpenseNonstockDTO(ns entity.ExpenseNonstock) ExpenseNonstockDTO {
var nonstock *nonstockDTO.NonstockRelationDTO
if ns.Nonstock != nil && ns.Nonstock.Id != 0 {
mapped := nonstockDTO.ToNonstockRelationDTO(*ns.Nonstock)
nonstock = &mapped
}
return ExpenseNonstockDTO{
Id: ns.Id,
Qty: ns.Qty,
UnitPrice: ns.UnitPrice,
TotalPrice: ns.TotalPrice,
Note: &ns.Note,
Nonstock: nonstock,
}
}
func ToKandangGroupDTO(pengajuans []ExpenseNonstockDTO, realisasi []ExpenseRealizationDTO, nonstocks []entity.ExpenseNonstock) []KandangGroupDTO {
kandangMap := make(map[uint64]*KandangGroupDTO)
for _, p := range pengajuans {
var kandangId uint64
var kandangName string
for _, ns := range nonstocks {
if ns.Id == p.Id && ns.Kandang != nil {
kandangId = uint64(ns.Kandang.Id)
kandangName = ns.Kandang.Name
break
}
}
if kandangId > 0 {
if kandangMap[kandangId] == nil {
kandangMap[kandangId] = &KandangGroupDTO{
Id: kandangId,
KandangId: kandangId,
Name: kandangName,
Pengajuans: []ExpenseNonstockDTO{},
Realisasi: []ExpenseRealizationDTO{},
}
}
kandangMap[kandangId].Pengajuans = append(kandangMap[kandangId].Pengajuans, p)
}
}
for _, r := range realisasi {
var kandangId uint64
var kandangName string
for _, ns := range nonstocks {
if ns.Realization != nil && ns.Realization.Id == r.Id && ns.Kandang != nil {
kandangId = uint64(ns.Kandang.Id)
kandangName = ns.Kandang.Name
break
}
}
if kandangId > 0 {
if kandangMap[kandangId] == nil {
kandangMap[kandangId] = &KandangGroupDTO{
Id: kandangId,
KandangId: kandangId,
Name: kandangName,
Pengajuans: []ExpenseNonstockDTO{},
Realisasi: []ExpenseRealizationDTO{},
}
}
kandangMap[kandangId].Realisasi = append(kandangMap[kandangId].Realisasi, r)
}
}
var kandangs []KandangGroupDTO
for _, k := range kandangMap {
kandangs = append(kandangs, *k)
}
return kandangs
}
+47
View File
@@ -0,0 +1,47 @@
package expenses
import (
"fmt"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
rExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
sExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
rNonstock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/repositories"
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
)
type ExpenseModule struct{}
func (ExpenseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
expenseRepo := rExpense.NewExpenseRepository(db)
userRepo := rUser.NewUserRepository(db)
supplierRepo := rSupplier.NewSupplierRepository(db)
nonstockRepo := rNonstock.NewNonstockRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db)
realizationRepo := rExpense.NewExpenseRealizationRepository(db)
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
approvalSvc := commonSvc.NewApprovalService(approvalRepo)
// Register workflow steps for EXPENSES approval
if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowExpense, utils.ExpenseApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register expense approval workflow: %v", err))
}
expenseService := sExpense.NewExpenseService(expenseRepo, supplierRepo, nonstockRepo, approvalSvc, realizationRepo, projectFlockKandangRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
ExpenseRoutes(router, userService, expenseService)
}
@@ -0,0 +1,51 @@
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 ExpenseRepository interface {
repository.BaseRepository[entity.Expense]
IdExists(ctx context.Context, id uint64) (bool, error)
GetNextSequence(ctx context.Context) (int, error)
GetWithSupplier(ctx context.Context, id uint64) (*entity.Expense, error)
}
type ExpenseRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Expense]
}
func NewExpenseRepository(db *gorm.DB) ExpenseRepository {
return &ExpenseRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Expense](db),
}
}
func (r *ExpenseRepositoryImpl) IdExists(ctx context.Context, id uint64) (bool, error) {
return repository.Exists[entity.Expense](ctx, r.DB(), uint(id))
}
func (r *ExpenseRepositoryImpl) GetNextSequence(ctx context.Context) (int, error) {
var sequence int
err := r.DB().Raw("SELECT nextval('expenses_ref_seq')").Scan(&sequence).Error
if err != nil {
return 0, err
}
return sequence, nil
}
func (r *ExpenseRepositoryImpl) GetWithSupplier(ctx context.Context, id uint64) (*entity.Expense, error) {
var expense entity.Expense
err := r.DB().WithContext(ctx).
Where("id = ?", id).
Preload("Supplier").
First(&expense).Error
if err != nil {
return nil, err
}
return &expense, nil
}
@@ -0,0 +1,55 @@
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 ExpenseNonstockRepository interface {
repository.BaseRepository[entity.ExpenseNonstock]
IdExists(ctx context.Context, id uint64) (bool, error)
GetByExpenseID(ctx context.Context, expenseID uint64, id uint64) (bool, error)
GetWithRelations(ctx context.Context, id uint64) (*entity.ExpenseNonstock, error)
}
type ExpenseNonstockRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.ExpenseNonstock]
}
func NewExpenseNonstockRepository(db *gorm.DB) ExpenseNonstockRepository {
return &ExpenseNonstockRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.ExpenseNonstock](db),
}
}
func (r *ExpenseNonstockRepositoryImpl) IdExists(ctx context.Context, id uint64) (bool, error) {
return repository.Exists[entity.ExpenseNonstock](ctx, r.DB(), uint(id))
}
func (r *ExpenseNonstockRepositoryImpl) GetByExpenseID(ctx context.Context, expenseID uint64, id uint64) (bool, error) {
var count int64
err := r.DB().WithContext(ctx).Model(&entity.ExpenseNonstock{}).
Where("id = ? AND expense_id = ?", id, expenseID).
Count(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}
func (r *ExpenseNonstockRepositoryImpl) GetWithRelations(ctx context.Context, id uint64) (*entity.ExpenseNonstock, error) {
var expenseNonstock entity.ExpenseNonstock
err := r.DB().WithContext(ctx).
Where("id = ?", id).
Preload("Nonstock", func(db *gorm.DB) *gorm.DB {
return db.Preload("Suppliers")
}).
First(&expenseNonstock).Error
if err != nil {
return nil, err
}
return &expenseNonstock, nil
}
@@ -0,0 +1,40 @@
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 ExpenseRealizationRepository interface {
repository.BaseRepository[entity.ExpenseRealization]
IdExists(ctx context.Context, id uint64) (bool, error)
GetByExpenseNonstockID(ctx context.Context, expenseNonstockID uint64) (*entity.ExpenseRealization, error)
}
type ExpenseRealizationRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.ExpenseRealization]
}
func NewExpenseRealizationRepository(db *gorm.DB) ExpenseRealizationRepository {
return &ExpenseRealizationRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.ExpenseRealization](db),
}
}
func (r *ExpenseRealizationRepositoryImpl) IdExists(ctx context.Context, id uint64) (bool, error) {
return repository.Exists[entity.ExpenseRealization](ctx, r.DB(), uint(id))
}
func (r *ExpenseRealizationRepositoryImpl) GetByExpenseNonstockID(ctx context.Context, expenseNonstockID uint64) (*entity.ExpenseRealization, error) {
var realization entity.ExpenseRealization
err := r.DB().WithContext(ctx).
Where("expense_nonstock_id = ?", expenseNonstockID).
First(&realization).Error
if err != nil {
return nil, err
}
return &realization, nil
}
+35
View File
@@ -0,0 +1,35 @@
package expenses
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/controllers"
expense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func ExpenseRoutes(v1 fiber.Router, u user.UserService, s expense.ExpenseService) {
ctrl := controller.NewExpenseController(s)
route := v1.Group("/expenses")
// route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
route.Post("/approvals/manager", ctrl.Approval)
route.Post("/approvals/finance", ctrl.Approval)
route.Post("/:id/realizations", ctrl.CreateRealization)
route.Patch("/:id/realizations", ctrl.UpdateRealization)
route.Post("/:id/complete", ctrl.CompleteExpense)
route.Delete("/:id/documents/:documentId", ctrl.DeleteDocument)
route.Delete("/:id/realization-documents/:documentId", ctrl.DeleteRealizationDocument)
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,64 @@
package validation
import (
"mime/multipart"
)
type Create struct {
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"`
Category string `form:"category" json:"category" validate:"required,oneof=BOP NON-BOP"`
SupplierID uint64 `form:"supplier_id" json:"supplier_id" validate:"required,gt=0"`
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
CostPerKandangs []CostPerKandang `form:"cost_per_kandangs" json:"cost_per_kandangs" validate:"required,min=1,dive"`
}
type CostPerKandang struct {
KandangID uint64 `form:"kandang_id" json:"kandang_id" validate:"required,gt=0"`
CostItems []CostItem `form:"cost_items" json:"cost_items" validate:"required,min=1,dive"`
}
type CostItem struct {
NonstockID uint64 `form:"nonstock_id" json:"nonstock_id" validate:"required,gt=0"`
Quantity float64 `form:"quantity" json:"quantity" validate:"required,gt=0"`
TotalCost float64 `form:"total_cost" json:"total_cost" validate:"required,gt=0"`
Notes string `form:"notes" json:"notes" validate:"required,max=500"`
}
type Update struct {
TransactionDate *string `form:"transaction_date" json:"transaction_date" validate:"omitempty,datetime=2006-01-02"`
CostPerKandang *[]CostPerKandang `form:"cost_per_kandang" json:"cost_per_kandang" validate:"omitempty,min=1,dive"`
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
Search string `query:"search" validate:"omitempty,max=50"`
}
type CreateRealization struct {
RealizationDate string `form:"realization_date" json:"realization_date" validate:"required,datetime=2006-01-02"`
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
Realizations []RealizationItem `form:"realizations" json:"realizations" validate:"required,min=1,dive"`
}
type UpdateRealization struct {
RealizationDate string `form:"realization_date" json:"realization_date" validate:"omitempty,datetime=2006-01-02"`
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
Realizations []RealizationItem `form:"realizations" json:"realizations" validate:"required,min=1,dive"`
}
type RealizationItem struct {
ExpenseNonstockID uint64 `form:"expense_nonstock_id" json:"expense_nonstock_id" validate:"required,gt=0"`
Qty float64 `form:"qty" json:"qty" validate:"required,gt=0"`
UnitPrice float64 `form:"unit_price" json:"unit_price" validate:"required,gt=0"`
TotalPrice float64 `form:"total_price" json:"total_price" validate:"required,gt=0"`
Notes *string `form:"notes" json:"notes" validate:"omitempty,max=500"`
}
type ApprovalRequest struct {
Action string `json:"action" form:"action" validate:"required,oneof=APPROVED REJECTED"`
ApprovableIds []uint `json:"approvable_ids" validate:"required,min=1,dive,gt=0"`
Notes *string `json:"notes" form:"notes"`
}
@@ -10,28 +10,28 @@ import (
// === DTO Structs ===
type ProductBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
SKU string `json:"sku"`
ProductCategory *productCategoryDTO.ProductCategoryBaseDTO `json:"product_category,omitempty"`
type ProductRelationDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
SKU string `json:"sku"`
ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"`
}
type WarehouseBaseDTO struct {
type WarehouseRelationDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type ProductWarehouseDTO struct {
Id uint `json:"id"`
ProductId uint `json:"product_id"`
WarehouseId uint `json:"warehouse_id"`
Quantity float64 `json:"quantity"`
Product *ProductBaseDTO `json:"product,omitempty"`
Warehouse *WarehouseBaseDTO `json:"warehouse,omitempty"`
Id uint `json:"id"`
ProductId uint `json:"product_id"`
WarehouseId uint `json:"warehouse_id"`
Quantity float64 `json:"quantity"`
Product *ProductRelationDTO `json:"product,omitempty"`
Warehouse *WarehouseRelationDTO `json:"warehouse,omitempty"`
}
type AdjustmentBaseDTO struct {
type AdjustmentRelationDTO struct {
Id uint `json:"id"`
TransactionType string `json:"transaction_type"`
Quantity float64 `json:"quantity"`
@@ -43,9 +43,9 @@ type AdjustmentBaseDTO struct {
}
type AdjustmentListDTO struct {
AdjustmentBaseDTO
CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"`
AdjustmentRelationDTO
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
type AdjustmentDetailDTO struct {
@@ -55,7 +55,7 @@ type AdjustmentDetailDTO struct {
// === Mapper Functions ===
func ToProductBaseDTO(e *entity.Product) *ProductBaseDTO {
func ToProductRelationDTO(e *entity.Product) *ProductRelationDTO {
if e == nil {
return nil
}
@@ -64,13 +64,13 @@ func ToProductBaseDTO(e *entity.Product) *ProductBaseDTO {
sku = *e.Sku
}
var category *productCategoryDTO.ProductCategoryBaseDTO
var category *productCategoryDTO.ProductCategoryRelationDTO
if e.ProductCategory.Id != 0 {
mapped := productCategoryDTO.ToProductCategoryBaseDTO(e.ProductCategory)
mapped := productCategoryDTO.ToProductCategoryRelationDTO(e.ProductCategory)
category = &mapped
}
return &ProductBaseDTO{
return &ProductRelationDTO{
Id: e.Id,
Name: e.Name,
SKU: sku,
@@ -78,11 +78,11 @@ func ToProductBaseDTO(e *entity.Product) *ProductBaseDTO {
}
}
func ToWarehouseBaseDTO(e *entity.Warehouse) *WarehouseBaseDTO {
func ToWarehouseRelationDTO(e *entity.Warehouse) *WarehouseRelationDTO {
if e == nil {
return nil
}
return &WarehouseBaseDTO{
return &WarehouseRelationDTO{
Id: e.Id,
Name: e.Name,
}
@@ -97,13 +97,13 @@ func ToProductWarehouseDTO(e *entity.ProductWarehouse) *ProductWarehouseDTO {
ProductId: e.ProductId,
WarehouseId: e.WarehouseId,
Quantity: e.Quantity,
Product: ToProductBaseDTO(&e.Product),
Warehouse: ToWarehouseBaseDTO(&e.Warehouse),
Product: ToProductRelationDTO(&e.Product),
Warehouse: ToWarehouseRelationDTO(&e.Warehouse),
}
}
func ToAdjustmentBaseDTO(e *entity.StockLog) AdjustmentBaseDTO {
return AdjustmentBaseDTO{
func ToAdjustmentRelationDTO(e *entity.StockLog) AdjustmentRelationDTO {
return AdjustmentRelationDTO{
Id: e.Id,
TransactionType: e.TransactionType,
Quantity: e.Quantity,
@@ -116,9 +116,9 @@ func ToAdjustmentBaseDTO(e *entity.StockLog) AdjustmentBaseDTO {
}
func ToAdjustmentListDTO(e *entity.StockLog) AdjustmentListDTO {
var createdUser *userDTO.UserBaseDTO
var createdUser *userDTO.UserRelationDTO
if e.CreatedUser != nil {
createdUser = &userDTO.UserBaseDTO{
createdUser = &userDTO.UserRelationDTO{
Id: e.CreatedUser.Id,
IdUser: e.CreatedUser.IdUser,
Email: e.CreatedUser.Email,
@@ -127,9 +127,9 @@ func ToAdjustmentListDTO(e *entity.StockLog) AdjustmentListDTO {
}
return AdjustmentListDTO{
AdjustmentBaseDTO: ToAdjustmentBaseDTO(e),
CreatedUser: createdUser,
CreatedAt: e.CreatedAt,
AdjustmentRelationDTO: ToAdjustmentRelationDTO(e),
CreatedUser: createdUser,
CreatedAt: e.CreatedAt,
}
}
@@ -0,0 +1,51 @@
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
}
@@ -29,6 +29,7 @@ func (u *ProductWarehouseController) GetAll(c *fiber.Ctx) error {
ProductId: uint(c.QueryInt("product_id", 0)),
WarehouseId: uint(c.QueryInt("warehouse_id", 0)),
Flags: c.Query("flags", ""),
KandangId: uint(c.QueryInt("kandang_id", 0)),
}
if query.Page < 1 || query.Limit < 1 {
@@ -4,27 +4,34 @@ import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
)
// === DTO Structs ===
type ProductWarehouseBaseDTO struct {
type ProductWarehouseRelationDTO struct {
Id uint `json:"id"`
ProductId uint `json:"product_id"`
WarehouseId uint `json:"warehouse_id"`
Quantity float64 `json:"quantity"`
}
type ProductWarehouseListDTO struct {
ProductWarehouseBaseDTO
Product *ProductBaseDTO `json:"product,omitempty"`
Warehouse *WarehouseBaseDTO `json:"warehouse,omitempty"`
CreatedUser *UserBaseDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
type ProductWarehousNestedDTO struct {
Id uint `json:"id"`
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
Warehouse *WarehouseRelationDTO `json:"warehouse,omitempty"`
}
type UserBaseDTO struct {
type ProductWarehouseListDTO struct {
ProductWarehouseRelationDTO
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
Warehouse *WarehouseRelationDTO `json:"warehouse,omitempty"`
CreatedUser *UserRelationDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type UserRelationDTO struct {
Id uint `json:"id"`
Username string `json:"username"`
}
@@ -34,40 +41,40 @@ type ProductWarehouseDetailDTO struct {
}
// Nested DTOs for relations
type ProductBaseDTO struct {
type ProductRelationDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Sku string `json:"sku"`
Flags []string `json:"flags"`
}
type WarehouseBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Kandang *KandangBaseDTO `json:"kandang,omitempty"`
Location *LocationBaseDTO `json:"location,omitempty"`
Area *AreaBaseDTO `json:"area,omitempty"`
type WarehouseRelationDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Kandang *KandangRelationDTO `json:"kandang,omitempty"`
Location *LocationRelationDTO `json:"location,omitempty"`
Area *AreaRelationDTO `json:"area,omitempty"`
}
type KandangBaseDTO struct {
type KandangRelationDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type LocationBaseDTO struct {
type LocationRelationDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type AreaBaseDTO struct {
type AreaRelationDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
// === Mapper Functions ===
func ToProductWarehouseBaseDTO(e entity.ProductWarehouse) ProductWarehouseBaseDTO {
return ProductWarehouseBaseDTO{
func ToProductWarehouseRelationDTO(e entity.ProductWarehouse) ProductWarehouseRelationDTO {
return ProductWarehouseRelationDTO{
Id: e.Id,
ProductId: e.ProductId, // Field yang benar dari entity
WarehouseId: e.WarehouseId, // Field yang benar dari entity
@@ -75,53 +82,55 @@ func ToProductWarehouseBaseDTO(e entity.ProductWarehouse) ProductWarehouseBaseDT
}
}
func ToProductWarehouseNestedDTO(e entity.ProductWarehouse) ProductWarehousNestedDTO {
product := productDTO.ToProductRelationDTO(e.Product)
return ProductWarehousNestedDTO{
Id: e.Id,
Product: &product,
Warehouse: &WarehouseRelationDTO{
Id: e.Warehouse.Id,
Name: e.Warehouse.Name,
},
}
}
func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDTO {
dto := ProductWarehouseListDTO{
ProductWarehouseBaseDTO: ToProductWarehouseBaseDTO(e),
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
ProductWarehouseRelationDTO: ToProductWarehouseRelationDTO(e),
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
}
// Map Product relation jika ada
if e.Product.Id != 0 {
product := ProductBaseDTO{
Id: e.Product.Id,
Name: e.Product.Name,
}
if e.Product.Sku != nil {
product.Sku = *e.Product.Sku
}
if len(e.Product.Flags) > 0 {
for _, f := range e.Product.Flags {
product.Flags = append(product.Flags, f.Name)
}
}
product := productDTO.ToProductRelationDTO(e.Product)
dto.Product = &product
}
// Map Warehouse relation jika ada
if e.Warehouse.Id != 0 {
warehouse := WarehouseBaseDTO{
warehouse := WarehouseRelationDTO{
Id: e.Warehouse.Id,
Name: e.Warehouse.Name,
}
// Map Kandang jika ada
if e.Warehouse.Kandang != nil && e.Warehouse.Kandang.Id != 0 {
warehouse.Kandang = &KandangBaseDTO{
warehouse.Kandang = &KandangRelationDTO{
Id: e.Warehouse.Kandang.Id,
Name: e.Warehouse.Kandang.Name,
}
}
// Map Location jika ada
if e.Warehouse.Location != nil && e.Warehouse.Location.Id != 0 {
warehouse.Location = &LocationBaseDTO{
warehouse.Location = &LocationRelationDTO{
Id: e.Warehouse.Location.Id,
Name: e.Warehouse.Location.Name,
}
}
if &e.Warehouse.Area != nil && e.Warehouse.Area.Id != 0 {
warehouse.Area = &AreaBaseDTO{
if e.Warehouse.Area.Id != 0 {
warehouse.Area = &AreaRelationDTO{
Id: e.Warehouse.Area.Id,
Name: e.Warehouse.Area.Name,
}
@@ -132,7 +141,7 @@ func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDT
// Map CreatedUser relation jika ada
if e.CreatedUser.Id != 0 {
user := UserBaseDTO{
user := UserRelationDTO{
Id: e.CreatedUser.Id,
Username: e.CreatedUser.Name,
}
@@ -156,22 +165,22 @@ func ToProductWarehouseDetailDTO(e entity.ProductWarehouse) ProductWarehouseDeta
}
}
func ToKandangBaseDTO(e entity.Kandang) KandangBaseDTO {
return KandangBaseDTO{
func ToKandangRelationDTO(e entity.Kandang) KandangRelationDTO {
return KandangRelationDTO{
Id: e.Id,
Name: e.Name,
}
}
func ToLocationBaseDTO(e entity.Location) LocationBaseDTO {
return LocationBaseDTO{
func ToLocationRelationDTO(e entity.Location) LocationRelationDTO {
return LocationRelationDTO{
Id: e.Id,
Name: e.Name,
}
}
func ToAreaBaseDTO(e entity.Area) AreaBaseDTO {
return AreaBaseDTO{
func ToAreaRelationDTO(e entity.Area) AreaRelationDTO {
return AreaRelationDTO{
Id: e.Id,
Name: e.Name,
}
@@ -7,6 +7,7 @@ import (
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
sProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/services"
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -17,8 +18,9 @@ type ProductWarehouseModule struct{}
func (ProductWarehouseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
userRepo := rUser.NewUserRepository(db)
kandangRepo := rKandang.NewKandangRepository(db)
productWarehouseService := sProductWarehouse.NewProductWarehouseService(productWarehouseRepo, validate)
productWarehouseService := sProductWarehouse.NewProductWarehouseService(productWarehouseRepo, validate, kandangRepo)
userService := sUser.NewUserService(userRepo, validate)
ProductWarehouseRoutes(router, userService, productWarehouseService)
@@ -2,6 +2,7 @@ package repository
import (
"context"
"errors"
"fmt"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
@@ -19,8 +20,14 @@ type ProductWarehouseRepository interface {
GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error)
GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error)
GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint, db *gorm.DB) (*entity.ProductWarehouse, error)
GetByFlagAndWarehouseID(ctx context.Context, flagName string, warehouseId uint) ([]entity.ProductWarehouse, error)
GetFirstProductByFlag(ctx context.Context, flagName string) (*entity.Product, error)
ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB
AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error
GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error)
IdExists(ctx context.Context, id uint) (bool, error)
CleanupEmpty(ctx context.Context, affected map[uint]struct{}) error
EnsureProductWarehouse(ctx context.Context, productID, warehouseID uint, createdBy uint64) (uint, error)
}
type ProductWarehouseRepositoryImpl struct {
@@ -44,6 +51,10 @@ func (r *ProductWarehouseRepositoryImpl) ExistsByID(ctx context.Context, id uint
return repository.Exists[entity.ProductWarehouse](ctx, r.DB(), id)
}
func (r *ProductWarehouseRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.ProductWarehouse](ctx, r.DB(), id)
}
func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExists(ctx context.Context, productId, warehouseId uint, excludeID *uint) (bool, error) {
var count int64
query := r.DB().WithContext(ctx).Model(&entity.ProductWarehouse{}).
@@ -78,14 +89,14 @@ func (r *ProductWarehouseRepositoryImpl) GetProductWarehouseByProductAndWarehous
func (r *ProductWarehouseRepositoryImpl) GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error) {
var productWarehouses []entity.ProductWarehouse
err := r.DB().WithContext(ctx).
Table("product_warehouses").
Select("product_warehouses.*").
q := r.DB().WithContext(ctx).Model(&entity.ProductWarehouse{}).
Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", categoryCode, warehouseId).
Order("product_warehouses.created_at DESC").
Find(&productWarehouses).Error
Order("product_warehouses.created_at DESC")
// preload relations so nested Product and Warehouse are populated
err := q.Preload("Product").Preload("Warehouse").Find(&productWarehouses).Error
if err != nil {
return nil, err
}
@@ -100,12 +111,12 @@ func (r *ProductWarehouseRepositoryImpl) GetLatestByCategoryCodeAndWarehouseID(c
}
fmt.Println(warehouseId)
err := query.WithContext(ctx).
Table("product_warehouses").
Select("product_warehouses.*").
Model(&entity.ProductWarehouse{}).
Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", categoryCode, warehouseId).
Order("product_warehouses.created_at DESC").
Preload("Product").Preload("Warehouse").
First(&productWarehouse).Error
if err != nil {
return nil, err
@@ -146,3 +157,123 @@ func (r *ProductWarehouseRepositoryImpl) AdjustQuantities(ctx context.Context, d
}
return nil
}
func (r *ProductWarehouseRepositoryImpl) CleanupEmpty(ctx context.Context, affected map[uint]struct{}) error {
if len(affected) == 0 {
return nil
}
ids := make([]uint, 0, len(affected))
for id := range affected {
ids = append(ids, id)
}
var emptyIDs []uint
if err := r.DB().WithContext(ctx).
Model(&entity.ProductWarehouse{}).
Where("id IN ? AND COALESCE(quantity,0) <= 0", ids).
Pluck("id", &emptyIDs).Error; err != nil {
return err
}
if len(emptyIDs) == 0 {
return nil
}
if err := r.DB().WithContext(ctx).
Model(&entity.PurchaseItem{}).
Where("product_warehouse_id IN ?", emptyIDs).
Update("product_warehouse_id", nil).Error; err != nil {
return err
}
if err := r.DB().WithContext(ctx).
Where("id IN ?", emptyIDs).
Delete(&entity.ProductWarehouse{}).Error; err != nil {
return err
}
return nil
}
func (r *ProductWarehouseRepositoryImpl) EnsureProductWarehouse(
ctx context.Context,
productID uint,
warehouseID uint,
createdBy uint64,
) (uint, error) {
record, err := r.GetProductWarehouseByProductAndWarehouseID(ctx, productID, warehouseID)
if err == nil {
return record.Id, nil
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
return 0, err
}
entity := &entity.ProductWarehouse{
ProductId: productID,
WarehouseId: warehouseID,
Quantity: 0,
CreatedBy: uint(createdBy),
}
if entity.CreatedBy == 0 {
entity.CreatedBy = 1
}
if err := r.CreateOne(ctx, entity, nil); err != nil {
return 0, err
}
return entity.Id, nil
}
func (r *ProductWarehouseRepositoryImpl) GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error) {
var productWarehouse entity.ProductWarehouse
err := r.DB().WithContext(ctx).
Preload("Product").
Preload("Warehouse").
Preload("Warehouse.Area").
Preload("Warehouse.Location").
First(&productWarehouse, id).Error
if err != nil {
return nil, err
}
return &productWarehouse, nil
}
func (r *ProductWarehouseRepositoryImpl) GetFirstProductByCategoryCode(ctx context.Context, categoryCode string) (*entity.Product, error) {
var product entity.Product
err := r.DB().WithContext(ctx).
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
Where("product_categories.code = ?", categoryCode).
First(&product).Error
if err != nil {
return nil, err
}
return &product, nil
}
func (r *ProductWarehouseRepositoryImpl) GetByFlagAndWarehouseID(ctx context.Context, flagName string, warehouseId uint) ([]entity.ProductWarehouse, error) {
var productWarehouses []entity.ProductWarehouse
err := r.DB().WithContext(ctx).Model(&entity.ProductWarehouse{}).
Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'").
Where("flags.name = ? AND product_warehouses.warehouse_id = ?", flagName, warehouseId).
Order("product_warehouses.created_at DESC").
Preload("Product").Preload("Warehouse").
Find(&productWarehouses).Error
if err != nil {
return nil, err
}
return productWarehouses, nil
}
func (r *ProductWarehouseRepositoryImpl) GetFirstProductByFlag(ctx context.Context, flagName string) (*entity.Product, error) {
var product entity.Product
err := r.DB().WithContext(ctx).
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'").
Where("flags.name = ?", flagName).
First(&product).Error
if err != nil {
return nil, err
}
return &product, nil
}
@@ -6,6 +6,7 @@ import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/validations"
kandangrepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
@@ -20,16 +21,18 @@ type ProductWarehouseService interface {
}
type productWarehouseService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.ProductWarehouseRepository
Log *logrus.Logger
Validate *validator.Validate
Repository repository.ProductWarehouseRepository
KandangRepo kandangrepo.KandangRepository
}
func NewProductWarehouseService(repo repository.ProductWarehouseRepository, validate *validator.Validate) ProductWarehouseService {
func NewProductWarehouseService(repo repository.ProductWarehouseRepository, validate *validator.Validate, kandangRepo kandangrepo.KandangRepository) ProductWarehouseService {
return &productWarehouseService{
Log: utils.Log,
Validate: validate,
Repository: repo,
Log: utils.Log,
Validate: validate,
Repository: repo,
KandangRepo: kandangRepo,
}
}
@@ -69,6 +72,16 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query)
}
}
if params.KandangId > 0 {
isKandangExist, err := s.KandangRepo.IdExists(c.Context(), params.KandangId)
if err != nil {
return nil, 0, err
}
if !isKandangExist {
return nil, 0, fiber.NewError(fiber.StatusNotFound, "Kandang not found")
}
}
offset := (params.Page - 1) * params.Limit
cleanFlags := utils.ParseFlags(params.Flags)
@@ -80,6 +93,11 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query)
db = db.Where("product_id = ?", params.ProductId)
}
if params.KandangId != 0 {
db = db.Joins("JOIN warehouses ON product_warehouses.warehouse_id = warehouses.id").
Where("warehouses.kandang_id = ?", params.KandangId)
}
if params.WarehouseId != 0 {
db = db.Where("warehouse_id = ?", params.WarehouseId)
}
@@ -18,4 +18,5 @@ type Query struct {
ProductId uint `query:"product_id" validate:"omitempty,number,min=1"`
WarehouseId uint `query:"warehouse_id" validate:"omitempty,number,min=1"`
Flags string `query:"flags" validate:"omitempty"`
KandangId uint `query:"kandang_id" validate:"omitempty,number,min=1"`
}
@@ -9,7 +9,7 @@ import (
// === DTO Structs ===
type TransferBaseDTO struct {
type TransferRelationDTO struct {
Id uint64 `json:"id"`
TransferReason string `json:"transfer_reason"`
TransferDate string `json:"transfer_date"`
@@ -51,12 +51,12 @@ type WarehouseDetailDTO struct {
}
type TransferListDTO struct {
TransferBaseDTO
CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Details []TransferDetailItemDTO `json:"details"`
Deliveries []TransferDeliveryDTO `json:"deliveries"`
TransferRelationDTO
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Details []TransferDetailItemDTO `json:"details"`
Deliveries []TransferDeliveryDTO `json:"deliveries"`
}
type TransferDetailDTO struct {
@@ -93,7 +93,7 @@ type TransferDeliveryItemDTO struct {
// === Mapper Functions ===
func ToTransferBaseDTO(e entity.StockTransfer) TransferBaseDTO {
func ToTransferRelationDTO(e entity.StockTransfer) TransferRelationDTO {
var sourceWarehouse *WarehouseDetailDTO
if e.FromWarehouse != nil && e.FromWarehouse.Id != 0 {
@@ -103,7 +103,7 @@ func ToTransferBaseDTO(e entity.StockTransfer) TransferBaseDTO {
if e.ToWarehouse != nil && e.ToWarehouse.Id != 0 {
destinationWarehouse = toWarehouseDetailDTO(e.ToWarehouse)
}
return TransferBaseDTO{
return TransferRelationDTO{
Id: e.Id,
TransferReason: e.Reason,
TransferDate: e.CreatedAt.Format("2006-01-02"),
@@ -145,9 +145,9 @@ func toWarehouseDetailDTO(w *entity.Warehouse) *WarehouseDetailDTO {
}
func ToTransferListDTO(e entity.StockTransfer) TransferListDTO {
var createdUser *userDTO.UserBaseDTO
var createdUser *userDTO.UserRelationDTO
if e.CreatedUser != nil {
mapped := userDTO.ToUserBaseDTO(*e.CreatedUser)
mapped := userDTO.ToUserRelationDTO(*e.CreatedUser)
createdUser = &mapped
}
// Map details
@@ -190,12 +190,12 @@ func ToTransferListDTO(e entity.StockTransfer) TransferListDTO {
})
}
return TransferListDTO{
TransferBaseDTO: ToTransferBaseDTO(e),
CreatedUser: createdUser,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
Details: details,
Deliveries: deliveries,
TransferRelationDTO: ToTransferRelationDTO(e),
CreatedUser: createdUser,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
Details: details,
Deliveries: deliveries,
}
}
@@ -0,0 +1,124 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type DeliveryOrdersController struct {
DeliveryOrdersService service.DeliveryOrdersService
}
func NewDeliveryOrdersController(deliveryOrdersService service.DeliveryOrdersService) *DeliveryOrdersController {
return &DeliveryOrdersController{
DeliveryOrdersService: deliveryOrdersService,
}
}
func (u *DeliveryOrdersController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
MarketingId: uint(c.QueryInt("marketing_id", 0)),
}
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.DeliveryOrdersService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.MarketingListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all deliveryOrderss successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: result,
})
}
func (u *DeliveryOrdersController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
result, err := u.DeliveryOrdersService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get deliveryOrders successfully",
Data: *result,
})
}
func (u *DeliveryOrdersController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.DeliveryOrdersService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create delivery products successfully",
Data: result,
})
}
func (u *DeliveryOrdersController) UpdateOne(c *fiber.Ctx) error {
req := new(validation.Update)
param := c.Params("id")
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.DeliveryOrdersService.UpdateOne(c, req, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Update deliveryOrders successfully",
Data: result,
})
}
@@ -0,0 +1,336 @@
package dto
import (
"fmt"
"sort"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
productwarehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/dto"
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
type MarketingRelationDTO struct {
Id uint `json:"id"`
SoNumber string `json:"so_number"`
SoDate time.Time `json:"so_date"`
Notes string `json:"notes,omitempty"`
}
type MarketingListDTO struct {
MarketingRelationDTO
Customer customerDTO.CustomerRelationDTO `json:"customer"`
SalesPerson userDTO.UserRelationDTO `json:"sales_person"`
SoDocs string `json:"so_docs"`
SalesOrder []MarketingProductDTO `json:"sales_order"`
CreatedUser userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
LatestApproval approvalDTO.ApprovalRelationDTO `json:"latest_approval"`
}
type MarketingDetailDTO struct {
MarketingRelationDTO
Customer customerDTO.CustomerRelationDTO `json:"customer"`
SalesPerson userDTO.UserRelationDTO `json:"sales_person"`
SoDocs string `json:"so_docs"`
SalesOrder []MarketingProductDTO `json:"sales_order"`
DeliveryOrder []DeliveryGroupDTO `json:"delivery_order"`
CreatedUser userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
LatestApproval approvalDTO.ApprovalRelationDTO `json:"latest_approval"`
}
type MarketingDeliveryProductDTO struct {
Id uint `json:"id"`
MarketingProductId uint `json:"marketing_product_id"`
Qty float64 `json:"qty"`
UnitPrice float64 `json:"unit_price"`
TotalWeight float64 `json:"total_weight"`
AvgWeight float64 `json:"avg_weight"`
TotalPrice float64 `json:"total_price"`
DeliveryDate *time.Time `json:"delivery_date"`
VehicleNumber string `json:"vehicle_number"`
ProductWarehouse *productwarehouseDTO.ProductWarehousNestedDTO `json:"product_warehouse,omitempty"`
}
type DeliveryItemDTO struct {
ProductWarehouse *productwarehouseDTO.ProductWarehousNestedDTO `json:"product_warehouse"`
Qty float64 `json:"qty"`
UnitPrice float64 `json:"unit_price"`
TotalWeight float64 `json:"total_weight"`
AvgWeight float64 `json:"avg_weight"`
TotalPrice float64 `json:"total_price"`
VehicleNumber string `json:"vehicle_number"`
}
type DeliveryGroupDTO struct {
DoNumber string `json:"do_number"`
DeliveryDate *time.Time `json:"delivery_date"`
Warehouse *productwarehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"`
Deliveries []DeliveryItemDTO `json:"deliveries"`
}
type MarketingProductDTO struct {
Id uint `json:"id"`
MarketingId uint `json:"marketing_id"`
ProductWarehouseId uint `json:"product_warehouse_id"`
Qty float64 `json:"qty"`
UnitPrice float64 `json:"unit_price"`
AvgWeight float64 `json:"avg_weight"`
TotalWeight float64 `json:"total_weight"`
TotalPrice float64 `json:"total_price"`
ProductWarehouse *productwarehouseDTO.ProductWarehousNestedDTO `json:"product_warehouse,omitempty"`
VehicleNumber string `json:"vehicle_number,omitempty"`
}
func ToMarketingRelationDTO(marketing *entity.Marketing) MarketingRelationDTO {
return MarketingRelationDTO{
Id: marketing.Id,
SoNumber: marketing.SoNumber,
SoDate: marketing.SoDate,
Notes: marketing.Notes,
}
}
func ToMarketingProductDTO(e entity.MarketingProduct) MarketingProductDTO {
var productWarehouse *productwarehouseDTO.ProductWarehousNestedDTO
if e.ProductWarehouse.Id != 0 {
mapped := productwarehouseDTO.ToProductWarehouseNestedDTO(e.ProductWarehouse)
productWarehouse = &mapped
}
return MarketingProductDTO{
Id: e.Id,
MarketingId: e.MarketingId,
ProductWarehouseId: e.ProductWarehouseId,
Qty: e.Qty,
UnitPrice: e.UnitPrice,
AvgWeight: e.AvgWeight,
TotalWeight: e.TotalWeight,
TotalPrice: e.TotalPrice,
ProductWarehouse: productWarehouse,
VehicleNumber: getVehicleNumber(e),
}
}
func ToMarketingDeliveryProductDTO(e entity.MarketingDeliveryProduct) MarketingDeliveryProductDTO {
return MarketingDeliveryProductDTO{
Id: e.Id,
MarketingProductId: e.MarketingProductId,
Qty: e.Qty,
UnitPrice: e.UnitPrice,
TotalWeight: e.TotalWeight,
AvgWeight: e.AvgWeight,
TotalPrice: e.TotalPrice,
DeliveryDate: e.DeliveryDate,
VehicleNumber: e.VehicleNumber,
}
}
func ToMarketingListDTO(marketing *entity.Marketing, deliveryProducts []entity.MarketingDeliveryProduct) MarketingListDTO {
var createdUser userDTO.UserRelationDTO
if marketing.CreatedUser.Id != 0 {
mapped := userDTO.ToUserRelationDTO(marketing.CreatedUser)
createdUser = mapped
}
var customer customerDTO.CustomerRelationDTO
if marketing.Customer.Id != 0 {
mapped := customerDTO.ToCustomerRelationDTO(marketing.Customer)
customer = mapped
}
var salesPerson userDTO.UserRelationDTO
if marketing.SalesPerson.Id != 0 {
mapped := userDTO.ToUserRelationDTO(marketing.SalesPerson)
salesPerson = mapped
}
var latestApproval approvalDTO.ApprovalRelationDTO
if marketing.LatestApproval != nil {
mapped := approvalDTO.ToApprovalDTO(*marketing.LatestApproval)
latestApproval = mapped
}
var salesOrderProducts []MarketingProductDTO
if len(marketing.Products) > 0 {
salesOrderProducts = make([]MarketingProductDTO, len(marketing.Products))
for i, product := range marketing.Products {
salesOrderProducts[i] = ToMarketingProductDTO(product)
}
}
return MarketingListDTO{
MarketingRelationDTO: ToMarketingRelationDTO(marketing),
Customer: customer,
SalesPerson: salesPerson,
SoDocs: marketing.SoDocs,
SalesOrder: salesOrderProducts,
CreatedUser: createdUser,
CreatedAt: marketing.CreatedAt,
UpdatedAt: marketing.UpdatedAt,
LatestApproval: latestApproval,
}
}
func ToMarketingDetailDTO(marketing *entity.Marketing, deliveryProducts []entity.MarketingDeliveryProduct) MarketingDetailDTO {
var createdUser userDTO.UserRelationDTO
if marketing.CreatedUser.Id != 0 {
mapped := userDTO.ToUserRelationDTO(marketing.CreatedUser)
createdUser = mapped
}
var customer customerDTO.CustomerRelationDTO
if marketing.Customer.Id != 0 {
mapped := customerDTO.ToCustomerRelationDTO(marketing.Customer)
customer = mapped
}
var salesPerson userDTO.UserRelationDTO
if marketing.SalesPerson.Id != 0 {
mapped := userDTO.ToUserRelationDTO(marketing.SalesPerson)
salesPerson = mapped
}
var salesOrderProducts []MarketingProductDTO
if len(marketing.Products) > 0 {
salesOrderProducts = make([]MarketingProductDTO, len(marketing.Products))
for i, product := range marketing.Products {
salesOrderProducts[i] = ToMarketingProductDTO(product)
}
}
var deliveryProductsDTOs []MarketingDeliveryProductDTO
if len(deliveryProducts) > 0 {
deliveryProductsDTOs = make([]MarketingDeliveryProductDTO, len(deliveryProducts))
for i, dp := range deliveryProducts {
deliveryProductsDTOs[i] = ToMarketingDeliveryProductDTO(dp)
}
deliveryProductsDTOs = enrichDeliveryProductDTOsWithWarehouse(deliveryProductsDTOs, marketing)
}
deliveryGroups := groupDeliveryProducts(deliveryProductsDTOs, marketing.SoNumber)
var latestApproval approvalDTO.ApprovalRelationDTO
if marketing.LatestApproval != nil {
mapped := approvalDTO.ToApprovalDTO(*marketing.LatestApproval)
latestApproval = mapped
}
return MarketingDetailDTO{
MarketingRelationDTO: ToMarketingRelationDTO(marketing),
SoDocs: marketing.SoDocs,
Customer: customer,
SalesPerson: salesPerson,
SalesOrder: salesOrderProducts,
DeliveryOrder: deliveryGroups,
CreatedUser: createdUser,
CreatedAt: marketing.CreatedAt,
UpdatedAt: marketing.UpdatedAt,
LatestApproval: latestApproval,
}
}
func ToMarketingListDTOs(marketings []entity.Marketing) []MarketingListDTO {
result := make([]MarketingListDTO, len(marketings))
for i, m := range marketings {
result[i] = ToMarketingListDTO(&m, []entity.MarketingDeliveryProduct{})
}
return result
}
func enrichDeliveryProductDTOsWithWarehouse(deliveryProductDTOs []MarketingDeliveryProductDTO, marketing *entity.Marketing) []MarketingDeliveryProductDTO {
if len(deliveryProductDTOs) == 0 || marketing == nil || len(marketing.Products) == 0 {
return deliveryProductDTOs
}
productMap := make(map[uint]*entity.MarketingProduct)
for i := range marketing.Products {
productMap[marketing.Products[i].Id] = &marketing.Products[i]
}
for i := range deliveryProductDTOs {
if product, exists := productMap[deliveryProductDTOs[i].MarketingProductId]; exists {
if product.ProductWarehouse.Id != 0 {
mapped := productwarehouseDTO.ToProductWarehouseNestedDTO(product.ProductWarehouse)
deliveryProductDTOs[i].ProductWarehouse = &mapped
}
}
}
return deliveryProductDTOs
}
func groupDeliveryProducts(products []MarketingDeliveryProductDTO, soNumber string) []DeliveryGroupDTO {
groupMap := make(map[string]*DeliveryGroupDTO)
for _, product := range products {
if product.DeliveryDate == nil {
continue
}
var warehouseId uint
var warehouseName string
if product.ProductWarehouse != nil {
warehouseId = product.ProductWarehouse.Warehouse.Id
warehouseName = product.ProductWarehouse.Warehouse.Name
}
key := fmt.Sprintf("%d_%s", warehouseId, product.DeliveryDate.Format("2006-01-02"))
group, exists := groupMap[key]
if !exists {
group = &DeliveryGroupDTO{
DeliveryDate: product.DeliveryDate,
Warehouse: &productwarehouseDTO.WarehouseRelationDTO{
Id: warehouseId,
Name: warehouseName,
},
Deliveries: []DeliveryItemDTO{},
}
groupMap[key] = group
}
deliveryItem := DeliveryItemDTO{
ProductWarehouse: product.ProductWarehouse,
Qty: product.Qty,
UnitPrice: product.UnitPrice,
TotalWeight: product.TotalWeight,
AvgWeight: product.AvgWeight,
TotalPrice: product.TotalPrice,
VehicleNumber: product.VehicleNumber,
}
group.Deliveries = append(group.Deliveries, deliveryItem)
}
var groups []DeliveryGroupDTO
for _, group := range groupMap {
groups = append(groups, *group)
}
sort.Slice(groups, func(i, j int) bool {
if groups[i].DeliveryDate == nil || groups[j].DeliveryDate == nil {
return false
}
return groups[i].DeliveryDate.Before(*groups[j].DeliveryDate)
})
for i := range groups {
if groups[i].DeliveryDate != nil {
dateStr := groups[i].DeliveryDate.Format("20060102")
groups[i].DoNumber = fmt.Sprintf("%s-%s-%d", soNumber, dateStr, groups[i].Warehouse.Id)
}
}
return groups
}
func getVehicleNumber(e entity.MarketingProduct) string {
if e.DeliveryProduct != nil && e.DeliveryProduct.VehicleNumber != "" {
return e.DeliveryProduct.VehicleNumber
}
return ""
}
@@ -0,0 +1,39 @@
package delivery_orderss
import (
"fmt"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
rMarketingDeliveryProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/marketing-delivery-products/repositories"
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"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
)
type DeliveryOrdersModule struct{}
func (DeliveryOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
marketingRepo := rMarketing.NewMarketingRepository(db)
marketingProductRepo := rMarketing.NewMarketingProductRepository(db)
marketingDeliveryProductRepo := rMarketingDeliveryProduct.NewMarketingDeliveryProductRepository(db)
userRepo := rUser.NewUserRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalSvc := commonSvc.NewApprovalService(approvalRepo)
// Register workflow steps for MARKETINGS approval
if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowMarketing, utils.MarketingApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register marketing approval workflow: %v", err))
}
deliveryOrdersService := sDeliveryOrders.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, approvalSvc, validate)
userService := sUser.NewUserService(userRepo, validate)
DeliveryOrdersRoutes(router, userService, deliveryOrdersService)
}
@@ -0,0 +1,30 @@
package delivery_orderss
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
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"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func DeliveryOrdersRoutes(v1 fiber.Router, u user.UserService, s deliveryOrders.DeliveryOrdersService) {
ctrl := controller.NewDeliveryOrdersController(s)
v1.Get("/", ctrl.GetAll)
v1.Get("/:id", ctrl.GetOne)
// Sisanya di group /delivery-orders
route := v1.Group("/delivery-orders")
// route.Get("/", m.Auth(u), ctrl.GetAll)
// 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.Post("/", ctrl.CreateOne)
route.Patch("/:id", ctrl.UpdateOne)
}

Some files were not shown because too many files have changed in this diff Show More