anoderb commited on
Commit
ef874e6
·
0 Parent(s):

feat: initial commit of Tokiva POS Node backend for Hugging Face

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +35 -0
  2. Dockerfile +25 -0
  3. README.md +181 -0
  4. package-lock.json +1833 -0
  5. package.json +32 -0
  6. prisma/schema.prisma +538 -0
  7. src/app.ts +24 -0
  8. src/controllers/auth.controller.ts +69 -0
  9. src/controllers/bon.controller.ts +63 -0
  10. src/controllers/kategori.controller.ts +60 -0
  11. src/controllers/member.controller.ts +60 -0
  12. src/controllers/ops.controller.ts +159 -0
  13. src/controllers/pengguna.controller.ts +84 -0
  14. src/controllers/produk.controller.ts +88 -0
  15. src/controllers/stok.controller.ts +73 -0
  16. src/controllers/supplier.controller.ts +60 -0
  17. src/controllers/transaksi.controller.ts +118 -0
  18. src/controllers/upload.controller.ts +41 -0
  19. src/middlewares/auth.middleware.ts +56 -0
  20. src/middlewares/error.middleware.ts +17 -0
  21. src/middlewares/validate.middleware.ts +32 -0
  22. src/routes/auth.route.ts +13 -0
  23. src/routes/bon.route.ts +17 -0
  24. src/routes/index.ts +28 -0
  25. src/routes/kategori.route.ts +17 -0
  26. src/routes/member.route.ts +16 -0
  27. src/routes/ops.route.ts +30 -0
  28. src/routes/pengguna.route.ts +19 -0
  29. src/routes/produk.route.ts +19 -0
  30. src/routes/stok.route.ts +18 -0
  31. src/routes/supplier.route.ts +16 -0
  32. src/routes/transaksi.route.ts +18 -0
  33. src/routes/upload.route.ts +14 -0
  34. src/schemas/auth.schema.ts +14 -0
  35. src/schemas/bon.schema.ts +8 -0
  36. src/schemas/kategori.schema.ts +20 -0
  37. src/schemas/member.schema.ts +29 -0
  38. src/schemas/ops.schema.ts +33 -0
  39. src/schemas/pengguna.schema.ts +28 -0
  40. src/schemas/produk.schema.ts +55 -0
  41. src/schemas/stok.schema.ts +21 -0
  42. src/schemas/supplier.schema.ts +26 -0
  43. src/schemas/transaksi.schema.ts +30 -0
  44. src/schemas/upload.schema.ts +8 -0
  45. src/server.ts +10 -0
  46. src/services/auth.service.ts +115 -0
  47. src/services/bon.service.ts +188 -0
  48. src/services/diskon.service.ts +214 -0
  49. src/services/kategori.service.ts +149 -0
  50. src/services/member.service.ts +196 -0
.gitignore ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dependency directories
2
+ node_modules/
3
+
4
+ # Optional npm cache directory
5
+ .npm/
6
+
7
+ # Output directory
8
+ dist/
9
+
10
+ # Local env files
11
+ .env
12
+ .env.local
13
+ .env.development.local
14
+ .env.test.local
15
+ .env.production.local
16
+
17
+ # Log files
18
+ npm-debug.log*
19
+ yarn-debug.log*
20
+ yarn-error.log*
21
+ .pnpm-debug.log*
22
+ trace.txt
23
+
24
+ # Editor / IDE configuration folders
25
+ .vscode/
26
+ .idea/
27
+ *.suo
28
+ *.ntvs*
29
+ *.njsproj
30
+ *.sln
31
+ *.swp
32
+ *~
33
+
34
+ # TypeScript build info cache
35
+ *.tsbuildinfo
Dockerfile ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:18-alpine
2
+
3
+ WORKDIR /app
4
+
5
+ # Copy package files and install dependencies
6
+ COPY package*.json tsconfig.json ./
7
+ RUN npm ci
8
+
9
+ # Copy project files
10
+ COPY . .
11
+
12
+ # Generate Prisma Client
13
+ RUN npx prisma generate
14
+
15
+ # Build TypeScript to JavaScript
16
+ RUN npm run build
17
+
18
+ # Expose port 7860 (required by Hugging Face)
19
+ EXPOSE 7860
20
+
21
+ # Override PORT environment variable
22
+ ENV PORT=7860
23
+
24
+ # Run the backend server
25
+ CMD ["npm", "start"]
README.md ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Tokiva POS Backend (Node.js & TypeScript Edition)
2
+
3
+ Backend ini dibangun menggunakan **Node.js**, **Express**, **TypeScript**, ORM **Prisma**, dan database **MySQL**. Aplikasi ini dirancang untuk mendukung operasional sistem POS (Point of Sales) ritel modern, lengkap dengan fitur manajemen stok berbasis FEFO (First Expired First Out), kasir shift, audit logging, dan integrasi penyimpanan cloud (Supabase Storage).
4
+
5
+ ---
6
+
7
+ ## 🛠️ Persyaratan System (Prerequisites)
8
+
9
+ - Node.js (v18 atau lebih tinggi)
10
+ - npm atau yarn
11
+ - Database MySQL
12
+
13
+ ---
14
+
15
+ ## 🚀 Cara Menjalankan secara Lokal
16
+
17
+ ### 1. Install Dependensi
18
+ Masuk ke direktori `backend-node` dan jalankan instalasi:
19
+ ```bash
20
+ cd backend-node
21
+ npm install
22
+ ```
23
+
24
+ ### 2. Setup Environment Variable
25
+ Buat file `.env` di dalam folder root `backend-node` dan isi sesuai dengan kebutuhan Anda:
26
+ ```env
27
+ PORT=5000
28
+ DATABASE_URL="mysql://username:password@127.0.0.1:3306/tokiva"
29
+
30
+ # JWT Configuration
31
+ JWT_SECRET_KEY="rahasia_super_kuat_anda"
32
+ JWT_EXPIRES_IN="24h"
33
+
34
+ # Supabase Storage Configuration (Opsional, untuk upload gambar produk/member)
35
+ SUPABASE_URL="https://your-supabase-url.supabase.co"
36
+ SUPABASE_ANON_KEY="your-anon-key"
37
+ SUPABASE_SERVICE_KEY="your-service-role-key"
38
+ ```
39
+
40
+ ### 3. Sinkronisasi Skema Database & Generate Prisma Client
41
+ Jalankan perintah berikut untuk mensinkronkan skema database lokal Anda dengan Prisma Client:
42
+ ```bash
43
+ npx prisma generate
44
+ ```
45
+
46
+ ### 4. Jalankan Server Development
47
+ Jalankan server dalam mode development (menggunakan `ts-node-dev` untuk auto-reload):
48
+ ```bash
49
+ npm run dev
50
+ ```
51
+ Server akan berjalan di: **`http://localhost:5000`**
52
+
53
+ ---
54
+
55
+ ## 📂 Struktur Direktori Proyek
56
+
57
+ ```text
58
+ backend-node/
59
+ ├── prisma/ # Skema database Prisma
60
+ │ └── schema.prisma
61
+ ├── src/
62
+ │ ├── controllers/ # Menangani HTTP request dan format response
63
+ │ ├── middlewares/ # Middleware Express (Auth, Zod Validation, Error handler)
64
+ │ ├── routes/ # Defini endpoint API Express
65
+ │ ├── schemas/ # Validator skema request input menggunakan Zod
66
+ │ ├── services/ # Logika bisnis inti aplikasi
67
+ │ ├── utils/ # Helper (Supabase, JWT, response formatter, audit logger)
68
+ │ ├── app.ts # Setup Express application
69
+ │ └── server.ts # Entrypoint API server
70
+ ├── .env # File konfigurasi environment
71
+ ├── package.json # File dependensi proyek
72
+ └── tsconfig.json # Konfigurasi TypeScript compiler
73
+ ```
74
+
75
+ ---
76
+
77
+ ## 📘 Referensi Endpoint API
78
+
79
+ Seluruh endpoint API memiliki prefix `/api`.
80
+ Endpoint yang membutuhkan header **Authorization** harus melampirkan token JWT: `Bearer <token_jwt>`.
81
+
82
+ ### 1. Autentikasi (`/api/auth`)
83
+ | Method | Endpoint | Auth | Deskripsi |
84
+ |---|---|---|---|
85
+ | `POST` | `/auth/login` | Publik | Login pengguna menggunakan username dan password |
86
+ | `POST` | `/auth/refresh` | Publik | Melakukan refresh JWT token |
87
+
88
+ ### 2. Manajemen Pengguna / Karyawan (`/api/users`)
89
+ | Method | Endpoint | Auth | Deskripsi |
90
+ |---|---|---|---|
91
+ | `GET` | `/users` | Admin | Memuat daftar semua pengguna |
92
+ | `POST` | `/users` | Admin | Membuat akun pengguna baru |
93
+ | `PUT` | `/users/:id` | Admin | Mengedit informasi akun pengguna |
94
+ | `DELETE` | `/users/:id` | Admin | Menghapus akun pengguna (Soft-Delete) |
95
+
96
+ ### 3. Kategori Produk (`/api/kategori`)
97
+ | Method | Endpoint | Auth | Deskripsi |
98
+ |---|---|---|---|
99
+ | `GET` | `/kategori` | Token | Memuat seluruh kategori produk |
100
+ | `POST` | `/kategori` | Token | Menambah kategori baru |
101
+ | `PUT` | `/kategori/:id` | Token | Mengubah kategori |
102
+ | `DELETE` | `/kategori/:id` | Token | Menghapus kategori |
103
+
104
+ ### 4. Supplier / Pemasok (`/api/supplier`)
105
+ | Method | Endpoint | Auth | Deskripsi |
106
+ |---|---|---|---|
107
+ | `GET` | `/supplier` | Token | Memuat seluruh supplier aktif |
108
+ | `POST` | `/supplier` | Token | Menambah supplier baru |
109
+ | `PUT` | `/supplier/:id` | Token | Mengubah data supplier |
110
+ | `DELETE` | `/supplier/:id` | Token | Menghapus supplier |
111
+
112
+ ### 5. Member / Pelanggan Loyalitas (`/api/member`)
113
+ | Method | Endpoint | Auth | Deskripsi |
114
+ |---|---|---|---|
115
+ | `GET` | `/member` | Token | Memuat daftar member |
116
+ | `POST` | `/member` | Token | Mendaftarkan member baru |
117
+ | `PUT` | `/member/:id` | Token | Memperbarui profil member |
118
+ | `DELETE` | `/member/:id` | Token | Menghapus member |
119
+
120
+ ### 6. Produk (`/api/produk`)
121
+ | Method | Endpoint | Auth | Deskripsi |
122
+ |---|---|---|---|
123
+ | `GET` | `/produk` | Token | Memuat daftar produk (mendukung pencarian & filter) |
124
+ | `GET` | `/produk/:id` | Token | Memuat detail lengkap satu produk |
125
+ | `GET` | `/produk/scan/:barcode_or_kode` | Token | Mencari produk berdasarkan barcode/kode untuk scan cepat |
126
+ | `POST` | `/produk` | Token | Menambah produk baru |
127
+ | `PUT` | `/produk/:id` | Token | Memperbarui produk |
128
+ | `DELETE` | `/produk/:id` | Token | Menghapus produk (Soft-Delete) |
129
+
130
+ ### 7. Manajemen Stok & Batch FEFO (`/api/stok`)
131
+ | Method | Endpoint | Auth | Deskripsi |
132
+ |---|---|---|---|
133
+ | `POST` | `/stok/masuk` | Token | Mencatat barang masuk/restock ke batch tertentu |
134
+ | `POST` | `/stok/opname` | Token | Membuat draf stok opname (penyesuaian fisik) |
135
+ | `POST` | `/stok/opname/:opname_id/approve` | Token | Menyetujui opname dan menyesuaikan stok utama produk |
136
+ | `GET` | `/stok/mutasi` | Token | Memuat kartu stok / riwayat mutasi stok produk |
137
+ | `GET` | `/stok/batches` | Token | Memuat batch stok produk yang masih aktif (qty sisa > 0) |
138
+
139
+ ### 8. Transaksi Penjualan (`/api/transaksi`)
140
+ | Method | Endpoint | Auth | Deskripsi |
141
+ |---|---|---|---|
142
+ | `POST` | `/transaksi` | Token | Mencatat transaksi kasir baru (FEFO auto-deduction) |
143
+ | `GET` | `/transaksi` | Token | Memuat riwayat transaksi penjualan |
144
+ | `POST` | `/transaksi/retur` | Token | Melakukan retur barang dari suatu transaksi (refund/exchange) |
145
+
146
+ ### 9. Bon & Piutang Member (`/api/bon`)
147
+ | Method | Endpoint | Auth | Deskripsi |
148
+ |---|---|---|---|
149
+ | `GET` | `/bon` | Token | Memuat semua daftar bon piutang member |
150
+ | `POST` | `/bon/:bon_id/cicil` | Token | Mencatat pembayaran cicilan bon member |
151
+
152
+ ### 10. Laporan Finansial & Operasional (`/api`)
153
+ | Method | Endpoint | Auth | Deskripsi |
154
+ |---|---|---|---|
155
+ | `GET` | `/laporan/ringkasan` | Admin | Memuat ringkasan keuangan (pendapatan, laba kotor, dll) |
156
+ | `GET` | `/laporan/penjualan` | Admin | Laporan detail penjualan berdasarkan rentang tanggal |
157
+ | `GET` | `/laporan/stok` | Admin | Laporan analisis stok produk saat ini |
158
+
159
+ ### 11. Manajemen Shift Kasir (`/api`)
160
+ | Method | Endpoint | Auth | Deskripsi |
161
+ |---|---|---|---|
162
+ | `POST` | `/shift/buka` | Token | Membuka shift kasir baru dan mencatat modal awal |
163
+ | `POST` | `/shift/tutup` | Token | Menutup shift kasir, mencatat total fisik uang, & mendeteksi selisih |
164
+ | `GET` | `/shift/aktif` | Token | Mendapatkan informasi shift yang sedang berjalan |
165
+
166
+ ### 12. Upload File Media (`/api/upload`)
167
+ | Method | Endpoint | Auth | Deskripsi |
168
+ |---|---|---|---|
169
+ | `POST` | `/upload/produk` | Token | Upload gambar produk ke Supabase Storage (Base64/Form-Data) |
170
+ | `POST` | `/upload/member` | Token | Upload foto member ke Supabase Storage |
171
+
172
+ ---
173
+
174
+ ## 🧪 Jalankan Tes Integrasi CRUD
175
+
176
+ Untuk memverifikasi fungsionalitas CRUD secara end-to-end terhadap database MySQL lokal Anda:
177
+ 1. Pastikan server backend sedang berjalan (`npm run dev`).
178
+ 2. Jalankan perintah script uji coba:
179
+ ```bash
180
+ node scratch/test-crud.js
181
+ ```
package-lock.json ADDED
@@ -0,0 +1,1833 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "tokiva-pos-backend-node",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "tokiva-pos-backend-node",
9
+ "version": "1.0.0",
10
+ "dependencies": {
11
+ "@prisma/client": "^5.19.0",
12
+ "bcryptjs": "^2.4.3",
13
+ "cors": "^2.8.5",
14
+ "dotenv": "^16.4.5",
15
+ "express": "^4.21.0",
16
+ "jsonwebtoken": "^9.0.2",
17
+ "zod": "^3.23.8"
18
+ },
19
+ "devDependencies": {
20
+ "@types/bcryptjs": "^2.4.6",
21
+ "@types/cors": "^2.8.17",
22
+ "@types/express": "^4.17.21",
23
+ "@types/jsonwebtoken": "^9.0.7",
24
+ "@types/node": "^22.5.0",
25
+ "prisma": "^5.19.0",
26
+ "ts-node-dev": "^2.0.0",
27
+ "typescript": "^5.5.4"
28
+ }
29
+ },
30
+ "node_modules/@cspotcode/source-map-support": {
31
+ "version": "0.8.1",
32
+ "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
33
+ "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
34
+ "dev": true,
35
+ "dependencies": {
36
+ "@jridgewell/trace-mapping": "0.3.9"
37
+ },
38
+ "engines": {
39
+ "node": ">=12"
40
+ }
41
+ },
42
+ "node_modules/@jridgewell/resolve-uri": {
43
+ "version": "3.1.2",
44
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
45
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
46
+ "dev": true,
47
+ "engines": {
48
+ "node": ">=6.0.0"
49
+ }
50
+ },
51
+ "node_modules/@jridgewell/sourcemap-codec": {
52
+ "version": "1.5.5",
53
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
54
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
55
+ "dev": true
56
+ },
57
+ "node_modules/@jridgewell/trace-mapping": {
58
+ "version": "0.3.9",
59
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
60
+ "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
61
+ "dev": true,
62
+ "dependencies": {
63
+ "@jridgewell/resolve-uri": "^3.0.3",
64
+ "@jridgewell/sourcemap-codec": "^1.4.10"
65
+ }
66
+ },
67
+ "node_modules/@prisma/client": {
68
+ "version": "5.22.0",
69
+ "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz",
70
+ "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==",
71
+ "hasInstallScript": true,
72
+ "engines": {
73
+ "node": ">=16.13"
74
+ },
75
+ "peerDependencies": {
76
+ "prisma": "*"
77
+ },
78
+ "peerDependenciesMeta": {
79
+ "prisma": {
80
+ "optional": true
81
+ }
82
+ }
83
+ },
84
+ "node_modules/@prisma/debug": {
85
+ "version": "5.22.0",
86
+ "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz",
87
+ "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==",
88
+ "devOptional": true
89
+ },
90
+ "node_modules/@prisma/engines": {
91
+ "version": "5.22.0",
92
+ "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz",
93
+ "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==",
94
+ "devOptional": true,
95
+ "hasInstallScript": true,
96
+ "dependencies": {
97
+ "@prisma/debug": "5.22.0",
98
+ "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
99
+ "@prisma/fetch-engine": "5.22.0",
100
+ "@prisma/get-platform": "5.22.0"
101
+ }
102
+ },
103
+ "node_modules/@prisma/engines-version": {
104
+ "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
105
+ "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz",
106
+ "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==",
107
+ "devOptional": true
108
+ },
109
+ "node_modules/@prisma/fetch-engine": {
110
+ "version": "5.22.0",
111
+ "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz",
112
+ "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==",
113
+ "devOptional": true,
114
+ "dependencies": {
115
+ "@prisma/debug": "5.22.0",
116
+ "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
117
+ "@prisma/get-platform": "5.22.0"
118
+ }
119
+ },
120
+ "node_modules/@prisma/get-platform": {
121
+ "version": "5.22.0",
122
+ "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz",
123
+ "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==",
124
+ "devOptional": true,
125
+ "dependencies": {
126
+ "@prisma/debug": "5.22.0"
127
+ }
128
+ },
129
+ "node_modules/@tsconfig/node10": {
130
+ "version": "1.0.12",
131
+ "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
132
+ "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==",
133
+ "dev": true
134
+ },
135
+ "node_modules/@tsconfig/node12": {
136
+ "version": "1.0.11",
137
+ "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
138
+ "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
139
+ "dev": true
140
+ },
141
+ "node_modules/@tsconfig/node14": {
142
+ "version": "1.0.3",
143
+ "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
144
+ "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
145
+ "dev": true
146
+ },
147
+ "node_modules/@tsconfig/node16": {
148
+ "version": "1.0.4",
149
+ "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
150
+ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
151
+ "dev": true
152
+ },
153
+ "node_modules/@types/bcryptjs": {
154
+ "version": "2.4.6",
155
+ "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
156
+ "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==",
157
+ "dev": true
158
+ },
159
+ "node_modules/@types/body-parser": {
160
+ "version": "1.19.6",
161
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
162
+ "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
163
+ "dev": true,
164
+ "dependencies": {
165
+ "@types/connect": "*",
166
+ "@types/node": "*"
167
+ }
168
+ },
169
+ "node_modules/@types/connect": {
170
+ "version": "3.4.38",
171
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
172
+ "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
173
+ "dev": true,
174
+ "dependencies": {
175
+ "@types/node": "*"
176
+ }
177
+ },
178
+ "node_modules/@types/cors": {
179
+ "version": "2.8.19",
180
+ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
181
+ "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
182
+ "dev": true,
183
+ "dependencies": {
184
+ "@types/node": "*"
185
+ }
186
+ },
187
+ "node_modules/@types/express": {
188
+ "version": "4.17.25",
189
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz",
190
+ "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==",
191
+ "dev": true,
192
+ "dependencies": {
193
+ "@types/body-parser": "*",
194
+ "@types/express-serve-static-core": "^4.17.33",
195
+ "@types/qs": "*",
196
+ "@types/serve-static": "^1"
197
+ }
198
+ },
199
+ "node_modules/@types/express-serve-static-core": {
200
+ "version": "4.19.8",
201
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz",
202
+ "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==",
203
+ "dev": true,
204
+ "dependencies": {
205
+ "@types/node": "*",
206
+ "@types/qs": "*",
207
+ "@types/range-parser": "*",
208
+ "@types/send": "*"
209
+ }
210
+ },
211
+ "node_modules/@types/http-errors": {
212
+ "version": "2.0.5",
213
+ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
214
+ "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
215
+ "dev": true
216
+ },
217
+ "node_modules/@types/jsonwebtoken": {
218
+ "version": "9.0.10",
219
+ "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
220
+ "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==",
221
+ "dev": true,
222
+ "dependencies": {
223
+ "@types/ms": "*",
224
+ "@types/node": "*"
225
+ }
226
+ },
227
+ "node_modules/@types/mime": {
228
+ "version": "1.3.5",
229
+ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
230
+ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
231
+ "dev": true
232
+ },
233
+ "node_modules/@types/ms": {
234
+ "version": "2.1.0",
235
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
236
+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
237
+ "dev": true
238
+ },
239
+ "node_modules/@types/node": {
240
+ "version": "22.20.0",
241
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.0.tgz",
242
+ "integrity": "sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==",
243
+ "dev": true,
244
+ "dependencies": {
245
+ "undici-types": "~6.21.0"
246
+ }
247
+ },
248
+ "node_modules/@types/qs": {
249
+ "version": "6.15.1",
250
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz",
251
+ "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==",
252
+ "dev": true
253
+ },
254
+ "node_modules/@types/range-parser": {
255
+ "version": "1.2.7",
256
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
257
+ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
258
+ "dev": true
259
+ },
260
+ "node_modules/@types/send": {
261
+ "version": "1.2.1",
262
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
263
+ "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
264
+ "dev": true,
265
+ "dependencies": {
266
+ "@types/node": "*"
267
+ }
268
+ },
269
+ "node_modules/@types/serve-static": {
270
+ "version": "1.15.10",
271
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz",
272
+ "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==",
273
+ "dev": true,
274
+ "dependencies": {
275
+ "@types/http-errors": "*",
276
+ "@types/node": "*",
277
+ "@types/send": "<1"
278
+ }
279
+ },
280
+ "node_modules/@types/serve-static/node_modules/@types/send": {
281
+ "version": "0.17.6",
282
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz",
283
+ "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==",
284
+ "dev": true,
285
+ "dependencies": {
286
+ "@types/mime": "^1",
287
+ "@types/node": "*"
288
+ }
289
+ },
290
+ "node_modules/@types/strip-bom": {
291
+ "version": "3.0.0",
292
+ "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz",
293
+ "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==",
294
+ "dev": true
295
+ },
296
+ "node_modules/@types/strip-json-comments": {
297
+ "version": "0.0.30",
298
+ "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz",
299
+ "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==",
300
+ "dev": true
301
+ },
302
+ "node_modules/accepts": {
303
+ "version": "1.3.8",
304
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
305
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
306
+ "dependencies": {
307
+ "mime-types": "~2.1.34",
308
+ "negotiator": "0.6.3"
309
+ },
310
+ "engines": {
311
+ "node": ">= 0.6"
312
+ }
313
+ },
314
+ "node_modules/acorn": {
315
+ "version": "8.17.0",
316
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
317
+ "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
318
+ "dev": true,
319
+ "bin": {
320
+ "acorn": "bin/acorn"
321
+ },
322
+ "engines": {
323
+ "node": ">=0.4.0"
324
+ }
325
+ },
326
+ "node_modules/acorn-walk": {
327
+ "version": "8.3.5",
328
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz",
329
+ "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==",
330
+ "dev": true,
331
+ "dependencies": {
332
+ "acorn": "^8.11.0"
333
+ },
334
+ "engines": {
335
+ "node": ">=0.4.0"
336
+ }
337
+ },
338
+ "node_modules/anymatch": {
339
+ "version": "3.1.3",
340
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
341
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
342
+ "dev": true,
343
+ "dependencies": {
344
+ "normalize-path": "^3.0.0",
345
+ "picomatch": "^2.0.4"
346
+ },
347
+ "engines": {
348
+ "node": ">= 8"
349
+ }
350
+ },
351
+ "node_modules/arg": {
352
+ "version": "4.1.3",
353
+ "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
354
+ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
355
+ "dev": true
356
+ },
357
+ "node_modules/array-flatten": {
358
+ "version": "1.1.1",
359
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
360
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
361
+ },
362
+ "node_modules/balanced-match": {
363
+ "version": "1.0.2",
364
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
365
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
366
+ "dev": true
367
+ },
368
+ "node_modules/bcryptjs": {
369
+ "version": "2.4.3",
370
+ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
371
+ "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ=="
372
+ },
373
+ "node_modules/binary-extensions": {
374
+ "version": "2.3.0",
375
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
376
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
377
+ "dev": true,
378
+ "engines": {
379
+ "node": ">=8"
380
+ },
381
+ "funding": {
382
+ "url": "https://github.com/sponsors/sindresorhus"
383
+ }
384
+ },
385
+ "node_modules/body-parser": {
386
+ "version": "1.20.5",
387
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
388
+ "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==",
389
+ "dependencies": {
390
+ "bytes": "~3.1.2",
391
+ "content-type": "~1.0.5",
392
+ "debug": "2.6.9",
393
+ "depd": "2.0.0",
394
+ "destroy": "~1.2.0",
395
+ "http-errors": "~2.0.1",
396
+ "iconv-lite": "~0.4.24",
397
+ "on-finished": "~2.4.1",
398
+ "qs": "~6.15.1",
399
+ "raw-body": "~2.5.3",
400
+ "type-is": "~1.6.18",
401
+ "unpipe": "~1.0.0"
402
+ },
403
+ "engines": {
404
+ "node": ">= 0.8",
405
+ "npm": "1.2.8000 || >= 1.4.16"
406
+ }
407
+ },
408
+ "node_modules/brace-expansion": {
409
+ "version": "1.1.15",
410
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
411
+ "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
412
+ "dev": true,
413
+ "dependencies": {
414
+ "balanced-match": "^1.0.0",
415
+ "concat-map": "0.0.1"
416
+ }
417
+ },
418
+ "node_modules/braces": {
419
+ "version": "3.0.3",
420
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
421
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
422
+ "dev": true,
423
+ "dependencies": {
424
+ "fill-range": "^7.1.1"
425
+ },
426
+ "engines": {
427
+ "node": ">=8"
428
+ }
429
+ },
430
+ "node_modules/buffer-equal-constant-time": {
431
+ "version": "1.0.1",
432
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
433
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
434
+ },
435
+ "node_modules/buffer-from": {
436
+ "version": "1.1.2",
437
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
438
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
439
+ "dev": true
440
+ },
441
+ "node_modules/bytes": {
442
+ "version": "3.1.2",
443
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
444
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
445
+ "engines": {
446
+ "node": ">= 0.8"
447
+ }
448
+ },
449
+ "node_modules/call-bind-apply-helpers": {
450
+ "version": "1.0.2",
451
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
452
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
453
+ "dependencies": {
454
+ "es-errors": "^1.3.0",
455
+ "function-bind": "^1.1.2"
456
+ },
457
+ "engines": {
458
+ "node": ">= 0.4"
459
+ }
460
+ },
461
+ "node_modules/call-bound": {
462
+ "version": "1.0.4",
463
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
464
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
465
+ "dependencies": {
466
+ "call-bind-apply-helpers": "^1.0.2",
467
+ "get-intrinsic": "^1.3.0"
468
+ },
469
+ "engines": {
470
+ "node": ">= 0.4"
471
+ },
472
+ "funding": {
473
+ "url": "https://github.com/sponsors/ljharb"
474
+ }
475
+ },
476
+ "node_modules/chokidar": {
477
+ "version": "3.6.0",
478
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
479
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
480
+ "dev": true,
481
+ "dependencies": {
482
+ "anymatch": "~3.1.2",
483
+ "braces": "~3.0.2",
484
+ "glob-parent": "~5.1.2",
485
+ "is-binary-path": "~2.1.0",
486
+ "is-glob": "~4.0.1",
487
+ "normalize-path": "~3.0.0",
488
+ "readdirp": "~3.6.0"
489
+ },
490
+ "engines": {
491
+ "node": ">= 8.10.0"
492
+ },
493
+ "funding": {
494
+ "url": "https://paulmillr.com/funding/"
495
+ },
496
+ "optionalDependencies": {
497
+ "fsevents": "~2.3.2"
498
+ }
499
+ },
500
+ "node_modules/concat-map": {
501
+ "version": "0.0.1",
502
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
503
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
504
+ "dev": true
505
+ },
506
+ "node_modules/content-disposition": {
507
+ "version": "0.5.4",
508
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
509
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
510
+ "dependencies": {
511
+ "safe-buffer": "5.2.1"
512
+ },
513
+ "engines": {
514
+ "node": ">= 0.6"
515
+ }
516
+ },
517
+ "node_modules/content-type": {
518
+ "version": "1.0.5",
519
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
520
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
521
+ "engines": {
522
+ "node": ">= 0.6"
523
+ }
524
+ },
525
+ "node_modules/cookie": {
526
+ "version": "0.7.2",
527
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
528
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
529
+ "engines": {
530
+ "node": ">= 0.6"
531
+ }
532
+ },
533
+ "node_modules/cookie-signature": {
534
+ "version": "1.0.7",
535
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
536
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="
537
+ },
538
+ "node_modules/cors": {
539
+ "version": "2.8.6",
540
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
541
+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
542
+ "dependencies": {
543
+ "object-assign": "^4",
544
+ "vary": "^1"
545
+ },
546
+ "engines": {
547
+ "node": ">= 0.10"
548
+ },
549
+ "funding": {
550
+ "type": "opencollective",
551
+ "url": "https://opencollective.com/express"
552
+ }
553
+ },
554
+ "node_modules/create-require": {
555
+ "version": "1.1.1",
556
+ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
557
+ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
558
+ "dev": true
559
+ },
560
+ "node_modules/debug": {
561
+ "version": "2.6.9",
562
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
563
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
564
+ "dependencies": {
565
+ "ms": "2.0.0"
566
+ }
567
+ },
568
+ "node_modules/depd": {
569
+ "version": "2.0.0",
570
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
571
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
572
+ "engines": {
573
+ "node": ">= 0.8"
574
+ }
575
+ },
576
+ "node_modules/destroy": {
577
+ "version": "1.2.0",
578
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
579
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
580
+ "engines": {
581
+ "node": ">= 0.8",
582
+ "npm": "1.2.8000 || >= 1.4.16"
583
+ }
584
+ },
585
+ "node_modules/diff": {
586
+ "version": "4.0.4",
587
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz",
588
+ "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==",
589
+ "dev": true,
590
+ "engines": {
591
+ "node": ">=0.3.1"
592
+ }
593
+ },
594
+ "node_modules/dotenv": {
595
+ "version": "16.6.1",
596
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
597
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
598
+ "engines": {
599
+ "node": ">=12"
600
+ },
601
+ "funding": {
602
+ "url": "https://dotenvx.com"
603
+ }
604
+ },
605
+ "node_modules/dunder-proto": {
606
+ "version": "1.0.1",
607
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
608
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
609
+ "dependencies": {
610
+ "call-bind-apply-helpers": "^1.0.1",
611
+ "es-errors": "^1.3.0",
612
+ "gopd": "^1.2.0"
613
+ },
614
+ "engines": {
615
+ "node": ">= 0.4"
616
+ }
617
+ },
618
+ "node_modules/dynamic-dedupe": {
619
+ "version": "0.3.0",
620
+ "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz",
621
+ "integrity": "sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==",
622
+ "dev": true,
623
+ "dependencies": {
624
+ "xtend": "^4.0.0"
625
+ }
626
+ },
627
+ "node_modules/ecdsa-sig-formatter": {
628
+ "version": "1.0.11",
629
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
630
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
631
+ "dependencies": {
632
+ "safe-buffer": "^5.0.1"
633
+ }
634
+ },
635
+ "node_modules/ee-first": {
636
+ "version": "1.1.1",
637
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
638
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
639
+ },
640
+ "node_modules/encodeurl": {
641
+ "version": "2.0.0",
642
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
643
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
644
+ "engines": {
645
+ "node": ">= 0.8"
646
+ }
647
+ },
648
+ "node_modules/es-define-property": {
649
+ "version": "1.0.1",
650
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
651
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
652
+ "engines": {
653
+ "node": ">= 0.4"
654
+ }
655
+ },
656
+ "node_modules/es-errors": {
657
+ "version": "1.3.0",
658
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
659
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
660
+ "engines": {
661
+ "node": ">= 0.4"
662
+ }
663
+ },
664
+ "node_modules/es-object-atoms": {
665
+ "version": "1.1.2",
666
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
667
+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
668
+ "dependencies": {
669
+ "es-errors": "^1.3.0"
670
+ },
671
+ "engines": {
672
+ "node": ">= 0.4"
673
+ }
674
+ },
675
+ "node_modules/escape-html": {
676
+ "version": "1.0.3",
677
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
678
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
679
+ },
680
+ "node_modules/etag": {
681
+ "version": "1.8.1",
682
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
683
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
684
+ "engines": {
685
+ "node": ">= 0.6"
686
+ }
687
+ },
688
+ "node_modules/express": {
689
+ "version": "4.22.2",
690
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
691
+ "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
692
+ "dependencies": {
693
+ "accepts": "~1.3.8",
694
+ "array-flatten": "1.1.1",
695
+ "body-parser": "~1.20.5",
696
+ "content-disposition": "~0.5.4",
697
+ "content-type": "~1.0.4",
698
+ "cookie": "~0.7.1",
699
+ "cookie-signature": "~1.0.6",
700
+ "debug": "2.6.9",
701
+ "depd": "2.0.0",
702
+ "encodeurl": "~2.0.0",
703
+ "escape-html": "~1.0.3",
704
+ "etag": "~1.8.1",
705
+ "finalhandler": "~1.3.1",
706
+ "fresh": "~0.5.2",
707
+ "http-errors": "~2.0.0",
708
+ "merge-descriptors": "1.0.3",
709
+ "methods": "~1.1.2",
710
+ "on-finished": "~2.4.1",
711
+ "parseurl": "~1.3.3",
712
+ "path-to-regexp": "~0.1.12",
713
+ "proxy-addr": "~2.0.7",
714
+ "qs": "~6.15.1",
715
+ "range-parser": "~1.2.1",
716
+ "safe-buffer": "5.2.1",
717
+ "send": "~0.19.0",
718
+ "serve-static": "~1.16.2",
719
+ "setprototypeof": "1.2.0",
720
+ "statuses": "~2.0.1",
721
+ "type-is": "~1.6.18",
722
+ "utils-merge": "1.0.1",
723
+ "vary": "~1.1.2"
724
+ },
725
+ "engines": {
726
+ "node": ">= 0.10.0"
727
+ },
728
+ "funding": {
729
+ "type": "opencollective",
730
+ "url": "https://opencollective.com/express"
731
+ }
732
+ },
733
+ "node_modules/fill-range": {
734
+ "version": "7.1.1",
735
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
736
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
737
+ "dev": true,
738
+ "dependencies": {
739
+ "to-regex-range": "^5.0.1"
740
+ },
741
+ "engines": {
742
+ "node": ">=8"
743
+ }
744
+ },
745
+ "node_modules/finalhandler": {
746
+ "version": "1.3.2",
747
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
748
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
749
+ "dependencies": {
750
+ "debug": "2.6.9",
751
+ "encodeurl": "~2.0.0",
752
+ "escape-html": "~1.0.3",
753
+ "on-finished": "~2.4.1",
754
+ "parseurl": "~1.3.3",
755
+ "statuses": "~2.0.2",
756
+ "unpipe": "~1.0.0"
757
+ },
758
+ "engines": {
759
+ "node": ">= 0.8"
760
+ }
761
+ },
762
+ "node_modules/forwarded": {
763
+ "version": "0.2.0",
764
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
765
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
766
+ "engines": {
767
+ "node": ">= 0.6"
768
+ }
769
+ },
770
+ "node_modules/fresh": {
771
+ "version": "0.5.2",
772
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
773
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
774
+ "engines": {
775
+ "node": ">= 0.6"
776
+ }
777
+ },
778
+ "node_modules/fs.realpath": {
779
+ "version": "1.0.0",
780
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
781
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
782
+ "dev": true
783
+ },
784
+ "node_modules/fsevents": {
785
+ "version": "2.3.3",
786
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
787
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
788
+ "dev": true,
789
+ "hasInstallScript": true,
790
+ "optional": true,
791
+ "os": [
792
+ "darwin"
793
+ ],
794
+ "engines": {
795
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
796
+ }
797
+ },
798
+ "node_modules/function-bind": {
799
+ "version": "1.1.2",
800
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
801
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
802
+ "funding": {
803
+ "url": "https://github.com/sponsors/ljharb"
804
+ }
805
+ },
806
+ "node_modules/get-intrinsic": {
807
+ "version": "1.3.0",
808
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
809
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
810
+ "dependencies": {
811
+ "call-bind-apply-helpers": "^1.0.2",
812
+ "es-define-property": "^1.0.1",
813
+ "es-errors": "^1.3.0",
814
+ "es-object-atoms": "^1.1.1",
815
+ "function-bind": "^1.1.2",
816
+ "get-proto": "^1.0.1",
817
+ "gopd": "^1.2.0",
818
+ "has-symbols": "^1.1.0",
819
+ "hasown": "^2.0.2",
820
+ "math-intrinsics": "^1.1.0"
821
+ },
822
+ "engines": {
823
+ "node": ">= 0.4"
824
+ },
825
+ "funding": {
826
+ "url": "https://github.com/sponsors/ljharb"
827
+ }
828
+ },
829
+ "node_modules/get-proto": {
830
+ "version": "1.0.1",
831
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
832
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
833
+ "dependencies": {
834
+ "dunder-proto": "^1.0.1",
835
+ "es-object-atoms": "^1.0.0"
836
+ },
837
+ "engines": {
838
+ "node": ">= 0.4"
839
+ }
840
+ },
841
+ "node_modules/glob": {
842
+ "version": "7.2.3",
843
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
844
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
845
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
846
+ "dev": true,
847
+ "dependencies": {
848
+ "fs.realpath": "^1.0.0",
849
+ "inflight": "^1.0.4",
850
+ "inherits": "2",
851
+ "minimatch": "^3.1.1",
852
+ "once": "^1.3.0",
853
+ "path-is-absolute": "^1.0.0"
854
+ },
855
+ "engines": {
856
+ "node": "*"
857
+ },
858
+ "funding": {
859
+ "url": "https://github.com/sponsors/isaacs"
860
+ }
861
+ },
862
+ "node_modules/glob-parent": {
863
+ "version": "5.1.2",
864
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
865
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
866
+ "dev": true,
867
+ "dependencies": {
868
+ "is-glob": "^4.0.1"
869
+ },
870
+ "engines": {
871
+ "node": ">= 6"
872
+ }
873
+ },
874
+ "node_modules/gopd": {
875
+ "version": "1.2.0",
876
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
877
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
878
+ "engines": {
879
+ "node": ">= 0.4"
880
+ },
881
+ "funding": {
882
+ "url": "https://github.com/sponsors/ljharb"
883
+ }
884
+ },
885
+ "node_modules/has-symbols": {
886
+ "version": "1.1.0",
887
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
888
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
889
+ "engines": {
890
+ "node": ">= 0.4"
891
+ },
892
+ "funding": {
893
+ "url": "https://github.com/sponsors/ljharb"
894
+ }
895
+ },
896
+ "node_modules/hasown": {
897
+ "version": "2.0.4",
898
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
899
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
900
+ "dependencies": {
901
+ "function-bind": "^1.1.2"
902
+ },
903
+ "engines": {
904
+ "node": ">= 0.4"
905
+ }
906
+ },
907
+ "node_modules/http-errors": {
908
+ "version": "2.0.1",
909
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
910
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
911
+ "dependencies": {
912
+ "depd": "~2.0.0",
913
+ "inherits": "~2.0.4",
914
+ "setprototypeof": "~1.2.0",
915
+ "statuses": "~2.0.2",
916
+ "toidentifier": "~1.0.1"
917
+ },
918
+ "engines": {
919
+ "node": ">= 0.8"
920
+ },
921
+ "funding": {
922
+ "type": "opencollective",
923
+ "url": "https://opencollective.com/express"
924
+ }
925
+ },
926
+ "node_modules/iconv-lite": {
927
+ "version": "0.4.24",
928
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
929
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
930
+ "dependencies": {
931
+ "safer-buffer": ">= 2.1.2 < 3"
932
+ },
933
+ "engines": {
934
+ "node": ">=0.10.0"
935
+ }
936
+ },
937
+ "node_modules/inflight": {
938
+ "version": "1.0.6",
939
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
940
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
941
+ "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.",
942
+ "dev": true,
943
+ "dependencies": {
944
+ "once": "^1.3.0",
945
+ "wrappy": "1"
946
+ }
947
+ },
948
+ "node_modules/inherits": {
949
+ "version": "2.0.4",
950
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
951
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
952
+ },
953
+ "node_modules/ipaddr.js": {
954
+ "version": "1.9.1",
955
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
956
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
957
+ "engines": {
958
+ "node": ">= 0.10"
959
+ }
960
+ },
961
+ "node_modules/is-binary-path": {
962
+ "version": "2.1.0",
963
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
964
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
965
+ "dev": true,
966
+ "dependencies": {
967
+ "binary-extensions": "^2.0.0"
968
+ },
969
+ "engines": {
970
+ "node": ">=8"
971
+ }
972
+ },
973
+ "node_modules/is-core-module": {
974
+ "version": "2.16.2",
975
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
976
+ "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
977
+ "dev": true,
978
+ "dependencies": {
979
+ "hasown": "^2.0.3"
980
+ },
981
+ "engines": {
982
+ "node": ">= 0.4"
983
+ },
984
+ "funding": {
985
+ "url": "https://github.com/sponsors/ljharb"
986
+ }
987
+ },
988
+ "node_modules/is-extglob": {
989
+ "version": "2.1.1",
990
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
991
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
992
+ "dev": true,
993
+ "engines": {
994
+ "node": ">=0.10.0"
995
+ }
996
+ },
997
+ "node_modules/is-glob": {
998
+ "version": "4.0.3",
999
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
1000
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
1001
+ "dev": true,
1002
+ "dependencies": {
1003
+ "is-extglob": "^2.1.1"
1004
+ },
1005
+ "engines": {
1006
+ "node": ">=0.10.0"
1007
+ }
1008
+ },
1009
+ "node_modules/is-number": {
1010
+ "version": "7.0.0",
1011
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
1012
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
1013
+ "dev": true,
1014
+ "engines": {
1015
+ "node": ">=0.12.0"
1016
+ }
1017
+ },
1018
+ "node_modules/jsonwebtoken": {
1019
+ "version": "9.0.3",
1020
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
1021
+ "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
1022
+ "dependencies": {
1023
+ "jws": "^4.0.1",
1024
+ "lodash.includes": "^4.3.0",
1025
+ "lodash.isboolean": "^3.0.3",
1026
+ "lodash.isinteger": "^4.0.4",
1027
+ "lodash.isnumber": "^3.0.3",
1028
+ "lodash.isplainobject": "^4.0.6",
1029
+ "lodash.isstring": "^4.0.1",
1030
+ "lodash.once": "^4.0.0",
1031
+ "ms": "^2.1.1",
1032
+ "semver": "^7.5.4"
1033
+ },
1034
+ "engines": {
1035
+ "node": ">=12",
1036
+ "npm": ">=6"
1037
+ }
1038
+ },
1039
+ "node_modules/jsonwebtoken/node_modules/ms": {
1040
+ "version": "2.1.3",
1041
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1042
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
1043
+ },
1044
+ "node_modules/jwa": {
1045
+ "version": "2.0.1",
1046
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
1047
+ "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
1048
+ "dependencies": {
1049
+ "buffer-equal-constant-time": "^1.0.1",
1050
+ "ecdsa-sig-formatter": "1.0.11",
1051
+ "safe-buffer": "^5.0.1"
1052
+ }
1053
+ },
1054
+ "node_modules/jws": {
1055
+ "version": "4.0.1",
1056
+ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
1057
+ "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
1058
+ "dependencies": {
1059
+ "jwa": "^2.0.1",
1060
+ "safe-buffer": "^5.0.1"
1061
+ }
1062
+ },
1063
+ "node_modules/lodash.includes": {
1064
+ "version": "4.3.0",
1065
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
1066
+ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
1067
+ },
1068
+ "node_modules/lodash.isboolean": {
1069
+ "version": "3.0.3",
1070
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
1071
+ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
1072
+ },
1073
+ "node_modules/lodash.isinteger": {
1074
+ "version": "4.0.4",
1075
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
1076
+ "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
1077
+ },
1078
+ "node_modules/lodash.isnumber": {
1079
+ "version": "3.0.3",
1080
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
1081
+ "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
1082
+ },
1083
+ "node_modules/lodash.isplainobject": {
1084
+ "version": "4.0.6",
1085
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
1086
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
1087
+ },
1088
+ "node_modules/lodash.isstring": {
1089
+ "version": "4.0.1",
1090
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
1091
+ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
1092
+ },
1093
+ "node_modules/lodash.once": {
1094
+ "version": "4.1.1",
1095
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
1096
+ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
1097
+ },
1098
+ "node_modules/make-error": {
1099
+ "version": "1.3.6",
1100
+ "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
1101
+ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
1102
+ "dev": true
1103
+ },
1104
+ "node_modules/math-intrinsics": {
1105
+ "version": "1.1.0",
1106
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
1107
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
1108
+ "engines": {
1109
+ "node": ">= 0.4"
1110
+ }
1111
+ },
1112
+ "node_modules/media-typer": {
1113
+ "version": "0.3.0",
1114
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
1115
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
1116
+ "engines": {
1117
+ "node": ">= 0.6"
1118
+ }
1119
+ },
1120
+ "node_modules/merge-descriptors": {
1121
+ "version": "1.0.3",
1122
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
1123
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
1124
+ "funding": {
1125
+ "url": "https://github.com/sponsors/sindresorhus"
1126
+ }
1127
+ },
1128
+ "node_modules/methods": {
1129
+ "version": "1.1.2",
1130
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
1131
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
1132
+ "engines": {
1133
+ "node": ">= 0.6"
1134
+ }
1135
+ },
1136
+ "node_modules/mime": {
1137
+ "version": "1.6.0",
1138
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
1139
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
1140
+ "bin": {
1141
+ "mime": "cli.js"
1142
+ },
1143
+ "engines": {
1144
+ "node": ">=4"
1145
+ }
1146
+ },
1147
+ "node_modules/mime-db": {
1148
+ "version": "1.52.0",
1149
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
1150
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
1151
+ "engines": {
1152
+ "node": ">= 0.6"
1153
+ }
1154
+ },
1155
+ "node_modules/mime-types": {
1156
+ "version": "2.1.35",
1157
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
1158
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
1159
+ "dependencies": {
1160
+ "mime-db": "1.52.0"
1161
+ },
1162
+ "engines": {
1163
+ "node": ">= 0.6"
1164
+ }
1165
+ },
1166
+ "node_modules/minimatch": {
1167
+ "version": "3.1.5",
1168
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
1169
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
1170
+ "dev": true,
1171
+ "dependencies": {
1172
+ "brace-expansion": "^1.1.7"
1173
+ },
1174
+ "engines": {
1175
+ "node": "*"
1176
+ }
1177
+ },
1178
+ "node_modules/minimist": {
1179
+ "version": "1.2.8",
1180
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
1181
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
1182
+ "dev": true,
1183
+ "funding": {
1184
+ "url": "https://github.com/sponsors/ljharb"
1185
+ }
1186
+ },
1187
+ "node_modules/mkdirp": {
1188
+ "version": "1.0.4",
1189
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
1190
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
1191
+ "dev": true,
1192
+ "bin": {
1193
+ "mkdirp": "bin/cmd.js"
1194
+ },
1195
+ "engines": {
1196
+ "node": ">=10"
1197
+ }
1198
+ },
1199
+ "node_modules/ms": {
1200
+ "version": "2.0.0",
1201
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
1202
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
1203
+ },
1204
+ "node_modules/negotiator": {
1205
+ "version": "0.6.3",
1206
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
1207
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
1208
+ "engines": {
1209
+ "node": ">= 0.6"
1210
+ }
1211
+ },
1212
+ "node_modules/normalize-path": {
1213
+ "version": "3.0.0",
1214
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
1215
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
1216
+ "dev": true,
1217
+ "engines": {
1218
+ "node": ">=0.10.0"
1219
+ }
1220
+ },
1221
+ "node_modules/object-assign": {
1222
+ "version": "4.1.1",
1223
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
1224
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
1225
+ "engines": {
1226
+ "node": ">=0.10.0"
1227
+ }
1228
+ },
1229
+ "node_modules/object-inspect": {
1230
+ "version": "1.13.4",
1231
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
1232
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
1233
+ "engines": {
1234
+ "node": ">= 0.4"
1235
+ },
1236
+ "funding": {
1237
+ "url": "https://github.com/sponsors/ljharb"
1238
+ }
1239
+ },
1240
+ "node_modules/on-finished": {
1241
+ "version": "2.4.1",
1242
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
1243
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
1244
+ "dependencies": {
1245
+ "ee-first": "1.1.1"
1246
+ },
1247
+ "engines": {
1248
+ "node": ">= 0.8"
1249
+ }
1250
+ },
1251
+ "node_modules/once": {
1252
+ "version": "1.4.0",
1253
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
1254
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
1255
+ "dev": true,
1256
+ "dependencies": {
1257
+ "wrappy": "1"
1258
+ }
1259
+ },
1260
+ "node_modules/parseurl": {
1261
+ "version": "1.3.3",
1262
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
1263
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
1264
+ "engines": {
1265
+ "node": ">= 0.8"
1266
+ }
1267
+ },
1268
+ "node_modules/path-is-absolute": {
1269
+ "version": "1.0.1",
1270
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
1271
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
1272
+ "dev": true,
1273
+ "engines": {
1274
+ "node": ">=0.10.0"
1275
+ }
1276
+ },
1277
+ "node_modules/path-parse": {
1278
+ "version": "1.0.7",
1279
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
1280
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
1281
+ "dev": true
1282
+ },
1283
+ "node_modules/path-to-regexp": {
1284
+ "version": "0.1.13",
1285
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
1286
+ "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="
1287
+ },
1288
+ "node_modules/picomatch": {
1289
+ "version": "2.3.2",
1290
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
1291
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
1292
+ "dev": true,
1293
+ "engines": {
1294
+ "node": ">=8.6"
1295
+ },
1296
+ "funding": {
1297
+ "url": "https://github.com/sponsors/jonschlinkert"
1298
+ }
1299
+ },
1300
+ "node_modules/prisma": {
1301
+ "version": "5.22.0",
1302
+ "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz",
1303
+ "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==",
1304
+ "devOptional": true,
1305
+ "hasInstallScript": true,
1306
+ "dependencies": {
1307
+ "@prisma/engines": "5.22.0"
1308
+ },
1309
+ "bin": {
1310
+ "prisma": "build/index.js"
1311
+ },
1312
+ "engines": {
1313
+ "node": ">=16.13"
1314
+ },
1315
+ "optionalDependencies": {
1316
+ "fsevents": "2.3.3"
1317
+ }
1318
+ },
1319
+ "node_modules/proxy-addr": {
1320
+ "version": "2.0.7",
1321
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
1322
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
1323
+ "dependencies": {
1324
+ "forwarded": "0.2.0",
1325
+ "ipaddr.js": "1.9.1"
1326
+ },
1327
+ "engines": {
1328
+ "node": ">= 0.10"
1329
+ }
1330
+ },
1331
+ "node_modules/qs": {
1332
+ "version": "6.15.3",
1333
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
1334
+ "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==",
1335
+ "dependencies": {
1336
+ "es-define-property": "^1.0.1",
1337
+ "side-channel": "^1.1.1"
1338
+ },
1339
+ "engines": {
1340
+ "node": ">=0.6"
1341
+ },
1342
+ "funding": {
1343
+ "url": "https://github.com/sponsors/ljharb"
1344
+ }
1345
+ },
1346
+ "node_modules/range-parser": {
1347
+ "version": "1.2.1",
1348
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
1349
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
1350
+ "engines": {
1351
+ "node": ">= 0.6"
1352
+ }
1353
+ },
1354
+ "node_modules/raw-body": {
1355
+ "version": "2.5.3",
1356
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
1357
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
1358
+ "dependencies": {
1359
+ "bytes": "~3.1.2",
1360
+ "http-errors": "~2.0.1",
1361
+ "iconv-lite": "~0.4.24",
1362
+ "unpipe": "~1.0.0"
1363
+ },
1364
+ "engines": {
1365
+ "node": ">= 0.8"
1366
+ }
1367
+ },
1368
+ "node_modules/readdirp": {
1369
+ "version": "3.6.0",
1370
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
1371
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
1372
+ "dev": true,
1373
+ "dependencies": {
1374
+ "picomatch": "^2.2.1"
1375
+ },
1376
+ "engines": {
1377
+ "node": ">=8.10.0"
1378
+ }
1379
+ },
1380
+ "node_modules/resolve": {
1381
+ "version": "1.22.12",
1382
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
1383
+ "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
1384
+ "dev": true,
1385
+ "dependencies": {
1386
+ "es-errors": "^1.3.0",
1387
+ "is-core-module": "^2.16.1",
1388
+ "path-parse": "^1.0.7",
1389
+ "supports-preserve-symlinks-flag": "^1.0.0"
1390
+ },
1391
+ "bin": {
1392
+ "resolve": "bin/resolve"
1393
+ },
1394
+ "engines": {
1395
+ "node": ">= 0.4"
1396
+ },
1397
+ "funding": {
1398
+ "url": "https://github.com/sponsors/ljharb"
1399
+ }
1400
+ },
1401
+ "node_modules/rimraf": {
1402
+ "version": "2.7.1",
1403
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
1404
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
1405
+ "deprecated": "Rimraf versions prior to v4 are no longer supported",
1406
+ "dev": true,
1407
+ "dependencies": {
1408
+ "glob": "^7.1.3"
1409
+ },
1410
+ "bin": {
1411
+ "rimraf": "bin.js"
1412
+ }
1413
+ },
1414
+ "node_modules/safe-buffer": {
1415
+ "version": "5.2.1",
1416
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
1417
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
1418
+ "funding": [
1419
+ {
1420
+ "type": "github",
1421
+ "url": "https://github.com/sponsors/feross"
1422
+ },
1423
+ {
1424
+ "type": "patreon",
1425
+ "url": "https://www.patreon.com/feross"
1426
+ },
1427
+ {
1428
+ "type": "consulting",
1429
+ "url": "https://feross.org/support"
1430
+ }
1431
+ ]
1432
+ },
1433
+ "node_modules/safer-buffer": {
1434
+ "version": "2.1.2",
1435
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
1436
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
1437
+ },
1438
+ "node_modules/semver": {
1439
+ "version": "7.8.5",
1440
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
1441
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
1442
+ "bin": {
1443
+ "semver": "bin/semver.js"
1444
+ },
1445
+ "engines": {
1446
+ "node": ">=10"
1447
+ }
1448
+ },
1449
+ "node_modules/send": {
1450
+ "version": "0.19.2",
1451
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
1452
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
1453
+ "dependencies": {
1454
+ "debug": "2.6.9",
1455
+ "depd": "2.0.0",
1456
+ "destroy": "1.2.0",
1457
+ "encodeurl": "~2.0.0",
1458
+ "escape-html": "~1.0.3",
1459
+ "etag": "~1.8.1",
1460
+ "fresh": "~0.5.2",
1461
+ "http-errors": "~2.0.1",
1462
+ "mime": "1.6.0",
1463
+ "ms": "2.1.3",
1464
+ "on-finished": "~2.4.1",
1465
+ "range-parser": "~1.2.1",
1466
+ "statuses": "~2.0.2"
1467
+ },
1468
+ "engines": {
1469
+ "node": ">= 0.8.0"
1470
+ }
1471
+ },
1472
+ "node_modules/send/node_modules/ms": {
1473
+ "version": "2.1.3",
1474
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1475
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
1476
+ },
1477
+ "node_modules/serve-static": {
1478
+ "version": "1.16.3",
1479
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
1480
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
1481
+ "dependencies": {
1482
+ "encodeurl": "~2.0.0",
1483
+ "escape-html": "~1.0.3",
1484
+ "parseurl": "~1.3.3",
1485
+ "send": "~0.19.1"
1486
+ },
1487
+ "engines": {
1488
+ "node": ">= 0.8.0"
1489
+ }
1490
+ },
1491
+ "node_modules/setprototypeof": {
1492
+ "version": "1.2.0",
1493
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
1494
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
1495
+ },
1496
+ "node_modules/side-channel": {
1497
+ "version": "1.1.1",
1498
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
1499
+ "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
1500
+ "dependencies": {
1501
+ "es-errors": "^1.3.0",
1502
+ "object-inspect": "^1.13.4",
1503
+ "side-channel-list": "^1.0.1",
1504
+ "side-channel-map": "^1.0.1",
1505
+ "side-channel-weakmap": "^1.0.2"
1506
+ },
1507
+ "engines": {
1508
+ "node": ">= 0.4"
1509
+ },
1510
+ "funding": {
1511
+ "url": "https://github.com/sponsors/ljharb"
1512
+ }
1513
+ },
1514
+ "node_modules/side-channel-list": {
1515
+ "version": "1.0.1",
1516
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
1517
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
1518
+ "dependencies": {
1519
+ "es-errors": "^1.3.0",
1520
+ "object-inspect": "^1.13.4"
1521
+ },
1522
+ "engines": {
1523
+ "node": ">= 0.4"
1524
+ },
1525
+ "funding": {
1526
+ "url": "https://github.com/sponsors/ljharb"
1527
+ }
1528
+ },
1529
+ "node_modules/side-channel-map": {
1530
+ "version": "1.0.1",
1531
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
1532
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
1533
+ "dependencies": {
1534
+ "call-bound": "^1.0.2",
1535
+ "es-errors": "^1.3.0",
1536
+ "get-intrinsic": "^1.2.5",
1537
+ "object-inspect": "^1.13.3"
1538
+ },
1539
+ "engines": {
1540
+ "node": ">= 0.4"
1541
+ },
1542
+ "funding": {
1543
+ "url": "https://github.com/sponsors/ljharb"
1544
+ }
1545
+ },
1546
+ "node_modules/side-channel-weakmap": {
1547
+ "version": "1.0.2",
1548
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
1549
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
1550
+ "dependencies": {
1551
+ "call-bound": "^1.0.2",
1552
+ "es-errors": "^1.3.0",
1553
+ "get-intrinsic": "^1.2.5",
1554
+ "object-inspect": "^1.13.3",
1555
+ "side-channel-map": "^1.0.1"
1556
+ },
1557
+ "engines": {
1558
+ "node": ">= 0.4"
1559
+ },
1560
+ "funding": {
1561
+ "url": "https://github.com/sponsors/ljharb"
1562
+ }
1563
+ },
1564
+ "node_modules/source-map": {
1565
+ "version": "0.6.1",
1566
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
1567
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
1568
+ "dev": true,
1569
+ "engines": {
1570
+ "node": ">=0.10.0"
1571
+ }
1572
+ },
1573
+ "node_modules/source-map-support": {
1574
+ "version": "0.5.21",
1575
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
1576
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
1577
+ "dev": true,
1578
+ "dependencies": {
1579
+ "buffer-from": "^1.0.0",
1580
+ "source-map": "^0.6.0"
1581
+ }
1582
+ },
1583
+ "node_modules/statuses": {
1584
+ "version": "2.0.2",
1585
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
1586
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
1587
+ "engines": {
1588
+ "node": ">= 0.8"
1589
+ }
1590
+ },
1591
+ "node_modules/strip-bom": {
1592
+ "version": "3.0.0",
1593
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
1594
+ "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
1595
+ "dev": true,
1596
+ "engines": {
1597
+ "node": ">=4"
1598
+ }
1599
+ },
1600
+ "node_modules/strip-json-comments": {
1601
+ "version": "2.0.1",
1602
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
1603
+ "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
1604
+ "dev": true,
1605
+ "engines": {
1606
+ "node": ">=0.10.0"
1607
+ }
1608
+ },
1609
+ "node_modules/supports-preserve-symlinks-flag": {
1610
+ "version": "1.0.0",
1611
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
1612
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
1613
+ "dev": true,
1614
+ "engines": {
1615
+ "node": ">= 0.4"
1616
+ },
1617
+ "funding": {
1618
+ "url": "https://github.com/sponsors/ljharb"
1619
+ }
1620
+ },
1621
+ "node_modules/to-regex-range": {
1622
+ "version": "5.0.1",
1623
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
1624
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
1625
+ "dev": true,
1626
+ "dependencies": {
1627
+ "is-number": "^7.0.0"
1628
+ },
1629
+ "engines": {
1630
+ "node": ">=8.0"
1631
+ }
1632
+ },
1633
+ "node_modules/toidentifier": {
1634
+ "version": "1.0.1",
1635
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
1636
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
1637
+ "engines": {
1638
+ "node": ">=0.6"
1639
+ }
1640
+ },
1641
+ "node_modules/tree-kill": {
1642
+ "version": "1.2.2",
1643
+ "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
1644
+ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
1645
+ "dev": true,
1646
+ "bin": {
1647
+ "tree-kill": "cli.js"
1648
+ }
1649
+ },
1650
+ "node_modules/ts-node": {
1651
+ "version": "10.9.2",
1652
+ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
1653
+ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
1654
+ "dev": true,
1655
+ "dependencies": {
1656
+ "@cspotcode/source-map-support": "^0.8.0",
1657
+ "@tsconfig/node10": "^1.0.7",
1658
+ "@tsconfig/node12": "^1.0.7",
1659
+ "@tsconfig/node14": "^1.0.0",
1660
+ "@tsconfig/node16": "^1.0.2",
1661
+ "acorn": "^8.4.1",
1662
+ "acorn-walk": "^8.1.1",
1663
+ "arg": "^4.1.0",
1664
+ "create-require": "^1.1.0",
1665
+ "diff": "^4.0.1",
1666
+ "make-error": "^1.1.1",
1667
+ "v8-compile-cache-lib": "^3.0.1",
1668
+ "yn": "3.1.1"
1669
+ },
1670
+ "bin": {
1671
+ "ts-node": "dist/bin.js",
1672
+ "ts-node-cwd": "dist/bin-cwd.js",
1673
+ "ts-node-esm": "dist/bin-esm.js",
1674
+ "ts-node-script": "dist/bin-script.js",
1675
+ "ts-node-transpile-only": "dist/bin-transpile.js",
1676
+ "ts-script": "dist/bin-script-deprecated.js"
1677
+ },
1678
+ "peerDependencies": {
1679
+ "@swc/core": ">=1.2.50",
1680
+ "@swc/wasm": ">=1.2.50",
1681
+ "@types/node": "*",
1682
+ "typescript": ">=2.7"
1683
+ },
1684
+ "peerDependenciesMeta": {
1685
+ "@swc/core": {
1686
+ "optional": true
1687
+ },
1688
+ "@swc/wasm": {
1689
+ "optional": true
1690
+ }
1691
+ }
1692
+ },
1693
+ "node_modules/ts-node-dev": {
1694
+ "version": "2.0.0",
1695
+ "resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-2.0.0.tgz",
1696
+ "integrity": "sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==",
1697
+ "dev": true,
1698
+ "dependencies": {
1699
+ "chokidar": "^3.5.1",
1700
+ "dynamic-dedupe": "^0.3.0",
1701
+ "minimist": "^1.2.6",
1702
+ "mkdirp": "^1.0.4",
1703
+ "resolve": "^1.0.0",
1704
+ "rimraf": "^2.6.1",
1705
+ "source-map-support": "^0.5.12",
1706
+ "tree-kill": "^1.2.2",
1707
+ "ts-node": "^10.4.0",
1708
+ "tsconfig": "^7.0.0"
1709
+ },
1710
+ "bin": {
1711
+ "ts-node-dev": "lib/bin.js",
1712
+ "tsnd": "lib/bin.js"
1713
+ },
1714
+ "engines": {
1715
+ "node": ">=0.8.0"
1716
+ },
1717
+ "peerDependencies": {
1718
+ "node-notifier": "*",
1719
+ "typescript": "*"
1720
+ },
1721
+ "peerDependenciesMeta": {
1722
+ "node-notifier": {
1723
+ "optional": true
1724
+ }
1725
+ }
1726
+ },
1727
+ "node_modules/tsconfig": {
1728
+ "version": "7.0.0",
1729
+ "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz",
1730
+ "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==",
1731
+ "dev": true,
1732
+ "dependencies": {
1733
+ "@types/strip-bom": "^3.0.0",
1734
+ "@types/strip-json-comments": "0.0.30",
1735
+ "strip-bom": "^3.0.0",
1736
+ "strip-json-comments": "^2.0.0"
1737
+ }
1738
+ },
1739
+ "node_modules/type-is": {
1740
+ "version": "1.6.18",
1741
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
1742
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
1743
+ "dependencies": {
1744
+ "media-typer": "0.3.0",
1745
+ "mime-types": "~2.1.24"
1746
+ },
1747
+ "engines": {
1748
+ "node": ">= 0.6"
1749
+ }
1750
+ },
1751
+ "node_modules/typescript": {
1752
+ "version": "5.9.3",
1753
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
1754
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
1755
+ "dev": true,
1756
+ "bin": {
1757
+ "tsc": "bin/tsc",
1758
+ "tsserver": "bin/tsserver"
1759
+ },
1760
+ "engines": {
1761
+ "node": ">=14.17"
1762
+ }
1763
+ },
1764
+ "node_modules/undici-types": {
1765
+ "version": "6.21.0",
1766
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
1767
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
1768
+ "dev": true
1769
+ },
1770
+ "node_modules/unpipe": {
1771
+ "version": "1.0.0",
1772
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
1773
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
1774
+ "engines": {
1775
+ "node": ">= 0.8"
1776
+ }
1777
+ },
1778
+ "node_modules/utils-merge": {
1779
+ "version": "1.0.1",
1780
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
1781
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
1782
+ "engines": {
1783
+ "node": ">= 0.4.0"
1784
+ }
1785
+ },
1786
+ "node_modules/v8-compile-cache-lib": {
1787
+ "version": "3.0.1",
1788
+ "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
1789
+ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
1790
+ "dev": true
1791
+ },
1792
+ "node_modules/vary": {
1793
+ "version": "1.1.2",
1794
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
1795
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
1796
+ "engines": {
1797
+ "node": ">= 0.8"
1798
+ }
1799
+ },
1800
+ "node_modules/wrappy": {
1801
+ "version": "1.0.2",
1802
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
1803
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
1804
+ "dev": true
1805
+ },
1806
+ "node_modules/xtend": {
1807
+ "version": "4.0.2",
1808
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
1809
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
1810
+ "dev": true,
1811
+ "engines": {
1812
+ "node": ">=0.4"
1813
+ }
1814
+ },
1815
+ "node_modules/yn": {
1816
+ "version": "3.1.1",
1817
+ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
1818
+ "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
1819
+ "dev": true,
1820
+ "engines": {
1821
+ "node": ">=6"
1822
+ }
1823
+ },
1824
+ "node_modules/zod": {
1825
+ "version": "3.25.76",
1826
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
1827
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
1828
+ "funding": {
1829
+ "url": "https://github.com/sponsors/colinhacks"
1830
+ }
1831
+ }
1832
+ }
1833
+ }
package.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "tokiva-pos-backend-node",
3
+ "version": "1.0.0",
4
+ "description": "Node.js Backend for Tokiva POS using Express and TypeScript",
5
+ "main": "dist/server.js",
6
+ "scripts": {
7
+ "dev": "ts-node-dev --respawn --transpile-only src/server.ts",
8
+ "build": "tsc",
9
+ "start": "node dist/server.js",
10
+ "prisma:pull": "npx prisma db pull",
11
+ "prisma:generate": "npx prisma generate"
12
+ },
13
+ "dependencies": {
14
+ "@prisma/client": "^5.19.0",
15
+ "bcryptjs": "^2.4.3",
16
+ "cors": "^2.8.5",
17
+ "dotenv": "^16.4.5",
18
+ "express": "^4.21.0",
19
+ "jsonwebtoken": "^9.0.2",
20
+ "zod": "^3.23.8"
21
+ },
22
+ "devDependencies": {
23
+ "@types/bcryptjs": "^2.4.6",
24
+ "@types/cors": "^2.8.17",
25
+ "@types/express": "^4.17.21",
26
+ "@types/jsonwebtoken": "^9.0.7",
27
+ "@types/node": "^22.5.0",
28
+ "prisma": "^5.19.0",
29
+ "ts-node-dev": "^2.0.0",
30
+ "typescript": "^5.5.4"
31
+ }
32
+ }
prisma/schema.prisma ADDED
@@ -0,0 +1,538 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ generator client {
2
+ provider = "prisma-client-js"
3
+ }
4
+
5
+ datasource db {
6
+ provider = "mysql"
7
+ url = env("DATABASE_URL")
8
+ }
9
+
10
+ model alembic_version {
11
+ version_num String @id @db.VarChar(32)
12
+ }
13
+
14
+ model audit_log {
15
+ id Int @id @default(autoincrement())
16
+ user_id Int?
17
+ aksi String @db.VarChar(50)
18
+ entitas String @db.VarChar(50)
19
+ entitas_id Int?
20
+ data_lama Json?
21
+ data_baru Json?
22
+ ip_address String? @db.VarChar(45)
23
+ user_agent String? @db.VarChar(255)
24
+ created_at DateTime @default(now()) @db.DateTime(0)
25
+ user user? @relation(fields: [user_id], references: [id], onUpdate: NoAction, map: "audit_log_ibfk_1")
26
+
27
+ @@index([created_at], map: "idx_audit_created")
28
+ @@index([entitas, entitas_id], map: "idx_audit_entitas_id")
29
+ @@index([user_id], map: "idx_audit_user")
30
+ }
31
+
32
+ model bon {
33
+ id Int @id @default(autoincrement())
34
+ transaksi_id Int
35
+ member_id Int
36
+ total_bon Decimal @db.Decimal(15, 2)
37
+ sisa_bon Decimal @db.Decimal(15, 2)
38
+ jatuh_tempo DateTime? @db.Date
39
+ status bon_status @default(aktif)
40
+ reminder_sent Int @default(0) @db.SmallInt
41
+ created_at DateTime @default(now()) @db.DateTime(0)
42
+ updated_at DateTime @default(now()) @db.DateTime(0)
43
+ member member @relation(fields: [member_id], references: [id], onUpdate: NoAction, map: "bon_ibfk_1")
44
+ transaksi transaksi @relation(fields: [transaksi_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "bon_ibfk_2")
45
+ bon_cicilan bon_cicilan[]
46
+
47
+ @@index([jatuh_tempo], map: "idx_bon_jatuh_tempo")
48
+ @@index([member_id], map: "idx_bon_member")
49
+ @@index([status], map: "idx_bon_status")
50
+ @@index([transaksi_id], map: "idx_bon_transaksi")
51
+ }
52
+
53
+ model bon_cicilan {
54
+ id Int @id @default(autoincrement())
55
+ bon_id Int
56
+ nominal Decimal @db.Decimal(15, 2)
57
+ metode bon_cicilan_metode
58
+ user_id Int
59
+ created_at DateTime @default(now()) @db.DateTime(0)
60
+ bon bon @relation(fields: [bon_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "bon_cicilan_ibfk_1")
61
+ user user @relation(fields: [user_id], references: [id], onUpdate: NoAction, map: "bon_cicilan_ibfk_2")
62
+
63
+ @@index([bon_id], map: "idx_cicilan_bon")
64
+ @@index([user_id], map: "idx_cicilan_user")
65
+ }
66
+
67
+ model diskon {
68
+ id Int @id @default(autoincrement())
69
+ nama String @db.VarChar(200)
70
+ tipe diskon_tipe
71
+ nilai Decimal @db.Decimal(15, 2)
72
+ min_belanja Decimal @default(0.00) @db.Decimal(15, 2)
73
+ produk_ids Json?
74
+ member_only Int @default(0) @db.SmallInt
75
+ tgl_mulai DateTime @db.DateTime(0)
76
+ tgl_selesai DateTime @db.DateTime(0)
77
+ jam_mulai DateTime? @db.Time(0)
78
+ jam_selesai DateTime? @db.Time(0)
79
+ is_aktif Int @default(1) @db.SmallInt
80
+ created_at DateTime @default(now()) @db.DateTime(0)
81
+ diskon_bundling diskon_bundling[]
82
+ transaksi_detail transaksi_detail[]
83
+ voucher voucher[]
84
+
85
+ @@index([tgl_mulai, tgl_selesai], map: "idx_diskon_periode")
86
+ @@index([is_aktif], map: "idx_diskon_status")
87
+ }
88
+
89
+ model diskon_bundling {
90
+ id Int @id @default(autoincrement())
91
+ diskon_id Int
92
+ produk_id Int
93
+ qty Decimal @db.Decimal(10, 2)
94
+ diskon diskon @relation(fields: [diskon_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "diskon_bundling_ibfk_1")
95
+ produk produk @relation(fields: [produk_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "diskon_bundling_ibfk_2")
96
+
97
+ @@index([diskon_id], map: "idx_bundling_diskon")
98
+ @@index([produk_id], map: "idx_bundling_produk")
99
+ }
100
+
101
+ model harga_tingkat {
102
+ id Int @id @default(autoincrement())
103
+ produk_id Int
104
+ tingkat harga_tingkat_tingkat
105
+ min_qty Decimal @db.Decimal(10, 2)
106
+ harga Decimal @db.Decimal(15, 2)
107
+ created_at DateTime @default(now()) @db.DateTime(0)
108
+ produk produk @relation(fields: [produk_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "harga_tingkat_ibfk_1")
109
+
110
+ @@unique([produk_id, tingkat], map: "uq_produk_tingkat")
111
+ }
112
+
113
+ model kategori {
114
+ id Int @id @default(autoincrement())
115
+ nama String @unique(map: "nama") @db.VarChar(100)
116
+ icon String? @db.VarChar(50)
117
+ aktif Int @default(1) @db.SmallInt
118
+ created_at DateTime @default(now()) @db.DateTime(0)
119
+ updated_at DateTime @default(now()) @db.DateTime(0)
120
+ deleted_at DateTime? @db.DateTime(0)
121
+ produk produk[]
122
+
123
+ @@index([nama], map: "idx_kategori_nama")
124
+ }
125
+
126
+ model member {
127
+ id Int @id @default(autoincrement())
128
+ kode String @unique(map: "kode") @db.VarChar(20)
129
+ nama String @db.VarChar(200)
130
+ nomor_hp String @unique(map: "nomor_hp") @db.VarChar(20)
131
+ alamat String? @db.Text
132
+ total_poin Int @default(0)
133
+ limit_bon Decimal @default(0.00) @db.Decimal(15, 2)
134
+ total_bon Decimal @default(0.00) @db.Decimal(15, 2)
135
+ foto_url String? @db.VarChar(500)
136
+ aktif Int @default(1) @db.SmallInt
137
+ created_at DateTime @default(now()) @db.DateTime(0)
138
+ updated_at DateTime @default(now()) @db.DateTime(0)
139
+ deleted_at DateTime? @db.DateTime(0)
140
+ bon bon[]
141
+ transaksi transaksi[]
142
+
143
+ @@index([kode], map: "idx_pelanggan_kode")
144
+ @@index([nomor_hp], map: "idx_pelanggan_nomor_hp")
145
+ }
146
+
147
+ model notifikasi {
148
+ id Int @id @default(autoincrement())
149
+ user_id Int?
150
+ tipe notifikasi_tipe
151
+ judul String @db.VarChar(255)
152
+ pesan String @db.Text
153
+ referensi_id Int?
154
+ is_dibaca Int @default(0) @db.SmallInt
155
+ was_sent Int @default(0) @db.SmallInt
156
+ created_at DateTime @default(now()) @db.DateTime(0)
157
+ user user? @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "notifikasi_ibfk_1")
158
+
159
+ @@index([created_at], map: "idx_notif_created")
160
+ @@index([user_id, is_dibaca], map: "idx_notif_user_dibaca")
161
+ }
162
+
163
+ model pembayaran {
164
+ id Int @id @default(autoincrement())
165
+ transaksi_id Int
166
+ metode pembayaran_metode
167
+ nominal Decimal @db.Decimal(15, 2)
168
+ referensi String? @db.VarChar(100)
169
+ created_at DateTime @default(now()) @db.DateTime(0)
170
+ transaksi transaksi @relation(fields: [transaksi_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "pembayaran_ibfk_1")
171
+
172
+ @@index([metode], map: "idx_pembayaran_metode")
173
+ @@index([transaksi_id], map: "idx_pembayaran_transaksi")
174
+ }
175
+
176
+ model pengaturan {
177
+ id Int @id @default(autoincrement())
178
+ kunci String @unique(map: "kunci") @db.VarChar(100)
179
+ nilai String @db.Text
180
+ deskripsi String? @db.VarChar(255)
181
+ updated_at DateTime @default(now()) @db.DateTime(0)
182
+
183
+ @@index([kunci], map: "idx_pengaturan_kunci")
184
+ }
185
+
186
+ model produk {
187
+ id Int @id @default(autoincrement())
188
+ kode String @unique(map: "kode") @db.VarChar(50)
189
+ barcode String? @unique(map: "barcode") @db.VarChar(50)
190
+ nama String @db.VarChar(200)
191
+ kategori_id Int
192
+ supplier_id Int?
193
+ satuan String @default("pcs") @db.VarChar(20)
194
+ harga_beli Decimal @db.Decimal(15, 2)
195
+ harga_jual Decimal @db.Decimal(15, 2)
196
+ stok Decimal @default(0.00) @db.Decimal(12, 2)
197
+ stok_min Decimal @default(5.00) @db.Decimal(12, 2)
198
+ expired_date DateTime? @db.Date
199
+ foto_url String? @db.LongText
200
+ qr_url String? @db.VarChar(500)
201
+ deskripsi String? @db.Text
202
+ is_ecer Int @default(1) @db.SmallInt
203
+ is_aktif Int @default(1) @db.SmallInt
204
+ created_at DateTime @default(now()) @db.DateTime(0)
205
+ updated_at DateTime @default(now()) @db.DateTime(0)
206
+ deleted_at DateTime? @db.DateTime(0)
207
+ diskon_bundling diskon_bundling[]
208
+ harga_tingkat harga_tingkat[]
209
+ kategori kategori @relation(fields: [kategori_id], references: [id], onUpdate: NoAction, map: "produk_ibfk_1")
210
+ supplier supplier? @relation(fields: [supplier_id], references: [id], onUpdate: NoAction, map: "produk_ibfk_2")
211
+ stok_batch stok_batch[]
212
+ stok_log stok_log[]
213
+ stok_opname stok_opname[]
214
+ transaksi_detail transaksi_detail[]
215
+ transaksi_retur transaksi_retur[]
216
+
217
+ @@index([barcode], map: "idx_produk_barcode")
218
+ @@index([kategori_id], map: "idx_produk_kategori")
219
+ @@index([kode], map: "idx_produk_kode")
220
+ @@index([nama], map: "idx_produk_nama")
221
+ @@index([supplier_id], map: "idx_produk_supplier")
222
+ }
223
+
224
+ model shift {
225
+ id Int @id @default(autoincrement())
226
+ user_id Int
227
+ kode String @db.VarChar(30)
228
+ modal_awal Decimal @default(0.00) @db.Decimal(15, 2)
229
+ waktu_buka DateTime @db.DateTime(0)
230
+ waktu_tutup DateTime? @db.DateTime(0)
231
+ total_transaksi Int @default(0)
232
+ total_tunai Decimal @default(0.00) @db.Decimal(15, 2)
233
+ total_qris Decimal @default(0.00) @db.Decimal(15, 2)
234
+ total_transfer Decimal @default(0.00) @db.Decimal(15, 2)
235
+ total_bon Decimal @default(0.00) @db.Decimal(15, 2)
236
+ total_diskon Decimal @default(0.00) @db.Decimal(15, 2)
237
+ total_selisih Decimal @default(0.00) @db.Decimal(15, 2)
238
+ status shift_status @default(buka)
239
+ catatan String? @db.Text
240
+ user user @relation(fields: [user_id], references: [id], onUpdate: NoAction, map: "shift_ibfk_1")
241
+ transaksi transaksi[]
242
+
243
+ @@index([kode], map: "idx_shift_kode")
244
+ @@index([user_id], map: "idx_shift_user")
245
+ }
246
+
247
+ model stok_batch {
248
+ id Int @id @default(autoincrement())
249
+ produk_id Int
250
+ batch_no String? @db.VarChar(50)
251
+ qty_masuk Decimal @db.Decimal(12, 2)
252
+ qty_sisa Decimal @db.Decimal(12, 2)
253
+ harga_beli Decimal @db.Decimal(15, 2)
254
+ expired_date DateTime? @db.Date
255
+ supplier_id Int?
256
+ catatan String? @db.Text
257
+ created_at DateTime @default(now()) @db.DateTime(0)
258
+ produk produk @relation(fields: [produk_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "stok_batch_ibfk_1")
259
+ supplier supplier? @relation(fields: [supplier_id], references: [id], onUpdate: NoAction, map: "stok_batch_ibfk_2")
260
+ stok_log stok_log[]
261
+ transaksi_detail transaksi_detail[]
262
+
263
+ @@index([expired_date], map: "idx_batch_expired")
264
+ @@index([produk_id], map: "idx_batch_produk")
265
+ @@index([supplier_id], map: "idx_batch_supplier")
266
+ }
267
+
268
+ model stok_log {
269
+ id Int @id @default(autoincrement())
270
+ produk_id Int
271
+ batch_id Int?
272
+ tipe stok_log_tipe
273
+ qty Decimal @db.Decimal(12, 2)
274
+ stok_sebelum Decimal @db.Decimal(12, 2)
275
+ stok_sesudah Decimal @db.Decimal(12, 2)
276
+ referensi String? @db.VarChar(50)
277
+ user_id Int?
278
+ catatan String? @db.Text
279
+ created_at DateTime @default(now()) @db.DateTime(0)
280
+ stok_batch stok_batch? @relation(fields: [batch_id], references: [id], onUpdate: NoAction, map: "stok_log_ibfk_1")
281
+ produk produk @relation(fields: [produk_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "stok_log_ibfk_2")
282
+ user user? @relation(fields: [user_id], references: [id], onUpdate: NoAction, map: "stok_log_ibfk_3")
283
+
284
+ @@index([batch_id], map: "batch_id")
285
+ @@index([created_at], map: "idx_stoklog_date")
286
+ @@index([produk_id], map: "idx_stoklog_produk")
287
+ @@index([tipe], map: "idx_stoklog_tipe")
288
+ @@index([user_id], map: "user_id")
289
+ }
290
+
291
+ model stok_opname {
292
+ id Int @id @default(autoincrement())
293
+ produk_id Int
294
+ user_id Int
295
+ stok_sistem Decimal @db.Decimal(12, 2)
296
+ stok_fisik Decimal @db.Decimal(12, 2)
297
+ selisih Decimal @db.Decimal(12, 2)
298
+ alasan String? @db.VarChar(255)
299
+ status stok_opname_status @default(draft)
300
+ created_at DateTime @default(now()) @db.DateTime(0)
301
+ produk produk @relation(fields: [produk_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "stok_opname_ibfk_1")
302
+ user user @relation(fields: [user_id], references: [id], onUpdate: NoAction, map: "stok_opname_ibfk_2")
303
+
304
+ @@index([produk_id], map: "idx_opname_produk")
305
+ @@index([status], map: "idx_opname_status")
306
+ @@index([user_id], map: "idx_opname_user")
307
+ }
308
+
309
+ model supplier {
310
+ id Int @id @default(autoincrement())
311
+ nama String @db.VarChar(200)
312
+ kontak String? @db.VarChar(100)
313
+ nomor_hp String? @db.VarChar(20)
314
+ alamat String? @db.Text
315
+ hutang Decimal @default(0.00) @db.Decimal(15, 2)
316
+ aktif Int @default(1) @db.SmallInt
317
+ created_at DateTime @default(now()) @db.DateTime(0)
318
+ updated_at DateTime @default(now()) @db.DateTime(0)
319
+ deleted_at DateTime? @db.DateTime(0)
320
+ produk produk[]
321
+ stok_batch stok_batch[]
322
+
323
+ @@index([nama], map: "idx_pemasok_nama")
324
+ }
325
+
326
+ model transaksi {
327
+ id Int @id @default(autoincrement())
328
+ no_transaksi String @unique(map: "no_transaksi") @db.VarChar(30)
329
+ shift_id Int
330
+ user_id Int
331
+ member_id Int?
332
+ tanggal DateTime @db.Date
333
+ waktu DateTime @db.Time(0)
334
+ subtotal Decimal @db.Decimal(15, 2)
335
+ diskon_total Decimal @default(0.00) @db.Decimal(15, 2)
336
+ diskon_member Decimal @default(0.00) @db.Decimal(15, 2)
337
+ pajak Decimal @default(0.00) @db.Decimal(15, 2)
338
+ total Decimal @db.Decimal(15, 2)
339
+ bayar Decimal @default(0.00) @db.Decimal(15, 2)
340
+ kembalian Decimal @default(0.00) @db.Decimal(15, 2)
341
+ status transaksi_status @default(lunas)
342
+ is_synced Int @default(0) @db.SmallInt
343
+ sync_at DateTime? @db.DateTime(0)
344
+ local_id String? @db.VarChar(50)
345
+ catatan String? @db.Text
346
+ created_at DateTime @default(now()) @db.DateTime(0)
347
+ deleted_at DateTime? @db.DateTime(0)
348
+ bon bon[]
349
+ pembayaran pembayaran[]
350
+ member member? @relation(fields: [member_id], references: [id], onUpdate: NoAction, map: "transaksi_ibfk_1")
351
+ shift shift @relation(fields: [shift_id], references: [id], onUpdate: NoAction, map: "transaksi_ibfk_2")
352
+ user user @relation(fields: [user_id], references: [id], onUpdate: NoAction, map: "transaksi_ibfk_3")
353
+ transaksi_detail transaksi_detail[]
354
+ transaksi_retur transaksi_retur[]
355
+
356
+ @@index([member_id], map: "idx_transaksi_member")
357
+ @@index([no_transaksi], map: "idx_transaksi_no")
358
+ @@index([shift_id], map: "idx_transaksi_shift")
359
+ @@index([tanggal], map: "idx_transaksi_tanggal")
360
+ @@index([user_id], map: "idx_transaksi_user")
361
+ }
362
+
363
+ model transaksi_detail {
364
+ id Int @id @default(autoincrement())
365
+ transaksi_id Int
366
+ produk_id Int
367
+ nama_produk String @db.VarChar(200)
368
+ qty Decimal @db.Decimal(10, 2)
369
+ satuan String @db.VarChar(20)
370
+ harga_satuan Decimal @db.Decimal(15, 2)
371
+ diskon_item Decimal @default(0.00) @db.Decimal(15, 2)
372
+ subtotal Decimal @db.Decimal(15, 2)
373
+ diskon_id Int?
374
+ batch_id Int?
375
+ created_at DateTime @default(now()) @db.DateTime(0)
376
+ stok_batch stok_batch? @relation(fields: [batch_id], references: [id], onUpdate: NoAction, map: "transaksi_detail_ibfk_1")
377
+ diskon diskon? @relation(fields: [diskon_id], references: [id], onUpdate: NoAction, map: "transaksi_detail_ibfk_2")
378
+ produk produk @relation(fields: [produk_id], references: [id], onUpdate: NoAction, map: "transaksi_detail_ibfk_3")
379
+ transaksi transaksi @relation(fields: [transaksi_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "transaksi_detail_ibfk_4")
380
+
381
+ @@index([batch_id], map: "batch_id")
382
+ @@index([diskon_id], map: "diskon_id")
383
+ @@index([produk_id], map: "idx_detail_produk")
384
+ @@index([transaksi_id], map: "idx_detail_transaksi")
385
+ }
386
+
387
+ model transaksi_retur {
388
+ id Int @id @default(autoincrement())
389
+ transaksi_id Int
390
+ no_retur String @unique(map: "no_retur") @db.VarChar(30)
391
+ user_id Int
392
+ produk_id Int
393
+ qty Decimal @db.Decimal(10, 2)
394
+ alasan String @db.VarChar(255)
395
+ tipe transaksi_retur_tipe
396
+ nominal_kembali Decimal @default(0.00) @db.Decimal(15, 2)
397
+ stok_kembali Int @default(1) @db.SmallInt
398
+ created_at DateTime @default(now()) @db.DateTime(0)
399
+ produk produk @relation(fields: [produk_id], references: [id], onUpdate: NoAction, map: "transaksi_retur_ibfk_1")
400
+ transaksi transaksi @relation(fields: [transaksi_id], references: [id], onUpdate: NoAction, map: "transaksi_retur_ibfk_2")
401
+ user user @relation(fields: [user_id], references: [id], onUpdate: NoAction, map: "transaksi_retur_ibfk_3")
402
+
403
+ @@index([no_retur], map: "idx_retur_no")
404
+ @@index([produk_id], map: "idx_retur_produk")
405
+ @@index([transaksi_id], map: "idx_retur_transaksi")
406
+ @@index([user_id], map: "user_id")
407
+ }
408
+
409
+ model trash {
410
+ id Int @id @default(autoincrement())
411
+ entity_type String @db.VarChar(50)
412
+ entity_id Int
413
+ data String @db.Text
414
+ deleted_by Int?
415
+ deleted_at DateTime @default(now()) @db.DateTime(0)
416
+
417
+ @@index([deleted_at], map: "idx_trash_date")
418
+ @@index([entity_type, entity_id], map: "idx_trash_entity")
419
+ }
420
+
421
+ model user {
422
+ id Int @id @default(autoincrement())
423
+ nama String @db.VarChar(100)
424
+ username String @unique(map: "username") @db.VarChar(50)
425
+ password_hash String @db.VarChar(255)
426
+ role user_role
427
+ nomor_hp String? @db.VarChar(20)
428
+ foto_url String? @db.VarChar(500)
429
+ aktif Int @default(1) @db.SmallInt
430
+ created_at DateTime @default(now()) @db.DateTime(0)
431
+ updated_at DateTime @default(now()) @db.DateTime(0)
432
+ audit_log audit_log[]
433
+ bon_cicilan bon_cicilan[]
434
+ notifikasi notifikasi[]
435
+ shift shift[]
436
+ stok_log stok_log[]
437
+ stok_opname stok_opname[]
438
+ transaksi transaksi[]
439
+ transaksi_retur transaksi_retur[]
440
+
441
+ @@index([username], map: "idx_pengguna_username")
442
+ }
443
+
444
+ model voucher {
445
+ id Int @id @default(autoincrement())
446
+ kode String @unique(map: "kode") @db.VarChar(50)
447
+ diskon_id Int
448
+ kuota Int @default(1)
449
+ terpakai Int @default(0)
450
+ tgl_kadaluarsa DateTime @db.Date
451
+ is_aktif Int @default(1) @db.SmallInt
452
+ created_at DateTime @default(now()) @db.DateTime(0)
453
+ diskon diskon @relation(fields: [diskon_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "voucher_ibfk_1")
454
+
455
+ @@index([diskon_id], map: "idx_voucher_diskon")
456
+ @@index([kode], map: "idx_voucher_kode")
457
+ }
458
+
459
+ enum diskon_tipe {
460
+ per_item_persen
461
+ per_item_nominal
462
+ total_persen
463
+ total_nominal
464
+ bundling
465
+ buy_x_get_y
466
+ member
467
+ flash_sale
468
+ }
469
+
470
+ enum harga_tingkat_tingkat {
471
+ ecer
472
+ grosir
473
+ partai
474
+ }
475
+
476
+ enum notifikasi_tipe {
477
+ stok_rendah
478
+ stok_habis
479
+ expired
480
+ bon_jatuh_tempo
481
+ shift_tutup
482
+ transaksi_void
483
+ info
484
+ }
485
+
486
+ enum pembayaran_metode {
487
+ tunai
488
+ qris
489
+ transfer
490
+ bon
491
+ voucher
492
+ }
493
+
494
+ enum bon_cicilan_metode {
495
+ tunai
496
+ transfer
497
+ }
498
+
499
+ enum stok_log_tipe {
500
+ masuk
501
+ keluar
502
+ opname
503
+ retur
504
+ expired
505
+ adjustment
506
+ }
507
+
508
+ enum user_role {
509
+ admin
510
+ kasir
511
+ }
512
+
513
+ enum bon_status {
514
+ aktif
515
+ lunas
516
+ macet
517
+ }
518
+
519
+ enum stok_opname_status {
520
+ draft
521
+ approved
522
+ }
523
+
524
+ enum transaksi_retur_tipe {
525
+ refund
526
+ exchange
527
+ }
528
+
529
+ enum shift_status {
530
+ buka
531
+ tutup
532
+ }
533
+
534
+ enum transaksi_status {
535
+ lunas
536
+ bon
537
+ void
538
+ }
src/app.ts ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express, { Application, Request, Response } from 'express';
2
+ import cors from 'cors';
3
+ import router from './routes';
4
+ import { errorHandler } from './middlewares/error.middleware';
5
+
6
+ const app: Application = express();
7
+
8
+ // Global Middleware
9
+ app.use(cors());
10
+ app.use(express.json({ limit: '50mb' })); // Allow larger payloads (useful for base64 images)
11
+ app.use(express.urlencoded({ extended: true, limit: '50mb' }));
12
+
13
+ // Routing API
14
+ app.use('/api', router);
15
+
16
+ // Default Route
17
+ app.get('/', (req: Request, res: Response) => {
18
+ res.json({ message: 'Welcome to Tokiva POS Node.js API Server' });
19
+ });
20
+
21
+ // Global Error Handler
22
+ app.use(errorHandler);
23
+
24
+ export default app;
src/controllers/auth.controller.ts ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { AuthService } from '../services/auth.service';
3
+ import { sukses } from '../utils/response';
4
+ import { AuthenticatedRequest } from '../middlewares/auth.middleware';
5
+
6
+ export class AuthController {
7
+ static async login(req: Request, res: Response, next: NextFunction) {
8
+ try {
9
+ const { username, password } = req.body;
10
+ const ip = (req.ip || req.socket.remoteAddress || 'unknown').replace('::ffff:', '');
11
+ const userAgent = (req.headers['user-agent'] || 'unknown') as string;
12
+
13
+ const result = await AuthService.login({
14
+ username,
15
+ passwordSecret: password,
16
+ ip,
17
+ userAgent,
18
+ });
19
+
20
+ return sukses(
21
+ res,
22
+ {
23
+ access_token: result.accessToken,
24
+ refresh_token: result.refreshToken,
25
+ token_type: 'bearer',
26
+ role: result.user.role,
27
+ nama: result.user.nama,
28
+ },
29
+ 'Login berhasil'
30
+ );
31
+ } catch (error) {
32
+ next(error);
33
+ }
34
+ }
35
+
36
+ static async refresh(req: Request, res: Response, next: NextFunction) {
37
+ try {
38
+ const { refresh_token } = req.body;
39
+ const result = await AuthService.refresh(refresh_token);
40
+
41
+ return sukses(
42
+ res,
43
+ {
44
+ access_token: result.accessToken,
45
+ refresh_token: result.refreshToken,
46
+ token_type: 'bearer',
47
+ role: result.role,
48
+ },
49
+ 'Token berhasil diperbarui'
50
+ );
51
+ } catch (error) {
52
+ next(error);
53
+ }
54
+ }
55
+
56
+ static async logout(req: AuthenticatedRequest, res: Response, next: NextFunction) {
57
+ try {
58
+ const ip = (req.ip || req.socket.remoteAddress || 'unknown').replace('::ffff:', '');
59
+ const userAgent = (req.headers['user-agent'] || 'unknown') as string;
60
+ const userId = req.user.id;
61
+
62
+ await AuthService.logout(userId, ip, userAgent);
63
+
64
+ return sukses(res, null, 'Logout berhasil');
65
+ } catch (error) {
66
+ next(error);
67
+ }
68
+ }
69
+ }
src/controllers/bon.controller.ts ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { BonService } from '../services/bon.service';
3
+ import { sukses } from '../utils/response';
4
+ import { AuthenticatedRequest } from '../middlewares/auth.middleware';
5
+
6
+ export class BonController {
7
+ static async list(req: Request, res: Response, next: NextFunction) {
8
+ try {
9
+ const skip = parseInt(req.query.skip as string) || 0;
10
+ const limit = parseInt(req.query.limit as string) || 10;
11
+ const statusFilter = (req.query.status_filter as any) || undefined;
12
+ const memberId = req.query.member_id ? parseInt(req.query.member_id as string) : undefined;
13
+
14
+ const bonList = await BonService.getBon(skip, limit, statusFilter, memberId);
15
+ return sukses(res, bonList, 'Daftar kasbon berhasil dimuat');
16
+ } catch (error) {
17
+ next(error);
18
+ }
19
+ }
20
+
21
+ static async get(req: Request, res: Response, next: NextFunction) {
22
+ try {
23
+ const bonId = parseInt(req.params.id);
24
+ const bon = await BonService.getBonById(bonId);
25
+ return sukses(res, bon, 'Detail kasbon berhasil dimuat');
26
+ } catch (error) {
27
+ next(error);
28
+ }
29
+ }
30
+
31
+ static async payInstallment(req: AuthenticatedRequest, res: Response, next: NextFunction) {
32
+ try {
33
+ const bonId = parseInt(req.params.id);
34
+ const actorId = req.user.id;
35
+ const ip = (req.ip || req.socket.remoteAddress || 'unknown').replace('::ffff:', '');
36
+ const userAgent = (req.headers['user-agent'] || 'unknown') as string;
37
+
38
+ const { nominal, metode } = req.body;
39
+ const bon = await BonService.bayarCicilan({
40
+ bonId,
41
+ nominal,
42
+ metode,
43
+ actorId,
44
+ ip,
45
+ userAgent,
46
+ });
47
+
48
+ return sukses(res, bon, 'Cicilan kasbon berhasil dibayarkan');
49
+ } catch (error) {
50
+ next(error);
51
+ }
52
+ }
53
+
54
+ static async sendDebtReminder(req: AuthenticatedRequest, res: Response, next: NextFunction) {
55
+ try {
56
+ const bonId = parseInt(req.params.id);
57
+ const waLink = await BonService.kirimReminderWA(bonId);
58
+ return sukses(res, { wa_link: waLink }, 'WhatsApp reminder link generated successfully');
59
+ } catch (error) {
60
+ next(error);
61
+ }
62
+ }
63
+ }
src/controllers/kategori.controller.ts ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { KategoriService } from '../services/kategori.service';
3
+ import { sukses } from '../utils/response';
4
+ import { AuthenticatedRequest } from '../middlewares/auth.middleware';
5
+
6
+ export class KategoriController {
7
+ static async list(req: Request, res: Response, next: NextFunction) {
8
+ try {
9
+ const skip = parseInt(req.query.skip as string) || 0;
10
+ const limit = parseInt(req.query.limit as string) || 100;
11
+ const pencarian = req.query.pencarian as string || undefined;
12
+
13
+ const categories = await KategoriService.getKategori(skip, limit, pencarian);
14
+ return sukses(res, categories, 'Daftar kategori berhasil dimuat');
15
+ } catch (error) {
16
+ next(error);
17
+ }
18
+ }
19
+
20
+ static async create(req: AuthenticatedRequest, res: Response, next: NextFunction) {
21
+ try {
22
+ const actorId = req.user.id;
23
+ const ip = (req.ip || req.socket.remoteAddress || 'unknown').replace('::ffff:', '');
24
+ const userAgent = (req.headers['user-agent'] || 'unknown') as string;
25
+
26
+ const category = await KategoriService.create(req.body, actorId, ip, userAgent);
27
+ return sukses(res, category, 'Kategori berhasil dibuat');
28
+ } catch (error) {
29
+ next(error);
30
+ }
31
+ }
32
+
33
+ static async update(req: AuthenticatedRequest, res: Response, next: NextFunction) {
34
+ try {
35
+ const kategoriId = parseInt(req.params.id);
36
+ const actorId = req.user.id;
37
+ const ip = (req.ip || req.socket.remoteAddress || 'unknown').replace('::ffff:', '');
38
+ const userAgent = (req.headers['user-agent'] || 'unknown') as string;
39
+
40
+ const category = await KategoriService.update(kategoriId, req.body, actorId, ip, userAgent);
41
+ return sukses(res, category, 'Kategori berhasil diperbarui');
42
+ } catch (error) {
43
+ next(error);
44
+ }
45
+ }
46
+
47
+ static async delete(req: AuthenticatedRequest, res: Response, next: NextFunction) {
48
+ try {
49
+ const kategoriId = parseInt(req.params.id);
50
+ const actorId = req.user.id;
51
+ const ip = (req.ip || req.socket.remoteAddress || 'unknown').replace('::ffff:', '');
52
+ const userAgent = (req.headers['user-agent'] || 'unknown') as string;
53
+
54
+ await KategoriService.delete(kategoriId, actorId, ip, userAgent);
55
+ return sukses(res, null, 'Kategori berhasil dinonaktifkan');
56
+ } catch (error) {
57
+ next(error);
58
+ }
59
+ }
60
+ }
src/controllers/member.controller.ts ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { MemberService } from '../services/member.service';
3
+ import { sukses } from '../utils/response';
4
+ import { AuthenticatedRequest } from '../middlewares/auth.middleware';
5
+
6
+ export class MemberController {
7
+ static async list(req: Request, res: Response, next: NextFunction) {
8
+ try {
9
+ const skip = parseInt(req.query.skip as string) || 0;
10
+ const limit = parseInt(req.query.limit as string) || 10;
11
+ const pencarian = req.query.pencarian as string || undefined;
12
+
13
+ const members = await MemberService.getMembers(skip, limit, pencarian);
14
+ return sukses(res, members, 'Daftar member berhasil dimuat');
15
+ } catch (error) {
16
+ next(error);
17
+ }
18
+ }
19
+
20
+ static async create(req: AuthenticatedRequest, res: Response, next: NextFunction) {
21
+ try {
22
+ const actorId = req.user.id;
23
+ const ip = (req.ip || req.socket.remoteAddress || 'unknown').replace('::ffff:', '');
24
+ const userAgent = (req.headers['user-agent'] || 'unknown') as string;
25
+
26
+ const member = await MemberService.create(req.body, actorId, ip, userAgent);
27
+ return sukses(res, member, 'Member berhasil didaftarkan');
28
+ } catch (error) {
29
+ next(error);
30
+ }
31
+ }
32
+
33
+ static async update(req: AuthenticatedRequest, res: Response, next: NextFunction) {
34
+ try {
35
+ const memberId = parseInt(req.params.id);
36
+ const actorId = req.user.id;
37
+ const ip = (req.ip || req.socket.remoteAddress || 'unknown').replace('::ffff:', '');
38
+ const userAgent = (req.headers['user-agent'] || 'unknown') as string;
39
+
40
+ const member = await MemberService.update(memberId, req.body, actorId, ip, userAgent);
41
+ return sukses(res, member, 'Member berhasil diperbarui');
42
+ } catch (error) {
43
+ next(error);
44
+ }
45
+ }
46
+
47
+ static async delete(req: AuthenticatedRequest, res: Response, next: NextFunction) {
48
+ try {
49
+ const memberId = parseInt(req.params.id);
50
+ const actorId = req.user.id;
51
+ const ip = (req.ip || req.socket.remoteAddress || 'unknown').replace('::ffff:', '');
52
+ const userAgent = (req.headers['user-agent'] || 'unknown') as string;
53
+
54
+ await MemberService.delete(memberId, actorId, ip, userAgent);
55
+ return sukses(res, null, 'Member berhasil dinonaktifkan');
56
+ } catch (error) {
57
+ next(error);
58
+ }
59
+ }
60
+ }
src/controllers/ops.controller.ts ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { OpsService } from '../services/ops.service';
3
+ import { sukses } from '../utils/response';
4
+ import { AuthenticatedRequest } from '../middlewares/auth.middleware';
5
+
6
+ export class OpsController {
7
+ // Shift Kasir
8
+ static async getActiveShift(req: AuthenticatedRequest, res: Response, next: NextFunction) {
9
+ try {
10
+ const userId = req.user.id;
11
+ const shift = await OpsService.getActiveShift(userId);
12
+ if (!shift) {
13
+ return sukses(res, null, 'Tidak ada shift aktif untuk kasir saat ini');
14
+ }
15
+ return sukses(res, shift, 'Shift aktif berhasil dimuat');
16
+ } catch (error) {
17
+ next(error);
18
+ }
19
+ }
20
+
21
+ static async openShift(req: AuthenticatedRequest, res: Response, next: NextFunction) {
22
+ try {
23
+ const actorId = req.user.id;
24
+ const ip = (req.ip || req.socket.remoteAddress || 'unknown').replace('::ffff:', '');
25
+ const userAgent = (req.headers['user-agent'] || 'unknown') as string;
26
+ const { modal_awal, catatan } = req.body;
27
+
28
+ const shift = await OpsService.bukaShift(modal_awal, catatan, actorId, ip, userAgent);
29
+ return sukses(res, shift, 'Shift berhasil dibuka');
30
+ } catch (error) {
31
+ next(error);
32
+ }
33
+ }
34
+
35
+ static async closeShift(req: AuthenticatedRequest, res: Response, next: NextFunction) {
36
+ try {
37
+ const actorId = req.user.id;
38
+ const ip = (req.ip || req.socket.remoteAddress || 'unknown').replace('::ffff:', '');
39
+ const userAgent = (req.headers['user-agent'] || 'unknown') as string;
40
+ const { modal_akhir_fisik, catatan } = req.body;
41
+
42
+ const shift = await OpsService.tutupShift(modal_akhir_fisik, catatan, actorId, ip, userAgent);
43
+ return sukses(res, shift, 'Shift kasir ditutup, selisih kas fisik berhasil dicatat');
44
+ } catch (error) {
45
+ next(error);
46
+ }
47
+ }
48
+
49
+ // Dashboard & Laporan
50
+ static async getDashboardSummary(req: AuthenticatedRequest, res: Response, next: NextFunction) {
51
+ try {
52
+ const summary = await OpsService.getDashboardSummary();
53
+ return sukses(res, summary, 'Dashboard summary berhasil dimuat');
54
+ } catch (error) {
55
+ next(error);
56
+ }
57
+ }
58
+
59
+ static async getSalesReport(req: AuthenticatedRequest, res: Response, next: NextFunction) {
60
+ try {
61
+ const tglMulai = new Date(req.query.tgl_mulai as string);
62
+ const tglSelesai = new Date(req.query.tgl_selesai as string);
63
+
64
+ const report = await OpsService.buatLaporanPenjualan(tglMulai, tglSelesai);
65
+ return sukses(res, report, 'Laporan penjualan berhasil dibuat');
66
+ } catch (error) {
67
+ next(error);
68
+ }
69
+ }
70
+
71
+ static async getProfitLossReport(req: AuthenticatedRequest, res: Response, next: NextFunction) {
72
+ try {
73
+ const tglMulai = new Date(req.query.tgl_mulai as string);
74
+ const tglSelesai = new Date(req.query.tgl_selesai as string);
75
+
76
+ const report = await OpsService.buatLaporanLabaRugi(tglMulai, tglSelesai);
77
+ return sukses(res, report, 'Laporan laba rugi berhasil dibuat');
78
+ } catch (error) {
79
+ next(error);
80
+ }
81
+ }
82
+
83
+ // Settings/Pengaturan
84
+ static async getSettings(req: Request, res: Response, next: NextFunction) {
85
+ try {
86
+ const settings = await OpsService.getSettings();
87
+ return sukses(res, settings, 'Pengaturan toko berhasil dimuat');
88
+ } catch (error) {
89
+ next(error);
90
+ }
91
+ }
92
+
93
+ static async getSettingByKey(req: Request, res: Response, next: NextFunction) {
94
+ try {
95
+ const { kunci } = req.params;
96
+ const value = await OpsService.getSettingByKey(kunci);
97
+ return sukses(res, { kunci, nilai: value }, `Pengaturan '${kunci}' berhasil dimuat`);
98
+ } catch (error) {
99
+ next(error);
100
+ }
101
+ }
102
+
103
+ static async updateSetting(req: AuthenticatedRequest, res: Response, next: NextFunction) {
104
+ try {
105
+ const { kunci } = req.params;
106
+ const actorId = req.user.id;
107
+ const ip = (req.ip || req.socket.remoteAddress || 'unknown').replace('::ffff:', '');
108
+ const userAgent = (req.headers['user-agent'] || 'unknown') as string;
109
+
110
+ await OpsService.updateSetting(kunci, req.body, actorId, ip, userAgent);
111
+ const value = await OpsService.getSettingByKey(kunci);
112
+ return sukses(res, { kunci, nilai: value }, 'Pengaturan berhasil diperbarui');
113
+ } catch (error) {
114
+ next(error);
115
+ }
116
+ }
117
+
118
+ // Notifikasi
119
+ static async listNotifications(req: Request, res: Response, next: NextFunction) {
120
+ try {
121
+ const skip = parseInt(req.query.skip as string) || 0;
122
+ const limit = parseInt(req.query.limit as string) || 20;
123
+ const dibaca = req.query.dibaca !== undefined ? parseInt(req.query.dibaca as string) : undefined;
124
+
125
+ const notifs = await OpsService.getNotifications(skip, limit, dibaca);
126
+ return sukses(res, notifs, 'Notifikasi berhasil dimuat');
127
+ } catch (error) {
128
+ next(error);
129
+ }
130
+ }
131
+
132
+ static async readAllNotifications(req: Request, res: Response, next: NextFunction) {
133
+ try {
134
+ const affected = await OpsService.markAllNotifAsRead();
135
+ return sukses(res, { jumlah_diubah: affected }, 'Semua notifikasi ditandai dibaca');
136
+ } catch (error) {
137
+ next(error);
138
+ }
139
+ }
140
+
141
+ static async readNotification(req: Request, res: Response, next: NextFunction) {
142
+ try {
143
+ const notifId = parseInt(req.params.notif_id);
144
+ const notif = await OpsService.markNotifAsRead(notifId);
145
+ return sukses(res, notif, 'Notifikasi ditandai dibaca');
146
+ } catch (error) {
147
+ next(error);
148
+ }
149
+ }
150
+
151
+ static async getUnreadNotificationCount(req: Request, res: Response, next: NextFunction) {
152
+ try {
153
+ const count = await OpsService.getUnreadNotifCount();
154
+ return sukses(res, { unread_count: count }, 'Jumlah notifikasi belum dibaca berhasil dihitung');
155
+ } catch (error) {
156
+ next(error);
157
+ }
158
+ }
159
+ }
src/controllers/pengguna.controller.ts ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { PenggunaService } from '../services/pengguna.service';
3
+ import { sukses } from '../utils/response';
4
+ import { AuthenticatedRequest } from '../middlewares/auth.middleware';
5
+
6
+ export class PenggunaController {
7
+ private static formatUser(user: any) {
8
+ const { password_hash, ...rest } = user;
9
+ return rest;
10
+ }
11
+
12
+ static async list(req: AuthenticatedRequest, res: Response, next: NextFunction) {
13
+ try {
14
+ const skip = parseInt(req.query.skip as string) || 0;
15
+ const limit = parseInt(req.query.limit as string) || 10;
16
+ const aktif = req.query.aktif !== undefined ? parseInt(req.query.aktif as string) : undefined;
17
+
18
+ const users = await PenggunaService.getUsers(skip, limit, aktif);
19
+ const data = users.map((u: any) => PenggunaController.formatUser(u));
20
+
21
+ return sukses(res, data, 'Daftar pengguna berhasil dimuat');
22
+ } catch (error) {
23
+ next(error);
24
+ }
25
+ }
26
+
27
+ static async get(req: AuthenticatedRequest, res: Response, next: NextFunction) {
28
+ try {
29
+ const userId = parseInt(req.params.id);
30
+ const user = await PenggunaService.getUserById(userId);
31
+ const data = PenggunaController.formatUser(user);
32
+
33
+ return sukses(res, data, 'Detail pengguna berhasil dimuat');
34
+ } catch (error) {
35
+ next(error);
36
+ }
37
+ }
38
+
39
+ static async create(req: AuthenticatedRequest, res: Response, next: NextFunction) {
40
+ try {
41
+ const actorId = req.user.id;
42
+ const ip = (req.ip || req.socket.remoteAddress || 'unknown').replace('::ffff:', '');
43
+ const userAgent = (req.headers['user-agent'] || 'unknown') as string;
44
+
45
+ const user = await PenggunaService.create(req.body, actorId, ip, userAgent);
46
+ const data = PenggunaController.formatUser(user);
47
+
48
+ return sukses(res, data, 'Pengguna berhasil dibuat', null, 200);
49
+ } catch (error) {
50
+ next(error);
51
+ }
52
+ }
53
+
54
+ static async update(req: AuthenticatedRequest, res: Response, next: NextFunction) {
55
+ try {
56
+ const userId = parseInt(req.params.id);
57
+ const actorId = req.user.id;
58
+ const ip = (req.ip || req.socket.remoteAddress || 'unknown').replace('::ffff:', '');
59
+ const userAgent = (req.headers['user-agent'] || 'unknown') as string;
60
+
61
+ const user = await PenggunaService.update(userId, req.body, actorId, ip, userAgent);
62
+ const data = PenggunaController.formatUser(user);
63
+
64
+ return sukses(res, data, 'Pengguna berhasil diperbarui');
65
+ } catch (error) {
66
+ next(error);
67
+ }
68
+ }
69
+
70
+ static async delete(req: AuthenticatedRequest, res: Response, next: NextFunction) {
71
+ try {
72
+ const userId = parseInt(req.params.id);
73
+ const actorId = req.user.id;
74
+ const ip = (req.ip || req.socket.remoteAddress || 'unknown').replace('::ffff:', '');
75
+ const userAgent = (req.headers['user-agent'] || 'unknown') as string;
76
+
77
+ await PenggunaService.delete(userId, actorId, ip, userAgent);
78
+
79
+ return sukses(res, null, 'Pengguna berhasil dinonaktifkan');
80
+ } catch (error) {
81
+ next(error);
82
+ }
83
+ }
84
+ }
src/controllers/produk.controller.ts ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { ProdukService } from '../services/produk.service';
3
+ import { sukses } from '../utils/response';
4
+ import { AuthenticatedRequest } from '../middlewares/auth.middleware';
5
+
6
+ export class ProdukController {
7
+ static async list(req: Request, res: Response, next: NextFunction) {
8
+ try {
9
+ const skip = parseInt(req.query.skip as string) || 0;
10
+ const limit = parseInt(req.query.limit as string) || 10;
11
+ const kategoriId = req.query.kategori_id ? parseInt(req.query.kategori_id as string) : undefined;
12
+ const supplierId = req.query.supplier_id ? parseInt(req.query.supplier_id as string) : undefined;
13
+ const pencarian = req.query.pencarian as string || undefined;
14
+
15
+ const { products, total } = await ProdukService.getProduk(skip, limit, kategoriId, supplierId, pencarian);
16
+ const page = Math.floor(skip / limit) + 1;
17
+
18
+ return sukses(res, products, 'Daftar produk berhasil dimuat', {
19
+ total,
20
+ page,
21
+ per_page: limit,
22
+ });
23
+ } catch (error) {
24
+ next(error);
25
+ }
26
+ }
27
+
28
+ static async get(req: Request, res: Response, next: NextFunction) {
29
+ try {
30
+ const produkId = parseInt(req.params.id);
31
+ const product = await ProdukService.getProdukById(produkId);
32
+ return sukses(res, product, 'Detail produk berhasil dimuat');
33
+ } catch (error) {
34
+ next(error);
35
+ }
36
+ }
37
+
38
+ static async scan(req: Request, res: Response, next: NextFunction) {
39
+ try {
40
+ const { barcode_or_kode } = req.params;
41
+ const product = await ProdukService.getProdukByBarcodeOrKode(barcode_or_kode);
42
+ return sukses(res, product, 'Produk terdeteksi');
43
+ } catch (error) {
44
+ next(error);
45
+ }
46
+ }
47
+
48
+ static async create(req: AuthenticatedRequest, res: Response, next: NextFunction) {
49
+ try {
50
+ const actorId = req.user.id;
51
+ const ip = (req.ip || req.socket.remoteAddress || 'unknown').replace('::ffff:', '');
52
+ const userAgent = (req.headers['user-agent'] || 'unknown') as string;
53
+
54
+ const product = await ProdukService.create(req.body, actorId, ip, userAgent);
55
+ return sukses(res, product, 'Produk berhasil dibuat');
56
+ } catch (error) {
57
+ next(error);
58
+ }
59
+ }
60
+
61
+ static async update(req: AuthenticatedRequest, res: Response, next: NextFunction) {
62
+ try {
63
+ const produkId = parseInt(req.params.id);
64
+ const actorId = req.user.id;
65
+ const ip = (req.ip || req.socket.remoteAddress || 'unknown').replace('::ffff:', '');
66
+ const userAgent = (req.headers['user-agent'] || 'unknown') as string;
67
+
68
+ const product = await ProdukService.update(produkId, req.body, actorId, ip, userAgent);
69
+ return sukses(res, product, 'Produk berhasil diperbarui');
70
+ } catch (error) {
71
+ next(error);
72
+ }
73
+ }
74
+
75
+ static async delete(req: AuthenticatedRequest, res: Response, next: NextFunction) {
76
+ try {
77
+ const produkId = parseInt(req.params.id);
78
+ const actorId = req.user.id;
79
+ const ip = (req.ip || req.socket.remoteAddress || 'unknown').replace('::ffff:', '');
80
+ const userAgent = (req.headers['user-agent'] || 'unknown') as string;
81
+
82
+ await ProdukService.delete(produkId, actorId, ip, userAgent);
83
+ return sukses(res, null, 'Produk berhasil dihapus (soft-delete)');
84
+ } catch (error) {
85
+ next(error);
86
+ }
87
+ }
88
+ }
src/controllers/stok.controller.ts ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { StokService } from '../services/stok.service';
3
+ import { sukses } from '../utils/response';
4
+ import { AuthenticatedRequest } from '../middlewares/auth.middleware';
5
+
6
+ export class StokController {
7
+ static async addIncomingStock(req: AuthenticatedRequest, res: Response, next: NextFunction) {
8
+ try {
9
+ const actorId = req.user.id;
10
+ const ip = (req.ip || req.socket.remoteAddress || 'unknown').replace('::ffff:', '');
11
+ const userAgent = (req.headers['user-agent'] || 'unknown') as string;
12
+
13
+ const batch = await StokService.tambahBarangMasuk(req.body, actorId, ip, userAgent);
14
+ return sukses(res, batch, 'Barang masuk berhasil dicatat');
15
+ } catch (error) {
16
+ next(error);
17
+ }
18
+ }
19
+
20
+ static async createStockOpname(req: AuthenticatedRequest, res: Response, next: NextFunction) {
21
+ try {
22
+ const actorId = req.user.id;
23
+ const ip = (req.ip || req.socket.remoteAddress || 'unknown').replace('::ffff:', '');
24
+ const userAgent = (req.headers['user-agent'] || 'unknown') as string;
25
+
26
+ const opname = await StokService.buatStokOpname(req.body, actorId, ip, userAgent);
27
+ return sukses(res, opname, 'Draft stok opname berhasil dibuat');
28
+ } catch (error) {
29
+ next(error);
30
+ }
31
+ }
32
+
33
+ static async approveStockOpname(req: AuthenticatedRequest, res: Response, next: NextFunction) {
34
+ try {
35
+ const opnameId = parseInt(req.params.opname_id);
36
+ const actorId = req.user.id;
37
+ const ip = (req.ip || req.socket.remoteAddress || 'unknown').replace('::ffff:', '');
38
+ const userAgent = (req.headers['user-agent'] || 'unknown') as string;
39
+
40
+ const opname = await StokService.approveStokOpname(opnameId, actorId, ip, userAgent);
41
+ return sukses(res, opname, 'Stok opname disetujui, kuantitas stok produk disesuaikan');
42
+ } catch (error) {
43
+ next(error);
44
+ }
45
+ }
46
+
47
+ static async getStockMutations(req: Request, res: Response, next: NextFunction) {
48
+ try {
49
+ const skip = parseInt(req.query.skip as string) || 0;
50
+ const limit = parseInt(req.query.limit as string) || 20;
51
+ const produkId = req.query.produk_id ? parseInt(req.query.produk_id as string) : undefined;
52
+ const tipe = req.query.tipe as string || undefined;
53
+
54
+ const logs = await StokService.getStokLogs(skip, limit, produkId, tipe);
55
+ return sukses(res, logs, 'Kartu stok berhasil dimuat');
56
+ } catch (error) {
57
+ next(error);
58
+ }
59
+ }
60
+
61
+ static async getStockBatches(req: Request, res: Response, next: NextFunction) {
62
+ try {
63
+ const skip = parseInt(req.query.skip as string) || 0;
64
+ const limit = parseInt(req.query.limit as string) || 20;
65
+ const produkId = req.query.produk_id ? parseInt(req.query.produk_id as string) : undefined;
66
+
67
+ const batches = await StokService.getStokBatches(skip, limit, produkId);
68
+ return sukses(res, batches, 'Batch stok berhasil dimuat');
69
+ } catch (error) {
70
+ next(error);
71
+ }
72
+ }
73
+ }
src/controllers/supplier.controller.ts ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { SupplierService } from '../services/supplier.service';
3
+ import { sukses } from '../utils/response';
4
+ import { AuthenticatedRequest } from '../middlewares/auth.middleware';
5
+
6
+ export class SupplierController {
7
+ static async list(req: Request, res: Response, next: NextFunction) {
8
+ try {
9
+ const skip = parseInt(req.query.skip as string) || 0;
10
+ const limit = parseInt(req.query.limit as string) || 10;
11
+ const pencarian = req.query.pencarian as string || undefined;
12
+
13
+ const suppliers = await SupplierService.getSuppliers(skip, limit, pencarian);
14
+ return sukses(res, suppliers, 'Daftar supplier berhasil dimuat');
15
+ } catch (error) {
16
+ next(error);
17
+ }
18
+ }
19
+
20
+ static async create(req: AuthenticatedRequest, res: Response, next: NextFunction) {
21
+ try {
22
+ const actorId = req.user.id;
23
+ const ip = (req.ip || req.socket.remoteAddress || 'unknown').replace('::ffff:', '');
24
+ const userAgent = (req.headers['user-agent'] || 'unknown') as string;
25
+
26
+ const supplier = await SupplierService.create(req.body, actorId, ip, userAgent);
27
+ return sukses(res, supplier, 'Supplier berhasil ditambahkan');
28
+ } catch (error) {
29
+ next(error);
30
+ }
31
+ }
32
+
33
+ static async update(req: AuthenticatedRequest, res: Response, next: NextFunction) {
34
+ try {
35
+ const supplierId = parseInt(req.params.id);
36
+ const actorId = req.user.id;
37
+ const ip = (req.ip || req.socket.remoteAddress || 'unknown').replace('::ffff:', '');
38
+ const userAgent = (req.headers['user-agent'] || 'unknown') as string;
39
+
40
+ const supplier = await SupplierService.update(supplierId, req.body, actorId, ip, userAgent);
41
+ return sukses(res, supplier, 'Supplier berhasil diperbarui');
42
+ } catch (error) {
43
+ next(error);
44
+ }
45
+ }
46
+
47
+ static async delete(req: AuthenticatedRequest, res: Response, next: NextFunction) {
48
+ try {
49
+ const supplierId = parseInt(req.params.id);
50
+ const actorId = req.user.id;
51
+ const ip = (req.ip || req.socket.remoteAddress || 'unknown').replace('::ffff:', '');
52
+ const userAgent = (req.headers['user-agent'] || 'unknown') as string;
53
+
54
+ await SupplierService.delete(supplierId, actorId, ip, userAgent);
55
+ return sukses(res, null, 'Supplier berhasil dinonaktifkan');
56
+ } catch (error) {
57
+ next(error);
58
+ }
59
+ }
60
+ }
src/controllers/transaksi.controller.ts ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { TransaksiService } from '../services/transaksi.service';
3
+ import { sukses, gagal } from '../utils/response';
4
+ import { AuthenticatedRequest } from '../middlewares/auth.middleware';
5
+ import prisma from '../utils/db';
6
+
7
+ export class TransaksiController {
8
+ static async create(req: AuthenticatedRequest, res: Response, next: NextFunction) {
9
+ try {
10
+ const actorId = req.user.id;
11
+ const ip = (req.ip || req.socket.remoteAddress || 'unknown').replace('::ffff:', '');
12
+ const userAgent = (req.headers['user-agent'] || 'unknown') as string;
13
+
14
+ const trans = await TransaksiService.prosesTransaksi({
15
+ data: req.body,
16
+ actorId,
17
+ ip,
18
+ userAgent,
19
+ });
20
+
21
+ return sukses(res, trans, 'Transaksi berhasil diproses');
22
+ } catch (error) {
23
+ next(error);
24
+ }
25
+ }
26
+
27
+ static async list(req: Request, res: Response, next: NextFunction) {
28
+ try {
29
+ const skip = parseInt(req.query.skip as string) || 0;
30
+ const limit = parseInt(req.query.limit as string) || 20;
31
+ const statusFilter = (req.query.status_filter as any) || undefined;
32
+ const memberId = req.query.member_id ? parseInt(req.query.member_id as string) : undefined;
33
+
34
+ const where: any = {};
35
+ if (statusFilter) where.status = statusFilter;
36
+ if (memberId) where.member_id = memberId;
37
+
38
+ const total = await prisma.transaksi.count({ where });
39
+ const trans = await prisma.transaksi.findMany({
40
+ where,
41
+ orderBy: { created_at: 'desc' },
42
+ skip,
43
+ take: limit,
44
+ include: {
45
+ transaksi_detail: true,
46
+ pembayaran: true,
47
+ },
48
+ });
49
+
50
+ const page = Math.floor(skip / limit) + 1;
51
+
52
+ return sukses(res, trans, 'Daftar transaksi berhasil dimuat', {
53
+ total,
54
+ page,
55
+ per_page: limit,
56
+ });
57
+ } catch (error) {
58
+ next(error);
59
+ }
60
+ }
61
+
62
+ static async get(req: Request, res: Response, next: NextFunction) {
63
+ try {
64
+ const transaksiId = parseInt(req.params.id);
65
+ const trans = await prisma.transaksi.findUnique({
66
+ where: { id: transaksiId },
67
+ include: {
68
+ transaksi_detail: true,
69
+ pembayaran: true,
70
+ user: {
71
+ select: {
72
+ id: true,
73
+ nama: true,
74
+ username: true,
75
+ },
76
+ },
77
+ member: true,
78
+ },
79
+ });
80
+
81
+ if (!trans) {
82
+ return gagal(res, 404, 'Transaksi tidak ditemukan');
83
+ }
84
+
85
+ return sukses(res, trans, 'Detail transaksi berhasil dimuat');
86
+ } catch (error) {
87
+ next(error);
88
+ }
89
+ }
90
+
91
+ static async void(req: AuthenticatedRequest, res: Response, next: NextFunction) {
92
+ try {
93
+ const transaksiId = parseInt(req.params.id);
94
+ const actorId = req.user.id;
95
+ const ip = (req.ip || req.socket.remoteAddress || 'unknown').replace('::ffff:', '');
96
+ const userAgent = (req.headers['user-agent'] || 'unknown') as string;
97
+ const alasan = req.body.alasan || 'Pembatalan transaksi oleh kasir';
98
+
99
+ const trans = await TransaksiService.prosesVoidTransaksi(transaksiId, alasan, actorId, ip, userAgent);
100
+ return sukses(res, trans, 'Transaksi berhasil di-void');
101
+ } catch (error) {
102
+ next(error);
103
+ }
104
+ }
105
+
106
+ static async retur(req: AuthenticatedRequest, res: Response, next: NextFunction) {
107
+ try {
108
+ const actorId = req.user.id;
109
+ const ip = (req.ip || req.socket.remoteAddress || 'unknown').replace('::ffff:', '');
110
+ const userAgent = (req.headers['user-agent'] || 'unknown') as string;
111
+
112
+ const retur = await TransaksiService.prosesReturBarang(req.body, actorId, ip, userAgent);
113
+ return sukses(res, retur, 'Retur barang berhasil dicatat');
114
+ } catch (error) {
115
+ next(error);
116
+ }
117
+ }
118
+ }
src/controllers/upload.controller.ts ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { uploadBase64ToSupabase } from '../utils/supabase';
3
+ import { sukses, gagal } from '../utils/response';
4
+ import crypto from 'crypto';
5
+
6
+ export class UploadController {
7
+ static async uploadProdukFoto(req: Request, res: Response, next: NextFunction) {
8
+ try {
9
+ const { images, prefix } = req.body;
10
+ const cleanPrefix = (prefix || 'img').replace(/[^a-zA-Z0-9-_]/g, '').toLowerCase();
11
+ const uploadedUrls: string[] = [];
12
+
13
+ for (let i = 0; i < images.length; i++) {
14
+ const imgBase64 = images[i];
15
+
16
+ if (imgBase64.startsWith('http')) {
17
+ uploadedUrls.push(imgBase64);
18
+ continue;
19
+ }
20
+
21
+ // Generate unique path/filename
22
+ const uniqueId = crypto.randomBytes(4).toString('hex'); // 8 chars
23
+ const timestamp = Math.floor(Date.now() / 1000);
24
+ const fileName = `${cleanPrefix}_${timestamp}_${uniqueId}_${i}.jpg`;
25
+ const filePath = `produk/${fileName}`;
26
+
27
+ const url = await uploadBase64ToSupabase('produk', filePath, imgBase64);
28
+
29
+ if (url) {
30
+ uploadedUrls.push(url);
31
+ } else {
32
+ return gagal(res, 500, `Gagal mengunggah foto ke-${i + 1} ke Supabase`);
33
+ }
34
+ }
35
+
36
+ return sukses(res, { urls: uploadedUrls }, 'Foto berhasil diunggah ke Supabase');
37
+ } catch (error) {
38
+ next(error);
39
+ }
40
+ }
41
+ }
src/middlewares/auth.middleware.ts ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { verifyToken } from '../utils/jwt';
3
+ import prisma from '../utils/db';
4
+ import { gagal } from '../utils/response';
5
+
6
+ export interface AuthenticatedRequest extends Request {
7
+ user?: any;
8
+ }
9
+
10
+ export const authenticate = async (
11
+ req: AuthenticatedRequest,
12
+ res: Response,
13
+ next: NextFunction
14
+ ) => {
15
+ try {
16
+ const authHeader = req.headers.authorization;
17
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
18
+ return gagal(res, 401, 'Token tidak ditemukan atau tidak valid');
19
+ }
20
+
21
+ const token = authHeader.split(' ')[1];
22
+ const decoded = verifyToken(token);
23
+
24
+ if (!decoded || decoded.type !== 'access') {
25
+ return gagal(res, 401, 'Token tidak valid atau kedaluwarsa');
26
+ }
27
+
28
+ const username = decoded.sub;
29
+ const user = await prisma.user.findFirst({
30
+ where: {
31
+ username,
32
+ aktif: 1,
33
+ },
34
+ });
35
+
36
+ if (!user) {
37
+ return gagal(res, 401, 'Pengguna tidak aktif atau tidak ditemukan');
38
+ }
39
+
40
+ req.user = user;
41
+ next();
42
+ } catch (error) {
43
+ next(error);
44
+ }
45
+ };
46
+
47
+ export const requireAdmin = (
48
+ req: AuthenticatedRequest,
49
+ res: Response,
50
+ next: NextFunction
51
+ ) => {
52
+ if (!req.user || req.user.role !== 'admin') {
53
+ return gagal(res, 403, 'Akses ditolak. Peran Admin diperlukan.');
54
+ }
55
+ next();
56
+ };
src/middlewares/error.middleware.ts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { gagal } from '../utils/response';
3
+
4
+ export const errorHandler = (
5
+ err: any,
6
+ req: Request,
7
+ res: Response,
8
+ next: NextFunction
9
+ ) => {
10
+ console.error('[Error Handler]:', err);
11
+
12
+ const statusCode = err.statusCode || 500;
13
+ const message = err.message || 'Terjadi kesalahan pada server';
14
+ const detail = err.detail || err.errors || message;
15
+
16
+ return gagal(res, statusCode, message, err.stack || null, detail);
17
+ };
src/middlewares/validate.middleware.ts ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { AnyZodObject, ZodError } from 'zod';
3
+
4
+ export const validate = (schema: AnyZodObject) => {
5
+ return async (req: Request, res: Response, next: NextFunction) => {
6
+ try {
7
+ const parsed = await schema.parseAsync({
8
+ body: req.body,
9
+ query: req.query,
10
+ params: req.params,
11
+ });
12
+
13
+ if (parsed.body !== undefined) req.body = parsed.body;
14
+ if (parsed.query !== undefined) req.query = parsed.query;
15
+ if (parsed.params !== undefined) req.params = parsed.params;
16
+
17
+ return next();
18
+ } catch (error) {
19
+ if (error instanceof ZodError) {
20
+ return res.status(400).json({
21
+ sukses: false,
22
+ pesan: 'Validasi input gagal',
23
+ errors: error.errors.map((err) => ({
24
+ field: err.path.slice(1).join('.'),
25
+ message: err.message,
26
+ })),
27
+ });
28
+ }
29
+ return next(error);
30
+ }
31
+ };
32
+ };
src/routes/auth.route.ts ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Router } from 'express';
2
+ import { AuthController } from '../controllers/auth.controller';
3
+ import { validate } from '../middlewares/validate.middleware';
4
+ import { loginSchema, refreshSchema } from '../schemas/auth.schema';
5
+ import { authenticate } from '../middlewares/auth.middleware';
6
+
7
+ const authRouter = Router();
8
+
9
+ authRouter.post('/login', validate(loginSchema), AuthController.login);
10
+ authRouter.post('/refresh', validate(refreshSchema), AuthController.refresh);
11
+ authRouter.post('/logout', authenticate, AuthController.logout);
12
+
13
+ export default authRouter;
src/routes/bon.route.ts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Router } from 'express';
2
+ import { BonController } from '../controllers/bon.controller';
3
+ import { authenticate } from '../middlewares/auth.middleware';
4
+ import { validate } from '../middlewares/validate.middleware';
5
+ import { bayarBonSchema } from '../schemas/bon.schema';
6
+
7
+ const bonRouter = Router();
8
+
9
+ // Rute Publik
10
+ bonRouter.get('/', BonController.list);
11
+ bonRouter.get('/:id', BonController.get);
12
+
13
+ // Rute Terproteksi Token
14
+ bonRouter.post('/:id/bayar', authenticate, validate(bayarBonSchema), BonController.payInstallment);
15
+ bonRouter.post('/:id/reminder', authenticate, BonController.sendDebtReminder);
16
+
17
+ export default bonRouter;
src/routes/index.ts ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Router } from 'express';
2
+ import authRouter from './auth.route';
3
+ import penggunaRouter from './pengguna.route';
4
+ import uploadRouter from './upload.route';
5
+ import kategoriRouter from './kategori.route';
6
+ import supplierRouter from './supplier.route';
7
+ import memberRouter from './member.route';
8
+ import produkRouter from './produk.route';
9
+ import stokRouter from './stok.route';
10
+ import bonRouter from './bon.route';
11
+ import transaksiRouter from './transaksi.route';
12
+ import opsRouter from './ops.route';
13
+
14
+ const router = Router();
15
+
16
+ router.use('/auth', authRouter);
17
+ router.use('/users', penggunaRouter);
18
+ router.use('/upload', uploadRouter);
19
+ router.use('/kategori', kategoriRouter);
20
+ router.use('/supplier', supplierRouter);
21
+ router.use('/member', memberRouter);
22
+ router.use('/produk', produkRouter);
23
+ router.use('/stok', stokRouter);
24
+ router.use('/bon', bonRouter);
25
+ router.use('/transaksi', transaksiRouter);
26
+ router.use('/', opsRouter);
27
+
28
+ export default router;
src/routes/kategori.route.ts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Router } from 'express';
2
+ import { KategoriController } from '../controllers/kategori.controller';
3
+ import { authenticate } from '../middlewares/auth.middleware';
4
+ import { validate } from '../middlewares/validate.middleware';
5
+ import { createKategoriSchema, updateKategoriSchema } from '../schemas/kategori.schema';
6
+
7
+ const kategoriRouter = Router();
8
+
9
+ // Rute Publik
10
+ kategoriRouter.get('/', KategoriController.list);
11
+
12
+ // Rute Terproteksi Token
13
+ kategoriRouter.post('/', authenticate, validate(createKategoriSchema), KategoriController.create);
14
+ kategoriRouter.put('/:id', authenticate, validate(updateKategoriSchema), KategoriController.update);
15
+ kategoriRouter.delete('/:id', authenticate, KategoriController.delete);
16
+
17
+ export default kategoriRouter;
src/routes/member.route.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Router } from 'express';
2
+ import { MemberController } from '../controllers/member.controller';
3
+ import { authenticate } from '../middlewares/auth.middleware';
4
+ import { validate } from '../middlewares/validate.middleware';
5
+ import { createMemberSchema, updateMemberSchema } from '../schemas/member.schema';
6
+
7
+ const memberRouter = Router();
8
+
9
+ memberRouter.get('/', MemberController.list);
10
+
11
+ // Protected routes below
12
+ memberRouter.post('/', authenticate, validate(createMemberSchema), MemberController.create);
13
+ memberRouter.put('/:id', authenticate, validate(updateMemberSchema), MemberController.update);
14
+ memberRouter.delete('/:id', authenticate, MemberController.delete);
15
+
16
+ export default memberRouter;
src/routes/ops.route.ts ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Router } from 'express';
2
+ import { OpsController } from '../controllers/ops.controller';
3
+ import { authenticate } from '../middlewares/auth.middleware';
4
+ import { validate } from '../middlewares/validate.middleware';
5
+ import { bukaShiftSchema, tutupShiftSchema, updatePengaturanSchema, laporanQuerySchema } from '../schemas/ops.schema';
6
+
7
+ const opsRouter = Router();
8
+
9
+ // ================= SHIFT KASIR =================
10
+ opsRouter.get('/shift/aktif', authenticate, OpsController.getActiveShift);
11
+ opsRouter.post('/shift/buka', authenticate, validate(bukaShiftSchema), OpsController.openShift);
12
+ opsRouter.post('/shift/tutup', authenticate, validate(tutupShiftSchema), OpsController.closeShift);
13
+
14
+ // ================= REPORT & DASHBOARD =================
15
+ opsRouter.get('/dashboard/summary', authenticate, OpsController.getDashboardSummary);
16
+ opsRouter.get('/laporan/penjualan', authenticate, validate(laporanQuerySchema), OpsController.getSalesReport);
17
+ opsRouter.get('/laporan/laba', authenticate, validate(laporanQuerySchema), OpsController.getProfitLossReport);
18
+
19
+ // ================= SETTINGS =================
20
+ opsRouter.get('/pengaturan', OpsController.getSettings);
21
+ opsRouter.get('/pengaturan/:kunci', OpsController.getSettingByKey);
22
+ opsRouter.put('/pengaturan/:kunci', authenticate, validate(updatePengaturanSchema), OpsController.updateSetting);
23
+
24
+ // ================= NOTIFIKASI =================
25
+ opsRouter.get('/notifikasi', OpsController.listNotifications);
26
+ opsRouter.get('/notifikasi/unread-count', OpsController.getUnreadNotificationCount);
27
+ opsRouter.post('/notifikasi/read-all', OpsController.readAllNotifications);
28
+ opsRouter.post('/notifikasi/:notif_id/read', OpsController.readNotification);
29
+
30
+ export default opsRouter;
src/routes/pengguna.route.ts ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Router } from 'express';
2
+ import { PenggunaController } from '../controllers/pengguna.controller';
3
+ import { authenticate, requireAdmin } from '../middlewares/auth.middleware';
4
+ import { validate } from '../middlewares/validate.middleware';
5
+ import { createPenggunaSchema, updatePenggunaSchema } from '../schemas/pengguna.schema';
6
+
7
+ const penggunaRouter = Router();
8
+
9
+ // Proteksi Admin untuk semua rute pengguna
10
+ penggunaRouter.use(authenticate);
11
+ penggunaRouter.use(requireAdmin);
12
+
13
+ penggunaRouter.get('/', PenggunaController.list);
14
+ penggunaRouter.get('/:id', PenggunaController.get);
15
+ penggunaRouter.post('/', validate(createPenggunaSchema), PenggunaController.create);
16
+ penggunaRouter.put('/:id', validate(updatePenggunaSchema), PenggunaController.update);
17
+ penggunaRouter.delete('/:id', PenggunaController.delete);
18
+
19
+ export default penggunaRouter;
src/routes/produk.route.ts ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Router } from 'express';
2
+ import { ProdukController } from '../controllers/produk.controller';
3
+ import { authenticate } from '../middlewares/auth.middleware';
4
+ import { validate } from '../middlewares/validate.middleware';
5
+ import { createProdukSchema, updateProdukSchema } from '../schemas/produk.schema';
6
+
7
+ const produkRouter = Router();
8
+
9
+ // Rute Publik
10
+ produkRouter.get('/', ProdukController.list);
11
+ produkRouter.get('/:id', ProdukController.get);
12
+ produkRouter.get('/scan/:barcode_or_kode', ProdukController.scan);
13
+
14
+ // Rute Terproteksi Token
15
+ produkRouter.post('/', authenticate, validate(createProdukSchema), ProdukController.create);
16
+ produkRouter.put('/:id', authenticate, validate(updateProdukSchema), ProdukController.update);
17
+ produkRouter.delete('/:id', authenticate, ProdukController.delete);
18
+
19
+ export default produkRouter;
src/routes/stok.route.ts ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Router } from 'express';
2
+ import { StokController } from '../controllers/stok.controller';
3
+ import { authenticate } from '../middlewares/auth.middleware';
4
+ import { validate } from '../middlewares/validate.middleware';
5
+ import { barangMasukSchema, stokOpnameSchema } from '../schemas/stok.schema';
6
+
7
+ const stokRouter = Router();
8
+
9
+ // Rute Publik
10
+ stokRouter.get('/mutasi', StokController.getStockMutations);
11
+ stokRouter.get('/batches', StokController.getStockBatches);
12
+
13
+ // Rute Terproteksi Token
14
+ stokRouter.post('/masuk', authenticate, validate(barangMasukSchema), StokController.addIncomingStock);
15
+ stokRouter.post('/opname', authenticate, validate(stokOpnameSchema), StokController.createStockOpname);
16
+ stokRouter.post('/opname/:opname_id/approve', authenticate, StokController.approveStockOpname);
17
+
18
+ export default stokRouter;
src/routes/supplier.route.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Router } from 'express';
2
+ import { SupplierController } from '../controllers/supplier.controller';
3
+ import { authenticate } from '../middlewares/auth.middleware';
4
+ import { validate } from '../middlewares/validate.middleware';
5
+ import { createSupplierSchema, updateSupplierSchema } from '../schemas/supplier.schema';
6
+
7
+ const supplierRouter = Router();
8
+
9
+ supplierRouter.get('/', SupplierController.list);
10
+
11
+ // Protected routes below
12
+ supplierRouter.post('/', authenticate, validate(createSupplierSchema), SupplierController.create);
13
+ supplierRouter.put('/:id', authenticate, validate(updateSupplierSchema), SupplierController.update);
14
+ supplierRouter.delete('/:id', authenticate, SupplierController.delete);
15
+
16
+ export default supplierRouter;
src/routes/transaksi.route.ts ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Router } from 'express';
2
+ import { TransaksiController } from '../controllers/transaksi.controller';
3
+ import { authenticate } from '../middlewares/auth.middleware';
4
+ import { validate } from '../middlewares/validate.middleware';
5
+ import { createTransaksiSchema, createTransaksiReturSchema } from '../schemas/transaksi.schema';
6
+
7
+ const transaksiRouter = Router();
8
+
9
+ // Rute Publik
10
+ transaksiRouter.get('/', TransaksiController.list);
11
+ transaksiRouter.get('/:id', TransaksiController.get);
12
+
13
+ // Rute Terproteksi Token
14
+ transaksiRouter.post('/', authenticate, validate(createTransaksiSchema), TransaksiController.create);
15
+ transaksiRouter.post('/:id/void', authenticate, TransaksiController.void);
16
+ transaksiRouter.post('/retur', authenticate, validate(createTransaksiReturSchema), TransaksiController.retur);
17
+
18
+ export default transaksiRouter;
src/routes/upload.route.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Router } from 'express';
2
+ import { UploadController } from '../controllers/upload.controller';
3
+ import { authenticate } from '../middlewares/auth.middleware';
4
+ import { validate } from '../middlewares/validate.middleware';
5
+ import { uploadSchema } from '../schemas/upload.schema';
6
+
7
+ const uploadRouter = Router();
8
+
9
+ // Proteksi token untuk semua rute upload
10
+ uploadRouter.use(authenticate);
11
+
12
+ uploadRouter.post('/produk', validate(uploadSchema), UploadController.uploadProdukFoto);
13
+
14
+ export default uploadRouter;
src/schemas/auth.schema.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { z } from 'zod';
2
+
3
+ export const loginSchema = z.object({
4
+ body: z.object({
5
+ username: z.string({ required_error: 'Username wajib diisi' }).min(1, 'Username tidak boleh kosong'),
6
+ password: z.string({ required_error: 'Password wajib diisi' }).min(1, 'Password tidak boleh kosong'),
7
+ }),
8
+ });
9
+
10
+ export const refreshSchema = z.object({
11
+ body: z.object({
12
+ refresh_token: z.string({ required_error: 'Refresh token wajib diisi' }).min(1, 'Refresh token tidak boleh kosong'),
13
+ }),
14
+ });
src/schemas/bon.schema.ts ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import { z } from 'zod';
2
+
3
+ export const bayarBonSchema = z.object({
4
+ body: z.object({
5
+ nominal: z.number({ required_error: 'Nominal pembayaran wajib diisi' }).positive('Nominal harus lebih besar dari 0'),
6
+ metode: z.enum(['tunai', 'transfer'], { required_error: 'Metode cicilan wajib diisi (tunai/transfer)' }),
7
+ }),
8
+ });
src/schemas/kategori.schema.ts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { z } from 'zod';
2
+
3
+ const booleanOrNumber = z.union([z.boolean(), z.number()])
4
+ .transform((val) => (typeof val === 'boolean' ? (val ? 1 : 0) : val));
5
+
6
+ export const createKategoriSchema = z.object({
7
+ body: z.object({
8
+ nama: z.string({ required_error: 'Nama kategori wajib diisi' }).min(1, 'Nama kategori tidak boleh kosong'),
9
+ icon: z.string().optional().nullable(),
10
+ aktif: booleanOrNumber.optional().default(1),
11
+ }),
12
+ });
13
+
14
+ export const updateKategoriSchema = z.object({
15
+ body: z.object({
16
+ nama: z.string().optional(),
17
+ icon: z.string().optional().nullable(),
18
+ aktif: booleanOrNumber.optional(),
19
+ }),
20
+ });
src/schemas/member.schema.ts ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { z } from 'zod';
2
+
3
+ const booleanOrNumber = z.union([z.boolean(), z.number()])
4
+ .transform((val) => (typeof val === 'boolean' ? (val ? 1 : 0) : val));
5
+
6
+ export const createMemberSchema = z.object({
7
+ body: z.object({
8
+ kode: z.string({ required_error: 'Kode member wajib diisi' }).min(1, 'Kode member tidak boleh kosong'),
9
+ nama: z.string({ required_error: 'Nama member wajib diisi' }).min(1, 'Nama member tidak boleh kosong'),
10
+ nomor_hp: z.string({ required_error: 'Nomor HP wajib diisi' }).min(1, 'Nomor HP tidak boleh kosong'),
11
+ alamat: z.string().optional().nullable(),
12
+ limit_bon: z.number().optional().default(0),
13
+ foto_url: z.string().optional().nullable(),
14
+ aktif: booleanOrNumber.optional().default(1),
15
+ }),
16
+ });
17
+
18
+ export const updateMemberSchema = z.object({
19
+ body: z.object({
20
+ nama: z.string().optional(),
21
+ nomor_hp: z.string().optional(),
22
+ alamat: z.string().optional().nullable(),
23
+ total_poin: z.number().optional(),
24
+ limit_bon: z.number().optional(),
25
+ total_bon: z.number().optional(),
26
+ foto_url: z.string().optional().nullable(),
27
+ aktif: booleanOrNumber.optional(),
28
+ }),
29
+ });
src/schemas/ops.schema.ts ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { z } from 'zod';
2
+
3
+ export const bukaShiftSchema = z.object({
4
+ body: z.object({
5
+ modal_awal: z.number({ required_error: 'Modal awal wajib diisi' }).nonnegative('Modal awal tidak boleh negatif'),
6
+ catatan: z.string().optional().nullable(),
7
+ }),
8
+ });
9
+
10
+ export const tutupShiftSchema = z.object({
11
+ body: z.object({
12
+ modal_akhir_fisik: z.number({ required_error: 'Modal akhir fisik wajib diisi' }).nonnegative('Modal akhir fisik tidak boleh negatif'),
13
+ catatan: z.string().optional().nullable(),
14
+ }),
15
+ });
16
+
17
+ export const updatePengaturanSchema = z.object({
18
+ body: z.object({
19
+ nilai: z.string({ required_error: 'Nilai pengaturan wajib diisi' }),
20
+ deskripsi: z.string().optional().nullable(),
21
+ }),
22
+ });
23
+
24
+ export const laporanQuerySchema = z.object({
25
+ query: z.object({
26
+ tgl_mulai: z.string({ required_error: 'tgl_mulai wajib diisi' }).refine((val) => !isNaN(Date.parse(val)), {
27
+ message: 'Format tgl_mulai tidak valid (harus YYYY-MM-DD)',
28
+ }),
29
+ tgl_selesai: z.string({ required_error: 'tgl_selesai wajib diisi' }).refine((val) => !isNaN(Date.parse(val)), {
30
+ message: 'Format tgl_selesai tidak valid (harus YYYY-MM-DD)',
31
+ }),
32
+ }),
33
+ });
src/schemas/pengguna.schema.ts ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { z } from 'zod';
2
+
3
+ const booleanOrNumber = z.union([z.boolean(), z.number()])
4
+ .transform((val) => (typeof val === 'boolean' ? (val ? 1 : 0) : val));
5
+
6
+ export const createPenggunaSchema = z.object({
7
+ body: z.object({
8
+ nama: z.string({ required_error: 'Nama wajib diisi' }).min(1, 'Nama tidak boleh kosong'),
9
+ username: z.string({ required_error: 'Username wajib diisi' }).min(1, 'Username tidak boleh kosong'),
10
+ password: z.string({ required_error: 'Password wajib diisi' }).min(6, 'Password minimal 6 karakter'),
11
+ role: z.enum(['admin', 'kasir'], { required_error: 'Role wajib diisi (admin/kasir)' }),
12
+ nomor_hp: z.string().optional().nullable(),
13
+ foto_url: z.string().optional().nullable(),
14
+ aktif: booleanOrNumber.default(1),
15
+ }),
16
+ });
17
+
18
+ export const updatePenggunaSchema = z.object({
19
+ body: z.object({
20
+ nama: z.string().optional(),
21
+ username: z.string().optional(),
22
+ password: z.string().min(6, 'Password minimal 6 karakter').optional(),
23
+ role: z.enum(['admin', 'kasir']).optional(),
24
+ nomor_hp: z.string().optional().nullable(),
25
+ foto_url: z.string().optional().nullable(),
26
+ aktif: booleanOrNumber.optional(),
27
+ }),
28
+ });
src/schemas/produk.schema.ts ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { z } from 'zod';
2
+
3
+ const booleanOrNumber = z.union([z.boolean(), z.number()])
4
+ .transform((val) => (typeof val === 'boolean' ? (val ? 1 : 0) : val));
5
+
6
+ export const createProdukSchema = z.object({
7
+ body: z.object({
8
+ kode: z.string({ required_error: 'Kode produk wajib diisi' }).min(1, 'Kode produk tidak boleh kosong'),
9
+ barcode: z.string().optional().nullable(),
10
+ nama: z.string({ required_error: 'Nama produk wajib diisi' }).min(1, 'Nama produk tidak boleh kosong'),
11
+ kategori_id: z.number({ required_error: 'Kategori ID wajib diisi' }),
12
+ supplier_id: z.number().optional().nullable(),
13
+ satuan: z.string().optional().default('pcs'),
14
+ harga_beli: z.number({ required_error: 'Harga beli wajib diisi' }).nonnegative(),
15
+ harga_jual: z.number({ required_error: 'Harga jual wajib diisi' }).nonnegative(),
16
+ stok: z.number().optional().default(0),
17
+ stok_min: z.number().optional().default(5),
18
+ expired_date: z.string().optional().nullable().transform((val) => (val ? new Date(val) : null)),
19
+ foto_url: z.string().optional().nullable(),
20
+ qr_url: z.string().optional().nullable(),
21
+ deskripsi: z.string().optional().nullable(),
22
+ is_ecer: booleanOrNumber.optional().default(1),
23
+ is_aktif: booleanOrNumber.optional().default(1),
24
+ harga_tingkat: z.array(z.object({
25
+ tingkat: z.enum(['ecer', 'grosir', 'partai']),
26
+ min_qty: z.number().positive(),
27
+ harga: z.number().nonnegative(),
28
+ })).optional(),
29
+ }),
30
+ });
31
+
32
+ export const updateProdukSchema = z.object({
33
+ body: z.object({
34
+ kode: z.string().optional(),
35
+ barcode: z.string().optional().nullable(),
36
+ nama: z.string().optional(),
37
+ kategori_id: z.number().optional(),
38
+ supplier_id: z.number().optional().nullable(),
39
+ satuan: z.string().optional(),
40
+ harga_beli: z.number().nonnegative().optional(),
41
+ harga_jual: z.number().nonnegative().optional(),
42
+ stok_min: z.number().optional(),
43
+ expired_date: z.string().optional().nullable().transform((val) => (val ? new Date(val) : null)),
44
+ foto_url: z.string().optional().nullable(),
45
+ qr_url: z.string().optional().nullable(),
46
+ deskripsi: z.string().optional().nullable(),
47
+ is_ecer: booleanOrNumber.optional(),
48
+ is_aktif: booleanOrNumber.optional(),
49
+ harga_tingkat: z.array(z.object({
50
+ tingkat: z.enum(['ecer', 'grosir', 'partai']),
51
+ min_qty: z.number().positive(),
52
+ harga: z.number().nonnegative(),
53
+ })).optional().nullable(),
54
+ }),
55
+ });
src/schemas/stok.schema.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { z } from 'zod';
2
+
3
+ export const barangMasukSchema = z.object({
4
+ body: z.object({
5
+ produk_id: z.number({ required_error: 'produk_id wajib diisi' }),
6
+ qty: z.number({ required_error: 'Kuantitas (qty) wajib diisi' }).positive(),
7
+ harga_beli: z.number({ required_error: 'Harga beli wajib diisi' }).positive(),
8
+ batch_no: z.string().optional().nullable(),
9
+ expired_date: z.string().optional().nullable().transform((val) => (val ? new Date(val) : null)),
10
+ supplier_id: z.number().optional().nullable(),
11
+ catatan: z.string().optional().nullable(),
12
+ }),
13
+ });
14
+
15
+ export const stokOpnameSchema = z.object({
16
+ body: z.object({
17
+ produk_id: z.number({ required_error: 'produk_id wajib diisi' }),
18
+ stok_fisik: z.number({ required_error: 'stok_fisik wajib diisi' }).nonnegative(),
19
+ alasan: z.string().optional().nullable(),
20
+ }),
21
+ });
src/schemas/supplier.schema.ts ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { z } from 'zod';
2
+
3
+ const booleanOrNumber = z.union([z.boolean(), z.number()])
4
+ .transform((val) => (typeof val === 'boolean' ? (val ? 1 : 0) : val));
5
+
6
+ export const createSupplierSchema = z.object({
7
+ body: z.object({
8
+ nama: z.string({ required_error: 'Nama supplier wajib diisi' }).min(1, 'Nama supplier tidak boleh kosong'),
9
+ kontak: z.string().optional().nullable(),
10
+ nomor_hp: z.string().optional().nullable(),
11
+ alamat: z.string().optional().nullable(),
12
+ hutang: z.number().optional().default(0),
13
+ aktif: booleanOrNumber.optional().default(1),
14
+ }),
15
+ });
16
+
17
+ export const updateSupplierSchema = z.object({
18
+ body: z.object({
19
+ nama: z.string().optional(),
20
+ kontak: z.string().optional().nullable(),
21
+ nomor_hp: z.string().optional().nullable(),
22
+ alamat: z.string().optional().nullable(),
23
+ hutang: z.number().optional(),
24
+ aktif: booleanOrNumber.optional(),
25
+ }),
26
+ });
src/schemas/transaksi.schema.ts ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { z } from 'zod';
2
+
3
+ export const createTransaksiSchema = z.object({
4
+ body: z.object({
5
+ member_id: z.number().optional().nullable(),
6
+ local_id: z.string().optional().nullable(),
7
+ catatan: z.string().optional().nullable(),
8
+ items: z.array(z.object({
9
+ produk_id: z.number({ required_error: 'produk_id item wajib diisi' }),
10
+ qty: z.number({ required_error: 'qty item wajib diisi' }).positive(),
11
+ })).min(1, 'Transaksi minimal harus memiliki 1 item'),
12
+ pembayaran: z.array(z.object({
13
+ metode: z.enum(['tunai', 'qris', 'transfer', 'bon', 'voucher'], { required_error: 'metode pembayaran wajib diisi' }),
14
+ nominal: z.number({ required_error: 'nominal pembayaran wajib diisi' }).nonnegative(),
15
+ referensi: z.string().optional().nullable(),
16
+ })).min(1, 'Transaksi minimal harus memiliki 1 metode pembayaran'),
17
+ }),
18
+ });
19
+
20
+ export const createTransaksiReturSchema = z.object({
21
+ body: z.object({
22
+ transaksi_id: z.number({ required_error: 'transaksi_id wajib diisi' }),
23
+ produk_id: z.number({ required_error: 'produk_id wajib diisi' }),
24
+ qty: z.number({ required_error: 'Kuantitas retur (qty) wajib diisi' }).positive(),
25
+ alasan: z.string({ required_error: 'Alasan retur wajib diisi' }).min(1),
26
+ tipe: z.enum(['refund', 'exchange'], { required_error: 'Tipe retur wajib diisi (refund/exchange)' }),
27
+ nominal_kembali: z.number().optional().default(0),
28
+ stok_kembali: z.number().optional().default(1),
29
+ }),
30
+ });
src/schemas/upload.schema.ts ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import { z } from 'zod';
2
+
3
+ export const uploadSchema = z.object({
4
+ body: z.object({
5
+ images: z.array(z.string({ required_error: 'Images array is required' })),
6
+ prefix: z.string().optional().default('img'),
7
+ }),
8
+ });
src/server.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import app from './app';
2
+ import dotenv from 'dotenv';
3
+
4
+ dotenv.config();
5
+
6
+ const PORT = process.env.PORT || 5000;
7
+
8
+ app.listen(PORT, () => {
9
+ console.log(`[Server]: Server sedang berjalan di http://localhost:${PORT}`);
10
+ });
src/services/auth.service.ts ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import prisma from '../utils/db';
2
+ import bcrypt from 'bcryptjs';
3
+ import { createAccessToken, createRefreshToken, verifyToken } from '../utils/jwt';
4
+ import { catatAudit } from '../utils/audit';
5
+ import { APIError } from '../utils/errors';
6
+
7
+ export class AuthService {
8
+ static async login(params: {
9
+ username: string;
10
+ passwordSecret: string;
11
+ ip: string;
12
+ userAgent: string;
13
+ }) {
14
+ const { username, passwordSecret, ip, userAgent } = params;
15
+
16
+ // Search for user
17
+ const user = await prisma.user.findFirst({
18
+ where: {
19
+ username,
20
+ aktif: 1,
21
+ },
22
+ });
23
+
24
+ if (!user) {
25
+ // Log failed audit
26
+ await catatAudit({
27
+ aksi: 'login_gagal',
28
+ entitas: 'user',
29
+ ipAddress: ip,
30
+ userAgent: userAgent,
31
+ dataBaru: { username },
32
+ });
33
+ throw new APIError('Username atau password salah', 401);
34
+ }
35
+
36
+ const isMatch = await bcrypt.compare(passwordSecret, user.password_hash);
37
+ if (!isMatch) {
38
+ // Log failed audit
39
+ await catatAudit({
40
+ userId: user.id,
41
+ aksi: 'login_gagal',
42
+ entitas: 'user',
43
+ entitasId: user.id,
44
+ ipAddress: ip,
45
+ userAgent: userAgent,
46
+ dataBaru: { username },
47
+ });
48
+ throw new APIError('Username atau password salah', 401);
49
+ }
50
+
51
+ // Log successful login
52
+ await catatAudit({
53
+ userId: user.id,
54
+ aksi: 'login_sukses',
55
+ entitas: 'user',
56
+ entitasId: user.id,
57
+ ipAddress: ip,
58
+ userAgent: userAgent,
59
+ });
60
+
61
+ // Generate tokens
62
+ const userData = { sub: user.username, role: user.role };
63
+ const accessToken = createAccessToken(userData);
64
+ const refreshToken = createRefreshToken(userData);
65
+
66
+ return {
67
+ user,
68
+ accessToken,
69
+ refreshToken,
70
+ };
71
+ }
72
+
73
+ static async refresh(refreshToken: string) {
74
+ const payload = verifyToken(refreshToken);
75
+ if (!payload || payload.type !== 'refresh') {
76
+ throw new APIError('Refresh token tidak valid atau kedaluwarsa', 401);
77
+ }
78
+
79
+ const username = payload.sub;
80
+ const role = payload.role;
81
+
82
+ const user = await prisma.user.findFirst({
83
+ where: {
84
+ username,
85
+ aktif: 1,
86
+ },
87
+ });
88
+
89
+ if (!user) {
90
+ throw new APIError('Pengguna tidak aktif atau tidak ditemukan', 401);
91
+ }
92
+
93
+ const userData = { sub: username, role };
94
+ const newAccessToken = createAccessToken(userData);
95
+ const newRefreshToken = createRefreshToken(userData);
96
+
97
+ return {
98
+ role: user.role,
99
+ accessToken: newAccessToken,
100
+ refreshToken: newRefreshToken,
101
+ };
102
+ }
103
+
104
+ static async logout(userId: number, ip: string, userAgent: string) {
105
+ await catatAudit({
106
+ userId,
107
+ aksi: 'logout',
108
+ entitas: 'user',
109
+ entitasId: userId,
110
+ ipAddress: ip,
111
+ userAgent,
112
+ });
113
+ return true;
114
+ }
115
+ }
src/services/bon.service.ts ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import prisma from '../utils/db';
2
+ import { catatAudit } from '../utils/audit';
3
+ import { APIError } from '../utils/errors';
4
+
5
+ export class BonService {
6
+ static async getBon(skip = 0, limit = 10, statusFilter?: 'aktif' | 'lunas' | 'macet', memberId?: number) {
7
+ const where: any = {};
8
+ if (statusFilter) where.status = statusFilter;
9
+ if (memberId) where.member_id = memberId;
10
+
11
+ return await prisma.bon.findMany({
12
+ where,
13
+ orderBy: { created_at: 'desc' },
14
+ skip,
15
+ take: limit,
16
+ include: {
17
+ member: true,
18
+ transaksi: true,
19
+ },
20
+ });
21
+ }
22
+
23
+ static async getBonById(id: number) {
24
+ const bon = await prisma.bon.findUnique({
25
+ where: { id },
26
+ include: {
27
+ member: true,
28
+ transaksi: true,
29
+ bon_cicilan: true,
30
+ },
31
+ });
32
+ if (!bon) {
33
+ throw new APIError('Pencatatan kasbon tidak ditemukan', 404);
34
+ }
35
+ return bon;
36
+ }
37
+
38
+ static async bayarCicilan(params: {
39
+ bonId: number;
40
+ nominal: number;
41
+ metode: 'tunai' | 'transfer';
42
+ actorId: number;
43
+ ip: string;
44
+ userAgent: string;
45
+ }) {
46
+ const { bonId, nominal, metode, actorId, ip, userAgent } = params;
47
+
48
+ const bon = await this.getBonById(bonId);
49
+ if (bon.status === 'lunas') {
50
+ throw new APIError('Kasbon ini sudah lunas didebet sebelumnya', 400);
51
+ }
52
+
53
+ const sisaBon = Number(bon.sisa_bon);
54
+ if (nominal > sisaBon) {
55
+ throw new APIError(`Nominal pembayaran Rp ${nominal.toLocaleString('id-ID')} melebihi sisa tagihan Rp ${sisaBon.toLocaleString('id-ID')}`, 400);
56
+ }
57
+
58
+ // Pastikan kasir memiliki shift aktif
59
+ const shift = await prisma.shift.findFirst({
60
+ where: {
61
+ user_id: actorId,
62
+ status: 'buka',
63
+ },
64
+ });
65
+
66
+ if (!shift) {
67
+ throw new APIError('Shift kasir Anda belum dibuka. Harus membuka shift terlebih dahulu untuk menerima pembayaran.', 400);
68
+ }
69
+
70
+ // 1. Buat record cicilan
71
+ await prisma.bon_cicilan.create({
72
+ data: {
73
+ bon_id: bon.id,
74
+ nominal: nominal,
75
+ metode: metode,
76
+ user_id: actorId,
77
+ },
78
+ });
79
+
80
+ // 2. Kurangi sisa bon
81
+ const sisaLama = sisaBon;
82
+ const sisaBaru = sisaLama - nominal;
83
+ const statusBaru = sisaBaru <= 0 ? 'lunas' : bon.status;
84
+
85
+ await prisma.bon.update({
86
+ where: { id: bonId },
87
+ data: {
88
+ sisa_bon: sisaBaru,
89
+ status: statusBaru as any,
90
+ },
91
+ });
92
+
93
+ // 3. Kurangi total bon member
94
+ const totalBonMemberLama = Number(bon.member.total_bon);
95
+ const totalBonMemberBaru = Math.max(0, totalBonMemberLama - nominal);
96
+
97
+ await prisma.member.update({
98
+ where: { id: bon.member_id },
99
+ data: { total_bon: totalBonMemberBaru },
100
+ });
101
+
102
+ // 4. Masukkan ke kas shift
103
+ const updateShift: any = {};
104
+ if (metode === 'tunai') {
105
+ updateShift.total_tunai = Number(shift.total_tunai) + nominal;
106
+ } else if (metode === 'transfer') {
107
+ updateShift.total_transfer = Number(shift.total_transfer) + nominal;
108
+ }
109
+
110
+ await prisma.shift.update({
111
+ where: { id: shift.id },
112
+ data: updateShift,
113
+ });
114
+
115
+ // 5. Audit log
116
+ await catatAudit({
117
+ userId: actorId,
118
+ aksi: 'update',
119
+ entitas: 'bon',
120
+ entitasId: bon.id,
121
+ dataLama: { sisa_bon: sisaLama, status: bon.status },
122
+ dataBaru: { sisa_bon: sisaBaru, status: statusBaru, pembayaran_cicilan: nominal },
123
+ ipAddress: ip,
124
+ userAgent,
125
+ });
126
+
127
+ return await this.getBonById(bonId);
128
+ }
129
+
130
+ static async kirimReminderWA(bonId: number) {
131
+ const bon = await prisma.bon.findUnique({
132
+ where: { id: bonId },
133
+ include: { member: true },
134
+ });
135
+
136
+ if (!bon || !bon.member) {
137
+ throw new APIError('Gagal membuat pengingat WhatsApp. Data member tidak ditemukan.', 400);
138
+ }
139
+
140
+ const { nama, nomor_hp } = bon.member;
141
+ if (!nomor_hp) {
142
+ throw new APIError('Gagal membuat pengingat WhatsApp. Nomor HP member tidak terisi.', 400);
143
+ }
144
+
145
+ // Bersihkan nomor HP
146
+ let cleanedNo = nomor_hp.trim();
147
+ if (cleanedNo.startsWith('0')) {
148
+ cleanedNo = '62' + cleanedNo.slice(1);
149
+ } else if (cleanedNo.startsWith('+')) {
150
+ cleanedNo = cleanedNo.slice(1);
151
+ }
152
+
153
+ // Format pesan
154
+ const jatuhTempoStr = bon.jatuh_tempo
155
+ ? new Date(bon.jatuh_tempo).toLocaleDateString('id-ID', { day: '2-digit', month: '2-digit', year: 'numeric' })
156
+ : 'secepatnya';
157
+ const sisaRupiah = Math.floor(Number(bon.sisa_bon)).toLocaleString('id-ID').replace(/,/g, '.');
158
+
159
+ const pesan =
160
+ `Halo *${nama}*,\n\n` +
161
+ `Kami dari *Tokiva POS* ingin mengingatkan bahwa Anda memiliki tagihan kasbon ` +
162
+ `sebesar *Rp ${sisaRupiah}* yang jatuh tempo pada tanggal *{jatuh_tempo_str}*.\n\n` +
163
+ `Silakan melakukan pembayaran cicilan atau pelunasan melalui kasir Tokiva. ` +
164
+ `Terima kasih atas kerjasamanya!`;
165
+ const formatPesan = pesan.replace('{jatuh_tempo_str}', jatuhTempoStr);
166
+
167
+ const encodedMessage = encodeURIComponent(formatPesan);
168
+ const waLink = `https://wa.me/${cleanedNo}?text=${encodedMessage}`;
169
+
170
+ // Tandai reminder terkirim
171
+ await prisma.bon.update({
172
+ where: { id: bonId },
173
+ data: { reminder_sent: 1 },
174
+ });
175
+
176
+ // Catat notifikasi
177
+ await prisma.notifikasi.create({
178
+ data: {
179
+ tipe: 'bon_jatuh_tempo',
180
+ judul: `WhatsApp Tagihan Dikirim: ${nama}`,
181
+ pesan: `Link WhatsApp pengingat tagihan Rp ${sisaRupiah} untuk ${nama} telah dibuat.`,
182
+ referensi_id: bon.id,
183
+ },
184
+ });
185
+
186
+ return waLink;
187
+ }
188
+ }
src/services/diskon.service.ts ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import prisma from '../utils/db';
2
+ import { APIError } from '../utils/errors';
3
+
4
+ export class DiskonService {
5
+ static async getActiveDiscounts(isMember: boolean) {
6
+ const now = new Date();
7
+
8
+ // Filter diskon yang aktif dan periode tanggalnya cocok
9
+ const discounts = await prisma.diskon.findMany({
10
+ where: {
11
+ is_aktif: 1,
12
+ tgl_mulai: { lte: now },
13
+ tgl_selesai: { gte: now },
14
+ ...(isMember ? {} : { member_only: 0 }),
15
+ },
16
+ });
17
+
18
+ const currentTimeString = now.toTimeString().split(' ')[0]; // "HH:MM:SS"
19
+
20
+ const activeDiscounts = discounts.filter((d) => {
21
+ if (d.tipe === 'flash_sale') {
22
+ if (d.jam_mulai && d.jam_selesai) {
23
+ const jamMulai = new Date(d.jam_mulai).toTimeString().split(' ')[0];
24
+ const jamSelesai = new Date(d.jam_selesai).toTimeString().split(' ')[0];
25
+ return currentTimeString >= jamMulai && currentTimeString <= jamSelesai;
26
+ }
27
+ return true;
28
+ }
29
+ return true;
30
+ });
31
+
32
+ return activeDiscounts;
33
+ }
34
+
35
+ static async hitungDiskonDanHarga(items: Array<{ produk_id: number; qty: number }>, memberId?: number | null) {
36
+ const isMember = memberId !== undefined && memberId !== null;
37
+ const activePromos = await this.getActiveDiscounts(isMember);
38
+
39
+ // 1. Hitung harga dasar bertingkat per item (volume pricing)
40
+ const cartItems: any[] = [];
41
+ let subtotalDasar = 0;
42
+
43
+ for (const item of items) {
44
+ const produkId = item.produk_id;
45
+ const qty = item.qty;
46
+
47
+ const produk = await prisma.produk.findUnique({
48
+ where: { id: produkId },
49
+ include: { harga_tingkat: true },
50
+ });
51
+
52
+ if (!produk) {
53
+ throw new APIError(`Produk ID ${produkId} tidak ditemukan.`, 404);
54
+ }
55
+
56
+ // Tentukan harga dasar berdasarkan volume pricing (harga_tingkat)
57
+ let hargaSatuan = Number(produk.harga_jual); // Default ecer
58
+
59
+ if (produk.harga_tingkat && produk.harga_tingkat.length > 0) {
60
+ // Urutkan berdasarkan min_qty desc untuk mencari batas tertinggi yang terpenuhi
61
+ const sortedTingkat = [...produk.harga_tingkat].sort((a, b) => Number(b.min_qty) - Number(a.min_qty));
62
+ for (const ht of sortedTingkat) {
63
+ if (qty >= Number(ht.min_qty)) {
64
+ hargaSatuan = Number(ht.harga);
65
+ break;
66
+ }
67
+ }
68
+ }
69
+
70
+ const itemSubtotal = qty * hargaSatuan;
71
+ subtotalDasar += itemSubtotal;
72
+
73
+ cartItems.push({
74
+ produk,
75
+ qty,
76
+ harga_original: hargaSatuan,
77
+ diskon_item: 0,
78
+ diskon_id: null,
79
+ subtotal: itemSubtotal,
80
+ });
81
+ }
82
+
83
+ // 2. Terapkan diskon per item (per_item_persen, per_item_nominal, flash_sale)
84
+ for (const item of cartItems) {
85
+ const produk = item.produk;
86
+ const qty = item.qty;
87
+ const hargaOriginal = item.harga_original;
88
+
89
+ let bestDiscount = 0;
90
+ let bestDiskonId: number | null = null;
91
+
92
+ for (const promo of activePromos) {
93
+ // Cek apakah promo berlaku untuk produk ini
94
+ let isApplicable = false;
95
+ if (!promo.produk_ids) {
96
+ // Berlaku untuk semua produk
97
+ isApplicable = true;
98
+ } else {
99
+ try {
100
+ let pIds: number[] = [];
101
+ if (typeof promo.produk_ids === 'string') {
102
+ pIds = JSON.parse(promo.produk_ids);
103
+ } else if (Array.isArray(promo.produk_ids)) {
104
+ pIds = promo.produk_ids as number[];
105
+ }
106
+ if (pIds.includes(produk.id)) {
107
+ isApplicable = true;
108
+ }
109
+ } catch (e) {
110
+ isApplicable = false;
111
+ }
112
+ }
113
+
114
+ if (!isApplicable) continue;
115
+
116
+ // Hitung nilai diskon
117
+ let currentDiscount = 0;
118
+ if (promo.tipe === 'per_item_persen' || promo.tipe === 'flash_sale') {
119
+ currentDiscount = hargaOriginal * (Number(promo.nilai) / 100) * qty;
120
+ } else if (promo.tipe === 'per_item_nominal') {
121
+ currentDiscount = Number(promo.nilai) * qty;
122
+ }
123
+
124
+ if (currentDiscount > bestDiscount) {
125
+ bestDiscount = currentDiscount;
126
+ bestDiskonId = promo.id;
127
+ }
128
+ }
129
+
130
+ if (bestDiscount > 0) {
131
+ item.diskon_item = bestDiscount;
132
+ item.diskon_id = bestDiskonId;
133
+ item.subtotal = (hargaOriginal * qty) - bestDiscount;
134
+ }
135
+ }
136
+
137
+ // Kalkulasi subtotal setelah item-level discounts
138
+ let subtotalSetelahItem = cartItems.reduce((acc, item) => acc + item.subtotal, 0);
139
+ const totalDiskonItem = cartItems.reduce((acc, item) => acc + item.diskon_item, 0);
140
+
141
+ // 3. Terapkan diskon bundling (paket promo)
142
+ let totalDiskonBundling = 0;
143
+ const bundlingPromos = activePromos.filter((p) => p.tipe === 'bundling');
144
+
145
+ for (const promo of bundlingPromos) {
146
+ const bundleItems = await prisma.diskon_bundling.findMany({
147
+ where: { diskon_id: promo.id },
148
+ });
149
+ if (bundleItems.length === 0) continue;
150
+
151
+ // Hitung berapa banyak paket bundle yang bisa dibuat dari keranjang
152
+ let maxPossibleBundles = 999999;
153
+ for (const bItem of bundleItems) {
154
+ const matchingCartItem = cartItems.find((item) => item.produk.id === bItem.produk_id);
155
+ if (!matchingCartItem) {
156
+ maxPossibleBundles = 0;
157
+ break;
158
+ }
159
+ const availableMultiples = Math.floor(matchingCartItem.qty / Number(bItem.qty));
160
+ if (availableMultiples < maxPossibleBundles) {
161
+ maxPossibleBundles = availableMultiples;
162
+ }
163
+ }
164
+
165
+ if (maxPossibleBundles > 0) {
166
+ const diskonBundleTotal = Number(promo.nilai) * maxPossibleBundles;
167
+ totalDiskonBundling += diskonBundleTotal;
168
+ subtotalSetelahItem -= diskonBundleTotal;
169
+ }
170
+ }
171
+
172
+ // 4. Terapkan diskon tingkat transaksi (total_persen, total_nominal, member)
173
+ const transactionPromos = activePromos.filter((p) =>
174
+ ['total_persen', 'total_nominal', 'member'].includes(p.tipe)
175
+ );
176
+
177
+ let bestTransDiscount = 0;
178
+ for (const promo of transactionPromos) {
179
+ // Cek minimal belanja
180
+ if (subtotalSetelahItem < Number(promo.min_belanja)) {
181
+ continue;
182
+ }
183
+
184
+ let currentTransDiscount = 0;
185
+ if (promo.tipe === 'total_persen' || promo.tipe === 'member') {
186
+ currentTransDiscount = subtotalSetelahItem * (Number(promo.nilai) / 100);
187
+ } else if (promo.tipe === 'total_nominal') {
188
+ currentTransDiscount = Number(promo.nilai);
189
+ }
190
+
191
+ if (currentTransDiscount > bestTransDiscount) {
192
+ bestTransDiscount = currentTransDiscount;
193
+ }
194
+ }
195
+
196
+ const totalDiskonTransaksi = bestTransDiscount;
197
+
198
+ // Hitung total akhir
199
+ let totalAkhir = subtotalSetelahItem - totalDiskonTransaksi;
200
+ if (totalAkhir < 0) {
201
+ totalAkhir = 0;
202
+ }
203
+
204
+ return {
205
+ cart_items: cartItems,
206
+ subtotal_dasar: subtotalDasar,
207
+ total_diskon_item: totalDiskonItem,
208
+ total_diskon_bundling: totalDiskonBundling,
209
+ total_diskon_transaksi: totalDiskonTransaksi,
210
+ total_diskon_keseluruhan: totalDiskonItem + totalDiskonBundling + totalDiskonTransaksi,
211
+ total_akhir: totalAkhir,
212
+ };
213
+ }
214
+ }
src/services/kategori.service.ts ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import prisma from '../utils/db';
2
+ import { catatAudit } from '../utils/audit';
3
+ import { moveToTrash } from '../utils/trash';
4
+ import { APIError } from '../utils/errors';
5
+
6
+ export class KategoriService {
7
+ static async getKategori(skip = 0, limit = 100, pencarian?: string) {
8
+ const where: any = {
9
+ deleted_at: null,
10
+ };
11
+
12
+ if (pencarian) {
13
+ where.nama = {
14
+ contains: pencarian,
15
+ };
16
+ }
17
+
18
+ return await prisma.kategori.findMany({
19
+ where,
20
+ skip,
21
+ take: limit,
22
+ });
23
+ }
24
+
25
+ static async getKategoriById(id: number) {
26
+ const kategori = await prisma.kategori.findFirst({
27
+ where: { id, deleted_at: null },
28
+ });
29
+ if (!kategori) {
30
+ throw new APIError('Kategori tidak ditemukan', 404);
31
+ }
32
+ return kategori;
33
+ }
34
+
35
+ static async create(data: any, actorId: number, ip: string, userAgent: string) {
36
+ // Check duplicate
37
+ const existing = await prisma.kategori.findFirst({
38
+ where: { nama: data.nama },
39
+ });
40
+
41
+ if (existing) {
42
+ if (existing.deleted_at !== null || existing.aktif === 0) {
43
+ // Reactivate
44
+ const reactivated = await prisma.kategori.update({
45
+ where: { id: existing.id },
46
+ data: {
47
+ deleted_at: null,
48
+ aktif: 1,
49
+ icon: data.icon ?? existing.icon,
50
+ },
51
+ });
52
+ return reactivated;
53
+ }
54
+ throw new APIError('Nama kategori sudah digunakan', 400);
55
+ }
56
+
57
+ const kategori = await prisma.kategori.create({
58
+ data: {
59
+ nama: data.nama,
60
+ icon: data.icon ?? null,
61
+ aktif: data.aktif ?? 1,
62
+ },
63
+ });
64
+
65
+ await catatAudit({
66
+ userId: actorId,
67
+ aksi: 'create',
68
+ entitas: 'kategori',
69
+ entitasId: kategori.id,
70
+ dataBaru: { nama: kategori.nama, icon: kategori.icon },
71
+ ipAddress: ip,
72
+ userAgent,
73
+ });
74
+
75
+ return kategori;
76
+ }
77
+
78
+ static async update(id: number, data: any, actorId: number, ip: string, userAgent: string) {
79
+ const kategori = await this.getKategoriById(id);
80
+
81
+ const dataLama = { nama: kategori.nama, icon: kategori.icon, aktif: kategori.aktif };
82
+
83
+ if (data.nama !== undefined) {
84
+ const dup = await prisma.kategori.findFirst({
85
+ where: { nama: data.nama, id: { not: id } },
86
+ });
87
+ if (dup) {
88
+ throw new APIError('Nama kategori sudah digunakan oleh kategori lain', 400);
89
+ }
90
+ }
91
+
92
+ const updateData: any = {};
93
+ if (data.nama !== undefined) updateData.nama = data.nama;
94
+ if (data.icon !== undefined) updateData.icon = data.icon;
95
+ if (data.aktif !== undefined) updateData.aktif = data.aktif;
96
+
97
+ const updated = await prisma.kategori.update({
98
+ where: { id },
99
+ data: updateData,
100
+ });
101
+
102
+ const dataBaru = { nama: updated.nama, icon: updated.icon, aktif: updated.aktif };
103
+
104
+ await catatAudit({
105
+ userId: actorId,
106
+ aksi: 'update',
107
+ entitas: 'kategori',
108
+ entitasId: updated.id,
109
+ dataLama,
110
+ dataBaru,
111
+ ipAddress: ip,
112
+ userAgent,
113
+ });
114
+
115
+ return updated;
116
+ }
117
+
118
+ static async delete(id: number, actorId: number, ip: string, userAgent: string) {
119
+ const kategori = await this.getKategoriById(id);
120
+
121
+ // Check if active products reference this category
122
+ const productCount = await prisma.produk.count({
123
+ where: { kategori_id: id, deleted_at: null },
124
+ });
125
+
126
+ if (productCount > 0) {
127
+ throw new APIError(`Tidak dapat menghapus kategori. Masih digunakan oleh ${productCount} produk.`, 400);
128
+ }
129
+
130
+ await moveToTrash({
131
+ instance: kategori,
132
+ entityType: 'kategori',
133
+ actorId,
134
+ });
135
+
136
+ await catatAudit({
137
+ userId: actorId,
138
+ aksi: 'delete',
139
+ entitas: 'kategori',
140
+ entitasId: kategori.id,
141
+ dataLama: { nama: kategori.nama, aktif: 1 },
142
+ dataBaru: { nama: kategori.nama, aktif: 0 },
143
+ ipAddress: ip,
144
+ userAgent,
145
+ });
146
+
147
+ return true;
148
+ }
149
+ }
src/services/member.service.ts ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import prisma from '../utils/db';
2
+ import { catatAudit } from '../utils/audit';
3
+ import { moveToTrash } from '../utils/trash';
4
+ import { APIError } from '../utils/errors';
5
+
6
+ export class MemberService {
7
+ static async getMembers(skip = 0, limit = 10, pencarian?: string) {
8
+ const where: any = {
9
+ deleted_at: null,
10
+ };
11
+
12
+ if (pencarian) {
13
+ where.OR = [
14
+ { nama: { contains: pencarian } },
15
+ { kode: { contains: pencarian } },
16
+ { nomor_hp: { contains: pencarian } },
17
+ ];
18
+ }
19
+
20
+ return await prisma.member.findMany({
21
+ where,
22
+ skip,
23
+ take: limit,
24
+ });
25
+ }
26
+
27
+ static async getMemberById(id: number) {
28
+ const member = await prisma.member.findFirst({
29
+ where: { id, deleted_at: null },
30
+ });
31
+ if (!member) {
32
+ throw new APIError('Member tidak ditemukan', 404);
33
+ }
34
+ return member;
35
+ }
36
+
37
+ static async create(data: any, actorId: number, ip: string, userAgent: string) {
38
+ const existingPhone = await prisma.member.findFirst({
39
+ where: { nomor_hp: data.nomor_hp },
40
+ });
41
+ const existingCode = await prisma.member.findUnique({
42
+ where: { kode: data.kode },
43
+ });
44
+
45
+ const existing = existingPhone || existingCode;
46
+ if (existing) {
47
+ if (existing.deleted_at === null) {
48
+ const fieldType = existing.id === existingPhone?.id ? 'Nomor HP' : 'Kode';
49
+ const fieldVal = existing.id === existingPhone?.id ? data.nomor_hp : data.kode;
50
+ throw new APIError(`${fieldType} member '${fieldVal}' sudah terdaftar/digunakan`, 400);
51
+ } else {
52
+ // Soft deleted duplicate. Reactivate and update!
53
+ const restored = await prisma.member.update({
54
+ where: { id: existing.id },
55
+ data: {
56
+ deleted_at: null,
57
+ aktif: 1,
58
+ kode: data.kode,
59
+ nama: data.nama,
60
+ nomor_hp: data.nomor_hp,
61
+ alamat: data.alamat ?? null,
62
+ limit_bon: data.limit_bon ?? 0,
63
+ foto_url: data.foto_url ?? null,
64
+ },
65
+ });
66
+
67
+ await catatAudit({
68
+ userId: actorId,
69
+ aksi: 'restore',
70
+ entitas: 'member',
71
+ entitasId: restored.id,
72
+ dataBaru: { kode: restored.kode, nama: restored.nama, nomor_hp: restored.nomor_hp, limit_bon: Number(restored.limit_bon) },
73
+ ipAddress: ip,
74
+ userAgent,
75
+ });
76
+
77
+ return restored;
78
+ }
79
+ }
80
+
81
+ const member = await prisma.member.create({
82
+ data: {
83
+ kode: data.kode,
84
+ nama: data.nama,
85
+ nomor_hp: data.nomor_hp,
86
+ alamat: data.alamat ?? null,
87
+ limit_bon: data.limit_bon ?? 0,
88
+ total_bon: 0,
89
+ foto_url: data.foto_url ?? null,
90
+ aktif: data.aktif ?? 1,
91
+ },
92
+ });
93
+
94
+ await catatAudit({
95
+ userId: actorId,
96
+ aksi: 'create',
97
+ entitas: 'member',
98
+ entitasId: member.id,
99
+ dataBaru: { kode: member.kode, nama: member.nama, nomor_hp: member.nomor_hp, limit_bon: Number(member.limit_bon) },
100
+ ipAddress: ip,
101
+ userAgent,
102
+ });
103
+
104
+ return member;
105
+ }
106
+
107
+ static async update(id: number, data: any, actorId: number, ip: string, userAgent: string) {
108
+ const member = await this.getMemberById(id);
109
+
110
+ const dataLama = {
111
+ nama: member.nama,
112
+ nomor_hp: member.nomor_hp,
113
+ alamat: member.alamat,
114
+ total_poin: member.total_poin,
115
+ limit_bon: Number(member.limit_bon),
116
+ total_bon: Number(member.total_bon),
117
+ foto_url: member.foto_url,
118
+ aktif: member.aktif,
119
+ };
120
+
121
+ if (data.nomor_hp !== undefined) {
122
+ const dup = await prisma.member.findFirst({
123
+ where: { nomor_hp: data.nomor_hp, id: { not: id } },
124
+ });
125
+ if (dup) {
126
+ throw new APIError('Nomor HP tersebut sudah digunakan oleh member lain', 400);
127
+ }
128
+ }
129
+
130
+ const updateData: any = {};
131
+ if (data.nama !== undefined) updateData.nama = data.nama;
132
+ if (data.nomor_hp !== undefined) updateData.nomor_hp = data.nomor_hp;
133
+ if (data.alamat !== undefined) updateData.alamat = data.alamat;
134
+ if (data.total_poin !== undefined) updateData.total_poin = data.total_poin;
135
+ if (data.limit_bon !== undefined) updateData.limit_bon = data.limit_bon;
136
+ if (data.total_bon !== undefined) updateData.total_bon = data.total_bon;
137
+ if (data.foto_url !== undefined) updateData.foto_url = data.foto_url;
138
+ if (data.aktif !== undefined) updateData.aktif = data.aktif;
139
+
140
+ const updated = await prisma.member.update({
141
+ where: { id },
142
+ data: updateData,
143
+ });
144
+
145
+ const dataBaru = {
146
+ nama: updated.nama,
147
+ nomor_hp: updated.nomor_hp,
148
+ alamat: updated.alamat,
149
+ total_poin: updated.total_poin,
150
+ limit_bon: Number(updated.limit_bon),
151
+ total_bon: Number(updated.total_bon),
152
+ foto_url: updated.foto_url,
153
+ aktif: updated.aktif,
154
+ };
155
+
156
+ await catatAudit({
157
+ userId: actorId,
158
+ aksi: 'update',
159
+ entitas: 'member',
160
+ entitasId: updated.id,
161
+ dataLama,
162
+ dataBaru,
163
+ ipAddress: ip,
164
+ userAgent,
165
+ });
166
+
167
+ return updated;
168
+ }
169
+
170
+ static async delete(id: number, actorId: number, ip: string, userAgent: string) {
171
+ const member = await this.getMemberById(id);
172
+
173
+ if (Number(member.total_bon) > 0) {
174
+ throw new APIError(`Tidak dapat menghapus member. Masih memiliki tanggungan kasbon sebesar Rp ${Number(member.total_bon).toLocaleString('id-ID')}`, 400);
175
+ }
176
+
177
+ await moveToTrash({
178
+ instance: member,
179
+ entityType: 'member',
180
+ actorId,
181
+ });
182
+
183
+ await catatAudit({
184
+ userId: actorId,
185
+ aksi: 'delete',
186
+ entitas: 'member',
187
+ entitasId: member.id,
188
+ dataLama: { kode: member.kode, nama: member.nama, aktif: 1 },
189
+ dataBaru: { kode: member.kode, nama: member.nama, aktif: 0 },
190
+ ipAddress: ip,
191
+ userAgent,
192
+ });
193
+
194
+ return true;
195
+ }
196
+ }