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

Automated Billing - Poly Site Calculations #1914

Merged
merged 7 commits into from
May 28, 2020
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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