Use Denver local time for all scraper DB timestamps
All checks were successful
CI/CD Pipeline - Apartment API / Lint & Test (push) Successful in 44s
CI/CD Pipeline - Apartment API / Scan Dependencies (push) Successful in 13s
CI/CD Pipeline - Apartment API / Send Webhook Notification (push) Has been skipped
CI/CD Pipeline - Apartment API / Build & Push Image (push) Successful in 1m37s
CI/CD Pipeline - Apartment API / Deploy to Production (push) Successful in 15s

The scraper used new Date().toISOString() for timestamp fields written
to MongoDB, producing UTC times. This replaces all 6 DB timestamp sites
with a new getNow() helper that uses Intl.DateTimeFormat with the
configured SCRAPER_TIMEZONE to produce Denver-local timestamps.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-09 12:35:48 -07:00
parent 275857963c
commit 911f76520f
6 changed files with 51 additions and 27 deletions

View File

@ -403,7 +403,7 @@ function getYesterday(dateStr) {
*/
async function upsertUnits(db, units, logger) {
const collection = db.collection(config.COLLECTIONS.UNITS);
const now = new Date().toISOString();
const now = getNow();
const operations = units.map(unit => ({
updateOne: {
@ -461,7 +461,7 @@ async function upsertUnits(db, units, logger) {
*/
async function insertPrices(db, units, date, logger) {
const collection = db.collection(config.COLLECTIONS.PRICES);
const now = new Date().toISOString();
const now = getNow();
// Filter out units without prices
const unitsWithPrices = units.filter(u => u.price !== null);
@ -581,7 +581,7 @@ async function updateDailySummary(db, summaryData, logger) {
const summary = {
date,
timestamp: new Date().toISOString(),
timestamp: getNow(),
new_units: newUnits,
rented_units: rentedUnits,
stale_units: [], // Stale units list is not tracked per PRD
@ -633,6 +633,26 @@ function getToday() {
return new Date().toLocaleDateString('en-CA', { timeZone: config.SCRAPER_TIMEZONE });
}
/**
* Get current timestamp in the configured timezone as an ISO-like string.
* Returns format: YYYY-MM-DDTHH:mm:ss.mmm (no Z suffix, local time).
* @returns {string} Current timestamp in configured timezone
*/
function getNow() {
const now = new Date();
const formatter = new Intl.DateTimeFormat('en-CA', {
timeZone: config.SCRAPER_TIMEZONE,
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit',
hour12: false
});
const parts = Object.fromEntries(
formatter.formatToParts(now).map(({ type, value }) => [type, value])
);
const ms = String(now.getMilliseconds()).padStart(3, '0');
return `${parts.year}-${parts.month}-${parts.day}T${parts.hour}:${parts.minute}:${parts.second}.${ms}`;
}
/**
* Get the set of unit codes that had price records yesterday.
* Used to calculate new and rented units by comparison.
@ -718,7 +738,7 @@ async function recordScraperRun(db, runData, logger) {
const collection = db.collection(config.COLLECTIONS.SCRAPER_RUNS);
const result = await collection.insertOne({
...runData,
recordedAt: new Date()
recordedAt: getNow()
});
return result;
@ -790,7 +810,7 @@ async function runScrape(db, options = {}) {
trigger,
dryRun,
status: 'running',
startedAt: new Date().toISOString(),
startedAt: getNow(),
completedAt: null,
duration: null,
unitsProcessed: 0,
@ -875,7 +895,7 @@ async function runScrape(db, options = {}) {
result.errors.push(cleanError.message);
} finally {
result.completedAt = new Date().toISOString();
result.completedAt = getNow();
result.duration = Date.now() - startTime;
// Record run to history (always runs, even on failure)
@ -905,6 +925,7 @@ module.exports = {
createScraperIndexes,
// Export helpers for testing
getToday,
getNow,
getYesterdayUnitCodes,
getYesterday,
parseInteger,