Repository navigation
Expand file tree
/
Copy pathserver.js
More file actions
272 lines (236 loc) · 13.2 KB
/
Copy pathserver.js
File metadata and controls
272 lines (236 loc) · 13.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
/**
* JS-Basic-Example-Payment
* نمونهی سادهی اتصال به درگاه پرداخت پیپینگ (API نسخه ۳) با Node.js — بدون هیچ وابستگی خارجی.
*
* پیادهسازی بر اساس کنترلر رسمی سرویس پرداخت پیپینگ (PaymentController با مسیر v3/pay):
* POST /v3/pay ساخت پرداخت → پاسخ شامل paymentCode و url آمادهی هدایت کاربر
* POST /v3/pay/verify تایید نهایی پرداخت با paymentRefId و paymentCode
*
* جریان کامل:
* 1. کاربر فرم را پر میکند و به POST /payment میرود.
* 2. سرور با فراخوانی v3/pay یک دستور پرداخت میسازد و کاربر را به url برگشتی ریدایرکت میکند.
* 3. پس از پرداخت در درگاه بانکی، پیپینگ نتیجه را با یک فرم POST به returnUrl میفرستد:
* status (1=موفق، 0=ناموفق)، errorCode و data (رشته JSON با فیلدهای camelCase)
* 4. سرور در صورت موفقیت، تراکنش را با v3/pay/verify تایید و نتیجه را نمایش میدهد.
*
* اجرا:
* PAYPING_TOKEN=توکن شما node server.js
*
* پیشنیاز: Node.js نسخهی ۱۸ یا بالاتر (به دلیل fetch داخلی).
*/
const http = require('http');
/* ----------------------------- تنظیمات ----------------------------- */
/* توکن دریافتی از پنل payping.ir | آن را در متغیر محیطی PAYPING_TOKEN قرار دهید و هرگز در کد کامیت نکنید. */
const TOKEN = process.env.PAYPING_TOKEN || 'Token';
/* پورت سرور */
const PORT = process.env.PORT || 3000;
/*
* آدرس صفحهی برگشت کاربر بعد از صفحهی پرداخت.
* برای استفادهی واقعی، آدرس دامنهی خود را در متغیر محیطی RETURN_URL قرار دهید.
*/
const RETURN_URL = process.env.RETURN_URL || `http://localhost:${PORT}/verify`;
/* آدرس سرویسهای پیپینگ (نسخه ۳) */
const PAY_URL = 'https://api.payping.ir/v3/pay';
const VERIFY_URL = 'https://api.payping.ir/v3/pay/verify';
/* --------------------------- توابع کمکی ---------------------------- */
/** خواندن بدنهی درخواست و تبدیل فرم urlencoded به شیء جاوااسکریپت */
function parseBody(req) {
return new Promise((resolve) => {
let raw = '';
req.on('data', (chunk) => (raw += chunk));
req.on('end', () => {
const data = {};
new URLSearchParams(raw).forEach((value, key) => (data[key] = value));
resolve(data);
});
});
}
/** فراخوانی API پیپینگ و برگرداندن کد وضعیت بههمراه پاسخ JSON */
async function paypingPost(url, body) {
const response = await fetch(url, {
method: 'POST',
headers: {
accept: 'application/json',
authorization: `Bearer ${TOKEN}`,
'cache-control': 'no-cache',
'content-type': 'application/json',
},
body: JSON.stringify(body),
});
let data = null;
try {
data = await response.json();
} catch {
/* پاسخ JSON نبود؛ همینطور نگه میداریم */
}
return { status: response.status, data };
}
/** ارسال پاسخ HTML با کد وضعیت دلخواه */
function sendHtml(res, statusCode, html) {
res.writeHead(statusCode, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(html);
}
/** قالب مشترک صفحات HTML (راستبهچپ) */
function renderPage(title, body) {
return `<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${title}</title>
<style>
body { font-family: Tahoma, sans-serif; font-size: 14px; direction: rtl; background: #f5f6f8; }
h1 { font-size: 18px; text-align: center; margin-top: 40px; color: #172b4d; }
.box { max-width: 320px; margin: 20px auto; padding: 20px; background: #172b4d; color: #fff; border-radius: 8px; text-align: justify; line-height: 1.8; }
.form-payment input, .form-payment button { width: 100%; padding: 10px; margin: 6px 0; border: none; border-radius: 4px; box-sizing: border-box; font-family: inherit; font-size: 13px; }
.form-payment input { direction: ltr; text-align: left; }
.form-payment button { background: #0080ff; color: #fff; cursor: pointer; font-size: 15px; }
.form-payment button:hover { background: #fff; color: #172b4d; }
</style>
</head>
<body>
${body}
</body>
</html>`;
}
/** صفحهی فرم پرداخت */
function renderFormPage() {
return renderPage(
'فرم درگاه پرداخت پیپینگ',
`<h1>فرم درگاه پرداخت پیپینگ</h1>
<form class="form-payment box" action="/payment" method="post">
<input type="email" name="clientRefId" placeholder="example@gmail.com" required>
<input type="text" name="mobile" placeholder="0912...">
<input type="text" name="payerName" placeholder="نام پرداخت کننده">
<input type="number" name="amount" min="1000" step="100" placeholder="مبلغ بین 1000 تا 50 میلیون تومان" required>
<input type="text" name="description" placeholder="توضیحات">
<button type="submit">پرداخت</button>
</form>`
);
}
/** صفحهی نتیجه (موفقیت یا خطا) */
function renderMessagePage(message) {
return renderPage('نتیجه پرداخت', `<div class="box">${message}</div>`);
}
/* ----------------------------- سرور ------------------------------- */
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
try {
/* ---------- صفحهی اصلی: فرم پرداخت ---------- */
if (req.method === 'GET' && url.pathname === '/') {
return sendHtml(res, 200, renderFormPage());
}
/* ---------- مرحله ۱ و ۲: ساخت پرداخت و هدایت به درگاه (معادل POST v3/pay) ---------- */
if (req.method === 'POST' && url.pathname === '/payment') {
const form = await parseBody(req);
/* مبلغ دستور پرداخت (تومان) */
const amount = parseInt(form.amount, 10);
if (!amount || amount < 1000) {
return sendHtml(res, 400, renderMessagePage('مبلغ نامعتبر است. مبلغ باید بین 1000 تا 50 میلیون تومان باشد.'));
}
/* شناسهی ارجاع پذیرنده: در حالت واقعی بهتر است شمارهی فاکتور در نظر گرفته شود. */
const clientRefId = form.clientRefId || 'example@gmail.com';
/* payerIdentity: شماره موبایل یا ایمیل پرداختکننده — اگر موبایل باشد کارتهای ذخیرهشدهاش در درگاه نمایش داده میشود. */
const payerIdentity = form.mobile || form.clientRefId || String(Date.now());
/*
* مبلغ را بهصورت پارامتر به returnUrl اضافه میکنیم تا هنگام تایید تراکنش در دسترس باشد،
* چون سرویس verify باید همان «مبلغ دستور پرداخت» را دریافت کند.
* نکتهی امنیتی: در پروژهی واقعی مبلغ را از فاکتور/دیتابیس بخوانید، نه از ورودی کاربر.
*/
const returnUrlWithAmount = `${RETURN_URL}${RETURN_URL.includes('?') ? '&' : '?'}amount=${amount}`;
const { status, data } = await paypingPost(PAY_URL, {
amount,
returnUrl: returnUrlWithAmount,
payerIdentity,
payerName: form.payerName || '',
description: form.description || '',
clientRefId,
/* فیلدهای اختیاری سایر مدل PaymentModel در صورت نیاز:
* nationalCode: 'کد ملی پرداختکننده',
* isReversible: true (فعالسازی امکان بازگشت وجه تا ۳۰ دقیقه پس از وریفای),
* isBlocked: true (پرداخت مسدودشونده برای تسهیم پس از پرداخت),
*/
});
/* پاسخ موفق: { paymentCode, url, amount, payerWage, businessWage, gatewayAmount, paypingVat } */
if (status === 200 && data && data.url) {
/* طبق مستندات، کافیست کاربر را به url دادهشده ریدایرکت کنید */
res.writeHead(302, { Location: data.url });
return res.end();
}
const detail = status === 400 ? JSON.stringify(data) : `HTTP ${status}`;
return sendHtml(res, 500, renderMessagePage(`ساخت پرداخت ناموفق بود، شرح خطا: ${detail}`));
}
/* ---------- مرحله ۳: بازگشت کاربر از درگاه به returnUrl ---------- */
if (url.pathname === '/verify') {
/*
* پیپینگ نتیجه را با یک فرم POST به returnUrl میفرستد:
* status (1=موفق، 0=ناموفق) | errorCode (در موفقیت خالی است) | data (رشته JSON)
* فیلدهای data در پرداخت موفق: clientRefId, paymentCode, paymentRefId, amount, gatewayAmount, cardNumber, cardHashPan
*/
const form = req.method === 'POST' ? await parseBody(req) : {};
const status = parseInt(form.status ?? url.searchParams.get('status'), 10);
const errorCode = form.errorCode || url.searchParams.get('errorCode') || '';
let data = {};
try {
data = JSON.parse(form.data || url.searchParams.get('data') || '{}');
} catch {
return sendHtml(res, 400, renderMessagePage('دادههای بازگشتی از درگاه قابل خواندن نبود.'));
}
/* پرداخت ناموفق: status صفر است و refid معتبر وجود ندارد */
if (status !== 1) {
return sendHtml(res, 200, renderMessagePage(`پرداخت ناموفق بود یا توسط کاربر لغو شد. (کد خطا: ${errorCode || 'نامشخص'})`));
}
/*
* نکتهی امنیتی طبق مستندات: صحت amount و clientRefId ارسالی از پیپینگ را با پایگاه داده خود مقایسه کنید.
* اینجا مبلغ اعلامی خودمان (که هنگام ساخت به returnUrl الصاق کردیم) ملاک است.
*/
const expectedAmount = parseInt(url.searchParams.get('amount'), 10) || 0;
if (!expectedAmount || data.amount !== expectedAmount) {
return sendHtml(res, 400, renderMessagePage('مغایرت در مبلغ تراکنش! از ادامهی فرایند صرفنظر شد.'));
}
/* ---------- مرحله ۴: تایید نهایی (معادل POST v3/pay/verify) ---------- */
const { status: verifyStatus, data: verifyData } = await paypingPost(VERIFY_URL, {
paymentRefId: data.paymentRefId,
paymentCode: data.paymentCode,
amount: expectedAmount,
});
/* تایید موفق: اطلاعات کامل تراکنش شامل مبلغ، شماره کارت و کارمزدها برمیگردد */
if (verifyStatus === 200) {
return sendHtml(
res,
200,
renderMessagePage(
`پرداخت با موفقیت تایید شد.<br>` +
`شماره پیگیری: ${verifyData.paymentRefId}<br>` +
`مبلغ: ${verifyData.amount} تومان<br>` +
`شماره کارت: ${verifyData.cardNumber || '-'}`
)
);
}
/* 409 با metaData.code برابر 110: تراکنش قبلاً وریفای شده و نیازی به تایید مجدد نیست */
if (verifyStatus === 409 && verifyData?.metaData?.code === 110) {
return sendHtml(res, 200, renderMessagePage(`این تراکنش قبلاً تایید شده است. شماره پیگیری: ${data.paymentRefId}`));
}
/* 202 یا 502: تایید هنوز در حال پردازش است؛ verify باید مجدداً فراخوانی شود */
if (verifyStatus === 202 || verifyStatus === 502) {
return sendHtml(res, 202, renderMessagePage('تایید تراکنش در حال پردازش است؛ لطفاً چند لحظه دیگر دوباره تلاش کنید.'));
}
const detail = verifyStatus === 400 ? JSON.stringify(verifyData) : `HTTP ${verifyStatus}`;
return sendHtml(res, 500, renderMessagePage(`تایید تراکنش ناموفق بود، شرح خطا: ${detail}`));
}
/* ---------- مسیر ناشناخته ---------- */
return sendHtml(res, 404, renderMessagePage('صفحهای که دنبال آن هستید وجود ندارد.'));
} catch (error) {
return sendHtml(
res,
500,
renderMessagePage(`تراکنش ناموفق بود، شرح خطا سمت برنامه شما: ${error.message}`)
);
}
});
server.listen(PORT, () => {
console.log(`سرور روی پورت ${PORT} اجرا شد: http://localhost:${PORT}`);
if (TOKEN === 'Token') {
console.warn('هشدار: توکن پیپینگ تنظیم نشده است. آن را در متغیر محیطی PAYPING_TOKEN قرار دهید.');
}
});