-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
NOBUG add sellout time estimation script (#402)
* NOBUG add sellout time estimation script * moving directory
- Loading branch information
1 parent
4f7d33e
commit 9bd8e45
Showing
1 changed file
with
355 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,355 @@ | ||
const AWS = require('aws-sdk'); | ||
const { dynamodb, TABLE_NAME, getParks, getFacilities } = require('./lambda/dynamoUtil'); | ||
// Install inquirer @v8.0.0 to avoid EMCAScript error | ||
const inquirer = require('inquirer'); | ||
const { DateTime, Interval, Duration } = require('luxon'); | ||
const { cancel } = require('aws-crt'); | ||
const { sk } = require('date-fns/locale'); | ||
const fs = require('fs'); | ||
|
||
async function run() { | ||
console.log('*** Check pass sellout time ***'); | ||
try { | ||
|
||
// Get facility | ||
let facility = await getFacility(); | ||
// Get date | ||
let date = await getDate(); | ||
// Get pass type | ||
let passType = await getPassType(facility); | ||
|
||
// Get reservation obj | ||
let reservation = await getReservation(facility, date); | ||
// run calcuation | ||
if (!reservation.Count) { | ||
throw new Error(`No reservations found for ${date} at ${facility.name}`); | ||
} | ||
|
||
// Unmarshall the reservation object | ||
reservation = reservation.Items.map((item) => AWS.DynamoDB.Converter.unmarshall(item)); | ||
|
||
// Get passes | ||
let passes = await getPasses(facility, date, passType); | ||
|
||
// Sort passes by status | ||
let passStatuses = sortPassesByStatus(passes.Items); | ||
|
||
// If the facility is fully booked, the sellout time is the time the last reserved pass WAS CREATED, not committed. | ||
const allPasses = passes.Items.map((pass) => AWS.DynamoDB.Converter.unmarshall(pass)); | ||
let count = [...allPasses].reduce((acc, pass) => acc + pass.numberOfGuests, 0); | ||
|
||
// Sort passes by creation time | ||
let sortedPasses = sortByCreatedTime(allPasses); | ||
|
||
// Create capacity change event array | ||
const events = createCapacityChangeEventArray(sortedPasses); | ||
|
||
// Estimate sellout time | ||
const selloutTime = estSelloutTime(events, passType, reservation[0]); | ||
showPassBreakdown(passStatuses, selloutTime, facility, passType, date, events); | ||
|
||
} catch (error) { | ||
console.log('error:', error); | ||
} | ||
|
||
} | ||
|
||
/** | ||
* Retrieves the facility information by prompting the user to select a facility. | ||
* | ||
* @returns {Promise<Object>} A promise that resolves to the selected facility object. | ||
* @throws {Error} If there is an error selecting the facility. | ||
*/ | ||
async function getFacility() { | ||
let facilities = []; | ||
const parks = await getParks(); | ||
for (const park of parks) { | ||
const parkFacilities = await getFacilities(park.orcs); | ||
facilities = facilities.concat(parkFacilities); | ||
} | ||
let facility; | ||
try { | ||
facility = await inquirer.prompt([ | ||
{ | ||
type: 'list', | ||
name: 'facility', | ||
message: 'Select facility:', | ||
choices: facilities.map(facility => { | ||
return { | ||
name: facility.name, | ||
value: { | ||
pk: facility.pk, | ||
sk: facility.sk, | ||
name: facility.name, | ||
passTypes: Object.keys(facility.bookingTimes) | ||
} | ||
}; | ||
}) | ||
} | ||
]); | ||
return facility.facility; | ||
} catch (error) { | ||
throw new Error('Error selecting facility:', error); | ||
} | ||
} | ||
|
||
/** | ||
* Retrieves a valid date from the user. | ||
* @returns {Promise<string>} The selected date in the format 'YYYY-MM-DD'. | ||
* @throws {Error} If there is an error selecting the date. | ||
*/ | ||
async function getDate() { | ||
let date; | ||
try { | ||
while (!date || date.invalid) { | ||
userDate = await inquirer.prompt([ | ||
{ | ||
type: 'input', | ||
name: 'date', | ||
message: 'Enter date (YYYY-MM-DD):' | ||
} | ||
]); | ||
date = DateTime.fromFormat(userDate.date, 'yyyy-LL-dd').setZone('America/Vancouver'); | ||
console.log('date:', date); | ||
if (date.invalid) { | ||
console.log('Invalid date. Please try again.'); | ||
} | ||
} | ||
return userDate.date; | ||
} catch (error) { | ||
throw new Error('Error selecting date:', error); | ||
} | ||
} | ||
|
||
/** | ||
* Retrieves the pass type for a given facility. | ||
* | ||
* @param {Object} facility - The facility object. | ||
* @returns {Promise<string>} - The selected pass type. | ||
* @throws {Error} - If there is an error selecting the pass type. | ||
*/ | ||
async function getPassType(facility) { | ||
let passType; | ||
console.log('facility.passTypes:', facility.passTypes); | ||
try { | ||
passType = await inquirer.prompt([ | ||
{ | ||
type: 'list', | ||
name: 'passType', | ||
message: 'Select pass type:', | ||
choices: facility.passTypes | ||
} | ||
]); | ||
return passType.passType; | ||
} catch (error) { | ||
throw new Error('Error selecting passtype:', error); | ||
} | ||
} | ||
|
||
/** | ||
* Retrieves a reservation object for a given facility and date. | ||
* @param {Object} facility - The facility object. | ||
* @param {string} date - The date of the reservation. | ||
* @returns {Promise<Object>} - A promise that resolves to the reservation object. | ||
* @throws {Error} - If there is an error getting the reservation object. | ||
*/ | ||
async function getReservation(facility, date) { | ||
let orcs = facility.pk.split('::')[1]; | ||
const query = { | ||
TableName: TABLE_NAME, | ||
KeyConditionExpression: 'pk = :pk AND sk = :sk', | ||
ExpressionAttributeValues: { | ||
':pk': { S: `reservations::${orcs}::${facility.sk}` }, | ||
':sk': { S: date } | ||
} | ||
}; | ||
try { | ||
const reservation = await dynamodb.query(query).promise(); | ||
return reservation; | ||
} catch (error) { | ||
throw new Error('Error getting reservation object:', error); | ||
} | ||
} | ||
|
||
/** | ||
* Retrieves passes from the database based on the specified facility, date, and pass type. | ||
* @param {Object} facility - The facility object containing the facility name. | ||
* @param {string} date - The date in string format. | ||
* @param {string} passType - The type of pass. | ||
* @returns {Promise<Object>} - A promise that resolves to the passes retrieved from the database. | ||
* @throws {Error} - If there is an error collecting passes from the database. | ||
*/ | ||
async function getPasses(facility, date, passType) { | ||
const query = { | ||
TableName: TABLE_NAME, | ||
IndexName: 'manualLookup-index', | ||
KeyConditionExpression: 'shortPassDate = :date AND facilityName = :facility', | ||
ExpressionAttributeNames: { | ||
'#type': 'type' | ||
}, | ||
FilterExpression: '#type = :type', | ||
ExpressionAttributeValues: { | ||
':date': { S: date }, | ||
':facility': { S: facility.name }, | ||
':type': { S: passType } | ||
} | ||
}; | ||
try { | ||
const passes = await dynamodb.query(query).promise(); | ||
return passes; | ||
} catch (error) { | ||
throw new Error('Error collecting passes:', error); | ||
} | ||
} | ||
|
||
/** | ||
* Sorts an array of passes by their status. | ||
* | ||
* @param {Array} passes - The array of passes to be sorted. | ||
* @returns {Object} - An object containing arrays of passes grouped by status. | ||
*/ | ||
function sortPassesByStatus(passes) { | ||
let statuses = {}; | ||
for (const pass of passes) { | ||
if (!statuses[pass.passStatus.S]) { | ||
statuses[pass.passStatus.S] = []; | ||
} | ||
statuses[pass.passStatus.S].push(AWS.DynamoDB.Converter.unmarshall(pass)); | ||
} | ||
return statuses; | ||
} | ||
|
||
/** | ||
* Sorts an array of passes by their creation date in ascending order. | ||
* | ||
* @param {Array} passes - The array of passes to be sorted. | ||
* @returns {Array} - The sorted array of passes. | ||
*/ | ||
function sortByCreatedTime(passes) { | ||
let sortedPasses = passes.sort((a, b) => { | ||
return a.creationDate.localeCompare(b.creationDate); | ||
}); | ||
return sortedPasses; | ||
} | ||
|
||
/** | ||
* Creates an array of capacity change events based on the given passes. | ||
* | ||
* @param {Array} passes - An array of passes. | ||
* @returns {Array} - An array of capacity change events sorted by date. | ||
*/ | ||
function createCapacityChangeEventArray(passes) { | ||
const events = []; | ||
for (const pass of passes) { | ||
let auditTrail = pass.audit; | ||
for (const auditEvent of auditTrail) { | ||
if (auditEvent?.passStatus === 'cancelled') { | ||
events.push({ | ||
date: auditEvent.dateUpdated, | ||
sk: pass.sk, | ||
passStatus: pass.passStatus, | ||
auditStatus: auditEvent.passStatus, | ||
numberOfGuests: pass.numberOfGuests, | ||
change: -1 * pass.numberOfGuests | ||
}); | ||
} else if (auditEvent?.passStatus === 'hold') { | ||
events.push({ | ||
date: pass.creationDate, | ||
sk: pass.sk, | ||
passStatus: pass.passStatus, | ||
auditStatus: auditEvent.passStatus, | ||
numberOfGuests: pass.numberOfGuests, | ||
change: pass.numberOfGuests | ||
}); | ||
} | ||
} | ||
} | ||
return events.sort((a, b) => a.date.localeCompare(b.date)); | ||
} | ||
|
||
/** | ||
* Estimates the sellout time based on the given events, facility, pass type, and reservation. | ||
* | ||
* @param {Array} events - The array of events. | ||
* @param {string} passType - The pass type. | ||
* @param {Object} reservation - The reservation object. | ||
* @param {Object} reservation.capacities - The capacities object. | ||
* @param {number} reservation.capacities.baseCapacity - The base capacity. | ||
* @param {number} reservation.capacities.capacityModifier - The capacity modifier. | ||
* @returns {Date|null} - The estimated sellout time or null if no sellout occurs. | ||
*/ | ||
function estSelloutTime(events, passType, reservation) { | ||
// Get capacity | ||
const capacity = reservation.capacities[passType].baseCapacity + reservation.capacities[passType].capacityModifier; | ||
let runningCapacity = capacity; | ||
let selloutTime; | ||
for (const event of events) { | ||
runningCapacity -= event.change; | ||
event.capacity = runningCapacity; | ||
if (runningCapacity <= 0 && !selloutTime) { | ||
selloutTime = event.date; | ||
} | ||
} | ||
return selloutTime; | ||
} | ||
|
||
/** | ||
* Displays the breakdown of passes and their statuses. | ||
* | ||
* @param {Object} passes - The passes object containing pass statuses. | ||
* @param {string} selloutTime - The sellout time of the passes. | ||
* @param {Object} facility - The facility object. | ||
* @param {string} passType - The type of pass. | ||
* @param {string} date - The date of the passes. | ||
* @param {Object} events - The events object. | ||
* @returns {Promise<void>} - A promise that resolves when the breakdown is displayed. | ||
*/ | ||
async function showPassBreakdown(passes, selloutTime, facility, passType, date, events) { | ||
let statuses = {}; | ||
let total = 0; | ||
for (const status of Object.keys(passes)) { | ||
statuses[status] = passes[status].reduce((acc, pass) => acc + pass.numberOfGuests, 0); | ||
total += statuses[status]; | ||
} | ||
statuses['total'] = total; | ||
console.log('************'); | ||
console.log(`${facility.name} (${passType}) on ${date}`); | ||
console.log('statuses:', statuses); | ||
if (selloutTime) { | ||
const time = DateTime.fromISO(selloutTime).setZone('America/Vancouver'); | ||
const open = time.startOf('day').plus({ hours: 7 }); | ||
const interval = Interval.fromDateTimes(open, time); | ||
const duration = interval.toDuration(); | ||
console.log(`Sold out at ${time.toFormat('HH:mm:ss')} (${duration.as('minutes')} minutes)`); | ||
} else { | ||
console.log('Not yet sold out.'); | ||
} | ||
console.log('************'); | ||
let csv = await inquirer.prompt( | ||
{ | ||
type: 'confirm', | ||
name: 'csv', | ||
message: 'Export breakdown as csv?:', | ||
} | ||
); | ||
if (csv.csv) { | ||
exportAsCsv(events, facility, passType, date); | ||
} | ||
} | ||
|
||
function exportAsCsv(events, facility, passType, date){ | ||
let csv = 'date,sk,passStatus,auditStatus,numberOfGuests,change,capacity\n'; | ||
for (const event of events) { | ||
csv += `${event.date},${event.sk},${event.passStatus},${event.auditStatus},${event.numberOfGuests},${event.change},${event.capacity}\n`; | ||
} | ||
fs.writeFile(`selloutTime_${facility.name}_${passType}_${date}.csv`, csv, (err) => { | ||
if (err) { | ||
console.log('err:', err); | ||
} | ||
}) | ||
} | ||
|
||
// Avoid AWS SDK V2 warning text | ||
setTimeout(() => { | ||
run(); | ||
}, 500); |