From 83474a25398a89b62eef604faaaa30df1282b253 Mon Sep 17 00:00:00 2001 From: Afif Hendrawan Date: Fri, 8 Aug 2025 14:12:40 +0700 Subject: [PATCH] initial commit --- .gitignore | 3 + .gitlab-ci.yml | 25 + README.md | 173 + api-docs/README.md | 0 api-docs/postman-koperasi-blockchain.zip | Bin 0 -> 70720 bytes api-docs/swagger-output.json | 3403 +++++ env-init.sh | 4 + install.sh | 2 + services/backend/.env.example | 22 + services/backend/.gitignore | 36 + services/backend/LICENSE | 21 + services/backend/README.md | 53 + .../statis/formatan-dokumen-proyeksi.docx | Bin 0 -> 12672 bytes .../assets/uploads/brosur_produk/.gitkeep | 0 .../assets/uploads/bukti_pembayaran/.gitkeep | 0 .../assets/uploads/bukti_transfer/.gitkeep | 0 .../assets/uploads/dokumen_pendukung/.gitkeep | 0 .../uploads/dokumen_prospektus/.gitkeep | 0 .../assets/uploads/dokumen_proyeksi/.gitkeep | 0 .../backend/assets/uploads/foto_diri/.gitkeep | 0 .../backend/assets/uploads/foto_ktp/.gitkeep | 0 .../assets/uploads/foto_profile/.gitkeep | 0 .../assets/uploads/laporan_keuangan/.gitkeep | 0 .../assets/uploads/laporan_mutasi/.gitkeep | 0 .../assets/uploads/tanda_tangan/.gitkeep | 0 .../uploads/tanda_tangan_admin/.gitkeep | 0 services/backend/contractABI.json | 2031 +++ services/backend/drizzle.config.ts | 12 + services/backend/express.d.ts | 6 + services/backend/package-lock.json | 4869 ++++++ services/backend/package.json | 65 + .../src/controllers/agreement-letter.ts | 56 + services/backend/src/controllers/auth.ts | 63 + services/backend/src/controllers/baileys.ts | 23 + .../backend/src/controllers/chart-project.ts | 32 + .../backend/src/controllers/chart-token.ts | 50 + .../src/controllers/history-project-wallet.ts | 15 + .../src/controllers/history-project.ts | 13 + .../backend/src/controllers/history-token.ts | 20 + services/backend/src/controllers/mutation.ts | 54 + .../src/controllers/project-category.ts | 70 + .../backend/src/controllers/project-report.ts | 63 + .../backend/src/controllers/project-token.ts | 124 + .../backend/src/controllers/project-wallet.ts | 42 + services/backend/src/controllers/project.ts | 247 + services/backend/src/controllers/topup.ts | 272 + .../backend/src/controllers/transaction.ts | 57 + services/backend/src/controllers/user.ts | 148 + services/backend/src/controllers/wilayah.ts | 84 + services/backend/src/drizzle/db.ts | 15 + services/backend/src/drizzle/migrate.ts | 16 + services/backend/src/drizzle/schema.ts | 270 + services/backend/src/drizzle/seed.ts | 142 + .../src/drizzle/seeder/agreement-letter.ts | 52 + .../src/drizzle/seeder/chart-project.ts | 49 + .../backend/src/drizzle/seeder/chart-token.ts | 55 + .../drizzle/seeder/history-project-wallet.ts | 37 + .../src/drizzle/seeder/history-project.ts | 33 + .../src/drizzle/seeder/history-token.ts | 39 + .../src/drizzle/seeder/project-mutation.ts | 70 + .../src/drizzle/seeder/project-report.ts | 71 + .../src/drizzle/seeder/project-wallet.ts | 31 + .../backend/src/drizzle/seeder/project.ts | 125 + .../src/drizzle/seeder/signature-admin.ts | 9 + services/backend/src/drizzle/seeder/token.ts | 33 + services/backend/src/drizzle/seeder/topup.ts | 101 + .../backend/src/drizzle/seeder/transaction.ts | 37 + services/backend/src/drizzle/seeder/user.ts | 91 + services/backend/src/drizzle/seeder/wallet.ts | 89 + services/backend/src/main.ts | 119 + .../src/middlewares/agreement-letter.ts | 40 + services/backend/src/middlewares/api-key.ts | 15 + services/backend/src/middlewares/auth.ts | 110 + .../src/middlewares/history-project-wallet.ts | 34 + services/backend/src/middlewares/mutation.ts | 33 + .../backend/src/middlewares/project-report.ts | 33 + services/backend/src/middlewares/project.ts | 150 + services/backend/src/middlewares/topup.ts | 34 + .../src/middlewares/validate.resource.ts | 24 + services/backend/src/routes/auth.ts | 13 + services/backend/src/routes/baileys.ts | 11 + services/backend/src/routes/chart-project.ts | 9 + services/backend/src/routes/chart-token.ts | 10 + .../src/routes/history-project-wallet.ts | 8 + .../backend/src/routes/history-project.ts | 8 + services/backend/src/routes/history-token.ts | 8 + services/backend/src/routes/mutation.ts | 14 + .../backend/src/routes/project-category.ts | 15 + services/backend/src/routes/project-report.ts | 15 + services/backend/src/routes/project-token.ts | 18 + services/backend/src/routes/project-wallet.ts | 14 + services/backend/src/routes/project.ts | 53 + services/backend/src/routes/topup.ts | 48 + services/backend/src/routes/transaction.ts | 14 + services/backend/src/routes/user.ts | 34 + services/backend/src/routes/wilayah.ts | 11 + .../backend/src/services/agreement-letter.ts | 189 + services/backend/src/services/auth.ts | 52 + services/backend/src/services/baileys.ts | 80 + .../backend/src/services/chart-project.ts | 45 + services/backend/src/services/chart-token.ts | 98 + .../src/services/history-project-wallet.ts | 41 + .../backend/src/services/history-project.ts | 15 + .../backend/src/services/history-token.ts | 64 + services/backend/src/services/jwt.ts | 13 + services/backend/src/services/mutation.ts | 393 + .../backend/src/services/project-category.ts | 60 + .../backend/src/services/project-report.ts | 209 + .../backend/src/services/project-token.ts | 457 + .../backend/src/services/project-wallet.ts | 87 + services/backend/src/services/project.ts | 1220 ++ services/backend/src/services/topup.ts | 617 + services/backend/src/services/transaction.ts | 83 + services/backend/src/services/user.ts | 419 + services/backend/src/services/wallet.ts | 84 + services/backend/src/swagger.ts | 16 + services/backend/src/validations/auth.ts | 17 + services/backend/src/validations/mutation.ts | 10 + .../src/validations/project-category.ts | 13 + .../backend/src/validations/project-report.ts | 10 + .../backend/src/validations/project-token.ts | 7 + .../backend/src/validations/project-wallet.ts | 8 + services/backend/src/validations/project.ts | 39 + services/backend/src/validations/topup.ts | 32 + services/backend/src/validations/user.ts | 9 + services/backend/tsconfig.json | 14 + services/frontend/.env.example | 3 + services/frontend/.gitignore | 5 + services/frontend/.vscode/settings.json | 13 + services/frontend/README.md | 111 + .../frontend/app/components/back-button.tsx | 18 + .../app/components/cards/bordered-card.tsx | 13 + .../app/components/cards/modal-card.tsx | 21 + .../app/components/cards/project-card.tsx | 134 + .../cards/project-used-token-card.tsx | 77 + .../app/components/document-uploader.tsx | 88 + .../document/aggrement-letter.client.tsx | 338 + .../app/components/extensions/file-upload.tsx | 337 + services/frontend/app/components/icons.tsx | 66 + .../app/components/input/PasswordObscure.tsx | 39 + .../app/components/layouts/dashboard-nav.tsx | 142 + .../app/components/layouts/header.tsx | 36 + .../app/components/layouts/mobile-sidebar.tsx | 41 + .../app/components/layouts/sidebar.tsx | 53 + .../app/components/layouts/user-nav.tsx | 68 + .../app/components/modal/alert-modal.tsx | 44 + .../app/components/page-container.tsx | 18 + .../app/components/table/data-table.tsx | 115 + .../frontend/app/components/table/table.tsx | 67 + .../frontend/app/components/theme-toogle.tsx | 28 + .../frontend/app/components/ui/accordion.tsx | 52 + .../app/components/ui/alert-dialog.tsx | 115 + .../frontend/app/components/ui/avatar.tsx | 45 + services/frontend/app/components/ui/badge.tsx | 31 + .../frontend/app/components/ui/button.tsx | 49 + services/frontend/app/components/ui/card.tsx | 56 + .../frontend/app/components/ui/carousel.tsx | 240 + services/frontend/app/components/ui/chart.tsx | 328 + .../frontend/app/components/ui/checkbox.tsx | 26 + .../frontend/app/components/ui/combobox.tsx | 91 + .../frontend/app/components/ui/command.tsx | 143 + .../frontend/app/components/ui/dialog.tsx | 102 + .../app/components/ui/dropdown-menu.tsx | 185 + services/frontend/app/components/ui/form.tsx | 167 + .../frontend/app/components/ui/input-otp.tsx | 69 + services/frontend/app/components/ui/input.tsx | 24 + services/frontend/app/components/ui/label.tsx | 19 + services/frontend/app/components/ui/modal.tsx | 35 + .../app/components/ui/navigation-menu.tsx | 120 + .../frontend/app/components/ui/pagination.tsx | 98 + .../frontend/app/components/ui/popover.tsx | 29 + .../frontend/app/components/ui/progress.tsx | 23 + .../app/components/ui/radio-group.tsx | 36 + .../app/components/ui/required-icon.tsx | 3 + .../app/components/ui/scroll-area.tsx | 44 + .../frontend/app/components/ui/select.tsx | 151 + .../frontend/app/components/ui/separator.tsx | 24 + services/frontend/app/components/ui/sheet.tsx | 119 + .../frontend/app/components/ui/spinner.tsx | 23 + .../app/components/ui/styled-pagination.tsx | 106 + services/frontend/app/components/ui/table.tsx | 91 + .../frontend/app/components/ui/textarea.tsx | 23 + .../frontend/app/components/ui/tooltip.tsx | 28 + services/frontend/app/entry.client.tsx | 18 + services/frontend/app/entry.server.tsx | 118 + .../frontend/app/hooks/use-jwt-payload.ts | 12 + services/frontend/app/hooks/use-sidebar.ts | 11 + services/frontend/app/lib/clsx.ts | 6 + services/frontend/app/lib/env.ts | 20 + services/frontend/app/lib/get-env.ts | 14 + services/frontend/app/lib/http.ts | 18 + services/frontend/app/lib/middleware.ts | 38 + services/frontend/app/lib/react-query.ts | 14 + services/frontend/app/root.tsx | 165 + .../app/routes/_index/components/Footer.tsx | 51 + .../app/routes/_index/components/Header.tsx | 181 + services/frontend/app/routes/_index/route.tsx | 206 + services/frontend/app/routes/action.logout.ts | 26 + .../frontend/app/routes/action.set-theme.ts | 4 + .../components/dashboard/basic-dashboard.tsx | 113 + .../dashboard/platinum-dashboard.tsx | 175 + .../app._index/components/table/columns.tsx | 75 + .../app._index/components/upgrade-dialog.tsx | 89 + .../frontend/app/routes/app._index/route.tsx | 19 + .../components/modal/top-up/topup.tsx | 59 + .../components/table/columns.tsx | 75 + .../app/routes/app.dompet._index/route.tsx | 111 + .../route.tsx | 79 + .../components/verify-dialog.tsx | 54 + .../route.tsx | 217 + .../route.tsx | 79 + .../components/verify-dialog.tsx | 54 + .../route.tsx | 217 + .../components/first-section.tsx | 169 + .../components/second-section.tsx | 29 + .../components/table/columns.tsx | 96 + .../app.dompet.tarik-saldo._index/route.tsx | 54 + .../app.member.upgrade._index/route.tsx | 77 + .../components/verify-dialog.tsx | 54 + .../app.member.upgrade.verify/route.tsx | 215 + .../components/first-section.tsx | 114 + .../components/modal/sign-contract.tsx | 106 + .../components/second-section.tsx | 181 + .../components/third-section.tsx | 19 + .../app.my-projects.$id._index/route.tsx | 222 + .../components/form/edit-project-form.tsx | 434 + .../routes/app.my-projects.$id.edit/route.tsx | 29 + .../routes/app.my-projects._index/route.tsx | 111 + .../components/form/create-project-form.tsx | 430 + .../routes/app.my-projects.create/route.tsx | 22 + .../app/routes/app.profile._index/route.tsx | 356 + .../components/first-section.tsx | 114 + .../components/modal/beli.tsx | 124 + .../components/modal/konfirmasi-pembelian.tsx | 122 + .../components/second-section.tsx | 19 + .../app/routes/app.projects.$id/route.tsx | 233 + .../app/routes/app.projects._index/route.tsx | 79 + .../app/routes/app.projects/route.tsx | 10 + .../components/first-section.tsx | 32 + .../components/second-section.tsx | 32 + .../components/table/columns.tsx | 68 + .../routes/app.riwayat-laporan.$id/route.tsx | 70 + .../components/table/columns.tsx | 83 + .../routes/app.riwayat-mutasi.$id/route.tsx | 36 + .../components/first-section.tsx | 114 + .../components/second-section.tsx | 19 + .../components/table/columns.tsx | 63 + .../components/third-section.tsx | 89 + .../app/routes/app.token.$id/route.tsx | 263 + .../app/routes/app/constants/nav-items.ts | 61 + services/frontend/app/routes/app/route.tsx | 51 + .../frontend/app/routes/auth.login/route.tsx | 109 + .../components/confirm-dialog.tsx | 43 + .../auth.register/components/first-form.tsx | 102 + .../auth.register/components/second-form.tsx | 251 + .../app/routes/auth.register/route.tsx | 144 + .../auth/components/RegisterCarousel.tsx | 77 + services/frontend/app/routes/auth/route.tsx | 56 + .../components/table/columns.tsx | 72 + .../app/routes/dashboard._index/route.tsx | 83 + .../routes/dashboard.anggota.$id/route.tsx | 405 + .../components/table/cell-action.tsx | 20 + .../components/table/columns.tsx | 71 + .../routes/dashboard.anggota._index/route.tsx | 28 + .../routes/dashboard.profile._index/route.tsx | 353 + .../components/first-section.tsx | 114 + .../components/modal/approval.tsx | 288 + .../components/modal/publish.tsx | 237 + .../components/modal/revisi.tsx | 99 + .../components/modal/selesai.tsx | 50 + .../components/modal/terima.tsx | 49 + .../components/modal/tolak.tsx | 96 + .../components/second-section.tsx | 125 + .../dashboard.proyek.pengajuan.$id/route.tsx | 266 + .../components/table/cell-action.tsx | 20 + .../components/table/columns.tsx | 78 + .../route.tsx | 33 + .../components/first-section.tsx | 91 + .../modal/laba-rugi/laporan-laba.tsx | 151 + .../modal/laba-rugi/laporan-rugi.tsx | 158 + .../components/modal/laba-rugi/laporan.tsx | 75 + .../components/modal/mutasi/mutasi.tsx | 135 + .../components/second-section.tsx | 19 + .../dashboard.proyek.report.$id/route.tsx | 205 + .../components/table/cell-action.tsx | 20 + .../components/table/columns.tsx | 53 + .../dashboard.proyek.report._index/route.tsx | 62 + .../components/first-section.tsx | 33 + .../components/second-section.tsx | 33 + .../components/table/columns.tsx | 68 + .../dashboard.riwayat-laporan.$id/route.tsx | 70 + .../components/table/columns.tsx | 83 + .../dashboard.riwayat-mutasi.$id/route.tsx | 36 + .../components/modal/detail-penarikan.tsx | 157 + .../components/table/cell-action.tsx | 14 + .../components/table/columns.tsx | 81 + .../dashboard.saldo.penarikan/route.tsx | 25 + .../components/modal/detail-transfer.tsx | 216 + .../components/table/cell-action.tsx | 14 + .../components/table/columns.tsx | 81 + .../route.tsx | 34 + .../components/first-section.tsx | 141 + .../components/second-section.tsx | 36 + .../components/table/columns.tsx | 71 + .../route.tsx | 67 + .../components/table/cell-action.tsx | 20 + .../components/table/columns.tsx | 94 + .../route.tsx | 62 + .../components/modal/penarikan-proyek.tsx | 80 + .../components/table/cell-action.tsx | 14 + .../components/table/columns.tsx | 78 + .../route.tsx | 39 + .../components/modal/detail-topup.tsx | 154 + .../components/table/cell-action.tsx | 14 + .../components/table/columns.tsx | 78 + .../routes/dashboard.saldo.top-up/route.tsx | 45 + .../routes/dashboard/constants/nav-items.ts | 78 + .../frontend/app/routes/dashboard/route.tsx | 31 + .../app/routes/member._index/route.tsx | 45 + .../app/routes/member.pembayaran/route.tsx | 70 + .../components/verify-dialog.tsx | 55 + .../app/routes/member.verify/route.tsx | 194 + services/frontend/app/routes/member/route.tsx | 63 + .../frontend/app/routes/otp._index/route.tsx | 132 + services/frontend/app/routes/otp/route.tsx | 30 + .../frontend/app/services/auth/register.ts | 29 + .../frontend/app/services/category/get-all.ts | 15 + .../app/services/category/get-by-id.ts | 14 + .../chart-project/get-by-project-id.ts | 17 + .../app/services/chart-project/get-by-user.ts | 16 + .../app/services/chart-token/get-all.ts | 16 + .../app/services/history-project/get-by-id.ts | 17 + .../app/services/history-token/get-by-id.ts | 17 + .../frontend/app/services/indonesia-region.ts | 39 + .../frontend/app/services/mutation/create.ts | 37 + .../services/mutation/get-by-project-id.ts | 16 + .../app/services/my-projects/get-all.ts | 25 + .../app/services/profile/get-by-id.ts | 16 + .../app/services/project-report/create.ts | 32 + .../app/services/project-report/get-all.ts | 14 + .../app/services/project-report/get-by-id.ts | 14 + .../project-report/get-by-project-id.ts | 20 + .../app/services/project-wallet/get-all.ts | 48 + .../app/services/project-wallet/get-by-id.ts | 25 + .../project-wallet/get-by-wallet-id.ts | 17 + .../services/project-wallet/transfer-saldo.ts | 28 + .../frontend/app/services/projects/accept.ts | 25 + .../app/services/projects/agreement.ts | 30 + .../frontend/app/services/projects/approve.ts | 33 + .../app/services/projects/completing.ts | 25 + .../frontend/app/services/projects/count.ts | 13 + .../frontend/app/services/projects/create.ts | 45 + .../projects/get-agreement-by-project-id.ts | 16 + .../frontend/app/services/projects/get-all.ts | 48 + .../app/services/projects/get-user-token.ts | 48 + .../frontend/app/services/projects/publish.ts | 31 + .../frontend/app/services/projects/reject.ts | 27 + .../frontend/app/services/projects/revise.ts | 25 + .../app/services/projects/share-profit.ts | 27 + .../app/services/projects/total-profit.ts | 16 + .../frontend/app/services/projects/update.ts | 49 + .../app/services/token/buy-project.ts | 29 + .../app/services/token/token-usage.ts | 15 + .../app/services/token/total-token.ts | 17 + .../app/services/top-up/acc-withdraw.ts | 28 + .../frontend/app/services/top-up/get-all.ts | 22 + .../top-up/get-bagian-pemilik-pelaksana.ts | 24 + .../app/services/top-up/get-by-user.ts | 22 + .../app/services/top-up/get-saldo-user.ts | 13 + .../frontend/app/services/top-up/get-saldo.ts | 22 + .../services/top-up/get-sum-simpanan-pokok.ts | 13 + .../services/top-up/get-sum-simpanan-wajib.ts | 13 + .../app/services/top-up/kas-koperasi.ts | 17 + .../top-up/pay-bagian-pemilik-pelaksana.ts | 31 + .../app/services/top-up/pay-member.ts | 32 + .../app/services/top-up/pay-simpanan-wajib.ts | 33 + .../frontend/app/services/top-up/pay-topup.ts | 33 + .../services/top-up/update-status-by-id.ts | 16 + .../app/services/top-up/withdraw-saldo.ts | 28 + .../app/services/transaction/get-all.ts | 14 + .../services/transaction/get-by-project-id.ts | 16 + .../services/transaction/get-by-user-id.ts | 16 + .../frontend/app/services/user/acc-upgrade.ts | 17 + services/frontend/app/services/user/count.ts | 13 + .../frontend/app/services/user/get-all.ts | 15 + .../frontend/app/services/user/send-otp.ts | 14 + services/frontend/app/services/user/update.ts | 35 + .../app/services/user/upgrade-platinum.ts | 36 + .../frontend/app/services/user/verify-otp.ts | 16 + .../app/services/whatsapp/get-qr-code.ts | 14 + services/frontend/app/sessions/auth.server.ts | 28 + .../frontend/app/sessions/session.server.ts | 17 + .../frontend/app/sessions/themes.server.ts | 17 + services/frontend/app/tailwind.css | 85 + services/frontend/app/types/api/agreement.ts | 15 + services/frontend/app/types/api/anggota.ts | 80 + services/frontend/app/types/api/auth.ts | 64 + .../frontend/app/types/api/chart-project.ts | 13 + .../frontend/app/types/api/dompet-reguler.ts | 32 + .../frontend/app/types/api/history-token.ts | 7 + .../app/types/api/indonesia-region.ts | 22 + .../frontend/app/types/api/investor-data.ts | 6 + services/frontend/app/types/api/kategori.ts | 17 + services/frontend/app/types/api/laporan.ts | 109 + .../app/types/api/pemilik-pelaksana.ts | 45 + .../app/types/api/penarikan-proyek.ts | 42 + .../frontend/app/types/api/proyek-beli.ts | 13 + .../app/types/api/proyek-pendanaan.ts | 50 + services/frontend/app/types/api/proyek.ts | 334 + services/frontend/app/types/api/top-up.ts | 127 + .../frontend/app/types/api/transaction.ts | 16 + .../frontend/app/types/api/verify-payment.ts | 61 + .../app/types/api/whatsapp-qr-code.ts | 4 + .../app/types/constants/base-response.ts | 5 + .../app/types/constants/count-response.ts | 3 + .../frontend/app/types/constants/dokumen.ts | 5 + .../app/types/constants/jwt-payload.ts | 6 + .../frontend/app/types/constants/nav-item.ts | 12 + .../app/types/constants/status-data.ts | 11 + .../app/types/constants/status-project.ts | 31 + .../app/types/constants/status-top-up.ts | 5 + .../frontend/app/utils/file-or-url-schema.ts | 12 + .../frontend/app/utils/format-input-date.ts | 7 + .../app/utils/format-to-locale-time.ts | 8 + .../frontend/app/utils/prefix-file-path.ts | 13 + .../frontend/app/utils/prefix-wallet-path.ts | 13 + services/frontend/app/utils/to-presentase.ts | 14 + services/frontend/app/utils/to-rupiah.ts | 9 + services/frontend/biome.json | 38 + services/frontend/components.json | 17 + services/frontend/package-lock.json | 12332 ++++++++++++++++ services/frontend/package.json | 83 + services/frontend/postcss.config.js | 6 + services/frontend/public/favicon.ico | Bin 0 -> 15406 bytes services/frontend/public/img/404.svg | 9 + .../frontend/public/img/auth/carousel-1.png | Bin 0 -> 308246 bytes .../frontend/public/img/auth/carousel-2.png | Bin 0 -> 302410 bytes .../frontend/public/img/auth/carousel-3.png | Bin 0 -> 242589 bytes services/frontend/public/img/auth/hero.png | Bin 0 -> 8662289 bytes .../public/img/auth/verify-avatar.svg | 44 + services/frontend/public/img/bank/bca.svg | 9 + services/frontend/public/img/bank/bni.svg | 9 + services/frontend/public/img/bank/bri.svg | 9 + services/frontend/public/img/bank/bsi.svg | 9 + services/frontend/public/img/bank/btn.svg | 19 + services/frontend/public/img/bank/mandiri.svg | 9 + services/frontend/public/img/card/diamond.png | Bin 0 -> 19489 bytes services/frontend/public/img/card/emerald.png | Bin 0 -> 11736 bytes .../frontend/public/img/card/gold-bars.png | Bin 0 -> 36710 bytes services/frontend/public/img/card/silver.png | Bin 0 -> 21320 bytes .../frontend/public/img/home/about-us.svg | 128 + services/frontend/public/img/home/hero.svg | 278 + services/frontend/public/img/icon.png | Bin 0 -> 82512 bytes services/frontend/public/img/ttd-koperasi.png | Bin 0 -> 8048 bytes services/frontend/public/img/user-dummy.jpg | Bin 0 -> 89119 bytes .../frontend/public/img/vector/laba-rugi.svg | 36 + .../frontend/public/img/vector/mutasi.svg | 18 + .../frontend/public/img/vector/payment.svg | 338 + services/frontend/tailwind.config.ts | 91 + services/frontend/tsconfig.json | 32 + services/frontend/vite.config.ts | 16 + services/smartcontract/.gitignore | 17 + services/smartcontract/README.md | 35 + .../smartcontract/contracts/ProjectToken.sol | 798 + services/smartcontract/hardhat.config.ts | 28 + .../smartcontract/ignition/modules/Deploy.ts | 9 + services/smartcontract/package-lock.json | 7805 ++++++++++ services/smartcontract/package.json | 18 + services/smartcontract/tsconfig.json | 11 + services/wallet/.env.example | 13 + services/wallet/.gitignore | 24 + services/wallet/LICENSE | 21 + services/wallet/README.md | 50 + .../assets/uploads/bukti_pembayaran/.gitkeep | 0 services/wallet/drizzle.config.ts | 12 + services/wallet/express.d.ts | 6 + services/wallet/package-lock.json | 3329 +++++ services/wallet/package.json | 51 + services/wallet/src/controllers/topup.ts | 143 + services/wallet/src/controllers/wallet.ts | 86 + services/wallet/src/drizzle/db.ts | 15 + services/wallet/src/drizzle/migrate.ts | 16 + services/wallet/src/drizzle/schema.ts | 43 + services/wallet/src/drizzle/seed.ts | 44 + services/wallet/src/drizzle/seeder/topup.ts | 30 + services/wallet/src/drizzle/seeder/wallet.ts | 14 + services/wallet/src/main.ts | 37 + services/wallet/src/middlewares/api-key.ts | 12 + services/wallet/src/middlewares/topup.ts | 34 + services/wallet/src/middlewares/wallet.ts | 0 services/wallet/src/routes/topup.ts | 16 + services/wallet/src/routes/wallet.ts | 13 + services/wallet/src/services/topup.ts | 141 + services/wallet/src/services/wallet.ts | 61 + services/wallet/src/swagger.ts | 16 + services/wallet/src/validations/wallet.ts | 0 services/wallet/tsconfig.json | 14 + 497 files changed, 66375 insertions(+) create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 README.md create mode 100644 api-docs/README.md create mode 100644 api-docs/postman-koperasi-blockchain.zip create mode 100644 api-docs/swagger-output.json create mode 100644 env-init.sh create mode 100644 install.sh create mode 100644 services/backend/.env.example create mode 100644 services/backend/.gitignore create mode 100644 services/backend/LICENSE create mode 100644 services/backend/README.md create mode 100644 services/backend/assets/statis/formatan-dokumen-proyeksi.docx create mode 100644 services/backend/assets/uploads/brosur_produk/.gitkeep create mode 100644 services/backend/assets/uploads/bukti_pembayaran/.gitkeep create mode 100644 services/backend/assets/uploads/bukti_transfer/.gitkeep create mode 100644 services/backend/assets/uploads/dokumen_pendukung/.gitkeep create mode 100644 services/backend/assets/uploads/dokumen_prospektus/.gitkeep create mode 100644 services/backend/assets/uploads/dokumen_proyeksi/.gitkeep create mode 100644 services/backend/assets/uploads/foto_diri/.gitkeep create mode 100644 services/backend/assets/uploads/foto_ktp/.gitkeep create mode 100644 services/backend/assets/uploads/foto_profile/.gitkeep create mode 100644 services/backend/assets/uploads/laporan_keuangan/.gitkeep create mode 100644 services/backend/assets/uploads/laporan_mutasi/.gitkeep create mode 100644 services/backend/assets/uploads/tanda_tangan/.gitkeep create mode 100644 services/backend/assets/uploads/tanda_tangan_admin/.gitkeep create mode 100644 services/backend/contractABI.json create mode 100644 services/backend/drizzle.config.ts create mode 100644 services/backend/express.d.ts create mode 100644 services/backend/package-lock.json create mode 100644 services/backend/package.json create mode 100644 services/backend/src/controllers/agreement-letter.ts create mode 100644 services/backend/src/controllers/auth.ts create mode 100644 services/backend/src/controllers/baileys.ts create mode 100644 services/backend/src/controllers/chart-project.ts create mode 100644 services/backend/src/controllers/chart-token.ts create mode 100644 services/backend/src/controllers/history-project-wallet.ts create mode 100644 services/backend/src/controllers/history-project.ts create mode 100644 services/backend/src/controllers/history-token.ts create mode 100644 services/backend/src/controllers/mutation.ts create mode 100644 services/backend/src/controllers/project-category.ts create mode 100644 services/backend/src/controllers/project-report.ts create mode 100644 services/backend/src/controllers/project-token.ts create mode 100644 services/backend/src/controllers/project-wallet.ts create mode 100644 services/backend/src/controllers/project.ts create mode 100644 services/backend/src/controllers/topup.ts create mode 100644 services/backend/src/controllers/transaction.ts create mode 100644 services/backend/src/controllers/user.ts create mode 100644 services/backend/src/controllers/wilayah.ts create mode 100644 services/backend/src/drizzle/db.ts create mode 100644 services/backend/src/drizzle/migrate.ts create mode 100644 services/backend/src/drizzle/schema.ts create mode 100644 services/backend/src/drizzle/seed.ts create mode 100644 services/backend/src/drizzle/seeder/agreement-letter.ts create mode 100644 services/backend/src/drizzle/seeder/chart-project.ts create mode 100644 services/backend/src/drizzle/seeder/chart-token.ts create mode 100644 services/backend/src/drizzle/seeder/history-project-wallet.ts create mode 100644 services/backend/src/drizzle/seeder/history-project.ts create mode 100644 services/backend/src/drizzle/seeder/history-token.ts create mode 100644 services/backend/src/drizzle/seeder/project-mutation.ts create mode 100644 services/backend/src/drizzle/seeder/project-report.ts create mode 100644 services/backend/src/drizzle/seeder/project-wallet.ts create mode 100644 services/backend/src/drizzle/seeder/project.ts create mode 100644 services/backend/src/drizzle/seeder/signature-admin.ts create mode 100644 services/backend/src/drizzle/seeder/token.ts create mode 100644 services/backend/src/drizzle/seeder/topup.ts create mode 100644 services/backend/src/drizzle/seeder/transaction.ts create mode 100644 services/backend/src/drizzle/seeder/user.ts create mode 100644 services/backend/src/drizzle/seeder/wallet.ts create mode 100644 services/backend/src/main.ts create mode 100644 services/backend/src/middlewares/agreement-letter.ts create mode 100644 services/backend/src/middlewares/api-key.ts create mode 100644 services/backend/src/middlewares/auth.ts create mode 100644 services/backend/src/middlewares/history-project-wallet.ts create mode 100644 services/backend/src/middlewares/mutation.ts create mode 100644 services/backend/src/middlewares/project-report.ts create mode 100644 services/backend/src/middlewares/project.ts create mode 100644 services/backend/src/middlewares/topup.ts create mode 100644 services/backend/src/middlewares/validate.resource.ts create mode 100644 services/backend/src/routes/auth.ts create mode 100644 services/backend/src/routes/baileys.ts create mode 100644 services/backend/src/routes/chart-project.ts create mode 100644 services/backend/src/routes/chart-token.ts create mode 100644 services/backend/src/routes/history-project-wallet.ts create mode 100644 services/backend/src/routes/history-project.ts create mode 100644 services/backend/src/routes/history-token.ts create mode 100644 services/backend/src/routes/mutation.ts create mode 100644 services/backend/src/routes/project-category.ts create mode 100644 services/backend/src/routes/project-report.ts create mode 100644 services/backend/src/routes/project-token.ts create mode 100644 services/backend/src/routes/project-wallet.ts create mode 100644 services/backend/src/routes/project.ts create mode 100644 services/backend/src/routes/topup.ts create mode 100644 services/backend/src/routes/transaction.ts create mode 100644 services/backend/src/routes/user.ts create mode 100644 services/backend/src/routes/wilayah.ts create mode 100644 services/backend/src/services/agreement-letter.ts create mode 100644 services/backend/src/services/auth.ts create mode 100644 services/backend/src/services/baileys.ts create mode 100644 services/backend/src/services/chart-project.ts create mode 100644 services/backend/src/services/chart-token.ts create mode 100644 services/backend/src/services/history-project-wallet.ts create mode 100644 services/backend/src/services/history-project.ts create mode 100644 services/backend/src/services/history-token.ts create mode 100644 services/backend/src/services/jwt.ts create mode 100644 services/backend/src/services/mutation.ts create mode 100644 services/backend/src/services/project-category.ts create mode 100644 services/backend/src/services/project-report.ts create mode 100644 services/backend/src/services/project-token.ts create mode 100644 services/backend/src/services/project-wallet.ts create mode 100644 services/backend/src/services/project.ts create mode 100644 services/backend/src/services/topup.ts create mode 100644 services/backend/src/services/transaction.ts create mode 100644 services/backend/src/services/user.ts create mode 100644 services/backend/src/services/wallet.ts create mode 100644 services/backend/src/swagger.ts create mode 100644 services/backend/src/validations/auth.ts create mode 100644 services/backend/src/validations/mutation.ts create mode 100644 services/backend/src/validations/project-category.ts create mode 100644 services/backend/src/validations/project-report.ts create mode 100644 services/backend/src/validations/project-token.ts create mode 100644 services/backend/src/validations/project-wallet.ts create mode 100644 services/backend/src/validations/project.ts create mode 100644 services/backend/src/validations/topup.ts create mode 100644 services/backend/src/validations/user.ts create mode 100644 services/backend/tsconfig.json create mode 100644 services/frontend/.env.example create mode 100644 services/frontend/.gitignore create mode 100644 services/frontend/.vscode/settings.json create mode 100644 services/frontend/README.md create mode 100644 services/frontend/app/components/back-button.tsx create mode 100644 services/frontend/app/components/cards/bordered-card.tsx create mode 100644 services/frontend/app/components/cards/modal-card.tsx create mode 100644 services/frontend/app/components/cards/project-card.tsx create mode 100644 services/frontend/app/components/cards/project-used-token-card.tsx create mode 100644 services/frontend/app/components/document-uploader.tsx create mode 100644 services/frontend/app/components/document/aggrement-letter.client.tsx create mode 100644 services/frontend/app/components/extensions/file-upload.tsx create mode 100644 services/frontend/app/components/icons.tsx create mode 100644 services/frontend/app/components/input/PasswordObscure.tsx create mode 100644 services/frontend/app/components/layouts/dashboard-nav.tsx create mode 100644 services/frontend/app/components/layouts/header.tsx create mode 100644 services/frontend/app/components/layouts/mobile-sidebar.tsx create mode 100644 services/frontend/app/components/layouts/sidebar.tsx create mode 100644 services/frontend/app/components/layouts/user-nav.tsx create mode 100644 services/frontend/app/components/modal/alert-modal.tsx create mode 100644 services/frontend/app/components/page-container.tsx create mode 100644 services/frontend/app/components/table/data-table.tsx create mode 100644 services/frontend/app/components/table/table.tsx create mode 100644 services/frontend/app/components/theme-toogle.tsx create mode 100644 services/frontend/app/components/ui/accordion.tsx create mode 100644 services/frontend/app/components/ui/alert-dialog.tsx create mode 100644 services/frontend/app/components/ui/avatar.tsx create mode 100644 services/frontend/app/components/ui/badge.tsx create mode 100644 services/frontend/app/components/ui/button.tsx create mode 100644 services/frontend/app/components/ui/card.tsx create mode 100644 services/frontend/app/components/ui/carousel.tsx create mode 100644 services/frontend/app/components/ui/chart.tsx create mode 100644 services/frontend/app/components/ui/checkbox.tsx create mode 100644 services/frontend/app/components/ui/combobox.tsx create mode 100644 services/frontend/app/components/ui/command.tsx create mode 100644 services/frontend/app/components/ui/dialog.tsx create mode 100644 services/frontend/app/components/ui/dropdown-menu.tsx create mode 100644 services/frontend/app/components/ui/form.tsx create mode 100644 services/frontend/app/components/ui/input-otp.tsx create mode 100644 services/frontend/app/components/ui/input.tsx create mode 100644 services/frontend/app/components/ui/label.tsx create mode 100644 services/frontend/app/components/ui/modal.tsx create mode 100644 services/frontend/app/components/ui/navigation-menu.tsx create mode 100644 services/frontend/app/components/ui/pagination.tsx create mode 100644 services/frontend/app/components/ui/popover.tsx create mode 100644 services/frontend/app/components/ui/progress.tsx create mode 100644 services/frontend/app/components/ui/radio-group.tsx create mode 100644 services/frontend/app/components/ui/required-icon.tsx create mode 100644 services/frontend/app/components/ui/scroll-area.tsx create mode 100644 services/frontend/app/components/ui/select.tsx create mode 100644 services/frontend/app/components/ui/separator.tsx create mode 100644 services/frontend/app/components/ui/sheet.tsx create mode 100644 services/frontend/app/components/ui/spinner.tsx create mode 100644 services/frontend/app/components/ui/styled-pagination.tsx create mode 100644 services/frontend/app/components/ui/table.tsx create mode 100644 services/frontend/app/components/ui/textarea.tsx create mode 100644 services/frontend/app/components/ui/tooltip.tsx create mode 100644 services/frontend/app/entry.client.tsx create mode 100644 services/frontend/app/entry.server.tsx create mode 100644 services/frontend/app/hooks/use-jwt-payload.ts create mode 100644 services/frontend/app/hooks/use-sidebar.ts create mode 100644 services/frontend/app/lib/clsx.ts create mode 100644 services/frontend/app/lib/env.ts create mode 100644 services/frontend/app/lib/get-env.ts create mode 100644 services/frontend/app/lib/http.ts create mode 100644 services/frontend/app/lib/middleware.ts create mode 100644 services/frontend/app/lib/react-query.ts create mode 100644 services/frontend/app/root.tsx create mode 100644 services/frontend/app/routes/_index/components/Footer.tsx create mode 100644 services/frontend/app/routes/_index/components/Header.tsx create mode 100644 services/frontend/app/routes/_index/route.tsx create mode 100644 services/frontend/app/routes/action.logout.ts create mode 100644 services/frontend/app/routes/action.set-theme.ts create mode 100644 services/frontend/app/routes/app._index/components/dashboard/basic-dashboard.tsx create mode 100644 services/frontend/app/routes/app._index/components/dashboard/platinum-dashboard.tsx create mode 100644 services/frontend/app/routes/app._index/components/table/columns.tsx create mode 100644 services/frontend/app/routes/app._index/components/upgrade-dialog.tsx create mode 100644 services/frontend/app/routes/app._index/route.tsx create mode 100644 services/frontend/app/routes/app.dompet._index/components/modal/top-up/topup.tsx create mode 100644 services/frontend/app/routes/app.dompet._index/components/table/columns.tsx create mode 100644 services/frontend/app/routes/app.dompet._index/route.tsx create mode 100644 services/frontend/app/routes/app.dompet.pembayaran-topup._index/route.tsx create mode 100644 services/frontend/app/routes/app.dompet.pembayaran-topup.verify._index/components/verify-dialog.tsx create mode 100644 services/frontend/app/routes/app.dompet.pembayaran-topup.verify._index/route.tsx create mode 100644 services/frontend/app/routes/app.dompet.pembayaran-wajib._index/route.tsx create mode 100644 services/frontend/app/routes/app.dompet.pembayaran-wajib.verify._index/components/verify-dialog.tsx create mode 100644 services/frontend/app/routes/app.dompet.pembayaran-wajib.verify._index/route.tsx create mode 100644 services/frontend/app/routes/app.dompet.tarik-saldo._index/components/first-section.tsx create mode 100644 services/frontend/app/routes/app.dompet.tarik-saldo._index/components/second-section.tsx create mode 100644 services/frontend/app/routes/app.dompet.tarik-saldo._index/components/table/columns.tsx create mode 100644 services/frontend/app/routes/app.dompet.tarik-saldo._index/route.tsx create mode 100644 services/frontend/app/routes/app.member.upgrade._index/route.tsx create mode 100644 services/frontend/app/routes/app.member.upgrade.verify/components/verify-dialog.tsx create mode 100644 services/frontend/app/routes/app.member.upgrade.verify/route.tsx create mode 100644 services/frontend/app/routes/app.my-projects.$id._index/components/first-section.tsx create mode 100644 services/frontend/app/routes/app.my-projects.$id._index/components/modal/sign-contract.tsx create mode 100644 services/frontend/app/routes/app.my-projects.$id._index/components/second-section.tsx create mode 100644 services/frontend/app/routes/app.my-projects.$id._index/components/third-section.tsx create mode 100644 services/frontend/app/routes/app.my-projects.$id._index/route.tsx create mode 100644 services/frontend/app/routes/app.my-projects.$id.edit/components/form/edit-project-form.tsx create mode 100644 services/frontend/app/routes/app.my-projects.$id.edit/route.tsx create mode 100644 services/frontend/app/routes/app.my-projects._index/route.tsx create mode 100644 services/frontend/app/routes/app.my-projects.create/components/form/create-project-form.tsx create mode 100644 services/frontend/app/routes/app.my-projects.create/route.tsx create mode 100644 services/frontend/app/routes/app.profile._index/route.tsx create mode 100644 services/frontend/app/routes/app.projects.$id/components/first-section.tsx create mode 100644 services/frontend/app/routes/app.projects.$id/components/modal/beli.tsx create mode 100644 services/frontend/app/routes/app.projects.$id/components/modal/konfirmasi-pembelian.tsx create mode 100644 services/frontend/app/routes/app.projects.$id/components/second-section.tsx create mode 100644 services/frontend/app/routes/app.projects.$id/route.tsx create mode 100644 services/frontend/app/routes/app.projects._index/route.tsx create mode 100644 services/frontend/app/routes/app.projects/route.tsx create mode 100644 services/frontend/app/routes/app.riwayat-laporan.$id/components/first-section.tsx create mode 100644 services/frontend/app/routes/app.riwayat-laporan.$id/components/second-section.tsx create mode 100644 services/frontend/app/routes/app.riwayat-laporan.$id/components/table/columns.tsx create mode 100644 services/frontend/app/routes/app.riwayat-laporan.$id/route.tsx create mode 100644 services/frontend/app/routes/app.riwayat-mutasi.$id/components/table/columns.tsx create mode 100644 services/frontend/app/routes/app.riwayat-mutasi.$id/route.tsx create mode 100644 services/frontend/app/routes/app.token.$id/components/first-section.tsx create mode 100644 services/frontend/app/routes/app.token.$id/components/second-section.tsx create mode 100644 services/frontend/app/routes/app.token.$id/components/table/columns.tsx create mode 100644 services/frontend/app/routes/app.token.$id/components/third-section.tsx create mode 100644 services/frontend/app/routes/app.token.$id/route.tsx create mode 100644 services/frontend/app/routes/app/constants/nav-items.ts create mode 100644 services/frontend/app/routes/app/route.tsx create mode 100644 services/frontend/app/routes/auth.login/route.tsx create mode 100644 services/frontend/app/routes/auth.register/components/confirm-dialog.tsx create mode 100644 services/frontend/app/routes/auth.register/components/first-form.tsx create mode 100644 services/frontend/app/routes/auth.register/components/second-form.tsx create mode 100644 services/frontend/app/routes/auth.register/route.tsx create mode 100644 services/frontend/app/routes/auth/components/RegisterCarousel.tsx create mode 100644 services/frontend/app/routes/auth/route.tsx create mode 100644 services/frontend/app/routes/dashboard._index/components/table/columns.tsx create mode 100644 services/frontend/app/routes/dashboard._index/route.tsx create mode 100644 services/frontend/app/routes/dashboard.anggota.$id/route.tsx create mode 100644 services/frontend/app/routes/dashboard.anggota._index/components/table/cell-action.tsx create mode 100644 services/frontend/app/routes/dashboard.anggota._index/components/table/columns.tsx create mode 100644 services/frontend/app/routes/dashboard.anggota._index/route.tsx create mode 100644 services/frontend/app/routes/dashboard.profile._index/route.tsx create mode 100644 services/frontend/app/routes/dashboard.proyek.pengajuan.$id/components/first-section.tsx create mode 100644 services/frontend/app/routes/dashboard.proyek.pengajuan.$id/components/modal/approval.tsx create mode 100644 services/frontend/app/routes/dashboard.proyek.pengajuan.$id/components/modal/publish.tsx create mode 100644 services/frontend/app/routes/dashboard.proyek.pengajuan.$id/components/modal/revisi.tsx create mode 100644 services/frontend/app/routes/dashboard.proyek.pengajuan.$id/components/modal/selesai.tsx create mode 100644 services/frontend/app/routes/dashboard.proyek.pengajuan.$id/components/modal/terima.tsx create mode 100644 services/frontend/app/routes/dashboard.proyek.pengajuan.$id/components/modal/tolak.tsx create mode 100644 services/frontend/app/routes/dashboard.proyek.pengajuan.$id/components/second-section.tsx create mode 100644 services/frontend/app/routes/dashboard.proyek.pengajuan.$id/route.tsx create mode 100644 services/frontend/app/routes/dashboard.proyek.pengajuan._index/components/table/cell-action.tsx create mode 100644 services/frontend/app/routes/dashboard.proyek.pengajuan._index/components/table/columns.tsx create mode 100644 services/frontend/app/routes/dashboard.proyek.pengajuan._index/route.tsx create mode 100644 services/frontend/app/routes/dashboard.proyek.report.$id/components/first-section.tsx create mode 100644 services/frontend/app/routes/dashboard.proyek.report.$id/components/modal/laba-rugi/laporan-laba.tsx create mode 100644 services/frontend/app/routes/dashboard.proyek.report.$id/components/modal/laba-rugi/laporan-rugi.tsx create mode 100644 services/frontend/app/routes/dashboard.proyek.report.$id/components/modal/laba-rugi/laporan.tsx create mode 100644 services/frontend/app/routes/dashboard.proyek.report.$id/components/modal/mutasi/mutasi.tsx create mode 100644 services/frontend/app/routes/dashboard.proyek.report.$id/components/second-section.tsx create mode 100644 services/frontend/app/routes/dashboard.proyek.report.$id/route.tsx create mode 100644 services/frontend/app/routes/dashboard.proyek.report._index/components/table/cell-action.tsx create mode 100644 services/frontend/app/routes/dashboard.proyek.report._index/components/table/columns.tsx create mode 100644 services/frontend/app/routes/dashboard.proyek.report._index/route.tsx create mode 100644 services/frontend/app/routes/dashboard.riwayat-laporan.$id/components/first-section.tsx create mode 100644 services/frontend/app/routes/dashboard.riwayat-laporan.$id/components/second-section.tsx create mode 100644 services/frontend/app/routes/dashboard.riwayat-laporan.$id/components/table/columns.tsx create mode 100644 services/frontend/app/routes/dashboard.riwayat-laporan.$id/route.tsx create mode 100644 services/frontend/app/routes/dashboard.riwayat-mutasi.$id/components/table/columns.tsx create mode 100644 services/frontend/app/routes/dashboard.riwayat-mutasi.$id/route.tsx create mode 100644 services/frontend/app/routes/dashboard.saldo.penarikan/components/modal/detail-penarikan.tsx create mode 100644 services/frontend/app/routes/dashboard.saldo.penarikan/components/table/cell-action.tsx create mode 100644 services/frontend/app/routes/dashboard.saldo.penarikan/components/table/columns.tsx create mode 100644 services/frontend/app/routes/dashboard.saldo.penarikan/route.tsx create mode 100644 services/frontend/app/routes/dashboard.saldo.profit-manajemen._index/components/modal/detail-transfer.tsx create mode 100644 services/frontend/app/routes/dashboard.saldo.profit-manajemen._index/components/table/cell-action.tsx create mode 100644 services/frontend/app/routes/dashboard.saldo.profit-manajemen._index/components/table/columns.tsx create mode 100644 services/frontend/app/routes/dashboard.saldo.profit-manajemen._index/route.tsx create mode 100644 services/frontend/app/routes/dashboard.saldo.proyek-pendanaan.$id/components/first-section.tsx create mode 100644 services/frontend/app/routes/dashboard.saldo.proyek-pendanaan.$id/components/second-section.tsx create mode 100644 services/frontend/app/routes/dashboard.saldo.proyek-pendanaan.$id/components/table/columns.tsx create mode 100644 services/frontend/app/routes/dashboard.saldo.proyek-pendanaan.$id/route.tsx create mode 100644 services/frontend/app/routes/dashboard.saldo.proyek-pendanaan._index/components/table/cell-action.tsx create mode 100644 services/frontend/app/routes/dashboard.saldo.proyek-pendanaan._index/components/table/columns.tsx create mode 100644 services/frontend/app/routes/dashboard.saldo.proyek-pendanaan._index/route.tsx create mode 100644 services/frontend/app/routes/dashboard.saldo.tarik-proyek._index/components/modal/penarikan-proyek.tsx create mode 100644 services/frontend/app/routes/dashboard.saldo.tarik-proyek._index/components/table/cell-action.tsx create mode 100644 services/frontend/app/routes/dashboard.saldo.tarik-proyek._index/components/table/columns.tsx create mode 100644 services/frontend/app/routes/dashboard.saldo.tarik-proyek._index/route.tsx create mode 100644 services/frontend/app/routes/dashboard.saldo.top-up/components/modal/detail-topup.tsx create mode 100644 services/frontend/app/routes/dashboard.saldo.top-up/components/table/cell-action.tsx create mode 100644 services/frontend/app/routes/dashboard.saldo.top-up/components/table/columns.tsx create mode 100644 services/frontend/app/routes/dashboard.saldo.top-up/route.tsx create mode 100644 services/frontend/app/routes/dashboard/constants/nav-items.ts create mode 100644 services/frontend/app/routes/dashboard/route.tsx create mode 100644 services/frontend/app/routes/member._index/route.tsx create mode 100644 services/frontend/app/routes/member.pembayaran/route.tsx create mode 100644 services/frontend/app/routes/member.verify/components/verify-dialog.tsx create mode 100644 services/frontend/app/routes/member.verify/route.tsx create mode 100644 services/frontend/app/routes/member/route.tsx create mode 100644 services/frontend/app/routes/otp._index/route.tsx create mode 100644 services/frontend/app/routes/otp/route.tsx create mode 100644 services/frontend/app/services/auth/register.ts create mode 100644 services/frontend/app/services/category/get-all.ts create mode 100644 services/frontend/app/services/category/get-by-id.ts create mode 100644 services/frontend/app/services/chart-project/get-by-project-id.ts create mode 100644 services/frontend/app/services/chart-project/get-by-user.ts create mode 100644 services/frontend/app/services/chart-token/get-all.ts create mode 100644 services/frontend/app/services/history-project/get-by-id.ts create mode 100644 services/frontend/app/services/history-token/get-by-id.ts create mode 100644 services/frontend/app/services/indonesia-region.ts create mode 100644 services/frontend/app/services/mutation/create.ts create mode 100644 services/frontend/app/services/mutation/get-by-project-id.ts create mode 100644 services/frontend/app/services/my-projects/get-all.ts create mode 100644 services/frontend/app/services/profile/get-by-id.ts create mode 100644 services/frontend/app/services/project-report/create.ts create mode 100644 services/frontend/app/services/project-report/get-all.ts create mode 100644 services/frontend/app/services/project-report/get-by-id.ts create mode 100644 services/frontend/app/services/project-report/get-by-project-id.ts create mode 100644 services/frontend/app/services/project-wallet/get-all.ts create mode 100644 services/frontend/app/services/project-wallet/get-by-id.ts create mode 100644 services/frontend/app/services/project-wallet/get-by-wallet-id.ts create mode 100644 services/frontend/app/services/project-wallet/transfer-saldo.ts create mode 100644 services/frontend/app/services/projects/accept.ts create mode 100644 services/frontend/app/services/projects/agreement.ts create mode 100644 services/frontend/app/services/projects/approve.ts create mode 100644 services/frontend/app/services/projects/completing.ts create mode 100644 services/frontend/app/services/projects/count.ts create mode 100644 services/frontend/app/services/projects/create.ts create mode 100644 services/frontend/app/services/projects/get-agreement-by-project-id.ts create mode 100644 services/frontend/app/services/projects/get-all.ts create mode 100644 services/frontend/app/services/projects/get-user-token.ts create mode 100644 services/frontend/app/services/projects/publish.ts create mode 100644 services/frontend/app/services/projects/reject.ts create mode 100644 services/frontend/app/services/projects/revise.ts create mode 100644 services/frontend/app/services/projects/share-profit.ts create mode 100644 services/frontend/app/services/projects/total-profit.ts create mode 100644 services/frontend/app/services/projects/update.ts create mode 100644 services/frontend/app/services/token/buy-project.ts create mode 100644 services/frontend/app/services/token/token-usage.ts create mode 100644 services/frontend/app/services/token/total-token.ts create mode 100644 services/frontend/app/services/top-up/acc-withdraw.ts create mode 100644 services/frontend/app/services/top-up/get-all.ts create mode 100644 services/frontend/app/services/top-up/get-bagian-pemilik-pelaksana.ts create mode 100644 services/frontend/app/services/top-up/get-by-user.ts create mode 100644 services/frontend/app/services/top-up/get-saldo-user.ts create mode 100644 services/frontend/app/services/top-up/get-saldo.ts create mode 100644 services/frontend/app/services/top-up/get-sum-simpanan-pokok.ts create mode 100644 services/frontend/app/services/top-up/get-sum-simpanan-wajib.ts create mode 100644 services/frontend/app/services/top-up/kas-koperasi.ts create mode 100644 services/frontend/app/services/top-up/pay-bagian-pemilik-pelaksana.ts create mode 100644 services/frontend/app/services/top-up/pay-member.ts create mode 100644 services/frontend/app/services/top-up/pay-simpanan-wajib.ts create mode 100644 services/frontend/app/services/top-up/pay-topup.ts create mode 100644 services/frontend/app/services/top-up/update-status-by-id.ts create mode 100644 services/frontend/app/services/top-up/withdraw-saldo.ts create mode 100644 services/frontend/app/services/transaction/get-all.ts create mode 100644 services/frontend/app/services/transaction/get-by-project-id.ts create mode 100644 services/frontend/app/services/transaction/get-by-user-id.ts create mode 100644 services/frontend/app/services/user/acc-upgrade.ts create mode 100644 services/frontend/app/services/user/count.ts create mode 100644 services/frontend/app/services/user/get-all.ts create mode 100644 services/frontend/app/services/user/send-otp.ts create mode 100644 services/frontend/app/services/user/update.ts create mode 100644 services/frontend/app/services/user/upgrade-platinum.ts create mode 100644 services/frontend/app/services/user/verify-otp.ts create mode 100644 services/frontend/app/services/whatsapp/get-qr-code.ts create mode 100644 services/frontend/app/sessions/auth.server.ts create mode 100644 services/frontend/app/sessions/session.server.ts create mode 100644 services/frontend/app/sessions/themes.server.ts create mode 100644 services/frontend/app/tailwind.css create mode 100644 services/frontend/app/types/api/agreement.ts create mode 100644 services/frontend/app/types/api/anggota.ts create mode 100644 services/frontend/app/types/api/auth.ts create mode 100644 services/frontend/app/types/api/chart-project.ts create mode 100644 services/frontend/app/types/api/dompet-reguler.ts create mode 100644 services/frontend/app/types/api/history-token.ts create mode 100644 services/frontend/app/types/api/indonesia-region.ts create mode 100644 services/frontend/app/types/api/investor-data.ts create mode 100644 services/frontend/app/types/api/kategori.ts create mode 100644 services/frontend/app/types/api/laporan.ts create mode 100644 services/frontend/app/types/api/pemilik-pelaksana.ts create mode 100644 services/frontend/app/types/api/penarikan-proyek.ts create mode 100644 services/frontend/app/types/api/proyek-beli.ts create mode 100644 services/frontend/app/types/api/proyek-pendanaan.ts create mode 100644 services/frontend/app/types/api/proyek.ts create mode 100644 services/frontend/app/types/api/top-up.ts create mode 100644 services/frontend/app/types/api/transaction.ts create mode 100644 services/frontend/app/types/api/verify-payment.ts create mode 100644 services/frontend/app/types/api/whatsapp-qr-code.ts create mode 100644 services/frontend/app/types/constants/base-response.ts create mode 100644 services/frontend/app/types/constants/count-response.ts create mode 100644 services/frontend/app/types/constants/dokumen.ts create mode 100644 services/frontend/app/types/constants/jwt-payload.ts create mode 100644 services/frontend/app/types/constants/nav-item.ts create mode 100644 services/frontend/app/types/constants/status-data.ts create mode 100644 services/frontend/app/types/constants/status-project.ts create mode 100644 services/frontend/app/types/constants/status-top-up.ts create mode 100644 services/frontend/app/utils/file-or-url-schema.ts create mode 100644 services/frontend/app/utils/format-input-date.ts create mode 100644 services/frontend/app/utils/format-to-locale-time.ts create mode 100644 services/frontend/app/utils/prefix-file-path.ts create mode 100644 services/frontend/app/utils/prefix-wallet-path.ts create mode 100644 services/frontend/app/utils/to-presentase.ts create mode 100644 services/frontend/app/utils/to-rupiah.ts create mode 100644 services/frontend/biome.json create mode 100644 services/frontend/components.json create mode 100644 services/frontend/package-lock.json create mode 100644 services/frontend/package.json create mode 100644 services/frontend/postcss.config.js create mode 100644 services/frontend/public/favicon.ico create mode 100644 services/frontend/public/img/404.svg create mode 100644 services/frontend/public/img/auth/carousel-1.png create mode 100644 services/frontend/public/img/auth/carousel-2.png create mode 100644 services/frontend/public/img/auth/carousel-3.png create mode 100644 services/frontend/public/img/auth/hero.png create mode 100644 services/frontend/public/img/auth/verify-avatar.svg create mode 100644 services/frontend/public/img/bank/bca.svg create mode 100644 services/frontend/public/img/bank/bni.svg create mode 100644 services/frontend/public/img/bank/bri.svg create mode 100644 services/frontend/public/img/bank/bsi.svg create mode 100644 services/frontend/public/img/bank/btn.svg create mode 100644 services/frontend/public/img/bank/mandiri.svg create mode 100644 services/frontend/public/img/card/diamond.png create mode 100644 services/frontend/public/img/card/emerald.png create mode 100644 services/frontend/public/img/card/gold-bars.png create mode 100644 services/frontend/public/img/card/silver.png create mode 100644 services/frontend/public/img/home/about-us.svg create mode 100644 services/frontend/public/img/home/hero.svg create mode 100644 services/frontend/public/img/icon.png create mode 100644 services/frontend/public/img/ttd-koperasi.png create mode 100644 services/frontend/public/img/user-dummy.jpg create mode 100644 services/frontend/public/img/vector/laba-rugi.svg create mode 100644 services/frontend/public/img/vector/mutasi.svg create mode 100644 services/frontend/public/img/vector/payment.svg create mode 100644 services/frontend/tailwind.config.ts create mode 100644 services/frontend/tsconfig.json create mode 100644 services/frontend/vite.config.ts create mode 100644 services/smartcontract/.gitignore create mode 100644 services/smartcontract/README.md create mode 100644 services/smartcontract/contracts/ProjectToken.sol create mode 100644 services/smartcontract/hardhat.config.ts create mode 100644 services/smartcontract/ignition/modules/Deploy.ts create mode 100644 services/smartcontract/package-lock.json create mode 100644 services/smartcontract/package.json create mode 100644 services/smartcontract/tsconfig.json create mode 100644 services/wallet/.env.example create mode 100644 services/wallet/.gitignore create mode 100644 services/wallet/LICENSE create mode 100644 services/wallet/README.md create mode 100644 services/wallet/assets/uploads/bukti_pembayaran/.gitkeep create mode 100644 services/wallet/drizzle.config.ts create mode 100644 services/wallet/express.d.ts create mode 100644 services/wallet/package-lock.json create mode 100644 services/wallet/package.json create mode 100644 services/wallet/src/controllers/topup.ts create mode 100644 services/wallet/src/controllers/wallet.ts create mode 100644 services/wallet/src/drizzle/db.ts create mode 100644 services/wallet/src/drizzle/migrate.ts create mode 100644 services/wallet/src/drizzle/schema.ts create mode 100644 services/wallet/src/drizzle/seed.ts create mode 100644 services/wallet/src/drizzle/seeder/topup.ts create mode 100644 services/wallet/src/drizzle/seeder/wallet.ts create mode 100644 services/wallet/src/main.ts create mode 100644 services/wallet/src/middlewares/api-key.ts create mode 100644 services/wallet/src/middlewares/topup.ts create mode 100644 services/wallet/src/middlewares/wallet.ts create mode 100644 services/wallet/src/routes/topup.ts create mode 100644 services/wallet/src/routes/wallet.ts create mode 100644 services/wallet/src/services/topup.ts create mode 100644 services/wallet/src/services/wallet.ts create mode 100644 services/wallet/src/swagger.ts create mode 100644 services/wallet/src/validations/wallet.ts create mode 100644 services/wallet/tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5e70b8e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/.idea +/.vscode +/node_modules \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..cb3b946 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,25 @@ +image: ubuntu:22.04 + +stages: + - deploy + +deploy_to_vps: + stage: deploy + + before_script: + - apt-get update && apt-get install -y openssh-client curl # Install curl jika belum ada + + script: + # Memuat private key yang disimpan di GitLab CI/CD variable + - eval "$(ssh-agent -s)" + - echo "$FRONT_PRIVATE_KEY" | sed 's/\r//g' | ssh-add - + + # Menambahkan VPS ke known_hosts secara otomatis untuk menghindari konfirmasi manual + - mkdir -p ~/.ssh + - ssh-keyscan -H $VPS_HOST >> ~/.ssh/known_hosts + + - ssh $FRONTEND_USER@$VPS_HOST "export PATH="/home/frontend/.nvm/versions/node/v20.16.0/bin:$PATH" && cd $FRONTEND_DIR && git checkout dev && git pull && npm i && npm run build && pm2 stop 0 && pm2 start 0" + + + only: + - dev # Jalankan hanya ketika ada perubahan di branch dev diff --git a/README.md b/README.md new file mode 100644 index 0000000..28bf34a --- /dev/null +++ b/README.md @@ -0,0 +1,173 @@ +# koperasi + +## Getting started + +# Wallet Service + +## Configuration + +Create database postgresql with name `wallet` + +Open new terminal in `services/wallet` directory + +Instalation dependencies + +```shell +npm install +``` + +Copy .env.example to .env + +```shell +cp .env.example .env +``` + +Set your database configuration in .env + +generate secret key for `API_KEY` with command + +```shell +node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" +``` + +> Note: +> API_KEY Backend Service and Wallet Service must be the same + +## Run project + +Migration database + +```shell +npm run migrate:fresh +``` + +Run project + +```shell +npm run dev +``` + +# Smart Contract Service + +## Configuration + +Open new terminal in `services/smartcontract` directory + +Instalation dependencies + +```shell +npm install +``` + +## Run project + +Run localhost network hardhat (network blockchain for development) + +```shell +npx hardhat node +``` + +Open new terminal again in `services/smartcontract` directory + +Deploy + +```shell +npx hardhat ignition deploy ./ignition/modules/Deploy.ts --network localhost +``` + +# Backend Service + +## Configuration + +Create database postgresql with name `koperasi` + +Open new terminal in `services/backend` directory + +Instalation dependencies + +```shell +npm install +``` + +Copy .env.example to .env + +```shell +cp .env.example .env +``` + +Set your database configuration in .env + +generate secret key for `JWT_SECRET` with command + +```shell +node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" +``` + +Set `contractABI.json` with contract abi after compile contract + +set `PRIVATE_KEY=" "` with private key wallet blockchain + +Set `CONTRACT_ADDRESS=" "` with contract address after deploy contract + +## Run project + +Migration database + +```shell +npm run migrate:fresh +``` + +Seed fake data + +```shell +npm run db:seed +``` + +Run project + +```shell +npm run dev +``` + +# Frontend Service + +## Configuration + +Open new terminal in `services/frontend` directory + +Instalation dependencies + +```shell +npm install +``` + +Copy .env.example to .env + +```shell +cp .env.example .env +``` + +set `API_BASE_URL` with backend service url +example: `API_BASE_URL=http://localhost:3000` + +generate `SECRET_COOKIE_PASSWORD` with command + +```shell +openssl rand -base64 32 +``` + +## Run project + +Run project + +```shell +npm run dev +``` + +# Import Postman Collection + +create new workspace in postman + +extract `api-docs/postman-koperasi-blockchain.zip` + +import postman collection and environment in directory `postman-koperasi-blockchain` to your postman \ No newline at end of file diff --git a/api-docs/README.md b/api-docs/README.md new file mode 100644 index 0000000..e69de29 diff --git a/api-docs/postman-koperasi-blockchain.zip b/api-docs/postman-koperasi-blockchain.zip new file mode 100644 index 0000000000000000000000000000000000000000..39fdb84bdc387bc56e49f679db73b134b3e1b455 GIT binary patch literal 70720 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 z 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:@0.0.0.0:5432/ +DB_HOST=localhost +DB_PORT=5432 +DB_USER=postgres +DB_PASSWORD= +DB_NAME= + +# JWT +JWT_SECRET= + +# SMART CONTRACT +API_URL="http://127.0.0.1:8545" +PRIVATE_KEY="..." +CONTRACT_ADDRESS="0x..." \ No newline at end of file diff --git a/services/backend/.gitignore b/services/backend/.gitignore new file mode 100644 index 0000000..b77e900 --- /dev/null +++ b/services/backend/.gitignore @@ -0,0 +1,36 @@ +# code editor settings +/.idea +/.vscode + +# dependencies +/node_modules + +# dotenv environment variables file +.env + +# build / generate output +dist + +# drizzle migrations +/src/drizzle/migrations/ + +# user uploaded files +/assets/uploads/foto_diri/* +/assets/uploads/foto_ktp/* +/assets/uploads/foto_profile/* +/assets/uploads/dokumen_pendukung/* +/assets/uploads/dokumen_proyeksi/* +/assets/uploads/brosur_produk/* +/assets/uploads/tanda_tangan/* +/assets/uploads/tanda_tangan_admin/* +/assets/uploads/bukti_pembayaran/* +/assets/uploads/laporan_keuangan/* +/assets/uploads/laporan_mutasi/* +/assets/uploads/bukti_transfer/* +/assets/uploads/dokumen_prospektus/* + +# not ignore empty directories when have .gitkeep file +!/**/.gitkeep + +# auth baileys +/auth_baileys \ No newline at end of file diff --git a/services/backend/LICENSE b/services/backend/LICENSE new file mode 100644 index 0000000..242e9ff --- /dev/null +++ b/services/backend/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Geshan Manandhar + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/services/backend/README.md b/services/backend/README.md new file mode 100644 index 0000000..22ad603 --- /dev/null +++ b/services/backend/README.md @@ -0,0 +1,53 @@ +# Backend Service + +## Configuration + +Create database postgresql with name `koperasi` + +Go to `services/backend` directory + +Instalation dependencies + +```shell +npm install +``` + +Copy .env.example to .env + +```shell +cp .env.example .env +``` + +Set your database configuration in .env + +generate secret key for `JWT_SECRET` with command + +```shell +node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" +``` + +Set `contractABI.json` with contract abi after compile contract + +set `PRIVATE_KEY=" "` with private key wallet blockchain + +Set `CONTRACT_ADDRESS=" "` with contract address after deploy contract + +## Run project + +Migration database + +```shell +npm run migrate:fresh +``` + +Seed fake data + +```shell +npm run db:seed +``` + +Run project + +```shell +npm run dev +``` diff --git a/services/backend/assets/statis/formatan-dokumen-proyeksi.docx b/services/backend/assets/statis/formatan-dokumen-proyeksi.docx new file mode 100644 index 0000000000000000000000000000000000000000..314f4c0b4410d58a0f9890c6c7a78dd9930b6703 GIT binary patch literal 12672 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 AAOHXW literal 0 HcmV?d00001 diff --git a/services/backend/assets/uploads/brosur_produk/.gitkeep b/services/backend/assets/uploads/brosur_produk/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/services/backend/assets/uploads/bukti_pembayaran/.gitkeep b/services/backend/assets/uploads/bukti_pembayaran/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/services/backend/assets/uploads/bukti_transfer/.gitkeep b/services/backend/assets/uploads/bukti_transfer/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/services/backend/assets/uploads/dokumen_pendukung/.gitkeep b/services/backend/assets/uploads/dokumen_pendukung/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/services/backend/assets/uploads/dokumen_prospektus/.gitkeep b/services/backend/assets/uploads/dokumen_prospektus/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/services/backend/assets/uploads/dokumen_proyeksi/.gitkeep b/services/backend/assets/uploads/dokumen_proyeksi/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/services/backend/assets/uploads/foto_diri/.gitkeep b/services/backend/assets/uploads/foto_diri/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/services/backend/assets/uploads/foto_ktp/.gitkeep b/services/backend/assets/uploads/foto_ktp/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/services/backend/assets/uploads/foto_profile/.gitkeep b/services/backend/assets/uploads/foto_profile/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/services/backend/assets/uploads/laporan_keuangan/.gitkeep b/services/backend/assets/uploads/laporan_keuangan/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/services/backend/assets/uploads/laporan_mutasi/.gitkeep b/services/backend/assets/uploads/laporan_mutasi/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/services/backend/assets/uploads/tanda_tangan/.gitkeep b/services/backend/assets/uploads/tanda_tangan/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/services/backend/assets/uploads/tanda_tangan_admin/.gitkeep b/services/backend/assets/uploads/tanda_tangan_admin/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/services/backend/contractABI.json b/services/backend/contractABI.json new file mode 100644 index 0000000..931973b --- /dev/null +++ b/services/backend/contractABI.json @@ -0,0 +1,2031 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "string", + "name": "idProjek", + "type": "string" + }, + { + "indexed": false, + "internalType": "string", + "name": "idUser", + "type": "string" + }, + { + "indexed": false, + "internalType": "string", + "name": "namaProyek", + "type": "string" + }, + { + "indexed": false, + "internalType": "string", + "name": "namaPetugas", + "type": "string" + }, + { + "indexed": false, + "internalType": "string", + "name": "alamatPetugas", + "type": "string" + }, + { + "indexed": false, + "internalType": "string", + "name": "namaPemilikProyek", + "type": "string" + }, + { + "indexed": false, + "internalType": "string", + "name": "nik", + "type": "string" + }, + { + "indexed": false, + "internalType": "string", + "name": "noHp", + "type": "string" + }, + { + "indexed": false, + "internalType": "string", + "name": "alamat", + "type": "string" + }, + { + "indexed": false, + "internalType": "string", + "name": "signature", + "type": "string" + }, + { + "indexed": false, + "internalType": "string", + "name": "tandaTangan", + "type": "string" + }, + { + "indexed": false, + "internalType": "int256", + "name": "nominalDisetujui", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "createdAt", + "type": "uint256" + } + ], + "name": "AgreementCreated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "string", + "name": "chartTokenId", + "type": "string" + }, + { + "indexed": false, + "internalType": "string", + "name": "idUser", + "type": "string" + }, + { + "indexed": false, + "internalType": "string", + "name": "idProjek", + "type": "string" + }, + { + "indexed": false, + "internalType": "int256", + "name": "nominal", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "createdAt", + "type": "uint256" + } + ], + "name": "ChartTokenCreated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "string", + "name": "idProjek", + "type": "string" + }, + { + "indexed": false, + "internalType": "int256", + "name": "pelaksana", + "type": "int256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "pemilik", + "type": "int256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "koperasi", + "type": "int256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "pendana", + "type": "int256" + } + ], + "name": "DividenProfitAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "string", + "name": "idProjek", + "type": "string" + }, + { + "indexed": false, + "internalType": "int256", + "name": "pelaksana", + "type": "int256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "pemilik", + "type": "int256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "koperasi", + "type": "int256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "pendana", + "type": "int256" + } + ], + "name": "DividenProfitUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "string", + "name": "historyTokenId", + "type": "string" + }, + { + "indexed": false, + "internalType": "string", + "name": "chartTokenId", + "type": "string" + }, + { + "indexed": false, + "internalType": "int256", + "name": "totalNilai", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "createdAt", + "type": "uint256" + } + ], + "name": "HistoryTokenCreated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "string", + "name": "tokenId", + "type": "string" + }, + { + "indexed": false, + "internalType": "string", + "name": "idProjek", + "type": "string" + }, + { + "indexed": false, + "internalType": "string", + "name": "idUser", + "type": "string" + }, + { + "indexed": false, + "internalType": "int256", + "name": "nilai", + "type": "int256" + } + ], + "name": "TokenCreated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "string", + "name": "tokenId", + "type": "string" + } + ], + "name": "TokenNominalReset", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "string", + "name": "tokenId", + "type": "string" + }, + { + "indexed": false, + "internalType": "string", + "name": "newIdUser", + "type": "string" + } + ], + "name": "TokenUserUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "idUser", + "type": "string" + }, + { + "internalType": "string", + "name": "idProjek", + "type": "string" + }, + { + "internalType": "int256", + "name": "nominal", + "type": "int256" + }, + { + "internalType": "int256", + "name": "totalNilai", + "type": "int256" + } + ], + "name": "addChartToken", + "outputs": [ + { + "internalType": "string", + "name": "chartTokenId", + "type": "string" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "idProjek", + "type": "string" + }, + { + "internalType": "int256", + "name": "pelaksana", + "type": "int256" + }, + { + "internalType": "int256", + "name": "pemilik", + "type": "int256" + }, + { + "internalType": "int256", + "name": "koperasi", + "type": "int256" + }, + { + "internalType": "int256", + "name": "pendana", + "type": "int256" + } + ], + "name": "addDividenProfit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "idUser", + "type": "string" + }, + { + "internalType": "string", + "name": "namaUser", + "type": "string" + }, + { + "internalType": "string", + "name": "idProjek", + "type": "string" + }, + { + "internalType": "string", + "name": "judulProjek", + "type": "string" + }, + { + "internalType": "string", + "name": "ownerProjek", + "type": "string" + }, + { + "internalType": "int256", + "name": "jumlahToken", + "type": "int256" + }, + { + "internalType": "int256", + "name": "totalNominal", + "type": "int256" + } + ], + "name": "addTransaction", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "agreementCount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "agreements", + "outputs": [ + { + "internalType": "string", + "name": "idProjek", + "type": "string" + }, + { + "internalType": "string", + "name": "idUser", + "type": "string" + }, + { + "internalType": "string", + "name": "namaProyek", + "type": "string" + }, + { + "internalType": "string", + "name": "namaPetugas", + "type": "string" + }, + { + "internalType": "string", + "name": "alamatPetugas", + "type": "string" + }, + { + "internalType": "string", + "name": "namaPemilikProyek", + "type": "string" + }, + { + "internalType": "string", + "name": "nik", + "type": "string" + }, + { + "internalType": "string", + "name": "noHp", + "type": "string" + }, + { + "internalType": "string", + "name": "alamat", + "type": "string" + }, + { + "internalType": "string", + "name": "signature", + "type": "string" + }, + { + "internalType": "string", + "name": "tandaTangan", + "type": "string" + }, + { + "internalType": "int256", + "name": "nominalDisetujui", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "createdAt", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "allChartTokenIds", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "allHistoryTokenIds", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "allTokenIds", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "name": "chartTokens", + "outputs": [ + { + "internalType": "string", + "name": "chartTokenId", + "type": "string" + }, + { + "internalType": "string", + "name": "idUser", + "type": "string" + }, + { + "internalType": "string", + "name": "idProjek", + "type": "string" + }, + { + "internalType": "int256", + "name": "nominal", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "createdAt", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "_idProjek", + "type": "string" + }, + { + "internalType": "string", + "name": "_idUser", + "type": "string" + }, + { + "internalType": "string", + "name": "_namaProyek", + "type": "string" + }, + { + "internalType": "string", + "name": "_namaPetugas", + "type": "string" + }, + { + "internalType": "string", + "name": "_alamatPetugas", + "type": "string" + }, + { + "internalType": "string", + "name": "_namaPemilikProyek", + "type": "string" + }, + { + "internalType": "string", + "name": "_nik", + "type": "string" + }, + { + "internalType": "string", + "name": "_noHp", + "type": "string" + }, + { + "internalType": "string", + "name": "_alamat", + "type": "string" + }, + { + "internalType": "string", + "name": "_signature", + "type": "string" + }, + { + "internalType": "string", + "name": "_tandaTangan", + "type": "string" + }, + { + "internalType": "int256", + "name": "_nominalDisetujui", + "type": "int256" + } + ], + "name": "createAgreementLetter", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "idProjek", + "type": "string" + }, + { + "internalType": "string", + "name": "idUser", + "type": "string" + }, + { + "internalType": "int256", + "name": "nilai", + "type": "int256" + } + ], + "name": "createToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "subtractedValue", + "type": "uint256" + } + ], + "name": "decreaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "name": "dividenProfit", + "outputs": [ + { + "internalType": "string", + "name": "idProjek", + "type": "string" + }, + { + "internalType": "int256", + "name": "pelaksana", + "type": "int256" + }, + { + "internalType": "int256", + "name": "pemilik", + "type": "int256" + }, + { + "internalType": "int256", + "name": "koperasi", + "type": "int256" + }, + { + "internalType": "int256", + "name": "pendana", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "idProjek", + "type": "string" + } + ], + "name": "getAgreementByProjectId", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "idProjek", + "type": "string" + }, + { + "internalType": "string", + "name": "idUser", + "type": "string" + }, + { + "internalType": "string", + "name": "namaProyek", + "type": "string" + }, + { + "internalType": "string", + "name": "namaPetugas", + "type": "string" + }, + { + "internalType": "string", + "name": "alamatPetugas", + "type": "string" + }, + { + "internalType": "string", + "name": "namaPemilikProyek", + "type": "string" + }, + { + "internalType": "string", + "name": "nik", + "type": "string" + }, + { + "internalType": "string", + "name": "noHp", + "type": "string" + }, + { + "internalType": "string", + "name": "alamat", + "type": "string" + }, + { + "internalType": "string", + "name": "signature", + "type": "string" + }, + { + "internalType": "string", + "name": "tandaTangan", + "type": "string" + }, + { + "internalType": "int256", + "name": "nominalDisetujui", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "createdAt", + "type": "uint256" + } + ], + "internalType": "struct ProjectToken.Agreement[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getAllAgreement", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "idProjek", + "type": "string" + }, + { + "internalType": "string", + "name": "idUser", + "type": "string" + }, + { + "internalType": "string", + "name": "namaProyek", + "type": "string" + }, + { + "internalType": "string", + "name": "namaPetugas", + "type": "string" + }, + { + "internalType": "string", + "name": "alamatPetugas", + "type": "string" + }, + { + "internalType": "string", + "name": "namaPemilikProyek", + "type": "string" + }, + { + "internalType": "string", + "name": "nik", + "type": "string" + }, + { + "internalType": "string", + "name": "noHp", + "type": "string" + }, + { + "internalType": "string", + "name": "alamat", + "type": "string" + }, + { + "internalType": "string", + "name": "signature", + "type": "string" + }, + { + "internalType": "string", + "name": "tandaTangan", + "type": "string" + }, + { + "internalType": "int256", + "name": "nominalDisetujui", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "createdAt", + "type": "uint256" + } + ], + "internalType": "struct ProjectToken.Agreement[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "idUser", + "type": "string" + } + ], + "name": "getAllChartTokensByUserId", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "chartTokenId", + "type": "string" + }, + { + "internalType": "string", + "name": "idUser", + "type": "string" + }, + { + "internalType": "string", + "name": "idProjek", + "type": "string" + }, + { + "internalType": "int256", + "name": "nominal", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "createdAt", + "type": "uint256" + } + ], + "internalType": "struct ProjectToken.ChartToken[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getAllTokens", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "tokenId", + "type": "string" + }, + { + "internalType": "string", + "name": "idProjek", + "type": "string" + }, + { + "internalType": "string", + "name": "idUser", + "type": "string" + }, + { + "internalType": "int256", + "name": "nilai", + "type": "int256" + } + ], + "internalType": "struct ProjectToken.TokenDetail[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getAllTransaction", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "idUser", + "type": "string" + }, + { + "internalType": "string", + "name": "namaUser", + "type": "string" + }, + { + "internalType": "string", + "name": "idProjek", + "type": "string" + }, + { + "internalType": "string", + "name": "judulProjek", + "type": "string" + }, + { + "internalType": "string", + "name": "ownerProjek", + "type": "string" + }, + { + "internalType": "int256", + "name": "jumlahToken", + "type": "int256" + }, + { + "internalType": "int256", + "name": "totalNominal", + "type": "int256" + } + ], + "internalType": "struct ProjectToken.Transaksi[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "idUser", + "type": "string" + }, + { + "internalType": "string", + "name": "idProjek", + "type": "string" + } + ], + "name": "getChartTokensByUserIdAndProjectId", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "chartTokenId", + "type": "string" + }, + { + "internalType": "string", + "name": "idUser", + "type": "string" + }, + { + "internalType": "string", + "name": "idProjek", + "type": "string" + }, + { + "internalType": "int256", + "name": "nominal", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "createdAt", + "type": "uint256" + } + ], + "internalType": "struct ProjectToken.ChartToken[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "idProjek", + "type": "string" + } + ], + "name": "getDividenProfitByProjectId", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "idProjek", + "type": "string" + }, + { + "internalType": "int256", + "name": "pelaksana", + "type": "int256" + }, + { + "internalType": "int256", + "name": "pemilik", + "type": "int256" + }, + { + "internalType": "int256", + "name": "koperasi", + "type": "int256" + }, + { + "internalType": "int256", + "name": "pendana", + "type": "int256" + } + ], + "internalType": "struct ProjectToken.DividenProfit", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "chartTokenId", + "type": "string" + } + ], + "name": "getHistoryTokenByChartTokenId", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "historyTokenId", + "type": "string" + }, + { + "internalType": "string", + "name": "chartTokenId", + "type": "string" + }, + { + "internalType": "int256", + "name": "totalNilai", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "createdAt", + "type": "uint256" + } + ], + "internalType": "struct ProjectToken.HistoryToken", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "idUser", + "type": "string" + }, + { + "internalType": "string", + "name": "idProjek", + "type": "string" + } + ], + "name": "getHistoryTokensByUserIdAndProjectId", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "historyTokenId", + "type": "string" + }, + { + "internalType": "string", + "name": "chartTokenId", + "type": "string" + }, + { + "internalType": "int256", + "name": "totalNilai", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "createdAt", + "type": "uint256" + } + ], + "internalType": "struct ProjectToken.HistoryToken[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "idUser", + "type": "string" + }, + { + "internalType": "string", + "name": "idProjek", + "type": "string" + } + ], + "name": "getLatestChartTokenByUserIdAndProjectId", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "chartTokenId", + "type": "string" + }, + { + "internalType": "string", + "name": "idUser", + "type": "string" + }, + { + "internalType": "string", + "name": "idProjek", + "type": "string" + }, + { + "internalType": "int256", + "name": "nominal", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "createdAt", + "type": "uint256" + } + ], + "internalType": "struct ProjectToken.ChartToken", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "idProjek", + "type": "string" + } + ], + "name": "getLatestChartTokensByProjectId", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "chartTokenId", + "type": "string" + }, + { + "internalType": "string", + "name": "idUser", + "type": "string" + }, + { + "internalType": "string", + "name": "idProjek", + "type": "string" + }, + { + "internalType": "int256", + "name": "nominal", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "createdAt", + "type": "uint256" + } + ], + "internalType": "struct ProjectToken.ChartToken[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "tokenId", + "type": "string" + } + ], + "name": "getTokenById", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "tokenId", + "type": "string" + }, + { + "internalType": "string", + "name": "idProjek", + "type": "string" + }, + { + "internalType": "string", + "name": "idUser", + "type": "string" + }, + { + "internalType": "int256", + "name": "nilai", + "type": "int256" + } + ], + "internalType": "struct ProjectToken.TokenDetail", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "idProjek", + "type": "string" + } + ], + "name": "getTokenByProjectId", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "tokenId", + "type": "string" + }, + { + "internalType": "string", + "name": "idProjek", + "type": "string" + }, + { + "internalType": "string", + "name": "idUser", + "type": "string" + }, + { + "internalType": "int256", + "name": "nilai", + "type": "int256" + } + ], + "internalType": "struct ProjectToken.TokenDetail[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "idUser", + "type": "string" + }, + { + "internalType": "string", + "name": "idProjek", + "type": "string" + } + ], + "name": "getTokenByUserAndProject", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "tokenId", + "type": "string" + }, + { + "internalType": "string", + "name": "idProjek", + "type": "string" + }, + { + "internalType": "string", + "name": "idUser", + "type": "string" + }, + { + "internalType": "int256", + "name": "nilai", + "type": "int256" + } + ], + "internalType": "struct ProjectToken.TokenDetail[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "idUser", + "type": "string" + }, + { + "internalType": "string", + "name": "idProjek", + "type": "string" + } + ], + "name": "getTotalNominalToken", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getTotalTokens", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "idProjek", + "type": "string" + } + ], + "name": "getTransactionByProjectId", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "idUser", + "type": "string" + }, + { + "internalType": "string", + "name": "namaUser", + "type": "string" + }, + { + "internalType": "string", + "name": "idProjek", + "type": "string" + }, + { + "internalType": "string", + "name": "judulProjek", + "type": "string" + }, + { + "internalType": "string", + "name": "ownerProjek", + "type": "string" + }, + { + "internalType": "int256", + "name": "jumlahToken", + "type": "int256" + }, + { + "internalType": "int256", + "name": "totalNominal", + "type": "int256" + } + ], + "internalType": "struct ProjectToken.Transaksi[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "idUser", + "type": "string" + } + ], + "name": "getTransactionByUserId", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "idUser", + "type": "string" + }, + { + "internalType": "string", + "name": "namaUser", + "type": "string" + }, + { + "internalType": "string", + "name": "idProjek", + "type": "string" + }, + { + "internalType": "string", + "name": "judulProjek", + "type": "string" + }, + { + "internalType": "string", + "name": "ownerProjek", + "type": "string" + }, + { + "internalType": "int256", + "name": "jumlahToken", + "type": "int256" + }, + { + "internalType": "int256", + "name": "totalNominal", + "type": "int256" + } + ], + "internalType": "struct ProjectToken.Transaksi[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "name": "historyTokens", + "outputs": [ + { + "internalType": "string", + "name": "historyTokenId", + "type": "string" + }, + { + "internalType": "string", + "name": "chartTokenId", + "type": "string" + }, + { + "internalType": "int256", + "name": "totalNilai", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "createdAt", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "addedValue", + "type": "uint256" + } + ], + "name": "increaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "tokenId", + "type": "string" + } + ], + "name": "resetTokenNominal", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "name": "tokenDetails", + "outputs": [ + { + "internalType": "string", + "name": "tokenId", + "type": "string" + }, + { + "internalType": "string", + "name": "idProjek", + "type": "string" + }, + { + "internalType": "string", + "name": "idUser", + "type": "string" + }, + { + "internalType": "int256", + "name": "nilai", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "transaksiList", + "outputs": [ + { + "internalType": "string", + "name": "idUser", + "type": "string" + }, + { + "internalType": "string", + "name": "namaUser", + "type": "string" + }, + { + "internalType": "string", + "name": "idProjek", + "type": "string" + }, + { + "internalType": "string", + "name": "judulProjek", + "type": "string" + }, + { + "internalType": "string", + "name": "ownerProjek", + "type": "string" + }, + { + "internalType": "int256", + "name": "jumlahToken", + "type": "int256" + }, + { + "internalType": "int256", + "name": "totalNominal", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "idProjek", + "type": "string" + }, + { + "internalType": "int256", + "name": "newPelaksana", + "type": "int256" + }, + { + "internalType": "int256", + "name": "newPemilik", + "type": "int256" + }, + { + "internalType": "int256", + "name": "newKoperasi", + "type": "int256" + }, + { + "internalType": "int256", + "name": "newPendana", + "type": "int256" + } + ], + "name": "updateDividenProfit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "tokenId", + "type": "string" + }, + { + "internalType": "string", + "name": "newIdUser", + "type": "string" + } + ], + "name": "updateTokenUser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/services/backend/drizzle.config.ts b/services/backend/drizzle.config.ts new file mode 100644 index 0000000..ccf4513 --- /dev/null +++ b/services/backend/drizzle.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + dialect: "postgresql", + schema: "./src/drizzle/schema.ts", + out: "./src/drizzle/migrations", + dbCredentials: { + url: process.env.DATABASE_URL as string, + }, + verbose: true, + strict: true, +}); diff --git a/services/backend/express.d.ts b/services/backend/express.d.ts new file mode 100644 index 0000000..be6fb3d --- /dev/null +++ b/services/backend/express.d.ts @@ -0,0 +1,6 @@ +declare namespace Express { + interface Request { + user?: any; + query?: any; + } + } \ No newline at end of file diff --git a/services/backend/package-lock.json b/services/backend/package-lock.json new file mode 100644 index 0000000..488c67a --- /dev/null +++ b/services/backend/package-lock.json @@ -0,0 +1,4869 @@ +{ + "name": "expressjs-structure", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "expressjs-structure", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@faker-js/faker": "^8.4.1", + "@types/express-validator": "^3.0.0", + "@types/jsonwebtoken": "^9.0.6", + "@types/swagger-ui-express": "^4.1.6", + "@whiskeysockets/baileys": "^6.7.7", + "axios": "^1.7.8", + "bcrypt": "^5.1.1", + "bcryptjs": "^2.4.3", + "body-parser": "^1.20.2", + "cors": "^2.8.5", + "date-fns": "^3.6.0", + "dotenv": "^16.4.5", + "drizzle-orm": "^0.32.2", + "ethers": "^6.13.3", + "express-validator": "^7.1.0", + "idn-area-data": "^3.1.1", + "jsonwebtoken": "^9.0.2", + "multer": "^1.4.5-lts.1", + "node-cron": "^3.0.3", + "nodemailer": "^6.9.14", + "pg": "^8.12.0", + "postgres": "^3.4.4", + "qrcode-terminal": "^0.12.0", + "swagger-autogen": "^2.23.7", + "swagger-ui-express": "^5.0.1", + "tsx": "^4.17.0", + "zod": "^3.23.8", + "zod-validation-error": "^3.3.1" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/multer": "^1.4.11", + "@types/node": "^22.1.0", + "@types/node-cron": "^3.0.11", + "@types/nodemailer": "^6.4.15", + "drizzle-kit": "^0.23.2", + "express": "^4.19.2", + "ts-node": "^10.9.2", + "typescript": "^5.5.4" + } + }, + "node_modules/@adiwajshing/keyed-db": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@adiwajshing/keyed-db/-/keyed-db-0.2.4.tgz", + "integrity": "sha512-yprSnAtj80/VKuDqRcFFLDYltoNV8tChNwFfIgcf6PGD4sjzWIBgs08pRuTqGH5mk5wgL6PBRSsMCZqtZwzFEw==", + "license": "MIT" + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", + "license": "MIT" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@drizzle-team/brocli": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.8.2.tgz", + "integrity": "sha512-zTrFENsqGvOkBOuHDC1pXCkDXNd2UhP4lI3gYGhQ1R1SPeAAfqzPsV1dcpMy4uNU6kB5VpU5NGhvwxVNETR02A==", + "dev": true + }, + "node_modules/@esbuild-kit/core-utils": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", + "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==", + "dev": true, + "dependencies": { + "esbuild": "~0.18.20", + "source-map-support": "^0.5.21" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/@esbuild-kit/esm-loader": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", + "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==", + "dev": true, + "dependencies": { + "@esbuild-kit/core-utils": "^3.3.2", + "get-tsconfig": "^4.7.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz", + "integrity": "sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eshaz/web-worker": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@eshaz/web-worker/-/web-worker-1.2.2.tgz", + "integrity": "sha512-WxXiHFmD9u/owrzempiDlBB1ZYqiLnm9s6aPc8AlFQalq2tKmqdmMr9GXOupDgzXtqnBipj8Un0gkIm7Sjf8mw==", + "license": "Apache-2.0" + }, + "node_modules/@faker-js/faker": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz", + "integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0", + "npm": ">=6.14.13" + } + }, + "node_modules/@hapi/boom": { + "version": "9.1.4", + "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-9.1.4.tgz", + "integrity": "sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "9.x.x" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@thi.ng/bitstream": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@thi.ng/bitstream/-/bitstream-2.4.1.tgz", + "integrity": "sha512-pw8IRl4tES/BgZV87ugksoUP7/rEXVUwLBAQkiqlE0EKZArsr+ZdVvJs5H4x1S3gDStl9oUD818VRuS9byYDuA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/postspectacular" + }, + { + "type": "patreon", + "url": "https://patreon.com/thing_umbrella" + } + ], + "license": "Apache-2.0", + "dependencies": { + "@thi.ng/errors": "^2.5.15" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@thi.ng/errors": { + "version": "2.5.15", + "resolved": "https://registry.npmjs.org/@thi.ng/errors/-/errors-2.5.15.tgz", + "integrity": "sha512-vQ0M3yf6UbB8k6rdQMo2zgFpvcvEl95aLA1y5Fhxtm1KU6JfzDO1YZL3eIYPPnzhjUHw9zc0htbeBLI1x2QDnw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/postspectacular" + }, + { + "type": "patreon", + "url": "https://patreon.com/thing_umbrella" + } + ], + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.5", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", + "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/express-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/express-validator/-/express-validator-3.0.0.tgz", + "integrity": "sha512-LusnB0YhTXpBT25PXyGPQlK7leE1e41Vezq1hHEUwjfkopM1Pkv2X2Ppxqh9c+w/HZ6Udzki8AJotKNjDTGdkQ==", + "deprecated": "This is a stub types definition for express-validator (https://github.com/ctavan/express-validator). express-validator provides its own type definitions, so you don't need @types/express-validator installed!", + "dependencies": { + "express-validator": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz", + "integrity": "sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + }, + "node_modules/@types/multer": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.11.tgz", + "integrity": "sha512-svK240gr6LVWvv3YGyhLlA+6LRRWA4mnGIU7RcNmgjBYFl6665wcXrRfxGp5tEPVHUNm5FMcmq7too9bxCwX/w==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/node": { + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.1.0.tgz", + "integrity": "sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==", + "dependencies": { + "undici-types": "~6.13.0" + } + }, + "node_modules/@types/node-cron": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.11.tgz", + "integrity": "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/nodemailer": { + "version": "6.4.15", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.15.tgz", + "integrity": "sha512-0EBJxawVNjPkng1zm2vopRctuWVCxk34JcIlRuXSf54habUWdz1FB7wHDqOqvDa8Mtpt0Q3LTXQkAs2LNyK5jQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.9.15", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", + "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.6.tgz", + "integrity": "sha512-UVSiGYXa5IzdJJG3hrc86e8KdZWLYxyEsVoUI4iPXc7CO4VZ3AfNP8d/8+hrDRIqz+HAaSMtZSqAsF3Nq2X/Dg==", + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@wasm-audio-decoders/common": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@wasm-audio-decoders/common/-/common-9.0.5.tgz", + "integrity": "sha512-b9JNh9sPAvn8PVIizNh9D60WkfQong/u9ea873H47u7zvVDLctxYIp2aZw9CQqXaQdk7JB3MoU5UHiseO40swg==", + "license": "MIT", + "dependencies": { + "@eshaz/web-worker": "1.2.2", + "simple-yenc": "^1.0.4" + } + }, + "node_modules/@wasm-audio-decoders/flac": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@wasm-audio-decoders/flac/-/flac-0.2.4.tgz", + "integrity": "sha512-bsUlwIjd5y+IAEyILCQdi8y0LocKEkZ0enA8ljDL+NVVwN+5Rv5Xkm/HcdUxnB7MtekxN2cNcTsv1zkb2aZyWg==", + "license": "MIT", + "dependencies": { + "@wasm-audio-decoders/common": "9.0.5", + "codec-parser": "2.4.3" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/eshaz" + } + }, + "node_modules/@wasm-audio-decoders/ogg-vorbis": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/@wasm-audio-decoders/ogg-vorbis/-/ogg-vorbis-0.1.15.tgz", + "integrity": "sha512-skAN3NIrRzMkVouyfyq3gYT/op/K9iutMZr7kr5/9fnIaCnpYdrdbv69X8PZ6y3K2J5zy5KuGno5kzH8yGLOOg==", + "license": "MIT", + "dependencies": { + "@wasm-audio-decoders/common": "9.0.5", + "codec-parser": "2.4.3" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/eshaz" + } + }, + "node_modules/@whiskeysockets/baileys": { + "version": "6.7.7", + "resolved": "https://registry.npmjs.org/@whiskeysockets/baileys/-/baileys-6.7.7.tgz", + "integrity": "sha512-QWF+D/+bxGvh5xvFJUOXeZ0UTgbm6LEUVXeHtDHYL8wCeK2ND5qnbGrOEFC7LJzhnOhaJZA/N0v3SutpH7zKYw==", + "license": "MIT", + "dependencies": { + "@adiwajshing/keyed-db": "^0.2.4", + "@hapi/boom": "^9.1.3", + "async-lock": "^1.4.1", + "audio-decode": "^2.1.3", + "axios": "^1.6.0", + "cache-manager": "4.0.1", + "futoin-hkdf": "^1.5.1", + "libphonenumber-js": "^1.10.20", + "libsignal": "github:WhiskeySockets/libsignal-node", + "lodash": "^4.17.21", + "music-metadata": "^7.12.3", + "node-cache": "^5.1.2", + "pino": "^7.0.0", + "protobufjs": "^7.2.4", + "uuid": "^10.0.0", + "ws": "^8.13.0" + }, + "peerDependencies": { + "jimp": "^0.16.1", + "link-preview-js": "^3.0.0", + "qrcode-terminal": "^0.12.0", + "sharp": "^0.32.6" + }, + "peerDependenciesMeta": { + "jimp": { + "optional": true + }, + "link-preview-js": { + "optional": true + }, + "qrcode-terminal": { + "optional": true + }, + "sharp": { + "optional": true + } + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", + "license": "MIT" + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/async": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", + "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==", + "license": "MIT" + }, + "node_modules/async-lock": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", + "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/audio-buffer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/audio-buffer/-/audio-buffer-5.0.0.tgz", + "integrity": "sha512-gsDyj1wwUp8u7NBB+eW6yhLb9ICf+0eBmDX8NGaAS00w8/fLqFdxUlL5Ge/U8kB64DlQhdonxYC59dXy1J7H/w==", + "license": "MIT" + }, + "node_modules/audio-decode": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/audio-decode/-/audio-decode-2.2.2.tgz", + "integrity": "sha512-xyh7z6dpRT+5Ez4ggV2cEkSShkDvvIBBmVPR3kYY7uIBqRO1BGNjofip6JnjBnvezhrU3ypBGZjepyKFDZWnDw==", + "license": "MIT", + "dependencies": { + "@wasm-audio-decoders/flac": "^0.2.4", + "@wasm-audio-decoders/ogg-vorbis": "^0.1.15", + "audio-buffer": "^5.0.0", + "audio-type": "^2.2.1", + "mpg123-decoder": "^1.0.0", + "node-wav": "^0.0.2", + "ogg-opus-decoder": "^1.6.12", + "qoa-format": "^1.0.1" + } + }, + "node_modules/audio-type": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/audio-type/-/audio-type-2.2.1.tgz", + "integrity": "sha512-En9AY6EG1qYqEy5L/quryzbA4akBpJrnBZNxeKTqGHC2xT9Qc4aZ8b7CcbOMFTTc/MGdoNyp+SN4zInZNKxMYA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.1.tgz", + "integrity": "sha512-+H+kuK34PfMaI9PNU/NSjBKL5hh/KDM9J72kwYeYEm0A8B1AC4fuCy3qsjnA7lxklgyXsB68yn8Z2xoZEjgwCQ==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axios": { + "version": "1.7.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.8.tgz", + "integrity": "sha512-Uu0wb7KNqK2t5K+YQyVCLM76prD5sRFjKHbJYCP1J7JFGEQ6nN7HWn9+04LAeiJ3ji54lgS/gZCH1oxyrf1SPw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cache-manager": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-4.0.1.tgz", + "integrity": "sha512-JWdtjdX8e0e6eMehAZsdJvBMvHn/pVQGYUjgzc1ILFH0vtcffb9R7XIEAqfYgEeaVJVCOSP4+dxCius+ciW0RA==", + "license": "MIT", + "dependencies": { + "async": "3.2.3", + "lodash.clonedeep": "^4.5.0", + "lru-cache": "^7.10.1" + } + }, + "node_modules/cache-manager/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/codec-parser": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/codec-parser/-/codec-parser-2.4.3.tgz", + "integrity": "sha512-3dAvFtdpxn4YLstqsB2ZiJXXNg7n1j7R5ONeDuk+2kBkb39PwrCRytOFHlSWA8q5jCjW3PumeMv9q37bFHsijg==", + "license": "LGPL-3.0-or-later" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/concat-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/concat-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/curve25519-js": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/curve25519-js/-/curve25519-js-0.0.4.tgz", + "integrity": "sha512-axn2UMEnkhyDUPWOwVKBMVIzSQy2ejH2xRGy1wq81dqRwApXfIzfbE3hIX0ZRFBIihf/KDqK158DLwESu4AK1w==", + "license": "MIT" + }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/drizzle-kit": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.23.2.tgz", + "integrity": "sha512-NWkQ7GD2OTbQ7HzcjsaCOf3n0tlFPSEAF38fvDpwDj8jRbGWGFtN2cD8I8wp4lU+5Os/oyP2xycTKGLHdPipUw==", + "dev": true, + "dependencies": { + "@drizzle-team/brocli": "^0.8.2", + "@esbuild-kit/esm-loader": "^2.5.5", + "esbuild": "^0.19.7", + "esbuild-register": "^3.5.0" + }, + "bin": { + "drizzle-kit": "bin.cjs" + } + }, + "node_modules/drizzle-orm": { + "version": "0.32.2", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.32.2.tgz", + "integrity": "sha512-3fXKzPzrgZIcnWCSLiERKN5Opf9Iagrag75snfFlKeKSYB1nlgPBshzW3Zn6dQymkyiib+xc4nIz0t8U+Xdpuw==", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=3", + "@electric-sql/pglite": ">=0.1.1", + "@libsql/client": "*", + "@neondatabase/serverless": ">=0.1", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/react": ">=18", + "@types/sql.js": "*", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=13.2.0", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "react": ">=18", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/react": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "react": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/esbuild-register": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", + "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "esbuild": ">=0.12 <1" + } + }, + "node_modules/esbuild-register/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/esbuild-register/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ethers": { + "version": "6.13.3", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.13.3.tgz", + "integrity": "sha512-/DzbZOLVtoO4fKvvQwpEucHAQgIwBGWuRvBdwE/lMXgXvvHHTSkn7XqAQ2b+gjJzZDJjWA9OD05bVceVOsBHbg==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "1.10.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "18.15.13", + "aes-js": "4.0.0-beta.5", + "tslib": "2.4.0", + "ws": "8.17.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ethers/node_modules/@types/node": { + "version": "18.15.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.13.tgz", + "integrity": "sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==", + "license": "MIT" + }, + "node_modules/ethers/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/express": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.10", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express-validator": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.1.0.tgz", + "integrity": "sha512-ePn6NXjHRZiZkwTiU1Rl2hy6aUqmi6Cb4/s8sfUsKH7j2yYl9azSpl8xEHcOj1grzzQ+UBEoLWtE1s6FDxW++g==", + "dependencies": { + "lodash": "^4.17.21", + "validator": "~13.12.0" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fast-redact": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/file-type": { + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz", + "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==", + "license": "MIT", + "dependencies": { + "readable-web-to-node-stream": "^3.0.0", + "strtok3": "^6.2.4", + "token-types": "^4.1.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/futoin-hkdf": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/futoin-hkdf/-/futoin-hkdf-1.5.3.tgz", + "integrity": "sha512-SewY5KdMpaoCeh7jachEWFsh1nNlaDjNHZXWqL5IGwtpEYHTgkr2+AMCgNwKWkcc0wpSYrZfR7he4WdmHFtDxQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "optional": true, + "peer": true, + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.6.tgz", + "integrity": "sha512-ZAqrLlu18NbDdRaHq+AKXzAmqIUPswPWKUchfytdAjiRFnCe5ojG2bstg6mRiZabkKfCoL/e98pbBELIV/YCeA==", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/idn-area-data": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/idn-area-data/-/idn-area-data-3.1.1.tgz", + "integrity": "sha512-70KqGmV1Bpa8Rjy4pMk3iJs4w9QLedOMRY3wKV7Jicnp5nVBaadzbOK40F4el7YZ2AYQvpJ2PUJJWBy8N2uxlg==", + "dependencies": { + "papaparse": "^5.4.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/fityannugroho" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "optional": true, + "peer": true + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/libphonenumber-js": { + "version": "1.11.7", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.7.tgz", + "integrity": "sha512-x2xON4/Qg2bRIS11KIN9yCNYUjhtiEjNyptjX0mX+pyKHecxuJVLIpfX1lq9ZD6CrC/rB+y4GBi18c6CEcUR+A==", + "license": "MIT" + }, + "node_modules/libsignal": { + "name": "@whiskeysockets/libsignal-node", + "version": "2.0.1", + "resolved": "git+ssh://git@github.com/WhiskeySockets/libsignal-node.git#83a3e3a3864511cb74df1b796373f0d49d071134", + "license": "GPL-3.0", + "dependencies": { + "curve25519-js": "^0.0.4", + "protobufjs": "6.8.8" + } + }, + "node_modules/libsignal/node_modules/@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", + "license": "MIT" + }, + "node_modules/libsignal/node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "license": "Apache-2.0" + }, + "node_modules/libsignal/node_modules/protobufjs": { + "version": "6.8.8", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz", + "integrity": "sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.0", + "@types/node": "^10.1.0", + "long": "^4.0.0" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz", + "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==", + "license": "ISC", + "optional": true, + "peer": true, + "engines": { + "node": ">=16.14" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mpg123-decoder": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mpg123-decoder/-/mpg123-decoder-1.0.0.tgz", + "integrity": "sha512-WV+pyuMUhRqv7s8S6p/Ii4KQHdBD1pb3yaABxcKJRsNp+HQ/Y6z2iIBIaOZu0JMHPTOoICYt0REDZ7XfLu+n/g==", + "license": "MIT", + "dependencies": { + "@wasm-audio-decoders/common": "9.0.5" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/eshaz" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/multer": { + "version": "1.4.5-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", + "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/multer/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/music-metadata": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-7.14.0.tgz", + "integrity": "sha512-xrm3w7SV0Wk+OythZcSbaI8mcr/KHd0knJieu8bVpaPfMv/Agz5EooCAPz3OR5hbYMiUG6dgAPKZKnMzV+3amA==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "content-type": "^1.0.5", + "debug": "^4.3.4", + "file-type": "^16.5.4", + "media-typer": "^1.1.0", + "strtok3": "^6.3.0", + "token-types": "^4.2.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/music-metadata/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/music-metadata/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/music-metadata/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mysql2": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.11.0.tgz", + "integrity": "sha512-J9phbsXGvTOcRVPR95YedzVSxJecpW5A5+cQ57rhHIFXteTP10HCs+VBjS7DHIKfEaI1zQ5tlVrquCd64A6YvA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru-cache": "^8.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "optional": true, + "peer": true, + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/named-placeholders/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "optional": true, + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" + }, + "node_modules/node-cache": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", + "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", + "license": "MIT", + "dependencies": { + "clone": "2.x" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/node-cron": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", + "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==", + "license": "ISC", + "dependencies": { + "uuid": "8.3.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/node-cron/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-wav": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/node-wav/-/node-wav-0.0.2.tgz", + "integrity": "sha512-M6Rm/bbG6De/gKGxOpeOobx/dnGuP0dz40adqx38boqHhlWssBJZgLCPBNtb9NkrmnKYiV04xELq+R6PFOnoLA==", + "license": "MIT", + "engines": { + "node": ">=4.4.0" + } + }, + "node_modules/nodemailer": { + "version": "6.9.14", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.14.tgz", + "integrity": "sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ogg-opus-decoder": { + "version": "1.6.12", + "resolved": "https://registry.npmjs.org/ogg-opus-decoder/-/ogg-opus-decoder-1.6.12.tgz", + "integrity": "sha512-6MY/rgFegJABKVE7LS10lmVoy8dFhvLDbIlcymgMnn0qZG0YHqcUU+bW+MkVyhhWN3H0vqtkRlPHGOXU6yR5YQ==", + "license": "MIT", + "dependencies": { + "@wasm-audio-decoders/common": "9.0.5", + "codec-parser": "2.4.3", + "opus-decoder": "0.7.6" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/eshaz" + } + }, + "node_modules/on-exit-leak-free": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-0.2.0.tgz", + "integrity": "sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg==", + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/opus-decoder": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/opus-decoder/-/opus-decoder-0.7.6.tgz", + "integrity": "sha512-5QYSl1YQYbSzWL7vM4dJoyrLC804xIvBFjfKTZZ6/z/EgmdFouOTT+8PDM2V18vzgnhRNPDuyB2aTfl/2hvMRA==", + "license": "MIT", + "dependencies": { + "@wasm-audio-decoders/common": "9.0.5" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/eshaz" + } + }, + "node_modules/papaparse": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz", + "integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "license": "MIT" + }, + "node_modules/peek-readable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", + "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/pg": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.12.0.tgz", + "integrity": "sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==", + "dependencies": { + "pg-connection-string": "^2.6.4", + "pg-pool": "^3.6.2", + "pg-protocol": "^1.6.1", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.4.tgz", + "integrity": "sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.2.tgz", + "integrity": "sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.1.tgz", + "integrity": "sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/pino": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-7.11.0.tgz", + "integrity": "sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.0.0", + "on-exit-leak-free": "^0.2.0", + "pino-abstract-transport": "v0.5.0", + "pino-std-serializers": "^4.0.0", + "process-warning": "^1.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.1.0", + "safe-stable-stringify": "^2.1.0", + "sonic-boom": "^2.2.1", + "thread-stream": "^0.15.1" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-0.5.0.tgz", + "integrity": "sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ==", + "license": "MIT", + "dependencies": { + "duplexify": "^4.1.2", + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-4.0.0.tgz", + "integrity": "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==", + "license": "MIT" + }, + "node_modules/postgres": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.4.tgz", + "integrity": "sha512-IbyN+9KslkqcXa8AO9fxpk97PA4pzewvpi2B3Dwy9u4zpV32QicaEdgmF3eSQUzdRk7ttDHQejNgAEr4XoeH4A==", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/process-warning": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", + "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==", + "license": "MIT" + }, + "node_modules/protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/qoa-format": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/qoa-format/-/qoa-format-1.0.1.tgz", + "integrity": "sha512-dMB0Z6XQjdpz/Cw4Rf6RiBpQvUSPCfYlQMWvmuWlWkAT7nDQD29cVZ1SwDUB6DYJSitHENwbt90lqfI+7bvMcw==", + "license": "MIT", + "dependencies": { + "@thi.ng/bitstream": "^2.2.12" + } + }, + "node_modules/qrcode-terminal": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz", + "integrity": "sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==", + "bin": { + "qrcode-terminal": "bin/qrcode-terminal.js" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readable-web-to-node-stream": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", + "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/real-require": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.1.0.tgz", + "integrity": "sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==", + "optional": true, + "peer": true + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/simple-yenc": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/simple-yenc/-/simple-yenc-1.0.4.tgz", + "integrity": "sha512-5gvxpSd79e9a3V4QDYUqnqxeD4HGlhCakVpb6gMnDD7lexJggSBJRBO5h52y/iJrdXRilX9UCuDaIJhSWm5OWw==", + "license": "MIT", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/eshaz" + } + }, + "node_modules/sonic-boom": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-2.8.0.tgz", + "integrity": "sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "optional": true, + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT" + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strtok3": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", + "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^4.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/swagger-autogen": { + "version": "2.23.7", + "resolved": "https://registry.npmjs.org/swagger-autogen/-/swagger-autogen-2.23.7.tgz", + "integrity": "sha512-vr7uRmuV0DCxWc0wokLJAwX3GwQFJ0jwN+AWk0hKxre2EZwusnkGSGdVFd82u7fQLgwSTnbWkxUL7HXuz5LTZQ==", + "license": "MIT", + "dependencies": { + "acorn": "^7.4.1", + "deepmerge": "^4.2.2", + "glob": "^7.1.7", + "json5": "^2.2.3" + } + }, + "node_modules/swagger-autogen/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.17.14", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz", + "integrity": "sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw==", + "license": "Apache-2.0" + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/thread-stream": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-0.15.2.tgz", + "integrity": "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA==", + "license": "MIT", + "dependencies": { + "real-require": "^0.1.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", + "integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/acorn-walk": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz", + "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.17.0.tgz", + "integrity": "sha512-eN4mnDA5UMKDt4YZixo9tBioibaMBpoxBkD+rIPAjVmYERSG0/dWEY1CEFuV89CgASlKL499q8AhmkMnnjtOJg==", + "license": "MIT", + "dependencies": { + "esbuild": "~0.23.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz", + "integrity": "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.0.tgz", + "integrity": "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz", + "integrity": "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.0.tgz", + "integrity": "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz", + "integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz", + "integrity": "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz", + "integrity": "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz", + "integrity": "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz", + "integrity": "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz", + "integrity": "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz", + "integrity": "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz", + "integrity": "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz", + "integrity": "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz", + "integrity": "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz", + "integrity": "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz", + "integrity": "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz", + "integrity": "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz", + "integrity": "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz", + "integrity": "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz", + "integrity": "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz", + "integrity": "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz", + "integrity": "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz", + "integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz", + "integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.0", + "@esbuild/android-arm": "0.23.0", + "@esbuild/android-arm64": "0.23.0", + "@esbuild/android-x64": "0.23.0", + "@esbuild/darwin-arm64": "0.23.0", + "@esbuild/darwin-x64": "0.23.0", + "@esbuild/freebsd-arm64": "0.23.0", + "@esbuild/freebsd-x64": "0.23.0", + "@esbuild/linux-arm": "0.23.0", + "@esbuild/linux-arm64": "0.23.0", + "@esbuild/linux-ia32": "0.23.0", + "@esbuild/linux-loong64": "0.23.0", + "@esbuild/linux-mips64el": "0.23.0", + "@esbuild/linux-ppc64": "0.23.0", + "@esbuild/linux-riscv64": "0.23.0", + "@esbuild/linux-s390x": "0.23.0", + "@esbuild/linux-x64": "0.23.0", + "@esbuild/netbsd-x64": "0.23.0", + "@esbuild/openbsd-arm64": "0.23.0", + "@esbuild/openbsd-x64": "0.23.0", + "@esbuild/sunos-x64": "0.23.0", + "@esbuild/win32-arm64": "0.23.0", + "@esbuild/win32-ia32": "0.23.0", + "@esbuild/win32-x64": "0.23.0" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, + "node_modules/typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz", + "integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.3.1.tgz", + "integrity": "sha512-uFzCZz7FQis256dqw4AhPQgD6f3pzNca/Zh62RNELavlumQB3nDIUFbF5JQfFLcMbO1s02Q7Xg/gpcOBlEnYZA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.18.0" + } + } + } +} diff --git a/services/backend/package.json b/services/backend/package.json new file mode 100644 index 0000000..0744172 --- /dev/null +++ b/services/backend/package.json @@ -0,0 +1,65 @@ +{ + "name": "expressjs-structure", + "version": "1.0.0", + "description": "", + "main": "main.js", + "scripts": { + "dev": "tsx watch src/main.ts", + "build": "tsc", + "serve": "node dist/main.js", + "bot": "ts-node src/services/baileys.ts", + "db:generate": "drizzle-kit generate", + "db:migrate": "tsx src/drizzle/migrate.ts", + "db:drop": "psql -U postgres -d postgres -c \"DROP DATABASE IF EXISTS koperasi;\"", + "db:create": "psql -U postgres -d postgres -c \"CREATE DATABASE koperasi;\"", + "db:seed": "tsx src/drizzle/seed.ts", + "migrate:fresh": "rm -rf src/drizzle/migrations/* && npm run db:generate && npm run db:drop && npm run db:create && npm run db:migrate", + "rm:upload": "rm -rf assets/uploads/foto_diri/* && rm -rf assets/uploads/foto_ktp/* && rm -rf assets/uploads/brosur_produk/* && rm -rf assets/uploads/dokumen_pendukung/* && rm -rf assets/uploads/dokumen_prospektus/* && rm -rf assets/uploads/dokumen_proyeksi/* && rm -rf assets/uploads/tanda_tangan/* && rm -rf assets/uploads/bukti_pembayaran/* && rm -rf assets/uploads/laporan_keuangan/* && rm -rf assets/uploads/laporan_mutasi/* && rm -rf assets/uploads/tanda_tangan_admin/*" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/multer": "^1.4.11", + "@types/node": "^22.1.0", + "@types/node-cron": "^3.0.11", + "@types/nodemailer": "^6.4.15", + "drizzle-kit": "^0.23.2", + "express": "^4.19.2", + "ts-node": "^10.9.2", + "typescript": "^5.5.4" + }, + "dependencies": { + "@faker-js/faker": "^8.4.1", + "@types/express-validator": "^3.0.0", + "@types/jsonwebtoken": "^9.0.6", + "@types/swagger-ui-express": "^4.1.6", + "@whiskeysockets/baileys": "^6.7.7", + "axios": "^1.7.8", + "bcrypt": "^5.1.1", + "bcryptjs": "^2.4.3", + "body-parser": "^1.20.2", + "cors": "^2.8.5", + "date-fns": "^3.6.0", + "dotenv": "^16.4.5", + "drizzle-orm": "^0.32.2", + "ethers": "^6.13.3", + "express-validator": "^7.1.0", + "idn-area-data": "^3.1.1", + "jsonwebtoken": "^9.0.2", + "multer": "^1.4.5-lts.1", + "node-cron": "^3.0.3", + "nodemailer": "^6.9.14", + "pg": "^8.12.0", + "postgres": "^3.4.4", + "qrcode-terminal": "^0.12.0", + "swagger-autogen": "^2.23.7", + "swagger-ui-express": "^5.0.1", + "tsx": "^4.17.0", + "zod": "^3.23.8", + "zod-validation-error": "^3.3.1" + } +} diff --git a/services/backend/src/controllers/agreement-letter.ts b/services/backend/src/controllers/agreement-letter.ts new file mode 100644 index 0000000..f0cd44f --- /dev/null +++ b/services/backend/src/controllers/agreement-letter.ts @@ -0,0 +1,56 @@ +import { Request, Response } from "express"; +import * as agreementLetterService from "../services/agreement-letter.js"; + +export const createAgreementLetterHandler = async (req: Request, res: Response) => { + try { + await agreementLetterService.createAgreementLetter(req.body); + return res.status(201).json({ message: "Agreement letter created successfully" }); + } catch (error: any) { + if (error.message.includes("Project not found")) { + return res.status(404).json({ message: "Project not found" }); + } + if (error.message.includes("User associated with the project not found")) { + return res.status(404).json({ message: "User associated with the project not found" }); + } + if (error.message.includes("Admin user not found")) { + return res.status(404).json({ message: "Admin user not found" }); + } + if (error.message.includes("Admin signature not found")) { + return res.status(404).json({ message: "Admin signature not found" }); + } + + console.error("Error creating agreement letter:", error); + return res.status(500).json({ message: "Failed to create agreement letter" }); + } +}; + +export const getAllAgreementLetterHandler = async (req: Request, res: Response) => { + try { + const { search } = req.query; + const response = await agreementLetterService.getAllAgreementLetter(search as string | undefined); + + return res.status(200).json({ data: response }); + } catch (error: any) { + if (error.statusCode === 404) { + return res.status(404).json({ message: error.message }); + } + + console.error("Error fetching all agreements:", error); + return res.status(500).json({ message: "Failed to fetch agreement letters" }); + } +}; + +export const getAgreementLetterByProjectIdHandler = async (req: Request, res: Response) => { + try { + const { id } = req.params; + const response = await agreementLetterService.getAgreementByProjectId(id); + return res.status(200).json({ data: response }); + } catch (error: any) { + if (error.message.includes("No agreements found for this project ID")) { + return res.status(404).json({ message: "No agreements found for this project ID" }); + } + + console.error("Error fetching agreement by project ID:", error); + return res.status(500).json({ message: "Failed to fetch agreement letter by project ID" }); + } +}; diff --git a/services/backend/src/controllers/auth.ts b/services/backend/src/controllers/auth.ts new file mode 100644 index 0000000..87cd7d1 --- /dev/null +++ b/services/backend/src/controllers/auth.ts @@ -0,0 +1,63 @@ +import { Request, Response, NextFunction } from "express"; +import * as auth from "../services/auth.js"; +import { generateToken, tokenBlacklist } from "../services/jwt.js"; +import jwt from "jsonwebtoken"; + +export async function register(req: Request, res: Response, next: NextFunction): Promise { + try { + const user = req.body; + const response = await auth.register(user); + + if (response.success) { + res.status(201).json({ message: response.message }); + } else { + res.status(response.statusCode).json({ message: response.message }); + } + } catch (error) { + console.error(`Error while creating user`); + res.status(500).json({ message: "Internal Server Error" }); + next(error); + } +} + +export async function login(req: Request, res: Response): Promise { + try { + const { no_hp, password } = req.body; + const user = await auth.login(no_hp, password); + + if (user) { + const token = generateToken({ id: user.id, role: user.role, status: user.status }); + res.status(200).json({ token }); + } + } catch (error: any) { + if (error.statusCode) { + res.status(error.statusCode).json({ message: error.message }); + } else { + res.status(500).json({ message: "Internal Server Error" }); + } + } +} + +export function logout(req: Request, res: Response) { + try { + const authHeader = req.headers.authorization; + const token = authHeader?.split(" ")[1]; + + if (!token) { + return res.status(401).json({ message: "No token provided" }); + } + + const decoded = jwt.decode(token) as jwt.JwtPayload; + + if (!decoded || !decoded.exp) { + return res.status(400).json({ message: "Invalid token structure" }); + } + + tokenBlacklist[token] = decoded.exp * 1000; + + res.status(200).json({ message: "Logout successful" }); + } catch (error) { + console.error(error); + res.status(500).json({ message: "Something went wrong during logout" }); + } +} diff --git a/services/backend/src/controllers/baileys.ts b/services/backend/src/controllers/baileys.ts new file mode 100644 index 0000000..82f4db4 --- /dev/null +++ b/services/backend/src/controllers/baileys.ts @@ -0,0 +1,23 @@ +import { Request, Response } from "express"; +import { qrCode, isConnected } from "../services/baileys.js"; + +export const getQRCodeHandler = async (req: Request, res: Response) => { + try { + if (isConnected) { + return res.status(200).json({ status: "SUCCESS", message: "WA terhubung" }); + } else if (qrCode) { + return res.status(200).json({ status: "NOTCONNECTED", qr: qrCode }); + } else { + return res.status(404).json({ + status: "ERROR", + message: "QR code not available, please try again later.", + }); + } + } catch (error) { + console.error("Error in getQRCodeHandler:", error); + return res.status(500).json({ + status: "ERROR", + message: "Internal server error, please try again later.", + }); + } +}; diff --git a/services/backend/src/controllers/chart-project.ts b/services/backend/src/controllers/chart-project.ts new file mode 100644 index 0000000..56ef174 --- /dev/null +++ b/services/backend/src/controllers/chart-project.ts @@ -0,0 +1,32 @@ +import { Request, Response } from "express"; +import * as chartProjectService from "../services/chart-project.js"; + +export const getChartProjectByIdHandler = async (req: Request, res: Response) => { + try { + const chartProject = await chartProjectService.getChartProjectById(req.params.id); + + if (!chartProject || chartProject.length === 0) { + return res.status(404).json({ message: "Chart Project not found" }); + } + + return res.status(200).json({ data: chartProject }); + } catch (error) { + console.error("Error fetching chart project by ID:", error); + return res.status(500).json({ message: "Internal server error" }); + } +}; + +export const getAllChartProjectByUserIdHandler = async (req: Request, res: Response) => { + try { + const chartProject = await chartProjectService.getAllChartProjectByUserId(req.user.id); + + if (!chartProject || chartProject.length === 0) { + return res.status(404).json({ message: "No Chart Projects found for this user" }); + } + + return res.status(200).json({ data: chartProject }); + } catch (error) { + console.error("Error fetching all chart projects by user ID:", error); + return res.status(500).json({ message: "Internal server error" }); + } +}; diff --git a/services/backend/src/controllers/chart-token.ts b/services/backend/src/controllers/chart-token.ts new file mode 100644 index 0000000..e0b6fb5 --- /dev/null +++ b/services/backend/src/controllers/chart-token.ts @@ -0,0 +1,50 @@ +import { Request, Response } from "express"; +import * as chartTokenService from "../services/chart-token.js"; + +export const getLastChartTokenByUserIdandProjectIdHandler = async (req: Request, res: Response) => { + try { + const chartToken = await chartTokenService.getLastChartTokenByUserIdandProjectId(req.user.id, req.params.id); + + if (!chartToken) { + return res.status(404).json({ message: "Chart Token not found" }); + } + + return res.status(200).json({ data: chartToken }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Internal Server Error"; + res.status(statusCode).json({ message }); + } +}; + +export const getChartTokenByUserIdandProjectIdHandler = async (req: Request, res: Response) => { + try { + const chartToken = await chartTokenService.getChartTokenByUserIdandProjectId(req.user.id, req.params.id); + + if (!chartToken || chartToken.length === 0) { + return res.status(404).json({ message: "Chart Token not found" }); + } + + return res.status(200).json({ data: chartToken }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Internal Server Error"; + res.status(statusCode).json({ message }); + } +}; + +export const getAllChartTokenByUserIdHandler = async (req: Request, res: Response) => { + try { + const chartToken = await chartTokenService.getAllChartTokenByUserId(req.user.id); + + if (!chartToken || chartToken.length === 0) { + return res.status(404).json({ message: "Chart Token not found" }); + } + + return res.status(200).json({ data: chartToken }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Internal Server Error"; + res.status(statusCode).json({ message }); + } +}; \ No newline at end of file diff --git a/services/backend/src/controllers/history-project-wallet.ts b/services/backend/src/controllers/history-project-wallet.ts new file mode 100644 index 0000000..c2fe828 --- /dev/null +++ b/services/backend/src/controllers/history-project-wallet.ts @@ -0,0 +1,15 @@ +import { Request, Response } from "express"; +import * as historyProjectWalletService from "../services/history-project-wallet.js"; + +export const getHistoryProjectWalletByProjectWalletIdHandler = async (req: Request, res: Response) => { + try { + const historyProjectWallet = await historyProjectWalletService.getHistoryProjectWalletByProjectWalletId(req.params.id); + return res.status(200).json({ data: historyProjectWallet }); + } catch (error: any) { + if (error.statusCode) { + res.status(error.statusCode).json({ message: error.message }); + } else { + res.status(500).json({ message: "Internal Server Error" }); + } + } +}; \ No newline at end of file diff --git a/services/backend/src/controllers/history-project.ts b/services/backend/src/controllers/history-project.ts new file mode 100644 index 0000000..4d4b560 --- /dev/null +++ b/services/backend/src/controllers/history-project.ts @@ -0,0 +1,13 @@ +import { Request, Response } from "express"; +import * as historyProjectService from "../services/history-project.js"; + +export const getHistoryProjectByProjectIdHandler = async (req: Request, res: Response) => { + try { + const historyProject = await historyProjectService.getHistoryProjectByProjectId(req.params.id); + return res.status(200).json({ data: historyProject }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Internal Server Error"; + res.status(statusCode).json({ message }); + } +}; \ No newline at end of file diff --git a/services/backend/src/controllers/history-token.ts b/services/backend/src/controllers/history-token.ts new file mode 100644 index 0000000..9effe6a --- /dev/null +++ b/services/backend/src/controllers/history-token.ts @@ -0,0 +1,20 @@ +import { Request, Response } from "express"; +import * as historyTokenService from "../services/history-token.js"; + +export const getHistoryTokenByUserIdandProjectIdHandler = async (req: Request, res: Response) => { + try { + const historyToken = await historyTokenService.getHistoryTokenByUserIdAndProjectId(req.user.id, req.params.id); + + if (!historyToken || historyToken.length === 0) { + return res.status(404).json({ message: "History Token not found" }); + } + + return res.status(200).json({ data: historyToken }); + } catch (error: any) { + if (error.statusCode) { + res.status(error.statusCode).json({ message: error.message }); + } else { + res.status(500).json({ message: "Internal Server Error" }); + } + } +}; \ No newline at end of file diff --git a/services/backend/src/controllers/mutation.ts b/services/backend/src/controllers/mutation.ts new file mode 100644 index 0000000..6ee8e82 --- /dev/null +++ b/services/backend/src/controllers/mutation.ts @@ -0,0 +1,54 @@ +import { Request, Response } from "express"; +import * as mutationService from "../services/mutation.js"; +export const createMutationHandler = async (req: Request, res: Response) => { + try { + const mutation = await mutationService.createMutation(req.body); + return res.status(201).json({ message: "Mutation created successfully", data: mutation }); + } catch (error: any) { + if (error.statusCode) { + return res.status(error.statusCode).json({ message: error.message }); + } else { + return res.status(500).json({ message: "Internal server error" }); + } + } +}; + +export const getAllMutationHandler = async (req: Request, res: Response) => { + try { + const { search } = req.query; + const mutation = await mutationService.getAllMutation(search as string | undefined); + return res.status(200).json({ data: mutation }); + } catch (error: any) { + if (error.statusCode) { + return res.status(error.statusCode).json({ message: error.message }); + } else { + return res.status(500).json({ message: "Internal server error" }); + } + } +}; + +export const getMutationByIdHandler = async (req: Request, res: Response) => { + try { + const mutation = await mutationService.getMutationById(req.params.id); + return res.status(200).json({ data: mutation }); + } catch (error: any) { + if (error.statusCode) { + return res.status(error.statusCode).json({ message: error.message }); + } else { + return res.status(500).json({ message: "Internal server error" }); + } + } +}; + +export const getMutationByProjectIdHandler = async (req: Request, res: Response) => { + try { + const mutation = await mutationService.getMutationByProjectId(req.params.id); + return res.status(200).json({ data: mutation }); + } catch (error: any) { + if (error.statusCode) { + return res.status(error.statusCode).json({ canUploadReport: error.canUploadReport, message: error.message }); + } else { + return res.status(500).json({ message: "Internal server error" }); + } + } +}; diff --git a/services/backend/src/controllers/project-category.ts b/services/backend/src/controllers/project-category.ts new file mode 100644 index 0000000..90248e7 --- /dev/null +++ b/services/backend/src/controllers/project-category.ts @@ -0,0 +1,70 @@ +import { Request, Response } from "express"; +import * as projectCategoryService from "../services/project-category.js"; + +export const createProjectCategoryHandler = async (req: Request, res: Response) => { + try { + const response = await projectCategoryService.createProjectCategory(req.body); + return res.status(201).json({ message: response.message }); + } catch (error: any) { + if (error.message === "Category already exists") { + return res.status(409).json({ message: "Project Category already exists" }); + } else if (error.message === "Category is required") { + return res.status(400).json({ message: "Category is required" }); + } + console.error(error); + return res.status(500).json({ message: "Project Category failed to be created" }); + } +}; + +export const getProjectCategoryHandler = async (req: Request, res: Response) => { + try { + const { search } = req.query; + const projects = await projectCategoryService.getAllProjectCategory(search as string | undefined); + return res.status(200).json({ data: projects }); + } catch (error) { + console.error(error); + return res.status(500).json({ message: "Failed to retrieve Project Categories" }); + } +}; + +export const getProjectCategoryByIdHandler = async (req: Request, res: Response) => { + try { + const { id } = req.params; + const response = await projectCategoryService.getProjectCategoryById(id); + return res.status(200).json({ data: response }); + } catch (error: any) { + if (error.message === "Project Category not found") { + return res.status(404).json({ message: "Project Category not found" }); + } + console.error(error); + return res.status(500).json({ message: "Failed to retrieve Project Category" }); + } +}; + +export const updateProjectCategoryByIdHandler = async (req: Request, res: Response) => { + try { + const { id } = req.body; + await projectCategoryService.updateProjectCategoryById(req.body, id); + return res.status(200).json({ message: "Project Category updated successfully" }); + } catch (error: any) { + if (error.message === "Project Category not found") { + return res.status(404).json({ message: "Project Category not found" }); + } + console.error(error); + return res.status(500).json({ message: "Failed to update Project Category" }); + } +}; + +export const deleteProjectCategoryByIdHandler = async (req: Request, res: Response) => { + try { + const { id } = req.body; + await projectCategoryService.deleteProjectCategoryById(id); + return res.status(200).json({ message: "Project Category deleted successfully" }); + } catch (error: any) { + if (error.message === "Project Category not found") { + return res.status(404).json({ message: "Project Category not found" }); + } + console.error(error); + return res.status(500).json({ message: "Failed to delete Project Category" }); + } +}; diff --git a/services/backend/src/controllers/project-report.ts b/services/backend/src/controllers/project-report.ts new file mode 100644 index 0000000..3bcccee --- /dev/null +++ b/services/backend/src/controllers/project-report.ts @@ -0,0 +1,63 @@ +import { Request, Response } from "express"; +import * as projectReportService from "../services/project-report.js"; + +export const createProjectReportHandler = async (req: Request, res: Response) => { + try { + const response = await projectReportService.createProjectReport(req.body); + return res.status(201).json({ message: response.message }); + } catch (error: any) { + if (error.message === "Project not found") { + return res.status(404).json({ message: "Project not found" }); + } else if (error.message.includes("You can upload the report after")) { + return res.status(400).json({ message: error.message }); + } + console.error(error); + return res.status(500).json({ message: "Project report failed to be created" }); + } +}; + +export const getAllProjectReportHandler = async (req: Request, res: Response) => { + try { + const { search } = req.query; + const response = await projectReportService.getAllProjectReport(search as string | undefined); + + if (!response || response.length === 0) { + return res.status(404).json({ message: "Project report not found" }); + } + + return res.status(200).json({ data: response }); + } catch (error) { + console.error("Error retrieving project reports:", error); + return res.status(500).json({ message: "Failed to retrieve project reports" }); + } +}; + +export const getProjectReportHandler = async (req: Request, res: Response) => { + try { + const { id } = req.params; + const response = await projectReportService.getProjectReportById(id); + if (!response || response.length === 0) { + return res.status(404).json({ message: "Project report not found" }); + } + return res.status(200).json({ data: response }); + } catch (error) { + console.error(error); + return res.status(500).json({ message: "Failed to retrieve project report" }); + } +}; + +export const getProjectReportByProjectIdHandler = async (req: Request, res: Response) => { + try { + const { id } = req.params; + const { jenis } = req.query; + + const response = await projectReportService.getProjectReportByProjectId(id, jenis as string | undefined); + if (!response.projectReport || response.projectReport.length === 0) { + return res.status(404).json({ message: "Project report not found" }); + } + return res.status(200).json({ data: response }); + } catch (error) { + console.error(error); + return res.status(500).json({ message: "Failed to retrieve project report" }); + } +}; diff --git a/services/backend/src/controllers/project-token.ts b/services/backend/src/controllers/project-token.ts new file mode 100644 index 0000000..62f8c5e --- /dev/null +++ b/services/backend/src/controllers/project-token.ts @@ -0,0 +1,124 @@ +import * as projectTokenService from "../services/project-token.js"; +import { Request, Response } from "express"; + +export const getTokenByIdProjectHandler = async (req: Request, res: Response) => { + try { + const { id } = req.params; + const response = await projectTokenService.getTokenByIdProject(id); + if (!response || response.length === 0) { + return res.status(404).json({ message: "Tokens not found for the given project ID" }); + } + return res.status(200).json({ data: response }); + } catch (error) { + console.error("Error in getTokenByIdProjectHandler:", error); + return res.status(500).json({ message: "Internal server error while retrieving tokens" }); + } +}; + +export const getTokenProjectByUserHandler = async (req: Request, res: Response) => { + try { + const { id } = req.params; + const id_user = req.user.id; + const response = await projectTokenService.getTokenProjectByUser(id_user, id); + if (!response || response.length === 0) { + return res.status(404).json({ message: "Tokens not found for the user and project" }); + } + return res.status(200).json({ data: response }); + } catch (error) { + console.error("Error in getTokenProjectByUserHandler:", error); + return res.status(500).json({ message: "Internal server error while retrieving tokens for the user and project" }); + } +}; + +export const getTotalTokenUsageByIdProjectHandler = async (req: Request, res: Response) => { + try { + const { id } = req.params; + const response = await projectTokenService.getTotalTokenTerbeliByIdProject(id); + if (!response || response.length === 0) { + return res.status(404).json({ message: "Tokens not found for the user and project" }); + } + return res.status(200).json({ data: response }); + } catch (error) { + console.error("Error in getTokenProjectByUserHandler:", error); + return res.status(500).json({ message: "Internal server error while retrieving tokens for the user and project" }); + } +}; + +export const buyTokenProjectHandler = async (req: Request, res: Response) => { + try { + const id_user = req.user.id; + const { id_projek, jumlah_token } = req.body; + const buyToken = await projectTokenService.buyTokenProject(id_user, id_projek, jumlah_token); + return res.status(201).json({ message: "Token purchased successfully", data: buyToken }); + } catch (error: any) { + if (error.message.includes("Available tokens not found")) { + return res.status(404).json({ message: "No available tokens found for this project" }); + } else if (error.message.includes("User not found")) { + return res.status(404).json({ message: "User not found" }); + } else if (error.message.includes("Project not found")) { + return res.status(404).json({ message: "Project not found" }); + } else if (error.message.includes("Insufficient tokens available for purchase")) { + return res.status(400).json({ message: "Insufficient tokens available for purchase" }); + } else if (error.message.includes("Insufficient balance")) { + return res.status(400).json({ message: "Insufficient balance in user's wallet" }); + } + console.error("Error in buyTokenProjectHandler:", error); + return res.status(500).json({ message: "Internal server error during token purchase" }); + } +}; + +export const getAllTokenHandler = async (req: Request, res: Response) => { + try { + const { search, ...filter } = req.query; + const response = await projectTokenService.getAllToken(); + if (!response || response.length === 0) { + return res.status(404).json({ message: "No tokens found" }); + } + return res.status(200).json({ data: response }); + } catch (error) { + console.error("Error in getAllTokenHandler:", error); + return res.status(500).json({ message: "Internal server error while retrieving all tokens" }); + } +}; + +export const getTotalTokenHandler = async (req: Request, res: Response) => { + try { + const response = await projectTokenService.getTotalToken(); + if (!response || response === "0") { + return res.status(404).json({ message: "Total token not found" }); + } + return res.status(200).json({ data: response }); + } catch (error) { + console.error("Error in getTotalTokenHandler:", error); + return res.status(500).json({ message: "Internal server error while retrieving total tokens" }); + } +}; + +export const tokenUsageDetailsByIdUserHandler = async (req: Request, res: Response) => { + try { + const { id } = req.user; + const { search, ...filter } = req.query; + const response = await projectTokenService.tokenUsageDetailsByIdUser(id); + if (!response || response.length === 0) { + return res.status(404).json({ message: "No token usage details found for the user" }); + } + return res.status(200).json({ data: response }); + } catch (error) { + console.error("Error in tokenUsageDetailsByIdUserHandler:", error); + return res.status(500).json({ message: "Internal server error while retrieving token usage details" }); + } +}; + +export const getTotalTokenRupiahByUserHandler = async (req: Request, res: Response) => { + try { + const { id } = req.user; + const response = await projectTokenService.getTotalTokenRupiahByUser(id); + if (!response) { + return res.status(404).json({ message: "Total token rupiah not found" }); + } + return res.status(200).json({ data: response }); + } catch (error) { + console.error("Error in getTotalTokenRupiahByUserHandler:", error); + return res.status(500).json({ message: "Internal server error while retrieving total token rupiah" }); + } +} \ No newline at end of file diff --git a/services/backend/src/controllers/project-wallet.ts b/services/backend/src/controllers/project-wallet.ts new file mode 100644 index 0000000..f476913 --- /dev/null +++ b/services/backend/src/controllers/project-wallet.ts @@ -0,0 +1,42 @@ +import { Request, Response } from "express"; +import * as projectWalletService from "../services/project-wallet.js"; + +export const getProjectWalletHandler = async (req: Request, res: Response) => { + try { + const { search } = req.query; + const wallet = await projectWalletService.getAllProjectWallet(search as string); + return res.status(200).json({ data: wallet }); + } catch (error: any) { + if (error.statusCode) { + return res.status(error.statusCode).json({ message: error.message }); + } else { + return res.status(500).json({ message: "Internal server error" }); + } + } +}; + +export const getProjectWalletByProjectIdHandler = async (req: Request, res: Response) => { + try { + const wallet = await projectWalletService.getProjectWalletByProjectId(req.params.id); + return res.status(200).json({ data: wallet }); + } catch (error: any) { + if (error.statusCode) { + res.status(error.statusCode).json({ message: error.message }); + } else { + res.status(500).json({ message: "Internal Server Error" }); + } + } +}; + +export const transferSaldoProjectHandler = async (req: Request, res: Response) => { + try { + await projectWalletService.transferSaldoProject(req.body); + return res.status(201).json({ message: "Balance transferred successfully" }); + } catch (error: any) { + if (error.statusCode) { + res.status(error.statusCode).json({ message: error.message }); + } else { + res.status(500).json({ message: "Internal Server Error" }); + } + } +}; diff --git a/services/backend/src/controllers/project.ts b/services/backend/src/controllers/project.ts new file mode 100644 index 0000000..d6b528a --- /dev/null +++ b/services/backend/src/controllers/project.ts @@ -0,0 +1,247 @@ +import { Request, Response } from "express"; +import * as projectService from "../services/project.js"; +import path from "path"; + +export const countProjectHandler = async (req: Request, res: Response) => { + try { + const response = await projectService.countProject(); + return res.status(200).json({ data: response }); + } catch (error) { + console.error(error); + return res.status(500).json({ message: "Failed to retrieve projects" }); + } +}; + +export const createProjectHandler = async (req: Request, res: Response) => { + try { + const project = await projectService.createProject(req.user.id, req.body); + return res.json(project); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Internal Server Error"; + res.status(statusCode).json({ message }); + } +}; + +export const getAllProjectHandler = async (req: Request, res: Response) => { + try { + const { search, ...filter } = req.query; + + const projects = await projectService.getAllProject(search as string | undefined, filter as { [key: string]: string | undefined }); + + return res.status(200).json({ data: projects }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Internal Server Error"; + res.status(statusCode).json({ message }); + } +}; + +export const getProjectByIdHandler = async (req: Request, res: Response) => { + try { + const { id } = req.params; + const response = await projectService.getProjectById(id); + if (!response) { + return res.status(404).json({ message: "Project not found" }); + } + return res.status(200).json({ data: response }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Internal Server Error"; + res.status(statusCode).json({ message }); + } +}; + +export const getProjectByUserIdHandler = async (req: Request, res: Response) => { + try { + const { search, ...filter } = req.query; + const response = await projectService.getProjectByUserId(req.user.id, search as string | undefined, filter as { [key: string]: string | undefined }); + if (!response || response.length === 0) { + return res.status(404).json({ message: "Project not found" }); + } + return res.status(200).json({ data: response }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Internal Server Error"; + res.status(statusCode).json({ message }); + } +}; + +export const getUserHaveTokenInProjectHandler = async (req: Request, res: Response) => { + try { + const { id } = req.params; + const response = await projectService.getUserHaveTokenInProject(id); + + return res.status(200).json({ data: response }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Internal Server Error"; + res.status(statusCode).json({ message }); + } +}; + +export const acceptProjectHandler = async (req: Request, res: Response) => { + try { + const { id } = req.body; + await projectService.acceptProjectById(id); + return res.status(200).json({ message: "Project accepted" }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Internal Server Error"; + res.status(statusCode).json({ message }); + } +}; + +export const rejectProjectHandler = async (req: Request, res: Response) => { + try { + const { id } = req.params; + const { keterangan } = req.body; + await projectService.rejectProjectById(id, keterangan); + return res.status(200).json({ message: "Project rejected" }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Internal Server Error"; + res.status(statusCode).json({ message }); + } +}; + +export const reviseProjectHandler = async (req: Request, res: Response) => { + try { + const { id } = req.params; + const { keterangan } = req.body; + await projectService.reviseProjectById(id, keterangan); + return res.status(200).json({ message: "Project revised" }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Internal Server Error"; + res.status(statusCode).json({ message }); + } +}; + +export const getKeteranganReviseProjectByIdHandler = async (req: Request, res: Response) => { + try { + const { id } = req.params; + const data = await projectService.getKeteranganReviseProjectById(id); + return res.status(200).json({ data }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Internal Server Error"; + res.status(statusCode).json({ message }); + } +}; + +export const updateProjectHandler = async (req: Request, res: Response) => { + try { + const project = await projectService.updateProjectById(req.user.id, req.body); + return res.status(201).json(project); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Internal Server Error"; + res.status(statusCode).json({ message }); + } +}; + +export const approveProjectHandler = async (req: Request, res: Response) => { + try { + const { id } = req.params; + const detail = req.body; + await projectService.approveProjectById(id, detail); + return res.status(200).json({ message: "Project approved" }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Internal Server Error"; + res.status(statusCode).json({ message }); + } +}; + +export const deleteProjectHandler = async (req: Request, res: Response) => { + try { + const { id } = req.params; + await projectService.deleteProjectById(id); + return res.status(200).json({ message: "Project deleted" }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Internal Server Error"; + res.status(statusCode).json({ message }); + } +}; + +export const publishProjectHandler = async (req: Request, res: Response) => { + try { + await projectService.publishProjectById(req.body); + console.log(req.body); + return res.status(200).json({ message: "Project published" }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Internal Server Error"; + res.status(statusCode).json({ message }); + } +}; + +export const checkProjectFundingOpenedHandler = async (req: Request, res: Response) => { + try { + const response = await projectService.checkProjectFundingOpened(); + return res.status(200).json({ data: response }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Internal Server Error"; + res.status(statusCode).json({ message }); + } +}; + +export const completingProjectHandler = async (req: Request, res: Response) => { + try { + const { id } = req.body; + const project = await projectService.completingProjectById(id); + return res.status(200).json({ message: "Project completed" }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Internal Server Error"; + res.status(statusCode).json({ message }); + } +}; + +export const totalProfitHandler = async (req: Request, res: Response) => { + try { + const { id } = req.params; + const totalProfit = await projectService.totalProfit(id); + if (!totalProfit) { + return res.status(404).json({ message: "Total profit not found" }); + } + return res.status(200).json({ total: totalProfit }); + } catch (error) { + console.error(error); + return res.status(500).json({ message: "Failed to retrieve total profit" }); + } +}; + +export const shareProfitHandler = async (req: Request, res: Response) => { + try { + const { id } = req.body; + const project = await projectService.shareProfit(id); + return res.status(200).json(project); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Internal Server Error"; + res.status(statusCode).json({ message }); + } +}; + +export const getDokumenProspektusByIdHandler = async (req: Request, res: Response) => { + try { + const { id } = req.params; + const dokumenPath = await projectService.getDokumenProspektusById(id); + + if (!dokumenPath) { + return res.status(404).json({ message: "Dokumen prospektus not found" }); + } + + const filePath = path.resolve(__dirname, "../../assets/", dokumenPath); + + return res.sendFile(filePath); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Internal Server Error"; + return res.status(statusCode).json({ message }); + } +}; diff --git a/services/backend/src/controllers/topup.ts b/services/backend/src/controllers/topup.ts new file mode 100644 index 0000000..243f8b2 --- /dev/null +++ b/services/backend/src/controllers/topup.ts @@ -0,0 +1,272 @@ +import { + accSimpananWajib, + accTopup, + accWithdrawSaldo, + getAllTopUp, + getWithdrawSaldo, + payMember, + paySimpananWajib, + payTopup, + withdrawSaldo, + getTopupById, + getBagianPemilikPelaksana, + payBagianPemilikPelaksana, + getTotalWalletSimpananPokokByUserId, + getTotalWalletSimpananWajibByUserId, + getKasKoperasi, + formatGetTopupByUserId, + formatGetAllTopup, +} from "../services/topup.js"; +import { Request, Response } from "express"; +import axios from "axios"; +import { walletServiceUrl } from "../main.js"; +import { getWalletSaldoByUserId, updateWalletById } from "../services/wallet.js"; + +export const getAllTopupHandler = async (req: Request, res: Response) => { + try { + const { search } = req.query; + const topups = await formatGetAllTopup(search as string | undefined); + return res.status(200).json({ message: "Topups found", data: topups }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Failed to retrieve topups"; + res.status(statusCode).json({ message }); + } +}; + +export const getTopupByIdHandler = async (req: Request, res: Response) => { + try { + const { id } = req.params; + const topup = await getTopupById(id); + return res.status(200).json({ message: "Topup found", data: topup }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Failed to retrieve topup"; + res.status(statusCode).json({ message }); + } +}; + +export const getTopupByUserIdHandler = async (req: Request, res: Response) => { + try { + const userId = req.user.id; + const data = await formatGetTopupByUserId(userId); + + return res.status(200).json({ + message: "Topups found", + data, + }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "An error occurred while fetching topups"; + return res.status(statusCode).json({ message }); + } +}; + +export const getTotalWalletSimpananPokokByUserIdHandler = async (req: Request, res: Response) => { + try { + const userId = req.user.id; + + const total = await getTotalWalletSimpananPokokByUserId(userId); + + if (total === 0) { + return res.status(404).json({ message: "No Wallet simpanan pokok found for the user" }); + } + return res.status(200).json({ message: "Total wallet simpanan pokok found", total }); + } catch (error: any) { + console.error("Error calculating total Wallet simpanan pokok:", error); + const statusCode = error.statusCode || 500; + const message = error.message || "An error occurred while calculating total simpanan pokok"; + res.status(statusCode).json({ message }); + } +}; + +export const getTotalWalletSimpananWajibByUserIdHandler = async (req: Request, res: Response) => { + try { + const userId = req.user.id; + const total = await getTotalWalletSimpananWajibByUserId(userId); + + if (total === 0) { + return res.status(404).json({ message: "No Wallet simpanan wajib found for the user" }); + } + + return res.status(200).json({ message: "Total Wallet simpanan wajib found", total }); + } catch (error: any) { + console.error("Error calculating total Wallet simpanan wajib:", error); + const statusCode = error.statusCode || 500; + const message = error.message || "An error occurred while calculating total simpanan wajib"; + res.status(statusCode).json({ message }); + } +}; + +export const getTotalWalletSaldoByUserIdHandler = async (req: Request, res: Response) => { + try { + const response = await axios.get(`${walletServiceUrl}/wallet/total/${req.user.id}`); + return res.status(200).json(response.data); + } catch (error: any) { + console.error("Error calculating total Wallet saldo:", error); + + if (error.response && error.response.status === 404) { + return res.status(404).json({ message: "Wallet saldo not found" }); + } + + const statusCode = error.response?.status || 500; + const message = error.message || "An error occurred while calculating total Wallet saldo"; + return res.status(statusCode).json({ message }); + } +}; + +export const getWalletSaldoByUserIdHandler = async (req: Request, res: Response) => { + try { + const response = await getWalletSaldoByUserId(req.params.id); + return res.status(200).json(response.data); + } catch (error: any) { + console.error("Error get Wallet saldo:", error); + + if (error.response && error.response.status === 404) { + return res.status(404).json({ message: "Wallet saldo not found" }); + } + + const statusCode = error.response?.status || 500; + const message = error.message || "An error occurred while calculating Wallet saldo"; + return res.status(statusCode).json({ message }); + } +}; + +export const getKasKoperasiHandler = async (req: Request, res: Response) => { + try { + const total = await getKasKoperasi(); + if (total === 0) { + return res.status(404).json({ message: "No Wallet saldo found for the user" }); + } + + return res.status(200).json({ message: "Kas Koperasi found", total }); + } catch (error: any) { + console.error("Error calculating total Wallet saldo:", error); + const statusCode = error.statusCode || 500; + const message = error.message || "An error occurred while calculating total Wallet saldo"; + res.status(statusCode).json({ message }); + } +}; + +export const getWithdrawSaldoHandler = async (req: Request, res: Response) => { + try { + const topup = await getWithdrawSaldo(); + return res.status(200).json({ message: "Withdraws found", data: topup }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Failed to retrieve withdraws"; + res.status(statusCode).json({ message }); + } +}; + +export const payMemberHandler = async (req: Request, res: Response) => { + try { + await payMember(req.user.id, req.body); + return res.status(201).json({ message: "Topup created, awaiting payment confirmation" }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Failed to create topup"; + res.status(statusCode).json({ message }); + } +}; + +export const payTopupHandler = async (req: Request, res: Response) => { + try { + await payTopup(req.user.id, req.body); + return res.status(201).json({ message: "Topup created, awaiting payment confirmation" }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Failed to create topup"; + res.status(statusCode).json({ message }); + } +}; + +export const accTopupHandler = async (req: Request, res: Response) => { + try { + await accTopup(req.body.id); + return res.status(200).json({ message: "Topup has been approved" }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Failed to approve topup"; + res.status(statusCode).json({ message }); + } +}; + +export const withdrawSaldoHandler = async (req: Request, res: Response) => { + try { + await withdrawSaldo(req.user.id, req.body); + return res.status(201).json({ message: "Withdraw request created, awaiting approval" }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Failed to create withdraw request"; + res.status(statusCode).json({ message }); + } +}; + +export const accWithdrawSaldoHandler = async (req: Request, res: Response) => { + try { + await accWithdrawSaldo(req.body.id, req.body); + return res.status(200).json({ message: "Withdraw request has been approved" }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Failed to approve withdraw request"; + res.status(statusCode).json({ message }); + } +}; + +export const paySimpananWajibHandler = async (req: Request, res: Response) => { + try { + await paySimpananWajib(req.user.id, req.body); + return res.status(201).json({ message: "Simpanan Wajib created, awaiting payment confirmation" }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Failed to create simpanan wajib"; + res.status(statusCode).json({ message }); + } +}; + +export const accSimpananWajibHandler = async (req: Request, res: Response) => { + try { + await accSimpananWajib(req.body.id); + return res.status(200).json({ message: "Simpanan Wajib has been approved" }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Failed to approve simpanan wajib"; + res.status(statusCode).json({ message }); + } +}; + +export const getBagianPemilikPelaksanaHandler = async (req: Request, res: Response) => { + try { + const { search } = req.query; + const topup = await getBagianPemilikPelaksana(search as string | undefined); + return res.status(200).json({ message: "Topups found", data: topup }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Failed to retrieve topups"; + res.status(statusCode).json({ message }); + } +}; + +export const payBagianPemilikPelaksanaHandler = async (req: Request, res: Response) => { + try { + await payBagianPemilikPelaksana(req.body); + return res.status(201).json({ message: "Pembayaran pemilik / pelaksana telah dikonfirmasi" }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Failed to confirm payment"; + res.status(statusCode).json({ message }); + } +}; +export const updateWalletByIdHandler = async (req: Request, res: Response) => { + try { + const { id } = req.params; + const wallet = req.body; + await updateWalletById(wallet, id); + return res.status(200).json({ message: "Wallet updated" }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Internal Server Error"; + res.status(statusCode).json({ message }); + } +}; \ No newline at end of file diff --git a/services/backend/src/controllers/transaction.ts b/services/backend/src/controllers/transaction.ts new file mode 100644 index 0000000..e9af7f6 --- /dev/null +++ b/services/backend/src/controllers/transaction.ts @@ -0,0 +1,57 @@ +import { Request, Response } from "express"; +import * as transactionService from "../services/transaction.js"; + +export async function getAllTransactionHandler(req: Request, res: Response) { + try { + const { search } = req.query; + + const transactions = await transactionService.getAllTransaction(search as string); + + if (!transactions.length) { + return res.status(404).json({ + message: "No transactions found", + }); + } + + return res.status(200).json({ + data: transactions, + }); + } catch (error) { + console.error("Error in getAllTransactionHandler:", error); + return res.status(500).json({ message: "Failed to retrieve transactions" }); + } +} + +export const getTransactionByUserIdHandler = async (req: Request, res: Response) => { + try { + const transaction = await transactionService.getTransactionByUserId(req.user.id); + + if (!transaction.length) { + return res.status(404).json({ message: "No transactions found for this user" }); + } + + return res.status(200).json({ data: transaction }); + } catch (error: any) { + console.error("Error in getTransactionByUserIdHandler:", error); + const statusCode = error.statusCode || 500; + const message = error.message || "Failed to retrieve transactions"; + return res.status(statusCode).json({ message }); + } +}; + +export const getTransactionByProjectIdHandler = async (req: Request, res: Response) => { + try { + const transaction = await transactionService.getTransactionByProjectId(req.params.id); + + if (!transaction.length) { + return res.status(404).json({ message: "No transactions found for this project" }); + } + + return res.status(200).json({ data: transaction }); + } catch (error: any) { + console.error("Error in getTransactionByProjectIdHandler:", error); + const statusCode = error.statusCode || 500; + const message = error.message || "Failed to retrieve transactions"; + return res.status(statusCode).json({ message }); + } +}; diff --git a/services/backend/src/controllers/user.ts b/services/backend/src/controllers/user.ts new file mode 100644 index 0000000..6c1c81a --- /dev/null +++ b/services/backend/src/controllers/user.ts @@ -0,0 +1,148 @@ +import { Request, Response } from "express"; +import * as userService from "../services/user.js"; + +export const countUserHandler = async (req: Request, res: Response) => { + try { + const response = await userService.countUser(); + return res.status(200).json({ data: response }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Internal Server Error"; + res.status(statusCode).json({ message }); + } +}; + +export const getAllUserHandler = async (req: Request, res: Response) => { + try { + const { search, ...filter } = req.query; + const response = await userService.getAllUser(search as string | undefined, filter as { [key: string]: string | undefined }); + return res.status(200).json({ data: response }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Internal Server Error"; + res.status(statusCode).json({ message }); + } +}; + +export const getUserByIdHandler = async (req: Request, res: Response) => { + try { + const { id } = req.params; + const response = await userService.getUserById(id); + return res.status(200).json({ data: response }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Internal Server Error"; + res.status(statusCode).json({ message }); + } +}; + +export const updateUserByIdHandler = async (req: Request, res: Response) => { + try { + const userId = req.params.id; + const user = req.user; + + if (user.role === "ADMIN" || user.id === userId) { + await userService.updateUserById(req.body, userId); + return res.status(200).json({ message: "User updated successfully" }); + } else { + return res.status(403).json({ message: "You are not authorized to update this user" }); + } + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Internal Server Error"; + res.status(statusCode).json({ message }); + } +}; + +export const deleteUserByIdHandler = async (req: Request, res: Response) => { + try { + const { id } = req.params; + await userService.deleteUserById(id); + return res.status(200).json({ message: "User deleted successfully" }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Internal Server Error"; + res.status(statusCode).json({ message }); + } +}; + +export const rejectUserByIdHandler = async (req: Request, res: Response) => { + try { + const { id } = req.params; + const { message } = req.body; + await userService.rejectUserById(id, message); + return res.status(200).json({ message: "User rejected successfully" }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Internal Server Error"; + res.status(statusCode).json({ message }); + } +}; + +export const sendOtpHandler = async (req: Request, res: Response) => { + try { + const { id } = req.body; + await userService.sendOtp(id); + return res.status(200).json({ message: "OTP sent successfully" }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Internal Server Error"; + res.status(statusCode).json({ message }); + } +}; + +export const verifyOtpHandler = async (req: Request, res: Response) => { + try { + const id = req.user.id; + const { otp } = req.body; + await userService.verifyOtp(id, otp); + return res.status(200).json({ message: "OTP verified successfully" }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Internal Server Error"; + res.status(statusCode).json({ message }); + } +}; + +export const getPhoneNumberAdminHandler = async (req: Request, res: Response) => { + try { + const response = await userService.getPhoneNumberAdmin(); + return res.status(200).json({ + data: response.no_hp, + }); + } catch (error: any) { + console.error("Error in getPhoneNumberAdminHandler:", error); + const statusCode = error.statusCode || 500; + return res.status(statusCode).json({ + message: error.message || "Internal server error", + }); + } +}; + +export const upgradeUserToPlatinumHandler = async (req: Request, res: Response) => { + try { + const filePath = req.body.bukti_pembayaran; + + await userService.upgradeUserToPlatinum(req.user.id, req.body.nominal, req.body, filePath); + + return res.status(201).json({ + message: "User upgrade request created, awaiting approval" + }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Internal Server Error"; + res.status(statusCode).json({ message }); + } +}; + + +export const accUpgradeUserToPlatinumHandler = async (req: Request, res: Response) => { + try { + await userService.accUpgradeUserToPlatinum(req.body.id); + return res.status(200).json({ message: "User upgrade request approved" }); + } catch (error: any) { + const statusCode = error.statusCode || 500; + const message = error.message || "Internal Server Error"; + res.status(statusCode).json({ message }); + } +}; diff --git a/services/backend/src/controllers/wilayah.ts b/services/backend/src/controllers/wilayah.ts new file mode 100644 index 0000000..c20b82f --- /dev/null +++ b/services/backend/src/controllers/wilayah.ts @@ -0,0 +1,84 @@ +import { Request, Response } from "express"; + +export const getProvincesHandler = async (req: Request, res: Response) => { + try { + const IdnArea = await import("idn-area-data"); + const provinces = await IdnArea.getProvinces(); + return res.status(200).json(provinces); + } catch (error) { + console.error(error); + return res.status(500).json({ message: "Error retrieving provinces" }); + } +}; + +export const getRegenciesHandler = async (req: Request, res: Response) => { + try { + const IdnArea = await import("idn-area-data"); + const { province_code } = req.params; + if (!province_code) { + return res.status(400).json({ message: "Province code is required" }); + } + + const allRegencies = await IdnArea.getRegencies(); + const filteredRegencies = allRegencies.filter( + (regency: any) => regency.province_code === province_code + ); + + if (filteredRegencies.length === 0) { + return res.status(404).json({ message: "No regencies found for this province code" }); + } + + return res.status(200).json(filteredRegencies); + } catch (error) { + console.error(error); + return res.status(500).json({ message: "Error retrieving regencies" }); + } +}; + +export const getDistrictHandler = async (req: Request, res: Response) => { + try { + const IdnArea = await import("idn-area-data"); + const { regency_code } = req.params; + if (!regency_code) { + return res.status(400).json({ message: "Regency code is required" }); + } + + const allDistricts = await IdnArea.getDistricts(); + const filteredDistrict = allDistricts.filter( + (district: any) => district.regency_code === regency_code + ); + + if (filteredDistrict.length === 0) { + return res.status(404).json({ message: "No districts found for this regency code" }); + } + + return res.status(200).json(filteredDistrict); + } catch (error) { + console.error(error); + return res.status(500).json({ message: "Error retrieving districts" }); + } +}; + +export const getVillagesHandler = async (req: Request, res: Response) => { + try { + const IdnArea = await import("idn-area-data"); + const { district_code } = req.params; + if (!district_code) { + return res.status(400).json({ message: "District code is required" }); + } + + const allVillages = await IdnArea.getVillages(); + const filteredVillages = allVillages.filter( + (village: any) => village.district_code === district_code + ); + + if (filteredVillages.length === 0) { + return res.status(404).json({ message: "No villages found for this district code" }); + } + + return res.status(200).json(filteredVillages); + } catch (error) { + console.error(error); + return res.status(500).json({ message: "Error retrieving villages" }); + } +}; diff --git a/services/backend/src/drizzle/db.ts b/services/backend/src/drizzle/db.ts new file mode 100644 index 0000000..4a05905 --- /dev/null +++ b/services/backend/src/drizzle/db.ts @@ -0,0 +1,15 @@ +// src/config/db.ts +import { drizzle } from 'drizzle-orm/postgres-js'; +import postgres from 'postgres'; +import 'dotenv/config'; + +// Konfigurasi koneksi ke database +export const connection = postgres({ + host: process.env.DB_HOST, + port: parseInt(process.env.DB_PORT || '5432'), + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, +}); + +export const db = drizzle(connection); \ No newline at end of file diff --git a/services/backend/src/drizzle/migrate.ts b/services/backend/src/drizzle/migrate.ts new file mode 100644 index 0000000..a698081 --- /dev/null +++ b/services/backend/src/drizzle/migrate.ts @@ -0,0 +1,16 @@ +import "dotenv/config" +import { drizzle } from 'drizzle-orm/postgres-js'; +import { migrate } from 'drizzle-orm/postgres-js/migrator'; +import postgres from 'postgres'; + +const migrationClient = postgres(process.env.DATABASE_URL as string, { max: 1 }); + +async function main() { + await migrate(drizzle(migrationClient), { + migrationsFolder: './src/drizzle/migrations', + }) + + await migrationClient.end(); +} + +main() \ No newline at end of file diff --git a/services/backend/src/drizzle/schema.ts b/services/backend/src/drizzle/schema.ts new file mode 100644 index 0000000..8d92c13 --- /dev/null +++ b/services/backend/src/drizzle/schema.ts @@ -0,0 +1,270 @@ +import { relations } from "drizzle-orm"; +import { pgTable, varchar, uuid, integer, pgEnum, timestamp, text, date } from "drizzle-orm/pg-core"; + +export const UserRole = pgEnum("user_role", ["ADMIN", "BASIC", "PLATINUM"]); +export const UserStatus = pgEnum("user_status", ["AKTIF", "TIDAK AKTIF", "DITOLAK", "MENUNGGU KONFIRMASI", "OTP TERKIRIM"]); +export const ReportProgress = pgEnum("report_progress", ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]); +export const StatusProject = pgEnum("status_project", ["DRAFT", "PROSES VERIFIKASI", "REVISI", "APPROVAL", "TTD KONTRAK", "PENDANAAN DIBUKA", "BERJALAN", "DIBATALKAN", "DITOLAK", "SELESAI", "BERJALAN SIKLUS 2", "BERJALAN SIKLUS 3", "BERJALAN SIKLUS 4", "BERJALAN SIKLUS 5", "BERJALAN SIKLUS 6", "BERJALAN SIKLUS 7", "BERJALAN SIKLUS 8", "BERJALAN SIKLUS 9", "BERJALAN SIKLUS 10", "BERJALAN SIKLUS 11", "BERJALAN SIKLUS 12", "BERJALAN SIKLUS 13", "BERJALAN SIKLUS 14", "BERJALAN SIKLUS 15", "BERJALAN SIKLUS 16", "BERJALAN SIKLUS 17", "BERJALAN SIKLUS 18", "BERJALAN SIKLUS 19", "BERJALAN SIKLUS 20"]); +export const StatusHistoryProject = pgEnum("status_history", ["SUCCESS", "FAILED", "PENDING"]); +export const JenisWallet = pgEnum("jenis_wallet", ["SIMPANAN WAJIB", "SIMPANAN POKOK", "SALDO", "KAS KOPERASI"]); +export const JenisTopup = pgEnum("jenis_topup", ["SIMPANAN WAJIB", "SIMPANAN POKOK", "TOPUP SALDO", "PENARIKAN SALDO", "UPGRADE USER", "PELAKSANA", "PEMILIK"]); +export const JenisLaporan = pgEnum("jenis_laporan", ["UNTUNG", "RUGI"]); +export const StatusPembayaran = pgEnum("status", ["SUKSES", "GAGAL", "MENUNGGU KONFIRMASI"]); + +export const UserTable = pgTable("user", { + id: uuid("id").primaryKey().defaultRandom(), + nama: varchar("nama", { length: 255 }).notNull(), + no_hp: varchar("no_hp").notNull().unique(), + role: UserRole("user_role"), + status: UserStatus("user_status").default("TIDAK AKTIF").notNull(), + password: varchar("password").notNull(), + tempat_lahir: varchar("tempat_lahir").notNull(), + tanggal_lahir: date("tanggal_lahir").notNull(), + provinsi: varchar("provinsi").notNull(), + kota: varchar("kota").notNull(), + kecamatan: varchar("kecamatan").notNull(), + alamat: varchar("alamat").notNull(), + nik: varchar("nik").notNull(), + foto_profile: text("foto_profile"), + foto_diri: text("foto_diri").notNull(), + foto_ktp: text("foto_ktp").notNull(), + created_at: timestamp("created_at").notNull().defaultNow(), + updated_at: timestamp("updated_at"), + otp: varchar("otp"), +}); + +export const ProjectCategoryTable = pgTable("project_category", { + id: uuid("id").primaryKey().defaultRandom(), + kategori: varchar("kategori", { length: 255 }).notNull(), + created_at: timestamp("created_at").notNull().defaultNow(), + updated_at: timestamp("updated_at"), +}); + +export const ProjectTable = pgTable("project", { + id: uuid("id").primaryKey().defaultRandom(), + id_user: uuid("id_user") + .notNull() + .references(() => UserTable.id), + id_kategori: uuid("id_kategori") + .notNull() + .references(() => ProjectCategoryTable.id), + judul: varchar("judul", { length: 255 }).notNull(), + deskripsi: text("deskripsi").notNull(), + nominal: integer("nominal").notNull(), + asset_jaminan: varchar("asset_jaminan").notNull(), + nilai_jaminan: integer("nilai_jaminan").notNull(), + lokasi_usaha: varchar("lokasi_usaha").notNull(), + detail_lokasi: text("detail_lokasi").notNull(), + brosur_produk: text("brosur_produk"), + pendapatan_perbulan: integer("pendapatan_perbulan").notNull(), + pengeluaran_perbulan: integer("pengeluaran_perbulan").notNull(), + report_progress: ReportProgress("report_progress"), + dokumen_proyeksi: text("dokumen_proyeksi").notNull(), + status: StatusProject("status_project").notNull(), + nominal_disetujui: integer("nominal_disetujui"), + harga_per_unit: integer("harga_per_unit"), + jumlah_koin: integer("jumlah_koin"), + minimal_pembelian: integer("minimal_pembelian"), + maksimal_pembelian: integer("maksimal_pembelian"), + mulai_penggalangan_dana: date("mulai_penggalangan_dana"), + selesai_penggalangan_dana: date("selesai_penggalangan_dana"), + dokumen_prospektus: text("dokumen_prospektus"), + limit_siklus: integer("limit_siklus"), + created_at: timestamp("created_at").notNull().defaultNow(), + updated_at: timestamp("updated_at"), +}); + +export const SupportDocumentTable = pgTable("support_document", { + id: uuid("id").primaryKey().defaultRandom(), + id_projek: uuid("id_projek") + .notNull() + .references(() => ProjectTable.id), + dokumen: text("dokumen").notNull(), + created_at: timestamp("created_at").notNull().defaultNow(), + updated_at: timestamp("updated_at"), +}); + +export const ProjectReportTable = pgTable("project_report", { + id: uuid("id").primaryKey().defaultRandom(), + id_projek: uuid("id_projek") + .notNull() + .references(() => ProjectTable.id), + judul: varchar("judul", { length: 255 }).notNull(), + jenis_laporan: JenisLaporan("jenis_laporan").notNull(), + modal: integer("modal").notNull(), + nominal: integer("nominal").notNull(), + // laporan: text("laporan").notNull(), + created_at: timestamp("created_at").defaultNow(), + updated_at: timestamp("updated_at"), +}); + +export const ProjectMutationReportTable = pgTable("project_mutation_report", { + id: uuid("id").primaryKey().defaultRandom(), + id_projek: uuid("id_projek") + .notNull() + .references(() => ProjectTable.id), + judul: varchar("judul", { length: 255 }).notNull(), + pemasukan: integer("pemasukan"), + pengeluaran: integer("pengeluaran"), + laporan: text("laporan").notNull(), + created_at: timestamp("created_at").notNull().defaultNow(), + updated_at: timestamp("updated_at"), +}); + +export const HistoryProjectTable = pgTable("history_project", { + id: uuid("id").primaryKey().defaultRandom(), + id_projek: uuid("id_projek") + .notNull() + .references(() => ProjectTable.id), + history: text("history").notNull(), + keterangan: text("keterangan"), + status: StatusHistoryProject("status_project").notNull(), + created_at: timestamp("created_at").notNull().defaultNow(), + updated_at: timestamp("updated_at"), +}); + +export const WalletTable = pgTable("wallet", { + id: uuid("id").primaryKey().defaultRandom(), + id_user: uuid("id_user") + .notNull() + .references(() => UserTable.id), + jenis_wallet: JenisWallet("jenis_wallet").notNull(), + saldo: integer("saldo").notNull().default(0), + created_at: timestamp("created_at").notNull().defaultNow(), + updated_at: timestamp("updated_at"), +}); + +export const TopupTable = pgTable("topup", { + id: uuid("id").primaryKey().defaultRandom(), + id_wallet: uuid("id_wallet") + .notNull() + .references(() => WalletTable.id), + nama: varchar("nama").notNull(), + nama_bank: varchar("nama_bank"), + no_rekening: varchar("no_rekening"), + nama_pemilik_rekening: varchar("nama_pemilik_rekening"), + nominal: integer("nominal").notNull(), + jenis: JenisTopup("jenis_topup").notNull(), + bukti_pembayaran: text("bukti_pembayaran"), + status: StatusPembayaran("status").default("MENUNGGU KONFIRMASI"), + created_at: timestamp("created_at").notNull().defaultNow(), + updated_at: timestamp("updated_at"), +}); + +export const SignatureAdminTable = pgTable("signature_admin", { + id: uuid("id").primaryKey().defaultRandom(), + id_user: uuid("id_user") + .notNull() + .references(() => UserTable.id), + signature: text("signature"), + created_at: timestamp("created_at").notNull().defaultNow(), +}); + +export const ChartProjectTable = pgTable("chart_project", { + id: uuid("id").primaryKey().defaultRandom(), + id_projek: uuid("id_projek") + .notNull() + .references(() => ProjectTable.id), + nominal: integer("nominal").notNull(), + created_at: timestamp("created_at").notNull().defaultNow(), + updated_at: timestamp("updated_at"), +}); + +export const ProjectWalletTable = pgTable("project_wallet", { + id: uuid("id").primaryKey().defaultRandom(), + id_projek: uuid("id_projek") + .notNull() + .references(() => ProjectTable.id), + dana_terkumpul: integer("dana_terkumpul").notNull().default(0), + saldo: integer("saldo").notNull().default(0), + created_at: timestamp("created_at").notNull().defaultNow(), + updated_at: timestamp("updated_at"), +}); + +export const HistoryProjectWalletTable = pgTable("history_project_wallet", { + id: uuid("id").primaryKey().defaultRandom(), + id_project_wallet: uuid("id_project_wallet") + .notNull() + .references(() => ProjectWalletTable.id), + nominal: integer("nominal").notNull(), + dana_tersisa: integer("dana_tersisa").notNull(), + deskripsi: text("deskripsi").notNull(), + bukti_transfer: text("bukti_transfer"), + created_at: timestamp("created_at").notNull().defaultNow(), +}); + +// Relation definition + +export const usersWalletRelations = relations(UserTable, ({ many }) => ({ + wallet: many(WalletTable), + project: many(ProjectTable), +})); + +export const walletUsersRelations = relations(WalletTable, ({ one }) => ({ + author: one(UserTable, { + fields: [WalletTable.id_user], + references: [UserTable.id], + }), +})); + +export const walletTopupRelations = relations(WalletTable, ({ many }) => ({ + topup: many(TopupTable), +})); + +export const topupWalletRelations = relations(TopupTable, ({ one }) => ({ + wallet: one(WalletTable, { + fields: [TopupTable.id_wallet], + references: [WalletTable.id], + }), +})); + +export const projectCategoryRelations = relations(ProjectCategoryTable, ({ many }) => ({ + project: many(ProjectTable), +})); + +export const projectRelations = relations(ProjectTable, ({ one, many }) => ({ + category: one(ProjectCategoryTable, { + fields: [ProjectTable.id_kategori], + references: [ProjectCategoryTable.id], + }), + document: many(SupportDocumentTable), + report: many(ProjectReportTable), + mutation: many(ProjectMutationReportTable), + history: many(HistoryProjectTable), + wallet: one(ProjectWalletTable), +})); + +export const supportDocumentRelations = relations(SupportDocumentTable, ({ one }) => ({ + project: one(ProjectTable, { + fields: [SupportDocumentTable.id_projek], + references: [ProjectTable.id], + }), +})); + +export const projectReportRelations = relations(ProjectReportTable, ({ one }) => ({ + project: one(ProjectTable, { + fields: [ProjectReportTable.id_projek], + references: [ProjectTable.id], + }), +})); + +export const projectMutationReportRelations = relations(ProjectMutationReportTable, ({ one }) => ({ + project: one(ProjectTable, { + fields: [ProjectMutationReportTable.id_projek], + references: [ProjectTable.id], + }), +})); + +export const historyProjectRelations = relations(HistoryProjectTable, ({ one }) => ({ + project: one(ProjectTable, { + fields: [HistoryProjectTable.id_projek], + references: [ProjectTable.id], + }), +})); + +export const chartProjectRelations = relations(ChartProjectTable, ({ one }) => ({ + project: one(ProjectTable, { + fields: [ChartProjectTable.id_projek], + references: [ProjectTable.id], + }), +})); \ No newline at end of file diff --git a/services/backend/src/drizzle/seed.ts b/services/backend/src/drizzle/seed.ts new file mode 100644 index 0000000..1df8ec9 --- /dev/null +++ b/services/backend/src/drizzle/seed.ts @@ -0,0 +1,142 @@ +import { db, connection } from "./db.js"; +import { + ProjectCategoryTable, + UserTable, + ProjectTable, + SupportDocumentTable, + TopupTable, + WalletTable, + ProjectMutationReportTable, + ChartProjectTable, + HistoryProjectTable, + ProjectReportTable, + ProjectWalletTable, + HistoryProjectWalletTable, + SignatureAdminTable, +} from "./schema.js"; +import { chartProjectData, chartProjectDataFactory } from "./seeder/chart-project.js"; +import { historyProjectData, historyProjectDataFactory } from "./seeder/history-project.js"; +import { HistoryProjectWalletData } from "./seeder/history-project-wallet.js"; +import { projectCategoryData, projectData, projectDataFactory, supportDocumentData, supportDocumentDataFactory } from "./seeder/project.js"; +import { projectMutationReportData, projectMutationReportDataFactory } from "./seeder/project-mutation.js"; +import { projectReportData, projectReportDataFactory } from "./seeder/project-report.js"; +import { ProjectWalletData } from "./seeder/project-wallet.js"; +import { topupData, topupDataFactory } from "./seeder/topup.js"; +import { userData, userDataFactory } from "./seeder/user.js"; +import { walletData, walletDataFactory } from "./seeder/wallet.js"; +import { SignatureAdminData } from "./seeder/signature-admin.js"; + +// Define a helper function for type-safe inserts +async function safeInsert(table: T, data: Partial[]) { + if (data.length === 0) return; + + try { + await db.insert(table as any).values(data as any); + } catch (error) { + console.error(`Error inserting into ${table.name}:`, error); + console.error("Problematic data:", JSON.stringify(data, null, 2)); + throw error; + } +} + +async function seed() { + try { + console.log("Menghapus semua data..."); + await db.delete(HistoryProjectWalletTable); + await db.delete(ProjectWalletTable); + await db.delete(SupportDocumentTable); + await db.delete(ChartProjectTable); + await db.delete(ProjectReportTable); + await db.delete(ProjectMutationReportTable); + await db.delete(HistoryProjectTable); + await db.delete(ProjectTable); + await db.delete(TopupTable); + await db.delete(WalletTable); + await db.delete(SignatureAdminTable); + await db.delete(UserTable); + await db.delete(ProjectCategoryTable); + + console.log("semua data berhasil dihapus 🗑️ 🗑️ 🗑️"); + + console.log("Memulai seeding..."); + + const userSeedData = await userData(); + // const userSeedFactory = await userDataFactory(10); + await safeInsert(UserTable, userSeedData); + // await safeInsert(UserTable, userSeedFactory); + + const userRecords = await db.select().from(UserTable); + const userIds = userRecords.map((row) => row.id); + + const projectCategorySeedData = await projectCategoryData(); + await safeInsert(ProjectCategoryTable, projectCategorySeedData); + + // const projectCategoryRecords = await db.select().from(ProjectCategoryTable); + // const projectCategoryIds = projectCategoryRecords.map((row) => row.id); + + // const projectSeedData = await projectData(userIds, projectCategoryIds); + // const projectSeedFactory = await projectDataFactory(userIds, projectCategoryIds, 10); + // await safeInsert(ProjectTable, projectSeedData); + // await safeInsert(ProjectTable, projectSeedFactory); + + // const projectRecords = await db.select().from(ProjectTable); + // const projectIds = projectRecords.map((row) => row.id); + + // const supportDocumentSeedData = await supportDocumentData(projectIds); + // const supportDocumentSeedFactory = await supportDocumentDataFactory(projectIds, 10); + // await safeInsert(SupportDocumentTable, supportDocumentSeedData); + // await safeInsert(SupportDocumentTable, supportDocumentSeedFactory); + + const walletSeedData = await walletData(userIds); + // const walletSeedFactory = await walletDataFactory(userIds, 10, walletSeedData.usedUserIds); + await safeInsert(WalletTable, walletSeedData.data); + // await safeInsert(WalletTable, walletSeedFactory); + + const walletRecords = await db.select().from(WalletTable); + const walletIds = walletRecords.map((row) => row.id); + + const topupSeedData = await topupData(walletIds); + // const topupSeedFactory = await topupDataFactory(walletIds, 10); + await safeInsert(TopupTable, topupSeedData); + // await safeInsert(TopupTable, topupSeedFactory); + + // const projectReportSeedData = await projectReportData(projectIds); + // const projectReportSeedFactory = await projectReportDataFactory(projectIds, 10); + // await safeInsert(ProjectReportTable, projectReportSeedData); + // await safeInsert(ProjectReportTable, projectReportSeedFactory); + + // const projectMutationReportSeedData = await projectMutationReportData(projectIds); + // const projectMutationReportSeedFactory = await projectMutationReportDataFactory(projectIds, 10); + // await safeInsert(ProjectMutationReportTable, projectMutationReportSeedData); + // await safeInsert(ProjectMutationReportTable, projectMutationReportSeedFactory); + + // const chartProjectSeedData = await chartProjectData(projectIds); + // const chartProjectSeedFactory = await chartProjectDataFactory(projectIds, 10); + // await safeInsert(ChartProjectTable, chartProjectSeedData); + // await safeInsert(ChartProjectTable, chartProjectSeedFactory); + + // const historyProjectSeedData = await historyProjectData(projectIds); + // const historyProjectSeedFactory = await historyProjectDataFactory(projectIds, 10); + // await safeInsert(HistoryProjectTable, historyProjectSeedData); + // await safeInsert(HistoryProjectTable, historyProjectSeedFactory); + + // const projectWalletSeedData = await ProjectWalletData(projectIds); + // await safeInsert(ProjectWalletTable, projectWalletSeedData.data); + + // const projectWalletRecords = await db.select().from(ProjectWalletTable); + // const projectWalletIds = projectWalletRecords.map((row) => row.id); + // const historyProjectWalletSeedData = await HistoryProjectWalletData(projectWalletIds); + // await safeInsert(HistoryProjectWalletTable, historyProjectWalletSeedData.data); + + const signatureAdminSeedData = await SignatureAdminData(userIds); + await safeInsert(SignatureAdminTable, signatureAdminSeedData); + + console.log("Seeding berhasil 🥳🥳🥳"); + } catch (error) { + console.error("Seeding gagal:", error); + } finally { + await connection.end(); + } +} + +seed(); \ No newline at end of file diff --git a/services/backend/src/drizzle/seeder/agreement-letter.ts b/services/backend/src/drizzle/seeder/agreement-letter.ts new file mode 100644 index 0000000..8a7b0a4 --- /dev/null +++ b/services/backend/src/drizzle/seeder/agreement-letter.ts @@ -0,0 +1,52 @@ +import { faker } from "@faker-js/faker/locale/id_ID"; +export const agreementLetterData = async (userIds: string[], projectIds: string[]) => [ + { + id: faker.string.uuid(), + id_projek: projectIds[0], + id_user: userIds[0], + nama_proyek: "Proyek peternakan naga", + nama_petugas: "Ali Murrofid", + alamat_petugas: "Ki. Ruecker no 9", + nama_pemilik_proyek: "alimurrofid", + nik: "3501234567890123", + no_hp: "6281234567890", + alamat: "Jl. Raya Tlogomas No. 246", + tanda_tangan: "https://coursius.com/storage/images/thumb/2021_04_08_12_24_32_b46a6b4bb45b6863c78df7e2524b48ae_900x450_thumb.jpg", + nominal_disetujui: 10000000, + created_at: new Date(), + }, + { + id: faker.string.uuid(), + id_projek: projectIds[1], + id_user: userIds[1], + nama_proyek: "Proyek Jual Beli T-Rex", + nama_petugas: "Ali Murrofid", + alamat_petugas: "Ki. Ruecker no 9", + nama_pemilik_proyek: "budi santoso", + nik: "3501234567890124", + no_hp: "6281234567891", + alamat: "Jl. Darmo No. 10", + tanda_tangan: "https://coursius.com/storage/images/thumb/2021_04_08_12_24_32_b46a6b4bb45b6863c78df7e2524b48ae_900x450_thumb.jpg", + nominal_disetujui: 20000000, + created_at: new Date(), + }, +]; + +export const agreementLetterDataFactory = async (userIds: string[], projectIds: string[], count: number) => { + const promises = Array.from({ length: count }).map(async () => ({ + id: faker.string.uuid(), + id_projek: faker.helpers.arrayElement(projectIds), + id_user: faker.helpers.arrayElement(userIds), + nama_proyek: faker.lorem.words(), + nama_petugas: "Ali Murrofid", + alamat_petugas: "Ki. Ruecker no 9", + nama_pemilik_proyek: faker.person.fullName(), + nik: faker.string.numeric({ length: 16 }), + no_hp: `62${faker.string.numeric(11)}`, + alamat: faker.location.streetAddress(), + tanda_tangan: faker.image.avatar(), + nominal_disetujui: parseInt(faker.string.numeric({ length: 8 })), + created_at: new Date(), + })); + return Promise.all(promises); +}; diff --git a/services/backend/src/drizzle/seeder/chart-project.ts b/services/backend/src/drizzle/seeder/chart-project.ts new file mode 100644 index 0000000..2fa31b3 --- /dev/null +++ b/services/backend/src/drizzle/seeder/chart-project.ts @@ -0,0 +1,49 @@ +import { faker } from "@faker-js/faker/locale/id_ID"; +export const chartProjectData = async (projectIds: string[]) => [ + { + id: faker.string.uuid(), + id_projek: projectIds[0], + nominal: 200000, + created_at: new Date("2023-12-10T13:39:24.000Z"), + }, + { + id: faker.string.uuid(), + id_projek: projectIds[0], + nominal: 1500000, + created_at: new Date("2024-03-10T13:39:24.000Z"), + }, + { + id: faker.string.uuid(), + id_projek: projectIds[0], + nominal: 2300000, + created_at: new Date("2024-06-10T13:39:24.000Z"), + }, + { + id: faker.string.uuid(), + id_projek: projectIds[1], + nominal: 400000, + created_at: new Date("2023-12-10T13:39:24.000Z"), + }, + { + id: faker.string.uuid(), + id_projek: projectIds[1], + nominal: 1100000, + created_at: new Date("2024-03-10T13:39:24.000Z"), + }, + { + id: faker.string.uuid(), + id_projek: projectIds[1], + nominal: -300000, + created_at: new Date("2024-06-10T13:39:24.000Z"), + }, +]; + +export const chartProjectDataFactory = async (projectIds: string[], count: number) => { + const promises = Array.from({ length: count }).map(async () => ({ + id: faker.string.uuid(), + id_projek: faker.helpers.arrayElement(projectIds), + nominal: parseInt(faker.string.numeric({ length: 6 })), + created_at: faker.date.past(), + })); + return Promise.all(promises); +}; diff --git a/services/backend/src/drizzle/seeder/chart-token.ts b/services/backend/src/drizzle/seeder/chart-token.ts new file mode 100644 index 0000000..f880206 --- /dev/null +++ b/services/backend/src/drizzle/seeder/chart-token.ts @@ -0,0 +1,55 @@ +import { faker } from "@faker-js/faker/locale/id_ID"; +export const chartTokenData = async (userIds: string[], projectIds: string[]) => [ + { + id: faker.string.uuid(), + id_user: userIds[0], + id_projek: projectIds[0], + nominal: 200000, + created_at: new Date("2023-12-10T13:39:24.000Z"), + }, + { + id: faker.string.uuid(), + id_user: userIds[0], + id_projek: projectIds[0], + nominal: 1500000, + created_at: new Date("2024-03-10T13:39:24.000Z"), + }, + { + id: faker.string.uuid(), + id_user: userIds[0], + id_projek: projectIds[0], + nominal: 2300000, + created_at: new Date("2024-06-10T13:39:24.000Z"), + }, + { + id: faker.string.uuid(), + id_user: userIds[0], + id_projek: projectIds[1], + nominal: 400000, + created_at: new Date("2023-12-10T13:39:24.000Z"), + }, + { + id: faker.string.uuid(), + id_user: userIds[0], + id_projek: projectIds[1], + nominal: 1100000, + created_at: new Date("2024-03-10T13:39:24.000Z"), + }, + { + id: faker.string.uuid(), + id_user: userIds[0], + id_projek: projectIds[1], + nominal: -300000, + created_at: new Date("2024-06-10T13:39:24.000Z"), + }, +]; +export const chartTokenDataFactory = async (userIds: string[], projectIds: string[], count: number) => { + const promises = Array.from({ length: count }).map(async () => ({ + id: faker.string.uuid(), + id_user: faker.helpers.arrayElement(userIds), + id_projek: faker.helpers.arrayElement(projectIds), + nominal: parseInt(faker.string.numeric({ length: 6 })), + created_at: new Date(), + })); + return Promise.all(promises); +}; diff --git a/services/backend/src/drizzle/seeder/history-project-wallet.ts b/services/backend/src/drizzle/seeder/history-project-wallet.ts new file mode 100644 index 0000000..c72dc6a --- /dev/null +++ b/services/backend/src/drizzle/seeder/history-project-wallet.ts @@ -0,0 +1,37 @@ +import { faker } from "@faker-js/faker/locale/id_ID"; + +export const HistoryProjectWalletData = async (projectIds: string[]) => { + const data = [ + { + id: faker.string.uuid(), + id_project_wallet: projectIds[0], + nominal: 100000, + dana_tersisa: 900000, + deskripsi: "Deskripsi", + bukti_transfer: "https://example.com/bukti-transfer.jpg", + created_at: new Date(), + }, + { + id: faker.string.uuid(), + id_project_wallet: projectIds[1], + nominal: 200000, + dana_tersisa: 700000, + deskripsi: "Deskripsi", + bukti_transfer: "https://example.com/bukti-transfer.jpg", + created_at: new Date(), + }, + { + id: faker.string.uuid(), + id_project_wallet: projectIds[2], + nominal: 300000, + dana_tersisa: 400000, + deskripsi: "Deskripsi", + bukti_transfer: "https://example.com/bukti-transfer.jpg", + created_at: new Date(), + }, + ]; + + const usedProjectWalletIds = new Set(data.map((item) => item.id_project_wallet)); + + return { data, usedProjectWalletIds }; +}; diff --git a/services/backend/src/drizzle/seeder/history-project.ts b/services/backend/src/drizzle/seeder/history-project.ts new file mode 100644 index 0000000..b8485ce --- /dev/null +++ b/services/backend/src/drizzle/seeder/history-project.ts @@ -0,0 +1,33 @@ +import { faker } from "@faker-js/faker/locale/id_ID"; +export const historyProjectData = async (projectIds: string[]) => [ + { + id: faker.string.uuid(), + id_projek: projectIds[0], + history: "Proses Approval dari Komitee Koperasi", + keterangan: + "Project disetujui oleh komitee dengan catatan sebagai berikut:
  • Nominal disetujui: Rp 10000000
  • Jumlah Unit: 10
  • Maks Pembelian: 10 atau Rp 10000000
", + status: "SUCCESS", + created_at: new Date(), + }, + { + id: faker.string.uuid(), + id_projek: projectIds[1], + history: "Proses Approval dari Komitee Koperasi", + keterangan: + "Project disetujui oleh komitee dengan catatan sebagai berikut:
  • Nominal disetujui: Rp 20000000
  • Jumlah Unit: 20
  • Maks Pembelian: 20 atau Rp 20000000
", + status: "SUCCESS", + created_at: new Date(), + }, +]; + +export const historyProjectDataFactory = async (projectIds: string[], count: number) => { + const promises = Array.from({ length: count }).map(async () => ({ + id: faker.string.uuid(), + id_projek: faker.helpers.arrayElement(projectIds), + history: faker.lorem.words(), + keterangan: faker.lorem.sentence(), + status: faker.helpers.arrayElement(["SUCCESS", "FAILED", "PENDING"]), + created_at: new Date(), + })); + return Promise.all(promises); +}; diff --git a/services/backend/src/drizzle/seeder/history-token.ts b/services/backend/src/drizzle/seeder/history-token.ts new file mode 100644 index 0000000..956baf8 --- /dev/null +++ b/services/backend/src/drizzle/seeder/history-token.ts @@ -0,0 +1,39 @@ +import { faker } from "@faker-js/faker/locale/id_ID"; +export const historyTokenData = async (chartTokenIds: string[]) => [ + { + id: faker.string.uuid(), + id_chart_token: chartTokenIds[0], + nilai: 300000, + created_at: new Date(), + }, + { + id: faker.string.uuid(), + id_chart_token: chartTokenIds[0], + nilai: 1600000, + created_at: new Date(), + }, + { + id: faker.string.uuid(), + id_chart_token: chartTokenIds[0], + nilai: 2400000, + created_at: new Date(), + }, + { + id: faker.string.uuid(), + id_chart_token: chartTokenIds[1], + nilai: 600000, + created_at: new Date(), + }, + { + id: faker.string.uuid(), + id_chart_token: chartTokenIds[1], + nilai: 1300000, + created_at: new Date(), + }, + { + id: faker.string.uuid(), + id_chart_token: chartTokenIds[1], + nilai: 1000000, + created_at: new Date(), + }, +]; diff --git a/services/backend/src/drizzle/seeder/project-mutation.ts b/services/backend/src/drizzle/seeder/project-mutation.ts new file mode 100644 index 0000000..8b84274 --- /dev/null +++ b/services/backend/src/drizzle/seeder/project-mutation.ts @@ -0,0 +1,70 @@ +import { faker } from "@faker-js/faker/locale/id_ID"; +export const projectMutationReportData = async (projectIds: string[]) => [ + { + id: faker.string.uuid(), + id_projek: projectIds[0], + judul: "Projek Naga Mutasi 1", + pemasukan: 1000000, + pengeluaran: 800000, + laporan: faker.image.avatar(), + created_at: new Date('2023-12-10T13:39:24.000Z'), + }, + { + id: faker.string.uuid(), + id_projek: projectIds[0], + judul: "Projek Naga Mutasi 2", + pemasukan: 2000000, + pengeluaran: 500000, + laporan: faker.image.avatar(), + created_at: new Date('2024-03-10T13:39:24.000Z'), + }, + { + id: faker.string.uuid(), + id_projek: projectIds[0], + judul: "Projek Naga Mutasi 3", + pemasukan: 3000000, + pengeluaran: 700000, + laporan: faker.image.avatar(), + created_at: new Date('2024-06-10T13:39:24.000Z'), + }, + { + id: faker.string.uuid(), + id_projek: projectIds[1], + judul: "Projek T-Rex Mutasi 1", + pemasukan: 1200000, + pengeluaran: 800000, + laporan: faker.image.avatar(), + created_at: new Date('2023-12-10T13:39:24.000Z'), + }, + { + id: faker.string.uuid(), + id_projek: projectIds[1], + judul: "Projek T-Rex Mutasi 2", + pemasukan: 2000000, + pengeluaran: 900000, + laporan: faker.image.avatar(), + created_at: new Date('2024-03-10T13:39:24.000Z'), + }, + { + id: faker.string.uuid(), + id_projek: projectIds[1], + judul: "Projek T-Rex Mutasi 3", + pemasukan: 400000, + pengeluaran: 700000, + laporan: faker.image.avatar(), + created_at: new Date('2024-06-10T13:39:24.000Z'), + }, +]; + +export const projectMutationReportDataFactory = async (projectIds: string[], count: number) => { + const promises = Array.from({ length: count }).map(async () => ({ + id: faker.string.uuid(), + id_projek: faker.helpers.arrayElement(projectIds), + judul: faker.lorem.words(), + pemasukan: parseInt(faker.string.numeric({ length: 7 })), + pengeluaran: parseInt(faker.string.numeric({ length: 6 })), + laporan: faker.image.avatar(), + created_at: new Date(), + })); + return Promise.all(promises); +}; diff --git a/services/backend/src/drizzle/seeder/project-report.ts b/services/backend/src/drizzle/seeder/project-report.ts new file mode 100644 index 0000000..5db9778 --- /dev/null +++ b/services/backend/src/drizzle/seeder/project-report.ts @@ -0,0 +1,71 @@ +import { faker } from "@faker-js/faker/locale/id_ID"; +export const projectReportData = async (projectIds: string[]) => [ + { + id: faker.string.uuid(), + id_projek: projectIds[0], + judul: "Laporan Projek Naga 1", + jenis_laporan: "UNTUNG", + nominal: 1000000, + laporan: faker.image.avatar(), + created_at: new Date("2023-12-10T13:39:24.000Z"), + }, + { + id: faker.string.uuid(), + id_projek: projectIds[0], + judul: "Laporan Projek Naga 2", + jenis_laporan: "RUGI", + nominal: 2000000, + laporan: faker.image.avatar(), + created_at: new Date("2024-03-10T13:39:24.000Z"), + }, + { + id: faker.string.uuid(), + id_projek: projectIds[0], + judul: "Laporan Projek Naga 3", + jenis_laporan: "UNTUNG", + nominal: 3000000, + laporan: faker.image.avatar(), + created_at: new Date("2024-06-10T13:39:24.000Z"), + }, + { + id: faker.string.uuid(), + id_projek: projectIds[1], + judul: "Laporan Projek T-Rex 1", + jenis_laporan: "RUGI", + nominal: 1200000, + laporan: faker.image.avatar(), + created_at: new Date("2023-12-10T13:39:24.000Z"), + }, + { + id: faker.string.uuid(), + id_projek: projectIds[1], + judul: "Laporan Projek T-Rex 2", + jenis_laporan: "UNTUNG", + nominal: 2000000, + laporan: faker.image.avatar(), + created_at: new Date("2024-03-10T13:39:24.000Z"), + }, + { + id: faker.string.uuid(), + id_projek: projectIds[1], + judul: "Laporan Projek T-Rex 3", + jenis_laporan: "RUGI", + nominal: 400000, + laporan: faker.image.avatar(), + created_at: new Date("2024-06-10T13:39:24.000Z"), + }, +]; + +export const projectReportDataFactory = async (projectIds: string[], count: number) => { + const promises = Array.from({ length: count }).map(async () => ({ + id: faker.string.uuid(), + id_projek: faker.helpers.arrayElement(projectIds), + judul: faker.lorem.sentence(), + jenis_laporan: faker.helpers.arrayElement(["UNTUNG", "RUGI"]), + nominal: parseInt(faker.string.numeric({ length: 7 })), + laporan: faker.image.avatar(), + created_at: faker.date.recent(), + })); + + return Promise.all(promises); +}; diff --git a/services/backend/src/drizzle/seeder/project-wallet.ts b/services/backend/src/drizzle/seeder/project-wallet.ts new file mode 100644 index 0000000..224af02 --- /dev/null +++ b/services/backend/src/drizzle/seeder/project-wallet.ts @@ -0,0 +1,31 @@ +import { faker } from "@faker-js/faker/locale/id_ID"; + +export const ProjectWalletData = async (projectIds: string[]) => { + const data = [ + { + id: faker.string.uuid(), + id_projek: projectIds[0], + dana_terkumpul: 1000000, + saldo: 1000000, + created_at: new Date(), + }, + { + id: faker.string.uuid(), + id_projek: projectIds[1], + dana_terkumpul: 2000000, + saldo: 2000000, + created_at: new Date(), + }, + { + id: faker.string.uuid(), + id_projek: projectIds[2], + dana_terkumpul: 3000000, + saldo: 3000000, + created_at: new Date(), + }, + ]; + + const usedProjectIds = new Set(data.map(item => item.id_projek)); + + return { data, usedProjectIds }; +}; \ No newline at end of file diff --git a/services/backend/src/drizzle/seeder/project.ts b/services/backend/src/drizzle/seeder/project.ts new file mode 100644 index 0000000..67e5da6 --- /dev/null +++ b/services/backend/src/drizzle/seeder/project.ts @@ -0,0 +1,125 @@ +import { faker } from "@faker-js/faker/locale/id_ID"; +export const projectCategoryData = async () => [ + { + id: "a2269857-dd6e-43bd-9216-f3f617dde418", + kategori: "Peternakan", + created_at: new Date(), + }, + { + id: "b44a051b-e063-4a30-831b-a7b01aaee620", + kategori: "Pertanian", + created_at: new Date(), + }, + { + id: "c6480dac-f999-40a0-a468-67974794be17", + kategori: "Jual Beli", + created_at: new Date(), + }, +]; + +export const projectData = async (userIds: string[], categoryIds: string[]) => [ + { + id: "5310c3ba-8da7-44f6-9c2d-57363b082aee", + id_user: userIds[0], // Use actual user ID + id_kategori: categoryIds[0], // Use actual category ID + judul: "Proyek Peternakan", + deskripsi: "Proyek peternakan naga", + nominal: 10000000, + asset_jaminan: "Tanah", + nilai_jaminan: 20000000, + lokasi_usaha: "Malang", + detail_lokasi: "Jl. Raya Tlogomas No. 246", + brosur_produk: "https://everpro.id/blog/contoh-brosur-minuman/", + pendapatan_perbulan: 2000000, + pengeluaran_perbulan: 500000, + report_progress: "3", + dokumen_proyeksi: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRm7Ed-GxL_MOmlJj7Zo2pzKtOdS9gKBDHxTA&s", + status: "APPROVAL", + nominal_disetujui: 10000000, + harga_per_unit: 1000000, + jumlah_koin: 10, + minimal_pembelian: 1, + maksimal_pembelian: 10, + limit_siklus: 3, + created_at: new Date("2023-05-10T13:39:24.000Z"), + }, + { + id: "848fdf64-a0f8-4dc4-8cc3-81678734edca", + id_user: userIds[0], + id_kategori: categoryIds[2], + judul: "Proyek Jual Beli", + deskripsi: "Proyek Jual Beli T-Rex", + nominal: 20000000, + asset_jaminan: "Tanah", + nilai_jaminan: 40000000, + lokasi_usaha: "Malang", + detail_lokasi: "Jl. Raya Tlogomas No. 246", + brosur_produk: "https://everpro.id/blog/contoh-brosur-minuman/", + pendapatan_perbulan: 2000000, + pengeluaran_perbulan: 500000, + report_progress: "3", + dokumen_proyeksi: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRm7Ed-GxL_MOmlJj7Zo2pzKtOdS9gKBDHxTA&s", + status: "APPROVAL", + nominal_disetujui: 20000000, + harga_per_unit: 1000000, + jumlah_koin: 20, + minimal_pembelian: 1, + maksimal_pembelian: 20, + limit_siklus: 3, + created_at: new Date("2023-05-10T13:39:24.000Z"), + }, +]; + +export const projectDataFactory = async (userIds: string[], categoryIds: string[], count: number) => { + const promises = Array.from({ length: count }).map(async () => ({ + id: faker.string.uuid(), + id_user: faker.helpers.arrayElement(userIds), // Use actual user IDs + id_kategori: faker.helpers.arrayElement(categoryIds), // Use actual category IDs + judul: faker.lorem.words(), + deskripsi: faker.lorem.sentence(), + nominal: parseInt(faker.string.numeric({ length: 8 })), + asset_jaminan: faker.lorem.word(), + nilai_jaminan: parseInt(faker.string.numeric({ length: 8 })), + lokasi_usaha: faker.location.city(), + detail_lokasi: faker.location.streetAddress(), + brosur_produk: faker.image.url(), + pendapatan_perbulan: parseInt(faker.string.numeric({ length: 7 })), + pengeluaran_perbulan: parseInt(faker.string.numeric({ length: 6 })), + report_progress: faker.helpers.arrayElement(["1", "3", "6", "12"]), + dokumen_proyeksi: faker.image.url(), + status: faker.helpers.arrayElement(["DRAFT", "PROSES VERIFIKASI", "REVISI", "APPROVAL", "TTD KONTRAK", "PENDANAAN DIBUKA"]), + limit_siklus: 3, + })); + return Promise.all(promises); +}; + +export const supportDocumentData = async (projectIds: string[]) => [ + { + id: faker.string.uuid(), + id_projek: projectIds[0], // Use actual project ID + dokumen: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRm7Ed-GxL_MOmlJj7Zo2pzKtOdS9gKBDHxTA&s", + created_at: new Date(), + }, + { + id: faker.string.uuid(), + id_projek: projectIds[1], // Use actual project ID + dokumen: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRm7Ed-GxL_MOmlJj7Zo2pzKtOdS9gKBDHxTA&s", + created_at: new Date(), + }, + { + id: faker.string.uuid(), + id_projek: projectIds[2], // Use actual project ID + dokumen: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRm7Ed-GxL_MOmlJj7Zo2pzKtOdS9gKBDHxTA&s", + created_at: new Date(), + }, +]; + +export const supportDocumentDataFactory = async (projectIds: string[], count: number) => { + const promises = Array.from({ length: count }).map(async () => ({ + id: faker.string.uuid(), + id_projek: faker.helpers.arrayElement(projectIds), // Use actual project IDs + dokumen: faker.image.url(), + created_at: new Date(), + })); + return Promise.all(promises); +}; diff --git a/services/backend/src/drizzle/seeder/signature-admin.ts b/services/backend/src/drizzle/seeder/signature-admin.ts new file mode 100644 index 0000000..4d92e45 --- /dev/null +++ b/services/backend/src/drizzle/seeder/signature-admin.ts @@ -0,0 +1,9 @@ +import { faker } from "@faker-js/faker/locale/id_ID"; +export const SignatureAdminData = async (userIds: string[]) => [ + { + id: faker.string.uuid(), + id_user: userIds[0], + signature: "/uploads/tanda_tangan_admin/1730169870902-ttd.jpeg", + created_at: new Date(), + }, +]; diff --git a/services/backend/src/drizzle/seeder/token.ts b/services/backend/src/drizzle/seeder/token.ts new file mode 100644 index 0000000..1fac0bf --- /dev/null +++ b/services/backend/src/drizzle/seeder/token.ts @@ -0,0 +1,33 @@ +import { faker } from "@faker-js/faker/locale/id_ID"; +export const tokenData = async (userIds: string[], count: number) => { + const promises = Array.from({ length: count }).flatMap(() => [ + { + id: faker.string.uuid(), + id_projek: "5310c3ba-8da7-44f6-9c2d-57363b082aee", + id_user: userIds[1], + nilai: 1000000, + created_at: new Date(), + }, + { + id: faker.string.uuid(), + id_projek: "848fdf64-a0f8-4dc4-8cc3-81678734edca", + id_user: userIds[1], + nilai: 2000000, + created_at: new Date(), + }, + ]); + + return Promise.all(promises); +}; + +export const tokenDataFactory = async (userIds: string[], projectIds: string[], count: number) => { + const promises = Array.from({ length: count }).map(async () => ({ + id: faker.string.uuid(), + id_projek: faker.helpers.arrayElement(projectIds), + id_user: faker.helpers.arrayElement(userIds), + nilai: faker.helpers.arrayElement([1000000, 2000000]), + created_at: new Date(), + })); + + return Promise.all(promises); +}; diff --git a/services/backend/src/drizzle/seeder/topup.ts b/services/backend/src/drizzle/seeder/topup.ts new file mode 100644 index 0000000..b03408e --- /dev/null +++ b/services/backend/src/drizzle/seeder/topup.ts @@ -0,0 +1,101 @@ +import { faker } from "@faker-js/faker/locale/id_ID"; +import { db } from "../db.js"; // Sesuaikan dengan path instance database Anda +import { UserTable, WalletTable } from "../schema.js"; // Sesuaikan dengan path schema yang digunakan +import { eq, inArray } from "drizzle-orm"; // Pastikan `inArray` di-import + +// Fungsi untuk mengambil nama pengguna berdasarkan wallet ID +type UserWalletRecord = { + walletId: string; + nama: string | null; +}; + +const getUserNamesByWallets = async (walletIds: string[]): Promise> => { + // Query untuk mengambil data wallet dan nama pengguna terkait + const userWalletRecords = await db + .select({ + walletId: WalletTable.id, + nama: UserTable.nama, + }) + .from(WalletTable) + .leftJoin(UserTable, eq(UserTable.id, WalletTable.id_user)) + .where(inArray(WalletTable.id, walletIds)) as UserWalletRecord[]; + + // Mapping hasil query ke dalam Map dengan key wallet ID dan value nama pengguna + const walletUserMap = new Map(); + userWalletRecords.forEach((record) => { + walletUserMap.set(record.walletId, record.nama || 'Unknown User'); + }); + + return walletUserMap; +}; + +// Fungsi untuk membuat data top-up dengan nama pengguna yang diambil berdasarkan wallet ID +export const topupData = async (walletIds: string[]) => { + const walletUserMap = await getUserNamesByWallets(walletIds); + + return [ + { + id: faker.string.uuid(), + id_wallet: walletIds[2], + nama: walletUserMap.get(walletIds[2]) || "Unknown User", + nama_bank: "BRI", + no_rekening: "1234567890", + nama_pemilik_rekening: walletUserMap.get(walletIds[2]) || "Unknown User", + nominal: 50000, + jenis: "SIMPANAN POKOK", + status: "SUKSES", + bukti_pembayaran: "https://example.com/bukti-pembayaran.jpg", + created_at: new Date(), + }, + { + id: faker.string.uuid(), + id_wallet: walletIds[3], + nama: walletUserMap.get(walletIds[3]) || "Unknown User", + nama_bank: "BCA", + no_rekening: "0987654321", + nama_pemilik_rekening: walletUserMap.get(walletIds[3]) || "Unknown User", + nominal: 120000, + jenis: "SIMPANAN WAJIB", + status: "SUKSES", + bukti_pembayaran: "https://example.com/bukti-pembayaran.jpg", + created_at: new Date(), + }, + ]; +}; + +// Fungsi factory untuk menghasilkan beberapa data top-up dengan nama pengguna berdasarkan wallet ID +export const topupDataFactory = async (walletIds: string[], count: number) => { + const walletUserMap = await getUserNamesByWallets(walletIds); + + const promises = Array.from({ length: count }).map(async () => { + const selectedWalletId = faker.helpers.arrayElement(walletIds); + const jenis = faker.helpers.arrayElement(["SIMPANAN WAJIB", "SIMPANAN POKOK", "TOPUP SALDO"]); + + // Menentukan nominal berdasarkan jenis simpanan + let nominal; + if (jenis === "SIMPANAN WAJIB") { + nominal = 50000; + } else if (jenis === "SIMPANAN POKOK") { + nominal = 120000; + } else { + // Jika jenis adalah "TOPUP SALDO", gunakan nominal acak + nominal = parseInt(faker.string.numeric({ length: 6 })); + } + + return { + id: faker.string.uuid(), + id_wallet: selectedWalletId, + nama: walletUserMap.get(selectedWalletId) || "Unknown User", + nama_bank: "MANDIRI", + no_rekening: faker.finance.accountNumber(), + nama_pemilik_rekening: walletUserMap.get(selectedWalletId) || "Unknown User", + nominal, + jenis, + status: faker.helpers.arrayElement(["SUKSES", "GAGAL", "MENUNGGU KONFIRMASI"]), + bukti_pembayaran: faker.image.avatar(), + created_at: new Date(), + }; + }); + + return Promise.all(promises); +}; diff --git a/services/backend/src/drizzle/seeder/transaction.ts b/services/backend/src/drizzle/seeder/transaction.ts new file mode 100644 index 0000000..1423300 --- /dev/null +++ b/services/backend/src/drizzle/seeder/transaction.ts @@ -0,0 +1,37 @@ +import { faker } from "@faker-js/faker/locale/id_ID"; + +export const TransactionData = async (projectIds: string[], userIds: string[]) => [ + { + id: "6b0005e5-0ae4-4691-b019-e88b7241b34b", + id_user: userIds[1], + nama_user: "John Doe", + id_projek: projectIds[2], + judul_projek: "Projek Naga", + owner_projek: "Budiono Siregar", + jumlah_token: 10, + total_nominal: 1000000, + created_at: new Date(), + }, + { + id: "77b41ce9-fb75-4e05-9276-e97e0ebee6d1", + id_user: userIds[1], + nama_user: "Rudi Doe", + id_projek: projectIds[2], + judul_projek: "Projek T-Rex", + owner_projek: "Budi Siregar", + jumlah_token: 20, + total_nominal: 2000000, + created_at: new Date(), + }, + { + id: "8f9de48b-cfdb-4f33-8494-64418b0935de", + id_user: userIds[1], + nama_user: "John Doe", + id_projek: projectIds[2], + judul_projek: "Projek Naga", + owner_projek: "Budiono Siregar", + jumlah_token: 30, + total_nominal: 3000000, + created_at: new Date(), + } +]; diff --git a/services/backend/src/drizzle/seeder/user.ts b/services/backend/src/drizzle/seeder/user.ts new file mode 100644 index 0000000..66d179b --- /dev/null +++ b/services/backend/src/drizzle/seeder/user.ts @@ -0,0 +1,91 @@ +import { faker } from "@faker-js/faker/locale/id_ID"; +import { hash } from "bcryptjs"; +import { format, subMonths } from "date-fns"; + +export const userData = async () => [ + { + id: "0937d7fc-8aa2-4ce5-b437-64b755ce5ff6", + nama: "Admin Koperasi", + no_hp: "6281234567890", + role: "ADMIN", + status: "AKTIF", + password: await hash("password", 10), + tempat_lahir: "Malang", + tanggal_lahir: format(new Date("2002-02-02"), "yyyy-MM-dd"), + provinsi: "35", + kota: "35.73", + kecamatan: "35.73.05", + alamat: "Jl. Raya Tlogomas No. 246", + nik: "3501234567890123", + foto_diri: "/uploads/foto_diri/1730168622601-fotodiri.png", + foto_ktp: "/uploads/foto_ktp/1730168622601-fotoktp.png", + created_at: new Date(), + }, + { + id: "1d798a08-0602-4030-b8df-b808134ec171", + nama: "budiono siregar", + no_hp: "6281234567891", + role: "BASIC", + status: "AKTIF", + password: await hash("password", 10), + tempat_lahir: "Surabaya", + tanggal_lahir: format(new Date("2003-03-03"), "yyyy-MM-dd"), + provinsi: "35", + kota: "35.78", + kecamatan: "35.78.03", + alamat: "Jl. Darmo No. 10", + nik: "3501234567890124", + foto_diri: "/uploads/foto_diri/1730168622602-fotodiri.png", + foto_ktp: "/uploads/foto_ktp/1730168622602-fotoktp.png", + created_at: new Date(), + otp: "2233", + }, + { + id: "a258fc20-32fd-4bce-9a99-85dd2551fab7", + nama: "siti nurhaliza", + no_hp: "6281234567892", + role: "PLATINUM", + status: "AKTIF", + password: await hash("password", 10), + tempat_lahir: "Jakarta", + tanggal_lahir: format(new Date("2004-04-04"), "yyyy-MM-dd"), + provinsi: "31", + kota: "31.74", + kecamatan: "31.74.01", + alamat: "Jl. Mampang Prapatan No. 20", + nik: "3501234567890125", + foto_diri: "/uploads/foto_diri/1730168622603-fotodiri.png", + foto_ktp: "/uploads/foto_ktp/1730168622603-fotoktp.png", + created_at: new Date(), + otp: "2234", + }, +]; + +export const userDataFactory = async (count: any) => { + const promises = Array.from({ length: count }).map(async () => { + const birthDate = subMonths(new Date(), faker.number.int({ min: 1, max: 12 })); + + return { + id: faker.string.uuid(), + nama: faker.person.fullName(), + no_hp: `62${faker.string.numeric(11)}`, + role: "BASIC", + status: "TIDAK AKTIF", + password: await hash("password", 10), + tempat_lahir: faker.location.city(), + tanggal_lahir: format(birthDate, "yyyy-MM-dd"), + provinsi: "35", + kota: "35.73", + kecamatan: faker.helpers.arrayElement(["35.73.01", "35.73.02", "35.73.03", "35.73.04", "35.73.05"]), + alamat: faker.location.streetAddress(), + nik: faker.string.numeric({ length: 16 }), + foto_diri: faker.image.avatar(), + foto_ktp: faker.image.avatar(), + created_at: new Date(), + verification_token: faker.string.numeric({ length: 64 }), + is_verified: true, + }; + }); + + return Promise.all(promises); +}; diff --git a/services/backend/src/drizzle/seeder/wallet.ts b/services/backend/src/drizzle/seeder/wallet.ts new file mode 100644 index 0000000..590dbd5 --- /dev/null +++ b/services/backend/src/drizzle/seeder/wallet.ts @@ -0,0 +1,89 @@ +import { faker } from "@faker-js/faker/locale/id_ID"; + +export const walletData = async (userIds: string[]) => { + const data = [ + { + id: faker.string.uuid(), + id_user: userIds[0], + jenis_wallet: "SIMPANAN POKOK", + saldo: 50_000, + created_at: new Date(), + }, + { + id: faker.string.uuid(), + id_user: userIds[0], + jenis_wallet: "SIMPANAN WAJIB", + saldo: 120_000, + created_at: new Date(), + }, + { + id: faker.string.uuid(), + id_user: userIds[1], + jenis_wallet: "SIMPANAN POKOK", + saldo: 50_000, + created_at: new Date(), + }, + { + id: faker.string.uuid(), + id_user: userIds[1], + jenis_wallet: "SIMPANAN WAJIB", + saldo: 120_000, + created_at: new Date(), + }, + { + id: faker.string.uuid(), + id_user: userIds[2], + jenis_wallet: "SIMPANAN POKOK", + saldo: 50_000, + created_at: new Date(), + }, + { + id: faker.string.uuid(), + id_user: userIds[2], + jenis_wallet: "SIMPANAN WAJIB", + saldo: 120_000, + created_at: new Date(), + }, + ]; + + const usedUserIds = new Set(data.map((item) => item.id_user)); + + return { data, usedUserIds }; +}; + +export const walletDataFactory = async (userIds: string[], count: number, usedUserIds: Set) => { + const availableUserIds = userIds.filter((id) => !usedUserIds.has(id)); + + const usedIds = new Set(); + + const promises = Array.from({ length: count }).map(async () => { + let selectedUserId: string; + + do { + selectedUserId = faker.helpers.arrayElement(availableUserIds); + } while (usedIds.has(selectedUserId)); + + usedIds.add(selectedUserId); + + const jenis_wallet = faker.helpers.arrayElement(["SIMPANAN WAJIB", "SIMPANAN POKOK", "SALDO"]); + + let nominal; + if (jenis_wallet === "SIMPANAN WAJIB") { + nominal = 50000; + } else if (jenis_wallet === "SIMPANAN POKOK") { + nominal = 120000; + } else { + nominal = parseInt(faker.string.numeric({ length: 6 })); + } + + return { + id: faker.string.uuid(), + id_user: selectedUserId, + jenis_wallet, + saldo: parseInt(faker.string.numeric({ length: 6 })), + created_at: new Date(), + }; + }); + + return Promise.all(promises); +}; diff --git a/services/backend/src/main.ts b/services/backend/src/main.ts new file mode 100644 index 0000000..3e88836 --- /dev/null +++ b/services/backend/src/main.ts @@ -0,0 +1,119 @@ +import express, { Request, Response, NextFunction } from "express"; +import cors from "cors"; +import authRouter from "./routes/auth.js"; +import userRouter from "./routes/user.js"; +import projectCategoryRouter from "./routes/project-category.js"; +import projectRouter from "./routes/project.js"; +import wilayahRouter from "./routes/wilayah.js"; +import projectReportRouter from "./routes/project-report.js"; +import topupRouter from "./routes/topup.js"; +import projectTokenRouter from "./routes/project-token.js"; +import mutationRouter from "./routes/mutation.js"; +import chartProjectRouter from "./routes/chart-project.js"; +import chartTokenRouter from "./routes/chart-token.js"; +import historyTokenRouter from "./routes/history-token.js"; +import historyProjectRouter from "./routes/history-project.js"; +import whatsappRouter from "./routes/baileys.js"; +import projectWalletRouter from "./routes/project-wallet.js"; +import historyProjectWalletRouter from "./routes/history-project-wallet.js"; +import transactionRouter from "./routes/transaction.js"; +import swaggerUi from "swagger-ui-express"; +import { ethers } from "ethers"; +import path from "path"; +import fs from "fs"; +import { configureAxiosInterceptor } from "./middlewares/api-key.js"; + +if (!process.env.API_URL) { + throw new Error("Api Url is not defined in the environment variables."); +} +if (!process.env.PRIVATE_KEY) { + throw new Error("Private key is not defined in the environment variables."); +} +if (!process.env.CONTRACT_ADDRESS) { + throw new Error("Contract address is not defined in the environment variables."); +} +export const walletServiceUrl: string = process.env.WALLET_URL || "http://localhost:3001"; +export const walletServiceApiKey: any = process.env.API_KEY; +const apiUrl: string = process.env.API_URL; +const privateKey: string = process.env.PRIVATE_KEY; +const contractAddress: string = process.env.CONTRACT_ADDRESS; + +const abiPath = path.join(__dirname, "../contractABI.json"); +const contractABI = JSON.parse(fs.readFileSync(abiPath, "utf8")); + +const provider = new ethers.JsonRpcProvider(apiUrl); + +async function setupContract() { + try { + // Create a wallet instance from the private key + const signer = new ethers.Wallet(privateKey, provider); + console.log("\x1b[36m%s\x1b[0m", "Signer 👲 :", await signer.getAddress()); + + // Create the contract instance with the signer + const contract = new ethers.Contract(contractAddress, contractABI, signer); + console.log("\x1b[36m%s\x1b[0m", "Contract instance 🚀"); + + // Check if the contract instance is valid + if (!contract || typeof contract !== "object") { + throw new Error("Contract instance is not valid"); + } + + return contract; + } catch (error) { + console.error("Error in setupContract:", error); + throw error; + } +} + +export const getContract = setupContract; + +const app = express(); +const port = process.env.PORT || 3000; + +configureAxiosInterceptor(walletServiceUrl, walletServiceApiKey); + +app.use(express.json()); +app.use(cors()); + +app.use(express.static(__dirname + "/../assets")); + +app.get("/formatan-dokumen-proyeksi", (req: Request, res: Response) => { + const file = `statis/formatan-dokumen-proyeksi.docx`; + res.json({ data: file }); +}); + +app.get("/", (req: Request, res: Response) => { + res.send("Hello, Koperasi Service!"); +}); + +app.use("/auth", authRouter); +app.use("/user", userRouter); +app.use("/project", projectRouter); +app.use("/project-category", projectCategoryRouter); +app.use("/project-report", projectReportRouter); +app.use("/wilayah", wilayahRouter); +app.use("/topup", topupRouter); +app.use("/token", projectTokenRouter); +app.use("/mutation", mutationRouter); +app.use("/chart-project", chartProjectRouter); +app.use("/chart-token", chartTokenRouter); +app.use("/history-token", historyTokenRouter); +app.use("/whatsapp", whatsappRouter); +app.use("/project-wallet", projectWalletRouter); +app.use("/history-project-wallet", historyProjectWalletRouter); +app.use("/history-project", historyProjectRouter); +app.use("/transaction", transactionRouter); + +const swaggerDocument = require("../../../api-docs/swagger-output.json"); + +app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument)); + +// Add this error handling middleware +app.use((err: Error, req: Request, res: Response, next: NextFunction) => { + console.error(err.stack); + res.status(500).send("Something went wrong"); +}); + +app.listen(port, () => { + console.log(`Server running at http://localhost:${port}`); +}); diff --git a/services/backend/src/middlewares/agreement-letter.ts b/services/backend/src/middlewares/agreement-letter.ts new file mode 100644 index 0000000..80256b4 --- /dev/null +++ b/services/backend/src/middlewares/agreement-letter.ts @@ -0,0 +1,40 @@ +// middlewares/agreement-letter.ts + +import { NextFunction, Request, Response } from "express"; +import multer from "multer"; +import path from "path"; + +// Set up Multer storage +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, path.join(__dirname, "../../assets/uploads/tanda_tangan")); + }, + filename: (req, file, cb) => { + cb(null, `${Date.now()}-${file.originalname}`); + }, +}); + +// Initialize Multer with storage configuration +const upload = multer({ storage }); + +// Middleware to handle file upload and process the uploaded file +export const AgreementMiddleware = (req: Request, res: Response, next: NextFunction) => { + upload.single("tanda_tangan")(req, res, (err) => { + if (err) { + console.error("Error in file upload:", err); + return res.status(500).json({ message: "File upload failed" }); + } + + const file = req.file; + + // Check if file upload was successful + if (!file) { + return res.status(400).json({ message: "File upload failed" }); + } + + // Set the file path to the request body + req.body.tanda_tangan = `/uploads/tanda_tangan/${file.filename}`; + + next(); + }); +}; diff --git a/services/backend/src/middlewares/api-key.ts b/services/backend/src/middlewares/api-key.ts new file mode 100644 index 0000000..3fda7dc --- /dev/null +++ b/services/backend/src/middlewares/api-key.ts @@ -0,0 +1,15 @@ +import axios from "axios"; + +export const configureAxiosInterceptor = (walletServiceUrl: string, walletServiceApiKey: any) => { + axios.interceptors.request.use( + (config) => { + if (config.baseURL === walletServiceUrl || (config.url && config.url.startsWith(walletServiceUrl))) { + config.headers["x-api-key"] = walletServiceApiKey; + } + return config; + }, + (error) => { + return Promise.reject(error); + } + ); +}; \ No newline at end of file diff --git a/services/backend/src/middlewares/auth.ts b/services/backend/src/middlewares/auth.ts new file mode 100644 index 0000000..1de91ca --- /dev/null +++ b/services/backend/src/middlewares/auth.ts @@ -0,0 +1,110 @@ +import multer from "multer"; +import path from "path"; +import { NextFunction, Request, Response } from "express"; +import jwt from "jsonwebtoken"; +import { tokenBlacklist } from "../services/jwt.js"; +import { db } from "../drizzle/db.js"; +import { UserTable } from "../drizzle/schema.js"; +import { eq } from "drizzle-orm"; + +// Configure disk storage for multer +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + switch (file.fieldname) { + case "foto_diri": + cb(null, path.join(__dirname, "../../assets/uploads/foto_diri")); + break; + case "foto_ktp": + cb(null, path.join(__dirname, "../../assets/uploads/foto_ktp")); + break; + case "foto_profile": + cb(null, path.join(__dirname, "../../assets/uploads/foto_profile")); + break; + case "signature": + cb(null, path.join(__dirname, "../../assets/uploads/tanda_tangan_admin")); + break; + default: + cb(new Error("Invalid field name"), ""); + } + }, + filename: (req, file, cb) => { + cb(null, `${Date.now()}-${file.originalname}`); + }, +}); + +const upload = multer({ storage }); + +export const parseFormData = upload.fields([ + { name: "foto_diri", maxCount: 1 }, + { name: "foto_ktp", maxCount: 1 }, + { name: "foto_profile", maxCount: 1 }, + { name: "signature", maxCount: 1 }, +]); + +export const formDataParserMiddleware = (req: Request, res: Response, next: NextFunction) => { + try { + const files = req.files as { [fieldname: string]: Express.Multer.File[] } | undefined; + + // Initialize paths as null or uploaded file values + const fotoDiriPath = files?.["foto_diri"]?.[0] ? `/uploads/foto_diri/${files["foto_diri"][0].filename}` : null; + + const fotoKTPPath = files?.["foto_ktp"]?.[0] ? `/uploads/foto_ktp/${files["foto_ktp"][0].filename}` : null; + + const fotoProfilePath = files?.["foto_profile"]?.[0] ? `/uploads/foto_profile/${files["foto_profile"][0].filename}` : null; + + const signaturePath = files?.["signature"]?.[0] ? `/uploads/tanda_tangan_admin/${files["signature"][0].filename}` : null; + + // Set paths in req.body + if (fotoDiriPath) req.body.foto_diri = fotoDiriPath; + if (fotoKTPPath) req.body.foto_ktp = fotoKTPPath; + if (fotoProfilePath) req.body.foto_profile = fotoProfilePath; + if (signaturePath) req.body.signature = signaturePath; + + next(); + } catch (err) { + console.error("Error parsing form-data: ", err); + res.status(500).json({ message: "Error parsing form-data" }); + } +}; + +export const validateToken = async (req: Request, res: Response, next: NextFunction) => { + try { + const token = req.headers.authorization?.split(" ")[1]; + if (!token) { + return res.status(401).json({ message: "JWT Token not found" }); + } + + if (tokenBlacklist[token]) { + return res.status(401).json({ error: "JWT Token has been invalidated" }); + } + + const decoded = jwt.verify(token, process.env.JWT_SECRET as string); + req.user = decoded; + next(); + } catch (err) { + res.status(401).json({ message: "Invalid token" }); + } +}; + +export const adminOnly = async (req: Request, res: Response, next: NextFunction) => { + try { + const user = await db.select().from(UserTable).where(eq(UserTable.id, req.user.id)).limit(1).execute(); + if (!user[0] || user[0].role !== "ADMIN") { + return res.status(403).json({ message: "Forbidden" }); + } + next(); + } catch (err) { + res.status(500).json({ message: "Internal server error" }); + } +}; + +export const platinumOnly = async (req: Request, res: Response, next: NextFunction) => { + try { + if (req.user.role !== "PLATINUM" && req.user.role !== "ADMIN") { + return res.status(403).json({ message: "Forbidden" }); + } + next(); + } catch (err) { + res.status(500).json({ message: "Internal server error" }); + } +}; diff --git a/services/backend/src/middlewares/history-project-wallet.ts b/services/backend/src/middlewares/history-project-wallet.ts new file mode 100644 index 0000000..52e6330 --- /dev/null +++ b/services/backend/src/middlewares/history-project-wallet.ts @@ -0,0 +1,34 @@ +import { NextFunction, Request, Response } from "express"; +import multer from "multer"; +import path from "path"; + +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, path.join(__dirname, "../../assets/uploads/bukti_transfer")); + }, + filename: (req, file, cb) => { + cb(null, `${Date.now()}-${file.originalname}`); + }, +}); + +const upload = multer({ storage }); + +export const uploadBuktiTransferMiddleware = (req: Request, res: Response, next: NextFunction) => { + console.log(req.user); + upload.single("bukti_transfer")(req, res, (err) => { + if (err) { + console.error("Error in file upload:", err); + return res.status(500).json({ message: "File upload failed" }); + } + + const file = req.file; + + if (!file) { + return res.status(400).json({ message: "File upload failed" }); + } + + req.body.bukti_transfer = `/uploads/bukti_transfer/${file.filename}`; + + next(); + }); +}; diff --git a/services/backend/src/middlewares/mutation.ts b/services/backend/src/middlewares/mutation.ts new file mode 100644 index 0000000..c37685c --- /dev/null +++ b/services/backend/src/middlewares/mutation.ts @@ -0,0 +1,33 @@ +import { NextFunction, Request, Response } from "express"; +import multer from "multer"; +import path from "path"; + +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, path.join(__dirname, "../../assets/uploads/laporan_mutasi")); + }, + filename: (req, file, cb) => { + cb(null, `${Date.now()}-${file.originalname}`); + }, +}); + +const upload = multer({ storage }); + +export const uploadMutationMiddleware = (req: Request, res: Response, next: NextFunction) => { + upload.single("laporan")(req, res, (err) => { + if (err) { + console.error("Error in file upload:", err); + return res.status(500).json({ message: "File upload failed" }); + } + + const file = req.file; + + if (!file) { + return res.status(400).json({ message: "File upload failed" }); + } + + req.body.laporan = `/uploads/laporan_mutasi/${file.filename}`; + + next(); + }); +}; diff --git a/services/backend/src/middlewares/project-report.ts b/services/backend/src/middlewares/project-report.ts new file mode 100644 index 0000000..ba1c881 --- /dev/null +++ b/services/backend/src/middlewares/project-report.ts @@ -0,0 +1,33 @@ +import { NextFunction, Request, Response } from "express"; +import multer from "multer"; +import path from "path"; + +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, path.join(__dirname, "../../assets/uploads/laporan_keuangan")); + }, + filename: (req, file, cb) => { + cb(null, `${Date.now()}-${file.originalname}`); + }, +}); + +const upload = multer({ storage }); + +export const uploadLaporanMiddleware = (req: Request, res: Response, next: NextFunction) => { + upload.single("laporan")(req, res, (err) => { + if (err) { + console.error("Error in file upload:", err); + return res.status(500).json({ message: "File upload failed" }); + } + + const file = req.file; + + if (!file) { + return res.status(400).json({ message: "File upload failed" }); + } + + req.body.laporan = `/uploads/laporan_keuangan/${file.filename}`; + + next(); + }); +}; diff --git a/services/backend/src/middlewares/project.ts b/services/backend/src/middlewares/project.ts new file mode 100644 index 0000000..49c8be6 --- /dev/null +++ b/services/backend/src/middlewares/project.ts @@ -0,0 +1,150 @@ +import multer from "multer"; +import path from "path"; +import { Request, Response, NextFunction } from "express"; + +// Definisikan tipe untuk file yang diizinkan +type AllowedFileTypes = { + [key: string]: string[]; +}; + +// Tipe file yang diizinkan +const allowedFileTypes: AllowedFileTypes = { + brosur_produk: ["image/jpg", "image/jpeg", "image/png", "application/pdf", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"], + dokumen_proyeksi: ["image/jpg", "image/jpeg", "image/png", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "text/csv", "application/pdf", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"], + dokumen: ["image/jpg", "image/jpeg", "image/png", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "text/csv", "application/pdf", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"], + dokumen_prospektus: ["image/jpg", "image/jpeg", "image/png", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "text/csv", "application/pdf", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"], +}; + +// Maksimum ukuran file dalam bytes (2MB) +const maxSize = 2 * 1024 * 1024; + +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + const uploadPath = path.join(__dirname, "../../assets/uploads"); + const folderName = file.fieldname === "dokumen" ? "dokumen_pendukung" : file.fieldname; + cb(null, path.join(uploadPath, folderName)); + }, + filename: (req, file, cb) => { + cb(null, `${Date.now()}-${file.originalname}`); + }, +}); + +// Validasi tipe dan ukuran file +const fileFilter = (req: Request, file: Express.Multer.File, cb: multer.FileFilterCallback) => { + const fileTypes = allowedFileTypes[file.fieldname]; + if (fileTypes && fileTypes.includes(file.mimetype)) { + cb(null, true); + } else { + cb(new Error(`File type not allowed for ${file.fieldname}.`)); + } +}; + +const upload = multer({ + storage, + fileFilter, + limits: { fileSize: maxSize }, +}).fields([ + { name: "brosur_produk", maxCount: 1 }, + { name: "dokumen_proyeksi", maxCount: 1 }, + { name: "dokumen", maxCount: 5 }, + { name: "dokumen_prospektus", maxCount: 1 }, +]); + +// Helper function to process uploaded files +const processUploadedFiles = (req: Request, files: Express.Multer.File[] | undefined, fieldName: string): string | string[] | null => { + if (!files || files.length === 0) return null; + + const basePath = `uploads/${fieldName === "dokumen" ? "dokumen_pendukung" : fieldName}`; + + return fieldName === "dokumen" + ? files.map(file => `${basePath}/${file.filename}`) + : `${basePath}/${files[0].filename}`; +}; + +export const uploadMiddleware = (req: Request, res: Response, next: NextFunction) => { + upload(req, res, (err) => { + if (err instanceof multer.MulterError) { + return res.status(400).json({ error: `Multer error: ${err.message}` }); + } else if (err) { + return res.status(400).json({ error: err.message }); + } + + const files = req.files as { [fieldname: string]: Express.Multer.File[] } | undefined; + + if (!files) { + return res.status(400).json({ error: "No files were uploaded." }); + } + + console.log("Files yang tertangkap:", files); + + const fieldNames = ["brosur_produk", "dokumen_proyeksi", "dokumen", "dokumen_prospektus"]; + + fieldNames.forEach(fieldName => { + const processedFiles = processUploadedFiles(req, files[fieldName], fieldName); + if (processedFiles) { + req.body[fieldName] = processedFiles; + } + }); + + if (!req.body.dokumen_proyeksi) { + return res.status(400).json({ error: "File dokumen_proyeksi is required." }); + } + + next(); + }); +}; + +export const uploadUpdateProjectMiddleware = (req: Request, res: Response, next: NextFunction) => { + upload(req, res, (err) => { + if (err instanceof multer.MulterError) { + return res.status(400).json({ error: `Multer error: ${err.message}` }); + } else if (err) { + return res.status(400).json({ error: err.message }); + } + + const files = req.files as { [fieldname: string]: Express.Multer.File[] } | undefined; + + if (!files) { + return res.status(400).json({ error: "No files were uploaded." }); + } + + console.log("Files yang tertangkap:", files); + + const fieldNames = ["brosur_produk", "dokumen_proyeksi", "dokumen"]; + + fieldNames.forEach(fieldName => { + const processedFiles = processUploadedFiles(req, files[fieldName], fieldName); + if (processedFiles) { + req.body[fieldName] = processedFiles; + } + }); + + next(); + }); +}; + +export const uploadDokumenProspektusMiddleware = (req: Request, res: Response, next: NextFunction) => { + upload(req, res, (err) => { + if (err instanceof multer.MulterError) { + return res.status(400).json({ error: `Multer error: ${err.message}` }); + } else if (err) { + return res.status(400).json({ error: err.message }); + } + + const files = req.files as { [fieldname: string]: Express.Multer.File[] } | undefined; + + if (!files) { + return res.status(400).json({ error: "No files were uploaded." }); + } + + const processedFiles = processUploadedFiles(req, files.dokumen_prospektus, "dokumen_prospektus"); + + if (processedFiles) { + req.body.dokumen_prospektus = processedFiles; + } else { + return res.status(400).json({ error: "File dokumen_prospektus is required." }); + } + + next(); + }); +}; \ No newline at end of file diff --git a/services/backend/src/middlewares/topup.ts b/services/backend/src/middlewares/topup.ts new file mode 100644 index 0000000..1131522 --- /dev/null +++ b/services/backend/src/middlewares/topup.ts @@ -0,0 +1,34 @@ +import { NextFunction, Request, Response } from "express"; +import multer from "multer"; +import path from "path"; + +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, path.join(__dirname, "../../assets/uploads/bukti_pembayaran")); + }, + filename: (req, file, cb) => { + cb(null, `${Date.now()}-${file.originalname}`); + }, +}); + +const upload = multer({ storage }); + +export const uploadBuktiMiddleware = (req: Request, res: Response, next: NextFunction) => { + console.log(req.user); + upload.single("bukti_pembayaran")(req, res, (err) => { + if (err) { + console.error("Error in file upload:", err); + return res.status(500).json({ message: "File upload failed" }); + } + + const file = req.file; + + if (!file) { + return res.status(400).json({ message: "File upload failed" }); + } + + req.body.bukti_pembayaran = `/uploads/bukti_pembayaran/${file.filename}`; + + next(); + }); +}; diff --git a/services/backend/src/middlewares/validate.resource.ts b/services/backend/src/middlewares/validate.resource.ts new file mode 100644 index 0000000..2d3cbc9 --- /dev/null +++ b/services/backend/src/middlewares/validate.resource.ts @@ -0,0 +1,24 @@ +import type { Request, Response, NextFunction } from "express"; +import type { AnyZodObject } from "zod"; +import { fromZodError } from "zod-validation-error"; +// import { UnprocessableEntityException } from "../exceptions/validation"; +// import { ErrorCode } from "../exceptions/root"; + +export const validateResource = + (schema: AnyZodObject) => + (req: Request, res: Response, next: NextFunction) => { + const result = schema.safeParse({ + body: req.body, + query: req.query, + params: req.params, + }); + + if (!result.success) { + return res.status(400).json({ error: fromZodError(result.error) }); + // throw new UnprocessableEntityException("Validation error", ErrorCode.UNPROCESSABLE_ENTITY, fromZodError(result.error)) + } + + next(); + }; + +export default validateResource; \ No newline at end of file diff --git a/services/backend/src/routes/auth.ts b/services/backend/src/routes/auth.ts new file mode 100644 index 0000000..923db03 --- /dev/null +++ b/services/backend/src/routes/auth.ts @@ -0,0 +1,13 @@ +import express from "express"; +import * as authController from "../controllers/auth.js"; +import validateResource from "../middlewares/validate.resource.js"; +import { RegisterValidation } from "../validations/auth.js"; +import { parseFormData, formDataParserMiddleware, validateToken } from "../middlewares/auth.js"; + +const router = express.Router(); + +router.post("/register", parseFormData, formDataParserMiddleware, validateResource(RegisterValidation), authController.register); +router.post("/login", authController.login); +router.post("/logout", validateToken, authController.logout); + +export default router; diff --git a/services/backend/src/routes/baileys.ts b/services/backend/src/routes/baileys.ts new file mode 100644 index 0000000..cb66325 --- /dev/null +++ b/services/backend/src/routes/baileys.ts @@ -0,0 +1,11 @@ +// routes/whatsapp.ts +import express from "express"; +import { getQRCodeHandler } from "../controllers/baileys.js"; +import { validateToken, adminOnly } from '../middlewares/auth.js'; + +const router = express.Router(); + +// Endpoint to get the QR code +router.get("/qr-code", validateToken, adminOnly,getQRCodeHandler); + +export default router; \ No newline at end of file diff --git a/services/backend/src/routes/chart-project.ts b/services/backend/src/routes/chart-project.ts new file mode 100644 index 0000000..e2f8483 --- /dev/null +++ b/services/backend/src/routes/chart-project.ts @@ -0,0 +1,9 @@ +import express from "express"; +import { getAllChartProjectByUserIdHandler, getChartProjectByIdHandler } from "../controllers/chart-project.js"; +import { validateToken } from '../middlewares/auth.js'; +const router = express.Router(); + +router.get("/user", validateToken,getAllChartProjectByUserIdHandler); +router.get("/:id", validateToken, getChartProjectByIdHandler); + +export default router; diff --git a/services/backend/src/routes/chart-token.ts b/services/backend/src/routes/chart-token.ts new file mode 100644 index 0000000..0bb60c9 --- /dev/null +++ b/services/backend/src/routes/chart-token.ts @@ -0,0 +1,10 @@ +import express from "express"; +import { validateToken } from '../middlewares/auth.js'; +import { getAllChartTokenByUserIdHandler, getChartTokenByUserIdandProjectIdHandler, getLastChartTokenByUserIdandProjectIdHandler } from "../controllers/chart-token.js"; +const router = express.Router(); + +router.get("/user/project/:id/latest", validateToken, getLastChartTokenByUserIdandProjectIdHandler); +router.get("/user/project/:id", validateToken, getChartTokenByUserIdandProjectIdHandler); +router.get("/user", validateToken, getAllChartTokenByUserIdHandler); + +export default router; diff --git a/services/backend/src/routes/history-project-wallet.ts b/services/backend/src/routes/history-project-wallet.ts new file mode 100644 index 0000000..d8fd6d4 --- /dev/null +++ b/services/backend/src/routes/history-project-wallet.ts @@ -0,0 +1,8 @@ +import express from "express"; +import { validateToken } from '../middlewares/auth.js'; +import { getHistoryProjectWalletByProjectWalletIdHandler } from "../controllers/history-project-wallet.js"; +const router = express.Router(); + +router.get("/project-wallet/:id", validateToken, getHistoryProjectWalletByProjectWalletIdHandler); + +export default router; diff --git a/services/backend/src/routes/history-project.ts b/services/backend/src/routes/history-project.ts new file mode 100644 index 0000000..4f2e88e --- /dev/null +++ b/services/backend/src/routes/history-project.ts @@ -0,0 +1,8 @@ +import express from "express"; +import { validateToken } from '../middlewares/auth.js'; +import { getHistoryProjectByProjectIdHandler } from "../controllers/history-project.js"; +const router = express.Router(); + +router.get("/project/:id", validateToken, getHistoryProjectByProjectIdHandler); + +export default router; diff --git a/services/backend/src/routes/history-token.ts b/services/backend/src/routes/history-token.ts new file mode 100644 index 0000000..401d7d6 --- /dev/null +++ b/services/backend/src/routes/history-token.ts @@ -0,0 +1,8 @@ +import express from "express"; +import { validateToken } from '../middlewares/auth.js'; +import { getHistoryTokenByUserIdandProjectIdHandler } from "../controllers/history-token.js"; +const router = express.Router(); + +router.get("/project/:id", validateToken, getHistoryTokenByUserIdandProjectIdHandler); + +export default router; diff --git a/services/backend/src/routes/mutation.ts b/services/backend/src/routes/mutation.ts new file mode 100644 index 0000000..1440efc --- /dev/null +++ b/services/backend/src/routes/mutation.ts @@ -0,0 +1,14 @@ +import express from "express"; +import { uploadMutationMiddleware } from "../middlewares/mutation.js"; +import { createMutationHandler, getAllMutationHandler, getMutationByIdHandler, getMutationByProjectIdHandler } from "../controllers/mutation.js"; +import { validateToken, adminOnly } from "../middlewares/auth.js"; +import validateResource from "../middlewares/validate.resource.js"; +import { CreateMutationValidation } from "../validations/mutation.js"; +const router = express.Router(); + +router.post("/", validateToken, adminOnly, uploadMutationMiddleware, validateResource(CreateMutationValidation), createMutationHandler); +router.get("/", validateToken, getAllMutationHandler); +router.get("/:id", validateToken, getMutationByIdHandler); +router.get("/project/:id", validateToken, getMutationByProjectIdHandler); + +export default router; diff --git a/services/backend/src/routes/project-category.ts b/services/backend/src/routes/project-category.ts new file mode 100644 index 0000000..5b7e9bb --- /dev/null +++ b/services/backend/src/routes/project-category.ts @@ -0,0 +1,15 @@ +import express from "express"; +import * as projectCategoryController from "../controllers/project-category.js"; +import { adminOnly, validateToken } from "../middlewares/auth.js"; +import validateResource from "../middlewares/validate.resource.js"; +import { CreateProjectCategoryValidation, UpdateProjectCategoryValidation } from "../validations/project-category.js"; + +const router = express.Router(); + +router.post("/", validateToken, adminOnly, validateResource(CreateProjectCategoryValidation), projectCategoryController.createProjectCategoryHandler); +router.get("/", validateToken, projectCategoryController.getProjectCategoryHandler); +router.get("/:id", validateToken, projectCategoryController.getProjectCategoryByIdHandler); +router.put("/", validateToken, adminOnly, validateResource(UpdateProjectCategoryValidation), projectCategoryController.updateProjectCategoryByIdHandler); +router.delete("/", validateToken, adminOnly, projectCategoryController.deleteProjectCategoryByIdHandler); + +export default router; diff --git a/services/backend/src/routes/project-report.ts b/services/backend/src/routes/project-report.ts new file mode 100644 index 0000000..2b0438f --- /dev/null +++ b/services/backend/src/routes/project-report.ts @@ -0,0 +1,15 @@ +import express from "express"; +import * as projectReportController from "../controllers/project-report.js"; +import { adminOnly, validateToken } from "../middlewares/auth.js"; +import { uploadLaporanMiddleware } from "../middlewares/project-report.js"; +import validateResource from "../middlewares/validate.resource.js"; +import { CreateProjectReportValidation } from "../validations/project-report.js"; + +const router = express.Router(); + +router.post("/", validateToken, adminOnly, validateResource(CreateProjectReportValidation), projectReportController.createProjectReportHandler); +router.get("/:id", validateToken, projectReportController.getProjectReportHandler); +router.get("/project/:id", validateToken, projectReportController.getProjectReportByProjectIdHandler); +router.get("/", validateToken, projectReportController.getAllProjectReportHandler); + +export default router; diff --git a/services/backend/src/routes/project-token.ts b/services/backend/src/routes/project-token.ts new file mode 100644 index 0000000..caa3c64 --- /dev/null +++ b/services/backend/src/routes/project-token.ts @@ -0,0 +1,18 @@ +import express from "express"; +import * as projectTokenController from "../controllers/project-token.js"; +import { adminOnly, validateToken } from "../middlewares/auth.js"; +import validateResource from "../middlewares/validate.resource.js"; +import { BuyTokenValidation } from "../validations/project-token.js"; + +const router = express.Router(); + +router.get("/", validateToken, adminOnly, projectTokenController.getAllTokenHandler); +router.get("/total-token", validateToken, projectTokenController.getTotalTokenHandler); +router.get("/project/:id", validateToken, projectTokenController.getTokenByIdProjectHandler); +router.get("/total-usage/:id", validateToken, projectTokenController.getTotalTokenUsageByIdProjectHandler); +router.get("/project/:id/user", validateToken, projectTokenController.getTokenProjectByUserHandler); +router.post("/buy-token", validateToken, validateResource(BuyTokenValidation), projectTokenController.buyTokenProjectHandler); +router.get("/usage-details", validateToken, projectTokenController.tokenUsageDetailsByIdUserHandler); +router.get("/total-token-rupiah", validateToken, projectTokenController.getTotalTokenRupiahByUserHandler); + +export default router; diff --git a/services/backend/src/routes/project-wallet.ts b/services/backend/src/routes/project-wallet.ts new file mode 100644 index 0000000..730113a --- /dev/null +++ b/services/backend/src/routes/project-wallet.ts @@ -0,0 +1,14 @@ +import express from "express"; +import * as projectWalletController from "../controllers/project-wallet.js"; +import { adminOnly, validateToken } from "../middlewares/auth.js"; +import { uploadBuktiTransferMiddleware } from "../middlewares/history-project-wallet.js"; +import validateResource from "../middlewares/validate.resource.js"; +import { TransferSaldoProjectWalletValidation } from "../validations/project-wallet.js"; + +const router = express.Router(); + +router.get("/", validateToken, adminOnly, projectWalletController.getProjectWalletHandler); +router.get("/project/:id", validateToken, adminOnly, projectWalletController.getProjectWalletByProjectIdHandler); +router.post("/transfer-saldo", validateToken, adminOnly, uploadBuktiTransferMiddleware, validateResource(TransferSaldoProjectWalletValidation), projectWalletController.transferSaldoProjectHandler); + +export default router; diff --git a/services/backend/src/routes/project.ts b/services/backend/src/routes/project.ts new file mode 100644 index 0000000..1715149 --- /dev/null +++ b/services/backend/src/routes/project.ts @@ -0,0 +1,53 @@ +import express from "express"; +import { + acceptProjectHandler, + approveProjectHandler, + checkProjectFundingOpenedHandler, + completingProjectHandler, + countProjectHandler, + createProjectHandler, + getAllProjectHandler, + getDokumenProspektusByIdHandler, + getKeteranganReviseProjectByIdHandler, + getProjectByIdHandler, + getProjectByUserIdHandler, + getUserHaveTokenInProjectHandler, + publishProjectHandler, + rejectProjectHandler, + reviseProjectHandler, + shareProfitHandler, + totalProfitHandler, + updateProjectHandler, +} from "../controllers/project.js"; +import validateResource from "../middlewares/validate.resource.js"; +import { CreateProjectValidation, UpdateProjectValidation } from "../validations/project.js"; +import { uploadDokumenProspektusMiddleware, uploadMiddleware, uploadUpdateProjectMiddleware } from "../middlewares/project.js"; +import { adminOnly, platinumOnly, validateToken } from "../middlewares/auth.js"; +import { AgreementMiddleware } from "../middlewares/agreement-letter.js"; +import { createAgreementLetterHandler, getAgreementLetterByProjectIdHandler, getAllAgreementLetterHandler } from "../controllers/agreement-letter.js"; + +const router = express.Router(); + +router.post("/", validateToken, uploadMiddleware, validateResource(CreateProjectValidation), createProjectHandler); +router.get("/count", validateToken, adminOnly, countProjectHandler); +router.get("/", validateToken, platinumOnly, getAllProjectHandler); +router.get("/user", validateToken, getProjectByUserIdHandler); +router.get("/check-funding", validateToken, adminOnly, checkProjectFundingOpenedHandler); +router.put("/update", validateToken, uploadUpdateProjectMiddleware, validateResource(UpdateProjectValidation), updateProjectHandler); +router.post("/agreement-letter", validateToken, AgreementMiddleware, createAgreementLetterHandler); +router.get("/agreement-letter", validateToken, adminOnly, getAllAgreementLetterHandler); +router.get("/:id/agreement-letter", validateToken, getAgreementLetterByProjectIdHandler); +router.put("/publish", validateToken, adminOnly, uploadDokumenProspektusMiddleware, publishProjectHandler); +router.put("/accept", validateToken, adminOnly, acceptProjectHandler); +router.put("/complete", validateToken, adminOnly, completingProjectHandler); +router.put("/share-profit", validateToken, adminOnly, shareProfitHandler); +router.get("/:id/user", validateToken, getUserHaveTokenInProjectHandler); +router.get("/:id", validateToken, getProjectByIdHandler); +router.put("/approve/:id", validateToken, adminOnly, approveProjectHandler); +router.put("/revise/:id", validateToken, adminOnly, reviseProjectHandler); +router.put("/reject/:id", validateToken, adminOnly, rejectProjectHandler); +router.get("/keterangan-revisi/:id", validateToken, getKeteranganReviseProjectByIdHandler); +router.get("/:id/dokumen-prospektus", validateToken, getDokumenProspektusByIdHandler); +router.get("/total-profit/:id", validateToken, adminOnly, totalProfitHandler); + +export default router; diff --git a/services/backend/src/routes/topup.ts b/services/backend/src/routes/topup.ts new file mode 100644 index 0000000..7629b44 --- /dev/null +++ b/services/backend/src/routes/topup.ts @@ -0,0 +1,48 @@ +import express from "express"; +import { + getTopupByUserIdHandler, + payMemberHandler, + payTopupHandler, + accTopupHandler, + withdrawSaldoHandler, + accWithdrawSaldoHandler, + paySimpananWajibHandler, + accSimpananWajibHandler, + getWithdrawSaldoHandler, + getTopupByIdHandler, + getTotalWalletSaldoByUserIdHandler, + getTotalWalletSimpananWajibByUserIdHandler, + getTotalWalletSimpananPokokByUserIdHandler, + getBagianPemilikPelaksanaHandler, + payBagianPemilikPelaksanaHandler, + getKasKoperasiHandler, + getAllTopupHandler, + getWalletSaldoByUserIdHandler, + updateWalletByIdHandler, +} from "../controllers/topup.js"; +import { validateToken, adminOnly } from "../middlewares/auth.js"; +import { uploadBuktiMiddleware } from "../middlewares/topup.js"; +import validateResource from "../middlewares/validate.resource.js"; +import { PayMemberValidation, PaySimpananWajibValidation, PayTopupValidation, WithdrawSaldoValidation } from "../validations/topup.js"; +const router = express.Router(); + +router.get("/", validateToken, adminOnly, getAllTopupHandler); +router.get("/user", validateToken, getTopupByUserIdHandler); +router.get("/simpanan_pokok", validateToken, getTotalWalletSimpananPokokByUserIdHandler); +router.get("/simpanan_wajib", validateToken, getTotalWalletSimpananWajibByUserIdHandler); +router.get("/saldo", validateToken, adminOnly, getWithdrawSaldoHandler); +router.get("/saldo/user", validateToken, getTotalWalletSaldoByUserIdHandler); +router.get("/saldo/user/:id", validateToken, getWalletSaldoByUserIdHandler); +router.get("/kas-koperasi", validateToken, getKasKoperasiHandler); +router.get("/bagian-pemilik-pelaksana", validateToken, adminOnly, getBagianPemilikPelaksanaHandler); +router.get("/:id", validateToken, getTopupByIdHandler); +router.post("/pay-member", validateToken, uploadBuktiMiddleware, validateResource(PayMemberValidation), payMemberHandler); +router.post("/pay-topup", validateToken, uploadBuktiMiddleware, validateResource(PayTopupValidation), payTopupHandler); +router.post("/acc-topup", validateToken, adminOnly, accTopupHandler); +router.post("/withdraw", validateToken, validateResource(WithdrawSaldoValidation), withdrawSaldoHandler); +router.post("/acc-withdraw", validateToken, adminOnly, uploadBuktiMiddleware, accWithdrawSaldoHandler); +router.post("/pay-simpanan-wajib", validateToken, uploadBuktiMiddleware, validateResource(PaySimpananWajibValidation), paySimpananWajibHandler); +router.post("/acc-simpanan-wajib", validateToken, adminOnly, accSimpananWajibHandler); +router.post("/pay-bagian-pemilik-pelaksana", validateToken, adminOnly, uploadBuktiMiddleware, payBagianPemilikPelaksanaHandler); +router.put("/wallet/:id", validateToken, adminOnly, updateWalletByIdHandler); +export default router; diff --git a/services/backend/src/routes/transaction.ts b/services/backend/src/routes/transaction.ts new file mode 100644 index 0000000..a998c48 --- /dev/null +++ b/services/backend/src/routes/transaction.ts @@ -0,0 +1,14 @@ +import express from "express"; +import { + getAllTransactionHandler, + getTransactionByProjectIdHandler, + getTransactionByUserIdHandler, +} from "../controllers/transaction.js"; +import { validateToken } from "../middlewares/auth.js"; +const router = express.Router(); + +router.get("/", validateToken, getAllTransactionHandler); +router.get("/user", validateToken, getTransactionByUserIdHandler); +router.get("/project/:id", validateToken, getTransactionByProjectIdHandler); + +export default router; \ No newline at end of file diff --git a/services/backend/src/routes/user.ts b/services/backend/src/routes/user.ts new file mode 100644 index 0000000..86d712a --- /dev/null +++ b/services/backend/src/routes/user.ts @@ -0,0 +1,34 @@ +import express from "express"; +import { adminOnly, validateToken } from "../middlewares/auth.js"; +import { + accUpgradeUserToPlatinumHandler, + countUserHandler, + deleteUserByIdHandler, + getAllUserHandler, + getPhoneNumberAdminHandler, + getUserByIdHandler, + rejectUserByIdHandler, + sendOtpHandler, + updateUserByIdHandler, + upgradeUserToPlatinumHandler, + verifyOtpHandler, +} from "../controllers/user.js"; +import { parseFormData, formDataParserMiddleware } from "../middlewares/auth.js"; +import { uploadBuktiMiddleware } from "../middlewares/topup.js"; +import validateResource from "../middlewares/validate.resource.js"; +import { UpgradePlatinumValidation } from "../validations/user.js"; + +const router = express.Router(); +router.get("/", validateToken, adminOnly, getAllUserHandler); +router.get("/count", validateToken, adminOnly, countUserHandler); +router.get("/phone-number", validateToken, getPhoneNumberAdminHandler); +router.get("/:id", validateToken, getUserByIdHandler); +router.put("/:id", validateToken, parseFormData, formDataParserMiddleware, updateUserByIdHandler); +router.delete("/:id", validateToken, adminOnly, deleteUserByIdHandler); +router.put("/reject/:id", validateToken, adminOnly, rejectUserByIdHandler); +router.post("/send-otp", validateToken, adminOnly, sendOtpHandler); +router.post("/verify-otp", validateToken, verifyOtpHandler); +router.post("/upgrade-platinum", validateToken, uploadBuktiMiddleware, validateResource(UpgradePlatinumValidation), upgradeUserToPlatinumHandler); +router.post("/acc-upgrade-platinum", validateToken, adminOnly, accUpgradeUserToPlatinumHandler); + +export default router; diff --git a/services/backend/src/routes/wilayah.ts b/services/backend/src/routes/wilayah.ts new file mode 100644 index 0000000..0e8343f --- /dev/null +++ b/services/backend/src/routes/wilayah.ts @@ -0,0 +1,11 @@ +import express from "express"; +import * as wilayahController from "../controllers/wilayah.js"; + +const router = express.Router(); + +router.get("/provinces", wilayahController.getProvincesHandler); +router.get("/regencies/:province_code", wilayahController.getRegenciesHandler); +router.get("/districts/:regency_code", wilayahController.getDistrictHandler); +router.get("/villages/:district_code", wilayahController.getVillagesHandler); + +export default router; \ No newline at end of file diff --git a/services/backend/src/services/agreement-letter.ts b/services/backend/src/services/agreement-letter.ts new file mode 100644 index 0000000..3366159 --- /dev/null +++ b/services/backend/src/services/agreement-letter.ts @@ -0,0 +1,189 @@ +import { db } from "../drizzle/db.js"; +import { HistoryProjectTable, ProjectTable, SignatureAdminTable, UserTable } from "../drizzle/schema.js"; +import { desc, eq } from "drizzle-orm"; +import { getContract } from "../main.js"; + +export const createAgreementLetter = async (data: any) => { + try { + const contract = await getContract(); + + // Check if project exists + const project = await db.select().from(ProjectTable).where(eq(ProjectTable.id, data.id_projek)).execute(); + if (project.length === 0) { + throw new Error("Project not found"); + } + + // Get project owner details + const projectOwner = await db.select().from(UserTable).where(eq(UserTable.id, project[0].id_user)).execute(); + if (projectOwner.length === 0) { + throw new Error("User associated with the project not found"); + } + + // Get admin user details + const admin = await db.select().from(UserTable).where(eq(UserTable.role, "ADMIN")).execute(); + if (admin.length === 0) { + throw new Error("Admin user not found"); + } + + // Get the latest admin signature + const adminSignature = await db.select().from(SignatureAdminTable).where(eq(SignatureAdminTable.id_user, admin[0].id)).orderBy(desc(SignatureAdminTable.created_at)).limit(1).execute(); + if (adminSignature.length === 0) { + throw new Error("Admin signature not found"); + } + + // Ensure all values are strings and handle null/undefined values + const params = { + idProjek: data.id_projek || "", + idUser: projectOwner[0].id || "", + namaProyek: project[0].judul || "", + namaPetugas: admin[0].nama || "", + alamatPetugas: admin[0].alamat || "", + namaPemilikProyek: projectOwner[0].nama || "", + nik: projectOwner[0].nik || "", + noHp: projectOwner[0].no_hp || "", + alamat: projectOwner[0].alamat || "", + adminSignature: adminSignature[0].signature || "", + ownerSignature: data.tanda_tangan || "", + nominalDisetujui: project[0].nominal_disetujui || 0, + }; + + // Create agreement letter with both signatures + const transaction = await contract.createAgreementLetter( + params.idProjek, + params.idUser, + params.namaProyek, + params.namaPetugas, + params.alamatPetugas, + params.namaPemilikProyek, + params.nik, + params.noHp, + params.alamat, + params.adminSignature, + params.ownerSignature, + BigInt(params.nominalDisetujui) // Convert to BigInt for Solidity uint256 + ); + + await transaction.wait(); + + // Create history entries + await db + .insert(HistoryProjectTable) + .values([ + { + id_projek: data.id_projek, + history: "Kontrak Perjanjian", + keterangan: "Kontrak perjanjian sudah ditandatangani oleh pemilik proyek dan admin", + status: "SUCCESS", + }, + { + id_projek: data.id_projek, + history: "Proses Penggalangan Penyertaan Modal", + keterangan: "Menunggu proyek dipublish", + status: "PENDING", + }, + ]) + .execute(); + + return { + status: "success", + message: "Agreement letter created successfully", + data: { + projectId: params.idProjek, + adminSignature: params.adminSignature, + ownerSignature: params.ownerSignature, + timestamp: Date.now(), + }, + }; + } catch (error) { + console.error("Error creating agreement letter:", error); + throw error; // Re-throw error for controller to handle it + } +}; + + +function formatTimestamp(timestamp: string): string { + try { + const date = new Date(parseInt(timestamp) * 1000); + return date.toISOString(); + } catch (error) { + console.error("Error formatting timestamp:", error); + return timestamp; + } +} + +export async function getAllAgreementLetter(search?: string) { + let agreementLetter; + try { + const contract = await getContract(); + const agreements = await contract.getAllAgreement(); + + agreementLetter = agreements.map((agreement: any) => ({ + idProjek: agreement.idProjek, + idUser: agreement.idUser, + namaProyek: agreement.namaProyek, + namaPetugas: agreement.namaPetugas, + alamatPetugas: agreement.alamatPetugas, + namaPemilikProyek: agreement.namaPemilikProyek, + nik: agreement.nik, + noHp: agreement.noHp, + alamat: agreement.alamat, + signature: agreement.signature, + tandaTangan: agreement.tandaTangan, + nominalDisetujui: agreement.nominalDisetujui.toString(), + createdAt: formatTimestamp(agreement.createdAt.toString()), + })); + + // Cek apakah tidak ada agreement letter + if (agreementLetter.length === 0) { + const error = new Error("No agreement letters found"); + (error as any).statusCode = 404; // Set statusCode untuk error + throw error; // Lempar error untuk ditangkap di controller + } + } catch (error) { + console.error("Error fetching agreements:", error); + throw error; // Re-throw error agar bisa ditangkap di controller + } + + // Filter berdasarkan search query jika ada + if (search) { + agreementLetter = agreementLetter.filter((agreement: any) => agreement.namaPemilikProyek.toLowerCase().includes(search.toLowerCase())); + } + + return agreementLetter; +} + + +export async function getAgreementByProjectId(idProjek: string) { + let projectAgreements; + try { + const contract = await getContract(); + projectAgreements = await contract.getAgreementByProjectId(idProjek); + + projectAgreements = projectAgreements.map((agreement: any) => ({ + idProjek: agreement.idProjek, + idUser: agreement.idUser, + namaProyek: agreement.namaProyek, + namaPetugas: agreement.namaPetugas, + alamatPetugas: agreement.alamatPetugas, + namaPemilikProyek: agreement.namaPemilikProyek, + nik: agreement.nik, + noHp: agreement.noHp, + alamat: agreement.alamat, + signature: agreement.signature, + tandaTangan: agreement.tandaTangan, + nominalDisetujui: agreement.nominalDisetujui.toString(), + createdAt: formatTimestamp(agreement.createdAt.toString()), + })); + } catch (error) { + console.error("Error fetching agreements by project ID:", error); + throw new Error("Error fetching agreements from the smart contract"); + } + + if (projectAgreements.length === 0) { + const error = new Error("No agreements found for this project ID"); + (error as any).statusCode = 404; + throw error; + } + + return projectAgreements; +} diff --git a/services/backend/src/services/auth.ts b/services/backend/src/services/auth.ts new file mode 100644 index 0000000..69c4fe7 --- /dev/null +++ b/services/backend/src/services/auth.ts @@ -0,0 +1,52 @@ +import { db } from "../drizzle/db.js"; +import { UserTable } from "../drizzle/schema.js"; +import bcrypt from "bcryptjs"; +import { eq } from "drizzle-orm"; + +export const register = async (user: any): Promise<{ success: boolean; message: string; statusCode: number }> => { + try { + const tanggalLahir = typeof user.tanggal_lahir === "string" ? user.tanggal_lahir : user.tanggal_lahir.toISOString().split('T')[0]; + + const hashedPassword = await bcrypt.hash(user.password, 10); + + const userData = { + ...user, + password: hashedPassword, + tanggal_lahir: tanggalLahir, + }; + + await db.insert(UserTable).values(userData).execute(); + + return { success: true, message: "User successfully registered", statusCode: 201 }; + } catch (e: any) { + console.error(e); + + if (e.code === "23505" && e.detail.includes("Key (no_hp)")) { + return { success: false, message: "Phone number is already in use by another user", statusCode: 409 }; + } + + return { success: false, message: "User unsuccessfully registered", statusCode: 400 }; + } +}; + +export async function login(no_hp: string, password: string): Promise { + try { + const user = await db.select().from(UserTable).where(eq(UserTable.no_hp, no_hp)).limit(1).execute(); + + if (!user[0] || !(await bcrypt.compare(password, user[0].password))) { + const error = new Error("Invalid username or password"); + (error as any).statusCode = 401; + throw error; + } + + return user[0]; + } catch (error) { + console.error(error); + if ((error as any).statusCode) { + throw error; + } + const e = new Error("Login failed due to server error"); + (e as any).statusCode = 500; + throw e; + } +} diff --git a/services/backend/src/services/baileys.ts b/services/backend/src/services/baileys.ts new file mode 100644 index 0000000..7d3b37c --- /dev/null +++ b/services/backend/src/services/baileys.ts @@ -0,0 +1,80 @@ +import { makeWASocket, DisconnectReason, useMultiFileAuthState } from "@whiskeysockets/baileys"; +import fs from "fs"; + +let sock: any; +export let qrCode: string | null = null; +export let isConnected: boolean = false; + +const startSock = async () => { + const { state, saveCreds } = await useMultiFileAuthState("./auth_baileys"); + + sock = makeWASocket({ + printQRInTerminal: true, + auth: state, + }); + + sock.ev.on("connection.update", async (update: any) => { + const { connection, lastDisconnect, qr } = update; + + if (qr) { + qrCode = qr; + } + + if (connection === "close") { + isConnected = false; + const statusCode = (lastDisconnect?.error as any)?.output?.statusCode; + + if (statusCode === DisconnectReason.loggedOut) { + console.log("Logged out, resetting auth state..."); + await resetAuthState(); + await startSock(); + } else { + console.log("Connection closed with status code:", statusCode); + await startSock(); + } + } else if (connection === "open") { + isConnected = true; + qrCode = null; + console.log("WhatsApp connection opened."); + } + + console.log("Connection update:", update); + }); + + sock.ev.on("creds.update", saveCreds); + + sock.ev.on("messages.upsert", async (m: any) => { + const msg = m.messages[0]; + if (!msg.key.fromMe && m.type === "notify") { + console.log("Sender Number:", msg.key.remoteJid); + console.log("Message:", msg.message.conversation); + } + }); +}; + +const resetAuthState = async () => { + const authDir = "./auth_baileys"; + + if (fs.existsSync(authDir)) { + fs.rmSync(authDir, { recursive: true, force: true }); + console.log("Auth state reset successfully."); + } else { + console.warn("Auth state directory does not exist."); + } +}; + +export const sendTextWA = async (number: string, message: string) => { + const formattedNumber = `${number}@s.whatsapp.net`; + try { + if (!isConnected) { + throw new Error("WhatsApp is not connected."); + } + + await sock.sendMessage(formattedNumber, { text: message }); + console.log(`Message sent to ${number}`); + } catch (error) { + console.error("Failed to send message:", error); + } +}; + +startSock(); diff --git a/services/backend/src/services/chart-project.ts b/services/backend/src/services/chart-project.ts new file mode 100644 index 0000000..1a20a67 --- /dev/null +++ b/services/backend/src/services/chart-project.ts @@ -0,0 +1,45 @@ +import { eq, sql, sum } from "drizzle-orm"; +import { db } from "../drizzle/db.js"; +import { ChartProjectTable, ProjectTable, UserTable } from "../drizzle/schema.js"; + +export async function getChartProjectById(id: string) { + let query = db + .select({chart: ChartProjectTable, project: ProjectTable}) + .from(ChartProjectTable) + .innerJoin(ProjectTable, eq(ChartProjectTable.id_projek, ProjectTable.id)) + .where(eq(ChartProjectTable.id_projek, id)); + + const chartProject = await query.execute(); + + const groupedChartProject = chartProject.reduce((acc, curr) => { + const chartId = curr.chart.id; + + if (!acc[chartId]) { + acc[chartId] = { + ...curr.chart, + project: curr.project, + }; + } + + return acc; + }, {} as { [key: string]: any }); + + return Object.values(groupedChartProject); +} + +export async function getAllChartProjectByUserId(id: string) { + const chartProject = await db + .select({ + month: sql`extract(month from ${ChartProjectTable.created_at})`.as("month"), + year: sql`extract(year from ${ChartProjectTable.created_at})`.as("year"), + sum_nominal: sum(ChartProjectTable.nominal).as("sum_nominal"), + }) + .from(ChartProjectTable) + .innerJoin(ProjectTable, eq(ChartProjectTable.id_projek, ProjectTable.id)) + .innerJoin(UserTable, eq(ProjectTable.id_user, UserTable.id)) + .where(eq(UserTable.id, id)) + .groupBy(sql`extract(year from ${ChartProjectTable.created_at})`, sql`extract(month from ${ChartProjectTable.created_at})`) + .execute(); + + return chartProject; +} diff --git a/services/backend/src/services/chart-token.ts b/services/backend/src/services/chart-token.ts new file mode 100644 index 0000000..b9ac2a4 --- /dev/null +++ b/services/backend/src/services/chart-token.ts @@ -0,0 +1,98 @@ +import { eq } from "drizzle-orm"; +import { db } from "../drizzle/db.js"; +import { ProjectTable } from "../drizzle/schema.js"; +import { getContract } from "../main.js"; + +export async function getLastChartTokenByUserIdandProjectId(id_user: any, id_projek: any) { + const contract = await getContract(); + + const chartToken = await contract.getLatestChartTokenByUserIdAndProjectId(id_user, id_projek); + + if (!chartToken || !chartToken.nominal) { + return null; + } + + const serializedChartToken = { + id: chartToken.chartTokenId, + id_user: chartToken.idUser, + id_projek: chartToken.idProjek, + nominal: Number(chartToken.nominal), + }; + + console.log("Serialized ChartToken:", serializedChartToken); + + const project = await db.select().from(ProjectTable).where(eq(ProjectTable.id, id_projek)).execute(); + + if (!project || !project[0].nominal_disetujui) { + throw new Error("Project not found or nominal not approved"); + } + + const persentase = ((serializedChartToken.nominal / project[0].nominal_disetujui) * 100).toFixed(2); + + return { perubahan: serializedChartToken.nominal, persentase }; +} + +export async function getChartTokenByUserIdandProjectId(id_user: string, id_projek: string) { + const contract = await getContract(); + + const chartTokens = await contract.getChartTokensByUserIdAndProjectId(id_user, id_projek); + console.log(chartTokens); + + if (!chartTokens || chartTokens.length === 0) { + const error = new Error("Chart Token not found"); + (error as any).statusCode = 404; + throw error; + } + + const serializedChartTokens = chartTokens.map((token: any) => ({ + id: token.chartTokenId, + id_user: token.idUser, + id_projek: token.idProjek, + nominal: Number(token.nominal), + })); + + return serializedChartTokens; +} + +export async function getAllChartTokenByUserId(id_user: string) { + const contract = await getContract(); + + const chartTokens = await contract.getAllChartTokensByUserId(id_user); + + if (!chartTokens || chartTokens.length === 0) { + const error = new Error("Chart Token not found"); + (error as any).statusCode = 404; + throw error; + } + + const serializedChartTokens = chartTokens.map((token: any) => ({ + created_at: new Date(Number(token.createdAt) * 1000), + nominal: Number(token.nominal), + id_projek: token.idProjek, + id_user: token.idUser, + })); + + const chartProject = serializedChartTokens.reduce((acc: any[], token: any) => { + const date = token.created_at; + const month = date.getMonth() + 1; + const year = date.getFullYear(); + + const existingGroup = acc.find( + (item) => item.month === month && item.year === year + ); + + if (existingGroup) { + existingGroup.sum_nominal += token.nominal; + } else { + acc.push({ + month, + year, + sum_nominal: token.nominal, + }); + } + + return acc; + }, []); + + return chartProject; +} diff --git a/services/backend/src/services/history-project-wallet.ts b/services/backend/src/services/history-project-wallet.ts new file mode 100644 index 0000000..d681afe --- /dev/null +++ b/services/backend/src/services/history-project-wallet.ts @@ -0,0 +1,41 @@ +import { desc, eq } from "drizzle-orm"; +import { db } from "../drizzle/db.js"; +import { HistoryProjectWalletTable, ProjectWalletTable } from "../drizzle/schema.js"; + +export async function getHistoryProjectWalletByProjectWalletId(id: string) { + const historyProjectWallet = await db.select().from(HistoryProjectWalletTable).where(eq(HistoryProjectWalletTable.id_project_wallet, id)).orderBy(desc(HistoryProjectWalletTable.created_at)).execute(); + + if (historyProjectWallet.length === 0) { + const error = new Error("History Project Wallet not found"); + (error as any).statusCode = 404; + throw error; + } + + return historyProjectWallet; +} + +export async function createHistoryProjectWallet(historyProjectWallet: any) { + const projectWallet = await db.select().from(ProjectWalletTable).where(eq(ProjectWalletTable.id, historyProjectWallet.id_project_wallet)).execute(); + + if (!projectWallet || projectWallet.length === 0) { + const error = new Error("Project Wallet not found"); + (error as any).statusCode = 404; + throw error; + } + + if (!historyProjectWallet.nominal || historyProjectWallet.nominal <= 0) { + const error = new Error("Invalid nominal value"); + (error as any).statusCode = 400; + throw error; + } + + if (!historyProjectWallet.deskripsi || historyProjectWallet.deskripsi.trim() === "") { + const error = new Error("Description is required"); + (error as any).statusCode = 400; + throw error; + } + + await db.insert(HistoryProjectWalletTable).values({...historyProjectWallet}).execute(); + + return { message: "History Project Wallet created successfully" }; +} diff --git a/services/backend/src/services/history-project.ts b/services/backend/src/services/history-project.ts new file mode 100644 index 0000000..6047475 --- /dev/null +++ b/services/backend/src/services/history-project.ts @@ -0,0 +1,15 @@ +import { asc, eq } from "drizzle-orm"; +import { db } from "../drizzle/db.js"; +import { HistoryProjectTable } from "../drizzle/schema.js"; + +export async function getHistoryProjectByProjectId(id: string) { + const historyProject = await db.select().from(HistoryProjectTable).where(eq(HistoryProjectTable.id_projek, id)).orderBy(asc(HistoryProjectTable.created_at)).execute(); + + if (historyProject.length === 0) { + const error = new Error("History Project not found"); + (error as any).statusCode = 404; + throw error; + } + + return historyProject; +} \ No newline at end of file diff --git a/services/backend/src/services/history-token.ts b/services/backend/src/services/history-token.ts new file mode 100644 index 0000000..3eabe14 --- /dev/null +++ b/services/backend/src/services/history-token.ts @@ -0,0 +1,64 @@ +import { eq } from "drizzle-orm"; +import { db } from "../drizzle/db.js"; +import { + ProjectTable, +} from "../drizzle/schema.js"; +import { getContract } from "../main.js"; + +export async function getHistoryTokenByUserIdAndProjectId( + id_user: string, + id_projek: string +) { + const contract = await getContract(); + + const project = await db + .select({ + nominal_disetujui: ProjectTable.nominal_disetujui, + }) + .from(ProjectTable) + .where(eq(ProjectTable.id, id_projek)) + .execute(); + + if (project.length === 0 || !project[0].nominal_disetujui) { + const error = new Error("Project not found or nominal not approved"); + (error as any).statusCode = 404; + throw error; + } + + const nominalDisetujui = project[0].nominal_disetujui; + + const chartTokens = await contract.getChartTokensByUserIdAndProjectId( + id_user, + id_projek + ); + + if (chartTokens.length === 0) { + const error = new Error("Chart Tokens not found"); + (error as any).statusCode = 404; + throw error; + } + + const results = []; + + for (const chartToken of chartTokens) { + const historyToken = await contract.getHistoryTokenByChartTokenId(chartToken.chartTokenId); + + if (!historyToken) { + throw new Error(`No history token found for chartTokenId: ${chartToken.chartTokenId}`); + } + + const persentase = ((Number(chartToken.nominal) / nominalDisetujui) * 100).toFixed(2); + + results.push({ + nilai: Number(historyToken.totalNilai), + perubahan: Number(chartToken.nominal), + persentase, + totalNominalToken: Number(await contract.getTotalNominalToken(id_user, id_projek)), + created_at: new Date(Number(historyToken.createdAt) * 1000), + }); + + // console.log(results); + } + + return results; +} diff --git a/services/backend/src/services/jwt.ts b/services/backend/src/services/jwt.ts new file mode 100644 index 0000000..b761379 --- /dev/null +++ b/services/backend/src/services/jwt.ts @@ -0,0 +1,13 @@ +import jwt from "jsonwebtoken"; + +const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key"; + +export const tokenBlacklist: { [key: string]: number } = {}; + +export function generateToken(payload: object) { + return jwt.sign(payload, JWT_SECRET, { expiresIn: "7 days" }); +} + +export function verifyToken(token: string) { + return jwt.verify(token, JWT_SECRET); +} diff --git a/services/backend/src/services/mutation.ts b/services/backend/src/services/mutation.ts new file mode 100644 index 0000000..28b9b39 --- /dev/null +++ b/services/backend/src/services/mutation.ts @@ -0,0 +1,393 @@ +import { desc, eq, like, or, sql, sum } from "drizzle-orm"; +import { db } from "../drizzle/db.js"; +import { differenceInMonths } from "date-fns"; +import { + ChartProjectTable, + ProjectMutationReportTable, + ProjectTable, +} from "../drizzle/schema.js"; +import { getProjectById } from "./project.js"; +import { getContract } from "../main.js"; + +export async function createMutation(mutation: any) { + try { + const project = await db + .select() + .from(ProjectTable) + .where(eq(ProjectTable.id, mutation.id_projek)) + .execute(); + + if (project.length === 0) { + const error = new Error("Project not found"); + (error as any).statusCode = 404; + throw error; + } + + if (!project[0].report_progress) { + const error = new Error("Report Progress not found"); + (error as any).statusCode = 404; + throw error; + } + + const chartProjectValue = mutation.pemasukan - mutation.pengeluaran; + // const reportFrequency = parseInt(project[0].report_progress); + + // const lastMutation = await db + // .select() + // .from(ProjectMutationReportTable) + // .where(eq(ProjectMutationReportTable.id_projek, mutation.id_projek)) + // .orderBy(desc(ProjectMutationReportTable.created_at)) + // .limit(1) + // .execute(); + + // const monthsSinceLastReport = + // lastMutation.length === 0 + // ? differenceInMonths(new Date(), project[0].created_at) + // : differenceInMonths(new Date(), new Date(lastMutation[0].created_at)); + + // if (monthsSinceLastReport < reportFrequency) { + // const error = new Error( + // `You can upload the next report after ${ + // reportFrequency - monthsSinceLastReport + // } months.` + // ); + // (error as any).statusCode = 400; + // throw error; + // } + + await insertProjectMutationAndChart(mutation, chartProjectValue); + + return { + message: + // lastMutation.length === 0 + // ? "First report uploaded successfully." + // : "Report uploaded successfully.", + "Report uploaded successfully." + }; + } catch (error: any) { + if (!error.statusCode) { + error = new Error( + "An unexpected error occurred during mutation creation" + ); + (error as any).statusCode = 500; + } + throw error; + } +} + +async function insertProjectMutationAndChart( + mutation: any, + chartProjectValue: number +) { + try { + const contract = await getContract(); + + const tokens = await contract.getTokenByProjectId(mutation.id_projek); + + const mutationResult = await db + .insert(ProjectMutationReportTable) + .values(mutation) + .execute(); + if (!mutationResult) { + const error = new Error("Failed to insert project mutation report"); + (error as any).statusCode = 500; + throw error; + } + + const chartResult = await db + .insert(ChartProjectTable) + .values({ id_projek: mutation.id_projek, nominal: chartProjectValue }) + .execute(); + if (!chartResult) { + const error = new Error("Failed to insert into chart project table"); + (error as any).statusCode = 500; + throw error; + } + + const totalKomulatif = await db + .select({ value: sum(ChartProjectTable.nominal) }) + .from(ChartProjectTable) + .where(eq(ChartProjectTable.id_projek, mutation.id_projek)) + .execute(); + + if (totalKomulatif.length === 0) { + const error = new Error("Failed to calculate total cumulative value"); + (error as any).statusCode = 500; + throw error; + } + + const modalProject = await db + .select({ modal: ProjectTable.nominal_disetujui }) + .from(ProjectTable) + .where(eq(ProjectTable.id, mutation.id_projek)) + .execute(); + + if (modalProject.length === 0) { + const error = new Error("Project modal not found"); + (error as any).statusCode = 404; + throw error; + } + + const project = await getProjectById(mutation.id_projek); + interface GroupedToken { + id_user: string; + totalNilai: number; + totalNominal: number; + } + + // const groupedTokens: Record = {}; + const dividenProfit = await contract.getDividenProfitByProjectId(mutation.id_projek); + console.log("Dividen Profit:", dividenProfit); + + const groupedTokens = tokens.reduce( + (acc: Record, token: any) => { + console.log(token); + const id_user = token.idUser; + const nilai = parseFloat(token.nilai); + + if (!totalKomulatif[0].value) { + const error = new Error("Failed to calculate total cumulative value"); + (error as any).statusCode = 500; + throw error; + } + + if (!modalProject[0].modal) { + const error = new Error("Failed to calculate modal project value"); + (error as any).statusCode = 500; + throw error; + } + + if (!dividenProfit.pendana) { + const error = new Error("Project not found"); + (error as any).statusCode = 404; + throw error; + } + + const totalNilai = + (Number(totalKomulatif[0].value) / modalProject[0].modal) * + nilai * + (project.bagian_pendana / 100) + + nilai; + const totalNominal = + (Number(totalKomulatif[0].value) / modalProject[0].modal) * + nilai * + (project.bagian_pendana / 100); + + if (acc[id_user]) { + acc[id_user].totalNilai += Math.round(totalNilai); + acc[id_user].totalNominal += Math.round(totalNominal); + } else { + acc[id_user] = { + id_user: id_user, + totalNilai: Math.round(totalNilai), + totalNominal: Math.round(totalNominal), + }; + } + + return acc; + }, + {} + ); + + const groupedTokensArray: GroupedToken[] = Object.values(groupedTokens); + + if (!groupedTokens) { + const error = new Error("No tokens found for the project"); + (error as any).statusCode = 404; + throw error; + } + + for (const group of groupedTokensArray) { + console.log("Processing group:", group); + try { + console.log("Before addChartToken for user:", group.id_user); + const chartTokenTx = await contract.addChartToken( + group.id_user, + mutation.id_projek, + group.totalNominal, + group.totalNilai + ); + console.log("Transaction hash:", chartTokenTx.hash); + const chartTokenReceipt = await chartTokenTx.wait(); + console.log("Transaction confirmed:", chartTokenReceipt); + } catch (error) { + console.error( + "Error during contract transaction for user:", + group.id_user, + error + ); + } + } + } catch (error: any) { + if (!error.statusCode) { + error = new Error("An unexpected error occurred during insertion"); + (error as any).statusCode = 500; + } + throw error; + } +} + +export async function getAllMutation(search?: string) { + try { + const query = db + .select({ + mutation: ProjectMutationReportTable, + project: ProjectTable, + }) + .from(ProjectMutationReportTable) + .innerJoin( + ProjectTable, + eq(ProjectTable.id, ProjectMutationReportTable.id_projek) + ) + .orderBy(desc(ProjectMutationReportTable.created_at)); + + if (search) { + query.where( + or( + like(ProjectTable.judul, `%${search}%`), + like(ProjectMutationReportTable.judul, `%${search}%`) + ) + ); + } + + const mutationResult = await query.execute(); + console.log(mutationResult); + + if (!mutationResult || mutationResult.length === 0) { + const error = new Error("No mutations found"); + (error as any).statusCode = 404; + throw error; + } + + const groupedMutations = mutationResult.reduce((acc, curr) => { + const mutationId = curr.mutation.id; + + if (!acc[mutationId]) { + acc[mutationId] = { + ...curr.mutation, + project: curr.project, + }; + } + + return acc; + }, {} as { [key: string]: any }); + return Object.values(groupedMutations); + } catch (error: any) { + if (!error.statusCode) { + error = new Error( + "An unexpected error occurred while fetching all mutations" + ); + (error as any).statusCode = 500; + } + throw error; + } +} + +export async function getMutationById(id: string) { + try { + const mutation = await db + .select() + .from(ProjectMutationReportTable) + .where(eq(ProjectMutationReportTable.id, id)) + .execute(); + + if (!mutation || mutation.length === 0) { + const error = new Error(`Mutation with ID ${id} not found`); + (error as any).statusCode = 404; + throw error; + } + + return mutation; + } catch (error: any) { + if (!error.statusCode) { + error = new Error( + `An unexpected error occurred while fetching mutation with ID ${id}` + ); + (error as any).statusCode = 500; + } + throw error; + } +} + +export async function getMutationByProjectId(id: string) { + try { + const project = await db + .select() + .from(ProjectTable) + .where(eq(ProjectTable.id, id)) + .execute(); + + if (!project || project.length === 0) { + const error = new Error(`Project with ID ${id} not found`); + (error as any).statusCode = 404; + throw error; + } + + const mutation = await db + .select() + .from(ProjectMutationReportTable) + .where(eq(ProjectMutationReportTable.id_projek, id)) + .orderBy(desc(ProjectMutationReportTable.created_at)) + .execute(); + + const lastMutation = await db + .select() + .from(ProjectMutationReportTable) + .where(eq(ProjectMutationReportTable.id_projek, id)) + .orderBy(desc(ProjectMutationReportTable.created_at)) + .limit(1) + .execute(); + + if (lastMutation.length === 0) { + const error = new Error("No reports yet, you can upload the first report."); + (error as any).statusCode = 404; + (error as any).canUploadReport = true; + throw error; + } + + + const lastReportDate = new Date(lastMutation[0].created_at); + if (!project[0].report_progress) { + const error = new Error("Report Progress not found"); + (error as any).statusCode = 404; + throw error; + } + const reportFrequency = parseInt(project[0].report_progress); + + const monthsSinceProjectCreated = differenceInMonths( + new Date(), + new Date(project[0].created_at) + ); + const monthsSinceLastReport = differenceInMonths( + new Date(), + lastReportDate + ); + + if ( + monthsSinceProjectCreated < reportFrequency || + monthsSinceLastReport < reportFrequency + ) { + return { + mutation, + canUploadReport: false, + message: `You can upload the next report after ${ + reportFrequency - monthsSinceLastReport + } months.`, + }; + } + + return { + mutation, + canUploadReport: true, + message: "You can upload the report now.", + }; + } catch (error: any) { + if (!error.statusCode) { + error = new Error( + `An unexpected error occurred while fetching mutation for project with ID ${id}` + ); + (error as any).statusCode = 500; + } + throw error; + } +} diff --git a/services/backend/src/services/project-category.ts b/services/backend/src/services/project-category.ts new file mode 100644 index 0000000..6b7ca74 --- /dev/null +++ b/services/backend/src/services/project-category.ts @@ -0,0 +1,60 @@ +import { db } from "../drizzle/db.js"; +import { ProjectCategoryTable } from "../drizzle/schema.js"; +import { eq, like } from "drizzle-orm"; + +export const createProjectCategory = async (projectCategory: any): Promise<{ message: string }> => { + if (!projectCategory || !projectCategory.kategori) { + throw new Error("Category is required"); + } + + const categoryLower = projectCategory.kategori.toLowerCase(); + + const existingCategories = await db.select({ kategori: ProjectCategoryTable.kategori }).from(ProjectCategoryTable).execute(); + + const isCategoryExists = existingCategories.some((existingCategory) => existingCategory.kategori.toLowerCase() === categoryLower); + + if (isCategoryExists) { + throw new Error("Category already exists"); + } + + await db + .insert(ProjectCategoryTable) + .values({ ...projectCategory }) + .execute(); + + return { message: "Project Category successfully created" }; +}; + +export const getAllProjectCategory = async (search?: string) => { + const query = db.select().from(ProjectCategoryTable); + + if (search) { + query.where(like(ProjectCategoryTable.kategori, `%${search}%`)); + } + + return await query.execute(); +}; + +export async function getProjectCategoryById(id: string) { + const projectCategory = await db.select().from(ProjectCategoryTable).where(eq(ProjectCategoryTable.id, id)).limit(1).execute(); + + if (!projectCategory[0]) { + throw new Error("Project Category not found"); + } + + return projectCategory[0]; +} + +export async function updateProjectCategoryById(projectCategory: any, id: string) { + await getProjectCategoryById(id); + await db + .update(ProjectCategoryTable) + .set({ ...projectCategory }) + .where(eq(ProjectCategoryTable.id, id)) + .execute(); +} + +export async function deleteProjectCategoryById(id: string) { + await getProjectCategoryById(id); + await db.delete(ProjectCategoryTable).where(eq(ProjectCategoryTable.id, id)).execute(); +} diff --git a/services/backend/src/services/project-report.ts b/services/backend/src/services/project-report.ts new file mode 100644 index 0000000..7704e1d --- /dev/null +++ b/services/backend/src/services/project-report.ts @@ -0,0 +1,209 @@ +import { and, desc, eq, like, or, SQL } from "drizzle-orm"; +import { db } from "../drizzle/db.js"; +import { ProjectCategoryTable, ProjectReportTable, ProjectTable, SupportDocumentTable, UserTable } from "../drizzle/schema.js"; +import { differenceInMonths } from "date-fns"; + +export async function createProjectReport(projectReport: any) { + const project = await db.select().from(ProjectTable).where(eq(ProjectTable.id, projectReport.id_projek)).execute(); + + if (project.length === 0) { + throw new Error("Project not found"); + } + + // const lastProjectReport = await db.select().from(ProjectReportTable).where(eq(ProjectReportTable.id_projek, projectReport.id_projek)).orderBy(desc(ProjectReportTable.created_at)).limit(1).execute(); + + // if (!project[0].report_progress) { + // const error = new Error("Report Progress not found"); + // (error as any).statusCode = 404; + // throw error; + // } + // const reportFrequency = parseInt(project[0].report_progress); + + // if (lastProjectReport.length === 0) { + // const monthsSinceProjectCreated = differenceInMonths(new Date(), project[0].created_at); + // if (monthsSinceProjectCreated < reportFrequency) { + // throw new Error(`You can upload the report after ${reportFrequency - monthsSinceProjectCreated} months.`); + // } + // await db.insert(ProjectReportTable).values(projectReport).execute(); + // return { canUploadReport: true, message: "First report uploaded successfully." }; + // } + + // if (!lastProjectReport[0].created_at) { + // const error = new Error("Last Project Report not found"); + // (error as any).statusCode = 404; + // throw error; + // } + + // const lastReportDate = new Date(lastProjectReport[0].created_at); + + // const monthsSinceLastReport = differenceInMonths(new Date(), lastReportDate); + + // if (monthsSinceLastReport < reportFrequency) { + // throw new Error(`You can upload the next report after ${reportFrequency - monthsSinceLastReport} months.`); + // } + + await db.insert(ProjectReportTable).values(projectReport).execute(); + + const status = [ + "BERJALAN", + "BERJALAN SIKLUS 2", + "BERJALAN SIKLUS 3", + "BERJALAN SIKLUS 4", + "BERJALAN SIKLUS 5", + "BERJALAN SIKLUS 6", + "BERJALAN SIKLUS 7", + "BERJALAN SIKLUS 8", + "BERJALAN SIKLUS 9", + "BERJALAN SIKLUS 10", + "BERJALAN SIKLUS 11", + "BERJALAN SIKLUS 12", + "BERJALAN SIKLUS 13", + "BERJALAN SIKLUS 14", + "BERJALAN SIKLUS 15", + "BERJALAN SIKLUS 16", + "BERJALAN SIKLUS 17", + "BERJALAN SIKLUS 18", + "BERJALAN SIKLUS 19", + "BERJALAN SIKLUS 20" + ]; + + const currentStatusIndex = status.indexOf(project[0].status); + + if (currentStatusIndex !== -1 && currentStatusIndex < status.length - 1) { + const nextStatus = status[currentStatusIndex + 1] as "BERJALAN" | "BERJALAN SIKLUS 2" | "BERJALAN SIKLUS 3" | "BERJALAN SIKLUS 4" | "BERJALAN SIKLUS 5" | "BERJALAN SIKLUS 6" | "BERJALAN SIKLUS 7" | "BERJALAN SIKLUS 8" | "BERJALAN SIKLUS 9" | "BERJALAN SIKLUS 10" | "BERJALAN SIKLUS 11" | "BERJALAN SIKLUS 12" | "BERJALAN SIKLUS 13" | "BERJALAN SIKLUS 14" | "BERJALAN SIKLUS 15" | "BERJALAN SIKLUS 16" | "BERJALAN SIKLUS 17" | "BERJALAN SIKLUS 18" | "BERJALAN SIKLUS 19" | "BERJALAN SIKLUS 20"; + await db.update(ProjectTable) + .set({ status: nextStatus }) + .where(eq(ProjectTable.id, projectReport.id_projek)) + .execute(); + } + + return { canUploadReport: true, message: "Report uploaded successfully." }; +} + +export async function getAllProjectReport( + search?: string +) { + try { + const query = db + .select({ + user: UserTable, + project: ProjectTable, + kategori: ProjectCategoryTable, + supportDocument: SupportDocumentTable, + projectReport: ProjectReportTable, + }) + .from(ProjectReportTable) + .innerJoin(ProjectTable, eq(ProjectTable.id, ProjectReportTable.id_projek)) + .innerJoin(UserTable, eq(UserTable.id, ProjectTable.id_user)) + .innerJoin(ProjectCategoryTable, eq(ProjectTable.id_kategori, ProjectCategoryTable.id)) + .innerJoin(SupportDocumentTable, eq(ProjectTable.id, SupportDocumentTable.id_projek)) + .orderBy(desc(ProjectReportTable.created_at)); + + if (search) { + query.where( + or( + like(UserTable.nama, `%${search}%`), + like(ProjectTable.judul, `%${search}%`), + like(ProjectReportTable.judul, `%${search}%`) + ) + ); + } + + const queryResult = await query.execute(); + + const groupedProjectReports = queryResult.reduce((acc, curr) => { + const reportId = curr.projectReport.id; + + if (!acc[reportId]) { + acc[reportId] = { + ...curr.projectReport, + project: { + ...curr.project, + user: curr.user, + kategori: curr.kategori, + dokumenTambahan: curr.supportDocument ? [curr.supportDocument] : [], + }, + }; + } else { + if (curr.supportDocument) { + acc[reportId].project.dokumenTambahan.push(curr.supportDocument); + } + } + + return acc; + }, {} as { [key: string]: any }); + + return Object.values(groupedProjectReports); + } catch (error) { + console.error("Error fetching project reports:", error); + throw new Error("Could not retrieve project reports. Please try again later."); + } +} + +export async function getProjectReportById(id: string) { + const projectReport = await db.select().from(ProjectReportTable).where(eq(ProjectReportTable.id, id)).execute(); + return projectReport; +} + +export async function getProjectReportByProjectId(id: string, jenis?: any) { + const project = await db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).execute(); + + if (project.length === 0) { + const error = new Error("Project not found"); + (error as any).statusCode = 404; + throw error; + } + + // Build the conditions array + const conditions: SQL[] = [eq(ProjectReportTable.id_projek, id)]; + if (jenis) { + conditions.push(eq(ProjectReportTable.jenis_laporan, jenis)); + } + + // Use the conditions in both queries + const projectReport = await db + .select() + .from(ProjectReportTable) + .where(and(...conditions)) + .orderBy(desc(ProjectReportTable.created_at)) + .execute(); + + // const lastProjectReport = await db + // .select() + // .from(ProjectReportTable) + // .where(and(...conditions)) + // .orderBy(desc(ProjectReportTable.created_at)) + // .limit(1) + // .execute(); + + // if (lastProjectReport.length === 0) { + // return { canUploadReport: true, message: "No reports yet, you can upload the first report." }; + // } + + // if (!lastProjectReport[0].created_at) { + // const error = new Error("Last Project Report not found"); + // (error as any).statusCode = 404; + // throw error; + // } + // const lastReportDate = new Date(lastProjectReport[0].created_at); + + // if (!project[0].report_progress) { + // const error = new Error("Report Progress not found"); + // (error as any).statusCode = 404; + // throw error; + // } + // const reportFrequency = parseInt(project[0].report_progress); + + // const monthsSinceProjectCreated = differenceInMonths(new Date(), new Date(project[0].created_at)); + // const monthsSinceLastReport = differenceInMonths(new Date(), lastReportDate); + + // if (monthsSinceProjectCreated < reportFrequency || monthsSinceLastReport < reportFrequency) { + // return { + // projectReport, + // canUploadReport: false, + // message: `You can upload the next report after ${reportFrequency - monthsSinceLastReport} months.` + // }; + // } + + return { projectReport, canUploadReport: true, message: "You can upload the report now." }; +} \ No newline at end of file diff --git a/services/backend/src/services/project-token.ts b/services/backend/src/services/project-token.ts new file mode 100644 index 0000000..fc23f13 --- /dev/null +++ b/services/backend/src/services/project-token.ts @@ -0,0 +1,457 @@ +import { and, eq } from "drizzle-orm"; +import { db } from "../drizzle/db.js"; +import { HistoryProjectTable, ProjectTable, ProjectWalletTable, UserTable, WalletTable } from "../drizzle/schema.js"; +import { getProjectById, updateStatusProjectById } from "./project.js"; +import { getContract } from "../main.js"; +import { ethers } from "ethers"; +import { getWalletSaldoByUserId, updateWalletById } from "./wallet.js"; + +const apiUrl: any = process.env.API_URL; +const privateKey: any = process.env.PRIVATE_KEY; +const provider = new ethers.JsonRpcProvider(apiUrl); +const wallet = new ethers.Wallet(privateKey, provider); + +export async function getAllToken() { + try { + const contract = await getContract(); + + const allTokens = await contract.getAllTokens(); + console.log("Fetched tokens from blockchain:", allTokens); + + const formattedTokens = []; + + for (const token of allTokens) { + const tokenId = token[0]; + const idProjek = token[1]; + const idUser = token[2]; + const nilai = token[3].toString(); + + let user = null; + let project = null; + + if (idUser !== "0") { + try { + const userResult = await db.select().from(UserTable).where(eq(UserTable.id, idUser)).limit(1); + user = userResult[0] || null; + } catch (userError) { + console.error(`Error fetching user with ID ${idUser}:`, userError); + } + } + + try { + const projectResult = await db.select().from(ProjectTable).where(eq(ProjectTable.id, idProjek)).limit(1); + project = projectResult[0] || null; + } catch (projectError) { + console.error(`Error fetching project with ID ${idProjek}:`, projectError); + } + + formattedTokens.push({ + tokenId, + idProjek, + idUser, + nilai, + user, + project, + }); + } + + return formattedTokens; + } catch (error) { + console.error("Error in fetching tokens from blockchain and database:", error); + throw error; + } +} + +export async function getTotalToken() { + try { + const contract = await getContract(); + const totalToken = await contract.getTotalTokens(); + + console.log("Fetched total token count from blockchain:", totalToken); + + if (totalToken === null || totalToken === undefined || totalToken.toString() === "0") { + return "0"; + } + + return totalToken.toString(); + } catch (error) { + console.error("Error in fetching total token count from blockchain:", error); + throw error; + } +} + +export async function getTokenByIdProject(id: string) { + try { + const contract = await getContract(); + + const tokens = await contract.getTokenByProjectId(id); + + const serializedTokens = tokens.map((token: any) => { + return { + tokenId: token[0], + idProjek: token[1], + idUser: token[2], + nilai: token[3].toString(), + }; + }); + + console.log("Fetched token by project ID from blockchain:", serializedTokens); + + return serializedTokens; + } catch (error) { + console.error("Error in fetching token by project ID from blockchain:", error); + throw error; + } +} + +export async function getTotalTokenTerbeliByIdProject(id: string) { + try { + const contract = await getContract(); + const tokens = await contract.getTokenByProjectId(id); + + const tokenCount = tokens.reduce((count: number, token: any) => { + if (token[2].toString() !== "0") { + return count + 1; + } + return count; + }, 0); + + console.log("Number of tokens for non-zero users:", tokenCount); + return tokenCount; + } catch (error) { + console.error("Error in counting tokens for non-zero users:", error); + throw error; + } +} + +export async function getTokenProjectByUser(id_user: string, id_projek: string) { + try { + console.log(id_user, id_projek); + const contract = await getContract(); + + const tokens = await contract.getTokenByUserAndProject(id_user, id_projek); + + const serializedTokens = tokens.map((token: any) => { + return { + tokenId: token[0], + idProjek: token[1], + idUser: token[2], + nilai: token[3].toString(), + }; + }); + + console.log("Fetched token by project ID from blockchain:", serializedTokens); + + return serializedTokens; + } catch (error) { + console.error("Error in fetching token by project ID from blockchain:", error); + throw error; + } +} + +const updateTokenQueue: ((nonce: number) => Promise)[] = []; +let processingUpdate = false; +let currentNonce: number | null = null; + +async function processUpdateQueue() { + if (processingUpdate || updateTokenQueue.length === 0) return; + processingUpdate = true; + + try { + if (currentNonce === null) { + // Ambil nonce terbaru dari provider hanya jika currentNonce belum diinisialisasi + currentNonce = await provider.getTransactionCount(wallet.address, "latest"); + } + + console.log("Processing token update transaction with nonce", currentNonce); + + while (updateTokenQueue.length > 0) { + console.log("Processing token update transaction with nonce", currentNonce); + + const tx = updateTokenQueue.shift(); + if (tx && currentNonce !== null) { + try { + await tx(currentNonce); + console.log(`Transaction completed with nonce: ${currentNonce}`); + currentNonce++; // Increment nonce setelah transaksi berhasil + } catch (error: any) { + console.error("Error processing token update transaction", error); + // Jika error adalah NONCE_EXPIRED, reset currentNonce ke nonce terbaru + if (error.code === "NONCE_EXPIRED") { + currentNonce = await provider.getTransactionCount(wallet.address, "latest"); + // Jika ada transaksi yang tertunda, lanjutkan dengan yang tersisa + updateTokenQueue.unshift(tx); // Masukkan kembali transaksi yang gagal + break; // Keluar dari loop untuk menghindari pengolahan lebih lanjut + } else { + throw error; // Jika bukan NONCE_EXPIRED, lempar kembali error + } + } + } + } + } catch (error: any) { + console.error("Error processing token update transaction", error); + // Jika error adalah NONCE_EXPIRED, reset currentNonce ke nonce terbaru + if (error.code === "NONCE_EXPIRED") { + currentNonce = await provider.getTransactionCount(wallet.address, "latest"); + } + } finally { + processingUpdate = false; + processUpdateQueue(); // Proses transaksi berikutnya + } +} + +export async function buyTokenProject(id_user: string, id_projek: string, jumlah_token: number) { + const contract = await getContract(); + const allTokens = await contract.getTokenByProjectId(id_projek); + const availableTokens = allTokens.filter((token: any) => token.idUser === "0"); + console.log(availableTokens); + + if (availableTokens.length === 0) { + const error = new Error("Available tokens not found"); + (error as any).statusCode = 404; + throw error; + } + + // Cek user, project, wallet, dll. seperti pada kode Anda + if (availableTokens.length < jumlah_token) { + const error = new Error("Insufficient tokens available for purchase"); + (error as any).statusCode = 400; + throw error; + } + + const user = await db.select().from(UserTable).where(eq(UserTable.id, id_user)).execute(); + if (user.length === 0) { + const error = new Error("User not found"); + (error as any).statusCode = 404; + throw error; + } + + const project = await db.select().from(ProjectTable).where(eq(ProjectTable.id, id_projek)).execute(); + if (project.length === 0) { + const error = new Error("Project not found"); + (error as any).statusCode = 404; + throw error; + } + + const ownerProject = await db.select().from(UserTable).where(eq(UserTable.id, project[0].id_user)).execute(); + if (ownerProject.length === 0) { + const error = new Error("Project owner not found"); + (error as any).statusCode = 404; + throw error; + } + + // const walletUser = await db + // .select() + // .from(WalletTable) + // .where(and(eq(WalletTable.id_user, id_user), eq(WalletTable.jenis_wallet, "SALDO"))) + // .execute(); + const walletUser = await getWalletSaldoByUserId(id_user); + console.log(walletUser); + if (walletUser.length === 0) { + const error = new Error("User wallet not found"); + (error as any).statusCode = 404; + throw error; + } + const saldo = walletUser.data.saldo; + const totalHargaToken = Number(availableTokens[0].nilai) * jumlah_token; + + if (saldo < totalHargaToken) { + const error = new Error("Insufficient balance"); + (error as any).statusCode = 400; + throw error; + } + + // Tambahkan setiap panggilan updateTokenUser ke dalam queue dengan nonce + for (let i = 0; i < jumlah_token; i++) { + const tokenId = availableTokens[i].tokenId; + + updateTokenQueue.push(async (nonce: number) => { + const tx = await contract.updateTokenUser(tokenId, id_user, { nonce }); + await tx.wait(); // Tunggu hingga transaksi selesai + console.log(`Token ${i + 1} purchased and updated for user ${id_user}`); + }); + } + + // Mulai proses antrean pembaruan token + processUpdateQueue(); + + // Pembaruan saldo dan project wallet seperti sebelumnya + const saldoSekarang = saldo - totalHargaToken; + // await db + // .update(WalletTable) + // .set({ saldo: saldoSekarang }) + // .where(and(eq(WalletTable.id_user, id_user), eq(WalletTable.jenis_wallet, "SALDO"))) + // .execute(); + // console.log(walletUser.data.id); + // console.log("Updating wallet ", walletUser.id, "to", saldoSekarang); + await updateWalletById({ saldo: saldoSekarang }, walletUser.data.id); + + const project_wallet = await db.select().from(ProjectWalletTable).where(eq(ProjectWalletTable.id_projek, id_projek)).execute(); + if (project_wallet.length === 0) { + await db.insert(ProjectWalletTable).values({ id_projek, saldo: totalHargaToken, dana_terkumpul: totalHargaToken }).execute(); + } else { + await db + .update(ProjectWalletTable) + .set({ saldo: project_wallet[0].saldo + totalHargaToken, dana_terkumpul: project_wallet[0].dana_terkumpul + totalHargaToken }) + .where(eq(ProjectWalletTable.id_projek, id_projek)) + .execute(); + } + + const projectTokens = await contract.getTokenByProjectId(id_projek); + console.log("Checking project tokens status:", { + projectId: id_projek, + totalTokens: projectTokens.length, + unsoldTokens: projectTokens.filter((token: any) => token.idUser === "0").length, + tokens: projectTokens, + }); + + // Important: Wait for all token updates in the queue to complete + while (updateTokenQueue.length > 0 || processingUpdate) { + await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for 1 second + } + + // Recheck tokens after all updates are complete + const finalProjectTokens = await contract.getTokenByProjectId(id_projek); + const unsoldTokensCount = finalProjectTokens.filter((token: any) => token.idUser === "0").length; + console.log("Final token status check:", { + unsoldTokensCount, + totalTokens: finalProjectTokens.length, + }); + + if (unsoldTokensCount === 0) { + console.log("All tokens sold, updating project status to BERJALAN"); + await updateStatusProjectById(id_projek, "BERJALAN"); + await db + .insert(HistoryProjectTable) + .values({ + id_projek, + history: "Proses Penggalangan Penyertaan Modal", + keterangan: "Penggalangan penyertaan modal untuk proyek anda berhasil memenuhi target dan sedang berjalan.", + status: "SUCCESS", + }) + .execute(); + + const transaction = await contract.addTransaction(id_user, user[0].nama, id_projek, project[0].judul, ownerProject[0].nama, jumlah_token, totalHargaToken); + await transaction.wait(); + } + + return { + message: unsoldTokensCount === 0 ? "Token successfully purchased and project is now running" : "Token successfully purchased", + }; +} + +// Utility function to check and update project status +async function checkAndUpdateProjectStatus(id_projek: string) { + const contract = await getContract(); + const projectTokens = await contract.getTokenByProjectId(id_projek); + const unsoldTokensCount = projectTokens.filter((token: any) => token.idUser === "0").length; + + if (unsoldTokensCount === 0) { + const project = await getProjectById(id_projek); + if (project.status !== "BERJALAN") { + await updateStatusProjectById(id_projek, "BERJALAN"); + await db + .insert(HistoryProjectTable) + .values({ + id_projek, + history: "Proses Penggalangan Penyertaan Modal", + keterangan: "Penggalangan penyertaan modal untuk proyek anda berhasil memenuhi target dan sedang berjalan.", + status: "SUCCESS", + }) + .execute(); + } + return true; + } + return false; +} + +export async function tokenUsageDetailsByIdUser(id_user: string) { + try { + const contract = await getContract(); + + // Get all tokens and filter by user ID + const allTokens = await contract.getAllTokens(); + const userTokens = allTokens.filter((token: any) => token.idUser === id_user); + + // Get all chart tokens for the user + const chartTokens = await contract.getAllChartTokensByUserId(id_user); + + const projectTokenMap = new Map(); + + // Process each project that the user has tokens for + for (const token of userTokens) { + const projectId = token.idProjek; + + if (!projectTokenMap.has(projectId)) { + // Get project and owner details + const project = await db.select().from(ProjectTable).where(eq(ProjectTable.id, projectId)).execute(); + + if (project.length > 0) { + const owner = await db.select().from(UserTable).where(eq(UserTable.id, project[0].id_user)).execute(); + + // Get project tokens count by filtering all tokens + const projectTokens = allTokens.filter((token: any) => + token.idUser === id_user && token.idProjek === projectId + ); + + // Check if this project has chart tokens + const projectChartTokens = chartTokens.filter((token: any) => token.idProjek === projectId); + + let nominal = "0"; + let percentage = 0; + + // If project has chart tokens, get the token details + if (projectChartTokens.length > 0) { + const latestChartToken = await contract.getLatestChartTokenByUserIdAndProjectId(id_user, projectId); + const totalNominalTokens = await contract.getTotalNominalToken(id_user, projectId); + + nominal = latestChartToken.nominal.toString(); + percentage = totalNominalTokens.toString() === "0" ? 0 : + (Number(latestChartToken.nominal) / Number(totalNominalTokens)) * 100; + } + + // Add project to map with all details + projectTokenMap.set(projectId, { + ...project[0], + user: owner[0], + token_count: projectTokens.length.toString(), + total_nominal: nominal, + persentase: percentage.toFixed(2), + }); + } + } + } + + return Array.from(projectTokenMap.values()); + } catch (error) { + console.error("Error fetching token usage details:", error); + throw error; + } +} + +export async function getTotalTokenRupiahByUser(id_user: string) { + try { + const contract = await getContract(); + + const allTokens = await contract.getAllTokens(); + console.log("Fetched tokens from blockchain:", allTokens); + + let totalValue = 0; + + for (const token of allTokens) { + const idUser = token[2]; + const nilai = parseFloat(token[3].toString()); + + if (idUser === id_user) { + totalValue += nilai; + } + } + + console.log("Id User:", id_user); + console.log("Total token value for user:", totalValue); + return totalValue; + } catch (error) { + console.error("Error in fetching tokens and summing values:", error); + throw error; + } +} diff --git a/services/backend/src/services/project-wallet.ts b/services/backend/src/services/project-wallet.ts new file mode 100644 index 0000000..c50b206 --- /dev/null +++ b/services/backend/src/services/project-wallet.ts @@ -0,0 +1,87 @@ +import { desc, eq, like } from "drizzle-orm"; +import { db } from "../drizzle/db.js"; +import { ProjectTable, ProjectWalletTable, UserTable } from "../drizzle/schema.js"; +import { createHistoryProjectWallet } from "./history-project-wallet.js"; + +export async function getAllProjectWallet(search?: string) { + const query = db.select().from(ProjectWalletTable).innerJoin(ProjectTable, eq(ProjectWalletTable.id_projek, ProjectTable.id)).innerJoin(UserTable, eq(ProjectTable.id_user, UserTable.id)).orderBy(desc(ProjectWalletTable.created_at)); + + if (search) { + query.where(like(ProjectTable.judul, `%${search}%`)); + } + + const projectsWallet = await query.execute(); + + if (!projectsWallet.length) { + const error = new Error("No project wallet found"); + (error as any).statusCode = 404; + throw error; + } + + const groupedProjectWallets = projectsWallet.reduce((acc, curr) => { + const projectWalletId = curr.project_wallet.id; + + if (!acc[projectWalletId]) { + acc[projectWalletId] = { + ...curr.project_wallet, + project: curr.project, + user: curr.user, + }; + } + + return acc; + }, {} as { [key: string]: any }); + + return Object.values(groupedProjectWallets); +} + +export async function getProjectWalletByProjectId(id_projek: string) { + const wallet = await db.select().from(ProjectWalletTable).where(eq(ProjectWalletTable.id_projek, id_projek)).execute(); + + if (wallet.length === 0) { + const error = new Error("Project Wallet not found"); + (error as any).statusCode = 404; + throw error; + } + + return wallet; +} + +export async function transferSaldoProject(transfer: any) { + try { + const wallet = await getProjectWalletByProjectId(transfer.id_projek); + if (!wallet || wallet.length === 0) { + const error = new Error(`Wallet for project ID ${transfer.id_projek} not found`); + (error as any).statusCode = 404; + throw error; + } + + if (wallet[0].saldo < transfer.nominal) { + const error = new Error("Insufficient balance in project wallet"); + (error as any).statusCode = 400; + throw error; + } + + await db + .update(ProjectWalletTable) + .set({ saldo: wallet[0].saldo - transfer.nominal }) + .where(eq(ProjectWalletTable.id_projek, transfer.id_projek)) + .execute(); + + await createHistoryProjectWallet({ + id_project_wallet: wallet[0].id, + nominal: transfer.nominal, + dana_tersisa: wallet[0].saldo - transfer.nominal, + deskripsi: transfer.deskripsi, + bukti_transfer: transfer.bukti_transfer, + }); + + return { message: "Balance transferred successfully" }; + } catch (error: any) { + if (!error.statusCode) { + error = new Error("An unexpected error occurred during balance transfer"); + (error as any).statusCode = 500; + } + throw error; + } +} diff --git a/services/backend/src/services/project.ts b/services/backend/src/services/project.ts new file mode 100644 index 0000000..62c5c7f --- /dev/null +++ b/services/backend/src/services/project.ts @@ -0,0 +1,1220 @@ +import { db } from "../drizzle/db.js"; +import { + ChartProjectTable, + HistoryProjectTable, + ProjectCategoryTable, + ProjectReportTable, + ProjectTable, + ProjectWalletTable, + SupportDocumentTable, + TopupTable, + UserTable, + WalletTable, +} from "../drizzle/schema.js"; +import { + and, + count, + desc, + eq, + gte, + inArray, + like, + SQL, + sum, +} from "drizzle-orm"; +import cron from "node-cron"; +import { getContract } from "../main.js"; +import { ethers } from "ethers"; +import { getWalletSaldoByUserId, updateWalletById } from "./wallet.js"; + +const apiUrl: any = process.env.API_URL; +const privateKey: any = process.env.PRIVATE_KEY; +const provider = new ethers.JsonRpcProvider(apiUrl); +const wallet = new ethers.Wallet(privateKey, provider); + +export const countProject = async () => { + const projects = await db.select().from(ProjectTable).execute(); + return projects.length; +}; + +export const createProject = async (id_user: string, project: any) => { + const contract = await getContract(); + if (!id_user || typeof id_user !== "string") { + const error = new Error("Invalid user ID"); + (error as any).statusCode = 400; + throw error; + } + + if (!project || typeof project !== "object") { + const error = new Error("Invalid project data"); + (error as any).statusCode = 400; + throw error; + } + + const category = await db + .select() + .from(ProjectCategoryTable) + .where(eq(ProjectCategoryTable.id, project.id_kategori)) + .execute(); + if (!category || category.length === 0) { + const error = new Error("Invalid project category"); + (error as any).statusCode = 400; + throw error; + } + + const [newProject] = await db + .insert(ProjectTable) + .values({ + id_user: id_user, + ...project, + status: "PROSES VERIFIKASI", + }) + .returning({ id: ProjectTable.id }); + + const dividenProfit = await contract.addDividenProfit( + newProject.id, + project.bagian_pelaksana, + project.bagian_pemilik, + project.bagian_koperasi, + project.bagian_pendana + ); + await dividenProfit.wait(); + console.log(dividenProfit); + + if (!newProject || !newProject.id) { + const error = new Error("Failed to retrieve new project ID."); + (error as any).statusCode = 500; + throw error; + } + + const dokumenArray = Array.isArray(project.dokumen) + ? project.dokumen + : [project.dokumen].filter(Boolean); + + console.log("Document array:", dokumenArray); + + for (const document of dokumenArray) { + if (typeof document === "string") { + console.log("Inserting document with path:", document); + + await db + .insert(SupportDocumentTable) + .values({ + id_projek: newProject.id, + dokumen: document, + }) + .execute(); + } else if (document && document.path) { + console.log("Inserting document with path:", document.path); + + await db + .insert(SupportDocumentTable) + .values({ + id_projek: newProject.id, + dokumen: document.path, + }) + .execute(); + } else { + console.warn("Invalid document entry:", document); + const error = new Error("Invalid document entry"); + (error as any).statusCode = 400; + throw error; + } + } + + const result = await db + .insert(HistoryProjectTable) + .values({ + id_projek: newProject.id, + history: "Proposal Project Terkirim", + keterangan: + "Proposal project Anda berhasil terkirim dan akan diperiksa oleh Tim Kami", + status: "SUCCESS", + }) + .execute(); + + if (!result) { + const error = new Error("Failed to insert project history."); + (error as any).statusCode = 500; + throw error; + } + + return { message: "Project created successfully" }; +}; + +export const getAllProject = async ( + search?: string, + filter?: { + [key: string]: string | undefined; + } +) => { + let query = db + .select({ + project: ProjectTable, + user: UserTable, + kategori: ProjectCategoryTable, + supportDocument: SupportDocumentTable, + }) + .from(ProjectTable) + .innerJoin(UserTable, eq(ProjectTable.id_user, UserTable.id)) + .innerJoin( + SupportDocumentTable, + eq(ProjectTable.id, SupportDocumentTable.id_projek) + ) + .innerJoin( + ProjectCategoryTable, + eq(ProjectTable.id_kategori, ProjectCategoryTable.id) + ) + .orderBy(desc(ProjectTable.created_at)); + + if (filter) { + Object.keys(filter).forEach((key) => { + if (filter[key]) { + switch (key) { + case "status": + query.where( + eq( + ProjectTable.status, + filter[key] as + | "DITOLAK" + | "DRAFT" + | "PROSES VERIFIKASI" + | "REVISI" + | "APPROVAL" + | "TTD KONTRAK" + | "PENDANAAN DIBUKA" + | "BERJALAN" + | "DIBATALKAN" + | "SELESAI" + ) + ); + break; + // Add more cases for other columns as needed + default: + const error = new Error("No projects found"); + (error as any).statusCode = 404; + throw error; + } + } + }); + } + + if (search) { + query.where(like(ProjectTable.judul, `%${search}%`)); + } + + const projectsWithDocuments = await query.execute(); + + if (!projectsWithDocuments.length) { + const error = new Error("No projects found"); + (error as any).statusCode = 404; + throw error; + } + + const groupedProjects = projectsWithDocuments.reduce((acc, curr) => { + const projectId = curr.project.id; + + if (!acc[projectId]) { + acc[projectId] = { + ...curr.project, + user: curr.user, + kategori: curr.kategori, + dokumenTambahan: [], + }; + } + + if (curr.supportDocument) { + acc[projectId].dokumenTambahan.push(curr.supportDocument); + } + + return acc; + }, {} as { [key: string]: any }); + + return Object.values(groupedProjects); +}; + +export async function getProjectById(id: string) { + const contract = await getContract(); + const projectWithDocuments = await db + .select({ + project: ProjectTable, + user: UserTable, + kategori: ProjectCategoryTable, + supportDocument: SupportDocumentTable, + }) + .from(ProjectTable) + .where(eq(ProjectTable.id, id)) + .innerJoin(UserTable, eq(ProjectTable.id_user, UserTable.id)) + .innerJoin( + SupportDocumentTable, + eq(ProjectTable.id, SupportDocumentTable.id_projek) + ) + .innerJoin( + ProjectCategoryTable, + eq(ProjectTable.id_kategori, ProjectCategoryTable.id) + ) + .execute(); + + if (!projectWithDocuments.length) { + const error = new Error("Project not found"); + (error as any).statusCode = 404; + throw error; + } + + const getDividenProfit = await contract.getDividenProfitByProjectId(id); + + const modifiedDividenProfit = { + bagian_pelaksana: Number(getDividenProfit[1]), + bagian_pemilik: Number(getDividenProfit[2]), + bagian_koperasi: Number(getDividenProfit[3]), + bagian_pendana: Number(getDividenProfit[4]), + }; + + const { project, user, kategori } = projectWithDocuments[0]; + + const dokumenTambahan = projectWithDocuments.map( + ({ supportDocument }) => supportDocument + ); + + const jumlahReport = await db + .select({ count: count(ProjectReportTable.id) }) + .from(ProjectReportTable) + .where(eq(ProjectReportTable.id_projek, id)) + .execute(); + + const lastReport = await db + .select() + .from(ProjectReportTable) + .where(eq(ProjectReportTable.id_projek, id)) + .orderBy(desc(ProjectReportTable.created_at)) + .limit(1) + .execute(); + + let modal = project.nominal; // Default ke nominal asli + if (lastReport.length) { + modal = lastReport[0].nominal - lastReport[0].modal; + } + + const status = + jumlahReport[0].count > 0 + ? `BERJALAN SIKLUS ${jumlahReport[0].count + 1}` + : project.status; + + const { id_user, id_kategori, ...filteredProject } = project; + + return { + ...filteredProject, + nominal: modal, + status, + ...modifiedDividenProfit, + user, + kategori, + dokumenTambahan, + }; +} + +export async function getProjectByUserId( + id: any, + search?: string, + filter?: { + [key: string]: string | undefined; + } +) { + console.log(id); + let conditions: SQL[] = [eq(ProjectTable.id_user, id)]; + + if (filter) { + Object.keys(filter).forEach((key) => { + if (filter[key]) { + switch (key) { + case "status": + conditions.push( + eq( + ProjectTable.status, + filter[key] as + | "DITOLAK" + | "DRAFT" + | "PROSES VERIFIKASI" + | "REVISI" + | "APPROVAL" + | "TTD KONTRAK" + | "PENDANAAN DIBUKA" + | "BERJALAN" + | "DIBATALKAN" + | "SELESAI" + ) + ); + break; + // Add more cases for other columns as needed + default: + const error = new Error("No projects found"); + (error as any).statusCode = 404; + throw error; + } + } + }); + } + + if (search) { + conditions.push(like(ProjectTable.judul, `%${search}%`)); + } + + const query = db + .select({ + project: ProjectTable, + user: UserTable, + kategori: ProjectCategoryTable, + supportDocument: SupportDocumentTable, + }) + .from(ProjectTable) + .innerJoin(UserTable, eq(ProjectTable.id_user, UserTable.id)) + .innerJoin( + SupportDocumentTable, + eq(ProjectTable.id, SupportDocumentTable.id_projek) + ) + .innerJoin( + ProjectCategoryTable, + eq(ProjectTable.id_kategori, ProjectCategoryTable.id) + ) + .where(and(...conditions)) + .orderBy(desc(ProjectTable.created_at)); + + const projectWithDocuments = await query.execute(); + + if (!projectWithDocuments.length) { + const error = new Error("Project not found"); + (error as any).statusCode = 404; + throw error; + } + + const groupedProjects = projectWithDocuments.reduce>( + (acc, curr) => { + const projectId = curr.project.id; + + const existingProject = acc.find((proj) => proj.id === projectId); + + if (existingProject) { + if (curr.supportDocument) { + existingProject.dokumenTambahan.push(curr.supportDocument); + } + } else { + acc.push({ + ...curr.project, + user: curr.user, + kategori: curr.kategori, + dokumenTambahan: curr.supportDocument ? [curr.supportDocument] : [], + }); + } + + return acc; + }, + [] + ); + + return groupedProjects; +} + +interface UserTokenSummary { + nama: string; + token_created_at: string; + jumlah_token: number; + total_nilai_token: number; +} + +export async function getUserHaveTokenInProject(id: string) { + try { + const contract = await getContract(); + + const tokens = await contract.getTokenByProjectId(id); + + const userSummaryMap: Record = {}; + + for (const token of tokens) { + const [tokenId, idProjek, idUser, nilai] = token; + + if (idUser === "0") continue; + + if (!userSummaryMap[idUser]) { + userSummaryMap[idUser] = { + nama: "", + token_created_at: new Date().toISOString(), + jumlah_token: 0, + total_nilai_token: 0, + }; + } + + userSummaryMap[idUser].jumlah_token += 1; + userSummaryMap[idUser].total_nilai_token += Number(nilai); + } + + const userIds = Object.keys(userSummaryMap); + const users = await db + .select({ + id: UserTable.id, + nama: UserTable.nama, + }) + .from(UserTable) + .where(inArray(UserTable.id, userIds)) + .execute(); + + for (const user of users) { + if (userSummaryMap[user.id]) { + userSummaryMap[user.id].nama = user.nama; + } + } + + const userTokenSummary = Object.values(userSummaryMap); + + if (userTokenSummary.length === 0) { + const error = new Error("No tokens found for this project"); + (error as any).statusCode = 404; + throw error; + } + + return userTokenSummary; + } catch (error) { + console.error("Error in getUserHaveTokenInProject:", error); + throw error; + } +} + +export async function updateStatusProjectById(id: string, status: any) { + await getProjectById(id); + await db + .update(ProjectTable) + .set({ status }) + .where(eq(ProjectTable.id, id)) + .execute(); +} + +export async function acceptProjectById(id: string) { + await getProjectById(id); + await updateStatusProjectById(id, "APPROVAL"); + await db + .insert(HistoryProjectTable) + .values({ + id_projek: id, + history: "Peninjauan Proposal", + keterangan: + "Proposal project Anda telah memenuhi syarat, selanjutnya akan dilakukan proses approval dari komitee koperasi", + status: "SUCCESS", + }) + .execute(); + await db + .insert(HistoryProjectTable) + .values({ + id_projek: id, + history: "Proses Approval dari Komitee Koperasi", + keterangan: "Project sedang dalam proses approval komitee koperasi", + status: "PENDING", + }) + .execute(); +} + +export async function rejectProjectById(id: string, keterangan: string) { + await getProjectById(id); + await updateStatusProjectById(id, "DITOLAK"); + await db + .insert(HistoryProjectTable) + .values({ + id_projek: id, + history: "Peninjauan Proposal", + keterangan: keterangan, + status: "FAILED", + }) + .execute(); +} + +export async function reviseProjectById(id: string, keterangan: string) { + await getProjectById(id); + await updateStatusProjectById(id, "REVISI"); + await db.insert(HistoryProjectTable).values({ + id_projek: id, + history: "Peninjauan Proposal", + keterangan: keterangan, + status: "FAILED", + }); +} + +export async function getKeteranganReviseProjectById(id: string) { + const projectHistory = await db + .select({ keterangan: HistoryProjectTable.keterangan }) + .from(HistoryProjectTable) + .where( + and( + eq(HistoryProjectTable.id_projek, id), + eq(HistoryProjectTable.history, "Peninjauan Proposal"), + eq(HistoryProjectTable.status, "FAILED") + ) + ) + .orderBy(desc(HistoryProjectTable.created_at)) + .limit(1) + .execute(); + if (projectHistory.length === 0) { + const error = new Error("History not found"); + (error as any).statusCode = 404; + throw error; + } + return projectHistory[0]; +} + +export async function updateProjectById(id_user: any, updatedProject: any) { + const id = updatedProject.id; + const existingProject = await db + .select() + .from(ProjectTable) + .where(eq(ProjectTable.id, id)) + .execute(); + const contract = await getContract(); + + if (existingProject.length === 0) { + const error = new Error("Project not found"); + (error as any).statusCode = 404; + throw error; + } + + if (existingProject[0].id_user !== id_user) { + const error = new Error("You are not authorized to update this project"); + (error as any).statusCode = 403; + throw error; + } + + const projectUpdateData: any = { + status: "PROSES VERIFIKASI", + }; + + if (updatedProject.dokumen_proyeksi !== undefined) { + projectUpdateData.dokumen_proyeksi = updatedProject.dokumen_proyeksi; + } + + Object.keys(updatedProject).forEach((key) => { + if (key !== "dokumen_proyeksi" && updatedProject[key] !== undefined) { + projectUpdateData[key] = updatedProject[key]; + } + }); + + await db + .update(ProjectTable) + .set(projectUpdateData) + .where(eq(ProjectTable.id, id)) + .execute(); + const getDividenProfit = await contract.getDividenProfitByProjectId(id); + console.log(getDividenProfit); + + const modifiedDividenProfit = { + bagianPelaksana: Number(getDividenProfit[1]), + bagianPemilik: Number(getDividenProfit[2]), + bagianKoperasi: Number(getDividenProfit[3]), + bagianPendana: Number(getDividenProfit[4]), + }; + + if (updatedProject.bagian_pelaksana) { + modifiedDividenProfit.bagianPelaksana = updatedProject.bagian_pelaksana; + } + + if (updatedProject.bagian_pemilik) { + modifiedDividenProfit.bagianPemilik = updatedProject.bagian_pemilik; + } + + if (updatedProject.bagian_koperasi) { + modifiedDividenProfit.bagianKoperasi = updatedProject.bagian_koperasi; + } + + if (updatedProject.bagian_pendana) { + modifiedDividenProfit.bagianPendana = updatedProject.bagian_pendana; + } + + const updateDividenProfit = await contract.updateDividenProfit( + updatedProject.id, + modifiedDividenProfit.bagianPelaksana, + modifiedDividenProfit.bagianPemilik, + modifiedDividenProfit.bagianKoperasi, + modifiedDividenProfit.bagianPendana + ); + + const dokumenArray = Array.isArray(updatedProject.dokumen) + ? updatedProject.dokumen + : [updatedProject.dokumen].filter(Boolean); + + const existingDocuments = await db + .select() + .from(SupportDocumentTable) + .where(eq(SupportDocumentTable.id_projek, id)) + .execute(); + + const existingDocumentPaths = existingDocuments.map((doc) => doc.dokumen); + + for (const existingDocument of existingDocuments) { + if (!dokumenArray.includes(existingDocument.dokumen)) { + await db + .delete(SupportDocumentTable) + .where(eq(SupportDocumentTable.id, existingDocument.id)) + .execute(); + } + } + + for (const document of dokumenArray) { + const documentPath = + typeof document === "string" ? document : document.path; + if (documentPath && !existingDocumentPaths.includes(documentPath)) { + await db + .insert(SupportDocumentTable) + .values({ + id_projek: id, + dokumen: documentPath, + }) + .execute(); + } + } + + const historyResult = await db + .insert(HistoryProjectTable) + .values({ + id_projek: id, + history: "Peninjauan Proposal", + keterangan: "Form pengajuan proyek sudah direvisi", + status: "PENDING", + }) + .execute(); + + if (!historyResult) { + const error = new Error("Failed to insert project history."); + (error as any).statusCode = 500; + throw error; + } + + return { message: "Project successfully updated" }; +} + +const queue: (() => Promise)[] = []; +let processing = false; +let currentNonce = 0; + +async function processQueue() { + if (processing || queue.length === 0) return; + processing = true; + + const tx = queue.shift(); + try { + if (tx) { + await tx(); // Proses transaksi + } + } catch (error) { + console.error("Error processing transaction", error); + } finally { + processing = false; + processQueue(); + } +} + +export async function approveProjectById(id: string, detail: any) { + try { + await getProjectById(id); + await updateStatusProjectById(id, "TTD KONTRAK"); + + await db + .update(ProjectTable) + .set({ ...detail }) + .where(eq(ProjectTable.id, id)) + .execute(); + + const contract = await getContract(); + + // Dapatkan nonce awal sebelum memasukkan transaksi ke antrean + currentNonce = await provider.getTransactionCount(wallet.address, "latest"); + console.log("Current nonce:", currentNonce); + + // Tambahkan transaksi createToken ke dalam queue + for (let i = 0; i < detail.jumlah_koin; i++) { + queue.push(async () => { + const tx = await contract.createToken( + id, + "0", + detail.harga_per_unit.toString(), + { nonce: currentNonce } + ); + await tx.wait(); // Tunggu hingga transaksi selesai + console.log( + "\x1b[42m%s\x1b[0m", + `Token ${i + 1} created for project ${id}` + ); + + currentNonce++; // Increment nonce setelah transaksi berhasil + console.log("Current nonce:", currentNonce); + }); + } + + // Mulai memproses queue + processQueue(); + + await db.insert(HistoryProjectTable).values({ + id_projek: id, + history: "Proses Approval dari Komitee Koperasi", + keterangan: ` + Project disetujui oleh komitee dengan catatan sebagai berikut: +
    +
  • Nominal disetujui: Rp ${ + detail.nominal_disetujui + }
  • +
  • Jumlah Unit: ${detail.jumlah_koin}
  • +
  • Maks Pembelian: ${ + detail.maksimal_pembelian + } atau Rp ${detail.maksimal_pembelian * detail.harga_per_unit}
  • +
+ `, + status: "SUCCESS", + }); + + await db + .insert(HistoryProjectTable) + .values({ + id_projek: id, + history: "Kontrak Perjanjian", + keterangan: "Menunggu tanda tangan kontrak perjanjian", + status: "PENDING", + }) + .execute(); + + console.log(`All tokens created for project ${id}`); + } catch (error) { + console.error("Error in approveProjectById:", error); + throw error; + } +} + +export async function deleteProjectById(id: string) { + await getProjectById(id); + await db.delete(ProjectTable).where(eq(ProjectTable.id, id)).execute(); +} + +export async function publishProjectById(penggalangan: any) { + await getProjectById(penggalangan.id_projek); + await db.insert(HistoryProjectTable).values({ + id_projek: penggalangan.id_projek, + history: "Proses Penggalangan Penyertaan Modal", + keterangan: "Proyek Telah dipublish", + status: "PENDING", + }); + + await db.insert(HistoryProjectTable).values({ + id_projek: penggalangan.id_projek, + history: "Proses Penggalangan Penyertaan Modal", + keterangan: "Proses pendanaan sedang berlangsung", + status: "SUCCESS", + }); + + await db + .update(ProjectTable) + .set({ + status: "PENDANAAN DIBUKA", + mulai_penggalangan_dana: penggalangan.mulai_penggalangan_dana, + selesai_penggalangan_dana: penggalangan.selesai_penggalangan_dana, + dokumen_prospektus: penggalangan.dokumen_prospektus, + updated_at: new Date(), + }) + .where(eq(ProjectTable.id, penggalangan.id_projek)) + .execute(); + + return { message: "Project published successfully" }; +} + +interface TokenDetail { + tokenId: string; + idUser: string; + nilai: number; +} + +interface UserTokens { + totalNominal: number; + tokenIds: string[]; +} + +// Function to validate UUID +function isValidUUID(uuid: string) { + const uuidRegex = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + return uuidRegex.test(uuid); +} + +export async function checkProjectFundingOpened() { + const today = new Date().toISOString().split("T")[0]; + try { + const projects = await db + .select() + .from(ProjectTable) + .innerJoin( + ProjectWalletTable, + eq(ProjectTable.id, ProjectWalletTable.id_projek) + ) + .where( + and( + eq(ProjectTable.selesai_penggalangan_dana, today), + gte(ProjectTable.nilai_jaminan, ProjectWalletTable.saldo) + ) + ) + .execute(); + + if (!projects || projects.length === 0) { + const error = new Error("Project not found"); + (error as any).statusCode = 404; + throw error; + } + + const contract = await getContract(); + + for (const project of projects) { + try { + const projectTokens = await contract.getTokenByProjectId( + project.project.id + ); + + console.log("Project tokens:", projectTokens); + + const tokenMap: Record = projectTokens.reduce( + (map: Record, token: TokenDetail) => { + const userId = token.idUser; + + // Validate userId before proceeding + if (!isValidUUID(userId)) { + console.error(`Invalid userId: ${userId}`); + return map; + } + + if (!map[userId]) { + map[userId] = { totalNominal: 0, tokenIds: [] }; + } + + const tokenNominal = + typeof token.nilai === "bigint" + ? Number(token.nilai) + : token.nilai; + + map[userId].totalNominal += tokenNominal; + map[userId].tokenIds.push(token.tokenId); + + return map; + }, + {} + ); + + for (const [userId, { totalNominal, tokenIds }] of Object.entries( + tokenMap + )) { + try { + // Reset token nominals using smart contract + for (const tokenId of tokenIds) { + const tx = await contract.resetTokenNominal(tokenId); + await tx.wait(); + console.log( + `Token ${tokenId} nilai has been reset to 0 via smart contract` + ); + } + + const userWallet = await getWalletSaldoByUserId(userId); + console.log("User wallet:", userWallet.data.id); + + if (userWallet && userWallet.data) { + const currentSaldo = userWallet.data.saldo; + + await updateWalletById( + { saldo: currentSaldo + totalNominal }, + userWallet.data.id + ); + + console.log( + `Saldo user ${userId} telah ditambahkan sebesar ${totalNominal}` + ); + } else { + console.error( + `Wallet dengan jenis 'SALDO' tidak ditemukan untuk user ${userId}` + ); + } + } catch (userError) { + console.error(`Error processing user ${userId}:`, userError); + } + } + + try { + await db + .update(ProjectTable) + .set({ status: "DIBATALKAN", updated_at: new Date() }) + .where(eq(ProjectTable.id, project.project.id)) + .execute(); + } catch (updateProjectError) { + console.error( + `Error updating project ${project.project.id} status to 'DIBATALKAN':`, + updateProjectError + ); + } + + try { + await db.insert(HistoryProjectTable).values({ + id_projek: project.project.id, + history: "Proses Penggalangan Penyertaan Modal", + keterangan: + "Proyek telah dibatalkan karena Pendanaan tidak memenuhi target, Dana telah dikembalikan Otomatis ke pembeli proyek", + status: "FAILED", + }); + } catch (historyError) { + console.error( + `Error inserting history for project ${project.project.id}:`, + historyError + ); + } + + try { + await db + .update(ProjectWalletTable) + .set({ dana_terkumpul: 0, saldo: 0, updated_at: new Date() }) + .where(eq(ProjectWalletTable.id_projek, project.project.id)) + .execute(); + console.log( + `Saldo project ${project.project.id} telah di-reset menjadi 0` + ); + } catch (updateWalletError) { + console.error( + `Error resetting project wallet saldo for project ${project.project.id}:`, + updateWalletError + ); + } + + console.log( + `Project ${project.project.id} funding has been closed and tokens returned to users` + ); + } catch (projectError) { + console.error( + `Error processing project ${project.project.id}:`, + projectError + ); + } + } + } catch (error) { + console.error("Error checking project funding:", error); + } +} + +cron.schedule("0 0 * * *", () => { + console.log("Running project status check..."); + checkProjectFundingOpened(); +}); + +export async function completingProjectById(id: string) { + const project = await getProjectById(id); + + if (!project) { + const error = new Error("Project not found."); + (error as any).statusCode = 404; + throw error; + return error; + } + + await db + .update(ProjectTable) + .set({ status: "SELESAI" }) + .where(eq(ProjectTable.id, id)) + .execute(); +} + +export async function totalProfit(id: string) { + const totalKomulatif = await db + .select({ value: sum(ChartProjectTable.nominal) }) + .from(ChartProjectTable) + .where(eq(ChartProjectTable.id_projek, id)) + .execute(); + + if (!totalKomulatif || totalKomulatif.length === 0) { + const error = new Error("Project not found"); + (error as any).statusCode = 404; + throw error; + } + return totalKomulatif[0].value; +} + +export async function shareProfit(id: any) { + let chartTokens; + const contract = await getContract(); + const project = await getProjectById(id); + const admin = await db + .select() + .from(UserTable) + .where(eq(UserTable.role, "ADMIN")) + .execute(); + let walletKoperasi = await db + .select() + .from(WalletTable) + .where( + and( + eq(WalletTable.id_user, admin[0].id), + eq(WalletTable.jenis_wallet, "KAS KOPERASI") + ) + ) + .execute(); + + if (!walletKoperasi || walletKoperasi.length === 0) { + const buatWalletKoperasi = await db + .insert(WalletTable) + .values({ + id_user: admin[0].id, + jenis_wallet: "KAS KOPERASI", + }) + .returning({ id: WalletTable.id }) + .execute(); + walletKoperasi = await db + .select() + .from(WalletTable) + .where(eq(WalletTable.id, buatWalletKoperasi[0].id)) + .execute(); + } + + const totalKomulatif = await db + .select({ value: sum(ChartProjectTable.nominal) }) + .from(ChartProjectTable) + .where(eq(ChartProjectTable.id_projek, id)) + .execute(); + + if (!totalKomulatif[0].value) { + const error = new Error("Failed to calculate total cumulative value"); + (error as any).statusCode = 500; + throw error; + } + + const dividenProfit = await contract.getDividenProfitByProjectId(id); + if (!dividenProfit.pelaksana) { + const error = new Error("Bagian pelaksana not found."); + (error as any).statusCode = 404; + throw error; + } + + const bagianPelaksana = Math.round( + (Number(totalKomulatif[0].value) * Number(dividenProfit.pelaksana)) / 100 + ); + await db + .insert(TopupTable) + .values({ + id_wallet: walletKoperasi[0].id, + nama: project.judul, + nominal: bagianPelaksana, + jenis: "PELAKSANA", + }) + .execute(); + + if (!dividenProfit.koperasi) { + const error = new Error("Bagian koperasi not found."); + (error as any).statusCode = 404; + throw error; + } + + const bagianKoperasi = Math.round( + (Number(totalKomulatif[0].value) * Number(dividenProfit.koperasi)) / 100 + ); + await db + .update(WalletTable) + .set({ saldo: walletKoperasi[0].saldo + bagianKoperasi }) + .where(eq(WalletTable.id, walletKoperasi[0].id)) + .execute(); + + if (!dividenProfit.pemilik) { + const error = new Error("Bagian pemilik not found."); + (error as any).statusCode = 404; + throw error; + } + + const bagianPemilik = Math.round( + (Number(totalKomulatif[0].value) * Number(dividenProfit.pemilik)) / 100 + ); + await db + .insert(TopupTable) + .values({ + id_wallet: walletKoperasi[0].id, + nama: project.judul, + nominal: bagianPemilik, + jenis: "PEMILIK", + created_at: new Date(), + }) + .execute(); + + try { + chartTokens = await contract.getLatestChartTokensByProjectId(id); + console.log(chartTokens); + } catch (err) { + const error = new Error("Failed to fetch chart tokens."); + (error as any).statusCode = 500; + throw error; + } + + if (!chartTokens || chartTokens.length === 0) { + const error = new Error("No chart tokens found for the project."); + (error as any).statusCode = 404; + throw error; + } + + for (const token of chartTokens) { + let historyToken; + try { + historyToken = await contract.getHistoryTokenByChartTokenId( + token.chartTokenId + ); + } catch (err) { + const error = new Error( + `Failed to fetch history tokens for chart token ${token.chartTokenId}.` + ); + (error as any).statusCode = 500; + throw error; + } + + if (!historyToken || historyToken.length === 0) { + const error = new Error( + `No history token found for chart token ${token.chartTokenId}.` + ); + (error as any).statusCode = 404; + throw error; + } + + const userId = token.idUser; + let wallet; + try { + wallet = await getWalletSaldoByUserId(userId); + } catch (err) { + const error = new Error(`Failed to fetch wallet for user ${userId}.`); + (error as any).statusCode = 500; + throw error; + } + + if (!wallet || wallet.length === 0) { + const error = new Error(`No wallet found for user ${userId}.`); + (error as any).statusCode = 404; + throw error; + } + + console.log(wallet.data.saldo + "+" + Number(historyToken.totalNilai)); + const updatedBalance = wallet.data.saldo + Number(historyToken.totalNilai); + console.log( + `Updating wallet balance for user ${userId} to ${updatedBalance}` + ); + try { + await updateWalletById({ saldo: updatedBalance }, wallet.data.id); + } catch (err) { + const error = new Error( + `Failed to update wallet balance for user ${userId}.` + ); + (error as any).statusCode = 500; + throw error; + } + } + + return { message: "Profit shared" }; +} + +export async function getDokumenProspektusById(id: any) { + const project = await db + .select({ dokumen_prospektus: ProjectTable.dokumen_prospektus }) + .from(ProjectTable) + .where(eq(ProjectTable.id, id)) + .execute(); + if (!project || project.length === 0) { + const error = new Error("Project not found"); + (error as any).statusCode = 404; + throw error; + } + return project[0].dokumen_prospektus; +} diff --git a/services/backend/src/services/topup.ts b/services/backend/src/services/topup.ts new file mode 100644 index 0000000..50d24f0 --- /dev/null +++ b/services/backend/src/services/topup.ts @@ -0,0 +1,617 @@ +import { and, desc, eq, like, or } from "drizzle-orm"; +import { db } from "../drizzle/db.js"; +import { TopupTable, UserTable, WalletTable } from "../drizzle/schema.js"; +import { getUserById, updateUserStatus } from "./user.js"; +import { + createWallet, + getWalletById, + getWalletSaldoByUserId, + getWalletWajibByUserId, +} from "./wallet.js"; +import axios from "axios"; +import { walletServiceUrl } from "../main.js"; +import path from "path"; +import * as fs from "fs"; +import Response from 'express'; + +export async function getAllTopUp(search?: string) { + const query = db + .select() + .from(TopupTable) + .innerJoin(WalletTable, eq(TopupTable.id_wallet, WalletTable.id)) + .innerJoin(UserTable, eq(WalletTable.id_user, UserTable.id)) + .orderBy(desc(TopupTable.created_at)); + + if (search) { + query.where(like(TopupTable.nama, `%${search}%`)); + } + + const topUp = await query.execute(); + if (!topUp || topUp.length === 0) { + const error = new Error("No topups found"); + (error as any).statusCode = 404; + throw error; + } + return topUp; +} + +export async function fetchGetAllTopupWalletService() { + try { + const response = await axios.get(`${walletServiceUrl}/topup`); + return response.data; + } catch (error: any) { + throw new Error( + error.response?.data?.message || "Failed to fetch external topups" + ); + } +} + +export async function formatGetAllTopup(search?: string) { + try { + const backendTopup = await getAllTopUp(search); + + const walletDataResponse = await fetchGetAllTopupWalletService(); + const walletData = walletDataResponse.data || []; + + const combinedData = await Promise.all( + walletData.map(async (walletTopup: any) => { + const user = await getUserById(walletTopup.wallet.id_user); + + return { + topup: walletTopup.topup, + wallet: walletTopup.wallet, + user: user || null, + }; + }) + ); + + const finalData = [...backendTopup, ...combinedData]; + + return finalData; + } catch (error: any) { + throw new Error(error.message || "Failed to fetch topups"); + } +} + +export async function getTopupById(id: string) { + const backendTopup = await db + .select() + .from(TopupTable) + .innerJoin(WalletTable, eq(TopupTable.id_wallet, WalletTable.id)) + .innerJoin(UserTable, eq(WalletTable.id_user, UserTable.id)) + .where(eq(TopupTable.id, id)) + .execute(); + + if (backendTopup.length > 0) { + return backendTopup.map((item) => ({ + topup: item.topup, + wallet: item.wallet, + user: item.user, + })); + } + + try { + const response = await axios.get(`${walletServiceUrl}/topup/${id}`); + const walletTopup = response.data.data[0]; + + const user = await db + .select() + .from(UserTable) + .where(eq(UserTable.id, walletTopup.wallet.id_user)) + .execute(); + + if (user.length === 0) { + throw new Error("User not found for the given wallet ID"); + } + + return [ + { + topup: walletTopup.topup, + wallet: walletTopup.wallet, + user: user[0], + }, + ]; + } catch (error: any) { + if (error.response?.status === 404) { + const notFoundError = new Error("Topup not found"); + (notFoundError as any).statusCode = 404; + throw notFoundError; + } + throw new Error("Failed to fetch data from wallet service"); + } +} + +export async function getTopupByUserId(id_user: string) { + const topup = await db + .select() + .from(TopupTable) + .innerJoin(WalletTable, eq(TopupTable.id_wallet, WalletTable.id)) + .innerJoin(UserTable, eq(WalletTable.id_user, UserTable.id)) + .where(eq(UserTable.id, id_user)) + .orderBy(desc(TopupTable.created_at)) + .execute(); + if (!topup || topup.length === 0) { + const error = new Error("Topup not found"); + (error as any).statusCode = 404; + throw error; + } + return topup; +} +export async function fetchGetTopupByUserIdWalletService(userId: string) { + try { + const response = await axios.get( + `${walletServiceUrl}/topup/user/${userId}` + ); + return response.data; + } catch (error: any) { + throw new Error( + error.response?.data?.message || "Failed to fetch wallet topups" + ); + } +} + +export async function formatGetTopupByUserId(userId: string) { + const backendTopup = await getTopupByUserId(userId); + + const walletDataResponse = await fetchGetTopupByUserIdWalletService(userId); + const walletData = walletDataResponse.data || []; + + const enhancedWalletData = await Promise.all( + walletData.map(async (walletTopup: any) => { + const userData = await db + .select() + .from(UserTable) + .where(eq(UserTable.id, walletTopup.wallet.id_user)) + .execute() + .then((users) => users[0] || null); + + return { + topup: walletTopup.topup, + wallet: walletTopup.wallet, + user: userData || null, + }; + }) + ); + + return [...backendTopup, ...enhancedWalletData]; +} + +export async function getTotalWalletSimpananPokokByUserId(id_user: string) { + const result = await db + .select({ + total: WalletTable.saldo, + }) + .from(WalletTable) + .where( + and( + eq(WalletTable.id_user, id_user), + eq(WalletTable.jenis_wallet, "SIMPANAN POKOK") + ) + ) + .execute(); + + const total = result[0]?.total || 0; + + if (total === 0) { + const error = new Error("No Wallet simpanan pokok found for the user"); + (error as any).statusCode = 404; + throw error; + } + + return total; +} + +export async function getTotalWalletSimpananWajibByUserId(id_user: string) { + const result = await db + .select({ + total: WalletTable.saldo, + }) + .from(WalletTable) + .where( + and( + eq(WalletTable.id_user, id_user), + eq(WalletTable.jenis_wallet, "SIMPANAN WAJIB") + ) + ) + .execute(); + + const total = result[0]?.total || 0; + if (total === 0) { + const error = new Error("No Wallet simpanan pokok found for the user"); + (error as any).statusCode = 404; + throw error; + } + + return total; +} + +export async function getKasKoperasi() { + const userId = await db + .select() + .from(UserTable) + .where(eq(UserTable.role, "ADMIN")) + .execute(); + const result = await db + .select({ + total: WalletTable.saldo, + }) + .from(WalletTable) + .where( + and( + eq(WalletTable.id_user, userId[0].id), + eq(WalletTable.jenis_wallet, "KAS KOPERASI") + ) + ) + .execute(); + + const total = result[0]?.total || 0; + + if (total === 0) { + const error = new Error("No Wallet kas koperasi found"); + (error as any).statusCode = 404; + throw error; + } + return total; +} + +export async function getWithdrawSaldo() { + const response = await axios.get(`${walletServiceUrl}/topup/withdraw`); + + const formattedData = await Promise.all( + response.data.data.map(async (data: any) => { + const walletResponse = await axios.get( + `${walletServiceUrl}/wallet/${data.id_wallet}` + ); + const walletData = walletResponse.data.data; + + const userData = await db + .select() + .from(UserTable) + .where(eq(UserTable.id, walletData.id_user)) + .execute(); + + return { + topup: { + id: data.id, + id_wallet: data.id_wallet, + nama: data.nama, + nama_bank: data.nama_bank, + no_rekening: data.no_rekening, + nama_pemilik_rekening: data.nama_pemilik_rekening, + nominal: data.nominal, + jenis: data.jenis, + bukti_pembayaran: data.bukti_pembayaran, + status: data.status, + created_at: data.created_at, + updated_at: data.updated_at, + }, + wallet: walletData, + user: userData[0], + }; + }) + ); + + if (!formattedData || formattedData.length === 0) { + const error = new Error("No withdraw saldo found"); + (error as any).statusCode = 404; + throw error; + } + + return formattedData; +} + +export async function payMember( + id_user: string, + topUp: any +): Promise<{ message: string }> { + const user = await getUserById(id_user); + + const walletPokok = await createWallet({ + id_user: id_user, + jenis_wallet: "SIMPANAN POKOK", + }); + + const walletWajib = await createWallet({ + id_user: id_user, + jenis_wallet: "SIMPANAN WAJIB", + }); + + await db + .insert(TopupTable) + .values({ + id_wallet: walletPokok.id, + nama: user.nama, + jenis: "SIMPANAN POKOK", + nominal: 50000, + ...topUp, + }) + .execute(); + + await db + .insert(TopupTable) + .values({ + id_wallet: walletWajib.id, + nama: user.nama, + jenis: "SIMPANAN WAJIB", + nominal: 120000, + ...topUp, + }) + .execute(); + + await updateUserStatus(id_user, "MENUNGGU KONFIRMASI"); + + return { message: "Topup has been created, awaiting payment confirmation" }; +} + +// export async function payTopup(id_user: string, topUp: any): Promise<{ message: string }> { +// const user = await getUserById(id_user); + +// const wallet = await getWalletSaldoByUserId(id_user); + +// console.log(topUp); + +// // await axios.post(`${walletServiceUrl}/topup/create`, { +// // id_wallet: wallet.data.id, +// // nama: user.nama, +// // ...topUp, +// // }); + +// console.log(await axios.post(`${walletServiceUrl}/topup/create`, { +// id_wallet: wallet.data.id, +// nama: user.nama, +// ...topUp, +// })); + +// return { message: "Topup has been created, awaiting payment confirmation" }; +// } + +export async function payTopup( + id_user: string, + topUp: any +): Promise<{ message: string }> { + try { + const user = await getUserById(id_user); + const wallet = await getWalletSaldoByUserId(id_user); + + if (!wallet || !wallet.data || !wallet.data.id) { + throw new Error("Wallet not found for the specified user"); + } + + console.log("Wallet Data:", wallet.data); + console.log("TopUp Data:", topUp); + + const absoluteFilePath = path.join( + __dirname, + "../../assets", + topUp.bukti_pembayaran + ); + + const fileContent = fs.readFileSync(absoluteFilePath); + + const boundary = "--------------------------" + Date.now().toString(16); + let formParts = []; + + formParts.push( + Buffer.from( + `--${boundary}\r\nContent-Disposition: form-data; name="id_wallet"\r\n\r\n${wallet.data.id}\r\n` + ), + Buffer.from( + `--${boundary}\r\nContent-Disposition: form-data; name="nama"\r\n\r\n${user.nama}\r\n` + ) + ); + + Object.entries(topUp).forEach(([key, value]) => { + if (key !== "bukti_pembayaran") { + formParts.push( + Buffer.from( + `--${boundary}\r\nContent-Disposition: form-data; name="${key}"\r\n\r\n${value}\r\n` + ) + ); + } + }); + + const fileName = path.basename(absoluteFilePath); + formParts.push( + Buffer.from( + `--${boundary}\r\nContent-Disposition: form-data; name="bukti_pembayaran"; filename="${fileName}"\r\nContent-Type: image/png\r\n\r\n` + ), + fileContent, + Buffer.from(`\r\n--${boundary}--\r\n`) + ); + + const formData = Buffer.concat(formParts); + + const walletServiceUrl = process.env.WALLET_URL || "http://localhost:3001"; + await axios.post(`${walletServiceUrl}/topup/create`, formData, { + headers: { + "Content-Type": `multipart/form-data; boundary=${boundary}`, + "Content-Length": formData.length, + }, + }); + + return { message: "Topup has been created, awaiting payment confirmation" }; + } catch (error) { + console.error("Error creating top-up:", error); + throw error; + } +} + +export async function accTopup(id: string): Promise<{ message: string }> { + const tes = await axios.get(`${walletServiceUrl}/topup/${id}`); + console.log(tes); + const topup = await axios.put(`${walletServiceUrl}/topup/acc/${id}`); + if (topup.status !== 200) { + const error = new Error("Failed to update topup status"); + (error as any).statusCode = 500; + throw error; + } + + return { message: "Topup has been confirmed" }; +} + +export async function withdrawSaldo( + id_user: string, + topup: any +): Promise<{ message: string }> { + const user = await getUserById(id_user); + + const topupData = { + id_user: id_user, + nama: user.nama, + ...topup, + }; + + try { + const response = await axios.post( + `${walletServiceUrl}/topup/withdraw`, + topupData + ); + + return { + message: "Withdraw has been created, awaiting payment confirmation", + }; + } catch (error: any) { + console.error("Error sending withdrawSaldo request:", error); + throw new Error( + error.response?.data?.message || "Failed to create withdraw request" + ); + } +} + +export async function accWithdrawSaldo( + id: string, + body: any +): Promise<{ message: string }> { + try { + const absoluteFilePath = path.join( + __dirname, + "../../assets", + body.bukti_pembayaran + ); + + const fileContent = fs.readFileSync(absoluteFilePath); + + const boundary = "--------------------------" + Date.now().toString(16); + + let formParts = []; + + Object.entries(body).forEach(([key, value]) => { + if (key !== "bukti_pembayaran") { + formParts.push( + Buffer.from( + `--${boundary}\r\nContent-Disposition: form-data; name="${key}"\r\n\r\n${value}\r\n` + ) + ); + } + }); + + const fileName = path.basename(absoluteFilePath); + formParts.push( + Buffer.from( + `--${boundary}\r\nContent-Disposition: form-data; name="bukti_pembayaran"; filename="${fileName}"\r\nContent-Type: image/png\r\n\r\n` + ), + fileContent, + Buffer.from(`\r\n--${boundary}--\r\n`) + ); + + const formData = Buffer.concat(formParts); + + const response = await axios.put( + `${walletServiceUrl}/topup/withdraw/${id}`, + formData, + { + headers: { + "Content-Type": `multipart/form-data; boundary=${boundary}`, + "Content-Length": formData.length, + }, + } + ); + + return { message: "Withdraw has been confirmed" }; + } catch (error) { + console.error("Error confirming withdraw:", error); + throw error; + } +} + +export async function paySimpananWajib( + id_user: string, + topUp: any +): Promise<{ message: string }> { + const user = await getUserById(id_user); + + const wallet = await getWalletWajibByUserId(id_user); + + await db + .insert(TopupTable) + .values({ + id_wallet: wallet.id, + nama: user.nama, + jenis: "SIMPANAN WAJIB", + status: "MENUNGGU KONFIRMASI", + ...topUp, + }) + .execute(); + + return { + message: "Simpanan Wajib has been created, awaiting payment confirmation", + }; +} + +export async function accSimpananWajib( + id: string +): Promise<{ message: string }> { + const topup = await db + .update(TopupTable) + .set({ status: "SUKSES", updated_at: new Date() }) + .where(eq(TopupTable.id, id)) + .returning({ id_wallet: TopupTable.id_wallet, nominal: TopupTable.nominal }) + .execute(); + const wallet = await getWalletById(topup[0].id_wallet); + await db + .update(WalletTable) + .set({ saldo: wallet.saldo + topup[0].nominal, updated_at: new Date() }) + .where(eq(WalletTable.id, topup[0].id_wallet)) + .execute(); + + return { message: "Simpanan Wajib has been confirmed" }; +} + +export async function getBagianPemilikPelaksana(search?: string) { + const query = db.select().from(TopupTable); + + if (search) { + query.where(like(TopupTable.nama, `%${search}%`)); + } + + const topUp = await query + .where( + or(eq(TopupTable.jenis, "PEMILIK"), eq(TopupTable.jenis, "PELAKSANA")) + ) + .orderBy(desc(TopupTable.created_at)) + .execute(); + + if (!topUp || topUp.length === 0) { + const error = new Error("No bagian pemilik pengelola found"); + (error as any).statusCode = 404; + throw error; + } + return topUp; +} + +export async function payBagianPemilikPelaksana( + body: any +): Promise<{ message: string }> { + const topup = getTopupById(body.id); + if (!topup) { + const error = new Error("Bagian Pemilik / Pelaksana not found"); + (error as any).statusCode = 404; + throw error; + } + await db + .update(TopupTable) + .set({ status: "SUKSES", updated_at: new Date(), ...body }) + .where(eq(TopupTable.id, body.id)) + .execute(); + return { message: "Pembayaran pemilik / pelaksana telah dikonfirmasi" }; +} diff --git a/services/backend/src/services/transaction.ts b/services/backend/src/services/transaction.ts new file mode 100644 index 0000000..8e07ebd --- /dev/null +++ b/services/backend/src/services/transaction.ts @@ -0,0 +1,83 @@ +import { getContract } from "../main.js"; + +export async function getAllTransaction(search?: string) { + try { + const contract = await getContract(); + const allTransactions = await contract.getAllTransaction(); + + let formattedTransactions = allTransactions.map((transaction: any) => ({ + idUser: transaction[0], + namaUser: transaction[1], + idProjek: transaction[2], + judulProjek: transaction[3], + ownerProjek: transaction[4], + jumlahToken: transaction[5].toString(), + totalNominal: transaction[6].toString(), + })); + + if (search && search.trim()) { + formattedTransactions = formattedTransactions.filter((transaction: any) => transaction.namaUser.toLowerCase().includes(search.toLowerCase())); + } + + return formattedTransactions; + } catch (error) { + console.error("Error in fetching all transactions from blockchain:", error); + throw error; + } +} + +export async function getTransactionByUserId(idUser: string) { + try { + const contract = await getContract(); + const userTransactions = await contract.getTransactionByUserId(idUser); + + if (!userTransactions.length) { + const error = new Error("No transactions found for this user"); + (error as any).statusCode = 404; + throw error; + } + + const formattedTransactions = userTransactions.map((transaction: any) => ({ + idUser: transaction.idUser, + namaUser: transaction.namaUser, + idProjek: transaction.idProjek, + judulProjek: transaction.judulProjek, + ownerProjek: transaction.ownerProjek, + jumlahToken: transaction.jumlahToken.toString(), + totalNominal: transaction.totalNominal.toString(), + })); + + return formattedTransactions; + } catch (error) { + console.error("Error in fetching transactions by user from blockchain:", error); + throw error; + } +} + +export async function getTransactionByProjectId(idProjek: string) { + try { + const contract = await getContract(); + const projectTransactions = await contract.getTransactionByProjectId(idProjek); + + if (!projectTransactions.length) { + const error = new Error("No transactions found for this project"); + (error as any).statusCode = 404; + throw error; + } + + const formattedTransactions = projectTransactions.map((transaction: any) => ({ + idUser: transaction.idUser, + namaUser: transaction.namaUser, + idProjek: transaction.idProjek, + judulProjek: transaction.judulProjek, + ownerProjek: transaction.ownerProjek, + jumlahToken: transaction.jumlahToken.toString(), + totalNominal: transaction.totalNominal.toString(), + })); + + return formattedTransactions; + } catch (error) { + console.error("Error in fetching transactions by project from blockchain:", error); + throw error; + } +} diff --git a/services/backend/src/services/user.ts b/services/backend/src/services/user.ts new file mode 100644 index 0000000..36d5d99 --- /dev/null +++ b/services/backend/src/services/user.ts @@ -0,0 +1,419 @@ +import { and, desc, eq, like } from "drizzle-orm"; +import { db } from "../drizzle/db.js"; +import { + SignatureAdminTable, + TopupTable, + UserTable, + WalletTable, +} from "../drizzle/schema.js"; +import { sendTextWA } from "./baileys.js"; +import axios from "axios"; +import dotenv from "dotenv"; +import * as fs from "fs"; +import path from 'path'; +import { walletServiceUrl } from "../main.js"; + +export async function getAllUser( + search?: string, + filter?: { [key: string]: string | undefined } +) { + const query = db.select().from(UserTable).orderBy(desc(UserTable.created_at)); + + if (filter) { + Object.keys(filter).forEach((key) => { + if (filter[key]) { + switch (key) { + case "status": + query.where( + eq( + UserTable.status, + filter[key] as + | "AKTIF" + | "TIDAK AKTIF" + | "DITOLAK" + | "MENUNGGU KONFIRMASI" + | "OTP TERKIRIM" + ) + ); + break; + default: + const error = new Error("No users found"); + (error as any).statusCode = 404; + throw error; + } + } + }); + } + + if (search) { + query.where(like(UserTable.nama, `%${search}%`)); + } + + const users = await query.execute(); + if (users.length === 0) { + const error = new Error("No users found"); + (error as any).statusCode = 404; + throw error; + } + return users; +} + +export async function countUser() { + const users = await db.select().from(UserTable).execute(); + return users.length; +} + +export async function getUserById(id: string) { + const user = await db + .select() + .from(UserTable) + .where(eq(UserTable.id, id)) + .limit(1) + .execute(); + + if (!user[0]) { + const error = new Error("User not found"); + (error as any).statusCode = 404; + throw error; + } + + const responseData = { + id: user[0].id, + nama: user[0].nama, + no_hp: user[0].no_hp, + role: user[0].role, + status: user[0].status, + password: user[0].password, + tempat_lahir: user[0].tempat_lahir, + tanggal_lahir: user[0].tanggal_lahir, + provinsi: user[0].provinsi, + kota: user[0].kota, + kecamatan: user[0].kecamatan, + alamat: user[0].alamat, + nik: user[0].nik, + foto_profile: user[0].foto_profile, + foto_diri: user[0].foto_diri, + foto_ktp: user[0].foto_ktp, + created_at: user[0].created_at, + updated_at: user[0].updated_at, + otp: user[0].otp, + }; + + if (user[0].role === "ADMIN") { + const adminSignature = await db + .select() + .from(SignatureAdminTable) + .where(eq(SignatureAdminTable.id_user, id)) + .orderBy(desc(SignatureAdminTable.created_at)) + .limit(1) + .execute(); + + return { + ...responseData, + signature: adminSignature[0]?.signature || null, + }; + } + + return responseData; +} + +export async function updateUserById(user: any, id: string) { + return await db.transaction(async (tx) => { + const existingUser = await tx + .select({ + id: UserTable.id, + role: UserTable.role, + foto_ktp: UserTable.foto_ktp, + foto_diri: UserTable.foto_diri, + foto_profile: UserTable.foto_profile, + }) + .from(UserTable) + .where(eq(UserTable.id, id)) + .limit(1) + .execute(); + + if (existingUser.length === 0) { + const error = new Error("User not found"); + (error as any).statusCode = 404; + throw error; + } + + const userData = existingUser[0]; + const isAdmin = userData.role === "ADMIN"; + + const { signature, ...restUserData } = user; + + const updatedData = { + ...restUserData, + foto_ktp: restUserData.foto_ktp || userData.foto_ktp, + foto_diri: restUserData.foto_diri || userData.foto_diri, + foto_profile: restUserData.foto_profile || userData.foto_profile, + }; + + await tx + .update(UserTable) + .set(updatedData) + .where(eq(UserTable.id, id)) + .execute(); + + if (isAdmin && signature) { + await tx + .insert(SignatureAdminTable) + .values({ + id_user: id, + signature, + }) + .execute(); + } + + return { + message: "User successfully updated", + status: "success", + data: { + id, + ...updatedData, + ...(isAdmin && signature ? { signature } : {}), + }, + }; + }); +} + +export async function deleteUserById(id: string) { + await getUserById(id); + await db.delete(UserTable).where(eq(UserTable.id, id)).execute(); +} + +export async function updateUserStatus(id: string, status: any) { + await getUserById(id); + await db + .update(UserTable) + .set({ status }) + .where(eq(UserTable.id, id)) + .execute(); +} + +export async function sendOtp(id: string) { + const user = await getUserById(id); + + const topupData = await db + .select({ + topup: TopupTable, + wallet: WalletTable, + }) + .from(TopupTable) + .innerJoin(WalletTable, eq(TopupTable.id_wallet, WalletTable.id)) + .where(eq(WalletTable.id_user, id)) + .execute(); + + if (topupData.length === 0) { + const error = new Error("No topup data found for this user"); + (error as any).statusCode = 404; + throw error; + } + + for (const { topup, wallet } of topupData) { + if (!topup || !wallet) { + const error = new Error("Topup or Wallet data is missing"); + (error as any).statusCode = 500; + throw error; + } + + await db + .update(TopupTable) + .set({ status: "SUKSES", updated_at: new Date() }) + .where(eq(TopupTable.id, topup.id)) // Update hanya topup ini + .execute(); + await db + .update(WalletTable) + .set({ saldo: wallet.saldo + topup.nominal, updated_at: new Date() }) + .where(eq(WalletTable.id, wallet.id)) + .execute(); + } + + const otp = Math.floor(Math.random() * 9999); + const otpCode = otp.toString().padStart(4, "0"); + + await db + .update(UserTable) + .set({ otp: otpCode, updated_at: new Date(), status: "OTP TERKIRIM" }) + .where(eq(UserTable.id, id)) + .execute(); + + if (user.no_hp) { + const message = `*${otpCode}* adalah kode verifikasi Anda. Gunakan kode ini untuk verifikasi akun Koperasi Anda.`; + await sendTextWA(user.no_hp, message); + } else { + console.log("Phone number not available for this user"); + } +} + +export async function verifyOtp(id: string, otp: string) { + await getUserById(id); + const user = await db + .select() + .from(UserTable) + .where(eq(UserTable.id, id)) + .limit(1) + .execute(); + if (!user[0]) { + const error = new Error("User not found"); + (error as any).statusCode = 404; + throw error; + } + if (user[0].otp === otp) { + await db + .update(UserTable) + .set({ status: "AKTIF" }) + .where(eq(UserTable.id, id)) + .execute(); + } else { + throw new Error("Invalid OTP"); + } +} + +export async function rejectUserById(id: string, message: string) { + const user = await getUserById(id); + await updateUserStatus(id, "DITOLAK"); + if (user.no_hp) { + const msg = `Maaf, akun Anda ditolak oleh admin karena ${message}`; + await sendTextWA(user.no_hp, msg); + } else { + console.log("Phone number not available for this user"); + } +} + +export async function getPhoneNumberAdmin() { + const admin = await db + .select({ no_hp: UserTable.no_hp }) + .from(UserTable) + .where(eq(UserTable.role, "ADMIN")) + .limit(1) + .execute(); + console.log("admin", admin); + + if (!admin[0].no_hp) { + const error = new Error("Phone number not found"); + (error as any).statusCode = 404; + throw error; + } + + return admin[0]; +} + +export async function upgradeUserToPlatinum( + id: string, + nominal: number | undefined, + topUp: any, + filePath: string +) { + try { + const user = await getUserById(id); + const walletUrl = process.env.WALLET_URL || "http://localhost:3001"; + let walletSaldo; + + try { + const response = await axios.get(`${walletUrl}/wallet/user/${id}`); + walletSaldo = response.data.data.id; + } catch (error) { + if ( + axios.isAxiosError(error) && + error.response?.data?.message?.includes("not found") + ) { + console.log("No wallet found, will create new one"); + const response = await axios.post(`${walletUrl}/wallet/create`, { + id_user: id, + }); + walletSaldo = response.data.data.id; + } else { + throw error; + } + } + + const pembayaranWajib = 1000000; + const nominalSetor = + pembayaranWajib + (nominal ? parseInt(nominal.toString(), 10) : 0); + + const absoluteFilePath = path.join(__dirname, "../../assets", filePath); + + const fileContent = fs.readFileSync(absoluteFilePath); + + const boundary = '--------------------------' + Date.now().toString(16); + + let formParts = []; + + formParts.push( + Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="id_wallet"\r\n\r\n${walletSaldo}\r\n`), + Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="nama"\r\n\r\n${user.nama}\r\n`), + Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="nama_bank"\r\n\r\n${topUp.nama_bank}\r\n`), + Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="no_rekening"\r\n\r\n${topUp.no_rekening}\r\n`), + Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="nama_pemilik_rekening"\r\n\r\n${topUp.nama_pemilik_rekening}\r\n`), + Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="nominal"\r\n\r\n${nominalSetor}\r\n`), + Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="jenis"\r\n\r\nUPGRADE USER\r\n`), + Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="status"\r\n\r\nMENUNGGU KONFIRMASI\r\n`) + ); + + const fileName = path.basename(absoluteFilePath); + formParts.push( + Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="bukti_pembayaran"; filename="${fileName}"\r\nContent-Type: image/png\r\n\r\n`), + fileContent, + Buffer.from(`\r\n--${boundary}--\r\n`) + ); + + const formData = Buffer.concat(formParts); + + const createTopup = await axios.post( + `${walletUrl}/topup/create`, + formData, + { + headers: { + 'Content-Type': `multipart/form-data; boundary=${boundary}`, + 'Content-Length': formData.length + } + } + ); + + console.log("Topup created successfully via API:", createTopup.data); + + return { + message: "Upgrade user has been created, awaiting payment confirmation", + }; + } catch (error) { + console.error("Error upgrading user:", error); + throw error; + } +} + +export async function accUpgradeUserToPlatinum(id: string) { + try { + const topup = await axios.get(`${walletServiceUrl}/topup/${id}`); + if (!topup) { + const error = new Error("Topup not found"); + (error as any).statusCode = 404; + throw error; + } + const wallet = await axios.get(`${walletServiceUrl}/wallet/${topup.data.data[0].topup.id_wallet}`); + if (!wallet) { + const error = new Error("Wallet not found"); + (error as any).statusCode = 404; + throw error; + } + + const response = await axios.put(`${walletServiceUrl}/topup/acc/${id}`); + if (!response) { + const error = new Error("Error accepting topup"); + (error as any).statusCode = 500; + throw error; + } + + await db + .update(UserTable) + .set({ role: "PLATINUM", updated_at: new Date() }) + .where(eq(UserTable.id, wallet.data.data.id_user)) + .execute(); + + return { message: "Upgrade user has been confirmed" }; + } catch (error) { + console.error("Error acc upgrading user:", error); + throw error; + } +} diff --git a/services/backend/src/services/wallet.ts b/services/backend/src/services/wallet.ts new file mode 100644 index 0000000..48c2b43 --- /dev/null +++ b/services/backend/src/services/wallet.ts @@ -0,0 +1,84 @@ +import { WalletTable } from "../drizzle/schema.js"; +import { db } from "../drizzle/db.js"; +import { and, eq } from "drizzle-orm"; +import axios from "axios"; +import { walletServiceUrl } from "../main.js"; + +export async function getWalletById(id: string) { + const wallet = await db.select().from(WalletTable).where(eq(WalletTable.id, id)).limit(1).execute(); + if (!wallet[0]) { + const error = new Error("Wallet not found"); + (error as any).statusCode = 404; + throw error; + } + return wallet[0]; +} + +export async function getWalletPokokByUserId(id_user: string) { + const wallet = await db + .select() + .from(WalletTable) + .where(and(eq(WalletTable.id_user, id_user), eq(WalletTable.jenis_wallet, "SIMPANAN POKOK"))) + .execute(); + if (!wallet[0]) { + const error = new Error("Wallet not found"); + (error as any).statusCode = 404; + throw error; + } + return wallet[0]; +} + +export async function getWalletWajibByUserId(id_user: string) { + const wallet = await db + .select() + .from(WalletTable) + .where(and(eq(WalletTable.id_user, id_user), eq(WalletTable.jenis_wallet, "SIMPANAN WAJIB"))) + .limit(1) + .execute(); + if (!wallet[0]) { + const error = new Error("Wallet wajib not found"); + (error as any).statusCode = 404; + throw error; + } + return wallet[0]; +} + +export async function getWalletSaldoByUserId(id_user: string) { + try { + const response = await axios.get(`${walletServiceUrl}/wallet/user/${id_user}`); + return response.data; + } catch (error: any) { + throw new Error(error.response?.data?.message || "Failed to fetch wallet saldo"); + } +} + +export async function createWallet(wallet: any) { + const newWallet = await db.insert(WalletTable).values(wallet).returning({ id: WalletTable.id }).execute(); + return newWallet[0]; +} + +export async function updateWalletById(wallet: any, id: string) { + try { + const response = await axios.put(`${walletServiceUrl}/wallet/${id}`, wallet, { + headers: { + "Content-Type": "application/json", + }, + }); + + if (response.status === 200 || response.status === 204) { + console.log(`Wallet updated successfully for ID: ${id}`); + } else { + console.error(`Failed to update wallet: ${response.statusText}`); + throw new Error(`Failed to update wallet: ${response.statusText}`); + } + } catch (error) { + console.error("Error updating wallet via wallet service:", error); + throw error; + } +} + + +export async function deleteWalletById(id: string) { + await getWalletById(id); + await db.delete(WalletTable).where(eq(WalletTable.id, id)).execute(); +} diff --git a/services/backend/src/swagger.ts b/services/backend/src/swagger.ts new file mode 100644 index 0000000..04b9a0a --- /dev/null +++ b/services/backend/src/swagger.ts @@ -0,0 +1,16 @@ +const swaggerAutogen = require("swagger-autogen")(); + +const doc = { + info: { + title: "Koperasi Backend API", + description: "Documentation for Koperasi Backend API", + }, + host: "localhost:3000", +}; + +const outputFile = "../../../api-docs/swagger-output.json"; +const routes = ["./main.ts"]; + +swaggerAutogen(outputFile, routes, doc).then(() => { + require("./main.ts"); +}); diff --git a/services/backend/src/validations/auth.ts b/services/backend/src/validations/auth.ts new file mode 100644 index 0000000..2f01b1b --- /dev/null +++ b/services/backend/src/validations/auth.ts @@ -0,0 +1,17 @@ +import * as z from "zod"; + +export const RegisterValidation = z.object({ + body: z.object({ + nama: z.string(), + no_hp: z.string().min(10), + role: z.enum(["ADMIN", "BASIC", "PLATINUM"]).optional(), + password: z.string().min(8), + tempat_lahir: z.string(), + tanggal_lahir: z.string().date(), + provinsi: z.string(), + kota: z.string(), + kecamatan: z.string(), + alamat: z.string(), + nik: z.string().min(16), + }), +}); diff --git a/services/backend/src/validations/mutation.ts b/services/backend/src/validations/mutation.ts new file mode 100644 index 0000000..2fd1a8c --- /dev/null +++ b/services/backend/src/validations/mutation.ts @@ -0,0 +1,10 @@ +import * as z from "zod"; + +export const CreateMutationValidation = z.object({ + body: z.object({ + id_projek: z.string(), + judul: z.string(), + pemasukan: z.coerce.number(), + pengeluaran: z.coerce.number(), + }), +}); diff --git a/services/backend/src/validations/project-category.ts b/services/backend/src/validations/project-category.ts new file mode 100644 index 0000000..0b49a28 --- /dev/null +++ b/services/backend/src/validations/project-category.ts @@ -0,0 +1,13 @@ +import { z } from "zod"; +export const CreateProjectCategoryValidation = z.object({ + body: z.object({ + kategori: z.string(), + }), +}); + +export const UpdateProjectCategoryValidation = z.object({ + body: z.object({ + id: z.string(), + kategori: z.string() + }), +}); diff --git a/services/backend/src/validations/project-report.ts b/services/backend/src/validations/project-report.ts new file mode 100644 index 0000000..d4e8085 --- /dev/null +++ b/services/backend/src/validations/project-report.ts @@ -0,0 +1,10 @@ +import { z } from "zod"; +export const CreateProjectReportValidation = z.object({ + body: z.object({ + id_projek: z.string(), + judul: z.string(), + jenis_laporan: z.enum(["UNTUNG", "RUGI"]), + nominal: z.coerce.string(), + modal: z.coerce.string(), + }), +}); diff --git a/services/backend/src/validations/project-token.ts b/services/backend/src/validations/project-token.ts new file mode 100644 index 0000000..33fa8d4 --- /dev/null +++ b/services/backend/src/validations/project-token.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; +export const BuyTokenValidation = z.object({ + body: z.object({ + id_projek: z.string(), + jumlah_token: z.coerce.number(), + }), +}); diff --git a/services/backend/src/validations/project-wallet.ts b/services/backend/src/validations/project-wallet.ts new file mode 100644 index 0000000..0e686c1 --- /dev/null +++ b/services/backend/src/validations/project-wallet.ts @@ -0,0 +1,8 @@ +import { z } from "zod"; +export const TransferSaldoProjectWalletValidation = z.object({ + body: z.object({ + id_projek: z.string(), + nominal: z.coerce.number(), + deskripsi: z.string(), + }), +}); diff --git a/services/backend/src/validations/project.ts b/services/backend/src/validations/project.ts new file mode 100644 index 0000000..c0eb5a4 --- /dev/null +++ b/services/backend/src/validations/project.ts @@ -0,0 +1,39 @@ +import { z } from "zod"; +export const CreateProjectValidation = z.object({ + body: z.object({ + id_kategori: z.string(), + judul: z.string(), + deskripsi: z.string(), + nominal: z.coerce.number(), + asset_jaminan: z.string(), + nilai_jaminan: z.coerce.number(), + lokasi_usaha: z.string(), + detail_lokasi: z.string(), + pendapatan_perbulan: z.coerce.number(), + pengeluaran_perbulan: z.coerce.number(), + limit_siklus: z.coerce.number(), + bagian_pelaksana: z.coerce.number(), + bagian_koperasi: z.coerce.number(), + bagian_pemilik: z.coerce.number(), + bagian_pendana: z.coerce.number(), + }), +}); + +export const UpdateProjectValidation = z.object({ + body: z.object({ + judul: z.string().optional(), + deskripsi: z.string().optional(), + nominal: z.coerce.number().optional(), + asset_jaminan: z.string().optional(), + nilai_jaminan: z.coerce.number().optional(), + lokasi_usaha: z.string().optional(), + detail_lokasi: z.string().optional(), + pendapatan_perbulan: z.coerce.number().optional(), + pengeluaran_perbulan: z.coerce.number().optional(), + limit_siklus: z.coerce.number().optional(), + bagian_pelaksana: z.coerce.number().optional(), + bagian_koperasi: z.coerce.number().optional(), + bagian_pemilik: z.coerce.number().optional(), + bagian_pendana: z.coerce.number().optional(), + }), +}); diff --git a/services/backend/src/validations/topup.ts b/services/backend/src/validations/topup.ts new file mode 100644 index 0000000..e658e12 --- /dev/null +++ b/services/backend/src/validations/topup.ts @@ -0,0 +1,32 @@ +import { z } from "zod"; +export const PayMemberValidation = z.object({ + body: z.object({ + nama_bank: z.string(), + no_rekening: z.string(), + nama_pemilik_rekening: z.string(), + }), +}); +export const PayTopupValidation = z.object({ + body: z.object({ + nama_bank: z.string(), + no_rekening: z.string(), + nama_pemilik_rekening: z.string(), + nominal: z.coerce.number(), + }), +}); +export const PaySimpananWajibValidation = z.object({ + body: z.object({ + nama_bank: z.string(), + no_rekening: z.string(), + nama_pemilik_rekening: z.string(), + nominal: z.coerce.number(), + }), +}); +export const WithdrawSaldoValidation = z.object({ + body: z.object({ + nama_bank: z.string(), + no_rekening: z.string(), + nama_pemilik_rekening: z.string(), + nominal: z.coerce.number(), + }), +}); diff --git a/services/backend/src/validations/user.ts b/services/backend/src/validations/user.ts new file mode 100644 index 0000000..7effade --- /dev/null +++ b/services/backend/src/validations/user.ts @@ -0,0 +1,9 @@ +import { z } from "zod"; +export const UpgradePlatinumValidation = z.object({ + body: z.object({ + nama_bank: z.string(), + no_rekening: z.string(), + nama_pemilik_rekening: z.string(), + nominal: z.coerce.number(), + }), +}); \ No newline at end of file diff --git a/services/backend/tsconfig.json b/services/backend/tsconfig.json new file mode 100644 index 0000000..7f22db1 --- /dev/null +++ b/services/backend/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "nodenext", + "outDir": "./dist", + "moduleResolution": "nodenext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts", "express.d.ts"], + "exclude": ["node_modules"] + } \ No newline at end of file diff --git a/services/frontend/.env.example b/services/frontend/.env.example new file mode 100644 index 0000000..966afd3 --- /dev/null +++ b/services/frontend/.env.example @@ -0,0 +1,3 @@ +PORT=8000 +API_BASE_URL= +SECRET_COOKIE_PASSWORD= \ No newline at end of file diff --git a/services/frontend/.gitignore b/services/frontend/.gitignore new file mode 100644 index 0000000..80ec311 --- /dev/null +++ b/services/frontend/.gitignore @@ -0,0 +1,5 @@ +node_modules + +/.cache +/build +.env diff --git a/services/frontend/.vscode/settings.json b/services/frontend/.vscode/settings.json new file mode 100644 index 0000000..f9959d6 --- /dev/null +++ b/services/frontend/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + "editor.codeActionsOnSave": { + "quickfix.biome": "explicit", + "source.organizeImports.biome": "explicit" + }, + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnSave": true, + "search.exclude": { + "node_modules/**": true, + "build/**": true, + "package-lock.json": true + } +} diff --git a/services/frontend/README.md b/services/frontend/README.md new file mode 100644 index 0000000..dfddee4 --- /dev/null +++ b/services/frontend/README.md @@ -0,0 +1,111 @@ +# Nama Proyek + +Deskripsi proyek Anda di sini. + +## Prasyarat + +- Node.js (disarankan v18 atau lebih tinggi) +- npm atau yarn + +## Panduan Memulai + +### 1. Mengunduh Repositori + +```bash +git clone [url-repositori-anda] +cd [direktori-proyek] +``` + +### 2. Instalasi Dependensi + +```bash +# Menggunakan npm +npm install + +# Atau menggunakan yarn +yarn install +``` + +## Pengembangan Lokal + +Untuk menjalankan server pengembangan: + +```bash +# Menggunakan npm +npm run dev + +# Atau menggunakan yarn +yarn dev +``` + +Aplikasi akan tersedia di `http://localhost:3000` + +## Membangun untuk Produksi + +### 1. Membuat Build Produksi + +```bash +# Menggunakan npm +npm run build + +# Atau menggunakan yarn +yarn build +``` + +### 2. Menjalankan Server Produksi + +```bash +# Menggunakan npm +npm start + +# Atau menggunakan yarn +yarn start +``` + +Aplikasi akan tersedia di `http://localhost:3000` (atau PORT yang telah dikonfigurasi) + +## Hasil Build + +Proses build akan menghasilkan dua direktori utama: +- `build/server` - Kode sisi server +- `build/client` - Aset statis dan kode sisi klien + +## Deployment + +### Variabel Lingkungan + +Buat file `.env` di direktori utama: + +```env +DATABASE_URL="url-database-anda" +SESSION_SECRET="kunci-rahasia-anda" +# Tambahkan variabel lingkungan lain sesuai kebutuhan +``` + +### Pilihan Hosting + +Anda dapat men-deploy aplikasi Remix ini ke berbagai platform: +- Vercel +- Netlify +- Fly.io +- Server Node.js kustom + +### Deployment Manual + +Untuk deployment ke server kustom: +1. Transfer hasil build (`build/` directory) ke server Anda +2. Instal dependensi produksi +3. Atur variabel lingkungan +4. Jalankan server produksi menggunakan `npm start` + +## Teknologi yang Digunakan + +- [Remix](https://remix.run/docs) +- [Tailwind CSS](https://tailwindcss.com/) +- [Vite](https://vitejs.dev/) + +## Dokumentasi Tambahan + +- [Dokumentasi Remix](https://remix.run/docs) +- [Dokumentasi Tailwind CSS](https://tailwindcss.com/docs) +- [Dokumentasi Vite](https://vitejs.dev/guide/) \ No newline at end of file diff --git a/services/frontend/app/components/back-button.tsx b/services/frontend/app/components/back-button.tsx new file mode 100644 index 0000000..d15aada --- /dev/null +++ b/services/frontend/app/components/back-button.tsx @@ -0,0 +1,18 @@ +import { Link, type LinkProps } from "@remix-run/react"; +import { ChevronLeft } from "lucide-react"; +import { forwardRef } from "react"; +import { Button } from "./ui/button"; + +type BackButtonProps = LinkProps; + +const BackButton = forwardRef((props, ref) => { + return ( + + ); +}); + +export default BackButton; diff --git a/services/frontend/app/components/cards/bordered-card.tsx b/services/frontend/app/components/cards/bordered-card.tsx new file mode 100644 index 0000000..11dda0e --- /dev/null +++ b/services/frontend/app/components/cards/bordered-card.tsx @@ -0,0 +1,13 @@ +type BorderedCardProps = { + title: string; + value: string | number; +}; + +export default function BorderedCard({ title, value }: BorderedCardProps) { + return ( +
+

{title}

+

{value}

+
+ ); +} diff --git a/services/frontend/app/components/cards/modal-card.tsx b/services/frontend/app/components/cards/modal-card.tsx new file mode 100644 index 0000000..e763397 --- /dev/null +++ b/services/frontend/app/components/cards/modal-card.tsx @@ -0,0 +1,21 @@ +import type { TokenResponse } from "~/types/api/proyek"; +import { toLocaleDateTime } from "~/utils/format-to-locale-time"; +import toRupiah from "~/utils/to-rupiah"; + +export function InvestorListItem({ investor }: { investor: TokenResponse }) { + return ( +
+
+
+

{investor.nama}

+

{toLocaleDateTime(investor.token_created_at)}

+
+
+
+ {/*

Jumlah Modal

*/} +

{investor.jumlah_token} Token

+

{toRupiah(investor.total_nilai_token)}

+
+
+ ); +} diff --git a/services/frontend/app/components/cards/project-card.tsx b/services/frontend/app/components/cards/project-card.tsx new file mode 100644 index 0000000..51c6664 --- /dev/null +++ b/services/frontend/app/components/cards/project-card.tsx @@ -0,0 +1,134 @@ +import { Link, type LinkProps } from "@remix-run/react"; +import { BadgeDollarSign, User2 } from "lucide-react"; +import { forwardRef } from "react"; +import { useGetProjectToken } from "~/services/projects/get-user-token"; +import type { Proyek } from "~/types/api/proyek"; +import { StatusProject } from "~/types/constants/status-project"; +import toRupiah from "~/utils/to-rupiah"; +import { Badge } from "../ui/badge"; +import { Progress } from "../ui/progress"; +import BorderedCard from "./bordered-card"; + +export interface ProjectCardProps extends LinkProps { + data: Proyek | undefined; +} + +const ProjectCard = forwardRef( + ({ data, to, ...props }, ref) => { + const { data: tokenData } = useGetProjectToken(data?.id); + + const tokenValue = () => { + const collectedTokens = tokenData?.jumlah_token ?? 0; + const totalTokens = data?.jumlah_koin ? Number.parseInt(data.jumlah_koin, 10) : 0; + + if (totalTokens === 0) return 0; + + const percentage = (collectedTokens / totalTokens) * 100; + + return Math.min(100, Math.max(0, percentage)); + }; + + const getTokenPriceTier = (price: number) => { + if (price >= 10000000) return "text-blue-500 font-bold"; + if (price >= 5000000) return "text-green-500 font-bold"; + if (price >= 1000000) return "text-yellow-500 font-bold"; + if (price >= 100000) return "text-gray-400 font-bold"; + return "text-gray-500"; + }; + + const getTokenPriceLabel = (price: number) => { + if (price >= 10000000) + return ( +
+ Diamond Tier +
+ ); + if (price >= 5000000) + return ( +
+ Emerald Tier +
+ ); + if (price >= 1000000) + return ( +
+ Gold Tier +
+ ); + if (price >= 100000) + return ( +
+ +
+ ); + return ""; + }; + + return ( + +
+
+ +
+ {data && ( + + {data.status} + + )} +
+
+ {data && ( + <> +

{data.judul}

+

{data.user?.nama}

+
+

+ Harga PerToken: {toRupiah(data.harga_per_unit?.toString() ?? "0")} +

+ + {getTokenPriceLabel(Number(data.harga_per_unit))} + +
+ + )} +
+
+ + +
+ +
+
+

Terkumpul

+

{tokenData?.jumlah_token ?? 0} Token

+
+
+

Sisa Hari

+

{0} Hari

+
+
+ + ); + }, +); + +ProjectCard.displayName = "ProjectCard"; + +export default ProjectCard; diff --git a/services/frontend/app/components/cards/project-used-token-card.tsx b/services/frontend/app/components/cards/project-used-token-card.tsx new file mode 100644 index 0000000..2573cc4 --- /dev/null +++ b/services/frontend/app/components/cards/project-used-token-card.tsx @@ -0,0 +1,77 @@ +import { Link, type LinkProps } from "@remix-run/react"; +import { User2 } from "lucide-react"; +import { forwardRef } from "react"; +import type { Proyek } from "~/types/api/proyek"; +import { StatusProject } from "~/types/constants/status-project"; +import { Badge } from "../ui/badge"; +import BorderedCard from "./bordered-card"; +import toRupiah from "~/utils/to-rupiah"; +import { Button } from "../ui/button"; +import toPercentage from "~/utils/to-presentase"; + +export interface ProjectUseTokenCardProps extends LinkProps { + data: Proyek | undefined; +} + +const ProjectUseTokenCard = forwardRef( + ({ data, to, ...props }, ref) => { + return ( + +
+
+ +
+ {data && ( + + {data.status} + + )} +
+
+ {data && ( + <> +

{data.judul}

+

{data.user?.nama}

+ + )} +
+
+ +
+

{toRupiah(data?.total_nominal ?? "0")}

+
+

Return :

+

+ {data?.persentase ? toPercentage(data.persentase) : "0.00"} +

+
+
+
+
+ +
+ + ); + }, +); + +ProjectUseTokenCard.displayName = "ProjectUseTokenCard"; + +export default ProjectUseTokenCard; diff --git a/services/frontend/app/components/document-uploader.tsx b/services/frontend/app/components/document-uploader.tsx new file mode 100644 index 0000000..4b0b9ce --- /dev/null +++ b/services/frontend/app/components/document-uploader.tsx @@ -0,0 +1,88 @@ +import { File, Paperclip } from "lucide-react"; +import { useEffect, useState } from "react"; +import { + FileInput, + FileUploader, + FileUploaderContent, + FileUploaderItem, +} from "~/components/extensions/file-upload"; + +interface DocumentUploaderProps { + id: string; + maxFiles?: number; + files: (File | string)[] | null | undefined; + setFiles: (files: (File | string)[] | null) => void; + defaultFileUrl?: string; +} + +export default function DocumentUploader({ + id, + maxFiles = 1, + files, + setFiles, + defaultFileUrl, +}: DocumentUploaderProps) { + const [displayFiles, setDisplayFiles] = useState<(File | string)[]>([]); + + useEffect(() => { + if (defaultFileUrl && (!files || files.length === 0)) { + setFiles([defaultFileUrl]); + } + }, [defaultFileUrl, files, setFiles]); + + useEffect(() => { + setDisplayFiles(files || []); + }, [files]); + + const dropZoneConfig = { + maxFiles: maxFiles, + maxSize: 1024 * 1024 * 4, + multiple: maxFiles > 1, + }; + + const handleValueChange = (newFiles: File[] | null) => { + if (newFiles) { + setFiles(newFiles); + } else { + setFiles(null); + } + }; + + return ( + + {displayFiles.length === 0 && ( + +
+ +
+

+ + Tarik file di sini atau klik untuk mengunggah. + +

+

Unggah hingga {maxFiles} file.

+
+
+
+ )} + + {displayFiles.map((file, i) => ( + + + {typeof file === "string" ? file.split("/").pop() : file.name} + + ))} + +
+ ); +} diff --git a/services/frontend/app/components/document/aggrement-letter.client.tsx b/services/frontend/app/components/document/aggrement-letter.client.tsx new file mode 100644 index 0000000..fe2bf12 --- /dev/null +++ b/services/frontend/app/components/document/aggrement-letter.client.tsx @@ -0,0 +1,338 @@ +import { Document, Font, Image, Page, StyleSheet, Text, View } from "@react-pdf/renderer"; + +Font.register({ + family: "Roboto", + fonts: [ + { + src: "https://fonts.gstatic.com/s/roboto/v27/KFOmCnqEu92Fr1Mu4mxP.ttf", + fontWeight: "normal", + }, + { + src: "https://fonts.gstatic.com/s/roboto/v27/KFOlCnqEu92Fr1MmWUlfBBc9.ttf", + fontWeight: "bold", + }, + { + src: "https://fonts.gstatic.com/s/roboto/v27/KFOkCnqEu92Fr1Mu51xIIzc.ttf", + fontWeight: "normal", + fontStyle: "italic", + }, + ], +}); + +const styles = StyleSheet.create({ + page: { + flexDirection: "column", + backgroundColor: "#ffffff", + padding: 40, + fontFamily: "Roboto", + }, + header: { + marginBottom: 20, + textAlign: "center", + }, + title: { + fontSize: 18, + marginBottom: 10, + fontWeight: "bold", + }, + subtitle: { + fontSize: 14, + marginBottom: 20, + }, + section: { + marginBottom: 10, + }, + table: { + width: "auto", + marginBottom: 10, + }, + tableRow: { + flexDirection: "row", + }, + tableCol1: { + width: "25%", + }, + tableCol2: { + width: "5%", + }, + tableCol3: { + width: "70%", + }, + text: { + fontFamily: "Roboto", + fontSize: 12, + marginBottom: 5, + }, + bold: { + fontFamily: "Roboto", + fontWeight: "bold", + }, + signatureSection: { + flexDirection: "row", + justifyContent: "space-between", + marginTop: 50, + }, + signatureBox: { + width: "40%", + textAlign: "center", + }, + signatureLine: { + borderBottomWidth: 1, + borderBottomColor: "#000000", + marginTop: 40, + marginBottom: 10, + }, + signatureLineEmpty: { + borderBottomWidth: 1, + borderBottomColor: "#000000", + marginTop: 90, + marginBottom: 10, + }, +}); + +export interface AgreementProps { + idProjek: string; + idUser: string; + namaProyek: string; + namaPetugas: string; + alamatPetugas: string; + namaPemilikProyek: string; + nik: string; + noHp: string; + alamat: string; + tandaTangan: string; + signature: string; + nominalDisetujui: string; + tanggal: string; +} + +const AgreementPDF: React.FC = ({ + namaProyek, + namaPetugas, + alamatPetugas, + namaPemilikProyek, + nik, + noHp, + alamat, + tandaTangan, + signature, + nominalDisetujui, + tanggal, +}) => ( + + + + SURAT PERJANJIAN KERJASAMA + Nomor : 001/SPK/RSB/......................... + + + + Yang bertanda tangan dibawah ini : + + + + Nama + + + : + + + {namaPetugas} + + + + + Jabatan + + + : + + + - + + + + + Instansi + + + : + + + Koperasi Rejeki Sukses Berkah + + + + + Alamat + + + : + + + {alamatPetugas} + + + + + Dalam hal ini bertindak untuk dan atas nama Koperasi Rejeki Sukses Berkah, yang + selanjutnya disebut sebagai PIHAK PERTAMA + + + + + + + + Nama + + + : + + + {namaPemilikProyek} + + + + + NIK + + + : + + + {nik} + + + + + Nama Proyek + + + : + + + {namaProyek} + + + + + Alamat + + + : + + + {alamat} + + + + + No.HP + + + : + + + {noHp} + + + + + Dalam hal ini bertindak untuk dan atas nama pribadi, yang selanjutnya disebut sebagai{" "} + PIHAK KEDUA + + + + + + Pada hari ini, tanggal {tanggal}, Kedua belah pihak secara sadar mengadakan perjanjian + kontrak kerja, dengan isi sebagai berikut: + + + + + + PASAL 1 + HAK DAN KEWAJIBAN + + + 1. PIHAK KEDUA telah menerima uang tunai sebesar Rp.{nominalDisetujui} ({nominalDisetujui}{" "} + Rupiah) dari PIHAK PERTAMA yang dimana uang tunai tersebut adalah modal usaha. + + + 2. PIHAK KEDUA berkewajiban melampirkan laporan keuangan kepada penyelenggara melalui + email resmi PIHAK PERTAMA maupun website Koperasi Rejeki Sukses Berkah. + + + 3. PIHAK PERTAMA berkewajiban untuk memberitahukan kepada pemodal terkait kenaikan maupun + penurunan nilai unit proyek. + + + 4. Apabila dikemudian hari ternyata proyek yang dijalankan PIHAK KEDUA mengalami pailit, + maka PIHAK PERTAMA memiliki hak penuh atas barang jaminan baik untuk dimilliki pribadi + maupun untuk dijual kepada orang lain. Hasil dari penjualan aset akan dibagikan kepada + pemodal sesuai jumlah unit yang dimiliki. + + + + + + PASAL 2 + PENYELESAIAN PERSELISIHAN + + + 1. Apabila ada hal-hal yang tidak atau belum diatur dalam perjanjian ini dan juga jika + terjadi perbedaan penafsiran atas seluruh atau sebagian dari perjanjian ini, maka kedua + belah pihak telah sepakat untuk menyelesaikannya secara musyawarah untuk mufakat. + + + 2. Jika penyelesaian secara musyawarah untuk mufakat tidak menyelesaikan perselisihan + tersebut, maka perselisihan tersebut akan diselesaikan secara hukum yang berlaku di + Indonesia + + + + + + PASAL 3 + LAIN - LAIN + + + 1. Hal-hal yang belum tercantum didalam perjanjian ini, akan diatur kemudian. + + + 2. Segala perubahan terhadap sebagian atau seluruh pasal-pasal dalam Perjanjian Kerjasama + ini hanya dapat dilakukan dengan persetujuan kedua belah pihak. + + + 3. Perjanjian ini dibuat bermaterai cukup dan mempunyai kekuatan hukum. + + + + + + Demikianlah Perjanjian Kerjasama ini dibuat oleh kedua belah pihak dalam keadaan sehat + jasmani dan rohani tanpa adanya paksaan ataupun tekanan dari pihak manapun. + + + + + + PIHAK PERTAMA + + + ({namaPetugas}) + + + PIHAK KEDUA + + + ({namaPemilikProyek}) + + + + +); + +export default AgreementPDF; diff --git a/services/frontend/app/components/extensions/file-upload.tsx b/services/frontend/app/components/extensions/file-upload.tsx new file mode 100644 index 0000000..fbb3abe --- /dev/null +++ b/services/frontend/app/components/extensions/file-upload.tsx @@ -0,0 +1,337 @@ +import { Trash2 as RemoveIcon } from "lucide-react"; +import { + type Dispatch, + type SetStateAction, + createContext, + forwardRef, + useCallback, + useContext, + useEffect, + useRef, + useState, +} from "react"; +import { + type DropzoneOptions, + type DropzoneState, + type FileRejection, + useDropzone, +} from "react-dropzone-esm"; +import { toast } from "sonner"; +import { buttonVariants } from "~/components/ui/button"; +import { Input } from "~/components/ui/input"; +import { cn } from "~/lib/clsx"; + +type DirectionOptions = "rtl" | "ltr" | undefined; + +type FileUploaderContextType = { + dropzoneState: DropzoneState; + isLOF: boolean; + isFileTooBig: boolean; + removeFileFromSet: (index: number) => void; + activeIndex: number; + setActiveIndex: Dispatch>; + orientation: "horizontal" | "vertical"; + direction: DirectionOptions; +}; + +const FileUploaderContext = createContext(null); + +export const useFileUpload = () => { + const context = useContext(FileUploaderContext); + if (!context) { + throw new Error("useFileUpload must be used within a FileUploaderProvider"); + } + return context; +}; + +type FileUploaderProps = { + value: File[] | null | undefined; + reSelect?: boolean; + onValueChange: (value: File[] | null) => void; + dropzoneOptions: DropzoneOptions; + orientation?: "horizontal" | "vertical"; +}; + +export const FileUploader = forwardRef< + HTMLDivElement, + FileUploaderProps & React.HTMLAttributes +>( + ( + { + className, + dropzoneOptions, + value, + onValueChange, + reSelect, + orientation = "vertical", + children, + dir, + ...props + }, + ref, + ) => { + const [isFileTooBig, setIsFileTooBig] = useState(false); + const [isLOF, setIsLOF] = useState(false); + const [activeIndex, setActiveIndex] = useState(-1); + const { + accept = { + "image/*": [".jpg", ".jpeg", ".png", ".gif"], + }, + maxFiles = 1, + maxSize = 4 * 1024 * 1024, + multiple = true, + } = dropzoneOptions; + + const reSelectAll = maxFiles === 1 ? true : reSelect; + const direction: DirectionOptions = dir === "rtl" ? "rtl" : "ltr"; + + const removeFileFromSet = useCallback( + (i: number) => { + if (!value) return; + const newFiles = value.filter((_, index) => index !== i); + onValueChange(newFiles); + }, + [value, onValueChange], + ); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + e.preventDefault(); + e.stopPropagation(); + + if (!value) return; + + const moveNext = () => { + const nextIndex = activeIndex + 1; + setActiveIndex(nextIndex > value.length - 1 ? 0 : nextIndex); + }; + + const movePrev = () => { + const nextIndex = activeIndex - 1; + setActiveIndex(nextIndex < 0 ? value.length - 1 : nextIndex); + }; + + const prevKey = + orientation === "horizontal" + ? direction === "ltr" + ? "ArrowLeft" + : "ArrowRight" + : "ArrowUp"; + + const nextKey = + orientation === "horizontal" + ? direction === "ltr" + ? "ArrowRight" + : "ArrowLeft" + : "ArrowDown"; + + if (e.key === nextKey) { + moveNext(); + } else if (e.key === prevKey) { + movePrev(); + } else if (e.key === "Enter" || e.key === "Space") { + if (activeIndex === -1) { + dropzoneState.inputRef.current?.click(); + } + } else if (e.key === "Delete" || e.key === "Backspace") { + if (activeIndex !== -1) { + removeFileFromSet(activeIndex); + if (value.length - 1 === 0) { + setActiveIndex(-1); + return; + } + movePrev(); + } + } else if (e.key === "Escape") { + setActiveIndex(-1); + } + }, + [value, activeIndex, removeFileFromSet, orientation, direction], + ); + + const onDrop = useCallback( + (acceptedFiles: File[], rejectedFiles: FileRejection[]) => { + const files = acceptedFiles; + + if (!files) { + toast.error("file error , probably too big"); + return; + } + + const newValues: File[] = value ? [...value] : []; + + if (reSelectAll) { + newValues.splice(0, newValues.length); + } + + for (const file of files) { + if (newValues.length < maxFiles) { + newValues.push(file); + } + } + + onValueChange(newValues); + + if (rejectedFiles.length > 0) { + for (let i = 0; i < rejectedFiles.length; i++) { + if (rejectedFiles[i].errors[0]?.code === "file-too-large") { + toast.error(`File is too large. Max size is ${maxSize / 1024 / 1024}MB`); + break; + } + if (rejectedFiles[i].errors[0]?.message) { + toast.error(rejectedFiles[i].errors[0].message); + break; + } + } + } + }, + [value, onValueChange, maxFiles, maxSize, reSelectAll], + ); + + useEffect(() => { + if (!value) return; + if (value.length === maxFiles) { + setIsLOF(true); + return; + } + setIsLOF(false); + }, [value, maxFiles]); + + const opts = dropzoneOptions ? dropzoneOptions : { accept, maxFiles, maxSize, multiple }; + + const dropzoneState = useDropzone({ + ...opts, + onDrop, + onDropRejected: () => setIsFileTooBig(true), + onDropAccepted: () => setIsFileTooBig(false), + }); + + return ( + +
0, + })} + dir={dir} + {...props} + > + {children} +
+
+ ); + }, +); + +FileUploader.displayName = "FileUploader"; + +export const FileUploaderContent = forwardRef>( + ({ children, className, ...props }, ref) => { + const { orientation } = useFileUpload(); + const containerRef = useRef(null); + + return ( +
+
+ {children} +
+
+ ); + }, +); + +FileUploaderContent.displayName = "FileUploaderContent"; + +export const FileUploaderItem = forwardRef< + HTMLDivElement, + { index: number } & React.HTMLAttributes +>(({ className, index, children, ...props }, ref) => { + const { removeFileFromSet, activeIndex, direction } = useFileUpload(); + const isSelected = index === activeIndex; + return ( +
+
+ {children} +
+ +
+ ); +}); + +FileUploaderItem.displayName = "FileUploaderItem"; + +export const FileInput = forwardRef>( + ({ className, children, ...props }, ref) => { + const { dropzoneState, isFileTooBig, isLOF } = useFileUpload(); + const rootProps = isLOF ? {} : dropzoneState.getRootProps(); + return ( +
+
+ {children} +
+ +
+ ); + }, +); + +FileInput.displayName = "FileInput"; diff --git a/services/frontend/app/components/icons.tsx b/services/frontend/app/components/icons.tsx new file mode 100644 index 0000000..75a4cc1 --- /dev/null +++ b/services/frontend/app/components/icons.tsx @@ -0,0 +1,66 @@ +import { + AlertTriangle, + ArrowLeftRight, + ArrowRight, + Banknote, + Check, + ChevronLeft, + ChevronRight, + CircuitBoardIcon, + ClipboardPenLine, + Command, + CreditCard, + File, + FileText, + HandCoins, + HelpCircle, + Image, + LayoutDashboard, + LogIn, + LogOut, + type LucideIcon, + Moon, + MoreVertical, + Plus, + Settings, + SunMedium, + Trash, + User, + User2Icon, + Users2, + X, +} from "lucide-react"; + +export type Icon = LucideIcon; + +export const Icons = { + add: Plus, + arrowRight: ArrowRight, + billing: CreditCard, + chevronLeft: ChevronLeft, + chevronRight: ChevronRight, + check: Check, + close: X, + dashboard: LayoutDashboard, + ellipsis: MoreVertical, + help: HelpCircle, + kanban: CircuitBoardIcon, + logo: Command, + login: LogIn, + logout: LogOut, + media: Image, + moon: Moon, + page: File, + pinjaman: ArrowLeftRight, + post: FileText, + profile: User2Icon, + projects: ClipboardPenLine, + project: HandCoins, + saldo: Banknote, + settings: Settings, + sun: SunMedium, + trash: Trash, + user: User, + users: Users2, + warning: AlertTriangle, +}; diff --git a/services/frontend/app/components/input/PasswordObscure.tsx b/services/frontend/app/components/input/PasswordObscure.tsx new file mode 100644 index 0000000..787d4d9 --- /dev/null +++ b/services/frontend/app/components/input/PasswordObscure.tsx @@ -0,0 +1,39 @@ +import { EyeIcon, EyeOff } from "lucide-react"; +import { forwardRef, useState } from "react"; +import { Input } from "../ui/input"; + +export interface InputProps extends React.InputHTMLAttributes {} + +const PasswordObscure = forwardRef(({ className, ...props }, ref) => { + const [isPasswordVisible, setIsPasswordVisible] = useState(false); + + const togglePasswordVisibility = () => { + setIsPasswordVisible(!isPasswordVisible); + }; + + return ( +
+ + {isPasswordVisible ? ( + + ) : ( + + )} +
+ ); +}); + +PasswordObscure.displayName = "PasswordObscure"; + +export default PasswordObscure; diff --git a/services/frontend/app/components/layouts/dashboard-nav.tsx b/services/frontend/app/components/layouts/dashboard-nav.tsx new file mode 100644 index 0000000..f7fcdfc --- /dev/null +++ b/services/frontend/app/components/layouts/dashboard-nav.tsx @@ -0,0 +1,142 @@ +import { Link, useLocation } from "@remix-run/react"; +import type { Dispatch, SetStateAction } from "react"; +import { Icons } from "~/components/icons"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "~/components/ui/accordion"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "~/components/ui/tooltip"; +import { useSidebar } from "~/hooks/use-sidebar"; +import { cn } from "~/lib/clsx"; +import type { NavItem } from "~/types/constants/nav-item"; + +interface DashboardNavProps { + items: NavItem[]; + setOpen?: Dispatch>; + isMobileNav?: boolean; +} + +export function DashboardNav({ items, setOpen, isMobileNav = false }: DashboardNavProps) { + const location = useLocation(); + const { isMinimized } = useSidebar(); + + if (!items?.length) { + return null; + } + + return ( + + ); +} diff --git a/services/frontend/app/components/layouts/header.tsx b/services/frontend/app/components/layouts/header.tsx new file mode 100644 index 0000000..b41fab7 --- /dev/null +++ b/services/frontend/app/components/layouts/header.tsx @@ -0,0 +1,36 @@ +import { useJWTPayload } from "~/hooks/use-jwt-payload"; +import { useGetUserById } from "~/services/profile/get-by-id"; +import type { NavItem } from "~/types/constants/nav-item"; +import { MobileSidebar } from "./mobile-sidebar"; +import { UserNav } from "./user-nav"; + +type HeaderProps = { + navItems: NavItem[]; + profileHref: string; +}; + +export default function Header({ navItems, profileHref }: HeaderProps) { + const { jwtPayload } = useJWTPayload(); + const { data } = useGetUserById(jwtPayload.id); + + return ( +
+ +
+ ); +} diff --git a/services/frontend/app/components/layouts/mobile-sidebar.tsx b/services/frontend/app/components/layouts/mobile-sidebar.tsx new file mode 100644 index 0000000..4e37a06 --- /dev/null +++ b/services/frontend/app/components/layouts/mobile-sidebar.tsx @@ -0,0 +1,41 @@ +import { MenuIcon } from "lucide-react"; +import { useState } from "react"; +import { + Sheet, + SheetContent, + SheetDescription, + SheetTitle, + SheetTrigger, +} from "~/components/ui/sheet"; +import type { NavItem } from "~/types/constants/nav-item"; +import { DashboardNav } from "./dashboard-nav"; + +type MobileSidebarProps = { + navItems: NavItem[]; +}; + +export function MobileSidebar({ navItems }: MobileSidebarProps) { + const [open, setOpen] = useState(false); + return ( + + + + + + + Menu Navigasi + + + Menu Navigasi + +
+
+
+ +
+
+
+
+
+ ); +} diff --git a/services/frontend/app/components/layouts/sidebar.tsx b/services/frontend/app/components/layouts/sidebar.tsx new file mode 100644 index 0000000..6ae33ef --- /dev/null +++ b/services/frontend/app/components/layouts/sidebar.tsx @@ -0,0 +1,53 @@ +import { Link } from "@remix-run/react"; +import { ChevronLeft } from "lucide-react"; +import { useSidebar } from "~/hooks/use-sidebar"; +import { cn } from "~/lib/clsx"; +import type { NavItem } from "~/types/constants/nav-item"; +import { DashboardNav } from "./dashboard-nav"; + +type SidebarProps = { + className?: string; + navItems: NavItem[]; +}; + +export default function Sidebar({ className, navItems }: SidebarProps) { + const { isMinimized, toggle } = useSidebar(); + + const handleToggle = () => { + toggle(); + }; + + return ( + + ); +} diff --git a/services/frontend/app/components/layouts/user-nav.tsx b/services/frontend/app/components/layouts/user-nav.tsx new file mode 100644 index 0000000..d50869d --- /dev/null +++ b/services/frontend/app/components/layouts/user-nav.tsx @@ -0,0 +1,68 @@ +import { Form, Link, useNavigation } from "@remix-run/react"; +import { ChevronDownIcon, LogOutIcon, User2 } from "lucide-react"; +import { Button } from "~/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "~/components/ui/dropdown-menu"; + +type UserNavProps = { + profileHref: string; + username: string; + phone: string; +}; + +export function UserNav({ profileHref, username, phone }: UserNavProps) { + const navigation = useNavigation(); + + return ( + + + + + + +
+

{username}

+

{phone}

+
+
+ + + + + Profile + + + + + +
+ +
+
+
+
+ ); +} diff --git a/services/frontend/app/components/modal/alert-modal.tsx b/services/frontend/app/components/modal/alert-modal.tsx new file mode 100644 index 0000000..aeae1f6 --- /dev/null +++ b/services/frontend/app/components/modal/alert-modal.tsx @@ -0,0 +1,44 @@ +import { useEffect, useState } from "react"; +import { Button } from "~/components/ui/button"; +import { Modal } from "../ui/modal"; + +interface AlertModalProps { + title: string; + description: string; + isOpen: boolean; + onClose: () => void; + onConfirm: () => void; + loading: boolean; +} + +export function AlertModal({ + title = "Apakah anda yakin?", + description = "Tindakan ini tidak dapat dibatalkan.", + isOpen, + onClose, + onConfirm, + loading, +}: AlertModalProps) { + const [isMounted, setIsMounted] = useState(false); + + useEffect(() => { + setIsMounted(true); + }, []); + + if (!isMounted) { + return null; + } + + return ( + +
+ + +
+
+ ); +} diff --git a/services/frontend/app/components/page-container.tsx b/services/frontend/app/components/page-container.tsx new file mode 100644 index 0000000..e5bf892 --- /dev/null +++ b/services/frontend/app/components/page-container.tsx @@ -0,0 +1,18 @@ +import type React from "react"; +import { ScrollArea } from "~/components/ui/scroll-area"; + +export default function PageContainer({ + children, + scrollable = false, +}: { + children: React.ReactNode; + scrollable?: boolean; +}) { + return scrollable ? ( + +
{children}
+
+ ) : ( +
{children}
+ ); +} diff --git a/services/frontend/app/components/table/data-table.tsx b/services/frontend/app/components/table/data-table.tsx new file mode 100644 index 0000000..2198434 --- /dev/null +++ b/services/frontend/app/components/table/data-table.tsx @@ -0,0 +1,115 @@ +import { + type ColumnDef, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { ChevronLeft, ChevronRight } from "lucide-react"; +import { Button } from "../ui/button"; +import { Input } from "../ui/input"; +import { ScrollArea, ScrollBar } from "../ui/scroll-area"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table"; + +interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; + searchKey: string; + searchLabel: string; +} + +export function DataTable({ + columns, + data, + searchKey, + searchLabel, +}: DataTableProps) { + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + }); + + return ( + <> + table.getColumn(searchKey)?.setFilterValue(event.target.value)} + placeholder={`Cari berdasarkan ${searchLabel}...`} + value={(table.getColumn(searchKey)?.getFilterValue() as string) ?? ""} + /> + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell, index) => + index === 0 ? ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ) : ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ), + )} + + )) + ) : ( + + + Tidak ada data yang ditemukan. + + + )} + +
+ +
+
+
+ {table.getFilteredSelectedRowModel().rows.length} dari{" "} + {table.getFilteredRowModel().rows.length} dipilih. +
+
+ + +
+
+ + ); +} diff --git a/services/frontend/app/components/table/table.tsx b/services/frontend/app/components/table/table.tsx new file mode 100644 index 0000000..4647bcc --- /dev/null +++ b/services/frontend/app/components/table/table.tsx @@ -0,0 +1,67 @@ +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "~/components/ui/table"; +import { ScrollArea, ScrollBar } from "../ui/scroll-area"; + +export interface TableColumn { + header: string; + accessor: keyof T; + cell?: (value: T[keyof T]) => React.ReactNode; +} + +interface TableProps> { + columns: TableColumn[]; + data: T[]; +} + +export default function TableComponent>({ + columns, + data, +}: TableProps) { + return ( + <> + + + + + {columns.map((column, index) => ( + + {column.header} + + ))} + + + + {data.map((row, rowIndex) => ( + + {columns.map((column, cellIndex) => ( + + {column.cell + ? column.cell(row[column.accessor]) + : (row[column.accessor] as React.ReactNode)} + + ))} + + ))} + {data.length === 0 && ( + + + Tidak ada data yang tersedia + + + )} + +
+ +
+

+ Menampilkan {data.length} Data terakhir +

+ + ); +} diff --git a/services/frontend/app/components/theme-toogle.tsx b/services/frontend/app/components/theme-toogle.tsx new file mode 100644 index 0000000..7fd6d84 --- /dev/null +++ b/services/frontend/app/components/theme-toogle.tsx @@ -0,0 +1,28 @@ +import { MoonIcon, SunIcon } from "lucide-react"; +import { Theme, useTheme } from "remix-themes"; +import { Button } from "./ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "./ui/dropdown-menu"; + +export default function ThemeToggle() { + const [, setTheme] = useTheme(); + return ( + + + + + + setTheme(Theme.LIGHT)}>Light + setTheme(Theme.DARK)}>Dark + + + ); +} diff --git a/services/frontend/app/components/ui/accordion.tsx b/services/frontend/app/components/ui/accordion.tsx new file mode 100644 index 0000000..fc0403b --- /dev/null +++ b/services/frontend/app/components/ui/accordion.tsx @@ -0,0 +1,52 @@ +import * as AccordionPrimitive from "@radix-ui/react-accordion"; +import { ChevronDown } from "lucide-react"; +import * as React from "react"; + +import { cn } from "~/lib/clsx"; + +const Accordion = AccordionPrimitive.Root; + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AccordionItem.displayName = "AccordionItem"; + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className, + )} + {...props} + > + {children} + + + +)); +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)); + +AccordionContent.displayName = AccordionPrimitive.Content.displayName; + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/services/frontend/app/components/ui/alert-dialog.tsx b/services/frontend/app/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..f509ffd --- /dev/null +++ b/services/frontend/app/components/ui/alert-dialog.tsx @@ -0,0 +1,115 @@ +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; +import * as React from "react"; + +import { buttonVariants } from "~/components/ui/button"; +import { cn } from "~/lib/clsx"; + +const AlertDialog = AlertDialogPrimitive.Root; + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger; + +const AlertDialogPortal = AlertDialogPrimitive.Portal; + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName; + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)); +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName; + +const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( +
+); +AlertDialogHeader.displayName = "AlertDialogHeader"; + +const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( +
+); +AlertDialogFooter.displayName = "AlertDialogFooter"; + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName; + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName; + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName; + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +}; diff --git a/services/frontend/app/components/ui/avatar.tsx b/services/frontend/app/components/ui/avatar.tsx new file mode 100644 index 0000000..794b1f0 --- /dev/null +++ b/services/frontend/app/components/ui/avatar.tsx @@ -0,0 +1,45 @@ +import * as AvatarPrimitive from "@radix-ui/react-avatar"; +import * as React from "react"; + +import { cn } from "~/lib/clsx"; + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Avatar.displayName = AvatarPrimitive.Root.displayName; + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AvatarImage.displayName = AvatarPrimitive.Image.displayName; + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; + +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/services/frontend/app/components/ui/badge.tsx b/services/frontend/app/components/ui/badge.tsx new file mode 100644 index 0000000..78346c8 --- /dev/null +++ b/services/frontend/app/components/ui/badge.tsx @@ -0,0 +1,31 @@ +import { type VariantProps, cva } from "class-variance-authority"; +import type * as React from "react"; + +import { cn } from "~/lib/clsx"; + +const badgeVariants = cva( + "inline-flex items-center rounded-lg border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: "border-transparent bg-primary text-primary-foreground", + secondary: "border-transparent bg-secondary text-secondary-foreground", + destructive: "border-transparent bg-destructive text-destructive-foreground", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return
; +} + +export { Badge, badgeVariants }; diff --git a/services/frontend/app/components/ui/button.tsx b/services/frontend/app/components/ui/button.tsx new file mode 100644 index 0000000..6e9ad3e --- /dev/null +++ b/services/frontend/app/components/ui/button.tsx @@ -0,0 +1,49 @@ +import { Slot } from "@radix-ui/react-slot"; +import { type VariantProps, cva } from "class-variance-authority"; +import * as React from "react"; + +import { cn } from "~/lib/clsx"; + +const buttonVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/80", + destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ( + + ); + }, +); +Button.displayName = "Button"; + +export { Button, buttonVariants }; diff --git a/services/frontend/app/components/ui/card.tsx b/services/frontend/app/components/ui/card.tsx new file mode 100644 index 0000000..17afd38 --- /dev/null +++ b/services/frontend/app/components/ui/card.tsx @@ -0,0 +1,56 @@ +import * as React from "react"; + +import { cn } from "~/lib/clsx"; + +const Card = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +Card.displayName = "Card"; + +const CardHeader = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +CardHeader.displayName = "CardHeader"; + +const CardTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +

+ ), +); +CardTitle.displayName = "CardTitle"; + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardDescription.displayName = "CardDescription"; + +const CardContent = React.forwardRef>( + ({ className, ...props }, ref) => ( +

+ ), +); +CardContent.displayName = "CardContent"; + +const CardFooter = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +CardFooter.displayName = "CardFooter"; + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }; diff --git a/services/frontend/app/components/ui/carousel.tsx b/services/frontend/app/components/ui/carousel.tsx new file mode 100644 index 0000000..90cc1de --- /dev/null +++ b/services/frontend/app/components/ui/carousel.tsx @@ -0,0 +1,240 @@ +import useEmblaCarousel, { type UseEmblaCarouselType } from "embla-carousel-react"; +import { ArrowLeft, ArrowRight } from "lucide-react"; +import * as React from "react"; + +import { Button } from "~/components/ui/button"; +import { cn } from "~/lib/clsx"; + +type CarouselApi = UseEmblaCarouselType[1]; +type UseCarouselParameters = Parameters; +type CarouselOptions = UseCarouselParameters[0]; +type CarouselPlugin = UseCarouselParameters[1]; + +type CarouselProps = { + opts?: CarouselOptions; + plugins?: CarouselPlugin; + orientation?: "horizontal" | "vertical"; + setApi?: (api: CarouselApi) => void; +}; + +type CarouselContextProps = { + carouselRef: ReturnType[0]; + api: ReturnType[1]; + scrollPrev: () => void; + scrollNext: () => void; + canScrollPrev: boolean; + canScrollNext: boolean; +} & CarouselProps; + +const CarouselContext = React.createContext(null); + +function useCarousel() { + const context = React.useContext(CarouselContext); + + if (!context) { + throw new Error("useCarousel must be used within a "); + } + + return context; +} + +const Carousel = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & CarouselProps +>(({ orientation = "horizontal", opts, setApi, plugins, className, children, ...props }, ref) => { + const [carouselRef, api] = useEmblaCarousel( + { + ...opts, + axis: orientation === "horizontal" ? "x" : "y", + }, + plugins, + ); + const [canScrollPrev, setCanScrollPrev] = React.useState(false); + const [canScrollNext, setCanScrollNext] = React.useState(false); + + const onSelect = React.useCallback((api: CarouselApi) => { + if (!api) { + return; + } + + setCanScrollPrev(api.canScrollPrev()); + setCanScrollNext(api.canScrollNext()); + }, []); + + const scrollPrev = React.useCallback(() => { + api?.scrollPrev(); + }, [api]); + + const scrollNext = React.useCallback(() => { + api?.scrollNext(); + }, [api]); + + const handleKeyDown = React.useCallback( + (event: React.KeyboardEvent) => { + if (event.key === "ArrowLeft") { + event.preventDefault(); + scrollPrev(); + } else if (event.key === "ArrowRight") { + event.preventDefault(); + scrollNext(); + } + }, + [scrollPrev, scrollNext], + ); + + React.useEffect(() => { + if (!api || !setApi) { + return; + } + + setApi(api); + }, [api, setApi]); + + React.useEffect(() => { + if (!api) { + return; + } + + onSelect(api); + api.on("reInit", onSelect); + api.on("select", onSelect); + + return () => { + api?.off("select", onSelect); + }; + }, [api, onSelect]); + + return ( + +
+ {children} +
+
+ ); +}); +Carousel.displayName = "Carousel"; + +const CarouselContent = React.forwardRef>( + ({ className, ...props }, ref) => { + const { carouselRef, orientation } = useCarousel(); + + return ( +
+
+
+ ); + }, +); +CarouselContent.displayName = "CarouselContent"; + +const CarouselItem = React.forwardRef>( + ({ className, ...props }, ref) => { + const { orientation } = useCarousel(); + + return ( +
+ ); + }, +); +CarouselItem.displayName = "CarouselItem"; + +const CarouselPrevious = React.forwardRef>( + ({ className, variant = "outline", size = "icon", ...props }, ref) => { + const { orientation, scrollPrev, canScrollPrev } = useCarousel(); + + return ( + + ); + }, +); +CarouselPrevious.displayName = "CarouselPrevious"; + +const CarouselNext = React.forwardRef>( + ({ className, variant = "outline", size = "icon", ...props }, ref) => { + const { orientation, scrollNext, canScrollNext } = useCarousel(); + + return ( + + ); + }, +); +CarouselNext.displayName = "CarouselNext"; + +export { + type CarouselApi, + Carousel, + CarouselContent, + CarouselItem, + CarouselPrevious, + CarouselNext, +}; diff --git a/services/frontend/app/components/ui/chart.tsx b/services/frontend/app/components/ui/chart.tsx new file mode 100644 index 0000000..48cb032 --- /dev/null +++ b/services/frontend/app/components/ui/chart.tsx @@ -0,0 +1,328 @@ +import * as React from "react"; +import * as RechartsPrimitive from "recharts"; + +import { cn } from "~/lib/clsx"; + +// Format: { THEME_NAME: CSS_SELECTOR } +const THEMES = { light: "", dark: ".dark" } as const; + +export type ChartConfig = { + [k in string]: { + label?: React.ReactNode; + icon?: React.ComponentType; + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record } + ); +}; + +type ChartContextProps = { + config: ChartConfig; +}; + +const ChartContext = React.createContext(null); + +function useChart() { + const context = React.useContext(ChartContext); + + if (!context) { + throw new Error("useChart must be used within a "); + } + + return context; +} + +const ChartContainer = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + config: ChartConfig; + children: React.ComponentProps["children"]; + } +>(({ id, className, children, config, ...props }, ref) => { + const uniqueId = React.useId(); + const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`; + + return ( + +
+ + {children} +
+
+ ); +}); +ChartContainer.displayName = "Chart"; + +const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { + const colorConfig = Object.entries(config).filter(([_, config]) => config.theme || config.color); + + if (!colorConfig.length) { + return null; + } + + return ( +