From 071758f8fc04fe9e33906b0b1cc001c9788d86ea Mon Sep 17 00:00:00 2001 From: Erin Hughes Date: Mon, 30 Apr 2018 13:56:32 +0100 Subject: [PATCH] Adding letters of credit network Signed-off-by: Erin Hughes updated readme, network and acl (#2) Signed-off-by: awjh-ibm fix bank employees unable to approve (#3) Signed-off-by: awjh-ibm Updating with final bank names Signed-off-by: Erin Hughes Adding network images Signed-off-by: Erin Hughes Removing acme from namespace Signed-off-by: Erin Hughes Removing acme from acl file Signed-off-by: Erin Hughes --- package.json | 2 +- .../letters-of-credit-network/.eslintignore | 17 + .../letters-of-credit-network/.eslintrc.yml | 45 ++ packages/letters-of-credit-network/.gitignore | 63 ++ packages/letters-of-credit-network/.npmignore | 60 ++ .../letters-of-credit-network/LICENSE.txt | 11 + packages/letters-of-credit-network/README.md | 243 +++++++ .../config/default.json | 4 + packages/letters-of-credit-network/index.js | 13 + packages/letters-of-credit-network/jsdoc.json | 35 + .../letters-of-credit-network/lib/logic.js | 311 ++++++++ .../models/org.acme.loc.cto | 146 ++++ .../networkimage.svg | 1 + .../networkimageanimated.svg | 1 + .../letters-of-credit-network/package.json | 40 ++ .../letters-of-credit-network/permissions.acl | 276 +++++++ .../letters-of-credit-network/test/script.js | 671 ++++++++++++++++++ packages/vehicle-lifecycle-network/jsdoc.json | 2 +- .../vehicle-manufacture-network/jsdoc.json | 2 +- 19 files changed, 1940 insertions(+), 3 deletions(-) create mode 100644 packages/letters-of-credit-network/.eslintignore create mode 100755 packages/letters-of-credit-network/.eslintrc.yml create mode 100644 packages/letters-of-credit-network/.gitignore create mode 100644 packages/letters-of-credit-network/.npmignore create mode 100644 packages/letters-of-credit-network/LICENSE.txt create mode 100644 packages/letters-of-credit-network/README.md create mode 100644 packages/letters-of-credit-network/config/default.json create mode 100644 packages/letters-of-credit-network/index.js create mode 100644 packages/letters-of-credit-network/jsdoc.json create mode 100644 packages/letters-of-credit-network/lib/logic.js create mode 100644 packages/letters-of-credit-network/models/org.acme.loc.cto create mode 100644 packages/letters-of-credit-network/networkimage.svg create mode 100644 packages/letters-of-credit-network/networkimageanimated.svg create mode 100644 packages/letters-of-credit-network/package.json create mode 100644 packages/letters-of-credit-network/permissions.acl create mode 100644 packages/letters-of-credit-network/test/script.js diff --git a/package.json b/package.json index aea9230..69af139 100644 --- a/package.json +++ b/package.json @@ -62,4 +62,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/letters-of-credit-network/.eslintignore b/packages/letters-of-credit-network/.eslintignore new file mode 100644 index 0000000..e40f16a --- /dev/null +++ b/packages/letters-of-credit-network/.eslintignore @@ -0,0 +1,17 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +coverage +dist +go +node_modules +out \ No newline at end of file diff --git a/packages/letters-of-credit-network/.eslintrc.yml b/packages/letters-of-credit-network/.eslintrc.yml new file mode 100755 index 0000000..3438670 --- /dev/null +++ b/packages/letters-of-credit-network/.eslintrc.yml @@ -0,0 +1,45 @@ +env: + es6: true + node: true + mocha: true +extends: 'eslint:recommended' +parserOptions: + ecmaVersion: 8 + sourceType: + - script +rules: + indent: + - error + - 4 + linebreak-style: + - error + - unix + quotes: + - error + - single + semi: + - error + - always + no-unused-vars: + - error + - args: none + no-var: error + no-console: off + curly: error + eqeqeq: error + no-throw-literal: error + dot-notation: error + no-tabs: error + no-trailing-spaces: error + no-useless-call: error + no-with: error + operator-linebreak: error + require-jsdoc: + - error + - require: + ClassDeclaration: true + MethodDefinition: true + FunctionDeclaration: true + yoda: error + no-confusing-arrow: 2 + no-constant-condition: 2 diff --git a/packages/letters-of-credit-network/.gitignore b/packages/letters-of-credit-network/.gitignore new file mode 100644 index 0000000..244d061 --- /dev/null +++ b/packages/letters-of-credit-network/.gitignore @@ -0,0 +1,63 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules +jspm_packages + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# JSDoc +out + +# Mac files. +**/.DS_Store + +*.swp + +# Build generated files should be ignored by git, but not by npm. +dist + +node_modules +composer-logs diff --git a/packages/letters-of-credit-network/.npmignore b/packages/letters-of-credit-network/.npmignore new file mode 100644 index 0000000..2b32e2b --- /dev/null +++ b/packages/letters-of-credit-network/.npmignore @@ -0,0 +1,60 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules +jspm_packages + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# JSDoc +out + +# Mac files. +**/.DS_Store + +*.swp + +# Build generated files should be ignored by git, but not by npm. +# dist diff --git a/packages/letters-of-credit-network/LICENSE.txt b/packages/letters-of-credit-network/LICENSE.txt new file mode 100644 index 0000000..86a8bee --- /dev/null +++ b/packages/letters-of-credit-network/LICENSE.txt @@ -0,0 +1,11 @@ +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/packages/letters-of-credit-network/README.md b/packages/letters-of-credit-network/README.md new file mode 100644 index 0000000..0a78346 --- /dev/null +++ b/packages/letters-of-credit-network/README.md @@ -0,0 +1,243 @@ +# Letters of Credit Network + +> This network tracks letters of credit from application through to closure. + +## Models within this business network + +### Participants +`Customer`, `Bank Employee` + +### Assets +`LetterOfCredit` + +### Transactions +`InitialApplication`, `Approve`, `Reject`, `SuggestChanges`, `ShipProduct`, `ReceiveProduct`, `ReadyForPayment`, `Close`, `CreateDemoParticipants` + +### Events +`InitialApplicationEvent`, `ApproveEvent`, `RejectEvent`, `SuggestChangesEvent`, `ShipProductEvent`, `ReceiveProductEvent`, `ReadyForPaymentEvent`, `CloseEvent` + +## Example use of this Business network +Two parties, each a `Customer` of a bank come to an agreement that Party A will import x number of Product Y from Party B. + +Party A uses their banking application to request a letter of credit using details from the agreement. The banking application submits an `InitialApplication` transaction which creates a new `LetterOfCredit` asset containing details of the two parties, their banks and their agreement. + +A `BankEmployee` at Party A's bank uses the banks internal application to review the letter of credit. The employee decides they do not like the rules set out in the application and therefore they decide to suggest changes using their application to submit a `SuggestChanges` transaction. + +Party A is alerted on their banking application that changes have been suggested in the letter of credit. Party A reviews the changes to the rules suggested by their bank and approves the changes using their banking application to submit an `Approve` transaction. + +The letter is now approved by both Party A and their bank. A `BankEmployee` at Party B's bank uses the banks internal application to review the letter of credit. The employee uses their application to submit an `Approve` transaction to mark that Party B's bank is happy with the issuance of the letter of credit to their customer, Party B. + +Party B is alerted to the letter of credit in their banking application. They use the application to review the letter and approve it using their application to submit an `Approve` transaction. The letter is now unable to be changed any further. + +Once Party B has manufactured the goods and sent them for shipping Party B uses their banking application to access the letter of credit and mark the goods relating to it as shipped using a `ShipProduct` transaction, passing with the transaction a hash of the shipping documents to be used as proof of shipping. + +When the goods arrive at their destination Party A reviews what they have received and checks that the received goods match the rules set out in the letter of credit. Party A is happy that they do and uses the banking application to submit a `ReceiveProduct` transaction. + +A `BankEmployee` at Party A's bank is alerted that Party A has stated that the goods are received. The employee does their own checks on the goods against the letter of credit rules and then uses their banking application to submit a `ReadyForPayment` transaction. Their own internal system then transfers funds to Party B's bank. + +A `BankEmployee` at Party B's bank is alerted that the letter of credit is ready to be paid and confirms that Party A has transferred the funds. The employee uses their banking application to close the letter of credit using a `Close` transaction and pay the money into Party B's account. + +## Testing this network within playground +The steps below will simulate the above scenario within playground. + +Navigate to the **Test** tab and then submit a `CreateDemoParticipants` transaction: + +``` +{ + "$class": "org.acme.loc.CreateDemoParticipants" +} +``` + +Navigate to the ID registry and generate IDs for: + +``` +org.acme.loc.Customer#alice +org.acme.loc.Customer#bob +org.acme.loc.BankEmployee#matias +org.acme.loc.BankEmployee#ella +``` + +Select to Alice to be your identity. + +Navigate to the **Test** tab and then submit an `InitialApplication` transaction to request a letter of credit. The application will cover Alice and Bob's prior agreement to the following: +- Bob will deliver 100 computers +- Alice will pay $450 for each computer +- The computers will be received in working order +- The computers will be received within 30 days + +``` +{ + "$class": "org.acme.loc.InitialApplication", + "letterId": "LETTER-REF-123", + "applicant": "resource:org.acme.loc.Customer#alice", + "beneficiary": "resource:org.acme.loc.Customer#bob", + "rules": [ + { + "ruleId": "LETTER-REF-123-RULE-1", + "ruleText": "The computers will be received in working order" + }, + { + "ruleId": "LETTER-REF-123-RULE-2", + "ruleText": "The computers will be received within 30 days" + } + ], + "productDetails": { + "$class": "org.acme.loc.ProductDetails", + "productType": "Computers", + "quantity": 100, + "pricePerUnit": 450 + } +} +``` + +This creates a `LetterOfCredit` asset and sets the issuingBank to be Alice's bank and exportersBank to be Bob's bank. + +--- + +Use the ID registry to select Matias to be your identity. + +Matias works at Alice's bank. Review the letter of credit by selecting the `LetterOfCredit` asset. Matias decides that the rules are not suitable for the bank and therefore he decides to suggest changes. + +Submit a `SuggestChanges` transaction to change the rules: + +``` +{ + "$class": "org.acme.loc.SuggestChanges", + "loc": "resource:org.acme.loc.LetterOfCredit#4572", + "rules": [ + { + "ruleId": "LETTER-REF-123-RULE-1", + "ruleText": "The computers will be received in working order" + }, + { + "ruleId": "LETTER-REF-123-RULE-2-UPDATED", + "ruleText": "The computers will be received within 15 days" + } + ], + "suggestingParty": "resource:org.acme.loc.BankEmployee#matias" +} +``` + +This transaction clears the array `LetterOfCredit` asset's `approval` field, updating it contain only the suggesting party and replaces the rules with the updated rules. + +--- + +Use the ID registry to select Alice to be your identity. + +Review the changes made to the letter of credit by selecting the `LetterOfCredit` asset. Alice agrees with the changes and decides she will approve. + +Approve the letter by submitting an `ApproveTransaction`: + +``` +{ + "$class": "org.acme.loc.Approve", + "loc": "resource:org.acme.loc.LetterOfCredit#LETTER-REF-123", + "approvingParty": "resource:org.acme.loc.Customer#alice" +} +``` + +This transaction adds Alice to the array of parties in the `approval` field of the `LetterOfCredit` asset. + +--- + +Use the ID registry to select Ella to be your identity. + +Ella works at Bob's bank. Review the letter of credit by selecting the `LetterOfCredit` asset. Ella decides that the letter of credit is acceptable to her bank and that she will approve the request. + +Approve the letter by submitting an `ApproveTransaction`: + +``` +{ + "$class": "org.acme.loc.Approve", + "loc": "resource:org.acme.loc.LetterOfCredit#LETTER-REF-123", + "approvingParty": "resource:org.acme.loc.BankEmployee#ella" +} +``` + +This transaction adds Ella to the array of parties in the `approval` field of the `LetterOfCredit` asset. + +--- + +Use the ID registry to select Bob to be your identity. + +Bob reviews the letter of credit to ensure that it matches with his and Alice's agreement. He notices that the rules don't quite match the agreement however he still decides to accept the letter of credit and go through with the deal. + +Approve the letter by submitting an `ApproveTransaction`: + +``` +{ + "$class": "org.acme.loc.Approve", + "loc": "resource:org.acme.loc.LetterOfCredit#LETTER-REF-123", + "approvingParty": "resource:org.acme.loc.Customer#bob" +} +``` + +This transaction adds Bob to the array of parties in the `approval` field of the `LetterOfCredit` asset. As now all the parties have submitted their approval the `status` field is also updated to be `APPROVED`. At this point no participant may reject or suggest changes to the letter. Further participants (e.g. other bank employees) are also blocked from adding their approval. + +Bob manufactures the computers and gives ships them. He updates the `LetterOfCredit` asset to add proof that he has shipped the goods. + +Submit a `ShipProduct` transaction: + +``` +{ + "$class": "org.acme.loc.ShipProduct", + "loc": "resource:org.acme.loc.LetterOfCredit#LETTER-REF-123", + "evidence": "337478411cab754ce47fcaa72ec1d0f6" +} +``` + +This transaction updates the `status` field of the `LetterOfCredit` asset to be `SHIPPED` and adds the evidence provided (in this case a hash of the shipping invoice) to the `evidence` array. + +--- + +Use the ID registry to select Alice to be your identity. + +Alice now receives the goods and has inspected them. The goods arrived within 14 days and were in working order and therefore Alice decides to update the letter of credit to show she has accepted delivery. + +Submit a `ReceiveProduct` transaction: + +``` +{ + "$class": "org.acme.loc.ReceiveProduct", + "loc": "resource:org.acme.loc.LetterOfCredit#LETTER-REF-123" +} +``` + +This transaction updates the `LetterOfCredit` asset to have a `status` of `RECEIVED`. + +--- + +Use the ID registry to select Matias to be your identity. + +Matias inspects the goods received by Alice and agrees that they meet the rules set out in the letter. He therefore decides to approve the transfer of funds to Bob's bank and updates the letter of credit to show that these funds are ready. + +Submit a `ReadyForPayment` transaction: + +``` +{ + "$class": "org.acme.loc.ReadyForPayment", + "loc": "resource:org.acme.loc.LetterOfCredit#LETTER-REF-123" +} +``` + +This transaction updates the `status` of the letter of credit to be `READY_FOR_PAYMENT`. The payment is not covered by this network. + +--- + +Use the ID registry to select Ella to be your identity. + +Ella having received the funds from Alice's bank can close the letter of credit and deposit the funds in Bob's bank account. + +Submit a `Close` transaction: + +``` +{ + "$class": "org.acme.loc.Close", + "loc": "resource:org.acme.loc.LetterOfCredit#LETTER-REF-123", + "closeReason": "Payment made" +} +``` + +This transaction updates the `status` of the letter to be `CLOSED`. The letter is now complete and no further transactions can take place. + +This business network has been used to create demo application that simulate the scenario above. You can find more detail on these at https://github.com/hyperledger/composer-sample-applications/tree/master/packages/letters-of-credit \ No newline at end of file diff --git a/packages/letters-of-credit-network/config/default.json b/packages/letters-of-credit-network/config/default.json new file mode 100644 index 0000000..8bad574 --- /dev/null +++ b/packages/letters-of-credit-network/config/default.json @@ -0,0 +1,4 @@ +{ + + "config":"" +} diff --git a/packages/letters-of-credit-network/index.js b/packages/letters-of-credit-network/index.js new file mode 100644 index 0000000..e1971c4 --- /dev/null +++ b/packages/letters-of-credit-network/index.js @@ -0,0 +1,13 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ diff --git a/packages/letters-of-credit-network/jsdoc.json b/packages/letters-of-credit-network/jsdoc.json new file mode 100644 index 0000000..ca000e4 --- /dev/null +++ b/packages/letters-of-credit-network/jsdoc.json @@ -0,0 +1,35 @@ +{ + "tags": { + "allowUnknownTags": true, + "dictionaries": ["jsdoc","closure"] + }, + "source": { + "include": [ + "./lib" + ], + "includePattern": ".+\\.js(doc|x)?$" + }, + "plugins": ["plugins/markdown"], + "templates": { + "logoFile": "", + "cleverLinks": false, + "monospaceLinks": false, + "dateFormat": "ddd MMM Do YYYY", + "outputSourceFiles": true, + "outputSourcePath": true, + "systemName": "Perishable Goods Network", + "footer": "", + "copyright": "Released under the Apache License v2.0", + "navType": "vertical", + "theme": "spacelab", + "linenums": true, + "collapseSymbols": false, + "inverseNav": true, + "protocol": "html://", + "methodHeadingReturns": false + }, + "markdown": { + "parser": "gfm", + "hardwrap": true + } +} diff --git a/packages/letters-of-credit-network/lib/logic.js b/packages/letters-of-credit-network/lib/logic.js new file mode 100644 index 0000000..929d985 --- /dev/null +++ b/packages/letters-of-credit-network/lib/logic.js @@ -0,0 +1,311 @@ +'use strict'; + +/** + * Create the LOC asset + * @param {org.example.loc.InitialApplication} initalAppliation - the InitialApplication transaction + * @transaction + */ +async function initialApplication(application) { + const factory = getFactory(); + const namespace = 'org.example.loc'; + + const letter = factory.newResource(namespace, 'LetterOfCredit', application.letterId); + letter.applicant = factory.newRelationship(namespace, 'Customer', application.applicant.getIdentifier()); + letter.beneficiary = factory.newRelationship(namespace, 'Customer', application.beneficiary.getIdentifier()); + letter.issuingBank = factory.newRelationship(namespace, 'Bank', application.applicant.bank.getIdentifier()); + letter.exportingBank = factory.newRelationship(namespace, 'Bank', application.beneficiary.bank.getIdentifier()); + letter.rules = application.rules; + letter.productDetails = application.productDetails; + letter.evidence = []; + letter.approval = [factory.newRelationship(namespace, 'Customer', application.applicant.getIdentifier())]; + letter.status = 'AWAITING_APPROVAL'; + + //save the application + const assetRegistry = await getAssetRegistry(letter.getFullyQualifiedType()); + await assetRegistry.add(letter); + + // emit event + const applicationEvent = factory.newEvent(namespace, 'InitialApplicationEvent'); + applicationEvent.loc = letter; + emit(applicationEvent); +} + +/** + * Update the LOC to show that it has been approved by a given person + * @param {org.example.loc.Approve} approve - the Approve transaction + * @transaction + */ +async function approve(approveRequest) { + const factory = getFactory(); + const namespace = 'org.example.loc'; + + let letter = approveRequest.loc; + + if (letter.status === 'CLOSED' || letter.status === 'REJECTED') { + throw new Error ('This letter of credit has already been closed'); + } else if (letter.approval.length === 4) { + throw new Error ('All four parties have already approved this letter of credit'); + } else if (letter.approval.includes(approveRequest.approvingParty)) { + throw new Error ('This person has already approved this letter of credit'); + } else if (approveRequest.approvingParty.getType() === 'BankEmployee') { + letter.approval.forEach((approvingParty) => { + if (approvingParty.getType() === 'BankEmployee' && approvingParty.bank.getIdentifier() === approveRequest.approvingParty.bank.getIdentifier()) { + throw new Error('Your bank has already approved of this request'); + } + }); + } + + letter.approval.push(factory.newRelationship(namespace, approveRequest.approvingParty.getType(), approveRequest.approvingParty.getIdentifier())); + // update the status of the letter if everyone has approved + if (letter.approval.length === 4) { + letter.status = 'APPROVED'; + } + + // update approval[] + const assetRegistry = await getAssetRegistry(approveRequest.loc.getFullyQualifiedType()); + await assetRegistry.update(letter); + + // emit event + const approveEvent = factory.newEvent(namespace, 'ApproveEvent'); + approveEvent.loc = approveRequest.loc; + approveEvent.approvingParty = approveRequest.approvingParty; + emit(approveEvent); +} + +/** + * Reject the LOC + * @param {org.example.loc.Reject} reject - the Reject transaction + * @transaction + */ +async function reject(rejectRequest) { + const factory = getFactory(); + const namespace = 'org.example.loc'; + + let letter = rejectRequest.loc; + + if (letter.status === 'CLOSED' || letter.status === 'REJECTED') { + throw new Error('This letter of credit has already been closed'); + } else if (letter.status === 'APPROVED') { + throw new Error('This letter of credit has already been approved'); + } else { + letter.status = 'REJECTED'; + letter.closeReason = rejectRequest.closeReason; + + // update the status of the LOC + const assetRegistry = await getAssetRegistry(rejectRequest.loc.getFullyQualifiedType()); + await assetRegistry.update(letter); + + // emit event + const rejectEvent = factory.newEvent(namespace, 'RejectEvent'); + rejectEvent.loc = rejectRequest.loc; + rejectEvent.closeReason = rejectRequest.closeReason; + emit(rejectEvent); + } +} + +/** + * Suggest changes to the current rules in the LOC + * @param {org.example.loc.SuggestChanges} suggestChanges - the SuggestChanges transaction + * @transaction + */ +async function suggestChanges(changeRequest) { + const factory = getFactory(); + const namespace = 'org.example.loc'; + + let letter = changeRequest.loc; + + if (letter.status === 'CLOSED' || letter.status === 'REJECTED') { + throw new Error ('This letter of credit has already been closed'); + } else if (letter.status === 'APPROVED') { + throw new Error('This letter of credit has already been approved'); + } else if (letter.status === 'SHIPPED' || letter.status === 'RECEIVED' || letter.status === 'READY_FOR_PAYMENT') { + throw new Error ('The product has already been shipped'); + } else { + letter.rules = changeRequest.rules; + // the rules have been changed - clear the approval array and update status + letter.approval = [changeRequest.suggestingParty]; + letter.status = 'AWAITING_APPROVAL'; + + // update the loc with the new rules + const assetRegistry = await getAssetRegistry(changeRequest.loc.getFullyQualifiedType()); + await assetRegistry.update(letter); + + // emit event + const changeEvent = factory.newEvent(namespace, 'SuggestChangesEvent'); + changeEvent.loc = changeRequest.loc; + changeEvent.rules = changeRequest.rules; + changeEvent.suggestingParty = changeRequest.suggestingParty; + emit(changeEvent); + } +} + +/** + * "Ship" the product + * @param {org.example.loc.ShipProduct} shipProduct - the ShipProduct transaction + * @transaction + */ +async function shipProduct(shipRequest) { + const factory = getFactory(); + const namespace = 'org.example.loc'; + + let letter = shipRequest.loc; + + if (letter.status === 'APPROVED') { + letter.status = 'SHIPPED'; + letter.evidence.push(shipRequest.evidence); + + //update the status of the loc + const assetRegistry = await getAssetRegistry(shipRequest.loc.getFullyQualifiedType()); + await assetRegistry.update(letter); + + // emit event + const shipEvent = factory.newEvent(namespace, 'ShipProductEvent'); + shipEvent.loc = shipRequest.loc; + emit(shipEvent); + } else if (letter.status === 'AWAITING_APPROVAL') { + throw new Error ('This letter needs to be fully approved before the product can be shipped'); + } else if (letter.status === 'CLOSED' || letter.status === 'REJECTED') { + throw new Error ('This letter of credit has already been closed'); + } else { + throw new Error ('The product has already been shipped'); + } +} + +/** + * "Recieve" the product that has been "shipped" + * @param {org.example.loc.ReceiveProduct} receiveProduct - the ReceiveProduct transaction + * @transaction + */ +async function receiveProduct(receiveRequest) { + const factory = getFactory(); + const namespace = 'org.example.loc'; + + let letter = receiveRequest.loc; + + if (letter.status === 'SHIPPED') { + letter.status = 'RECEIVED'; + + // update the status of the loc + const assetRegistry = await getAssetRegistry(receiveRequest.loc.getFullyQualifiedType()); + await assetRegistry.update(letter); + + // emit event + const receiveEvent = factory.newEvent(namespace, 'ReceiveProductEvent'); + receiveEvent.loc = receiveRequest.loc; + emit(receiveEvent); + } else if (letter.status === 'AWAITING_APPROVAL' || letter.status === 'APPROVED'){ + throw new Error('The product needs to be shipped before it can be received'); + } else if (letter.status === 'CLOSED' || letter.status === 'REJECTED') { + throw new Error ('This letter of credit has already been closed'); + } else { + throw new Error('The product has already been received'); + } +} + + +/** + * Mark a given letter as "ready for payment" + * @param {org.example.loc.ReadyForPayment} readyForPayment - the ReadyForPayment transaction + * @transaction + */ +async function readyForPayment(paymentRequest) { + const factory = getFactory(); + const namespace = 'org.example.loc'; + + let letter = paymentRequest.loc; + + if (letter.status === 'RECEIVED') { + letter.status = 'READY_FOR_PAYMENT'; + + // update the status of the loc + const assetRegistry = await getAssetRegistry(paymentRequest.loc.getFullyQualifiedType()); + await assetRegistry.update(letter); + + // emit event + const paymentEvent = factory.newEvent(namespace, 'ReadyForPaymentEvent'); + paymentEvent.loc = paymentRequest.loc; + emit(paymentEvent); + } else if (letter.status === 'CLOSED' || letter.status === 'REJECTED') { + throw new Error('This letter of credit has already been closed'); + } else if (letter.status === 'READY_FOR_PAYMENT') { + throw new Error('The payment has already been made'); + } else { + throw new Error('The payment cannot be made until the product has been received by the applicant'); + } +} + +/** + * Close the LOC + * @param {org.example.loc.Close} close - the Close transaction + * @transaction + */ +async function close(closeRequest) { + const factory = getFactory(); + const namespace = 'org.example.loc'; + + let letter = closeRequest.loc; + + if (letter.status === 'READY_FOR_PAYMENT') { + letter.status = 'CLOSED'; + letter.closeReason = closeRequest.closeReason; + + // update the status of the loc + const assetRegistry = await getAssetRegistry(closeRequest.loc.getFullyQualifiedType()); + await assetRegistry.update(letter); + + // emit event + const closeEvent = factory.newEvent(namespace, 'CloseEvent'); + closeEvent.loc = closeRequest.loc; + closeEvent.closeReason = closeRequest.closeReason; + emit(closeEvent); + } else if (letter.status === 'CLOSED' || letter.status === 'REJECTED') { + throw new Error('This letter of credit has already been closed'); + } else { + throw new Error('Cannot close this letter of credit until it is fully approved and the product has been received by the applicant'); + } +} + +/** + * Create the participants needed for the demo + * @param {org.example.loc.CreateDemoParticipants} createDemoParticipants - the CreateDemoParticipants transaction + * @transaction + */ +async function createDemoParticipants() { + const factory = getFactory(); + const namespace = 'org.example.loc'; + + // create the banks + const bankRegistry = await getParticipantRegistry(namespace + '.Bank'); + const bank1 = factory.newResource(namespace, 'Bank', 'BoD'); + bank1.name = 'Bank of Dinero'; + await bankRegistry.add(bank1); + const bank2 = factory.newResource(namespace, 'Bank', 'EB'); + bank2.name = 'Eastwood Banking'; + await bankRegistry.add(bank2); + + // create bank employees + const employeeRegistry = await getParticipantRegistry(namespace + '.BankEmployee'); + const employee1 = factory.newResource(namespace, 'BankEmployee', 'matias'); + employee1.name = 'Matías'; + employee1.bank = factory.newRelationship(namespace, 'Bank', 'BoD'); + await employeeRegistry.add(employee1); + const employee2 = factory.newResource(namespace, 'BankEmployee', 'ella'); + employee2.name = 'Ella'; + employee2.bank = factory.newRelationship(namespace, 'Bank', 'EB'); + await employeeRegistry.add(employee2); + + // create customers + const customerRegistry = await getParticipantRegistry(namespace + '.Customer'); + const customer1 = factory.newResource(namespace, 'Customer', 'alice'); + customer1.name = 'Alice'; + customer1.lastName= 'Hamilton'; + customer1.bank = factory.newRelationship(namespace, 'Bank', 'BoD'); + customer1.companyName = 'QuickFix IT'; + await customerRegistry.add(customer1); + const customer2 = factory.newResource(namespace, 'Customer', 'bob'); + customer2.name = 'Bob'; + customer2.lastName= 'Appleton'; + customer2.bank = factory.newRelationship(namespace, 'Bank', 'EB'); + customer2.companyName = 'Conga Computers'; + await customerRegistry.add(customer2); +} \ No newline at end of file diff --git a/packages/letters-of-credit-network/models/org.acme.loc.cto b/packages/letters-of-credit-network/models/org.acme.loc.cto new file mode 100644 index 0000000..804c2c1 --- /dev/null +++ b/packages/letters-of-credit-network/models/org.acme.loc.cto @@ -0,0 +1,146 @@ +/** + * New model file + */ + +namespace org.example.loc + +// ENUMS +enum LetterStatus { + o AWAITING_APPROVAL + o APPROVED + o SHIPPED + o RECEIVED + o READY_FOR_PAYMENT + o CLOSED + o REJECTED +} + +// ASSETS +asset LetterOfCredit identified by letterId { + o String letterId + --> Customer applicant + --> Customer beneficiary + --> Bank issuingBank + --> Bank exportingBank + o Rule[] rules + o ProductDetails productDetails + o String [] evidence + --> Person [] approval + o LetterStatus status + o String closeReason optional +} + +// PARTICIPANTS +participant Bank identified by bankID { + o String bankID + o String name +} + +abstract participant Person identified by personId { + o String personId + o String name + o String lastName optional + --> Bank bank +} + +participant Customer extends Person { + o String companyName +} + +participant BankEmployee extends Person { +} + +// CONCEPTS +concept Rule { + o String ruleId + o String ruleText +} + +concept ProductDetails { + o String productType + o Integer quantity + o Double pricePerUnit +} + +// TRANSACTIONS + EVENTS +transaction InitialApplication { + o String letterId + --> Customer applicant + --> Customer beneficiary + o Rule[] rules + o ProductDetails productDetails +} + +event InitialApplicationEvent { + --> LetterOfCredit loc +} + +transaction Approve { + --> LetterOfCredit loc + --> Person approvingParty +} + +event ApproveEvent { + --> LetterOfCredit loc + --> Person approvingParty +} + +transaction Reject { + --> LetterOfCredit loc + o String closeReason +} + +event RejectEvent { + --> LetterOfCredit loc + o String closeReason +} + +transaction SuggestChanges { + --> LetterOfCredit loc + o Rule[] rules + --> Person suggestingParty +} + +event SuggestChangesEvent { + --> LetterOfCredit loc + o Rule[] rules + --> Person suggestingParty +} + +transaction ShipProduct { + --> LetterOfCredit loc + o String evidence +} + +event ShipProductEvent { + --> LetterOfCredit loc +} + +transaction ReceiveProduct { + --> LetterOfCredit loc +} + +event ReceiveProductEvent { + --> LetterOfCredit loc +} + +transaction ReadyForPayment { + --> LetterOfCredit loc +} +event ReadyForPaymentEvent { + --> LetterOfCredit loc +} + +transaction Close { + --> LetterOfCredit loc + o String closeReason +} + +event CloseEvent { + --> LetterOfCredit loc + o String closeReason +} + +// TRANSACTIONS FOR SETUP +transaction CreateDemoParticipants { +} \ No newline at end of file diff --git a/packages/letters-of-credit-network/networkimage.svg b/packages/letters-of-credit-network/networkimage.svg new file mode 100644 index 0000000..2b2a1ec --- /dev/null +++ b/packages/letters-of-credit-network/networkimage.svg @@ -0,0 +1 @@ +Asset 17 \ No newline at end of file diff --git a/packages/letters-of-credit-network/networkimageanimated.svg b/packages/letters-of-credit-network/networkimageanimated.svg new file mode 100644 index 0000000..c50a770 --- /dev/null +++ b/packages/letters-of-credit-network/networkimageanimated.svg @@ -0,0 +1 @@ +Asset 17 \ No newline at end of file diff --git a/packages/letters-of-credit-network/package.json b/packages/letters-of-credit-network/package.json new file mode 100644 index 0000000..313b6d0 --- /dev/null +++ b/packages/letters-of-credit-network/package.json @@ -0,0 +1,40 @@ +{ + "engines": { + "composer": "^0.19.0" + }, + "name": "letters-of-credit-network", + "description": "letters of credit network", + "networkImage": "https://hyperledger.github.io/composer-sample-networks/packages/letters-of-credit-network/networkimage.svg", + "networkImageanimated": "https://hyperledger.github.io/composer-sample-networks/packages/letters-of-credit-network/networkimageanimated.svg", + "version": "0.2.4", + "scripts": { + "prepublish": "mkdirp ./dist && composer archive create --sourceType dir --sourceName . -a ./dist/letters-of-credit-network.bna", + "lint": "eslint .", + "test": "mocha -t 0 --recursive", + "test-cover": "nyc npm run test" + }, + "repository": { + "type": "git", + "url": "https://github.com/hyperledger/composer-sample-networks.git" + }, + "author": "Hyperledger Composer", + "license": "Apache-2.0", + "devDependencies": { + "browserfs": "^1.2.0", + "chai": "^3.5.0", + "chai-as-promised": "^6.0.0", + "composer-admin": "^0.19.0-0", + "composer-cli": "^0.19.0-0", + "composer-client": "^0.19.0-0", + "composer-common": "^0.19.0-0", + "composer-connector-embedded": "^0.19.0-0", + "eslint": "^3.6.1", + "istanbul": "^0.4.5", + "jsdoc": "^3.5.5", + "license-check-and-add": "~2.3.1", + "mkdirp": "^0.5.1", + "mocha": "^3.2.0", + "moment": "^2.17.1", + "nyc": "^11.0.2" + } +} diff --git a/packages/letters-of-credit-network/permissions.acl b/packages/letters-of-credit-network/permissions.acl new file mode 100644 index 0000000..81ee2d1 --- /dev/null +++ b/packages/letters-of-credit-network/permissions.acl @@ -0,0 +1,276 @@ +rule ParticipantsSeeSelves { + description: "Let participants see themselves" + participant(p): "org.hyperledger.composer.system.Participant" + operation: ALL + resource(r): "org.hyperledger.composer.system.Participant" + condition: (r.getIdentifier() == p.getIdentifier()) + action: ALLOW +} + +rule SeeOtherCustomers { + description: "Let Customers see other Customers" + participant: "org.example.loc.Customer" + operation: READ + resource: "org.example.loc.Customer" + action: ALLOW +} + +rule CustomerSeeBankEmployee { + description: "Let Customers see their BankEmployees" + participant(p): "org.example.loc.Customer" + operation: READ + resource(r): "org.example.loc.BankEmployee" + condition: (r.bank.getIdentifier() == p.bank.getIdentifier()) + action: ALLOW +} + +rule BankEmployeeSeeCustomer { + description: "Let BankEmployees see their Customers" + participant(p): "org.example.loc.BankEmployee" + operation: READ + resource(r): "org.example.loc.Customer" + condition: (r.bank.getIdentifier() == p.bank.getIdentifier()) + action: ALLOW +} + +rule CustomerMakeApplication { + description: "All customers can submit an InitialApplication transaction" + participant: "org.example.loc.Customer" + operation: CREATE + resource: "org.example.loc.InitialApplication" + action: ALLOW +} + +rule CustomerCreateLOC { + description: "All customers can create a LetterOfCredit asset" + participant: "org.example.loc.Customer" + operation: CREATE + resource: "org.example.loc.LetterOfCredit" + transaction: "org.example.loc.InitialApplication" + action: ALLOW +} + +rule CustomerViewLetterOfCredit { + description: "All customers can view letters of credit they are involved with" + participant(p): "org.example.loc.Customer" + operation: READ + resource(r): "org.example.loc.LetterOfCredit" + condition: (p.getIdentifier() === r.applicant.getIdentifier() || p.getIdentifier() === r.beneficiary.getIdentifier()) + action: ALLOW +} + + +rule BankEmployeeViewLetterOfCredit { + description: "All bank employees can view letters of credit their bank is involved with" + participant(p): "org.example.loc.BankEmployee" + operation: READ + resource(r): "org.example.loc.LetterOfCredit" + condition: (p.bank.getIdentifier() === r.issuingBank.getIdentifier() || p.bank.getIdentifier() === r.exportingBank.getIdentifier()) + action: ALLOW +} + +rule CustomerApproveApplication { + description: "All customers can submit an Approve transaction for an LoC they are involved with" + participant(p): "org.example.loc.Customer" + operation: CREATE + resource(r): "org.example.loc.Approve" + condition: (p.getIdentifier() === r.loc.applicant.getIdentifier() || p.getIdentifier() === r.loc.beneficiary.getIdentifier()) + action: ALLOW +} + +rule BankEmployeeApproveApplication { + description: "All bank employees can submit an Approve transaction for an LoC their bank is involved with" + participant(p): "org.example.loc.BankEmployee" + operation: CREATE + resource(r): "org.example.loc.Approve" + condition: (p.bank.getIdentifier() === r.loc.issuingBank.getIdentifier() || p.bank.getIdentifier() === r.loc.exportingBank.getIdentifier()) + action: ALLOW +} + +rule CustomerAddApproval { + description: "All customers can add their approval to a Letter of Credit they are involved with" + participant(p): "org.example.loc.Customer" + operation: UPDATE + resource(r): "org.example.loc.LetterOfCredit" + transaction(t): "org.example.loc.Approve" + condition: (p.getIdentifier() === r.applicant.getIdentifier() || p.getIdentifier() === r.beneficiary.getIdentifier()) + action: ALLOW +} + +rule BankEmployeeAddApproval { + description: "All bank employee can add their approval to a Letter of Credit their bank is involved with" + participant(p): "org.example.loc.BankEmployee" + operation: UPDATE + resource(r): "org.example.loc.LetterOfCredit" + transaction(t): "org.example.loc.Approve" + condition: (p.bank.getIdentifier() === r.issuingBank.getIdentifier() || p.bank.getIdentifier() === r.exportingBank.getIdentifier()) + action: ALLOW +} + +rule CustomerSuggestChanges { + description: "All customers can submit a SuggestChanges transaction to a Letter of Credit they are involved with" + participant(p): "org.example.loc.Customer" + operation: CREATE + resource(r): "org.example.loc.SuggestChanges" + condition: (p.getIdentifier() === r.loc.applicant.getIdentifier() || p.getIdentifier() === r.loc.beneficiary.getIdentifier()) + action: ALLOW +} + +rule BankEmployeeSuggestChanges { + description: "All bank employees can submit a SuggestChanges transaction to a Letter of Credit their bank is involved with" + participant(p): "org.example.loc.Customer" + operation: CREATE + resource(r): "org.example.loc.SuggestChanges" + condition: (p.bank.getIdentifier() === r.loc.issuingBank.getIdentifier() || p.bank.getIdentifier() === r.loc.exportingBank.getIdentifier()) + action: ALLOW +} + +rule CustomerAddChanges { + description: "All customers can update a LetterOfCredit with their suggested rules if they are involved in it" + participant(p): "org.example.loc.Customer" + operation: UPDATE + resource(r): "org.example.loc.LetterOfCredit" + transaction(t): "org.example.loc.SuggestChanges" + condition: (p.getIdentifier() === r.applicant.getIdentifier() || p.getIdentifier() === r.beneficiary.getIdentifier()) + action: ALLOW +} + +rule BankEmployeeAddChanges { + description: "All bank employees can update a LetterOfCredit with their suggested rules if their bank is involved in it" + participant(p): "org.example.loc.Customer" + operation: UPDATE + resource(r): "org.example.loc.LetterOfCredit" + transaction(t): "org.example.loc.SuggestChanges" + condition: (p.bank.getIdentifier() === r.issuingBank.getIdentifier() || p.bank.getIdentifier() === r.exportingBank.getIdentifier()) + action: ALLOW +} + +rule CustomerRejectApplication { + description: "All customers can submit a Reject transaction for an LoC they are involved with" + participant(p): "org.example.loc.Customer" + operation: CREATE + resource(r): "org.example.loc.Reject" + condition: (p.getIdentifier() === r.loc.applicant.getIdentifier() || p.getIdentifier() === r.loc.beneficiary.getIdentifier()) + action: ALLOW +} + +rule BankEmployeeRejectApplication { + description: "All bank employees can submit a Reject transaction for an LoC their bank is involved with" + participant(p): "org.example.loc.BankEmployee" + operation: CREATE + resource(r): "org.example.loc.Reject" + condition: (p.bank.getIdentifier() === r.loc.issuingBank.getIdentifier() || p.bank.getIdentifier() === r.loc.exportingBank.getIdentifier()) + action: ALLOW +} + +rule CustomerMarksAsRejected { + description: "All customers can update a LetterOfCredit they are involved with with a REJECTED status" + participant(p): "org.example.loc.Customer" + operation: UPDATE + resource(r): "org.example.loc.LetterOfCredit" + transaction(t): "org.example.loc.Reject" + condition: (p.getIdentifier() === r.applicant.getIdentifier() || p.getIdentifier() === r.beneficiary.getIdentifier()) + action: ALLOW +} + +rule BankEmployeeMarksAsRejected { + description: "All bank employees can update a LetterOfCredit their bank is involved with with a REJECTED status" + participant(p): "org.example.loc.BankEmployee" + operation: UPDATE + resource(r): "org.example.loc.LetterOfCredit" + transaction(t): "org.example.loc.Reject" + condition: (p.bank.getIdentifier() === r.issuingBank.getIdentifier() || p.bank.getIdentifier() === r.exportingBank.getIdentifier()) + action: ALLOW +} + +rule BeneficiaryShipProduct { + description: "The beneficiary send a transaction to mark a letter of credit as relating to goods that have been shipped" + participant(p): "org.example.loc.Customer" + operation: CREATE + resource(r): "org.example.loc.ShipProduct" + condition: (p.getIdentifier() === r.loc.beneficiary.getIdentifier()) + action: ALLOW +} + +rule BeneficiaryMarkAsShippedProduct { + description: "The applicant can mark a letter of credit as relating to goods that have been shipped" + participant(p): "org.example.loc.Customer" + operation: UPDATE + resource(r): "org.example.loc.LetterOfCredit" + transaction(t): "org.example.loc.ShipProduct" + condition: (p.getIdentifier() === r.beneficiary.getIdentifier()) + action: ALLOW +} + +rule ApplicantReceiveProduct { + description: "The applicant send a transaction to mark a letter of credit as relating to goods that have been received" + participant(p): "org.example.loc.Customer" + operation: CREATE + resource(r): "org.example.loc.ReceiveProduct" + condition: (p.getIdentifier() === r.loc.applicant.getIdentifier()) + action: ALLOW +} + +rule ApplicantMarkAsReceivedProduct { + description: "The applicant can mark a letter of credit as relating to goods that have been received" + participant(p): "org.example.loc.Customer" + operation: UPDATE + resource(r): "org.example.loc.LetterOfCredit" + transaction(t): "org.example.loc.ReceiveProduct" + condition: (p.getIdentifier() === r.applicant.getIdentifier()) + action: ALLOW +} + +rule IssuingBankReadyForPayment { + description: "The issuing bank employee can state the letter is ready for payment" + participant(p): "org.example.loc.BankEmployee" + operation: CREATE + resource(r): "org.example.loc.ReadyForPayment" + condition: (p.bank.getIdentifier() === r.loc.issuingBank.getIdentifier()) + action: ALLOW +} + +rule IssuingBankMarkReadyForPayment { + description: "The issuing bank employee can mark the letter as ready for payment" + participant(p): "org.example.loc.BankEmployee" + operation: UPDATE + resource(r): "org.example.loc.LetterOfCredit" + transaction(t): "org.example.loc.ReadyForPayment" + condition: (p.bank.getIdentifier() === r.issuingBank.getIdentifier()) + action: ALLOW +} + +rule ExportingBankCloseLetter { + description: "The exporting bank employee can close the letter" + participant(p): "org.example.loc.BankEmployee" + operation: CREATE + resource(r): "org.example.loc.Close" + condition: (p.bank.getIdentifier() === r.loc.exportingBank.getIdentifier()) + action: ALLOW +} + +rule ExportingBankMarkLetterClosed { + description: "The exporting bank employee can mark the letter as closed" + participant(p): "org.example.loc.BankEmployee" + operation: UPDATE + resource(r): "org.example.loc.LetterOfCredit" + transaction(t): "org.example.loc.Close" + condition: (p.bank.getIdentifier() === r.exportingBank.getIdentifier()) + action: ALLOW +} + +rule NetworkAdminUser { + description: "Grant business network administrators full access to user resources" + participant: "org.hyperledger.composer.system.NetworkAdmin" + operation: ALL + resource: "**" + action: ALLOW +} + +rule System { + description: "Grant all full access to system resources" + participant: "org.**" + operation: ALL + resource: "org.hyperledger.composer.system.**" + action: ALLOW +} \ No newline at end of file diff --git a/packages/letters-of-credit-network/test/script.js b/packages/letters-of-credit-network/test/script.js new file mode 100644 index 0000000..70454f1 --- /dev/null +++ b/packages/letters-of-credit-network/test/script.js @@ -0,0 +1,671 @@ +'use strict'; + +const AdminConnection = require('composer-admin').AdminConnection; +const BusinessNetworkConnection = require('composer-client').BusinessNetworkConnection; +const { BusinessNetworkDefinition, CertificateUtil, IdCard } = require('composer-common'); +const path = require('path'); + +const chai = require('chai'); +chai.should(); +chai.use(require('chai-as-promised')); + +const namespace = 'org.example.loc'; +const letterId = 'L123'; + +describe('Letters of Credit Network', () => { + const cardStore = require('composer-common').NetworkCardStoreManager.getCardStore( { type: 'composer-wallet-inmemory' } ); + let adminConnection; + let businessNetworkConnection; + let factory; + + let alice; + let bob; + let aliceRelationship; + let bobRelationship; + let matiasRelationship; + let ellaRelationship; + let rules; + let productDetails; + let letter; + let letterRegistry; + + before(async () => { + // Embedded connection used for local testing + const connectionProfile = { + name: 'embedded', + 'x-type': 'embedded' + }; + // Generate certificates for use with the embedded connection + const credentials = CertificateUtil.generate({ commonName: 'admin' }); + + // PeerAdmin identity used with the admin connection to deploy business networks + const deployerMetadata = { + version: 1, + userName: 'PeerAdmin', + roles: [ 'PeerAdmin', 'ChannelAdmin' ] + }; + + const deployerCard = new IdCard(deployerMetadata, connectionProfile); + deployerCard.setCredentials(credentials); + + const deployerCardName = 'PeerAdmin'; + adminConnection = new AdminConnection({ cardStore: cardStore }); + + await adminConnection.importCard(deployerCardName, deployerCard); + await adminConnection.connect(deployerCardName); + }); + + beforeEach(async () => { + businessNetworkConnection = new BusinessNetworkConnection({ cardStore: cardStore }); + + const adminUserName = 'admin'; + let adminCardName; + const businessNetworkDefinition = await BusinessNetworkDefinition.fromDirectory(path.resolve(__dirname, '..')); + + // Install the Composer runtime for the new business network + await adminConnection.install(businessNetworkDefinition); + + // Start the business network and configure a network admin identity + const startOptions = { + networkAdmins: [ + { + userName: adminUserName, + enrollmentSecret: 'adminpw' + } + ] + }; + const adminCards = await adminConnection.start(businessNetworkDefinition.getName(), businessNetworkDefinition.getVersion(), startOptions); + + // Import the network admin identity for us to use + adminCardName = `${adminUserName}@${businessNetworkDefinition.getName()}`; + + await adminConnection.importCard(adminCardName, adminCards.get(adminUserName)); + + // Connect to the business network using the network admin identity + await businessNetworkConnection.connect(adminCardName); + + factory = businessNetworkConnection.getBusinessNetwork().getFactory(); + + // create bank participants + const bank1 = factory.newResource(namespace, 'Bank', 'BoD'); + bank1.name = 'Bank of Dinero'; + const bank2 = factory.newResource(namespace, 'Bank', 'EB'); + bank2.name = 'Eastwood Banking'; + + const bankRegistry = await businessNetworkConnection.getParticipantRegistry(namespace + '.Bank'); + await bankRegistry.add(bank1); + await bankRegistry.add(bank2); + + // create bank employees + const employee1 = factory.newResource(namespace, 'BankEmployee', 'matias'); + employee1.name = 'Matías'; + employee1.bank = factory.newRelationship(namespace, 'Bank', 'BoD'); + + const employee2 = factory.newResource(namespace, 'BankEmployee', 'ella'); + employee2.name = 'Ella'; + employee2.bank = factory.newRelationship(namespace, 'Bank', 'EB'); + + const employeeRegistry = await businessNetworkConnection.getParticipantRegistry(namespace + '.BankEmployee'); + await employeeRegistry.add(employee1); + await employeeRegistry.add(employee2); + + // create alice participant + alice = factory.newResource(namespace, 'Customer', 'alice'); + alice.name = 'Alice'; + alice.lastName= 'Hamilton'; + alice.bank = factory.newRelationship(namespace, 'Bank', 'BoD'); + alice.companyName = 'QuickFix IT'; + + // create bob participant + bob = factory.newResource(namespace, 'Customer', 'bob'); + bob.name = 'Bob'; + bob.lastName= 'Bobbins'; + bob.bank = factory.newRelationship(namespace, 'Bank', 'EB'); + bob.companyName = 'Conga Computers'; + + // add alice and bob participants to the registry + const customerRegistry = await businessNetworkConnection.getParticipantRegistry(namespace + '.Customer'); + await customerRegistry.add(alice); + await customerRegistry.add(bob); + + aliceRelationship = factory.newRelationship(namespace, 'Customer', alice.getIdentifier()); + bobRelationship = factory.newRelationship(namespace, 'Customer', bob.getIdentifier()); + matiasRelationship = factory.newRelationship(namespace, 'BankEmployee', employee1.getIdentifier()); + ellaRelationship = factory.newRelationship(namespace, 'BankEmployee', employee2.getIdentifier()); + + // create some rules for the letter + rules = []; + const rule1 = factory.newConcept(namespace, 'Rule'); + rule1.ruleId = 'rule1'; + rule1.ruleText = 'This is a test rule'; + rules.push(rule1); + const rule2 = factory.newConcept(namespace, 'Rule'); + rule2.ruleId = 'rule2'; + rule2.ruleText = 'This is another test rule'; + rules.push(rule2); + + // create the product details + productDetails = factory.newConcept(namespace, 'ProductDetails'); + productDetails.productType = 'Computers'; + productDetails.quantity = 100; + productDetails.pricePerUnit = 250; + + // create a generic letter + letter = factory.newResource(namespace, 'LetterOfCredit', letterId); + letter.applicant = factory.newRelationship(namespace, 'Customer', alice.getIdentifier()); + letter.beneficiary = factory.newRelationship(namespace, 'Customer', bob.getIdentifier()); + letter.issuingBank = alice.bank; + letter.exportingBank = bob.bank; + letter.rules = rules; + letter.productDetails = productDetails; + letter.evidence = []; + letter.approval = []; + letter.status = 'AWAITING_APPROVAL'; + + letterRegistry = await businessNetworkConnection.getAssetRegistry(namespace + '.LetterOfCredit'); + await letterRegistry.add(letter); + }); + + describe('InitialApplication', () => { + it('should be able to create a letter of credit asset', async () => { + // create and submit the InitialApplication transaction + const initialApplicationTx = factory.newTransaction(namespace, 'InitialApplication'); + initialApplicationTx.letterId = 'newLetter'; + initialApplicationTx.applicant = aliceRelationship; + initialApplicationTx.beneficiary = bobRelationship; + initialApplicationTx.rules = rules; + initialApplicationTx.productDetails = productDetails; + await businessNetworkConnection.submitTransaction(initialApplicationTx); + + const letterRegistry = await businessNetworkConnection.getAssetRegistry(namespace + '.LetterOfCredit'); + const letter = await letterRegistry.get('newLetter'); + + letter.letterId.should.deep.equal('newLetter'); + letter.applicant.should.deep.equal(initialApplicationTx.applicant); + letter.beneficiary.should.deep.equal(initialApplicationTx.beneficiary); + letter.issuingBank.should.deep.equal(alice.bank); + letter.exportingBank.should.deep.equal(bob.bank); + letter.rules.should.deep.equal(rules); + letter.productDetails.should.deep.equal(productDetails); + letter.evidence.should.deep.equal([]); + letter.approval.should.deep.equal([aliceRelationship]); + letter.status.should.deep.equal('AWAITING_APPROVAL'); + }); + }); + + describe('Approve', () => { + it('should update the approval array to show that a participant has approved the letter', async () => { + // create and submit an Approve transaction + const approveTx = factory.newTransaction(namespace, 'Approve'); + approveTx.loc = factory.newRelationship(namespace, 'LetterOfCredit', letterId); + approveTx.approvingParty = aliceRelationship; + await businessNetworkConnection.submitTransaction(approveTx); + + const approvedLetter = await letterRegistry.get(letterId); + approvedLetter.approval.should.deep.equal([aliceRelationship]); + }); + + it('should not allow the same person to approve the letter twice', async () => { + // update the letter to have already been approved by Alice + let updatedLetter = await letterRegistry.get(letterId); + updatedLetter.approval = [aliceRelationship]; + await letterRegistry.update(updatedLetter); + + // attempt to submit the Approve transaction and check the error + const approveTx = factory.newTransaction(namespace, 'Approve'); + approveTx.loc = factory.newRelationship(namespace, 'LetterOfCredit', letterId); + approveTx.approvingParty = aliceRelationship; + return businessNetworkConnection.submitTransaction(approveTx).should.be.rejectedWith('This person has already approved this letter of credit'); + }); + + it('should not allow an employee of a bank to approve when another employee of the same bank already has', async () => { + const otherBoDEmployee = factory.newResource(namespace, 'BankEmployee', 'trevor'); + otherBoDEmployee.name = 'Trevor'; + otherBoDEmployee.bank = factory.newRelationship(namespace, 'Bank', 'BoD'); + const employeeRegistry = await businessNetworkConnection.getParticipantRegistry(namespace + '.BankEmployee'); + await employeeRegistry.add(otherBoDEmployee); + + // update the letter to have already been approved by Alice + let updatedLetter = await letterRegistry.get(letterId); + updatedLetter.approval = [factory.newRelationship(namespace, 'BankEmployee', otherBoDEmployee.getIdentifier())]; + await letterRegistry.update(updatedLetter); + + // attempt to submit the Approve transaction and check the error + const approveTx = factory.newTransaction(namespace, 'Approve'); + approveTx.loc = factory.newRelationship(namespace, 'LetterOfCredit', letterId); + approveTx.approvingParty = matiasRelationship; + return businessNetworkConnection.submitTransaction(approveTx).should.be.rejectedWith('Your bank has already approved of this request'); + }); + + it('should mark the letter as \'approved\' when all four parties have approved the letter', async () => { + // update the letter to have been approved by three people already + let updatedLetter = await letterRegistry.get(letterId); + updatedLetter.approval = [aliceRelationship, matiasRelationship, ellaRelationship]; + await letterRegistry.update(updatedLetter); + + // create and submit an Approve transaction + const approveTx = factory.newTransaction(namespace, 'Approve'); + approveTx.loc = factory.newRelationship(namespace, 'LetterOfCredit', letterId); + approveTx.approvingParty = bobRelationship; + await businessNetworkConnection.submitTransaction(approveTx); + + const approvedLetter = await letterRegistry.get(letterId); + approvedLetter.approval.should.deep.equal([aliceRelationship, matiasRelationship, ellaRelationship, bobRelationship]); + approvedLetter.status.should.deep.equal('APPROVED'); + }); + + it('should be unable to submit an Approve transaction on a letter that has already been closed', async () => { + // update the letter to be closed + let updatedLetter = await letterRegistry.get(letterId); + updatedLetter.status = 'CLOSED'; + await letterRegistry.update(updatedLetter); + + // attempt to submit an Approve transaction and check the error + const approveTx = factory.newTransaction(namespace, 'Approve'); + approveTx.loc = factory.newRelationship(namespace, 'LetterOfCredit', letterId); + approveTx.approvingParty = aliceRelationship; + return businessNetworkConnection.submitTransaction(approveTx).should.be.rejectedWith('This letter of credit has already been closed'); + }); + + it('should be unable to submit an Approve transaction on a letter that has already been rejected', async () => { + // update the letter to be closed + let updatedLetter = await letterRegistry.get(letterId); + updatedLetter.status = 'REJECTED'; + await letterRegistry.update(updatedLetter); + + // attempt to submit an Approve transaction and check the error + const approveTx = factory.newTransaction(namespace, 'Approve'); + approveTx.loc = factory.newRelationship(namespace, 'LetterOfCredit', letterId); + approveTx.approvingParty = aliceRelationship; + return businessNetworkConnection.submitTransaction(approveTx).should.be.rejectedWith('This letter of credit has already been closed'); + }); + }); + + describe('Reject', () => { + it('should allow the rejection of the letter before it is fully approved', async () => { + // create and submit a Reject transaction + const rejectTx = factory.newTransaction(namespace, 'Reject'); + rejectTx.loc = factory.newRelationship(namespace, 'LetterOfCredit', letterId); + rejectTx.closeReason = 'testing the Reject transaction'; + await businessNetworkConnection.submitTransaction(rejectTx); + + const rejectedLetter = await letterRegistry.get(letterId); + rejectedLetter.status.should.deep.equal('REJECTED'); + rejectedLetter.closeReason.should.deep.equal('testing the Reject transaction'); + }); + + it('should be unable to submit a Reject transaction on a letter that has already been closed', async () => { + // update the letter to have already been closed + let updatedLetter = await letterRegistry.get(letterId); + updatedLetter.approval = [aliceRelationship, matiasRelationship, ellaRelationship, bobRelationship]; + updatedLetter.status = 'CLOSED'; + await letterRegistry.update(updatedLetter); + + // attempt to submit a Reject transaction and check the error + const rejectTx = factory.newTransaction(namespace, 'Reject'); + rejectTx.loc = factory.newRelationship(namespace, 'LetterOfCredit', letterId); + rejectTx.closeReason = 'testing the Reject transaction'; + return businessNetworkConnection.submitTransaction(rejectTx).should.be.rejectedWith('This letter of credit has already been closed'); + }); + + it('should be unable to submit a Reject transaction on a letter that has already been rejected', async () => { + // update the letter to have been rejected + let updatedLetter = await letterRegistry.get(letterId); + updatedLetter.status = 'REJECTED'; + await letterRegistry.update(updatedLetter); + + // attempt to submit a Reject transaction and check the error + const rejectTx = factory.newTransaction(namespace, 'Reject'); + rejectTx.loc = factory.newRelationship(namespace, 'LetterOfCredit', letterId); + rejectTx.closeReason = 'testing the Reject transaction'; + return businessNetworkConnection.submitTransaction(rejectTx).should.be.rejectedWith('This letter of credit has already been closed'); + }); + }); + + describe('SuggestChanges', () => { + let newRules; + before(async () => { + // define a new rules array to be used in the transactions + newRules = []; + const rule1 = factory.newConcept(namespace, 'Rule'); + rule1.ruleId = 'newRule1'; + rule1.ruleText = 'This is an updated test rule'; + newRules.push(rule1); + const rule2 = factory.newConcept(namespace, 'Rule'); + rule2.ruleId = 'newRule2'; + rule2.ruleText = 'This is another updated test rule'; + newRules.push(rule2); + }); + + it('should update the array of rules and reset the approval array', async () => { + // create and submit a SuggestChanges transaction + const suggestChangesTx = factory.newTransaction(namespace, 'SuggestChanges'); + suggestChangesTx.loc = factory.newRelationship(namespace, 'LetterOfCredit', letterId); + suggestChangesTx.rules = newRules; + suggestChangesTx.suggestingParty = matiasRelationship; + await businessNetworkConnection.submitTransaction(suggestChangesTx); + + const changedLetter = await letterRegistry.get(letterId); + changedLetter.rules.should.deep.equal(newRules); + changedLetter.approval.should.deep.equal([matiasRelationship]); + }); + + it('should be unable to submit a SuggestChanges transaction on a letter that has already been shipped', async () => { + // update the letter to have been shipped + let updatedLetter = await letterRegistry.get(letterId); + updatedLetter.approval = [aliceRelationship, matiasRelationship, ellaRelationship, bobRelationship]; + updatedLetter.status = 'SHIPPED'; + await letterRegistry.update(updatedLetter); + + // attempt to submit a SuggestChanges transaction and check the error + const suggestChangesTx = factory.newTransaction(namespace, 'SuggestChanges'); + suggestChangesTx.loc = factory.newRelationship(namespace, 'LetterOfCredit', letterId); + suggestChangesTx.rules = newRules; + suggestChangesTx.suggestingParty = matiasRelationship; + return businessNetworkConnection.submitTransaction(suggestChangesTx).should.be.rejectedWith('The product has already been shipped'); + }); + + it('should be unable to submit a SuggestChanges transaction on a letter that has already been received', async () => { + // update the letter to have been shipped + let updatedLetter = await letterRegistry.get(letterId); + updatedLetter.approval = [aliceRelationship, matiasRelationship, ellaRelationship, bobRelationship]; + updatedLetter.status = 'RECEIVED'; + await letterRegistry.update(updatedLetter); + + // attempt to submit a SuggestChanges transaction and check the error + const suggestChangesTx = factory.newTransaction(namespace, 'SuggestChanges'); + suggestChangesTx.loc = factory.newRelationship(namespace, 'LetterOfCredit', letterId); + suggestChangesTx.rules = newRules; + suggestChangesTx.suggestingParty = matiasRelationship; + return businessNetworkConnection.submitTransaction(suggestChangesTx).should.be.rejectedWith('The product has already been shipped'); + }); + + + it('should be unable to submit a SuggestChanges transaction on a letter that has already been closed', async () => { + // update the letter to have already been closed + let updatedLetter = await letterRegistry.get(letterId); + updatedLetter.approval = [aliceRelationship, matiasRelationship, ellaRelationship, bobRelationship]; + updatedLetter.status = 'CLOSED'; + await letterRegistry.update(updatedLetter); + + // attempt to submit a SuggestChanges transaction and check the error + const suggestChangesTx = factory.newTransaction(namespace, 'SuggestChanges'); + suggestChangesTx.loc = factory.newRelationship(namespace, 'LetterOfCredit', letterId); + suggestChangesTx.rules = newRules; + suggestChangesTx.suggestingParty = matiasRelationship; + return businessNetworkConnection.submitTransaction(suggestChangesTx).should.be.rejectedWith('This letter of credit has already been closed'); + }); + + it('should be unable to submit a SuggestChanges transaction on a letter that has already been rejected', async () => { + // update the letter to have been rejected + let updatedLetter = await letterRegistry.get(letterId); + updatedLetter.status = 'REJECTED'; + await letterRegistry.update(updatedLetter); + + // attempt to submit a SuggestChanges transaction and check the error + const suggestChangesTx = factory.newTransaction(namespace, 'SuggestChanges'); + suggestChangesTx.loc = factory.newRelationship(namespace, 'LetterOfCredit', letterId); + suggestChangesTx.rules = newRules; + suggestChangesTx.suggestingParty = matiasRelationship; + return businessNetworkConnection.submitTransaction(suggestChangesTx).should.be.rejectedWith('This letter of credit has already been closed'); + }); + }); + + describe('ShipProduct', () => { + it('should not update the status if the letter has not been fully approved', async () => { + // attempt to submit the ShipProduct transaction against the unapproved letter, and check the error + const shipProductTx = factory.newTransaction(namespace, 'ShipProduct'); + shipProductTx.loc = factory.newRelationship(namespace, 'LetterOfCredit', letterId); + shipProductTx.evidence = 'asdfghjk'; + return businessNetworkConnection.submitTransaction(shipProductTx).should.be.rejectedWith('This letter needs to be fully approved before the product can be shipped'); + }); + + it('should mark the letter as \'shipped\' if it has been fully approved', async () => { + // update the created letter so that it is ready to be shipped + let updatedLetter = await letterRegistry.get(letterId); + updatedLetter.approval = [aliceRelationship, matiasRelationship, ellaRelationship, bobRelationship]; + updatedLetter.status = 'APPROVED'; + await letterRegistry.update(updatedLetter); + + // create and submit a ShipProduct transaction + const shipProductTx = factory.newTransaction(namespace, 'ShipProduct'); + shipProductTx.loc = factory.newRelationship(namespace, 'LetterOfCredit', letterId); + shipProductTx.evidence = 'asdfghjk'; + await businessNetworkConnection.submitTransaction(shipProductTx); + + const shippedLetter = await letterRegistry.get(letterId); + shippedLetter.status.should.deep.equal('SHIPPED'); + shippedLetter.evidence.should.deep.equal(['asdfghjk']); + }); + + it('should do nothing if the letter has already been marked as \'shipped\'', async () => { + // update the letter so that is has already been marked as shipped + let updatedLetter = await letterRegistry.get(letterId); + updatedLetter.approval = [aliceRelationship, matiasRelationship, ellaRelationship, bobRelationship]; + updatedLetter.status = 'SHIPPED'; + await letterRegistry.update(updatedLetter); + + // attempt to submit the ShipProduct transaction and check the error + const shipProductTx = factory.newTransaction(namespace, 'ShipProduct'); + shipProductTx.loc = factory.newRelationship(namespace, 'LetterOfCredit', letterId); + shipProductTx.evidence = 'asdfghjk'; + return businessNetworkConnection.submitTransaction(shipProductTx).should.be.rejectedWith('The product has already been shipped'); + }); + + it('should be unable to submit a ShipProduct transaction on a letter that has already been closed', async () => { + // update the letter to have already been closed + let updatedLetter = await letterRegistry.get(letterId); + updatedLetter.approval = [aliceRelationship, matiasRelationship, ellaRelationship, bobRelationship]; + updatedLetter.status = 'CLOSED'; + await letterRegistry.update(updatedLetter); + + // attempt to submit a ShipProduct transaction and check the error + const shipProductTx = factory.newTransaction(namespace, 'ShipProduct'); + shipProductTx.loc = factory.newRelationship(namespace, 'LetterOfCredit', letterId); + shipProductTx.evidence = 'asdfghjk'; + return businessNetworkConnection.submitTransaction(shipProductTx).should.be.rejectedWith('This letter of credit has already been closed'); + }); + + it('should be unable to submit a ShipProduct transaction on a letter that has already been rejected', async () => { + // update the letter to have been rejected + let updatedLetter = await letterRegistry.get(letterId); + updatedLetter.status = 'REJECTED'; + await letterRegistry.update(updatedLetter); + + // attempt to submit a ShipProduct transaction and check the error + const shipProductTx = factory.newTransaction(namespace, 'ShipProduct'); + shipProductTx.loc = factory.newRelationship(namespace, 'LetterOfCredit', letterId); + shipProductTx.evidence = 'asdfghjk'; + return businessNetworkConnection.submitTransaction(shipProductTx).should.be.rejectedWith('This letter of credit has already been closed'); + }); + + }); + + describe('ReceiveProduct', () => { + it('should not update the status if the letter has not yet been fully approved', async () => { + // attempt to submit a ReceiveProduct transaction against the unshipped letter, and check the error + const receiveProductTx = factory.newTransaction(namespace, 'ReceiveProduct'); + receiveProductTx. loc = factory.newRelationship(namespace, 'LetterOfCredit', letterId); + return businessNetworkConnection.submitTransaction(receiveProductTx).should.be.rejectedWith('The product needs to be shipped before it can be received'); + }); + + it('should not update the status if the letter has not been marked as \'shipped\'', async () => { + // update the letter to be approved but not shipped + let updatedLetter = await letterRegistry.get(letterId); + updatedLetter.approval = [aliceRelationship, matiasRelationship, ellaRelationship, bobRelationship]; + updatedLetter.status = 'APPROVED'; + await letterRegistry.update(updatedLetter); + + // attempt to submit a ReceiveProduct transaction against the unshipped letter, and check the error + const receiveProductTx = factory.newTransaction(namespace, 'ReceiveProduct'); + receiveProductTx. loc = factory.newRelationship(namespace, 'LetterOfCredit', letterId); + return businessNetworkConnection.submitTransaction(receiveProductTx).should.be.rejectedWith('The product needs to be shipped before it can be received'); + }); + + it('should mark the letter as \'received\' if it has been shipped', async () => { + // update the letter to be shipped + let updatedLetter = await letterRegistry.get(letterId); + updatedLetter.approval = [aliceRelationship, matiasRelationship, ellaRelationship, bobRelationship]; + updatedLetter.status = 'SHIPPED'; + await letterRegistry.update(updatedLetter); + + // create and submit the ReceiveProduct transaction + const receiveProductTx = factory.newTransaction(namespace, 'ReceiveProduct'); + receiveProductTx.loc = factory.newRelationship(namespace, 'LetterOfCredit', letterId); + await businessNetworkConnection.submitTransaction(receiveProductTx); + + const receivedLetter = await letterRegistry.get(letterId); + receivedLetter.status.should.deep.equal('RECEIVED'); + }); + + it('should do nothing if the letter has already been marked as \'received\'', async () => { + // update the letter so that is has already been marked as received + let updatedLetter = await letterRegistry.get(letterId); + updatedLetter.approval = [aliceRelationship, matiasRelationship, ellaRelationship, bobRelationship]; + updatedLetter.status = 'RECEIVED'; + await letterRegistry.update(updatedLetter); + + // attempt to submit the ReceiveProduct transaction and check the error + const receiveProductTx = factory.newTransaction(namespace, 'ReceiveProduct'); + receiveProductTx.loc = factory.newRelationship(namespace, 'LetterOfCredit', letterId); + return businessNetworkConnection.submitTransaction(receiveProductTx).should.be.rejectedWith('The product has already been received'); + }); + + it('should be unable to submit a ReceiveProduct transaction on a letter that has already been closed', async () => { + // update the letter to have already been closed + let updatedLetter = await letterRegistry.get(letterId); + updatedLetter.approval = [aliceRelationship, matiasRelationship, ellaRelationship, bobRelationship]; + updatedLetter.status = 'CLOSED'; + await letterRegistry.update(updatedLetter); + + // attempt to submit a ReceiveProduct transaction and check the error + const receiveProductTx = factory.newTransaction(namespace, 'ReceiveProduct'); + receiveProductTx.loc = factory.newRelationship(namespace, 'LetterOfCredit', letterId); + return businessNetworkConnection.submitTransaction(receiveProductTx).should.be.rejectedWith('This letter of credit has already been closed'); + }); + + it('should be unable to submit a ReceiveProduct transaction on a letter that has already been rejected', async () => { + // update the letter to have been rejected + let updatedLetter = await letterRegistry.get(letterId); + updatedLetter.status = 'REJECTED'; + await letterRegistry.update(updatedLetter); + + // attempt to submit a ReceiveProduct transaction and check the error + const receiveProductTx = factory.newTransaction(namespace, 'ReceiveProduct'); + receiveProductTx.loc = factory.newRelationship(namespace, 'LetterOfCredit', letterId); + return businessNetworkConnection.submitTransaction(receiveProductTx).should.be.rejectedWith('This letter of credit has already been closed'); + }); + + }); + + describe('ReadyForPayment', () => { + it('should not ready payment the letter if it is not marked as \'received\'', async () => { + const readyTx = factory.newTransaction(namespace, 'ReadyForPayment'); + readyTx.loc = factory.newRelationship(namespace, 'LetterOfCredit', letterId); + return businessNetworkConnection.submitTransaction(readyTx).should.be.rejectedWith('The payment cannot be made until the product has been received by the applicant'); + }); + + it('should mark the letter as ready for payment', async () => { + let updatedLetter = await letterRegistry.get(letterId); + updatedLetter.approval = [aliceRelationship, matiasRelationship, ellaRelationship, bobRelationship]; + updatedLetter.status = 'RECEIVED'; + await letterRegistry.update(updatedLetter); + + const readyTx = factory.newTransaction(namespace, 'ReadyForPayment'); + readyTx.loc = factory.newRelationship(namespace, 'LetterOfCredit', letterId); + await businessNetworkConnection.submitTransaction(readyTx); + + const readyLetter = await letterRegistry.get(letterId); + readyLetter.status.should.deep.equal('READY_FOR_PAYMENT'); + }); + + it('should not ready payment the letter if it is marked as \'closed\'', async () => { + let updatedLetter = await letterRegistry.get(letterId); + updatedLetter.approval = [aliceRelationship, matiasRelationship, ellaRelationship, bobRelationship]; + updatedLetter.status = 'CLOSED'; + await letterRegistry.update(updatedLetter); + + const readyTx = factory.newTransaction(namespace, 'ReadyForPayment'); + readyTx.loc = factory.newRelationship(namespace, 'LetterOfCredit', letterId); + return businessNetworkConnection.submitTransaction(readyTx).should.be.rejectedWith('This letter of credit has already been closed'); + }); + + it('should not ready payment the letter if it is marked as \'rejected\'', async () => { + let updatedLetter = await letterRegistry.get(letterId); + updatedLetter.approval = [aliceRelationship, matiasRelationship, ellaRelationship, bobRelationship]; + updatedLetter.status = 'REJECTED'; + await letterRegistry.update(updatedLetter); + + const readyTx = factory.newTransaction(namespace, 'ReadyForPayment'); + readyTx.loc = factory.newRelationship(namespace, 'LetterOfCredit', letterId); + return businessNetworkConnection.submitTransaction(readyTx).should.be.rejectedWith('This letter of credit has already been closed'); + }); + + it('should not ready payment the letter if it is marked as \'ready for payment\'', async () => { + let updatedLetter = await letterRegistry.get(letterId); + updatedLetter.approval = [aliceRelationship, matiasRelationship, ellaRelationship, bobRelationship]; + updatedLetter.status = 'READY_FOR_PAYMENT'; + await letterRegistry.update(updatedLetter); + + const readyTx = factory.newTransaction(namespace, 'ReadyForPayment'); + readyTx.loc = factory.newRelationship(namespace, 'LetterOfCredit', letterId); + return businessNetworkConnection.submitTransaction(readyTx).should.be.rejectedWith('The payment has already been made'); + }); + }); + + describe('Close', () => { + it('should not close the letter if it is not marked as \'ready for payment\'', async () => { + // attempt to submit a Close transaction and check the error + const closeTx = factory.newTransaction(namespace, 'Close'); + closeTx.loc = factory.newRelationship(namespace, 'LetterOfCredit', letterId); + closeTx.closeReason = 'testing the Close transaction'; + return businessNetworkConnection.submitTransaction(closeTx).should.be.rejectedWith('Cannot close this letter of credit until it is fully approved and the product has been received by the applicant'); + }); + + it('should mark the letter as closed', async () => { + // update the letter so it is ready to close + let updatedLetter = await letterRegistry.get(letterId); + updatedLetter.approval = [aliceRelationship, matiasRelationship, ellaRelationship, bobRelationship]; + updatedLetter.status = 'READY_FOR_PAYMENT'; + await letterRegistry.update(updatedLetter); + + // create and submit a Close transaction + const closeTx = factory.newTransaction(namespace, 'Close'); + closeTx.loc = factory.newRelationship(namespace, 'LetterOfCredit', letterId); + closeTx.closeReason = 'testing the Close transaction'; + await businessNetworkConnection.submitTransaction(closeTx); + + const closedLetter = await letterRegistry.get(letterId); + closedLetter.status.should.deep.equal('CLOSED'); + closedLetter.closeReason.should.deep.equal('testing the Close transaction'); + }); + + it('should be unable to submit a Close transaction on a letter that has already been closed', async () => { + // update the letter to have already been closed + let updatedLetter = await letterRegistry.get(letterId); + updatedLetter.approval = [aliceRelationship, matiasRelationship, ellaRelationship, bobRelationship]; + updatedLetter.status = 'CLOSED'; + await letterRegistry.update(updatedLetter); + + // attempt to submit a Close transaction and check the error + const closeTx = factory.newTransaction(namespace, 'Close'); + closeTx.loc = factory.newRelationship(namespace, 'LetterOfCredit', letterId); + closeTx.closeReason = 'testing the Close transaction'; + return businessNetworkConnection.submitTransaction(closeTx).should.be.rejectedWith('This letter of credit has already been closed'); + }); + + it('should be unable to submit a Close transaction on a letter that has already been rejected', async () => { + // update the letter to have been rejected + let updatedLetter = await letterRegistry.get(letterId); + updatedLetter.status = 'REJECTED'; + await letterRegistry.update(updatedLetter); + + // attempt to submit a Close transaction and check the error + const closeTx = factory.newTransaction(namespace, 'Close'); + closeTx.loc = factory.newRelationship(namespace, 'LetterOfCredit', letterId); + closeTx.closeReason = 'testing the Close transaction'; + return businessNetworkConnection.submitTransaction(closeTx).should.be.rejectedWith('This letter of credit has already been closed'); + }); + }); + +}); diff --git a/packages/vehicle-lifecycle-network/jsdoc.json b/packages/vehicle-lifecycle-network/jsdoc.json index ca000e4..5752ca8 100644 --- a/packages/vehicle-lifecycle-network/jsdoc.json +++ b/packages/vehicle-lifecycle-network/jsdoc.json @@ -17,7 +17,7 @@ "dateFormat": "ddd MMM Do YYYY", "outputSourceFiles": true, "outputSourcePath": true, - "systemName": "Perishable Goods Network", + "systemName": "Vehicle Lifecycle Network", "footer": "", "copyright": "Released under the Apache License v2.0", "navType": "vertical", diff --git a/packages/vehicle-manufacture-network/jsdoc.json b/packages/vehicle-manufacture-network/jsdoc.json index ca000e4..d045f03 100644 --- a/packages/vehicle-manufacture-network/jsdoc.json +++ b/packages/vehicle-manufacture-network/jsdoc.json @@ -17,7 +17,7 @@ "dateFormat": "ddd MMM Do YYYY", "outputSourceFiles": true, "outputSourcePath": true, - "systemName": "Perishable Goods Network", + "systemName": "Vehicle Manufacture Network", "footer": "", "copyright": "Released under the Apache License v2.0", "navType": "vertical",