mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
158 lines
6.2 KiB
JavaScript
158 lines
6.2 KiB
JavaScript
'use strict';
|
|
const mdl_CoursePurchase = require('../../models/courses/course_purchases.mdl');
|
|
const mdl_Product = require('../../models/courses/products.mdl');
|
|
const paypal = require('../../services/paypal.service');
|
|
const R = require('../../utils/response.util');
|
|
|
|
// ─── CREATE ORDER ─────────────────────────────────────────────────────────────
|
|
|
|
exports.createCourseOrder = async (req, res) => {
|
|
try {
|
|
const { product_id } = req.body;
|
|
if (!product_id) return R.error(res, 'product_id is required.', 400);
|
|
|
|
const product = await mdl_Product.findOne({ where: { id: product_id, is_active: true } });
|
|
if (!product) return R.error(res, 'Product not found or inactive.', 404);
|
|
|
|
// Block if user already has an active completed purchase for this product
|
|
const existing = await mdl_CoursePurchase.findOne({
|
|
where: { user_id: req.user.user_id, product_id, status: 'completed' },
|
|
});
|
|
if (existing) {
|
|
const stillActive = !existing.expires_at || new Date(existing.expires_at) > new Date();
|
|
if (stillActive) return R.error(res, 'You already have active access to this course.', 409);
|
|
}
|
|
|
|
const ppOrder = await paypal.createOrder({
|
|
amount: Number(product.price).toFixed(2),
|
|
currency: product.currency,
|
|
referenceId: `user_${req.user.user_id}_product_${product_id}`,
|
|
returnUrl: `${process.env.FRONTEND_URL}/course/${product.course_id}/checkout`,
|
|
cancelUrl: `${process.env.FRONTEND_URL}/course/${product.course_id}/checkout?cancelled=true`,
|
|
});
|
|
|
|
const approvalUrl = ppOrder.links?.find((l) => l.rel === 'approve')?.href ?? null;
|
|
|
|
const expiresAt = product.access_days
|
|
? new Date(Date.now() + product.access_days * 86400000)
|
|
: null;
|
|
|
|
const purchase = await mdl_CoursePurchase.create({
|
|
user_id: req.user.user_id,
|
|
product_id,
|
|
amount: product.price,
|
|
currency: product.currency,
|
|
status: 'pending',
|
|
provider: 'paypal',
|
|
expires_at: expiresAt,
|
|
provider_payload: { order_id: ppOrder.id, approval_url: approvalUrl },
|
|
});
|
|
|
|
return R.success(res, 'Order created.', {
|
|
purchase_id: purchase.id,
|
|
order_id: ppOrder.id,
|
|
approval_url: approvalUrl,
|
|
amount: product.price,
|
|
currency: product.currency,
|
|
}, 201);
|
|
} catch (err) {
|
|
console.error('[CLIENT][COURSE PURCHASE][CREATE ORDER]', err);
|
|
return R.error(res, 'Could not create order.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── CAPTURE ORDER ────────────────────────────────────────────────────────────
|
|
|
|
exports.captureCourseOrder = async (req, res) => {
|
|
try {
|
|
const { order_id } = req.body;
|
|
if (!order_id) return R.error(res, 'order_id is required.', 400);
|
|
|
|
const purchase = await mdl_CoursePurchase.findOne({
|
|
where: { user_id: req.user.user_id, status: 'pending', provider: 'paypal' },
|
|
include: [{ model: mdl_Product, as: 'product' }],
|
|
order: [['createdAt', 'DESC']],
|
|
});
|
|
|
|
if (!purchase || purchase.provider_payload?.order_id !== order_id)
|
|
return R.error(res, 'Pending purchase not found.', 404);
|
|
|
|
let captureData;
|
|
try {
|
|
captureData = await paypal.captureOrder(order_id);
|
|
} catch (ppErr) {
|
|
await purchase.update({
|
|
status: 'failed',
|
|
provider_payload: { ...purchase.provider_payload, error: ppErr?.response?.data ?? {} },
|
|
});
|
|
return R.error(res, 'PayPal capture failed.', 402);
|
|
}
|
|
|
|
const capture = captureData.purchase_units?.[0]?.payments?.captures?.[0];
|
|
|
|
await purchase.update({
|
|
status: 'completed',
|
|
paid_at: new Date(),
|
|
provider_payload: {
|
|
...purchase.provider_payload,
|
|
capture_id: capture?.id,
|
|
payer_id: captureData.payer?.payer_id,
|
|
capture: captureData,
|
|
},
|
|
});
|
|
|
|
return R.success(res, 'Payment successful. Course access granted.', {
|
|
purchase_id: purchase.id,
|
|
expires_at: purchase.expires_at,
|
|
course_id: purchase.product.course_id,
|
|
});
|
|
} catch (err) {
|
|
console.error('[CLIENT][COURSE PURCHASE][CAPTURE]', err);
|
|
return R.error(res, 'Could not capture order.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── CANCEL ORDER ─────────────────────────────────────────────────────────────
|
|
|
|
exports.cancelCourseOrder = async (req, res) => {
|
|
try {
|
|
const { order_id } = req.body;
|
|
if (!order_id) return R.error(res, 'order_id is required.', 400);
|
|
|
|
const purchase = await mdl_CoursePurchase.findOne({
|
|
where: { user_id: req.user.user_id, status: 'pending', provider: 'paypal' },
|
|
order: [['createdAt', 'DESC']],
|
|
});
|
|
|
|
if (!purchase || purchase.provider_payload?.order_id !== order_id)
|
|
return R.error(res, 'Pending purchase not found.', 404);
|
|
|
|
await purchase.update({
|
|
status: 'cancelled',
|
|
provider_payload: { ...purchase.provider_payload, cancelled_at: new Date().toISOString() },
|
|
});
|
|
|
|
return R.success(res, 'Purchase cancelled.');
|
|
} catch (err) {
|
|
console.error('[CLIENT][COURSE PURCHASE][CANCEL]', err);
|
|
return R.error(res, 'Could not cancel purchase.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── MY PURCHASES ─────────────────────────────────────────────────────────────
|
|
|
|
exports.getMyPurchases = async (req, res) => {
|
|
try {
|
|
const purchases = await mdl_CoursePurchase.findAll({
|
|
where: { user_id: req.user.user_id },
|
|
include: [{ model: mdl_Product, as: 'product', attributes: ['id', 'name', 'course_id', 'access_days'] }],
|
|
attributes: { exclude: ['provider_payload'] },
|
|
order: [['createdAt', 'DESC']],
|
|
});
|
|
return R.success(res, 'Purchases retrieved.', purchases);
|
|
} catch (err) {
|
|
console.error('[CLIENT][COURSE PURCHASE][GET MINE]', err);
|
|
return R.error(res, 'Could not retrieve purchases.', 500);
|
|
}
|
|
};
|