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.
- package/DEPLOY.md +199 -0
- package/README.md +133 -0
- package/backend/.env.example +25 -0
- package/backend/.keystone_temp/admin/next.config.js +14 -0
- package/backend/.keystone_temp/admin/pages/_app.js +22 -0
- package/backend/.keystone_temp/admin/pages/index.js +1 -0
- package/backend/.keystone_temp/admin/pages/init.js +5 -0
- package/backend/.keystone_temp/admin/pages/no-access.js +3 -0
- package/backend/.keystone_temp/admin/pages/orders/[id].js +3 -0
- package/backend/.keystone_temp/admin/pages/orders/create.js +3 -0
- package/backend/.keystone_temp/admin/pages/orders/index.js +3 -0
- package/backend/.keystone_temp/admin/pages/salami-packages/[id].js +3 -0
- package/backend/.keystone_temp/admin/pages/salami-packages/create.js +3 -0
- package/backend/.keystone_temp/admin/pages/salami-packages/index.js +3 -0
- package/backend/.keystone_temp/admin/pages/signin.js +3 -0
- package/backend/.keystone_temp/admin/pages/users/[id].js +3 -0
- package/backend/.keystone_temp/admin/pages/users/create.js +3 -0
- package/backend/.keystone_temp/admin/pages/users/index.js +3 -0
- package/backend/.keystone_temp/admin/public/favicon.ico +0 -0
- package/backend/.keystone_temp/config.js +369 -0
- package/backend/.keystone_temp/config.js.map +7 -0
- package/backend/bkash/bkash.service.ts +184 -0
- package/backend/keystone/routes.ts +193 -0
- package/backend/keystone/schema.ts +143 -0
- package/backend/keystone.ts +54 -0
- package/backend/package.json +23 -0
- package/backend/schema.graphql +530 -0
- package/backend/schema.prisma +55 -0
- package/backend/seed.ts +53 -0
- package/backend/tsconfig.json +15 -0
- package/frontend/.env.example +6 -0
- package/frontend/index.html +16 -0
- package/frontend/package.json +24 -0
- package/frontend/src/App.js +11 -0
- package/frontend/src/App.tsx +22 -0
- package/frontend/src/api/client.js +11 -0
- package/frontend/src/api/client.ts +57 -0
- package/frontend/src/components/Footer.js +5 -0
- package/frontend/src/components/Footer.module.css +27 -0
- package/frontend/src/components/Footer.tsx +13 -0
- package/frontend/src/components/Navbar.js +6 -0
- package/frontend/src/components/Navbar.module.css +54 -0
- package/frontend/src/components/Navbar.tsx +16 -0
- package/frontend/src/components/PackageCard.js +5 -0
- package/frontend/src/components/PackageCard.module.css +64 -0
- package/frontend/src/components/PackageCard.tsx +24 -0
- package/frontend/src/main.js +7 -0
- package/frontend/src/main.tsx +13 -0
- package/frontend/src/pages/HomePage.js +31 -0
- package/frontend/src/pages/HomePage.module.css +416 -0
- package/frontend/src/pages/HomePage.tsx +146 -0
- package/frontend/src/pages/OrderPage.js +98 -0
- package/frontend/src/pages/OrderPage.module.css +624 -0
- package/frontend/src/pages/OrderPage.tsx +221 -0
- package/frontend/src/pages/PaymentCallbackPage.js +25 -0
- package/frontend/src/pages/PaymentCallbackPage.module.css +38 -0
- package/frontend/src/pages/PaymentCallbackPage.tsx +37 -0
- package/frontend/src/pages/PaymentResultPage.js +28 -0
- package/frontend/src/pages/PaymentResultPage.module.css +182 -0
- package/frontend/src/pages/PaymentResultPage.tsx +92 -0
- package/frontend/src/styles/global.css +66 -0
- package/frontend/src/vite-env.d.ts +5 -0
- package/frontend/tsconfig.json +15 -0
- package/frontend/vite.config.ts +15 -0
- 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
|
+
})
|