full
This commit is contained in:
292
controllers/application/wallet/walletController.js
Normal file
292
controllers/application/wallet/walletController.js
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user