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(
+ ,
+ ,
);
}
}