diff --git a/__tests__/checkActivation.test.js b/__tests__/checkActivation.test.js index 0139fa9a..2cbb7d79 100644 --- a/__tests__/checkActivation.test.js +++ b/__tests__/checkActivation.test.js @@ -87,41 +87,6 @@ describe('checkActivationHandler', () => { return setupDb(); }); - test('should not update old passes', async () => { - const oldDate = new Date(); - oldDate.setDate(oldDate.getDate() - 5); - - await docClient - .put({ - TableName: TABLE_NAME, - Item: { - pk: 'pass::Test Park', - sk: '123456700', - facilityName: 'Parking Lot A', - type: 'DAY', - registrationNumber: '123456700', - passStatus: 'reserved', - date: formatISO(oldDate, { representation: 'date' }) - } - }) - .promise(); - - MockDate.set(new Date('2021-12-08T11:01:58.135Z')); - await checkActivation.handler(null, {}); - MockDate.reset(); - - const result = await docClient - .get({ - TableName: TABLE_NAME, - Key: { - pk: 'pass::Test Park', - sk: '123456700' - } - }) - .promise(); - expect(result.Item.passStatus).toBe('reserved'); - }); - test.each([['AM', '123456702'], ['DAY', '123456703']])('should set %s passes with default opening hour to active', async (passType, sk) => { const passDate = new Date('2021-12-08T19:01:58.135Z'); await docClient @@ -134,7 +99,7 @@ describe('checkActivationHandler', () => { type: passType, registrationNumber: sk, passStatus: 'reserved', - date: formatISO(passDate, { representation: 'date' }) + date: formatISO(passDate) } }) .promise(); @@ -167,7 +132,7 @@ describe('checkActivationHandler', () => { type: passType, registrationNumber: sk, passStatus: 'reserved', - date: formatISO(passDate, { representation: 'date' }) + date: formatISO(passDate) } }) .promise(); @@ -200,7 +165,7 @@ describe('checkActivationHandler', () => { type: passType, registrationNumber: sk, passStatus: 'reserved', - date: formatISO(passDate, { representation: 'date' }) + date: formatISO(passDate) } }) .promise(); @@ -233,7 +198,7 @@ describe('checkActivationHandler', () => { type: 'PM', registrationNumber: '123456708', passStatus: 'reserved', - date: formatISO(passDate, { representation: 'date' }) + date: formatISO(passDate) } }) .promise(); @@ -266,7 +231,7 @@ describe('checkActivationHandler', () => { type: 'PM', registrationNumber: '123456709', passStatus: 'reserved', - date: formatISO(passDate, { representation: 'date' }) + date: formatISO(passDate) } }) .promise(); diff --git a/__tests__/global/setup.js b/__tests__/global/setup.js index bd03b165..3a214fc6 100644 --- a/__tests__/global/setup.js +++ b/__tests__/global/setup.js @@ -8,6 +8,8 @@ module.exports = async () => { endpoint: ENDPOINT }); + // TODO: This should pull in the JSON version of our serverless.yml! + try { let res = await dynamoDb .createTable({ @@ -62,6 +64,7 @@ module.exports = async () => { NonKeyAttributes: [ 'type', 'date', + 'facilityName', 'pk', 'sk' ] diff --git a/lambda/checkActivation/index.js b/lambda/checkActivation/index.js index 57c3c91b..8f17fafd 100644 --- a/lambda/checkActivation/index.js +++ b/lambda/checkActivation/index.js @@ -1,36 +1,100 @@ -const { formatISO } = require('date-fns'); -const { utcToZonedTime } = require('date-fns-tz'); - -const { runQuery, setStatus, getConfig, getParks, getFacilities, TABLE_NAME } = require('../dynamoUtil'); +const { endOfToday, compareAsc, addHours, startOfDay } = require('date-fns'); +const { utcToZonedTime, zonedTimeToUtc } = require('date-fns-tz'); + +const { setStatus, + getPassesByStatus, + getParks, + getFacilities, + RESERVED_STATUS, + ACTIVE_STATUS, + EXPIRED_STATUS, + PASS_TYPE_PM, + timeZone } = require('../dynamoUtil'); const { sendResponse } = require('../responseUtil'); -const ACTIVE_STATUS = 'active'; -const RESERVED_STATUS = 'reserved'; -const PASS_TYPE_AM = 'AM'; -const PASS_TYPE_PM = 'PM'; -const PASS_TYPE_DAY = 'DAY'; -const TIMEZONE = 'America/Vancouver'; -const PM_ACTIVATION_HOUR = 12; - exports.handler = async (event, context) => { console.log('Event:', event, context); try { - const utcNow = Date.now(); - const localNow = utcToZonedTime(utcNow, TIMEZONE); - console.log(`UTC: ${utcNow}; local (${TIMEZONE}): ${localNow}`); + const theDate = zonedTimeToUtc(endOfToday(), timeZone); + + console.log("Checking against date:", theDate); + + const filter = { + FilterExpression: '#theDate <=:theDate', + ExpressionAttributeValues: { + ':theDate': { S: theDate.toISOString() } + }, + ExpressionAttributeNames: { + '#theDate': 'date' + } + }; + + console.log("Getting passes by status:", RESERVED_STATUS, filter); + + const passes = await getPassesByStatus(RESERVED_STATUS, filter); + console.log("Reserved Passes:", passes.length); - const [config] = await getConfig(); + // Query the passStatus-index for passStatus = 'reserved' + // NB: Filter on date <= endOfToday for fixing previous bad data. + // What period are we in? AM/PM? + const currentTime = utcToZonedTime(new Date(), timeZone); + const noonTime = addHours(startOfDay(currentTime), 12); + const startOfDayLocalTime = startOfDay(currentTime); + + // 1. If currentTimeLocal < noon => AM + // 2. If currentTimeLocal >= noon => PM + const isAM = compareAsc(currentTime, noonTime) <= 0 ? true : false; + + let passesToActiveStatus = []; + let passesToExpiredStatus = []; + + // Get all facilities for opening hour lookups. + let facilities = []; const parks = await getParks(); - for (const park of parks) { - let activatedCount = 0; + for(let i=0;i fac.pk === 'facility::' + passParkName && fac.sk === passFacilityName); + + if (theFacility && theFacility.length > 0 && theFacility[0].bookingOpeningHour !== undefined && theFacility[0].bookingOpeningHour !== null) { + openingHourTimeForFacility = theFacility[0].bookingOpeningHour; + } + + const isWithinOpeningHour = compareAsc(currentTime, addHours(startOfDayLocalTime, openingHourTimeForFacility)) >= 0 ? true : false; + + if (isAM === true && pass.type !== PASS_TYPE_PM && isWithinOpeningHour) { + passesToActiveStatus.push(pass); + } else if (isAM === false && pass.type === PASS_TYPE_PM) { + passesToActiveStatus.push(pass); } - console.log(`Activated ${activatedCount} passes for ${park.sk}`); + // If we added an item to passesToActiveStatus that was date < begginingOfToday, set to expired, woops! + if (compareAsc(new Date(pass.date), startOfDayLocalTime) <= 0) { + // Prune from the active list + passesToActiveStatus = passesToActiveStatus.filter(item => item.sk !== pass.sk && item.date !== pass.date); + + // Push this one instead to an expired list. + passesToExpiredStatus.push(pass); + } } + console.log("Passes => active:", passesToActiveStatus.length); + console.log("Passes => expired:", passesToExpiredStatus.length); + + await setStatus(passesToActiveStatus, ACTIVE_STATUS); + await setStatus(passesToExpiredStatus, EXPIRED_STATUS); return sendResponse(200, {}, context); } catch (err) { @@ -39,66 +103,3 @@ exports.handler = async (event, context) => { return sendResponse(500, {}, context); } }; - -async function getCurrentPasses(passType, localNow, parkSk, facilityName) { - const activeDateSelector = formatISO(localNow, { representation: 'date' }); - - console.log(`Loading ${passType} passes on ${activeDateSelector} for ${parkSk} ${facilityName}`); - - const passesQuery = { - TableName: TABLE_NAME, - KeyConditionExpression: 'pk = :pk', - ExpressionAttributeNames: { - '#dateselector': 'date', - '#passType': 'type' - }, - ExpressionAttributeValues: { - ':pk': { S: `pass::${parkSk}` }, - ':facilityName': { S: facilityName }, - ':activeDate': { S: activeDateSelector }, - ':reservedStatus': { S: RESERVED_STATUS }, - ':passType': { S: passType } - }, - FilterExpression: - 'begins_with(#dateselector, :activeDate) AND #passType = :passType AND passStatus = :reservedStatus AND facilityName = :facilityName' - }; - - return await runQuery(passesQuery); -} - -async function activateFacilityPasses(config, park, facility, localNow) { - const localHour = localNow.getHours(); - const defaultOpeningHour = config.BOOKING_OPENING_HOUR || 7; - - let activatedCount = 0; - const facilityBookingOpeningHour = facility.bookingOpeningHour || defaultOpeningHour; - const isFacilityAmOpen = localHour >= facilityBookingOpeningHour; - const isFacilityPmOpen = localHour >= PM_ACTIVATION_HOUR; - - for (const passType of [PASS_TYPE_AM, PASS_TYPE_PM, PASS_TYPE_DAY]) { - let isOpen = false; - switch (passType) { - case PASS_TYPE_AM: - isOpen = isFacilityAmOpen; - break; - case PASS_TYPE_PM: - isOpen = isFacilityPmOpen; - break; - case PASS_TYPE_DAY: - // DAY passes open at the same time as AM - isOpen = isFacilityAmOpen; - break; - } - - if (isOpen) { - console.log(`Facility ${facility.sk} is open for ${passType} passes`); - const passes = await getCurrentPasses(passType, localNow, park.sk, facility.name); - await setStatus(passes, ACTIVE_STATUS); - activatedCount += passes.length; - } else { - console.log(`Facility ${facility.sk} is not open for ${passType} passes`); - } - } - - return activatedCount; -} diff --git a/lambda/checkExpiry/index.js b/lambda/checkExpiry/index.js index ffddbe76..f677c3f5 100644 --- a/lambda/checkExpiry/index.js +++ b/lambda/checkExpiry/index.js @@ -1,17 +1,14 @@ const { compareAsc, addHours, endOfYesterday, startOfDay } = require('date-fns'); const { utcToZonedTime } = require('date-fns-tz'); -const { runQuery, setStatus, TABLE_NAME } = require('../dynamoUtil'); +const { getPassesByStatus, + setStatus, + ACTIVE_STATUS, + EXPIRED_STATUS, + PASS_TYPE_EXPIRY_HOURS, + PASS_TYPE_AM, + timeZone } = require('../dynamoUtil'); const { sendResponse } = require('../responseUtil'); -const timeZone = 'America/Vancouver'; -const ACTIVE_STATUS = 'active'; -const EXPIRED_STATUS = 'expired'; -const PASS_TYPE_EXPIRY_HOURS = { - AM: 12, - PM: 0, - DAY: 0 -}; - exports.handler = async (event, context) => { console.log('Check Expiry', event, context); try { @@ -19,7 +16,7 @@ exports.handler = async (event, context) => { const currentTime = utcToZonedTime(new Date(), timeZone); let passesToChange = []; - const passes = await getActivePasses(); + const passes = await getPassesByStatus(ACTIVE_STATUS); console.log("Active Passes:", passes); for(pass of passes) { @@ -32,7 +29,7 @@ exports.handler = async (event, context) => { // If AM, see if we're currently in the afternoon or later compared to the pass date's noon time. const noonTime = addHours(startOfDay(zonedPassTime), PASS_TYPE_EXPIRY_HOURS.AM); - if (pass.type === 'AM' && compareAsc(currentTime, noonTime) > 0) { + if (pass.type === PASS_TYPE_AM && compareAsc(currentTime, noonTime) > 0) { console.log("Expiring:", pass); passesToChange.push(pass); } @@ -50,27 +47,3 @@ exports.handler = async (event, context) => { return sendResponse(500, {}, context); } }; - -async function getActivePasses() { - console.log(`Loading passes`); - - const passesQuery = { - TableName: TABLE_NAME, - KeyConditionExpression: 'passStatus = :activeStatus', - IndexName: 'passStatus-index', - ExpressionAttributeValues: { - ':activeStatus': { S: ACTIVE_STATUS } - } - }; - - // Grab all the results, don't skip any. - let results = []; - let passData; - do { - passData = await runQuery(passesQuery, true); - passData.data.forEach((item) => results.push(item)); - passesQuery.ExclusiveStartKey = passData.LastEvaluatedKey; - } while(typeof passData.LastEvaluatedKey !== "undefined"); - - return results; -} diff --git a/lambda/dynamoUtil.js b/lambda/dynamoUtil.js index 4ccea75b..fa2883a7 100644 --- a/lambda/dynamoUtil.js +++ b/lambda/dynamoUtil.js @@ -8,6 +8,20 @@ const options = { if (process.env.IS_OFFLINE) { options.endpoint = 'http://localhost:8000'; } +const ACTIVE_STATUS = 'active'; +const RESERVED_STATUS = 'reserved'; +const EXPIRED_STATUS = 'expired'; +const timeZone = 'America/Vancouver'; +const PASS_TYPE_AM = 'AM'; +const PASS_TYPE_PM = 'PM'; +const PASS_TYPE_DAY = 'DAY'; +const TIMEZONE = 'America/Vancouver'; +const PM_ACTIVATION_HOUR = 12; +const PASS_TYPE_EXPIRY_HOURS = { + AM: 12, + PM: 0, + DAY: 0 +}; const dynamodb = new AWS.DynamoDB(options); @@ -36,11 +50,11 @@ async function setStatus(passes, status) { async function runQuery(query, paginated = false) { console.log('query:', query); const data = await dynamodb.query(query).promise(); - console.log('data:', data); + // console.log('data:', data); var unMarshalled = data.Items.map(item => { return AWS.DynamoDB.Converter.unmarshall(item); }); - console.log(unMarshalled); + // console.log(unMarshalled); if (paginated) { return { LastEvaluatedKey: data.LastEvaluatedKey, @@ -54,11 +68,11 @@ async function runQuery(query, paginated = false) { async function runScan(query, paginated = false) { console.log('query:', query); const data = await dynamodb.scan(query).promise(); - console.log('data:', data); + // console.log('data:', data); var unMarshalled = data.Items.map(item => { return AWS.DynamoDB.Converter.unmarshall(item); }); - console.log(unMarshalled); + // console.log(unMarshalled); if (paginated) { return { LastEvaluatedKey: data.LastEvaluatedKey, @@ -111,7 +125,55 @@ const expressionBuilder = function (operator, existingExpression, newFilterExpre } }; +const getPassesByStatus = async function(status, filterExpression = undefined) { + console.log(`Loading passes`, filterExpression); + + const passesQuery = { + TableName: TABLE_NAME, + KeyConditionExpression: 'passStatus = :activeStatus', + IndexName: 'passStatus-index' + }; + + if (filterExpression && filterExpression.FilterExpression) { + passesQuery.FilterExpression = filterExpression.FilterExpression; + } + if (filterExpression && filterExpression.ExpressionAttributeValues) { + passesQuery.ExpressionAttributeValues = filterExpression.ExpressionAttributeValues; + } + if (filterExpression && filterExpression.ExpressionAttributeNames) { + passesQuery.ExpressionAttributeNames = filterExpression.ExpressionAttributeNames; + } + + if (!passesQuery.ExpressionAttributeValues) { + passesQuery.ExpressionAttributeValues = {}; + } + passesQuery.ExpressionAttributeValues[':activeStatus'] = { S: status }; + + console.log("Query:", passesQuery); + + // Grab all the results, don't skip any. + let results = []; + let passData; + do { + passData = await runQuery(passesQuery, true); + passData.data.forEach((item) => results.push(item)); + passesQuery.ExclusiveStartKey = passData.LastEvaluatedKey; + } while(typeof passData.LastEvaluatedKey !== "undefined"); + + return results; +} + module.exports = { + ACTIVE_STATUS, + RESERVED_STATUS, + EXPIRED_STATUS, + PASS_TYPE_AM, + PASS_TYPE_PM, + PASS_TYPE_DAY, + TIMEZONE, + PM_ACTIVATION_HOUR, + PASS_TYPE_EXPIRY_HOURS, + timeZone, TABLE_NAME, dynamodb, setStatus, @@ -120,5 +182,6 @@ module.exports = { getConfig, getParks, getFacilities, + getPassesByStatus, expressionBuilder }; diff --git a/serverless.yml b/serverless.yml index f786f4fe..0f88c19e 100644 --- a/serverless.yml +++ b/serverless.yml @@ -191,6 +191,8 @@ resources: AttributeType: S - AttributeName: facilityName AttributeType: S + - AttributeName: passStatus + AttributeType: S KeySchema: - AttributeName: pk KeyType: HASH @@ -206,6 +208,7 @@ resources: ProjectionType: INCLUDE NonKeyAttributes: - type + - facilityName - date - pk - sk diff --git a/tools/dynamoRestore.js b/tools/dynamoRestore.js index 8e02afae..3c0c4dd5 100644 --- a/tools/dynamoRestore.js +++ b/tools/dynamoRestore.js @@ -11,14 +11,21 @@ const options = { const dynamodb = new AWS.DynamoDB(options); +let action = ["|","/","-","\\"]; +let index = 0; + async function run() { + console.log("Running importer"); for (const item of data.Items) { + process.stdout.write(action[index % 4] + " " + index.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') + "\r"); + index++; const itemObj = { TableName: TABLE_NAME, Item: item }; const res = await dynamodb.putItem(itemObj).promise(); } + process.stdout.write(`${index.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')} Records Processed\r\n`); } run();