Compare commits

...

107 Commits

Author SHA1 Message Date
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
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
Hafizh A. Y. 98e0d56c64 Merge branch 'dev/ragil-before-sso' into 'development-before-sso'
[Feat/BE][US-76/TASK-122,133,121,120] Recording add create delete edit get days

See merge request mbugroup/lti-api!45
2025-11-03 08:09:43 +00:00
ragil adi prasetio f5c0441337 Fix:delete relation dto in flock in project_flock 2025-11-03 04:14:15 +00: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
ragilap 6ab6ee8070 feat/BE/US-76/US-78/US-79/TASK-112,120,133,121-Recording growing/TASK-187,189,202,190-Recording Laying/TASK-191,192,194,197,203-Grading Telur 2025-10-31 19:15:24 +07:00
ragilap 4f4c6d66d4 Merge branch 'development-before-sso' of https://gitlab.com/mbugroup/lti-api into dev/ragil-before-sso 2025-10-31 18:31:07 +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
ragilap 4b39f52d5a feat/BE/US-76/US-78/US-79/TASK-112,120,133,121-Recording growing/TASK-187,189,202,190-Recording Laying/TASK-191,192,194,197,203-Grading Telur 2025-10-31 16:04:22 +07:00
ragilap f869943573 feat/BE/US-76/US-78/US-79/TASK-112,120,133,121-Recording growing/TASK-187,189,202,190-Recording Laying/TASK-191,192,194,197,203-Grading Telur 2025-10-31 16:03:05 +07: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
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
ragilap 614da067f7 merge: ragil-before-sso from development-before-sso 2025-10-28 12:22:08 +07:00
Hafizh A. Y. 7f00a5a7a4 Merge branch 'feat/BE/US-75/chick-in-doc' into 'development-before-sso'
FIX/BE][US#75] : adjust some param and body request with FE needs, and fix minor bugs on Master data

See merge request mbugroup/lti-api!47
2025-10-28 03:26:50 +00:00
Hafizh A. Y. 9e08b9c44c Merge branch 'dev/teguh' into 'feat/BE/US-75/chick-in-doc'
FIX/BE][US#75] : adjust some param and body request with FE needs, and fix minor bugs on Master data

See merge request mbugroup/lti-api!46
2025-10-28 03:24:38 +00:00
aguhh18 cb4d27de7e Merge branch 'dev/ragil-before-sso' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2025-10-27 09:28:51 +07:00
ragilap 8ae614540f feat/BE/US-76/TASK-122,133,121,120 Recording add create delete edit 2025-10-26 15:27:19 +07:00
ragilap cedd5365d8 Merge branch 'development-before-sso' of https://gitlab.com/mbugroup/lti-api into dev/ragil-before-sso 2025-10-26 15:08:48 +07:00
ragil adi prasetio 63fb7d37f1 feat/BE/US-76/TASK-122,133,121,120 Recording 2025-10-24 08:50:27 +00:00
ragil adi prasetio 313276001c feat/BE/US-76/TASK-122,133,121,120 Recording 2025-10-24 08:45:05 +00:00
ragil adi prasetio 3065669d60 feat/BE/US-76/TASK-122,133,121,120 Recording add create delete edit 2025-10-24 08:18:16 +00:00
Hafizh A. Y. 29b5a2aa81 Merge branch 'dev/ragil-before-sso' into 'development-before-sso'
Dev/ragil before sso

See merge request mbugroup/lti-api!44
2025-10-24 07:40:19 +00:00
aguhh18 ef99a4a3c1 FIX[BE] : fix productwarehouses flags faram become multiple param 2025-10-24 13:29:37 +07:00
ragilap 3a162972ba feat/BE/US-76/TASK-122,133,121,120 Recording add create delete edit 2025-10-24 12:46:43 +07:00
aguhh18 7f2175a8cf Feat[Be-117]: Menambahkan note upda update chickin api 2025-10-24 11:16:12 +07:00
aguhh18 222d53aa37 FIX[BE] : use repository instead of raw query on service on productflock service 2025-10-24 10:25:05 +07:00
aguhh18 aeeb5a38c1 Feat[BE] : add avaibility DOC on lookup porject flock API, add note request json on chickin 2025-10-24 09:51:50 +07:00
aguhh18 79b3dd47b8 fix[BE]: validate query page/limit defaults and add exists helpers 2025-10-23 22:00:55 +07:00
ragilap 69ded31eb1 feat(BE): recording 2025-10-23 15:23:28 +07:00
Hafizh A. Y. df7cf86711 Merge branch 'feat/BE/US-75/chick-in-doc' into 'development-before-sso'
[FIX/BE][US#75/TASK#119] Align chickin seeder and DeleteOne with CreateOne behavior, create project lookup API

See merge request mbugroup/lti-api!38
2025-10-23 04:25:35 +00:00
Hafizh A. Y. 26825ab831 Merge branch 'dev/teguh' into 'feat/BE/US-75/chick-in-doc'
[FIX/BE][US#75/TASK#119] Align chickin seeder and DeleteOne with CreateOne behavior, create project lookup API

See merge request mbugroup/lti-api!37
2025-10-23 04:24:04 +00:00
aguhh18 1b02b660b9 feat(BE-119,117]) : create seeder for chikin details and fix busines logic on delete one chickin 2025-10-23 10:05:57 +07:00
aguhh18 4c6ead4272 Feat[117]: add lookup api in project flock to get pivot id 2025-10-23 09:05:03 +07:00
aguhh18 f33eb7fcc7 feat[BE-117]: create lookup API in project flock to get projectFlocks kandang id 2025-10-23 09:03:28 +07:00
aguhh18 00837e0da2 FIX[BE}: add stock logs for transfer stock 2025-10-22 22:20:40 +07:00
ragilap 346ae15314 feat(BE): recording 2025-10-22 22:20:08 +07:00
aguhh18 1dbf3ce93e Merge branch 'development-before-sso' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2025-10-22 18:36:56 +07:00
Hafizh A. Y. dae9fe9e01 Merge branch 'feat/BE/US-82/approval-workflow' into 'development-before-sso'
Feat/be/us 82/approval workflow

See merge request mbugroup/lti-api!35
2025-10-22 09:55:24 +00:00
Hafizh A. Y. b330fbfeb1 Merge branch 'dev/hafizh' into 'feat/BE/US-82/approval-workflow'
[FIX/BE] Req body approval and fix projectflock.dto

See merge request mbugroup/lti-api!34
2025-10-22 09:53:36 +00:00
Hafizh A. Y 56b1134872 fix(BE): req body approval and fix projectflock.dto 2025-10-22 16:49:45 +07:00
aguhh18 6dd45c3289 fix[BE]: adjust chickin if product warehouse have more than one DOC product 2025-10-22 14:56:32 +07:00
aguhh18 e2818b11f0 fix[BE] delete unused entity and repository 2025-10-22 08:29:00 +07:00
Hafizh A. Y. 3ed2c9027a Merge branch 'dev/hafizh' into 'development-before-sso'
fix(BE): preload in projectflock.service

See merge request mbugroup/lti-api!30
2025-10-21 09:00:09 +00:00
Hafizh A. Y 8d0bd3724d fix(BE): preload in projectflock.service 2025-10-21 15:59:00 +07:00
Hafizh A. Y. 95d6eb3445 Merge branch 'feat/BE/US-74/pengajuan-flock' into 'development-before-sso'
Feat/be/us 74/pengajuan flock

See merge request mbugroup/lti-api!29
2025-10-21 08:55:16 +00:00
Hafizh A. Y. 69f5ec8775 Merge branch 'dev/ragil-before-sso' into 'feat/BE/US-74/pengajuan-flock'
FIX(BE): category dto do not show

See merge request mbugroup/lti-api!28
2025-10-21 08:54:29 +00:00
ragilap 445789edfe FIX(BE): category dto do not show 2025-10-21 15:51:19 +07:00
Hafizh A. Y. 0dedd1149e Merge branch 'feat/BE/US-82/approval-workflow' into 'development-before-sso'
[FEAT/BE][US#82/TASK#99,100,101,108] approval_workflow, adjusment project_flocks, common, and migration

See merge request mbugroup/lti-api!27
2025-10-21 08:18:54 +00:00
Hafizh A. Y. 04ffa66a59 Merge branch 'dev/hafizh' into 'feat/BE/US-82/approval-workflow'
[FEAT/BE][US#82/TASK#99,100,101,108] approval_workflow, adjusment project_flocks, common, and migration

See merge request mbugroup/lti-api!26
2025-10-21 08:17:43 +00:00
Hafizh A. Y e4799fa2dd fix(BE): merge conflict 2025-10-21 15:11:04 +07:00
Hafizh A. Y 55b14f5fc7 feat(BE): approval_workflow, adjusment project_flocks, common, and migration 2025-10-21 14:15:53 +07:00
Hafizh A. Y. 13c04460f0 Merge branch 'feat/BE/US-75/chick-in-doc' into 'development-before-sso'
fix(BE): add missing product json in transfer get all & support flag param...

See merge request mbugroup/lti-api!25
2025-10-21 06:54:59 +00:00
Hafizh A. Y. 7f39ad8fed Merge branch 'feat/BE/US-74/pengajuan-flock' into 'development-before-sso'
FIX[BE]: name duplicate flock,projectflock category change,menerapkan dto...

See merge request mbugroup/lti-api!24
2025-10-21 06:54:27 +00:00
Hafizh A. Y. 0824f03a61 Merge branch 'dev/ragil-before-sso' into 'feat/BE/US-74/pengajuan-flock'
FIX[BE]: name duplicate flock,projectflock category change,menerapkan dto...

See merge request mbugroup/lti-api!22
2025-10-21 06:53:39 +00:00
Hafizh A. Y. a4ac431ed4 Merge branch 'dev/teguh' into 'feat/BE/US-75/chick-in-doc'
[FEAT/BE][US#90/TASK#90,115,116,117,119,135] Chickin schema, endpoint, seeding,

See merge request mbugroup/lti-api!23
2025-10-21 06:52:25 +00:00
aguhh18 b1b63d266a fix[BE]: menggunakan base dto dari dto utama entity ketimbang buat simple dto baru 2025-10-21 11:48:54 +07:00
aguhh18 1afbdea4ff fix[BE]: fix logic pengambilan quatity untuk chick in dan penggunaan helper common 2025-10-21 10:20:34 +07:00
aguhh18 542e503360 fix[BE]: change dummy document path on transfer create 2025-10-21 09:01:02 +07:00
ragilap 9b2b62429c FIX[BE]: name duplicate flock,projectflock category change,menerapkan dto seperti warehouse di projectflock 2025-10-20 22:49:30 +07:00
ragilap ee033b8fe6 FIX[BE]: name duplicate flock,projectflock category change,menerapkan dto seperti warehouse di projectflock 2025-10-20 16:39:16 +07:00
aguhh18 a1f579f616 feat(BE-119,135): add seeding and API documentation
- Implement project data seeding logic
- Add API documentation using Hoppscotch
2025-10-20 12:55:19 +07:00
aguhh18 748c959dbe FIX[BE]: fix json wrong json field name 2025-10-20 11:36:38 +07:00
aguhh18 7b99b39529 feat(BE-117): implement CRUD endpoints for project 2025-10-20 11:25:42 +07:00
Hafizh A. Y. c61bccb700 Merge branch 'feat/BE/US-74/pengajuan-flock' into 'development-before-sso'
FIX[BE]: if project flocs deleted kandangs reset to non_active and add filter...

See merge request mbugroup/lti-api!21
2025-10-20 03:34:37 +00:00
Hafizh A. Y. 81d6b2c6fc Merge branch 'dev/ragil-before-sso' into 'feat/BE/US-74/pengajuan-flock'
FIX(BE-74): if project flocs deleted kandangs reset to non_active, add filter get all project_flock by area,kandangs,period and location and make table pivot in project flocs - kandangs

See merge request mbugroup/lti-api!20
2025-10-20 03:33:49 +00:00
aguhh18 5c3787886b FIX[BE]: adjust response on proudctwarehouses 2025-10-20 08:45:31 +07:00
aguhh18 4218298234 Merge branch 'dev/ragil-before-sso' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2025-10-20 06:06:22 +07:00
aguhh18 83c3e61113 feat(BE-115,116,117): implement chickin CRUD, approve logic, and stock availabilit 2025-10-20 06:01:16 +07:00
ragilap f15e0d62e3 FIX[BE]: if project flocs deleted kandangs reset to non_active and add filter get all project_flock by area,kandangs,period and location 2025-10-19 23:24:56 +07:00
aguhh18 68a670a2bd feat(BE-116): add project chick in database schema 2025-10-18 16:30:13 +07:00
aguhh18 a45c20d2ff fix(BE): improve product and warehouse existence check in adjustment service 2025-10-17 20:43:31 +07:00
aguhh18 79700420d4 fix(BE): add missing product json in transfer get all & support flag param filter in product warehouses 2025-10-17 12:04:19 +07:00
Hafizh A. Y. c9b4b3008e Merge branch 'feat/BE/US-35/stock-transfer' into 'development-before-sso'
(BE-58,,59): extend db schema & build stock transfer api

See merge request mbugroup/lti-api!19
2025-10-17 03:35:43 +00:00
Hafizh A. Y. 47823963ae Merge branch 'feat/BE/US-74/pengajuan-flock' into 'development-before-sso'
Feat/be/us 74/pengajuan flock

See merge request mbugroup/lti-api!18
2025-10-17 03:33:57 +00:00
Hafizh A. Y. 151f66b4cc Merge branch 'dev/ragil-before-sso' into 'feat/BE/US-74/pengajuan-flock'
FIX[BE]: period and adjustment helper to function

See merge request mbugroup/lti-api!17
2025-10-17 03:32:01 +00:00
ragilap 8c0790627a FIX[BE]: period and adjustment helper to function 2025-10-16 16:44:26 +07:00
ragilap 62a1011a4b FIX[BE]: period and adjustment helper to function 2025-10-16 16:35:01 +07:00
Hafizh A. Y. b44322e448 Merge branch 'dev/teguh' into 'feat/BE/US-35/stock-transfer'
[FEAT/BE][US#35/TASK#58,59,60,61] Complete stock transfer API, DB schema, validation, and audit log

See merge request mbugroup/lti-api!15
2025-10-16 08:38:03 +00:00
Hafizh A. Y. 3a8a1318f5 Merge branch 'dev/ragil-before-sso' into 'feat/BE/US-74/pengajuan-flock'
[FEAT/BE][US#74/TASK#107,109,110,142,112] Complete Flock API, DB schema, validation

See merge request mbugroup/lti-api!16
2025-10-16 08:37:52 +00:00
ragilap 3ec05eb76f FIX[BE]: period without autoincrement 2025-10-16 15:30:36 +07:00
aguhh18 9f26d5c784 feat(BE): add product flags to product warehouse response 2025-10-16 12:51:41 +07:00
ragilap 6c7ab8a0f8 feat/BE/US-74/pengajuan-flock 2025-10-16 10:06:18 +07:00
aguhh18 f6f62246c6 feat(BE-59,60,61): build stock transfer API with validation and audit log 2025-10-16 07:37:10 +07:00
aguhh18 0ffb8a44f2 feat(BE-59,60,61): build stock transfer API with validation and audit log
- Implement CreateOne for stock transfer with multi-delivery and validation
- Preload warehouse, location, and area relations in transfer response
- Add audit log for transfer
- Improve transaction handling and error management
2025-10-15 22:26:06 +07:00
aguhh18 4107cf19ec feat(BE-59,60,61): build stock transfer API with validation and audit log
- Implement CreateOne for stock transfer with multi-delivery and validation
- Preload warehouse, location, and area relations in transfer response
- Add audit log for transfer
- Improve transaction handling and error management
2025-10-15 22:25:50 +07:00
aguhh18 d1b377ddac feat(BE-58,59,60,61): implement stock transfer API, validation, audit log, and schema update
- Build stock transfer API with nested details, deliveries, and items
- Extend DB schema for stock transfers
- Implement validation for transfer request and stock
- Prepare/implement transfer audit log structure
- Preload all relations for complete response
- Update DTOs for nested response
- Remove redundant root fields, use relation objects
2025-10-15 11:20:32 +07:00
aguhh18 9b016dc30a (BE-58,,59): extend db schema & build stock transfer api
- Extend DB schema for stock transfers
- Build stock transfer API (create,)
2025-10-14 22:16:50 +07:00
Hafizh A. Y. 7392d8a679 Merge branch 'feat/BE/US-34/stock-adjusment' into 'development-before-sso'
feat(BE-48): auto-create product_warehouse on stock adjustment & remove unused APIs

See merge request mbugroup/lti-api!14
2025-10-14 04:18:46 +00:00
Hafizh A. Y. 6628356958 Merge branch 'dev/teguh' into 'feat/BE/US-34/stock-adjusment'
[FEAT/BE][US#34/TASK#47,48,49,50] Inventory adjustment system

See merge request mbugroup/lti-api!13
2025-10-14 02:35:50 +00:00
aguhh18 5283aed996 feat(BE-48): auto-create product_warehouse on stock adjustment & remove unused APIs
- Change logic: automatically create product_warehouse if it does not exist during stock adjustment
- Remove unnecessary/unused API endpoints
- Ensure adjustment process continues even if product_warehouse was not previously available
2025-10-13 11:38:05 +07:00
aguhh18 ce28429efd feat(BE-50): add getOne endpoint for adjustment history 2025-10-13 09:36:38 +07:00
Hafizh A. Y. 05bf2f4fff Merge branch 'feat/BE/US-34/stock-adjusment' into 'development-before-sso'
[FEAT/BE][US#34/TASK#47,48,49,50] Inventory adjustment system

See merge request mbugroup/lti-api!12
2025-10-13 02:29:12 +00:00
Hafizh A. Y. a0a416c330 Merge branch 'dev/teguh' into 'feat/BE/US-34/stock-adjusment'
[FEAT/BE][US#34/TASK#47,48,49,50] Implement inventory adjustment system

See merge request mbugroup/lti-api!11
2025-10-10 09:32:24 +00:00
aguhh18 cd4c908334 refactor(BE-48): clean up ProductWarehouse entity structure
- Remove JSON tags from Product, Warehouse, and CreatedUser relations
- Keep GORM tags for database functionality
- Simplify entity definition for better maintainability
2025-10-10 13:52:50 +07:00
aguhh18 81cbb230f3 fix(BE-48): improve adjustment history filtering and fix pointer conversion
- Add search parameter to adjustment history API
- Fix JOIN query logic to avoid duplicate JOINs
- Use EXISTS subquery for cleaner product/warehouse filtering
- Fix pointer conversion issue in slice iteration
- Improve query performance and code readability
2025-10-10 12:36:11 +07:00
aguhh18 91b320d489 feat(BE-47,48,49,50): implement inventory adjustment system
- Extend DB schema with product_warehouses and stock_logs tables
- Implement stock adjustment API (increase/decrease operations)
- Add comprehensive validation for all adjustment operations
- Implement audit log system for each adjustment with history tracking
- Include transaction handling, DTOs, seeders, and proper error handling
- Add adjustment history API with pagination and filtering

TODO: Integration testing pending
2025-10-10 09:24:17 +07:00
aguhh18 a0bdc7b23c feat(BE-34): extend DB schema and update master data APIs [partial]
 DB Schema: product_warehouse entity and migration
 Master Data: added filter params to getall APIs
🚧 Pending: stock_logs implementation and adjustment APIs
2025-10-09 13:33:30 +07:00
198 changed files with 15046 additions and 283 deletions
Vendored
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -57,7 +57,7 @@ wait-db:
# Contoh: make migration-create_users_table
# ":" akan diubah ke "_" (biar aman untuk nama file)
migration-%:
@migrate create -ext sql -dir internal/database/migrations $(subst :,_,$*)
@migrate create -ext sql -dir $(MIGRATIONS_DIR) $(subst :,_,$*)
# --- Migration (apply via docker image 'migrate') ---
migrate-up: db-up wait-db
+1 -3
View File
@@ -4,6 +4,7 @@ go 1.23
require (
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
@@ -28,7 +29,6 @@ require (
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/glebarez/sqlite v1.11.0 // 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
@@ -47,7 +47,6 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect
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
@@ -76,7 +75,6 @@ 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
gorm.io/driver/sqlite v1.5.5 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
+3 -5
View File
@@ -51,6 +51,8 @@ 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=
@@ -69,8 +71,8 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU=
github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -88,8 +90,6 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
@@ -211,8 +211,6 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E=
gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE=
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=
@@ -0,0 +1,106 @@
package repository
import (
"context"
"errors"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type ApprovalRepository interface {
BaseRepository[entity.Approval]
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)
}
type approvalRepositoryImpl struct {
*BaseRepositoryImpl[entity.Approval]
}
func NewApprovalRepository(db *gorm.DB) ApprovalRepository {
return &approvalRepositoryImpl{
BaseRepositoryImpl: NewBaseRepository[entity.Approval](db),
}
}
func (r *approvalRepositoryImpl) FindByTarget(
ctx context.Context,
workflow string,
approvableID uint,
modifier func(*gorm.DB) *gorm.DB,
) ([]entity.Approval, error) {
var approvals []entity.Approval
q := r.DB().WithContext(ctx).Where("approvable_type = ? AND approvable_id = ?", workflow, approvableID)
if modifier != nil {
q = modifier(q)
}
if err := q.Order("action_at ASC").Find(&approvals).Error; err != nil {
return nil, err
}
return approvals, nil
}
func (r *approvalRepositoryImpl) LatestByTarget(
ctx context.Context,
workflow string,
approvableID uint,
modifier func(*gorm.DB) *gorm.DB,
) (*entity.Approval, error) {
var approval entity.Approval
q := r.DB().WithContext(ctx).
Where("approvable_type = ? AND approvable_id = ?", workflow, approvableID).
Order("action_at DESC")
if modifier != nil {
q = modifier(q)
}
if err := q.Limit(1).First(&approval).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &approval, nil
}
func (r *approvalRepositoryImpl) LatestByTargets(
ctx context.Context,
workflow string,
approvableIDs []uint,
modifier func(*gorm.DB) *gorm.DB,
) (map[uint]entity.Approval, error) {
if len(approvableIDs) == 0 {
return nil, nil
}
result := make(map[uint]entity.Approval, len(approvableIDs))
q := r.DB().WithContext(ctx).
Where("approvable_type = ? AND approvable_id IN ?", workflow, approvableIDs).
Order("action_at DESC")
if modifier != nil {
q = modifier(q)
}
var approvals []entity.Approval
if err := q.Find(&approvals).Error; err != nil {
return nil, err
}
for _, approval := range approvals {
if _, exists := result[approval.ApprovableId]; exists {
continue
}
result[approval.ApprovableId] = approval
}
return result, nil
}
@@ -2,6 +2,7 @@ package repository
import (
"context"
"fmt"
"gorm.io/gorm"
)
@@ -32,3 +33,21 @@ func ExistsByName[T any](ctx context.Context, db *gorm.DB, name string, excludeI
}
return count > 0, nil
}
func ExistsByField[T any](ctx context.Context, db *gorm.DB, field string, value any, excludeID *uint) (bool, error) {
if field == "" {
return false, fmt.Errorf("field is required")
}
var count int64
q := db.WithContext(ctx).
Model(new(T)).
Where(fmt.Sprintf("%s = ?", field), value).
Where("deleted_at IS NULL")
if excludeID != nil {
q = q.Where("id <> ?", *excludeID)
}
if err := q.Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
@@ -0,0 +1,234 @@
package service
import (
"context"
"strings"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
"gorm.io/gorm"
)
type ApprovalService interface {
RegisterWorkflowSteps(workflow approvalutils.ApprovalWorkflowKey, steps map[approvalutils.ApprovalStep]string) error
WorkflowSteps(workflow approvalutils.ApprovalWorkflowKey) map[approvalutils.ApprovalStep]string
WorkflowStepName(workflow approvalutils.ApprovalWorkflowKey, step approvalutils.ApprovalStep) (string, bool)
CreateApproval(ctx context.Context, workflow approvalutils.ApprovalWorkflowKey, approvableID uint, step approvalutils.ApprovalStep, action *entity.ApprovalAction, actorID uint, note *string) (*entity.Approval, error)
List(ctx context.Context, module string, approvableID *uint, page, limit int, search string) ([]entity.Approval, int64, error)
ListByTarget(ctx context.Context, workflow approvalutils.ApprovalWorkflowKey, approvableID uint, modifier func(*gorm.DB) *gorm.DB) ([]entity.Approval, error)
LatestByTarget(ctx context.Context, workflow approvalutils.ApprovalWorkflowKey, approvableID uint, modifier func(*gorm.DB) *gorm.DB) (*entity.Approval, error)
LatestByTargets(ctx context.Context, workflow approvalutils.ApprovalWorkflowKey, approvableIDs []uint, modifier func(*gorm.DB) *gorm.DB) (map[uint]*entity.Approval, error)
}
type approvalService struct {
repo commonRepo.ApprovalRepository
}
func NewApprovalService(repo commonRepo.ApprovalRepository) ApprovalService {
return &approvalService{repo: repo}
}
func (s *approvalService) RegisterWorkflowSteps(workflow approvalutils.ApprovalWorkflowKey, steps map[approvalutils.ApprovalStep]string) error {
return approvalutils.RegisterWorkflowSteps(workflow, steps)
}
func (s *approvalService) WorkflowSteps(workflow approvalutils.ApprovalWorkflowKey) map[approvalutils.ApprovalStep]string {
return approvalutils.WorkflowSteps(workflow)
}
func (s *approvalService) WorkflowStepName(workflow approvalutils.ApprovalWorkflowKey, step approvalutils.ApprovalStep) (string, bool) {
return approvalutils.ApprovalStepName(workflow, step)
}
func (s *approvalService) CreateApproval(
ctx context.Context,
workflow approvalutils.ApprovalWorkflowKey,
approvableID uint,
step approvalutils.ApprovalStep,
action *entity.ApprovalAction,
actorID uint,
note *string,
) (*entity.Approval, error) {
record, err := approvalutils.NewApproval(workflow, approvableID, step, action, actorID, note)
if err != nil {
return nil, err
}
if err := s.repo.CreateOne(ctx, record, nil); err != nil {
return nil, err
}
s.decorateApproval(workflow, record)
return record, nil
}
func (s *approvalService) List(
ctx context.Context,
module string,
approvableID *uint,
page, limit int,
search string,
) ([]entity.Approval, int64, error) {
module = strings.TrimSpace(strings.ToUpper(module))
search = strings.TrimSpace(search)
if limit <= 0 {
limit = 10
}
if page <= 0 {
page = 1
}
offset := (page - 1) * limit
records, total, err := s.repo.GetAll(
ctx,
offset,
limit,
func(db *gorm.DB) *gorm.DB {
query := db.
Where("approvable_type = ?", module).
Order("action_at DESC").
Preload("ActionUser")
if approvableID != nil {
query = query.Where("approvable_id = ?", *approvableID)
}
if search != "" {
like := "%" + strings.ToLower(search) + "%"
query = query.Where("(LOWER(step_name) LIKE ? OR LOWER(action) LIKE ? OR LOWER(notes) LIKE ?)", like, like, like)
}
return query
},
)
if err != nil {
if s.isApprovalTableMissing(err) {
return nil, 0, nil
}
return nil, 0, err
}
if len(records) == 0 {
return nil, total, nil
}
workflow := approvalutils.ApprovalWorkflowKey(module)
for i := range records {
s.decorateApproval(workflow, &records[i])
}
return records, total, nil
}
func (s *approvalService) ListByTarget(
ctx context.Context,
workflow approvalutils.ApprovalWorkflowKey,
approvableID uint,
modifier func(*gorm.DB) *gorm.DB,
) ([]entity.Approval, error) {
records, err := s.repo.FindByTarget(ctx, workflow.String(), approvableID, modifier)
if err != nil {
if s.isApprovalTableMissing(err) {
return nil, nil
}
return nil, err
}
for i := range records {
s.decorateApproval(workflow, &records[i])
}
return records, nil
}
func (s *approvalService) LatestByTarget(
ctx context.Context,
workflow approvalutils.ApprovalWorkflowKey,
approvableID uint,
modifier func(*gorm.DB) *gorm.DB,
) (*entity.Approval, error) {
record, err := s.repo.LatestByTarget(ctx, workflow.String(), approvableID, modifier)
if err != nil {
if s.isApprovalTableMissing(err) {
return nil, nil
}
return nil, err
}
if record == nil {
return nil, nil
}
s.decorateApproval(workflow, record)
return record, nil
}
func (s *approvalService) LatestByTargets(
ctx context.Context,
workflow approvalutils.ApprovalWorkflowKey,
approvableIDs []uint,
modifier func(*gorm.DB) *gorm.DB,
) (map[uint]*entity.Approval, error) {
records, err := s.repo.LatestByTargets(ctx, workflow.String(), approvableIDs, modifier)
if err != nil {
if s.isApprovalTableMissing(err) {
return nil, nil
}
return nil, err
}
if len(records) == 0 {
return nil, nil
}
result := make(map[uint]*entity.Approval, len(records))
for approvableID, approval := range records {
approvalCopy := approval
s.decorateApproval(workflow, &approvalCopy)
result[approvableID] = &approvalCopy
}
return result, nil
}
func (s *approvalService) decorateApproval(workflow approvalutils.ApprovalWorkflowKey, approval *entity.Approval) {
if approval == nil {
return
}
currentName := strings.TrimSpace(approval.StepName)
if currentName == "" {
if name, ok := approvalutils.ApprovalStepName(workflow, approvalutils.ApprovalStep(approval.StepNumber)); ok {
approval.StepName = name
}
} else {
approval.StepName = currentName
}
}
func (s *approvalService) isApprovalTableMissing(err error) bool {
if err == nil {
return false
}
errMsg := strings.ToLower(err.Error())
if strings.Contains(errMsg, "no such table: approvals") {
return true
}
schemaIssues := []string{
`relation "approvals" does not exist`,
`column "step_name" does not exist`,
`column "step_number" does not exist`,
`column "action" does not exist`,
`column "status" does not exist`,
`column "step" does not exist`,
}
for _, issue := range schemaIssues {
if strings.Contains(errMsg, issue) {
return true
}
}
return false
}
@@ -1,4 +1,9 @@
DROP TABLE IF EXISTS stock_logs;
DROP INDEX IF EXISTS idx_product_warehouses_unique;
DROP INDEX IF EXISTS idx_product_warehouses_deleted_at;
DROP INDEX IF EXISTS idx_product_warehouses_warehouse_id;
DROP INDEX IF EXISTS idx_product_warehouses_product_id;
DROP TABLE IF EXISTS product_warehouses;
DROP TABLE IF EXISTS fcr_standards;
DROP INDEX IF EXISTS suppliers_name_unique;
DROP TABLE IF EXISTS product_suppliers;
@@ -35,4 +40,4 @@ DROP TABLE IF EXISTS fcrs;
DROP TABLE IF EXISTS projects;
DROP INDEX IF EXISTS users_id_user_unique;
DROP INDEX IF EXISTS users_email_unique;
DROP TABLE IF EXISTS users;
DROP TABLE IF EXISTS users;
@@ -1,234 +1,337 @@
-- USERS
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
id_user BIGINT NOT NULL,
name VARCHAR NOT NULL,
email VARCHAR NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ
id BIGSERIAL PRIMARY KEY,
id_user BIGINT NOT NULL,
name VARCHAR NOT NULL,
email VARCHAR NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE UNIQUE INDEX users_id_user_unique ON users (id_user) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX users_email_unique ON users (email) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX users_id_user_unique ON users (id_user)
WHERE
deleted_at IS NULL;
CREATE UNIQUE INDEX users_email_unique ON users (email)
WHERE
deleted_at IS NULL;
-- FLAGS
CREATE TABLE flags (
id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
flagable_id BIGINT NOT NULL,
flagable_type VARCHAR(50) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
flagable_id BIGINT NOT NULL,
flagable_type VARCHAR(50) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE UNIQUE INDEX flags_unique_flagable ON flags (
name,
flagable_id,
flagable_type
);
CREATE UNIQUE INDEX flags_unique_flagable ON flags (name, flagable_id, flagable_type);
CREATE INDEX flags_flagable_lookup ON flags (flagable_type, flagable_id);
-- PRODUCT CATEGORIES
CREATE TABLE product_categories (
id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
code VARCHAR(10) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
code VARCHAR(10) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
);
CREATE UNIQUE INDEX product_categories_name_unique ON product_categories (name) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX product_categories_code_unique ON product_categories (code) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX product_categories_name_unique ON product_categories (name)
WHERE
deleted_at IS NULL;
CREATE UNIQUE INDEX product_categories_code_unique ON product_categories (code)
WHERE
deleted_at IS NULL;
-- UOM
CREATE TABLE uoms (
id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
);
CREATE UNIQUE INDEX uoms_name_unique ON uoms (name) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX uoms_name_unique ON uoms (name)
WHERE
deleted_at IS NULL;
-- BANKS
CREATE TABLE banks (
id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
alias VARCHAR(5) NOT NULL,
owner VARCHAR,
account_number VARCHAR(50) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
alias VARCHAR(5) NOT NULL,
owner VARCHAR,
account_number VARCHAR(50) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
);
CREATE UNIQUE INDEX banks_name_unique ON banks (name) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX banks_name_unique ON banks (name)
WHERE
deleted_at IS NULL;
-- AREAS
CREATE TABLE areas (
id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
);
CREATE UNIQUE INDEX areas_name_unique ON areas (name) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX areas_name_unique ON areas (name)
WHERE
deleted_at IS NULL;
-- LOCATIONS
CREATE TABLE locations (
id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
address TEXT NOT NULL,
area_id BIGINT NOT NULL REFERENCES areas(id) ON DELETE RESTRICT ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
address TEXT NOT NULL,
area_id BIGINT NOT NULL REFERENCES areas (id) ON DELETE RESTRICT ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
);
CREATE UNIQUE INDEX locations_name_unique ON locations (name) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX locations_name_unique ON locations (name)
WHERE
deleted_at IS NULL;
-- KANDANG
CREATE TABLE kandangs (
id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
location_id BIGINT NOT NULL REFERENCES locations(id) ON DELETE RESTRICT ON UPDATE CASCADE,
pic_id BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
location_id BIGINT NOT NULL REFERENCES locations (id) ON DELETE RESTRICT ON UPDATE CASCADE,
pic_id BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
);
CREATE UNIQUE INDEX kandangs_name_unique ON kandangs (name) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX kandangs_name_unique ON kandangs (name)
WHERE
deleted_at IS NULL;
-- WAREHOUSES
CREATE TABLE warehouses (
id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
type VARCHAR(50) NOT NULL,
area_id BIGINT NOT NULL REFERENCES areas(id) ON DELETE RESTRICT ON UPDATE CASCADE,
location_id BIGINT REFERENCES locations(id) ON DELETE SET NULL ON UPDATE CASCADE,
kandang_id BIGINT REFERENCES kandangs(id) ON DELETE SET NULL ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
type VARCHAR(50) NOT NULL,
area_id BIGINT NOT NULL REFERENCES areas (id) ON DELETE RESTRICT ON UPDATE CASCADE,
location_id BIGINT REFERENCES locations (id) ON DELETE SET NULL ON UPDATE CASCADE,
kandang_id BIGINT REFERENCES kandangs (id) ON DELETE SET NULL ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
);
CREATE UNIQUE INDEX warehouses_name_unique ON warehouses (name) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX warehouses_name_unique ON warehouses (name)
WHERE
deleted_at IS NULL;
-- CUSTOMERS
CREATE TABLE customers (
id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
pic_id BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE,
type VARCHAR(50) NOT NULL,
address TEXT NOT NULL,
phone VARCHAR(20) NOT NULL,
email VARCHAR NOT NULL,
account_number VARCHAR(50) NOT NULL,
balance NUMERIC(15,3) DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
pic_id BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
type VARCHAR(50) NOT NULL,
address TEXT NOT NULL,
phone VARCHAR(20) NOT NULL,
email VARCHAR NOT NULL,
account_number VARCHAR(50) NOT NULL,
balance NUMERIC(15, 3) DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
);
CREATE UNIQUE INDEX customers_name_unique ON customers (name) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX customers_name_unique ON customers (name)
WHERE
deleted_at IS NULL;
-- NONSTOCK
CREATE TABLE nonstocks (
id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
uom_id BIGINT NOT NULL REFERENCES uoms(id) ON DELETE RESTRICT ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
uom_id BIGINT NOT NULL REFERENCES uoms (id) ON DELETE RESTRICT ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
);
CREATE UNIQUE INDEX nonstocks_name_unique ON nonstocks (name) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX nonstocks_name_unique ON nonstocks (name)
WHERE
deleted_at IS NULL;
-- FCR
CREATE TABLE fcrs (
id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
);
CREATE UNIQUE INDEX fcrs_name_unique ON fcrs (name) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX fcrs_name_unique ON fcrs (name)
WHERE
deleted_at IS NULL;
CREATE TABLE fcr_standards (
id BIGSERIAL PRIMARY KEY,
fcr_id BIGINT NOT NULL REFERENCES fcrs(id) ON DELETE CASCADE ON UPDATE CASCADE,
weight NUMERIC(15,3) NOT NULL,
fcr_number NUMERIC(15,3) NOT NULL,
mortality NUMERIC(15,3) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ
id BIGSERIAL PRIMARY KEY,
fcr_id BIGINT NOT NULL REFERENCES fcrs (id) ON DELETE CASCADE ON UPDATE CASCADE,
weight NUMERIC(15, 3) NOT NULL,
fcr_number NUMERIC(15, 3) NOT NULL,
mortality NUMERIC(15, 3) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
-- SUPPLIERS
CREATE TABLE suppliers (
id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
alias VARCHAR(5) NOT NULL,
pic VARCHAR NOT NULL,
type VARCHAR(50) NOT NULL,
category VARCHAR(20) NOT NULL,
hatchery VARCHAR,
phone VARCHAR(20) NOT NULL,
email VARCHAR NOT NULL,
address TEXT NOT NULL,
npwp VARCHAR(50),
account_number VARCHAR(50),
balance NUMERIC(15,3) DEFAULT 0,
due_date INT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
alias VARCHAR(5) NOT NULL,
pic VARCHAR NOT NULL,
type VARCHAR(50) NOT NULL,
category VARCHAR(20) NOT NULL,
hatchery VARCHAR,
phone VARCHAR(20) NOT NULL,
email VARCHAR NOT NULL,
address TEXT NOT NULL,
npwp VARCHAR(50),
account_number VARCHAR(50),
balance NUMERIC(15, 3) DEFAULT 0,
due_date INT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
);
CREATE UNIQUE INDEX suppliers_name_unique ON suppliers (name) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX suppliers_name_unique ON suppliers (name)
WHERE
deleted_at IS NULL;
CREATE TABLE nonstock_suppliers (
nonstock_id BIGINT NOT NULL REFERENCES nonstocks(id) ON DELETE CASCADE ON UPDATE CASCADE,
supplier_id BIGINT NOT NULL REFERENCES suppliers(id) ON DELETE CASCADE ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (nonstock_id, supplier_id)
nonstock_id BIGINT NOT NULL REFERENCES nonstocks (id) ON DELETE CASCADE ON UPDATE CASCADE,
supplier_id BIGINT NOT NULL REFERENCES suppliers (id) ON DELETE CASCADE ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (nonstock_id, supplier_id)
);
-- PRODUCTS
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
brand VARCHAR NOT NULL,
sku VARCHAR(100),
uom_id BIGINT NOT NULL REFERENCES uoms(id) ON DELETE RESTRICT ON UPDATE CASCADE,
product_category_id BIGINT NOT NULL REFERENCES product_categories(id) ON DELETE RESTRICT ON UPDATE CASCADE,
product_price NUMERIC(15,3) NOT NULL,
selling_price NUMERIC(15,3),
tax NUMERIC(15,3),
expiry_period INT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
brand VARCHAR NOT NULL,
sku VARCHAR(100),
uom_id BIGINT NOT NULL REFERENCES uoms (id) ON DELETE RESTRICT ON UPDATE CASCADE,
product_category_id BIGINT NOT NULL REFERENCES product_categories (id) ON DELETE RESTRICT ON UPDATE CASCADE,
product_price NUMERIC(15, 3) NOT NULL,
selling_price NUMERIC(15, 3),
tax NUMERIC(15, 3),
expiry_period INT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
);
CREATE UNIQUE INDEX products_name_unique ON products (name) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX products_sku_unique ON products (sku) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX products_name_unique ON products (name)
WHERE
deleted_at IS NULL;
CREATE UNIQUE INDEX products_sku_unique ON products (sku)
WHERE
deleted_at IS NULL;
CREATE TABLE product_suppliers (
product_id BIGINT NOT NULL REFERENCES products(id) ON DELETE CASCADE ON UPDATE CASCADE,
supplier_id BIGINT NOT NULL REFERENCES suppliers(id) ON DELETE CASCADE ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (product_id, supplier_id)
product_id BIGINT NOT NULL REFERENCES products (id) ON DELETE CASCADE ON UPDATE CASCADE,
supplier_id BIGINT NOT NULL REFERENCES suppliers (id) ON DELETE CASCADE ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (product_id, supplier_id)
);
-- PROJECTS
CREATE TABLE projects (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
);
-- PRODUCT WAREHOUSES TABLE
CREATE TABLE product_warehouses (
id BIGSERIAL PRIMARY KEY,
product_id BIGINT NOT NULL REFERENCES products (id),
warehouse_id BIGINT NOT NULL REFERENCES warehouses (id),
quantity INTEGER NOT NULL DEFAULT 0,
created_by BIGINT NOT NULL REFERENCES users (id),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
-- INDEXES
CREATE INDEX idx_product_warehouses_product_id ON product_warehouses (product_id);
CREATE INDEX idx_product_warehouses_warehouse_id ON product_warehouses (warehouse_id);
CREATE INDEX idx_product_warehouses_deleted_at ON product_warehouses (deleted_at);
CREATE UNIQUE INDEX idx_product_warehouses_unique ON product_warehouses (product_id, warehouse_id)
WHERE
deleted_at IS NULL;
-- STOCK LOGS
CREATE TABLE stock_logs (
id BIGSERIAL PRIMARY KEY,
transaction_type VARCHAR(20) NOT NULL,
quantity NUMERIC(15, 3) NOT NULL,
before_quantity NUMERIC(15, 3) NOT NULL,
after_quantity NUMERIC(15, 3) NOT NULL,
log_type VARCHAR(50) NOT NULL,
log_id BIGINT,
note TEXT,
product_warehouse_id BIGINT NOT NULL REFERENCES product_warehouses (id) ON DELETE CASCADE ON UPDATE CASCADE,
created_by BIGINT NOT NULL REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
-- Create indexes for better performance
CREATE INDEX stock_logs_product_warehouse_id_idx ON stock_logs (product_warehouse_id);
CREATE INDEX stock_logs_log_type_log_id_idx ON stock_logs (log_type, log_id);
CREATE INDEX stock_logs_created_by_idx ON stock_logs (created_by);
CREATE INDEX stock_logs_created_at_idx ON stock_logs (created_at);
CREATE INDEX stock_logs_deleted_at_idx ON stock_logs (deleted_at);
@@ -0,0 +1,4 @@
-- DROP TABLE: STOCK_TRANSFERS DAN SEQUENCE-NYA
DROP TABLE IF EXISTS stock_transfers CASCADE;
DROP SEQUENCE IF EXISTS stock_transfer_seq CASCADE;
@@ -0,0 +1,57 @@
-- ===============================================================
-- STOCK TRANSFERS (HEADER)
-- ===============================================================
CREATE SEQUENCE IF NOT EXISTS stock_transfer_seq START 1;
CREATE TABLE IF NOT EXISTS stock_transfers (
id BIGSERIAL PRIMARY KEY,
movement_number VARCHAR(50) UNIQUE NOT NULL,
from_warehouse_id BIGINT NOT NULL,
to_warehouse_id BIGINT NOT NULL,
area_id BIGINT,
reason TEXT,
transfer_date DATE NOT NULL,
created_by BIGINT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'warehouses') THEN
ALTER TABLE stock_transfers
ADD CONSTRAINT fk_stock_transfers_from_warehouse
FOREIGN KEY (from_warehouse_id)
REFERENCES warehouses(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE stock_transfers
ADD CONSTRAINT fk_stock_transfers_to_warehouse
FOREIGN KEY (to_warehouse_id)
REFERENCES warehouses(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'areas') THEN
ALTER TABLE stock_transfers
ADD CONSTRAINT fk_stock_transfers_area
FOREIGN KEY (area_id)
REFERENCES areas(id)
ON DELETE SET NULL ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
ALTER TABLE stock_transfers
ADD CONSTRAINT fk_stock_transfers_created_by
FOREIGN KEY (created_by)
REFERENCES users(id)
ON DELETE SET NULL ON UPDATE CASCADE;
END IF;
END $$;
-- INDEXES
CREATE INDEX IF NOT EXISTS idx_stock_transfers_from_warehouse_id ON stock_transfers(from_warehouse_id);
CREATE INDEX IF NOT EXISTS idx_stock_transfers_to_warehouse_id ON stock_transfers(to_warehouse_id);
CREATE INDEX IF NOT EXISTS idx_stock_transfers_transfer_date ON stock_transfers(transfer_date);
@@ -0,0 +1,2 @@
-- DROP TABLE: STOCK_TRANSFER_DETAILS
DROP TABLE IF EXISTS stock_transfer_details CASCADE;
@@ -0,0 +1,48 @@
-- ===============================================================
-- STOCK TRANSFER DETAILS (PRODUK)
-- ===============================================================
CREATE TABLE IF NOT EXISTS stock_transfer_details (
id BIGSERIAL PRIMARY KEY,
stock_transfer_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
quantity NUMERIC(15, 3) NOT NULL CHECK (quantity > 0),
before_quantity NUMERIC(15, 3),
after_quantity NUMERIC(15, 3),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
-- ===============================================================
-- FOREIGN KEYS (dengan pengecekan tabel agar anti gagal)
-- ===============================================================
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'stock_transfers') THEN
EXECUTE
'ALTER TABLE stock_transfer_details
ADD CONSTRAINT fk_stock_transfer_details_transfer
FOREIGN KEY (stock_transfer_id)
REFERENCES stock_transfers(id)
ON DELETE CASCADE ON UPDATE CASCADE';
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'products') THEN
EXECUTE
'ALTER TABLE stock_transfer_details
ADD CONSTRAINT fk_stock_transfer_details_product
FOREIGN KEY (product_id)
REFERENCES products(id)
ON DELETE RESTRICT ON UPDATE CASCADE';
END IF;
END $$;
-- ===============================================================
-- INDEXES
-- ===============================================================
CREATE INDEX IF NOT EXISTS idx_stock_transfer_details_transfer_id ON stock_transfer_details (stock_transfer_id);
CREATE INDEX IF NOT EXISTS idx_stock_transfer_details_product_id ON stock_transfer_details (product_id);
@@ -0,0 +1,2 @@
-- DROP TABLE: STOCK_TRANSFER_DELIVERIES
DROP TABLE IF EXISTS stock_transfer_deliveries CASCADE;
@@ -0,0 +1,42 @@
-- ===============================================================
-- STOCK TRANSFER DELIVERIES (EKSPEDISI)
-- ===============================================================
CREATE TABLE IF NOT EXISTS stock_transfer_deliveries (
id BIGSERIAL PRIMARY KEY,
stock_transfer_id BIGINT NOT NULL,
supplier_id BIGINT,
vehicle_plate VARCHAR(20),
driver_name VARCHAR(100),
document_number VARCHAR(50),
document_path TEXT,
shipping_cost_item NUMERIC(15,3),
shipping_cost_total NUMERIC(15,3),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
-- FOREIGN KEYS
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'stock_transfers') THEN
ALTER TABLE stock_transfer_deliveries
ADD CONSTRAINT fk_stock_transfer_deliveries_transfer
FOREIGN KEY (stock_transfer_id)
REFERENCES stock_transfers(id)
ON DELETE CASCADE ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'suppliers') THEN
ALTER TABLE stock_transfer_deliveries
ADD CONSTRAINT fk_stock_transfer_deliveries_supplier
FOREIGN KEY (supplier_id)
REFERENCES suppliers(id)
ON DELETE SET NULL ON UPDATE CASCADE;
END IF;
END $$;
-- INDEXES
CREATE INDEX IF NOT EXISTS idx_stock_transfer_deliveries_transfer_id ON stock_transfer_deliveries(stock_transfer_id);
CREATE INDEX IF NOT EXISTS idx_stock_transfer_deliveries_supplier_id ON stock_transfer_deliveries(supplier_id);
@@ -0,0 +1,2 @@
-- DROP PIVOT TABLE: STOCK_TRANSFER_DELIVERY_ITEMS
DROP TABLE IF EXISTS stock_transfer_delivery_items CASCADE;
@@ -0,0 +1,35 @@
-- ===============================================================
-- STOCK TRANSFER DELIVERY ITEMS (PIVOT)
-- ===============================================================
CREATE TABLE IF NOT EXISTS stock_transfer_delivery_items (
id BIGSERIAL PRIMARY KEY,
stock_transfer_delivery_id BIGINT NOT NULL,
stock_transfer_detail_id BIGINT NOT NULL,
quantity NUMERIC(15, 3) NOT NULL CHECK (quantity > 0)
);
-- FOREIGN KEYS
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'stock_transfer_deliveries') THEN
ALTER TABLE stock_transfer_delivery_items
ADD CONSTRAINT fk_delivery_items_delivery
FOREIGN KEY (stock_transfer_delivery_id)
REFERENCES stock_transfer_deliveries(id)
ON DELETE CASCADE ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'stock_transfer_details') THEN
ALTER TABLE stock_transfer_delivery_items
ADD CONSTRAINT fk_delivery_items_detail
FOREIGN KEY (stock_transfer_detail_id)
REFERENCES stock_transfer_details(id)
ON DELETE CASCADE ON UPDATE CASCADE;
END IF;
END $$;
-- INDEXES
CREATE INDEX IF NOT EXISTS idx_stock_transfer_delivery_items_delivery_id ON stock_transfer_delivery_items (stock_transfer_delivery_id);
CREATE INDEX IF NOT EXISTS idx_stock_transfer_delivery_items_detail_id ON stock_transfer_delivery_items (stock_transfer_detail_id);
@@ -0,0 +1,2 @@
ALTER TABLE kandangs
DROP COLUMN IF EXISTS status;
@@ -0,0 +1,9 @@
ALTER TABLE kandangs
ADD COLUMN status VARCHAR(20);
UPDATE kandangs
SET status = 'NON_ACTIVE'
WHERE status IS NULL;
ALTER TABLE kandangs
ALTER COLUMN status SET NOT NULL;
@@ -0,0 +1,6 @@
ALTER TABLE kandangs
DROP COLUMN IF EXISTS project_flock_id;
DROP TABLE IF EXISTS project_flocks;
DROP TABLE IF EXISTS flocks;
@@ -0,0 +1,29 @@
CREATE TABLE flocks (
id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
);
CREATE UNIQUE INDEX flocks_name_unique ON flocks (name)
WHERE
deleted_at IS NULL;
CREATE TABLE project_flocks (
id BIGSERIAL PRIMARY KEY,
flock_id BIGINT NOT NULL REFERENCES flocks (id) ON DELETE RESTRICT ON UPDATE CASCADE,
area_id BIGINT NOT NULL REFERENCES areas (id) ON DELETE RESTRICT ON UPDATE CASCADE,
product_category_id BIGINT NOT NULL REFERENCES product_categories (id) ON DELETE RESTRICT ON UPDATE CASCADE,
fcr_id BIGINT NOT NULL REFERENCES fcrs (id) ON DELETE RESTRICT ON UPDATE CASCADE,
location_id BIGINT NOT NULL REFERENCES locations (id) ON DELETE RESTRICT ON UPDATE CASCADE,
period INT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
);
ALTER TABLE kandangs
ADD COLUMN project_flock_id BIGINT REFERENCES project_flocks (id) ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,2 @@
DROP INDEX IF EXISTS approvals_approvable_lookup;
DROP TABLE IF EXISTS approvals;
@@ -0,0 +1,12 @@
CREATE TABLE approvals (
id BIGSERIAL PRIMARY KEY,
approvable_type VARCHAR(50) NOT NULL,
approvable_id BIGINT NOT NULL,
step SMALLINT NOT NULL,
status VARCHAR(20) NOT NULL,
notes TEXT,
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
action_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
);
CREATE INDEX approvals_approvable_lookup ON approvals (approvable_type, approvable_id);
@@ -0,0 +1,18 @@
ALTER TABLE approvals
RENAME COLUMN action TO status;
UPDATE approvals
SET status = 'PENDING'
WHERE status IS NULL;
ALTER TABLE approvals
ALTER COLUMN status SET NOT NULL;
ALTER TABLE approvals
RENAME COLUMN step_number TO step;
ALTER TABLE approvals
DROP COLUMN step_name;
ALTER TABLE approvals
RENAME COLUMN action_at TO created_at;
@@ -0,0 +1,14 @@
ALTER TABLE approvals
RENAME COLUMN status TO action;
ALTER TABLE approvals
ALTER COLUMN action DROP NOT NULL;
ALTER TABLE approvals
RENAME COLUMN step TO step_number;
ALTER TABLE approvals
ADD COLUMN step_name VARCHAR NOT NULL;
ALTER TABLE approvals
RENAME COLUMN created_at TO action_at;
@@ -0,0 +1 @@
DROP TABLE IF EXISTS project_flock_kandangs;
@@ -0,0 +1,17 @@
CREATE TABLE project_flock_kandangs (
id BIGSERIAL PRIMARY KEY,
project_flock_id BIGINT NOT NULL REFERENCES project_flocks (id) ON DELETE CASCADE ON UPDATE CASCADE,
kandang_id BIGINT NOT NULL REFERENCES kandangs (id) ON DELETE CASCADE ON UPDATE CASCADE,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
detached_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_project_flock_kandangs_project ON project_flock_kandangs (project_flock_id);
CREATE INDEX idx_project_flock_kandangs_kandang ON project_flock_kandangs (kandang_id);
CREATE UNIQUE INDEX idx_project_flock_kandangs_active ON project_flock_kandangs (project_flock_id, kandang_id)
WHERE
detached_at IS NULL;
@@ -0,0 +1 @@
DROP TABLE IF EXISTS project_chickins;
@@ -0,0 +1 @@
DROP TABLE IF EXISTS project_flock_populations;
@@ -0,0 +1,25 @@
BEGIN;
-- Recreate legacy columns on project_flock_kandangs
DROP INDEX IF EXISTS idx_project_flock_kandangs_unique;
ALTER TABLE project_flock_kandangs
ADD COLUMN IF NOT EXISTS created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
ADD COLUMN IF NOT EXISTS assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
ADD COLUMN IF NOT EXISTS detached_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW();
CREATE UNIQUE INDEX IF NOT EXISTS idx_project_flock_kandangs_active
ON project_flock_kandangs (project_flock_id, kandang_id)
WHERE detached_at IS NULL;
-- Restore product_category_id reference and drop category column
ALTER TABLE project_flocks
ADD COLUMN IF NOT EXISTS product_category_id BIGINT REFERENCES product_categories (id) ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE project_flocks
DROP COLUMN IF EXISTS category;
COMMIT;
DROP INDEX IF EXISTS project_flocks_flock_period_unique;
@@ -0,0 +1,43 @@
BEGIN;
-- Add category column to project_flocks and backfill existing rows
ALTER TABLE project_flocks
ADD COLUMN IF NOT EXISTS category VARCHAR(20);
UPDATE project_flocks
SET category = 'GROWING'
WHERE category IS NULL;
ALTER TABLE project_flocks
ALTER COLUMN category SET NOT NULL;
ALTER TABLE project_flocks
ALTER COLUMN category SET DEFAULT 'GROWING';
-- Drop legacy foreign key reference and column
ALTER TABLE project_flocks
DROP CONSTRAINT IF EXISTS project_flocks_product_category_id_fkey;
ALTER TABLE project_flocks
DROP COLUMN IF EXISTS product_category_id;
-- Simplify project_flock_kandangs structure
DROP INDEX IF EXISTS idx_project_flock_kandangs_active;
ALTER TABLE project_flock_kandangs
DROP COLUMN IF EXISTS created_by,
DROP COLUMN IF EXISTS assigned_at,
DROP COLUMN IF EXISTS detached_at,
DROP COLUMN IF EXISTS updated_at;
ALTER TABLE project_flock_kandangs
ALTER COLUMN created_at SET DEFAULT NOW();
CREATE UNIQUE INDEX IF NOT EXISTS idx_project_flock_kandangs_unique
ON project_flock_kandangs (project_flock_id, kandang_id);
COMMIT;
CREATE UNIQUE INDEX project_flocks_flock_period_unique
ON project_flocks (flock_id, period)
WHERE deleted_at IS NULL;
@@ -0,0 +1 @@
DROP TABLE IF EXISTS project_chickin_details;
@@ -0,0 +1,45 @@
CREATE TABLE IF NOT EXISTS project_chickin_details (
id BIGSERIAL PRIMARY KEY,
project_chickin_id BIGINT NOT NULL,
product_warehouse_id BIGINT NOT NULL,
quantity NUMERIC(15, 3) NOT NULL,
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 = 'project_chickins') THEN
ALTER TABLE project_chickin_details
ADD CONSTRAINT fk_project_chickin_id
FOREIGN KEY (project_chickin_id)
REFERENCES project_chickins(id)
ON DELETE CASCADE ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
ALTER TABLE project_chickin_details
ADD CONSTRAINT fk_product_warehouse_id
FOREIGN KEY (product_warehouse_id)
REFERENCES product_warehouses(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
ALTER TABLE project_chickin_details
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_chickin_details_project_chickin_id ON project_chickin_details (project_chickin_id);
CREATE INDEX IF NOT EXISTS idx_project_chickin_details_product_warehouse_id ON project_chickin_details (product_warehouse_id);
CREATE INDEX IF NOT EXISTS idx_project_chickin_details_created_by ON project_chickin_details (created_by);
@@ -0,0 +1,24 @@
BEGIN;
--? Child Indexes(optional, biar rapi tapi klo gada juga ilang pas di drop)
DROP INDEX IF EXISTS idx_recording_stocks_product;
DROP INDEX IF EXISTS idx_recording_stocks_recording;
DROP INDEX IF EXISTS idx_recording_depl_recording;
DROP INDEX IF EXISTS idx_recording_bws_recording;
--? Child Tables
DROP TABLE IF EXISTS recording_stocks;
DROP TABLE IF EXISTS recording_depletions;
DROP TABLE IF EXISTS recording_bws;
--? Parent Indexes ON recordings
DROP INDEX IF EXISTS uq_recordings_flock_record_date;
DROP INDEX IF EXISTS idx_recordings_flock_datetime;
--? Parent table
DROP TABLE IF EXISTS recordings;
COMMIT;
@@ -0,0 +1,150 @@
BEGIN;
--? RECORDINGS (tabel induk recording harian)
CREATE TABLE IF NOT EXISTS recordings (
id BIGSERIAL PRIMARY KEY,
project_flock_id BIGINT NOT NULL,
record_datetime TIMESTAMPTZ NOT NULL,
record_date DATE,
status INT NOT NULL DEFAULT 0, --? 0=draft,1=submitted,2=approved,3=rejected
ontime INT NOT NULL DEFAULT 0, --? 1=ontime,0=late (pakai INT/BOOLEAN sesuai preferensi)
day INT,
total_depletion INT,
cum_depletion_rate NUMERIC(7,3),
daily_gain NUMERIC(7,3),
avg_daily_gain NUMERIC(7,3),
cum_intake INT,
fcr_value NUMERIC(7,3),
total_chick BIGINT,
daily_depletion_rate NUMERIC(7,3),
cum_depletion INT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT,
CONSTRAINT fk_recordings_project_flock
FOREIGN KEY (project_flock_id) REFERENCES project_flock_kandangs(id),
CONSTRAINT fk_recordings_created_by
FOREIGN KEY (created_by) REFERENCES users(id),
CONSTRAINT chk_recordings_status
CHECK (status IN (0,1,2,3)),
CONSTRAINT chk_recordings_ontime
CHECK (ontime IN (0,1)),
CONSTRAINT chk_recordings_day
CHECK (day IS NULL OR day >= 1),
CONSTRAINT chk_recordings_nonnegatives
CHECK (
(total_depletion IS NULL OR total_depletion >= 0) AND
(cum_depletion IS NULL OR cum_depletion >= 0) AND
(total_chick IS NULL OR total_chick >= 0) AND
(cum_intake IS NULL OR cum_intake >= 0) AND
(daily_gain IS NULL OR daily_gain >= 0) AND
(avg_daily_gain IS NULL OR avg_daily_gain >= 0) AND
(fcr_value IS NULL OR fcr_value > 0) AND
(daily_depletion_rate IS NULL OR daily_depletion_rate >= 0) AND
(cum_depletion_rate IS NULL OR cum_depletion_rate >= 0)
)
);
--? Set record_date otomatis berdasarkan record_datetime (pakai zona Asia/Jakarta)
CREATE OR REPLACE FUNCTION trg_set_record_date() RETURNS trigger AS $$
BEGIN
NEW.record_date := (NEW.record_datetime AT TIME ZONE 'Asia/Jakarta')::date;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS recordings_set_record_date_trg ON recordings;
CREATE TRIGGER recordings_set_record_date_trg
BEFORE INSERT OR UPDATE OF record_datetime ON recordings
FOR EACH ROW EXECUTE FUNCTION trg_set_record_date();
CREATE INDEX IF NOT EXISTS idx_recordings_flock_datetime
ON recordings (project_flock_id, record_datetime);
--? Unique harian (1 recording per hari dan per flock)
CREATE UNIQUE INDEX IF NOT EXISTS uq_recordings_flock_record_date
ON recordings (project_flock_id, record_date)
WHERE deleted_at IS NULL;
--? RECORDING_BWS (BW per recording)
CREATE TABLE IF NOT EXISTS recording_bws (
id BIGSERIAL PRIMARY KEY,
recording_id BIGINT NOT NULL,
weight NUMERIC(8,2) NOT NULL, --? bobot per ekor/kelompok
qty INT NOT NULL DEFAULT 1, --? jumlah ekor pada bobot ini
notes VARCHAR,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT fk_recording_bws_recording
FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE CASCADE,
CONSTRAINT chk_recording_bws_nonneg
CHECK (weight >= 0 AND qty >= 1)
);
CREATE INDEX IF NOT EXISTS idx_recording_bws_recording
ON recording_bws (recording_id);
--? RECORDING_DEPLETIONS
CREATE TABLE IF NOT EXISTS recording_depletions (
id BIGSERIAL PRIMARY KEY,
recording_id BIGINT NOT NULL,
product_warehouse_id BIGINT NOT NULL,
total BIGINT NOT NULL,
notes VARCHAR,
CONSTRAINT fk_recording_depl_recording
FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE CASCADE,
CONSTRAINT fk_recording_depl_prodwh
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id),
CONSTRAINT chk_recording_depl_total
CHECK (total >= 0)
);
CREATE INDEX IF NOT EXISTS idx_recording_depl_recording
ON recording_depletions (recording_id);
--? RECORDING_STOCKS
CREATE TABLE IF NOT EXISTS recording_stocks (
id BIGSERIAL PRIMARY KEY,
recording_id BIGINT NOT NULL,
product_warehouse_id BIGINT NOT NULL,
increase NUMERIC(10,3), --? penambahan (boleh NULL)
decrease NUMERIC(10,3), --? pengurangan (boleh NULL)
usage_amount BIGINT, --? pemakaian (opsional, jika konsep dipisah dari decrease)
notes VARCHAR,
CONSTRAINT fk_recording_stocks_recording
FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE CASCADE,
CONSTRAINT fk_recording_stocks_prodwh
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id),
CONSTRAINT chk_recording_stocks_nonneg
CHECK (
(increase IS NULL OR increase >= 0) AND
(decrease IS NULL OR decrease >= 0) AND
(usage_amount IS NULL OR usage_amount >= 0)
)
);
CREATE INDEX IF NOT EXISTS idx_recording_stocks_recording
ON recording_stocks (recording_id);
CREATE INDEX IF NOT EXISTS idx_recording_stocks_product
ON recording_stocks (product_warehouse_id);
COMMIT;
@@ -0,0 +1,30 @@
ALTER TABLE kandangs
DROP CONSTRAINT IF EXISTS kandangs_project_flock_id_fkey;
ALTER TABLE kandangs DROP COLUMN IF EXISTS project_flock_id;
-- 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;
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,98 @@
BEGIN;
DROP INDEX IF EXISTS project_flocks_base_period_unique;
ALTER TABLE project_flocks
ADD COLUMN IF NOT EXISTS flock_id BIGINT;
WITH normalized AS (
SELECT
pf.id,
COALESCE(
NULLIF(TRIM(regexp_replace(pf.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g')), ''),
CONCAT('Project Flock ', pf.id)
) AS normalized_name,
COALESCE(NULLIF(pf.created_by, 0), 1) AS created_by
FROM project_flocks pf
),
seed_flocks AS (
SELECT DISTINCT
n.normalized_name,
MIN(n.created_by) AS created_by
FROM normalized n
GROUP BY n.normalized_name
)
INSERT INTO flocks (name, created_by, created_at, updated_at)
SELECT sf.normalized_name, sf.created_by, NOW(), NOW()
FROM seed_flocks sf
ON CONFLICT DO NOTHING;
WITH normalized AS (
SELECT
pf.id,
COALESCE(
NULLIF(TRIM(regexp_replace(pf.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g')), ''),
CONCAT('Project Flock ', pf.id)
) AS normalized_name
FROM project_flocks pf
),
resolved AS (
SELECT
n.id,
f.id AS flock_id
FROM normalized n
JOIN flocks f ON LOWER(f.name) = LOWER(n.normalized_name)
)
UPDATE project_flocks pf
SET flock_id = resolved.flock_id
FROM resolved
WHERE pf.id = resolved.id;
WITH missing AS (
SELECT
pf.id,
COALESCE(
NULLIF(TRIM(regexp_replace(pf.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g')), ''),
CONCAT('Project Flock ', pf.id)
) AS normalized_name,
COALESCE(NULLIF(pf.created_by, 0), 1) AS created_by
FROM project_flocks pf
WHERE pf.flock_id IS NULL
),
seed_missing AS (
SELECT DISTINCT normalized_name, created_by FROM missing
)
INSERT INTO flocks (name, created_by, created_at, updated_at)
SELECT sm.normalized_name, sm.created_by, NOW(), NOW()
FROM seed_missing sm
ON CONFLICT DO NOTHING;
WITH missing AS (
SELECT
pf.id,
COALESCE(
NULLIF(TRIM(regexp_replace(pf.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g')), ''),
CONCAT('Project Flock ', pf.id)
) AS normalized_name
FROM project_flocks pf
WHERE pf.flock_id IS NULL
)
UPDATE project_flocks pf
SET flock_id = f.id
FROM missing m
JOIN flocks f ON LOWER(f.name) = LOWER(m.normalized_name)
WHERE pf.id = m.id;
ALTER TABLE project_flocks
ALTER COLUMN flock_id SET NOT NULL;
DROP INDEX IF EXISTS project_flocks_flock_name_unique;
ALTER TABLE project_flocks
DROP COLUMN IF EXISTS flock_name;
CREATE UNIQUE INDEX IF NOT EXISTS project_flocks_flock_period_unique
ON project_flocks (flock_id, period)
WHERE deleted_at IS NULL;
COMMIT;
@@ -0,0 +1,55 @@
BEGIN;
ALTER TABLE project_flocks
ADD COLUMN IF NOT EXISTS flock_name VARCHAR(255);
WITH generated_names AS (
SELECT
pf.id,
COALESCE(f.name, CONCAT('Project Flock ', pf.id)) AS base_name,
pf.period,
ROW_NUMBER() OVER (PARTITION BY COALESCE(f.name, CONCAT('Project Flock ', pf.id)) ORDER BY pf.id) AS rn
FROM project_flocks pf
LEFT JOIN flocks f ON f.id = pf.flock_id
)
UPDATE project_flocks pf
SET flock_name = CASE
WHEN gn.period IS NOT NULL THEN
CASE
WHEN gn.rn = 1 THEN CONCAT(gn.base_name, ' ', gn.period)
ELSE CONCAT(gn.base_name, ' ', gn.period, ' ', gn.rn)
END
ELSE
CASE
WHEN gn.rn = 1 THEN gn.base_name
ELSE CONCAT(gn.base_name, ' ', gn.rn)
END
END
FROM generated_names gn
WHERE pf.id = gn.id
AND (pf.flock_name IS NULL OR pf.flock_name = '');
UPDATE project_flocks
SET flock_name = CONCAT('Project Flock ', id)
WHERE flock_name IS NULL OR flock_name = '';
ALTER TABLE project_flocks
ALTER COLUMN flock_name SET NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS project_flocks_flock_name_unique
ON project_flocks (flock_name)
WHERE deleted_at IS NULL;
DROP INDEX IF EXISTS project_flocks_flock_period_unique;
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;
ALTER TABLE project_flocks
DROP COLUMN IF EXISTS flock_id;
COMMIT;
@@ -0,0 +1,143 @@
BEGIN;
-- Drop newly introduced egg tables
DROP TABLE IF EXISTS grading_eggs;
DROP TABLE IF EXISTS recording_eggs;
-- Revert recording_stocks structure
ALTER TABLE recording_stocks
DROP CONSTRAINT IF EXISTS chk_recording_stocks_nonneg;
ALTER TABLE recording_stocks
DROP COLUMN IF EXISTS usage_qty,
DROP COLUMN IF EXISTS pending_qty;
ALTER TABLE recording_stocks
ADD COLUMN increase NUMERIC(10,3),
ADD COLUMN decrease NUMERIC(10,3),
ADD COLUMN usage_amount BIGINT,
ADD COLUMN notes VARCHAR;
ALTER TABLE recording_stocks
ADD CONSTRAINT chk_recording_stocks_nonneg CHECK (
(increase IS NULL OR increase >= 0) AND
(decrease IS NULL OR decrease >= 0) AND
(usage_amount IS NULL OR usage_amount >= 0)
);
-- Revert recording_depletions structure
ALTER TABLE recording_depletions
DROP CONSTRAINT IF EXISTS chk_recording_depl_qty;
ALTER TABLE recording_depletions
ALTER COLUMN qty TYPE BIGINT USING COALESCE(qty, 0)::BIGINT;
ALTER TABLE recording_depletions
RENAME COLUMN qty TO total;
ALTER TABLE recording_depletions
ADD COLUMN notes VARCHAR;
ALTER TABLE recording_depletions
ADD CONSTRAINT chk_recording_depl_total CHECK (total >= 0);
-- Revert recording_bws structure
ALTER TABLE recording_bws
DROP CONSTRAINT IF EXISTS chk_recording_bws_nonneg;
ALTER TABLE recording_bws
ALTER COLUMN qty TYPE INT USING COALESCE(qty, 0)::INT;
ALTER TABLE recording_bws
DROP COLUMN IF EXISTS total_weight;
ALTER TABLE recording_bws
ALTER COLUMN avg_weight TYPE NUMERIC(8,2) USING COALESCE(avg_weight, 0)::NUMERIC(8,2);
ALTER TABLE recording_bws
RENAME COLUMN avg_weight TO weight;
ALTER TABLE recording_bws
ADD COLUMN notes VARCHAR;
UPDATE recording_bws
SET qty = GREATEST(qty, 1);
ALTER TABLE recording_bws
ADD CONSTRAINT chk_recording_bws_nonneg CHECK (weight >= 0 AND qty >= 1);
-- Revert recordings header
DROP INDEX IF EXISTS idx_recordings_flock_datetime;
ALTER TABLE recordings
DROP CONSTRAINT IF EXISTS fk_recordings_project_flock_kandang,
DROP CONSTRAINT IF EXISTS chk_recordings_nonnegatives_v2;
ALTER TABLE recordings
ALTER COLUMN total_depletion_qty TYPE INT USING COALESCE(total_depletion_qty, 0)::INT,
ALTER COLUMN total_chick_qty TYPE BIGINT USING COALESCE(total_chick_qty, 0)::BIGINT;
ALTER TABLE recordings
RENAME COLUMN total_depletion_qty TO total_depletion;
ALTER TABLE recordings
RENAME COLUMN total_chick_qty TO total_chick;
ALTER TABLE recordings
ADD COLUMN record_date DATE,
ADD COLUMN status INT NOT NULL DEFAULT 0,
ADD COLUMN ontime INT NOT NULL DEFAULT 0,
ADD COLUMN daily_depletion_rate NUMERIC(7,3),
ADD COLUMN cum_depletion INT;
ALTER TABLE recordings
RENAME COLUMN project_flock_kandangs_id TO project_flock_id;
ALTER TABLE recordings
ADD CONSTRAINT fk_recordings_project_flock
FOREIGN KEY (project_flock_id) REFERENCES project_flock_kandangs(id);
ALTER TABLE recordings
ADD CONSTRAINT chk_recordings_status CHECK (status IN (0,1,2,3));
ALTER TABLE recordings
ADD CONSTRAINT chk_recordings_ontime CHECK (ontime IN (0,1));
ALTER TABLE recordings
ADD CONSTRAINT chk_recordings_nonnegatives CHECK (
(total_depletion IS NULL OR total_depletion >= 0) AND
(cum_depletion IS NULL OR cum_depletion >= 0) AND
(total_chick IS NULL OR total_chick >= 0) AND
(cum_intake IS NULL OR cum_intake >= 0) AND
(daily_gain IS NULL OR daily_gain >= 0) AND
(avg_daily_gain IS NULL OR avg_daily_gain >= 0) AND
(fcr_value IS NULL OR fcr_value > 0) AND
(daily_depletion_rate IS NULL OR daily_depletion_rate >= 0) AND
(cum_depletion_rate IS NULL OR cum_depletion_rate >= 0)
);
-- Ensure new columns carry derived data
UPDATE recordings
SET record_date = (record_datetime AT TIME ZONE 'Asia/Jakarta')::date
WHERE record_date IS NULL;
-- Restore helper trigger/function and indexes
CREATE OR REPLACE FUNCTION trg_set_record_date() RETURNS trigger AS $$
BEGIN
NEW.record_date := (NEW.record_datetime AT TIME ZONE 'Asia/Jakarta')::date;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER recordings_set_record_date_trg
BEFORE INSERT OR UPDATE OF record_datetime ON recordings
FOR EACH ROW EXECUTE FUNCTION trg_set_record_date();
CREATE INDEX idx_recordings_flock_datetime
ON recordings (project_flock_id, record_datetime);
CREATE UNIQUE INDEX uq_recordings_flock_record_date
ON recordings (project_flock_id, record_date)
WHERE deleted_at IS NULL;
COMMIT;
@@ -0,0 +1,168 @@
BEGIN;
-- Drop trigger & helper function tied to record_date before removing the column
DROP TRIGGER IF EXISTS recordings_set_record_date_trg ON recordings;
DROP FUNCTION IF EXISTS trg_set_record_date();
-- Drop indexes and constraints that reference legacy columns
DROP INDEX IF EXISTS uq_recordings_flock_record_date;
DROP INDEX IF EXISTS idx_recordings_flock_datetime;
ALTER TABLE recordings
DROP CONSTRAINT IF EXISTS fk_recordings_project_flock,
DROP CONSTRAINT IF EXISTS chk_recordings_status,
DROP CONSTRAINT IF EXISTS chk_recordings_ontime,
DROP CONSTRAINT IF EXISTS chk_recordings_nonnegatives;
-- Align recordings header with the new schema
ALTER TABLE recordings
RENAME COLUMN project_flock_id TO project_flock_kandangs_id;
ALTER TABLE recordings
DROP COLUMN IF EXISTS record_date,
DROP COLUMN IF EXISTS status,
DROP COLUMN IF EXISTS ontime,
DROP COLUMN IF EXISTS daily_depletion_rate,
DROP COLUMN IF EXISTS cum_depletion;
ALTER TABLE recordings
RENAME COLUMN total_depletion TO total_depletion_qty;
ALTER TABLE recordings
RENAME COLUMN total_chick TO total_chick_qty;
ALTER TABLE recordings
ALTER COLUMN total_depletion_qty TYPE NUMERIC(15,3) USING COALESCE(total_depletion_qty, 0)::NUMERIC(15,3),
ALTER COLUMN total_chick_qty TYPE NUMERIC(15,3) USING COALESCE(total_chick_qty, 0)::NUMERIC(15,3),
ALTER COLUMN cum_intake TYPE INT USING COALESCE(cum_intake, 0)::INT;
ALTER TABLE recordings
ADD CONSTRAINT fk_recordings_project_flock_kandang
FOREIGN KEY (project_flock_kandangs_id) REFERENCES project_flock_kandangs(id);
ALTER TABLE recordings
ADD CONSTRAINT chk_recordings_nonnegatives_v2 CHECK (
(total_depletion_qty IS NULL OR total_depletion_qty >= 0) AND
(cum_depletion_rate IS NULL OR cum_depletion_rate >= 0) AND
(daily_gain IS NULL OR daily_gain >= 0) AND
(avg_daily_gain IS NULL OR avg_daily_gain >= 0) AND
(cum_intake IS NULL OR cum_intake >= 0) AND
(fcr_value IS NULL OR fcr_value >= 0) AND
(total_chick_qty IS NULL OR total_chick_qty >= 0)
);
CREATE INDEX idx_recordings_flock_datetime
ON recordings (project_flock_kandangs_id, record_datetime);
-- recording_bws reshape
ALTER TABLE recording_bws
RENAME COLUMN weight TO avg_weight;
ALTER TABLE recording_bws
ALTER COLUMN avg_weight TYPE NUMERIC(8,2) USING COALESCE(avg_weight, 0)::NUMERIC(8,2);
ALTER TABLE recording_bws
ADD COLUMN total_weight NUMERIC(10,3);
UPDATE recording_bws
SET total_weight = COALESCE(avg_weight, 0) * COALESCE(qty, 0);
ALTER TABLE recording_bws
ALTER COLUMN total_weight SET NOT NULL;
ALTER TABLE recording_bws
ALTER COLUMN qty TYPE NUMERIC(15,3) USING COALESCE(qty, 0)::NUMERIC(15,3);
ALTER TABLE recording_bws
DROP COLUMN IF EXISTS notes;
ALTER TABLE recording_bws
DROP CONSTRAINT IF EXISTS chk_recording_bws_nonneg;
ALTER TABLE recording_bws
ADD CONSTRAINT chk_recording_bws_nonneg CHECK (
avg_weight >= 0 AND qty >= 0 AND total_weight >= 0
);
-- recording_depletions reshape
ALTER TABLE recording_depletions
RENAME COLUMN total TO qty;
ALTER TABLE recording_depletions
ALTER COLUMN qty TYPE NUMERIC(15,3) USING COALESCE(qty, 0)::NUMERIC(15,3);
ALTER TABLE recording_depletions
DROP COLUMN IF EXISTS notes;
ALTER TABLE recording_depletions
DROP CONSTRAINT IF EXISTS chk_recording_depl_total;
ALTER TABLE recording_depletions
ADD CONSTRAINT chk_recording_depl_qty CHECK (qty >= 0);
-- recording_stocks reshape
ALTER TABLE recording_stocks
DROP CONSTRAINT IF EXISTS chk_recording_stocks_nonneg;
ALTER TABLE recording_stocks
DROP COLUMN IF EXISTS increase,
DROP COLUMN IF EXISTS decrease,
DROP COLUMN IF EXISTS usage_amount,
DROP COLUMN IF EXISTS notes;
ALTER TABLE recording_stocks
ADD COLUMN usage_qty NUMERIC(15,3),
ADD COLUMN pending_qty NUMERIC(15,3);
ALTER TABLE recording_stocks
ADD CONSTRAINT chk_recording_stocks_nonneg CHECK (
(usage_qty IS NULL OR usage_qty >= 0) AND
(pending_qty IS NULL OR pending_qty >= 0)
);
-- recording_eggs table
CREATE TABLE recording_eggs (
id BIGSERIAL PRIMARY KEY,
recording_id BIGINT NOT NULL,
product_warehouse_id BIGINT NOT NULL,
qty INT NOT NULL,
created_by BIGINT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT fk_recording_eggs_recording
FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE CASCADE,
CONSTRAINT fk_recording_eggs_product_warehouse
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id),
CONSTRAINT fk_recording_eggs_created_by
FOREIGN KEY (created_by) REFERENCES users(id),
CONSTRAINT chk_recording_eggs_qty CHECK (qty >= 0)
);
CREATE INDEX idx_recording_eggs_recording
ON recording_eggs (recording_id);
CREATE INDEX idx_recording_eggs_product
ON recording_eggs (product_warehouse_id);
-- grading_eggs table
CREATE TABLE grading_eggs (
id BIGSERIAL PRIMARY KEY,
recording_egg_id BIGINT NOT NULL,
qty NUMERIC(15,3) NOT NULL,
grade VARCHAR,
created_by BIGINT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT fk_grading_eggs_recording_egg
FOREIGN KEY (recording_egg_id) REFERENCES recording_eggs(id) ON DELETE CASCADE,
CONSTRAINT fk_grading_eggs_created_by
FOREIGN KEY (created_by) REFERENCES users(id),
CONSTRAINT chk_grading_eggs_qty CHECK (qty >= 0)
);
CREATE INDEX idx_grading_eggs_recording_egg
ON grading_eggs (recording_egg_id);
COMMIT;
@@ -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);
+262 -20
View File
@@ -35,6 +35,19 @@ func Run(db *gorm.DB) error {
return err
}
productCategories, err := seedProductCategories(tx, adminID)
if err != nil {
return err
}
if _, err := seedFlocks(tx, adminID); err != nil {
return err
}
if _, err := seedFcr(tx, adminID); err != nil {
return err
}
kandangs, err := seedKandangs(tx, adminID, locations, users)
if err != nil {
return err
@@ -44,11 +57,6 @@ func Run(db *gorm.DB) error {
return err
}
productCategories, err := seedProductCategories(tx, adminID)
if err != nil {
return err
}
suppliers, err := seedSuppliers(tx, adminID)
if err != nil {
return err
@@ -58,10 +66,6 @@ func Run(db *gorm.DB) error {
return err
}
if err := seedFcr(tx, adminID); err != nil {
return err
}
if err := seedProducts(tx, adminID, uoms, productCategories, suppliers); err != nil {
return err
}
@@ -74,6 +78,13 @@ func Run(db *gorm.DB) error {
return err
}
if err := seedProductWarehouse(tx, adminID); err != nil {
return err
}
if err := seedTransferStock(tx, adminID); err != nil {
return err
}
fmt.Println("✅ Master data seeding completed")
return nil
})
@@ -190,16 +201,47 @@ func seedLocations(tx *gorm.DB, createdBy uint, areas map[string]uint) (map[stri
return result, nil
}
func seedFlocks(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
names := []string{"Flock Priangan", "Flock Banten"}
result := make(map[string]uint, len(names))
for _, name := range names {
var flock entity.Flock
err := tx.Where("name = ?", name).First(&flock).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
flock = entity.Flock{
Name: name,
CreatedBy: createdBy,
}
if err := tx.Create(&flock).Error; err != nil {
return nil, err
}
} else if err != nil {
return nil, err
} else {
if err := tx.Model(&entity.Flock{}).Where("id = ?", flock.Id).Updates(map[string]any{
"created_by": createdBy,
}).Error; err != nil {
return nil, err
}
}
result[name] = flock.Id
}
return result, nil
}
func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users map[string]uint) (map[string]uint, error) {
seeds := []struct {
Name string
Status utils.KandangStatus
Location string
PicKey string
}{
{"Singaparna 1", "Singaparna", "admin"},
{"Singaparna 2", "Singaparna", "admin"},
{"Cikaum 1", "Cikaum", "admin"},
{"Cikaum 2", "Cikaum", "admin"},
{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"},
}
result := make(map[string]uint, len(seeds))
@@ -219,6 +261,7 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
if errors.Is(err, gorm.ErrRecordNotFound) {
kandang = entity.Kandang{
Name: seed.Name,
Status: string(seed.Status),
LocationId: locID,
PicId: picID,
CreatedBy: createdBy,
@@ -228,6 +271,15 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
}
} else if err != nil {
return nil, err
} else {
updates := map[string]any{
"location_id": locID,
"pic_id": picID,
"status": string(seed.Status),
}
if err := tx.Model(&entity.Kandang{}).Where("id = ?", kandang.Id).Updates(updates).Error; err != nil {
return nil, err
}
}
result[seed.Name] = kandang.Id
}
@@ -311,8 +363,10 @@ func seedProductCategories(tx *gorm.DB, createdBy uint) (map[string]uint, error)
Name string
Code string
}{
{"Pullet", "PLT"},
{"Bahan Baku", "RAW"},
{"Day Old Chick", "DOC"},
{"Telur", "EGG"},
}
result := make(map[string]uint, len(seeds))
@@ -426,7 +480,7 @@ func seedCustomers(tx *gorm.DB, createdBy uint, users map[string]uint) error {
return nil
}
func seedFcr(tx *gorm.DB, createdBy uint) error {
func seedFcr(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
seeds := []struct {
Name string
Standards []struct {
@@ -448,17 +502,20 @@ func seedFcr(tx *gorm.DB, createdBy uint) error {
},
}
result := make(map[string]uint, len(seeds))
for _, seed := range seeds {
var fcr entity.Fcr
err := tx.Where("name = ?", seed.Name).First(&fcr).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
fcr = entity.Fcr{Name: seed.Name, CreatedBy: createdBy}
if err := tx.Create(&fcr).Error; err != nil {
return err
return nil, err
}
} else if err != nil {
return err
return nil, err
}
result[seed.Name] = fcr.Id
for _, std := range seed.Standards {
var standard entity.FcrStandard
@@ -471,22 +528,22 @@ func seedFcr(tx *gorm.DB, createdBy uint) error {
Mortality: std.Mortality,
}
if err := tx.Create(&standard).Error; err != nil {
return err
return nil, err
}
} else if err != nil {
return err
return nil, err
} else {
if err := tx.Model(&entity.FcrStandard{}).Where("id = ?", standard.Id).Updates(map[string]any{
"fcr_number": std.FcrNumber,
"mortality": std.Mortality,
}).Error; err != nil {
return err
return nil, err
}
}
}
}
return nil
return result, nil
}
func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories map[string]uint, suppliers map[string]uint) error {
@@ -513,6 +570,56 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
Flags: []utils.FlagType{utils.FlagDOC},
},
{
Name: "Ayam Pullet",
Brand: "MBU Pullet",
Sku: "PLT0001",
Uom: "Ekor",
Category: "Pullet",
Price: 15000,
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
Flags: []utils.FlagType{utils.FlagPullet},
},
{
Name: "Ayam Afkir",
Brand: "-",
Sku: "1",
Uom: "Ekor",
Category: "Day Old Chick",
Price: 1,
},
{
Name: "Ayam Mati",
Brand: "-",
Sku: "2",
Uom: "Ekor",
Category: "Day Old Chick",
Price: 1,
},
{
Name: "Ayam Culling",
Brand: "-",
Sku: "3",
Uom: "Ekor",
Category: "Day Old Chick",
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",
Brand: "281 STARTER",
@@ -674,6 +781,8 @@ func seedNonstocks(tx *gorm.DB, createdBy uint, uoms map[string]uint, suppliers
return nil
}
// nanti saya isi
func seedFlags(tx *gorm.DB, flagableID uint, flagableType string, flags []utils.FlagType) error {
if len(flags) == 0 {
return nil
@@ -760,6 +869,139 @@ func seedBanks(tx *gorm.DB, createdBy uint) error {
return nil
}
func seedProductWarehouse(tx *gorm.DB, createdBy uint) error {
seeds := []struct {
ProductName string
WarehouseName string
Quantity float64
}{
{ProductName: "DOC Broiler", WarehouseName: "Gudang Priangan", Quantity: 100},
{ProductName: "281 SPECIAL STARTER", WarehouseName: "Gudang Singaparna", Quantity: 200},
{ProductName: "281 SPECIAL STARTER", WarehouseName: "Gudang Banten", Quantity: 300},
{ProductName: "DOC Broiler", WarehouseName: "Gudang Singaparna 1", Quantity: 5000},
{ProductName: "Telur Konsumsi Baik", WarehouseName: "Gudang Singaparna 1", Quantity: 600},
{ProductName: "Telur Pecah", WarehouseName: "Gudang Singaparna 1", Quantity: 80},
{ProductName: "Telur Konsumsi Baik", WarehouseName: "Gudang Cikaum 1", Quantity: 450},
{ProductName: "Telur Pecah", WarehouseName: "Gudang Cikaum 1", Quantity: 60},
}
for _, seed := range seeds {
var product entity.Product
if err := tx.Where("name = ?", seed.ProductName).First(&product).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("product %q not found for product warehouse seeding", seed.ProductName)
}
return err
}
var warehouse entity.Warehouse
if err := tx.Where("name = ?", seed.WarehouseName).First(&warehouse).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("warehouse %q not found for product warehouse seeding", seed.WarehouseName)
}
return err
}
var productWarehouse entity.ProductWarehouse
err := tx.Where("product_id = ? AND warehouse_id = ?", product.Id, warehouse.Id).First(&productWarehouse).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
productWarehouse = entity.ProductWarehouse{
ProductId: product.Id,
WarehouseId: warehouse.Id,
Quantity: seed.Quantity,
CreatedBy: createdBy,
}
if err := tx.Create(&productWarehouse).Error; err != nil {
return err
}
} else if err != nil {
return err
} else {
if err := tx.Model(&productWarehouse).Updates(map[string]any{
"quantity": seed.Quantity,
}).Error; err != nil {
return err
}
}
}
return nil
}
func seedTransferStock(tx *gorm.DB, createdBy uint) error {
transfer := entity.StockTransfer{
FromWarehouseId: 1,
ToWarehouseId: 2,
Reason: "Seed transfer stock",
TransferDate: time.Now(),
MovementNumber: "SEED-TRF-00001",
CreatedBy: 1,
}
if err := tx.Create(&transfer).Error; err != nil {
return err
}
details := []entity.StockTransferDetail{
{
StockTransferId: transfer.Id,
ProductId: 1,
Quantity: 10,
},
{
StockTransferId: transfer.Id,
ProductId: 2,
Quantity: 5,
},
}
for i := range details {
if err := tx.Create(&details[i]).Error; err != nil {
return err
}
}
deliveries := []entity.StockTransferDelivery{
{
StockTransferId: transfer.Id,
SupplierId: 1,
VehiclePlate: "B 1234 XYZ",
DriverName: "Driver Seed",
DocumentPath: "seed.pdf",
ShippingCostItem: 1000,
ShippingCostTotal: 2000,
},
}
for i := range deliveries {
if err := tx.Create(&deliveries[i]).Error; err != nil {
return err
}
}
detailMap := make(map[uint64]uint64)
for _, d := range details {
detailMap[d.ProductId] = d.Id
}
deliveryItems := []entity.StockTransferDeliveryItem{
{
StockTransferDeliveryId: deliveries[0].Id,
StockTransferDetailId: detailMap[1],
Quantity: 50,
},
{
StockTransferDeliveryId: deliveries[0].Id,
StockTransferDetailId: detailMap[2],
Quantity: 30,
},
}
for i := range deliveryItems {
if err := tx.Create(&deliveryItems[i]).Error; err != nil {
return err
}
}
return nil
}
func ptr[T any](v T) *T {
return &v
}
+22
View File
@@ -0,0 +1,22 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type ProjectChickinDetail struct {
Id uint `gorm:"primaryKey"`
ProjectChickinId uint `gorm:"column:project_chickin_id;not null"`
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
Quantity float64 `gorm:"type:numeric(15,3);not null"`
CreatedBy uint `gorm:"column:created_by;not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
ProjectChickin *ProjectChickin `gorm:"foreignKey:ProjectChickinId;references:Id"`
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
}
+28
View File
@@ -0,0 +1,28 @@
package entities
import (
"time"
)
type ApprovalAction string
const (
ApprovalActionApproved ApprovalAction = "APPROVED"
ApprovalActionRejected ApprovalAction = "REJECTED"
ApprovalActionCreated ApprovalAction = "CREATED"
ApprovalActionUpdated ApprovalAction = "UPDATED"
)
type Approval struct {
Id uint `gorm:"primaryKey"`
ApprovableType string `gorm:"size:50;not null;index:approvals_approvable_lookup,priority:1"`
ApprovableId uint `gorm:"not null;index:approvals_approvable_lookup,priority:2"`
StepNumber uint16 `gorm:"not null"`
StepName string `gorm:"not null"`
Action *ApprovalAction `gorm:"type:VARCHAR(20)"`
Notes *string `gorm:"type:text"`
ActionAt time.Time `gorm:"autoCreateTime"`
ActionBy *uint `gorm:"index"`
ActionUser *User `gorm:"foreignKey:ActionBy;references:Id"`
}
+17
View File
@@ -0,0 +1,17 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Flock struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:flocks_name_unique,where:deleted_at IS 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"`
}
+13 -12
View File
@@ -7,16 +7,17 @@ import (
)
type Kandang struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:kandangs_name_unique,where:deleted_at IS 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"`
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"`
}
+23
View File
@@ -0,0 +1,23 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type ProductWarehouse struct {
Id uint `gorm:"primaryKey;autoIncrement"`
ProductId uint `gorm:"not null"`
WarehouseId uint `gorm:"not null"`
Quantity float64 `gorm:"default:0"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
CreatedBy uint `gorm:"not null"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
// Relations
Product Product `gorm:"foreignKey:ProductId;references:Id"`
Warehouse Warehouse `gorm:"foreignKey:WarehouseId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
}
+27
View File
@@ -0,0 +1,27 @@
package entities
import (
"time"
"gorm.io/gorm"
)
const ()
type ProjectChickin struct {
Id uint `gorm:"primaryKey"`
ProjectFlockKandangId uint `gorm:"not null;index;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
ChickInDate time.Time `gorm:"not null"`
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"`
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
}
@@ -0,0 +1,24 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type ProjectFlockPopulation struct {
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"`
ProjectChickin *ProjectChickin `gorm:"foreignKey:ProjectChickinId;references:Id"`
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
}
+31
View File
@@ -0,0 +1,31 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type ProjectFlock struct {
Id uint `gorm:"primaryKey"`
FlockName string `gorm:"type:varchar(255);not null;uniqueIndex"`
AreaId uint `gorm:"not null"`
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"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Area Area `gorm:"foreignKey:AreaId;references:Id"`
Fcr Fcr `gorm:"foreignKey:FcrId;references:Id"`
Location Location `gorm:"foreignKey:LocationId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Kandangs []Kandang `gorm:"many2many:project_flock_kandangs;joinTableForeignKey:project_flock_id;joinTableReferences:kandang_id" json:"kandangs,omitempty"`
KandangHistory []ProjectFlockKandang `gorm:"foreignKey:ProjectFlockId;references:Id" json:"-"`
LatestApproval *Approval `gorm:"-" json:"-"`
}
+15
View File
@@ -0,0 +1,15 @@
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"`
Chickins []ProjectChickin `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
LatestApproval *Approval `gorm:"-" json:"-"`
}
+34
View File
@@ -0,0 +1,34 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Recording struct {
Id uint `gorm:"primaryKey"`
ProjectFlockKandangId uint `gorm:"column:project_flock_kandangs_id;not null;index"`
RecordDatetime time.Time `gorm:"column:record_datetime;not null"`
Day *int `gorm:"column:day"`
TotalDepletionQty *float64 `gorm:"column:total_depletion_qty"`
CumDepletionRate *float64 `gorm:"column:cum_depletion_rate"`
DailyGain *float64 `gorm:"column:daily_gain"`
AvgDailyGain *float64 `gorm:"column:avg_daily_gain"`
CumIntake *int `gorm:"column:cum_intake"`
FcrValue *float64 `gorm:"column:fcr_value"`
TotalChickQty *float64 `gorm:"column:total_chick_qty"`
CreatedBy uint `gorm:"column:created_by"`
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"`
BodyWeights []RecordingBW `gorm:"foreignKey:RecordingId;references:Id"`
Depletions []RecordingDepletion `gorm:"foreignKey:RecordingId;references:Id"`
Stocks []RecordingStock `gorm:"foreignKey:RecordingId;references:Id"`
Eggs []RecordingEgg `gorm:"foreignKey:RecordingId;references:Id"`
LatestApproval *Approval `gorm:"-" json:"-"`
}
+15
View File
@@ -0,0 +1,15 @@
package entities
import "time"
type RecordingBW struct {
Id uint `gorm:"primaryKey"`
RecordingId uint `gorm:"column:recording_id;not null;index"`
AvgWeight float64 `gorm:"column:avg_weight;not null"`
Qty float64 `gorm:"column:qty;not null"`
TotalWeight float64 `gorm:"column:total_weight;not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
}
+11
View File
@@ -0,0 +1,11 @@
package entities
type RecordingDepletion struct {
Id uint `gorm:"primaryKey"`
RecordingId uint `gorm:"column:recording_id;not null;index"`
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
Qty float64 `gorm:"column:qty;not null"`
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
}
+30
View File
@@ -0,0 +1,30 @@
package entities
import "time"
type RecordingEgg struct {
Id uint `gorm:"primaryKey"`
RecordingId uint `gorm:"column:recording_id;not null;index"`
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
Qty int `gorm:"column:qty;not null"`
CreatedBy uint `gorm:"column:created_by"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
GradingEggs []GradingEgg `gorm:"foreignKey:RecordingEggId;references:Id"`
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
}
type GradingEgg struct {
Id uint `gorm:"primaryKey"`
RecordingEggId uint `gorm:"column:recording_egg_id;not null;index"`
Qty float64 `gorm:"column:qty;not null"`
Grade string `gorm:"column:grade;type:varchar(50)"`
CreatedBy uint `gorm:"column:created_by"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
RecordingEgg RecordingEgg `gorm:"foreignKey:RecordingEggId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
}
+12
View File
@@ -0,0 +1,12 @@
package entities
type RecordingStock struct {
Id uint `gorm:"primaryKey"`
RecordingId uint `gorm:"column:recording_id;not null;index"`
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
UsageQty *float64 `gorm:"column:usage_qty"`
PendingQty *float64 `gorm:"column:pending_qty"`
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
}
+23
View File
@@ -0,0 +1,23 @@
package entities
import "time"
// HEADER
type StockTransfer struct {
Id uint64 `gorm:"primaryKey;autoIncrement"`
MovementNumber string `gorm:"uniqueIndex;not null"`
FromWarehouseId uint64
ToWarehouseId uint64
TransferDate time.Time
Reason string
CreatedBy uint64
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time `gorm:"index"`
// Relations
FromWarehouse *Warehouse `gorm:"foreignKey:FromWarehouseId"`
ToWarehouse *Warehouse `gorm:"foreignKey:ToWarehouseId"`
Details []StockTransferDetail `gorm:"foreignKey:StockTransferId"`
Deliveries []StockTransferDelivery `gorm:"foreignKey:StockTransferId"`
CreatedUser *User `gorm:"foreignKey:CreatedBy"`
}
+36
View File
@@ -0,0 +1,36 @@
package entities
import (
"time"
"gorm.io/gorm"
)
const (
LogTypeAdjustment = "ADJUSTMENT"
LogTypeTransfer = "TRANSFER"
)
const (
TransactionTypeIncrease = "INCREASE"
TransactionTypeDecrease = "DECREASE"
)
type StockLog struct {
Id uint `gorm:"primaryKey;column:id"`
TransactionType string `gorm:"type:varchar(20);not null"`
Quantity float64 `gorm:"type:numeric(15,3);not null"`
BeforeQuantity float64 `gorm:"type:numeric(15,3);not null"`
AfterQuantity float64 `gorm:"type:numeric(15,3);not null"`
LogType string `gorm:"type:varchar(50);not null;index:stock_logs_flaggable_lookup,priority:1"`
LogId uint `gorm:"not null;index:stock_logs_flaggable_lookup,priority:2"`
Note string `gorm:"type:text"`
ProductWarehouseId uint `gorm:"not null;index"`
CreatedBy uint `gorm:"index"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
ProductWarehouse *ProductWarehouse `json:"product_warehouse,omitempty" gorm:"foreignKey:ProductWarehouseId;references:Id"`
CreatedUser *User `json:"created_user,omitempty" gorm:"foreignKey:CreatedBy;references:Id"`
}
@@ -0,0 +1,23 @@
package entities
import "time"
// DETAIL EKSPEDISI
type StockTransferDelivery struct {
Id uint64 `gorm:"primaryKey;autoIncrement"`
StockTransferId uint64
SupplierId uint64
VehiclePlate string
DriverName string
DocumentNumber string
DocumentPath string
ShippingCostItem float64
ShippingCostTotal float64
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time `gorm:"index"`
// Relations
StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"`
Supplier *Supplier `gorm:"foreignKey:SupplierId"`
Items []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDeliveryId"`
}
@@ -0,0 +1,12 @@
package entities
// PIVOT TABLE TRANSFER
type StockTransferDeliveryItem struct {
Id uint64 `gorm:"primaryKey;autoIncrement"`
StockTransferDeliveryId uint64
StockTransferDetailId uint64
Quantity float64
// Relations
StockTransferDelivery *StockTransferDelivery `gorm:"foreignKey:StockTransferDeliveryId"`
StockTransferDetail *StockTransferDetail `gorm:"foreignKey:StockTransferDetailId"`
}
@@ -0,0 +1,18 @@
package entities
import "time"
// DETAIL PRODUK
type StockTransferDetail struct {
Id uint64 `gorm:"primaryKey;autoIncrement"`
StockTransferId uint64
ProductId uint64
Quantity float64
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time `gorm:"index"`
// Relations
StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"`
Product *Product `gorm:"foreignKey:ProductId"`
DeliveryItems []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDetailId"`
}
+82 -38
View File
@@ -1,55 +1,99 @@
package middleware
import (
"strings"
// import (
// "strings"
"gitlab.com/mbugroup/lti-api.git/internal/config"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
// "gitlab.com/mbugroup/lti-api.git/internal/config"
// service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
// "gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/gofiber/fiber/v2"
)
// "github.com/gofiber/fiber/v2"
// )
func Auth(userService service.UserService, requiredRights ...string) fiber.Handler {
return func(c *fiber.Ctx) error {
authHeader := c.Get("Authorization")
token := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer "))
// func Auth(userService service.UserService, requiredRights ...string) fiber.Handler {
// return func(c *fiber.Ctx) error {
// authHeader := c.Get("Authorization")
// token := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer "))
if token == "" {
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
// if token == "" {
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
// }
userID, err := utils.VerifyToken(token, config.JWTSecret, config.TokenTypeAccess)
if err != nil {
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
// userID, err := utils.VerifyToken(token, config.JWTSecret, config.TokenTypeAccess)
// if err != nil {
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
// }
user, err := userService.GetOne(c, userID)
if err != nil || user == nil {
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
// // Only end-user subjects are allowed by this middleware. Service tokens
// if verification.UserID == 0 {
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
// }
c.Locals("user", user)
// // Fail-closed on revocation check errors for stricter security posture.
// if revoker := session.GetRevocationStore(); revoker != nil {
// if fingerprint := session.TokenFingerprint(token); fingerprint != "" {
// revoked, err := revoker.IsRevoked(c.Context(), fingerprint)
// if err != nil {
// utils.Log.WithError(err).Warn("failed to check token revocation")
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
// }
// if revoked {
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
// }
// }
// }
// if len(requiredRights) > 0 {
// userRights, hasRights := config.RoleRights[user.Role]
// if (!hasRights || !hasAllRights(userRights, requiredRights)) && c.Params("userId") != userID {
// return fiber.NewError(fiber.StatusForbidden, "You don't have permission to access this resource")
// }
// }
// user, err := userService.GetBySSOUserID(c, verification.UserID)
// if err != nil || user == nil {
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
// }
return c.Next()
}
}
// if len(requiredRights) > 0 && verification.Claims != nil {
// if !hasAllScopes(verification.Claims.Scopes(), requiredRights) {
// return fiber.NewError(fiber.StatusForbidden, "Insufficient scope")
// }
// }
// func hasAllRights(userRights, requiredRights []string) bool {
// rightSet := make(map[string]struct{}, len(userRights))
// for _, right := range userRights {
// rightSet[right] = struct{}{}
// c.Locals("user", user)
// // if len(requiredRights) > 0 {
// // userRights, hasRights := config.RoleRights[user.Role]
// // if (!hasRights || !hasAllRights(userRights, requiredRights)) && c.Params("userId") != userID {
// // return fiber.NewError(fiber.StatusForbidden, "You don't have permission to access this resource")
// // }
// // }
// return c.Next()
// }
// }
// for _, right := range requiredRights {
// if _, exists := rightSet[right]; !exists {
// // bearerToken extracts a Bearer token from the Authorization header using
// // case-insensitive scheme matching and tolerant whitespace handling.
// func bearerToken(c *fiber.Ctx) string {
// parts := strings.Fields(c.Get("Authorization"))
// if len(parts) == 2 && strings.EqualFold(parts[0], "Bearer") {
// return strings.TrimSpace(parts[1])
// }
// return ""
// }
// func hasAllScopes(have, required []string) bool {
// if len(required) == 0 {
// return true
// }
// set := make(map[string]struct{}, len(have))
// for _, s := range have {
// s = strings.ToLower(strings.TrimSpace(s))
// if s != "" {
// set[s] = struct{}{}
// }
// }
// for _, r := range required {
// r = strings.ToLower(strings.TrimSpace(r))
// if r == "" {
// continue
// }
// if _, ok := set[r]; !ok {
// return false
// }
// }
@@ -0,0 +1,100 @@
package controller
import (
"math"
"strconv"
"strings"
"github.com/gofiber/fiber/v2"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
"gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
)
type ApprovalController struct {
ApprovalService common.ApprovalService
}
func NewApprovalController(approvalService common.ApprovalService) *ApprovalController {
return &ApprovalController{
ApprovalService: approvalService,
}
}
func (u *ApprovalController) GetAll(c *fiber.Ctx) error {
moduleName := strings.TrimSpace(c.Query("module_name", ""))
if moduleName == "" {
return fiber.NewError(fiber.StatusBadRequest, "`module_name` is required")
}
moduleIDParam := strings.TrimSpace(c.Query("module_id", ""))
var moduleID *uint
if moduleIDParam != "" {
value, err := strconv.ParseUint(moduleIDParam, 10, 64)
if err != nil || value == 0 {
return fiber.NewError(fiber.StatusBadRequest, "module_id must be a positive integer")
}
id := uint(value)
moduleID = &id
}
groupByStep := c.QueryBool("group_step_number", false)
page := c.QueryInt("page", 1)
limit := c.QueryInt("limit", 10)
search := strings.TrimSpace(c.Query("search", ""))
query := &validation.Query{
ModuleName: moduleName,
ModuleId: moduleID,
GroupByStep: groupByStep,
Page: page,
Limit: limit,
Search: search,
}
records, totalResults, err := u.ApprovalService.List(
c.Context(),
query.ModuleName,
query.ModuleId,
query.Page,
query.Limit,
query.Search,
)
if err != nil {
return err
}
if query.GroupByStep {
data := dto.ToApprovalGroupDTOs(records)
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.ApprovalGroupDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get All approvals successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: data,
})
}
flat := dto.ToApprovalDTOs(records)
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.ApprovalBaseDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get All approvals successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: flat,
})
}
@@ -0,0 +1,122 @@
package dto
import (
"sort"
"strings"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
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 ApprovalGroupDTO struct {
StepNumber uint16 `json:"step_number"`
StepName string `json:"step_name"`
Approvals []ApprovalBaseDTO `json:"approvals"`
}
func ToApprovalDTO(e entity.Approval) ApprovalBaseDTO {
dto := ApprovalBaseDTO{
Notes: e.Notes,
}
if e.StepNumber > 0 {
stepCopy := uint16(e.StepNumber)
dto.StepNumber = stepCopy
}
stepName := strings.TrimSpace(e.StepName)
if stepName == "" && e.ApprovableType != "" && e.StepNumber > 0 {
if label, ok := approvalutils.ApprovalStepName(approvalutils.ApprovalWorkflowKey(e.ApprovableType), approvalutils.ApprovalStep(e.StepNumber)); ok {
stepName = label
}
}
dto.StepName = stepName
if e.Action != nil {
value := strings.TrimSpace(string(*e.Action))
if value != "" {
valueCopy := value
dto.Action = &valueCopy
}
}
if e.ActionUser != nil && e.ActionUser.Id != 0 {
user := userDTO.ToUserBaseDTO(*e.ActionUser)
dto.ActionBy = user
} else if e.ActionBy != nil && *e.ActionBy != 0 {
dto.ActionBy = userDTO.UserBaseDTO{
Id: *e.ActionBy,
IdUser: int64(*e.ActionBy),
}
}
if !e.ActionAt.IsZero() {
at := e.ActionAt
dto.ActionAt = at
}
return dto
}
func ToApprovalDTOs(items []entity.Approval) []ApprovalBaseDTO {
result := make([]ApprovalBaseDTO, len(items))
for i, item := range items {
result[i] = ToApprovalDTO(item)
}
return result
}
func ToApprovalGroupDTOs(items []entity.Approval) []ApprovalGroupDTO {
if len(items) == 0 {
return nil
}
type groupAccumulator struct {
StepName string
Approvals []ApprovalBaseDTO
}
groups := make(map[uint16]*groupAccumulator)
order := make([]uint16, 0)
for _, item := range items {
step := item.StepNumber
acc, exists := groups[step]
if !exists {
stepName := strings.TrimSpace(item.StepName)
if stepName == "" && item.ApprovableType != "" && item.StepNumber > 0 {
if label, ok := approvalutils.ApprovalStepName(approvalutils.ApprovalWorkflowKey(item.ApprovableType), approvalutils.ApprovalStep(item.StepNumber)); ok {
stepName = label
}
}
acc = &groupAccumulator{StepName: stepName}
groups[step] = acc
order = append(order, step)
}
acc.Approvals = append(acc.Approvals, ToApprovalDTO(item))
}
sort.Slice(order, func(i, j int) bool { return order[i] < order[j] })
result := make([]ApprovalGroupDTO, len(order))
for i, step := range order {
acc := groups[step]
result[i] = ApprovalGroupDTO{
StepNumber: step,
StepName: acc.StepName,
Approvals: acc.Approvals,
}
}
return result
}
+25
View File
@@ -0,0 +1,25 @@
package approvals
import (
"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"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type ApprovalModule struct{}
func (ApprovalModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
approvalRepo := commonRepo.NewApprovalRepository(db)
userRepo := rUser.NewUserRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
userService := sUser.NewUserService(userRepo, validate)
ApprovalRoutes(router, userService, approvalService)
}
+19
View File
@@ -0,0 +1,19 @@
package approvals
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
"github.com/gofiber/fiber/v2"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/controllers"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
func ApprovalRoutes(v1 fiber.Router, u user.UserService, s common.ApprovalService) {
_ = u
ctrl := controller.NewApprovalController(s)
route := v1.Group("/approvals")
route.Get("/", ctrl.GetAll)
}
@@ -0,0 +1,10 @@
package validation
type Query struct {
ModuleName string `json:"module_name" validate:"required_strict"`
ModuleId *uint `json:"module_id,omitempty" validate:"omitempty,gt=0"`
GroupByStep bool `json:"group_by_step"`
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
Search string `query:"search" validate:"omitempty,max=50"`
}
@@ -1,9 +1,13 @@
package repository
import (
"sort"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
"gorm.io/gorm"
)
@@ -26,6 +30,50 @@ func (r *ConstantRepositoryImpl) GetConstants() map[string]interface{} {
for f := range utils.AllFlagTypes() {
flagList = append(flagList, string(f))
}
sort.Strings(flagList)
type approvalStepConstant struct {
StepNumber uint16 `json:"step_number"`
StepName string `json:"step_name"`
}
workflowConstants := approvalutils.WorkflowConstants()
workflowKeys := make([]string, 0, len(workflowConstants))
for key := range workflowConstants {
workflowKeys = append(workflowKeys, key)
}
sort.Strings(workflowKeys)
approvalWorkflows := make([]map[string]interface{}, 0, len(workflowKeys))
for _, key := range workflowKeys {
stepMap := workflowConstants[key]
if len(stepMap) == 0 {
continue
}
stepList := make([]approvalStepConstant, 0, len(stepMap))
for stepStr, label := range stepMap {
stepNum, err := strconv.ParseUint(stepStr, 10, 16)
if err != nil || stepNum == 0 {
continue
}
stepList = append(stepList, approvalStepConstant{
StepNumber: uint16(stepNum),
StepName: label,
})
}
if len(stepList) == 0 {
continue
}
sort.Slice(stepList, func(i, j int) bool {
return stepList[i].StepNumber < stepList[j].StepNumber
})
approvalWorkflows = append(approvalWorkflows, map[string]interface{}{
"key": key,
"steps": stepList,
})
}
return map[string]interface{}{
"flags": flagList,
@@ -34,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",
@@ -42,5 +94,6 @@ func (r *ConstantRepositoryImpl) GetConstants() map[string]interface{} {
"BISNIS",
"INDIVIDUAL",
},
"approval_workflows": approvalWorkflows,
}
}
@@ -0,0 +1,103 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type AdjustmentController struct {
AdjustmentService service.AdjustmentService
}
func NewAdjustmentController(adjustmentService service.AdjustmentService) *AdjustmentController {
return &AdjustmentController{
AdjustmentService: adjustmentService,
}
}
func (u *AdjustmentController) Adjustment(c *fiber.Ctx) error {
req := new(validation.Create)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
stockLog, err := u.AdjustmentService.Adjustment(c, req)
if err != nil {
return err
}
adjustmentDTO := dto.ToAdjustmentDetailDTO(stockLog)
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create adjustment successfully",
Data: adjustmentDTO,
})
}
func (u *AdjustmentController) AdjustmentHistory(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
ProductID: uint(c.QueryInt("product_id", 0)),
WarehouseID: uint(c.QueryInt("warehouse_id", 0)),
TransactionType: c.Query("transaction_type", ""),
}
result, totalResults, err := u.AdjustmentService.AdjustmentHistory(c, query)
if err != nil {
return err
}
// Convert to DTOs
adjustmentDTOs := make([]dto.AdjustmentDetailDTO, len(result))
for i, stockLog := range result {
adjustmentDTOs[i] = dto.ToAdjustmentDetailDTO(stockLog)
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.AdjustmentDetailDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get adjustment history successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: adjustmentDTOs,
})
}
func (u *AdjustmentController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
stockLog, err := u.AdjustmentService.GetOne(c, uint(id))
if err != nil {
return err
}
// Use DTO for response
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get adjustment successfully",
Data: dto.ToAdjustmentDetailDTO(stockLog),
})
}
@@ -0,0 +1,141 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
productCategoryDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === 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 WarehouseBaseDTO 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"`
}
type AdjustmentBaseDTO struct {
Id uint `json:"id"`
TransactionType string `json:"transaction_type"`
Quantity float64 `json:"quantity"`
BeforeQuantity float64 `json:"before_quantity"`
AfterQuantity float64 `json:"after_quantity"`
Note string `json:"note,omitempty"`
ProductWarehouseId uint `json:"product_warehouse_id"`
ProductWarehouse *ProductWarehouseDTO `json:"product_warehouse,omitempty"`
}
type AdjustmentListDTO struct {
AdjustmentBaseDTO
CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
type AdjustmentDetailDTO struct {
AdjustmentListDTO
UpdatedAt time.Time `json:"updated_at"`
}
// === Mapper Functions ===
func ToProductBaseDTO(e *entity.Product) *ProductBaseDTO {
if e == nil {
return nil
}
sku := ""
if e.Sku != nil {
sku = *e.Sku
}
var category *productCategoryDTO.ProductCategoryBaseDTO
if e.ProductCategory.Id != 0 {
mapped := productCategoryDTO.ToProductCategoryBaseDTO(e.ProductCategory)
category = &mapped
}
return &ProductBaseDTO{
Id: e.Id,
Name: e.Name,
SKU: sku,
ProductCategory: category,
}
}
func ToWarehouseBaseDTO(e *entity.Warehouse) *WarehouseBaseDTO {
if e == nil {
return nil
}
return &WarehouseBaseDTO{
Id: e.Id,
Name: e.Name,
}
}
func ToProductWarehouseDTO(e *entity.ProductWarehouse) *ProductWarehouseDTO {
if e == nil {
return nil
}
return &ProductWarehouseDTO{
Id: e.Id,
ProductId: e.ProductId,
WarehouseId: e.WarehouseId,
Quantity: e.Quantity,
Product: ToProductBaseDTO(&e.Product),
Warehouse: ToWarehouseBaseDTO(&e.Warehouse),
}
}
func ToAdjustmentBaseDTO(e *entity.StockLog) AdjustmentBaseDTO {
return AdjustmentBaseDTO{
Id: e.Id,
TransactionType: e.TransactionType,
Quantity: e.Quantity,
BeforeQuantity: e.BeforeQuantity,
AfterQuantity: e.AfterQuantity,
Note: e.Note,
ProductWarehouseId: e.ProductWarehouseId,
ProductWarehouse: ToProductWarehouseDTO(e.ProductWarehouse),
}
}
func ToAdjustmentListDTO(e *entity.StockLog) AdjustmentListDTO {
var createdUser *userDTO.UserBaseDTO
if e.CreatedUser != nil {
createdUser = &userDTO.UserBaseDTO{
Id: e.CreatedUser.Id,
IdUser: e.CreatedUser.IdUser,
Email: e.CreatedUser.Email,
Name: e.CreatedUser.Name,
}
}
return AdjustmentListDTO{
AdjustmentBaseDTO: ToAdjustmentBaseDTO(e),
CreatedUser: createdUser,
CreatedAt: e.CreatedAt,
}
}
func ToAdjustmentDetailDTO(e *entity.StockLog) AdjustmentDetailDTO {
return AdjustmentDetailDTO{
AdjustmentListDTO: ToAdjustmentListDTO(e),
UpdatedAt: e.UpdatedAt,
}
}
@@ -0,0 +1,31 @@
package adjustments
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
sAdjustment "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/services"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rproduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type AdjustmentModule struct{}
func (AdjustmentModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
stockLogsRepo := rStockLogs.NewStockLogRepository(db)
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
userRepo := rUser.NewUserRepository(db)
productRepo := rproduct.NewProductRepository(db)
adjustmentService := sAdjustment.NewAdjustmentService(productRepo, stockLogsRepo, warehouseRepo, productWarehouseRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
AdjustmentRoutes(router, userService, adjustmentService)
}
@@ -0,0 +1,22 @@
package adjustments
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/controllers"
adjustment "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func AdjustmentRoutes(v1 fiber.Router, u user.UserService, s adjustment.AdjustmentService) {
ctrl := controller.NewAdjustmentController(s)
route := v1.Group("/adjustments")
// Standard CRUD routes following master data pattern
route.Get("/", ctrl.AdjustmentHistory) // Get all with pagination and filters
route.Post("/", ctrl.Adjustment) // Create adjustment
route.Get("/:id", ctrl.GetOne)
}
@@ -0,0 +1,221 @@
package service
import (
"errors"
"strings"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/validations"
ProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
productRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
warehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
stockLogsRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gorm.io/gorm"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
)
type AdjustmentService interface {
Adjustment(ctx *fiber.Ctx, req *validation.Create) (*entity.StockLog, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.StockLog, error)
AdjustmentHistory(ctx *fiber.Ctx, query *validation.Query) ([]*entity.StockLog, int64, error)
}
type adjustmentService struct {
Log *logrus.Logger
Validate *validator.Validate
StockLogsRepository stockLogsRepo.StockLogRepository
WarehouseRepo warehouseRepo.WarehouseRepository
ProductWarehouseRepo ProductWarehouse.ProductWarehouseRepository
ProductRepo productRepo.ProductRepository
}
func NewAdjustmentService(productRepo productRepo.ProductRepository, stockLogsRepo stockLogsRepo.StockLogRepository, warehouseRepo warehouseRepo.WarehouseRepository, productWarehouseRepo ProductWarehouse.ProductWarehouseRepository, validate *validator.Validate) AdjustmentService {
return &adjustmentService{
Log: utils.Log,
Validate: validate,
StockLogsRepository: stockLogsRepo,
WarehouseRepo: warehouseRepo,
ProductWarehouseRepo: productWarehouseRepo,
ProductRepo: productRepo,
}
}
func (s *adjustmentService) withRelations(db *gorm.DB) *gorm.DB {
return db.
Preload("ProductWarehouse").
Preload("ProductWarehouse.Product").
Preload("ProductWarehouse.Warehouse").
Preload("CreatedUser")
}
func (s *adjustmentService) GetOne(c *fiber.Ctx, id uint) (*entity.StockLog, error) {
stockLog, err := s.StockLogsRepository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
return s.withRelations(db).Preload("ProductWarehouse.Product.ProductCategory")
})
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Adjustment not found")
}
s.Log.Errorf("Failed to get adjustment by id: %+v", err)
return nil, err
}
if stockLog.LogType != entity.LogTypeAdjustment {
return nil, fiber.NewError(fiber.StatusNotFound, "Adjustment not found")
}
return stockLog, nil
}
func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*entity.StockLog, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
ctx := c.Context()
if err := common.EnsureRelations(c.Context(),
common.RelationCheck{Name: "Product", ID: &req.ProductID, Exists: s.ProductRepo.IdExists},
common.RelationCheck{Name: "Warehouse", ID: &req.WarehouseID, Exists: s.WarehouseRepo.IdExists},
); err != nil {
return nil, err
}
if req.Quantity <= 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Quantity must be greater than zero")
}
transactionType := strings.ToUpper(req.TransactionType)
if transactionType != entity.TransactionTypeIncrease && transactionType != entity.TransactionTypeDecrease {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transaction type")
}
var createdLogId uint
isProductWarehouseExist, err := s.ProductWarehouseRepo.ProductWarehouseExistByProductAndWarehouseID(ctx, uint(req.ProductID), uint(req.WarehouseID))
if err != nil {
s.Log.Errorf("Failed to check product warehouse existence: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product warehouse")
}
if !isProductWarehouseExist {
newPW := &entity.ProductWarehouse{
ProductId: uint(req.ProductID),
WarehouseId: uint(req.WarehouseID),
Quantity: 0,
CreatedBy: 1, // TODO: should Get from auth middleware
}
if err := s.ProductWarehouseRepo.CreateOne(ctx, newPW, nil); err != nil {
s.Log.Errorf("Failed to create product warehouse: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create product warehouse")
}
s.Log.Infof("Product warehouse created: %+v", newPW.Id)
}
err = s.StockLogsRepository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
productWarehouse, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(ctx, uint(req.ProductID), uint(req.WarehouseID))
if err != nil {
s.Log.Errorf("Failed to get product warehouse: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
}
afterQuantity := productWarehouse.Quantity
if transactionType == entity.TransactionTypeIncrease {
afterQuantity += req.Quantity
} else {
if productWarehouse.Quantity < req.Quantity {
return fiber.NewError(fiber.StatusBadRequest, "Insufficient stock for adjustment")
}
afterQuantity -= req.Quantity
}
newLog := &entity.StockLog{
TransactionType: transactionType,
Quantity: req.Quantity,
BeforeQuantity: productWarehouse.Quantity,
AfterQuantity: afterQuantity,
LogType: entity.LogTypeAdjustment,
LogId: 0,
Note: req.Note,
ProductWarehouseId: productWarehouse.Id,
CreatedBy: 1, // TODO: should Get from auth middleware
}
if err := s.StockLogsRepository.WithTx(tx).CreateOne(ctx, newLog, nil); err != nil {
s.Log.Errorf("Failed to create stock log: %+v", err)
return err
}
productWarehouse.Quantity = afterQuantity
if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(ctx, productWarehouse.Id, productWarehouse, nil); err != nil {
s.Log.Errorf("Failed to update product warehouse quantity: %+v", err)
return err
}
createdLogId = newLog.Id
return nil
})
if err != nil {
s.Log.Errorf("Transaction failed in CreateOne: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to process adjustment transaction")
}
return s.GetOne(c, createdLogId)
}
func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Query) ([]*entity.StockLog, int64, error) {
if err := s.Validate.Struct(query); err != nil {
return nil, 0, err
}
offset := (query.Page - 1) * query.Limit
isWarehousesExist, err := s.WarehouseRepo.IdExists(c.Context(), uint(query.WarehouseID))
if err != nil {
s.Log.Errorf("Failed to check warehouse existence: %+v", err)
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate warehouse")
}
if query.WarehouseID > 0 && !isWarehousesExist {
return nil, 0, fiber.NewError(fiber.StatusNotFound, "Warehouse not found")
}
isProductsExist, err := s.ProductRepo.IdExists(c.Context(), uint(query.ProductID))
if err != nil {
s.Log.Errorf("Failed to check product existence: %+v", err)
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product")
}
if query.ProductID > 0 && !isProductsExist {
return nil, 0, fiber.NewError(fiber.StatusNotFound, "Product not found")
}
stockLogs, total, err := s.StockLogsRepository.GetAll(c.Context(), offset, query.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
db = db.Where("log_type = ?", entity.LogTypeAdjustment)
if query.TransactionType != "" {
db = db.Where("transaction_type = ?", strings.ToUpper(query.TransactionType))
}
db = s.StockLogsRepository.ApplyProductWarehouseFilters(db, uint(query.ProductID), uint(query.WarehouseID))
return db.Order("created_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get adjustments: %+v", err)
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to get adjustment history")
}
result := make([]*entity.StockLog, len(stockLogs))
for i, v := range stockLogs {
result[i] = &v
}
return result, total, nil
}
@@ -0,0 +1,17 @@
package validation
type Create struct {
ProductID uint `json:"product_id" validate:"required"`
WarehouseID uint `json:"warehouse_id" validate:"required"`
TransactionType string `json:"transaction_type" validate:"required,oneof=increase decrease"`
Quantity float64 `json:"quantity" validate:"required,gt=0"`
Note string `json:"note" validate:"omitempty,max=255"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,min=1"`
Limit int `query:"limit" validate:"omitempty,min=1,max=100"`
ProductID uint `query:"product_id" validate:"omitempty,min=0"`
WarehouseID uint `query:"warehouse_id" validate:"omitempty,min=0"`
TransactionType string `query:"transaction_type" validate:"omitempty,oneof=increase decrease"`
}
+13
View File
@@ -0,0 +1,13 @@
package inventory
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
)
type InventoryModule struct{}
func (InventoryModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
RegisterRoutes(router, db, validate)
}
@@ -0,0 +1,78 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type ProductWarehouseController struct {
ProductWarehouseService service.ProductWarehouseService
}
func NewProductWarehouseController(productWarehouseService service.ProductWarehouseService) *ProductWarehouseController {
return &ProductWarehouseController{
ProductWarehouseService: productWarehouseService,
}
}
func (u *ProductWarehouseController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
ProductId: uint(c.QueryInt("product_id", 0)),
WarehouseId: uint(c.QueryInt("warehouse_id", 0)),
Flags: c.Query("flags", ""),
}
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.ProductWarehouseService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.ProductWarehouseListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all productWarehouses successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToProductWarehouseListDTOs(result),
})
}
func (u *ProductWarehouseController) 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.ProductWarehouseService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get productWarehouse successfully",
Data: dto.ToProductWarehouseListDTO(*result),
})
}
@@ -0,0 +1,178 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
)
// === DTO Structs ===
type ProductWarehouseBaseDTO 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 UserBaseDTO struct {
Id uint `json:"id"`
Username string `json:"username"`
}
type ProductWarehouseDetailDTO struct {
ProductWarehouseListDTO
}
// Nested DTOs for relations
type ProductBaseDTO 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 KandangBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type LocationBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type AreaBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
// === Mapper Functions ===
func ToProductWarehouseBaseDTO(e entity.ProductWarehouse) ProductWarehouseBaseDTO {
return ProductWarehouseBaseDTO{
Id: e.Id,
ProductId: e.ProductId, // Field yang benar dari entity
WarehouseId: e.WarehouseId, // Field yang benar dari entity
Quantity: e.Quantity,
}
}
func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDTO {
dto := ProductWarehouseListDTO{
ProductWarehouseBaseDTO: ToProductWarehouseBaseDTO(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)
}
}
dto.Product = &product
}
// Map Warehouse relation jika ada
if e.Warehouse.Id != 0 {
warehouse := WarehouseBaseDTO{
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{
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{
Id: e.Warehouse.Location.Id,
Name: e.Warehouse.Location.Name,
}
}
if &e.Warehouse.Area != nil && e.Warehouse.Area.Id != 0 {
warehouse.Area = &AreaBaseDTO{
Id: e.Warehouse.Area.Id,
Name: e.Warehouse.Area.Name,
}
}
dto.Warehouse = &warehouse
}
// Map CreatedUser relation jika ada
if e.CreatedUser.Id != 0 {
user := UserBaseDTO{
Id: e.CreatedUser.Id,
Username: e.CreatedUser.Name,
}
dto.CreatedUser = &user
}
return dto
}
func ToProductWarehouseListDTOs(e []entity.ProductWarehouse) []ProductWarehouseListDTO {
result := make([]ProductWarehouseListDTO, len(e))
for i, r := range e {
result[i] = ToProductWarehouseListDTO(r)
}
return result
}
func ToProductWarehouseDetailDTO(e entity.ProductWarehouse) ProductWarehouseDetailDTO {
return ProductWarehouseDetailDTO{
ProductWarehouseListDTO: ToProductWarehouseListDTO(e),
}
}
func ToKandangBaseDTO(e entity.Kandang) KandangBaseDTO {
return KandangBaseDTO{
Id: e.Id,
Name: e.Name,
}
}
func ToLocationBaseDTO(e entity.Location) LocationBaseDTO {
return LocationBaseDTO{
Id: e.Id,
Name: e.Name,
}
}
func ToAreaBaseDTO(e entity.Area) AreaBaseDTO {
return AreaBaseDTO{
Id: e.Id,
Name: e.Name,
}
}
@@ -0,0 +1,26 @@
package productWarehouses
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
sProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type ProductWarehouseModule struct{}
func (ProductWarehouseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
userRepo := rUser.NewUserRepository(db)
productWarehouseService := sProductWarehouse.NewProductWarehouseService(productWarehouseRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
ProductWarehouseRoutes(router, userService, productWarehouseService)
}
@@ -0,0 +1,189 @@
package repository
import (
"context"
"fmt"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type ProductWarehouseRepository interface {
repository.BaseRepository[entity.ProductWarehouse]
ProductWarehouseExists(ctx context.Context, productId, warehouseId uint, excludeID *uint) (bool, error)
IsProductExist(ctx context.Context, productId uint) (bool, error)
IsWarehouseExist(ctx context.Context, warehouseId uint) (bool, error)
ProductWarehouseExistByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (bool, error)
ExistsByID(ctx context.Context, id uint) (bool, error)
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
}
type ProductWarehouseRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.ProductWarehouse]
}
func NewProductWarehouseRepository(db *gorm.DB) ProductWarehouseRepository {
return &ProductWarehouseRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProductWarehouse](db),
}
}
func (r *ProductWarehouseRepositoryImpl) IsProductExist(ctx context.Context, productId uint) (bool, error) {
return repository.Exists[entity.Product](ctx, r.DB(), productId)
}
func (r *ProductWarehouseRepositoryImpl) IsWarehouseExist(ctx context.Context, warehouseId uint) (bool, error) {
return repository.Exists[entity.Warehouse](ctx, r.DB(), warehouseId)
}
func (r *ProductWarehouseRepositoryImpl) ExistsByID(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{}).
Where("product_id = ? AND warehouse_id = ?", productId, warehouseId)
if excludeID != nil {
query = query.Where("id != ?", *excludeID)
}
if err := query.Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExistByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (bool, error) {
var count int64
if err := r.DB().WithContext(ctx).
Model(&entity.ProductWarehouse{}).
Where("product_id = ? AND warehouse_id = ?", productId, warehouseId).
Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
func (r *ProductWarehouseRepositoryImpl) GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error) {
var productWarehouse entity.ProductWarehouse
if err := r.DB().WithContext(ctx).Where("product_id = ? AND warehouse_id = ?", productId, warehouseId).First(&productWarehouse).Error; err != nil {
return nil, err
}
return &productWarehouse, nil
}
func (r *ProductWarehouseRepositoryImpl) GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error) {
var productWarehouses []entity.ProductWarehouse
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")
// preload relations so nested Product and Warehouse are populated
err := q.Preload("Product").Preload("Warehouse").Find(&productWarehouses).Error
if err != nil {
return nil, err
}
return productWarehouses, nil
}
func (r *ProductWarehouseRepositoryImpl) GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint, db *gorm.DB) (*entity.ProductWarehouse, error) {
var productWarehouse entity.ProductWarehouse
query := r.DB()
if db != nil {
query = db
}
fmt.Println(warehouseId)
err := query.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").
Preload("Product").Preload("Warehouse").
First(&productWarehouse).Error
if err != nil {
return nil, err
}
return &productWarehouse, nil
}
func (r *ProductWarehouseRepositoryImpl) ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB {
if len(flags) == 0 {
return db
}
return db.
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 IN ?", flags)
}
func (r *ProductWarehouseRepositoryImpl) AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error {
if len(deltas) == 0 {
return nil
}
base := r.DB().WithContext(ctx)
if modifier != nil {
base = modifier(base)
}
for id, delta := range deltas {
if delta == 0 {
continue
}
if err := base.Model(&entity.ProductWarehouse{}).
Where("id = ?", id).
Update("quantity", gorm.Expr("COALESCE(quantity,0) + ?", delta)).Error; err != nil {
return err
}
}
return 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
}
@@ -0,0 +1,26 @@
package productWarehouses
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/controllers"
productWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func ProductWarehouseRoutes(v1 fiber.Router, u user.UserService, s productWarehouse.ProductWarehouseService) {
ctrl := controller.NewProductWarehouseController(s)
route := v1.Group("/product-warehouses")
// route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll)
route.Get("/:id", ctrl.GetOne)
}
@@ -0,0 +1,109 @@
package service
import (
"errors"
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"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type ProductWarehouseService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProductWarehouse, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProductWarehouse, error)
}
type productWarehouseService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.ProductWarehouseRepository
}
func NewProductWarehouseService(repo repository.ProductWarehouseRepository, validate *validator.Validate) ProductWarehouseService {
return &productWarehouseService{
Log: utils.Log,
Validate: validate,
Repository: repo,
}
}
func (s productWarehouseService) withRelations(db *gorm.DB) *gorm.DB {
return db.
Preload("Product.Flags").
Preload("Product").
Preload("Warehouse").
Preload("Warehouse.Location").
Preload("Warehouse.Area").
Preload("Warehouse.Kandang").
Preload("CreatedUser")
}
func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProductWarehouse, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
if params.ProductId > 0 {
isProductExist, err := s.Repository.IsProductExist(c.Context(), params.ProductId)
if err != nil {
return nil, 0, err
}
if !isProductExist {
return nil, 0, fiber.NewError(fiber.StatusNotFound, "Product not found")
}
}
if params.WarehouseId > 0 {
isWarehouseExist, err := s.Repository.IsWarehouseExist(c.Context(), params.WarehouseId)
if err != nil {
return nil, 0, err
}
if !isWarehouseExist {
return nil, 0, fiber.NewError(fiber.StatusNotFound, "Warehouse not found")
}
}
offset := (params.Page - 1) * params.Limit
cleanFlags := utils.ParseFlags(params.Flags)
productWarehouses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.ProductId != 0 {
db = db.Where("product_id = ?", params.ProductId)
}
if params.WarehouseId != 0 {
db = db.Where("warehouse_id = ?", params.WarehouseId)
}
db = s.Repository.ApplyFlagsFilter(db, cleanFlags)
return db.Order("created_at DESC").Order("updated_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get productWarehouses: %+v", err)
return nil, 0, err
}
return productWarehouses, total, nil
}
func (s productWarehouseService) GetOne(c *fiber.Ctx, id uint) (*entity.ProductWarehouse, error) {
productWarehouse, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "ProductWarehouse not found")
}
if err != nil {
s.Log.Errorf("Failed get productWarehouse by id: %+v", err)
return nil, err
}
return productWarehouse, nil
}
@@ -0,0 +1,21 @@
package validation
type Create struct {
ProductId uint `json:"product_id" validate:"required,number,min=1"`
WarehouseId uint `json:"warehouse_id" validate:"required,number,min=1"`
Quantity float64 `json:"quantity" validate:"required,number,min=0"`
}
type Update struct {
ProductId *uint `json:"product_id,omitempty" validate:"omitempty,number,min=1"`
WarehouseId *uint `json:"warehouse_id,omitempty" validate:"omitempty,number,min=1"`
Quantity *float64 `json:"quantity,omitempty" validate:"omitempty,number,min=0"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
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"`
}
+30
View File
@@ -0,0 +1,30 @@
package inventory
import (
"gitlab.com/mbugroup/lti-api.git/internal/modules"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
productWarehouses "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses"
adjustments "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments"
transfers "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers"
// MODULE IMPORTS
)
func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
group := router.Group("/inventory")
allModules := []modules.Module{
productWarehouses.ProductWarehouseModule{},
adjustments.AdjustmentModule{},
transfers.TransferModule{},
// MODULE REGISTRY
}
for _, m := range allModules {
m.RegisterRoutes(group, db, validate)
}
}
@@ -0,0 +1,103 @@
package controller
import (
"encoding/json"
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type TransferController struct {
TransferService service.TransferService
}
func NewTransferController(transferService service.TransferService) *TransferController {
return &TransferController{
TransferService: transferService,
}
}
func (u *TransferController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
}
result, totalResults, err := u.TransferService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.TransferListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all transfers successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToTransferListDTOs(result),
})
}
func (u *TransferController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
result, err := u.TransferService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get transfer successfully",
Data: dto.ToTransferListDTO(*result),
})
}
func (u *TransferController) CreateOne(c *fiber.Ctx) error {
data := c.FormValue("data")
var req validation.TransferRequest
if err := json.Unmarshal([]byte(data), &req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
// ambil file
form, err := c.MultipartForm()
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form")
}
_ = form.File["documents"]
// todo: tunggu ada aws baru proses
result, err := u.TransferService.CreateOne(c, &req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create transfer successfully",
Data: dto.ToTransferListDTO(*result),
})
}
@@ -0,0 +1,245 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type TransferBaseDTO struct {
Id uint64 `json:"id"`
TransferReason string `json:"transfer_reason"`
TransferDate string `json:"transfer_date"`
SourceWarehouse *WarehouseDetailDTO `json:"source_warehouse,omitempty"`
DestinationWarehouse *WarehouseDetailDTO `json:"destination_warehouse,omitempty"`
}
// Only id and name for warehouse simple view
type WarehouseSimpleDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type ProductSimpleDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type AreaDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type LocationDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type SupplierSimpleDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type WarehouseDetailDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Location *LocationDTO `json:"location"`
Area *AreaDTO `json:"area"`
}
type TransferListDTO struct {
TransferBaseDTO
CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Details []TransferDetailItemDTO `json:"details"`
Deliveries []TransferDeliveryDTO `json:"deliveries"`
}
type TransferDetailDTO struct {
TransferListDTO
Details []TransferDetailItemDTO `json:"details"`
Deliveries []TransferDeliveryDTO `json:"deliveries"`
}
// Detail produk
type TransferDetailItemDTO struct {
Id uint64 `json:"id"`
Proudct ProductSimpleDTO `json:"product"`
Quantity float64 `json:"quantity"`
}
// Delivery ekspedisi
type TransferDeliveryDTO struct {
Id uint64 `json:"id"`
Supplier SupplierSimpleDTO `json:"supplier"`
VehiclePlate string `json:"vehicle_plate"`
DriverName string `json:"driver_name"`
DocumentNumber string `json:"document_number"`
DocumentPath string `json:"document_path"`
ShippingCostItem float64 `json:"shipping_cost_item"`
ShippingCostTotal float64 `json:"shipping_cost_total"`
Items []TransferDeliveryItemDTO `json:"items"`
}
type TransferDeliveryItemDTO struct {
Id uint64 `json:"id"`
StockTransferDetailId uint64 `json:"stock_transfer_detail_id"`
Quantity float64 `json:"quantity"`
}
// === Mapper Functions ===
func ToTransferBaseDTO(e entity.StockTransfer) TransferBaseDTO {
var sourceWarehouse *WarehouseDetailDTO
if e.FromWarehouse != nil && e.FromWarehouse.Id != 0 {
sourceWarehouse = toWarehouseDetailDTO(e.FromWarehouse)
}
var destinationWarehouse *WarehouseDetailDTO
if e.ToWarehouse != nil && e.ToWarehouse.Id != 0 {
destinationWarehouse = toWarehouseDetailDTO(e.ToWarehouse)
}
return TransferBaseDTO{
Id: e.Id,
TransferReason: e.Reason,
TransferDate: e.CreatedAt.Format("2006-01-02"),
SourceWarehouse: sourceWarehouse,
DestinationWarehouse: destinationWarehouse,
}
}
func toAreaDTO(a *entity.Area) *AreaDTO {
if a == nil {
return nil
}
return &AreaDTO{
Id: a.Id,
Name: a.Name,
}
}
func toLocationDTO(l *entity.Location) *LocationDTO {
if l == nil {
return nil
}
return &LocationDTO{
Id: l.Id,
Name: l.Name,
}
}
func toWarehouseDetailDTO(w *entity.Warehouse) *WarehouseDetailDTO {
if w == nil {
return nil
}
return &WarehouseDetailDTO{
Id: w.Id,
Name: w.Name,
Location: toLocationDTO(w.Location),
Area: toAreaDTO(&w.Area), // Ambil area langsung dari warehouse (area_id)
}
}
func ToTransferListDTO(e entity.StockTransfer) TransferListDTO {
var createdUser *userDTO.UserBaseDTO
if e.CreatedUser != nil {
mapped := userDTO.ToUserBaseDTO(*e.CreatedUser)
createdUser = &mapped
}
// Map details
var details []TransferDetailItemDTO
for _, d := range e.Details {
details = append(details, TransferDetailItemDTO{
Id: d.Id,
Proudct: ProductSimpleDTO{
Id: d.Product.Id,
Name: d.Product.Name,
},
Quantity: d.Quantity,
})
}
// Map deliveries
var deliveries []TransferDeliveryDTO
for _, del := range e.Deliveries {
// Map delivery items
var items []TransferDeliveryItemDTO
for _, item := range del.Items {
items = append(items, TransferDeliveryItemDTO{
Id: item.Id,
StockTransferDetailId: item.StockTransferDetailId,
Quantity: item.Quantity,
})
}
deliveries = append(deliveries, TransferDeliveryDTO{
Id: del.Id,
Supplier: SupplierSimpleDTO{
Id: del.Supplier.Id,
Name: del.Supplier.Name,
},
VehiclePlate: del.VehiclePlate,
DriverName: del.DriverName,
DocumentNumber: del.DocumentNumber,
DocumentPath: del.DocumentPath,
ShippingCostItem: del.ShippingCostItem,
ShippingCostTotal: del.ShippingCostTotal,
Items: items,
})
}
return TransferListDTO{
TransferBaseDTO: ToTransferBaseDTO(e),
CreatedUser: createdUser,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
Details: details,
Deliveries: deliveries,
}
}
func ToTransferListDTOs(e []entity.StockTransfer) []TransferListDTO {
result := make([]TransferListDTO, len(e))
for i, r := range e {
result[i] = ToTransferListDTO(r)
}
return result
}
func ToTransferDetailDTO(e entity.StockTransfer) TransferDetailDTO {
// Map details
var details []TransferDetailItemDTO
for _, d := range e.Details {
details = append(details, TransferDetailItemDTO{
Id: d.Id,
Proudct: ProductSimpleDTO{
Id: d.Product.Id,
Name: d.Product.Name,
},
Quantity: d.Quantity,
})
}
// Map deliveries
var deliveries []TransferDeliveryDTO
for _, del := range e.Deliveries {
deliveries = append(deliveries, TransferDeliveryDTO{
Id: del.Id,
Supplier: SupplierSimpleDTO{
Id: del.Supplier.Id,
Name: del.Supplier.Name,
},
VehiclePlate: del.VehiclePlate,
DriverName: del.DriverName,
DocumentNumber: del.DocumentNumber,
DocumentPath: del.DocumentPath,
ShippingCostItem: del.ShippingCostItem,
ShippingCostTotal: del.ShippingCostTotal,
})
}
return TransferDetailDTO{
TransferListDTO: ToTransferListDTO(e),
Details: details,
Deliveries: deliveries,
}
}
@@ -0,0 +1,33 @@
package transfers
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories"
sTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services"
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type TransferModule struct{}
func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
stockTransferRepo := rStockTransfer.NewStockTransferRepository(db)
stockTransferDetailRepo := rStockTransfer.NewStockTransferDetailRepository(db)
stockTransferDeliveryRepo := rStockTransfer.NewStockTransferDeliveryRepository(db)
StockTransferDeliveryItemRepo := rStockTransfer.NewStockTransferDeliveryItemRepository(db)
stockLogsRepo := rStockLogs.NewStockLogRepository(db)
supplierRepo := rSupplier.NewSupplierRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
userRepo := rUser.NewUserRepository(db)
transferService := sTransfer.NewTransferService(validate, stockTransferRepo, stockTransferDetailRepo, stockTransferDeliveryRepo, StockTransferDeliveryItemRepo, stockLogsRepo, productWarehouseRepo, supplierRepo)
userService := sUser.NewUserService(userRepo, validate)
TransferRoutes(router, userService, transferService)
}

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