Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): yargs in action #10

Merged
merged 2 commits into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,6 @@ options:
reference:
- more on dateISO - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
```

## How does it work?

18 changes: 11 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
{
"name": "pagerduty-schedules",
"bin": {
"caloohpay": "./dist/src/CalOohPay.js"
},
"version": "1.0.0",
"main": "index.js",
"main": "CalOohPay.js",
"scripts": {
"dev": "tsc -w",
"start": "node dist/src/pgapi.js",
"build": "tsc",
"test": "jest",
Expand All @@ -18,6 +22,7 @@
"@commitlint/config-conventional": "^19.5.0",
"@jest/globals": "^29.7.0",
"@types/jest": "^29.5.12",
"@types/node": "^22.7.5",
"@types/yargs": "^17.0.33",
"husky": "^9.1.6",
"jest": "^29.7.0",
Expand Down
170 changes: 170 additions & 0 deletions src/CalOohPay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
#!/usr/bin/env node
import { api } from '@pagerduty/pdjs';
import * as dotenv from 'dotenv';
import { hideBin } from 'yargs/helpers';
import yargs, { Argv, ArgumentsCamelCase } from "yargs";
import { OnCallUser } from './OnCallUser';
import { OnCallPeriod } from './OnCallPeriod';
import { FinalSchedule } from './FinalSchedule';
import { KaluzaOnCallPaymentsCalculator } from './KaluzaOnCallPaymentsCalculator';
import { ScheduleEntry } from './ScheduleEntry';

dotenv.config();

interface Environment {
API_TOKEN: string;
}

function sanitiseEnvVariable(envVars: NodeJS.ProcessEnv): Environment {
if (!envVars.API_TOKEN) {
throw new Error("API_TOKEN not defined");
}
return {
API_TOKEN: envVars.API_TOKEN,
};
}

const sanitisedEnvVars: Environment = sanitiseEnvVariable(process.env);

const yargsInstance = yargs(hideBin(process.argv));

const argv: CommandLineOptions = yargsInstance
.wrap(yargsInstance.terminalWidth())
.usage('$0 [options] <args>')
.option('rota-ids', {
alias: 'r',
describe: '1 scheduleId or multiple scheduleIds separated by comma',
type: 'string',
demandOption: true,
example: 'R1234567,R7654321'
})
.option('since', {
type: 'string',
alias: 's',
description: 'start of the schedule period',
})
.default('s', function firstDayOfPreviousMonth(): string {
let today = new Date();
return new Date(Date.UTC(today.getUTCFullYear(), (today.getUTCMonth() - 1), 1)).toISOString();
})
.option('until', {
type: 'string',
alias: 'u',
description: 'end of the schedule period',
})
.default('u', function lastDayOfPreviousMonth(): string {
let today = new Date();
return new Date(
Date.UTC(
today.getUTCFullYear(),
today.getUTCMonth(),
0,
23,
59,
59)
).toISOString();
})
.option('key', {
type: 'string',
demandOption: false,
alias: 'k',
description: 'this command line argument API_TOKEN to override environment variable API_TOKEN'
})
.option('output-file', {
type: 'string',
demandOption: false,
alias: 'o',
description: 'the path to the file where you want the on-call payments table printed'
})
.option('help', {
type: 'boolean',
alias: 'h',
description: 'Show help'
})
.help()
.check((argv) => {
if (argv.since && !Date.parse(argv.since)) {
throw new Error("Invalid date format for since");
}
if (argv.until && !Date.parse(argv.until)) {
throw new Error("Invalid date format for until");
}
return true;
}).argv as CommandLineOptions;

calOohPay(argv);


interface CommandLineOptions {
rotaIds: string;
since: string;
until: string;
key: string;
outputFile: string;
help: boolean;
}

function getOnCallUserFromScheduleEntry(scheduleEntry: ScheduleEntry): OnCallUser {
let onCallPeriod = new OnCallPeriod(scheduleEntry.start, scheduleEntry.end);
let onCallUser = new OnCallUser(
scheduleEntry.user?.id || "",
scheduleEntry.user?.summary || "", [onCallPeriod]);
return onCallUser
}

function extractOnCallUsersFromFinalSchedule(finalSchedule: FinalSchedule): Record<string,OnCallUser> {
let onCallUsers: Record<string,OnCallUser> = {};
if(finalSchedule.rendered_schedule_entries){
finalSchedule.rendered_schedule_entries.forEach(scheduleEntry => {
let onCallUser = getOnCallUserFromScheduleEntry(scheduleEntry);
if(onCallUser.id in onCallUsers){
onCallUsers[onCallUser.id].addOnCallPeriods(onCallUser.onCallPeriods);
} else {
onCallUsers[onCallUser.id] = onCallUser;
}
});
}
return onCallUsers;
}

function calOohPay(cliOptions: CommandLineOptions) {
const pagerDutyApi = api({ token: sanitisedEnvVars.API_TOKEN });
console.log("CLI Options: %s", JSON.stringify(cliOptions));
// invoke the pd api to get schedule data
for (let rotaId of cliOptions.rotaIds.split(',')) {
console.log(`Fetching schedule data for rotaId: ${rotaId} between ${cliOptions.since} and ${cliOptions.until}`);
pagerDutyApi
.get(`/schedules/${rotaId}`,
{
data: {
overflow: false,
since: cliOptions.since,
time_zone: "Europe/London",
until: cliOptions.until
}
}
).then(
({ data, resource, response, next }) => {
console.log("Schedule name: %s", data.schedule.name);
console.log("Schedule URL: %s", data.schedule.html_url);
let onCallUsers = extractOnCallUsersFromFinalSchedule(data.schedule.final_schedule);
let listOfOnCallUsers = Object.values(onCallUsers);

let auditableRecords = KaluzaOnCallPaymentsCalculator.getAuditableOnCallPaymentRecords(listOfOnCallUsers);
console.log("User, TotalComp, Mon-Thu, Fri-Sun");

for (const [userId, onCallCompensation] of Object.entries(auditableRecords)) {
console.log("%s, %d, %d, %d",
onCallCompensation.OnCallUser.name,
onCallCompensation.totalCompensation,
onCallCompensation.OnCallUser.getTotalOohWeekDays(),
onCallCompensation.OnCallUser.getTotalOohWeekendDays());
}
}
).catch(
(error) => {
console.error("Error: %s", error);
}
);
}
}
7 changes: 7 additions & 0 deletions src/FinalSchedule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { UserOncall } from './UserOncall';


export interface FinalSchedule {
name: String;
rendered_schedule_entries: UserOncall[];
}
1 change: 0 additions & 1 deletion src/IOnCallPaymentsCalculator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,4 @@ import { OnCallUser } from "./OnCallUser";
export interface IOnCallPaymentsCalculator {
calculateOnCallPayment(onCallUser: OnCallUser): number;
calculateOnCallPayments(onCallUsers: OnCallUser[]): Record<string, number>;
getAuditableOnCallPaymentRecords(onCallUsers: OnCallUser[]): Record<string, OnCallCompensation>;
}
33 changes: 11 additions & 22 deletions src/KaluzaOnCallPaymentsCalculator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,24 @@ import { IOnCallPaymentsCalculator } from "./IOnCallPaymentsCalculator";
import { OnCallCompensation } from "./OnCallCompensation";
import { OnCallUser } from "./OnCallUser";

function wasPersonOnCallOOH(dateToCheck: Date, onCallUntilDate: Date): boolean {
var dateToCheckEvening = new Date(dateToCheck);
dateToCheckEvening.setHours(18);
return (dateToCheckEvening > dateToCheck && dateToCheckEvening < onCallUntilDate)
}

export function dateDiffInHours(until: Date, since: Date): number {
return Math.round(Math.abs(until.getTime()) - Math.abs(since.getTime())) / (1000 * 60 * 60);
}

function validateOnCallUser(onCallUser: OnCallUser): void {
if (!onCallUser) {
throw new Error("User undefined!");
}
if (!onCallUser.onCallPeriods) {
throw new Error("No on call periods defined!");
}
}

export class KaluzaOnCallPaymentsCalculator implements IOnCallPaymentsCalculator {
public static WeekDayRate: number = 50;
public static WeekEndRate: number = 75;

static validateOnCallUser(onCallUser: OnCallUser): void {
if (!onCallUser) {
throw new Error("User undefined!");
}
if (!onCallUser.onCallPeriods) {
throw new Error("No on call periods defined!");
}
}
/**
* The calculator works on the assumption that the request was made with full date time format
* i.e. since is YYYY-MM-DDT00:00:00+01:00 AND until is YYYY-MM-DDT23:59:59+01:00
*/
calculateOnCallPayment(onCallUser: OnCallUser): number {
validateOnCallUser(onCallUser);
KaluzaOnCallPaymentsCalculator.validateOnCallUser(onCallUser);
return (onCallUser.getTotalOohWeekDays() * KaluzaOnCallPaymentsCalculator.WeekDayRate) +
(onCallUser.getTotalOohWeekendDays() * KaluzaOnCallPaymentsCalculator.WeekEndRate);
}
Expand All @@ -43,14 +32,14 @@ export class KaluzaOnCallPaymentsCalculator implements IOnCallPaymentsCalculator
return payments;
}

getAuditableOnCallPaymentRecords(onCallUsers: OnCallUser[]): Record<string, OnCallCompensation> {
static getAuditableOnCallPaymentRecords(onCallUsers: OnCallUser[]): Record<string, OnCallCompensation> {
/**
* for every OnCallUser item, create an OnCallCompensation object
* calculate number of weekdays and weekends that the person was on call
*/
let onCallCompensations: Record<string, OnCallCompensation> = {};
for (let onCallUser of onCallUsers) {
validateOnCallUser(onCallUser);
KaluzaOnCallPaymentsCalculator.validateOnCallUser(onCallUser);
onCallCompensations[onCallUser.id] = {
OnCallUser: onCallUser,
totalCompensation:
Expand Down
7 changes: 7 additions & 0 deletions src/ScheduleEntry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { User } from './User';

export interface ScheduleEntry {
user?: User;
start: Date;
end: Date;
}
8 changes: 8 additions & 0 deletions src/User.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

export interface User {
type: string;
id: string;
summary?: string;
self?: string;
html_url?: string;
}
8 changes: 8 additions & 0 deletions src/UserOncall.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { User } from './User';


export interface UserOncall {
start: Date;
end: Date;
user?: User;
}
36 changes: 0 additions & 36 deletions src/calc-ooh-pay.ts

This file was deleted.

Loading