Compare commits

...

137 Commits

Author SHA1 Message Date
ragilap 770adbd3ff fix(BE-69,70,71,72,73): add & implement middleware auth 2025-11-03 17:21:58 +07:00
ragilap 50119ac538 Merge branch 'development-before-sso' of https://gitlab.com/mbugroup/lti-api into refactor-to-serve/with-middleware 2025-11-03 16:57:10 +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
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
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
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
ragilap d4a0d5c68b feat/BE/US-76/TASK-122,133,121,120 Recording add create delete edit 2025-10-28 09:57:44 +07:00
ragilap 054ad2ad20 Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into refactor-to-serve/with-middleware 2025-10-27 10:21:15 +07: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
Adnan Zahir 171191c97d chore: ignore vendor, delete pipeline 2025-10-23 11:51:59 +07:00
Adnan Zahir 587cfabb4a Merge branch 'development-after-rebase' into 'development'
chore(REBASE): Development with SSO

See merge request mbugroup/lti-api!40
2025-10-23 11:50:06 +07:00
Adnan Zahir 3ede6461cf Merge branch 'development' into 'development-after-rebase'
# Conflicts:
#   Makefile
#   internal/middleware/auth.go
#   internal/route/route.go
2025-10-23 11:48:52 +07:00
ragilap 1dfd1f747e fix(BE): ignore makefile 2025-10-23 11:41:51 +07:00
ragilap 40665b0d8f Feat(BE-69,70,71,72,73): crud and integration sso with lti, revoke_token 2025-10-23 11:41:46 +07:00
kris 94f4929749 Update .gitlab-ci.yml file 2025-10-23 11:41:00 +07:00
GitLab Deploy Bot ad815b3412 . 2025-10-23 11:40:58 +07:00
Hafizh A. Y d41e16cab9 chore(BE): delete gitlab-ci.yml 2025-10-23 11:37:35 +07:00
ragilap 22e4728738 Feat(BE-69,70,71,72,73): crud and integration sso with lti, revoke_token 2025-10-23 11:37:32 +07:00
ragilap 501b6f8440 feat/login crud in users sync with sso 2025-10-23 11:36:29 +07:00
Adnan Zahir 3ea5bf6787 chore(CI): added [LTI API] label to webhook content 2025-10-23 11:30:14 +07:00
Adnan Zahir 0a4e06614b chore(CI): added gitlab ci yaml file for notify MR and MR-merged events 2025-10-23 11:30:13 +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
Adnan Zahir 45f41f87ff Merge branch 'fix/development' into 'development'
fix(BE): ignore makefile

See merge request mbugroup/lti-api!36
2025-10-22 21:29:00 +07:00
ragilap 02defcb86a fix(BE): ignore makefile 2025-10-22 21:25:16 +07:00
Mitra Berlian Unggas 0c791898ff Merge branch 'feat/sso-integration' into 'development'
Feat(BE-69,70,71,72,73): crud and integration sso with lti, revoke_token

See merge request mbugroup/lti-api!32
2025-10-22 12:12:23 +00: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
kris ddcda59239 Update .gitlab-ci.yml file 2025-10-21 17:07:46 +00:00
kris 85dfc33191 Merge branch 'fix-pipeline' into 'development'
fix: perbaikan file .gitlab-ci.yml dan docker-compose untuk LTI API

See merge request mbugroup/lti-api!33
2025-10-21 16:56:56 +00:00
GitLab Deploy Bot bb60e987e5 . 2025-10-21 23:45:13 +07:00
ragilap d5e8487f44 Merge branch 'feat/sso-integration' of https://gitlab.com/mbugroup/lti-api into feat/sso-integration 2025-10-21 20:32:52 +07:00
ragilap ab8c5d2ec4 Feat(BE-69,70,71,72,73): crud and integration sso with lti, revoke_token 2025-10-21 20:31:10 +07:00
Adnan Zahir 452403d71e Merge branch 'feat/sso-integration' into 'development'
Feat/sso integration

See merge request mbugroup/lti-api!31
2025-10-21 16:32:17 +07:00
Hafizh A. Y 5d676d5993 chore(BE): delete gitlab-ci.yml 2025-10-21 16:28:51 +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
ragilap e239246d02 Feat(BE-69,70,71,72,73): crud and integration sso with lti, revoke_token 2025-10-08 15:25:17 +07:00
Adnan Zahir 6c387b420c Merge branch 'feat/sso-integration' into 'development'
[FEAT/BE][Storyless-task/TASK-69-verify-authentication-flows-work-correctly] Sync login and crud in lti with sso

See merge request mbugroup/lti-api!9
2025-10-06 21:09:50 +07:00
Adnan Zahir ae84c9d8cc Merge branch 'chore/CI/merge-request-notify-workflow' into 'development'
chore(CI): added [LTI API] label to webhook content

See merge request mbugroup/lti-api!8
2025-10-06 16:50:17 +07:00
ragilap 6bddbbf9d9 feat/login crud in users sync with sso 2025-10-06 12:31:54 +07:00
Adnan Zahir d04b9278d2 chore(CI): added [LTI API] label to webhook content 2025-10-03 21:59:45 +07:00
Adnan Zahir 1684d69fae Merge branch 'chore/CI/merge-request-notify-workflow' into 'development'
chore(CI): added gitlab ci yaml file for notify MR and MR-merged events

See merge request mbugroup/lti-api!7
2025-10-03 21:51:14 +07:00
Adnan Zahir 3376f538fe chore(CI): added gitlab ci yaml file for notify MR and MR-merged events 2025-10-03 21:41:54 +07:00
Adnan Zahir d6c9747c54 Merge branch 'feat/BE/US-33/master-data-management' into 'development'
[FEAT/BE][US#33/TASK#36,37,38,39] Finish Master Data Management Api

See merge request mbugroup/lti-api!6
2025-10-03 21:20:04 +07:00
Hafizh A. Y. e4646faf30 Merge branch 'dev/hafizh' into 'feat/BE/US-33/master-data-management'
[FEAT/BE][US#33/TASK#36,37,38,39] Finish Master Data Management Api

See merge request mbugroup/lti-api!5
2025-10-03 14:08:08 +00:00
Hafizh A. Y 2d49ffe4cd Feat(BE-36,37,38,39): finish master data management api 2025-10-03 21:04:21 +07:00
Hafizh A. Y e8905be856 Feat(BE-36,37,38,39): master area, customer, kandang, location, warehouse 2025-10-02 10:51:15 +07:00
Adnan Zahir fac5f382ec Merge branch 'dev/hafizh' into 'development'
chore: update port so it doesn't conflict with docker sso

See merge request mbugroup/lti-api!4
2025-09-30 16:56:01 +07:00
Hafizh A. Y dbc1f79a36 chore: update port so it doesn't conflict with sso 2025-09-30 16:48:05 +07:00
Adnan Zahir 1053b779e4 Merge branch 'dev/hafizh' into 'development'
chore: adjust docker and any file for starting project

See merge request mbugroup/lti-api!3
2025-09-30 14:54:01 +07:00
Hafizh A. Y 94a6d41a61 fix: adjust docker and any file for starting project 2025-09-30 14:45:54 +07:00
Adnan Zahir 9444ad56dc Merge branch 'dev/hafizh' into 'development'
fix: adjust from boilerplate to lti project

See merge request mbugroup/lti-api!2
2025-09-25 11:28:53 +07:00
Hafizh A. Y c136206f2d fix: adjust from boilerplate to lti project 2025-09-25 11:08:01 +07:00
Adnan Zahir 4d26e6b7b4 Merge branch 'dev/hafizh' into 'development'
initial commit

See merge request mbugroup/lti-api!1
2025-09-25 10:50:10 +07:00
Hafizh A. Y 10506238ae initial commit 2025-09-25 10:47:28 +07:00
Adnan Zahir c43544e5e8 Delete initial README.md 2025-09-25 10:43:47 +07:00
328 changed files with 29051 additions and 62 deletions
Vendored
BIN
View File
Binary file not shown.
+17
View File
@@ -0,0 +1,17 @@
# .air.toml
root = "."
tmp_dir = "tmp"
[build]
# Build binary utama
cmd = "go build -o /lti-api/tmp/main ./cmd/api"
# Lokasi binary hasil build
bin = "/lti-api/tmp/main"
# Jalankan binary langsung dengan environment dev
full_bin = "APP_ENV=dev /lti-api/tmp/main"
# File yang dipantau oleh Air
include_ext = ["go", "tpl", "tmpl", "html"]
exclude_dir = ["vendor", "tmp"]
[log]
time = true
+40
View File
@@ -0,0 +1,40 @@
# Git & CI
.git
.gitignore
.github
# Build artifacts
/tmp
/bin
/out
*.exe
*.dll
*.so
*.dylib
*.test
*.out
# Go specific
*.exe
*.test
*.prof
*.log
# Dependencies cache (biar tidak kebawa)
vendor/
go-build-cache/
go-mod-cache/
# Editor/IDE
.vscode
.idea
*.swp
# Env & secrets
.env
.env.*
!/.env.example
# Docker sendiri
Dockerfile*
docker-compose*.yml
+56
View File
@@ -0,0 +1,56 @@
# server configuration
# Env value : prod || dev
VERSION=0.0.1
APP_ENV=dev
APP_HOST=0.0.0.0
APP_PORT=8081
APP_URL=http://localhost:8081
# database configuration
DB_HOST=postgresdb
DB_USER=postgres
DB_PASSWORD=changeme
DB_NAME=db_lti_erp
DB_PORT=5432
DB_PORT_HOST=5542
# JWT
JWT_SECRET=changeme
JWT_ACCESS_EXP_MINUTES=30
JWT_REFRESH_EXP_DAYS=30
JWT_RESET_PASSWORD_EXP_MINUTES=10
JWT_VERIFY_EMAIL_EXP_MINUTES=10
# CORS
CORS_ALLOW_ORIGINS=changeme
CORS_ALLOW_METHODS=GET,POST,PUT,PATCH,DELETE,OPTIONS
CORS_ALLOW_HEADERS=Authorization,Content-Type,X-Requested-With
CORS_EXPOSE_HEADERS=Link,Location
CORS_ALLOW_CREDENTIALS=true
CORS_MAX_AGE=600
# Redis
REDIS_URL=redis://redis:6379/0
REDIS_PORT_HOST=6381
# SSO Integration
SSO_ISSUER=http://localhost:8080/api
# SSO_JWKS_URL=http://localhost:8080/api/.well-known/jwks.json
SSO_JWKS_URL=http://host.docker.internal:8080/api/.well-known/jwks.json
SSO_ALLOWED_AUDIENCES=client:lti-api
SSO_AUTHORIZE_URL=http://localhost:8080/sso/authorize
SSO_TOKEN_URL=http://localhost:8080/sso/token
SSO_GETME_URL=http://localhost:8080/api/auth/get-me
SSO_ACCESS_COOKIE_NAME=sso_access
SSO_REFRESH_COOKIE_NAME=sso_refresh
SSO_COOKIE_DOMAIN=
SSO_COOKIE_SECURE=false
SSO_COOKIE_SAMESITE=Lax
SSO_TOKEN_BLACKLIST_PREFIX=sso:blacklist
SSO_PKCE_TTL_SECONDS=300
# Security window and payload limits for SSO user sync webhook
SSO_USER_SYNC_SIGNATURE_DRIFT_SECONDS=120
SSO_USER_SYNC_NONCE_TTL_SECONDS=600
SSO_USER_SYNC_MAX_BODY_BYTES=32768
# Example JSON (single-line) of client configs (each client requires a unique sync_secret)
SSO_CLIENTS={"LTI":{"public_id":"Lumbung-Telur-Indonesia","redirect_uri":"http://localhost:8081/api/sso/callback","scope":"openid profile","default_return_uri":"http://localhost:3000","allowed_return_origins":["http://localhost:3000"],"sync_secret":"onUyfODIMHOh4TgGLgyWLmsNeVNxFRHqoLJFLPjr"}}
+58
View File
@@ -0,0 +1,58 @@
# .env.lti-api (Development Server with Domain)
# =============================================
# Server configuration
VERSION=0.0.1
APP_ENV=dev
APP_HOST=0.0.0.0
APP_PORT=8081
APP_URL=https://dev-api-lti.mbugroup.id
# Database configuration (pakai PostgreSQL milik SSO)
DB_HOST=sso-postgres
DB_USER=postgres
DB_PASSWORD=postgres
DB_NAME=db_lti_erp
DB_PORT=5432
# JWT configuration
JWT_SECRET=changeme
JWT_ACCESS_EXP_MINUTES=30
JWT_REFRESH_EXP_DAYS=30
JWT_RESET_PASSWORD_EXP_MINUTES=10
JWT_VERIFY_EMAIL_EXP_MINUTES=10
# Redis (pakai Redis milik SSO)
REDIS_URL=redis://sso-redis:6379/0
# CORS configuration
CORS_ALLOW_ORIGINS=https://dev-api-sso.mbugroup.id,https://dev-lti.mbugroup.id,https://dev-api-lti.mbugroup.id,http://localhost:3000
CORS_ALLOW_METHODS=GET,POST,PUT,PATCH,DELETE,OPTIONS
CORS_ALLOW_HEADERS=Authorization,Content-Type,X-Requested-With
CORS_EXPOSE_HEADERS=Link,Location
CORS_ALLOW_CREDENTIALS=true
CORS_MAX_AGE=600
# SSO Integration (Gunakan domain backend SSO)
SSO_ISSUER=https://dev-api-sso.mbugroup.id
SSO_JWKS_URL=https://dev-api-sso.mbugroup.id/api/.well-known/jwks.json
SSO_ALLOWED_AUDIENCES=
SSO_AUTHORIZE_URL=https://dev-api-sso.mbugroup.id/api/sso/authorize
SSO_TOKEN_URL=https://dev-api-sso.mbugroup.id/api/sso/token
SSO_GETME_URL=https://dev-api-sso.mbugroup.id/api/auth/get-me
# Cookie & session configuration
SSO_ACCESS_COOKIE_NAME=sso_access
SSO_REFRESH_COOKIE_NAME=sso_refresh
SSO_COOKIE_DOMAIN=.mbugroup.id
SSO_COOKIE_SECURE=true
SSO_COOKIE_SAMESITE=Lax
SSO_PKCE_TTL_SECONDS=300
# SSO webhook / user sync settings
SSO_USER_SYNC_SIGNATURE_DRIFT_SECONDS=120
SSO_USER_SYNC_NONCE_TTL_SECONDS=600
SSO_USER_SYNC_MAX_BODY_BYTES=32768
# Client registration for SSO
SSO_CLIENTS={"Lumbung-Telur-Indonesia":{"public_id":"Lumbung-Telur-Indonesia","redirect_uri":"https://dev-api-lti.mbugroup.id/api/sso/callback","scope":"openid profile","default_return_uri":"https://dev-lti.mbugroup.id","allowed_return_origins":["https://dev-lti.mbugroup.id","http://localhost:3000"],"sync_secret":"onUyfODIMHOh4TgGLgyWLmsNeVNxFRHqoLJFLPjr"}}
+29
View File
@@ -0,0 +1,29 @@
# Environment variables
.env
# Air temp dir
tmp/
# Binaries
main
bin/
*.exe
*.out
Makefile
docker-compose.local.yml
docker-compose.yaml
Dockerfile.local
# Go build cache
.gocache/
vendor/
# Logs & reports
*.log
*.txt
coverage/
# IDE / editor files
.vscode/
.idea/
*.swp
+78
View File
@@ -0,0 +1,78 @@
stages:
- build
- deploy
- cleanup
# ==============================
# 🏗️ BUILD IMAGE (Overwrite :dev)
# ==============================
build_image:
stage: build
image: docker:latest
services:
- docker:dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
script:
- echo "🔧 Building Docker image for :dev..."
- docker login -u gitlab-ci-token -p "$CI_JOB_TOKEN" "$CI_REGISTRY"
- docker build -f Dockerfile.local -t registry.gitlab.com/mbugroup/sso-mbugroup/lti-api:dev .
- docker push registry.gitlab.com/mbugroup/sso-mbugroup/lti-api:dev
only:
- development
# ==============================
# 🚀 DEPLOY TO DEV SERVER
# ==============================
deploy_lti:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache openssh-client bash curl
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- ssh-keyscan -H "$SERVER_IP" >> ~/.ssh/known_hosts
script:
- echo "🚀 Deploy ke ${SERVER_USER}@${SERVER_IP} menggunakan image :dev"
- |
ssh -o StrictHostKeyChecking=no ${SERVER_USER}@${SERVER_IP} bash -s <<REMOTE
set -e
APP_NAME="lti-api"
DOCKER_IMAGE="registry.gitlab.com/mbugroup/sso-mbugroup/lti-api:dev"
NETWORK_NAME="lti-network"
ENV_PATH="/home/devops/code/api/lti-api/.env.lti-api"
PORT=8081
echo "🔑 Login ke GitLab Registry..."
echo "${GITLAB_TOKEN}" | docker login -u "${GITLAB_USER}" --password-stdin registry.gitlab.com
echo "🛑 Stop & remove old container..."
docker stop "\${APP_NAME}" >/dev/null 2>&1 || true
docker rm -f "\${APP_NAME}" >/dev/null 2>&1 || true
echo "🧹 Membersihkan container zombie di port \${PORT}..."
OLD_ID=\$(docker ps -aq --filter "publish=\${PORT}")
if [ -n "\${OLD_ID}" ]; then
echo "⚠️ Container lain masih pakai port \${PORT}, hapus..."
docker stop \${OLD_ID} >/dev/null 2>&1 || true
docker rm -f \${OLD_ID} >/dev/null 2>&1 || true
fi
echo "🐳 Pull image baru..."
docker pull "\${DOCKER_IMAGE}"
echo "🚀 Run container baru..."
docker run -d --name "\${APP_NAME}" --restart always \
--env-file "\${ENV_PATH}" \
-p \${PORT}:8081 \
--network "\${NETWORK_NAME}" \
"\${DOCKER_IMAGE}"
echo "✅ Deployment selesai di port \${PORT}"
REMOTE
only:
- development
+203
View File
@@ -0,0 +1,203 @@
## Config for golangci-lint v2 schema
version: "2"
run:
timeout: 3m
linters:
default: none
enable:
## enabled by default
- errcheck
- govet
- ineffassign
- staticcheck
- unused
## disabled by default
- asasalint
- asciicheck
- bidichk
- bodyclose
- canonicalheader
- cyclop
- dupl
- durationcheck
- errname
- errorlint
- exhaustive
- fatcontext
- forbidigo
- funlen
- gocheckcompilerdirectives
#- gochecknoglobals
#- gochecknoinits
- gochecksumtype
- gocognit
- goconst
- gocritic
- gocyclo
#- godot
- gomoddirectives
- gomodguard
- goprintffuncname
- gosec
- intrange
- lll
- loggercheck
- makezero
- mirror
#- mnd
- musttag
- nakedret
- nestif
- nilerr
- nilnil
- noctx
- nolintlint
- nonamedreturns
- nosprintfhostport
- perfsprint
- predeclared
- promlinter
- protogetter
- reassign
- revive
- rowserrcheck
- sloglint
- spancheck
- sqlclosecheck
- testableexamples
#- testifylint
- testpackage
- tparallel
- unconvert
- unparam
- usestdlibvars
- usetesting
- wastedassign
- whitespace
settings:
cyclop:
max-complexity: 30
package-average: 10.0
errcheck:
check-type-assertions: true
exhaustive:
check:
- switch
- map
exhaustruct:
exclude:
- "^net/http.Client$"
- "^net/http.Cookie$"
- "^net/http.Request$"
- "^net/http.Response$"
- "^net/http.Server$"
- "^net/http.Transport$"
- "^net/url.URL$"
- "^os/exec.Cmd$"
- "^reflect.StructField$"
- "^github.com/Shopify/sarama.Config$"
- "^github.com/Shopify/sarama.ProducerMessage$"
- "^github.com/mitchellh/mapstructure.DecoderConfig$"
- "^github.com/prometheus/client_golang/.+Opts$"
- "^github.com/spf13/cobra.Command$"
- "^github.com/spf13/cobra.CompletionOptions$"
- "^github.com/stretchr/testify/mock.Mock$"
- "^github.com/testcontainers/testcontainers-go.+Request$"
- "^github.com/testcontainers/testcontainers-go.FromDockerfile$"
- "^golang.org/x/tools/go/analysis.Analyzer$"
- "^google.golang.org/protobuf/.+Options$"
- "^gopkg.in/yaml.v3.Node$"
funlen:
lines: 100
statements: 50
ignore-comments: true
gocognit:
min-complexity: 20
gocritic:
settings:
captLocal:
paramsOnly: false
underef:
skipRecvDeref: false
gomodguard:
blocked:
modules:
- github.com/golang/protobuf:
recommendations:
- google.golang.org/protobuf
reason: "see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules"
- github.com/satori/go.uuid:
recommendations:
- github.com/google/uuid
reason: "satori's package is not maintained"
- github.com/gofrs/uuid:
recommendations:
- github.com/gofrs/uuid/v5
reason: "gofrs' package was not go module before v5"
govet:
enable-all: true
disable:
- fieldalignment
settings:
shadow:
strict: true
inamedparam:
skip-single-param: true
mnd:
ignored-functions:
- args.Error
- flag.Arg
- flag.Duration.*
- flag.Float.*
- flag.Int.*
- flag.Uint.*
- os.Chmod
- os.Mkdir.*
- os.OpenFile
- os.WriteFile
- prometheus.ExponentialBuckets.*
- prometheus.LinearBuckets
nakedret:
max-func-lines: 0
nolintlint:
allow-no-explanation:
- funlen
- gocognit
- lll
require-explanation: true
require-specific: true
perfsprint:
strconcat: false
rowserrcheck:
packages:
- github.com/jmoiron/sqlx
sloglint:
no-global: "all"
context: "scope"
exclusions:
rules:
- source: "(noinspection|TODO)"
linters:
- godot
- source: "//noinspection"
linters:
- gocritic
- path: "example\\.go"
linters:
- lll
- path: "_test\\.go"
linters:
- bodyclose
- dupl
- funlen
- goconst
- gosec
- noctx
- wrapcheck
- lll
- testpackage
issues:
max-same-issues: 50
+20
View File
@@ -0,0 +1,20 @@
FROM golang:1.23-alpine
# Install dependensi dasar
RUN apk add --no-cache git curl bash build-base
# Install Air (pakai repo baru air-verse)
RUN go install github.com/air-verse/air@v1.52.3
WORKDIR /lti-api
# Cache dependencies
COPY go.mod go.sum ./
RUN go mod download
# Copy source code
COPY . .
EXPOSE 8081
CMD ["air", "-c", ".air.toml"]
+59
View File
@@ -0,0 +1,59 @@
# ===============================
# LTI-API Makefile (Docker Setup)
# ===============================
APP_NAME := lti-api
COMPOSE := docker compose -f docker-compose.yaml
NETWORK := lti-network
ENV_FILE := .env.lti-api
include $(ENV_FILE)
export $(shell sed 's/=.*//' $(ENV_FILE))
MIGRATIONS_DIR := ./migrations
MIGRATE_IMAGE := migrate/migrate:v4.15.2
DB_URL := postgres://$(DB_USER):$(DB_PASSWORD)@lti-postgres:5432/$(DB_NAME)?sslmode=disable
# --- Docker ---
docker-local:
@echo "🚀 Starting $(APP_NAME) with local PostgreSQL & Redis..."
@$(COMPOSE) up --build -d
docker-down:
@$(COMPOSE) down --remove-orphans
docker-nuke:
@echo "💣 Removing all containers, images, and volumes..."
@$(COMPOSE) down --rmi all --volumes --remove-orphans
# --- Database / Migration ---
wait-db:
@echo "⏳ Waiting for database lti-postgres to be ready (inside Docker network)..."
@$(COMPOSE) run --rm app sh -c 'until nc -z lti-postgres 5432; do echo "Waiting for DB..."; sleep 2; done; echo "✅ Database is ready!"'
migrate-up: wait-db
@echo "⬆️ Running migrations..."
@docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" up
migrate-down: wait-db
@echo "⬇️ Rolling back all migrations..."
@docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" down -all
seed:
@echo "🌱 Running seed script..."
@$(COMPOSE) run --rm app go run cmd/seed/main.go
psql:
@docker exec -it lti-postgres psql -U $(DB_USER) -d $(DB_NAME)
logs:
@$(COMPOSE) logs -f app
restart:
@$(COMPOSE) restart
status:
@$(COMPOSE) ps
+82 -62
View File
@@ -1,93 +1,113 @@
# LTI API
# Lumbung Telur Indonesia ERP API (lti-api)
RESTful API for **Lumbung Telur Indonesia ERP**, built with **Go, Fiber, GORM** and **PostgreSQL**.
---
## Getting started
## 📦 Tech Stack
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
- **Go + Fiber** — Web framework
- **GORM** — ORM for PostgreSQL
- **PostgreSQL** — Relational database
- **go-playground/validator** — Input validation
- **JWT** — Authentication
- **Logrus** — Logging
- **Fiber middleware** — Rate limiting, CORS, recovery, logger
- **Air** — Hot reload for development
- **Docker + Docker Compose** — Containerization
- **golang-migrate** — Database migration tool
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
---
## Add your files
## 🚀 Getting Started
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
- [ ] [Add files using the command line](https://docs.gitlab.com/topics/git/add_files/#add-files-to-a-git-repository) or push an existing Git repository with the following command:
### 1. Clone Project
```
cd existing_repo
git remote add origin https://gitlab.com/mbugroup/lti-api.git
git branch -M main
git push -uf origin main
```bash
git clone https://gitlab.com/mbugroup/lti-api.git
cd lti-api
```
## Integrate with your tools
### 2. Install Dependencies
- [ ] [Set up project integrations](https://gitlab.com/mbugroup/lti-api/-/settings/integrations)
```bash
go mod tidy
```
## Collaborate with your team
### 3. Configure Environment
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
- [ ] [Set auto-merge](https://docs.gitlab.com/user/project/merge_requests/auto_merge/)
Copy .env.example to .env and adjust the variables (e.g. DATABASE_URL, JWT secrets, etc).
## Test and Deploy
```bash
cp .env.example .env
```
Use the built-in continuous integration in GitLab.
### 5. Setup Docker
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/)
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
Run initial docker.
***
```bash
make docker-local
```
# Editing this README
### 4. Migrate Database
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.
Run initial migrations and generate views.
## Suggestions for a good README
```bash
make migrate-up
```
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
### 5. Run App
## Name
Choose a self-explaining name for your project.
Run project via Docker
## Description
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
### 6. Create New Module
## Badges
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
```bash
make gen feat=user
```
## Visuals
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
output:
## Installation
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
```bash
cmd/
├── api/
│ └── main.go # Application entrypoint (initialize Fiber, load config, connect DB, register route)
## Usage
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
internal/
├── config/ # App config (env loader, logger, app settings)
├── database/ # Database connection + migration setup
├── middleware/ # Global Fiber middleware (auth, logger, recovery, rate limiting)
├── modules/ # Feature modules (users, products, suppliers, etc.)
│ ├── <module>/
│ │ ├── controllers/ # HTTP handler layer (receive request, call service, return response)
│ │ ├── dto/ # Data Transfer Objects (request & response payloads, separate from models)
│ │ ├── models/ # GORM models (represent database tables/entities)
│ │ ├── repositories/ # Data access layer (queries to DB, CRUD abstraction)
│ │ ├── services/ # Business logic layer (process rules, orchestrate repository calls)
│ │ ├── validation/ # Request validation (custom rules per module)
│ │ ├── module.go # Module bootstrapper (wire controller, service, repository together)
│ │ └── route.go # Module route (register module routes into Fiber app)
├── repository/ # Shared repositories (reusable DB access layer across multiple modules)
├── response/ # Standardized API responses (success, error, pagination)
├── utils/ # Helper functions (JWT, hashing, constants, enums, etc.)
├── validation/ # Shared request validation structs & rules
└── route/ # Central route aggregator (load all module routes into main app)
```
## Support
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
## ✨ Author
## Roadmap
If you have ideas for releases in the future, it is a good idea to list them in the README.
IT Development PT Mitra Berlian Unggas Group
## Contributing
State if you are open to contributions and what your requirements are for accepting them.
## 📃 License
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
## Authors and acknowledgment
Show your appreciation to those who have contributed to the project.
## License
For open source projects, say how it is licensed.
## Project status
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
This project is private. All rights reserved.
+213
View File
@@ -0,0 +1,213 @@
package main
import (
"context"
"fmt"
"os"
"os/signal"
"strings"
"syscall"
"time"
"gitlab.com/mbugroup/lti-api.git/internal/cache"
"gitlab.com/mbugroup/lti-api.git/internal/config"
"gitlab.com/mbugroup/lti-api.git/internal/database"
"gitlab.com/mbugroup/lti-api.git/internal/middleware"
"gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session"
"gitlab.com/mbugroup/lti-api.git/internal/route"
"gitlab.com/mbugroup/lti-api.git/internal/sso"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/compress"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/helmet"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
app := setupFiberApp()
db := setupDatabase()
defer closeDatabase(db)
rdb := setupRedis()
defer rdb.Close()
setupSSO(ctx, rdb)
setupRoutes(app, db, rdb)
address := fmt.Sprintf("%s:%d", config.AppHost, config.AppPort)
// Start server and handle graceful shutdown
serverErrors := make(chan error, 1)
go startServer(app, address, serverErrors)
handleGracefulShutdown(ctx, app, serverErrors)
}
func setupRedis() *redis.Client {
opt, err := redis.ParseURL(config.RedisURL)
if err != nil {
utils.Log.Fatalf("Redis URL parse error: %v", err)
}
rdb := redis.NewClient(opt)
if err := rdb.Ping(context.Background()).Err(); err != nil {
utils.Log.Fatalf("Redis ping failed: %v", err)
}
cache.SetRedis(rdb)
utils.Log.Infof("Redis connected: %s", config.RedisURL)
return rdb
}
func setupSSO(ctx context.Context, rdb *redis.Client) {
const (
maxAttempts = 12
retryDelay = 5 * time.Second
)
var lastErr error
for attempt := 1; attempt <= maxAttempts; attempt++ {
if err := sso.Init(ctx, config.SSOJWKSURL, config.SSOIssuer, config.SSOAllowedAudiences); err != nil {
lastErr = err
utils.Log.WithError(err).Warnf("SSO initialization attempt %d/%d failed", attempt, maxAttempts)
select {
case <-ctx.Done():
utils.Log.Fatalf("SSO initialization aborted: %v", ctx.Err())
case <-time.After(retryDelay):
}
continue
}
lastErr = nil
if attempt > 1 {
utils.Log.Infof("SSO initialization succeeded after %d attempts", attempt)
}
break
}
if lastErr != nil {
utils.Log.Fatalf("SSO initialization failed: %v", lastErr)
}
if rdb != nil {
session.SetRevocationStore(session.NewRevocationStore(rdb, config.SSOTokenBlacklistPrefix))
} else {
session.SetRevocationStore(nil)
}
}
func setupFiberApp() *fiber.App {
app := fiber.New(config.FiberConfig())
// Middleware setup
app.Use(middleware.LoggerConfig())
app.Use(helmet.New())
app.Use(compress.New())
app.Use(middleware.RecoverConfig())
origins := "*"
if len(config.CORSAllowOrigins) > 0 {
origins = strings.Join(config.CORSAllowOrigins, ",")
}
if config.CORSAllowCredentials && (origins == "" || origins == "*") {
origins = "http://localhost:3000"
}
app.Use(cors.New(cors.Config{
AllowOrigins: origins,
AllowMethods: strings.Join(config.CORSAllowMethods, ","),
AllowHeaders: strings.Join(config.CORSAllowHeaders, ","),
ExposeHeaders: strings.Join(config.CORSExposeHeaders, ","),
AllowCredentials: config.CORSAllowCredentials,
MaxAge: config.CORSMaxAge,
}))
return app
}
func setupDatabase() *gorm.DB {
db := database.Connect(config.DBHost, config.DBName)
return db
}
func setupRoutes(app *fiber.App, db *gorm.DB, rdb *redis.Client) {
// route.Routes(app, db)
// app.Use(utils.NotFoundHandler)
app.Get("/healthz", func(c *fiber.Ctx) error {
return c.Status(fiber.StatusOK).JSON(fiber.Map{
"status": "ok",
"service": "api",
"version": config.Version,
})
})
app.Get("/readyz", func(c *fiber.Ctx) error {
sqlDB, err := db.DB()
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"status": "error", "db": "unavailable", "redis": "unknown",
})
}
ctx, cancel := context.WithTimeout(c.Context(), 2*time.Second)
defer cancel()
dbOK := sqlDB.PingContext(ctx) == nil
redisOK := rdb.Ping(ctx).Err() == nil
status := fiber.StatusOK
statusText := "ok"
if !dbOK || !redisOK {
status = fiber.StatusServiceUnavailable
statusText = "degraded"
}
body := fiber.Map{
"status": statusText,
"db": map[bool]string{true: "up", false: "down"}[dbOK],
"redis": map[bool]string{true: "up", false: "down"}[redisOK],
}
return c.Status(status).JSON(body)
})
route.Routes(app, db)
app.Use(utils.NotFoundHandler)
}
func startServer(app *fiber.App, address string, errs chan<- error) {
if err := app.Listen(address); err != nil {
errs <- fmt.Errorf("error starting server: %w", err)
}
}
func closeDatabase(db *gorm.DB) {
sqlDB, errDB := db.DB()
if errDB != nil {
utils.Log.Errorf("Error getting database instance: %v", errDB)
return
}
if err := sqlDB.Close(); err != nil {
utils.Log.Errorf("Error closing database connection: %v", err)
} else {
utils.Log.Info("Database connection closed successfully")
}
}
func handleGracefulShutdown(ctx context.Context, app *fiber.App, serverErrors <-chan error) {
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
select {
case err := <-serverErrors:
utils.Log.Fatalf("Server error: %v", err)
case <-quit:
utils.Log.Info("Shutting down server...")
if err := app.Shutdown(); err != nil {
utils.Log.Fatalf("Error during server shutdown: %v", err)
}
case <-ctx.Done():
utils.Log.Info("Server exiting due to context cancellation")
}
utils.Log.Info("Server exited")
}
+19
View File
@@ -0,0 +1,19 @@
package main
import (
"log"
"gitlab.com/mbugroup/lti-api.git/internal/config"
"gitlab.com/mbugroup/lti-api.git/internal/database"
"gitlab.com/mbugroup/lti-api.git/internal/database/seed"
)
func main() {
db := database.Connect(config.DBHost, config.DBName)
if err := seed.Run(db); err != nil {
log.Fatalf("❌ Failed run seeder: %v", err)
}
log.Println("✅ Seed Successfully")
}
+75
View File
@@ -0,0 +1,75 @@
services:
postgresdb:
image: postgres:alpine
restart: always
ports:
- "${DB_PORT_HOST:-5542}:5432"
environment:
POSTGRES_USER: ${DB_USER:-postgres}
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
POSTGRES_DB: ${DB_NAME:-db_lti_erp}
volumes:
- dbdata:/var/lib/postgresql/data
- ./internal/database/init:/docker-entrypoint-initdb.d
networks: [go-network]
healthcheck:
test:
[
"CMD-SHELL",
"pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-db_lti_erp}",
]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
restart: unless-stopped
ports:
- "${REDIS_PORT_HOST:-6381}:6379"
healthcheck:
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
interval: 5s
timeout: 3s
retries: 10
networks: [go-network]
app:
build:
context: .
dockerfile: Dockerfile.local
image: cosmtrek/air:v1.52.3
working_dir: /lti-api
volumes:
- .:/lti-api
command: air -c .air.toml
env_file:
- .env
environment:
DB_HOST: postgresdb
DB_PORT: 5432
DB_USER: ${DB_USER:-postgres}
DB_PASSWORD: ${DB_PASSWORD:-postgres}
DB_NAME: ${DB_NAME:-db_lti_erp}
REDIS_URL: ${REDIS_URL:-redis://redis:6379/0}
ports:
- "${APP_PORT:-8081}:8081"
depends_on:
postgresdb:
condition: service_healthy
networks: [go-network]
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8081/healthz || exit 1"]
interval: 10s
timeout: 3s
retries: 10
start_period: 10s
volumes:
dbdata:
go-mod-cache:
go-build-cache:
networks:
go-network:
name: lti-api_go-network
driver: bridge
+77
View File
@@ -0,0 +1,77 @@
version: "3.9"
services:
dev-lti-api:
container_name: dev-lti-api
build:
context: .
dockerfile: Dockerfile.local
image: dev-lti-api:latest
working_dir: /lti-api
command: air -c .air.toml
ports:
- "8081:8081"
env_file:
- .env.lti-api
environment:
# override agar koneksi ke container internal
DB_HOST: dev-lti-postgres
DB_PORT: 5432
REDIS_URL: redis://dev-lti-redis:6379/0
volumes:
- .:/lti-api
- ./internal/config/jwtRS256.key:/run/keys/jwtRS256.key
- ./internal/config/jwtRS256.key.pub:/run/keys/jwtRS256.key.pub
depends_on:
- dev-lti-postgres
- dev-lti-redis
networks:
- lti-network
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8081/healthz || exit 1"]
interval: 10s
timeout: 3s
retries: 10
start_period: 10s
dev-lti-postgres:
image: postgres:15-alpine
container_name: dev-lti-postgres
restart: always
environment:
POSTGRES_USER: ${DB_USER:-postgres}
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
POSTGRES_DB: ${DB_NAME:-db_lti_erp}
ports:
- "5433:5432"
volumes:
- dev-lti-postgres-data:/var/lib/postgresql/data
networks:
- lti-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-db_lti_erp}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 5s
dev-lti-redis:
image: redis:7-alpine
container_name: dev-lti-redis
restart: always
ports:
- "6380:6379"
networks:
- lti-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 10
networks:
lti-network:
driver: bridge
volumes:
dev-lti-postgres-data:
+86
View File
@@ -0,0 +1,86 @@
module gitlab.com/mbugroup/lti-api.git
go 1.23
require (
github.com/MicahParks/keyfunc/v2 v2.1.0
github.com/bytedance/sonic v1.12.1
github.com/glebarez/sqlite v1.11.0
github.com/go-playground/validator/v10 v10.27.0
github.com/gofiber/contrib/jwt v1.0.10
github.com/gofiber/fiber/v2 v2.52.5
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/jackc/pgconn v1.14.1
github.com/redis/go-redis/v9 v9.14.0
github.com/sirupsen/logrus v1.9.3
github.com/spf13/viper v1.19.0
golang.org/x/crypto v0.33.0
gorm.io/driver/postgres v1.5.9
gorm.io/gorm v1.25.11
)
require (
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/bytedance/sonic/loader v0.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.2 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/cpuid/v2 v2.2.3 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
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/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/philhofer/fwd v1.1.2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tinylib/msgp v1.1.8 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.55.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/sqlite v1.23.1 // indirect
)
+328
View File
@@ -0,0 +1,328 @@
github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+GVjO99k=
github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bytedance/sonic v1.12.1 h1:jWl5Qz1fy7X1ioY74WqO0KjAMtAGQs4sYnjiEBiyX24=
github.com/bytedance/sonic v1.12.1/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM=
github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofiber/contrib/jwt v1.0.10 h1:/ilGepl6i0Bntl0Zcd+lAzagY8BiS1+fEiAj32HMApk=
github.com/gofiber/contrib/jwt v1.0.10/go.mod h1:1qBENE6sZ6PPT4xIpBzx1VxeyROQO7sj48OlM1I9qdU=
github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
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=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
github.com/jackc/pgconn v1.14.1 h1:smbxIaZA08n6YuxEX1sDyjV/qkbtUtkH20qLkR9MUR4=
github.com/jackc/pgconn v1.14.1/go.mod h1:9mBNlny0UvkgJdCDvdVHYSjI+8tD2rnKK69Wz8ti++E=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.3.2 h1:7eY55bdBeCz1F2fTzSz69QC+pG46jYq9/jtSPiJ5nn0=
github.com/jackc/pgproto3/v2 v2.3.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
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/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/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
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/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=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE=
github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8=
github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
BIN
View File
Binary file not shown.
+38
View File
@@ -0,0 +1,38 @@
package cache
import (
"errors"
"sync"
"github.com/redis/go-redis/v9"
)
var (
redisClient *redis.Client
mu sync.RWMutex
)
// SetRedis assigns the global redis client used across the application.
func SetRedis(client *redis.Client) {
mu.Lock()
defer mu.Unlock()
redisClient = client
}
// Redis returns the configured redis client. It may be nil if not yet initialised.
func Redis() *redis.Client {
mu.RLock()
defer mu.RUnlock()
return redisClient
}
// MustRedis returns the redis client or panics if it has not been set.
func MustRedis() *redis.Client {
mu.RLock()
client := redisClient
mu.RUnlock()
if client == nil {
panic(errors.New("redis client not initialised"))
}
return client
}
+44
View File
@@ -0,0 +1,44 @@
package capabilities
import (
"strings"
recordings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings"
)
// FromPermissions returns a filtered map of capabilities that the frontend can use
// to toggle features. Only permissions recognized by the application are exposed.
func FromPermissions(perms []string) map[string]bool {
if len(perms) == 0 {
return nil
}
out := make(map[string]bool)
for _, perm := range perms {
if key, ok := normalizeAndAllow(perm); ok {
out[key] = true
}
}
if len(out) == 0 {
return nil
}
return out
}
func normalizeAndAllow(perm string) (string, bool) {
perm = strings.ToLower(strings.TrimSpace(perm))
if perm == "" {
return "", false
}
if _, ok := allowed[perm]; !ok {
return "", false
}
return perm, true
}
var allowed = map[string]struct{}{
recordings.PermissionRecordingRead: {},
recordings.PermissionRecordingCreate: {},
recordings.PermissionRecordingUpdate: {},
recordings.PermissionRecordingDelete: {},
}
@@ -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
}
@@ -0,0 +1,258 @@
package repository
import (
"context"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type BaseRepository[T any] interface {
GetAll(ctx context.Context, offset, limit int, modifier func(*gorm.DB) *gorm.DB) ([]T, int64, error)
GetByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*T, error)
GetByIDs(ctx context.Context, ids []uint, modifier func(*gorm.DB) *gorm.DB) ([]T, error)
First(ctx context.Context, modifier func(*gorm.DB) *gorm.DB) (*T, error)
CreateOne(ctx context.Context, entity *T, modifier func(*gorm.DB) *gorm.DB) error
CreateMany(ctx context.Context, entities []*T, modifier func(*gorm.DB) *gorm.DB) error
UpdateOne(ctx context.Context, id uint, entity *T, modifier func(*gorm.DB) *gorm.DB) error
UpdateMany(ctx context.Context, entities []*T, modifier func(*gorm.DB) *gorm.DB) error
PatchOne(ctx context.Context, id uint, updates map[string]any, modifier func(*gorm.DB) *gorm.DB) error
DeleteOne(ctx context.Context, id uint) error
DeleteMany(ctx context.Context, modifier func(*gorm.DB) *gorm.DB) error
Upsert(ctx context.Context, entity *T, conflictColumns []clause.Column, modifier func(*gorm.DB) *gorm.DB) error
WithTx(tx *gorm.DB) BaseRepository[T]
DB() *gorm.DB
}
type BaseRepositoryImpl[T any] struct {
db *gorm.DB
}
func NewBaseRepository[T any](db *gorm.DB) *BaseRepositoryImpl[T] {
return &BaseRepositoryImpl[T]{db: db}
}
func (r *BaseRepositoryImpl[T]) GetAll(
ctx context.Context,
offset, limit int,
modifier func(*gorm.DB) *gorm.DB,
) ([]T, int64, error) {
var entities []T
var total int64
q := r.db.WithContext(ctx).Model(new(T))
if modifier != nil {
q = modifier(q)
}
if err := q.Count(&total).Error; err != nil {
return nil, 0, err
}
if err := q.Offset(offset).Limit(limit).Find(&entities).Error; err != nil {
return nil, 0, err
}
return entities, total, nil
}
func (r *BaseRepositoryImpl[T]) GetByID(
ctx context.Context,
id uint,
modifier func(*gorm.DB) *gorm.DB,
) (*T, error) {
entity := new(T)
q := r.db.WithContext(ctx)
if modifier != nil {
q = modifier(q)
}
if err := q.First(entity, id).Error; err != nil {
return nil, err
}
return entity, nil
}
func (r *BaseRepositoryImpl[T]) GetByIDs(
ctx context.Context,
ids []uint,
modifier func(*gorm.DB) *gorm.DB,
) ([]T, error) {
var entities []T
q := r.db.WithContext(ctx).Model(new(T))
if modifier != nil {
q = modifier(q)
}
if err := q.Where("id IN ?", ids).Find(&entities).Error; err != nil {
return nil, err
}
if len(entities) == 0 {
return nil, gorm.ErrRecordNotFound
}
return entities, nil
}
func (r *BaseRepositoryImpl[T]) First(
ctx context.Context,
modifier func(*gorm.DB) *gorm.DB,
) (*T, error) {
entity := new(T)
q := r.db.WithContext(ctx)
if modifier != nil {
q = modifier(q)
}
if err := q.First(entity).Error; err != nil {
return nil, err
}
return entity, nil
}
// ---- CREATE ----
func (r *BaseRepositoryImpl[T]) CreateOne(
ctx context.Context,
entity *T,
modifier func(*gorm.DB) *gorm.DB,
) error {
q := r.db.WithContext(ctx)
if modifier != nil {
q = modifier(q)
}
return q.Create(entity).Error
}
func (r *BaseRepositoryImpl[T]) CreateMany(
ctx context.Context,
entities []*T,
modifier func(*gorm.DB) *gorm.DB,
) error {
q := r.db.WithContext(ctx)
if modifier != nil {
q = modifier(q)
}
return q.Create(&entities).Error
}
// ---- UPDATE ----
func (r *BaseRepositoryImpl[T]) UpdateOne(
ctx context.Context,
id uint,
entity *T,
modifier func(*gorm.DB) *gorm.DB,
) error {
q := r.db.WithContext(ctx).Model(new(T)).Where("id = ?", id)
if modifier != nil {
q = modifier(q)
}
result := q.Updates(entity)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
func (r *BaseRepositoryImpl[T]) UpdateMany(
ctx context.Context,
entities []*T,
modifier func(*gorm.DB) *gorm.DB,
) error {
q := r.db.WithContext(ctx)
if modifier != nil {
q = modifier(q)
}
result := q.Save(&entities)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
func (r *BaseRepositoryImpl[T]) PatchOne(
ctx context.Context,
id uint,
updates map[string]any,
modifier func(*gorm.DB) *gorm.DB,
) error {
q := r.db.WithContext(ctx).Model(new(T)).Where("id = ?", id)
if modifier != nil {
q = modifier(q)
}
result := q.Updates(updates)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
// ---- DELETE ----
func (r *BaseRepositoryImpl[T]) DeleteOne(ctx context.Context, id uint) error {
result := r.db.WithContext(ctx).Delete(new(T), id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
func (r *BaseRepositoryImpl[T]) DeleteMany(ctx context.Context, modifier func(*gorm.DB) *gorm.DB) error {
q := r.db.WithContext(ctx).Model(new(T))
if modifier != nil {
q = modifier(q)
}
result := q.Delete(new(T))
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
// ---- UPSERT ----
func (r *BaseRepositoryImpl[T]) Upsert(
ctx context.Context,
entity *T,
conflictColumns []clause.Column,
modifier func(*gorm.DB) *gorm.DB,
) error {
q := r.db.WithContext(ctx).Clauses(clause.OnConflict{
Columns: conflictColumns,
UpdateAll: true,
})
if modifier != nil {
q = modifier(q)
}
return q.Create(entity).Error
}
func (r *BaseRepositoryImpl[T]) WithTx(tx *gorm.DB) BaseRepository[T] {
return &BaseRepositoryImpl[T]{db: tx}
}
func (r *BaseRepositoryImpl[T]) DB() *gorm.DB {
return r.db
}
@@ -0,0 +1,53 @@
package repository
import (
"context"
"fmt"
"gorm.io/gorm"
)
// Exists reports whether a record with the given ID exists for type T.
func Exists[T any](ctx context.Context, db *gorm.DB, id uint) (bool, error) {
var count int64
if err := db.WithContext(ctx).
Model(new(T)).
Where("id = ?", id).
Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
func ExistsByName[T any](ctx context.Context, db *gorm.DB, name string, excludeID *uint) (bool, error) {
var count int64
q := db.WithContext(ctx).
Model(new(T)).
Where("name = ?", name).
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
}
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
}
@@ -0,0 +1,41 @@
package service
import (
"context"
"fmt"
"strings"
"github.com/gofiber/fiber/v2"
)
// RelationCheck describes a foreign-key style dependency that must exist before processing.
type RelationCheck struct {
Name string
ID *uint
Exists func(context.Context, uint) (bool, error)
}
// EnsureRelations validates that each RelationCheck is satisfied, returning consistent Fiber errors.
func EnsureRelations(ctx context.Context, checks ...RelationCheck) error {
for _, check := range checks {
if check.ID == nil {
continue
}
exists, err := check.Exists(ctx, *check.ID)
if err != nil {
return fiber.NewError(
fiber.StatusInternalServerError,
fmt.Sprintf("Failed to check %s", strings.ToLower(check.Name)),
)
}
if !exists {
return fiber.NewError(
fiber.StatusNotFound,
fmt.Sprintf("%s with id %d not found", check.Name, *check.ID),
)
}
}
return nil
}
@@ -0,0 +1,83 @@
package validation
import (
"reflect"
"regexp"
"strings"
"github.com/go-playground/validator/v10"
)
var (
reUpper = regexp.MustCompile(`[A-Z]`)
reLower = regexp.MustCompile(`[a-z]`)
reDigit = regexp.MustCompile(`[0-9]`)
reSym = regexp.MustCompile(`[^A-Za-z0-9]`)
)
func Password(fl validator.FieldLevel) bool {
pw := fl.Field().String()
pw = strings.TrimSpace(pw)
if len(pw) < 8 {
return false
}
if !reUpper.MatchString(pw) {
return false
}
if !reLower.MatchString(pw) {
return false
}
if !reDigit.MatchString(pw) {
return false
}
if !reSym.MatchString(pw) {
return false
}
if strings.Contains(pw, " ") {
return false
}
parent := fl.Parent()
if parent.IsValid() && parent.Kind() == reflect.Struct {
emailField := parent.FieldByName("Email")
if emailField.IsValid() && emailField.Kind() == reflect.String {
if email := emailField.String(); email != "" {
if i := strings.IndexByte(email, '@'); i > 0 {
local := strings.ToLower(email[:i])
if local != "" && strings.Contains(strings.ToLower(pw), local) {
return false
}
}
}
}
}
return true
}
func RequiredStrict(fl validator.FieldLevel) bool {
field := fl.Field()
switch field.Kind() {
case reflect.String:
return field.String() != ""
case reflect.Ptr:
return !field.IsNil()
}
return field.IsValid() && !field.IsZero()
}
func OmitemptyStrict(fl validator.FieldLevel) bool {
field := fl.Field()
if !field.IsValid() || field.IsZero() {
return true
}
if field.Kind() == reflect.String {
return field.String() != ""
}
return true
}
+74
View File
@@ -0,0 +1,74 @@
package validation
import (
"errors"
"fmt"
"github.com/go-playground/validator/v10"
)
var customMessages = map[string]string{
"required": "Field %s is required",
"required_strict": "Field %s is required and cannot be null or empty",
"omitempty_strict": "Field %s cannot be null or empty when provided",
"email": "Invalid email address for field %s",
"min": "Field %s must have a minimum length of %s characters",
"max": "Field %s must have a maximum length of %s characters",
"len": "Field %s must be exactly %s characters long",
"number": "Field %s must be a number",
"positive": "Field %s must be a positive number",
"alphanum": "Field %s must contain only alphanumeric characters",
"oneof": "Invalid value for field %s",
"password": "Field %s must be at least 8 characters, contain uppercase, lowercase, number, and special character",
}
func CustomErrorMessages(err error) map[string]string {
var validationErrors validator.ValidationErrors
if errors.As(err, &validationErrors) {
return generateErrorMessages(validationErrors)
}
return nil
}
func generateErrorMessages(validationErrors validator.ValidationErrors) map[string]string {
errorsMap := make(map[string]string)
for _, err := range validationErrors {
fieldName := err.StructNamespace()
tag := err.Tag()
customMessage := customMessages[tag]
if customMessage != "" {
errorsMap[fieldName] = formatErrorMessage(customMessage, err, tag)
} else {
errorsMap[fieldName] = defaultErrorMessage(err)
}
}
return errorsMap
}
func formatErrorMessage(customMessage string, err validator.FieldError, tag string) string {
if tag == "min" || tag == "max" || tag == "len" {
return fmt.Sprintf(customMessage, err.Field(), err.Param())
}
return fmt.Sprintf(customMessage, err.Field())
}
func defaultErrorMessage(err validator.FieldError) string {
return fmt.Sprintf("Field validation for '%s' failed on the '%s' tag", err.Field(), err.Tag())
}
func Validator() *validator.Validate {
validate := validator.New()
if err := validate.RegisterValidation("password", Password); err != nil {
return nil
}
if err := validate.RegisterValidation("required_strict", RequiredStrict); err != nil {
return nil
}
if err := validate.RegisterValidation("omitempty_strict", OmitemptyStrict); err != nil {
return nil
}
return validate
}
BIN
View File
Binary file not shown.
+261
View File
@@ -0,0 +1,261 @@
package config
import (
"encoding/json"
"fmt"
"strings"
"time"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/spf13/viper"
)
type SSOClientConfig struct {
PublicID string `json:"public_id"`
RedirectURI string `json:"redirect_uri"`
Scope string `json:"scope"`
// Prompt string `json:"prompt"`
DefaultReturnURI string `json:"default_return_uri"`
AllowedReturnOrigins []string `json:"allowed_return_origins"`
SyncSecret string `json:"sync_secret"`
}
var (
IsProd bool
AppHost string
Version string
LogLevel string
AppPort int
DBHost string
DBUser string
DBPassword string
DBName string
DBPort int
DBSSLMode string
DBSSLRootCert string
DBSSLCert string
DBSSLKey string
JWTSecret string
JWTAccessExp int
JWTRefreshExp int
JWTResetPasswordExp int
JWTVerifyEmailExp int
RedisURL string
CORSAllowOrigins []string
CORSAllowMethods []string
CORSAllowHeaders []string
CORSExposeHeaders []string
CORSAllowCredentials bool
CORSMaxAge int
SSOIssuer string
SSOJWKSURL string
SSOAllowedAudiences []string
SSOAuthorizeURL string
SSOTokenURL string
SSOGetMeURL string
SSOClients map[string]SSOClientConfig
SSOAccessCookieName string
SSORefreshCookieName string
SSOCookieDomain string
SSOCookieSecure bool
SSOCookieSameSite string
SSOTokenBlacklistPrefix string
SSOPKCETTL time.Duration
SSOUserSyncDrift time.Duration
SSOUserSyncNonceTTL time.Duration
SSOUserSyncMaxBodyBytes int
)
func init() {
loadConfig()
// server configuration
IsProd = viper.GetString("APP_ENV") == "prod"
AppHost = viper.GetString("APP_HOST")
AppPort = viper.GetInt("APP_PORT")
Version = viper.GetString("VERSION")
LogLevel = viper.GetString("LOG_LEVEL")
// database configuration
DBHost = viper.GetString("DB_HOST")
DBUser = viper.GetString("DB_USER")
DBPassword = viper.GetString("DB_PASSWORD")
DBName = viper.GetString("DB_NAME")
DBPort = viper.GetInt("DB_PORT")
DBSSLMode = defaultString(viper.GetString("DB_SSLMODE"), "disable")
DBSSLRootCert = strings.TrimSpace(viper.GetString("DB_SSLROOTCERT"))
DBSSLCert = strings.TrimSpace(viper.GetString("DB_SSLCERT"))
DBSSLKey = strings.TrimSpace(viper.GetString("DB_SSLKEY"))
// jwt configuration
JWTSecret = viper.GetString("JWT_SECRET")
JWTAccessExp = viper.GetInt("JWT_ACCESS_EXP_MINUTES")
JWTRefreshExp = viper.GetInt("JWT_REFRESH_EXP_DAYS")
JWTResetPasswordExp = viper.GetInt("JWT_RESET_PASSWORD_EXP_MINUTES")
JWTVerifyEmailExp = viper.GetInt("JWT_VERIFY_EMAIL_EXP_MINUTES")
//Cors
CORSAllowOrigins = parseList("CORS_ALLOW_ORIGINS")
CORSAllowMethods = parseListWithDefault("CORS_ALLOW_METHODS", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
CORSAllowHeaders = parseListWithDefault("CORS_ALLOW_HEADERS", "Content-Type,Authorization,X-Requested-With")
CORSExposeHeaders = parseList("CORS_EXPOSE_HEADERS")
CORSAllowCredentials = viper.GetBool("CORS_ALLOW_CREDENTIALS")
CORSMaxAge = viper.GetInt("CORS_MAX_AGE")
// Redis
RedisURL = viper.GetString("REDIS_URL")
// SSO integration
SSOIssuer = viper.GetString("SSO_ISSUER")
SSOJWKSURL = viper.GetString("SSO_JWKS_URL")
SSOAllowedAudiences = parseList("SSO_ALLOWED_AUDIENCES")
SSOAuthorizeURL = viper.GetString("SSO_AUTHORIZE_URL")
SSOTokenURL = viper.GetString("SSO_TOKEN_URL")
SSOGetMeURL = viper.GetString("SSO_GETME_URL")
SSOAccessCookieName = defaultString(viper.GetString("SSO_ACCESS_COOKIE_NAME"), "sso_access")
SSORefreshCookieName = defaultString(viper.GetString("SSO_REFRESH_COOKIE_NAME"), "sso_refresh")
SSOCookieDomain = viper.GetString("SSO_COOKIE_DOMAIN")
SSOCookieSecure = viper.GetBool("SSO_COOKIE_SECURE")
SSOCookieSameSite = defaultString(viper.GetString("SSO_COOKIE_SAMESITE"), "Lax")
SSOTokenBlacklistPrefix = defaultString(viper.GetString("SSO_TOKEN_BLACKLIST_PREFIX"), "sso:blacklist")
if ttl := viper.GetInt("SSO_PKCE_TTL_SECONDS"); ttl > 0 {
SSOPKCETTL = time.Duration(ttl) * time.Second
} else {
SSOPKCETTL = 5 * time.Minute
}
SSOClients = loadSSOClients("SSO_CLIENTS")
if drift := viper.GetInt("SSO_USER_SYNC_SIGNATURE_DRIFT_SECONDS"); drift > 0 {
SSOUserSyncDrift = time.Duration(drift) * time.Second
} else {
SSOUserSyncDrift = 2 * time.Minute
}
if ttl := viper.GetInt("SSO_USER_SYNC_NONCE_TTL_SECONDS"); ttl > 0 {
SSOUserSyncNonceTTL = time.Duration(ttl) * time.Second
} else {
SSOUserSyncNonceTTL = 10 * time.Minute
}
SSOUserSyncMaxBodyBytes = viper.GetInt("SSO_USER_SYNC_MAX_BODY_BYTES")
if SSOUserSyncMaxBodyBytes <= 0 {
SSOUserSyncMaxBodyBytes = 32 * 1024
}
if IsProd {
ensureProdConfig()
}
}
func loadConfig() {
viper.AutomaticEnv()
viper.SetConfigFile(".env")
if err := viper.ReadInConfig(); err == nil {
utils.Log.Info("Config file loaded from .env")
} else {
utils.Log.Warn("No .env file found, using environment variables only")
}
}
func parseList(key string) []string {
raw := strings.TrimSpace(viper.GetString(key))
if raw == "" {
return nil
}
if strings.HasPrefix(raw, "[") {
var arr []string
if json.Unmarshal([]byte(raw), &arr) == nil {
for i := range arr {
arr[i] = strings.TrimSpace(arr[i])
}
return arr
}
}
parts := strings.Split(raw, ",")
out := make([]string, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
if p != "" {
out = append(out, p)
}
}
return out
}
func parseListWithDefault(key, def string) []string {
if v := parseList(key); len(v) > 0 {
return v
}
// fallback ke default CSV
parts := strings.Split(def, ",")
for i := range parts {
parts[i] = strings.TrimSpace(parts[i])
}
return parts
}
func loadSSOClients(key string) map[string]SSOClientConfig {
clients := make(map[string]SSOClientConfig)
raw := strings.TrimSpace(viper.GetString(key))
if raw == "" {
return clients
}
if err := json.Unmarshal([]byte(raw), &clients); err != nil {
utils.Log.Errorf("Failed to parse %s: %v", key, err)
return make(map[string]SSOClientConfig)
}
result := make(map[string]SSOClientConfig, len(clients))
for alias, cfg := range clients {
alias = strings.ToLower(strings.TrimSpace(alias))
for i, origin := range cfg.AllowedReturnOrigins {
cfg.AllowedReturnOrigins[i] = strings.TrimSpace(origin)
}
cfg.SyncSecret = strings.TrimSpace(cfg.SyncSecret)
result[alias] = cfg
}
return result
}
func defaultString(v, def string) string {
if strings.TrimSpace(v) == "" {
return def
}
return v
}
func ensureProdConfig() {
if SSOAuthorizeURL == "" || !strings.HasPrefix(SSOAuthorizeURL, "https://") {
panic("SSO_AUTHORIZE_URL must be https in production")
}
if SSOTokenURL == "" || !strings.HasPrefix(SSOTokenURL, "https://") {
panic("SSO_TOKEN_URL must be https in production")
}
if SSOGetMeURL == "" || !strings.HasPrefix(SSOGetMeURL, "https://") {
panic("SSO_GETME_URL must be https in production")
}
if !SSOCookieSecure {
panic("SSO_COOKIE_SECURE must be true in production")
}
if SSOCookieDomain == "" {
panic("SSO_COOKIE_DOMAIN must be configured in production")
}
if len(SSOAllowedAudiences) == 0 {
panic("SSO_ALLOWED_AUDIENCES must contain at least one audience in production")
}
for alias, cfg := range SSOClients {
if strings.TrimSpace(cfg.SyncSecret) == "" {
panic(fmt.Sprintf("SSO_CLIENTS[%s].sync_secret must be configured in production", alias))
}
if len(cfg.SyncSecret) < 16 {
panic(fmt.Sprintf("SSO_CLIENTS[%s].sync_secret must be at least 16 characters", alias))
}
}
if SSOUserSyncDrift <= 0 {
panic("SSO_USER_SYNC_SIGNATURE_DRIFT_SECONDS must be greater than zero in production")
}
if SSOUserSyncNonceTTL <= 0 {
panic("SSO_USER_SYNC_NONCE_TTL_SECONDS must be greater than zero in production")
}
if SSOUserSyncMaxBodyBytes <= 0 {
panic("SSO_USER_SYNC_MAX_BODY_BYTES must be greater than zero in production")
}
}
+20
View File
@@ -0,0 +1,20 @@
package config
import (
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/bytedance/sonic"
"github.com/gofiber/fiber/v2"
)
func FiberConfig() fiber.Config {
return fiber.Config{
Prefork: IsProd,
CaseSensitive: true,
ServerHeader: "Fiber",
AppName: "Fiber API",
ErrorHandler: utils.ErrorHandler,
JSONEncoder: sonic.Marshal,
JSONDecoder: sonic.Unmarshal,
}
}
+14
View File
@@ -0,0 +1,14 @@
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArf9cLsf3m4TituVqDwvM
yaUwQ0rzDfOcmF/N+rHvgMMv1yyR4FcozoGk1NFfL/4jDIVm9FLUS68foPDo0iu5
shNY0pwSsps9lcyWxQVhUVJzh489S53hU799PiDrUPBxYTcpy3EO/jX0HOZJs5dl
N/4C54LYrVdXyleG82NLNjcMnNGr3VGc6zE7B3YYd9/daPyr+QBpeUL5BIzUZbeu
sI0NMIxucaqxMKWF62CDWTrwfSSoFOubI9FZ9tkkWro01wVFK35GseQCsDtEmJ9v
kb81LvfM2AcPLr+g1kN8dVeZLNNQTMrmxaWXFiwwEgayJ8q01pHfgAxg42ariKEK
fX9kFx/3Rs80qsXhQNEkoCOwQBRNwrRxRzNfVkvuE0aRVoO6PVFE1gDOLUV2fJJs
QUpAWMzZ/+e/N+1gKMtbaCbz2dLqnA6KkdMdHe79dMFVGx2ZnRFbyALzM3S5XgNV
QtVvTri2PW/6ZH41T6MpLUANzuwaIEys1Az+8VLxOgBugb63xoORB2JDsebxEfsS
HBllECnBJVuBndkJRSnbqGjCKq4sl2xXo83nZ+2eNmZO/vkTxREl8aVp3DgaHWxp
OQIlZwbP9lsruTqSnQfH3/hLemrOhSh/hXfFguw3oOQjfeFwJBD8u7vGOl2vBi3C
hvb8hFdjzoUXAJLxWPl5+E0CAwEAAQ==
-----END PUBLIC KEY-----
+17
View File
@@ -0,0 +1,17 @@
package config
var allRoles = map[string][]string{
"user": {},
"admin": {"getUsers", "manageUsers"},
}
var Roles = getKeys(allRoles)
var RoleRights = allRoles
func getKeys(m map[string][]string) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
return keys
}
+8
View File
@@ -0,0 +1,8 @@
package config
const (
TokenTypeAccess = "access"
TokenTypeRefresh = "refresh"
TokenTypeResetPassword = "resetPassword"
TokenTypeVerifyEmail = "verifyEmail"
)
+58
View File
@@ -0,0 +1,58 @@
package database
import (
"fmt"
"strings"
"time"
"gitlab.com/mbugroup/lti-api.git/internal/config"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
func Connect(dbHost, dbName string) *gorm.DB {
parts := []string{
fmt.Sprintf("host=%s", dbHost),
fmt.Sprintf("user=%s", config.DBUser),
fmt.Sprintf("password=%s", config.DBPassword),
fmt.Sprintf("dbname=%s", dbName),
fmt.Sprintf("port=%d", config.DBPort),
fmt.Sprintf("sslmode=%s", config.DBSSLMode),
"TimeZone=Asia/Shanghai",
}
if config.DBSSLRootCert != "" {
parts = append(parts, fmt.Sprintf("sslrootcert=%s", config.DBSSLRootCert))
}
if config.DBSSLCert != "" {
parts = append(parts, fmt.Sprintf("sslcert=%s", config.DBSSLCert))
}
if config.DBSSLKey != "" {
parts = append(parts, fmt.Sprintf("sslkey=%s", config.DBSSLKey))
}
dsn := strings.Join(parts, " ")
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
SkipDefaultTransaction: true,
PrepareStmt: true,
TranslateError: true,
})
if err != nil {
utils.Log.Errorf("Failed to connect to database: %+v", err)
}
sqlDB, errDB := db.DB()
if errDB != nil {
utils.Log.Errorf("Failed to connect to database: %+v", errDB)
}
// Config connection pooling
sqlDB.SetMaxIdleConns(10)
sqlDB.SetMaxOpenConns(100)
sqlDB.SetConnMaxLifetime(60 * time.Minute)
return db
}
+1
View File
@@ -0,0 +1 @@
CREATE DATABASE IF NOT EXISTS db_lti_erp;
@@ -0,0 +1,43 @@
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;
DROP INDEX IF EXISTS products_sku_unique;
DROP INDEX IF EXISTS products_name_unique;
DROP TABLE IF EXISTS products;
DROP INDEX IF EXISTS flags_flagable_lookup;
DROP INDEX IF EXISTS flags_unique_flagable;
DROP TABLE IF EXISTS flags;
DROP INDEX IF EXISTS customers_name_unique;
DROP INDEX IF EXISTS customers_email_unique;
DROP TABLE IF EXISTS customers;
DROP INDEX IF EXISTS warehouses_name_unique;
DROP INDEX IF EXISTS product_categories_code_unique;
DROP INDEX IF EXISTS product_categories_name_unique;
DROP TABLE IF EXISTS product_categories;
DROP INDEX IF EXISTS nonstocks_name_unique;
DROP TABLE IF EXISTS nonstock_suppliers;
DROP TABLE IF EXISTS nonstocks;
DROP INDEX IF EXISTS banks_name_unique;
DROP TABLE IF EXISTS banks;
DROP INDEX IF EXISTS kandangs_name_unique;
DROP TABLE IF EXISTS warehouses;
DROP TABLE IF EXISTS kandangs;
DROP INDEX IF EXISTS locations_name_unique;
DROP TABLE IF EXISTS locations;
DROP INDEX IF EXISTS areas_name_unique;
DROP TABLE IF EXISTS areas;
DROP INDEX IF EXISTS uoms_name_unique;
DROP TABLE IF EXISTS uoms;
DROP TABLE IF EXISTS suppliers;
DROP INDEX IF EXISTS fcrs_name_unique;
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;
@@ -0,0 +1,333 @@
-- 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
);
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()
);
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
);
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
);
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
);
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
);
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
);
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
);
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
);
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
);
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
);
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
);
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
);
-- 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
);
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)
);
-- 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
);
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)
);
-- 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
);
-- 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,2 @@
ALTER TABLE users
DROP CONSTRAINT IF EXISTS users_id_user_key;
@@ -0,0 +1,2 @@
ALTER TABLE users
ADD CONSTRAINT users_id_user_key UNIQUE (id_user);
@@ -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,36 @@
CREATE TABLE IF NOT EXISTS project_chickins (
id BIGSERIAL PRIMARY KEY,
project_flock_kandang_id BIGINT NOT NULL,
chick_in_date DATE NOT NULL,
quantity NUMERIC(15, 3) NOT NULL,
note TEXT,
created_by BIGINT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
deleted_at TIMESTAMPTZ
);
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
ALTER TABLE project_chickins
ADD CONSTRAINT fk_project_flock_kandang_id
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
ALTER TABLE project_chickins
ADD CONSTRAINT fk_created_by
FOREIGN KEY (created_by)
REFERENCES users(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
END $$;
-- INDEXES
CREATE INDEX IF NOT EXISTS idx_project_chickins_project_flock_kandang_id ON project_chickins (project_flock_kandang_id);
CREATE INDEX IF NOT EXISTS idx_project_chickins_created_by ON project_chickins (created_by);
@@ -0,0 +1 @@
DROP TABLE IF EXISTS project_flock_populations;
@@ -0,0 +1,36 @@
CREATE TABLE IF NOT EXISTS project_flock_populations (
id BIGSERIAL PRIMARY KEY,
project_flock_kandang_id BIGINT NOT NULL,
initial_quantity NUMERIC(15, 3) NOT NULL,
current_quantity NUMERIC(15, 3) NOT NULL,
reserved_quantity NUMERIC(15, 3),
created_by BIGINT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
deleted_at TIMESTAMPTZ
);
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
ALTER TABLE project_flock_populations
ADD CONSTRAINT fk_project_flock_kandang_id
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
ALTER TABLE project_flock_populations
ADD CONSTRAINT fk_created_by
FOREIGN KEY (created_by)
REFERENCES users(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
END $$;
-- INDEXES
CREATE INDEX IF NOT EXISTS idx_project_flock_populations_project_flock_kandang_id ON project_flock_populations (project_flock_kandang_id);
CREATE INDEX IF NOT EXISTS idx_project_flock_populations_created_by ON project_flock_populations (created_by);
@@ -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
);
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
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,22 @@
ALTER TABLE kandangs
DROP CONSTRAINT IF EXISTS kandangs_project_flock_id_fkey;
ALTER TABLE kandangs
DROP COLUMN IF EXISTS project_flock_id;
ALTER TABLE project_chickins
DROP CONSTRAINT fk_project_flock_kandang_id,
ADD CONSTRAINT fk_project_flock_kandang_id
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs(id)
ON UPDATE CASCADE
ON DELETE CASCADE;
ALTER TABLE project_flock_populations
DROP CONSTRAINT fk_project_flock_kandang_id,
ADD CONSTRAINT fk_project_flock_kandang_id
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs(id)
ON UPDATE CASCADE
ON DELETE CASCADE;
@@ -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;
File diff suppressed because it is too large Load Diff
+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"`
}
+19
View File
@@ -0,0 +1,19 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Area struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:areas_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"`
Locations []Location `gorm:"foreignKey:AreaId;references:Id"`
}
+21
View File
@@ -0,0 +1,21 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Bank struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:banks_name_unique,where:deleted_at IS NULL"`
Alias string `gorm:"not null;size:5"`
Owner *string `gorm:""`
AccountNumber string `gorm:"not null;size:50"`
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"`
}
+18
View File
@@ -0,0 +1,18 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Constant struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:idx_suppliers_name,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"`
}
+26
View File
@@ -0,0 +1,26 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Customer struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:customers_name_unique,where:deleted_at IS NULL"`
PicId uint `gorm:"not null"`
Type string `gorm:"not null;size:50"`
Address string `gorm:"not null"`
Phone string `gorm:"not null;size:20"`
Email string `gorm:"not null"`
AccountNumber string `gorm:"not null;size:50"`
Balance float64 `gorm:"default:0"`
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"`
Pic User `gorm:"foreignKey:PicId;references:Id"`
}
+19
View File
@@ -0,0 +1,19 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Fcr struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:idx_suppliers_name,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"`
Standards []FcrStandard `gorm:"foreignKey:FcrID;references:Id"`
}
+20
View File
@@ -0,0 +1,20 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type FcrStandard struct {
Id uint `gorm:"primaryKey"`
FcrID uint `gorm:"not null;index"`
Weight float64 `gorm:"type:numeric(15,3);not null"`
FcrNumber float64 `gorm:"type:numeric(15,3);not null"`
Mortality float64 `gorm:"type:numeric(15,3);not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Fcr Fcr `gorm:"foreignKey:FcrID;references:Id"`
}
+17
View File
@@ -0,0 +1,17 @@
package entities
import "time"
const (
FlagableTypeProduct = "products"
FlagableTypeNonstock = "nonstocks"
)
type Flag struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:flags_unique_flagable"`
FlagableID uint `gorm:"not null;uniqueIndex:flags_unique_flagable;index:flags_flagable_lookup,priority:2"`
FlagableType string `gorm:"size:50;not null;uniqueIndex:flags_unique_flagable;index:flags_flagable_lookup,priority:1"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
}
+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"`
}
+23
View File
@@ -0,0 +1,23 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Kandang struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:kandangs_name_unique,where:deleted_at IS NULL"`
Status string `gorm:"type:varchar(50);not null"`
LocationId uint `gorm:"not null"`
PicId uint `gorm:"not null"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Location Location `gorm:"foreignKey:LocationId;references:Id"`
Pic User `gorm:"foreignKey:PicId;references:Id"`
ProjectFlockKandangs []ProjectFlockKandang `gorm:"foreignKey:KandangId;references:Id" json:"-"`
}
+21
View File
@@ -0,0 +1,21 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Location struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:locations_name_unique,where:deleted_at IS NULL"`
Address string `gorm:"not null"`
AreaId 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"`
Area Area `gorm:"foreignKey:AreaId;references:Id"`
}
+22
View File
@@ -0,0 +1,22 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Nonstock struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:nonstocks_name_unique,where:deleted_at IS NULL"`
UomId 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"`
Uom Uom `gorm:"foreignKey:UomId;references:Id"`
Suppliers []Supplier `gorm:"many2many:nonstock_suppliers;joinForeignKey:NonstockID;joinReferences:SupplierID"`
Flags []Flag `gorm:"polymorphic:Flagable;polymorphicValue:nonstocks"`
}
+9
View File
@@ -0,0 +1,9 @@
package entities
import "time"
type NonstockSupplier struct {
NonstockID uint `gorm:"primaryKey"`
SupplierID uint `gorm:"primaryKey"`
CreatedAt time.Time `gorm:"autoCreateTime"`
}
+19
View File
@@ -0,0 +1,19 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type ProductCategory struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:product_categories_name_unique,where:deleted_at IS NULL"`
Code string `gorm:"not null;size:10;uniqueIndex:product_categories_code_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"`
}
+30
View File
@@ -0,0 +1,30 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Product struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:products_name_unique,where:deleted_at IS NULL"`
Brand string `gorm:"not null"`
Sku *string `gorm:"size:100;uniqueIndex:products_sku_unique,where:deleted_at IS NULL"`
UomId uint `gorm:"not null"`
ProductCategoryId uint `gorm:"not null"`
ProductPrice float64 `gorm:"type:numeric(15,3);not null"`
SellingPrice *float64 `gorm:"type:numeric(15,3)"`
Tax *float64 `gorm:"type:numeric(15,3)"`
ExpiryPeriod *int `gorm:""`
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"`
Uom Uom `gorm:"foreignKey:UomId;references:Id"`
ProductCategory ProductCategory `gorm:"foreignKey:ProductCategoryId;references:Id"`
Suppliers []Supplier `gorm:"many2many:product_suppliers;joinForeignKey:ProductID;joinReferences:SupplierID"`
Flags []Flag `gorm:"polymorphic:Flagable;polymorphicValue:products"`
}
+9
View File
@@ -0,0 +1,9 @@
package entities
import "time"
type ProductSupplier struct {
ProductID uint `gorm:"primaryKey"`
SupplierID uint `gorm:"primaryKey"`
CreatedAt time.Time `gorm:"autoCreateTime"`
}
+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"`
}
+24
View File
@@ -0,0 +1,24 @@
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"`
Quantity float64 `gorm:"not null"`
Note string `gorm:"type:text"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
ProjectFlockKandang ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
}
@@ -0,0 +1,23 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type ProjectFlockPopulation struct {
Id uint `gorm:"primaryKey"`
ProjectFlockKandangId uint `gorm:"not null;index;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
InitialQuantity float64 `gorm:"type:numeric(15,3);not null"`
CurrentQuantity float64 `gorm:"type:numeric(15,3);not null"`
ReservedQuantity float64 `gorm:"type:numeric(15,3)"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"`
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
}
+30
View File
@@ -0,0 +1,30 @@
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"`
}
+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"`
}

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