1.1
This commit is contained in:
parent
a6bf68668a
commit
6909bf7269
@ -88,6 +88,11 @@ router.use(checkCrudPermissions('jobs'));
|
|||||||
* 500:
|
* 500:
|
||||||
* description: Some server error
|
* 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) => {
|
router.post('/', wrapAsync(async (req, res) => {
|
||||||
const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`;
|
const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`;
|
||||||
const link = new URL(referer);
|
const link = new URL(referer);
|
||||||
|
|||||||
@ -85,6 +85,11 @@ router.use(checkCrudPermissions('service_listings'));
|
|||||||
* 500:
|
* 500:
|
||||||
* description: Some server error
|
* 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) => {
|
router.post('/', wrapAsync(async (req, res) => {
|
||||||
const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`;
|
const referer = req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`;
|
||||||
const link = new URL(referer);
|
const link = new URL(referer);
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
const db = require("../db/models");
|
||||||
const UsersDBApi = require('../db/api/users');
|
const UsersDBApi = require('../db/api/users');
|
||||||
const ValidationError = require('./notifications/errors/validation');
|
const ValidationError = require('./notifications/errors/validation');
|
||||||
const ForbiddenError = require('./notifications/errors/forbidden');
|
const ForbiddenError = require('./notifications/errors/forbidden');
|
||||||
@ -59,6 +60,7 @@ class Auth {
|
|||||||
firstName: email.split('@')[0],
|
firstName: email.split('@')[0],
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
email: email,
|
email: email,
|
||||||
|
is_verified_student: email.toLowerCase().endsWith('.edu'),
|
||||||
|
|
||||||
},
|
},
|
||||||
options,
|
options,
|
||||||
|
|||||||
@ -12,6 +12,34 @@ const stream = require('stream');
|
|||||||
|
|
||||||
|
|
||||||
module.exports = class JobsService {
|
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) {
|
static async create(data, currentUser) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
try {
|
try {
|
||||||
@ -133,6 +161,4 @@ module.exports = class JobsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -12,6 +12,42 @@ const stream = require('stream');
|
|||||||
|
|
||||||
|
|
||||||
module.exports = class Service_listingsService {
|
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) {
|
static async create(data, currentUser) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
try {
|
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 router = useRouter();
|
||||||
const { id } = router.query;
|
const { id } = router.query;
|
||||||
const projectName = useAppSelector((state) => state.style.projectName) || 'FreelanceFusion';
|
const projectName = useAppSelector((state) => state.style.projectName) || 'FreelanceFusion';
|
||||||
|
const { currentUser } = useAppSelector((state) => state.auth);
|
||||||
const [service, setService] = useState<any>(null);
|
const [service, setService] = useState<any>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [purchasing, setPurchasing] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id && typeof id === 'string') {
|
if (id && typeof id === 'string') {
|
||||||
@ -36,6 +38,25 @@ export default function ServiceDetailsPage() {
|
|||||||
}
|
}
|
||||||
}, [id]);
|
}, [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) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col bg-gray-50 dark:bg-slate-900">
|
<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"
|
className="px-8 py-3 text-lg font-bold uppercase shadow-lg hover:shadow-indigo-200"
|
||||||
label="Back to Directory"
|
label="Back to Directory"
|
||||||
icon={mdiArrowLeft}
|
icon={mdiArrowLeft}
|
||||||
href="/web_pages/services"
|
onClick={() => router.push('/web_pages/services')}
|
||||||
color="indigo"
|
color="indigo"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -106,11 +127,18 @@ export default function ServiceDetailsPage() {
|
|||||||
{service.freelancer?.firstName?.[0] || 'S'}
|
{service.freelancer?.firstName?.[0] || 'S'}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-black text-gray-900 dark:text-white uppercase tracking-tight text-lg">Student Freelancer</p>
|
<p className="font-black text-gray-900 dark:text-white uppercase tracking-tight text-lg">{service.freelancer?.firstName || 'Student Freelancer'}</p>
|
||||||
<p className="text-sm text-gray-500 font-bold uppercase tracking-wider flex items-center gap-2">
|
{service.freelancer?.is_verified_student ? (
|
||||||
<BaseIcon path={mdiCheckDecagram} className="text-blue-500" size={16} />
|
<p className="text-sm text-green-600 dark:text-green-400 font-bold uppercase tracking-wider flex items-center gap-2">
|
||||||
Identity Verified
|
<BaseIcon path={mdiCheckDecagram} className="text-green-500" size={16} />
|
||||||
</p>
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -140,16 +168,16 @@ export default function ServiceDetailsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BaseButton
|
<BaseButton
|
||||||
label="Order Gig"
|
label={purchasing ? "Processing..." : (currentUser ? "Order Gig Instantly" : "Login to Order")}
|
||||||
color="indigo"
|
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"
|
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">
|
<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>
|
<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">
|
<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} />
|
<BaseIcon path={mdiCurrencyUsd} size={20} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user