Skip to content

Commit

Permalink
BRS-425: Update logic for pass expiry checks. (#79)
Browse files Browse the repository at this point in the history
  • Loading branch information
marklise authored Feb 18, 2022
1 parent 90ae6cd commit 9949a1b
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 107 deletions.
64 changes: 14 additions & 50 deletions __tests__/checkExpiry.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@ const checkExpiry = require('../lambda/checkExpiry/index');

const { REGION, ENDPOINT, TABLE_NAME } = require('./global/settings');

let dynamoDb;
let docClient;

async function setupDb() {
dynamoDb = new AWS.DynamoDB({
new AWS.DynamoDB({
region: REGION,
endpoint: ENDPOINT
});
Expand Down Expand Up @@ -60,43 +59,8 @@ describe('checkExpiryHandler', () => {
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: '123456710',
facilityName: 'Parking Lot A',
type: 'DAY',
registrationNumber: '123456710',
passStatus: 'active',
date: formatISO(oldDate, { representation: 'date' })
}
})
.promise();

MockDate.set(new Date('2021-12-08T11:01:30.135Z'));
await checkExpiry.handler(null, {});
MockDate.reset();

const result = await docClient
.get({
TableName: TABLE_NAME,
Key: {
pk: 'pass::Test Park',
sk: '123456710'
}
})
.promise();
expect(result.Item.passStatus).toBe('active');
});

test.each([['PM', '123456711'], ['DAY', '123456712']])('should set %s passes from yesterday to expired', async (passType, sk) => {
const passDate = new Date('2021-12-07T11:07:58.135Z');
test.each([['AM', '123456710'], ['PM', '123456711'], ['DAY', '123456712']])('should set %s passes from yesterday to expired', async (passType, sk) => {
const passDate = new Date('2021-12-08T20:00:00.000Z');
await docClient
.put({
TableName: TABLE_NAME,
Expand All @@ -107,12 +71,12 @@ describe('checkExpiryHandler', () => {
type: passType,
registrationNumber: sk,
passStatus: 'active',
date: formatISO(passDate, { representation: 'date' })
date: formatISO(passDate)
}
})
.promise();

MockDate.set(new Date('2021-12-08T08:01:58.135Z'));
MockDate.set(new Date('2021-12-19T00:00:00.000Z'));
await checkExpiry.handler(null, {});
MockDate.reset();

Expand All @@ -129,7 +93,7 @@ describe('checkExpiryHandler', () => {
});

test.each([['PM', '123456713'], ['DAY', '123456714']])('should not set %s passes from today to expired', async (passType, sk) => {
const passDate = new Date('2021-12-08T11:02:43.135Z');
const passDate = new Date('2021-12-08T20:00:00.000Z');
await docClient
.put({
TableName: TABLE_NAME,
Expand All @@ -140,12 +104,12 @@ describe('checkExpiryHandler', () => {
type: passType,
registrationNumber: sk,
passStatus: 'active',
date: formatISO(passDate, { representation: 'date' })
date: formatISO(passDate)
}
})
.promise();

MockDate.set(new Date('2021-12-08T19:01:58.135Z'));
MockDate.set(new Date('2021-12-08T21:00:00.000Z'));
await checkExpiry.handler(null, {});
MockDate.reset();

Expand All @@ -162,7 +126,7 @@ describe('checkExpiryHandler', () => {
});

test('should set AM passes to expired after 12:00', async () => {
const passDate = new Date('2021-12-08T11:01:02.135Z');
const passDate = new Date('2021-12-08T20:00:00.000Z');
await docClient
.put({
TableName: TABLE_NAME,
Expand All @@ -173,12 +137,12 @@ describe('checkExpiryHandler', () => {
type: 'AM',
registrationNumber: '123456715',
passStatus: 'active',
date: formatISO(passDate, { representation: 'date' })
date: formatISO(passDate)
}
})
.promise();

MockDate.set(new Date('2021-12-08T20:00:00.001Z'));
MockDate.set(new Date('2021-12-08T22:00:00.000Z'));
await checkExpiry.handler(null, {});
MockDate.reset();

Expand All @@ -195,7 +159,7 @@ describe('checkExpiryHandler', () => {
});

test('should set not AM passes to expired before 12:00', async () => {
const passDate = new Date('2021-12-08T11:01:58.135Z');
const passDate = new Date('2021-12-08T20:00:00.000Z');
await docClient
.put({
TableName: TABLE_NAME,
Expand All @@ -206,12 +170,12 @@ describe('checkExpiryHandler', () => {
type: 'AM',
registrationNumber: '123456716',
passStatus: 'active',
date: formatISO(passDate, { representation: 'date' })
date: formatISO(passDate)
}
})
.promise();

MockDate.set(new Date('2021-12-08T19:59:59.999Z'));
MockDate.set(new Date('2021-12-08T16:00:00.000Z'));
await checkExpiry.handler(null, {});
MockDate.reset();

Expand Down
77 changes: 75 additions & 2 deletions __tests__/global/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ module.exports = async () => {
});

try {
await dynamoDb
let res = await dynamoDb
.createTable({
TableName: TABLE_NAME,
KeySchema: [
Expand All @@ -30,12 +30,85 @@ module.exports = async () => {
{
AttributeName: 'sk',
AttributeType: 'S'
},
{
AttributeName: 'shortPassDate',
AttributeType: 'S'
},
{
AttributeName: 'facilityName',
AttributeType: 'S'
},
{
AttributeName: 'passStatus',
AttributeType: 'S'
}
],
ProvisionedThroughput: {
ReadCapacityUnits: 1,
WriteCapacityUnits: 1
}
},
GlobalSecondaryIndexes: [
{
IndexName: 'passStatus-index',
KeySchema: [
{
AttributeName: 'passStatus',
KeyType: 'HASH'
}
],
Projection: {
ProjectionType: 'INCLUDE',
NonKeyAttributes: [
'type',
'date',
'pk',
'sk'
]
},
ProvisionedThroughput: {
ReadCapacityUnits: 1,
WriteCapacityUnits: 1
}
},
{
IndexName: 'shortPassDate-index',
KeySchema: [
{
AttributeName: 'shortPassDate',
KeyType: 'HASH'
},
{
AttributeName: 'facilityName',
KeyType: 'RANGE'
}
],
Projection: {
ProjectionType: 'INCLUDE',
NonKeyAttributes: [
'firstName',
'searchFirstName',
'lastName',
'searchLastName',
'facilityName',
'email',
'date',
'shortPassDate',
'type',
'registrationNumber',
'numberOfGuests',
'passStatus',
'phoneNumber',
'facilityType',
'license'
]
},
ProvisionedThroughput: {
ReadCapacityUnits: 1,
WriteCapacityUnits: 1
}
}
]
})
.promise();
} catch (err) {
Expand Down
79 changes: 42 additions & 37 deletions lambda/checkExpiry/index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
const { formatISO, subDays, getHours } = require('date-fns');
const { compareAsc, addHours, endOfYesterday, startOfDay } = require('date-fns');
const { utcToZonedTime } = require('date-fns-tz');

const { runQuery, setStatus, getParks, TABLE_NAME } = require('../dynamoUtil');
const { runQuery, setStatus, TABLE_NAME } = require('../dynamoUtil');
const { sendResponse } = require('../responseUtil');

const TIMEZONE = 'America/Vancouver';
const timeZone = 'America/Vancouver';
const ACTIVE_STATUS = 'active';
const EXPIRED_STATUS = 'expired';
const PASS_TYPE_EXPIRY_HOURS = {
Expand All @@ -14,30 +13,36 @@ const PASS_TYPE_EXPIRY_HOURS = {
};

exports.handler = async (event, context) => {
console.log('Event', event, context);
console.log('Check Expiry', event, context);
try {
const utcNow = Date.now();
const localNow = utcToZonedTime(utcNow, TIMEZONE);
const localHour = getHours(localNow);
const yesterday = subDays(new Date(localNow), 1);
console.log(`UTC: ${utcNow}; local (${TIMEZONE}): ${localNow}; yesterday: ${yesterday}`);
const endOfYesterdayTime = endOfYesterday();
const currentTime = utcToZonedTime(new Date(), timeZone);

let passesToChange = [];
const passes = await getActivePasses();
console.log("Active Passes:", passes);

for (const passType in PASS_TYPE_EXPIRY_HOURS) {
const expiryHour = PASS_TYPE_EXPIRY_HOURS[passType];
if (localHour < expiryHour) {
console.log(`${passType} passes don't expire yet`);
continue;
for(pass of passes) {
const zonedPassTime = utcToZonedTime(pass.date, timeZone);
// If it's zoned date is before the end of yesterday, it's definitely expired (AM/PM/DAY)
if (compareAsc(zonedPassTime, endOfYesterdayTime) <= 0) {
console.log("Expiring:", pass);
passesToChange.push(pass);
}

// If expiring at midnight, check yesterday's passes.
const expiryDate = expiryHour === 0 ? yesterday : localNow;
const parks = await getParks();
for (const park of parks) {
const passes = await getExpiredPasses(passType, expiryDate, park.sk);
await setStatus(passes, EXPIRED_STATUS);
// 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) {
console.log("Expiring:", pass);
passesToChange.push(pass);
}
}

// Set passes => expired
if (passesToChange.length !== 0) {
await setStatus(passesToChange, EXPIRED_STATUS);
}

return sendResponse(200, {}, context);
} catch (err) {
console.error(err);
Expand All @@ -46,26 +51,26 @@ exports.handler = async (event, context) => {
}
};

async function getExpiredPasses(passType, passDate, parkSk) {
const dateSelector = formatISO(passDate, { representation: 'date' });

console.log(`Loading ${passType} passes on ${dateSelector} for ${parkSk}`);
async function getActivePasses() {
console.log(`Loading passes`);

const passesQuery = {
TableName: TABLE_NAME,
KeyConditionExpression: 'pk = :pk',
ExpressionAttributeNames: {
'#dateselector': 'date',
'#passType': 'type'
},
KeyConditionExpression: 'passStatus = :activeStatus',
IndexName: 'passStatus-index',
ExpressionAttributeValues: {
':pk': { S: `pass::${parkSk}` },
':activeDate': { S: dateSelector },
':activeStatus': { S: ACTIVE_STATUS },
':passType': { S: passType }
},
FilterExpression: 'begins_with(#dateselector, :activeDate) AND #passType = :passType AND passStatus = :activeStatus'
':activeStatus': { S: ACTIVE_STATUS }
}
};

return await runQuery(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;
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"dependencies": {
"axios": "^0.24.0",
"csvjson": "^5.1.0",
"date-fns": "^2.27.0",
"date-fns": "^2.28.0",
"date-fns-tz": "^1.1.6",
"jsonwebtoken": "^8.5.1",
"jwks-rsa": "^2.0.5",
Expand Down
12 changes: 12 additions & 0 deletions serverless.yml
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,18 @@ resources:
KeyType: RANGE
BillingMode: PAY_PER_REQUEST
GlobalSecondaryIndexes:
- IndexName: passStatus-index
KeySchema:
- AttributeName: passStatus
KeyType: HASH
Projection:
ProjectionType: INCLUDE
NonKeyAttributes:
- type
- date
- pk
- sk
BillingMode: PAY_PER_REQUEST
- IndexName: shortPassDate-index
KeySchema:
- AttributeName: shortPassDate
Expand Down
Loading

0 comments on commit 9949a1b

Please sign in to comment.