This commit is contained in:
payacom
2026-08-31 20:45:41 +03:30
parent fa9f23c61d
commit 3f416a7a60
367 changed files with 621895 additions and 6443 deletions

View File

@@ -0,0 +1,292 @@
/* eslint-disable camelcase */
const axios = require('axios')
const mongoose = require('mongoose')
const ShopModel = require('../../../models/ShopModel')
const OrderModel = require('../../../models/OrderModel')
const UserModel = require('../../../models/UserModel')
const WalletTransactionModel = require('../../../models/WalletTransactionModel')
const WalletWithdrawalRequestModel = require('../../../models/WalletWithdrawalRequestModel')
const MERCHANT_ID = process.env.ZARINPAL_MERCHANT_ID || process.env.MERCHENT_CODE
const GIFT_WITHDRAWAL_MIN = 1000000
const CHARGE_BONUS_RATE = 0.05
function walletError (res, status, message) {
return res.status(status).json({ error: true, message })
}
function toRial (tomanAmount) {
return Number(tomanAmount) * 10
}
async function getBucketBalance (userId, bucket, shopId) {
const match = { user: new mongoose.Types.ObjectId(userId), bucket }
if (shopId) match.shop = new mongoose.Types.ObjectId(shopId)
const result = await WalletTransactionModel.aggregate([
{ $match: match },
{
$group: {
_id: null,
total: {
$sum: {
$cond: [{ $eq: ['$type', 'credit'] }, '$amount', { $multiply: ['$amount', -1] }]
}
}
}
}
])
return result[0]?.total || 0
}
async function getPendingShopBalance (userId, shopId) {
let shopIds
if (shopId) {
shopIds = [new mongoose.Types.ObjectId(shopId)]
} else {
shopIds = await ShopModel.find({ owner: userId }).distinct('_id')
}
if (shopIds.length === 0) return 0
const result = await OrderModel.aggregate([
{ $match: { shop: { $in: shopIds }, status: { $in: ['on_hold', 'processing', 'shipped', 'ready_for_pickup'] } } },
{ $group: { _id: null, total: { $sum: '$seller_net_amount' } } }
])
return result[0]?.total || 0
}
const getWalletSummary = async (req, res, next) => {
try {
const userId = req.user._id
const { shopId } = req.query
if (shopId) {
const ownedShop = await ShopModel.exists({ _id: shopId, owner: userId })
if (!ownedShop) return walletError(res, 403, 'شما مالک این فروشگاه نیستید')
}
const [shopAvailable, giftAvailable, chargedAvailable, shopPending, academyAvailable, projectAvailable, bookingAvailable] = await Promise.all([
getBucketBalance(userId, 'shop', shopId),
getBucketBalance(userId, 'gift'),
getBucketBalance(userId, 'charged'),
getPendingShopBalance(userId, shopId),
getBucketBalance(userId, 'academy'),
getBucketBalance(userId, 'project'),
getBucketBalance(userId, 'booking')
])
return res.json({
shop: { available: shopAvailable, pending: shopPending },
gift: { available: giftAvailable, minWithdrawal: GIFT_WITHDRAWAL_MIN },
charged: { available: chargedAvailable },
academy: { available: academyAvailable },
project: { available: projectAvailable },
booking: { available: bookingAvailable }
})
} catch (error) {
next(error)
}
}
/** Paginated wallet ledger — backs "Income & Settlement" / financial-history views. */
const getWalletTransactions = async (req, res, next) => {
try {
const userId = req.user._id
const { bucket, shopId, type, startDate, endDate, page = 1, limit = 15 } = req.query
const filter = { user: userId }
if (bucket) filter.bucket = bucket
if (type && ['credit', 'debit'].includes(type)) filter.type = type
if (shopId) {
if (!mongoose.Types.ObjectId.isValid(shopId)) {
return walletError(res, 422, 'شناسه فروشگاه نامعتبر است')
}
const ownedShop = await ShopModel.exists({ _id: shopId, owner: userId })
if (!ownedShop) return walletError(res, 403, 'شما مالک این فروشگاه نیستید')
filter.shop = shopId
}
if (startDate || endDate) {
filter.createdAt = {}
if (startDate) filter.createdAt.$gte = new Date(startDate)
if (endDate) filter.createdAt.$lte = new Date(endDate)
}
const result = await WalletTransactionModel.paginate(filter, {
page: parseInt(page),
limit: parseInt(limit),
sort: { createdAt: -1 },
populate: [{ path: 'shop', select: 'name logo' }]
})
return res.json({
docs: result.docs,
totalPages: result.totalPages,
totalDocs: result.totalDocs,
page: result.page
})
} catch (error) {
next(error)
}
}
const requestWithdrawal = async (req, res, next) => {
try {
const user = req.user
const { bucket, amount, shopId } = req.body
if (!['shop', 'gift', 'academy', 'project', 'booking'].includes(bucket)) {
return walletError(res, 422, 'نوع کیف پول نامعتبر است')
}
const requestedAmount = Number(amount)
if (!Number.isFinite(requestedAmount) || requestedAmount <= 0) {
return walletError(res, 422, 'مبلغ نامعتبر است')
}
if (!user.shaba) {
return walletError(res, 422, 'ابتدا شماره شبا خود را در تنظیمات ثبت کنید')
}
let ownedShopId = null
if (bucket === 'shop' && shopId) {
if (!mongoose.Types.ObjectId.isValid(shopId)) {
return walletError(res, 422, 'شناسه فروشگاه نامعتبر است')
}
const ownedShop = await ShopModel.exists({ _id: shopId, owner: user._id })
if (!ownedShop) return walletError(res, 403, 'شما مالک این فروشگاه نیستید')
ownedShopId = shopId
}
const available = await getBucketBalance(user._id, bucket, ownedShopId)
if (requestedAmount > available) {
return walletError(res, 422, 'مبلغ درخواستی بیشتر از موجودی قابل تسویه است')
}
if (bucket === 'gift' && available < GIFT_WITHDRAWAL_MIN) {
return walletError(res, 422, `موجودی هدیه باید حداقل ${GIFT_WITHDRAWAL_MIN.toLocaleString()} تومان باشد`)
}
const withdrawalRequest = await WalletWithdrawalRequestModel.create({
user: user._id,
bucket,
shop: ownedShopId,
amount: requestedAmount,
shabaSnapshot: user.shaba,
status: 'منتظر تایید'
})
await WalletTransactionModel.create({
user: user._id,
bucket,
shop: ownedShopId,
type: 'debit',
amount: requestedAmount,
referenceType: 'withdrawal',
referenceId: withdrawalRequest._id,
description: 'درخواست تسویه کیف پول'
})
return res.json({ message: 'درخواست تسویه ثبت شد', withdrawalRequest })
} catch (error) {
next(error)
}
}
const initiateWalletCharge = async (req, res, next) => {
try {
const { amount } = req.body
const chargeAmount = Number(amount)
if (!Number.isFinite(chargeAmount) || chargeAmount <= 0) {
return walletError(res, 422, 'مبلغ نامعتبر است')
}
const callbackBase = process.env.APP_SITE_CallBack || process.env.APP_URL
if (!callbackBase) {
return walletError(res, 500, 'آدرس بازگشت پرداخت تنظیم نشده است')
}
const response = await axios.post(
'https://api.zarinpal.com/pg/v4/payment/request.json',
{
merchant_id: MERCHANT_ID,
amount: toRial(chargeAmount),
description: 'شارژ کیف پول مدستاگرام',
callback_url: `${callbackBase}/wallet/charge/callback?userId=${req.user._id}&amount=${chargeAmount}`
},
{ headers: { 'Content-Type': 'application/json' } }
)
const result = response.data?.data
if (result?.code === 100 && result?.authority) {
return res.json({ authority: result.authority })
}
console.error('ZarinPal wallet charge initiate error:', response.data)
return walletError(res, 502, 'درگاه پرداخت پاسخ نامعتبر داد. لطفاً دوباره تلاش کنید.')
} catch (error) {
next(error)
}
}
const handleWalletChargeCallback = async (req, res) => {
const { Authority, Status, userId, amount } = req.query
const failRedirect = `${process.env.APP_SITE}/settings/wallet?charge=failed`
const successRedirect = `${process.env.APP_SITE}/settings/wallet?charge=success`
try {
if (Status !== 'OK') return res.redirect(failRedirect)
const chargeAmount = Number(amount)
const verify = await axios.post(
'https://api.zarinpal.com/pg/v4/payment/verify.json',
{
merchant_id: MERCHANT_ID,
authority: Authority,
amount: toRial(chargeAmount)
},
{ headers: { 'Content-Type': 'application/json' } }
)
const data = verify.data?.data
if (data?.code === 100 || data?.code === 101) {
const user = await UserModel.findById(userId)
if (!user) return res.redirect(failRedirect)
await WalletTransactionModel.create({
user: userId,
bucket: 'charged',
type: 'credit',
amount: chargeAmount,
referenceType: 'payment',
description: 'شارژ کیف پول'
})
const bonus = Math.round(chargeAmount * CHARGE_BONUS_RATE)
if (bonus > 0) {
await WalletTransactionModel.create({
user: userId,
bucket: 'gift',
type: 'credit',
amount: bonus,
referenceType: 'payment',
description: 'پاداش شارژ کیف پول (۵٪)'
})
}
return res.redirect(successRedirect)
}
return res.redirect(failRedirect)
} catch (error) {
console.error('handleWalletChargeCallback error:', error?.response?.data || error.message)
return res.redirect(failRedirect)
}
}
module.exports = {
getWalletSummary,
getWalletTransactions,
requestWithdrawal,
initiateWalletCharge,
handleWalletChargeCallback,
getBucketBalance
}