1.1
This commit is contained in:
parent
a6bf68668a
commit
6909bf7269
@ -88,6 +88,11 @@ router.use(checkCrudPermissions('jobs'));
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*/
|
||||
router.post('/:id/dispute', wrapAsync(async (req, res) => {
|
||||
const result = await JobsService.dispute(req.params.id, req.currentUser);
|
||||
res.status(200).send(result);
|
||||
}));
|
||||
|
||||
router.post('/', wrapAsync(async (req, res) => {
|
||||
const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`;
|
||||
const link = new URL(referer);
|
||||
|
||||
@ -85,6 +85,11 @@ router.use(checkCrudPermissions('service_listings'));
|
||||
* 500:
|
||||
* description: Some server error
|
||||
*/
|
||||
router.post('/:id/purchase', wrapAsync(async (req, res) => {
|
||||
const result = await Service_listingsService.purchase(req.params.id, req.currentUser);
|
||||
res.status(200).send(result);
|
||||
}));
|
||||
|
||||
router.post('/', wrapAsync(async (req, res) => {
|
||||
const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`;
|
||||
const link = new URL(referer);
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
const db = require("../db/models");
|
||||
const UsersDBApi = require('../db/api/users');
|
||||
const ValidationError = require('./notifications/errors/validation');
|
||||
const ForbiddenError = require('./notifications/errors/forbidden');
|
||||
@ -59,6 +60,7 @@ class Auth {
|
||||
firstName: email.split('@')[0],
|
||||
password: hashedPassword,
|
||||
email: email,
|
||||
is_verified_student: email.toLowerCase().endsWith('.edu'),
|
||||
|
||||
},
|
||||
options,
|
||||
|
||||
@ -12,6 +12,34 @@ const stream = require('stream');
|
||||
|
||||
|
||||
module.exports = class JobsService {
|
||||
static async dispute(id, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
try {
|
||||
const job = await JobsDBApi.findBy({ id }, { transaction });
|
||||
if (!job) throw new Error('Job not found');
|
||||
|
||||
await db.jobs.update({ status: 'Disputed' }, { where: { id }, transaction });
|
||||
|
||||
if (db.escrow_transactions) {
|
||||
await db.escrow_transactions.update({ status: 'Held_in_Escrow' }, { where: { jobId: id }, transaction });
|
||||
}
|
||||
|
||||
await db.disputes.create({
|
||||
opened_byId: currentUser.id,
|
||||
subject: `Dispute for Job: ${job.title || id}`,
|
||||
description: `Dispute raised by ${currentUser.email} for Job ${id}`,
|
||||
status: 'open',
|
||||
opened_at: new Date()
|
||||
}, { transaction });
|
||||
|
||||
await transaction.commit();
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async create(data, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
try {
|
||||
@ -133,6 +161,4 @@ module.exports = class JobsService {
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
};
|
||||
@ -12,6 +12,42 @@ const stream = require('stream');
|
||||
|
||||
|
||||
module.exports = class Service_listingsService {
|
||||
static async purchase(id, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
try {
|
||||
const service = await Service_listingsDBApi.findBy({ id }, { transaction });
|
||||
if (!service) throw new Error('Service not found');
|
||||
|
||||
const job = await db.jobs.create({
|
||||
clientId: currentUser.id,
|
||||
title: `Order: ${service.title}`,
|
||||
description: service.description,
|
||||
budget: service.starting_price,
|
||||
status: 'In-Progress'
|
||||
}, { transaction });
|
||||
|
||||
const client_fee = Number(service.starting_price) * 0.01;
|
||||
const freelancer_fee = Number(service.starting_price) * 0.005;
|
||||
const total_amount = Number(service.starting_price) + client_fee;
|
||||
|
||||
const escrow = await db.escrow_transactions.create({
|
||||
jobId: job.id,
|
||||
clientId: currentUser.id,
|
||||
freelancerId: service.freelancerId,
|
||||
total_amount: total_amount,
|
||||
client_fee: client_fee,
|
||||
freelancer_fee: freelancer_fee,
|
||||
status: 'Held_in_Escrow'
|
||||
}, { transaction });
|
||||
|
||||
await transaction.commit();
|
||||
return { job, escrow };
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async create(data, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
try {
|
||||
@ -133,6 +169,4 @@ module.exports = class Service_listingsService {
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@ -17,8 +17,10 @@ export default function ServiceDetailsPage() {
|
||||
const router = useRouter();
|
||||
const { id } = router.query;
|
||||
const projectName = useAppSelector((state) => state.style.projectName) || 'FreelanceFusion';
|
||||
const { currentUser } = useAppSelector((state) => state.auth);
|
||||
const [service, setService] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [purchasing, setPurchasing] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (id && typeof id === 'string') {
|
||||
@ -36,6 +38,25 @@ export default function ServiceDetailsPage() {
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
const handlePurchase = async () => {
|
||||
if (!currentUser) {
|
||||
router.push('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
setPurchasing(true);
|
||||
try {
|
||||
await axios.post(`/service_listings/${id}/purchase`);
|
||||
alert('Gig purchased successfully! Your funds are held safely in escrow.');
|
||||
router.push('/jobs/jobs-list');
|
||||
} catch (err: any) {
|
||||
console.error('Failed to purchase gig:', err);
|
||||
alert('Failed to purchase gig. Please try again.');
|
||||
} finally {
|
||||
setPurchasing(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col bg-gray-50 dark:bg-slate-900">
|
||||
@ -65,7 +86,7 @@ export default function ServiceDetailsPage() {
|
||||
className="px-8 py-3 text-lg font-bold uppercase shadow-lg hover:shadow-indigo-200"
|
||||
label="Back to Directory"
|
||||
icon={mdiArrowLeft}
|
||||
href="/web_pages/services"
|
||||
onClick={() => router.push('/web_pages/services')}
|
||||
color="indigo"
|
||||
/>
|
||||
</div>
|
||||
@ -106,11 +127,18 @@ export default function ServiceDetailsPage() {
|
||||
{service.freelancer?.firstName?.[0] || 'S'}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-black text-gray-900 dark:text-white uppercase tracking-tight text-lg">Student Freelancer</p>
|
||||
<p className="text-sm text-gray-500 font-bold uppercase tracking-wider flex items-center gap-2">
|
||||
<BaseIcon path={mdiCheckDecagram} className="text-blue-500" size={16} />
|
||||
Identity Verified
|
||||
</p>
|
||||
<p className="font-black text-gray-900 dark:text-white uppercase tracking-tight text-lg">{service.freelancer?.firstName || 'Student Freelancer'}</p>
|
||||
{service.freelancer?.is_verified_student ? (
|
||||
<p className="text-sm text-green-600 dark:text-green-400 font-bold uppercase tracking-wider flex items-center gap-2">
|
||||
<BaseIcon path={mdiCheckDecagram} className="text-green-500" size={16} />
|
||||
Verified Student
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-sm text-gray-500 font-bold uppercase tracking-wider flex items-center gap-2">
|
||||
<BaseIcon path={mdiCheckDecagram} className="text-blue-500" size={16} />
|
||||
Identity Verified
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -140,16 +168,16 @@ export default function ServiceDetailsPage() {
|
||||
</div>
|
||||
|
||||
<BaseButton
|
||||
label="Order Gig"
|
||||
label={purchasing ? "Processing..." : (currentUser ? "Order Gig Instantly" : "Login to Order")}
|
||||
color="indigo"
|
||||
className="w-full mt-6 font-black py-5 text-xl uppercase tracking-widest shadow-xl hover:shadow-indigo-200 transition-all rounded-2xl"
|
||||
href="/login"
|
||||
onClick={handlePurchase}
|
||||
disabled={purchasing}
|
||||
/>
|
||||
|
||||
<div className="pt-6 border-t border-gray-50 dark:border-slate-700 text-center">
|
||||
<p className="text-[10px] uppercase font-black text-gray-400 tracking-widest mb-2">Secure Gig Economy</p>
|
||||
<div className="flex justify-center gap-4 opacity-30 grayscale hover:grayscale-0 transition-all">
|
||||
{/* Payment icons could go here */}
|
||||
<BaseIcon path={mdiCurrencyUsd} size={20} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user