Added scheduler for listing expiry after 48hrs
This commit is contained in:
parent
7534605f19
commit
24a89c71d2
|
|
@ -0,0 +1,71 @@
|
|||
import schedule from "node-cron";
|
||||
import pool from "../db/pool.js";
|
||||
import { getIO, getSocketId } from "../socket.js";
|
||||
|
||||
// Run every hour
|
||||
export const startExpirationJob = () => {
|
||||
schedule.schedule("0 * * * *", async () => {
|
||||
console.log("Running listing expiration check...");
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
// 1. Identify expired listings (active & not updated in last 48h)
|
||||
// Using INTERVAL '48 hours'
|
||||
const findExpiredQuery = `
|
||||
SELECT id, title, seller_id
|
||||
FROM listings
|
||||
WHERE status = 'active'
|
||||
AND updated_at < NOW() - INTERVAL '48 hours'
|
||||
AND deleted = FALSE
|
||||
FOR UPDATE SKIP LOCKED
|
||||
`;
|
||||
const { rows: expiredListings } = await client.query(findExpiredQuery);
|
||||
|
||||
if (expiredListings.length === 0) {
|
||||
await client.query("COMMIT");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Found ${expiredListings.length} listings to expire.`);
|
||||
|
||||
// 2. Update status to 'expired'
|
||||
const expiredIds = expiredListings.map(l => l.id);
|
||||
await client.query(`
|
||||
UPDATE listings
|
||||
SET status = 'expired'
|
||||
WHERE id = ANY($1::uuid[])
|
||||
`, [expiredIds]);
|
||||
|
||||
// 3. Create Notifications & Real-time Alerts
|
||||
for (const listing of expiredListings) {
|
||||
const message = `Your listing "${listing.title}" has expired after 48 hours of inactivity. Click here to re-list it.`;
|
||||
|
||||
// Insert Notification
|
||||
await client.query(`
|
||||
INSERT INTO notifications (user_id, type, message, data)
|
||||
VALUES ($1, 'listing_expired', $2, $3)
|
||||
`, [listing.seller_id, message, { listing_id: listing.id }]);
|
||||
|
||||
// Real-time Socket Emit
|
||||
const socketId = getSocketId(listing.seller_id);
|
||||
if (socketId) {
|
||||
getIO().to(socketId).emit("notification", {
|
||||
type: "listing_expired",
|
||||
message,
|
||||
data: { listing_id: listing.id }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
console.log("Expiration check completed successfully.");
|
||||
|
||||
} catch (err) {
|
||||
await client.query("ROLLBACK");
|
||||
console.error("Error in expiration job:", err);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
@ -12,6 +12,7 @@
|
|||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^5.1.0",
|
||||
"node-cron": "^4.2.1",
|
||||
"pg": "^8.16.3",
|
||||
"socket.io": "^4.8.1"
|
||||
}
|
||||
|
|
@ -610,6 +611,14 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/node-cron": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz",
|
||||
"integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^5.1.0",
|
||||
"node-cron": "^4.2.1",
|
||||
"pg": "^8.16.3",
|
||||
"socket.io": "^4.8.1"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -380,11 +380,18 @@ router.get("/:id", async (req, res) => {
|
|||
|
||||
// UPDATE Listing
|
||||
router.put("/:id", async (req, res) => {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { title, price, currency, is_negotiable, listing_type, status } = req.body;
|
||||
const {
|
||||
title, price, currency, is_negotiable, listing_type, status,
|
||||
animal // Animal object
|
||||
} = req.body;
|
||||
|
||||
const queryText = `
|
||||
await client.query("BEGIN");
|
||||
|
||||
// 1. Update Listing
|
||||
const updateListingQuery = `
|
||||
UPDATE listings
|
||||
SET title = COALESCE($1, title),
|
||||
price = COALESCE($2, price),
|
||||
|
|
@ -395,19 +402,67 @@ router.put("/:id", async (req, res) => {
|
|||
WHERE id = $7 AND deleted = FALSE
|
||||
RETURNING *
|
||||
`;
|
||||
const queryParams = [title, price, currency, is_negotiable, listing_type, status, id];
|
||||
const listingParams = [title, price, currency, is_negotiable, listing_type, status, id];
|
||||
const listingResult = await client.query(updateListingQuery, listingParams);
|
||||
|
||||
const result = await pool.query(queryText, queryParams);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
if (listingResult.rows.length === 0) {
|
||||
await client.query("ROLLBACK");
|
||||
return res.status(404).json({ error: "Listing not found" });
|
||||
}
|
||||
|
||||
res.json(result.rows[0]);
|
||||
const listing = listingResult.rows[0];
|
||||
|
||||
// 2. Update Animal (if provided)
|
||||
if (animal) {
|
||||
const updateAnimalQuery = `
|
||||
UPDATE animals
|
||||
SET species_id = COALESCE($1, species_id),
|
||||
breed_id = COALESCE($2, breed_id),
|
||||
sex = COALESCE($3, sex),
|
||||
age_months = COALESCE($4, age_months),
|
||||
weight_kg = COALESCE($5, weight_kg),
|
||||
color_markings = COALESCE($6, color_markings),
|
||||
quantity = COALESCE($7, quantity),
|
||||
purpose = COALESCE($8, purpose),
|
||||
health_status = COALESCE($9, health_status),
|
||||
vaccinated = COALESCE($10, vaccinated),
|
||||
dewormed = COALESCE($11, dewormed),
|
||||
pregnancy_status = COALESCE($12, pregnancy_status),
|
||||
calving_number = COALESCE($13, calving_number),
|
||||
milk_yield_litre_per_day = COALESCE($14, milk_yield_litre_per_day),
|
||||
ear_tag_no = COALESCE($15, ear_tag_no),
|
||||
description = COALESCE($16, description)
|
||||
WHERE id = $17
|
||||
RETURNING *
|
||||
`;
|
||||
const animalParams = [
|
||||
animal.species_id, animal.breed_id, animal.sex, animal.age_months,
|
||||
animal.weight_kg, animal.color_markings, animal.quantity, animal.purpose,
|
||||
animal.health_status, animal.vaccinated, animal.dewormed, animal.pregnancy_status,
|
||||
animal.calving_number, animal.milk_yield_litre_per_day, animal.ear_tag_no, animal.description,
|
||||
listing.animal_id
|
||||
];
|
||||
await client.query(updateAnimalQuery, animalParams);
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
|
||||
// Fetch complete updated data
|
||||
const completeResult = await client.query(`
|
||||
SELECT l.*, row_to_json(a) as animal
|
||||
FROM listings l
|
||||
JOIN animals a ON l.animal_id = a.id
|
||||
WHERE l.id = $1
|
||||
`, [id]);
|
||||
|
||||
res.json(completeResult.rows[0]);
|
||||
} catch (err) {
|
||||
await client.query("ROLLBACK");
|
||||
console.error("Error updating listing:", err);
|
||||
res.status(500).json({ error: "Internal server error" });
|
||||
}
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
});
|
||||
|
||||
// SOFT DELETE Listing
|
||||
|
|
@ -488,6 +543,30 @@ router.get("/user/:userId", async (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
// Reactivate Expired Listing
|
||||
router.put("/:id/relist", async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const queryText = `
|
||||
UPDATE listings
|
||||
SET status = 'active', updated_at = NOW()
|
||||
WHERE id = $1 AND status = 'expired'
|
||||
RETURNING *
|
||||
`;
|
||||
const result = await pool.query(queryText, [id]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: "Expired listing not found or already active" });
|
||||
}
|
||||
|
||||
res.json(result.rows[0]);
|
||||
} catch (err) {
|
||||
console.error("Error relisting item:", err);
|
||||
res.status(500).json({ error: "Internal server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// Update listing score & status
|
||||
router.patch("/:id/score", async (req, res) => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -18,10 +18,14 @@ app.use("/locations", locationRoutes);
|
|||
app.use("/chat", chatRoutes);
|
||||
import http from "http";
|
||||
import { initSocket } from "./socket.js";
|
||||
import { startExpirationJob } from "./jobs/expirationJob.js";
|
||||
|
||||
const server = http.createServer(app);
|
||||
initSocket(server);
|
||||
|
||||
// Start Background Jobs
|
||||
startExpirationJob();
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`BuySellService is running on port ${PORT}`);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue