-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
# initial Invoicing impl ## Change Justification Add the invoicing feature. ## Description - Implement the InvoiceHandler and associated functionality. - update docs - v0.2.0
- Loading branch information
Showing
11 changed files
with
208 additions
and
28 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
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 |
---|---|---|
@@ -1,5 +1,91 @@ | ||
import { IInvoiceHandler } from "../../domain/Invoice/IInvoiceHandler.ts"; | ||
import { Invoice } from "../../domain/Invoice/Invoice.ts"; | ||
import { Shift } from "../../domain/Shift/Shift.ts"; | ||
import { getFilesNamesInDirectory, getNonEmptyDirectoriesAsync } from "../../infra/IO/Files.ts"; | ||
import { Dictionary } from "../../infra/Types/Types.ts"; | ||
import * as FileManager from "../../infra/IO/Files.ts"; | ||
import { executeShellCommandAsync } from "../../infra/IO/Shell.ts"; | ||
import { parse } from "https://deno.land/std@0.201.0/datetime/mod.ts"; | ||
|
||
export class InvoiceHandler implements IInvoiceHandler { | ||
|
||
public async createInvoiceAsync(invoicee: string, companyName: string, rate: number): Promise<void> { | ||
const userDirs = await getNonEmptyDirectoriesAsync('./.timeclock/shifts'); | ||
const invoiceData: Dictionary<Shift[]> = {}; | ||
|
||
for(const userDir of userDirs) { | ||
const splitDir = userDir.split('/'); | ||
const userName = splitDir[splitDir.length-1]; | ||
invoiceData[userName] = await this.getUserShiftsAsync(userName, userDir, rate); | ||
} | ||
const invoice = new Invoice(invoiceData, invoicee, companyName, new Date()); | ||
|
||
await FileManager.createDirectoryAsync(invoice.invoiceDir); | ||
await this.writeInvoiceFile(invoice); | ||
|
||
(await executeShellCommandAsync("git", ["commit", "-m", `\"TIMECLOCK INVOICE - ${invoice.invoiceDate.toISOString().split('T')[0]}\"`])).verifyZeroReturnCode(); | ||
} | ||
|
||
private async getUserShiftsAsync(userName: string, userDir: string, rate: number): Promise<Shift[]> { | ||
const shifts: Shift[] = []; | ||
const filenames = await getFilesNamesInDirectory(userDir); | ||
for(const filename of filenames) { | ||
const shiftContent: string = await Deno.readTextFile(`${userDir}/${filename}`); | ||
const splitShift = shiftContent.split('_'); | ||
const shiftLength: number = Number.parseFloat(splitShift[0]); | ||
const shiftDate: Date = parse(splitShift[1], 'yyyy-MM-dd'); | ||
const shift = new Shift(userName, shiftLength, filename, shiftDate, rate); | ||
shifts.push(shift); | ||
await Deno.remove(shift.shiftFilePath); | ||
(await executeShellCommandAsync("git", ["add", shift.shiftFilePath])).verifyZeroReturnCode(); | ||
} | ||
return shifts; | ||
} | ||
|
||
private async writeInvoiceFile(invoice: Invoice): Promise<void> { | ||
let fileLines: string[] = []; | ||
|
||
fileLines.push('---------------\n'); | ||
fileLines.push(`${invoice.company} Invoice\n`); | ||
fileLines.push(`Bill to: ${invoice.invoicee}\n`); | ||
fileLines.push('---------------\n'); | ||
fileLines.push('\n'); | ||
fileLines.push('---------------\n'); | ||
fileLines.push('User Invoices\n'); | ||
fileLines.push('---------------\n'); | ||
fileLines.push('\n'); | ||
for(const user in invoice.userInvoices) { | ||
const [shifts, hours, cost] = invoice.userInvoices[user]; | ||
const userLines = this.createUserInvoiceLines(user, shifts, hours, cost); | ||
fileLines = fileLines.concat(userLines); | ||
fileLines.push('\n'); | ||
fileLines.push('-----\n'); | ||
fileLines.push('\n'); | ||
} | ||
fileLines.push('\n'); | ||
fileLines.push('---------------\n'); | ||
fileLines.push('Totals\n'); | ||
fileLines.push('---------------\n'); | ||
fileLines.push(`Total Hours: ${invoice.totalHours}\n`); | ||
fileLines.push(`Total Amount Due: ${invoice.amountDue}\n`); | ||
|
||
for(const line of fileLines) { | ||
await Deno.writeTextFile(invoice.invoiceFilePath, line, { append: true }); | ||
} | ||
|
||
(await executeShellCommandAsync("git", ["add", invoice.invoiceFilePath])).verifyZeroReturnCode(); | ||
} | ||
|
||
private createUserInvoiceLines(user: string, shifts: Shift[], hours: number, cost: number): string[] { | ||
const lines: string[] = []; | ||
|
||
lines.push(`User: ${user}\n`); | ||
lines.push('Shifts:\n'); | ||
lines.push(' Date|Hours\n'); | ||
for(const shift of shifts) { | ||
lines.push(` ${shift.date.toISOString().split('T')[0]}|${shift.diffHours}'\n`); | ||
} | ||
lines.push(`Hours: ${hours}\n`); | ||
lines.push(`Amount Due: ${cost}\n`); | ||
return lines; | ||
} | ||
} |
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
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
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
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 |
---|---|---|
@@ -1,3 +1,3 @@ | ||
export interface IInvoiceHandler { | ||
|
||
createInvoiceAsync(invoicee: string, companyName: string, rate: number): Promise<void>; | ||
} |
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 |
---|---|---|
@@ -1,3 +1,40 @@ | ||
export class Invoice { | ||
|
||
import { Dictionary } from "../../infra/Types/Types.ts"; | ||
import { Shift } from "../Shift/Shift.ts"; | ||
import { sumOf } from "https://deno.land/std@0.201.0/collections/mod.ts"; | ||
import { toFixed } from "https://deno.land/x/math@v1.1.0/to_fixed.ts"; | ||
|
||
export class Invoice{ | ||
userInvoices: Dictionary<[shifts: Shift[], totalHours: number, totalCost: number]>; | ||
totalHours: number; | ||
amountDue: number; | ||
invoiceDir: string; | ||
invoiceFilePath: string; | ||
invoicee: string; | ||
company: string; | ||
invoiceDate: Date; | ||
|
||
constructor(shifts: Dictionary<Shift[]>, invoicee: string, company: string, invoiceDate: Date) { | ||
this.userInvoices = {}; | ||
for (const user in shifts) { | ||
const userShifts: Shift[] = shifts[user]; | ||
const hours: number = sumOf(userShifts, x => x.diffHours); | ||
const cost: number = sumOf(userShifts, x => x.amountDue) | ||
this.userInvoices[user] = [userShifts, hours, cost]; | ||
} | ||
|
||
this.amountDue = 0; | ||
this.totalHours = 0; | ||
for(const user in this.userInvoices) { | ||
const [_, hours, amountDue] = this.userInvoices[user]; | ||
this.totalHours+=hours; | ||
this.amountDue+=amountDue; | ||
} | ||
this.invoiceDir = "./.timeclock/invoices"; | ||
this.invoiceFilePath = `${this.invoiceDir}/${invoiceDate.toISOString().split('T')[0]}`; | ||
this.invoicee = invoicee; | ||
this.company = company; | ||
this.invoiceDate = invoiceDate; | ||
this.totalHours = Number.parseFloat(toFixed(this.totalHours, 2)); | ||
this.amountDue = Number.parseFloat(toFixed(this.amountDue, 2)); | ||
} | ||
} |
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 |
---|---|---|
@@ -1,5 +1,5 @@ | ||
import { Punch } from "./Punch.ts"; | ||
|
||
export interface IPunchHandler { | ||
createPunchAsync(punch: Punch): Promise<void>; | ||
createPunchAsync(punch: Punch, rate: number): Promise<void>; | ||
} |
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 |
---|---|---|
@@ -1,16 +1,27 @@ | ||
import * as DomainConstants from "../Constants.ts"; | ||
import { toFixed } from "https://deno.land/x/math@v1.1.0/to_fixed.ts"; | ||
|
||
export class Shift { | ||
user: string; | ||
date: Date; | ||
diffHours: number | ||
shiftDir: string; | ||
shiftFilePath: string; | ||
amountDue: number; | ||
|
||
constructor(user: string, punchStartUtcMs: string, punchEndUtcMs: string) { | ||
this.user = user; | ||
public static createFromPunchTimes(user: string, punchStartUtcMs: string, punchEndUtcMs: string, date: Date, rate: number) { | ||
const diffMs: number = Number.parseInt(punchEndUtcMs) - Number.parseInt(punchStartUtcMs); | ||
this.diffHours = diffMs/DomainConstants.ONE_HOUR_IN_MS; | ||
const diffHours = diffMs/DomainConstants.ONE_HOUR_IN_MS; | ||
const fileName = `shift_${crypto.randomUUID()}`; | ||
return new Shift(user, diffHours, fileName, date, rate); | ||
} | ||
|
||
constructor(user: string, diffHours: number, filename: string, date: Date, rate: number) { | ||
this.user = user; | ||
this.diffHours = Number.parseFloat(toFixed(diffHours, 2)); | ||
this.shiftDir = `./.timeclock/shifts/${user}`; | ||
this.shiftFilePath = `${this.shiftDir}/shift_${crypto.randomUUID()}`; | ||
this.shiftFilePath = `${this.shiftDir}/${filename}`; | ||
this.date = date; | ||
this.amountDue = Number.parseFloat(toFixed(this.diffHours * rate, 2)); | ||
} | ||
} |
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
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,3 @@ | ||
export interface Dictionary<T> { | ||
[key: string]: T; | ||
} |