diff --git a/server/dapp/dapp.js b/server/dapp/dapp.js index ae2354d..c943ed6 100644 --- a/server/dapp/dapp.js +++ b/server/dapp/dapp.js @@ -158,6 +158,18 @@ function* receiveProject(userManager, projectManager, projectName) { return result; } +// receive 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 created the project + const buyer = yield userManager.getUser(buyerName); + // Reject the project: change state to REJECTED and send money back to the buyer + const result = yield projectManager.rejectProject(projectName, bid.address, buyer.account); + return result; +} + // handle project event function* handleEvent(userManager, projectManager, args) { const name = args.name; @@ -167,6 +179,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..00bfc6c 100644 --- a/server/lib/bid/contracts/Bid.sol +++ b/server/lib/bid/contracts/Bid.sol @@ -45,11 +45,26 @@ contract Bid is ErrorCodes, BidState { if (this.balance < amount) { return ErrorCodes.INSUFFICIENT_BALANCE; } - uint fee = 10000000 wei; // supplier absorbs the fee + uint fee = 0 wei; // supplier absorbs the fee uint amountWei = amount * 1 ether; // transfer will throw supplierAddress.send(amountWei-fee); return ErrorCodes.SUCCESS; } -} + + //this function rejects the order in transit and sends funds back to the buyer + //note, having anybody able to call this function is bad security practice, but is done for simplicity in this sample app + function reject(address buyerAddress) returns (ErrorCodes) { + // confirm balance, to return error + if (this.balance < amount) { + return ErrorCodes.INSUFFICIENT_BALANCE; + } + + uint amountWei = amount * 1 ether; + + // transfer will throw + buyerAddress.send(amountWei); + return ErrorCodes.SUCCESS; + } +} \ No newline at end of file diff --git a/server/lib/bid/test/bid.test.js b/server/lib/bid/test/bid.test.js index 1c01eed..4f0f65f 100644 --- a/server/lib/bid/test/bid.test.js +++ b/server/lib/bid/test/bid.test.js @@ -43,6 +43,7 @@ describe('Bid tests', function() { assert.equal(bid.name, name, 'name'); assert.equal(bid.supplier, supplier, 'supplier'); assert.equal(bid.amount, amount, 'amount'); + assert.equal(bid.buyer, admin.address, 'buyer'); }); it('Search Contract', function* () { 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..b6c1bda 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 bidAddress, address buyerAddress) returns (ErrorCodes) { + // validity + 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; + // reject + Bid bid = Bid(bidAddress); + return bid.reject(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 8b74a0c..b21b3b2 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, bidAddress, buyerAddress) { + return yield rejectProject(admin, contract, projectName, bidAddress, buyerAddress); + } contract.getAcceptedBid = getAcceptedBid; return contract; @@ -205,6 +208,22 @@ function* settleProject(admin, contract, projectName, supplierAddress, bidAddres } } +function* rejectProject(admin, contract, projectName, bidAddress, buyerAddress) { + rest.verbose('rejectProject', {projectName, bidAddress, buyerAddress}); + const method = 'rejectProject'; + const args = { + name: projectName, + bidAddress: bidAddress, + buyerAddress: buyerAddress, + }; + + 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..4887039 100644 --- a/server/lib/project/test/projectManager.test.js +++ b/server/lib/project/test/projectManager.test.js @@ -456,7 +456,7 @@ describe('ProjectManager Life Cycle tests', function() { assert.equal(filtered.length, 1, 'one and only one'); }); - it.skip('Accept a Bid (send funds into accepted bid), rejects the others, receive project, settle (send bid funds to supplier)', function* () { + it('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); const password = '1234'; @@ -470,7 +470,7 @@ describe('ProjectManager Life Cycle tests', function() { 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); + const suppliers = yield createSuppliers(1, password, uid); // create project const project = yield contract.createProject(projectArgs); @@ -513,13 +513,83 @@ describe('ProjectManager Life Cycle tests', function() { 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)); + delta.should.be.bignumber.eq(amountWei); } else { // everyone else should have the otiginal value supplier.balance.should.be.bignumber.eq(FAUCET_AWARD); } } + yield rest.getState(acceptedBid); + }); + + it.only('Accept a Bid (send funds into accepted bid), rejects the others, receive project, reject (send bid funds back to supplier)', 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(1, 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); + // 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'); + }; + }); + // deliver the project + const projectState = yield contract.handleEvent(projectArgs.name, ProjectEvent.DELIVER); + assert.equal(projectState, ProjectState.INTRANSIT, 'delivered project should be INTRANSIT '); + // receive the project + yield rejectProject(projectArgs.name, buyer.username); + + // get the suppliers balances + for (let supplier of suppliers) { + supplier.balance = yield userManagerContract.getBalance(supplier.username); + if (supplier.username == acceptedBid.supplier) { + // the winning supplier should NOT have the bid amount + const delta = supplier.balance.minus(FAUCET_AWARD); + delta.should.be.bignumber.eq(0); + } else { + // everyone else should have the original value + supplier.balance.should.be.bignumber.eq(FAUCET_AWARD); + } + } + + //we're just saying up to 1 ether is used for gas fees for testing purposes. The actual amount of gas used will be much lower. + const expectedValueFloor = buyer.initialBalance.minus(constants.ETHER); + buyer.balance.should.be.bignumber.lte(buyer.initialBalance); //accounting for gas fees we should be less than our initial balance + buyer.balance.should.be.bignumber.gte(expectedValueFloor); //we should be larger though than if we had still had 23 eth locked in escrow }); function* createSuppliers(count, password, uid) { @@ -544,6 +614,17 @@ describe('ProjectManager Life Cycle tests', function() { yield contract.settleProject(projectName, supplier.account, bid.address); } + // throws: ErrorCodes + function* rejectProject(projectName, buyerName) { + rest.verbose('rejectProject', projectName); + // get the accepted bid + const bid = yield projectManagerJs.getAcceptedBid(projectName); + // get the supplier for the accepted bid + const buyer = yield userManagerContract.getUser(buyerName); + // Settle the project: change state to REJECTED and send the funds back to the buyer + yield contract.rejectProject(projectName, bid.address, buyer.address); + } + }); // function createUser(address account, string username, bytes32 pwHash, UserRole role) returns (ErrorCodes) { diff --git a/ui/src/constants.js b/ui/src/constants.js index 69d53f4..7db0d0c 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: 'mood_bad' + }, 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 30e6c1a..23ae442 100644 --- a/ui/src/scenes/Projects/components/Project/index.js +++ b/ui/src/scenes/Projects/components/Project/index.js @@ -58,6 +58,7 @@ class Project extends Component { if (this.isBuyer) { if (parseInt(project.state, 10) === STATES.INTRANSIT) { + //the 3 is a receive event and the 4 is a REJECT event (see ProjectEvent.sol) actions.push( + , + , ); } }