Skip to content

Commit

Permalink
Merge pull request #4 from tewen/time-entry-utilities
Browse files Browse the repository at this point in the history
Time entry utilities
  • Loading branch information
tewen authored Sep 24, 2023
2 parents 82dd5d0 + ad9d0a7 commit 19ba53d
Show file tree
Hide file tree
Showing 13 changed files with 148 additions and 14 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "xero-hero",
"version": "0.3.0",
"version": "0.4.0",
"description": "",
"files": [
"dist",
Expand Down
9 changes: 9 additions & 0 deletions src/accounting/contacts/__tests__/links.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,14 @@ describe('contacts/links', () => {
'https://go.xero.com/app/!kR4N6/contacts/contact/kooolaid-9086-t728',
);
});

it('should work with an object that matches the interface for having a contactID property', () => {
const contact = {
contactID: '25OR6TO4',
};
expect(getContactLink(contact)).toBe(
'https://go.xero.com/app/!kR4N6/contacts/contact/25OR6TO4',
);
});
});
});
9 changes: 6 additions & 3 deletions src/accounting/contacts/links.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { Contact } from 'xero-node';
import type { Contact } from 'xero-node';

import { hasProperty } from '../../utils/properties';

export const getContactLink = (contact: Contact | string): string => {
return `https://go.xero.com/app/!kR4N6/contacts/contact/${
(contact instanceof Contact ? contact.contactID : contact) ||
'null-or-empty-contact-id'
(hasProperty(contact, 'contactID')
? (contact as Contact).contactID
: contact) || 'null-or-empty-contact-id'
}`;
};
7 changes: 7 additions & 0 deletions src/accounting/invoices/__tests__/links.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,12 @@ describe('invoices/links', () => {
'https://invoicing.xero.com/view/kooolaid-9086-t728',
);
});

it('should work with an object that matches the interface for having a invoiceID property', () => {
const invoice = { invoiceID: '25OR6TO4' };
expect(getInvoiceLink(invoice)).toBe(
'https://invoicing.xero.com/view/25OR6TO4',
);
});
});
});
9 changes: 6 additions & 3 deletions src/accounting/invoices/links.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { Invoice } from 'xero-node';
import type { Invoice } from 'xero-node';

import { hasProperty } from '../../utils/properties';

export const getInvoiceLink = (invoice: Invoice | string): string => {
return `https://invoicing.xero.com/view/${
(invoice instanceof Invoice ? invoice.invoiceID : invoice) ||
'null-or-empty-invoice-id'
(hasProperty(invoice, 'invoiceID')
? (invoice as Invoice).invoiceID
: invoice) || 'null-or-empty-invoice-id'
}`;
};
10 changes: 9 additions & 1 deletion src/accounting/journals/__tests__/links.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ManualJournal } from 'xero-node';

import { getManualJournalLink } from '../../../index';
import { getManualJournalLink } from '../../../';

describe('journals/links', () => {
describe('getManualJournalLink()', () => {
Expand Down Expand Up @@ -39,5 +39,13 @@ describe('journals/links', () => {
'https://go.xero.com/Journal/View.aspx?invoiceID=sdfhj-47629-sjdgdd',
);
});

it('should work with an object that matches the interface for having a manualJournalID property', () => {
const manualJournal = { manualJournalID: '25OR6TO4' };
// @ts-expect-error - This is an invalid type for the function.
expect(getManualJournalLink(manualJournal)).toBe(
'https://go.xero.com/Journal/View.aspx?invoiceID=25OR6TO4',
);
});
});
});
8 changes: 5 additions & 3 deletions src/accounting/journals/links.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import qs from 'qs';
import { ManualJournal } from 'xero-node';
import type { ManualJournal } from 'xero-node';

import { hasProperty } from '../../utils/properties';

export const getManualJournalLink = (
manualJournal: ManualJournal | string,
): string => {
return `https://go.xero.com/Journal/View.aspx?${qs.stringify({
invoiceID:
(manualJournal instanceof ManualJournal
? manualJournal.manualJournalID
(hasProperty(manualJournal, 'manualJournalID')
? (manualJournal as ManualJournal).manualJournalID
: manualJournal) || 'null-or-empty-manual-journal-id',
})}`;
};
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ export { filterInvoiceLineItems, getInvoiceLink } from './accounting/invoices';
// Journals
export { getManualJournalLink } from './accounting/journals';
// Projects
export { generateProjectAmountUSD } from './projects';
export { generateProjectAmountUSD, hoursFromTimeEntries } from './projects';
76 changes: 76 additions & 0 deletions src/projects/__tests__/timeEntries.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { TimeEntry } from 'xero-node/dist/gen/model/projects/timeEntry';

import { hoursFromTimeEntries } from '../../';

describe('projects/timeEntries', () => {
describe('hoursFromTimeEntries()', () => {
it('should throw an error if passed undefined', () => {
expect(() => {
// @ts-expect-error - This is an invalid type for the function.
return hoursFromTimeEntries();
}).toThrowError();
});

it('should throw an error if passed null', () => {
expect(() => {
// @ts-expect-error - This is an invalid type for the function.
return hoursFromTimeEntries(null);
}).toThrowError();
});

it('should return 0 if passed an empty array', () => {
expect(hoursFromTimeEntries([])).toBe(0);
});

it('should return the total hours from the given TimeEntries, defaulting to the nearest 15 minutes', () => {
const timeEntries = [
{
duration: 60,
},
{
duration: 30,
},
{
duration: 18,
},
];
expect(hoursFromTimeEntries(timeEntries)).toBe(1.75);
});

it('should play nice with a non-default denominator for rounding', () => {
/* eslint-disable functional/immutable-data */
const timeEntryA = new TimeEntry();
timeEntryA.duration = 60;
const timeEntryB = new TimeEntry();
timeEntryB.duration = 23;
/* eslint-enable functional/immutable-data */
const timeEntries = [timeEntryA, timeEntryB];
expect(hoursFromTimeEntries(timeEntries, 10)).toBe(1.4);
});

it('should play nice with non-standard decimal places for rounding / fixed numbers', () => {
const timeEntries = [
{
duration: 60,
},
{
duration: 30,
},
{
duration: 18,
},
];
expect(hoursFromTimeEntries(timeEntries, 2, 0)).toBe(2);
});

it('should sub in 0 when a Time Entry is missing a duration', () => {
/* eslint-disable functional/immutable-data */
const timeEntryA = new TimeEntry();
const timeEntryB = new TimeEntry();
timeEntryB.duration = 23;
/* eslint-enable functional/immutable-data */
const timeEntries = [timeEntryA, timeEntryB];
expect(hoursFromTimeEntries(timeEntries, 10)).toBe(0.4);
});
});
});
1 change: 1 addition & 0 deletions src/projects/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { generateProjectAmountUSD } from './generators';
export { hoursFromTimeEntries } from './timeEntries';
18 changes: 18 additions & 0 deletions src/projects/timeEntries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { roundToNearestFraction } from 'deep-cuts';
import type { TimeEntry } from 'xero-node/dist/gen/model/projects/timeEntry';

export const hoursFromTimeEntries = (
timeEntries: TimeEntry[],
denominator: number = 4,
maxDecimalPlaces: number = 2,
): number | undefined => {
const totalMinutes = timeEntries.reduce((totalMinutes, timeEntry) => {
const duration = timeEntry.duration || 0;
return totalMinutes + duration;
}, 0);
return roundToNearestFraction(
totalMinutes / 60,
denominator,
maxDecimalPlaces,
);
};
7 changes: 7 additions & 0 deletions src/utils/properties.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const hasProperty = (object: any, property: string): boolean => {
if (Boolean(object) && typeof object === 'object') {
return property in object;
}

return false;
};

0 comments on commit 19ba53d

Please sign in to comment.