Compare commits

..

222 Commits

Author SHA1 Message Date
Hafizh A. Y. 2d098cb6b1 Merge branch 'feat/BE/US-281-uniformity' into 'feat/BE/Sprint-8'
feat(BE-281): uniformity and create project flock triger... unfinished s3 read

See merge request mbugroup/lti-api!121
2025-12-31 02:20:01 +00:00
ragilap 709e304f7f feat(BE-281): adjustment bug erorr 500 if 404 record projectflock 2025-12-31 07:39:20 +07:00
ragilap d994cfdce7 productstock 2025-12-31 07:08:03 +07:00
Hafizh A. Y. d3bb00a06a Merge branch 'fix/nonstock-undefined-field' into 'development'
fix(be): nonstock response supplier null to empty array

See merge request mbugroup/lti-api!120
2025-12-30 23:57:54 +00:00
Hafizh A. Y 5302713811 fix(be): nonstock response supplier null to empty array 2025-12-31 06:52:38 +07:00
Hafizh A. Y. f698ca070c Merge branch 'fix/nonstock-undefined-field' into 'development'
fix(be): remove omitempty in dto and validation nonstock

See merge request mbugroup/lti-api!118
2025-12-30 23:44:27 +00:00
Hafizh A. Y 6c42119f4d fix(be): remove omitempty in dto and validation nonstock 2025-12-31 06:43:34 +07:00
Hafizh A. Y. 299c8c7177 Merge branch 'fix/setup-seeder' into 'development'
fix: setup seeder for development

See merge request mbugroup/lti-api!117
2025-12-30 16:54:23 +00:00
Hafizh A. Y 78359db880 fix: setup seeder for development 2025-12-30 23:52:37 +07:00
Hafizh A. Y. 10799cc1ed Merge branch 'feat/BE/Sprint-8' into 'development'
[FEAT/BE] Partial Merge #3

See merge request mbugroup/lti-api!116
2025-12-30 12:06:19 +00:00
Hafizh A. Y. c9c581ef30 Merge branch 'sprint-8/gio' into 'feat/BE/Sprint-8'
fix api get all closing; fix get closing sapronak; fix get all maste data product

See merge request mbugroup/lti-api!115
2025-12-30 09:36:48 +00:00
Hafizh A. Y. 6ee795cf2a Merge branch 'dev/teguh' into 'feat/BE/Sprint-8'
feat(BE US#386): add standard_fcr column to production_standard_details and update existing API

See merge request mbugroup/lti-api!114
2025-12-30 09:36:00 +00:00
aguhh18 471fd1dbbf feat(BE): enhance product warehouse handling and automatic calculations for delivery and sales orders 2025-12-30 16:30:44 +07:00
ragilap 4e5caa8cba feat(BE-281): add rbac for uniformity 2025-12-30 15:23:34 +07:00
MacBook Air M1 0285852c42 fix api get all closing; fix get closing sapronak; fix get all maste data product 2025-12-30 14:42:53 +07:00
ragilap 0c776e8332 feat(BE-281): uncoment auth 2025-12-30 12:08:49 +07:00
ragilap 90125ffe1a feat(BE-281):add dto standart mean bw and uniformity 2025-12-30 12:07:28 +07:00
aguhh18 c36719cc1a Merge branch 'feat/BE/Sprint-8' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2025-12-30 10:42:04 +07:00
aguhh18 e4acd9a21e feat(BE): add standard_fcr column to production_standard_details and update related services and validations 2025-12-30 10:27:12 +07:00
ragilap 9a094b8bfe feat(BE-281):fix document payload 2025-12-30 09:56:48 +07:00
Hafizh A. Y. ddda696454 Merge branch 'fix/BE/US-74-add_production_standart_project_flock' into 'feat/BE/Sprint-8'
feat(BE-74): add production standart to project_flock and implement rbac...

See merge request mbugroup/lti-api!113
2025-12-29 16:22:29 +00:00
ragilap 635049163e feat(BE-74): add production standart to project_flock and implement rbac finance and standart production 2025-12-29 23:15:34 +07:00
Hafizh A. Y. 49af2d6448 Merge branch 'feat/BE/Sprint-8' into 'development'
Feat[BE]: Partial Merge

See merge request mbugroup/lti-api!112
2025-12-29 14:39:58 +00:00
Hafizh A. Y. 68703d8752 Merge branch 'dev/teguh' into 'feat/BE/Sprint-8'
feat(BE): expense(adjust expense add option attach to farm and not to kandang ).

See merge request mbugroup/lti-api!111
2025-12-29 14:39:05 +00:00
Hafizh A. Y. f19a3cb76e Merge branch 'dev/hafizh' into 'feat/BE/Sprint-8'
feat(BE): finance (payment, initial_balance, injection). fix(BE): kandang capacity

See merge request mbugroup/lti-api!110
2025-12-29 14:37:42 +00:00
Hafizh A. Y. d1ba13de76 Merge branch 'feat/BE/Sprint-8' into 'dev/hafizh'
# Conflicts:
#   internal/route/route.go
#   internal/utils/constant.go
2025-12-29 14:37:02 +00:00
ragilap 6523290aaf feat(BE-281): change template excel 2025-12-29 19:44:10 +07:00
ragilap a2066979c1 feat(BE-281): adjustment uniformity for make unique for week,projectflockandang, and date 2025-12-29 19:04:10 +07:00
Hafizh A. Y e30ef5ef10 feat(BE): finance (payment, initial_balance, injection). fix(BE): kandang capacity 2025-12-29 15:48:08 +07:00
aguhh18 bb76d27f25 feat[BE#US386]: add production standards module with CRUD operations
- Created database migration for production standards and related tables.
- Implemented entities for ProductionStandard, ProductionStandardDetail, and StandardGrowthDetail.
- Developed controller for handling production standard requests.
- Added DTOs for data transfer between layers.
- Implemented service layer for business logic related to production standards.
- Created repository interfaces and implementations for data access.
- Added validation for production standard requests.
- Registered routes for production standards in the main application.
2025-12-29 15:47:37 +07:00
aguhh18 dbb13da7c4 Feat[BE]: add migration scripts for product warehouse ID management and create production standards tables with constraints and indexes 2025-12-29 15:47:05 +07:00
aguhh18 ac8536a4a1 Feat[BE]: implement FIFO stock management for marketing delivery products, including migration scripts and updates to related services and DTOs 2025-12-29 15:47:05 +07:00
aguhh18 96c2917834 Feat[BE]: add migration scripts to manage document columns in expenses table for Document service integration 2025-12-29 15:47:05 +07:00
aguhh18 c3302397cc Feat[BE]: integrate document service into expense module and update related DTOs for document handling 2025-12-29 15:47:05 +07:00
aguhh18 c7ae836cf0 Feat[BE]: refactor stock log handling and introduce new log types for adjustments and transfers 2025-12-29 15:47:05 +07:00
aguhh18 20f8a45823 Feat[BE]: update update dto for transfer document 2025-12-29 15:47:05 +07:00
aguhh18 67ddd8e667 Feat[BE]: enhance chickin stock management with FIFO service integration and fix key naming inconsistencies 2025-12-29 15:47:03 +07:00
aguhh18 ebf0f8c5ab Feat[BE]: refactor document handling in transfer service and introduce document type constants 2025-12-29 15:31:57 +07:00
aguhh18 7dc5c9e9a5 Feat[BE]: add document handling to stock transfer process 2025-12-29 15:26:38 +07:00
aguhh18 306cf11fee Feat[BE]: integrate FIFO service for chickin stock management 2025-12-29 15:26:38 +07:00
aguhh18 9ee3b7582c Feat[BE]: on chickin laying covert Pullet to Layer 2025-12-29 15:26:38 +07:00
ragilap 8dfb224614 feat(BE-281): changes std deviasi first 100 data to all 2025-12-29 10:13:29 +07:00
aguhh18 db4e8232b9 feat(BE): enhance closing service and repository with actual usage cost calculations and egg weight tracking 2025-12-29 08:03:00 +07:00
ragilap 644896edfa feat(BE-281): unfinished uniformity and create project flock triger productwarehouse and add new filtering lookup 2025-12-29 00:21:26 +07:00
aguhh18 d945fcd19c Merge branch 'dev/gio' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2025-12-28 19:16:53 +07:00
aguhh18 812db3f79e feat(BE): integrate FIFO service for stock adjustments and transfers
- Added FIFO service integration in the adjustments module to manage stockable and usable items for adjustments.
- Created a new repository for adjustment stocks to handle database operations.
- Enhanced the adjustment service to track stock adjustments using FIFO logic for both increase and decrease operations.
- Updated product warehouse DTOs and repositories to include project flock information.
- Implemented FIFO logic in the transfer module to manage stock transfers between warehouses.
- Added integration tests for FIFO operations in stock transfers, ensuring correct stock consumption and replenishment.
2025-12-28 19:15:41 +07:00
MacBook Air M1 10f42ed9c4 feat[BE-378]:Create API Get All HPP Harian Kandang 2025-12-28 18:41:46 +07:00
aguhh18 a0d2c1c7dd feat[BE]: enhance marketing module by adding ProductWarehouseId to marketing delivery product creation 2025-12-28 10:40:20 +07:00
aguhh18 56811f7c5b feat[BE]: integrate kandang repository into expense bridge for enhanced expense management 2025-12-28 08:57:35 +07:00
aguhh18 647bfbb667 Merge branch 'feat/BE/Sprint-8' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2025-12-28 08:20:32 +07:00
aguhh18 ec6da57510 feat[BE]: enhance expense management with location and project flock integration, including updates to migrations, entities, services, and validations 2025-12-28 08:13:50 +07:00
Hafizh A. Y. cdfa77566c Merge branch 'dev/teguh' into 'feat/BE/Sprint-8'
Feat[BE US# master data]: create standard production master data and adjust fifo stock module and document module on some main module

See merge request mbugroup/lti-api!109
2025-12-27 07:40:55 +00:00
Hafizh A. Y 1c875a916b feat(BE): finance (payment, initial_balance, injection). fix(BE): kandang capacity 2025-12-27 14:30:03 +07:00
aguhh18 85dc0ecd13 Merge branch 'feat/BE/Sprint-8' of https://gitlab.com/mbugroup/lti-api into HEAD 2025-12-27 11:59:10 +07:00
aguhh18 c9633d1308 feat[BE#US386]: add production standards module with CRUD operations
- Created database migration for production standards and related tables.
- Implemented entities for ProductionStandard, ProductionStandardDetail, and StandardGrowthDetail.
- Developed controller for handling production standard requests.
- Added DTOs for data transfer between layers.
- Implemented service layer for business logic related to production standards.
- Created repository interfaces and implementations for data access.
- Added validation for production standard requests.
- Registered routes for production standards in the main application.
2025-12-27 09:02:16 +07:00
aguhh18 b156e06cee Feat[BE]: add migration scripts for product warehouse ID management and create production standards tables with constraints and indexes 2025-12-26 23:36:53 +07:00
aguhh18 cd14de4dd2 Feat[BE]: implement FIFO stock management for marketing delivery products, including migration scripts and updates to related services and DTOs 2025-12-26 19:02:50 +07:00
aguhh18 54487b0fcf Feat[BE]: add migration scripts to manage document columns in expenses table for Document service integration 2025-12-26 11:21:23 +07:00
aguhh18 a9037991ef Feat[BE]: integrate document service into expense module and update related DTOs for document handling 2025-12-26 11:20:57 +07:00
aguhh18 12e5706318 Feat[BE]: refactor stock log handling and introduce new log types for adjustments and transfers 2025-12-26 09:19:39 +07:00
aguhh18 3e575d96a7 Feat[BE]: update update dto for transfer document 2025-12-24 10:42:27 +07:00
Adnan Zahir 98a34a1640 Merge branch 'feat/BE/Sprint-7' into 'development'
[FEAT/BE][Sprint #7] Reporting, Report Closing, and Adjustment

See merge request mbugroup/lti-api!107
2025-12-24 10:08:35 +07:00
aguhh18 c643e66282 Merge branch 'feat/BE/Sprint-7' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2025-12-24 09:25:14 +07:00
aguhh18 9c3d0a44a6 Feat[BE]: enhance chickin stock management with FIFO service integration and fix key naming inconsistencies 2025-12-24 09:24:32 +07:00
aguhh18 e935843cba Feat[BE]: refactor document handling in transfer service and introduce document type constants 2025-12-23 17:51:42 +07:00
Hafizh A. Y. e33b23a2aa Merge branch 'feat/BE/US-304/permission-middleware-adjustment' into 'feat/BE/Sprint-7'
[FIX/BE][US#304]: add refresh token and adjustment permission

See merge request mbugroup/lti-api!106
2025-12-23 07:50:11 +00:00
aguhh18 c55fdb75a7 Feat[BE]: add document handling to stock transfer process 2025-12-23 14:10:08 +07:00
ragilap 3a27917afc Merge branch 'feat/BE/Sprint-7' of https://gitlab.com/mbugroup/lti-api into feat/BE/US-304/permission-middleware-adjustment 2025-12-23 14:07:41 +07:00
Hafizh A. Y. c0132e5880 Merge branch 'dev/gio' into 'feat/BE/Sprint-7'
rename api closing data produksi

See merge request mbugroup/lti-api!105
2025-12-23 06:55:15 +00:00
aguhh18 3d13cd966a Feat[BE]: integrate FIFO service for chickin stock management 2025-12-23 12:26:35 +07:00
ragilap b41bb79125 Fix(BE-304):uncomment auth 2025-12-23 11:50:45 +07:00
ragilap a2b8ebe665 Fix(BE-278):fixing total price in purchase 2025-12-23 11:50:00 +07:00
ragilap 2d8f20b70e Fix(BE-304):add refresh token and adjustment permission 2025-12-23 08:57:41 +07:00
MacBook Air M1 824eb5905f resolve conflict to sprint 7 2025-12-22 15:22:12 +07:00
MacBook Air M1 817b6f82d0 rename api closing data produksi 2025-12-22 15:15:42 +07:00
aguhh18 cbd3047a17 Feat[BE]: on chickin laying covert Pullet to Layer 2025-12-22 13:51:27 +07:00
Hafizh A. Y. ff4b4afcca Merge branch 'feat/BE/US-304/permission-middleware-adjustment' into 'feat/BE/Sprint-7'
[FEAT/BE][US#304]: permission middleware adjustment

See merge request mbugroup/lti-api!104
2025-12-22 03:21:37 +00:00
Hafizh A. Y. 240cd72204 Merge branch 'dev/gio' into 'feat/BE/Sprint-7'
adjust age closing data produksi

See merge request mbugroup/lti-api!103
2025-12-22 03:20:17 +00:00
Hafizh A. Y. eae69a08fc Merge branch 'dev/teguh' into 'feat/BE/Sprint-7'
[FEAT/BE][US#333,336,338,340]: complete get closing penjualan, get closing keuangan, repport expense, and repport marketing

See merge request mbugroup/lti-api!101
2025-12-22 03:19:38 +00:00
ragilap 17be6abc49 Merge branch 'feat/BE/Sprint-7' of https://gitlab.com/mbugroup/lti-api into feat/BE/US-304/permission-middleware-adjustment 2025-12-22 10:04:25 +07:00
ragilap ef117e66d1 add permission deliveryorder and sales order 2025-12-22 10:03:32 +07:00
aguhh18 4dfb988994 Merge branch 'feat/BE/Sprint-7' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2025-12-22 08:27:40 +07:00
MacBook Air M1 dc726c49cf adjust age closing data produksi 2025-12-21 13:03:32 +07:00
Hafizh A. Y. a82df468d2 Merge branch 'feat/BE/US-304/permission-middleware-adjustment' into 'feat/BE/Sprint-7'
[FEAT/BE][US#304/TASK-307,306]: adjustment middleware check if user have permission,create all permission in modules lti

See merge request mbugroup/lti-api!102
2025-12-19 10:27:25 +00:00
ragilap 1af8f0a726 Feat(BE-304): add permission in report and closing 2025-12-19 15:55:30 +07:00
ragilap 63068b8c3e Merge branch 'feat/BE/Sprint-7' of https://gitlab.com/mbugroup/lti-api into feat/BE/US-304/permission-middleware-adjustment 2025-12-19 14:56:33 +07:00
Hafizh A. Y. 5461c8b0ce Merge branch 'feat/BE/US-334-Report-closing-hpp-expedisi' into 'feat/BE/Sprint-7'
[FEAT/BE][US#334] report closing hpp expedisi

See merge request mbugroup/lti-api!100
2025-12-19 07:51:03 +00:00
ragilap 5dc5f4c589 Merge branch 'feat/BE/Sprint-7' of https://gitlab.com/mbugroup/lti-api into feat/BE/US-334-Report-closing-hpp-expedisi 2025-12-19 14:43:45 +07:00
ragilap ab9c7c216a Feat(BE-304): add permission in report and closing 2025-12-19 14:37:54 +07:00
Hafizh A. Y. faa0861451 Merge branch 'dev/gio' into 'feat/BE/Sprint-7'
feat[BE-375]: add api get one closing data produksi

See merge request mbugroup/lti-api!99
2025-12-19 07:23:44 +00:00
Hafizh A. Y. 2eade07f0a Merge branch 'feat/BE/US-339-reporting-pembelian-per-supplier' into 'feat/BE/Sprint-7'
Feat/be/us 339 reporting pembelian per supplier

See merge request mbugroup/lti-api!98
2025-12-19 07:22:21 +00:00
ragilap dbb9db960f Merge branch 'feat/BE/Sprint-7' of https://gitlab.com/mbugroup/lti-api into feat/BE/US-304/permission-middleware-adjustment 2025-12-19 14:19:40 +07:00
aguhh18 fa6d82b79a feat[BE-384]: enhance closing reports by introducing calculation context and improving data handling; refactor related functions for better clarity and maintainability 2025-12-19 08:30:05 +07:00
MacBook Air M1 207382b3b0 fix get all inventory product stock 2025-12-19 07:05:11 +07:00
aguhh18 e551995c66 feat[BE-384]: enhance reporting by adding chickin quantity and egg production weight calculations; refactor HPP calculations to consider product categories 2025-12-18 17:56:18 +07:00
ragilap cb076d92ac Feat(BE-339):Fixing dto reporting per supplier, and adjust limit 2025-12-18 16:41:56 +07:00
ragilap f5c80fa560 Feat(BE-339):Fixing dto reporting per supplier 2025-12-18 16:21:46 +07:00
ragilap 14a4d9e944 Feat(BE-334):Fixing dto closing hpp expedisi 2025-12-18 16:02:57 +07:00
MacBook Air M1 84da0c27e0 merge sprint 7 and resolve conflict 2025-12-18 15:33:06 +07:00
MacBook Air M1 047162699e adjust response api closing data produksi 2025-12-18 15:25:15 +07:00
aguhh18 c95f90f0b9 Refactor[BE]: refactor expense category handling to use constants for BOP and NON-BOP 2025-12-18 15:03:37 +07:00
aguhh18 9e0b4be4dd feat[BE]: add flags to product seeds for better categorization 2025-12-18 14:52:51 +07:00
aguhh18 f2df7f4847 feat[BE]: add overhead and ekspedisi items to profit loss report; include total depletion in closing report calculation 2025-12-18 14:49:48 +07:00
MacBook Air M1 d675b1e826 feat[BE-375]: get api closing data produksi 2025-12-18 13:32:48 +07:00
ragilap e52a02b1c0 Feat(BE-339): make reporting purchase per supplier with filterization 2025-12-18 11:30:55 +07:00
aguhh18 096a446450 feat[BE]: update HPP calculations to use totalWeightProduced and totalActualPopulation 2025-12-18 10:45:04 +07:00
aguhh18 1b23861656 feat[BE]: membetulkan perhitungan hpp di module penjualan harian 2025-12-18 09:58:31 +07:00
aguhh18 a7069a2e50 Merge branch 'dev/gio' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2025-12-18 09:41:51 +07:00
ragilap 3bfc401206 Feat(BE-334): make reporting closing hpp for project_flock_kandang 2025-12-17 13:56:51 +07:00
MacBook Air M1 21d22c20a3 add constant flag 2025-12-17 13:20:00 +07:00
aguhh18 d9a1372077 feat[BE]:: add totalHppPricePerKg to marketing report summary 2025-12-17 11:34:08 +07:00
aguhh18 40f192660d Feat[BE]:: adjust marketing report API 2025-12-17 11:30:49 +07:00
aguhh18 afe4b2ffe3 feat[BE}: change get penjualan repport dto an add more params 2025-12-16 21:10:48 +07:00
ragilap eef254021c Merge branch 'feat/BE/Sprint-7' of https://gitlab.com/mbugroup/lti-api into feat/BE/US-334-Report-closing-hpp-expedisi 2025-12-16 14:49:53 +07:00
ragilap cd739f41b9 Feat(BE-339): make a report for purchasing supplier 2025-12-16 14:42:31 +07:00
Adnan Zahir 8f77031e02 Merge branch 'feat/BE/Sprint-6' into 'development'
[FEAT & FIX/BE] Closing Perhitungan Sapronak, Approval unclose issue, allocation issue, marketing report, lookup issue, etc.

See merge request mbugroup/lti-api!97
2025-12-16 14:05:41 +07:00
Hafizh A. Y. 062a7937e2 Merge branch 'feat/BE/US-284/Report-counting-sapronak' into 'feat/BE/Sprint-6'
Feat/be/us 284/report counting sapronak

See merge request mbugroup/lti-api!94
2025-12-16 04:15:43 +00:00
Hafizh A. Y. 4094d38d7b Merge branch 'dev/teguh' into 'feat/BE/Sprint-6'
[FIX/BE]: fixing stock adjustmetn get all, lookup project flock, and preload in project flock kandangs

See merge request mbugroup/lti-api!95
2025-12-16 04:14:32 +00:00
ragilap cf7b3418a5 fixing report-counting-sapronak 2025-12-16 10:44:19 +07:00
aguhh18 d5bc6838c8 FEAT[BE]: create marketing report API 2025-12-15 16:17:37 +07:00
aguhh18 efaeb89ca1 Fix[BE]: fix typo penamaan route 2025-12-15 13:39:02 +07:00
aguhh18 a0a143b8ac FEAT[BE} : adjust wrong response on get repport Expense 2025-12-15 09:18:26 +07:00
aguhh18 cbb3368141 FEAT[BE]: implement expense report retrieval with filtering options 2025-12-15 09:11:26 +07:00
ragilap fc49cef781 add counting hpp-expedition by project 2025-12-14 23:15:30 +07:00
aguhh18 c79e35c217 FIX[BE} fixing get all adjustment change respose json 2025-12-11 12:34:13 +07:00
ragilap f60564d673 fix projectflock approval with dto 2025-12-11 11:27:50 +07:00
kris b8425c0f58 Edit .air.toml 2025-12-11 04:06:51 +00:00
aguhh18 0de2021308 FIX[BE] : fix project flock kandang get all API 2025-12-11 09:42:32 +07:00
ragilap 3ada837b8b feat/BE/US-284/TASK-289-Create API (GET ONE in tab Perhitungan Sapronak),fix approval unclose issue,fix stock allocation issue 2025-12-11 09:38:20 +07:00
aguhh18 c062d838e0 Fix[BE]: fix 500 API Loookup project flock 2025-12-11 09:26:24 +07:00
ragilap 4ce7611c26 Merge branch 'feat/BE/Sprint-6' of https://gitlab.com/mbugroup/lti-api into feat/BE/US-284/Report-counting-sapronak 2025-12-11 09:05:20 +07:00
Adnan Zahir 2dd3e3e271 Merge branch 'feat/BE/Sprint-6' into 'development'
add approval projectflockkandang closed,expense must be done,stock must empty by flag  unfinished:need info approval fix

See merge request mbugroup/lti-api!91
2025-12-10 22:24:02 +07:00
Hafizh A. Y. e98d0a9fa1 Merge branch 'feat/BE/US-279/closing-produksi' into 'feat/BE/Sprint-6'
Feat/be/us 279/closing produksi

See merge request mbugroup/lti-api!93
2025-12-10 14:39:00 +00:00
ragilap 08c8c4a747 fix purchase due date and dto 2025-12-10 21:11:33 +07:00
ragilap de6304332b fix purchase due date 2025-12-10 21:10:46 +07:00
Hafizh A. Y. f073bcc2c1 Merge branch 'dev/gio' into 'feat/BE/Sprint-6'
fix migration down product warehouses

See merge request mbugroup/lti-api!92
2025-12-10 14:07:11 +00:00
giovanni-ce 4853891191 fix migration down product warehouses 2025-12-10 18:12:31 +07:00
Hafizh A. Y. 086184bbaa Merge branch 'feat/BE/US-279/closing-produksi' into 'feat/BE/Sprint-6'
[FEAT/BE][US#279,278]-restriction expense not finish and stock not used,add status project flock completed, fix dto purchase, fix dto nonstock supplier, purchase

See merge request mbugroup/lti-api!90
2025-12-10 10:13:32 +00:00
ragilap 4161dcfbdd change project flock change stepclosed to selesai 2025-12-10 17:13:05 +07:00
ragilap d0309f25dd uncomment auth 2025-12-10 17:09:01 +07:00
ragilap 59ebe29ec8 Merge branch 'feat/BE/Sprint-6' of https://gitlab.com/mbugroup/lti-api into feat/BE/US-279/closing-produksi 2025-12-10 17:03:40 +07:00
ragilap 2b6ba3a41d feat/BE/US-304/TASK-292,293-restriction expense not finish and stock not used,add status project flock completed, fix dto purchase, fix dto nonstock supplier, purchase 2025-12-10 16:30:17 +07:00
Adnan Zahir bb1e6833f0 Merge branch 'feat/BE/Sprint-6' into 'development'
Feat[BE-297]: Create sub module inventory product stock; create api list product stock and api get one product stock

See merge request mbugroup/lti-api!89
2025-12-10 15:21:59 +07:00
Hafizh A. Y. a536094481 Merge branch 'dev/teguh' into 'feat/BE/Sprint-6'
[FEAT/BE][US#333, 338] : creating getone overhead,  inisiating repport API and fixing some bugs on chikin

See merge request mbugroup/lti-api!87
2025-12-10 08:14:07 +00:00
aguhh18 c33cc05f72 Merge branch 'feat/BE/Sprint-6' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2025-12-10 13:47:30 +07:00
aguhh18 3f9865d267 feat[BE]: menambahkan repo expense dan menhapus API API yang tidak akan digunakan di module repport 2025-12-10 13:41:53 +07:00
Hafizh A. Y. 822ca0268e Merge branch 'dev/gio' into 'feat/BE/Sprint-6'
feat[BE-332]: add api get one closing tab sapronak; adjust response get one general information; resolve conflict to sprint 6

See merge request mbugroup/lti-api!86
2025-12-10 06:36:48 +00:00
aguhh18 16d1358b3a FIX[BE}: really fixed duplicate SO number 2025-12-10 11:53:21 +07:00
aguhh18 e00f168a15 Fix[BE} : Fixing duplocate SO number 2025-12-10 11:31:49 +07:00
giovanni-ce 79d488c979 adjust create product warehouse at adjustment and transfer 2025-12-10 11:22:12 +07:00
ragilap 2effa08648 feat/BE/US-304/TASK-307,306-adjustment middleware check if user have permission,create all permission in modules lti 2025-12-10 08:53:09 +07:00
aguhh18 576f8083a3 Feat[BE}: inisiate repport module 2025-12-10 08:23:52 +07:00
aguhh18 d7c543bc9d Refactor[BE]: : delete sales orders and delivery order folder and refactor to just one root marketing folder 2025-12-09 19:24:17 +07:00
giovanni-ce 4a2a80916f adjust response api get all closing, response api get closing tab sapronak 2025-12-09 16:23:05 +07:00
aguhh18 511e5501bb feat[BE]: create GetOverhead API, and fixing chickin use newest productwarehouse schema 2025-12-09 15:32:11 +07:00
ragilap 0fbf04fc1d add restrict for expense,purchase,adjustment transfer: unfinished 2025-12-09 15:16:01 +07:00
giovanni-ce 536e76d481 feat[BE-298]: add api get all list closing 2025-12-09 09:19:50 +07:00
giovanni-ce 29aa737422 resolve conlict to sprint 6 2025-12-08 22:14:55 +07:00
giovanni-ce 26f2f3ccbf adjust response get one general information closing 2025-12-08 22:02:02 +07:00
giovanni-ce 4b147a3be7 feat[BE-332]: add api get one tab sapronak 2025-12-08 21:33:29 +07:00
ragilap 7094d90034 Merge branch 'feat/BE/Sprint-6' of https://gitlab.com/mbugroup/lti-api into feat/BE/US-279/closing-produksi 2025-12-08 17:31:06 +07:00
ragilap e6094528b5 add project flock middleware 2025-12-08 17:30:11 +07:00
ragilap 347f21b45c uncomment auth 2025-12-08 14:19:34 +07:00
Hafizh A. Y. 89b23b0653 Merge branch 'fix/BE/US-282/adjustment-recording-egg' into 'feat/BE/Sprint-6'
fix/BE/US-282/TASK-301,302,303-Adjust Schema Database, Adjust Validation and Req Body, and fixing daily gain, and change logic daily gain

See merge request mbugroup/lti-api!85
2025-12-08 07:15:59 +00:00
Hafizh A. Y. e2a6c2a733 Merge branch 'feat/BE/US-278/Purchase-Expedition-bop' into 'feat/BE/Sprint-6'
feat/BE/US-278/TASK-288,289-adjust schema database,Create trigger in expense...

See merge request mbugroup/lti-api!84
2025-12-08 07:14:54 +00:00
ragilap e0e2d91db5 feat/BE/US-284/TASK-,299-Create API (GET ONE in tab Perhitungan Sapronak),add filtering by flag 2025-12-08 13:40:37 +07:00
ragilap 6e176688fa feat/BE/US-282/TASK-301,302,303-Adjust Schema Database, Adjust Validation and Req Body, and fixing daily gain, and change logic daily gain 2025-12-08 12:49:50 +07:00
ragilap cbb7f45c5f Merge branch 'feat/BE/Sprint-6' of https://gitlab.com/mbugroup/lti-api into fix/BE/US-282/adjustment-recording-egg 2025-12-08 12:43:38 +07:00
ragilap fc9197d00a feat/BE/US-278/TASK-288,289-adjust schema database,Create trigger in expense module, add filter in warehouse linked to project flock, and implement fifo system 2025-12-08 12:12:21 +07:00
ragilap f8e0614d50 Merge branch 'feat/BE/Sprint-6' of https://gitlab.com/mbugroup/lti-api into feat/BE/US-278/Purchase-Expedition-bop 2025-12-08 11:55:26 +07:00
ragilap a8434a5246 feat/BE/US-284/TASK-,299-Create API (GET ONE in tab Perhitungan Sapronak) 2025-12-08 11:28:32 +07:00
Hafizh A. Y. 9f239b1840 Merge branch 'dev/teguh' into 'feat/BE/Sprint-6'
[FEAT/BE][US#277, 280/TASK#290,291,294,295] : Adjust Schema Database table expense and adjust expense API, and finishing module project budgets

See merge request mbugroup/lti-api!80
2025-12-08 02:47:50 +00:00
Hafizh A. Y. 167fd6d6cb Merge branch 'dev/gio' into 'feat/BE/Sprint-6'
fix query changes field stock logs

See merge request mbugroup/lti-api!83
2025-12-08 02:46:50 +00:00
aguhh18 ec2aca936c Merge branch sprint 6 into dev/teguh 2025-12-08 09:20:54 +07:00
aguhh18 f701b30cb3 Merge branch 'feat/BE/Sprint-6' into 'dev/teguh' - merge all closing methods 2025-12-08 08:39:17 +07:00
ragilap 0a18753dde feat/BE/US-278/TASK-288,289-adjust schema database,Create trigger in expense module, add filter in warehouse linked to project flock 2025-12-08 01:23:21 +07:00
ragilap 4638fba318 Merge branch 'feat/BE/Sprint-6' of https://gitlab.com/mbugroup/lti-api into feat/BE/US-278/Purchase-Expedition-bop 2025-12-08 01:10:37 +07:00
giovanni-ce 296e8e4c18 fix query changes field stock logs 2025-12-06 22:29:50 +07:00
ragilap a586fe3781 update purchase triger to expense 2025-12-06 21:09:23 +07:00
ragilap 2d3f7f7ef9 update purchase triger to expense 2025-12-06 21:06:53 +07:00
Hafizh A. Y. 67f7ec3a40 Merge branch 'dev/gio' into 'feat/BE/Sprint-6'
feat[BE-298]: add api get one closing general information

See merge request mbugroup/lti-api!82
2025-12-06 04:34:31 +00:00
ragilap 2fbf66f9f7 add function for read closing project flock kandang and project flock 2025-12-05 22:51:59 +07:00
ragilap 70b2a5a2d1 deleted grade in recording egg unfinished: daily gain question, and confirm counting about fcr, adg, mortality and others 2025-12-05 21:58:51 +07:00
aguhh18 008709c19c Feat[BE-300]: add preload for kandang for get penjualan 2025-12-05 19:08:58 +07:00
ragilap 6572176cca feat/BE/US-33/TASK-292,293,Adjust Project Flock status (add status Selesai), Validate with restriction when expense not finish and stock is not used 2025-12-05 17:47:03 +07:00
giovanni-ce 4c63bd14c3 feat[BE-298]: add api get one closing general information 2025-12-05 17:15:05 +07:00
ragilap c593df661c Merge branch 'feat/BE/Sprint-6' of https://gitlab.com/mbugroup/lti-api into feat/BE/US-279/closing-produksi 2025-12-05 14:09:37 +07:00
ragilap ee2db748ea implement bop for expedition must recheck and qty in staff purchase need info 2025-12-05 14:08:54 +07:00
aguhh18 5afee298b0 FIX[BE]: uncomment middleware usage for delivery and sales orders routes 2025-12-05 13:44:57 +07:00
aguhh18 2bc67a8433 FIX[BE] : fixing deleted create at and create by on product warehouse 2025-12-05 13:35:12 +07:00
aguhh18 b4ccd33ea0 FIX{BE]: fixing product warehouse delete created user on preload 2025-12-05 13:30:36 +07:00
aguhh18 c279303b99 Feat[BE-300]: creating API Get closing penjualan 2025-12-05 12:31:52 +07:00
Hafizh A. Y. b8a769dc72 Merge branch 'dev/gio' into 'feat/BE/Sprint-6'
adjust for changes erd product_warehouse

See merge request mbugroup/lti-api!81
2025-12-05 03:38:55 +00:00
aguhh18 8c883669d3 Merge branch 'feat/BE/Sprint-6' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2025-12-04 20:11:17 +07:00
giovanni-ce 1e9a637202 adjust for changes erd product_warehouse 2025-12-04 20:00:46 +07:00
ragilap fc14f9a98f uncoment auth middleware 2025-12-04 18:58:20 +07:00
ragilap 17269d701c adjustment create project flock must have a relation for location,area and kandang 2025-12-04 18:54:04 +07:00
ragilap c3305d3089 uncomment auth middleware 2025-12-04 18:45:45 +07:00
ragilap b43e2b44ec deleted edit function in project-flock, and must retest closing feat after fixing product warehouse 2025-12-04 18:44:56 +07:00
ragilap 6e3a8f3551 Merge branch 'feat/BE/Sprint-6' of https://gitlab.com/mbugroup/lti-api into feat/BE/US-279/closing-produksi 2025-12-04 18:18:53 +07:00
Hafizh A. Y. c064fb1765 Merge branch 'dev/gio' into 'feat/BE/Sprint-6'
adjust response inventory stock-product

See merge request mbugroup/lti-api!79
2025-12-04 09:39:25 +00:00
giovanni-ce 4af631a1d3 adjust response inventory stock-product 2025-12-04 16:08:30 +07:00
Hafizh A. Y. 91e4762945 Merge branch 'dev/hafizh' into 'feat/BE/Sprint-6'
feat(BE-308,309): utility document and implementation s3 bucket

See merge request mbugroup/lti-api!78
2025-12-04 08:46:50 +00:00
Hafizh A. Y b8403f1c7e feat(BE-308,309): utility document and implementation s3 bucket 2025-12-04 15:46:06 +07:00
ragilap 415d5c0e67 Merge branch 'feat/BE/Sprint-6' of https://gitlab.com/mbugroup/lti-api into feat/BE/US-279/closing-produksi 2025-12-04 15:03:22 +07:00
ragilap 1bca29cd31 adjustment recording adding weight in recording egg : need info, deleted grading egg, adjustment validation if must be changed again 2025-12-04 14:55:42 +07:00
Hafizh A. Y. 753d8575c4 Merge branch 'dev/gio' into 'feat/BE/Sprint-6'
[FEAT/BE][US#286/TASK#296,297] : Adjust Schema Database table product_warehouse, product and stock_logs; create sub module inventory product_stock and api list and get one

See merge request mbugroup/lti-api!77
2025-12-04 06:37:45 +00:00
giovanni-ce 4c5266da23 resolve conflict to sprint 6 2025-12-04 12:20:47 +07:00
Hafizh A. Y. d79b1868fc Merge branch 'dev/ragil' into 'feat/BE/Sprint-6'
[FIX/BE][US#74] : Project flock dto

See merge request mbugroup/lti-api!72
2025-12-04 04:41:36 +00:00
giovanni-ce 33a9d7806e adjust response location to object 2025-12-04 11:27:02 +07:00
ragilap ea294c6a18 add approval projectflockkandang closed,expense must be done,stock must empty by flag unfinished:need info approval fix 2025-12-03 21:46:02 +07:00
ragilap d572d04e3b Merge branch 'dev/ragil' of https://gitlab.com/mbugroup/lti-api into feat/BE/US-279/closing-produksi 2025-12-03 19:56:00 +07:00
giovanni-ce 730fb22cc2 Feat[BE-297]: Create sub module inventory product stock; create api list product stock and api get one product stock 2025-12-03 19:47:12 +07:00
giovanni-ce 94fc9219af Feat[BE-296]: adjust schema db and entity products, product_warehouse, stock_logs 2025-12-03 18:43:03 +07:00
aguhh18 beee88322a FIX[BE] : fixing wrong project flock validation 2025-12-03 14:23:32 +07:00
aguhh18 1b464884c5 Feat[BE-290]: enhance expense update functionality and validation 2025-12-03 12:02:58 +07:00
aguhh18 31699f4162 FIX[BE]: fixing nonstock sometimes isn't appeared on get one 2025-12-03 11:14:26 +07:00
aguhh18 e667d88218 Feat[BE]: create resubmit projectflock API 2025-12-02 12:39:58 +07:00
aguhh18 002981e63b Merge branch 'feat/BE/Sprint-6' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2025-12-02 09:35:02 +07:00
aguhh18 1d0ef8fb93 Feat[BE#280]:add project budgets to body create API and get one API 2025-12-02 09:32:42 +07:00
ragilap d76db26a4d feat/BE/US-279/Closing unfinished 2025-12-01 16:49:13 +07:00
aguhh18 29f0fd6edb Merge branch 'dev/ragil' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2025-11-28 15:25:52 +07:00
290 changed files with 22123 additions and 3511 deletions
Vendored
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -3,7 +3,7 @@ root = "."
tmp_dir = "tmp" tmp_dir = "tmp"
[build] [build]
cmd = "go build -o ./tmp/main ./cmd/api" cmd = "go build -buildvcs=false -o ./tmp/main ./cmd/api"
bin = "tmp/main" bin = "tmp/main"
full_bin = "APP_ENV=dev ./tmp/main" full_bin = "APP_ENV=dev ./tmp/main"
include_ext = ["go", "tpl", "tmpl", "html"] include_ext = ["go", "tpl", "tmpl", "html"]
+35 -1
View File
@@ -4,15 +4,22 @@ go 1.23
require ( require (
github.com/MicahParks/keyfunc/v2 v2.1.0 github.com/MicahParks/keyfunc/v2 v2.1.0
github.com/aws/aws-sdk-go-v2 v1.40.0
github.com/aws/aws-sdk-go-v2/config v1.32.2
github.com/aws/aws-sdk-go-v2/credentials v1.19.2
github.com/aws/aws-sdk-go-v2/service/s3 v1.92.1
github.com/bytedance/sonic v1.12.1 github.com/bytedance/sonic v1.12.1
github.com/glebarez/sqlite v1.11.0
github.com/go-playground/validator/v10 v10.27.0 github.com/go-playground/validator/v10 v10.27.0
github.com/gofiber/contrib/jwt v1.0.10 github.com/gofiber/contrib/jwt v1.0.10
github.com/gofiber/fiber/v2 v2.52.5 github.com/gofiber/fiber/v2 v2.52.5
github.com/golang-jwt/jwt/v5 v5.2.1 github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0
github.com/jackc/pgconn v1.14.1 github.com/jackc/pgconn v1.14.1
github.com/redis/go-redis/v9 v9.14.0 github.com/redis/go-redis/v9 v9.14.0
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
github.com/spf13/viper v1.19.0 github.com/spf13/viper v1.19.0
github.com/xuri/excelize/v2 v2.9.0
golang.org/x/crypto v0.33.0 golang.org/x/crypto v0.33.0
gorm.io/driver/postgres v1.5.9 gorm.io/driver/postgres v1.5.9
gorm.io/gorm v1.25.11 gorm.io/gorm v1.25.11
@@ -20,17 +27,33 @@ require (
require ( require (
github.com/andybalholm/brotli v1.1.0 // indirect github.com/andybalholm/brotli v1.1.0 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.5 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.2 // indirect
github.com/aws/smithy-go v1.23.2 // indirect
github.com/bytedance/sonic/loader v0.2.0 // indirect github.com/bytedance/sonic/loader v0.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/base64x v0.1.4 // indirect
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/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgio v1.0.0 // indirect
@@ -49,8 +72,12 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // 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/richardlehane/mscfb v1.0.4 // indirect
github.com/richardlehane/msoleps v1.0.4 // 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
@@ -59,12 +86,15 @@ require (
github.com/spf13/afero v1.11.0 // indirect github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/subosito/gotenv v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect
github.com/tinylib/msgp v1.1.8 // indirect github.com/tinylib/msgp v1.1.8 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.55.0 // indirect github.com/valyala/fasthttp v1.55.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // indirect
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect
go.uber.org/atomic v1.9.0 // indirect go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
@@ -75,4 +105,8 @@ 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
) )
+78 -1
View File
@@ -2,6 +2,44 @@ github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+G
github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k= github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrKlwc=
github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 h1:DHctwEM8P8iTXFxC/QK0MRjwEpWQeM9yzidCRjldUz0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3/go.mod h1:xdCzcZEtnSTKVDOmUZs4l/j3pSV6rpo1WXl5ugNsL8Y=
github.com/aws/aws-sdk-go-v2/config v1.32.2 h1:4liUsdEpUUPZs5WVapsJLx5NPmQhQdez7nYFcovrytk=
github.com/aws/aws-sdk-go-v2/config v1.32.2/go.mod h1:l0hs06IFz1eCT+jTacU/qZtC33nvcnLADAPL/XyrkZI=
github.com/aws/aws-sdk-go-v2/credentials v1.19.2 h1:qZry8VUyTK4VIo5aEdUcBjPZHL2v4FyQ3QEOaWcFLu4=
github.com/aws/aws-sdk-go-v2/credentials v1.19.2/go.mod h1:YUqm5a1/kBnoK+/NY5WEiMocZihKSo15/tJdmdXnM5g=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 h1:WZVR5DbDgxzA0BJeudId89Kmgy6DIU4ORpxwsVHz0qA=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14/go.mod h1:Dadl9QO0kHgbrH1GRqGiZdYtW5w+IXXaBNCHTIaheM4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14/go.mod h1:VymhrMJUWs69D8u0/lZ7jSB6WgaG/NqHi3gX0aYf6U0=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 h1:bOS19y6zlJwagBfHxs0ESzr1XCOU2KXJCWcq3E2vfjY=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14/go.mod h1:1ipeGBMAxZ0xcTm6y6paC2C/J6f6OO7LBODV9afuAyM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14 h1:ITi7qiDSv/mSGDSWNpZ4k4Ve0DQR6Ug2SJQ8zEHoDXg=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14/go.mod h1:k1xtME53H1b6YpZt74YmwlONMWf4ecM+lut1WQLAF/U=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5 h1:Hjkh7kE6D81PgrHlE/m9gx+4TyyeLHuY8xJs7yXN5C4=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5/go.mod h1:nPRXgyCfAurhyaTMoBMwRBYBhaHI4lNPAnJmjM0Tslc=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 h1:FIouAnCE46kyYqyhs0XEBDFFSREtdnr8HQuLPQPLCrY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14/go.mod h1:UTwDc5COa5+guonQU8qBikJo1ZJ4ln2r1MkF7Dqag1E=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14 h1:FzQE21lNtUor0Fb7QNgnEyiRCBlolLTX/Z1j65S7teM=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14/go.mod h1:s1ydyWG9pm3ZwmmYN21HKyG9WzAZhYVW85wMHs5FV6w=
github.com/aws/aws-sdk-go-v2/service/s3 v1.92.1 h1:OgQy/+0+Kc3khtqiEOk23xQAglXi3Tj0y5doOxbi5tg=
github.com/aws/aws-sdk-go-v2/service/s3 v1.92.1/go.mod h1:wYNqY3L02Z3IgRYxOBPH9I1zD9Cjh9hI5QOy/eOjQvw=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.2 h1:MxMBdKTYBjPQChlJhi4qlEueqB1p1KcbTEa7tD5aqPs=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.2/go.mod h1:iS6EPmNeqCsGo+xQmXv0jIMjyYtQfnwg36zl2FwEouk=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.5 h1:ksUT5KtgpZd3SAiFJNJ0AFEJVva3gjBmN7eXUZjzUwQ=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.5/go.mod h1:av+ArJpoYf3pgyrj6tcehSFW+y9/QvAY8kMooR9bZCw=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10 h1:GtsxyiF3Nd3JahRBJbxLCCdYW9ltGQYrFWg8XdkGDd8=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10/go.mod h1:/j67Z5XBVDx8nZVp9EuFM9/BS5dvBznbqILGuu73hug=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.2 h1:a5UTtD4mHBU3t0o6aHQZFJTNKVfxFWfPX7J0Lr7G+uY=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.2/go.mod h1:6TxbXoDSgBQ225Qd8Q+MbxUxUh6TtNKwbRt/EPS9xso=
github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
@@ -27,12 +65,18 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/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=
@@ -50,6 +94,8 @@ github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17w
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/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=
@@ -136,6 +182,8 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/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=
@@ -146,6 +194,14 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/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/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
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=
@@ -189,8 +245,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
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=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
@@ -203,6 +260,16 @@ github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8
github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM= github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0 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/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY=
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE=
github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE=
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A=
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
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= 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.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
@@ -229,6 +296,8 @@ 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/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
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-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -306,4 +375,12 @@ 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=
-44
View File
@@ -1,44 +0,0 @@
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: {},
}
@@ -84,8 +84,9 @@ func (r *approvalRepositoryImpl) LatestByTargets(
result := make(map[uint]entity.Approval, len(approvableIDs)) result := make(map[uint]entity.Approval, len(approvableIDs))
q := r.DB().WithContext(ctx). q := r.DB().WithContext(ctx).
Select("DISTINCT ON (approvable_id) *").
Where("approvable_type = ? AND approvable_id IN ?", workflow, approvableIDs). Where("approvable_type = ? AND approvable_id IN ?", workflow, approvableIDs).
Order("action_at DESC") Order("approvable_id, action_at DESC")
if modifier != nil { if modifier != nil {
q = modifier(q) q = modifier(q)
@@ -0,0 +1,62 @@
package repository
import (
"context"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type DocumentRepository interface {
BaseRepository[entity.Document]
ListByTarget(ctx context.Context, documentableType string, documentableID uint64, modifier func(*gorm.DB) *gorm.DB) ([]entity.Document, error)
DeleteByTarget(ctx context.Context, documentableType string, documentableID uint64, modifier func(*gorm.DB) *gorm.DB) error
}
type documentRepositoryImpl struct {
*BaseRepositoryImpl[entity.Document]
}
func NewDocumentRepository(db *gorm.DB) DocumentRepository {
return &documentRepositoryImpl{
BaseRepositoryImpl: NewBaseRepository[entity.Document](db),
}
}
func (r *documentRepositoryImpl) ListByTarget(
ctx context.Context,
documentableType string,
documentableID uint64,
modifier func(*gorm.DB) *gorm.DB,
) ([]entity.Document, error) {
var documents []entity.Document
q := r.DB().WithContext(ctx).
Where("documentable_type = ? AND documentable_id = ?", documentableType, documentableID)
if modifier != nil {
q = modifier(q)
}
if err := q.Order("created_at ASC").Find(&documents).Error; err != nil {
return nil, err
}
return documents, nil
}
func (r *documentRepositoryImpl) DeleteByTarget(
ctx context.Context,
documentableType string,
documentableID uint64,
modifier func(*gorm.DB) *gorm.DB,
) error {
q := r.DB().WithContext(ctx).
Where("documentable_type = ? AND documentable_id = ?", documentableType, documentableID)
if modifier != nil {
q = modifier(q)
}
return q.Delete(&entity.Document{}).Error
}
@@ -2,6 +2,7 @@ package repository
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"gorm.io/gorm" "gorm.io/gorm"
@@ -9,45 +10,59 @@ import (
// Exists reports whether a record with the given ID exists for type T. // Exists reports whether a record with the given ID exists for type T.
func Exists[T any](ctx context.Context, db *gorm.DB, id uint) (bool, error) { func Exists[T any](ctx context.Context, db *gorm.DB, id uint) (bool, error) {
var count int64 var marker int
if err := db.WithContext(ctx). err := db.WithContext(ctx).
Model(new(T)). Model(new(T)).
Select("1").
Where("id = ?", id). Where("id = ?", id).
Count(&count).Error; err != nil { Limit(1).
Take(&marker).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return false, nil
}
if err != nil {
return false, err return false, err
} }
return count > 0, nil return true, nil
} }
func ExistsByName[T any](ctx context.Context, db *gorm.DB, name string, excludeID *uint) (bool, error) { func ExistsByName[T any](ctx context.Context, db *gorm.DB, name string, excludeID *uint) (bool, error) {
var count int64
q := db.WithContext(ctx). q := db.WithContext(ctx).
Model(new(T)). Model(new(T)).
Select("1").
Where("name = ?", name). Where("name = ?", name).
Where("deleted_at IS NULL") Where("deleted_at IS NULL")
if excludeID != nil { if excludeID != nil {
q = q.Where("id <> ?", *excludeID) q = q.Where("id <> ?", *excludeID)
} }
if err := q.Count(&count).Error; err != nil { var marker int
if err := q.Limit(1).Take(&marker).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return false, nil
}
return false, err return false, err
} }
return count > 0, nil return true, nil
} }
func ExistsByField[T any](ctx context.Context, db *gorm.DB, field string, value any, excludeID *uint) (bool, error) { func ExistsByField[T any](ctx context.Context, db *gorm.DB, field string, value any, excludeID *uint) (bool, error) {
if field == "" { if field == "" {
return false, fmt.Errorf("field is required") return false, fmt.Errorf("field is required")
} }
var count int64
q := db.WithContext(ctx). q := db.WithContext(ctx).
Model(new(T)). Model(new(T)).
Select("1").
Where(fmt.Sprintf("%s = ?", field), value). Where(fmt.Sprintf("%s = ?", field), value).
Where("deleted_at IS NULL") Where("deleted_at IS NULL")
if excludeID != nil { if excludeID != nil {
q = q.Where("id <> ?", *excludeID) q = q.Where("id <> ?", *excludeID)
} }
if err := q.Count(&count).Error; err != nil { var marker int
if err := q.Limit(1).Take(&marker).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return false, nil
}
return false, err return false, err
} }
return count > 0, nil return true, nil
} }
@@ -63,13 +63,14 @@ func (r *StockAllocationRepositoryImpl) ReleaseByUsable(
updates["note"] = *note updates["note"] = *note
} }
q := r.DB().WithContext(ctx). baseDB := r.DB()
if modifier != nil {
baseDB = modifier(baseDB)
}
q := baseDB.WithContext(ctx).
Model(&entity.StockAllocation{}). Model(&entity.StockAllocation{}).
Where("usable_type = ? AND usable_id = ? AND status = ?", usableType, usableID, entity.StockAllocationStatusActive) Where("usable_type = ? AND usable_id = ? AND status = ?", usableType, usableID, entity.StockAllocationStatusActive)
if modifier != nil {
q = modifier(q)
}
return q.Updates(updates).Error return q.Updates(updates).Error
} }
@@ -0,0 +1,120 @@
package service
import (
"context"
"errors"
"fmt"
productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
warehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
)
// Dipakai untuk semua module yang butuh cek:
// "PW ini → warehouse → kandang → project_flock_kandang sudah closing atau belum"
func EnsureProjectFlockNotClosedForProductWarehouses(
ctx context.Context,
db *gorm.DB,
productWarehouseIDs []uint,
) error {
if len(productWarehouseIDs) == 0 {
return nil
}
pwRepo := productWarehouseRepo.NewProductWarehouseRepository(db)
wRepo := warehouseRepo.NewWarehouseRepository(db)
pfkRepo := projectFlockKandangRepo.NewProjectFlockKandangRepository(db)
seenPW := make(map[uint]struct{})
seenKandang := make(map[uint]struct{})
for _, pwID := range productWarehouseIDs {
if pwID == 0 {
continue
}
if _, ok := seenPW[pwID]; ok {
continue
}
seenPW[pwID] = struct{}{}
pw, err := pwRepo.GetByID(ctx, pwID, nil)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusBadRequest,
fmt.Sprintf("Product warehouse %d tidak ditemukan", pwID))
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product warehouse")
}
wh, err := wRepo.GetByID(ctx, uint(pw.WarehouseId), nil)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusBadRequest,
fmt.Sprintf("Warehouse %d tidak ditemukan", pw.WarehouseId))
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate warehouse")
}
// Warehouse tanpa kandang → bukan kandang produksi → skip
if wh.KandangId == nil || *wh.KandangId == 0 {
continue
}
kandangID := uint(*wh.KandangId)
if _, ok := seenKandang[kandangID]; ok {
continue
}
seenKandang[kandangID] = struct{}{}
pfk, err := pfkRepo.GetActiveByKandangID(ctx, kandangID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// nggak ada project aktif untuk kandang ini → aman
continue
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate project flock")
}
// INTI RULE: kalau aktif tapi sudah punya ClosedAt → anggap "project sudah closing"
if pfk != nil && pfk.ClosedAt != nil {
return fiber.NewError(fiber.StatusBadRequest, "Project sudah closing")
}
}
return nil
}
func EnsureProjectFlockNotClosedByProjectFlockKandangID(
ctx context.Context,
db *gorm.DB,
pfkIDs []uint,
) error {
pfkRepo := projectFlockKandangRepo.NewProjectFlockKandangRepository(db)
seen := make(map[uint]struct{})
for _, id := range pfkIDs {
if id == 0 {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
pfk, err := pfkRepo.GetByID(ctx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusBadRequest,
fmt.Sprintf("Project flock kandang %d tidak ditemukan", id))
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate project flock")
}
if pfk.ClosedAt != nil {
return fiber.NewError(fiber.StatusBadRequest, "Project sudah closing")
}
}
return nil
}
@@ -0,0 +1,411 @@
package service
import (
"context"
"errors"
"fmt"
"mime"
"mime/multipart"
"path/filepath"
"strings"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
"gitlab.com/mbugroup/lti-api.git/internal/config"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/google/uuid"
)
const (
defaultDocumentPathLimit = 50
defaultDocumentKeyPrefix = "docs"
maxDocumentNameLength = 50
)
type DocumentService interface {
UploadDocuments(ctx context.Context, req DocumentUploadRequest) ([]DocumentUploadResult, error)
ListByTarget(ctx context.Context, documentableType string, documentableID uint64) ([]entity.Document, error)
DeleteDocuments(ctx context.Context, ids []uint, removeFromStorage bool) error
DeleteByTarget(ctx context.Context, documentableType string, documentableID uint64, removeFromStorage bool) error
PublicURL(document entity.Document) string
}
type DocumentUploadRequest struct {
DocumentableType string
DocumentableID uint64
CreatedBy *uint
Files []DocumentFile
}
type DocumentFile struct {
File *multipart.FileHeader
Type string
Index *int
}
type DocumentUploadResult struct {
Document entity.Document
URL string
Index *int
}
type DocumentServiceOption func(*documentService)
type documentService struct {
repo commonRepo.DocumentRepository
storage DocumentStorage
keyPrefix string
maxPathLength int
}
func NewDocumentService(repo commonRepo.DocumentRepository, storage DocumentStorage, opts ...DocumentServiceOption) DocumentService {
svc := &documentService{
repo: repo,
storage: storage,
keyPrefix: defaultDocumentKeyPrefix,
maxPathLength: defaultDocumentPathLimit,
}
for _, opt := range opts {
opt(svc)
}
return svc
}
func NewDocumentServiceFromConfig(ctx context.Context, repo commonRepo.DocumentRepository) (DocumentService, error) {
if repo == nil {
return nil, errors.New("document repository is required")
}
if strings.TrimSpace(config.S3Bucket) == "" {
return nil, errors.New("S3_BUCKET is not configured")
}
storage, err := NewS3DocumentStorage(ctx, S3DocumentStorageConfig{
Region: config.S3Region,
Bucket: config.S3Bucket,
AccessKey: config.S3AccessKey,
SecretKey: config.S3SecretKey,
Endpoint: config.S3Endpoint,
BaseURL: config.S3PublicBaseURL,
ForcePathStyle: config.S3ForcePathStyle,
})
if err != nil {
return nil, err
}
prefix := config.S3DocumentKeyPrefix
if prefix == "" {
prefix = defaultDocumentKeyPrefix
}
return NewDocumentService(
repo,
storage,
WithDocumentKeyPrefix(prefix),
WithDocumentPathLimit(defaultDocumentPathLimit),
), nil
}
func WithDocumentKeyPrefix(prefix string) DocumentServiceOption {
return func(svc *documentService) {
prefix = strings.Trim(prefix, "/")
if prefix == "" {
prefix = defaultDocumentKeyPrefix
}
svc.keyPrefix = prefix
}
}
func WithDocumentPathLimit(limit int) DocumentServiceOption {
return func(svc *documentService) {
if limit > 0 {
svc.maxPathLength = limit
}
}
}
func (s *documentService) UploadDocuments(ctx context.Context, req DocumentUploadRequest) ([]DocumentUploadResult, error) {
if s.repo == nil {
return nil, errors.New("document repository not configured")
}
if s.storage == nil {
return nil, errors.New("document storage not configured")
}
documentableType := strings.ToUpper(strings.TrimSpace(req.DocumentableType))
if documentableType == "" {
return nil, errors.New("documentable type is required")
}
if req.DocumentableID == 0 {
return nil, errors.New("documentable id is required")
}
if len(req.Files) == 0 {
return nil, errors.New("no files to upload")
}
var createdBy *uint
if req.CreatedBy != nil && *req.CreatedBy != 0 {
idCopy := *req.CreatedBy
createdBy = &idCopy
}
results := make([]DocumentUploadResult, 0, len(req.Files))
createdDocs := make([]entity.Document, 0, len(req.Files))
for _, file := range req.Files {
if file.File == nil {
return nil, errors.New("file header is required")
}
originalName := sanitizeDocumentName(file.File.Filename)
contentType := detectContentType(file.File, originalName)
ext := detectExtension(file.File.Filename, contentType)
key, err := s.generateObjectKey(ext)
if err != nil {
s.rollbackDocuments(ctx, createdDocs)
return nil, err
}
reader, err := file.File.Open()
if err != nil {
s.rollbackDocuments(ctx, createdDocs)
return nil, err
}
uploadRes, err := s.storage.Upload(ctx, key, reader, file.File.Size, contentType)
_ = reader.Close()
if err != nil {
s.rollbackDocuments(ctx, createdDocs)
return nil, err
}
docType := resolveDocumentType(file.Type, documentableType)
doc := entity.Document{
DocumentableType: documentableType,
DocumentableId: req.DocumentableID,
Type: docType,
Path: uploadRes.Key,
Name: originalName,
Ext: strings.TrimPrefix(ext, "."),
Size: float64(file.File.Size),
CreatedBy: createdBy,
}
if err := s.repo.CreateOne(ctx, &doc, nil); err != nil {
_ = s.storage.Delete(ctx, uploadRes.Key)
s.rollbackDocuments(ctx, createdDocs)
return nil, err
}
createdDocs = append(createdDocs, doc)
results = append(results, DocumentUploadResult{
Document: doc,
URL: uploadRes.URL,
Index: cloneIndex(file.Index),
})
}
return results, nil
}
func (s *documentService) ListByTarget(ctx context.Context, documentableType string, documentableID uint64) ([]entity.Document, error) {
if s.repo == nil {
return nil, errors.New("document repository not configured")
}
documentableType = strings.ToUpper(strings.TrimSpace(documentableType))
if documentableType == "" {
return nil, errors.New("documentable type is required")
}
if documentableID == 0 {
return nil, errors.New("documentable id is required")
}
return s.repo.ListByTarget(ctx, documentableType, documentableID, nil)
}
func (s *documentService) DeleteDocuments(ctx context.Context, ids []uint, removeFromStorage bool) error {
if s.repo == nil {
return errors.New("document repository not configured")
}
if len(ids) == 0 {
return nil
}
docs, err := s.repo.GetByIDs(ctx, ids, nil)
if err != nil {
return err
}
for _, doc := range docs {
if err := s.repo.DeleteOne(ctx, doc.Id); err != nil {
return err
}
if removeFromStorage && s.storage != nil {
if err := s.storage.Delete(ctx, doc.Path); err != nil {
utils.Log.WithError(err).Warnf("failed to delete document object %s", doc.Path)
}
}
}
return nil
}
func (s *documentService) DeleteByTarget(ctx context.Context, documentableType string, documentableID uint64, removeFromStorage bool) error {
if s.repo == nil {
return errors.New("document repository not configured")
}
documentableType = strings.ToUpper(strings.TrimSpace(documentableType))
if documentableType == "" || documentableID == 0 {
return errors.New("documentable type and id are required")
}
var docs []entity.Document
if removeFromStorage && s.storage != nil {
var err error
docs, err = s.repo.ListByTarget(ctx, documentableType, documentableID, nil)
if err != nil {
return err
}
}
if err := s.repo.DeleteByTarget(ctx, documentableType, documentableID, nil); err != nil {
return err
}
if removeFromStorage && len(docs) > 0 {
for _, doc := range docs {
if err := s.storage.Delete(ctx, doc.Path); err != nil {
utils.Log.WithError(err).Warnf("failed to delete document object %s", doc.Path)
}
}
}
return nil
}
func (s *documentService) PublicURL(document entity.Document) string {
if s.storage == nil || strings.TrimSpace(document.Path) == "" {
return ""
}
return s.storage.URL(document.Path)
}
func (s *documentService) generateObjectKey(ext string) (string, error) {
normalizedExt := strings.TrimSpace(ext)
if normalizedExt != "" && !strings.HasPrefix(normalizedExt, ".") {
normalizedExt = "." + normalizedExt
}
u := uuid.New().String()
key := fmt.Sprintf("%s/%s%s", strings.Trim(s.keyPrefix, "/"), u, normalizedExt)
if s.keyPrefix == "" {
key = fmt.Sprintf("%s%s", u, normalizedExt)
}
if len(key) > s.maxPathLength {
key = fmt.Sprintf("%s%s", u, normalizedExt)
}
if len(key) > s.maxPathLength {
return "", fmt.Errorf("object key exceeds maximum length (%d)", s.maxPathLength)
}
return key, nil
}
func (s *documentService) rollbackDocuments(ctx context.Context, docs []entity.Document) {
if len(docs) == 0 {
return
}
for i := len(docs) - 1; i >= 0; i-- {
doc := docs[i]
if s.repo != nil && doc.Id != 0 {
if err := s.repo.DeleteOne(ctx, doc.Id); err != nil {
utils.Log.WithError(err).Warnf("failed to rollback document #%d", doc.Id)
}
}
if s.storage != nil && strings.TrimSpace(doc.Path) != "" {
if err := s.storage.Delete(ctx, doc.Path); err != nil {
utils.Log.WithError(err).Warnf("failed to rollback document object %s", doc.Path)
}
}
}
}
func sanitizeDocumentName(name string) string {
name = filepath.Base(strings.TrimSpace(name))
if name == "." || name == "" {
name = "document"
}
name = strings.Map(func(r rune) rune {
if r < 32 {
return -1
}
switch r {
case '\\', '/', ':', '*', '?', '"', '<', '>', '|':
return '-'
default:
return r
}
}, name)
if len(name) > maxDocumentNameLength {
runes := []rune(name)
if len(runes) > maxDocumentNameLength {
name = string(runes[:maxDocumentNameLength])
}
}
return name
}
func detectExtension(filename, contentType string) string {
ext := strings.ToLower(strings.TrimSpace(filepath.Ext(filename)))
if ext == "" && contentType != "" {
if exts, _ := mime.ExtensionsByType(contentType); len(exts) > 0 {
ext = exts[0]
}
}
if ext == "" {
return ".bin"
}
if !strings.HasPrefix(ext, ".") {
ext = "." + ext
}
return ext
}
func detectContentType(file *multipart.FileHeader, filename string) string {
if file == nil {
return "application/octet-stream"
}
contentType := strings.TrimSpace(file.Header.Get("Content-Type"))
if contentType != "" {
return contentType
}
if ext := filepath.Ext(filename); ext != "" {
if guess := mime.TypeByExtension(ext); guess != "" {
return guess
}
}
return "application/octet-stream"
}
func resolveDocumentType(fileType, fallback string) string {
value := strings.ToUpper(strings.TrimSpace(fileType))
if value == "" {
return fallback
}
return value
}
func cloneIndex(index *int) *int {
if index == nil {
return nil
}
value := *index
return &value
}
@@ -0,0 +1,101 @@
package service
import (
"bytes"
"context"
"mime/multipart"
"net/http/httptest"
"strings"
"testing"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
"gitlab.com/mbugroup/lti-api.git/internal/config"
"gitlab.com/mbugroup/lti-api.git/internal/database"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
func TestDocumentServiceUpload(t *testing.T) {
if strings.TrimSpace(config.S3Bucket) == "" {
t.Fatal("S3 bucket is not configured; set S3_* env vars to run this test")
}
ctx := context.Background()
db := setupDocumentTestDB(t)
repo := commonRepo.NewDocumentRepository(db)
svc, err := NewDocumentServiceFromConfig(ctx, repo)
if err != nil {
t.Fatalf("failed to create document service from config: %v", err)
}
file := newTestFileHeader(t, "integration-proof.txt", "text/plain", []byte("document integration test"))
userID := uint(100)
results, err := svc.UploadDocuments(ctx, DocumentUploadRequest{
DocumentableType: "INVENTORY_TRANSFER",
DocumentableID: 99,
CreatedBy: &userID,
Files: []DocumentFile{
{File: file, Type: "integration"},
},
})
if err != nil {
t.Fatalf("upload to S3 failed: %v", err)
}
if len(results) != 1 {
t.Fatalf("expected 1 uploaded document, got %d", len(results))
}
doc := results[0].Document
if doc.Path == "" {
t.Fatalf("expected non-empty storage path")
}
if results[0].URL == "" {
t.Fatalf("expected public URL for uploaded document")
}
t.Logf("uploaded document #%d to %s (path=%s)", doc.Id, results[0].URL, doc.Path)
}
func setupDocumentTestDB(t *testing.T) *gorm.DB {
t.Helper()
if strings.TrimSpace(config.DBHost) == "" || strings.TrimSpace(config.DBName) == "" {
t.Fatal("database configuration missing; ensure DB_HOST and DB_NAME are set")
}
db := database.Connect(config.DBHost, config.DBName)
if db == nil {
t.Fatal("failed to create database connection")
}
if err := db.AutoMigrate(&entity.Document{}); err != nil {
t.Fatalf("failed to migrate document table: %v", err)
}
return db
}
func newTestFileHeader(t *testing.T, filename, contentType string, data []byte) *multipart.FileHeader {
t.Helper()
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("documents", filename)
if err != nil {
t.Fatalf("failed to create form file: %v", err)
}
if _, err := part.Write(data); err != nil {
t.Fatalf("failed to write file data: %v", err)
}
if err := writer.Close(); err != nil {
t.Fatalf("failed to close writer: %v", err)
}
req := httptest.NewRequest("POST", "http://example.com/upload", body)
req.Header.Set("Content-Type", writer.FormDataContentType())
_, fileHeader, err := req.FormFile("documents")
if err != nil {
t.Fatalf("failed to parse form file: %v", err)
}
fileHeader.Header.Set("Content-Type", contentType)
return fileHeader
}
@@ -0,0 +1,160 @@
package service
import (
"context"
"errors"
"fmt"
"io"
"strings"
"github.com/aws/aws-sdk-go-v2/aws"
awsconfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
type DocumentStorage interface {
Upload(ctx context.Context, key string, body io.Reader, size int64, contentType string) (DocumentStorageUploadResult, error)
Delete(ctx context.Context, key string) error
URL(key string) string
}
type DocumentStorageUploadResult struct {
Key string
URL string
ETag string
}
type S3DocumentStorageConfig struct {
Region string
Bucket string
AccessKey string
SecretKey string
Endpoint string
BaseURL string
ForcePathStyle bool
}
type s3DocumentStorage struct {
client *s3.Client
bucket string
base string
}
func NewS3DocumentStorage(ctx context.Context, cfg S3DocumentStorageConfig) (DocumentStorage, error) {
bucket := strings.TrimSpace(cfg.Bucket)
if bucket == "" {
return nil, errors.New("s3 bucket is required")
}
region := strings.TrimSpace(cfg.Region)
if region == "" {
region = "us-east-1"
}
options := []func(*awsconfig.LoadOptions) error{
awsconfig.WithRegion(region),
}
endpoint := strings.TrimSpace(cfg.Endpoint)
if endpoint != "" {
resolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, _ ...interface{}) (aws.Endpoint, error) {
if service == s3.ServiceID {
return aws.Endpoint{
URL: endpoint,
SigningRegion: region,
HostnameImmutable: true,
}, nil
}
return aws.Endpoint{}, &aws.EndpointNotFoundError{}
})
options = append(options, awsconfig.WithEndpointResolverWithOptions(resolver))
}
accessKey := strings.TrimSpace(cfg.AccessKey)
secretKey := strings.TrimSpace(cfg.SecretKey)
if accessKey != "" && secretKey != "" {
options = append(options, awsconfig.WithCredentialsProvider(
credentials.NewStaticCredentialsProvider(accessKey, secretKey, ""),
))
}
awsCfg, err := awsconfig.LoadDefaultConfig(ctx, options...)
if err != nil {
return nil, err
}
client := s3.NewFromConfig(awsCfg, func(o *s3.Options) {
o.UsePathStyle = cfg.ForcePathStyle
})
baseURL := strings.TrimSuffix(strings.TrimSpace(cfg.BaseURL), "/")
if baseURL == "" {
if endpoint != "" {
baseURL = fmt.Sprintf("%s/%s", strings.TrimSuffix(endpoint, "/"), bucket)
} else {
baseURL = fmt.Sprintf("https://%s.s3.%s.amazonaws.com", bucket, region)
}
}
return &s3DocumentStorage{
client: client,
bucket: bucket,
base: baseURL,
}, nil
}
func (s *s3DocumentStorage) Upload(ctx context.Context, key string, body io.Reader, size int64, contentType string) (DocumentStorageUploadResult, error) {
if strings.TrimSpace(key) == "" {
return DocumentStorageUploadResult{}, errors.New("storage key is required")
}
if size < 0 {
size = 0
}
input := &s3.PutObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(key),
Body: body,
}
input.ContentLength = aws.Int64(size)
if ct := strings.TrimSpace(contentType); ct != "" {
input.ContentType = aws.String(ct)
}
out, err := s.client.PutObject(ctx, input)
if err != nil {
return DocumentStorageUploadResult{}, err
}
var etag string
if out.ETag != nil {
etag = strings.Trim(*out.ETag, "\"")
}
return DocumentStorageUploadResult{
Key: key,
URL: s.URL(key),
ETag: etag,
}, nil
}
func (s *s3DocumentStorage) Delete(ctx context.Context, key string) error {
if strings.TrimSpace(key) == "" {
return nil
}
_, err := s.client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(key),
})
return err
}
func (s *s3DocumentStorage) URL(key string) string {
key = strings.TrimPrefix(strings.TrimSpace(key), "/")
if key == "" {
return s.base
}
if s.base == "" {
return key
}
return fmt.Sprintf("%s/%s", s.base, key)
}
+19 -6
View File
@@ -505,12 +505,25 @@ func (s *fifoService) fetchStockLots(ctx context.Context, tx *gorm.DB, productWa
var lots []stockLot var lots []stockLot
for key, cfg := range configs { for key, cfg := range configs {
selectStmt := fmt.Sprintf(
"%s AS id, %s AS available_qty, %s AS created_at", usesNumericTime := cfg.Columns.CreatedAt == cfg.Columns.ID
cfg.Columns.ID,
fmt.Sprintf("%s - COALESCE(%s,0)", cfg.Columns.TotalQuantity, cfg.Columns.TotalUsedQuantity), var selectStmt string
cfg.Columns.CreatedAt, if usesNumericTime {
)
selectStmt = fmt.Sprintf(
"%s AS id, %s AS available_qty, '1970-01-01 00:00:00 UTC'::timestamp AS created_at",
cfg.Columns.ID,
fmt.Sprintf("%s - COALESCE(%s,0)", cfg.Columns.TotalQuantity, cfg.Columns.TotalUsedQuantity),
)
} else {
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 { var rows []struct {
ID uint ID uint
+18
View File
@@ -65,6 +65,14 @@ var (
SSOUserSyncDrift time.Duration SSOUserSyncDrift time.Duration
SSOUserSyncNonceTTL time.Duration SSOUserSyncNonceTTL time.Duration
SSOUserSyncMaxBodyBytes int SSOUserSyncMaxBodyBytes int
S3Endpoint string
S3Region string
S3Bucket string
S3AccessKey string
S3SecretKey string
S3ForcePathStyle bool
S3PublicBaseURL string
S3DocumentKeyPrefix string
) )
func init() { func init() {
@@ -106,6 +114,16 @@ func init() {
// Redis // Redis
RedisURL = viper.GetString("REDIS_URL") RedisURL = viper.GetString("REDIS_URL")
// Object storage
S3Endpoint = strings.TrimSpace(viper.GetString("S3_ENDPOINT"))
S3Region = strings.TrimSpace(viper.GetString("S3_REGION"))
S3Bucket = strings.TrimSpace(viper.GetString("S3_BUCKET"))
S3AccessKey = strings.TrimSpace(viper.GetString("S3_ACCESS_KEY"))
S3SecretKey = strings.TrimSpace(viper.GetString("S3_SECRET_KEY"))
S3ForcePathStyle = viper.GetBool("S3_FORCE_PATH_STYLE")
S3PublicBaseURL = strings.TrimSuffix(strings.TrimSpace(viper.GetString("S3_PUBLIC_BASE_URL")), "/")
S3DocumentKeyPrefix = defaultString(strings.Trim(strings.TrimSpace(viper.GetString("S3_DOCUMENT_PREFIX")), "/"), "docs")
// SSO integration // SSO integration
SSOIssuer = viper.GetString("SSO_ISSUER") SSOIssuer = viper.GetString("SSO_ISSUER")
SSOJWKSURL = viper.GetString("SSO_JWKS_URL") SSOJWKSURL = viper.GetString("SSO_JWKS_URL")
@@ -29,7 +29,7 @@ ADD CONSTRAINT fk_project_chickins_kandang FOREIGN KEY (project_flock_kandang_id
-- Relasi ke product_warehouses -- Relasi ke product_warehouses
ALTER TABLE project_chickins ALTER TABLE project_chickins
ADD CONSTRAINT fk_project_chickins_warehouse FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses (id) ON DELETE RESTRICT ON UPDATE CASCADE; ADD CONSTRAINT fk_project_chickins_warehouse FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses (id) ON DELETE CASCADE ON UPDATE CASCADE;
-- Relasi ke users -- Relasi ke users
ALTER TABLE project_chickins ALTER TABLE project_chickins
@@ -0,0 +1,3 @@
DROP INDEX IF EXISTS idx_project_flock_kandangs_closed_at;
ALTER TABLE project_flock_kandangs
DROP COLUMN IF EXISTS closed_at;
@@ -0,0 +1,5 @@
ALTER TABLE project_flock_kandangs
ADD COLUMN IF NOT EXISTS closed_at TIMESTAMPTZ;
CREATE INDEX IF NOT EXISTS idx_project_flock_kandangs_closed_at
ON project_flock_kandangs (closed_at);
@@ -0,0 +1,2 @@
ALTER TABLE products
DROP COLUMN IF EXISTS is_visible;
@@ -0,0 +1,2 @@
ALTER TABLE products
ADD COLUMN IF NOT EXISTS is_visible BOOLEAN NOT NULL DEFAULT TRUE;
@@ -0,0 +1,2 @@
DROP INDEX IF EXISTS documents_documentable_polymorphic;
DROP TABLE IF EXISTS documents;
@@ -0,0 +1,14 @@
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
documentable_type VARCHAR(50) NOT NULL,
documentable_id BIGINT NOT NULL,
type VARCHAR(50) NOT NULL,
path VARCHAR(50) NOT NULL,
name VARCHAR(50) NOT NULL,
ext VARCHAR(50) NOT NULL,
size NUMERIC(15, 3) NOT NULL,
created_by BIGINT REFERENCES users (id) ON DELETE RESTRICT ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX documents_documentable_polymorphic ON documents (documentable_type, documentable_id);
@@ -0,0 +1,35 @@
BEGIN;
-- Drop new indexes and FK
DROP INDEX IF EXISTS idx_product_warehouses_project_flock_kandang_id;
DROP INDEX IF EXISTS idx_product_warehouses_unique;
ALTER TABLE product_warehouses
DROP CONSTRAINT IF EXISTS fk_product_warehouses_project_flock_kandang_id,
ALTER COLUMN project_flock_kandang_id DROP NOT NULL,
DROP COLUMN IF EXISTS project_flock_kandang_id;
-- Revert qty to integer quantity
ALTER TABLE product_warehouses
RENAME COLUMN qty TO quantity;
ALTER TABLE product_warehouses
ALTER COLUMN quantity TYPE INTEGER USING quantity::integer,
ALTER COLUMN quantity SET DEFAULT 0,
ALTER COLUMN quantity SET NOT NULL;
-- Restore audit/soft-delete columns
ALTER TABLE product_warehouses
ADD COLUMN IF NOT EXISTS created_by BIGINT REFERENCES users (id),
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW(),
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW(),
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
-- Recreate prior indexes
CREATE INDEX IF NOT EXISTS idx_product_warehouses_deleted_at ON product_warehouses (deleted_at);
CREATE UNIQUE INDEX IF NOT EXISTS idx_product_warehouses_unique
ON product_warehouses (product_id, warehouse_id)
WHERE deleted_at IS NULL;
COMMIT;
@@ -0,0 +1,41 @@
BEGIN;
-- Drop indexes that depend on deleted_at or old uniqueness
DROP INDEX IF EXISTS idx_product_warehouses_deleted_at;
DROP INDEX IF EXISTS idx_product_warehouses_unique;
-- Add new relation and adjust quantity column
ALTER TABLE product_warehouses
ADD COLUMN IF NOT EXISTS project_flock_kandang_id BIGINT;
ALTER TABLE product_warehouses
RENAME COLUMN quantity TO qty;
-- Enforce numeric quantity with precision and default
ALTER TABLE product_warehouses
ALTER COLUMN qty TYPE NUMERIC(15, 3) USING qty::numeric(15, 3),
ALTER COLUMN qty SET DEFAULT 0,
ALTER COLUMN qty SET NOT NULL;
-- Remove audit/soft-delete columns no longer used
ALTER TABLE product_warehouses
DROP COLUMN IF EXISTS created_by,
DROP COLUMN IF EXISTS created_at,
DROP COLUMN IF EXISTS updated_at,
DROP COLUMN IF EXISTS deleted_at;
-- Enforce FK and not-null for project_flock_kandang_id
ALTER TABLE product_warehouses
ADD CONSTRAINT fk_product_warehouses_project_flock_kandang_id
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs (id)
ON DELETE RESTRICT ON UPDATE CASCADE;
-- New indexes
CREATE INDEX IF NOT EXISTS idx_product_warehouses_project_flock_kandang_id
ON product_warehouses (project_flock_kandang_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_product_warehouses_unique
ON product_warehouses (product_id, warehouse_id, project_flock_kandang_id);
COMMIT;
@@ -0,0 +1,44 @@
BEGIN;
-- Drop new indexes
DROP INDEX IF EXISTS stock_logs_loggable_type_loggable_id_idx;
DROP INDEX IF EXISTS stock_logs_product_warehouse_id_idx;
DROP INDEX IF EXISTS stock_logs_created_by_idx;
DROP INDEX IF EXISTS stock_logs_created_at_idx;
-- Restore obsolete columns
ALTER TABLE stock_logs
ADD COLUMN IF NOT EXISTS transaction_type VARCHAR(20) DEFAULT '' NOT NULL,
ADD COLUMN IF NOT EXISTS quantity NUMERIC(15, 3) DEFAULT 0 NOT NULL,
ADD COLUMN IF NOT EXISTS before_quantity NUMERIC(15, 3) DEFAULT 0 NOT NULL,
ADD COLUMN IF NOT EXISTS after_quantity NUMERIC(15, 3) DEFAULT 0 NOT NULL,
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW(),
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
-- Rename columns back
ALTER TABLE stock_logs
RENAME COLUMN loggable_type TO log_type;
ALTER TABLE stock_logs
RENAME COLUMN loggable_id TO log_id;
ALTER TABLE stock_logs
RENAME COLUMN notes TO note;
-- Drop new columns
ALTER TABLE stock_logs
DROP COLUMN IF EXISTS increase,
DROP COLUMN IF EXISTS decrease;
-- Restore indexes for old structure
CREATE INDEX IF NOT EXISTS stock_logs_product_warehouse_id_idx ON stock_logs (product_warehouse_id);
CREATE INDEX IF NOT EXISTS stock_logs_log_type_log_id_idx ON stock_logs (log_type, log_id);
CREATE INDEX IF NOT EXISTS stock_logs_created_by_idx ON stock_logs (created_by);
CREATE INDEX IF NOT EXISTS stock_logs_created_at_idx ON stock_logs (created_at);
CREATE INDEX IF NOT EXISTS stock_logs_deleted_at_idx ON stock_logs (deleted_at);
COMMIT;
@@ -0,0 +1,50 @@
BEGIN;
-- Drop old indexes tied to removed columns
DROP INDEX IF EXISTS stock_logs_log_type_log_id_idx;
DROP INDEX IF EXISTS stock_logs_deleted_at_idx;
-- Rename columns to new naming
ALTER TABLE stock_logs
RENAME COLUMN log_type TO loggable_type;
ALTER TABLE stock_logs
RENAME COLUMN log_id TO loggable_id;
ALTER TABLE stock_logs
RENAME COLUMN note TO notes;
-- Add new increase/decrease columns
ALTER TABLE stock_logs
ADD COLUMN IF NOT EXISTS increase NUMERIC(15, 3) DEFAULT 0,
ADD COLUMN IF NOT EXISTS decrease NUMERIC(15, 3) DEFAULT 0;
-- Adjust column definitions
ALTER TABLE stock_logs
ALTER COLUMN loggable_type TYPE VARCHAR(50),
ALTER COLUMN loggable_type SET NOT NULL,
ALTER COLUMN loggable_id SET NOT NULL,
ALTER COLUMN increase SET DEFAULT 0,
ALTER COLUMN increase SET NOT NULL,
ALTER COLUMN decrease SET DEFAULT 0,
ALTER COLUMN decrease SET NOT NULL;
-- Remove obsolete columns
ALTER TABLE stock_logs
DROP COLUMN IF EXISTS transaction_type,
DROP COLUMN IF EXISTS quantity,
DROP COLUMN IF EXISTS before_quantity,
DROP COLUMN IF EXISTS after_quantity,
DROP COLUMN IF EXISTS updated_at,
DROP COLUMN IF EXISTS deleted_at;
-- Recreate indexes for new structure
CREATE INDEX IF NOT EXISTS stock_logs_product_warehouse_id_idx ON stock_logs (product_warehouse_id);
CREATE INDEX IF NOT EXISTS stock_logs_loggable_type_loggable_id_idx ON stock_logs (loggable_type, loggable_id);
CREATE INDEX IF NOT EXISTS stock_logs_created_by_idx ON stock_logs (created_by);
CREATE INDEX IF NOT EXISTS stock_logs_created_at_idx ON stock_logs (created_at);
COMMIT;
@@ -0,0 +1,33 @@
BEGIN;
-- Remove grading details from recording_eggs
ALTER TABLE recording_eggs
DROP CONSTRAINT IF EXISTS chk_recording_eggs_qty;
ALTER TABLE recording_eggs
DROP COLUMN IF EXISTS weight;
ALTER TABLE recording_eggs
ADD CONSTRAINT chk_recording_eggs_qty CHECK (qty >= 0);
-- Restore grading_eggs table for rollback scenarios
CREATE TABLE grading_eggs (
id BIGSERIAL PRIMARY KEY,
recording_egg_id BIGINT NOT NULL,
qty NUMERIC(15,3) NOT NULL,
grade VARCHAR,
created_by BIGINT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT fk_grading_eggs_recording_egg
FOREIGN KEY (recording_egg_id) REFERENCES recording_eggs(id) ON DELETE CASCADE,
CONSTRAINT fk_grading_eggs_created_by
FOREIGN KEY (created_by) REFERENCES users(id),
CONSTRAINT chk_grading_eggs_qty CHECK (qty >= 0)
);
CREATE INDEX idx_grading_eggs_recording_egg
ON grading_eggs (recording_egg_id);
COMMIT;
@@ -0,0 +1,18 @@
BEGIN;
-- Remove separate grading table and move grading details into recording_eggs
DROP INDEX IF EXISTS idx_grading_eggs_recording_egg;
DROP TABLE IF EXISTS grading_eggs;
ALTER TABLE recording_eggs
ADD COLUMN IF NOT EXISTS weight NUMERIC(10,3);
ALTER TABLE recording_eggs
DROP CONSTRAINT IF EXISTS chk_recording_eggs_qty;
ALTER TABLE recording_eggs
ADD CONSTRAINT chk_recording_eggs_qty CHECK (
qty >= 0 AND (weight IS NULL OR weight >= 0)
);
COMMIT;
@@ -0,0 +1,38 @@
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'fk_purchase_items_expense_nonstock'
) THEN
ALTER TABLE purchase_items
DROP CONSTRAINT fk_purchase_items_expense_nonstock;
END IF;
IF EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'fk_purchase_items_project_flock_kandang'
) THEN
ALTER TABLE purchase_items
DROP CONSTRAINT fk_purchase_items_project_flock_kandang;
END IF;
END $$;
DROP INDEX IF EXISTS idx_purchase_items_expense_nonstock_id;
DROP INDEX IF EXISTS idx_purchase_items_project_flock_kandang_id;
ALTER TABLE purchase_items
DROP COLUMN IF EXISTS expense_nonstock_id,
DROP COLUMN IF EXISTS project_flock_kandang_id,
ALTER COLUMN vehicle_number DROP NOT NULL,
ALTER COLUMN vehicle_number TYPE VARCHAR USING vehicle_number;
ALTER TABLE purchases
ALTER COLUMN pr_number TYPE VARCHAR USING pr_number,
ALTER COLUMN po_number TYPE VARCHAR USING po_number,
ALTER COLUMN created_at DROP DEFAULT,
ALTER COLUMN updated_at DROP DEFAULT;
ALTER TABLE purchases
ADD COLUMN credit_term INT NOT NULL DEFAULT 0,
ADD COLUMN grand_total NUMERIC(15, 3) NOT NULL DEFAULT 0;
ALTER TABLE purchases
ALTER COLUMN credit_term DROP DEFAULT,
ALTER COLUMN grand_total DROP DEFAULT;
@@ -0,0 +1,57 @@
-- Adjust purchases table to new purchasing schema
ALTER TABLE purchases
ALTER COLUMN pr_number TYPE VARCHAR(50) USING LEFT(pr_number, 50),
ALTER COLUMN po_number TYPE VARCHAR(50) USING LEFT(po_number, 50),
ALTER COLUMN created_at SET DEFAULT now(),
ALTER COLUMN updated_at SET DEFAULT now();
ALTER TABLE purchases
DROP COLUMN IF EXISTS credit_term,
DROP COLUMN IF EXISTS grand_total;
-- Bring purchase_items in line with new requirements
ALTER TABLE purchase_items
ADD COLUMN IF NOT EXISTS expense_nonstock_id BIGINT,
ADD COLUMN IF NOT EXISTS project_flock_kandang_id BIGINT;
UPDATE purchase_items
SET vehicle_number = ''
WHERE vehicle_number IS NULL;
ALTER TABLE purchase_items
ALTER COLUMN vehicle_number TYPE VARCHAR(10) USING LEFT(vehicle_number, 10),
ALTER COLUMN vehicle_number SET NOT NULL;
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'expense_nonstocks') THEN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'fk_purchase_items_expense_nonstock'
) THEN
EXECUTE
'ALTER TABLE purchase_items
ADD CONSTRAINT fk_purchase_items_expense_nonstock
FOREIGN KEY (expense_nonstock_id)
REFERENCES expense_nonstocks(id)
ON DELETE SET NULL ON UPDATE CASCADE';
END IF;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'fk_purchase_items_project_flock_kandang'
) THEN
EXECUTE
'ALTER TABLE purchase_items
ADD CONSTRAINT fk_purchase_items_project_flock_kandang
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs(id)
ON DELETE SET NULL ON UPDATE CASCADE';
END IF;
END IF;
END $$;
CREATE INDEX IF NOT EXISTS idx_purchase_items_expense_nonstock_id
ON purchase_items (expense_nonstock_id);
CREATE INDEX IF NOT EXISTS idx_purchase_items_project_flock_kandang_id
ON purchase_items (project_flock_kandang_id);
@@ -0,0 +1,3 @@
-- Drop function and sequence for sales order numbers
DROP FUNCTION IF EXISTS generate_so_number();
DROP SEQUENCE IF EXISTS so_number_seq;
@@ -0,0 +1,12 @@
-- Create sequence for sales order numbers
CREATE SEQUENCE so_number_seq START WITH 1 INCREMENT BY 1;
CREATE OR REPLACE FUNCTION generate_so_number()
RETURNS VARCHAR AS $$
DECLARE
next_val INTEGER;
BEGIN
next_val := nextval('so_number_seq');
RETURN 'SO-' || LPAD(next_val::TEXT, 5, '0');
END;
$$ LANGUAGE plpgsql;
@@ -0,0 +1,2 @@
ALTER TABLE purchases
DROP COLUMN IF EXISTS credit_term;
@@ -0,0 +1,5 @@
ALTER TABLE purchases
ADD COLUMN IF NOT EXISTS credit_term INT NOT NULL DEFAULT 0;
ALTER TABLE purchases
ALTER COLUMN credit_term DROP DEFAULT;
@@ -0,0 +1,3 @@
DROP INDEX IF EXISTS idx_payments_bank_id;
DROP INDEX IF EXISTS payments_party_polymorphic;
DROP TABLE IF EXISTS payments;
@@ -0,0 +1,22 @@
CREATE TABLE IF NOT EXISTS payments (
id BIGSERIAL PRIMARY KEY,
payment_code VARCHAR(50) NOT NULL,
reference_number VARCHAR(100) NULL,
transaction_type VARCHAR(50),
party_type VARCHAR(50) NOT NULL,
party_id BIGINT NOT NULL,
payment_date TIMESTAMPTZ NOT NULL,
payment_method VARCHAR(20) NOT NULL,
bank_id BIGINT NULL REFERENCES banks(id) ON DELETE RESTRICT ON UPDATE CASCADE,
direction VARCHAR(5) NOT NULL,
nominal NUMERIC(15, 3) NOT NULL,
notes TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ NULL,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
);
-- Indexes
CREATE INDEX payments_party_polymorphic ON payments (party_type, party_id);
CREATE INDEX idx_payments_bank_id ON payments (bank_id);
@@ -0,0 +1,18 @@
DO $$
DECLARE
r record;
trigger_name text;
BEGIN
FOR r IN
SELECT table_schema, table_name
FROM information_schema.columns
WHERE column_name = 'deleted_at'
AND table_schema = 'public'
GROUP BY table_schema, table_name
LOOP
trigger_name := format('trg_soft_delete_fk_%s', r.table_name);
EXECUTE format('DROP TRIGGER IF EXISTS %I ON %I.%I', trigger_name, r.table_schema, r.table_name);
END LOOP;
END $$;
DROP FUNCTION IF EXISTS soft_delete_handle_fk();
@@ -0,0 +1,126 @@
CREATE OR REPLACE FUNCTION soft_delete_handle_fk() RETURNS TRIGGER AS $$
DECLARE
fk record;
child_column text;
parent_column text;
parent_value text;
child_has_deleted_at boolean;
ref_exists boolean;
sql text;
BEGIN
IF OLD.deleted_at IS NULL AND NEW.deleted_at IS NOT NULL THEN
FOR fk IN
SELECT conrelid::regclass AS child_table,
conkey AS child_cols,
confkey AS parent_cols,
confdeltype
FROM pg_constraint
WHERE contype = 'f'
AND confrelid = TG_RELID
LOOP
IF array_length(fk.child_cols, 1) IS DISTINCT FROM 1
OR array_length(fk.parent_cols, 1) IS DISTINCT FROM 1 THEN
RAISE NOTICE 'soft_delete_handle_fk skipped composite fk on %', fk.child_table;
CONTINUE;
END IF;
SELECT attname INTO child_column
FROM pg_attribute
WHERE attrelid = fk.child_table
AND attnum = fk.child_cols[1]
AND NOT attisdropped;
SELECT attname INTO parent_column
FROM pg_attribute
WHERE attrelid = TG_RELID
AND attnum = fk.parent_cols[1]
AND NOT attisdropped;
EXECUTE format('SELECT ($1).%I', parent_column)
INTO parent_value
USING OLD;
SELECT EXISTS (
SELECT 1
FROM pg_attribute
WHERE attrelid = fk.child_table
AND attname = 'deleted_at'
AND NOT attisdropped
) INTO child_has_deleted_at;
IF fk.confdeltype IN ('r', 'a') THEN
sql := format(
'SELECT EXISTS (SELECT 1 FROM %s WHERE %I = $1 %s)',
fk.child_table,
child_column,
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
);
EXECUTE sql INTO ref_exists USING parent_value;
IF ref_exists THEN
RAISE EXCEPTION 'Cannot soft delete %, still referenced by %',
TG_TABLE_NAME, fk.child_table;
END IF;
ELSIF fk.confdeltype = 'n' THEN
sql := format(
'UPDATE %s SET %I = NULL WHERE %I = $1 %s',
fk.child_table,
child_column,
child_column,
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
);
EXECUTE sql USING parent_value;
ELSIF fk.confdeltype = 'c' THEN
IF child_has_deleted_at THEN
sql := format(
'UPDATE %s SET deleted_at = NOW() WHERE %I = $1 AND deleted_at IS NULL',
fk.child_table,
child_column
);
EXECUTE sql USING parent_value;
ELSE
sql := format(
'DELETE FROM %s WHERE %I = $1',
fk.child_table,
child_column
);
EXECUTE sql USING parent_value;
END IF;
ELSIF fk.confdeltype = 'd' THEN
sql := format(
'UPDATE %s SET %I = DEFAULT WHERE %I = $1 %s',
fk.child_table,
child_column,
child_column,
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
);
EXECUTE sql USING parent_value;
END IF;
END LOOP;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DO $$
DECLARE
r record;
trigger_name text;
BEGIN
FOR r IN
SELECT table_schema, table_name
FROM information_schema.columns
WHERE column_name = 'deleted_at'
AND table_schema = 'public'
GROUP BY table_schema, table_name
LOOP
trigger_name := format('trg_soft_delete_fk_%s', r.table_name);
EXECUTE format('DROP TRIGGER IF EXISTS %I ON %I.%I', trigger_name, r.table_schema, r.table_name);
EXECUTE format(
'CREATE TRIGGER %I BEFORE UPDATE OF deleted_at ON %I.%I FOR EACH ROW EXECUTE FUNCTION soft_delete_handle_fk()',
trigger_name,
r.table_schema,
r.table_name
);
END LOOP;
END $$;
@@ -0,0 +1 @@
DROP SEQUENCE IF EXISTS payments_code_seq;
@@ -0,0 +1 @@
CREATE SEQUENCE IF NOT EXISTS payments_code_seq START WITH 1 INCREMENT BY 1;
@@ -0,0 +1,3 @@
-- Rollback: restore document columns to expenses table
ALTER TABLE expenses ADD COLUMN IF NOT EXISTS document_path JSON;
ALTER TABLE expenses ADD COLUMN IF NOT EXISTS realization_document_path JSON;
@@ -0,0 +1,3 @@
-- Delete document columns from expenses table since we now use Document service with polymorphic relations
ALTER TABLE expenses DROP COLUMN IF EXISTS document_path;
ALTER TABLE expenses DROP COLUMN IF EXISTS realization_document_path;
@@ -0,0 +1,28 @@
-- ============================================
-- Rollback: Remove FIFO fields and restore qty column
-- ============================================
-- STEP 1: Drop indexes
DROP INDEX IF EXISTS idx_marketing_delivery_products_fifo_lookup;
DROP INDEX IF EXISTS idx_marketing_delivery_products_pending_qty;
DROP INDEX IF EXISTS idx_marketing_delivery_products_usage_qty;
DROP INDEX IF EXISTS idx_marketing_delivery_products_created_at;
-- STEP 2: Drop constraints
ALTER TABLE marketing_delivery_products
DROP CONSTRAINT IF EXISTS chk_marketing_delivery_products_fifo_nonneg;
-- STEP 3: Restore qty column from usage_qty data
ALTER TABLE marketing_delivery_products
ADD COLUMN IF NOT EXISTS qty NUMERIC(15, 3) DEFAULT 0 NOT NULL;
-- Migrate data back from usage_qty to qty
UPDATE marketing_delivery_products
SET qty = usage_qty
WHERE qty = 0;
-- STEP 4: Drop FIFO columns
ALTER TABLE marketing_delivery_products
DROP COLUMN IF EXISTS usage_qty,
DROP COLUMN IF EXISTS pending_qty,
DROP COLUMN IF EXISTS created_at;
@@ -0,0 +1,58 @@
-- ============================================
-- Add FIFO fields to marketing_delivery_products
-- This migration adds fields needed for FIFO stock management
-- and removes the old qty field in favor of FIFO-based allocation
-- ============================================
-- STEP 0: Drop orphan indexes from previous migration
DROP INDEX IF EXISTS idx_marketing_delivery_products_deleted_at;
-- STEP 1: Add created_at column (required for FIFO ordering)
ALTER TABLE marketing_delivery_products
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW();
-- STEP 2: Add FIFO tracking fields
ALTER TABLE marketing_delivery_products
ADD COLUMN IF NOT EXISTS usage_qty NUMERIC(15, 3) DEFAULT 0,
ADD COLUMN IF NOT EXISTS pending_qty NUMERIC(15, 3) DEFAULT 0;
-- STEP 3: Migrate data from old qty to usage_qty for existing records
-- This preserves existing quantity data as allocated quantity
UPDATE marketing_delivery_products
SET
usage_qty = COALESCE(qty, 0),
pending_qty = 0
WHERE usage_qty = 0;
-- STEP 4: Drop the old qty column (replaced by usage_qty + pending_qty)
ALTER TABLE marketing_delivery_products
DROP COLUMN IF EXISTS qty;
-- STEP 5: Make FIFO fields NOT NULL
ALTER TABLE marketing_delivery_products
ALTER COLUMN usage_qty SET NOT NULL,
ALTER COLUMN pending_qty SET NOT NULL,
ALTER COLUMN created_at SET NOT NULL;
-- STEP 6: Add constraints to ensure non-negative values
ALTER TABLE marketing_delivery_products
ADD CONSTRAINT chk_marketing_delivery_products_fifo_nonneg CHECK (
usage_qty >= 0 AND
pending_qty >= 0
);
-- STEP 7: Create indexes for FIFO operations
CREATE INDEX IF NOT EXISTS idx_marketing_delivery_products_created_at
ON marketing_delivery_products(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_marketing_delivery_products_usage_qty
ON marketing_delivery_products(usage_qty)
WHERE usage_qty > 0;
CREATE INDEX IF NOT EXISTS idx_marketing_delivery_products_pending_qty
ON marketing_delivery_products(pending_qty)
WHERE pending_qty > 0;
-- Composite index for FIFO lookups
CREATE INDEX IF NOT EXISTS idx_marketing_delivery_products_fifo_lookup
ON marketing_delivery_products(marketing_product_id, created_at DESC);
@@ -0,0 +1,7 @@
-- Remove foreign key constraint
ALTER TABLE marketing_delivery_products
DROP CONSTRAINT IF EXISTS fk_marketing_delivery_products_product_warehouse;
-- Drop product_warehouse_id column
ALTER TABLE marketing_delivery_products
DROP COLUMN IF EXISTS product_warehouse_id;
@@ -0,0 +1,19 @@
-- Add product_warehouse_id column to marketing_delivery_products
ALTER TABLE marketing_delivery_products
ADD COLUMN IF NOT EXISTS product_warehouse_id INT NOT NULL DEFAULT 0;
-- Fill product_warehouse_id from marketing_products
UPDATE marketing_delivery_products mdp
SET product_warehouse_id = mp.product_warehouse_id
FROM marketing_products mp
WHERE mdp.marketing_product_id = mp.id
AND mdp.product_warehouse_id = 0;
-- Set NOT NULL constraint
ALTER TABLE marketing_delivery_products
ALTER COLUMN product_warehouse_id SET NOT NULL;
-- Add foreign key constraint
ALTER TABLE marketing_delivery_products
ADD CONSTRAINT fk_marketing_delivery_products_product_warehouse
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id);
@@ -0,0 +1,10 @@
-- Drop indexes
DROP INDEX IF EXISTS idx_standard_growth_details_standard_week;
DROP INDEX IF EXISTS idx_production_standard_details_standard_week;
DROP INDEX IF EXISTS idx_production_standards_project_category;
DROP INDEX IF EXISTS idx_production_standards_deleted_at;
-- Drop tables (in reverse order due to foreign keys)
DROP TABLE IF EXISTS standard_growth_details;
DROP TABLE IF EXISTS production_standard_details;
DROP TABLE IF EXISTS production_standards;
@@ -0,0 +1,96 @@
-- Create production_standards table
CREATE TABLE IF NOT EXISTS production_standards (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) UNIQUE NOT NULL,
project_category VARCHAR(20) NOT NULL CHECK (project_category IN ('GROWING', 'LAYING')),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT
);
-- Create index for deleted_at (soft delete)
CREATE INDEX idx_production_standards_deleted_at ON production_standards(deleted_at);
-- Tambahkan Foreign Key ke users
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
ALTER TABLE production_standards
ADD CONSTRAINT fk_production_standards_created_by
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE;
END IF;
END $$;
-- Index
CREATE INDEX idx_production_standards_created_by ON production_standards(created_by);
-- Create production_standard_details table
CREATE TABLE IF NOT EXISTS production_standard_details (
id BIGSERIAL PRIMARY KEY,
production_standard_id BIGINT NOT NULL,
week INT NOT NULL,
target_hen_day_production NUMERIC(15, 3),
target_hen_house_production NUMERIC(15, 3),
target_egg_weight NUMERIC(15, 3),
target_egg_mass NUMERIC(15, 3),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Tambahkan Foreign Key ke production_standards
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'production_standards') THEN
ALTER TABLE production_standard_details
ADD CONSTRAINT fk_production_standard_details_standard
FOREIGN KEY (production_standard_id) REFERENCES production_standards(id) ON DELETE CASCADE;
END IF;
END $$;
-- Create unique constraint for standard_id + week
CREATE UNIQUE INDEX idx_production_standard_details_standard_week
ON production_standard_details(production_standard_id, week);
-- Create standard_growth_details table
CREATE TABLE IF NOT EXISTS standard_growth_details (
id BIGSERIAL PRIMARY KEY,
production_standard_id BIGINT NOT NULL,
target_mean_bw NUMERIC(15, 3),
max_depletion NUMERIC(15, 3),
min_uniformity NUMERIC(15, 3) NOT NULL,
week INT NOT NULL,
feed_intake NUMERIC(15, 3),
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by BIGINT
);
-- Tambahkan Foreign Key ke production_standards
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'production_standards') THEN
ALTER TABLE standard_growth_details
ADD CONSTRAINT fk_standard_growth_details_standard
FOREIGN KEY (production_standard_id) REFERENCES production_standards(id) ON DELETE CASCADE;
END IF;
END $$;
-- Tambahkan Foreign Key ke users
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
ALTER TABLE standard_growth_details
ADD CONSTRAINT fk_standard_growth_details_created_by
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE;
END IF;
END $$;
-- Create unique constraint for standard_id + week
CREATE UNIQUE INDEX idx_standard_growth_details_standard_week
ON standard_growth_details(production_standard_id, week);
-- Index
CREATE INDEX idx_standard_growth_details_created_by ON standard_growth_details(created_by);
-- Create index for project_category
CREATE INDEX idx_production_standards_project_category ON production_standards(project_category);
@@ -0,0 +1,24 @@
-- Rollback: Update expense and expense_nonstocks tables
-- Drop indexes
DROP INDEX IF EXISTS idx_expenses_project_flock_id;
DROP INDEX IF EXISTS idx_expenses_location_id;
-- Drop Foreign Key constraint
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'fk_expenses_location_id'
) THEN
ALTER TABLE expenses
DROP CONSTRAINT fk_expenses_location_id;
END IF;
END $$;
-- Drop columns from expenses table
ALTER TABLE expenses
DROP COLUMN IF EXISTS project_flock_id;
ALTER TABLE expenses
DROP COLUMN IF EXISTS location_id;
@@ -0,0 +1,29 @@
-- Migration: Update expense and expense_nonstocks tables
-- Add location_id column to expenses table
ALTER TABLE expenses
ADD COLUMN IF NOT EXISTS location_id BIGINT NOT NULL DEFAULT 1;
-- Add project_flock_id column to expenses table (JSON type)
ALTER TABLE expenses
ADD COLUMN IF NOT EXISTS project_flock_id JSON NULL;
-- Add Foreign Key constraint to locations table
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'locations') THEN
ALTER TABLE expenses
ADD CONSTRAINT fk_expenses_location_id
FOREIGN KEY (location_id) REFERENCES locations(id) ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
END $$;
-- Create index for location_id
CREATE INDEX IF NOT EXISTS idx_expenses_location_id ON expenses (location_id);
-- Create index for project_flock_id
CREATE INDEX IF NOT EXISTS idx_expenses_project_flock_id ON expenses ((project_flock_id::text));
-- Ensure kandang_id is nullable in expense_nonstocks table
ALTER TABLE expense_nonstocks
ALTER COLUMN kandang_id DROP NOT NULL;
@@ -0,0 +1,6 @@
DROP INDEX IF EXISTS idx_project_flock_kandang_uniformity_deleted_at;
DROP INDEX IF EXISTS idx_project_flock_kandang_uniformity_created_by;
DROP INDEX IF EXISTS idx_project_flock_kandang_uniformity_project_flock_kandang_week;
DROP INDEX IF EXISTS idx_project_flock_kandang_uniformity_project_flock_kandang_id;
DROP TABLE IF EXISTS project_flock_kandang_uniformity;
@@ -0,0 +1,58 @@
CREATE TABLE IF NOT EXISTS project_flock_kandang_uniformity (
id BIGSERIAL PRIMARY KEY,
uniformity NUMERIC(15, 3),
week INT NOT NULL,
cv NUMERIC(15, 3),
chick_qty_of_weight NUMERIC(15, 3),
mean_up NUMERIC(15, 3),
mean_down NUMERIC(15, 3),
project_flock_kandang_id BIGINT NOT NULL,
uniform_qty NUMERIC(15, 3),
not_uniform_qty NUMERIC(15, 3),
uniform_date TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT NOT NULL
);
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'fk_project_flock_kandang_uniformity_project_flock_kandang'
) THEN
EXECUTE
'ALTER TABLE project_flock_kandang_uniformity
ADD CONSTRAINT fk_project_flock_kandang_uniformity_project_flock_kandang
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs(id)
ON DELETE RESTRICT ON UPDATE CASCADE';
END IF;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'fk_project_flock_kandang_uniformity_created_by'
) THEN
EXECUTE
'ALTER TABLE project_flock_kandang_uniformity
ADD CONSTRAINT fk_project_flock_kandang_uniformity_created_by
FOREIGN KEY (created_by)
REFERENCES users(id)
ON DELETE RESTRICT ON UPDATE CASCADE';
END IF;
END IF;
END $$;
CREATE INDEX IF NOT EXISTS idx_project_flock_kandang_uniformity_project_flock_kandang_id
ON project_flock_kandang_uniformity (project_flock_kandang_id);
CREATE INDEX IF NOT EXISTS idx_project_flock_kandang_uniformity_project_flock_kandang_week
ON project_flock_kandang_uniformity (project_flock_kandang_id, week);
CREATE INDEX IF NOT EXISTS idx_project_flock_kandang_uniformity_created_by
ON project_flock_kandang_uniformity (created_by);
CREATE INDEX IF NOT EXISTS idx_project_flock_kandang_uniformity_deleted_at
ON project_flock_kandang_uniformity (deleted_at);
@@ -0,0 +1,42 @@
-- ===============================================================
-- ROLLBACK: Remove FIFO fields from STOCK_TRANSFER_DETAILS
-- ===============================================================
-- Drop indexes
DROP INDEX IF EXISTS idx_stock_transfer_details_dest_pw;
DROP INDEX IF EXISTS idx_stock_transfer_details_source_pw;
-- Drop foreign keys
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'fk_stock_transfer_details_source_pw'
) THEN
EXECUTE 'ALTER TABLE stock_transfer_details
DROP CONSTRAINT fk_stock_transfer_details_source_pw';
END IF;
IF EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'fk_stock_transfer_details_dest_pw'
) THEN
EXECUTE 'ALTER TABLE stock_transfer_details
DROP CONSTRAINT fk_stock_transfer_details_dest_pw';
END IF;
END $$;
-- Drop FIFO columns
ALTER TABLE stock_transfer_details
DROP COLUMN IF EXISTS total_used,
DROP COLUMN IF EXISTS total_qty,
DROP COLUMN IF EXISTS pending_qty,
DROP COLUMN IF EXISTS usage_qty,
DROP COLUMN IF EXISTS dest_product_warehouse_id,
DROP COLUMN IF EXISTS source_product_warehouse_id;
-- Restore original columns (in case rollback)
ALTER TABLE stock_transfer_details
ADD COLUMN IF NOT EXISTS quantity NUMERIC(15, 3) NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS before_quantity NUMERIC(15, 3),
ADD COLUMN IF NOT EXISTS after_quantity NUMERIC(15, 3);
@@ -0,0 +1,83 @@
-- ===============================================================
-- ADD FIFO FIELDS TO STOCK_TRANSFER_DETAILS
-- Enable transfer module to work with FIFO stock system
--
-- Notes:
-- - Field 'quantity' will be removed (replaced by usage_qty + pending_qty)
-- - Fields 'before_quantity' & 'after_quantity' will be removed (unused legacy)
-- - New FIFO fields track actual allocation instead of requested quantity
-- ===============================================================
-- Add FIFO tracking fields
ALTER TABLE stock_transfer_details
ADD COLUMN IF NOT EXISTS source_product_warehouse_id BIGINT,
ADD COLUMN IF NOT EXISTS dest_product_warehouse_id BIGINT,
ADD COLUMN IF NOT EXISTS usage_qty NUMERIC(15, 3) DEFAULT 0,
ADD COLUMN IF NOT EXISTS pending_qty NUMERIC(15, 3) DEFAULT 0,
ADD COLUMN IF NOT EXISTS total_qty NUMERIC(15, 3) DEFAULT 0,
ADD COLUMN IF NOT EXISTS total_used NUMERIC(15, 3) DEFAULT 0;
-- Remove obsolete columns (quantity replaced by FIFO fields, legacy fields never used)
ALTER TABLE stock_transfer_details
DROP COLUMN IF EXISTS quantity,
DROP COLUMN IF EXISTS before_quantity,
DROP COLUMN IF EXISTS after_quantity;
-- Add foreign keys for product warehouse references
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
-- Source warehouse foreign key
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'fk_stock_transfer_details_source_pw'
) THEN
EXECUTE
'ALTER TABLE stock_transfer_details
ADD CONSTRAINT fk_stock_transfer_details_source_pw
FOREIGN KEY (source_product_warehouse_id)
REFERENCES product_warehouses(id)
ON DELETE SET NULL ON UPDATE CASCADE';
END IF;
-- Destination warehouse foreign key
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'fk_stock_transfer_details_dest_pw'
) THEN
EXECUTE
'ALTER TABLE stock_transfer_details
ADD CONSTRAINT fk_stock_transfer_details_dest_pw
FOREIGN KEY (dest_product_warehouse_id)
REFERENCES product_warehouses(id)
ON DELETE SET NULL ON UPDATE CASCADE';
END IF;
END IF;
END $$;
-- Add indexes for FIFO operations
CREATE INDEX IF NOT EXISTS idx_stock_transfer_details_source_pw
ON stock_transfer_details (source_product_warehouse_id);
CREATE INDEX IF NOT EXISTS idx_stock_transfer_details_dest_pw
ON stock_transfer_details (dest_product_warehouse_id);
-- Add comments for documentation
COMMENT ON COLUMN stock_transfer_details.source_product_warehouse_id IS
'Source product warehouse ID - referensi warehouse asal (FIFO usable)';
COMMENT ON COLUMN stock_transfer_details.dest_product_warehouse_id IS
'Destination product warehouse ID - referensi warehouse tujuan (FIFO stockable)';
COMMENT ON COLUMN stock_transfer_details.usage_qty IS
'Actual quantity successfully taken from source warehouse (FIFO usable tracking) - replaces quantity field';
COMMENT ON COLUMN stock_transfer_details.pending_qty IS
'Quantity waiting for stock availability (FIFO usable tracking)';
COMMENT ON COLUMN stock_transfer_details.total_qty IS
'Total lot quantity available at destination warehouse (FIFO stockable tracking)';
COMMENT ON COLUMN stock_transfer_details.total_used IS
'Quantity already consumed from this lot at destination warehouse (FIFO stockable tracking)';
@@ -0,0 +1,16 @@
-- Rollback: Drop adjustment_stocks table
BEGIN;
DROP INDEX IF EXISTS idx_adjustment_stocks_product_warehouse;
DROP INDEX IF EXISTS idx_adjustment_stocks_stock_log;
ALTER TABLE adjustment_stocks
DROP CONSTRAINT IF EXISTS fk_adjustment_stocks_product_warehouse;
ALTER TABLE adjustment_stocks
DROP CONSTRAINT IF EXISTS fk_adjustment_stocks_stock_log;
DROP TABLE IF EXISTS adjustment_stocks;
COMMIT;
@@ -0,0 +1,40 @@
-- Migration: Create adjustment_stocks table for FIFO tracking
-- This table tracks FIFO allocation for stock adjustments (both increase and decrease)
BEGIN;
CREATE TABLE IF NOT EXISTS adjustment_stocks (
id BIGSERIAL PRIMARY KEY,
stock_log_id BIGINT NOT NULL,
product_warehouse_id BIGINT NOT NULL,
-- FIFO fields for Adjustment INCREASE (Stockable)
-- Tracks stock added to warehouse via adjustment
total_qty NUMERIC(15, 3) DEFAULT 0,
total_used NUMERIC(15, 3) DEFAULT 0,
-- FIFO fields for Adjustment DECREASE (Usable)
-- Tracks stock consumed from warehouse via adjustment
usage_qty NUMERIC(15, 3) DEFAULT 0,
pending_qty NUMERIC(15, 3) DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- Foreign keys
ALTER TABLE adjustment_stocks
ADD CONSTRAINT fk_adjustment_stocks_stock_log
FOREIGN KEY (stock_log_id) REFERENCES stock_logs(id)
ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE adjustment_stocks
ADD CONSTRAINT fk_adjustment_stocks_product_warehouse
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id)
ON DELETE CASCADE ON UPDATE CASCADE;
-- Indexes
CREATE INDEX idx_adjustment_stocks_stock_log ON adjustment_stocks(stock_log_id);
CREATE INDEX idx_adjustment_stocks_product_warehouse ON adjustment_stocks(product_warehouse_id);
COMMIT;
@@ -0,0 +1,7 @@
DROP INDEX IF EXISTS idx_project_flocks_production_standard_id;
ALTER TABLE project_flocks
DROP CONSTRAINT IF EXISTS fk_project_flocks_production_standard_id;
ALTER TABLE project_flocks
DROP COLUMN IF EXISTS production_standard_id;
@@ -0,0 +1,15 @@
-- Add production_standard_id to project_flocks
ALTER TABLE project_flocks
ADD COLUMN IF NOT EXISTS production_standard_id BIGINT;
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'production_standards') THEN
ALTER TABLE project_flocks
ADD CONSTRAINT fk_project_flocks_production_standard_id
FOREIGN KEY (production_standard_id) REFERENCES production_standards (id) ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
END $$;
CREATE INDEX IF NOT EXISTS idx_project_flocks_production_standard_id
ON project_flocks (production_standard_id);
@@ -0,0 +1,3 @@
-- Remove standard_fcr column from production_standard_details table
ALTER TABLE production_standard_details
DROP COLUMN IF EXISTS standard_fcr;
@@ -0,0 +1,3 @@
-- Add standard_fcr column to production_standard_details table
ALTER TABLE production_standard_details
ADD COLUMN standard_fcr NUMERIC(15, 3);
File diff suppressed because it is too large Load Diff
+128 -696
View File
@@ -4,7 +4,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
@@ -25,66 +24,20 @@ func Run(db *gorm.DB) error {
return err return err
} }
areas, err := seedAreas(tx, adminID)
if err != nil {
return err
}
locations, err := seedLocations(tx, adminID, areas)
if err != nil {
return err
}
productCategories, err := seedProductCategories(tx, adminID) productCategories, err := seedProductCategories(tx, adminID)
if err != nil { if err != nil {
return err return err
} }
if _, err := seedFlocks(tx, adminID); err != nil {
return err
}
if _, err := seedFcr(tx, adminID); err != nil {
return err
}
kandangs, err := seedKandangs(tx, adminID, locations, users)
if err != nil {
return err
}
if err := seedWarehouses(tx, adminID, areas, locations, kandangs); err != nil {
return err
}
suppliers, err := seedSuppliers(tx, adminID) suppliers, err := seedSuppliers(tx, adminID)
if err != nil { if err != nil {
return err return err
} }
if err := seedCustomers(tx, adminID, users); err != nil {
return err
}
if err := seedProducts(tx, adminID, uoms, productCategories, suppliers); err != nil { if err := seedProducts(tx, adminID, uoms, productCategories, suppliers); err != nil {
return err return err
} }
if err := seedNonstocks(tx, adminID, uoms, suppliers); err != nil {
return err
}
if err := seedBanks(tx, adminID); err != nil {
return err
}
if err := seedProductWarehouse(tx, adminID); err != nil {
return err
}
if err := seedTransferStock(tx); err != nil {
return err
}
fmt.Println("✅ Master data seeding completed") fmt.Println("✅ Master data seeding completed")
return nil return nil
}) })
@@ -141,224 +94,6 @@ func seedUoms(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
return result, nil return result, nil
} }
func seedAreas(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
names := []string{"Priangan", "Banten"}
result := make(map[string]uint, len(names))
for _, name := range names {
var area entity.Area
err := tx.Where("name = ?", name).First(&area).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
area = entity.Area{Name: name, CreatedBy: createdBy}
if err := tx.Create(&area).Error; err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}
result[name] = area.Id
}
return result, nil
}
func seedLocations(tx *gorm.DB, createdBy uint, areas map[string]uint) (map[string]uint, error) {
seeds := []struct {
Name string
Address string
Area string
}{
{"Singaparna", "Tasik", "Priangan"},
{"Cikaum", "Cikaum", "Banten"},
}
result := make(map[string]uint, len(seeds))
for _, seed := range seeds {
areaID, ok := areas[seed.Area]
if !ok {
return nil, fmt.Errorf("area %s not seeded", seed.Area)
}
var loc entity.Location
err := tx.Where("name = ?", seed.Name).First(&loc).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
loc = entity.Location{
Name: seed.Name,
Address: seed.Address,
AreaId: areaID,
CreatedBy: createdBy,
}
if err := tx.Create(&loc).Error; err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}
result[seed.Name] = loc.Id
}
return result, nil
}
func seedFlocks(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
names := []string{"Flock Priangan", "Flock Banten"}
result := make(map[string]uint, len(names))
for _, name := range names {
var flock entity.Flock
err := tx.Where("name = ?", name).First(&flock).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
flock = entity.Flock{
Name: name,
CreatedBy: createdBy,
}
if err := tx.Create(&flock).Error; err != nil {
return nil, err
}
} else if err != nil {
return nil, err
} else {
if err := tx.Model(&entity.Flock{}).Where("id = ?", flock.Id).Updates(map[string]any{
"created_by": createdBy,
}).Error; err != nil {
return nil, err
}
}
result[name] = flock.Id
}
return result, nil
}
func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users map[string]uint) (map[string]uint, error) {
seeds := []struct {
Name string
Status utils.KandangStatus
Capacity float64
Location string
PicKey string
}{
{Name: "Singaparna 1", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Singaparna", PicKey: "admin"},
{Name: "Singaparna 2", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Singaparna", PicKey: "admin"},
{Name: "Cikaum 1", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Cikaum", PicKey: "admin"},
{Name: "Cikaum 2", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Cikaum", PicKey: "admin"},
}
result := make(map[string]uint, len(seeds))
for _, seed := range seeds {
locID, ok := locations[seed.Location]
if !ok {
return nil, fmt.Errorf("location %s not seeded", seed.Location)
}
picID, ok := users[seed.PicKey]
if !ok {
return nil, fmt.Errorf("user %s not seeded", seed.PicKey)
}
var kandang entity.Kandang
err := tx.Where("name = ?", seed.Name).First(&kandang).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
kandang = entity.Kandang{
Name: seed.Name,
Status: string(seed.Status),
LocationId: locID,
PicId: picID,
CreatedBy: createdBy,
}
if err := tx.Create(&kandang).Error; err != nil {
return nil, err
}
} else if err != nil {
return nil, err
} else {
updates := map[string]any{
"location_id": locID,
"pic_id": picID,
"status": string(seed.Status),
}
if err := tx.Model(&entity.Kandang{}).Where("id = ?", kandang.Id).Updates(updates).Error; err != nil {
return nil, err
}
}
result[seed.Name] = kandang.Id
}
return result, nil
}
func seedWarehouses(tx *gorm.DB, createdBy uint, areas map[string]uint, locations map[string]uint, kandangs map[string]uint) error {
seeds := []struct {
Name string
Type string
Area string
Location *string
Kandang *string
}{
{Name: "Gudang Priangan", Type: string(utils.WarehouseTypeArea), Area: "Priangan"},
{Name: "Gudang Singaparna", Type: string(utils.WarehouseTypeLokasi), Area: "Priangan", Location: strPtr("Singaparna")},
{Name: "Gudang Singaparna 1", Type: string(utils.WarehouseTypeKandang), Area: "Priangan", Location: strPtr("Singaparna"), Kandang: strPtr("Singaparna 1")},
{Name: "Gudang Singaparna 2", Type: string(utils.WarehouseTypeKandang), Area: "Priangan", Location: strPtr("Singaparna"), Kandang: strPtr("Singaparna 2")},
{Name: "Gudang Banten", Type: string(utils.WarehouseTypeArea), Area: "Banten"},
{Name: "Gudang Cikaum", Type: string(utils.WarehouseTypeLokasi), Area: "Banten", Location: strPtr("Cikaum")},
{Name: "Gudang Cikaum 1", Type: string(utils.WarehouseTypeKandang), Area: "Banten", Location: strPtr("Cikaum"), Kandang: strPtr("Cikaum 1")},
{Name: "Gudang Cikaum 2", Type: string(utils.WarehouseTypeKandang), Area: "Banten", Location: strPtr("Cikaum"), Kandang: strPtr("Cikaum 2")},
}
for _, seed := range seeds {
areaID, ok := areas[seed.Area]
if !ok {
return fmt.Errorf("area %s not seeded", seed.Area)
}
var warehouse entity.Warehouse
err := tx.Where("name = ?", seed.Name).First(&warehouse).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
warehouse = entity.Warehouse{
Name: seed.Name,
Type: seed.Type,
AreaId: areaID,
CreatedBy: createdBy,
}
} else if err != nil {
return err
}
if seed.Location != nil {
locID, ok := locations[*seed.Location]
if !ok {
return fmt.Errorf("location %s not seeded", *seed.Location)
}
warehouse.LocationId = uintPtr(locID)
}
if seed.Kandang != nil {
kandangID, ok := kandangs[*seed.Kandang]
if !ok {
return fmt.Errorf("kandang %s not seeded", *seed.Kandang)
}
warehouse.KandangId = uintPtr(kandangID)
}
if warehouse.Id == 0 {
if err := tx.Create(&warehouse).Error; err != nil {
return err
}
} else {
if err := tx.Model(&entity.Warehouse{}).Where("id = ?", warehouse.Id).Updates(map[string]any{
"type": warehouse.Type,
"area_id": warehouse.AreaId,
"location_id": warehouse.LocationId,
"kandang_id": warehouse.KandangId,
}).Error; err != nil {
return err
}
}
}
return nil
}
func seedProductCategories(tx *gorm.DB, createdBy uint) (map[string]uint, error) { func seedProductCategories(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
seeds := []struct { seeds := []struct {
Name string Name string
@@ -440,113 +175,6 @@ func seedSuppliers(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
return result, nil return result, nil
} }
func seedCustomers(tx *gorm.DB, createdBy uint, users map[string]uint) error {
seeds := []struct {
Name string
PicKey string
Address string
Phone string
Email string
}{
{"Abdul Azis", "admin", "Jl. Raya Utama 1, Bekasi", "082100000001", "abdul.azis@gmail.com"},
}
for idx, seed := range seeds {
picID, ok := users[seed.PicKey]
if !ok {
return fmt.Errorf("user %s not seeded", seed.PicKey)
}
var customer entity.Customer
err := tx.Where("name = ?", seed.Name).First(&customer).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
customer = entity.Customer{
Name: seed.Name,
PicId: picID,
Type: string(utils.CustomerSupplierTypeBisnis),
Address: seed.Address,
Phone: seed.Phone,
Email: seed.Email,
AccountNumber: *strPtr(fmt.Sprintf("%03d", idx+1)),
CreatedBy: createdBy,
}
if err := tx.Create(&customer).Error; err != nil {
return err
}
} else if err != nil {
return err
}
}
return nil
}
func seedFcr(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
seeds := []struct {
Name string
Standards []struct {
Weight float64
FcrNumber float64
Mortality float64
}
}{
{
Name: "FCR Layer",
Standards: []struct {
Weight float64
FcrNumber float64
Mortality float64
}{
{Weight: 0.8, FcrNumber: 1.60, Mortality: 2.0},
{Weight: 1.5, FcrNumber: 1.75, Mortality: 3.5},
},
},
}
result := make(map[string]uint, len(seeds))
for _, seed := range seeds {
var fcr entity.Fcr
err := tx.Where("name = ?", seed.Name).First(&fcr).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
fcr = entity.Fcr{Name: seed.Name, CreatedBy: createdBy}
if err := tx.Create(&fcr).Error; err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}
result[seed.Name] = fcr.Id
for _, std := range seed.Standards {
var standard entity.FcrStandard
err := tx.Where("fcr_id = ? AND weight = ?", fcr.Id, std.Weight).First(&standard).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
standard = entity.FcrStandard{
FcrID: fcr.Id,
Weight: std.Weight,
FcrNumber: std.FcrNumber,
Mortality: std.Mortality,
}
if err := tx.Create(&standard).Error; err != nil {
return nil, err
}
} else if err != nil {
return nil, err
} else {
if err := tx.Model(&entity.FcrStandard{}).Where("id = ?", standard.Id).Updates(map[string]any{
"fcr_number": std.FcrNumber,
"mortality": std.Mortality,
}).Error; err != nil {
return nil, err
}
}
}
}
return result, nil
}
func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories map[string]uint, suppliers map[string]uint) error { func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories map[string]uint, suppliers map[string]uint) error {
seeds := []struct { seeds := []struct {
Name string Name string
@@ -560,77 +188,88 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
Expiry *int Expiry *int
Suppliers []string Suppliers []string
Flags []utils.FlagType Flags []utils.FlagType
IsVisible bool
}{ }{
{ {
Name: "DOC Broiler", Name: "ISA Brown",
Brand: "MBU Broiler", Brand: "ISA Brown",
Sku: "BRO0001", Sku: "ISA0001",
Uom: "Ekor", Uom: "Ekor",
Category: "Day Old Chick", Category: "Day Old Chick",
Price: 7500, Price: 7500,
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
Flags: []utils.FlagType{utils.FlagDOC}, Flags: []utils.FlagType{utils.FlagDOC, utils.FlagPullet, utils.FlagLayer},
IsVisible: true,
}, },
{ {
Name: "Ayam Pullet", Name: "Ayam Afkir",
Brand: "MBU Pullet", Brand: "-",
Sku: "PLT0001", Sku: "1",
Uom: "Ekor", Uom: "Ekor",
Category: "Pullet", Category: "Day Old Chick",
Price: 15000, Price: 1,
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, Flags: []utils.FlagType{utils.FlagAyamAfkir},
Flags: []utils.FlagType{utils.FlagPullet}, IsVisible: false,
}, },
{ {
Name: "Ayam Afkir", Name: "Ayam Mati",
Brand: "-", Brand: "-",
Sku: "1", Sku: "2",
Uom: "Ekor", Uom: "Ekor",
Category: "Day Old Chick", Category: "Day Old Chick",
Price: 1, Price: 1,
Flags: []utils.FlagType{utils.FlagAyamMati},
IsVisible: false,
}, },
{ {
Name: "Ayam Mati", Name: "Ayam Culling",
Brand: "-", Brand: "-",
Sku: "2", Sku: "3",
Uom: "Ekor", Uom: "Ekor",
Category: "Day Old Chick", Category: "Day Old Chick",
Price: 1, Price: 1,
Flags: []utils.FlagType{utils.FlagAyamCulling},
IsVisible: false,
}, },
{ {
Name: "Ayam Culling", Name: "Telur Utuh",
Brand: "-", Brand: "-",
Sku: "3", Sku: "4",
Uom: "Ekor", Uom: "Gram",
Category: "Day Old Chick", Category: "Telur",
Price: 1, Price: 1,
Flags: []utils.FlagType{utils.FlagTelurUtuh},
IsVisible: false,
}, },
{ {
Name: "Telur Konsumsi Baik", Name: "Telur Pecah",
Brand: "-", Brand: "-",
Sku: "4", Sku: "5",
Uom: "Unit", Uom: "Gram",
Category: "Telur", Category: "Telur",
Price: 1, Price: 1,
Flags: []utils.FlagType{utils.FlagTelurPecah},
IsVisible: false,
}, },
{ {
Name: "Telur Pecah", Name: "Telur Putih",
Brand: "-", Brand: "-",
Sku: "5", Sku: "6",
Uom: "Unit", Uom: "Gram",
Category: "Telur", Category: "Telur",
Price: 1, Price: 1,
Flags: []utils.FlagType{utils.FlagTelurPutih},
IsVisible: false,
}, },
{ {
Name: "281 SPECIAL STARTER", Name: "Telur Retak",
Brand: "281 STARTER", Brand: "-",
Sku: "281", Sku: "7",
Uom: "Kilogram", Uom: "Gram",
Category: "Bahan Baku", Category: "Telur",
Price: 7850, Price: 1,
Expiry: intPtr(60), Flags: []utils.FlagType{utils.FlagTelurRetak},
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, IsVisible: false,
Flags: []utils.FlagType{utils.FlagPakan, utils.FlagStarter},
}, },
} }
@@ -709,78 +348,78 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
return nil return nil
} }
func seedNonstocks(tx *gorm.DB, createdBy uint, uoms map[string]uint, suppliers map[string]uint) error { // func seedNonstocks(tx *gorm.DB, createdBy uint, uoms map[string]uint, suppliers map[string]uint) error {
seeds := []struct { // seeds := []struct {
Name string // Name string
Uom string // Uom string
Suppliers []string // Suppliers []string
Flags []utils.FlagType // Flags []utils.FlagType
}{ // }{
{ // {
Name: "Expedisi DOC", // Name: "LAJ",
Uom: "Ekor", // Uom: "Unit",
Suppliers: []string{"Ekspedisi"}, // Suppliers: []string{"Ekspedisi"},
Flags: []utils.FlagType{utils.FlagEkspedisi}, // Flags: []utils.FlagType{utils.FlagEkspedisi},
}, // },
{ // {
Name: "Solar", // Name: "Solar",
Uom: "Liter", // Uom: "Liter",
Suppliers: []string{"BOP Vendor"}, // Suppliers: []string{"BOP Vendor"},
Flags: []utils.FlagType{}, // Flags: []utils.FlagType{},
}, // },
} // }
for _, seed := range seeds { // for _, seed := range seeds {
uomID, ok := uoms[seed.Uom] // uomID, ok := uoms[seed.Uom]
if !ok { // if !ok {
return fmt.Errorf("uom %s not seeded", seed.Uom) // return fmt.Errorf("uom %s not seeded", seed.Uom)
} // }
var nonstock entity.Nonstock // var nonstock entity.Nonstock
err := tx.Where("name = ?", seed.Name).First(&nonstock).Error // err := tx.Where("name = ?", seed.Name).First(&nonstock).Error
if errors.Is(err, gorm.ErrRecordNotFound) { // if errors.Is(err, gorm.ErrRecordNotFound) {
nonstock = entity.Nonstock{ // nonstock = entity.Nonstock{
Name: seed.Name, // Name: seed.Name,
UomId: uomID, // UomId: uomID,
CreatedBy: createdBy, // CreatedBy: createdBy,
} // }
if err := tx.Create(&nonstock).Error; err != nil { // if err := tx.Create(&nonstock).Error; err != nil {
return err // return err
} // }
} else if err != nil { // } else if err != nil {
return err // return err
} else { // } else {
if err := tx.Model(&entity.Nonstock{}).Where("id = ?", nonstock.Id).Updates(map[string]any{ // if err := tx.Model(&entity.Nonstock{}).Where("id = ?", nonstock.Id).Updates(map[string]any{
"uom_id": uomID, // "uom_id": uomID,
}).Error; err != nil { // }).Error; err != nil {
return err // return err
} // }
} // }
for _, supplierName := range seed.Suppliers { // for _, supplierName := range seed.Suppliers {
supplierID, ok := suppliers[supplierName] // supplierID, ok := suppliers[supplierName]
if !ok { // if !ok {
return fmt.Errorf("supplier %s not seeded", supplierName) // return fmt.Errorf("supplier %s not seeded", supplierName)
} // }
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
} // }
} else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { // } else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err // return err
} // }
} // }
if err := seedFlags(tx, nonstock.Id, entity.FlagableTypeNonstock, seed.Flags); err != nil { // if err := seedFlags(tx, nonstock.Id, entity.FlagableTypeNonstock, seed.Flags); err != nil {
return err // return err
} // }
} // }
return nil // return nil
} // }
// nanti saya isi // nanti saya isi
@@ -808,213 +447,6 @@ func seedFlags(tx *gorm.DB, flagableID uint, flagableType string, flags []utils.
return nil return nil
} }
func seedBanks(tx *gorm.DB, createdBy uint) error {
seeds := []struct {
Name string
Alias string
Owner *string
AccountNumber string
}{
{
Name: "Bank Central Asia",
Alias: "BCA",
AccountNumber: "1234567890",
Owner: ptr("PT MBU Group"),
},
{
Name: "Bank Rakyat Indonesia",
Alias: "BRI",
AccountNumber: "9876543210",
Owner: ptr("PT MBU Group"),
},
{
Name: "Bank Mandiri",
Alias: "MAND",
AccountNumber: "1122334455",
Owner: ptr("PT MBU Group"),
},
}
for _, seed := range seeds {
var bank entity.Bank
err := tx.Where("name = ?", seed.Name).First(&bank).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
bank = entity.Bank{
Name: seed.Name,
Alias: seed.Alias,
Owner: seed.Owner,
AccountNumber: seed.AccountNumber,
CreatedBy: createdBy,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := tx.Create(&bank).Error; err != nil {
return err
}
} else if err != nil {
return err
} else {
// update data jika sudah ada
if err := tx.Model(&entity.Bank{}).Where("id = ?", bank.Id).Updates(map[string]any{
"alias": seed.Alias,
"owner": seed.Owner,
"account_number": seed.AccountNumber,
"updated_at": time.Now(),
}).Error; err != nil {
return err
}
}
}
return nil
}
func seedProductWarehouse(tx *gorm.DB, createdBy uint) error {
seeds := []struct {
ProductName string
WarehouseName string
Quantity float64
}{
{ProductName: "DOC Broiler", WarehouseName: "Gudang Priangan", Quantity: 100},
{ProductName: "281 SPECIAL STARTER", WarehouseName: "Gudang Singaparna", Quantity: 200},
{ProductName: "281 SPECIAL STARTER", WarehouseName: "Gudang Banten", Quantity: 300},
{ProductName: "DOC Broiler", WarehouseName: "Gudang Singaparna 1", Quantity: 5000},
{ProductName: "Telur Konsumsi Baik", WarehouseName: "Gudang Singaparna 1", Quantity: 600},
{ProductName: "Telur Pecah", WarehouseName: "Gudang Singaparna 1", Quantity: 80},
{ProductName: "Telur Konsumsi Baik", WarehouseName: "Gudang Cikaum 1", Quantity: 450},
{ProductName: "Telur Pecah", WarehouseName: "Gudang Cikaum 1", Quantity: 60},
}
for _, seed := range seeds {
var product entity.Product
if err := tx.Where("name = ?", seed.ProductName).First(&product).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("product %q not found for product warehouse seeding", seed.ProductName)
}
return err
}
var warehouse entity.Warehouse
if err := tx.Where("name = ?", seed.WarehouseName).First(&warehouse).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("warehouse %q not found for product warehouse seeding", seed.WarehouseName)
}
return err
}
var productWarehouse entity.ProductWarehouse
err := tx.Where("product_id = ? AND warehouse_id = ?", product.Id, warehouse.Id).First(&productWarehouse).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
productWarehouse = entity.ProductWarehouse{
ProductId: product.Id,
WarehouseId: warehouse.Id,
Quantity: seed.Quantity,
CreatedBy: createdBy,
}
if err := tx.Create(&productWarehouse).Error; err != nil {
return err
}
} else if err != nil {
return err
} else {
if err := tx.Model(&productWarehouse).Updates(map[string]any{
"quantity": seed.Quantity,
}).Error; err != nil {
return err
}
}
}
return nil
}
func seedTransferStock(tx *gorm.DB) error {
transfer := entity.StockTransfer{
FromWarehouseId: 1,
ToWarehouseId: 2,
Reason: "Seed transfer stock",
TransferDate: time.Now(),
MovementNumber: "SEED-TRF-00001",
CreatedBy: 1,
}
if err := tx.Create(&transfer).Error; err != nil {
return err
}
details := []entity.StockTransferDetail{
{
StockTransferId: transfer.Id,
ProductId: 1,
Quantity: 10,
},
{
StockTransferId: transfer.Id,
ProductId: 2,
Quantity: 5,
},
}
for i := range details {
if err := tx.Create(&details[i]).Error; err != nil {
return err
}
}
deliveries := []entity.StockTransferDelivery{
{
StockTransferId: transfer.Id,
SupplierId: 1,
VehiclePlate: "B 1234 XYZ",
DriverName: "Driver Seed",
DocumentPath: "seed.pdf",
ShippingCostItem: 1000,
ShippingCostTotal: 2000,
},
}
for i := range deliveries {
if err := tx.Create(&deliveries[i]).Error; err != nil {
return err
}
}
detailMap := make(map[uint64]uint64)
for _, d := range details {
detailMap[d.ProductId] = d.Id
}
deliveryItems := []entity.StockTransferDeliveryItem{
{
StockTransferDeliveryId: deliveries[0].Id,
StockTransferDetailId: detailMap[1],
Quantity: 50,
},
{
StockTransferDeliveryId: deliveries[0].Id,
StockTransferDetailId: detailMap[2],
Quantity: 30,
},
}
for i := range deliveryItems {
if err := tx.Create(&deliveryItems[i]).Error; err != nil {
return err
}
}
return nil
}
func ptr[T any](v T) *T {
return &v
}
func strPtr(s string) *string { func strPtr(s string) *string {
return &s return &s
} }
func intPtr(v int) *int {
return &v
}
func uintPtr(v uint) *uint {
return &v
}
+29
View File
@@ -0,0 +1,29 @@
package entities
import "time"
// AdjustmentStock tracks FIFO allocation for stock adjustments
// - For INCREASE adjustments (Stockable): Tracks stock added to warehouse
// - For DECREASE adjustments (Usable): Tracks stock consumed from warehouse
type AdjustmentStock struct {
Id uint `gorm:"primaryKey"`
StockLogId uint `gorm:"column:stock_log_id;not null;index"`
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
// === FIFO FIELDS FOR INCREASE ADJUSTMENT (Stockable) ===
// Tracks stock added to warehouse via adjustment INCREASE
TotalQty float64 `gorm:"column:total_qty;default:0"` // Total lot quantity available
TotalUsed float64 `gorm:"column:total_used;default:0"` // Quantity already used from this lot
// === FIFO FIELDS FOR DECREASE ADJUSTMENT (Usable) ===
// Tracks stock consumed from warehouse via adjustment DECREASE
UsageQty float64 `gorm:"column:usage_qty;default:0"` // Actual quantity consumed
PendingQty float64 `gorm:"column:pending_qty;default:0"` // Pending quantity (waiting for stock)
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"`
// Relations
StockLog *StockLog `gorm:"foreignKey:StockLogId;references:Id"`
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
}
+18
View File
@@ -0,0 +1,18 @@
package entities
import "time"
type Document struct {
Id uint `gorm:"primaryKey"`
DocumentableType string `gorm:"size:50;not null;index:documents_documentable_polymorphic,priority:1"`
DocumentableId uint64 `gorm:"not null;index:documents_documentable_polymorphic,priority:2"`
Type string `gorm:"size:50;not null"`
Path string `gorm:"size:50;not null"`
Name string `gorm:"size:50;not null"`
Ext string `gorm:"size:50;not null"`
Size float64 `gorm:"type:numeric(15,3);not null"`
CreatedBy *uint `gorm:"index"`
CreatedAt time.Time `gorm:"autoCreateTime"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
}
+9 -7
View File
@@ -1,7 +1,6 @@
package entities package entities
import ( import (
"database/sql"
"time" "time"
"gorm.io/gorm" "gorm.io/gorm"
@@ -13,8 +12,8 @@ type Expense struct {
SupplierId uint64 `gorm:""` SupplierId uint64 `gorm:""`
Category string `gorm:"type:varchar(50);not null"` Category string `gorm:"type:varchar(50);not null"`
PoNumber string `gorm:"type:varchar(50)"` PoNumber string `gorm:"type:varchar(50)"`
DocumentPath sql.NullString `gorm:"type:json"` LocationId uint64 `gorm:"not null"`
RealizationDocumentPath sql.NullString `gorm:"type:json;column:realization_document_path"` ProjectFlockId *string `gorm:"type:json"`
RealizationDate time.Time `gorm:"type:date;column:realization_date"` RealizationDate time.Time `gorm:"type:date;column:realization_date"`
TransactionDate time.Time `gorm:"type:date;not null"` TransactionDate time.Time `gorm:"type:date;not null"`
Notes string `gorm:"type:text;column:notes"` Notes string `gorm:"type:text;column:notes"`
@@ -23,8 +22,11 @@ type Expense struct {
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Supplier *Supplier `gorm:"foreignKey:SupplierId;references:Id"` Supplier *Supplier `gorm:"foreignKey:SupplierId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` Location *Location `gorm:"foreignKey:LocationId;references:Id"`
Nonstocks []ExpenseNonstock `gorm:"foreignKey:ExpenseId;references:Id"` CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
LatestApproval *Approval `gorm:"-" json:"latest_approval,omitempty"` Nonstocks []ExpenseNonstock `gorm:"foreignKey:ExpenseId;references:Id"`
Documents []Document `gorm:"foreignKey:DocumentableId;references:Id"`
RealizationDocuments []Document `gorm:"foreignKey:DocumentableId;references:Id"`
LatestApproval *Approval `gorm:"-" json:"latest_approval,omitempty"`
} }
+30
View File
@@ -0,0 +1,30 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Initial struct {
Id uint `gorm:"primaryKey;autoIncrement"`
ReferenceNumber string `gorm:"type:varchar(100);not null"`
TransactionType string `gorm:"type:varchar(50);not null"`
InitialBalanceType string `gorm:"type:varchar(20);not null"`
PartyType string `gorm:"type:varchar(50);not null;index:initials_party_polymorphic,priority:1"`
PartyId uint `gorm:"not null;index:initials_party_polymorphic,priority:2"`
BankId *uint `gorm:"index"`
Direction string `gorm:"type:varchar(5);not null"`
Nominal float64 `gorm:"type:numeric(15,3);not null"`
Notes string `gorm:"type:text;not null"`
CreatedBy uint `gorm:"index" json:"-"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Bank Bank `gorm:"foreignKey:BankId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Customer *Customer `gorm:"foreignKey:PartyId;references:Id"`
Supplier *Supplier `gorm:"foreignKey:PartyId;references:Id"`
LatestApproval *Approval `gorm:"-" json:"-"`
}
+1
View File
@@ -20,5 +20,6 @@ type Kandang struct {
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Location Location `gorm:"foreignKey:LocationId;references:Id"` Location Location `gorm:"foreignKey:LocationId;references:Id"`
Pic User `gorm:"foreignKey:PicId;references:Id"` Pic User `gorm:"foreignKey:PicId;references:Id"`
Warehouses []Warehouse `gorm:"foreignKey:KandangId;references:Id"`
ProjectFlockKandangs []ProjectFlockKandang `gorm:"foreignKey:KandangId;references:Id" json:"-"` ProjectFlockKandangs []ProjectFlockKandang `gorm:"foreignKey:KandangId;references:Id" json:"-"`
} }
@@ -5,15 +5,20 @@ import (
) )
type MarketingDeliveryProduct struct { type MarketingDeliveryProduct struct {
Id uint `gorm:"primaryKey;autoIncrement"` Id uint `gorm:"primaryKey;autoIncrement"`
MarketingProductId uint `gorm:"uniqueIndex;not null"` MarketingProductId uint `gorm:"uniqueIndex;not null"`
Qty float64 `gorm:"type:numeric(15,3)"` ProductWarehouseId uint `gorm:"not null"`
UnitPrice float64 `gorm:"type:numeric(15,3)"` UnitPrice float64 `gorm:"type:numeric(15,3)"`
TotalWeight float64 `gorm:"type:numeric(15,3)"` TotalWeight float64 `gorm:"type:numeric(15,3)"`
AvgWeight float64 `gorm:"type:numeric(15,3)"` AvgWeight float64 `gorm:"type:numeric(15,3)"`
TotalPrice float64 `gorm:"type:numeric(15,3)"` TotalPrice float64 `gorm:"type:numeric(15,3)"`
DeliveryDate *time.Time `gorm:"type:timestamptz"` DeliveryDate *time.Time `gorm:"type:timestamptz"`
VehicleNumber string `gorm:"type:varchar(50)"` VehicleNumber string `gorm:"type:varchar(50)"`
// FIFO Fields
UsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"`
PendingQty float64 `gorm:"type:numeric(15,3);default:0;not null"`
CreatedAt *time.Time `gorm:"type:timestamptz;not null"`
MarketingProduct MarketingProduct `gorm:"foreignKey:MarketingProductId;references:Id"` MarketingProduct MarketingProduct `gorm:"foreignKey:MarketingProductId;references:Id"`
} }
+32
View File
@@ -0,0 +1,32 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Payment struct {
Id uint `gorm:"primaryKey;autoIncrement"`
PaymentCode string `gorm:"type:varchar(50);not null"`
ReferenceNumber *string `gorm:"type:varchar(100)"`
TransactionType string `gorm:"type:varchar(50)"`
PartyType string `gorm:"type:varchar(50);not null;index:payments_party_polymorphic,priority:1"`
PartyId uint `gorm:"not null;index:payments_party_polymorphic,priority:2"`
PaymentDate time.Time `gorm:"not null"`
PaymentMethod string `gorm:"type:varchar(20);not null"`
BankId *uint `gorm:"not null;index:idx_payments_bank_id"`
Direction string `gorm:"type:varchar(5);not null"`
Nominal float64 `gorm:"type:numeric(15,3);not null"`
Notes string `gorm:"type:text;not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedBy uint `gorm:"index" json:"-"`
BankWarehouse Bank `gorm:"foreignKey:BankId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Customer *Customer `gorm:"foreignKey:PartyId;references:Id"`
Supplier *Supplier `gorm:"foreignKey:PartyId;references:Id"`
LatestApproval *Approval `gorm:"-" json:"-"`
}
+7 -5
View File
@@ -21,10 +21,12 @@ type Product struct {
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
IsVisible bool `gorm:"column:is_visible;default:true"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Uom Uom `gorm:"foreignKey:UomId;references:Id"` Uom Uom `gorm:"foreignKey:UomId;references:Id"`
ProductCategory ProductCategory `gorm:"foreignKey:ProductCategoryId;references:Id"` ProductCategory ProductCategory `gorm:"foreignKey:ProductCategoryId;references:Id"`
ProductSuppliers []ProductSupplier `gorm:"foreignKey:ProductId;references:Id"` ProductSuppliers []ProductSupplier `gorm:"foreignKey:ProductId;references:Id"`
Flags []Flag `gorm:"polymorphic:Flagable;polymorphicValue:products"` Flags []Flag `gorm:"polymorphic:Flagable;polymorphicValue:products"`
ProductWarehouses []ProductWarehouse `gorm:"foreignKey:ProductId;references:Id"`
} }
+9 -17
View File
@@ -1,23 +1,15 @@
package entities package entities
import (
"time"
"gorm.io/gorm"
)
type ProductWarehouse struct { type ProductWarehouse struct {
Id uint `gorm:"primaryKey;autoIncrement"` Id uint `gorm:"primaryKey;column:id"`
ProductId uint `gorm:"not null"` ProductId uint `gorm:"column:product_id;not null"`
WarehouseId uint `gorm:"not null"` WarehouseId uint `gorm:"column:warehouse_id;not null"`
Quantity float64 `gorm:"default:0"` ProjectFlockKandangId *uint `gorm:"column:project_flock_kandang_id"`
CreatedAt time.Time `gorm:"autoCreateTime"` Quantity float64 `gorm:"column:qty;type:numeric(15,3);default:0"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
CreatedBy uint `gorm:"not null"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
// Relations // Relations
Product Product `gorm:"foreignKey:ProductId;references:Id"` Product Product `gorm:"foreignKey:ProductId;references:Id"`
Warehouse Warehouse `gorm:"foreignKey:WarehouseId;references:Id"` Warehouse Warehouse `gorm:"foreignKey:WarehouseId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
StockLogs []StockLog `gorm:"foreignKey:ProductWarehouseId;references:Id"`
} }
+19
View File
@@ -0,0 +1,19 @@
package entities
import (
"time"
)
type ProductionStandard struct {
Id uint `gorm:"primaryKey;autoIncrement"`
Name string `gorm:"type:varchar(100);uniqueIndex;not null"`
ProjectCategory string `gorm:"type:varchar(20);not null"`
CreatedAt time.Time `gorm:"type:timestamptz;not null"`
UpdatedAt time.Time `gorm:"type:timestamptz;not null"`
DeletedAt *time.Time `gorm:"type:timestamptz"`
CreatedBy uint `gorm:"not null"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
ProductionStandardDetails []ProductionStandardDetail `gorm:"foreignKey:ProductionStandardId;references:Id"`
StandardGrowthDetails []StandardGrowthDetail `gorm:"foreignKey:ProductionStandardId;references:Id"`
}
@@ -0,0 +1,20 @@
package entities
import (
"time"
)
type ProductionStandardDetail struct {
Id uint `gorm:"primaryKey;autoIncrement"`
ProductionStandardId uint `gorm:"not null"`
Week int `gorm:"not null"`
TargetHenDayProduction *float64 `gorm:"type:numeric(15,3)"`
TargetHenHouseProduction *float64 `gorm:"type:numeric(15,3)"`
TargetEggWeight *float64 `gorm:"type:numeric(15,3)"`
TargetEggMass *float64 `gorm:"type:numeric(15,3)"`
StandardFCR *float64 `gorm:"type:numeric(15,3)"`
CreatedAt time.Time `gorm:"type:timestamptz;not null"`
UpdatedAt time.Time `gorm:"type:timestamptz;not null"`
ProductionStandard ProductionStandard `gorm:"foreignKey:ProductionStandardId;references:Id"`
}
+8 -6
View File
@@ -5,11 +5,13 @@ import (
) )
type ProjectBudget struct { type ProjectBudget struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
Qty float64 `gorm:"type:numeric(15,3);not null"` ProjectFlockId uint `gorm:"not null"`
Price float64 `gorm:"type:numeric(15,3);not null"` NonstockId uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` Qty float64 `gorm:"type:numeric(15,3);not null"`
Price float64 `gorm:"type:numeric(15,3);not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
Nonstock *Nonstock `gorm:"foreignKey:Id;references:Id"` Nonstock *Nonstock `gorm:"foreignKey:NonstockId;references:Id"`
ProjectFlock *ProjectFlock `gorm:"foreignKey:Id;references:Id"` ProjectFlock *ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
} }
@@ -0,0 +1,33 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type ProjectFlockKandangUniformity struct {
Id uint `gorm:"primaryKey"`
Uniformity float64 `gorm:"type:numeric(15,3)"`
Week int `gorm:"not null"`
Cv float64 `gorm:"type:numeric(15,3)"`
ChickQtyOfWeight float64 `gorm:"type:numeric(15,3)"`
MeanUp float64 `gorm:"type:numeric(15,3)"`
MeanDown float64 `gorm:"type:numeric(15,3)"`
ProjectFlockKandangId uint `gorm:"not null"`
UniformQty float64 `gorm:"type:numeric(15,3)"`
NotUniformQty float64 `gorm:"type:numeric(15,3)"`
UniformDate *time.Time `gorm:"type:timestamptz"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedBy uint `gorm:"not null"`
ProjectFlockKandang ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
LatestApproval *Approval `gorm:"-" json:"-"`
}
func (ProjectFlockKandangUniformity) TableName() string {
return "project_flock_kandang_uniformity"
}
+3
View File
@@ -12,6 +12,7 @@ type ProjectFlock struct {
AreaId uint `gorm:"not null"` AreaId uint `gorm:"not null"`
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"`
ProductionStandardId uint `gorm:"column:production_standard_id"`
LocationId uint `gorm:"not null"` LocationId uint `gorm:"not null"`
CreatedBy uint `gorm:"not null"` CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
@@ -20,10 +21,12 @@ type ProjectFlock struct {
Area Area `gorm:"foreignKey:AreaId;references:Id"` Area Area `gorm:"foreignKey:AreaId;references:Id"`
Fcr Fcr `gorm:"foreignKey:FcrId;references:Id"` Fcr Fcr `gorm:"foreignKey:FcrId;references:Id"`
ProductionStandard ProductionStandard `gorm:"foreignKey:ProductionStandardId;references:Id"`
Location Location `gorm:"foreignKey:LocationId;references:Id"` Location Location `gorm:"foreignKey:LocationId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Kandangs []Kandang `gorm:"many2many:project_flock_kandangs;joinTableForeignKey:project_flock_id;joinTableReferences:kandang_id" json:"kandangs,omitempty"` Kandangs []Kandang `gorm:"many2many:project_flock_kandangs;joinTableForeignKey:project_flock_id;joinTableReferences:kandang_id" json:"kandangs,omitempty"`
KandangHistory []ProjectFlockKandang `gorm:"foreignKey:ProjectFlockId;references:Id" json:"-"` KandangHistory []ProjectFlockKandang `gorm:"foreignKey:ProjectFlockId;references:Id" json:"-"`
Budgets []ProjectBudget `gorm:"foreignKey:ProjectFlockId;references:Id" json:"-"`
LatestApproval *Approval `gorm:"-" json:"-"` LatestApproval *Approval `gorm:"-" json:"-"`
} }
+6 -5
View File
@@ -3,11 +3,12 @@ package entities
import "time" import "time"
type ProjectFlockKandang struct { 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"` Period int `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` ClosedAt *time.Time `gorm:"index"`
CreatedAt time.Time `gorm:"autoCreateTime"`
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"` ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"` Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
+3 -4
View File
@@ -5,19 +5,18 @@ import (
) )
type Purchase struct { type Purchase struct {
Id uint `gorm:"primaryKey;autoIncrement"` Id uint `gorm:"primaryKey;autoIncrement"`
PrNumber string `gorm:"not null"` PrNumber string `gorm:"not null"`
PoNumber *string PoNumber *string
PoDate *time.Time PoDate *time.Time
SupplierId uint `gorm:"not null"` SupplierId uint `gorm:"not null"`
CreditTerm *int CreditTerm int `gorm:"column:credit_term;not null;default:0"`
DueDate *time.Time DueDate *time.Time
GrandTotal float64 `gorm:"type:numeric(15,3);default:0"`
Notes *string Notes *string
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt *time.Time `gorm:"index"` DeletedAt *time.Time `gorm:"index"`
CreatedBy uint `gorm:"not null"` CreatedBy uint `gorm:"not null"`
// Relations // Relations
Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"` Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"`
+17 -14
View File
@@ -5,22 +5,25 @@ import (
) )
type PurchaseItem struct { type PurchaseItem struct {
Id uint `gorm:"primaryKey;autoIncrement"` Id uint `gorm:"primaryKey;autoIncrement"`
PurchaseId uint `gorm:"not null"` PurchaseId uint `gorm:"not null"`
ProductId uint `gorm:"not null"` ProductId uint `gorm:"not null"`
WarehouseId uint `gorm:"not null"` WarehouseId uint `gorm:"not null"`
ProductWarehouseId *uint ProductWarehouseId *uint
ReceivedDate *time.Time ProjectFlockKandangId *uint
TravelNumber *string ReceivedDate *time.Time
TravelNumberDocs *string TravelNumber *string
VehicleNumber *string TravelNumberDocs *string
SubQty float64 `gorm:"type:numeric(15,3);not null"` VehicleNumber *string
TotalQty float64 `gorm:"type:numeric(15,3);default:0"` SubQty float64 `gorm:"type:numeric(15,3);not null"`
TotalUsed float64 `gorm:"type:numeric(15,3);default:0"` TotalQty float64 `gorm:"type:numeric(15,3);default:0"`
Price float64 `gorm:"type:numeric(15,3);default:0"` TotalUsed float64 `gorm:"type:numeric(15,3);default:0"`
TotalPrice 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"`
ExpenseNonstockId *uint64
// Relations // Relations
ExpenseNonstock *ExpenseNonstock `gorm:"foreignKey:ExpenseNonstockId;references:Id"`
Purchase *Purchase `gorm:"foreignKey:PurchaseId;references:Id"` Purchase *Purchase `gorm:"foreignKey:PurchaseId;references:Id"`
Product *Product `gorm:"foreignKey:ProductId;references:Id"` Product *Product `gorm:"foreignKey:ProductId;references:Id"`
Warehouse *Warehouse `gorm:"foreignKey:WarehouseId;references:Id"` Warehouse *Warehouse `gorm:"foreignKey:WarehouseId;references:Id"`
+1 -14
View File
@@ -7,24 +7,11 @@ type RecordingEgg struct {
RecordingId uint `gorm:"column:recording_id;not null;index"` RecordingId uint `gorm:"column:recording_id;not null;index"`
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"` ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
Qty int `gorm:"column:qty;not null"` Qty int `gorm:"column:qty;not null"`
Weight *float64 `gorm:"column:weight"`
CreatedBy uint `gorm:"column:created_by"` CreatedBy uint `gorm:"column:created_by"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
GradingEggs []GradingEgg `gorm:"foreignKey:RecordingEggId;references:Id"`
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"` Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
} }
type GradingEgg struct {
Id uint `gorm:"primaryKey"`
RecordingEggId uint `gorm:"column:recording_egg_id;not null;index"`
Qty float64 `gorm:"column:qty;not null"`
Grade string `gorm:"column:grade;type:varchar(50)"`
CreatedBy uint `gorm:"column:created_by"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
RecordingEgg RecordingEgg `gorm:"foreignKey:RecordingEggId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
}
@@ -0,0 +1,19 @@
package entities
import (
"time"
)
type StandardGrowthDetail struct {
Id uint `gorm:"primaryKey;autoIncrement"`
ProductionStandardId uint `gorm:"not null"`
TargetMeanBw *float64 `gorm:"type:numeric(15,3)"`
MaxDepletion *float64 `gorm:"type:numeric(15,3)"`
MinUniformity float64 `gorm:"type:numeric(15,3);not null"`
Week int `gorm:"not null"`
FeedIntake *float64 `gorm:"type:numeric(15,3)"`
CreatedAt time.Time `gorm:"type:timestamptz;not null"`
CreatedBy uint `gorm:"not null"`
ProductionStandard ProductionStandard `gorm:"foreignKey:ProductionStandardId;references:Id"`
}
+1
View File
@@ -20,4 +20,5 @@ type StockTransfer struct {
Details []StockTransferDetail `gorm:"foreignKey:StockTransferId"` Details []StockTransferDetail `gorm:"foreignKey:StockTransferId"`
Deliveries []StockTransferDelivery `gorm:"foreignKey:StockTransferId"` Deliveries []StockTransferDelivery `gorm:"foreignKey:StockTransferId"`
CreatedUser *User `gorm:"foreignKey:CreatedBy"` CreatedUser *User `gorm:"foreignKey:CreatedBy"`
Documents []Document `gorm:"foreignKey:DocumentableId;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
} }
+13 -28
View File
@@ -1,35 +1,20 @@
package entities package entities
import ( import "time"
"time"
"gorm.io/gorm"
)
const (
LogTypeAdjustment = "ADJUSTMENT"
LogTypeTransfer = "TRANSFER"
)
const (
TransactionTypeIncrease = "INCREASE"
TransactionTypeDecrease = "DECREASE"
)
type StockLog struct { type StockLog struct {
Id uint `gorm:"primaryKey;column:id"` Id uint `gorm:"primaryKey;column:id"`
TransactionType string `gorm:"type:varchar(20);not null"` ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null;index"`
Quantity float64 `gorm:"type:numeric(15,3);not null"` CreatedBy uint `gorm:"column:created_by;not null;index"`
BeforeQuantity float64 `gorm:"type:numeric(15,3);not null"`
AfterQuantity float64 `gorm:"type:numeric(15,3);not null"` Increase float64 `gorm:"column:increase;type:numeric(15,3);default:0"`
LogType string `gorm:"type:varchar(50);not null;index:stock_logs_flaggable_lookup,priority:1"` Decrease float64 `gorm:"column:decrease;type:numeric(15,3);default:0"`
LogId uint `gorm:"not null;index:stock_logs_flaggable_lookup,priority:2"`
Note string `gorm:"type:text"` LoggableType string `gorm:"column:loggable_type;type:varchar(50);not null"`
ProductWarehouseId uint `gorm:"not null;index"` LoggableId uint `gorm:"column:loggable_id;not null"`
CreatedBy uint `gorm:"index"`
CreatedAt time.Time `gorm:"autoCreateTime"` Notes string `gorm:"column:notes;type:text"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
ProductWarehouse *ProductWarehouse `json:"product_warehouse,omitempty" gorm:"foreignKey:ProductWarehouseId;references:Id"` ProductWarehouse *ProductWarehouse `json:"product_warehouse,omitempty" gorm:"foreignKey:ProductWarehouseId;references:Id"`
CreatedUser *User `json:"created_user,omitempty" gorm:"foreignKey:CreatedBy;references:Id"` CreatedUser *User `json:"created_user,omitempty" gorm:"foreignKey:CreatedBy;references:Id"`
+18 -17
View File
@@ -4,20 +4,21 @@ import "time"
// DETAIL EKSPEDISI // DETAIL EKSPEDISI
type StockTransferDelivery struct { type StockTransferDelivery struct {
Id uint64 `gorm:"primaryKey;autoIncrement"` Id uint64 `gorm:"primaryKey;autoIncrement"`
StockTransferId uint64 StockTransferId uint64
SupplierId uint64 SupplierId uint64
VehiclePlate string VehiclePlate string
DriverName string DriverName string
DocumentNumber string DocumentNumber string
DocumentPath string DocumentPath string
ShippingCostItem float64 ShippingCostItem float64
ShippingCostTotal float64 ShippingCostTotal float64
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
DeletedAt *time.Time `gorm:"index"` DeletedAt *time.Time `gorm:"index"`
// Relations // Relations
StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"` StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"`
Supplier *Supplier `gorm:"foreignKey:SupplierId"` Supplier *Supplier `gorm:"foreignKey:SupplierId"`
Items []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDeliveryId"` Items []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDeliveryId"`
} Documents []Document `gorm:"foreignKey:DocumentableId;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
}
+24 -8
View File
@@ -7,12 +7,28 @@ type StockTransferDetail struct {
Id uint64 `gorm:"primaryKey;autoIncrement"` Id uint64 `gorm:"primaryKey;autoIncrement"`
StockTransferId uint64 StockTransferId uint64
ProductId uint64 ProductId uint64
Quantity float64
CreatedAt time.Time // === FIFO FIELDS - SOURCE WAREHOUSE (Usable) ===
UpdatedAt time.Time // Tracking stock yang DIAMBIL dari source warehouse
DeletedAt *time.Time `gorm:"index"` SourceProductWarehouseID *uint64 `gorm:"column:source_product_warehouse_id"`
// Relations UsageQty float64 `gorm:"column:usage_qty;default:0"` // Actual yang berhasil diambil
StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"` PendingQty float64 `gorm:"column:pending_qty;default:0"` // Yang pending (nunggu stock)
Product *Product `gorm:"foreignKey:ProductId"`
DeliveryItems []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDetailId"` // === FIFO FIELDS - DESTINATION WAREHOUSE (Stockable) ===
// Tracking stock yang DITAMBAHKAN ke destination warehouse
DestProductWarehouseID *uint64 `gorm:"column:dest_product_warehouse_id"`
TotalQty float64 `gorm:"column:total_qty;default:0"` // Total lot yang tersedia
TotalUsed float64 `gorm:"column:total_used;default:0"` // Yang sudah dipakai dari lot ini
// === METADATA ===
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time `gorm:"index"`
// === RELATIONS ===
StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"`
Product *Product `gorm:"foreignKey:ProductId"`
SourceProductWarehouse *ProductWarehouse `gorm:"foreignKey:SourceProductWarehouseID"`
DestProductWarehouse *ProductWarehouse `gorm:"foreignKey:DestProductWarehouseID"`
DeliveryItems []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDetailId"`
} }
+18
View File
@@ -0,0 +1,18 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Transaction struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:idx_name,where:deleted_at IS NULL"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
}
+18
View File
@@ -0,0 +1,18 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Uniformity struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:idx_name,where:deleted_at IS NULL"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
}
+70 -4
View File
@@ -3,14 +3,13 @@ package middleware
import ( import (
"strings" "strings"
"github.com/gofiber/fiber/v2"
"gitlab.com/mbugroup/lti-api.git/internal/config" "gitlab.com/mbugroup/lti-api.git/internal/config"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session" "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" 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/sso"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/gofiber/fiber/v2"
) )
const ( const (
@@ -91,7 +90,6 @@ func Auth(userService service.UserService, requiredScopes ...string) fiber.Handl
c.Locals(authContextLocalsKey, ctx) c.Locals(authContextLocalsKey, ctx)
c.Locals(authUserLocalsKey, user) c.Locals(authUserLocalsKey, user)
return c.Next() return c.Next()
} }
} }
@@ -107,7 +105,7 @@ func AuthenticatedUser(c *fiber.Ctx) (*entity.User, bool) {
func ActorIDFromContext(c *fiber.Ctx) (uint, error) { func ActorIDFromContext(c *fiber.Ctx) (uint, error) {
user, ok := AuthenticatedUser(c) user, ok := AuthenticatedUser(c)
if !ok || user == nil || user.Id == 0 { if !ok || user == nil || user.Id == 0 {
return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
} }
return user.Id, nil return user.Id, nil
@@ -199,3 +197,71 @@ func hasAllScopes(have, required []string) bool {
} }
return true return true
} }
// 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))
}
+220 -62
View File
@@ -1,75 +1,233 @@
package middleware package middleware
import ( // project-flock
"strings" const (
P_ProjectFlockKandangsClosing = "lti.production.project_flock_kandangs.closing"
P_ProjectFlockKandangsCheckClosing = "lti.production.project_flock_kandangs.closing.detail"
P_ProjectFlockKandangsGetAll = "lti.production.project_flock_kandangs.list"
P_ProjectFlockKandangsGetOne = "lti.production.project_flock_kandangs.detail"
"github.com/gofiber/fiber/v2" P_ProjectFlockGetAll = "lti.production.project_flocks.list"
P_ProjectFlockCreate = "lti.production.project_flocks.create"
P_ProjectFlockGetOne = "lti.production.project_flocks.detail"
P_ProjectFlockUpdate = "lti.production.project_flocks.update"
P_ProjectFlockDelete = "lti.production.project_flocks.delete"
P_ProjectFlockApprove = "lti.production.project_flocks.approve"
P_ProjectFlockLookup = "lti.production.project_flocks.lookup"
P_ProjectFlockNextPeriod = "lti.production.project_flocks.next_period"
P_ProjectFlockResubmit = "lti.production.project_flocks.resubmit"
) )
// RequirePermissions ensures the authenticated user possesses all specified permissions. const (
func RequirePermissions(perms ...string) fiber.Handler { P_ExpenseGetAll = "lti.expense.list"
required := canonicalPermissions(perms) P_ExpenseCreateOne = "lti.expense.create"
return func(c *fiber.Ctx) error { P_ExpenseUpdateOne = "lti.expense.update"
if len(required) == 0 { P_ExpenseGetOne = "lti.expense.detail"
return c.Next() P_ExpenseDeleteOne = "lti.expense.delete"
} P_ExpenseApprovalManager = "lti.expense.approve.manager"
P_ExpenseApprovalFinance = "lti.expense.approve.finance"
P_ExpenseCreateRealizations = "lti.expense.create.realization"
P_ExpenseUpdateRealizations = "lti.expense.update.realization"
P_ExpenseCompleteExpense = "lti.expense.complete.expense"
P_ExpenseDocument = "lti.expense.document"
P_ExpenseDocumentRealizations = "lti.expense.document.realization"
)
const (
P_AdjustmentGetAll = "lti.inventory.list"
P_AdjustmentCreate = "lti.inventory.create"
P_AdjustmentGetOne = "lti.inventory.detail"
)
const (
P_ApprovalGetAll = "lti.approval.list"
)
const (
P_ReportExpenseGetAll = "lti.repport.expense.list"
P_ReportDeliveryGetAll = "lti.repport.delivery.list"
P_ReportPurchaseSupplierGetAll = "lti.repport.purchasesupplier.list"
)
ctx, ok := AuthDetails(c) const (
if !ok || ctx == nil { P_ProductStockGetAll = "lti.inventory.product_stock.list"
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") P_ProductStockGetOne = "lti.inventory.product_stock.detail"
} P_ProductWarehousekGetAll = "lti.inventory.product_warehouses.list"
P_ProductWarehouseGetOne = "lti.inventory.product_warehouses.detail"
)
const (
P_ClosingGetAll = "lti.closing.list"
P_ClosingDetail = "lti.closing.detail"
)
userPerms := ctx.permissionSet() const (
if len(userPerms) == 0 { P_TransferGetAll = "lti.inventory.transfer.list"
return fiber.NewError(fiber.StatusForbidden, "Insufficient permission") P_TransferGetOne = "lti.inventory.transfer.detail"
} P_TransferCreateOne = "lti.inventory.transfer.create"
)
for _, perm := range required { const (
if _, has := userPerms[perm]; !has { P_TransferToLaying_GetAll = "lti.production.transfer_to_laying.list"
return fiber.NewError(fiber.StatusForbidden, "Insufficient permission") P_TransferToLaying_GetOne = "lti.production.transfer_to_laying.detail"
} P_TransferToLaying_CreateOne = "lti.production.transfer_to_laying.create"
} P_TransferToLaying_UpdateOne = "lti.production.transfer_to_laying.update"
P_TransferToLaying_DeleteOne = "lti.production.transfer_to_laying.delete"
P_TransferToLaying_Approval = "lti.production.transfer_to_laying.approve"
P_TransferToLaying_GetAvailableQty = "lti.production.transfer_to_laying.getavailableqty"
)
return c.Next() const (
} P_DeliveryGetAll = "lti.marketing.delivery_order.list"
} P_DeliveryGetOne = "lti.marketing.delivery_order.detail"
P_DeliveryUpdateOne = "lti.marketing.delivery_order.update"
P_DeliveryCreateOne = "lti.marketing.delivery_order.Create"
P_SalesOrderDelete = "lti.marketing.sales_order.delete"
P_SalesOrderApproval = "lti.marketing.sales_order.approve"
P_SalesOrderCreateOne = "lti.marketing.sales_order.create"
P_SalesOrderUpdateOne = "lti.marketing.sales_order.update"
)
// HasPermission reports whether the current request context includes the given permission. const (
func HasPermission(c *fiber.Ctx, perm string) bool { P_AreaGetAll = "lti.master.area.list"
ctx, ok := AuthDetails(c) P_AreaGetOne = "lti.master.area.detail"
if !ok || ctx == nil { P_AreaCreateOne = "lti.master.area.create"
return false P_AreaUpdateOne = "lti.master.area.update"
} P_AreaDeleteOne = "lti.master.area.delete"
perm = canonicalPermission(perm)
if perm == "" {
return false
}
_, has := ctx.permissionSet()[perm]
return has
}
func (a *AuthContext) permissionSet() map[string]struct{} { P_BanksGetAll = "lti.master.banks.list"
if a == nil || a.Permissions == nil { P_BanksGetOne = "lti.master.banks.detail"
return nil P_BanksCreateOne = "lti.master.banks.create"
} P_BanksUpdateOne = "lti.master.banks.update"
return a.Permissions P_BanksDeleteOne = "lti.master.banks.delete"
}
func canonicalPermissions(perms []string) []string { P_CustomerGetAll = "lti.master.customer.list"
out := make([]string, 0, len(perms)) P_CustomerGetOne = "lti.master.customer.detail"
seen := make(map[string]struct{}, len(perms)) P_CustomerCreateOne = "lti.master.customer.create"
for _, perm := range perms { P_CustomerUpdateOne = "lti.master.customer.update"
if canonical := canonicalPermission(perm); canonical != "" { P_CustomerDeleteOne = "lti.master.customer.delete"
if _, ok := seen[canonical]; ok {
continue
}
seen[canonical] = struct{}{}
out = append(out, canonical)
}
}
return out
}
func canonicalPermission(perm string) string { P_FcrGetAll = "lti.master.fcr.list"
return strings.ToLower(strings.TrimSpace(perm)) P_FcrGetOne = "lti.master.fcr.detail"
} P_FcrCreateOne = "lti.master.fcr.create"
P_FcrUpdateOne = "lti.master.fcr.update"
P_FcrDeleteOne = "lti.master.fcr.delete"
P_FlocksGetAll = "lti.master.flocks.list"
P_FlocksGetOne = "lti.master.flocks.detail"
P_FlocksCreateOne = "lti.master.flocks.create"
P_FlocksUpdateOne = "lti.master.flocks.update"
P_FlocksDeleteOne = "lti.master.flocks.delete"
P_KandangsGetAll = "lti.master.kandangs.list"
P_KandangsGetOne = "lti.master.kandangs.detail"
P_KandangsCreateOne = "lti.master.kandangs.create"
P_KandangsUpdateOne = "lti.master.kandangs.update"
P_KandangsDeleteOne = "lti.master.kandangs.delete"
P_LocationsGetAll = "lti.master.locations.list"
P_LocationsGetOne = "lti.master.locations.detail"
P_LocationsCreateOne = "lti.master.locations.create"
P_LocationsUpdateOne = "lti.master.locations.update"
P_LocationsDeleteOne = "lti.master.locations.delete"
P_NonstocksGetAll = "lti.master.nonstocks.list"
P_NonstocksGetOne = "lti.master.nonstocks.detail"
P_NonstocksCreateOne = "lti.master.nonstocks.create"
P_NonstocksUpdateOne = "lti.master.nonstocks.update"
P_NonstocksDeleteOne = "lti.master.nonstocks.delete"
P_ProductCategoriesGetAll = "lti.master.Product_categories.list"
P_ProductCategoriesGetOne = "lti.master.Product_categories.detail"
P_ProductCategoriesCreateOne = "lti.master.Product_categories.create"
P_ProductCategoriesUpdateOne = "lti.master.Product_categories.update"
P_ProductCategoriesDeleteOne = "lti.master.Product_categories.delete"
P_ProductsGetAll = "lti.master.Products.list"
P_ProductsGetOne = "lti.master.Products.detail"
P_ProductsCreateOne = "lti.master.Products.create"
P_ProductsUpdateOne = "lti.master.Products.update"
P_ProductsDeleteOne = "lti.master.Products.delete"
P_SuppliersGetAll = "lti.master.suppliers.list"
P_SuppliersGetOne = "lti.master.suppliers.detail"
P_SuppliersCreateOne = "lti.master.suppliers.create"
P_SuppliersUpdateOne = "lti.master.suppliers.update"
P_SuppliersDeleteOne = "lti.master.suppliers.delete"
P_UomsGetAll = "lti.master.uoms.list"
P_UomsGetOne = "lti.master.uoms.detail"
P_UomsCreateOne = "lti.master.uoms.create"
P_UomsUpdateOne = "lti.master.uoms.update"
P_UomsDeleteOne = "lti.master.uoms.delete"
P_WarehousesGetAll = "lti.master.warehouses.list"
P_WarehousesGetOne = "lti.master.warehouses.detail"
P_WarehousesCreateOne = "lti.master.warehouses.create"
P_WarehousesUpdateOne = "lti.master.warehouses.update"
P_WarehousesDeleteOne = "lti.master.warehouses.delete"
P_Production_Standart_GetAll = "lti.master.production_standards.list"
P_Production_Standart_CreateOne = "lti.master.production_standards.create"
P_Production_Standart_GetOne = "lti.master.production_standards.detail"
P_Production_Standart_UpdateOne = "lti.master.production_standards.update"
P_Production_Standart_DeleteOne = "lti.master.production_standards.delete"
)
// finance
const (
P_Finances_Initial_Balances_CreateOne = "lti.finance.initial_balances.create"
P_Finances_Initial_Balances_GetOne = "lti.finance.initial_balances.detail"
P_Finances_Initial_Balances_UpdateOne = "lti.finance.initial_balances.update"
P_Finances_Injections_CreateOne = "lti.finance.injections.create"
P_Finances_Injections_GetOne = "lti.finance.injections.detail"
P_Finances_Injections_UpdateOne = "lti.finance.injections.update"
P_Finances_Payments_CreateOne = "lti.finance.payments.create"
P_Finances_Payments_UpdateOne = "lti.finance.payments.update"
P_Finances_Payments_GetOne = "lti.finance.payments.detail"
P_Finances_Transaction_GetAll = "lti.finance.transactions.list"
P_Finances_Transaction_GetOne = "lti.finance.transactions.detail"
P_Finances_Transaction_DeleteOne = "lti.finance.transactions.delete"
)
const (
P_ChickinsCreateOne = "lti.production.chickins.create"
P_ChickinsGetOne = "lti.production.chickins.detail"
P_ChickinsApproval = "lti.production.chickins.approve"
)
// recording
const (
P_RecordingGetAll = "lti.production.recording.list"
P_RecordingGetOne = "lti.production.recording.detail"
P_RecordingCreateOne = "lti.production.recording.create"
P_RecordingUpdateOne = "lti.production.recording.update"
P_RecordingDeleteOne = "lti.production.recording.delete"
P_RecordingNextDay = "lti.production.recording.next_day"
P_RecordingApproval = "lti.production.recording.approve"
)
const (
P_PurchaseGetAll = "lti.Purchase.list"
P_PurchaseGetOne = "lti.Purchase.detail"
P_PurchaseCreateOne = "lti.Purchase.create"
P_PurchaseUpdateOne = "lti.Purchase.update"
P_PurchaseDeleteOne = "lti.Purchase.delete"
P_PurchaseItemDeleteOne = "lti.Purchase.delete.item"
P_PurchaseReceive = "lti.Purchase.receive"
P_PurchaseApprovalStaff = "lti.Purchase.approve.staff"
P_PurchaseApprovalManager = "lti.Purchase.approve.manager"
)
const (
P_Uniformities_GetAll = "lti.production.uniformity.list"
P_Uniformities_GetOne = "lti.production.uniformity.detail"
P_Uniformities_Verify = "lti.production.uniformity.verify"
P_Uniformities_CreateOne = "lti.production.uniformity.create"
P_Uniformities_UpdateOne = "lti.production.uniformity.update"
P_Uniformities_DeleteOne = "lti.production.uniformity.delete"
P_Uniformities_Approval = "lti.production.uniformity.approve"
)
const (
P_UserGetAll = "lti.users.list"
P_UserGetOne = "lti.users.detail"
)
+1 -1
View File
@@ -15,5 +15,5 @@ func ApprovalRoutes(v1 fiber.Router, u user.UserService, s common.ApprovalServic
route := v1.Group("/approvals") route := v1.Group("/approvals")
route.Use(m.Auth(u)) route.Use(m.Auth(u))
route.Get("/", ctrl.GetAll) route.Get("/", ctrl.GetAll,m.RequirePermissions(m.P_ApprovalGetAll))
} }
@@ -3,6 +3,7 @@ package controller
import ( import (
"math" "math"
"strconv" "strconv"
"strings"
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto" "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services" service "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
@@ -13,12 +14,14 @@ import (
) )
type ClosingController struct { type ClosingController struct {
ClosingService service.ClosingService ClosingService service.ClosingService
SapronakService service.SapronakService
} }
func NewClosingController(closingService service.ClosingService) *ClosingController { func NewClosingController(closingService service.ClosingService, sapronakService service.SapronakService) *ClosingController {
return &ClosingController{ return &ClosingController{
ClosingService: closingService, ClosingService: closingService,
SapronakService: sapronakService,
} }
} }
@@ -39,17 +42,17 @@ func (u *ClosingController) GetAll(c *fiber.Ctx) error {
} }
return c.Status(fiber.StatusOK). return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.ClosingListDTO]{ JSON(response.SuccessWithPaginate[dto.ClosingListItemDTO]{
Code: fiber.StatusOK, Code: fiber.StatusOK,
Status: "success", Status: "success",
Message: "Get all closings successfully", Message: "Retrieved closing projects list successfully",
Meta: response.Meta{ Meta: response.Meta{
Page: query.Page, Page: query.Page,
Limit: query.Limit, Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults, TotalResults: totalResults,
}, },
Data: dto.ToClosingListDTOs(result), Data: result,
}) })
} }
@@ -57,11 +60,11 @@ func (u *ClosingController) GetOne(c *fiber.Ctx) error {
param := c.Params("id") param := c.Params("id")
id, err := strconv.Atoi(param) id, err := strconv.Atoi(param)
if err != nil { if err != nil || id <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") return fiber.NewError(fiber.StatusBadRequest, "Invalid id")
} }
result, err := u.ClosingService.GetOne(c, uint(id)) result, err := u.ClosingService.GetProjectFlockByID(c, uint(id))
if err != nil { if err != nil {
return err return err
} }
@@ -70,7 +73,281 @@ func (u *ClosingController) GetOne(c *fiber.Ctx) error {
JSON(response.Success{ JSON(response.Success{
Code: fiber.StatusOK, Code: fiber.StatusOK,
Status: "success", Status: "success",
Message: "Get closing successfully", Message: "Retrieved closing information successfully",
Data: dto.ToClosingListDTO(*result), Data: result,
})
}
func (u *ClosingController) GetClosingSummary(c *fiber.Ctx) error {
param := c.Params("projectFlockId")
id, err := strconv.Atoi(param)
if err != nil || id <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
}
result, err := u.ClosingService.GetClosingSummary(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Retrieved project information successfully",
Data: result,
})
}
func (u *ClosingController) GetPenjualan(c *fiber.Ctx) error {
param := c.Params("project_flock_id")
projectFlockID, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
}
projectFlock, err := u.ClosingService.GetProjectFlockByID(c, uint(projectFlockID))
if err != nil {
return err
}
result, err := u.ClosingService.GetPenjualan(c, uint(projectFlockID))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get closing penjualan successfully",
Data: dto.ToPenjualanRealisasiResponseDTO(projectFlock.Category, uint(projectFlockID), result),
})
}
func (u *ClosingController) GetOverhead(c *fiber.Ctx) error {
param := c.Params("project_flock_id")
projectFlockID, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
}
result, err := u.ClosingService.GetOverhead(c, uint(projectFlockID))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get overhead successfully",
Data: result,
})
}
func (u *ClosingController) GetClosingSapronak(c *fiber.Ctx) error {
param := c.Params("projectFlockId")
id, err := strconv.Atoi(param)
if err != nil || id <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
}
query := &validation.ClosingSapronakQuery{
Type: strings.ToLower(c.Query("type")),
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
}
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
if query.Type != validation.SapronakTypeIncoming && query.Type != validation.SapronakTypeOutgoing {
return fiber.NewError(fiber.StatusBadRequest, "type must be either incoming or outgoing")
}
result, totalResults, err := u.ClosingService.GetClosingSapronak(c, uint(id), query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.ClosingSapronakItemDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Retrieved closing report (sapronak) 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 *ClosingController) GetSapronakByProject(c *fiber.Ctx) error {
param := c.Params("project_flock_id")
flag := c.Query("flag", "")
projectID, err := strconv.Atoi(param)
if err != nil || projectID <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
}
result, err := u.SapronakService.GetSapronakByProject(c, uint(projectID), flag)
if err != nil {
return err
}
payload := dto.ToSapronakProjectAggregatedFromReports(result, flag)
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get perhitungan sapronak per project successfully",
Data: payload,
})
}
func (u *ClosingController) GetSapronakByKandang(c *fiber.Ctx) error {
projectParam := c.Params("project_flock_id")
kandangParam := c.Params("project_flock_kandang_id")
flag := c.Query("flag", "")
projectID, err := strconv.Atoi(projectParam)
if err != nil || projectID <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
}
pfkID, err := strconv.Atoi(kandangParam)
if err != nil || pfkID <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
}
result, err := u.SapronakService.GetSapronakByKandang(c, uint(projectID), uint(pfkID), flag)
if err != nil {
return err
}
payload := dto.ToSapronakProjectAggregatedFromReport(result, flag)
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get perhitungan sapronak per kandang successfully",
Data: payload,
})
}
func (u *ClosingController) GetClosingKeuangan(c *fiber.Ctx) error {
param := c.Params("project_flock_id")
projectFlockID, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
}
result, err := u.ClosingService.GetClosingKeuangan(c, uint(projectFlockID))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get closing keuangan successfully",
Data: result,
})
}
func (u *ClosingController) GetExpeditionHPP(c *fiber.Ctx) error {
param := c.Params("project_flock_id")
projectFlockID, err := strconv.Atoi(param)
if err != nil || projectFlockID <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
}
var projectFlockKandangID *uint
if raw := c.Query("project_flock_kandang_id"); raw != "" {
idInt, convErr := strconv.Atoi(raw)
if convErr != nil || idInt <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
}
idUint := uint(idInt)
projectFlockKandangID = &idUint
}
result, err := u.ClosingService.GetExpeditionHPP(c, uint(projectFlockID), projectFlockKandangID)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get expedition HPP successfully",
Data: result,
})
}
func (u *ClosingController) GetExpeditionHPPByKandang(c *fiber.Ctx) error {
projectParam := c.Params("project_flock_id")
kandangParam := c.Params("project_flock_kandang_id")
projectFlockID, err := strconv.Atoi(projectParam)
if err != nil || projectFlockID <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
}
pfkID, err := strconv.Atoi(kandangParam)
if err != nil || pfkID <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
}
kandangID := uint(pfkID)
result, err := u.ClosingService.GetExpeditionHPP(c, uint(projectFlockID), &kandangID)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get expedition HPP successfully",
Data: result,
})
}
func (u *ClosingController) GetClosingDataProduksi(c *fiber.Ctx) error {
param := c.Params("projectFlockId")
id, err := strconv.Atoi(param)
if err != nil || id <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
}
result, err := u.ClosingService.GetClosingDataProduksi(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Retrieved production data successfully",
Data: result,
}) })
} }
@@ -1,6 +1,7 @@
package dto package dto
import ( import (
"fmt"
"time" "time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -26,6 +27,149 @@ type ClosingDetailDTO struct {
ClosingListDTO ClosingListDTO
} }
type ClosingListItemDTO struct {
Id uint `json:"id"`
ProjectName string `json:"project_name"`
LocationID uint `json:"location_id"`
LocationName string `json:"location_name"`
ProjectCategory string `json:"project_category"`
Period int `json:"period"`
ClosingDate string `json:"closing_date"`
ShedLabel string `json:"shed_label"`
ShedCount int `json:"shed_count"`
// SalesPaidAmount int64 `json:"sales_paid_amount"`
// SalesRemainingAmount int64 `json:"sales_remaining_amount"`
// SalesPaymentStatus string `json:"sales_payment_status"`
ProjectStatus string `json:"project_status"`
}
type ClosingSummaryDTO struct {
FlockID uint `json:"flock_id"`
Period int `json:"period"`
// JenisProduk string `json:"jenis_produk"`
// LabelPopulasi string `json:"label_populasi"`
Population int `json:"population"`
PopulationFormatted string `json:"population_formatted"`
ProjectType string `json:"project_type"`
ActiveHouseCount int `json:"active_house_count"`
ActiveHouseLabel string `json:"active_house_label"`
SalesPaymentStatus string `json:"sales_payment_status"`
// StatusPembayaranMitra string `json:"status_pembayaran_mitra"`
StatusProject string `json:"project_status"`
StatusClosing string `json:"closing_status"`
}
type ClosingPurchaseDTO struct {
InitialPopulation int `json:"initial_population"`
ClaimCulling int `json:"claim_culling"`
FinalPopulation int `json:"final_population"`
FeedIn float64 `json:"feed_in"`
FeedUsed float64 `json:"feed_used"`
FeedUsedPerHead float64 `json:"feed_used_per_head"`
}
type ClosingSalesDTO struct {
SalesPopulation int `json:"sales_population"`
SalesWeight float64 `json:"sales_weight"`
AverageWeight float64 `json:"average_weight"`
AverageSellingPrice float64 `json:"chicken_average_selling_price"`
}
type ClosingEggSalesDTO struct {
EggPieces int `json:"egg_pieces"`
EggMassKg float64 `json:"egg_mass_kg"`
AverageEggWeightKg float64 `json:"average_egg_weight_kg"`
AverageSellingPrice float64 `json:"egg_average_selling_price"`
}
type ClosingPerformanceDTO struct {
Depletion float64 `json:"depletion"`
Age float64 `json:"age_day"`
MortalityStd float64 `json:"mortality_std"`
MortalityAct float64 `json:"mortality_act"`
DeffMortality float64 `json:"deff_mortality"`
FcrStd float64 `json:"fcr_std"`
FcrAct float64 `json:"fcr_act"`
DeffFcr float64 `json:"deff_fcr"`
Awg float64 `json:"awg"`
}
type ClosingSalesGroupDTO struct {
Chicken ClosingSalesDTO `json:"chicken"`
Egg *ClosingEggSalesDTO `json:"egg,omitempty"`
}
type ClosingProductionReportDTO struct {
Purchase ClosingPurchaseDTO `json:"purchase"`
Sales ClosingSalesGroupDTO `json:"sales"`
Performance ClosingPerformanceDTO `json:"performance"`
}
func ToClosingSummaryDTO(project entity.ProjectFlock, statusProject, statusClosing string) ClosingSummaryDTO {
history := project.KandangHistory
period := maxPeriod(history)
kandangCount := len(history)
population := sumPopulation(history)
populationInt := int(population)
return ClosingSummaryDTO{
FlockID: project.Id,
Period: period,
// JenisProduk: project.Category,
// LabelPopulasi: "",
Population: populationInt,
PopulationFormatted: fmt.Sprintf("%d Ekor", populationInt),
ProjectType: project.Category,
ActiveHouseCount: kandangCount,
ActiveHouseLabel: fmt.Sprintf("%d Kandang", kandangCount),
SalesPaymentStatus: "Tempo",
// StatusPembayaranMitra: "",
StatusProject: statusProject,
StatusClosing: statusClosing,
}
}
func ToClosingListItemDTO(project entity.ProjectFlock, projectStatus string) ClosingListItemDTO {
shedCount := len(project.KandangHistory)
return ClosingListItemDTO{
Id: project.Id,
ProjectName: project.FlockName,
LocationID: project.LocationId,
LocationName: project.Location.Name,
ProjectCategory: project.Category,
Period: maxPeriod(project.KandangHistory),
ClosingDate: "17-Nov-2025",
ShedLabel: fmt.Sprintf("%d Kandang", shedCount),
ShedCount: shedCount,
// SalesPaidAmount: 21993726,
// SalesRemainingAmount: 11075919,
// SalesPaymentStatus: "Lunas",
ProjectStatus: projectStatus,
}
}
func maxPeriod(history []entity.ProjectFlockKandang) int {
max := 0
for _, h := range history {
if h.Period > max {
max = h.Period
}
}
return max
}
func sumPopulation(history []entity.ProjectFlockKandang) float64 {
var total float64
for _, h := range history {
for _, chickin := range h.Chickins {
total += chickin.UsageQty + chickin.PendingUsageQty
}
}
return total
}
// === Mapper Functions === // === Mapper Functions ===
func ToClosingRelationDTO(e entity.ProjectFlock) ClosingRelationDTO { func ToClosingRelationDTO(e entity.ProjectFlock) ClosingRelationDTO {
@@ -62,3 +206,20 @@ func ToClosingDetailDTO(e entity.ProjectFlock) ClosingDetailDTO {
ClosingListDTO: ToClosingListDTO(e), ClosingListDTO: ToClosingListDTO(e),
} }
} }
func CalculateAgeFromChickinDataProduksi(projectFlockKandang *entity.ProjectFlockKandang, deliveryDate *time.Time) int {
if projectFlockKandang == nil || deliveryDate == nil || len(projectFlockKandang.Chickins) == 0 {
return 0
}
earliestChickinDate := projectFlockKandang.Chickins[0].ChickInDate
for _, chickin := range projectFlockKandang.Chickins {
if chickin.ChickInDate.Before(earliestChickinDate) {
earliestChickinDate = chickin.ChickInDate
}
}
ageInDays := int(deliveryDate.Sub(earliestChickinDate).Hours() / 24)
ageInWeeks := ageInDays / 7
return ageInWeeks
}
@@ -0,0 +1,14 @@
package dto
// ExpeditionCostItemDTO merepresentasikan biaya ekspedisi per vendor.
type ExpeditionCostItemDTO struct {
Id uint64 `json:"id"`
ExpeditionVendorName string `json:"expedition_vendor_name"`
HPPAmount float64 `json:"hpp_amount"`
}
// ExpeditionHPPDTO adalah struktur response utama untuk HPP Ekspedisi.
type ExpeditionHPPDTO struct {
ExpeditionCosts []ExpeditionCostItemDTO `json:"expedition_costs"`
TotalHPPAmount float64 `json:"total_hpp_amount"`
}
@@ -0,0 +1,589 @@
package dto
import (
"slices"
"strings"
"gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
)
// === CONSTANTS ===
const (
HPPGroupPengeluaran = "HPP dan Pengeluaran"
HPPGroupBahanBaku = "HPP dan Bahan Baku"
HPPLabelOverhead = "Pengeluaran Overhead"
HPPLabelEkspedisi = "Beban Ekspedisi"
HPPSummaryLabel = "HPP"
PLSalesTypeChicken = "Penjualan Ayam Besar"
PLSalesTypeEgg = "Penjualan Telur"
PLItemTypeSapronak = "Pembelian Sapronak"
PLItemTypeOverhead = "Pengeluaran Overhead"
PLItemTypeEkspedisi = "Beban Ekspedisi"
PLSummaryLabelGrossProfit = "LABA RUGI BRUTTO"
PLSummaryLabelSubTotal = "SUB TOTAL"
PLSummaryLabelNetProfit = "LABA RUGI NETTO"
PurchaseLabelPrefix = "Pembelian "
)
// === CONTEXT STRUCTS ===
type CalculationContext struct {
TotalPopulation float64
TotalWeightProduced float64
TotalEggWeightKg float64
TotalDepletion float64
TotalWeightSold float64
ActualPopulation float64
}
type ClosingKeuanganInput struct {
ProjectFlockCategory string
PurchaseItems []entities.PurchaseItem
Budgets []entities.ProjectBudget
Realizations []entities.ExpenseRealization
DeliveryProducts []entities.MarketingDeliveryProduct
Chickins []entities.ProjectChickin
TotalWeightProduced float64
TotalEggWeightKg float64
TotalDepletion float64
}
// === BASE METRICS ===
type FinancialMetrics struct {
RpPerBird float64 `json:"rp_per_bird"`
RpPerKg float64 `json:"rp_per_kg"`
Amount float64 `json:"amount"`
}
type Comparison struct {
Budgeting FinancialMetrics `json:"budgeting"`
Realization FinancialMetrics `json:"realization"`
}
// === HPP PURCHASES PACKAGE ===
type HppItem struct {
Type string `json:"type"`
Comparison
}
type HppGroup struct {
GroupName string `json:"group_name"`
Data []HppItem `json:"data"`
}
type SummaryHpp struct {
Label string `json:"label"`
Comparison `json:"-"`
EggBudgeting *FinancialMetrics `json:"egg_budgeting,omitempty"`
EggRealization *FinancialMetrics `json:"egg_realization,omitempty"`
}
type HppPurchasesSection struct {
Hpp []HppGroup `json:"hpp"`
SummaryHpp SummaryHpp `json:"summary_hpp"`
}
// === PROFIT LOSS PACKAGE ===
type PLItem struct {
Type string `json:"type"`
FinancialMetrics
}
type PLSummaryItem struct {
Label string `json:"label"`
FinancialMetrics
}
type PLSummaryGroup struct {
GrossProfit PLSummaryItem `json:"gross_profit"`
SubTotal PLSummaryItem `json:"sub_total"`
NetProfit PLSummaryItem `json:"net_profit"`
}
type ProfitLossData struct {
Penjualan []PLItem `json:"penjualan"`
Pembelian []PLItem `json:"pembelian"`
Overhead PLItem `json:"overhead"`
Ekspedisi PLItem `json:"ekspedisi"`
Summary PLSummaryGroup `json:"summary"`
}
type ProfitLossSection struct {
Data ProfitLossData `json:"data"`
}
// === RESPONSE DTO (ROOT) ===
type ReportResponse struct {
HppPurchases HppPurchasesSection `json:"hpp_purchases"`
ProfitLoss ProfitLossSection `json:"profit_loss"`
}
// === MAPPER FUNCTIONS ===
func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics {
return FinancialMetrics{
RpPerBird: rpPerBird,
RpPerKg: rpPerKg,
Amount: amount,
}
}
func ToComparison(budgeting, realization FinancialMetrics) Comparison {
return Comparison{
Budgeting: budgeting,
Realization: realization,
}
}
// === HPP PENGELUARAN (from Purchase Items) ===
func getFlagLabel(flagType utils.FlagType) string {
return PurchaseLabelPrefix + string(flagType)
}
func buildHppItemsByPurchaseFlags(purchaseItems []entities.PurchaseItem, ctx CalculationContext) []HppItem {
flags := []utils.FlagType{
utils.FlagDOC, utils.FlagPullet, utils.FlagLayer, utils.FlagPakan,
utils.FlagPreStarter, utils.FlagStarter, utils.FlagFinisher,
utils.FlagOVK, utils.FlagObat, utils.FlagVitamin, utils.FlagKimia,
}
items := []HppItem{}
seenFlags := make(map[utils.FlagType]bool)
for _, item := range purchaseItems {
if item.Product == nil || len(item.Product.Flags) == 0 {
continue
}
for _, flag := range item.Product.Flags {
flagType := utils.FlagType(flag.Name)
if slices.Contains(flags, flagType) && !seenFlags[flagType] {
amount := sumPurchasesByFlag(purchaseItems, flagType)
rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.TotalPopulation, ctx.TotalWeightProduced)
items = append(items, HppItem{
Type: getFlagLabel(flagType),
Comparison: ToComparison(
ToFinancialMetrics(rpPerBird, rpPerKg, amount),
ToFinancialMetrics(rpPerBird, rpPerKg, amount),
),
})
seenFlags[flagType] = true
}
}
}
return items
}
// === HPP BAHAN BAKU (from ProjectBudget + ExpenseRealization) ===
func createHppOverheadItem(budgetAmount, realizationAmount float64, ctx CalculationContext) HppItem {
budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(budgetAmount, ctx.TotalPopulation, ctx.TotalWeightProduced)
realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(realizationAmount, ctx.TotalPopulation, ctx.TotalWeightProduced)
return HppItem{
Type: HPPLabelOverhead,
Comparison: ToComparison(
ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, budgetAmount),
ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, realizationAmount),
),
}
}
func createHppEkspedisiItem(ekspedisiAmount float64, ctx CalculationContext) HppItem {
ekspedisiRpPerBird, ekspedisiRpPerKg := calculatePerUnitMetrics(ekspedisiAmount, ctx.TotalPopulation, ctx.TotalWeightProduced)
return HppItem{
Type: HPPLabelEkspedisi,
Comparison: ToComparison(
ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount),
ToFinancialMetrics(ekspedisiRpPerBird, ekspedisiRpPerKg, ekspedisiAmount),
),
}
}
func ToHppBahanBakuGroup(budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, ctx CalculationContext) HppGroup {
items := []HppItem{}
budgetAmount := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true })
realizationAmount := getOperationalExpenses(realizations)
if budgetAmount > 0 || realizationAmount > 0 {
items = append(items, createHppOverheadItem(budgetAmount, realizationAmount, ctx))
}
ekspedisiAmount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi))
items = append(items, createHppEkspedisiItem(ekspedisiAmount, ctx))
return HppGroup{
GroupName: HPPGroupBahanBaku,
Data: items,
}
}
// === HPP SUMMARY ===
func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, projectFlockCategory string, ctx CalculationContext) SummaryHpp {
purchaseTotal := sumPurchaseTotal(purchaseItems)
budgetTotal := sumBudgetsByFilter(budgets, func(*entities.ProjectBudget) bool { return true })
totalBudget := purchaseTotal + budgetTotal
totalRealization := sumRealizationsByFilter(realizations, func(*entities.ExpenseRealization) bool { return true })
budgetRpPerBird, budgetRpPerKg := calculatePerUnitMetrics(totalBudget, ctx.TotalPopulation, ctx.TotalWeightProduced)
realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(totalRealization, ctx.TotalPopulation, ctx.TotalWeightProduced)
summary := SummaryHpp{
Label: label,
Comparison: ToComparison(
ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget),
ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization),
),
}
if projectFlockCategory == string(utils.ProjectFlockCategoryLaying) && ctx.TotalEggWeightKg > 0 {
budgetEggRpPerKg, _ := calculatePerUnitMetrics(totalBudget, 0, ctx.TotalEggWeightKg)
realizationEggRpPerKg, _ := calculatePerUnitMetrics(totalRealization, 0, ctx.TotalEggWeightKg)
summary.EggBudgeting = &FinancialMetrics{
RpPerBird: 0,
RpPerKg: budgetEggRpPerKg,
Amount: totalBudget,
}
summary.EggRealization = &FinancialMetrics{
RpPerBird: 0,
RpPerKg: realizationEggRpPerKg,
Amount: totalRealization,
}
}
return summary
}
func ToHppPurchasesSection(purchaseItems []entities.PurchaseItem, budgets []entities.ProjectBudget, realizations []entities.ExpenseRealization, projectFlockCategory string, ctx CalculationContext) HppPurchasesSection {
hppGroups := []HppGroup{
{
GroupName: HPPGroupPengeluaran,
Data: buildHppItemsByPurchaseFlags(purchaseItems, ctx),
},
ToHppBahanBakuGroup(budgets, realizations, ctx),
}
summaryHpp := ToSummaryHpp(HPPSummaryLabel, purchaseItems, budgets, realizations, projectFlockCategory, ctx)
return HppPurchasesSection{
Hpp: hppGroups,
SummaryHpp: summaryHpp,
}
}
// === PROFIT & LOSS ===
func ToPLItem(itemType string, metrics FinancialMetrics) PLItem {
return PLItem{
Type: itemType,
FinancialMetrics: metrics,
}
}
func ToPLSummaryItem(label string, metrics FinancialMetrics) PLSummaryItem {
return PLSummaryItem{
Label: label,
FinancialMetrics: metrics,
}
}
func createPLItemWithMetrics(itemType string, amount float64, ctx CalculationContext) PLItem {
rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.ActualPopulation, ctx.TotalWeightProduced)
return ToPLItem(itemType, ToFinancialMetrics(rpPerBird, rpPerKg, amount))
}
func sumPLItems(items []PLItem) (totalAmount, totalPerBird float64) {
for _, item := range items {
totalAmount += item.Amount
totalPerBird += item.RpPerBird
}
return
}
func createPenjualanItem(salesType string, amount float64, ctx CalculationContext) PLItem {
rpPerBird, rpPerKg := calculatePerUnitMetrics(amount, ctx.ActualPopulation, ctx.TotalWeightSold)
return ToPLItem(salesType, ToFinancialMetrics(rpPerBird, rpPerKg, amount))
}
func ToPenjualanItems(projectFlockCategory string, deliveryProducts []entities.MarketingDeliveryProduct, ctx CalculationContext) []PLItem {
items := []PLItem{}
categorized := categorizeDeliveriesBySalesType(deliveryProducts)
if projectFlockCategory == string(utils.ProjectFlockCategoryLaying) {
ayamAmount := sumDeliveriesByCategory(categorized[PLSalesTypeChicken])
telurAmount := sumDeliveriesByCategory(categorized[PLSalesTypeEgg])
items = append(items, createPenjualanItem(PLSalesTypeChicken, ayamAmount, ctx))
items = append(items, createPenjualanItem(PLSalesTypeEgg, telurAmount, ctx))
} else {
ayamAmount := sumDeliveriesByCategory(categorized[PLSalesTypeChicken])
items = append(items, createPenjualanItem(PLSalesTypeChicken, ayamAmount, ctx))
}
return items
}
func ToPembelianItems(purchases []entities.PurchaseItem, realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem {
purchaseAmount := sumPurchaseTotal(purchases)
return []PLItem{
createPLItemWithMetrics(PLItemTypeSapronak, purchaseAmount, ctx),
}
}
func ToOverheadItems(realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem {
realizationAmount := getOperationalExpenses(realizations)
return []PLItem{
createPLItemWithMetrics(PLItemTypeOverhead, realizationAmount, ctx),
}
}
func ToEkspedisiItems(realizations []entities.ExpenseRealization, ctx CalculationContext) []PLItem {
amount := sumRealizationsByFilter(realizations, filterRealizationByNonstockFlag(utils.FlagEkspedisi))
return []PLItem{
createPLItemWithMetrics(PLItemTypeEkspedisi, amount, ctx),
}
}
func ToPLSummaryGroup(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) PLSummaryGroup {
totalPenjualan, totalPenjualanPerBird := sumPLItems(penjualanItems)
totalPembelian, totalPembelianPerBird := sumPLItems(pembelianItems)
totalOverhead, totalOverheadPerBird := sumPLItems(overheadItems)
totalEkspedisi, totalEkspedisiPerBird := sumPLItems(ekspedisiItems)
grossProfit := totalPenjualan - totalPembelian
grossProfitPerBird := totalPenjualanPerBird - totalPembelianPerBird
totalOtherExpenses := totalOverhead + totalEkspedisi
totalOtherExpensesPerBird := totalOverheadPerBird + totalEkspedisiPerBird
netProfit := grossProfit - totalOtherExpenses
netProfitPerBird := grossProfitPerBird - totalOtherExpensesPerBird
return PLSummaryGroup{
GrossProfit: ToPLSummaryItem(PLSummaryLabelGrossProfit, ToFinancialMetrics(grossProfitPerBird, 0, grossProfit)),
SubTotal: ToPLSummaryItem(PLSummaryLabelSubTotal, ToFinancialMetrics(totalOtherExpensesPerBird, 0, totalOtherExpenses)),
NetProfit: ToPLSummaryItem(PLSummaryLabelNetProfit, ToFinancialMetrics(netProfitPerBird, 0, netProfit)),
}
}
func ToProfitLossData(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) ProfitLossData {
summary := ToPLSummaryGroup(penjualanItems, pembelianItems, overheadItems, ekspedisiItems)
totalOverhead := aggregatePLItems(overheadItems, PLItemTypeOverhead)
totalEkspedisi := aggregatePLItems(ekspedisiItems, PLItemTypeEkspedisi)
return ProfitLossData{
Penjualan: penjualanItems,
Pembelian: pembelianItems,
Overhead: totalOverhead,
Ekspedisi: totalEkspedisi,
Summary: summary,
}
}
func ToProfitLossSection(penjualanItems, pembelianItems, overheadItems, ekspedisiItems []PLItem) ProfitLossSection {
return ProfitLossSection{
Data: ToProfitLossData(penjualanItems, pembelianItems, overheadItems, ekspedisiItems),
}
}
func aggregatePLItems(items []PLItem, label string) PLItem {
totalAmount, totalPerBird := sumPLItems(items)
return ToPLItem(label, ToFinancialMetrics(totalPerBird, 0, totalAmount))
}
func ToReportResponse(hppPurchases HppPurchasesSection, profitLoss ProfitLossSection) ReportResponse {
return ReportResponse{
HppPurchases: hppPurchases,
ProfitLoss: profitLoss,
}
}
func ToClosingKeuanganReport(input ClosingKeuanganInput) ReportResponse {
var totalPopulation float64
var totalWeightSold float64
for _, chickin := range input.Chickins {
totalPopulation += chickin.UsageQty
}
for _, delivery := range input.DeliveryProducts {
totalWeightSold += delivery.TotalWeight
}
ctx := CalculationContext{
TotalPopulation: totalPopulation,
TotalWeightProduced: input.TotalWeightProduced,
TotalEggWeightKg: input.TotalEggWeightKg,
TotalDepletion: input.TotalDepletion,
TotalWeightSold: totalWeightSold,
ActualPopulation: totalPopulation - input.TotalDepletion,
}
hppSection := ToHppPurchasesSection(input.PurchaseItems, input.Budgets, input.Realizations, input.ProjectFlockCategory, ctx)
penjualanItems := ToPenjualanItems(input.ProjectFlockCategory, input.DeliveryProducts, ctx)
pembelianItems := ToPembelianItems(input.PurchaseItems, input.Realizations, ctx)
overheadItems := ToOverheadItems(input.Realizations, ctx)
ekspedisiItems := ToEkspedisiItems(input.Realizations, ctx)
plSection := ToProfitLossSection(penjualanItems, pembelianItems, overheadItems, ekspedisiItems)
return ToReportResponse(hppSection, plSection)
}
// === HELPER FUNCTIONS ===
func calculatePerUnitMetrics(amount, totalPopulation, totalWeightSold float64) (rpPerBird, rpPerKg float64) {
if totalPopulation > 0 {
rpPerBird = amount / totalPopulation
}
if totalWeightSold > 0 {
rpPerKg = amount / totalWeightSold
}
return rpPerBird, rpPerKg
}
func hasProductFlag(flags []entities.Flag, flagType utils.FlagType) bool {
for _, flag := range flags {
if strings.ToUpper(flag.Name) == string(flagType) {
return true
}
}
return false
}
func filterByPurchaseFlag(flagType utils.FlagType) func(*entities.PurchaseItem) bool {
return func(item *entities.PurchaseItem) bool {
if item.Product == nil || len(item.Product.Flags) == 0 {
return false
}
return hasProductFlag(item.Product.Flags, flagType)
}
}
func filterRealizationByNonstockFlag(flagType utils.FlagType) func(*entities.ExpenseRealization) bool {
return func(realization *entities.ExpenseRealization) bool {
if realization.ExpenseNonstock == nil || realization.ExpenseNonstock.Nonstock == nil {
return false
}
return hasProductFlag(realization.ExpenseNonstock.Nonstock.Flags, flagType)
}
}
func filterRealizationExceptFlag(flagType utils.FlagType) func(*entities.ExpenseRealization) bool {
hasFlag := filterRealizationByNonstockFlag(flagType)
return func(realization *entities.ExpenseRealization) bool {
return !hasFlag(realization)
}
}
func sumByFilter[T any](items []T, extractor func(*T) float64, filter func(*T) bool) float64 {
amount := 0.0
for i := range items {
if filter(&items[i]) {
amount += extractor(&items[i])
}
}
return amount
}
func sumPurchasesByFilter(purchases []entities.PurchaseItem, filter func(*entities.PurchaseItem) bool) float64 {
return sumByFilter(purchases, func(p *entities.PurchaseItem) float64 { return p.TotalPrice }, filter)
}
func sumPurchasesByFlag(purchases []entities.PurchaseItem, flagType utils.FlagType) float64 {
return sumPurchasesByFilter(purchases, filterByPurchaseFlag(flagType))
}
func sumPurchaseTotal(purchases []entities.PurchaseItem) float64 {
return sumByFilter(purchases, func(p *entities.PurchaseItem) float64 { return p.TotalPrice }, func(*entities.PurchaseItem) bool { return true })
}
func sumBudgetsByFilter(budgets []entities.ProjectBudget, filter func(*entities.ProjectBudget) bool) float64 {
return sumByFilter(budgets, func(b *entities.ProjectBudget) float64 { return b.Price * b.Qty }, filter)
}
func sumRealizationsByFilter(realizations []entities.ExpenseRealization, filter func(*entities.ExpenseRealization) bool) float64 {
return sumByFilter(realizations, func(r *entities.ExpenseRealization) float64 { return r.Price * r.Qty }, filter)
}
func getOperationalExpenses(realizations []entities.ExpenseRealization) float64 {
return sumRealizationsByFilter(realizations, filterRealizationExceptFlag(utils.FlagEkspedisi))
}
func isChickenProductFlag(flagType utils.FlagType) bool {
switch flagType {
case utils.FlagDOC, utils.FlagPullet, utils.FlagLayer,
utils.FlagAyamAfkir, utils.FlagAyamCulling, utils.FlagAyamMati:
return true
}
return false
}
func isEggProductFlag(flagType utils.FlagType) bool {
switch flagType {
case utils.FlagTelur, utils.FlagTelurUtuh, utils.FlagTelurPecah,
utils.FlagTelurPutih, utils.FlagTelurRetak:
return true
}
return false
}
func getSalesTypeFromProductFlags(product *entities.Product) string {
if product == nil || len(product.Flags) == 0 {
return PLSalesTypeChicken
}
for _, flag := range product.Flags {
flagType := utils.FlagType(strings.ToUpper(flag.Name))
if isEggProductFlag(flagType) {
return PLSalesTypeEgg
}
if isChickenProductFlag(flagType) {
return PLSalesTypeChicken
}
}
return PLSalesTypeChicken
}
func categorizeDeliveriesBySalesType(deliveries []entities.MarketingDeliveryProduct) map[string][]entities.MarketingDeliveryProduct {
categorized := make(map[string][]entities.MarketingDeliveryProduct)
for _, delivery := range deliveries {
product := delivery.MarketingProduct.ProductWarehouse.Product
salesType := getSalesTypeFromProductFlags(&product)
categorized[salesType] = append(categorized[salesType], delivery)
}
return categorized
}
func sumDeliveriesByCategory(deliveries []entities.MarketingDeliveryProduct) float64 {
amount := 0.0
for _, delivery := range deliveries {
amount += delivery.TotalPrice
}
return amount
}
@@ -0,0 +1,119 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
deliveryOrdersDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
)
// === Response DTO ===
type SalesDTO struct {
Id uint `json:"id"`
RealizationDate time.Time `json:"realization_date"`
Age int `json:"age"`
DoNumber string `json:"do_number"`
Product *productDTO.ProductRelationDTO `json:"product,omitempty"`
Customer *customerDTO.CustomerRelationDTO `json:"customer,omitempty"`
Qty float64 `json:"qty"`
Weight float64 `json:"weight"`
AvgWeight float64 `json:"avg_weight"`
Price float64 `json:"price"`
TotalPrice float64 `json:"total_price"`
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
PaymentStatus string `json:"payment_status"`
}
type PenjualanRealisasiResponseDTO struct {
Sales []SalesDTO `json:"sales"`
}
// === Mapper Functions ===
func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO {
age := calculateAgeFromChickin(e.MarketingProduct.ProductWarehouse.ProjectFlockKandang, e.DeliveryDate)
var product *productDTO.ProductRelationDTO
if e.MarketingProduct.ProductWarehouse.Product.Id != 0 {
mapped := productDTO.ToProductRelationDTO(e.MarketingProduct.ProductWarehouse.Product)
product = &mapped
}
var customer *customerDTO.CustomerRelationDTO
if e.MarketingProduct.Marketing.Customer.Id != 0 {
mapped := customerDTO.ToCustomerRelationDTO(e.MarketingProduct.Marketing.Customer)
customer = &mapped
}
var kandang *kandangDTO.KandangRelationDTO
if e.MarketingProduct.ProductWarehouse.ProjectFlockKandang != nil && e.MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang.Id != 0 {
mapped := kandangDTO.ToKandangRelationDTO(e.MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang)
kandang = &mapped
}
doNumber := deliveryOrdersDTO.GenerateDeliveryOrderNumber(e.MarketingProduct.Marketing.SoNumber, e.DeliveryDate, e.MarketingProduct.ProductWarehouse.Warehouse.Id)
return SalesDTO{
Id: e.Id,
RealizationDate: *e.DeliveryDate,
Age: age,
DoNumber: doNumber,
Product: product,
Customer: customer,
Qty: e.UsageQty, // Show allocated quantity from FIFO
Weight: e.TotalWeight,
AvgWeight: e.AvgWeight,
Price: e.UnitPrice,
TotalPrice: e.TotalPrice,
Kandang: kandang,
PaymentStatus: "Paid",
}
}
func ToSalesDTOs(e []entity.MarketingDeliveryProduct) []SalesDTO {
result := make([]SalesDTO, len(e))
for i, r := range e {
result[i] = ToSalesDTO(r)
}
return result
}
func ToPenjualanRealisasiResponseDTO(projectType string, projectFlockID uint, e []entity.MarketingDeliveryProduct) PenjualanRealisasiResponseDTO {
return PenjualanRealisasiResponseDTO{
Sales: ToSalesDTOs(e),
}
}
func extractPeriodFromRealisasi(realisasi []entity.MarketingDeliveryProduct) int {
if len(realisasi) > 0 {
for _, item := range realisasi {
if item.MarketingProduct.ProductWarehouse.ProjectFlockKandang != nil {
return item.MarketingProduct.ProductWarehouse.ProjectFlockKandang.Period
}
}
}
return 0
}
func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, deliveryDate *time.Time) int {
if projectFlockKandang == nil || deliveryDate == nil || len(projectFlockKandang.Chickins) == 0 {
return 0
}
earliestChickinDate := projectFlockKandang.Chickins[0].ChickInDate
for _, chickin := range projectFlockKandang.Chickins {
if chickin.ChickInDate.Before(earliestChickinDate) {
earliestChickinDate = chickin.ChickInDate
}
}
ageInDays := int(deliveryDate.Sub(earliestChickinDate).Hours() / 24)
ageInWeeks := ageInDays / 7
return ageInWeeks
}
@@ -0,0 +1,176 @@
package dto
import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
)
// === DTO Structs ===
type OverheadDTO struct {
ItemName string `json:"item_name"`
UOMName string `json:"uom_name"`
BudgetQuantity float64 `json:"budget_quantity"`
BudgetUnitPrice float64 `json:"budget_unit_price"`
BudgetTotalAmount float64 `json:"budget_total_amount"`
ActualDate string `json:"actual_date"`
ActualQuantity float64 `json:"actual_quantity"`
ActualUnitPrice float64 `json:"actual_unit_price"`
ActualTotalAmount float64 `json:"actual_total_amount"`
CostPerBird float64 `json:"cost_per_bird"`
}
type TotalDTO struct {
BudgetQuantity float64 `json:"budget_quantity"`
BudgetTotalAmount float64 `json:"budget_total_amount"`
ActualQuantity float64 `json:"actual_quantity"`
ActualTotalAmount float64 `json:"actual_total_amount"`
CostPerBird float64 `json:"cost_per_bird"`
}
type OverheadListDTO struct {
Total TotalDTO `json:"total"`
Overheads []OverheadDTO `json:"overheads"`
}
// === Mapper Functions ===
func ToOverheadDTO(budget *entity.ProjectBudget, realization *entity.ExpenseRealization) OverheadDTO {
if budget == nil && realization == nil {
return OverheadDTO{}
}
var itemName, itemUOM string
if budget != nil {
itemName, itemUOM = getItemInfo(budget.Nonstock)
}
if itemName == "" && realization != nil && realization.ExpenseNonstock != nil {
itemName, itemUOM = getItemInfo(realization.ExpenseNonstock.Nonstock)
}
dto := OverheadDTO{
ItemName: itemName,
UOMName: itemUOM,
}
if budget != nil {
dto.BudgetQuantity = budget.Qty
dto.BudgetUnitPrice = budget.Price
dto.BudgetTotalAmount = calculateTotal(budget.Qty, budget.Price)
}
if realization != nil {
dto.ActualQuantity = realization.Qty
dto.ActualUnitPrice = realization.Price
dto.ActualTotalAmount = calculateTotal(realization.Qty, realization.Price)
dto.ActualDate = formatRealizationDate(realization)
}
return dto
}
func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty, totalActualPopulation float64) OverheadListDTO {
overheadsByNonstockID := make(map[uint]*OverheadDTO)
latestDateByNonstockID := make(map[uint]string)
for i := range budgets {
nonstockID := budgets[i].NonstockId
if overheadsByNonstockID[nonstockID] == nil {
overheadsByNonstockID[nonstockID] = &OverheadDTO{}
}
itemName, itemUOM := getItemInfo(budgets[i].Nonstock)
overheadsByNonstockID[nonstockID].ItemName = itemName
overheadsByNonstockID[nonstockID].UOMName = itemUOM
overheadsByNonstockID[nonstockID].BudgetQuantity = budgets[i].Qty
overheadsByNonstockID[nonstockID].BudgetUnitPrice = budgets[i].Price
overheadsByNonstockID[nonstockID].BudgetTotalAmount = calculateTotal(budgets[i].Qty, budgets[i].Price)
}
for i := range realizations {
if realizations[i].ExpenseNonstock == nil || realizations[i].ExpenseNonstock.NonstockId == nil {
continue
}
nonstockID := uint(*realizations[i].ExpenseNonstock.NonstockId)
if overheadsByNonstockID[nonstockID] == nil {
overheadsByNonstockID[nonstockID] = &OverheadDTO{}
}
overheadsByNonstockID[nonstockID].ActualQuantity += realizations[i].Qty
overheadsByNonstockID[nonstockID].ActualTotalAmount += calculateTotal(realizations[i].Qty, realizations[i].Price)
if overheadsByNonstockID[nonstockID].ItemName == "" {
itemName, itemUOM := getItemInfo(realizations[i].ExpenseNonstock.Nonstock)
overheadsByNonstockID[nonstockID].ItemName = itemName
overheadsByNonstockID[nonstockID].UOMName = itemUOM
}
realizationDateStr := formatRealizationDate(&realizations[i])
if realizationDateStr != "" {
if latestDateByNonstockID[nonstockID] == "" || realizationDateStr > latestDateByNonstockID[nonstockID] {
latestDateByNonstockID[nonstockID] = realizationDateStr
}
}
}
var totalBudgetQuantity, totalBudgetAmount, totalActualQuantity, totalActualAmount float64
overheadItems := make([]OverheadDTO, 0, len(overheadsByNonstockID))
for nonstockID, overhead := range overheadsByNonstockID {
overhead.ActualDate = latestDateByNonstockID[nonstockID]
overhead.CostPerBird = calculateCostPerBird(overhead.ActualTotalAmount, totalActualPopulation)
if overhead.ActualQuantity > 0 {
overhead.ActualUnitPrice = overhead.ActualTotalAmount / overhead.ActualQuantity
}
totalBudgetQuantity += overhead.BudgetQuantity
totalBudgetAmount += overhead.BudgetTotalAmount
totalActualQuantity += overhead.ActualQuantity
totalActualAmount += overhead.ActualTotalAmount
overheadItems = append(overheadItems, *overhead)
}
return OverheadListDTO{
Total: TotalDTO{
BudgetQuantity: totalBudgetQuantity,
BudgetTotalAmount: totalBudgetAmount,
ActualQuantity: totalActualQuantity,
ActualTotalAmount: totalActualAmount,
CostPerBird: calculateCostPerBird(totalActualAmount, totalActualPopulation),
},
Overheads: overheadItems,
}
}
// === Helper Functions ===
func getItemInfo(nonstock *entity.Nonstock) (string, string) {
if nonstock != nil && nonstock.Id != 0 {
return nonstock.Name, nonstock.Uom.Name
}
return "", ""
}
func calculateTotal(qty, price float64) float64 {
return qty * price
}
func calculateCostPerBird(totalPrice, totalActualPopulation float64) float64 {
if totalActualPopulation > 0 {
return totalPrice / totalActualPopulation
}
return 0
}
func formatRealizationDate(realization *entity.ExpenseRealization) string {
if realization != nil && realization.ExpenseNonstock != nil && realization.ExpenseNonstock.Expense != nil {
if !realization.ExpenseNonstock.Expense.RealizationDate.IsZero() {
return realization.ExpenseNonstock.Expense.RealizationDate.Format("2006-01-02T15:04:05Z07:00")
}
}
return ""
}
@@ -0,0 +1,252 @@
package dto
import (
"strings"
"time"
)
type SapronakDetailDTO struct {
ProductID uint `json:"product_id"`
ProductName string `json:"product_name"`
Flag string `json:"flag"`
Tanggal *time.Time `json:"tanggal,omitempty"`
NoReferensi string `json:"no_referensi,omitempty"`
JenisTransaksi string `json:"jenis_transaksi,omitempty"`
QtyMasuk float64 `json:"qty_masuk"`
QtyKeluar float64 `json:"qty_keluar"`
Harga float64 `json:"harga"`
Nilai float64 `json:"nilai"`
}
type SapronakGroupDTO struct {
Flag string `json:"flag"`
Items []SapronakDetailDTO `json:"items"`
TotalMasuk float64 `json:"total_masuk"`
TotalKeluar float64 `json:"total_keluar"`
SaldoAkhir float64 `json:"saldo_akhir"`
TotalNilai float64 `json:"total_nilai"`
}
type SapronakItemDTO struct {
ProductID uint `json:"product_id"`
ProductName string `json:"product_name"`
Flag string `json:"flag"`
IncomingQty float64 `json:"incoming_qty"`
IncomingValue float64 `json:"incoming_value"`
UsageQty float64 `json:"usage_qty"`
UsageValue float64 `json:"usage_value"`
RemainingQty float64 `json:"remaining_qty"`
AveragePrice float64 `json:"average_price"`
}
type SapronakReportDTO struct {
ProjectFlockKandangID uint `json:"project_flock_kandang_id"`
ProjectFlockID uint `json:"project_flock_id"`
ProjectName string `json:"project_name"`
KandangID uint `json:"kandang_id"`
KandangName string `json:"kandang_name"`
Period int `json:"period"`
Status string `json:"status"`
StartDate *time.Time `json:"start_date,omitempty"`
EndDate *time.Time `json:"end_date,omitempty"`
TotalIncomingValue float64 `json:"total_incoming_value"`
TotalUsageValue float64 `json:"total_usage_value"`
Items []SapronakItemDTO `json:"items"`
Groups []SapronakGroupDTO `json:"groups,omitempty"`
}
// Simplified view for project-level sapronak response
type SapronakCategoryRowDTO struct {
ID int `json:"id"`
Date string `json:"date"`
ReferenceNumber string `json:"reference_number"`
QtyIn float64 `json:"qty_in"`
QtyOut float64 `json:"qty_out"`
QtyUsed float64 `json:"qty_used"`
Description string `json:"description"`
ProductCategory string `json:"product_category"`
UnitPrice float64 `json:"unit_price"`
TotalAmount float64 `json:"total_amount"`
Notes string `json:"notes"`
}
type SapronakCategoryTotalDTO struct {
Label string `json:"label"`
QtyIn float64 `json:"qty_in"`
QtyOut float64 `json:"qty_out"`
QtyUsed float64 `json:"qty_used"`
AvgUnitPrice float64 `json:"avg_unit_price"`
TotalAmount float64 `json:"total_amount"`
}
type SapronakCategoryDTO struct {
Rows []SapronakCategoryRowDTO `json:"rows"`
Total SapronakCategoryTotalDTO `json:"total"`
}
type SapronakProjectAggregatedDTO struct {
Doc *SapronakCategoryDTO `json:"doc,omitempty"`
Ovk *SapronakCategoryDTO `json:"ovk,omitempty"`
Pakan *SapronakCategoryDTO `json:"pakan,omitempty"`
Pullet *SapronakCategoryDTO `json:"pullet,omitempty"`
}
type ClosingSapronakItemDTO struct {
Id uint64 `json:"id"`
Date string `json:"date"`
ReferenceNumber string `json:"reference_number"`
TransactionType string `json:"transaction_type"`
ProductName string `json:"product_name"`
ProductCategory string `json:"product_category"`
ProductSubCategory string `json:"product_sub_category"`
SourceWarehouse string `json:"source_warehouse"`
DestinationWarehouse string `json:"destination_warehouse,omitempty"`
// Destination string `json:"destination,omitempty"`
Quantity float64 `json:"quantity"`
Unit string `json:"unit"`
FormattedQuantity string `json:"formatted_quantity"`
Notes string `json:"notes"`
SortDate time.Time `json:"-"`
}
type ClosingSapronakDTO struct {
IncomingSapronak []ClosingSapronakItemDTO `json:"incoming_sapronak"`
OutgoingSapronak []ClosingSapronakItemDTO `json:"outgoing_sapronak"`
}
// === Mapper Functions for Aggregated Sapronak Response ===
func ToSapronakProjectAggregatedFromReports(reports []SapronakReportDTO, flag string) SapronakProjectAggregatedDTO {
result := SapronakProjectAggregatedDTO{}
if len(reports) == 0 {
return result
}
rep := reports[0]
return ToSapronakProjectAggregatedFromReport(&rep, flag)
}
func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag string) SapronakProjectAggregatedDTO {
result := SapronakProjectAggregatedDTO{}
if report == nil {
report = &SapronakReportDTO{}
}
filter := strings.ToUpper(strings.TrimSpace(flag))
byFlag := map[string]**SapronakCategoryDTO{}
if filter == "" || filter == "DOC" {
result.Doc = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
byFlag["DOC"] = &result.Doc
}
if filter == "" || filter == "OVK" {
result.Ovk = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
byFlag["OVK"] = &result.Ovk
}
if filter == "" || filter == "PAKAN" {
result.Pakan = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
byFlag["PAKAN"] = &result.Pakan
}
if filter == "" || filter == "PULLET" {
result.Pullet = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
byFlag["PULLET"] = &result.Pullet
}
formatDate := func(t *time.Time) string {
if t == nil {
return ""
}
return t.Format("02-Jan-2006")
}
for _, group := range report.Groups {
flagKey := strings.ToUpper(group.Flag)
ptr := byFlag[flagKey]
if ptr == nil || *ptr == nil {
continue
}
target := *ptr
rowIndexByProduct := make(map[string]int)
getOrCreateRow := func(productKey string, base SapronakCategoryRowDTO) *SapronakCategoryRowDTO {
if idx, ok := rowIndexByProduct[productKey]; ok {
return &target.Rows[idx]
}
target.Rows = append(target.Rows, base)
idx := len(target.Rows) - 1
rowIndexByProduct[productKey] = idx
return &target.Rows[idx]
}
for idx, item := range group.Items {
productKey := strings.ToUpper(group.Flag + "|" + item.ProductName)
baseRow := SapronakCategoryRowDTO{
ID: idx + 1,
Date: formatDate(item.Tanggal),
ReferenceNumber: item.NoReferensi,
Description: item.ProductName,
ProductCategory: item.ProductName,
UnitPrice: item.Harga,
Notes: "-",
}
row := getOrCreateRow(productKey, baseRow)
switch strings.ToLower(item.JenisTransaksi) {
case "pembelian", "adjustment masuk", "mutasi masuk":
row.QtyIn += item.QtyMasuk
row.TotalAmount += item.Nilai
case "pemakaian", "adjustment keluar":
row.QtyUsed += item.QtyKeluar
case "mutasi keluar":
row.QtyOut += item.QtyKeluar
default:
row.QtyIn += item.QtyMasuk
row.TotalAmount += item.Nilai
}
if row.QtyIn > 0 {
row.UnitPrice = row.TotalAmount / row.QtyIn
}
}
for i := range target.Rows {
target.Rows[i].ID = i + 1
}
}
buildTotals := func(cat *SapronakCategoryDTO, label string) {
if cat == nil {
return
}
var qtyIn, qtyOut, qtyUsed, total float64
for _, r := range cat.Rows {
qtyIn += r.QtyIn
qtyOut += r.QtyOut
qtyUsed += r.QtyUsed
total += r.TotalAmount
}
avg := 0.0
if qtyIn > 0 {
avg = total / qtyIn
}
cat.Total = SapronakCategoryTotalDTO{
Label: label,
QtyIn: qtyIn,
QtyOut: qtyOut,
QtyUsed: qtyUsed,
AvgUnitPrice: avg,
TotalAmount: total,
}
}
buildTotals(result.Doc, "TOTAL DOC")
buildTotals(result.Ovk, "TOTAL OVK")
buildTotals(result.Pakan, "TOTAL PAKAN")
buildTotals(result.Pullet, "TOTAL PULLET")
return result
}
+22 -3
View File
@@ -5,8 +5,16 @@ import (
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"gorm.io/gorm" "gorm.io/gorm"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
rClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories" rClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
sClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services" sClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
rExpenseRealization "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
rMarketings "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
rChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
rPurchase "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -17,10 +25,21 @@ type ClosingModule struct{}
func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
closingRepo := rClosing.NewClosingRepository(db) closingRepo := rClosing.NewClosingRepository(db)
userRepo := rUser.NewUserRepository(db) userRepo := rUser.NewUserRepository(db)
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db)
projectBudgetRepo := rProjectFlock.NewProjectBudgetRepository(db)
marketingRepo := rMarketings.NewMarketingRepository(db)
marketingDeliveryProductRepo := rMarketings.NewMarketingDeliveryProductRepository(db)
expenseRealizationRepo := rExpenseRealization.NewExpenseRealizationRepository(db)
chickinRepo := rChickin.NewChickinRepository(db)
recordingRepo := rRecording.NewRecordingRepository(db)
purchaseRepo := rPurchase.NewPurchaseRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
closingService := sClosing.NewClosingService(closingRepo, validate) closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, validate)
sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate)
userService := sUser.NewUserService(userRepo, validate) userService := sUser.NewUserService(userRepo, validate)
ClosingRoutes(router, userService, closingService) ClosingRoutes(router, userService, closingService, sapronakService)
} }
File diff suppressed because it is too large Load Diff
+15 -5
View File
@@ -1,7 +1,7 @@
package closings package closings
import ( import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware" m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/controllers" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/controllers"
closing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services" closing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -9,10 +9,11 @@ import (
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService) { func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService, sapronakSvc closing.SapronakService) {
ctrl := controller.NewClosingController(s) ctrl := controller.NewClosingController(s, sapronakSvc)
route := v1.Group("/closings") route := v1.Group("/closings")
route.Use(m.Auth(u))
// route.Get("/", m.Auth(u), ctrl.GetAll) // route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne) // route.Post("/", m.Auth(u), ctrl.CreateOne)
@@ -20,6 +21,15 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne) // route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll) route.Get("/", m.RequirePermissions(m.P_ClosingGetAll), ctrl.GetAll)
route.Get("/:id", ctrl.GetOne) route.Get("/:project_flock_id/penjualan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetPenjualan)
route.Get("/:projectFlockId", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSummary)
route.Get("/:project_flock_id/overhead", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetOverhead)
route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByKandang)
route.Get("/:project_flock_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByProject)
route.Get("/:projectFlockId/sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSapronak)
route.Get("/:project_flock_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPP)
route.Get("/:project_flock_id/:project_flock_kandang_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPPByKandang)
route.Get("/:projectFlockId/data-produksi", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingDataProduksi)
route.Get("/:projectFlockId/keuangan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingKeuangan)
} }

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