Skip to content

Commit

Permalink
Payroll: Add get total owed salary function (aragon#900)
Browse files Browse the repository at this point in the history
  • Loading branch information
facuspagnuolo authored Jul 3, 2019
1 parent 66b475e commit b0714ce
Show file tree
Hide file tree
Showing 3 changed files with 240 additions and 120 deletions.
35 changes: 28 additions & 7 deletions future-apps/payroll/contracts/Payroll.sol
Original file line number Diff line number Diff line change
Expand Up @@ -347,12 +347,7 @@ contract Payroll is EtherTokenConstant, IForwarder, IsContract, AragonApp {
// Salary is capped here to avoid reverting at this point if it becomes too big
// (so employees aren't DDOSed if their salaries get too large)
// If we do use a capped value, the employee's lastPayroll date will be adjusted accordingly
uint256 currentOwedSalary = _getOwedSalarySinceLastPayroll(employeeId, true); // cap amount
uint256 totalOwedSalary = currentOwedSalary + employee.accruedSalary;
if (totalOwedSalary < currentOwedSalary) {
totalOwedSalary = MAX_UINT256;
}

uint256 totalOwedSalary = _getTotalOwedCappedSalary(employeeId);
paymentAmount = _ensurePaymentAmount(totalOwedSalary, _requestedAmount);
_updateEmployeeAccountingBasedOnPaidSalary(employeeId, paymentAmount);
} else if (_type == PaymentType.Reimbursement) {
Expand Down Expand Up @@ -483,6 +478,15 @@ contract Payroll is EtherTokenConstant, IForwarder, IsContract, AragonApp {
endDate = employee.endDate;
}

/**
* @dev Get owed salary since last payroll for an employee. It will take into account the accrued salary as well.
* The result will be capped to max uint256 to avoid having an overflow.
* @return Employee's total owed salary: current owed payroll since the last payroll date, plus the accrued salary.
*/
function getTotalOwedSalary(uint256 _employeeId) public view employeeIdExists(_employeeId) returns (uint256) {
return _getTotalOwedCappedSalary(_employeeId);
}

/**
* @dev Get an employee's payment allocation for a token
* @param _employeeId Employee's identifier
Expand Down Expand Up @@ -778,7 +782,7 @@ contract Payroll is EtherTokenConstant, IForwarder, IsContract, AragonApp {
}

/**
* @dev Get owed salary since last payroll for an employee.
* @dev Get owed salary since last payroll for an employee
* @param _employeeId Employee's identifier
* @param _capped Safely cap the owed salary at max uint
* @return Owed salary in denomination tokens since last payroll for the employee.
Expand Down Expand Up @@ -824,6 +828,23 @@ contract Payroll is EtherTokenConstant, IForwarder, IsContract, AragonApp {
return uint256(date - employee.lastPayroll);
}

/**
* @dev Get owed salary since last payroll for an employee. It will take into account the accrued salary as well.
* The result will be capped to max uint256 to avoid having an overflow.
* @param _employeeId Employee's identifier
* @return Employee's total owed salary: current owed payroll since the last payroll date, plus the accrued salary.
*/
function _getTotalOwedCappedSalary(uint256 _employeeId) internal view returns (uint256) {
Employee storage employee = employees[_employeeId];

uint256 currentOwedSalary = _getOwedSalarySinceLastPayroll(_employeeId, true); // cap amount
uint256 totalOwedSalary = currentOwedSalary + employee.accruedSalary;
if (totalOwedSalary < currentOwedSalary) {
totalOwedSalary = MAX_UINT256;
}
return totalOwedSalary;
}

/**
* @dev Get payment reference for a given payment type
* @param _type Payment type to query the reference of
Expand Down
212 changes: 212 additions & 0 deletions future-apps/payroll/test/contracts/Payroll_employee_info.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
const { assertRevert } = require('@aragon/test-helpers/assertThrow')
const { getEventArgument } = require('@aragon/test-helpers/events')
const { annualSalaryPerSecond } = require('../helpers/numbers')(web3)
const { MAX_UINT256, MAX_UINT64 } = require('../helpers/numbers')(web3)
const { NOW, ONE_MONTH, RATE_EXPIRATION_TIME } = require('../helpers/time')
const { USD, deployDAI } = require('../helpers/tokens')(artifacts, web3)
const { deployContracts, createPayrollAndPriceFeed } = require('../helpers/deploy')(artifacts, web3)

contract('Payroll employee getters', ([owner, employee]) => {
let dao, payroll, payrollBase, finance, vault, priceFeed, DAI

const currentTimestamp = async () => payroll.getTimestampPublic()

before('deploy base apps and tokens', async () => {
({ dao, finance, vault, payrollBase } = await deployContracts(owner))
DAI = await deployDAI(owner, finance)
})

beforeEach('create payroll and price feed instance', async () => {
({ payroll, priceFeed } = await createPayrollAndPriceFeed(dao, payrollBase, owner, NOW))
})

describe('getEmployee', () => {
context('when it has already been initialized', () => {
beforeEach('initialize payroll app using USD as denomination token', async () => {
await payroll.initialize(finance.address, USD, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner })
})

context('when the given id exists', () => {
let employeeId
const salary = annualSalaryPerSecond(100000)

beforeEach('add employee', async () => {
const receipt = await payroll.addEmployee(employee, salary, await payroll.getTimestampPublic(), 'Boss', { from: owner })
employeeId = getEventArgument(receipt, 'AddEmployee', 'employeeId').toString()
})

it('adds a new employee', async () => {
const [address, employeeSalary, accruedSalary, bonus, reimbursements, lastPayroll, endDate] = await payroll.getEmployee(employeeId)

assert.equal(address, employee, 'employee address does not match')
assert.equal(employeeSalary.toString(), salary.toString(), 'employee salary does not match')
assert.equal(accruedSalary.toString(), 0, 'employee accrued salary does not match')
assert.equal(bonus.toString(), 0, 'employee bonus does not match')
assert.equal(reimbursements.toString(), 0, 'employee reimbursements does not match')
assert.equal(lastPayroll.toString(), (await currentTimestamp()).toString(), 'employee last payroll does not match')
assert.equal(endDate.toString(), MAX_UINT64, 'employee end date does not match')
})
})

context('when the given id does not exist', () => {
const employeeId = 0

it('reverts', async () => {
await assertRevert(payroll.getEmployee(employeeId), 'PAYROLL_EMPLOYEE_DOESNT_EXIST')
})
})
})

context('when it has not been initialized yet', () => {
const employeeId = 0

it('reverts', async () => {
await assertRevert(payroll.getEmployee(employeeId), 'PAYROLL_EMPLOYEE_DOESNT_EXIST')
})
})
})

describe('getEmployeeByAddress', () => {
context('when it has already been initialized', () => {
beforeEach('initialize payroll app using USD as denomination token', async () => {
await payroll.initialize(finance.address, USD, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner })
})

context('when the given address exists', () => {
let employeeId
const address = employee
const salary = annualSalaryPerSecond(100000)

beforeEach('add employee', async () => {
const receipt = await payroll.addEmployee(employee, salary, await payroll.getTimestampPublic(), 'Boss', { from: owner })
employeeId = getEventArgument(receipt, 'AddEmployee', 'employeeId')
})

it('adds a new employee', async () => {
const [id, employeeSalary, accruedSalary, bonus, reimbursements, lastPayroll, endDate] = await payroll.getEmployeeByAddress(address)

assert.equal(id.toString(), employeeId.toString(), 'employee id does not match')
assert.equal(employeeSalary.toString(), salary.toString(), 'employee salary does not match')
assert.equal(accruedSalary.toString(), 0, 'employee accrued salary does not match')
assert.equal(bonus.toString(), 0, 'employee bonus does not match')
assert.equal(reimbursements.toString(), 0, 'employee reimbursements does not match')
assert.equal(lastPayroll.toString(), (await currentTimestamp()).toString(), 'employee last payroll does not match')
assert.equal(endDate.toString(), MAX_UINT64, 'employee end date does not match')
})
})

context('when the given id does not exist', () => {

it('reverts', async () => {
await assertRevert(payroll.getEmployeeByAddress(employee), 'PAYROLL_EMPLOYEE_DOESNT_EXIST')
})
})
})

context('when it has not been initialized yet', () => {
it('reverts', async () => {
await assertRevert(payroll.getEmployeeByAddress(employee), 'PAYROLL_EMPLOYEE_DOESNT_EXIST')
})
})
})

describe('getTotalOwedSalary', () => {
context('when it has already been initialized', () => {
beforeEach('initialize payroll app using USD as denomination token', async () => {
await payroll.initialize(finance.address, USD, priceFeed.address, RATE_EXPIRATION_TIME, { from: owner })
})

context('when the given id exists', () => {
let employeeId
const salary = annualSalaryPerSecond(100000)

beforeEach('add employee', async () => {
const receipt = await payroll.addEmployee(employee, salary, await payroll.getTimestampPublic(), 'Boss', { from: owner })
employeeId = getEventArgument(receipt, 'AddEmployee', 'employeeId')
})

context('when the employee does not have owed salary', () => {
it('returns zero', async () => {
assert.equal((await payroll.getTotalOwedSalary(employeeId)).toString(), 0, 'total owed salary does not match')
})
})

context('when the employee has some owed salary', () => {
beforeEach('accumulate some payroll', async () => {
await payroll.mockIncreaseTime(ONE_MONTH)
})

context('when the employee does not have any other owed amount', () => {
it('returns the owed payroll', async () => {
const expectedOwedSalary = salary.mul(ONE_MONTH)
assert.equal((await payroll.getTotalOwedSalary(employeeId)).toString(), expectedOwedSalary.toString(), 'total owed salary does not match')
})
})

context('when the employee has some bonus', () => {
beforeEach('add bonus', async () => {
await payroll.addBonus(employeeId, 1000, { from: owner })
})

it('returns only the owed payroll', async () => {
const expectedOwedSalary = salary.mul(ONE_MONTH)
assert.equal((await payroll.getTotalOwedSalary(employeeId)).toString(), expectedOwedSalary.toString(), 'total owed salary does not match')
})
})

context('when the employee has some reimbursements', () => {
beforeEach('add reimbursement', async () => {
await payroll.addReimbursement(employeeId, 1000, { from: owner })
})

it('returns only the owed payroll', async () => {
const expectedOwedSalary = salary.mul(ONE_MONTH)
assert.equal((await payroll.getTotalOwedSalary(employeeId)).toString(), expectedOwedSalary.toString(), 'total owed salary does not match')
})
})

context('when the employee has some accrued salary', () => {
context('when the total owed amount does not overflow', () => {
beforeEach('add accrued salary', async () => {
await payroll.setEmployeeSalary(employeeId, salary.mul(2), { from: owner })
await payroll.mockIncreaseTime(ONE_MONTH)
})

it('returns the owed payroll plus the accrued salary', async () => {
const expectedOwedSalary = salary.mul(ONE_MONTH).plus(salary.mul(2).mul(ONE_MONTH))
assert.equal((await payroll.getTotalOwedSalary(employeeId)).toString(), expectedOwedSalary.toString(), 'total owed salary does not match')
})
})

context('when the total owed amount does overflow', () => {
beforeEach('add accrued salary', async () => {
await payroll.setEmployeeSalary(employeeId, MAX_UINT256, { from: owner })
await payroll.mockIncreaseTime(1)
})

it('returns max uint256', async () => {
assert.equal((await payroll.getTotalOwedSalary(employeeId)).toString(), MAX_UINT256.toString(), 'total owed salary does not match')
})
})
})
})
})

context('when the given id does not exist', () => {
const employeeId = 0

it('reverts', async () => {
await assertRevert(payroll.getTotalOwedSalary(employeeId), 'PAYROLL_EMPLOYEE_DOESNT_EXIST')
})
})
})

context('when it has not been initialized yet', () => {
const employeeId = 0

it('reverts', async () => {
await assertRevert(payroll.getTotalOwedSalary(employeeId), 'PAYROLL_EMPLOYEE_DOESNT_EXIST')
})
})
})
})
113 changes: 0 additions & 113 deletions future-apps/payroll/test/contracts/Payroll_get_employee.test.js

This file was deleted.

0 comments on commit b0714ce

Please sign in to comment.