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

Payroll: Add owed salary getter function #900

Merged
merged 1 commit into from
Jul 3, 2019
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 28 additions & 7 deletions future-apps/payroll/contracts/Payroll.sol
Original file line number Diff line number Diff line change
Expand Up @@ -337,12 +337,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 @@ -473,6 +468,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 @@ -768,7 +772,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 @@ -814,6 +818,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.