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",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"node-cron": "^4.2.1",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
"socket.io": "^4.8.1"
|
"socket.io": "^4.8.1"
|
||||||
}
|
}
|
||||||
|
|
@ -610,6 +611,14 @@
|
||||||
"node": ">= 0.6"
|
"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": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"node-cron": "^4.2.1",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
"socket.io": "^4.8.1"
|
"socket.io": "^4.8.1"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -380,11 +380,18 @@ router.get("/:id", async (req, res) => {
|
||||||
|
|
||||||
// UPDATE Listing
|
// UPDATE Listing
|
||||||
router.put("/:id", async (req, res) => {
|
router.put("/:id", async (req, res) => {
|
||||||
|
const client = await pool.connect();
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
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
|
UPDATE listings
|
||||||
SET title = COALESCE($1, title),
|
SET title = COALESCE($1, title),
|
||||||
price = COALESCE($2, price),
|
price = COALESCE($2, price),
|
||||||
|
|
@ -395,19 +402,67 @@ router.put("/:id", async (req, res) => {
|
||||||
WHERE id = $7 AND deleted = FALSE
|
WHERE id = $7 AND deleted = FALSE
|
||||||
RETURNING *
|
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 (listingResult.rows.length === 0) {
|
||||||
|
await client.query("ROLLBACK");
|
||||||
if (result.rows.length === 0) {
|
|
||||||
return res.status(404).json({ error: "Listing not found" });
|
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) {
|
} catch (err) {
|
||||||
|
await client.query("ROLLBACK");
|
||||||
console.error("Error updating listing:", err);
|
console.error("Error updating listing:", err);
|
||||||
res.status(500).json({ error: "Internal server error" });
|
res.status(500).json({ error: "Internal server error" });
|
||||||
}
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// SOFT DELETE Listing
|
// 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
|
// Update listing score & status
|
||||||
router.patch("/:id/score", async (req, res) => {
|
router.patch("/:id/score", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,14 @@ app.use("/locations", locationRoutes);
|
||||||
app.use("/chat", chatRoutes);
|
app.use("/chat", chatRoutes);
|
||||||
import http from "http";
|
import http from "http";
|
||||||
import { initSocket } from "./socket.js";
|
import { initSocket } from "./socket.js";
|
||||||
|
import { startExpirationJob } from "./jobs/expirationJob.js";
|
||||||
|
|
||||||
const server = http.createServer(app);
|
const server = http.createServer(app);
|
||||||
initSocket(server);
|
initSocket(server);
|
||||||
|
|
||||||
|
// Start Background Jobs
|
||||||
|
startExpirationJob();
|
||||||
|
|
||||||
server.listen(PORT, () => {
|
server.listen(PORT, () => {
|
||||||
console.log(`BuySellService is running on port ${PORT}`);
|
console.log(`BuySellService is running on port ${PORT}`);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue