This commit is contained in:
payacom
2026-10-06 17:08:44 +03:30
parent fade8543e2
commit 367416d419
14 changed files with 6087 additions and 5937 deletions

View File

@@ -21,9 +21,15 @@ const {
creditProjectShare
} = require('../../../utils/projectCommission')
const { isVerifiedBadge } = require('../../../utils/verifyBadge')
const { buildPaymentPageUrl } = require('../../../utils/zarinpalPaymentPage')
const apiKey = process.env.ZARINPAL_MERCHANT_ID || process.env.MERCHENT_CODE // Replace with your ZarinPal API Key
// زرین‌پال callback رو فقط وقتی قبول می‌کنه که دامنه‌اش همون دامنه‌ی ثبت‌شده‌ی مرچنت باشه
// (APP_SITE_CallBack = دامنه‌ی اصلی)؛ APP_URL (زیردامنه‌ی app) باعث خطای «دامنه مجاز نیست» می‌شه
const getCallbackBase = () =>
(process.env.APP_SITE_CallBack || process.env.APP_URL || '').replace(/\/$/, '')
// درگاه قدیمیِ REST زرین‌پال (/pg/rest/WebGate) منسوخ شده — مبلغ به API نسخه‌ی v4
// باید به ریال ارسال بشه، نه تومان (برخلاف API قدیمی)
function toRial (tomanAmount) {
@@ -108,7 +114,7 @@ const initiatePayment = async (req, res) => {
merchant_id: apiKey,
amount: toRial(price),
description: `پرداخت برای سفارش شماره ${projectId}`,
callback_url: `${process.env.APP_URL}/projects/payment?projectId=${projectId}&itemName=${itemName}&userId=${userId}` // Assuming your backend URL for handling payment callback
callback_url: `${getCallbackBase()}/projects/payment?projectId=${projectId}&itemName=${itemName}&userId=${userId}` // Assuming your backend URL for handling payment callback
},
{ headers: { 'Content-Type': 'application/json' } }
)
@@ -116,7 +122,7 @@ const initiatePayment = async (req, res) => {
const result = response.data?.data
if (result?.code === 100 && result?.authority) {
// Payment request was successful
res.status(200).json({ authority: result.authority })
res.status(200).json({ authority: result.authority, paymentUrl: buildPaymentPageUrl(result.authority) })
} else {
// Payment request failed
console.error('Error occurred while initiating payment:', response.data)
@@ -246,7 +252,7 @@ const paymentRequestStepOne = async (req, res) => {
merchant_id: apiKey,
amount: toRial(acceptAmount),
description: `پرداخت برای سفارش شماره ${projectId}`,
callback_url: `${process.env.APP_URL}/projects/payment-request-step-one-callback?projectId=${projectId}&requestId=${requestId}&userId=${userId}` // Callback URL including requestId
callback_url: `${getCallbackBase()}/projects/payment-request-step-one-callback?projectId=${projectId}&requestId=${requestId}&userId=${userId}` // Callback URL including requestId
},
{ headers: { 'Content-Type': 'application/json' } }
)
@@ -254,7 +260,7 @@ const paymentRequestStepOne = async (req, res) => {
const result = response.data?.data
if (result?.code === 100 && result?.authority) {
// Payment request was successful
res.status(200).json({ authority: result.authority })
res.status(200).json({ authority: result.authority, paymentUrl: buildPaymentPageUrl(result.authority) })
} else {
// Payment request failed
console.error('Error occurred while initiating payment:', response.data)
@@ -290,7 +296,7 @@ const paymentRequestStepTwo = async (req, res) => {
merchant_id: apiKey,
amount: toRial(secondInstallmentAmount),
description: `پرداخت برای سفارش شماره ${projectId}`,
callback_url: `${process.env.APP_URL}/projects/payment-request-step-two-callback?projectId=${projectId}&userId=${userId}&installmentNumber=${installmentNumber}` // Callback URL
callback_url: `${getCallbackBase()}/projects/payment-request-step-two-callback?projectId=${projectId}&userId=${userId}&installmentNumber=${installmentNumber}` // Callback URL
},
{ headers: { 'Content-Type': 'application/json' } }
)
@@ -298,7 +304,7 @@ const paymentRequestStepTwo = async (req, res) => {
const result = response.data?.data
if (result?.code === 100 && result?.authority) {
// Payment request was successful
res.status(200).json({ authority: result.authority })
res.status(200).json({ authority: result.authority, paymentUrl: buildPaymentPageUrl(result.authority) })
} else {
// Payment request failed
console.error('Error occurred while initiating payment:', response.data)
@@ -335,7 +341,7 @@ const paymentRequestStepAll = async (req, res) => {
merchant_id: apiKey,
amount: toRial(secondInstallmentAmount),
description: `پرداخت برای سفارش شماره ${projectId}`,
callback_url: `${process.env.APP_URL}/projects/payment-request-step-all-callback?projectId=${projectId}&userId=${userId}` // Callback URL
callback_url: `${getCallbackBase()}/projects/payment-request-step-all-callback?projectId=${projectId}&userId=${userId}` // Callback URL
},
{ headers: { 'Content-Type': 'application/json' } }
)
@@ -343,7 +349,7 @@ const paymentRequestStepAll = async (req, res) => {
const result = response.data?.data
if (result?.code === 100 && result?.authority) {
// Payment request was successful
res.status(200).json({ authority: result.authority })
res.status(200).json({ authority: result.authority, paymentUrl: buildPaymentPageUrl(result.authority) })
} else {
// Payment request failed
console.error('Error occurred while initiating payment:', response.data)
@@ -690,14 +696,14 @@ const initiateAdvertisingPayment = async (req, res) => {
merchant_id: apiKey,
amount: toRial(showDiscount === true || showDiscount === 'true' ? Number(price + 20000) : Number(price)),
description: `پرداخت برای سفارش شماره ${advertisingId}`,
callback_url: `${process.env.APP_URL}/advertising/payment?advertisingId=${advertisingId}&itemName=${itemName}&userId=${userId}&showDiscount=${showDiscount}` // Assuming your backend URL for handling payment callback
callback_url: `${getCallbackBase()}/advertising/payment?advertisingId=${advertisingId}&itemName=${itemName}&userId=${userId}&showDiscount=${showDiscount}` // Assuming your backend URL for handling payment callback
},
{ headers: { 'Content-Type': 'application/json' } }
)
const result = response.data?.data
if (result?.code === 100 && result?.authority) {
// Payment request was successful
res.status(200).json({ authority: result.authority })
res.status(200).json({ authority: result.authority, paymentUrl: buildPaymentPageUrl(result.authority) })
} else {
// Payment request failed
console.error('Error occurred while initiating payment:', response.data)
@@ -840,14 +846,14 @@ const republishAdvertisingPayment = async (req, res) => {
merchant_id: apiKey,
amount: toRial(showDiscount === true || showDiscount === 'true' ? Number(((price + 20000) * 70) / 100) : Number(((price) * 70) / 100)),
description: `پرداخت برای سفارش شماره ${advertisingId}`,
callback_url: `${process.env.APP_URL}/advertising/republish-verify-payment?advertisingId=${advertisingId}&itemName=${itemName}&userId=${userId}&showDiscount=${showDiscount}` // Assuming your backend URL for handling payment callback
callback_url: `${getCallbackBase()}/advertising/republish-verify-payment?advertisingId=${advertisingId}&itemName=${itemName}&userId=${userId}&showDiscount=${showDiscount}` // Assuming your backend URL for handling payment callback
},
{ headers: { 'Content-Type': 'application/json' } }
)
const result = response.data?.data
if (result?.code === 100 && result?.authority) {
// Payment request was successful
res.status(200).json({ authority: result.authority })
res.status(200).json({ authority: result.authority, paymentUrl: buildPaymentPageUrl(result.authority) })
} else {
// Payment request failed
console.error('Error occurred while initiating payment:', response.data)

View File

@@ -18,6 +18,8 @@ const { PROJECT_ACCEPT_RATE, PROJECT_COMPLETION_COMMISSION_RATE, creditProjectSh
const { isVerifiedBadge } = require('../../../utils/verifyBadge')
const { resolveWalletSplit, debitWalletForPurchase } = require('../../../utils/walletSplit')
const { needsUsernameAndPassword } = require('../../../utils/authSteps')
const { canUseFreeCreation, consumeFreeCreation } = require('../../../utils/freeCreationLimit')
const { buildPaymentPageUrl } = require('../../../utils/zarinpalPaymentPage')
const OFFER_COMMISSION_RATE = 0.5
@@ -331,7 +333,7 @@ const paymentRequestAcceptWeb = async (req, res) => {
if (result?.code === 100 && result?.authority) {
return res.status(200).json({
authority: result.authority,
paymentUrl: `https://www.zarinpal.com/pg/StartPay/${result.authority}`
paymentUrl: `${buildPaymentPageUrl(result.authority)}`
})
}
@@ -487,7 +489,7 @@ const initiateProjectCompletionPaymentWeb = async (req, res) => {
if (result?.code === 100 && result?.authority) {
return res.status(200).json({
authority: result.authority,
paymentUrl: `https://www.zarinpal.com/pg/StartPay/${result.authority}`
paymentUrl: `${buildPaymentPageUrl(result.authority)}`
})
}
@@ -914,38 +916,30 @@ const initiateOfferPaymentWeb = async (req, res) => {
})
}
// 🎁 free offer
if (offerType === 'free' && user.monthly_free_offer > 0) {
// 🎁 رایگان: فقط وقتی مجازه که گیرنده «دریافت هدیه» یا «دریافت پرداختی از چت» رو
// غیرفعال کرده باشه، و سهمیه‌ی رایگان ماهانه‌ی فرستنده هنوز باقی باشه.
// درخواستِ پولی (offerType غیر از free) همیشه با قیمت واقعی ثبت می‌شه
if (offerType === 'free') {
const receiverFreeEligible = receiver.gift_enabled === false || receiver.offer_payment_enabled === false
if (!receiverFreeEligible) {
return res.status(403).json({
error: true,
message: 'برای درخواست رایگان، گیرنده باید دریافت هدیه یا دریافت پرداختی از چت را غیرفعال کرده باشد'
})
}
if (!canUseFreeCreation(user)) {
return res.status(403).json({ error: true, message: 'سهمیه درخواست رایگان ماهانه شما تمام شده است.' })
}
const consumed = await consumeFreeCreation(user)
if (!consumed.ok) {
return res.status(403).json({ error: true, message: 'سهمیه درخواست رایگان ماهانه شما تمام شده است.' })
}
const offer = await OfferModel.create({
sender: userId,
receiver: receiverId,
transaction_id: null
})
// updateOne + $inc به‌جای user.save() — save() کل سند رو ری‌ولیدیت می‌کنه و برای
// کاربرهایی که مقدار قدیمی/نامعتبر توی فیلدهای دیگه (مثلاً income_range) دارن
// با ValidationError شکست می‌خوره، حتی وقتی فقط monthly_free_offer تغییر کرده
await UserModel.updateOne({ _id: userId }, { $inc: { monthly_free_offer: -1 } })
return res.json({
type: 'free',
offerId: offer._id
})
}
// 🎁 گیرنده «دریافت پرداختی از چت» رو غیرفعال کرده — درخواست همکاری براش همیشه
// رایگانه، مستقل از سهمیه‌ی رایگان ماهانه‌ی فرستنده
if (receiver.offer_payment_enabled === false) {
const offer = await OfferModel.create({
sender: userId,
receiver: receiverId,
transaction_id: null
})
return res.json({
type: 'free',
offerId: offer._id
})
return res.json({ type: 'free', offerId: offer._id })
}
// نوعِ «رایگان» قیمتی توی OfferTypeModel نداره (اصلاً پرایس‌دار نیست، سهمیه‌محوره) —
@@ -975,7 +969,7 @@ const initiateOfferPaymentWeb = async (req, res) => {
})
await debitWalletForPurchase(userId, walletAmount, {
referenceType: 'offer',
referenceType: 'payment',
referenceId: offer._id,
description: 'پرداخت آفر از کیف پول'
})
@@ -985,7 +979,7 @@ const initiateOfferPaymentWeb = async (req, res) => {
const amount = toRial(gatewayAmount)
const platformSuffix = platform ? `&platform=${encodeURIComponent(platform)}` : ''
const callbackUrl = `https://api.modstagram.com/api/v1/offers/payment-web?receiverId=${receiverId}&offerType=${offerType}&userId=${userId}&walletAmount=${walletAmount}${platformSuffix}`
const callbackUrl = `${(process.env.APP_SITE_CallBack || process.env.APP_URL || '').replace(/\/$/, '')}/offers/payment-web?receiverId=${receiverId}&offerType=${offerType}&userId=${userId}&walletAmount=${walletAmount}${platformSuffix}`
const zarinpalRes = await axios.post(
'https://api.zarinpal.com/pg/v4/payment/request.json',
@@ -1010,7 +1004,7 @@ const initiateOfferPaymentWeb = async (req, res) => {
if (result.code === 100) {
return res.json({
authority: result.authority,
paymentUrl: `https://www.zarinpal.com/pg/StartPay/${result.authority}`
paymentUrl: `${buildPaymentPageUrl(result.authority)}`
})
}
@@ -1086,7 +1080,7 @@ const handleOfferPaymentCallback = async (req, res) => {
// درگاه فقط بخشِ باقی‌مونده رو تایید کرد؛ حالا که پرداخت قطعاً موفق شد، سهمِ کیف
// پول هم واقعاً کسر می‌شه (نگاه کن به utils/walletSplit.js)
await debitWalletForPurchase(userId, usedWalletAmount, {
referenceType: 'offer',
referenceType: 'payment',
referenceId: offer._id,
description: 'پرداخت آفر از کیف پول'
})