Skip to content

Commit

Permalink
BRS-451: Fix pass state transitions. Add spinner on local CLI script …
Browse files Browse the repository at this point in the history
…for migration tool so we can see what it's doing. (#83)
  • Loading branch information
marklise authored Mar 7, 2022
1 parent 4dc447c commit 9cd3025
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 165 deletions.
45 changes: 5 additions & 40 deletions __tests__/checkActivation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -134,7 +99,7 @@ describe('checkActivationHandler', () => {
type: passType,
registrationNumber: sk,
passStatus: 'reserved',
date: formatISO(passDate, { representation: 'date' })
date: formatISO(passDate)
}
})
.promise();
Expand Down Expand Up @@ -167,7 +132,7 @@ describe('checkActivationHandler', () => {
type: passType,
registrationNumber: sk,
passStatus: 'reserved',
date: formatISO(passDate, { representation: 'date' })
date: formatISO(passDate)
}
})
.promise();
Expand Down Expand Up @@ -200,7 +165,7 @@ describe('checkActivationHandler', () => {
type: passType,
registrationNumber: sk,
passStatus: 'reserved',
date: formatISO(passDate, { representation: 'date' })
date: formatISO(passDate)
}
})
.promise();
Expand Down Expand Up @@ -233,7 +198,7 @@ describe('checkActivationHandler', () => {
type: 'PM',
registrationNumber: '123456708',
passStatus: 'reserved',
date: formatISO(passDate, { representation: 'date' })
date: formatISO(passDate)
}
})
.promise();
Expand Down Expand Up @@ -266,7 +231,7 @@ describe('checkActivationHandler', () => {
type: 'PM',
registrationNumber: '123456709',
passStatus: 'reserved',
date: formatISO(passDate, { representation: 'date' })
date: formatISO(passDate)
}
})
.promise();
Expand Down
3 changes: 3 additions & 0 deletions __tests__/global/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -62,6 +64,7 @@ module.exports = async () => {
NonKeyAttributes: [
'type',
'date',
'facilityName',
'pk',
'sk'
]
Expand Down
171 changes: 86 additions & 85 deletions lambda/checkActivation/index.js
Original file line number Diff line number Diff line change
@@ -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<parks.length;i++) {
const results = await getFacilities(parks[i].sk);
facilities = facilities.concat(results);
}
// console.log("Facilities:", facilities);

// For each pass determine if we're in the AM/DAY for that pass or the PM. Push into active
// accordingly, or set it to expired if it's anything < today
for (let i=0;i < passes.length;i++) {
let pass = passes[i];

const passParkName = pass.pk.split('::')[1];
const passFacilityName = pass.facilityName;

const facilities = await getFacilities(park.sk);
for (const facility of facilities) {
activatedCount += await activateFacilityPasses(config, park, facility, localNow);
// TODO: Fixme into a MAP for better lookups.
let openingHourTimeForFacility = 7;
const theFacility = facilities.filter(fac => 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) {
Expand All @@ -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;
}
45 changes: 9 additions & 36 deletions lambda/checkExpiry/index.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,22 @@
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 {
const endOfYesterdayTime = endOfYesterday();
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) {
Expand All @@ -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);
}
Expand All @@ -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;
}
Loading

0 comments on commit 9cd3025

Please sign in to comment.