This commit is contained in:
Flatlogic Bot 2026-02-28 10:37:47 +00:00
parent a6bf68668a
commit 6909bf7269
7 changed files with 156 additions and 833 deletions

View File

@ -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);

View File

@ -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);

View File

@ -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,

View File

@ -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 {
}
};
};

View File

@ -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

View File

@ -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>