eid-salami 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/DEPLOY.md +199 -0
  2. package/README.md +133 -0
  3. package/backend/.env.example +25 -0
  4. package/backend/.keystone_temp/admin/next.config.js +14 -0
  5. package/backend/.keystone_temp/admin/pages/_app.js +22 -0
  6. package/backend/.keystone_temp/admin/pages/index.js +1 -0
  7. package/backend/.keystone_temp/admin/pages/init.js +5 -0
  8. package/backend/.keystone_temp/admin/pages/no-access.js +3 -0
  9. package/backend/.keystone_temp/admin/pages/orders/[id].js +3 -0
  10. package/backend/.keystone_temp/admin/pages/orders/create.js +3 -0
  11. package/backend/.keystone_temp/admin/pages/orders/index.js +3 -0
  12. package/backend/.keystone_temp/admin/pages/salami-packages/[id].js +3 -0
  13. package/backend/.keystone_temp/admin/pages/salami-packages/create.js +3 -0
  14. package/backend/.keystone_temp/admin/pages/salami-packages/index.js +3 -0
  15. package/backend/.keystone_temp/admin/pages/signin.js +3 -0
  16. package/backend/.keystone_temp/admin/pages/users/[id].js +3 -0
  17. package/backend/.keystone_temp/admin/pages/users/create.js +3 -0
  18. package/backend/.keystone_temp/admin/pages/users/index.js +3 -0
  19. package/backend/.keystone_temp/admin/public/favicon.ico +0 -0
  20. package/backend/.keystone_temp/config.js +369 -0
  21. package/backend/.keystone_temp/config.js.map +7 -0
  22. package/backend/bkash/bkash.service.ts +184 -0
  23. package/backend/keystone/routes.ts +193 -0
  24. package/backend/keystone/schema.ts +143 -0
  25. package/backend/keystone.ts +54 -0
  26. package/backend/package.json +23 -0
  27. package/backend/schema.graphql +530 -0
  28. package/backend/schema.prisma +55 -0
  29. package/backend/seed.ts +53 -0
  30. package/backend/tsconfig.json +15 -0
  31. package/frontend/.env.example +6 -0
  32. package/frontend/index.html +16 -0
  33. package/frontend/package.json +24 -0
  34. package/frontend/src/App.js +11 -0
  35. package/frontend/src/App.tsx +22 -0
  36. package/frontend/src/api/client.js +11 -0
  37. package/frontend/src/api/client.ts +57 -0
  38. package/frontend/src/components/Footer.js +5 -0
  39. package/frontend/src/components/Footer.module.css +27 -0
  40. package/frontend/src/components/Footer.tsx +13 -0
  41. package/frontend/src/components/Navbar.js +6 -0
  42. package/frontend/src/components/Navbar.module.css +54 -0
  43. package/frontend/src/components/Navbar.tsx +16 -0
  44. package/frontend/src/components/PackageCard.js +5 -0
  45. package/frontend/src/components/PackageCard.module.css +64 -0
  46. package/frontend/src/components/PackageCard.tsx +24 -0
  47. package/frontend/src/main.js +7 -0
  48. package/frontend/src/main.tsx +13 -0
  49. package/frontend/src/pages/HomePage.js +31 -0
  50. package/frontend/src/pages/HomePage.module.css +416 -0
  51. package/frontend/src/pages/HomePage.tsx +146 -0
  52. package/frontend/src/pages/OrderPage.js +98 -0
  53. package/frontend/src/pages/OrderPage.module.css +624 -0
  54. package/frontend/src/pages/OrderPage.tsx +221 -0
  55. package/frontend/src/pages/PaymentCallbackPage.js +25 -0
  56. package/frontend/src/pages/PaymentCallbackPage.module.css +38 -0
  57. package/frontend/src/pages/PaymentCallbackPage.tsx +37 -0
  58. package/frontend/src/pages/PaymentResultPage.js +28 -0
  59. package/frontend/src/pages/PaymentResultPage.module.css +182 -0
  60. package/frontend/src/pages/PaymentResultPage.tsx +92 -0
  61. package/frontend/src/styles/global.css +66 -0
  62. package/frontend/src/vite-env.d.ts +5 -0
  63. package/frontend/tsconfig.json +15 -0
  64. package/frontend/vite.config.ts +15 -0
  65. package/package.json +14 -0
@@ -0,0 +1,369 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // keystone.ts
21
+ var keystone_exports = {};
22
+ __export(keystone_exports, {
23
+ default: () => keystone_default
24
+ });
25
+ module.exports = __toCommonJS(keystone_exports);
26
+ var import_config = require("dotenv/config");
27
+ var import_core2 = require("@keystone-6/core");
28
+ var import_session = require("@keystone-6/core/session");
29
+ var import_auth = require("@keystone-6/auth");
30
+
31
+ // keystone/schema.ts
32
+ var import_core = require("@keystone-6/core");
33
+ var import_access = require("@keystone-6/core/access");
34
+ var import_fields = require("@keystone-6/core/fields");
35
+ var import_fields_document = require("@keystone-6/fields-document");
36
+ var isAdmin = ({ session: session2 }) => session2?.data?.isAdmin === true;
37
+ var lists = {
38
+ // ── Admin User ──────────────────────────────────────────────────────────────
39
+ User: (0, import_core.list)({
40
+ access: {
41
+ operation: {
42
+ query: isAdmin,
43
+ create: isAdmin,
44
+ update: isAdmin,
45
+ delete: isAdmin
46
+ }
47
+ },
48
+ fields: {
49
+ name: (0, import_fields.text)({ validation: { isRequired: true } }),
50
+ email: (0, import_fields.text)({ validation: { isRequired: true }, isIndexed: "unique" }),
51
+ password: (0, import_fields.password)({ validation: { isRequired: true } }),
52
+ isAdmin: (0, import_fields.checkbox)({ defaultValue: false }),
53
+ createdAt: (0, import_fields.timestamp)({ defaultValue: { kind: "now" } })
54
+ },
55
+ ui: {
56
+ listView: { initialColumns: ["name", "email", "isAdmin", "createdAt"] }
57
+ }
58
+ }),
59
+ // ── Salami Package ──────────────────────────────────────────────────────────
60
+ SalamiPackage: (0, import_core.list)({
61
+ access: {
62
+ operation: {
63
+ query: import_access.allowAll,
64
+ // public can browse
65
+ create: isAdmin,
66
+ update: isAdmin,
67
+ delete: isAdmin
68
+ }
69
+ },
70
+ fields: {
71
+ name: (0, import_fields.text)({ validation: { isRequired: true } }),
72
+ slug: (0, import_fields.text)({ validation: { isRequired: true }, isIndexed: "unique" }),
73
+ description: (0, import_fields_document.document)({ formatting: true, dividers: true, links: true }),
74
+ amount: (0, import_fields.float)({ validation: { isRequired: true }, label: "Amount (BDT)" }),
75
+ isActive: (0, import_fields.checkbox)({ defaultValue: true }),
76
+ emoji: (0, import_fields.text)({ defaultValue: "\u{1F49A}" }),
77
+ sortOrder: (0, import_fields.integer)({ defaultValue: 0 }),
78
+ createdAt: (0, import_fields.timestamp)({ defaultValue: { kind: "now" } }),
79
+ orders: (0, import_fields.relationship)({ ref: "Order.package", many: true })
80
+ },
81
+ ui: {
82
+ listView: { initialColumns: ["name", "amount", "isActive", "sortOrder"] }
83
+ }
84
+ }),
85
+ // ── Order ───────────────────────────────────────────────────────────────────
86
+ Order: (0, import_core.list)({
87
+ access: {
88
+ operation: {
89
+ query: isAdmin,
90
+ create: import_access.allowAll,
91
+ // anyone can place an order
92
+ update: isAdmin,
93
+ delete: isAdmin
94
+ }
95
+ },
96
+ fields: {
97
+ // Sender
98
+ senderName: (0, import_fields.text)({ validation: { isRequired: true } }),
99
+ senderPhone: (0, import_fields.text)({ validation: { isRequired: true } }),
100
+ senderMessage: (0, import_fields.text)({ ui: { displayMode: "textarea" } }),
101
+ // Recipient
102
+ recipientName: (0, import_fields.text)(),
103
+ recipientPhone: (0, import_fields.text)(),
104
+ // Package
105
+ package: (0, import_fields.relationship)({ ref: "SalamiPackage.orders", many: false }),
106
+ amount: (0, import_fields.float)({ validation: { isRequired: true } }),
107
+ // Payment — manual bKash personal flow
108
+ paymentStatus: (0, import_fields.select)({
109
+ options: [
110
+ { label: "\u23F3 Pending", value: "pending" },
111
+ { label: "\u{1F50D} Pending Verification", value: "pending_verification" },
112
+ { label: "\u2705 Completed", value: "completed" },
113
+ { label: "\u274C Failed", value: "failed" },
114
+ { label: "\u21A9\uFE0F Refunded", value: "refunded" }
115
+ ],
116
+ defaultValue: "pending",
117
+ ui: { displayMode: "segmented-control" }
118
+ }),
119
+ // TrxID submitted by user after sending bKash — you verify manually
120
+ bkashTrxID: (0, import_fields.text)({ isIndexed: true, label: "bKash TrxID (user submitted)" }),
121
+ adminNote: (0, import_fields.text)({ label: "Admin Note", ui: { displayMode: "textarea" } }),
122
+ // Delivery
123
+ deliveryStatus: (0, import_fields.select)({
124
+ options: [
125
+ { label: "\u23F3 Waiting", value: "waiting" },
126
+ { label: "\u{1F4E4} Sent", value: "sent" },
127
+ { label: "\u2705 Delivered", value: "delivered" }
128
+ ],
129
+ defaultValue: "waiting",
130
+ ui: { displayMode: "segmented-control" }
131
+ }),
132
+ createdAt: (0, import_fields.timestamp)({ defaultValue: { kind: "now" } }),
133
+ updatedAt: (0, import_fields.timestamp)({ db: { updatedAt: true } })
134
+ },
135
+ ui: {
136
+ listView: {
137
+ initialColumns: [
138
+ "senderName",
139
+ "recipientName",
140
+ "amount",
141
+ "bkashTrxID",
142
+ "paymentStatus",
143
+ "deliveryStatus",
144
+ "createdAt"
145
+ ]
146
+ }
147
+ },
148
+ hooks: {
149
+ // Auto-fill amount from linked package
150
+ resolveInput: async ({ resolvedData, context }) => {
151
+ if (resolvedData.package?.connect?.id && !resolvedData.amount) {
152
+ const pkg = await context.query.SalamiPackage.findOne({
153
+ where: { id: resolvedData.package.connect.id },
154
+ query: "amount"
155
+ });
156
+ if (pkg)
157
+ resolvedData.amount = pkg.amount;
158
+ }
159
+ return resolvedData;
160
+ }
161
+ }
162
+ })
163
+ };
164
+
165
+ // keystone/routes.ts
166
+ async function parseBody(req) {
167
+ return new Promise((resolve, reject) => {
168
+ let data = "";
169
+ req.on("data", (chunk) => data += chunk);
170
+ req.on("end", () => {
171
+ try {
172
+ resolve(JSON.parse(data || "{}"));
173
+ } catch {
174
+ resolve({});
175
+ }
176
+ });
177
+ req.on("error", reject);
178
+ });
179
+ }
180
+ function json(res, status, body) {
181
+ res.writeHead(status, {
182
+ "Content-Type": "application/json",
183
+ "Access-Control-Allow-Origin": process.env.FRONTEND_URL ?? "*",
184
+ "Access-Control-Allow-Methods": "GET,POST,OPTIONS",
185
+ "Access-Control-Allow-Headers": "Content-Type"
186
+ });
187
+ res.end(JSON.stringify(body));
188
+ }
189
+ function isValidTrxID(trxID) {
190
+ return /^[A-Z0-9]{8,12}$/.test(trxID.trim().toUpperCase());
191
+ }
192
+ function makeCustomRoutes(app, commonMiddleware, keystoneContext) {
193
+ app.use((req, res, next) => {
194
+ if (req.method === "OPTIONS") {
195
+ res.writeHead(204, {
196
+ "Access-Control-Allow-Origin": process.env.FRONTEND_URL ?? "*",
197
+ "Access-Control-Allow-Methods": "GET,POST,OPTIONS",
198
+ "Access-Control-Allow-Headers": "Content-Type"
199
+ });
200
+ res.end();
201
+ return;
202
+ }
203
+ next();
204
+ });
205
+ app.use("/api/orders/create", commonMiddleware, async (req, res) => {
206
+ if (req.method !== "POST")
207
+ return json(res, 405, { error: "Method not allowed" });
208
+ try {
209
+ const body = await parseBody(req);
210
+ const { senderName, senderPhone, senderMessage, recipientName, recipientPhone, packageId } = body;
211
+ if (!senderName || !senderPhone || !packageId) {
212
+ return json(res, 400, { error: "Missing required fields" });
213
+ }
214
+ const pkg = await keystoneContext.sudo().query.SalamiPackage.findOne({
215
+ where: { id: packageId },
216
+ query: "id name amount isActive"
217
+ });
218
+ if (!pkg || !pkg.isActive) {
219
+ return json(res, 404, { error: "Package not found or inactive" });
220
+ }
221
+ const order = await keystoneContext.sudo().query.Order.createOne({
222
+ data: {
223
+ senderName,
224
+ senderPhone,
225
+ senderMessage: senderMessage ?? "",
226
+ recipientName: recipientName ?? "",
227
+ recipientPhone: recipientPhone ?? "",
228
+ package: { connect: { id: packageId } },
229
+ amount: pkg.amount,
230
+ paymentStatus: "pending",
231
+ deliveryStatus: "waiting"
232
+ },
233
+ query: "id amount"
234
+ });
235
+ return json(res, 200, {
236
+ orderId: order.id,
237
+ amount: order.amount,
238
+ // Your personal bKash number — set in .env
239
+ bkashNumber: process.env.BKASH_PERSONAL_NUMBER ?? "",
240
+ invoiceRef: `EID-${order.id.slice(-8).toUpperCase()}`
241
+ });
242
+ } catch (err) {
243
+ console.error("[create-order]", err?.message ?? err);
244
+ return json(res, 500, { error: "Failed to create order" });
245
+ }
246
+ });
247
+ app.use("/api/orders/submit-trxid", commonMiddleware, async (req, res) => {
248
+ if (req.method !== "POST")
249
+ return json(res, 405, { error: "Method not allowed" });
250
+ try {
251
+ const body = await parseBody(req);
252
+ const { orderId, trxID } = body;
253
+ if (!orderId || !trxID) {
254
+ return json(res, 400, { error: "orderId and trxID are required" });
255
+ }
256
+ const normalizedTrxID = trxID.trim().toUpperCase();
257
+ if (!isValidTrxID(normalizedTrxID)) {
258
+ return json(res, 400, { error: "Invalid TrxID format. bKash TrxIDs are 8\u201312 uppercase letters/numbers." });
259
+ }
260
+ const existing = await keystoneContext.sudo().query.Order.findOne({
261
+ where: { id: orderId },
262
+ query: "id paymentStatus bkashTrxID"
263
+ });
264
+ if (!existing)
265
+ return json(res, 404, { error: "Order not found" });
266
+ if (existing.paymentStatus === "completed") {
267
+ return json(res, 409, { error: "Order already completed" });
268
+ }
269
+ if (existing.bkashTrxID) {
270
+ return json(res, 409, { error: "TrxID already submitted for this order" });
271
+ }
272
+ const duplicate = await keystoneContext.sudo().query.Order.findMany({
273
+ where: { bkashTrxID: { equals: normalizedTrxID } },
274
+ query: "id"
275
+ });
276
+ if (duplicate.length > 0) {
277
+ return json(res, 409, { error: "\u098F\u0987 TrxID \u0986\u0997\u09C7\u0987 \u09AC\u09CD\u09AF\u09AC\u09B9\u09BE\u09B0 \u0995\u09B0\u09BE \u09B9\u09AF\u09BC\u09C7\u099B\u09C7\u0964 Please check your TrxID." });
278
+ }
279
+ await keystoneContext.sudo().query.Order.updateOne({
280
+ where: { id: orderId },
281
+ data: {
282
+ bkashTrxID: normalizedTrxID,
283
+ paymentStatus: "pending_verification"
284
+ }
285
+ });
286
+ return json(res, 200, { success: true, message: "TrxID submitted. Awaiting admin verification." });
287
+ } catch (err) {
288
+ console.error("[submit-trxid]", err?.message ?? err);
289
+ return json(res, 500, { error: "Failed to submit TrxID" });
290
+ }
291
+ });
292
+ app.use("/api/orders", commonMiddleware, async (req, res) => {
293
+ const id = req.url?.replace(/^\/+|\/+$/g, "");
294
+ if (!id)
295
+ return json(res, 400, { error: "Missing order id" });
296
+ try {
297
+ const order = await keystoneContext.sudo().query.Order.findOne({
298
+ where: { id },
299
+ query: `
300
+ id senderName recipientName amount senderMessage
301
+ paymentStatus deliveryStatus bkashTrxID
302
+ package { name amount emoji }
303
+ createdAt
304
+ `
305
+ });
306
+ if (!order)
307
+ return json(res, 404, { error: "Order not found" });
308
+ return json(res, 200, order);
309
+ } catch (err) {
310
+ return json(res, 500, { error: err?.message });
311
+ }
312
+ });
313
+ app.use("/api/packages", commonMiddleware, async (_req, res) => {
314
+ try {
315
+ const packages = await keystoneContext.sudo().query.SalamiPackage.findMany({
316
+ where: { isActive: { equals: true } },
317
+ orderBy: [{ sortOrder: "asc" }, { amount: "asc" }],
318
+ query: "id name slug amount emoji"
319
+ });
320
+ return json(res, 200, packages);
321
+ } catch (err) {
322
+ return json(res, 500, { error: err?.message });
323
+ }
324
+ });
325
+ }
326
+
327
+ // keystone.ts
328
+ var { withAuth } = (0, import_auth.createAuth)({
329
+ listKey: "User",
330
+ identityField: "email",
331
+ secretField: "password",
332
+ sessionData: "id email isAdmin",
333
+ initFirstItem: {
334
+ fields: ["name", "email", "password"],
335
+ itemData: { isAdmin: true }
336
+ }
337
+ });
338
+ var session = (0, import_session.statelessSessions)({
339
+ maxAge: 60 * 60 * 24 * 7,
340
+ secret: process.env.SESSION_SECRET ?? "CHANGE_THIS_TO_A_LONG_RANDOM_STRING_32CHARS!"
341
+ });
342
+ var keystone_default = withAuth(
343
+ (0, import_core2.config)({
344
+ db: {
345
+ provider: "postgresql",
346
+ url: process.env.DATABASE_URL ?? ""
347
+ },
348
+ lists,
349
+ session,
350
+ server: {
351
+ port: parseInt(process.env.PORT ?? "3000"),
352
+ cors: {
353
+ origin: [process.env.FRONTEND_URL ?? "http://localhost:5173"],
354
+ credentials: true
355
+ },
356
+ extendExpressApp: (app, context) => {
357
+ makeCustomRoutes(
358
+ app,
359
+ (req, res, next) => next(),
360
+ context
361
+ );
362
+ }
363
+ },
364
+ ui: {
365
+ isAccessAllowed: (context) => !!context.session?.data?.isAdmin
366
+ }
367
+ })
368
+ );
369
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../keystone.ts", "../keystone/schema.ts", "../keystone/routes.ts"],
4
+ "sourcesContent": ["import 'dotenv/config'\nimport { config } from '@keystone-6/core'\nimport { statelessSessions } from '@keystone-6/core/session'\nimport { createAuth } from '@keystone-6/auth'\nimport { lists } from './keystone/schema'\nimport { makeCustomRoutes } from './keystone/routes'\n\nconst { withAuth } = createAuth({\n listKey: 'User',\n identityField: 'email',\n secretField: 'password',\n sessionData: 'id email isAdmin',\n initFirstItem: {\n fields: ['name', 'email', 'password'],\n itemData: { isAdmin: true },\n },\n})\n\nconst session = statelessSessions({\n maxAge: 60 * 60 * 24 * 7,\n secret: process.env.SESSION_SECRET ?? 'CHANGE_THIS_TO_A_LONG_RANDOM_STRING_32CHARS!',\n})\n\nexport default withAuth(\n config({\n db: {\n provider: 'postgresql',\n url: process.env.DATABASE_URL ?? '',\n },\n lists,\n session,\n server: {\n port: parseInt(process.env.PORT ?? '3000'),\n cors: {\n origin: [process.env.FRONTEND_URL ?? 'http://localhost:5173'],\n credentials: true,\n },\n extendExpressApp: (app, context) => {\n makeCustomRoutes(\n app,\n (req: any, res: any, next: any) => next(),\n context\n )\n },\n },\n ui: {\n isAccessAllowed: (context) => !!context.session?.data?.isAdmin,\n },\n })\n)\n", "import { list } from '@keystone-6/core'\nimport { allowAll } from '@keystone-6/core/access'\nimport {\n text,\n password,\n timestamp,\n select,\n integer,\n float,\n relationship,\n checkbox,\n} from '@keystone-6/core/fields'\nimport { document } from '@keystone-6/fields-document'\n\nconst isAdmin = ({ session }: any) => session?.data?.isAdmin === true\n\nexport const lists = {\n // \u2500\u2500 Admin User \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n User: list({\n access: {\n operation: {\n query: isAdmin,\n create: isAdmin,\n update: isAdmin,\n delete: isAdmin,\n },\n },\n fields: {\n name: text({ validation: { isRequired: true } }),\n email: text({ validation: { isRequired: true }, isIndexed: 'unique' }),\n password: password({ validation: { isRequired: true } }),\n isAdmin: checkbox({ defaultValue: false }),\n createdAt: timestamp({ defaultValue: { kind: 'now' } }),\n },\n ui: {\n listView: { initialColumns: ['name', 'email', 'isAdmin', 'createdAt'] },\n },\n }),\n\n // \u2500\u2500 Salami Package \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n SalamiPackage: list({\n access: {\n operation: {\n query: allowAll, // public can browse\n create: isAdmin,\n update: isAdmin,\n delete: isAdmin,\n },\n },\n fields: {\n name: text({ validation: { isRequired: true } }),\n slug: text({ validation: { isRequired: true }, isIndexed: 'unique' }),\n description: document({ formatting: true, dividers: true, links: true }),\n amount: float({ validation: { isRequired: true }, label: 'Amount (BDT)' }),\n isActive: checkbox({ defaultValue: true }),\n emoji: text({ defaultValue: '\uD83D\uDC9A' }),\n sortOrder: integer({ defaultValue: 0 }),\n createdAt: timestamp({ defaultValue: { kind: 'now' } }),\n orders: relationship({ ref: 'Order.package', many: true }),\n },\n ui: {\n listView: { initialColumns: ['name', 'amount', 'isActive', 'sortOrder'] },\n },\n }),\n\n // \u2500\u2500 Order \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n Order: list({\n access: {\n operation: {\n query: isAdmin,\n create: allowAll, // anyone can place an order\n update: isAdmin,\n delete: isAdmin,\n },\n },\n fields: {\n // Sender\n senderName: text({ validation: { isRequired: true } }),\n senderPhone: text({ validation: { isRequired: true } }),\n senderMessage: text({ ui: { displayMode: 'textarea' } }),\n\n // Recipient\n recipientName: text(),\n recipientPhone: text(),\n\n // Package\n package: relationship({ ref: 'SalamiPackage.orders', many: false }),\n amount: float({ validation: { isRequired: true } }),\n\n // Payment \u2014 manual bKash personal flow\n paymentStatus: select({\n options: [\n { label: '\u23F3 Pending', value: 'pending' },\n { label: '\uD83D\uDD0D Pending Verification', value: 'pending_verification' },\n { label: '\u2705 Completed', value: 'completed' },\n { label: '\u274C Failed', value: 'failed' },\n { label: '\u21A9\uFE0F Refunded', value: 'refunded' },\n ],\n defaultValue: 'pending',\n ui: { displayMode: 'segmented-control' },\n }),\n\n // TrxID submitted by user after sending bKash \u2014 you verify manually\n bkashTrxID: text({ isIndexed: true, label: 'bKash TrxID (user submitted)' }),\n adminNote: text({ label: 'Admin Note', ui: { displayMode: 'textarea' } }),\n\n // Delivery\n deliveryStatus: select({\n options: [\n { label: '\u23F3 Waiting', value: 'waiting' },\n { label: '\uD83D\uDCE4 Sent', value: 'sent' },\n { label: '\u2705 Delivered', value: 'delivered' },\n ],\n defaultValue: 'waiting',\n ui: { displayMode: 'segmented-control' },\n }),\n\n createdAt: timestamp({ defaultValue: { kind: 'now' } }),\n updatedAt: timestamp({ db: { updatedAt: true } }),\n },\n ui: {\n listView: {\n initialColumns: [\n 'senderName', 'recipientName', 'amount',\n 'bkashTrxID', 'paymentStatus', 'deliveryStatus', 'createdAt',\n ],\n },\n },\n hooks: {\n // Auto-fill amount from linked package\n resolveInput: async ({ resolvedData, context }) => {\n if (resolvedData.package?.connect?.id && !resolvedData.amount) {\n const pkg = await context.query.SalamiPackage.findOne({\n where: { id: resolvedData.package.connect.id },\n query: 'amount',\n })\n if (pkg) resolvedData.amount = pkg.amount\n }\n return resolvedData\n },\n },\n }),\n}\n", "import { type KeystoneContext } from '@keystone-6/core/types'\nimport { type IncomingMessage, type ServerResponse } from 'http'\n\n// \u2500\u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nasync function parseBody(req: IncomingMessage): Promise<any> {\n return new Promise((resolve, reject) => {\n let data = ''\n req.on('data', chunk => (data += chunk))\n req.on('end', () => {\n try { resolve(JSON.parse(data || '{}')) } catch { resolve({}) }\n })\n req.on('error', reject)\n })\n}\n\nfunction json(res: ServerResponse, status: number, body: object) {\n res.writeHead(status, {\n 'Content-Type': 'application/json',\n 'Access-Control-Allow-Origin': process.env.FRONTEND_URL ?? '*',\n 'Access-Control-Allow-Methods': 'GET,POST,OPTIONS',\n 'Access-Control-Allow-Headers': 'Content-Type',\n })\n res.end(JSON.stringify(body))\n}\n\n// Basic TrxID format validation \u2014 bKash personal TrxIDs are 10 alphanumeric chars\nfunction isValidTrxID(trxID: string): boolean {\n return /^[A-Z0-9]{8,12}$/.test(trxID.trim().toUpperCase())\n}\n\nexport function makeCustomRoutes(\n app: any,\n commonMiddleware: any,\n keystoneContext: KeystoneContext\n) {\n // CORS preflight\n app.use((req: IncomingMessage, res: ServerResponse, next: () => void) => {\n if (req.method === 'OPTIONS') {\n res.writeHead(204, {\n 'Access-Control-Allow-Origin': process.env.FRONTEND_URL ?? '*',\n 'Access-Control-Allow-Methods': 'GET,POST,OPTIONS',\n 'Access-Control-Allow-Headers': 'Content-Type',\n })\n res.end()\n return\n }\n next()\n })\n\n // \u2500\u2500 POST /api/orders/create \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // Creates a pending order. Returns orderId + your bKash number so the\n // frontend can show \"Send \u09F3X to 017XXXXXXXX, then paste TrxID\"\n app.use('/api/orders/create', commonMiddleware, async (req: IncomingMessage, res: ServerResponse) => {\n if (req.method !== 'POST') return json(res, 405, { error: 'Method not allowed' })\n\n try {\n const body = await parseBody(req)\n const { senderName, senderPhone, senderMessage, recipientName, recipientPhone, packageId } = body\n\n if (!senderName || !senderPhone || !packageId) {\n return json(res, 400, { error: 'Missing required fields' })\n }\n\n const pkg = await keystoneContext.sudo().query.SalamiPackage.findOne({\n where: { id: packageId },\n query: 'id name amount isActive',\n })\n if (!pkg || !pkg.isActive) {\n return json(res, 404, { error: 'Package not found or inactive' })\n }\n\n const order = await keystoneContext.sudo().query.Order.createOne({\n data: {\n senderName,\n senderPhone,\n senderMessage: senderMessage ?? '',\n recipientName: recipientName ?? '',\n recipientPhone: recipientPhone ?? '',\n package: { connect: { id: packageId } },\n amount: pkg.amount,\n paymentStatus: 'pending',\n deliveryStatus: 'waiting',\n },\n query: 'id amount',\n })\n\n return json(res, 200, {\n orderId: order.id,\n amount: order.amount,\n // Your personal bKash number \u2014 set in .env\n bkashNumber: process.env.BKASH_PERSONAL_NUMBER ?? '',\n invoiceRef: `EID-${order.id.slice(-8).toUpperCase()}`,\n })\n } catch (err: any) {\n console.error('[create-order]', err?.message ?? err)\n return json(res, 500, { error: 'Failed to create order' })\n }\n })\n\n // \u2500\u2500 POST /api/orders/submit-trxid \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // User has sent the bKash payment and submits their TrxID.\n // Order is marked \"pending_verification\" \u2014 you confirm from admin panel.\n app.use('/api/orders/submit-trxid', commonMiddleware, async (req: IncomingMessage, res: ServerResponse) => {\n if (req.method !== 'POST') return json(res, 405, { error: 'Method not allowed' })\n\n try {\n const body = await parseBody(req)\n const { orderId, trxID } = body\n\n if (!orderId || !trxID) {\n return json(res, 400, { error: 'orderId and trxID are required' })\n }\n\n const normalizedTrxID = trxID.trim().toUpperCase()\n if (!isValidTrxID(normalizedTrxID)) {\n return json(res, 400, { error: 'Invalid TrxID format. bKash TrxIDs are 8\u201312 uppercase letters/numbers.' })\n }\n\n // Make sure order exists and is still pending\n const existing = await keystoneContext.sudo().query.Order.findOne({\n where: { id: orderId },\n query: 'id paymentStatus bkashTrxID',\n })\n if (!existing) return json(res, 404, { error: 'Order not found' })\n if (existing.paymentStatus === 'completed') {\n return json(res, 409, { error: 'Order already completed' })\n }\n if (existing.bkashTrxID) {\n return json(res, 409, { error: 'TrxID already submitted for this order' })\n }\n\n // Check for duplicate TrxID across all orders (prevent reuse)\n const duplicate = await keystoneContext.sudo().query.Order.findMany({\n where: { bkashTrxID: { equals: normalizedTrxID } },\n query: 'id',\n })\n if (duplicate.length > 0) {\n return json(res, 409, { error: '\u098F\u0987 TrxID \u0986\u0997\u09C7\u0987 \u09AC\u09CD\u09AF\u09AC\u09B9\u09BE\u09B0 \u0995\u09B0\u09BE \u09B9\u09AF\u09BC\u09C7\u099B\u09C7\u0964 Please check your TrxID.' })\n }\n\n await keystoneContext.sudo().query.Order.updateOne({\n where: { id: orderId },\n data: {\n bkashTrxID: normalizedTrxID,\n paymentStatus: 'pending_verification',\n },\n })\n\n return json(res, 200, { success: true, message: 'TrxID submitted. Awaiting admin verification.' })\n } catch (err: any) {\n console.error('[submit-trxid]', err?.message ?? err)\n return json(res, 500, { error: 'Failed to submit TrxID' })\n }\n })\n\n // \u2500\u2500 GET /api/orders/:id \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n app.use('/api/orders', commonMiddleware, async (req: IncomingMessage, res: ServerResponse) => {\n const id = req.url?.replace(/^\\/+|\\/+$/g, '')\n if (!id) return json(res, 400, { error: 'Missing order id' })\n\n try {\n const order = await keystoneContext.sudo().query.Order.findOne({\n where: { id },\n query: `\n id senderName recipientName amount senderMessage\n paymentStatus deliveryStatus bkashTrxID\n package { name amount emoji }\n createdAt\n `,\n })\n if (!order) return json(res, 404, { error: 'Order not found' })\n return json(res, 200, order)\n } catch (err: any) {\n return json(res, 500, { error: err?.message })\n }\n })\n\n // \u2500\u2500 GET /api/packages \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n app.use('/api/packages', commonMiddleware, async (_req: IncomingMessage, res: ServerResponse) => {\n try {\n const packages = await keystoneContext.sudo().query.SalamiPackage.findMany({\n where: { isActive: { equals: true } },\n orderBy: [{ sortOrder: 'asc' }, { amount: 'asc' }],\n query: 'id name slug amount emoji',\n })\n return json(res, 200, packages)\n } catch (err: any) {\n return json(res, 500, { error: err?.message })\n }\n })\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["import_core", "session"]
7
+ }
@@ -0,0 +1,184 @@
1
+ import axios, { AxiosInstance } from 'axios'
2
+
3
+ interface BkashConfig {
4
+ baseUrl: string
5
+ appKey: string
6
+ appSecret: string
7
+ username: string
8
+ password: string
9
+ }
10
+
11
+ interface BkashToken {
12
+ id_token: string
13
+ token_type: string
14
+ expires_in: number
15
+ refresh_token: string
16
+ }
17
+
18
+ interface CreatePaymentResponse {
19
+ paymentID: string
20
+ bkashURL: string
21
+ callbackURL: string
22
+ successCallbackURL: string
23
+ failureCallbackURL: string
24
+ cancelledCallbackURL: string
25
+ amount: string
26
+ intent: string
27
+ currency: string
28
+ paymentCreateTime: string
29
+ transactionStatus: string
30
+ merchantInvoiceNumber: string
31
+ }
32
+
33
+ interface ExecutePaymentResponse {
34
+ paymentID: string
35
+ trxID: string
36
+ transactionStatus: string
37
+ amount: string
38
+ currency: string
39
+ intent: string
40
+ paymentExecuteTime: string
41
+ merchantInvoiceNumber: string
42
+ customerMsisdn: string
43
+ }
44
+
45
+ export class BkashService {
46
+ private config: BkashConfig
47
+ private http: AxiosInstance
48
+ private token: string | null = null
49
+ private tokenExpiry: number = 0
50
+
51
+ constructor(config: BkashConfig) {
52
+ this.config = config
53
+ this.http = axios.create({
54
+ baseURL: config.baseUrl,
55
+ headers: { 'Content-Type': 'application/json' },
56
+ })
57
+ }
58
+
59
+ // ── Grant Token ─────────────────────────────────────────────────────────────
60
+ async grantToken(): Promise<string> {
61
+ const now = Date.now()
62
+ if (this.token && now < this.tokenExpiry) return this.token
63
+
64
+ const res = await this.http.post<BkashToken>(
65
+ '/tokenized/checkout/token/grant',
66
+ {
67
+ app_key: this.config.appKey,
68
+ app_secret: this.config.appSecret,
69
+ },
70
+ {
71
+ headers: {
72
+ username: this.config.username,
73
+ password: this.config.password,
74
+ },
75
+ }
76
+ )
77
+
78
+ this.token = res.data.id_token
79
+ // Expire 5 minutes early to be safe
80
+ this.tokenExpiry = now + (res.data.expires_in - 300) * 1000
81
+ return this.token
82
+ }
83
+
84
+ // ── Create Payment ──────────────────────────────────────────────────────────
85
+ async createPayment(params: {
86
+ amount: number
87
+ orderId: string
88
+ callbackUrl: string
89
+ merchantInvoiceNumber?: string
90
+ }): Promise<CreatePaymentResponse> {
91
+ const token = await this.grantToken()
92
+
93
+ const res = await this.http.post<CreatePaymentResponse>(
94
+ '/tokenized/checkout/create',
95
+ {
96
+ mode: '0011', // Checkout URL
97
+ payerReference: params.orderId,
98
+ callbackURL: params.callbackUrl,
99
+ amount: params.amount.toFixed(2),
100
+ currency: 'BDT',
101
+ intent: 'sale',
102
+ merchantInvoiceNumber: params.merchantInvoiceNumber ?? params.orderId,
103
+ },
104
+ {
105
+ headers: {
106
+ Authorization: token,
107
+ 'X-APP-Key': this.config.appKey,
108
+ },
109
+ }
110
+ )
111
+ return res.data
112
+ }
113
+
114
+ // ── Execute Payment ─────────────────────────────────────────────────────────
115
+ async executePayment(paymentID: string): Promise<ExecutePaymentResponse> {
116
+ const token = await this.grantToken()
117
+
118
+ const res = await this.http.post<ExecutePaymentResponse>(
119
+ '/tokenized/checkout/execute',
120
+ { paymentID },
121
+ {
122
+ headers: {
123
+ Authorization: token,
124
+ 'X-APP-Key': this.config.appKey,
125
+ },
126
+ }
127
+ )
128
+ return res.data
129
+ }
130
+
131
+ // ── Query Payment ───────────────────────────────────────────────────────────
132
+ async queryPayment(paymentID: string): Promise<any> {
133
+ const token = await this.grantToken()
134
+
135
+ const res = await this.http.post(
136
+ '/tokenized/checkout/payment/status',
137
+ { paymentID },
138
+ {
139
+ headers: {
140
+ Authorization: token,
141
+ 'X-APP-Key': this.config.appKey,
142
+ },
143
+ }
144
+ )
145
+ return res.data
146
+ }
147
+
148
+ // ── Refund ──────────────────────────────────────────────────────────────────
149
+ async refund(params: {
150
+ paymentID: string
151
+ trxID: string
152
+ amount: number
153
+ reason?: string
154
+ }): Promise<any> {
155
+ const token = await this.grantToken()
156
+
157
+ const res = await this.http.post(
158
+ '/tokenized/checkout/payment/refund',
159
+ {
160
+ paymentID: params.paymentID,
161
+ trxID: params.trxID,
162
+ amount: params.amount.toFixed(2),
163
+ currency: 'BDT',
164
+ reason: params.reason ?? 'Customer request',
165
+ },
166
+ {
167
+ headers: {
168
+ Authorization: token,
169
+ 'X-APP-Key': this.config.appKey,
170
+ },
171
+ }
172
+ )
173
+ return res.data
174
+ }
175
+ }
176
+
177
+ // Singleton
178
+ export const bkash = new BkashService({
179
+ baseUrl: process.env.BKASH_BASE_URL ?? 'https://tokenized.sandbox.bka.sh/v1.2.0-beta',
180
+ appKey: process.env.BKASH_APP_KEY ?? '',
181
+ appSecret: process.env.BKASH_APP_SECRET ?? '',
182
+ username: process.env.BKASH_USERNAME ?? '',
183
+ password: process.env.BKASH_PASSWORD ?? '',
184
+ })