Skip to content

Commit

Permalink
Merge pull request #1914 from justinlevi/feature/billing-poly-sites
Browse files Browse the repository at this point in the history
Automated Billing - Poly Site Calculations
  • Loading branch information
Schnitzel committed May 28, 2020
2 parents 64e88ef + c9cfc44 commit ed3a458
Show file tree
Hide file tree
Showing 8 changed files with 229 additions and 105 deletions.
2 changes: 1 addition & 1 deletion services/api/src/helpers/billingGroups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export const getAllProjectsNotInBillingGroup = async () => {
const projects = await projectHelpers(sqlClient).getAllProjectsNotIn(pids);

sqlClient.destroy()

return projects.map(project => ({
id: project.id,
name: project.name,
Expand Down
41 changes: 16 additions & 25 deletions services/api/src/models/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import { User } from './user';

import {
getProjectsData,
availabilityProjectsCosts,
extractMonthYear
} from '../resources/billing/helpers';

import { getProjectsCosts } from '../resources/billing/billingCalculations';

import ProjectModel, { Project } from './project';
import BillingModel from './billing'
import EnvironmentModel from './environment';
Expand Down Expand Up @@ -39,6 +40,7 @@ export interface Group {
export interface BillingGroup extends Group {
currency?: string;
billingSoftware?: string;
type?: string;
}

interface GroupMembership {
Expand Down Expand Up @@ -609,51 +611,40 @@ export const Group = (clients) => {
const groupProjects = await ProjectModel(clients).projectsByGroup(group);

// Map a subset of project fields to the initial projects array
const initialProjects: [{id: string, name: string, availability: string, month: string, year:string}] =
const initialProjects: [{id: string, name: string, availability: string, month: string, year:string}] =
groupProjects.map(({ id, name, availability }) => ({
id, name, availability, month, year
}));

const availability = initialProjects[0].availability;

// Check that all projects have availability set
const availabilityCheck = initialProjects.reduce((acc, project) => [...acc, ...(project.availability === '' ? [project.name] : [])], []);
const availabilityCheck = initialProjects.reduce((acc, project) => [
...acc,
...(project.availability === '' && project.availability == availability ? [project.name] : [])
], []);
if(availabilityCheck.length > 0){
throw new Error(`Project(s): [${availabilityCheck.join(', ')}] must have availability set.`);
throw new Error(`Project(s): [${availabilityCheck.join(', ')}] must all have availability set and be the same in a billing group.`);
}

const environment = EnvironmentModel(clients);

// Get the hit, storage, environment data for each project and month
const projects = await getProjectsData(initialProjects, yearMonth, environment);

// Get any modifiers for the month
const modifiers = await BillingModel(clients).getBillingModifiers(groupInput, yearMonth);
const costs = getProjectsCosts(currency, projects, modifiers);

// Calculate costs based on Availability - All projects in the billing group should have the same availability
const high = availabilityProjectsCosts(
projects,
'HIGH',
currency,
modifiers
);
const standard = availabilityProjectsCosts(
projects,
'STANDARD',
currency,
modifiers
);

// Mark the returned BillingGroupCosts as 'HIGH' | 'STANDARD'
const availability = (high as availabilityProjectCostsType).projects
? 'HIGH'
: 'STANDARD';
getProjectsCosts(currency, projects, modifiers)

// Return the JSON
return { id, name, yearMonth, currency, availability, ...high, ...standard };
return { id, name, yearMonth, currency, availability, ...costs };
};

const allBillingGroupCosts = async yearMonth => {
const allGroups: Group[] = await loadAllGroups();
const billingGroups = allGroups.filter(({ type }) => type === 'billing');
const billingGroups = allGroups.filter(({ type }) => type === 'billing' || type === 'billing-poly');
const billingGroupCosts = [];
for (let i = 0; i < billingGroups.length; i++) {
const costs = await billingGroupCost(
Expand Down
169 changes: 155 additions & 14 deletions services/api/src/resources/billing/billingCalculations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {
getProjectsCosts,
BillingGroupCosts
} from './billingCalculations';
import { availabilityProjectsCosts } from './helpers';
import { defaultModifier } from './resolvers.test';
import {
initializeGraphQL,
Expand All @@ -45,7 +44,7 @@ interface IMockDataType {
// month: 'July 2019',
const mockData: IMockDataType = {
billingGroups: [
{
{
// CH - July 2019
name: 'VF',
expectations: {
Expand Down Expand Up @@ -317,7 +316,7 @@ const mockData: IMockDataType = {
{
name: 'Dev Only',
expectations: {
hits: 136.2435,
hits: 136.24,
storage: 0,
prod: 31.02,
dev: 20.68,
Expand Down Expand Up @@ -347,6 +346,100 @@ const mockData: IMockDataType = {
},
],
},
//POLY
{
name: 'IDGSDF',
expectations: {
hits: 69.81,
storage: 0,
prod: 30.02,
dev: 10.01,
},
currency: CURRENCIES.EUR,
billingSoftware: 'xero',
projects: [
{
name: "IDGHKSLD",
availability: AVAILABILITY.POLYSITE,
month: 4,
year: 2020,
hits: 1394,
storageDays: 12.813023999999999,
prodHours: 720,
devHours: 720
},
{
name: "idg-standard-public-fr",
availability: AVAILABILITY.POLYSITE,
month: 4,
year: 2020,
hits: 12693,
storageDays: 21.669425999999998,
prodHours: 720,
devHours: 720,
},
{
name: "idg-standard-public-nl",
availability: AVAILABILITY.POLYSITE,
month: 4,
year: 2020,
hits: 162076,
storageDays: 13.590564,
prodHours: 720,
devHours: 720,
},
{
name: "idg-standard-public-ch",
availability: AVAILABILITY.POLYSITE,
month: 4,
year: 2020,
hits: 98569,
storageDays: 15.029644000000001,
prodHours: 720,
devHours: 720,
},
{
name: "idg-standard-public-be",
availability: AVAILABILITY.POLYSITE,
month: 4,
year: 2020,
hits: 1394,
storageDays: 7.220072,
prodHours: 720,
devHours: 720,
},
{
name: "idg-standard-public-africa",
availability: AVAILABILITY.POLYSITE,
month: 4,
year: 2020,
hits: 29275,
storageDays: 4.415667999999999,
prodHours: 720,
devHours: 720,
},
{
name: "idg-standard-public-it",
availability: AVAILABILITY.POLYSITE,
month: 4,
year: 2020,
hits: 0,
storageDays: 6.02611,
prodHours: 720,
devHours: 720,
},
{
name: "idg-standard-public-hu",
availability: AVAILABILITY.POLYSITE,
month: 4,
year: 2020,
hits: 0,
storageDays: 11.531086,
prodHours: 720,
devHours: 720,
}
]
},
],
};

Expand Down Expand Up @@ -439,8 +532,11 @@ const devEnvironmentCostTestString = (group: ITestBillingGroup) =>
const currencyFilter = currency => group =>
group.currency === CURRENCIES[currency];

const availabilityFilter = availability => group =>
group.projects[0].availability === AVAILABILITY[availability];

// Unit Under Test
describe('Billing Calculations', () => {
describe('Billing Calculations #only-billing-calculations', () => {
describe('Hit Tier #hit-tier', () => {
// scenarios and expectation
it('When hits are between { MIN: 300_001, MAX: 2_500_000 }, then the "hitTier should be 1', () => {
Expand Down Expand Up @@ -472,6 +568,18 @@ describe('Billing Calculations', () => {
});
});

describe('Hit Costs - POLY #Hits #POLY', () => {
// scenarios and expectation
mockData.billingGroups.filter(availabilityFilter(AVAILABILITY.POLYSITE)).map(group => {
it(hitsCostTestString(group), () => {
// Act
const { cost } = hitsCost(group);
// Assert
expect(cost).toBe(group.expectations.hits);
});
});
});

describe('Hit Costs - Customers billed in Pounds (GBP) #Hits #GBP', () => {
// scenarios and expectation
mockData.billingGroups.filter(currencyFilter(CURRENCIES.GBP)).map(group => {
Expand All @@ -496,6 +604,18 @@ describe('Billing Calculations', () => {
});
});

describe('Storage Costs - POLY #Storage #POLY', () => {
// scenarios and expectation
mockData.billingGroups.filter(availabilityFilter(AVAILABILITY.POLYSITE)).map(group => {
it(storageCostTestString(group), () => {
// Act
const { cost } = storageCost(group);
// Assert
expect(cost).toBe(group.expectations.storage);
});
});
});

describe('Storage Costs - Customers billed in Pounds (GBP) #Storage #GBP', () => {
// scenarios and expectation
mockData.billingGroups.filter(currencyFilter(CURRENCIES.GBP)).map(group => {
Expand All @@ -520,6 +640,18 @@ describe('Billing Calculations', () => {
});
});

describe('Prod Environment Costs - POLY #Environment #POLY', () => {
// scenarios and expectation
mockData.billingGroups.filter(availabilityFilter(AVAILABILITY.POLYSITE)).map(group => {
it(prodEnvironmentCostTestString(group), () => {
// Act
const { cost } = prodCost(group);
// Assert
expect(cost).toBe(group.expectations.prod);
});
});
});

describe('Dev Environment Costs - Customers billed in US Dollars (USD) #Environment #USD', () => {
// scenarios and expectation
mockData.billingGroups.filter(currencyFilter(CURRENCIES.USD)).map(group => {
Expand All @@ -537,6 +669,18 @@ describe('Billing Calculations', () => {
});
});

describe('Dev Environment Costs - POLY #Environment #POLY', () => {
// scenarios and expectation
mockData.billingGroups.filter(availabilityFilter(AVAILABILITY.POLYSITE)).map(group => {
it(devEnvironmentCostTestString(group), () => {
// Act
const { cost } = devCost(group);
// Assert
expect(cost).toBe(group.expectations.dev);
});
});
});

describe('Environment Costs - Customers billed in Pounds (GBP) #Environment #GBP', () => {
// scenarios and expectation
mockData.billingGroups.filter(currencyFilter(CURRENCIES.GBP)).map(group => {
Expand Down Expand Up @@ -947,26 +1091,23 @@ describe('Billing Calculations', () => {
} = currMonthData;

// Request costs for last month
const lastMonthCosts = availabilityProjectsCosts(
mockProjects,
AVAILABILITY.STANDARD,
const lastMonthCosts = getProjectsCosts(
CURRENCIES.CHF,
mockProjects,
lastMonthBillingGroupModifiers
) as BillingGroupCosts;

// Request costs for next month
const nextMonthCosts = availabilityProjectsCosts(
mockProjects,
AVAILABILITY.STANDARD,
const nextMonthCosts = getProjectsCosts(
CURRENCIES.CHF,
mockProjects,
nextMonthBillingGroupModifiers
);

// Request costs for current month
const currMonthCosts = availabilityProjectsCosts(
mockProjects,
AVAILABILITY.STANDARD,
const currMonthCosts = getProjectsCosts(
CURRENCIES.CHF,
mockProjects,
currMonthBillingGroupModifiers
);

Expand Down Expand Up @@ -1066,7 +1207,7 @@ describe('Billing Calculations', () => {
// Act

// Costs for current month
const currMonthCosts = availabilityProjectsCosts( mockProjects, AVAILABILITY.STANDARD, CURRENCIES.CHF, modifiers );
const currMonthCosts = getProjectsCosts( CURRENCIES.CHF, mockProjects, modifiers );

// Assert
expect(currMonthCosts.modifiers.length).toBe(1);
Expand Down

0 comments on commit ed3a458

Please sign in to comment.