diff --git a/server/dapp/dapp.js b/server/dapp/dapp.js index 9a53df4..b8ad8d8 100644 --- a/server/dapp/dapp.js +++ b/server/dapp/dapp.js @@ -145,6 +145,21 @@ function* receiveProject(userManager, projectManager, projectName) { return result; } +// function to reject the project +function* rejectProject(userManager, projectManager, projectName, buyerName) { + rest.verbose('dapp: rejectProject', projectName); + + // get the accepted bid + const bid = yield projectManager.getAcceptedBid(projectName); + + // get the buyer who create the project + const buyer = yield userManager.getUser(buyerName); + + // change state of the project to REJECTED and transfer the bid funds to buyer itself after REJECTING the project + const result = yield projectManager.rejectProject(projectName, buyer.account, bid.address); + return result; +} + // handle project event function* handleEvent(userManager, projectManager, args) { const name = args.name; @@ -154,6 +169,9 @@ function* handleEvent(userManager, projectManager, args) { case ProjectEvent.RECEIVE: return yield receiveProject(userManager, projectManager, args.projectName); + case ProjectEvent.REJECT: + return yield rejectProject(userManager, projectManager, args.projectName,args.username); + case ProjectEvent.ACCEPT: return yield acceptBid(userManager, projectManager, args.username, args.password, args.bidId, args.projectName); diff --git a/server/lib/bid/contracts/Bid.sol b/server/lib/bid/contracts/Bid.sol index 3263bb9..c9faa2b 100644 --- a/server/lib/bid/contracts/Bid.sol +++ b/server/lib/bid/contracts/Bid.sol @@ -52,4 +52,13 @@ contract Bid is ErrorCodes, BidState { supplierAddress.send(amountWei-fee); return ErrorCodes.SUCCESS; } + + function retrieve(address buyerAddress) returns (ErrorCodes) { + uint fee = 10000000 wei; // buyer absorbs the fee + uint amountWei = amount * 1 ether; + + // transfer to buyer + buyerAddress.send(amountWei-fee); + return ErrorCodes.SUCCESS; + } } diff --git a/server/lib/project/contracts/ProjectEvent.sol b/server/lib/project/contracts/ProjectEvent.sol index 9e27614..e190213 100644 --- a/server/lib/project/contracts/ProjectEvent.sol +++ b/server/lib/project/contracts/ProjectEvent.sol @@ -4,6 +4,7 @@ contract ProjectEvent { NULL, ACCEPT, DELIVER, - RECEIVE + RECEIVE, + REJECT } } diff --git a/server/lib/project/contracts/ProjectManager.sol b/server/lib/project/contracts/ProjectManager.sol index 63c91f0..fe10fba 100644 --- a/server/lib/project/contracts/ProjectManager.sol +++ b/server/lib/project/contracts/ProjectManager.sol @@ -98,6 +98,18 @@ contract ProjectManager is ErrorCodes, Util, ProjectState, ProjectEvent, BidStat return bid.settle(supplierAddress); } + function rejectProject(string name, address buyerAddress, address bidAddress) returns (ErrorCodes) { + // validate for the existence of the project + if (!exists(name)) return (ErrorCodes.NOT_FOUND); + // set project state + address projectAddress = getProject(name); + var (errorCode, state) = handleEvent(projectAddress, ProjectEvent.REJECT); + if (errorCode != ErrorCodes.SUCCESS) return errorCode; + // settle bid funds back to the buyer + Bid bid = Bid(bidAddress); + return bid.retrieve(buyerAddress); + } + /** * handleEvent - transition project to a new state based on incoming event */ @@ -133,6 +145,8 @@ contract ProjectManager is ErrorCodes, Util, ProjectState, ProjectEvent, BidStat if (state == ProjectState.INTRANSIT) { if (projectEvent == ProjectEvent.RECEIVE) return (ErrorCodes.SUCCESS, ProjectState.RECEIVED); + if (projectEvent == ProjectEvent.REJECT) + return (ErrorCodes.SUCCESS, ProjectState.REJECTED); } return (ErrorCodes.ERROR, state); } diff --git a/server/lib/project/contracts/ProjectState.sol b/server/lib/project/contracts/ProjectState.sol index e4fa8c6..c88b9d2 100644 --- a/server/lib/project/contracts/ProjectState.sol +++ b/server/lib/project/contracts/ProjectState.sol @@ -5,6 +5,7 @@ contract ProjectState { OPEN, PRODUCTION, INTRANSIT, - RECEIVED + RECEIVED, + REJECTED } } diff --git a/server/lib/project/projectManager.js b/server/lib/project/projectManager.js index 232d66b..54b7adf 100644 --- a/server/lib/project/projectManager.js +++ b/server/lib/project/projectManager.js @@ -62,6 +62,9 @@ function setContract(admin, contract) { contract.settleProject = function* (projectName, supplierAddress, bidAddress) { return yield settleProject(admin, contract, projectName, supplierAddress, bidAddress); } + contract.rejectProject = function* (projectName, buyerAddress, bidAddress) { + return yield rejectProject(admin, contract, projectName, buyerAddress, bidAddress); + } contract.getAcceptedBid = getAcceptedBid; return contract; @@ -208,6 +211,22 @@ function* settleProject(admin, contract, projectName, supplierAddress, bidAddres } } +function* rejectProject(admin, contract, projectName, buyerAddress, bidAddress) { + rest.verbose('rejectProject', {projectName, buyerAddress, bidAddress}); + const method = 'rejectProject'; + const args = { + name: projectName, + buyerAddress: buyerAddress, + bidAddress: bidAddress, + }; + + const result = yield rest.callMethod(admin, contract, method, args); + const errorCode = parseInt(result[0]); + if (errorCode != ErrorCodes.SUCCESS) { + throw new Error(errorCode); + } +} + function* getBid(bidId) { rest.verbose('getBid', bidId); return (yield rest.waitQuery(`Bid?id=eq.${bidId}`,1))[0]; diff --git a/server/lib/project/test/projectManager.test.js b/server/lib/project/test/projectManager.test.js index 13cc7b9..97f7242 100644 --- a/server/lib/project/test/projectManager.test.js +++ b/server/lib/project/test/projectManager.test.js @@ -456,6 +456,328 @@ describe('ProjectManager Life Cycle tests', function() { assert.equal(filtered.length, 1, 'one and only one'); }); + it('Reject the project and send bid price to Buyer', function* () { + const uid = util.uid(); + const projectArgs = createProjectArgs(uid); + const password = '1234'; + const amount = 23; + const amountWei = new BigNumber(amount).times(constants.ETHER); + const FAUCET_AWARD = new BigNumber(1000).times(constants.ETHER) ; + const GAS_LIMIT = new BigNumber(100000000); // default in bockapps-rest + + // create buyer and suppliers + const buyerArgs = createUserArgs(projectArgs.buyer, password, UserRole.BUYER); + const buyer = yield userManagerContract.createUser(buyerArgs); + buyer.password = password; // IRL this will be a prompt to the buyer + // create suppliers + const suppliers = yield createSuppliers(3, password, uid); + + // create project + const project = yield contract.createProject(projectArgs); + // create bids + const createdBids = yield createMultipleBids(projectArgs.name, suppliers, amount); + { // test + const bids = yield projectManagerJs.getBidsByName(projectArgs.name); + assert.equal(createdBids.length, bids.length, 'should find all the created bids'); + } + + // get the buyers balance before accepting a bid + buyer.initialBalance = yield userManagerContract.getBalance(buyer.username); + buyer.initialBalance.should.be.bignumber.eq(FAUCET_AWARD); + + // accept one bid (the first) + const acceptedBid = createdBids[0]; + yield contract.acceptBid(buyer, acceptedBid.id, projectArgs.name); + + // check the Project state after accepting the Bid + const projectInProduction = (yield rest.waitQuery(`${projectJs.contractName}?name=eq.${encodeURIComponent(projectArgs.name)}`, 1))[0]; + assert.equal(parseInt(projectInProduction.state), ProjectState.PRODUCTION, 'Project should be in PRODUCTION state'); + + // get the buyers balance after accepting a bid + buyer.balance = yield userManagerContract.getBalance(buyer.username); + + const delta = buyer.initialBalance.minus(buyer.balance); + delta.should.be.bignumber.gte(amountWei); // amount + fee + delta.should.be.bignumber.lte(amountWei.plus(GAS_LIMIT)); // amount + max fee (gas-limit) + // get the bids + const bids = yield projectManagerJs.getBidsByName(projectArgs.name); + // check that the expected bid is ACCEPTED and all others are REJECTED + bids.map(bid => { + if (bid.id === acceptedBid.id) { + assert.equal(parseInt(bid.state), BidState.ACCEPTED, 'bid should be ACCEPTED'); + } else { + assert.equal(parseInt(bid.state), BidState.REJECTED, 'bid should be REJECTED'); + }; + }); + + // buyer balance after accepting the bid + const buyerBalanceForBidIsAccepted = yield userManagerContract.getBalance(buyer.username); + + // deliver the project + const projectState = yield contract.handleEvent(projectArgs.name, ProjectEvent.DELIVER); + assert.equal(projectState, ProjectState.INTRANSIT, 'delivered project should be INTRANSIT '); + + // check for the supplier balance after delivering the project, bid amount should not be added + for (let supplier of suppliers) { + supplier.balance = yield userManagerContract.getBalance(supplier.username); + if (supplier.username == acceptedBid.supplier) { + supplier.balance.should.be.bignumber.eq(FAUCET_AWARD); + } + } + + // check for the buyer balance in "INTRANSIT" state, it should be same as in "PRODUCTION" state + const buyerBalanceInTransitState = yield userManagerContract.getBalance(buyer.username); + buyerBalanceForBidIsAccepted.should.be.bignumber.eq(buyerBalanceInTransitState); + + // Buyer balance before rejecting the project + const buyerValueBeforeRejecting = yield userManagerContract.getBalance(buyer.username); + + // reject the project with non-existing project + try { + nonExistingProject = yield rejectProject('',buyer.username); + } catch(error) { + const errorCode = error.message; + // error should be NOT_FOUND + assert.equal(errorCode, ErrorCodes.NOT_FOUND, 'error should be NOT_FOUND' + JSON.stringify(error)); + // check for the Buyer balance, bid value should not be added to his account + const buyerBalance = yield userManagerContract.getBalance(buyer.username); + buyerBalance.should.be.bignumber.eq(buyer.initialBalance.minus(delta)); + } + + // reject the exixting project + yield rejectProject(projectArgs.name,buyer.username); + + // Check whether bid value is added back to the Buyer account + delta.should.be.bignumber.eq(buyer.initialBalance.minus(buyerValueBeforeRejecting)); + + // check the Project state + const projectInRejection = (yield rest.waitQuery(`${projectJs.contractName}?name=eq.${encodeURIComponent(projectArgs.name)}`, 1))[0]; + assert.equal(parseInt(projectInRejection.state), ProjectState.REJECTED, 'Project should be in REJECTED state'); + + //supplier balance should be same + for (let supplier of suppliers) { + supplier.balance = yield userManagerContract.getBalance(supplier.username); + if (supplier.username == acceptedBid.supplier) { + supplier.balance.should.be.bignumber.eq(FAUCET_AWARD); + } + } + }); + + it('Reject the project which is in OPEN state', function* () { + const uid = util.uid(); + const projectArgs = createProjectArgs(uid); + const password = '1234'; + + // create buyer + const buyerArgs = createUserArgs(projectArgs.buyer, password, UserRole.BUYER); + const buyer = yield userManagerContract.createUser(buyerArgs); + buyer.password = password; // IRL this will be a prompt to the buyer + + // create project + const project = yield contract.createProject(projectArgs); + + // Project state should be OPEN after creating the project + assert.equal(parseInt(project.state), ProjectState.OPEN, 'Project should be in OPEN state'); + + // buyer amount before trying to Reject the project + const buyerAmountInOpenState = yield userManagerContract.getBalance(buyer.username); + + // Try to Reject the project in open state + try { + errorValue = yield rejectProject(projectArgs.name,buyer.username); + } catch(error) { + const errorCode = error.message; + //Project cannot be Rejected when it is OPEN : Results in NOT_FOUND because there is no accepted bid + assert.equal(errorCode, ErrorCodes.NOT_FOUND, 'error should be NOT_FOUND' + JSON.stringify(error)); + + // check for the Buyer balance, it should not change + const buyerBalance = yield userManagerContract.getBalance(buyer.username); + buyerBalance.should.be.bignumber.eq(buyerAmountInOpenState); + } + }); + + it('Reject the project which is in PRODUCTION', function* () { + const uid = util.uid(); + const projectArgs = createProjectArgs(uid); + const password = '1234'; + const amount = 23; + const amountWei = new BigNumber(amount).times(constants.ETHER); + const FAUCET_AWARD = new BigNumber(1000).times(constants.ETHER) ; + const GAS_LIMIT = new BigNumber(100000000); // default in bockapps-rest + + // create buyer and suppliers + const buyerArgs = createUserArgs(projectArgs.buyer, password, UserRole.BUYER); + const buyer = yield userManagerContract.createUser(buyerArgs); + buyer.password = password; // IRL this will be a prompt to the buyer + // create suppliers + const suppliers = yield createSuppliers(3, password, uid); + + // create project + const project = yield contract.createProject(projectArgs); + // create bids + const createdBids = yield createMultipleBids(projectArgs.name, suppliers, amount); + { // test + const bids = yield projectManagerJs.getBidsByName(projectArgs.name); + assert.equal(createdBids.length, bids.length, 'should find all the created bids'); + } + // get the buyers balance before accepting a bid + + buyer.initialBalance = yield userManagerContract.getBalance(buyer.username); + buyer.initialBalance.should.be.bignumber.eq(FAUCET_AWARD); + + // accept one bid (the first) + const acceptedBid = createdBids[0]; + yield contract.acceptBid(buyer, acceptedBid.id, projectArgs.name); + + // check the Project state after accepting the Bid + const projectInProduction = (yield rest.waitQuery(`${projectJs.contractName}?name=eq.${encodeURIComponent(projectArgs.name)}`, 1))[0]; + assert.equal(parseInt(projectInProduction.state), ProjectState.PRODUCTION, 'Project should be in PRODUCTION state'); + + // get the buyers balance after accepting a bid + buyer.balance = yield userManagerContract.getBalance(buyer.username); + + const delta = buyer.initialBalance.minus(buyer.balance); + delta.should.be.bignumber.gte(amountWei); // amount + fee + delta.should.be.bignumber.lte(amountWei.plus(GAS_LIMIT)); // amount + max fee (gas-limit) + // get the bids + const bids = yield projectManagerJs.getBidsByName(projectArgs.name); + // check that the expected bid is ACCEPTED and all others are REJECTED + bids.map(bid => { + if (bid.id === acceptedBid.id) { + assert.equal(parseInt(bid.state), BidState.ACCEPTED, 'bid should be ACCEPTED'); + } else { + assert.equal(parseInt(bid.state), BidState.REJECTED, 'bid should be REJECTED'); + }; + }); + + const buyerAmountAfterAcceptingBid = yield userManagerContract.getBalance(buyer.username); + + // Try to Reject the Project which is in PRODUCTION state + try { + errorValue = yield rejectProject(projectArgs.name,buyer.username); + } catch(error) { + const errorCode = error.message; + // Project cannot be Rejected in PRODUCTION state : Results in ERROR + assert.equal(errorCode, ErrorCodes.ERROR, 'error should be ERROR' + JSON.stringify(error)); + + // check for the Buyer balance, it should not change + const buyerBalance = yield userManagerContract.getBalance(buyer.username); + buyerBalance.should.be.bignumber.eq(buyerAmountAfterAcceptingBid); + } + }); + + + it('Reject the project after it is Accepted', function* () { + const uid = util.uid(); + const projectArgs = createProjectArgs(uid); + const password = '1234'; + const amount = 23; + const amountWei = new BigNumber(amount).times(constants.ETHER); + const FAUCET_AWARD = new BigNumber(1000).times(constants.ETHER) ; + const GAS_LIMIT = new BigNumber(100000000); // default in bockapps-rest + + // create buyer and suppliers + const buyerArgs = createUserArgs(projectArgs.buyer, password, UserRole.BUYER); + const buyer = yield userManagerContract.createUser(buyerArgs); + buyer.password = password; // IRL this will be a prompt to the buyer + // create suppliers + const suppliers = yield createSuppliers(3, password, uid); + + // create project + const project = yield contract.createProject(projectArgs); + // create bids + const createdBids = yield createMultipleBids(projectArgs.name, suppliers, amount); + { // test + const bids = yield projectManagerJs.getBidsByName(projectArgs.name); + assert.equal(createdBids.length, bids.length, 'should find all the created bids'); + } + + // get the buyers balance before accepting a bid + buyer.initialBalance = yield userManagerContract.getBalance(buyer.username); + buyer.initialBalance.should.be.bignumber.eq(FAUCET_AWARD); + + // accept one bid (the first) + const acceptedBid = createdBids[0]; + yield contract.acceptBid(buyer, acceptedBid.id, projectArgs.name); + + // check the Project state after accepting the Bid + const projectInProduction = (yield rest.waitQuery(`${projectJs.contractName}?name=eq.${encodeURIComponent(projectArgs.name)}`, 1))[0]; + assert.equal(parseInt(projectInProduction.state), ProjectState.PRODUCTION, 'Project should be in PRODUCTION state'); + + // get the buyers balance after accepting a bid + buyer.balance = yield userManagerContract.getBalance(buyer.username); + + const delta = buyer.initialBalance.minus(buyer.balance); + delta.should.be.bignumber.gte(amountWei); // amount + fee + delta.should.be.bignumber.lte(amountWei.plus(GAS_LIMIT)); // amount + max fee (gas-limit) + // get the bids + const bids = yield projectManagerJs.getBidsByName(projectArgs.name); + // check that the expected bid is ACCEPTED and all others are REJECTED + bids.map(bid => { + if (bid.id === acceptedBid.id) { + assert.equal(parseInt(bid.state), BidState.ACCEPTED, 'bid should be ACCEPTED'); + } else { + assert.equal(parseInt(bid.state), BidState.REJECTED, 'bid should be REJECTED'); + }; + }); + + // buyer balance after accepting the bid + const buyerBalanceAfterBidIsAccepted = yield userManagerContract.getBalance(buyer.username); + + // deliver the project + const projectState = yield contract.handleEvent(projectArgs.name, ProjectEvent.DELIVER); + assert.equal(projectState, ProjectState.INTRANSIT, 'delivered project should be INTRANSIT '); + + // check for the supplier balance after delivering the project, bid amount should not be added + for (let supplier of suppliers) { + supplier.balance = yield userManagerContract.getBalance(supplier.username); + if (supplier.username == acceptedBid.supplier) { + supplier.balance.should.be.bignumber.eq(FAUCET_AWARD); + } + } + + // check for the buyer balance in "INTRANSIT" state, it should be same as in "PRODUCTION" state + const buyerBalanceInTransitState = yield userManagerContract.getBalance(buyer.username); + buyerBalanceAfterBidIsAccepted.should.be.bignumber.eq(buyerBalanceInTransitState); + + // Accept the project which is in INTRANSIT state + yield receiveProject(projectArgs.name); + + // check the balace of suppliers after project is accepted + for (let supplier of suppliers) { + supplier.balance = yield userManagerContract.getBalance(supplier.username); + if (supplier.username == acceptedBid.supplier) { + // the winning supplier should have the bid amount minus the tx fee + const delta = supplier.balance.minus(FAUCET_AWARD); + const fee = new BigNumber(10000000); + delta.should.be.bignumber.eq(amountWei.minus(fee)); + } else { + // everyone else should have the otiginal value + supplier.balance.should.be.bignumber.eq(FAUCET_AWARD); + } + } + + // check the project state it should be in RECEIVED state + const projectInReceived = (yield rest.waitQuery(`${projectJs.contractName}?name=eq.${encodeURIComponent(projectArgs.name)}`, 1))[0]; + assert.equal(parseInt(projectInReceived.state), ProjectState.RECEIVED, 'Project should be in RECEIVED state'); + + //Buyer account balance after he Accepts the Project + const buyerBalanceAfterAcceptingProject = yield userManagerContract.getBalance(buyer.username); + + // Try to reject the project which is in RECEIVED state + try { + errorValue = yield rejectProject(projectArgs.name,buyer.username); + } catch(error) { + const errorCode = error.message; + // Project cannot be Rejected in RECEIVED state : Results in ERROR + assert.equal(errorCode, ErrorCodes.ERROR, 'error should be ERROR' + JSON.stringify(error)); + + // check for the Buyer balance, it should not change + const buyerBalance = yield userManagerContract.getBalance(buyer.username); + buyerBalance.should.be.bignumber.eq(buyerBalanceAfterAcceptingProject); + } + }); + it.skip('Accept a Bid (send funds into accepted bid), rejects the others, receive project, settle (send bid funds to supplier)', function* () { const uid = util.uid(); const projectArgs = createProjectArgs(uid); @@ -544,8 +866,22 @@ describe('ProjectManager Life Cycle tests', function() { yield contract.settleProject(projectName, supplier.account, bid.address); } + function* rejectProject(projectName,userName) { + rest.verbose('rejectProject', projectName); + + // get the accepted bid + const bid = yield projectManagerJs.getAcceptedBid(projectName); + + // get the Buyer who created the project + const buyer = yield userManagerContract.getUser(userName); + + // change state of the project to REJECTED and transfer the bid funds to buyer itself after REJECTING the project + yield contract.rejectProject(projectName, buyer.account, bid.address); + } + }); + // function createUser(address account, string username, bytes32 pwHash, UserRole role) returns (ErrorCodes) { function createUserArgs(name, password, role) { const args = { diff --git a/ui/src/constants.js b/ui/src/constants.js index 69d53f4..06fe83d 100644 --- a/ui/src/constants.js +++ b/ui/src/constants.js @@ -23,10 +23,15 @@ export const STATES = { state: 'RECEIVED', icon: 'mood' }, + 5: { + state: 'REJECTED', + icon: 'sentiment_very_dissatisfied' + }, OPEN: 1, PRODUCTION: 2, INTRANSIT: 3, RECEIVED: 4, + REJECTED: 5, } export const BID_STATES = { @@ -38,4 +43,4 @@ export const BID_STATES = { REJECTED: 3, } -export const PROJECT_EVENTS = ['NULL', 'Accepted', 'Shipped', 'Received'] +export const PROJECT_EVENTS = ['NULL', 'Accepted', 'Shipped', 'Received','Rejected'] diff --git a/ui/src/scenes/Projects/components/Project/index.js b/ui/src/scenes/Projects/components/Project/index.js index 91ba4ad..f971b18 100644 --- a/ui/src/scenes/Projects/components/Project/index.js +++ b/ui/src/scenes/Projects/components/Project/index.js @@ -67,7 +67,18 @@ class Project extends Component { key="mood" > mood - + , + + + ); } }