full
This commit is contained in:
194
controllers/application/bookings/bookingConfigController.js
Normal file
194
controllers/application/bookings/bookingConfigController.js
Normal file
@@ -0,0 +1,194 @@
|
||||
const BookingConfigModel = require('../../../models/BookingConfigModel')
|
||||
const UserModel = require('../../../models/UserModel')
|
||||
|
||||
const WEEK_DAYS = ['sat', 'sun', 'mon', 'tue', 'wed', 'thu', 'fri']
|
||||
|
||||
function bookingError (res, status, message) {
|
||||
return res.status(status).json({ error: true, message })
|
||||
}
|
||||
|
||||
async function findOwnedConfig (configId, userId) {
|
||||
const config = await BookingConfigModel.findById(configId)
|
||||
if (!config) return { error: 'NOT_FOUND' }
|
||||
if (String(config.owner) !== String(userId)) return { error: 'FORBIDDEN' }
|
||||
return { config }
|
||||
}
|
||||
|
||||
const createDraftBookingConfig = async (req, res, next) => {
|
||||
try {
|
||||
const { name } = req.body
|
||||
if (!name) {
|
||||
return bookingError(res, 422, 'نام رزرو الزامی است')
|
||||
}
|
||||
|
||||
const config = await BookingConfigModel.create({
|
||||
owner: req.user._id,
|
||||
name,
|
||||
status: 'draft'
|
||||
})
|
||||
|
||||
return res.json({ message: 'رزرو ایجاد شد', configId: config._id, config })
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
const getMyBookingConfigs = async (req, res, next) => {
|
||||
try {
|
||||
const configs = await BookingConfigModel.find({ owner: req.user._id }).sort({ createdAt: -1 })
|
||||
return res.json({ configs })
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
const getBookingConfigById = async (req, res, next) => {
|
||||
try {
|
||||
const { config, error } = await findOwnedConfig(req.params.id, req.user._id)
|
||||
if (error === 'NOT_FOUND') return bookingError(res, 404, 'رزرو یافت نشد')
|
||||
if (error === 'FORBIDDEN') return bookingError(res, 403, 'شما مالک این رزرو نیستید')
|
||||
return res.json({ config })
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
const updateBookingConfigServices = async (req, res, next) => {
|
||||
try {
|
||||
const { config, error } = await findOwnedConfig(req.params.id, req.user._id)
|
||||
if (error === 'NOT_FOUND') return bookingError(res, 404, 'رزرو یافت نشد')
|
||||
if (error === 'FORBIDDEN') return bookingError(res, 403, 'شما مالک این رزرو نیستید')
|
||||
|
||||
const { serviceIds } = req.body
|
||||
if (!Array.isArray(serviceIds) || serviceIds.length === 0) {
|
||||
return bookingError(res, 422, 'حداقل یک خدمت انتخاب کنید')
|
||||
}
|
||||
|
||||
const user = await UserModel.findById(req.user._id).select('services')
|
||||
const selected = (user.services || []).filter((service) =>
|
||||
serviceIds.includes(String(service._id))
|
||||
)
|
||||
if (selected.length === 0) {
|
||||
return bookingError(res, 422, 'خدمات انتخابشده یافت نشد')
|
||||
}
|
||||
|
||||
config.services = selected.map((service) => ({
|
||||
serviceId: String(service._id),
|
||||
title: service.title,
|
||||
price: Number(service.discountPrice || service.originalPrice || 0),
|
||||
image: service.image || null
|
||||
}))
|
||||
await config.save()
|
||||
|
||||
return res.json({ message: 'خدمات ذخیره شد', config })
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
const updateBookingConfigSchedule = async (req, res, next) => {
|
||||
try {
|
||||
const { config, error } = await findOwnedConfig(req.params.id, req.user._id)
|
||||
if (error === 'NOT_FOUND') return bookingError(res, 404, 'رزرو یافت نشد')
|
||||
if (error === 'FORBIDDEN') return bookingError(res, 403, 'شما مالک این رزرو نیستید')
|
||||
|
||||
const { weeklySchedule, holidayDates, depositRequired, weeklyRepeatCount } = req.body
|
||||
|
||||
if (weeklySchedule !== undefined) {
|
||||
if (!Array.isArray(weeklySchedule)) {
|
||||
return bookingError(res, 422, 'ساعات کاری نامعتبر است')
|
||||
}
|
||||
for (const entry of weeklySchedule) {
|
||||
if (!WEEK_DAYS.includes(entry.day)) {
|
||||
return bookingError(res, 422, 'روز هفته نامعتبر است')
|
||||
}
|
||||
}
|
||||
config.weeklySchedule = weeklySchedule
|
||||
}
|
||||
|
||||
if (holidayDates !== undefined) {
|
||||
if (!Array.isArray(holidayDates)) {
|
||||
return bookingError(res, 422, 'روزهای تعطیل نامعتبر است')
|
||||
}
|
||||
config.holidayDates = holidayDates.map((d) => new Date(d))
|
||||
}
|
||||
|
||||
if (depositRequired !== undefined) {
|
||||
config.depositRequired = Boolean(depositRequired)
|
||||
}
|
||||
|
||||
if (weeklyRepeatCount !== undefined) {
|
||||
if (weeklyRepeatCount === null || weeklyRepeatCount === '') {
|
||||
config.weeklyRepeatCount = null
|
||||
} else {
|
||||
const count = Number(weeklyRepeatCount)
|
||||
if (!Number.isInteger(count) || count < 1) {
|
||||
return bookingError(res, 422, 'تعداد تکرار هفته نامعتبر است')
|
||||
}
|
||||
config.weeklyRepeatCount = count
|
||||
}
|
||||
}
|
||||
|
||||
await config.save()
|
||||
|
||||
return res.json({ message: 'ساعات کاری ذخیره شد', config })
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
const updateBookingConfigStatus = async (req, res, next) => {
|
||||
try {
|
||||
const { config, error } = await findOwnedConfig(req.params.id, req.user._id)
|
||||
if (error === 'NOT_FOUND') return bookingError(res, 404, 'رزرو یافت نشد')
|
||||
if (error === 'FORBIDDEN') return bookingError(res, 403, 'شما مالک این رزرو نیستید')
|
||||
|
||||
const { status } = req.body
|
||||
if (!['active', 'inactive'].includes(status)) {
|
||||
return bookingError(res, 422, 'وضعیت نامعتبر است')
|
||||
}
|
||||
|
||||
config.status = status
|
||||
// فعالسازی/تمدید دستی: شمارش تکرار هفتگی از همین لحظه دوباره شروع میشه
|
||||
if (status === 'active' && config.weeklyRepeatCount) {
|
||||
config.repeatStartedAt = new Date()
|
||||
}
|
||||
await config.save()
|
||||
|
||||
return res.json({ message: 'وضعیت رزرو بهروزرسانی شد', config })
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
const submitBookingConfig = async (req, res, next) => {
|
||||
try {
|
||||
const { config, error } = await findOwnedConfig(req.params.id, req.user._id)
|
||||
if (error === 'NOT_FOUND') return bookingError(res, 404, 'رزرو یافت نشد')
|
||||
if (error === 'FORBIDDEN') return bookingError(res, 403, 'شما مالک این رزرو نیستید')
|
||||
|
||||
if (!config.name || !config.services?.length || !config.weeklySchedule?.length) {
|
||||
return bookingError(res, 422, 'اطلاعات رزرو ناقص است')
|
||||
}
|
||||
|
||||
config.status = 'active'
|
||||
if (config.weeklyRepeatCount) {
|
||||
config.repeatStartedAt = new Date()
|
||||
}
|
||||
await config.save()
|
||||
|
||||
return res.json({ message: 'رزرو با موفقیت ثبت شد', config })
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createDraftBookingConfig,
|
||||
getMyBookingConfigs,
|
||||
getBookingConfigById,
|
||||
updateBookingConfigServices,
|
||||
updateBookingConfigSchedule,
|
||||
updateBookingConfigStatus,
|
||||
submitBookingConfig
|
||||
}
|
||||
299
controllers/application/bookings/bookingController.js
Normal file
299
controllers/application/bookings/bookingController.js
Normal file
@@ -0,0 +1,299 @@
|
||||
const BookingConfigModel = require('../../../models/BookingConfigModel')
|
||||
const BookingModel = require('../../../models/BookingModel')
|
||||
const { notifyUser } = require('../../../utils/notifyUser')
|
||||
|
||||
const JS_DAY_TO_KEY = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']
|
||||
|
||||
function bookingError (res, status, message) {
|
||||
return res.status(status).json({ error: true, message })
|
||||
}
|
||||
|
||||
function toDateOnlyString (date) {
|
||||
return new Date(date).toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
/** null if the "تکرار هفته" window never ends (or isn't set); otherwise the Date it stops repeating. */
|
||||
function getRepeatExpiresAt (config) {
|
||||
if (!config.weeklyRepeatCount || !config.repeatStartedAt) return null
|
||||
return new Date(
|
||||
new Date(config.repeatStartedAt).getTime() + config.weeklyRepeatCount * 7 * 24 * 60 * 60 * 1000
|
||||
)
|
||||
}
|
||||
|
||||
/** Public: the buyer-facing view of a user's active booking config, for their profile page. */
|
||||
const getPublicBookingConfig = async (req, res, next) => {
|
||||
try {
|
||||
const config = await BookingConfigModel.findOne({
|
||||
owner: req.params.userId,
|
||||
status: 'active'
|
||||
})
|
||||
if (!config) return res.json({ config: null })
|
||||
return res.json({ config, repeatExpiresAt: getRepeatExpiresAt(config) })
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
/** Free time slots for one date: this weekday's ranges, minus holidays, minus already-booked ranges. */
|
||||
const getAvailability = async (req, res, next) => {
|
||||
try {
|
||||
const { configId, date } = req.query
|
||||
if (!configId || !date) {
|
||||
return bookingError(res, 422, 'اطلاعات درخواست ناقص است')
|
||||
}
|
||||
|
||||
const config = await BookingConfigModel.findOne({ _id: configId, status: 'active' })
|
||||
if (!config) return bookingError(res, 404, 'رزرو یافت نشد')
|
||||
|
||||
const targetDate = new Date(date)
|
||||
const isHoliday = (config.holidayDates || []).some(
|
||||
(d) => toDateOnlyString(d) === toDateOnlyString(targetDate)
|
||||
)
|
||||
if (isHoliday) return res.json({ slots: [] })
|
||||
|
||||
const repeatExpiresAt = getRepeatExpiresAt(config)
|
||||
if (repeatExpiresAt && targetDate >= repeatExpiresAt) return res.json({ slots: [] })
|
||||
|
||||
const dayKey = JS_DAY_TO_KEY[targetDate.getDay()]
|
||||
const dayEntry = (config.weeklySchedule || []).find((entry) => entry.day === dayKey)
|
||||
if (!dayEntry || !dayEntry.ranges?.length) return res.json({ slots: [] })
|
||||
|
||||
const startOfDay = new Date(targetDate)
|
||||
startOfDay.setHours(0, 0, 0, 0)
|
||||
const endOfDay = new Date(targetDate)
|
||||
endOfDay.setHours(23, 59, 59, 999)
|
||||
|
||||
const existingBookings = await BookingModel.find({
|
||||
bookingConfig: configId,
|
||||
date: { $gte: startOfDay, $lte: endOfDay },
|
||||
status: { $ne: 'cancelled' }
|
||||
})
|
||||
|
||||
const bookedRanges = new Set(
|
||||
existingBookings.map((b) => `${b.startTime}-${b.endTime}`)
|
||||
)
|
||||
|
||||
const slots = dayEntry.ranges.filter(
|
||||
(range) => !bookedRanges.has(`${range.start_time}-${range.end_time}`)
|
||||
)
|
||||
|
||||
return res.json({ slots })
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
/** Creates a booking. Confirmed immediately if no deposit required, otherwise pending_payment. */
|
||||
const createBooking = async (req, res, next) => {
|
||||
try {
|
||||
const { configId, serviceId, date, startTime, endTime } = req.body
|
||||
if (!configId || !serviceId || !date || !startTime || !endTime) {
|
||||
return bookingError(res, 422, 'اطلاعات رزرو ناقص است')
|
||||
}
|
||||
|
||||
const config = await BookingConfigModel.findOne({ _id: configId, status: 'active' })
|
||||
if (!config) return bookingError(res, 404, 'رزرو یافت نشد')
|
||||
|
||||
const service = (config.services || []).find((s) => s.serviceId === serviceId)
|
||||
if (!service) return bookingError(res, 422, 'خدمت یافت نشد')
|
||||
|
||||
const targetDate = new Date(date)
|
||||
const isHoliday = (config.holidayDates || []).some(
|
||||
(d) => toDateOnlyString(d) === toDateOnlyString(targetDate)
|
||||
)
|
||||
if (isHoliday) return bookingError(res, 422, 'این روز تعطیل است')
|
||||
|
||||
const repeatExpiresAt = getRepeatExpiresAt(config)
|
||||
if (repeatExpiresAt && targetDate >= repeatExpiresAt) {
|
||||
return bookingError(res, 422, 'مهلت تکرار هفتگی این رزرو به پایان رسیده است')
|
||||
}
|
||||
|
||||
const dayKey = JS_DAY_TO_KEY[targetDate.getDay()]
|
||||
const dayEntry = (config.weeklySchedule || []).find((entry) => entry.day === dayKey)
|
||||
const validRange = dayEntry?.ranges?.some(
|
||||
(r) => r.start_time === startTime && r.end_time === endTime
|
||||
)
|
||||
if (!validRange) return bookingError(res, 422, 'این بازه زمانی معتبر نیست')
|
||||
|
||||
const startOfDay = new Date(targetDate)
|
||||
startOfDay.setHours(0, 0, 0, 0)
|
||||
const endOfDay = new Date(targetDate)
|
||||
endOfDay.setHours(23, 59, 59, 999)
|
||||
|
||||
const alreadyBooked = await BookingModel.findOne({
|
||||
bookingConfig: configId,
|
||||
date: { $gte: startOfDay, $lte: endOfDay },
|
||||
startTime,
|
||||
endTime,
|
||||
status: { $ne: 'cancelled' }
|
||||
})
|
||||
if (alreadyBooked) return bookingError(res, 422, 'این بازه زمانی قبلاً رزرو شده است')
|
||||
|
||||
const depositRequired = Boolean(config.depositRequired)
|
||||
const depositAmount = depositRequired
|
||||
? Math.round(service.price * 0.3)
|
||||
: 0
|
||||
|
||||
const booking = await BookingModel.create({
|
||||
bookingConfig: config._id,
|
||||
provider: config.owner,
|
||||
buyer: req.user._id,
|
||||
serviceId,
|
||||
serviceTitle: service.title,
|
||||
servicePrice: service.price,
|
||||
date: targetDate,
|
||||
startTime,
|
||||
endTime,
|
||||
depositRequired,
|
||||
depositAmount,
|
||||
status: depositRequired ? 'pending_payment' : 'confirmed'
|
||||
})
|
||||
|
||||
if (!depositRequired) {
|
||||
await notifyUser(req.app.get('io'), config.owner, {
|
||||
type: 'booking',
|
||||
title: 'رزرو جدید',
|
||||
description: `خدمت «${service.title}» در تاریخ ${toDateOnlyString(targetDate)} ساعت ${startTime} رزرو شد`,
|
||||
project_post_id: booking._id
|
||||
})
|
||||
}
|
||||
|
||||
return res.json({ message: 'رزرو ثبت شد', booking })
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
/** Buyer's own bookings, or (role=provider) bookings received against the caller's own configs. */
|
||||
const getMyBookings = async (req, res, next) => {
|
||||
try {
|
||||
const role = req.query.role === 'provider' ? 'provider' : 'buyer'
|
||||
const filter = role === 'provider'
|
||||
? { provider: req.user._id }
|
||||
: { buyer: req.user._id }
|
||||
|
||||
const bookings = await BookingModel.find(filter)
|
||||
.populate('buyer', 'first_name last_name user_name profile_image user_level is_verified verify_badge')
|
||||
.populate('provider', 'first_name last_name user_name profile_image user_level is_verified verify_badge')
|
||||
.sort({ date: -1, createdAt: -1 })
|
||||
|
||||
return res.json({ bookings })
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
const BOOKING_ANALYTICS_RANGE_DAYS = { '7d': 7, '30d': 30, '90d': 90, all: null }
|
||||
|
||||
/** Revenue-over-time + top services + status breakdown for the provider's bookings — backs the "Booking" analytics tab. */
|
||||
const getBookingAnalytics = async (req, res, next) => {
|
||||
try {
|
||||
const userId = req.user._id
|
||||
const range = BOOKING_ANALYTICS_RANGE_DAYS[req.query.range] !== undefined ? req.query.range : '30d'
|
||||
const days = BOOKING_ANALYTICS_RANGE_DAYS[range]
|
||||
const since = days ? new Date(Date.now() - days * 24 * 60 * 60 * 1000) : new Date(0)
|
||||
|
||||
const confirmedMatch = { provider: userId, status: 'confirmed', date: { $gte: since } }
|
||||
|
||||
const [series, totalsAgg, topServicesAgg, statusCounts] = await Promise.all([
|
||||
BookingModel.aggregate([
|
||||
{ $match: confirmedMatch },
|
||||
{
|
||||
$group: {
|
||||
_id: { $dateToString: { format: '%Y-%m-%d', date: '$date' } },
|
||||
revenue: { $sum: '$servicePrice' },
|
||||
count: { $sum: 1 }
|
||||
}
|
||||
},
|
||||
{ $sort: { _id: 1 } }
|
||||
]),
|
||||
BookingModel.aggregate([
|
||||
{ $match: confirmedMatch },
|
||||
{ $group: { _id: null, totalRevenue: { $sum: '$servicePrice' }, totalCount: { $sum: 1 } } }
|
||||
]),
|
||||
BookingModel.aggregate([
|
||||
{ $match: confirmedMatch },
|
||||
{ $group: { _id: '$serviceTitle', revenue: { $sum: '$servicePrice' }, count: { $sum: 1 } } },
|
||||
{ $sort: { revenue: -1 } },
|
||||
{ $limit: 5 }
|
||||
]),
|
||||
BookingModel.aggregate([
|
||||
{ $match: { provider: userId, date: { $gte: since } } },
|
||||
{ $group: { _id: '$status', count: { $sum: 1 } } }
|
||||
])
|
||||
])
|
||||
|
||||
const topItems = topServicesAgg
|
||||
.filter((row) => row._id)
|
||||
.map((row) => ({ id: row._id, title: row._id, image: null, metric: row.revenue }))
|
||||
|
||||
const statusBreakdown = { pending_payment: 0, confirmed: 0, cancelled: 0 }
|
||||
for (const row of statusCounts) {
|
||||
if (row._id in statusBreakdown) statusBreakdown[row._id] = row.count
|
||||
}
|
||||
|
||||
return res.json({
|
||||
range,
|
||||
series,
|
||||
totals: {
|
||||
totalRevenue: totalsAgg[0]?.totalRevenue || 0,
|
||||
totalCount: totalsAgg[0]?.totalCount || 0
|
||||
},
|
||||
topItems,
|
||||
rating: null,
|
||||
statusBreakdown
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
/** Provider cancels a reservation made against one of their booking configs. */
|
||||
const updateBookingStatus = async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const { action } = req.body
|
||||
|
||||
if (action !== 'cancel') {
|
||||
return bookingError(res, 400, 'نوع عملیات نامعتبر است')
|
||||
}
|
||||
|
||||
const booking = await BookingModel.findById(id)
|
||||
if (!booking) return bookingError(res, 404, 'رزرو یافت نشد')
|
||||
|
||||
if (booking.provider.toString() !== req.user._id.toString()) {
|
||||
return bookingError(res, 403, 'اجازهی تغییر این رزرو را ندارید')
|
||||
}
|
||||
|
||||
if (booking.status === 'cancelled') {
|
||||
return bookingError(res, 400, 'این رزرو قبلاً لغو شده است')
|
||||
}
|
||||
|
||||
booking.status = 'cancelled'
|
||||
await booking.save()
|
||||
|
||||
await notifyUser(req.app.get('io'), booking.buyer, {
|
||||
type: 'booking',
|
||||
title: 'رزرو لغو شد',
|
||||
description: `رزرو خدمت «${booking.serviceTitle}» در تاریخ ${toDateOnlyString(booking.date)} ساعت ${booking.startTime} توسط ارائهدهنده لغو شد`,
|
||||
project_post_id: booking._id
|
||||
})
|
||||
|
||||
return res.json({
|
||||
message: 'رزرو لغو شد',
|
||||
bookingId: String(booking._id),
|
||||
status: booking.status
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getPublicBookingConfig,
|
||||
getAvailability,
|
||||
createBooking,
|
||||
getMyBookings,
|
||||
updateBookingStatus,
|
||||
getBookingAnalytics
|
||||
}
|
||||
138
controllers/application/bookings/bookingPaymentController.js
Normal file
138
controllers/application/bookings/bookingPaymentController.js
Normal file
@@ -0,0 +1,138 @@
|
||||
const { default: axios } = require('axios')
|
||||
const BookingModel = require('../../../models/BookingModel')
|
||||
const PaymentModel = require('../../../models/PaymentModel')
|
||||
const { notifyUser } = require('../../../utils/notifyUser')
|
||||
const { BOOKING_COMMISSION_RATE, creditBookingShare } = require('../../../utils/bookingCommission')
|
||||
|
||||
const apiKey =
|
||||
process.env.ZARINPAL_MERCHANT_ID ||
|
||||
process.env.MERCHENT_CODE ||
|
||||
'c7c41e8a-918f-4741-bcd5-58f3bc51db73'
|
||||
|
||||
function toDateOnlyString (date) {
|
||||
return new Date(date).toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
/** Initiates the 30% deposit payment for a pending booking. */
|
||||
const initiateBookingPayment = async (req, res) => {
|
||||
try {
|
||||
const { bookingId } = req.body
|
||||
const booking = await BookingModel.findById(bookingId)
|
||||
if (!booking) {
|
||||
return res.status(404).json({ error: true, message: 'رزرو یافت نشد' })
|
||||
}
|
||||
if (String(booking.buyer) !== String(req.user._id)) {
|
||||
return res.status(403).json({ error: true, message: 'شما مالک این رزرو نیستید' })
|
||||
}
|
||||
if (booking.status !== 'pending_payment') {
|
||||
return res.status(400).json({ error: true, message: 'این رزرو در انتظار پرداخت نیست' })
|
||||
}
|
||||
|
||||
const callbackBase =
|
||||
process.env.APP_SITE_CallBack ||
|
||||
process.env.APP_URL ||
|
||||
''
|
||||
if (!callbackBase) {
|
||||
return res.status(500).json({ error: true, message: 'آدرس بازگشت پرداخت تنظیم نشده است' })
|
||||
}
|
||||
|
||||
const response = await axios.post(
|
||||
'https://www.zarinpal.com/pg/rest/WebGate/PaymentRequest.json',
|
||||
{
|
||||
MerchantID: apiKey,
|
||||
Amount: booking.depositAmount,
|
||||
Description: `بیعانه رزرو خدمت ${booking.serviceTitle}`,
|
||||
CallbackURL: `${callbackBase}/bookings/payment-callback/web?bookingId=${booking._id}`
|
||||
},
|
||||
{ timeout: 20000 }
|
||||
)
|
||||
|
||||
const data = response.data
|
||||
if (data.Status === 100 && data.Authority) {
|
||||
booking.paymentAuthority = data.Authority
|
||||
await booking.save()
|
||||
return res.status(200).json({
|
||||
authority: data.Authority,
|
||||
paymentUrl: `https://www.zarinpal.com/pg/StartPay/${data.Authority}`
|
||||
})
|
||||
}
|
||||
|
||||
console.error('initiateBookingPayment ZarinPal error:', data)
|
||||
return res.status(502).json({ error: true, message: 'درگاه پرداخت پاسخ نامعتبر داد. لطفاً دوباره تلاش کنید.' })
|
||||
} catch (error) {
|
||||
console.error('initiateBookingPayment error:', error?.response?.data || error?.message || error)
|
||||
return res.status(500).json({ error: 'Internal server error' })
|
||||
}
|
||||
}
|
||||
|
||||
const handleBookingPaymentCallback = async (req, res) => {
|
||||
const { Authority, Status, bookingId } = req.query
|
||||
const failRedirect = `${process.env.APP_SITE}/settings/booking/payment/failed?bookingId=${bookingId || ''}`
|
||||
const successRedirect = `${process.env.APP_SITE}/settings/booking/payment/success?bookingId=${bookingId || ''}`
|
||||
|
||||
try {
|
||||
if (Status !== 'OK') return res.redirect(failRedirect)
|
||||
|
||||
const booking = await BookingModel.findById(bookingId)
|
||||
if (!booking) return res.redirect(failRedirect)
|
||||
|
||||
if (booking.status === 'confirmed') {
|
||||
return res.redirect(successRedirect)
|
||||
}
|
||||
|
||||
const verificationResponse = await axios.post(
|
||||
'https://www.zarinpal.com/pg/rest/WebGate/PaymentVerification.json',
|
||||
{ MerchantID: apiKey, Authority, Amount: booking.depositAmount }
|
||||
)
|
||||
|
||||
const verificationData = verificationResponse.data
|
||||
if (verificationData.Status !== 100 && verificationData.Status !== 101) {
|
||||
await PaymentModel.create({
|
||||
amount: booking.depositAmount,
|
||||
status: 'failed',
|
||||
authority: Authority,
|
||||
user_id: booking.buyer,
|
||||
type: 'booking_deposit'
|
||||
})
|
||||
return res.redirect(failRedirect)
|
||||
}
|
||||
|
||||
const payment = await PaymentModel.create({
|
||||
amount: booking.depositAmount,
|
||||
status: 'successful',
|
||||
authority: Authority,
|
||||
user_id: booking.buyer,
|
||||
type: 'booking_deposit'
|
||||
})
|
||||
|
||||
booking.depositPaid = true
|
||||
booking.status = 'confirmed'
|
||||
await booking.save()
|
||||
|
||||
const netAmount = Math.round(booking.depositAmount * (1 - BOOKING_COMMISSION_RATE))
|
||||
await creditBookingShare(
|
||||
req.app.get('io'),
|
||||
booking.provider,
|
||||
netAmount,
|
||||
payment._id,
|
||||
`بیعانه رزرو خدمت «${booking.serviceTitle}» (پس از کسر ۵٪ کارمزد)`
|
||||
)
|
||||
|
||||
await notifyUser(req.app.get('io'), booking.provider, {
|
||||
type: 'booking',
|
||||
title: 'رزرو جدید',
|
||||
description: `خدمت «${booking.serviceTitle}» در تاریخ ${toDateOnlyString(booking.date)} ساعت ${booking.startTime} رزرو شد`,
|
||||
project_post_id: booking._id
|
||||
})
|
||||
|
||||
return res.redirect(successRedirect)
|
||||
} catch (error) {
|
||||
console.error('handleBookingPaymentCallback error:', error?.response?.data || error?.message || error)
|
||||
return res.redirect(failRedirect)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initiateBookingPayment,
|
||||
handleBookingPaymentCallback
|
||||
}
|
||||
Reference in New Issue
Block a user