Compare commits

...

143 Commits

Author SHA1 Message Date
ragilap 5650253307 Merge branch 'feat/BE/Sprint-6' of https://gitlab.com/mbugroup/lti-api into dev/ragil 2025-12-03 17:51:48 +07:00
Hafizh A. Y. 79bbe61dab Merge branch 'dev/gio' into 'feat/BE/Sprint-6'
Feat[BE][US#283]: init module closing

See merge request mbugroup/lti-api!76
2025-12-03 09:30:00 +00:00
giovanni-ce fa5609c183 Feat[BE][US#283]: init module closing 2025-12-03 16:12:58 +07:00
Hafizh A. Y. 966d616022 Merge branch 'dev/hafizh' into 'feat/BE/Sprint-6'
unfinish: fifo system

See merge request mbugroup/lti-api!75
2025-12-02 10:38:19 +00:00
Hafizh A. Y. 53c321c3e3 Merge branch 'dev/teguh' into 'feat/BE/Sprint-6'
[FEAT/BE][277] : expense adjustment with new ERD and mockup

See merge request mbugroup/lti-api!73
2025-12-01 10:21:57 +00:00
kris 91ad7ad5e0 Update .gitlab-ci.yml change https to ssh 2025-12-01 04:40:38 +00:00
aguhh18 79c754312e FEAT[BE]: adjust api match with mock API 2025-11-28 15:18:49 +07:00
aguhh18 f3b14cb8f2 Feat[BE]: create project budget repo, entity, and migration 2025-11-27 14:28:48 +07:00
aguhh18 886446b55f Feat[BE]: refactor expense API and expense table match with new ERD 2025-11-27 13:53:35 +07:00
ragilap dbeb0b62cb Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into dev/ragil 2025-11-26 11:14:08 +07:00
ragilap 240496584f fix: project flock dto 2025-11-26 11:09:07 +07:00
ragilap c02f72c5e5 fix: next period,purchase before bop, integration auth module,fix validation-master data 2025-11-25 10:32:15 +07:00
aguhh18 99688c8e11 FIX[BE]: fixing issue failed delivery order, fixing unique constraint sales order 2025-11-24 14:35:20 +07:00
aguhh18 1ceda3623e Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2025-11-24 10:53:06 +07:00
Adnan Zahir 2e2aed67b8 Merge branch 'feat/BE/Sprint-5' into 'development'
[FEAT/BE][US#159,160,161,162,163,164,255,256] : Purchase Request, Purchase Order, Sales Order, Delivery Order, Expense Submission, Expense Realization

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

See merge request mbugroup/lti-api!52
2025-11-05 08:58:07 +00:00
ragilap f97d404121 fix(BE-78)change typedata in recording dto and validation for create and update 2025-11-04 12:05:04 +07:00
kris 3ecf39814e Update .gitlab-ci.yml file 2025-11-04 03:32:25 +00:00
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
GitLab Deploy Bot b459245c5c update secure DB setup and env isolation for LTI API 2025-10-31 10:32:05 +07: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
GitLab Deploy Bot c4448594e2 Actived .gitlab-ci.yml 2025-10-27 16:25:55 +07:00
GitLab Deploy Bot fb831208f4 Actived .gitlab-ci.yml 2025-10-27 16:22:20 +07:00
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
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
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
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
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
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
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
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
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
277 changed files with 15523 additions and 4927 deletions
-34
View File
@@ -1,34 +0,0 @@
# server configuration
# Env value : prod || dev
VERSION=0.0.1
APP_ENV=dev
APP_HOST=0.0.0.0
APP_PORT=8081
APP_URL=http://localhost:8081
# database configuration
DB_HOST=postgresdb
DB_USER=postgres
DB_PASSWORD=changeme
DB_NAME=db_lti_erp
DB_PORT=5432
DB_PORT_HOST=5542
# JWT
JWT_SECRET=changeme
JWT_ACCESS_EXP_MINUTES=30
JWT_REFRESH_EXP_DAYS=30
JWT_RESET_PASSWORD_EXP_MINUTES=10
JWT_VERIFY_EMAIL_EXP_MINUTES=10
# CORS
CORS_ALLOW_ORIGINS=changeme
CORS_ALLOW_METHODS=GET,POST,PUT,PATCH,DELETE,OPTIONS
CORS_ALLOW_HEADERS=Authorization,Content-Type,X-Requested-With
CORS_EXPOSE_HEADERS=Link,Location
CORS_ALLOW_CREDENTIALS=true
CORS_MAX_AGE=600
# Redis
REDIS_URL=redis://redis:6379/0
REDIS_PORT_HOST=6381
+5
View File
@@ -10,8 +10,13 @@ bin/
*.exe *.exe
*.out *.out
Makefile
docker-compose.local.yml
docker-compose.yaml
Dockerfile.local
# Go build cache # Go build cache
.gocache/ .gocache/
vendor
# Logs & reports # Logs & reports
*.log *.log
+90
View File
@@ -0,0 +1,90 @@
stages:
- deploy
deploy-dev:
stage: deploy
image: alpine:3.20
variables:
DEPLOY_APP: "LTI-MBUGROUP"
# Opsional: kalau pakai submodule, ini bikin clone submodule pakai SSH juga
GIT_SUBMODULE_STRATEGY: recursive
GIT_DEPTH: "1"
before_script:
- echo "🧰 Installing dependencies..."
- apk update && apk add --no-cache openssh git curl bash
# Setup SSH di runner
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- eval "$(ssh-agent -s)"
- ssh-add ~/.ssh/id_rsa
# Trust host keys (server + gitlab) biar SSH gak nanya interaktif
- ssh-keyscan -H "$SERVER_IP" >> ~/.ssh/known_hosts
- ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
script:
- echo "🚀 Deploying latest code to $SERVER_USER@$SERVER_IP"
- >
if ssh -o StrictHostKeyChecking=no "$SERVER_USER@$SERVER_IP" "
set -e
cd /home/devops/docker/deployment/development/lti-api
# Pastikan remote origin SSH (antisipasi kalau pernah ke-set HTTPS)
git remote set-url origin git@gitlab.com:mbugroup/lti-api.git
# Pastikan server percaya gitlab.com juga (untuk git fetch via SSH)
mkdir -p ~/.ssh
ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
# Fetch/reset pakai SSH
GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no' git fetch origin development
git reset --hard origin/development
docker compose restart dev-api-lti || docker compose up -d dev-api-lti
"; then
STATUS='success';
else
STATUS='failed';
fi;
RUN_URL="${CI_PROJECT_URL}/-/pipelines/${CI_PIPELINE_ID}";
if [ "$STATUS" = "success" ]; then
COLOR=3066993;
TITLE="✅ Deployment API Succeeded";
DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` completed successfully.";
else
COLOR=15158332;
TITLE="❌ Deployment API Failed Gaes";
DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` failed.";
fi;
echo "{
\"username\": \"CI Bot\",
\"embeds\": [{
\"title\": \"$TITLE\",
\"description\": \"$DESC\",
\"color\": $COLOR,
\"fields\": [
{\"name\": \"Repository\", \"value\": \"${CI_PROJECT_PATH}\", \"inline\": true},
{\"name\": \"Actor\", \"value\": \"${GITLAB_USER_LOGIN}\", \"inline\": true},
{\"name\": \"Commit\", \"value\": \"${CI_COMMIT_SHA}\", \"inline\": false},
{\"name\": \"Pipeline\", \"value\": \"[Open run](${RUN_URL})\", \"inline\": false}
]
}]
}" > payload.json;
echo "📡 Sending notification to Discord...";
curl -sS -H "Content-Type: application/json" \
-d @payload.json "$DISCORD_WEBHOOK_URL";
only:
- development
environment:
name: development
View File
-120
View File
@@ -1,120 +0,0 @@
# --- Load .env kalau ada, dan export ke shell child ---
ifneq (,$(wildcard .env))
include .env
export
endif
# --- Konfigurasi umum ---
COMPOSE ?= docker compose -f docker-compose.local.yml
NETWORK ?= lti-api_go-network
MIGRATE_IMAGE ?= migrate/migrate
MIGRATIONS_DIR := $(PWD)/internal/database/migrations
# Fallback agar tetap jalan meski .env kosong
DB_HOST ?= postgresdb
DB_PORT ?= 5432
DB_USER ?= postgres
DB_PASSWORD ?= postgres
DB_NAME ?= db_lti_erp
DB_URL := postgres://$(DB_USER):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable
# Tunggu DB ready memakai pg_isready dari image postgres
WAIT_DB := docker run --rm --network $(NETWORK) postgres:alpine \
sh -c 'until pg_isready -h $(DB_HOST) -p $(DB_PORT) -U $(DB_USER) -d $(DB_NAME); do echo "waiting for postgres..."; sleep 1; done'
# Default target
.DEFAULT_GOAL := start
# --- Daftar phony targets ---
.PHONY: start build test lint gen \
db-up wait-db \
migration-% migrate-up migrate-down migrate-fresh \
seed \
docker-local docker-down docker-nuke docker-cache psql
# --- Go workflow ---
start:
@go run cmd/api/main.go
build:
@go build -o tmp/app ./cmd/api
test:
@go test ./test/...
lint:
@golangci-lint run
# --- Compose / DB helpers ---
db-up:
@$(COMPOSE) up -d postgresdb
wait-db:
@$(WAIT_DB)
# --- Migration (pembuatan file) ---
# Contoh: make migration-create_users_table
# ":" akan diubah ke "_" (biar aman untuk nama file)
migration-%:
@migrate create -ext sql -dir $(MIGRATIONS_DIR) $(subst :,_,$*)
# --- Migration (apply via docker image 'migrate') ---
migrate-up: db-up wait-db
@docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" up
# Contoh:
# make migrate-down step=2 → rollback 2 step
# make migrate-down → rollback semua
migrate-down: db-up wait-db
@if [ -n "$(step)" ]; then \
echo "⬇️ Migrating down $(step) step(s)..."; \
docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" down $(step); \
else \
echo "⬇️ Migrating down ALL steps..."; \
docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" down -all; \
fi
migrate-fresh: migrate-down migrate-up
@true
# Pakai: make migrate-force v=20250917120000
migrate-force:
@docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" force $(v)
# --- Seeder ---
seed: db-up wait-db
@$(COMPOSE) run --rm app go run cmd/seed/main.go
# --- Docker orchestration convenience ---
docker-local:
@$(COMPOSE) up --build -d
docker-down:
@$(COMPOSE) down --remove-orphans
# ⚠️ Akan menghapus container, images dan volumes.
docker-nuke:
@$(COMPOSE) down --rmi all --volumes --remove-orphans
docker-cache:
@docker builder prune -f
# --- PSQL shell ke DB di container ---
psql: db-up
@$(COMPOSE) exec -it postgresdb psql -U $(DB_USER) -d $(DB_NAME)
# Single feature
# example: make gen feat=product-category
# Sub feature
# make gen feat=master/area
gen:
@go run tools/gen.go $(feat)
# @goimports -w internal
+41
View File
@@ -9,10 +9,13 @@ import (
"syscall" "syscall"
"time" "time"
"gitlab.com/mbugroup/lti-api.git/internal/cache"
"gitlab.com/mbugroup/lti-api.git/internal/config" "gitlab.com/mbugroup/lti-api.git/internal/config"
"gitlab.com/mbugroup/lti-api.git/internal/database" "gitlab.com/mbugroup/lti-api.git/internal/database"
"gitlab.com/mbugroup/lti-api.git/internal/middleware" "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/route"
"gitlab.com/mbugroup/lti-api.git/internal/sso"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@@ -33,6 +36,7 @@ func main() {
defer closeDatabase(db) defer closeDatabase(db)
rdb := setupRedis() rdb := setupRedis()
defer rdb.Close() defer rdb.Close()
setupSSO(ctx, rdb)
setupRoutes(app, db, rdb) setupRoutes(app, db, rdb)
address := fmt.Sprintf("%s:%d", config.AppHost, config.AppPort) address := fmt.Sprintf("%s:%d", config.AppHost, config.AppPort)
@@ -52,10 +56,47 @@ func setupRedis() *redis.Client {
if err := rdb.Ping(context.Background()).Err(); err != nil { if err := rdb.Ping(context.Background()).Err(); err != nil {
utils.Log.Fatalf("Redis ping failed: %v", err) utils.Log.Fatalf("Redis ping failed: %v", err)
} }
cache.SetRedis(rdb)
utils.Log.Infof("Redis connected: %s", config.RedisURL) utils.Log.Infof("Redis connected: %s", config.RedisURL)
return rdb 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 { func setupFiberApp() *fiber.App {
app := fiber.New(config.FiberConfig()) app := fiber.New(config.FiberConfig())
+3
View File
@@ -0,0 +1,3 @@
POSTGRES_USER=postgres
POSTGRES_PASSWORD=Postgres@Secure2025!
POSTGRES_DB=db_lti_erp
+47
View File
@@ -0,0 +1,47 @@
-- ============================================================
-- 🧩 INIT SCRIPT: CREATE LIMITED APP USER FOR LTI API
-- ============================================================
-- Buat user aplikasi jika belum ada
DO
$$
BEGIN
IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'app_lti_user') THEN
CREATE ROLE app_lti_user WITH LOGIN PASSWORD 'AppLti@Secure2025!' NOINHERIT NOCREATEROLE NOCREATEDB NOSUPERUSER;
RAISE NOTICE '✅ Role app_lti_user created successfully.';
ELSE
RAISE NOTICE '️ Role app_lti_user already exists.';
END IF;
END
$$;
-- Buat database jika belum ada
DO
$$
BEGIN
IF NOT EXISTS (SELECT FROM pg_database WHERE datname = 'db_lti_erp') THEN
CREATE DATABASE db_lti_erp OWNER app_lti_user;
RAISE NOTICE '✅ Database db_lti_erp created and owned by app_lti_user.';
ELSE
RAISE NOTICE '️ Database db_lti_erp already exists.';
END IF;
END
$$;
\connect db_lti_erp
-- Beri hak CRUD untuk app_lti_user
GRANT CONNECT ON DATABASE db_lti_erp TO app_lti_user;
GRANT USAGE ON SCHEMA public TO app_lti_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_lti_user;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_lti_user;
-- Set default privileges agar tabel baru juga bisa diakses
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_lti_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT USAGE, SELECT ON SEQUENCES TO app_lti_user;
-- Tampilkan hasil
\du app_lti_user
+98
View File
@@ -0,0 +1,98 @@
services:
dev-api-lti:
build:
context: .
dockerfile: Dockerfile
container_name: dev-api-lti
working_dir: /lti-api
command: ["/bin/sh", "scripts/entrypoint.sh"]
ports:
- "8081:8081"
env_file:
- .env
environment:
# override agar koneksi ke container internal
DB_HOST: dev-postgres-lti
DB_PORT: 5432
REDIS_URL: redis://dev-redis-lti:6379/0
volumes:
- .:/lti-api
- ./.air.toml:/lti-api/.air.toml:ro
- ./internal/config/jwtRS256.key:/run/keys/jwtRS256.key
- ./internal/config/jwtRS256.key.pub:/run/keys/jwtRS256.key.pub
depends_on:
- dev-postgres-lti
- dev-redis-lti
networks:
- lti-network
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8081/healthz || exit 1"]
interval: 10s
timeout: 3s
retries: 10
start_period: 10s
deploy:
resources:
limits:
cpus: "2.0"
memory: 2G
reservations:
cpus: "1.0"
memory: 512M
dev-postgres-lti:
image: postgres:15-alpine
container_name: dev-postgres-lti
restart: always
env_file:
- credential/.env.db
ports:
- "5433:5432"
volumes:
- dev-postgres-lti-data:/var/lib/postgresql/data
- ./credential:/docker-entrypoint-initdb.d:ro
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
deploy:
resources:
limits:
cpus: "1.0"
memory: 2G
reservations:
cpus: "0.5"
memory: 512M
dev-redis-lti:
image: redis:7-alpine
container_name: dev-redis-lti
restart: always
ports:
- "6380:6379"
networks:
- lti-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 10
deploy:
resources:
limits:
cpus: "0.5"
memory: 512M
reservations:
cpus: "0.2"
memory: 256M
networks:
lti-network:
driver: bridge
volumes:
dev-postgres-lti-data:
+5 -9
View File
@@ -3,12 +3,13 @@ module gitlab.com/mbugroup/lti-api.git
go 1.23 go 1.23
require ( require (
github.com/MicahParks/keyfunc/v2 v2.1.0
github.com/bytedance/sonic v1.12.1 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/go-playground/validator/v10 v10.27.0
github.com/gofiber/contrib/jwt v1.0.10 github.com/gofiber/contrib/jwt v1.0.10
github.com/gofiber/fiber/v2 v2.52.5 github.com/gofiber/fiber/v2 v2.52.5
github.com/golang-jwt/jwt/v5 v5.2.1 github.com/golang-jwt/jwt/v5 v5.2.1
github.com/jackc/pgconn v1.14.1
github.com/redis/go-redis/v9 v9.14.0 github.com/redis/go-redis/v9 v9.14.0
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
github.com/spf13/viper v1.19.0 github.com/spf13/viper v1.19.0
@@ -18,23 +19,23 @@ require (
) )
require ( require (
github.com/MicahParks/keyfunc/v2 v2.1.0 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect github.com/andybalholm/brotli v1.1.0 // indirect
github.com/bytedance/sonic/loader v0.2.0 // indirect github.com/bytedance/sonic/loader v0.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect github.com/cloudwego/iasm v0.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // 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/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // 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/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-cmp v0.6.0 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile 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/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect github.com/jackc/pgx/v5 v5.5.5 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect
@@ -50,7 +51,6 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/philhofer/fwd v1.1.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/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect
@@ -75,8 +75,4 @@ require (
golang.org/x/text v0.22.0 // indirect golang.org/x/text v0.22.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // 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
) )
+104 -19
View File
@@ -17,24 +17,22 @@ github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 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 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 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.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.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 h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 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 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 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 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 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 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= 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 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 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 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -43,6 +41,7 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 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 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= 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 h1:/ilGepl6i0Bntl0Zcd+lAzagY8BiS1+fEiAj32HMApk=
github.com/gofiber/contrib/jwt v1.0.10/go.mod h1:1qBENE6sZ6PPT4xIpBzx1VxeyROQO7sj48OlM1I9qdU= 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 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
@@ -51,18 +50,51 @@ github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17w
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/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 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 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 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 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 h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 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 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= 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 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 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 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
@@ -75,16 +107,28 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02
github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU= 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/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/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 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 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 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 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 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 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.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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
@@ -96,23 +140,28 @@ github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 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 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= 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.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 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE=
github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= 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.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 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 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 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 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= 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 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= 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 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 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 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
@@ -126,10 +175,15 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= 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/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.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.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.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 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.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.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.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.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ -150,24 +204,41 @@ github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRV
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= 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/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/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 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 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 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= 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 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 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-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.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 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= 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 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= 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.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/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-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-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.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.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 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= 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-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -175,7 +246,14 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.1.0/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 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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-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-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-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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -184,28 +262,43 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/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.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.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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 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/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-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.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.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.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.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.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.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 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 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-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.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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= 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-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 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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -213,12 +306,4 @@ gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= gorm.io/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 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= 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= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
+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: {},
}
@@ -13,6 +13,7 @@ type ApprovalRepository interface {
FindByTarget(ctx context.Context, workflow string, approvableID uint, modifier func(*gorm.DB) *gorm.DB) ([]entity.Approval, error) 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) 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) LatestByTargets(ctx context.Context, workflow string, approvableIDs []uint, modifier func(*gorm.DB) *gorm.DB) (map[uint]entity.Approval, error)
DeleteByTarget(ctx context.Context, workflow string, approvableID uint) error
} }
type approvalRepositoryImpl struct { type approvalRepositoryImpl struct {
@@ -104,3 +105,13 @@ func (r *approvalRepositoryImpl) LatestByTargets(
return result, nil return result, nil
} }
func (r *approvalRepositoryImpl) DeleteByTarget(
ctx context.Context,
workflow string,
approvableID uint,
) error {
return r.DB().WithContext(ctx).
Where("approvable_type = ? AND approvable_id = ?", workflow, approvableID).
Delete(&entity.Approval{}).Error
}
@@ -11,6 +11,7 @@ type BaseRepository[T any] interface {
GetAll(ctx context.Context, offset, limit int, modifier func(*gorm.DB) *gorm.DB) ([]T, int64, error) 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) 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) 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 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 CreateMany(ctx context.Context, entities []*T, modifier func(*gorm.DB) *gorm.DB) error
@@ -96,6 +97,21 @@ func (r *BaseRepositoryImpl[T]) GetByIDs(
return entities, nil 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 ---- // ---- CREATE ----
func (r *BaseRepositoryImpl[T]) CreateOne( func (r *BaseRepositoryImpl[T]) CreateOne(
ctx context.Context, ctx context.Context,
@@ -0,0 +1,75 @@
package repository
import (
"context"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type StockAllocationRepository interface {
BaseRepository[entity.StockAllocation]
FindActiveByUsable(ctx context.Context, usableType string, usableID uint, modifier func(*gorm.DB) *gorm.DB) ([]entity.StockAllocation, error)
ReleaseByUsable(ctx context.Context, usableType string, usableID uint, note *string, modifier func(*gorm.DB) *gorm.DB) error
}
type StockAllocationRepositoryImpl struct {
*BaseRepositoryImpl[entity.StockAllocation]
}
func NewStockAllocationRepository(db *gorm.DB) StockAllocationRepository {
return &StockAllocationRepositoryImpl{
BaseRepositoryImpl: NewBaseRepository[entity.StockAllocation](db),
}
}
func (r *StockAllocationRepositoryImpl) FindActiveByUsable(
ctx context.Context,
usableType string,
usableID uint,
modifier func(*gorm.DB) *gorm.DB,
) ([]entity.StockAllocation, error) {
var allocations []entity.StockAllocation
q := r.DB().WithContext(ctx).
Where("usable_type = ? AND usable_id = ? AND status = ?", usableType, usableID, entity.StockAllocationStatusActive)
if modifier != nil {
q = modifier(q)
}
if err := q.Order("created_at ASC").Find(&allocations).Error; err != nil {
return nil, err
}
return allocations, nil
}
func (r *StockAllocationRepositoryImpl) ReleaseByUsable(
ctx context.Context,
usableType string,
usableID uint,
note *string,
modifier func(*gorm.DB) *gorm.DB,
) error {
now := time.Now()
updates := map[string]any{
"status": entity.StockAllocationStatusReleased,
"released_at": now,
}
if note != nil {
updates["note"] = *note
}
q := r.DB().WithContext(ctx).
Model(&entity.StockAllocation{}).
Where("usable_type = ? AND usable_id = ? AND status = ?", usableType, usableID, entity.StockAllocationStatusActive)
if modifier != nil {
q = modifier(q)
}
return q.Updates(updates).Error
}
@@ -0,0 +1,820 @@
package service
import (
"context"
"errors"
"fmt"
"math"
"sort"
"strings"
"time"
"github.com/sirupsen/logrus"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
"gitlab.com/mbugroup/lti-api.git/internal/entities"
productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type FifoService interface {
RegisterStockable(cfg fifo.StockableConfig) error
RegisterUsable(cfg fifo.UsableConfig) error
Replenish(ctx context.Context, req StockReplenishRequest) (*StockReplenishResult, error)
Consume(ctx context.Context, req StockConsumeRequest) (*StockConsumeResult, error)
ReleaseUsage(ctx context.Context, req StockReleaseRequest) error
}
type fifoService struct {
db *gorm.DB
logger *logrus.Logger
allocations commonRepo.StockAllocationRepository
productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository
defaultOrderBy []string
pendingBatchPerUsable int
maxLotsPerStockable int
defaultAllocationNotes string
}
func NewFifoService(
db *gorm.DB,
allocations commonRepo.StockAllocationRepository,
productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository,
logger *logrus.Logger,
) FifoService {
if logger == nil {
logger = logrus.StandardLogger()
}
return &fifoService{
db: db,
logger: logger,
allocations: allocations,
productWarehouseRepo: productWarehouseRepo,
defaultOrderBy: []string{"created_at ASC", "id ASC"},
pendingBatchPerUsable: 25,
maxLotsPerStockable: 50,
}
}
func (s *fifoService) withTransaction(
ctx context.Context,
tx *gorm.DB,
fn func(*gorm.DB) error,
) error {
if tx != nil {
return fn(tx.WithContext(ctx))
}
return s.db.WithContext(ctx).Transaction(func(inner *gorm.DB) error {
return fn(inner)
})
}
func (s *fifoService) txOrDB(tx, db *gorm.DB) *gorm.DB {
if tx != nil {
return tx
}
return db
}
func (s *fifoService) RegisterStockable(cfg fifo.StockableConfig) error {
return fifo.RegisterStockable(cfg)
}
func (s *fifoService) RegisterUsable(cfg fifo.UsableConfig) error {
return fifo.RegisterUsable(cfg)
}
type StockReplenishRequest struct {
StockableKey fifo.StockableKey
StockableID uint
ProductWarehouseID uint
Quantity float64
Note *string
Tx *gorm.DB
}
type PendingResolution struct {
UsableKey fifo.UsableKey
UsableID uint
Quantity float64
}
type StockReplenishResult struct {
AddedQuantity float64
PendingResolved []PendingResolution
RemainingPending float64
}
type StockConsumeRequest struct {
UsableKey fifo.UsableKey
UsableID uint
ProductWarehouseID uint
Quantity float64
AllowPending bool
Note *string
Tx *gorm.DB
}
type AllocationDetail struct {
StockableKey fifo.StockableKey
StockableID uint
Quantity float64
}
type StockConsumeResult struct {
RequestedQuantity float64
UsageQuantity float64
PendingQuantity float64
AddedAllocations []AllocationDetail
ReleasedQuantity float64
}
type StockReleaseRequest struct {
UsableKey fifo.UsableKey
UsableID uint
Reason *string
Tx *gorm.DB
}
func (s *fifoService) Replenish(ctx context.Context, req StockReplenishRequest) (*StockReplenishResult, error) {
if req.StockableID == 0 || strings.TrimSpace(req.StockableKey.String()) == "" {
return nil, errors.New("stockable key and id are required")
}
if req.ProductWarehouseID == 0 {
return nil, errors.New("product warehouse id is required")
}
if req.Quantity <= 0 {
return nil, errors.New("quantity must be greater than zero")
}
cfg, ok := fifo.Stockable(req.StockableKey)
if !ok {
return nil, fmt.Errorf("stockable %q is not registered", req.StockableKey)
}
result := &StockReplenishResult{
AddedQuantity: req.Quantity,
}
err := s.withTransaction(ctx, req.Tx, func(tx *gorm.DB) error {
if err := s.incrementStockableQty(ctx, tx, cfg, req.StockableID, req.Quantity); err != nil {
return err
}
if err := s.productWarehouseRepo.AdjustQuantities(ctx, map[uint]float64{
req.ProductWarehouseID: req.Quantity,
}, func(db *gorm.DB) *gorm.DB {
return s.txOrDB(tx, db)
}); err != nil {
return err
}
resolved, err := s.resolvePendingForWarehouse(ctx, tx, req.ProductWarehouseID)
if err != nil {
return err
}
result.PendingResolved = resolved
return nil
})
if err != nil {
return nil, err
}
return result, nil
}
func (s *fifoService) Consume(ctx context.Context, req StockConsumeRequest) (*StockConsumeResult, error) {
if req.UsableID == 0 || strings.TrimSpace(req.UsableKey.String()) == "" {
return nil, errors.New("usable key and id are required")
}
if req.Quantity < 0 {
return nil, errors.New("quantity must be zero or greater")
}
cfg, ok := fifo.Usable(req.UsableKey)
if !ok {
return nil, fmt.Errorf("usable %q is not registered", req.UsableKey)
}
result := &StockConsumeResult{
RequestedQuantity: req.Quantity,
}
err := s.withTransaction(ctx, req.Tx, func(tx *gorm.DB) error {
ctxRow, err := s.loadUsableContext(ctx, tx, cfg, req.UsableID)
if err != nil {
return err
}
productWarehouseID := ctxRow.ProductWarehouseID
if productWarehouseID == 0 {
return fmt.Errorf("usable %q (id: %d) has no product warehouse reference", req.UsableKey, req.UsableID)
}
if req.ProductWarehouseID != 0 && req.ProductWarehouseID != productWarehouseID {
return fmt.Errorf("usable %q (id: %d) references product warehouse %d but %d was provided", req.UsableKey, req.UsableID, productWarehouseID, req.ProductWarehouseID)
}
currentUsage := ctxRow.UsageQty
currentPending := ctxRow.PendingQty
currentTotal := currentUsage + currentPending
delta := req.Quantity - currentTotal
var (
usageDelta float64
pendingDelta float64
addedAlloc []AllocationDetail
releasedAmount float64
)
switch {
case delta > 0:
allocationRes, err := s.allocateFromStock(ctx, tx, productWarehouseID, req.UsableKey, req.UsableID, delta)
if err != nil {
return err
}
if allocationRes.pending > 0 && !req.AllowPending {
return fmt.Errorf("insufficient stock: requested %.3f, allocated %.3f", req.Quantity, currentUsage+allocationRes.allocated)
}
usageDelta += allocationRes.allocated
pendingDelta += allocationRes.pending
addedAlloc = allocationRes.allocations
if allocationRes.allocated > 0 {
if err := s.productWarehouseRepo.AdjustQuantities(ctx, map[uint]float64{
productWarehouseID: -allocationRes.allocated,
}, func(db *gorm.DB) *gorm.DB {
return s.txOrDB(tx, db)
}); err != nil {
return err
}
}
case delta < 0:
reductionTarget := -delta
if currentPending > 0 {
pendingReduction := math.Min(currentPending, reductionTarget)
if pendingReduction > 0 {
pendingDelta -= pendingReduction
reductionTarget -= pendingReduction
}
}
if reductionTarget > 0 {
released, err := s.releaseUsagePortion(ctx, tx, req.UsableKey, req.UsableID, reductionTarget)
if err != nil {
return err
}
if released+1e-6 < reductionTarget {
return fmt.Errorf("unable to release %.3f from usable %d, only %.3f available", reductionTarget, req.UsableID, released)
}
usageDelta -= released
releasedAmount = released
}
default:
// no change
}
if err := s.applyUsableDeltas(ctx, tx, cfg, req.UsableID, usageDelta, pendingDelta); err != nil {
return err
}
result.AddedAllocations = addedAlloc
result.ReleasedQuantity = releasedAmount
result.UsageQuantity = currentUsage + usageDelta
result.PendingQuantity = currentPending + pendingDelta
return nil
})
if err != nil {
return nil, err
}
return result, nil
}
func (s *fifoService) ReleaseUsage(ctx context.Context, req StockReleaseRequest) error {
if req.UsableID == 0 || strings.TrimSpace(req.UsableKey.String()) == "" {
return errors.New("usable key and id are required")
}
return s.withTransaction(ctx, req.Tx, func(tx *gorm.DB) error {
cfg, ok := fifo.Usable(req.UsableKey)
if !ok {
return fmt.Errorf("usable %q is not registered", req.UsableKey)
}
ctxRow, err := s.loadUsableContext(ctx, tx, cfg, req.UsableID)
if err != nil {
return err
}
var usageDelta, pendingDelta float64
if ctxRow.UsageQty > 0 {
if _, err := s.releaseUsagePortion(ctx, tx, req.UsableKey, req.UsableID, ctxRow.UsageQty); err != nil {
return err
}
usageDelta -= ctxRow.UsageQty
}
if ctxRow.PendingQty > 0 {
pendingDelta -= ctxRow.PendingQty
}
if err := s.applyUsableDeltas(ctx, tx, cfg, req.UsableID, usageDelta, pendingDelta); err != nil {
return err
}
return s.allocations.ReleaseByUsable(ctx, req.UsableKey.String(), req.UsableID, req.Reason, func(db *gorm.DB) *gorm.DB {
return s.txOrDB(tx, db)
})
})
}
// --- helpers ---
type usableContextRow struct {
ProductWarehouseID uint
UsageQty float64
PendingQty float64
}
func (s *fifoService) loadUsableContext(ctx context.Context, tx *gorm.DB, cfg fifo.UsableConfig, id uint) (*usableContextRow, error) {
var row usableContextRow
query := tx.Table(cfg.Table).
Select(fmt.Sprintf("%s AS product_warehouse_id, COALESCE(%s,0) AS usage_qty, COALESCE(%s,0) AS pending_qty", cfg.Columns.ProductWarehouseID, cfg.Columns.UsageQuantity, cfg.Columns.PendingQuantity)).
Where(fmt.Sprintf("%s = ?", cfg.Columns.ID), id).
Clauses(clause.Locking{Strength: "UPDATE"})
if cfg.Scope != nil {
query = cfg.Scope(query)
}
if err := query.Take(&row).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("usable record %d not found", id)
}
return nil, err
}
return &row, nil
}
func (s *fifoService) incrementStockableQty(ctx context.Context, tx *gorm.DB, cfg fifo.StockableConfig, id uint, qty float64) error {
column := cfg.Columns.TotalQuantity
query := tx.Table(cfg.Table).
Where(fmt.Sprintf("%s = ?", cfg.Columns.ID), id)
if cfg.Scope != nil {
query = cfg.Scope(query)
}
updates := map[string]any{
column: gorm.Expr(fmt.Sprintf("COALESCE(%s,0) + ?", column), qty),
}
if cfg.Columns.TotalUsedQuantity != "" {
updates[cfg.Columns.TotalUsedQuantity] = gorm.Expr(fmt.Sprintf("COALESCE(%s,0)", cfg.Columns.TotalUsedQuantity))
}
return query.Updates(updates).Error
}
func (s *fifoService) incrementStockableUsage(ctx context.Context, tx *gorm.DB, cfg fifo.StockableConfig, id uint, qty float64) error {
if qty == 0 {
return nil
}
column := cfg.Columns.TotalUsedQuantity
query := tx.Table(cfg.Table).
Where(fmt.Sprintf("%s = ?", cfg.Columns.ID), id)
if cfg.Scope != nil {
query = cfg.Scope(query)
}
return query.Update(column, gorm.Expr(fmt.Sprintf("COALESCE(%s,0) + ?", column), qty)).Error
}
type allocationOutcome struct {
allocated float64
pending float64
allocations []AllocationDetail
}
type stockLot struct {
StockableKey fifo.StockableKey
RecordID uint
AvailableQty float64
CreatedAt time.Time
}
func (s *fifoService) allocateFromStock(
ctx context.Context,
tx *gorm.DB,
productWarehouseID uint,
usableKey fifo.UsableKey,
usableID uint,
requestQty float64,
) (*allocationOutcome, error) {
lots, err := s.fetchStockLots(ctx, tx, productWarehouseID)
if err != nil {
return nil, err
}
if len(lots) == 0 {
return &allocationOutcome{pending: requestQty}, nil
}
var (
remaining = requestQty
applied float64
allocations []*entities.StockAllocation
allocationSummaries []AllocationDetail
usageAdjustments = make(map[fifo.StockableKey]map[uint]float64)
)
for _, lot := range lots {
if remaining <= 0 {
break
}
if lot.AvailableQty <= 0 {
continue
}
portion := lot.AvailableQty
if portion > remaining {
portion = remaining
}
applied += portion
remaining -= portion
allocationSummaries = append(allocationSummaries, AllocationDetail{
StockableKey: lot.StockableKey,
StockableID: lot.RecordID,
Quantity: portion,
})
allocations = append(allocations, &entities.StockAllocation{
ProductWarehouseId: productWarehouseID,
StockableType: lot.StockableKey.String(),
StockableId: lot.RecordID,
UsableType: usableKey.String(),
UsableId: usableID,
Qty: portion,
Status: entities.StockAllocationStatusActive,
})
if _, ok := usageAdjustments[lot.StockableKey]; !ok {
usageAdjustments[lot.StockableKey] = make(map[uint]float64)
}
usageAdjustments[lot.StockableKey][lot.RecordID] += portion
}
if len(allocations) > 0 {
if err := s.allocations.CreateMany(ctx, allocations, func(db *gorm.DB) *gorm.DB {
return s.txOrDB(tx, db)
}); err != nil {
return nil, err
}
for key, deltas := range usageAdjustments {
cfg, ok := fifo.Stockable(key)
if !ok {
continue
}
for id, qty := range deltas {
if err := s.incrementStockableUsage(ctx, tx, cfg, id, qty); err != nil {
return nil, err
}
}
}
}
return &allocationOutcome{
allocated: applied,
pending: remaining,
allocations: allocationSummaries,
}, nil
}
func (s *fifoService) fetchStockLots(ctx context.Context, tx *gorm.DB, productWarehouseID uint) ([]stockLot, error) {
configs := fifo.Stockables()
if len(configs) == 0 {
return nil, nil
}
var lots []stockLot
for key, cfg := range configs {
selectStmt := fmt.Sprintf(
"%s AS id, %s AS available_qty, %s AS created_at",
cfg.Columns.ID,
fmt.Sprintf("%s - COALESCE(%s,0)", cfg.Columns.TotalQuantity, cfg.Columns.TotalUsedQuantity),
cfg.Columns.CreatedAt,
)
var rows []struct {
ID uint
AvailableQty float64
CreatedAt time.Time
}
query := tx.Table(cfg.Table).
Select(selectStmt).
Where(fmt.Sprintf("%s = ?", cfg.Columns.ProductWarehouseID), productWarehouseID).
Where(fmt.Sprintf("%s > %s", cfg.Columns.TotalQuantity, cfg.Columns.TotalUsedQuantity))
if cfg.Scope != nil {
query = cfg.Scope(query)
}
for _, order := range s.orderClauses(cfg.OrderBy) {
query = query.Order(order)
}
query = query.Limit(s.maxLotsPerStockable)
if err := query.Find(&rows).Error; err != nil {
return nil, err
}
for _, row := range rows {
if row.AvailableQty <= 0 {
continue
}
lots = append(lots, stockLot{
StockableKey: key,
RecordID: row.ID,
AvailableQty: row.AvailableQty,
CreatedAt: row.CreatedAt,
})
}
}
if len(lots) == 0 {
return nil, nil
}
sort.SliceStable(lots, func(i, j int) bool {
if lots[i].CreatedAt.Equal(lots[j].CreatedAt) {
return lots[i].RecordID < lots[j].RecordID
}
return lots[i].CreatedAt.Before(lots[j].CreatedAt)
})
return lots, nil
}
func (s *fifoService) applyUsableDeltas(ctx context.Context, tx *gorm.DB, cfg fifo.UsableConfig, id uint, usageDelta, pendingDelta float64) error {
if usageDelta == 0 && pendingDelta == 0 {
return nil
}
updates := map[string]any{}
if usageDelta != 0 {
updates[cfg.Columns.UsageQuantity] = gorm.Expr(fmt.Sprintf("COALESCE(%s,0) + ?", cfg.Columns.UsageQuantity), usageDelta)
}
if pendingDelta != 0 {
updates[cfg.Columns.PendingQuantity] = gorm.Expr(fmt.Sprintf("COALESCE(%s,0) + ?", cfg.Columns.PendingQuantity), pendingDelta)
}
query := tx.Table(cfg.Table).Where(fmt.Sprintf("%s = ?", cfg.Columns.ID), id)
if cfg.Scope != nil {
query = cfg.Scope(query)
}
return query.Updates(updates).Error
}
type pendingCandidate struct {
UsableKey fifo.UsableKey
Config fifo.UsableConfig
UsableID uint
Pending float64
CreatedAt time.Time
}
func (s *fifoService) resolvePendingForWarehouse(ctx context.Context, tx *gorm.DB, productWarehouseID uint) ([]PendingResolution, error) {
candidates, err := s.fetchPendingCandidates(ctx, tx, productWarehouseID)
if err != nil {
return nil, err
}
if len(candidates) == 0 {
return nil, nil
}
var resolutions []PendingResolution
for _, candidate := range candidates {
if candidate.Pending <= 0 {
continue
}
outcome, err := s.allocateFromStock(ctx, tx, productWarehouseID, candidate.UsableKey, candidate.UsableID, candidate.Pending)
if err != nil {
return nil, err
}
if outcome.allocated <= 0 {
break
}
if err := s.applyUsableDeltas(ctx, tx, candidate.Config, candidate.UsableID, outcome.allocated, -outcome.allocated); err != nil {
return nil, err
}
if err := s.productWarehouseRepo.AdjustQuantities(ctx, map[uint]float64{
productWarehouseID: -outcome.allocated,
}, func(db *gorm.DB) *gorm.DB {
return s.txOrDB(tx, db)
}); err != nil {
return nil, err
}
resolutions = append(resolutions, PendingResolution{
UsableKey: candidate.UsableKey,
UsableID: candidate.UsableID,
Quantity: outcome.allocated,
})
if outcome.pending > 0 {
// No more stock available for this warehouse at the moment.
break
}
}
return resolutions, nil
}
func (s *fifoService) releaseUsagePortion(
ctx context.Context,
tx *gorm.DB,
usableKey fifo.UsableKey,
usableID uint,
target float64,
) (float64, error) {
if target <= 0 {
return 0, nil
}
allocations, err := s.allocations.FindActiveByUsable(ctx, usableKey.String(), usableID, func(db *gorm.DB) *gorm.DB {
target := s.txOrDB(tx, db)
return target.Clauses(clause.Locking{Strength: "UPDATE"})
})
if err != nil {
return 0, err
}
if len(allocations) == 0 {
return 0, nil
}
var (
remaining = target
totalReleased float64
warehouseAdjustments = make(map[uint]float64)
stockableAdjustments = make(map[fifo.StockableKey]map[uint]float64)
)
now := time.Now()
for i := len(allocations) - 1; i >= 0 && remaining > 0; i-- {
allocation := allocations[i]
releaseAmt := allocation.Qty
if releaseAmt > remaining {
releaseAmt = remaining
}
remaining -= releaseAmt
totalReleased += releaseAmt
warehouseAdjustments[allocation.ProductWarehouseId] += releaseAmt
key := fifo.StockableKey(allocation.StockableType)
if _, ok := stockableAdjustments[key]; !ok {
stockableAdjustments[key] = make(map[uint]float64)
}
stockableAdjustments[key][allocation.StockableId] += releaseAmt
if releaseAmt == allocation.Qty {
if err := s.allocations.PatchOne(ctx, allocation.Id, map[string]any{
"status": entities.StockAllocationStatusReleased,
"released_at": now,
}, func(db *gorm.DB) *gorm.DB {
return s.txOrDB(tx, db)
}); err != nil {
return 0, err
}
} else {
if err := s.allocations.PatchOne(ctx, allocation.Id, map[string]any{
"quantity": allocation.Qty - releaseAmt,
}, func(db *gorm.DB) *gorm.DB {
return s.txOrDB(tx, db)
}); err != nil {
return 0, err
}
}
}
if totalReleased == 0 {
return 0, nil
}
for key, deltas := range stockableAdjustments {
cfg, ok := fifo.Stockable(key)
if !ok {
continue
}
for id, qty := range deltas {
if err := s.incrementStockableUsage(ctx, tx, cfg, id, -qty); err != nil {
return 0, err
}
}
}
if len(warehouseAdjustments) > 0 {
if err := s.productWarehouseRepo.AdjustQuantities(ctx, warehouseAdjustments, func(db *gorm.DB) *gorm.DB {
return s.txOrDB(tx, db)
}); err != nil {
return 0, err
}
for warehouseID := range warehouseAdjustments {
if _, err := s.resolvePendingForWarehouse(ctx, tx, warehouseID); err != nil {
return 0, err
}
}
}
return totalReleased, nil
}
func (s *fifoService) fetchPendingCandidates(ctx context.Context, tx *gorm.DB, productWarehouseID uint) ([]pendingCandidate, error) {
configs := fifo.Usables()
if len(configs) == 0 {
return nil, nil
}
var candidates []pendingCandidate
for key, cfg := range configs {
selectStmt := fmt.Sprintf(
"%s AS id, %s AS pending_qty, %s AS created_at",
cfg.Columns.ID,
cfg.Columns.PendingQuantity,
cfg.Columns.CreatedAt,
)
var rows []struct {
ID uint
Pending float64
CreatedAt time.Time
}
query := tx.Table(cfg.Table).
Select(selectStmt).
Where(fmt.Sprintf("%s = ?", cfg.Columns.ProductWarehouseID), productWarehouseID).
Where(fmt.Sprintf("%s > 0", cfg.Columns.PendingQuantity)).
Limit(s.pendingBatchPerUsable)
if cfg.Scope != nil {
query = cfg.Scope(query)
}
for _, order := range s.orderClauses(cfg.OrderBy) {
query = query.Order(order)
}
if err := query.Find(&rows).Error; err != nil {
return nil, err
}
for _, row := range rows {
if row.Pending <= 0 {
continue
}
candidates = append(candidates, pendingCandidate{
UsableKey: key,
Config: cfg,
UsableID: row.ID,
Pending: row.Pending,
CreatedAt: row.CreatedAt,
})
}
}
if len(candidates) == 0 {
return nil, nil
}
sort.SliceStable(candidates, func(i, j int) bool {
if candidates[i].CreatedAt.Equal(candidates[j].CreatedAt) {
return candidates[i].UsableID < candidates[j].UsableID
}
return candidates[i].CreatedAt.Before(candidates[j].CreatedAt)
})
return candidates, nil
}
func (s *fifoService) orderClauses(custom []string) []string {
if len(custom) > 0 {
return custom
}
return s.defaultOrderBy
}
+40 -8
View File
@@ -3,6 +3,8 @@ package validation
import ( import (
"errors" "errors"
"fmt" "fmt"
"reflect"
"strings"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
) )
@@ -21,34 +23,41 @@ var customMessages = map[string]string{
"alphanum": "Field %s must contain only alphanumeric characters", "alphanum": "Field %s must contain only alphanumeric characters",
"oneof": "Invalid value for field %s", "oneof": "Invalid value for field %s",
"password": "Field %s must be at least 8 characters, contain uppercase, lowercase, number, and special character", "password": "Field %s must be at least 8 characters, contain uppercase, lowercase, number, and special character",
"gt": "Invalid %s, must be greater than %s",
} }
func CustomErrorMessages(err error) map[string]string { func CustomErrorMessages(err error) (string, map[string]string) {
var validationErrors validator.ValidationErrors var validationErrors validator.ValidationErrors
if errors.As(err, &validationErrors) { if errors.As(err, &validationErrors) {
return generateErrorMessages(validationErrors) return generateErrorMessages(validationErrors)
} }
return nil return "", nil
} }
func generateErrorMessages(validationErrors validator.ValidationErrors) map[string]string { func generateErrorMessages(validationErrors validator.ValidationErrors) (string, map[string]string) {
errorsMap := make(map[string]string) errorsMap := make(map[string]string)
for _, err := range validationErrors { var firstMessage string
for i, err := range validationErrors {
fieldName := err.StructNamespace() fieldName := err.StructNamespace()
tag := err.Tag() tag := err.Tag()
customMessage := customMessages[tag] customMessage := customMessages[tag]
var msg string
if customMessage != "" { if customMessage != "" {
errorsMap[fieldName] = formatErrorMessage(customMessage, err, tag) msg = formatErrorMessage(customMessage, err, tag)
} else { } else {
errorsMap[fieldName] = defaultErrorMessage(err) msg = defaultErrorMessage(err)
}
errorsMap[fieldName] = msg
if i == 0 {
firstMessage = msg
} }
} }
return errorsMap return firstMessage, errorsMap
} }
func formatErrorMessage(customMessage string, err validator.FieldError, tag string) string { func formatErrorMessage(customMessage string, err validator.FieldError, tag string) string {
if tag == "min" || tag == "max" || tag == "len" { if tag == "min" || tag == "max" || tag == "len" || tag == "gt" {
return fmt.Sprintf(customMessage, err.Field(), err.Param()) return fmt.Sprintf(customMessage, err.Field(), err.Param())
} }
return fmt.Sprintf(customMessage, err.Field()) return fmt.Sprintf(customMessage, err.Field())
@@ -61,6 +70,16 @@ func defaultErrorMessage(err validator.FieldError) string {
func Validator() *validator.Validate { func Validator() *validator.Validate {
validate := validator.New() validate := validator.New()
validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
if jsonTag := getTagName(fld, "json"); jsonTag != "" {
return jsonTag
}
if queryTag := getTagName(fld, "query"); queryTag != "" {
return queryTag
}
return fld.Name
})
if err := validate.RegisterValidation("password", Password); err != nil { if err := validate.RegisterValidation("password", Password); err != nil {
return nil return nil
} }
@@ -72,3 +91,16 @@ func Validator() *validator.Validate {
} }
return validate return validate
} }
func getTagName(fld reflect.StructField, tag string) string {
value, ok := fld.Tag.Lookup(tag)
if !ok || value == "-" {
return ""
}
name := strings.Split(value, ",")[0]
if name == "" || name == "-" {
return ""
}
return name
}
BIN
View File
Binary file not shown.
+164 -22
View File
@@ -2,36 +2,69 @@ package config
import ( import (
"encoding/json" "encoding/json"
"fmt"
"strings" "strings"
"time"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/spf13/viper" "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 ( var (
IsProd bool IsProd bool
AppHost string AppHost string
Version string Version string
LogLevel string LogLevel string
AppPort int AppPort int
DBHost string DBHost string
DBUser string DBUser string
DBPassword string DBPassword string
DBName string DBName string
DBPort int DBPort int
JWTSecret string DBSSLMode string
JWTAccessExp int DBSSLRootCert string
JWTRefreshExp int DBSSLCert string
JWTResetPasswordExp int DBSSLKey string
JWTVerifyEmailExp int JWTSecret string
RedisURL string JWTAccessExp int
CORSAllowOrigins []string JWTRefreshExp int
CORSAllowMethods []string JWTResetPasswordExp int
CORSAllowHeaders []string JWTVerifyEmailExp int
CORSExposeHeaders []string RedisURL string
CORSAllowCredentials bool CORSAllowOrigins []string
CORSMaxAge int 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() { func init() {
@@ -50,6 +83,10 @@ func init() {
DBPassword = viper.GetString("DB_PASSWORD") DBPassword = viper.GetString("DB_PASSWORD")
DBName = viper.GetString("DB_NAME") DBName = viper.GetString("DB_NAME")
DBPort = viper.GetInt("DB_PORT") 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 // jwt configuration
JWTSecret = viper.GetString("JWT_SECRET") JWTSecret = viper.GetString("JWT_SECRET")
@@ -68,6 +105,44 @@ func init() {
// Redis // Redis
RedisURL = viper.GetString("REDIS_URL") 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() { func loadConfig() {
@@ -117,3 +192,70 @@ func parseListWithDefault(key, def string) []string {
} }
return parts 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 -4
View File
@@ -2,6 +2,7 @@ package database
import ( import (
"fmt" "fmt"
"strings"
"time" "time"
"gitlab.com/mbugroup/lti-api.git/internal/config" "gitlab.com/mbugroup/lti-api.git/internal/config"
@@ -13,10 +14,25 @@ import (
) )
func Connect(dbHost, dbName string) *gorm.DB { func Connect(dbHost, dbName string) *gorm.DB {
dsn := fmt.Sprintf( parts := []string{
"host=%s user=%s password=%s dbname=%s port=%d sslmode=disable TimeZone=Asia/Shanghai", fmt.Sprintf("host=%s", dbHost),
dbHost, config.DBUser, config.DBPassword, dbName, config.DBPort, 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{ db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info), Logger: logger.Default.LogMode(logger.Info),
@@ -2,10 +2,10 @@
CREATE TABLE users ( CREATE TABLE users (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
id_user BIGINT NOT NULL, id_user BIGINT NOT NULL,
name VARCHAR NOT NULL, name VARCHAR(50) NOT NULL,
email VARCHAR NOT NULL, email VARCHAR(50) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW (),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW (),
deleted_at TIMESTAMPTZ deleted_at TIMESTAMPTZ
); );
@@ -20,28 +20,24 @@ WHERE
-- FLAGS -- FLAGS
CREATE TABLE flags ( CREATE TABLE flags (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL, name VARCHAR(50) NOT NULL,
flagable_id BIGINT NOT NULL, flagable_id BIGINT NOT NULL,
flagable_type VARCHAR(50) NOT NULL, flagable_type VARCHAR(50) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW (),
updated_at TIMESTAMPTZ DEFAULT NOW() updated_at TIMESTAMPTZ DEFAULT NOW ()
); );
CREATE UNIQUE INDEX flags_unique_flagable ON flags ( CREATE UNIQUE INDEX flags_unique_flagable ON flags (name, flagable_id, flagable_type);
name,
flagable_id,
flagable_type
);
CREATE INDEX flags_flagable_lookup ON flags (flagable_type, flagable_id); CREATE INDEX flags_flagable_lookup ON flags (flagable_type, flagable_id);
-- PRODUCT CATEGORIES -- PRODUCT CATEGORIES
CREATE TABLE product_categories ( CREATE TABLE product_categories (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL, name VARCHAR(50) NOT NULL,
code VARCHAR(10) NOT NULL, code VARCHAR(10) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW (),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW (),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
@@ -57,9 +53,9 @@ WHERE
-- UOM -- UOM
CREATE TABLE uoms ( CREATE TABLE uoms (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL, name VARCHAR(50) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW (),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW (),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
@@ -71,12 +67,12 @@ WHERE
-- BANKS -- BANKS
CREATE TABLE banks ( CREATE TABLE banks (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL, name VARCHAR(50) NOT NULL,
alias VARCHAR(5) NOT NULL, alias VARCHAR(5) NOT NULL,
owner VARCHAR, owner VARCHAR(50),
account_number VARCHAR(50) NOT NULL, account_number VARCHAR(50) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW (),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW (),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
@@ -88,9 +84,9 @@ WHERE
-- AREAS -- AREAS
CREATE TABLE areas ( CREATE TABLE areas (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL, name VARCHAR(50) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW (),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW (),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
@@ -102,11 +98,11 @@ WHERE
-- LOCATIONS -- LOCATIONS
CREATE TABLE locations ( CREATE TABLE locations (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL, name VARCHAR(50) NOT NULL,
address TEXT NOT NULL, address TEXT NOT NULL,
area_id BIGINT NOT NULL REFERENCES areas (id) ON DELETE RESTRICT ON UPDATE CASCADE, area_id BIGINT NOT NULL REFERENCES areas (id) ON DELETE RESTRICT ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW (),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW (),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
@@ -118,11 +114,11 @@ WHERE
-- KANDANG -- KANDANG
CREATE TABLE kandangs ( CREATE TABLE kandangs (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL, name VARCHAR(50) NOT NULL,
location_id BIGINT NOT NULL REFERENCES locations (id) ON DELETE RESTRICT ON UPDATE CASCADE, 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, pic_id BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW (),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW (),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
@@ -134,13 +130,13 @@ WHERE
-- WAREHOUSES -- WAREHOUSES
CREATE TABLE warehouses ( CREATE TABLE warehouses (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL, name VARCHAR(50) NOT NULL,
type VARCHAR(50) NOT NULL, type VARCHAR(50) NOT NULL,
area_id BIGINT NOT NULL REFERENCES areas (id) ON DELETE RESTRICT ON UPDATE CASCADE, 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, 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, kandang_id BIGINT REFERENCES kandangs (id) ON DELETE SET NULL ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW (),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW (),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
@@ -152,16 +148,16 @@ WHERE
-- CUSTOMERS -- CUSTOMERS
CREATE TABLE customers ( CREATE TABLE customers (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL, name VARCHAR(50) NOT NULL,
pic_id BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE, pic_id BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
type VARCHAR(50) NOT NULL, type VARCHAR(50) NOT NULL,
address TEXT NOT NULL, address TEXT NOT NULL,
phone VARCHAR(20) NOT NULL, phone VARCHAR(20) NOT NULL,
email VARCHAR NOT NULL, email VARCHAR(50) NOT NULL,
account_number VARCHAR(50) NOT NULL, account_number VARCHAR(50) NOT NULL,
balance NUMERIC(15, 3) DEFAULT 0, balance NUMERIC(15, 3) DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW (),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW (),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
@@ -173,10 +169,10 @@ WHERE
-- NONSTOCK -- NONSTOCK
CREATE TABLE nonstocks ( CREATE TABLE nonstocks (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL, name VARCHAR(50) NOT NULL,
uom_id BIGINT NOT NULL REFERENCES uoms (id) ON DELETE RESTRICT ON UPDATE CASCADE, uom_id BIGINT NOT NULL REFERENCES uoms (id) ON DELETE RESTRICT ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW (),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW (),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
@@ -188,9 +184,9 @@ WHERE
-- FCR -- FCR
CREATE TABLE fcrs ( CREATE TABLE fcrs (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL, name VARCHAR(50) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW (),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW (),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
@@ -205,29 +201,29 @@ CREATE TABLE fcr_standards (
weight NUMERIC(15, 3) NOT NULL, weight NUMERIC(15, 3) NOT NULL,
fcr_number NUMERIC(15, 3) NOT NULL, fcr_number NUMERIC(15, 3) NOT NULL,
mortality NUMERIC(15, 3) NOT NULL, mortality NUMERIC(15, 3) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW (),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW (),
deleted_at TIMESTAMPTZ deleted_at TIMESTAMPTZ
); );
-- SUPPLIERS -- SUPPLIERS
CREATE TABLE suppliers ( CREATE TABLE suppliers (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL, name VARCHAR(50) NOT NULL,
alias VARCHAR(5) NOT NULL, alias VARCHAR(5) NOT NULL,
pic VARCHAR NOT NULL, pic VARCHAR(50) NOT NULL,
type VARCHAR(50) NOT NULL, type VARCHAR(50) NOT NULL,
category VARCHAR(20) NOT NULL, category VARCHAR(20) NOT NULL,
hatchery VARCHAR, hatchery VARCHAR(50),
phone VARCHAR(20) NOT NULL, phone VARCHAR(20) NOT NULL,
email VARCHAR NOT NULL, email VARCHAR(50) NOT NULL,
address TEXT NOT NULL, address TEXT NOT NULL,
npwp VARCHAR(50), npwp VARCHAR(50),
account_number VARCHAR(50), account_number VARCHAR(50),
balance NUMERIC(15, 3) DEFAULT 0, balance NUMERIC(15, 3) DEFAULT 0,
due_date INT NOT NULL, due_date INT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW (),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW (),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
@@ -239,15 +235,15 @@ WHERE
CREATE TABLE nonstock_suppliers ( CREATE TABLE nonstock_suppliers (
nonstock_id BIGINT NOT NULL REFERENCES nonstocks (id) ON DELETE CASCADE ON UPDATE CASCADE, 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, supplier_id BIGINT NOT NULL REFERENCES suppliers (id) ON DELETE CASCADE ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW (),
PRIMARY KEY (nonstock_id, supplier_id) PRIMARY KEY (nonstock_id, supplier_id)
); );
-- PRODUCTS -- PRODUCTS
CREATE TABLE products ( CREATE TABLE products (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR NOT NULL, name VARCHAR(50) NOT NULL,
brand VARCHAR NOT NULL, brand VARCHAR(50) NOT NULL,
sku VARCHAR(100), sku VARCHAR(100),
uom_id BIGINT NOT NULL REFERENCES uoms (id) ON DELETE RESTRICT ON UPDATE CASCADE, 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_category_id BIGINT NOT NULL REFERENCES product_categories (id) ON DELETE RESTRICT ON UPDATE CASCADE,
@@ -255,8 +251,8 @@ CREATE TABLE products (
selling_price NUMERIC(15, 3), selling_price NUMERIC(15, 3),
tax NUMERIC(15, 3), tax NUMERIC(15, 3),
expiry_period INT, expiry_period INT,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW (),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW (),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
@@ -272,15 +268,15 @@ WHERE
CREATE TABLE product_suppliers ( CREATE TABLE product_suppliers (
product_id BIGINT NOT NULL REFERENCES products (id) ON DELETE CASCADE ON UPDATE CASCADE, 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, supplier_id BIGINT NOT NULL REFERENCES suppliers (id) ON DELETE CASCADE ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW (),
PRIMARY KEY (product_id, supplier_id) PRIMARY KEY (product_id, supplier_id)
); );
-- PROJECTS -- PROJECTS
CREATE TABLE projects ( CREATE TABLE projects (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW (),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW (),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
@@ -292,8 +288,8 @@ CREATE TABLE product_warehouses (
warehouse_id BIGINT NOT NULL REFERENCES warehouses (id), warehouse_id BIGINT NOT NULL REFERENCES warehouses (id),
quantity INTEGER NOT NULL DEFAULT 0, quantity INTEGER NOT NULL DEFAULT 0,
created_by BIGINT NOT NULL REFERENCES users (id), created_by BIGINT NOT NULL REFERENCES users (id),
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW (),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW (),
deleted_at TIMESTAMPTZ deleted_at TIMESTAMPTZ
); );
@@ -320,8 +316,8 @@ CREATE TABLE stock_logs (
note TEXT, note TEXT,
product_warehouse_id BIGINT NOT NULL REFERENCES product_warehouses (id) ON DELETE CASCADE ON UPDATE CASCADE, 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_by BIGINT NOT NULL REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW (),
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW (),
deleted_at TIMESTAMPTZ deleted_at TIMESTAMPTZ
); );
@@ -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 @@
DROP TABLE IF EXISTS purchase_items;
@@ -0,0 +1,54 @@
CREATE TABLE IF NOT EXISTS purchase_items (
id BIGSERIAL PRIMARY KEY,
purchase_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
warehouse_id BIGINT NOT NULL,
product_warehouse_id BIGINT,
received_date TIMESTAMPTZ,
travel_number VARCHAR,
travel_number_docs VARCHAR,
vehicle_number VARCHAR,
sub_qty NUMERIC(15, 3) NOT NULL,
total_qty NUMERIC(15, 3) NOT NULL DEFAULT 0,
total_used NUMERIC(15, 3) NOT NULL DEFAULT 0,
price NUMERIC(15, 3) NOT NULL DEFAULT 0,
total_price NUMERIC(15, 3) NOT NULL DEFAULT 0,
CONSTRAINT uq_purchase_items_purchase_product_warehouse
UNIQUE (purchase_id, product_id, warehouse_id)
);
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'products') THEN
EXECUTE
'ALTER TABLE purchase_items
ADD CONSTRAINT fk_purchase_items_product
FOREIGN KEY (product_id)
REFERENCES products(id)
ON DELETE RESTRICT ON UPDATE CASCADE';
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'warehouses') THEN
EXECUTE
'ALTER TABLE purchase_items
ADD CONSTRAINT fk_purchase_items_warehouse
FOREIGN KEY (warehouse_id)
REFERENCES warehouses(id)
ON DELETE RESTRICT ON UPDATE CASCADE';
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
EXECUTE
'ALTER TABLE purchase_items
ADD CONSTRAINT fk_purchase_items_product_warehouse
FOREIGN KEY (product_warehouse_id)
REFERENCES product_warehouses(id)
ON DELETE SET NULL ON UPDATE CASCADE';
END IF;
END $$;
CREATE INDEX IF NOT EXISTS idx_purchase_items_product_id ON purchase_items (product_id);
CREATE INDEX IF NOT EXISTS idx_purchase_items_warehouse_id ON purchase_items (warehouse_id);
CREATE INDEX IF NOT EXISTS idx_purchase_items_product_warehouse_id ON purchase_items (product_warehouse_id);
CREATE INDEX IF NOT EXISTS idx_purchase_items_purchase_id ON purchase_items (purchase_id);
@@ -0,0 +1,14 @@
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'fk_purchase_items_purchase'
AND conrelid = 'purchase_items'::regclass
) THEN
ALTER TABLE purchase_items
DROP CONSTRAINT fk_purchase_items_purchase;
END IF;
END $$;
DROP TABLE IF EXISTS purchases;
@@ -0,0 +1,58 @@
CREATE TABLE IF NOT EXISTS purchases (
id BIGSERIAL PRIMARY KEY,
pr_number VARCHAR NOT NULL,
po_number VARCHAR NULL,
po_date TIMESTAMPTZ NULL,
supplier_id BIGINT NOT NULL,
credit_term INT NOT NULL,
due_date TIMESTAMPTZ,
grand_total NUMERIC(15, 3) NOT NULL,
notes TEXT,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL,
deleted_at TIMESTAMPTZ,
created_by BIGINT NOT NULL,
CONSTRAINT uq_purchases_pr_number UNIQUE (pr_number),
CONSTRAINT uq_purchases_po_number UNIQUE (po_number)
);
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'suppliers') THEN
EXECUTE
'ALTER TABLE purchases
ADD CONSTRAINT fk_purchases_supplier
FOREIGN KEY (supplier_id)
REFERENCES suppliers(id)
ON DELETE RESTRICT ON UPDATE CASCADE';
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
EXECUTE
'ALTER TABLE purchases
ADD CONSTRAINT fk_purchases_created_by
FOREIGN KEY (created_by)
REFERENCES users(id)
ON DELETE RESTRICT ON UPDATE CASCADE';
END IF;
IF EXISTS (
SELECT 1 FROM pg_tables WHERE tablename = 'purchase_items'
) AND NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'fk_purchase_items_purchase'
) THEN
EXECUTE
'ALTER TABLE purchase_items
ADD CONSTRAINT fk_purchase_items_purchase
FOREIGN KEY (purchase_id)
REFERENCES purchases(id)
ON DELETE CASCADE ON UPDATE CASCADE';
END IF;
END $$;
CREATE INDEX IF NOT EXISTS idx_purchases_supplier_id ON purchases (supplier_id);
CREATE INDEX IF NOT EXISTS idx_purchases_created_by ON purchases (created_by);
CREATE INDEX IF NOT EXISTS idx_purchases_po_date ON purchases (po_date);
CREATE INDEX IF NOT EXISTS idx_purchases_deleted_at ON purchases (deleted_at);
@@ -0,0 +1,5 @@
DROP TABLE IF EXISTS marketing_delivery_products CASCADE;
DROP TABLE IF EXISTS marketing_products CASCADE;
DROP TABLE IF EXISTS marketings CASCADE;
@@ -0,0 +1,44 @@
CREATE TABLE marketings (
id BIGSERIAL PRIMARY KEY,
so_number VARCHAR(255) UNIQUE NOT NULL,
customer_id BIGINT NOT NULL,
so_docs VARCHAR(20),
so_date DATE NOT NULL,
sales_person_id BIGINT NOT NULL,
notes TEXT,
created_by BIGINT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'customers') THEN
ALTER TABLE marketings
ADD CONSTRAINT fk_marketings_customer_id
FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE RESTRICT;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
ALTER TABLE marketings
ADD CONSTRAINT fk_marketings_sales_person_id
FOREIGN KEY (sales_person_id) REFERENCES users(id) ON DELETE RESTRICT;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
ALTER TABLE marketings
ADD CONSTRAINT fk_marketings_created_by
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE RESTRICT;
END IF;
END $$;
CREATE INDEX idx_marketings_customer_id ON marketings (customer_id);
CREATE INDEX idx_marketings_sales_person_id ON marketings (sales_person_id);
CREATE INDEX idx_marketings_created_by ON marketings (created_by);
CREATE INDEX idx_marketings_so_date ON marketings (so_date);
CREATE INDEX idx_marketings_deleted_at ON marketings (deleted_at);
@@ -0,0 +1 @@
DROP TABLE IF EXISTS marketing_products CASCADE;
@@ -0,0 +1,34 @@
CREATE TABLE marketing_products (
id BIGSERIAL PRIMARY KEY,
marketing_id BIGINT NOT NULL,
product_warehouse_id BIGINT NOT NULL,
qty NUMERIC(15, 3) NOT NULL,
unit_price NUMERIC(15, 3) NOT NULL,
avg_weight NUMERIC(15, 3) NOT NULL,
total_weight NUMERIC(15, 3) NOT NULL,
total_price NUMERIC(15, 3) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'marketings') THEN
ALTER TABLE marketing_products
ADD CONSTRAINT fk_marketing_products_marketing_id
FOREIGN KEY (marketing_id) REFERENCES marketings(id) ON DELETE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
ALTER TABLE marketing_products
ADD CONSTRAINT fk_marketing_products_product_warehouse_id
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id) ON DELETE RESTRICT;
END IF;
END $$;
CREATE INDEX idx_marketing_products_marketing_id ON marketing_products (marketing_id);
CREATE INDEX idx_marketing_products_product_warehouse_id ON marketing_products (product_warehouse_id);
CREATE INDEX idx_marketing_products_deleted_at ON marketing_products (deleted_at);
@@ -0,0 +1,2 @@
DROP TABLE IF EXISTS marketing_delivery_products CASCADE;
@@ -0,0 +1,29 @@
CREATE TABLE marketing_delivery_products (
id BIGSERIAL PRIMARY KEY,
marketing_product_id BIGINT UNIQUE NOT NULL,
qty NUMERIC(15, 3) NOT NULL,
unit_price NUMERIC(15, 3) NOT NULL,
total_weight NUMERIC(15, 3) NOT NULL,
avg_weight NUMERIC(15, 3) NOT NULL,
total_price NUMERIC(15, 3) NOT NULL,
delivery_date DATE,
vehicle_number VARCHAR(50),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'marketing_products') THEN
ALTER TABLE marketing_delivery_products
ADD CONSTRAINT fk_marketing_delivery_products_marketing_product_id
FOREIGN KEY (marketing_product_id) REFERENCES marketing_products(id) ON DELETE CASCADE;
END IF;
END $$;
CREATE INDEX idx_marketing_delivery_products_marketing_product_id ON marketing_delivery_products (marketing_product_id);
CREATE INDEX idx_marketing_delivery_products_delivery_date ON marketing_delivery_products (delivery_date);
CREATE INDEX idx_marketing_delivery_products_deleted_at ON marketing_delivery_products (deleted_at);
@@ -0,0 +1,7 @@
DROP INDEX IF EXISTS stock_allocations_released_at_idx;
DROP INDEX IF EXISTS stock_allocations_status_idx;
DROP INDEX IF EXISTS stock_allocations_usage_lookup;
DROP INDEX IF EXISTS stock_allocations_lookup;
DROP INDEX IF EXISTS stock_allocations_product_warehouse_id_idx;
DROP TABLE IF EXISTS stock_allocations;
@@ -0,0 +1,30 @@
CREATE TABLE IF NOT EXISTS stock_allocations (
id BIGSERIAL PRIMARY KEY,
product_warehouse_id BIGINT NOT NULL REFERENCES product_warehouses(id),
stockable_type VARCHAR(100) NOT NULL,
stockable_id BIGINT NOT NULL,
usable_type VARCHAR(100) NOT NULL,
usable_id BIGINT NOT NULL,
qty NUMERIC(15,3) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
note TEXT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
released_at TIMESTAMPTZ NULL,
deleted_at TIMESTAMPTZ NULL
);
CREATE INDEX IF NOT EXISTS stock_allocations_product_warehouse_id_idx
ON stock_allocations (product_warehouse_id);
CREATE INDEX IF NOT EXISTS stock_allocations_lookup
ON stock_allocations (stockable_type, stockable_id);
CREATE INDEX IF NOT EXISTS stock_allocations_usage_lookup
ON stock_allocations (usable_type, usable_id);
CREATE INDEX IF NOT EXISTS stock_allocations_status_idx
ON stock_allocations (status);
CREATE INDEX IF NOT EXISTS stock_allocations_released_at_idx
ON stock_allocations (released_at);
@@ -0,0 +1,2 @@
ALTER TABLE kandangs
DROP COLUMN IF EXISTS capacity;
@@ -0,0 +1,2 @@
ALTER TABLE kandangs
ADD COLUMN capacity NUMERIC(15,3) NOT NULL;
@@ -0,0 +1 @@
DROP TABLE IF EXISTS expenses;
@@ -0,0 +1,50 @@
CREATE TABLE expenses (
id BIGSERIAL PRIMARY KEY,
reference_number VARCHAR(50) UNIQUE NOT NULL,
supplier_id BIGINT NOT NULL,
category VARCHAR(50) NOT NULL CHECK (
category IN ('BOP', 'NON-BOP')
),
po_number VARCHAR(50) NULL,
document_path JSON,
realization_document_path JSON,
expense_date DATE NOT NULL,
realization_date DATE,
grand_total NUMERIC(15, 3) DEFAULT 0,
note TEXT,
created_by BIGINT,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
deleted_at TIMESTAMPTZ
);
CREATE SEQUENCE expenses_ref_seq INCREMENT BY 1 START WITH 1;
-- Tambahkan Foreign Key ke suppliers
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'suppliers') THEN
ALTER TABLE expenses
ADD CONSTRAINT fk_expenses_supplier_id
FOREIGN KEY (supplier_id) REFERENCES suppliers(id);
END IF;
END $$;
-- Tambahkan Foreign Key ke users (created_by)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
ALTER TABLE expenses
ADD CONSTRAINT fk_expenses_created_by
FOREIGN KEY (created_by) REFERENCES users(id);
END IF;
END $$;
-- Index
CREATE INDEX idx_expenses_supplier_id ON expenses (supplier_id);
CREATE INDEX idx_expenses_expense_date ON expenses (expense_date);
CREATE INDEX idx_expenses_deleted_at ON expenses (deleted_at);
@@ -0,0 +1,3 @@
DROP TABLE IF EXISTS expense_nonstocks;
DROP SEQUENCE expenses_ref_seq;
@@ -0,0 +1,56 @@
CREATE TABLE expense_nonstocks (
id BIGSERIAL PRIMARY KEY,
expense_id BIGINT NOT NULL,
project_flock_kandang_id BIGINT NULL,
kandang_id BIGINT NULL,
nonstock_id BIGINT,
qty NUMERIC(15, 3) NOT NULL,
unit_price NUMERIC(15, 3) NOT NULL,
total_price NUMERIC(15, 3) NOT NULL,
note TEXT NULL
);
-- Tambahkan Foreign Key ke expenses
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'expenses') THEN
ALTER TABLE expense_nonstocks
ADD CONSTRAINT fk_expense_nonstocks_expense_id
FOREIGN KEY (expense_id) REFERENCES expenses(id);
END IF;
END $$;
-- Tambahkan Foreign Key ke project_flock_kandangs
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
ALTER TABLE expense_nonstocks
ADD CONSTRAINT fk_expense_nonstocks_kandang_id
FOREIGN KEY (project_flock_kandang_id) REFERENCES project_flock_kandangs(id);
END IF;
END $$;
-- Tambahkan Foreign key ke kandang_id
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'kandangs') THEN
ALTER TABLE expense_nonstocks
ADD CONSTRAINT fk_expense_nonstocks_kandang_id_2
FOREIGN KEY (kandang_id) REFERENCES kandangs(id);
END IF;
END $$;
-- Tambahkan Foreign Key ke nonstocks
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'nonstocks') THEN
ALTER TABLE expense_nonstocks
ADD CONSTRAINT fk_expense_nonstocks_nonstock_id
FOREIGN KEY (nonstock_id) REFERENCES nonstocks(id);
END IF;
END $$;
-- Index
CREATE INDEX idx_expense_nonstocks_expense_id ON expense_nonstocks (expense_id);
CREATE INDEX idx_expense_nonstocks_nonstock_id ON expense_nonstocks (nonstock_id);
@@ -0,0 +1 @@
DROP TABLE IF EXISTS expense_realizations;
@@ -0,0 +1,35 @@
CREATE TABLE expense_realizations (
id BIGSERIAL PRIMARY KEY,
expense_nonstock_id BIGINT UNIQUE,
realization_qty NUMERIC(15, 3) NOT NULL,
realization_unit_price NUMERIC(15, 3) NOT NULL,
realization_total_price NUMERIC(15, 3) NOT NULL,
realization_date DATE NOT NULL,
note TEXT,
created_by BIGINT
);
-- Tambahkan Foreign Key ke expense_nonstocks
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'expense_nonstocks') THEN
ALTER TABLE expense_realizations
ADD CONSTRAINT fk_expense_realizations_nonstock_id
FOREIGN KEY (expense_nonstock_id) REFERENCES expense_nonstocks(id);
END IF;
END $$;
-- Tambahkan Foreign Key ke users (created_by)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
ALTER TABLE expense_realizations
ADD CONSTRAINT fk_expense_realizations_created_by
FOREIGN KEY (created_by) REFERENCES users(id);
END IF;
END $$;
-- Index
CREATE INDEX idx_expense_realizations_nonstock_id ON expense_realizations (expense_nonstock_id);
CREATE INDEX idx_expense_realizations_date ON expense_realizations (realization_date);
@@ -0,0 +1,16 @@
BEGIN;
ALTER TABLE project_flock_kandangs
DROP COLUMN IF EXISTS period;
ALTER TABLE project_flocks
ADD COLUMN IF NOT EXISTS period INT NOT NULL DEFAULT 0;
CREATE UNIQUE INDEX IF NOT EXISTS project_flocks_base_period_unique
ON project_flocks (
LOWER(TRIM(regexp_replace(flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g'))),
period
)
WHERE deleted_at IS NULL;
COMMIT;
@@ -0,0 +1,29 @@
BEGIN;
ALTER TABLE project_flock_kandangs
ADD COLUMN IF NOT EXISTS period INT;
UPDATE project_flock_kandangs pfk
SET period = pf.period
FROM project_flocks pf
WHERE pfk.project_flock_id = pf.id
AND (pfk.period IS NULL OR pfk.period = 0)
AND pf.period IS NOT NULL;
ALTER TABLE project_flock_kandangs
ALTER COLUMN period SET DEFAULT 0;
UPDATE project_flock_kandangs
SET period = 0
WHERE period IS NULL;
ALTER TABLE project_flock_kandangs
ALTER COLUMN period SET NOT NULL;
-- Drop period from project_flocks as the source of truth
DROP INDEX IF EXISTS project_flocks_base_period_unique;
ALTER TABLE project_flocks
DROP COLUMN IF EXISTS period;
COMMIT;
@@ -0,0 +1,11 @@
-- Add back timestamp columns to marketing_products table
ALTER TABLE marketing_products
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW(),
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW(),
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
-- Add back timestamp columns to marketing_delivery_products table
ALTER TABLE marketing_delivery_products
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW(),
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW(),
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
@@ -0,0 +1,27 @@
-- Drop timestamp columns from marketing_products table if it exists
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'marketing_products' AND column_name = 'created_at') THEN
ALTER TABLE marketing_products DROP COLUMN created_at;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'marketing_products' AND column_name = 'updated_at') THEN
ALTER TABLE marketing_products DROP COLUMN updated_at;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'marketing_products' AND column_name = 'deleted_at') THEN
ALTER TABLE marketing_products DROP COLUMN deleted_at;
END IF;
END $$;
-- Drop timestamp columns from marketing_delivery_products table if it exists
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'marketing_delivery_products' AND column_name = 'created_at') THEN
ALTER TABLE marketing_delivery_products DROP COLUMN created_at;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'marketing_delivery_products' AND column_name = 'updated_at') THEN
ALTER TABLE marketing_delivery_products DROP COLUMN updated_at;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'marketing_delivery_products' AND column_name = 'deleted_at') THEN
ALTER TABLE marketing_delivery_products DROP COLUMN deleted_at;
END IF;
END $$;
@@ -0,0 +1,44 @@
-- ============================
-- EXPENSES
-- ============================
ALTER TABLE expenses DROP COLUMN IF EXISTS grand_total;
ALTER TABLE expenses RENAME COLUMN note TO notes;
ALTER TABLE expenses RENAME COLUMN expense_date TO transaction_date;
-- ============================
-- EXPENSE_REALIZATIONS
-- ============================
ALTER TABLE expense_realizations
RENAME COLUMN realization_qty TO qty;
ALTER TABLE expense_realizations
RENAME COLUMN realization_unit_price TO price;
ALTER TABLE expense_realizations RENAME COLUMN note TO notes;
ALTER TABLE expense_realizations
DROP COLUMN IF EXISTS realization_total_price;
ALTER TABLE expense_realizations
DROP COLUMN IF EXISTS realization_date;
ALTER TABLE expense_realizations DROP COLUMN IF EXISTS created_by;
ALTER TABLE expense_realizations
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW();
-- ============================
-- EXPENSE_NONSTOCKS
-- ============================
ALTER TABLE expense_nonstocks RENAME COLUMN note TO notes;
ALTER TABLE expense_nonstocks DROP COLUMN IF EXISTS total_price;
ALTER TABLE expense_nonstocks RENAME COLUMN unit_price TO price;
ALTER TABLE expense_nonstocks DROP COLUMN IF EXISTS created_by;
ALTER TABLE expense_nonstocks
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW();
@@ -0,0 +1,2 @@
DROP Table IF EXISTS project_budgets;
@@ -0,0 +1,31 @@
CREATE TABLE project_budgets (
id BIGSERIAL PRIMARY KEY,
project_flock_id BIGINT NOT NULL,
nonstock_id BIGINT NOT NULL,
qty NUMERIC(15, 3) NOT NULL,
price NUMERIC(15, 3) NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);
-- Tambahkan Foreign Key ke project_flocks
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flocks') THEN
ALTER TABLE project_budgets
ADD CONSTRAINT fk_project_budgets_project_flock_id
FOREIGN KEY (project_flock_id) REFERENCES project_flocks(id);
END IF;
END $$;
-- Tambahkan Foreign Key ke nonstocks
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'nonstocks') THEN
ALTER TABLE project_budgets
ADD CONSTRAINT fk_project_budgets_nonstock_id
FOREIGN KEY (nonstock_id) REFERENCES nonstocks(id);
END IF;
END $$;
-- Index
CREATE INDEX idx_project_budgets_project_flock_id ON project_budgets (project_flock_id);
CREATE INDEX idx_project_budgets_nonstock_id ON project_budgets (nonstock_id);
+9 -8
View File
@@ -82,7 +82,7 @@ func Run(db *gorm.DB) error {
return err return err
} }
if err := seedTransferStock(tx, adminID); err != nil { if err := seedTransferStock(tx); err != nil {
return err return err
} }
fmt.Println("✅ Master data seeding completed") fmt.Println("✅ Master data seeding completed")
@@ -235,13 +235,14 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
seeds := []struct { seeds := []struct {
Name string Name string
Status utils.KandangStatus Status utils.KandangStatus
Capacity float64
Location string Location string
PicKey string PicKey string
}{ }{
{Name: "Singaparna 1", Status: utils.KandangStatusNonActive, Location: "Singaparna", PicKey: "admin"}, {Name: "Singaparna 1", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Singaparna", PicKey: "admin"},
{Name: "Singaparna 2", Status: utils.KandangStatusNonActive, Location: "Singaparna", PicKey: "admin"}, {Name: "Singaparna 2", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Singaparna", PicKey: "admin"},
{Name: "Cikaum 1", Status: utils.KandangStatusNonActive, Location: "Cikaum", PicKey: "admin"}, {Name: "Cikaum 1", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Cikaum", PicKey: "admin"},
{Name: "Cikaum 2", Status: utils.KandangStatusNonActive, Location: "Cikaum", PicKey: "admin"}, {Name: "Cikaum 2", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Cikaum", PicKey: "admin"},
} }
result := make(map[string]uint, len(seeds)) result := make(map[string]uint, len(seeds))
@@ -691,7 +692,7 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
var existing entity.ProductSupplier var existing entity.ProductSupplier
err := tx.Where("product_id = ? AND supplier_id = ?", product.Id, supplierID).First(&existing).Error err := tx.Where("product_id = ? AND supplier_id = ?", product.Id, supplierID).First(&existing).Error
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
link := entity.ProductSupplier{ProductID: product.Id, SupplierID: supplierID} link := entity.ProductSupplier{ProductId: product.Id, SupplierId: supplierID}
if err := tx.Create(&link).Error; err != nil { if err := tx.Create(&link).Error; err != nil {
return err return err
} }
@@ -764,7 +765,7 @@ func seedNonstocks(tx *gorm.DB, createdBy uint, uoms map[string]uint, suppliers
var existing entity.NonstockSupplier var existing entity.NonstockSupplier
err := tx.Where("nonstock_id = ? AND supplier_id = ?", nonstock.Id, supplierID).First(&existing).Error err := tx.Where("nonstock_id = ? AND supplier_id = ?", nonstock.Id, supplierID).First(&existing).Error
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
link := entity.NonstockSupplier{NonstockID: nonstock.Id, SupplierID: supplierID} link := entity.NonstockSupplier{NonstockId: nonstock.Id, SupplierId: supplierID}
if err := tx.Create(&link).Error; err != nil { if err := tx.Create(&link).Error; err != nil {
return err return err
} }
@@ -928,7 +929,7 @@ func seedProductWarehouse(tx *gorm.DB, createdBy uint) error {
return nil return nil
} }
func seedTransferStock(tx *gorm.DB, createdBy uint) error { func seedTransferStock(tx *gorm.DB) error {
transfer := entity.StockTransfer{ transfer := entity.StockTransfer{
FromWarehouseId: 1, FromWarehouseId: 1,
+1 -1
View File
@@ -8,7 +8,7 @@ import (
type Area struct { type Area struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:areas_name_unique,where:deleted_at IS NULL"` Name string `gorm:"type:varchar(50);not null;uniqueIndex:areas_name_unique,where:deleted_at IS NULL"`
CreatedBy uint `gorm:"not null"` CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
+2 -2
View File
@@ -8,9 +8,9 @@ import (
type Bank struct { type Bank struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:banks_name_unique,where:deleted_at IS NULL"` Name string `gorm:"type:varchar(50);not null;uniqueIndex:banks_name_unique,where:deleted_at IS NULL"`
Alias string `gorm:"not null;size:5"` Alias string `gorm:"not null;size:5"`
Owner *string `gorm:""` Owner *string `gorm:"type:varchar(50)"`
AccountNumber string `gorm:"not null;size:50"` AccountNumber string `gorm:"not null;size:50"`
CreatedBy uint `gorm:"not null"` CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
+2 -2
View File
@@ -8,12 +8,12 @@ import (
type Customer struct { type Customer struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:customers_name_unique,where:deleted_at IS NULL"` Name string `gorm:"type:varchar(50);not null;uniqueIndex:customers_name_unique,where:deleted_at IS NULL"`
PicId uint `gorm:"not null"` PicId uint `gorm:"not null"`
Type string `gorm:"not null;size:50"` Type string `gorm:"not null;size:50"`
Address string `gorm:"not null"` Address string `gorm:"not null"`
Phone string `gorm:"not null;size:20"` Phone string `gorm:"not null;size:20"`
Email string `gorm:"not null"` Email string `gorm:"type:varchar(50);not null"`
AccountNumber string `gorm:"not null;size:50"` AccountNumber string `gorm:"not null;size:50"`
Balance float64 `gorm:"default:0"` Balance float64 `gorm:"default:0"`
CreatedBy uint `gorm:"not null"` CreatedBy uint `gorm:"not null"`
+30
View File
@@ -0,0 +1,30 @@
package entities
import (
"database/sql"
"time"
"gorm.io/gorm"
)
type Expense struct {
Id uint64 `gorm:"primaryKey;autoIncrement"`
ReferenceNumber string `gorm:"type:varchar(50);uniqueIndex"`
SupplierId uint64 `gorm:""`
Category string `gorm:"type:varchar(50);not null"`
PoNumber string `gorm:"type:varchar(50)"`
DocumentPath sql.NullString `gorm:"type:json"`
RealizationDocumentPath sql.NullString `gorm:"type:json;column:realization_document_path"`
RealizationDate time.Time `gorm:"type:date;column:realization_date"`
TransactionDate time.Time `gorm:"type:date;not null"`
Notes string `gorm:"type:text;column:notes"`
CreatedBy uint64 `gorm:""`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Supplier *Supplier `gorm:"foreignKey:SupplierId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
Nonstocks []ExpenseNonstock `gorm:"foreignKey:ExpenseId;references:Id"`
LatestApproval *Approval `gorm:"-" json:"latest_approval,omitempty"`
}
+23
View File
@@ -0,0 +1,23 @@
package entities
import (
"time"
)
type ExpenseNonstock struct {
Id uint64 `gorm:"primaryKey;autoIncrement"`
ExpenseId *uint64 `gorm:""`
ProjectFlockKandangId *uint64 `gorm:""`
KandangId *uint64 `gorm:""`
NonstockId *uint64 `gorm:""`
Qty float64 `gorm:"type:numeric(15,3);not null"`
Price float64 `gorm:"type:numeric(15,3);not null;column:price"`
Notes string `gorm:"type:text;column:notes"`
CreatedAt time.Time `gorm:"type:timestamptz;default:CURRENT_TIMESTAMP"`
Expense *Expense `gorm:"foreignKey:ExpenseId;references:Id"`
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
Kandang *Kandang `gorm:"foreignKey:KandangId;references:Id"`
Nonstock *Nonstock `gorm:"foreignKey:NonstockId;references:Id"`
Realization *ExpenseRealization `gorm:"foreignKey:Id;references:ExpenseNonstockId"`
}
+16
View File
@@ -0,0 +1,16 @@
package entities
import (
"time"
)
type ExpenseRealization struct {
Id uint64 `gorm:"primaryKey;autoIncrement"`
ExpenseNonstockId *uint64 `gorm:""`
Qty float64 `gorm:"type:numeric(15,3);not null;"`
Price float64 `gorm:"type:numeric(15,3);not null;"`
Notes string `gorm:"type:text;"`
CreatedAt time.Time `gorm:"type:timestamptz;default:CURRENT_TIMESTAMP"`
ExpenseNonstock *ExpenseNonstock `gorm:"foreignKey:ExpenseNonstockId;references:Id"`
}
+1 -1
View File
@@ -8,7 +8,7 @@ import (
type Fcr struct { type Fcr struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:idx_suppliers_name,where:deleted_at IS NULL"` Name string `gorm:"type:varchar(50);not null;uniqueIndex:idx_suppliers_name,where:deleted_at IS NULL"`
CreatedBy uint `gorm:"not null"` CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
+1 -1
View File
@@ -9,7 +9,7 @@ const (
type Flag struct { type Flag struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:flags_unique_flagable"` Name string `gorm:"type:varchar(50);size:50;not null;uniqueIndex:flags_unique_flagable"`
FlagableID uint `gorm:"not null;uniqueIndex:flags_unique_flagable;index:flags_flagable_lookup,priority:2"` 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"` FlagableType string `gorm:"size:50;not null;uniqueIndex:flags_unique_flagable;index:flags_flagable_lookup,priority:1"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
+13 -12
View File
@@ -7,17 +7,18 @@ import (
) )
type Kandang struct { type Kandang struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:kandangs_name_unique,where:deleted_at IS NULL"` Name string `gorm:"type:varchar(50);not null;uniqueIndex:kandangs_name_unique,where:deleted_at IS NULL"`
Status string `gorm:"type:varchar(50);not null"` Status string `gorm:"type:varchar(50);not null"`
LocationId uint `gorm:"not null"` LocationId uint `gorm:"not null"`
PicId uint `gorm:"not null"` Capacity float64 `gorm:"not null"`
CreatedBy uint `gorm:"not null"` PicId uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedBy uint `gorm:"not null"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Location Location `gorm:"foreignKey:LocationId;references:Id"` CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Pic User `gorm:"foreignKey:PicId;references:Id"` Location Location `gorm:"foreignKey:LocationId;references:Id"`
Pic User `gorm:"foreignKey:PicId;references:Id"`
ProjectFlockKandangs []ProjectFlockKandang `gorm:"foreignKey:KandangId;references:Id" json:"-"` ProjectFlockKandangs []ProjectFlockKandang `gorm:"foreignKey:KandangId;references:Id" json:"-"`
} }
+4 -3
View File
@@ -8,7 +8,7 @@ import (
type Location struct { type Location struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:locations_name_unique,where:deleted_at IS NULL"` Name string `gorm:"type:varchar(50);not null;uniqueIndex:locations_name_unique,where:deleted_at IS NULL"`
Address string `gorm:"not null"` Address string `gorm:"not null"`
AreaId uint `gorm:"not null"` AreaId uint `gorm:"not null"`
CreatedBy uint `gorm:"not null"` CreatedBy uint `gorm:"not null"`
@@ -16,6 +16,7 @@ type Location struct {
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Area Area `gorm:"foreignKey:AreaId;references:Id"` Area Area `gorm:"foreignKey:AreaId;references:Id"`
Kandangs []Kandang `gorm:"foreignKey:LocationId;references:Id"`
} }
+27
View File
@@ -0,0 +1,27 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Marketing struct {
Id uint `gorm:"primaryKey;autoIncrement"`
SoNumber string `gorm:"uniqueIndex;not null"`
CustomerId uint `gorm:"not null"`
SoDocs string `gorm:"type:varchar(20)"`
SoDate time.Time `gorm:"type:date;not null"`
SalesPersonId uint `gorm:"not null"`
Notes string `gorm:"type:text"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Customer Customer `gorm:"foreignKey:CustomerId;references:Id"`
SalesPerson User `gorm:"foreignKey:SalesPersonId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Products []MarketingProduct `gorm:"foreignKey:MarketingId;references:Id"`
LatestApproval *Approval `gorm:"-" json:"latest_approval,omitempty"`
}
@@ -0,0 +1,19 @@
package entities
import (
"time"
)
type MarketingDeliveryProduct struct {
Id uint `gorm:"primaryKey;autoIncrement"`
MarketingProductId uint `gorm:"uniqueIndex;not null"`
Qty float64 `gorm:"type:numeric(15,3)"`
UnitPrice float64 `gorm:"type:numeric(15,3)"`
TotalWeight float64 `gorm:"type:numeric(15,3)"`
AvgWeight float64 `gorm:"type:numeric(15,3)"`
TotalPrice float64 `gorm:"type:numeric(15,3)"`
DeliveryDate *time.Time `gorm:"type:timestamptz"`
VehicleNumber string `gorm:"type:varchar(50)"`
MarketingProduct MarketingProduct `gorm:"foreignKey:MarketingProductId;references:Id"`
}
+16
View File
@@ -0,0 +1,16 @@
package entities
type MarketingProduct struct {
Id uint `gorm:"primaryKey;autoIncrement"`
MarketingId uint `gorm:"not null"`
ProductWarehouseId uint `gorm:"not null"`
Qty float64 `gorm:"type:numeric(15,3);not null"`
UnitPrice float64 `gorm:"type:numeric(15,3);not null"`
AvgWeight float64 `gorm:"type:numeric(15,3);not null"`
TotalWeight float64 `gorm:"type:numeric(15,3);not null"`
TotalPrice float64 `gorm:"type:numeric(15,3);not null"`
Marketing Marketing `gorm:"foreignKey:MarketingId;references:Id"`
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
DeliveryProduct *MarketingDeliveryProduct `gorm:"foreignKey:MarketingProductId;references:Id"`
}
+5 -5
View File
@@ -8,15 +8,15 @@ import (
type Nonstock struct { type Nonstock struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:nonstocks_name_unique,where:deleted_at IS NULL"` Name string `gorm:"type:varchar(50);not null;uniqueIndex:nonstocks_name_unique,where:deleted_at IS NULL"`
UomId uint `gorm:"not null"` UomId uint `gorm:"not null"`
CreatedBy uint `gorm:"not null"` CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Uom Uom `gorm:"foreignKey:UomId;references:Id"` Uom Uom `gorm:"foreignKey:UomId;references:Id"`
Suppliers []Supplier `gorm:"many2many:nonstock_suppliers;joinForeignKey:NonstockID;joinReferences:SupplierID"` NonstockSuppliers []NonstockSupplier `gorm:"foreignKey:NonstockId;references:Id"`
Flags []Flag `gorm:"polymorphic:Flagable;polymorphicValue:nonstocks"` Flags []Flag `gorm:"polymorphic:Flagable;polymorphicValue:nonstocks"`
} }
+5 -2
View File
@@ -3,7 +3,10 @@ package entities
import "time" import "time"
type NonstockSupplier struct { type NonstockSupplier struct {
NonstockID uint `gorm:"primaryKey"` NonstockId uint `gorm:"not null"`
SupplierID uint `gorm:"primaryKey"` SupplierId uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
Nonstock Nonstock `gorm:"foreignKey:NonstockId;references:Id"`
Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"`
} }
+1 -1
View File
@@ -8,7 +8,7 @@ import (
type ProductCategory struct { type ProductCategory struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:product_categories_name_unique,where:deleted_at IS NULL"` Name string `gorm:"type:varchar(50);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"` Code string `gorm:"not null;size:10;uniqueIndex:product_categories_code_unique,where:deleted_at IS NULL"`
CreatedBy uint `gorm:"not null"` CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
+7 -7
View File
@@ -8,8 +8,8 @@ import (
type Product struct { type Product struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:products_name_unique,where:deleted_at IS NULL"` Name string `gorm:"type:varchar(50);not null;uniqueIndex:products_name_unique,where:deleted_at IS NULL"`
Brand string `gorm:"not null"` Brand string `gorm:"type:varchar(50);not null"`
Sku *string `gorm:"size:100;uniqueIndex:products_sku_unique,where:deleted_at IS NULL"` Sku *string `gorm:"size:100;uniqueIndex:products_sku_unique,where:deleted_at IS NULL"`
UomId uint `gorm:"not null"` UomId uint `gorm:"not null"`
ProductCategoryId uint `gorm:"not null"` ProductCategoryId uint `gorm:"not null"`
@@ -22,9 +22,9 @@ type Product struct {
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Uom Uom `gorm:"foreignKey:UomId;references:Id"` Uom Uom `gorm:"foreignKey:UomId;references:Id"`
ProductCategory ProductCategory `gorm:"foreignKey:ProductCategoryId;references:Id"` ProductCategory ProductCategory `gorm:"foreignKey:ProductCategoryId;references:Id"`
Suppliers []Supplier `gorm:"many2many:product_suppliers;joinForeignKey:ProductID;joinReferences:SupplierID"` ProductSuppliers []ProductSupplier `gorm:"foreignKey:ProductId;references:Id"`
Flags []Flag `gorm:"polymorphic:Flagable;polymorphicValue:products"` Flags []Flag `gorm:"polymorphic:Flagable;polymorphicValue:products"`
} }
+5 -2
View File
@@ -3,7 +3,10 @@ package entities
import "time" import "time"
type ProductSupplier struct { type ProductSupplier struct {
ProductID uint `gorm:"primaryKey"` ProductId uint `gorm:"not null"`
SupplierID uint `gorm:"primaryKey"` SupplierId uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
Product Product `gorm:"foreignKey:ProductId;references:Id"`
Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"`
} }
+15
View File
@@ -0,0 +1,15 @@
package entities
import (
"time"
)
type ProjectBudget struct {
Id uint `gorm:"primaryKey"`
Qty float64 `gorm:"type:numeric(15,3);not null"`
Price float64 `gorm:"type:numeric(15,3);not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
Nonstock *Nonstock `gorm:"foreignKey:Id;references:Id"`
ProjectFlock *ProjectFlock `gorm:"foreignKey:Id;references:Id"`
}
-2
View File
@@ -13,7 +13,6 @@ type ProjectFlock struct {
Category string `gorm:"type:varchar(20);not null"` Category string `gorm:"type:varchar(20);not null"`
FcrId uint `gorm:"not null"` FcrId uint `gorm:"not null"`
LocationId uint `gorm:"not null"` LocationId uint `gorm:"not null"`
Period int `gorm:"not null"`
CreatedBy uint `gorm:"not null"` CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
@@ -28,4 +27,3 @@ type ProjectFlock struct {
LatestApproval *Approval `gorm:"-" json:"-"` LatestApproval *Approval `gorm:"-" json:"-"`
} }
@@ -6,6 +6,7 @@ type ProjectFlockKandang struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
ProjectFlockId uint `gorm:"not null;index:idx_project_flock_kandangs_project;uniqueIndex:idx_project_flock_kandangs_unique"` 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"` KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_unique"`
Period int `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"` ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
+27
View File
@@ -0,0 +1,27 @@
package entities
import (
"time"
)
type Purchase struct {
Id uint `gorm:"primaryKey;autoIncrement"`
PrNumber string `gorm:"not null"`
PoNumber *string
PoDate *time.Time
SupplierId uint `gorm:"not null"`
CreditTerm *int
DueDate *time.Time
GrandTotal float64 `gorm:"type:numeric(15,3);default:0"`
Notes *string
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt *time.Time `gorm:"index"`
CreatedBy uint `gorm:"not null"`
// Relations
Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"`
Items []PurchaseItem `gorm:"foreignKey:PurchaseId"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
LatestApproval *Approval `gorm:"-" json:"-"`
}
+28
View File
@@ -0,0 +1,28 @@
package entities
import (
"time"
)
type PurchaseItem struct {
Id uint `gorm:"primaryKey;autoIncrement"`
PurchaseId uint `gorm:"not null"`
ProductId uint `gorm:"not null"`
WarehouseId uint `gorm:"not null"`
ProductWarehouseId *uint
ReceivedDate *time.Time
TravelNumber *string
TravelNumberDocs *string
VehicleNumber *string
SubQty float64 `gorm:"type:numeric(15,3);not null"`
TotalQty float64 `gorm:"type:numeric(15,3);default:0"`
TotalUsed float64 `gorm:"type:numeric(15,3);default:0"`
Price float64 `gorm:"type:numeric(15,3);default:0"`
TotalPrice float64 `gorm:"type:numeric(15,3);default:0"`
// Relations
Purchase *Purchase `gorm:"foreignKey:PurchaseId;references:Id"`
Product *Product `gorm:"foreignKey:ProductId;references:Id"`
Warehouse *Warehouse `gorm:"foreignKey:WarehouseId;references:Id"`
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
}
+33
View File
@@ -0,0 +1,33 @@
package entities
import (
"time"
"gorm.io/gorm"
)
const (
StockAllocationStatusPending = "PENDING"
StockAllocationStatusActive = "ACTIVE"
StockAllocationStatusReleased = "RELEASED"
)
// StockAllocation links a usable record (consumption) with an incoming stock record.
// The combination lets us trace FIFO deductions while keeping each module focused on its own fields.
type StockAllocation struct {
Id uint `gorm:"primaryKey"`
ProductWarehouseId uint `gorm:"not null;index"`
StockableType string `gorm:"size:100;not null;index:stock_allocations_lookup,priority:1"`
StockableId uint `gorm:"not null;index:stock_allocations_lookup,priority:2"`
UsableType string `gorm:"size:100;not null;index:stock_allocations_usage_lookup,priority:1"`
UsableId uint `gorm:"not null;index:stock_allocations_usage_lookup,priority:2"`
Qty float64 `gorm:"type:numeric(15,3);not null"`
Status string `gorm:"size:20;not null;default:ACTIVE"`
Note *string `gorm:"type:text"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
ReleasedAt *time.Time `gorm:"index"`
DeletedAt gorm.DeletedAt `gorm:"index"`
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
}
+7 -5
View File
@@ -8,14 +8,14 @@ import (
type Supplier struct { type Supplier struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:suppliers_name_unique,where:deleted_at IS NULL"` Name string `gorm:"type:varchar(50);not null;uniqueIndex:suppliers_name_unique,where:deleted_at IS NULL"`
Alias string `gorm:"not null;size:5"` Alias string `gorm:"not null;size:5"`
Pic string `gorm:"not null"` Pic string `gorm:"type:varchar(50);not null"`
Type string `gorm:"not null;size:50"` Type string `gorm:"not null;size:50"`
Category string `gorm:"not null;size:20"` Category string `gorm:"not null;size:20"`
Hatchery *string `gorm:"size:255"` Hatchery *string `gorm:"type:varchar(50)"`
Phone string `gorm:"not null;size:20"` Phone string `gorm:"not null;size:20"`
Email string `gorm:"not null"` Email string `gorm:"type:varchar(50);not null"`
Address string `gorm:"not null"` Address string `gorm:"not null"`
Npwp *string `gorm:"size:50"` Npwp *string `gorm:"size:50"`
AccountNumber *string `gorm:"size:50"` AccountNumber *string `gorm:"size:50"`
@@ -26,5 +26,7 @@ type Supplier struct {
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
ProductSuppliers []ProductSupplier `gorm:"foreignKey:SupplierId;references:Id"`
NonstockSuppliers []NonstockSupplier `gorm:"foreignKey:SupplierId;references:Id"`
} }
+1 -1
View File
@@ -8,7 +8,7 @@ import (
type Uom struct { type Uom struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:uoms_name_unique,where:deleted_at IS NULL"` Name string `gorm:"type:varchar(50);not null;uniqueIndex:uoms_name_unique,where:deleted_at IS NULL"`
CreatedBy uint `gorm:"not null"` CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
+2 -2
View File
@@ -9,8 +9,8 @@ import (
type User struct { type User struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
IdUser int64 `gorm:"uniqueIndex"` IdUser int64 `gorm:"uniqueIndex"`
Email string `gorm:"uniqueIndex"` Email string `gorm:"type:varchar(50);uniqueIndex"`
Name string `gorm:"not null"` Name string `gorm:"type:varchar(50);not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
+1 -1
View File
@@ -8,7 +8,7 @@ import (
type Warehouse struct { type Warehouse struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
Name string `gorm:"not null"` Name string `gorm:"type:varchar(50);not null"`
Type string `gorm:"not null"` Type string `gorm:"not null"`
AreaId uint `gorm:"not null"` AreaId uint `gorm:"not null"`
LocationId *uint LocationId *uint
+185 -85
View File
@@ -1,101 +1,201 @@
package middleware package middleware
// import ( import (
// "strings" "strings"
// "gitlab.com/mbugroup/lti-api.git/internal/config" "gitlab.com/mbugroup/lti-api.git/internal/config"
// service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
// "gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"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"
// ) )
// func Auth(userService service.UserService, requiredRights ...string) fiber.Handler { const (
// return func(c *fiber.Ctx) error { authContextLocalsKey = "auth.context"
// authHeader := c.Get("Authorization") authUserLocalsKey = "auth.user"
// token := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer ")) )
// if token == "" { // AuthContext keeps authentication details captured by the middleware.
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") type AuthContext struct {
// } Token string
Verification *sso.VerificationResult
User *entity.User
Roles []sso.Role
Permissions map[string]struct{}
}
// userID, err := utils.VerifyToken(token, config.JWTSecret, config.TokenTypeAccess) // Auth validates the incoming request against the central SSO access token and
// if err != nil { // loads the corresponding local user. Optional scopes can be provided to enforce
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") // fine-grained authorization using the SSO access token scopes.
// } func Auth(userService service.UserService, requiredScopes ...string) fiber.Handler {
return func(c *fiber.Ctx) error {
token := bearerToken(c)
if token == "" {
token = strings.TrimSpace(c.Cookies(config.SSOAccessCookieName))
}
if token == "" {
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
// // Only end-user subjects are allowed by this middleware. Service tokens verification, err := sso.VerifyAccessToken(token)
// if verification.UserID == 0 { if err != nil {
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") utils.Log.WithError(err).Warn("auth: token verification failed")
// } return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
// // Fail-closed on revocation check errors for stricter security posture. if verification.UserID == 0 {
// if revoker := session.GetRevocationStore(); revoker != nil { return fiber.NewError(fiber.StatusForbidden, "Service authentication is not permitted for this endpoint")
// if fingerprint := session.TokenFingerprint(token); fingerprint != "" { }
// revoked, err := revoker.IsRevoked(c.Context(), fingerprint)
// if err != nil {
// utils.Log.WithError(err).Warn("failed to check token revocation")
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
// }
// if revoked {
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
// }
// }
// }
// user, err := userService.GetBySSOUserID(c, verification.UserID) if err := ensureNotRevoked(c, token, verification); err != nil {
// if err != nil || user == nil { return err
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") }
// }
// if len(requiredRights) > 0 && verification.Claims != nil { user, err := userService.GetBySSOUserID(c, verification.UserID)
// if !hasAllScopes(verification.Claims.Scopes(), requiredRights) { if err != nil || user == nil {
// return fiber.NewError(fiber.StatusForbidden, "Insufficient scope") utils.Log.WithError(err).Warn("auth: failed to resolve user from repository")
// } return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
// } }
// c.Locals("user", user) if len(requiredScopes) > 0 {
if verification.Claims == nil || !hasAllScopes(verification.Claims.Scopes(), requiredScopes) {
return fiber.NewError(fiber.StatusForbidden, "Insufficient scope")
}
}
// // if len(requiredRights) > 0 { var roles []sso.Role
// // userRights, hasRights := config.RoleRights[user.Role] permissions := make(map[string]struct{})
// // if (!hasRights || !hasAllRights(userRights, requiredRights)) && c.Params("userId") != userID { if verification.UserID != 0 {
// // return fiber.NewError(fiber.StatusForbidden, "You don't have permission to access this resource") if profile, err := sso.FetchProfile(c.Context(), token, verification); err != nil {
// // } utils.Log.WithError(err).Warn("auth: failed to fetch sso profile")
// // } } else if profile != nil {
roles = profile.Roles
for _, perm := range profile.PermissionNames() {
if perm != "" {
permissions[perm] = struct{}{}
}
}
}
}
// return c.Next() ctx := &AuthContext{
// } Token: token,
// } Verification: verification,
User: user,
Roles: roles,
Permissions: permissions,
}
// // bearerToken extracts a Bearer token from the Authorization header using c.Locals(authContextLocalsKey, ctx)
// // case-insensitive scheme matching and tolerant whitespace handling. c.Locals(authUserLocalsKey, user)
// func bearerToken(c *fiber.Ctx) string {
// parts := strings.Fields(c.Get("Authorization"))
// if len(parts) == 2 && strings.EqualFold(parts[0], "Bearer") {
// return strings.TrimSpace(parts[1])
// }
// return ""
// }
// func hasAllScopes(have, required []string) bool { return c.Next()
// if len(required) == 0 { }
// return true }
// }
// set := make(map[string]struct{}, len(have)) // AuthenticatedUser returns the authenticated user populated by Auth.
// for _, s := range have { func AuthenticatedUser(c *fiber.Ctx) (*entity.User, bool) {
// s = strings.ToLower(strings.TrimSpace(s)) value := c.Locals(authUserLocalsKey)
// if s != "" { if user, ok := value.(*entity.User); ok && user != nil {
// set[s] = struct{}{} return user, true
// } }
// } return nil, false
// for _, r := range required { }
// r = strings.ToLower(strings.TrimSpace(r))
// if r == "" { func ActorIDFromContext(c *fiber.Ctx) (uint, error) {
// continue user, ok := AuthenticatedUser(c)
// } if !ok || user == nil || user.Id == 0 {
// if _, ok := set[r]; !ok { return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
// return false }
// } return user.Id, nil
// } }
// return true
// } // AuthDetails returns the full authentication context (token, claims, user).
func AuthDetails(c *fiber.Ctx) (*AuthContext, bool) {
value := c.Locals(authContextLocalsKey)
if ctx, ok := value.(*AuthContext); ok && ctx != nil {
return ctx, true
}
return nil, false
}
// ensureNotRevoked ensures the token is not revoked or superseded by a forced logout.
func ensureNotRevoked(c *fiber.Ctx, token string, verification *sso.VerificationResult) error {
revoker := session.GetRevocationStore()
if revoker == nil {
return nil
}
if fingerprint := session.TokenFingerprint(token); fingerprint != "" {
revoked, err := revoker.IsRevoked(c.Context(), fingerprint)
if err != nil {
utils.Log.WithError(err).Warn("auth: token revocation check failed")
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
if revoked {
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
}
if verification.UserID == 0 {
return nil
}
logoutAt, err := revoker.UserLogoutTime(c.Context(), verification.UserID)
if err != nil {
utils.Log.WithError(err).Warn("auth: failed to load user logout marker")
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
if logoutAt.IsZero() {
return nil
}
claims := verification.Claims
if claims == nil || claims.IssuedAt == nil {
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
issuedAt := claims.IssuedAt.Time
// Treat tokens issued at or before the forced logout timestamp as invalid.
if !issuedAt.After(logoutAt) {
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
return nil
}
// bearerToken extracts a Bearer token from the Authorization header using
// case-insensitive scheme matching and tolerant whitespace handling.
func bearerToken(c *fiber.Ctx) string {
parts := strings.Fields(c.Get("Authorization"))
if len(parts) == 2 && strings.EqualFold(parts[0], "Bearer") {
return strings.TrimSpace(parts[1])
}
return ""
}
func hasAllScopes(have, required []string) bool {
if len(required) == 0 {
return true
}
set := make(map[string]struct{}, len(have))
for _, s := range have {
s = strings.ToLower(strings.TrimSpace(s))
if s != "" {
set[s] = struct{}{}
}
}
for _, r := range required {
r = strings.ToLower(strings.TrimSpace(r))
if r == "" {
continue
}
if _, ok := set[r]; !ok {
return false
}
}
return true
}
+21
View File
@@ -24,3 +24,24 @@ func LimiterConfig() fiber.Handler {
SkipSuccessfulRequests: true, SkipSuccessfulRequests: true,
}) })
} }
func NewLimiter(max int, expiration time.Duration) fiber.Handler {
if max <= 0 {
max = 10
}
if expiration <= 0 {
expiration = time.Minute
}
return limiter.New(limiter.Config{
Max: max,
Expiration: expiration,
LimitReached: func(c *fiber.Ctx) error {
return c.Status(fiber.StatusTooManyRequests).
JSON(response.Common{
Code: fiber.StatusTooManyRequests,
Status: "error",
Message: "Too many requests, please try again later",
})
},
})
}
+75
View File
@@ -0,0 +1,75 @@
package middleware
import (
"strings"
"github.com/gofiber/fiber/v2"
)
// RequirePermissions ensures the authenticated user possesses all specified permissions.
func RequirePermissions(perms ...string) fiber.Handler {
required := canonicalPermissions(perms)
return func(c *fiber.Ctx) error {
if len(required) == 0 {
return c.Next()
}
ctx, ok := AuthDetails(c)
if !ok || ctx == nil {
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
userPerms := ctx.permissionSet()
if len(userPerms) == 0 {
return fiber.NewError(fiber.StatusForbidden, "Insufficient permission")
}
for _, perm := range required {
if _, has := userPerms[perm]; !has {
return fiber.NewError(fiber.StatusForbidden, "Insufficient permission")
}
}
return c.Next()
}
}
// HasPermission reports whether the current request context includes the given permission.
func HasPermission(c *fiber.Ctx, perm string) bool {
ctx, ok := AuthDetails(c)
if !ok || ctx == nil {
return false
}
perm = canonicalPermission(perm)
if perm == "" {
return false
}
_, has := ctx.permissionSet()[perm]
return has
}
func (a *AuthContext) permissionSet() map[string]struct{} {
if a == nil || a.Permissions == nil {
return nil
}
return a.Permissions
}
func canonicalPermissions(perms []string) []string {
out := make([]string, 0, len(perms))
seen := make(map[string]struct{}, len(perms))
for _, perm := range perms {
if canonical := canonicalPermission(perm); canonical != "" {
if _, ok := seen[canonical]; ok {
continue
}
seen[canonical] = struct{}{}
out = append(out, canonical)
}
}
return out
}
func canonicalPermission(perm string) string {
return strings.ToLower(strings.TrimSpace(perm))
}
+4
View File
@@ -16,6 +16,10 @@ func JSONBody() fiber.Handler {
return c.Next() return c.Next()
} }
if strings.EqualFold(c.Path(), "/api/sso/users/sync") {
return c.Next()
}
body := c.Body() body := c.Body()
if len(body) == 0 { if len(body) == 0 {
return c.Next() return c.Next()
@@ -85,7 +85,7 @@ func (u *ApprovalController) GetAll(c *fiber.Ctx) error {
flat := dto.ToApprovalDTOs(records) flat := dto.ToApprovalDTOs(records)
return c.Status(fiber.StatusOK). return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.ApprovalBaseDTO]{ JSON(response.SuccessWithPaginate[dto.ApprovalRelationDTO]{
Code: fiber.StatusOK, Code: fiber.StatusOK,
Status: "success", Status: "success",
Message: "Get All approvals successfully", Message: "Get All approvals successfully",
+19 -17
View File
@@ -10,23 +10,25 @@ import (
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
) )
type ApprovalBaseDTO struct { type ApprovalRelationDTO struct {
StepNumber uint16 `json:"step_number"` Id uint `json:"id"`
StepName string `json:"step_name"` StepNumber uint16 `json:"step_number"`
Action *string `json:"action"` StepName string `json:"step_name"`
Notes *string `json:"notes"` Action *string `json:"action"`
ActionBy userDTO.UserBaseDTO `json:"action_by"` Notes *string `json:"notes"`
ActionAt time.Time `json:"action_at"` ActionBy userDTO.UserRelationDTO `json:"action_by"`
ActionAt time.Time `json:"action_at"`
} }
type ApprovalGroupDTO struct { type ApprovalGroupDTO struct {
StepNumber uint16 `json:"step_number"` StepNumber uint16 `json:"step_number"`
StepName string `json:"step_name"` StepName string `json:"step_name"`
Approvals []ApprovalBaseDTO `json:"approvals"` Approvals []ApprovalRelationDTO `json:"approvals"`
} }
func ToApprovalDTO(e entity.Approval) ApprovalBaseDTO { func ToApprovalDTO(e entity.Approval) ApprovalRelationDTO {
dto := ApprovalBaseDTO{ dto := ApprovalRelationDTO{
Id: e.Id,
Notes: e.Notes, Notes: e.Notes,
} }
@@ -52,10 +54,10 @@ func ToApprovalDTO(e entity.Approval) ApprovalBaseDTO {
} }
if e.ActionUser != nil && e.ActionUser.Id != 0 { if e.ActionUser != nil && e.ActionUser.Id != 0 {
user := userDTO.ToUserBaseDTO(*e.ActionUser) user := userDTO.ToUserRelationDTO(*e.ActionUser)
dto.ActionBy = user dto.ActionBy = user
} else if e.ActionBy != nil && *e.ActionBy != 0 { } else if e.ActionBy != nil && *e.ActionBy != 0 {
dto.ActionBy = userDTO.UserBaseDTO{ dto.ActionBy = userDTO.UserRelationDTO{
Id: *e.ActionBy, Id: *e.ActionBy,
IdUser: int64(*e.ActionBy), IdUser: int64(*e.ActionBy),
} }
@@ -69,8 +71,8 @@ func ToApprovalDTO(e entity.Approval) ApprovalBaseDTO {
return dto return dto
} }
func ToApprovalDTOs(items []entity.Approval) []ApprovalBaseDTO { func ToApprovalDTOs(items []entity.Approval) []ApprovalRelationDTO {
result := make([]ApprovalBaseDTO, len(items)) result := make([]ApprovalRelationDTO, len(items))
for i, item := range items { for i, item := range items {
result[i] = ToApprovalDTO(item) result[i] = ToApprovalDTO(item)
} }
@@ -84,7 +86,7 @@ func ToApprovalGroupDTOs(items []entity.Approval) []ApprovalGroupDTO {
type groupAccumulator struct { type groupAccumulator struct {
StepName string StepName string
Approvals []ApprovalBaseDTO Approvals []ApprovalRelationDTO
} }
groups := make(map[uint16]*groupAccumulator) groups := make(map[uint16]*groupAccumulator)
+2 -2
View File
@@ -3,7 +3,7 @@ package approvals
import ( import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware" // m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service" common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/controllers" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/controllers"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -12,8 +12,8 @@ import (
func ApprovalRoutes(v1 fiber.Router, u user.UserService, s common.ApprovalService) { func ApprovalRoutes(v1 fiber.Router, u user.UserService, s common.ApprovalService) {
_ = u _ = u
ctrl := controller.NewApprovalController(s) ctrl := controller.NewApprovalController(s)
route := v1.Group("/approvals") route := v1.Group("/approvals")
route.Use(m.Auth(u))
route.Get("/", ctrl.GetAll) route.Get("/", ctrl.GetAll)
} }
@@ -0,0 +1,76 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type ClosingController struct {
ClosingService service.ClosingService
}
func NewClosingController(closingService service.ClosingService) *ClosingController {
return &ClosingController{
ClosingService: closingService,
}
}
func (u *ClosingController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
}
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.ClosingService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.ClosingListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all closings successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToClosingListDTOs(result),
})
}
func (u *ClosingController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
result, err := u.ClosingService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get closing successfully",
Data: dto.ToClosingListDTO(*result),
})
}
@@ -0,0 +1,64 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type ClosingRelationDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type ClosingListDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ClosingDetailDTO struct {
ClosingListDTO
}
// === Mapper Functions ===
func ToClosingRelationDTO(e entity.ProjectFlock) ClosingRelationDTO {
return ClosingRelationDTO{
Id: e.Id,
}
}
func ToClosingListDTO(e entity.ProjectFlock) ClosingListDTO {
var createdUser *userDTO.UserRelationDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserRelationDTO(e.CreatedUser)
createdUser = &mapped
}
return ClosingListDTO{
Id: e.Id,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
}
}
func ToClosingListDTOs(e []entity.ProjectFlock) []ClosingListDTO {
result := make([]ClosingListDTO, len(e))
for i, r := range e {
result[i] = ToClosingListDTO(r)
}
return result
}
func ToClosingDetailDTO(e entity.ProjectFlock) ClosingDetailDTO {
return ClosingDetailDTO{
ClosingListDTO: ToClosingListDTO(e),
}
}
+26
View File
@@ -0,0 +1,26 @@
package closings
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
sClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type ClosingModule struct{}
func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
closingRepo := rClosing.NewClosingRepository(db)
userRepo := rUser.NewUserRepository(db)
closingService := sClosing.NewClosingService(closingRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
ClosingRoutes(router, userService, closingService)
}
@@ -0,0 +1,21 @@
package repository
import (
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type ClosingRepository interface {
repository.BaseRepository[entity.ProjectFlock]
}
type ClosingRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.ProjectFlock]
}
func NewClosingRepository(db *gorm.DB) ClosingRepository {
return &ClosingRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectFlock](db),
}
}
+25
View File
@@ -0,0 +1,25 @@
package closings
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/controllers"
closing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService) {
ctrl := controller.NewClosingController(s)
route := v1.Group("/closings")
// route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll)
route.Get("/:id", ctrl.GetOne)
}
@@ -0,0 +1,72 @@
package service
import (
"errors"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type ClosingService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error)
}
type closingService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.ClosingRepository
}
func NewClosingService(repo repository.ClosingRepository, validate *validator.Validate) ClosingService {
return &closingService{
Log: utils.Log,
Validate: validate,
Repository: repo,
}
}
func (s closingService) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser")
}
func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
closings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
return db.Where("name LIKE ?", "%"+params.Search+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get closings: %+v", err)
return nil, 0, err
}
return closings, total, nil
}
func (s closingService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) {
closing, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Closing not found")
}
if err != nil {
s.Log.Errorf("Failed get closing by id: %+v", err)
return nil, err
}
return closing, nil
}
@@ -0,0 +1,15 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
Search string `query:"search" validate:"omitempty,max=50"`
}
@@ -0,0 +1,403 @@
package controller
import (
"encoding/json"
"fmt"
"math"
"strconv"
"strings"
"gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type ExpenseController struct {
ExpenseService service.ExpenseService
}
func NewExpenseController(expenseService service.ExpenseService) *ExpenseController {
return &ExpenseController{
ExpenseService: expenseService,
}
}
func (u *ExpenseController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
}
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.ExpenseService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.ExpenseListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all expenses successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: result,
})
}
func (u *ExpenseController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
result, err := u.ExpenseService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get expense successfully",
Data: result,
})
}
func (u *ExpenseController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create)
req.TransactionDate = c.FormValue("transaction_date")
req.Category = c.FormValue("category")
supplierID, err := strconv.ParseUint(c.FormValue("supplier_id"), 10, 64)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid supplier_id format")
}
req.SupplierID = supplierID
form, err := c.MultipartForm()
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form")
}
req.Documents = form.File["documents"]
expenseNonstocksJSON := c.FormValue("expense_nonstocks")
if expenseNonstocksJSON != "" {
if err := json.Unmarshal([]byte(expenseNonstocksJSON), &req.ExpenseNonstocks); err != nil {
var singleExpenseNonstock validation.ExpenseNonstock
if err := json.Unmarshal([]byte(expenseNonstocksJSON), &singleExpenseNonstock); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid expense_nonstocks JSON: %v", err))
}
if singleExpenseNonstock.KandangID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Field KandangID is required")
}
req.ExpenseNonstocks = []validation.ExpenseNonstock{singleExpenseNonstock}
} else {
for i, expenseNonstock := range req.ExpenseNonstocks {
if expenseNonstock.KandangID == 0 {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Field KandangID is required for expense_nonstocks[%d]", i))
}
}
}
} else {
return fiber.NewError(fiber.StatusBadRequest, "Field expense_nonstocks is required")
}
result, err := u.ExpenseService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create expense successfully",
Data: result,
})
}
func (u *ExpenseController) UpdateOne(c *fiber.Ctx) error {
req := new(validation.Update)
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
form, err := c.MultipartForm()
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form")
}
req.Documents = form.File["documents"]
if transactionDate := c.FormValue("transaction_date"); transactionDate != "" {
req.TransactionDate = &transactionDate
}
categoryVal := c.FormValue("category")
req.Category = &categoryVal
supplierIDVal := c.FormValue("supplier_id")
if supplierIDVal != "" {
supplierID, err := strconv.ParseUint(supplierIDVal, 10, 64)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid supplier_id format")
}
req.SupplierID = &supplierID
}
expenseNonstocksJSON := c.FormValue("expense_nonstocks")
if expenseNonstocksJSON != "" {
var expenseNonstocks []validation.ExpenseNonstock
if err := json.Unmarshal([]byte(expenseNonstocksJSON), &expenseNonstocks); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid expense_nonstocks JSON: %v", err))
}
for i, expenseNonstock := range expenseNonstocks {
if expenseNonstock.KandangID == 0 {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Field KandangID is required for expense_nonstocks[%d]", i))
}
}
req.ExpenseNonstocks = &expenseNonstocks
}
result, err := u.ExpenseService.UpdateOne(c, req, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Update expense successfully",
Data: result,
})
}
func (u *ExpenseController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.ExpenseService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete expense successfully",
})
}
func (u *ExpenseController) Approval(c *fiber.Ctx) error {
req := new(validation.ApprovalRequest)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
path := c.Path()
approvalType := ""
if strings.Contains(path, "/approvals/manager") {
approvalType = "manager"
} else if strings.Contains(path, "/approvals/finance") {
approvalType = "finance"
} else {
return fiber.NewError(fiber.StatusBadRequest, "Invalid approval path")
}
results, err := u.ExpenseService.Approval(c, req, approvalType)
if err != nil {
return err
}
var (
data interface{}
message = "Submit expense approval successfully"
)
if len(results) == 1 {
data = results[0]
} else {
message = "Submit expense approvals successfully"
data = results
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: message,
Data: data,
})
}
func (u *ExpenseController) CreateRealization(c *fiber.Ctx) error {
expenseID := c.Params("id")
id, err := strconv.Atoi(expenseID)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid expense ID")
}
req := new(validation.CreateRealization)
form, err := c.MultipartForm()
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form")
}
req.Documents = form.File["documents"]
req.RealizationDate = c.FormValue("realization_date")
realizationsJSON := c.FormValue("realizations")
if realizationsJSON != "" {
if err := json.Unmarshal([]byte(realizationsJSON), &req.Realizations); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid realizations JSON: %v", err))
}
}
expense, err := u.ExpenseService.CreateRealization(c, uint(id), req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create realization successfully",
Data: expense,
})
}
func (u *ExpenseController) UpdateRealization(c *fiber.Ctx) error {
expenseID := c.Params("id")
id, err := strconv.Atoi(expenseID)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid expense ID")
}
var req validation.UpdateRealization
form, err := c.MultipartForm()
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form")
}
req.Documents = form.File["documents"]
req.RealizationDate = c.FormValue("realization_date")
realizationsJSON := c.FormValue("realizations")
if realizationsJSON != "" {
if err := json.Unmarshal([]byte(realizationsJSON), &req.Realizations); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid realizations JSON: %v", err))
}
}
expense, err := u.ExpenseService.UpdateRealization(c, uint(id), &req)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Update realization successfully",
Data: expense,
})
}
func (u *ExpenseController) CompleteExpense(c *fiber.Ctx) error {
expenseID := c.Params("id")
id, err := strconv.Atoi(expenseID)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid expense ID")
}
expense, err := u.ExpenseService.CompleteExpense(c, uint(id), nil)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Complete expense successfully",
Data: expense,
})
}
func (u *ExpenseController) DeleteDocument(c *fiber.Ctx) error {
expenseID, err := strconv.Atoi(c.Params("id"))
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid expense ID")
}
documentID, err := strconv.ParseUint(c.Params("documentId"), 10, 64)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid document ID")
}
if err := u.ExpenseService.DeleteDocument(c, uint(expenseID), documentID, false); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete document successfully",
})
}
func (u *ExpenseController) DeleteRealizationDocument(c *fiber.Ctx) error {
expenseID, err := strconv.Atoi(c.Params("id"))
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid expense ID")
}
documentID, err := strconv.ParseUint(c.Params("documentId"), 10, 64)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid document ID")
}
if err := u.ExpenseService.DeleteDocument(c, uint(expenseID), documentID, true); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete realization document successfully",
})
}
@@ -0,0 +1,328 @@
package dto
import (
"encoding/json"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
nonstockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/dto"
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type ExpenseRelationDTO struct {
Id uint64 `json:"id"`
PoNumber string `json:"po_number"`
TransactionDate time.Time `json:"transaction_date"`
}
type ExpenseBaseDTO struct {
Id uint64 `json:"id"`
ReferenceNumber string `json:"reference_number"`
PoNumber string `json:"po_number"`
Category string `json:"category"`
Supplier *supplierDTO.SupplierRelationDTO `json:"supplier,omitempty"`
RealizationDate *time.Time `json:"realization_date,omitempty"`
TransactionDate time.Time `json:"transaction_date"`
Location *locationDTO.LocationRelationDTO `json:"location,omitempty"`
}
type ExpenseListDTO struct {
ExpenseBaseDTO
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval,omitempty"`
}
type ExpenseDetailDTO struct {
ExpenseBaseDTO
Documents []DocumentDTO `json:"documents,omitempty"`
RealizationDocs []DocumentDTO `json:"realization_docs,omitempty"`
Kandangs []KandangGroupDTO `json:"kandangs,omitempty"`
TotalPengajuan float64 `json:"total_pengajuan"`
TotalRealisasi float64 `json:"total_realisasi"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval,omitempty"`
}
type ExpenseNonstockDTO struct {
Id uint64 `json:"id"`
ExpenseId *uint64 `json:"expense_id,omitempty"`
ProjectFlockKandangId *uint64 `json:"project_flock_kandang_id,omitempty"`
KandangId *uint64 `json:"kandang_id,omitempty"`
NonstockId *uint64 `json:"nonstock_id,omitempty"`
Qty float64 `json:"qty"`
Price float64 `json:"price"`
Notes string `json:"notes"`
Nonstock *nonstockDTO.NonstockRelationDTO `json:"nonstock,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
type ExpenseRealizationDTO struct {
Id uint64 `json:"id"`
ExpenseNonstockId *uint64 `json:"expense_nonstock_id,omitempty"`
Qty float64 `json:"qty"`
Price float64 `json:"price"`
Notes string `json:"notes"`
Nonstock *nonstockDTO.NonstockRelationDTO `json:"nonstock,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
type KandangGroupDTO struct {
Id uint64 `json:"id"`
KandangId uint64 `json:"kandang_id"`
Name string `json:"name,omitempty"`
Pengajuans []ExpenseNonstockDTO `json:"pengajuans,omitempty"`
Realisasi []ExpenseRealizationDTO `json:"realisasi,omitempty"`
}
type DocumentDTO struct {
ID uint64 `json:"id"`
Path string `json:"path"`
}
// === MAPPERS ===
func ToExpenseRelationDTO(e entity.Expense) ExpenseRelationDTO {
return ExpenseRelationDTO{
Id: e.Id,
PoNumber: e.PoNumber,
TransactionDate: e.TransactionDate,
}
}
func ToExpenseBaseDTO(e *entity.Expense) ExpenseBaseDTO {
var location *locationDTO.LocationRelationDTO
var supplier *supplierDTO.SupplierRelationDTO
var realizationDate *time.Time
if !e.RealizationDate.IsZero() {
realizationDate = &e.RealizationDate
}
if len(e.Nonstocks) > 0 && e.Nonstocks[0].Kandang != nil {
if e.Nonstocks[0].Kandang.Location.Id != 0 {
mapped := locationDTO.ToLocationRelationDTO(e.Nonstocks[0].Kandang.Location)
location = &mapped
}
}
if e.Supplier != nil && e.Supplier.Id != 0 {
mapped := supplierDTO.ToSupplierRelationDTO(*e.Supplier)
supplier = &mapped
}
return ExpenseBaseDTO{
Id: e.Id,
ReferenceNumber: e.ReferenceNumber,
PoNumber: e.PoNumber,
Category: e.Category,
Supplier: supplier,
RealizationDate: realizationDate,
TransactionDate: e.TransactionDate,
Location: location,
}
}
func ToExpenseListDTO(e entity.Expense) ExpenseListDTO {
var createdUser *userDTO.UserRelationDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserRelationDTO(*e.CreatedUser)
createdUser = &mapped
}
var latestApproval *approvalDTO.ApprovalRelationDTO
if e.LatestApproval != nil {
mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval)
latestApproval = &mapped
}
return ExpenseListDTO{
ExpenseBaseDTO: ToExpenseBaseDTO(&e),
CreatedUser: createdUser,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
LatestApproval: latestApproval,
}
}
func ToExpenseListDTOs(expenses []entity.Expense) []ExpenseListDTO {
result := make([]ExpenseListDTO, len(expenses))
for i, expense := range expenses {
result[i] = ToExpenseListDTO(expense)
}
return result
}
func ToExpenseDetailDTO(e *entity.Expense) ExpenseDetailDTO {
var documents []DocumentDTO
var realizationDocs []DocumentDTO
var createdUser *userDTO.UserRelationDTO
if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserRelationDTO(*e.CreatedUser)
createdUser = &mapped
}
var latestApproval *approvalDTO.ApprovalRelationDTO
if e.LatestApproval != nil {
mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval)
latestApproval = &mapped
}
var pengajuans []ExpenseNonstockDTO
var realisasi []ExpenseRealizationDTO
if e.DocumentPath.Valid && e.DocumentPath.String != "" {
json.Unmarshal([]byte(e.DocumentPath.String), &documents)
}
if e.RealizationDocumentPath.Valid && e.RealizationDocumentPath.String != "" {
json.Unmarshal([]byte(e.RealizationDocumentPath.String), &realizationDocs)
}
if len(e.Nonstocks) > 0 {
pengajuans = make([]ExpenseNonstockDTO, 0)
realisasi = make([]ExpenseRealizationDTO, 0)
for _, ns := range e.Nonstocks {
pengajuanDTO := ToExpenseNonstockDTO(ns)
pengajuans = append(pengajuans, pengajuanDTO)
if ns.Realization != nil {
var nonstock *nonstockDTO.NonstockRelationDTO
if ns.Nonstock != nil && ns.Nonstock.Id != 0 {
mapped := nonstockDTO.ToNonstockRelationDTO(*ns.Nonstock)
nonstock = &mapped
}
realisasiDTO := ExpenseRealizationDTO{
Id: ns.Realization.Id,
ExpenseNonstockId: ns.Realization.ExpenseNonstockId,
Qty: ns.Realization.Qty,
Price: ns.Realization.Price,
Notes: ns.Realization.Notes,
Nonstock: nonstock,
CreatedAt: ns.Realization.CreatedAt,
}
realisasi = append(realisasi, realisasiDTO)
}
}
}
var totalPengajuan float64
for _, p := range pengajuans {
totalPengajuan += p.Qty * p.Price
}
var totalRealisasi float64
for _, r := range realisasi {
totalRealisasi += r.Qty * r.Price
}
kandangs := ToKandangGroupDTO(pengajuans, realisasi, e.Nonstocks)
return ExpenseDetailDTO{
ExpenseBaseDTO: ToExpenseBaseDTO(e),
Documents: documents,
RealizationDocs: realizationDocs,
CreatedUser: createdUser,
Kandangs: kandangs,
TotalPengajuan: totalPengajuan,
TotalRealisasi: totalRealisasi,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
LatestApproval: latestApproval,
}
}
func ToExpenseNonstockDTO(ns entity.ExpenseNonstock) ExpenseNonstockDTO {
var nonstock *nonstockDTO.NonstockRelationDTO
if ns.Nonstock != nil && ns.Nonstock.Id != 0 {
mapped := nonstockDTO.ToNonstockRelationDTO(*ns.Nonstock)
nonstock = &mapped
}
return ExpenseNonstockDTO{
Id: ns.Id,
ExpenseId: ns.ExpenseId,
ProjectFlockKandangId: ns.ProjectFlockKandangId,
KandangId: ns.KandangId,
NonstockId: ns.NonstockId,
Qty: ns.Qty,
Price: ns.Price,
Notes: ns.Notes,
Nonstock: nonstock,
CreatedAt: ns.CreatedAt,
}
}
func ToKandangGroupDTO(pengajuans []ExpenseNonstockDTO, realisasi []ExpenseRealizationDTO, nonstocks []entity.ExpenseNonstock) []KandangGroupDTO {
kandangMap := make(map[uint64]*KandangGroupDTO)
for _, p := range pengajuans {
var kandangId uint64
var kandangName string
if p.KandangId != nil {
kandangId = *p.KandangId
for _, ns := range nonstocks {
if ns.Id == p.Id && ns.Kandang != nil {
kandangName = ns.Kandang.Name
break
}
}
}
if kandangId > 0 {
if kandangMap[kandangId] == nil {
kandangMap[kandangId] = &KandangGroupDTO{
Id: kandangId,
KandangId: kandangId,
Name: kandangName,
Pengajuans: []ExpenseNonstockDTO{},
Realisasi: []ExpenseRealizationDTO{},
}
}
kandangMap[kandangId].Pengajuans = append(kandangMap[kandangId].Pengajuans, p)
}
}
for _, r := range realisasi {
var kandangId uint64
var kandangName string
for _, ns := range nonstocks {
if ns.Realization != nil && ns.Realization.Id == r.Id && ns.Kandang != nil {
kandangId = uint64(ns.Kandang.Id)
kandangName = ns.Kandang.Name
break
}
}
if kandangId > 0 {
if kandangMap[kandangId] == nil {
kandangMap[kandangId] = &KandangGroupDTO{
Id: kandangId,
KandangId: kandangId,
Name: kandangName,
Pengajuans: []ExpenseNonstockDTO{},
Realisasi: []ExpenseRealizationDTO{},
}
}
kandangMap[kandangId].Realisasi = append(kandangMap[kandangId].Realisasi, r)
}
}
var kandangs []KandangGroupDTO
for _, k := range kandangMap {
kandangs = append(kandangs, *k)
}
return kandangs
}
+47
View File
@@ -0,0 +1,47 @@
package expenses
import (
"fmt"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
rExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
sExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
rNonstock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/repositories"
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
)
type ExpenseModule struct{}
func (ExpenseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
expenseRepo := rExpense.NewExpenseRepository(db)
userRepo := rUser.NewUserRepository(db)
supplierRepo := rSupplier.NewSupplierRepository(db)
nonstockRepo := rNonstock.NewNonstockRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db)
realizationRepo := rExpense.NewExpenseRealizationRepository(db)
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
approvalSvc := commonSvc.NewApprovalService(approvalRepo)
// Register workflow steps for EXPENSES approval
if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowExpense, utils.ExpenseApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register expense approval workflow: %v", err))
}
expenseService := sExpense.NewExpenseService(expenseRepo, supplierRepo, nonstockRepo, approvalSvc, realizationRepo, projectFlockKandangRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
ExpenseRoutes(router, userService, expenseService)
}
@@ -0,0 +1,51 @@
package repository
import (
"context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type ExpenseRepository interface {
repository.BaseRepository[entity.Expense]
IdExists(ctx context.Context, id uint) (bool, error)
GetNextSequence(ctx context.Context) (int, error)
GetWithSupplier(ctx context.Context, id uint64) (*entity.Expense, error)
}
type ExpenseRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Expense]
}
func NewExpenseRepository(db *gorm.DB) ExpenseRepository {
return &ExpenseRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Expense](db),
}
}
func (r *ExpenseRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.Expense](ctx, r.DB(), id)
}
func (r *ExpenseRepositoryImpl) GetNextSequence(ctx context.Context) (int, error) {
var sequence int
err := r.DB().Raw("SELECT nextval('expenses_ref_seq')").Scan(&sequence).Error
if err != nil {
return 0, err
}
return sequence, nil
}
func (r *ExpenseRepositoryImpl) GetWithSupplier(ctx context.Context, id uint64) (*entity.Expense, error) {
var expense entity.Expense
err := r.DB().WithContext(ctx).
Where("id = ?", id).
Preload("Supplier").
First(&expense).Error
if err != nil {
return nil, err
}
return &expense, nil
}
@@ -0,0 +1,55 @@
package repository
import (
"context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type ExpenseNonstockRepository interface {
repository.BaseRepository[entity.ExpenseNonstock]
IdExists(ctx context.Context, id uint64) (bool, error)
GetByExpenseID(ctx context.Context, expenseID uint64, id uint64) (bool, error)
GetWithRelations(ctx context.Context, id uint64) (*entity.ExpenseNonstock, error)
}
type ExpenseNonstockRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.ExpenseNonstock]
}
func NewExpenseNonstockRepository(db *gorm.DB) ExpenseNonstockRepository {
return &ExpenseNonstockRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.ExpenseNonstock](db),
}
}
func (r *ExpenseNonstockRepositoryImpl) IdExists(ctx context.Context, id uint64) (bool, error) {
return repository.Exists[entity.ExpenseNonstock](ctx, r.DB(), uint(id))
}
func (r *ExpenseNonstockRepositoryImpl) GetByExpenseID(ctx context.Context, expenseID uint64, id uint64) (bool, error) {
var count int64
err := r.DB().WithContext(ctx).Model(&entity.ExpenseNonstock{}).
Where("id = ? AND expense_id = ?", id, expenseID).
Count(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}
func (r *ExpenseNonstockRepositoryImpl) GetWithRelations(ctx context.Context, id uint64) (*entity.ExpenseNonstock, error) {
var expenseNonstock entity.ExpenseNonstock
err := r.DB().WithContext(ctx).
Where("id = ?", id).
Preload("Nonstock", func(db *gorm.DB) *gorm.DB {
return db.Preload("Suppliers")
}).
First(&expenseNonstock).Error
if err != nil {
return nil, err
}
return &expenseNonstock, nil
}

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