From 279c0b7520522b9ce1b342ca659bd73ef7b23f5c Mon Sep 17 00:00:00 2001 From: Andrew Hurt <31732124+awjh-ibm@users.noreply.github.com> Date: Fri, 9 Feb 2018 10:25:02 +0000 Subject: [PATCH] vehicle manufacture network initial (#146) Signed-off-by: awjh-ibm --- .../vehicle-manufacture-network/.eslintignore | 6 + .../vehicle-manufacture-network/.eslintrc.yml | 49 +++ .../vehicle-manufacture-network/.gitignore | 50 +++ .../vehicle-manufacture-network/.npmignore | 48 +++ .../vehicle-manufacture-network/README.md | 132 ++++++ .../config/default.json | 3 + .../vehicle-manufacture-network/header.txt | 13 + packages/vehicle-manufacture-network/index.js | 13 + .../vehicle-manufacture-network/jsdoc.json | 35 ++ .../vehicle-manufacture-network/lib/script.js | 266 ++++++++++++ .../models/org.vehicle.cto | 106 +++++ .../networkimage.svg | 1 + .../networkimageanimated.svg | 1 + .../vehicle-manufacture-network/package.json | 59 +++ .../permissions.acl | 107 +++++ .../test/script.js | 399 ++++++++++++++++++ 16 files changed, 1288 insertions(+) create mode 100644 packages/vehicle-manufacture-network/.eslintignore create mode 100755 packages/vehicle-manufacture-network/.eslintrc.yml create mode 100644 packages/vehicle-manufacture-network/.gitignore create mode 100644 packages/vehicle-manufacture-network/.npmignore create mode 100644 packages/vehicle-manufacture-network/README.md create mode 100644 packages/vehicle-manufacture-network/config/default.json create mode 100644 packages/vehicle-manufacture-network/header.txt create mode 100644 packages/vehicle-manufacture-network/index.js create mode 100644 packages/vehicle-manufacture-network/jsdoc.json create mode 100644 packages/vehicle-manufacture-network/lib/script.js create mode 100644 packages/vehicle-manufacture-network/models/org.vehicle.cto create mode 100644 packages/vehicle-manufacture-network/networkimage.svg create mode 100644 packages/vehicle-manufacture-network/networkimageanimated.svg create mode 100644 packages/vehicle-manufacture-network/package.json create mode 100644 packages/vehicle-manufacture-network/permissions.acl create mode 100644 packages/vehicle-manufacture-network/test/script.js diff --git a/packages/vehicle-manufacture-network/.eslintignore b/packages/vehicle-manufacture-network/.eslintignore new file mode 100644 index 0000000..ccb9c0a --- /dev/null +++ b/packages/vehicle-manufacture-network/.eslintignore @@ -0,0 +1,6 @@ +coverage +dist +go +lib +node_modules +out \ No newline at end of file diff --git a/packages/vehicle-manufacture-network/.eslintrc.yml b/packages/vehicle-manufacture-network/.eslintrc.yml new file mode 100755 index 0000000..59931e1 --- /dev/null +++ b/packages/vehicle-manufacture-network/.eslintrc.yml @@ -0,0 +1,49 @@ +env: + es6: true + node: true + mocha: true +extends: 'eslint:recommended' +parserOptions: + sourceType: + - script +globals: + getAssetRegistry: true + getFactory: true + getParticipantRegistry: true + getCurrentParticipant: true +rules: + indent: + - error + - 4 + linebreak-style: + - error + - unix + quotes: + - error + - single + semi: + - error + - always + no-unused-vars: + - 0 + - args: none + no-console: off + curly: error + eqeqeq: error + no-throw-literal: error + strict: 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/vehicle-manufacture-network/.gitignore b/packages/vehicle-manufacture-network/.gitignore new file mode 100644 index 0000000..6707743 --- /dev/null +++ b/packages/vehicle-manufacture-network/.gitignore @@ -0,0 +1,50 @@ +# 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 \ No newline at end of file diff --git a/packages/vehicle-manufacture-network/.npmignore b/packages/vehicle-manufacture-network/.npmignore new file mode 100644 index 0000000..74bbb1c --- /dev/null +++ b/packages/vehicle-manufacture-network/.npmignore @@ -0,0 +1,48 @@ +# 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/vehicle-manufacture-network/README.md b/packages/vehicle-manufacture-network/README.md new file mode 100644 index 0000000..bd89ca0 --- /dev/null +++ b/packages/vehicle-manufacture-network/README.md @@ -0,0 +1,132 @@ +# Vehicle Manufacture Network + +> This network tracks the manufacture of vehicles from an initial order request through to their completion by the manufacturer. A regulator is able to provide oversight throughout this whole process. + +## Models within this business network + +### Participants +`Person` `Manufacturer` `Regulator` + +### Assets + +`Order` `Vehicle` + +### Transactions + +`PlaceOrder` `UpdateOrderStatus` `SetupDemo` + +### Events + +`PlaceOrderEvent` `UpdateOrderStatusEvent` + +## Example use of this business network +A `Person` uses a manufacturer's application to build their desired car and order it. The application submits a `PlaceOrder` transaction which creates a new `Order` asset containing the details of the vehicle the `Person` wishes to have made for them. The `Manufacturer` begins work on the car and as it proceeds through stages of production the `Manufacturer` submits `UpdateOrderStatus` transactions to mark the change in status for the `Order` e.g. updating the status from PLACED to SCHEDULED_FOR_MANUFACTURE. Once the `Manufacturer` has completed production for the `Order` they register the car by submitting an `UpdateOrderStatus` transaction with the status VIN_ASSIGNED (also providing the VIN to register against) and a `Vehicle` asset is formally added to the registry using the details specified in the `Order`. Once the car is registered then the `Manufacturer` submits an `UpdateOrderStatus` transaction with a status of OWNER_ASSIGNED at which point the owner field of the `Vehicle` is set to match the orderer field of the `Order`. The regulator would perform oversight over this whole process. + +## Testing this network within playground +Navigate to the **Test** tab and then submit a `SetupDemo` transaction: + +``` +{ + "$class": "org.acme.vehicle_network.SetupDemo" +} +``` + +This will generate three `Manufacturer` participants, fourteen `Person` participants, a single `Regulator` participant and thirteen `Vehicle` assets. + +Next order your car (an orange Arium Gamora) by submitting a PlaceOrder transaction: + +``` +{ + "$class": "org.acme.vehicle_network.PlaceOrder", + "orderId": "1234", + "vehicleDetails": { + "$class": "org.acme.vehicle_network.VehicleDetails", + "make": "resource:org.acme.vehicle_network.Manufacturer#Arium", + "modelType": "Gamora", + "colour": "Sunburst Orange" + }, + "options": { + "trim": "executive", + "interior": "red rum", + "extras": ["tinted windows", "extended warranty"] + }, + "orderer": "resource:org.acme.vehicle_network.Person#Paul" +} +``` + +This `PlaceOrder` transaction generates a new `Order` asset in the registry and emits a `PlaceOrderEvent` event. + +Now simulate the order being accepted by the manufacturer by submitting an `UpdateOrderStatus` transaction: + +``` +{ + "$class": "org.acme.vehicle_network.UpdateOrderStatus", + "orderStatus": "SCHEDULED_FOR_MANUFACTURE", + "order": "resource:org.acme.vehicle_network.Order#1234" +} +``` + +This `UpdateOrderStatus` transaction updates the orderStatus of the `Order` with orderId 1234 in the asset registry and emits an `UpdateOrderStatusEvent` event. + +Simulate the manufacturer registering the vehicle with the regulator by submitting an `UpdateOrderStatus` transaction: + +``` +{ + "$class": "org.acme.vehicle_network.UpdateOrderStatus", + "orderStatus": "VIN_ASSIGNED", + "order": "resource:org.acme.vehicle_network.Order#1234", + "vin": 'abc123' +} +``` + +This `UpdateOrderStatus` transaction updates the orderStatus of the `Order` with orderId 1234 in the asset registry, create a new `Vehicle` based of that `Order` in the asset registry and emits an `UpdateOrderStatusEvent` event. At this stage as the vehicle does not have an owner assigned to it, its status is declared as OFF_THE_ROAD. + +Next assign the owner of the vehicle using an `UpdateOrderStatus` transaction: + +``` +{ + "$class": "org.acme.vehicle_network.UpdateOrderStatus", + "orderStatus": "OWNER_ASSIGNED", + "order": "resource:org.acme.vehicle_network.Order#1234", + "vin": 'abc123' +} +``` + +This `UpdateOrderStatus` transaction updates the orderStatus of the `Order` with orderId 1234 in the asset registry, update the `Vehicle` asset with VIN abc123 to have an owner of Paul (who we intially marked as the orderer in the `PlaceOrder` transaction) and status of ACTIVE and also emits an `UpdateOrderStatusEvent` event. + +Finally complete the ordering process by marking the order as `DELIVERED` through submitting another `UpdateOrderStatus` transaction: + +``` +{ + "$class": "org.acme.vehicle_network.UpdateOrderStatus", + "orderStatus": "DELIVERED", + "order": "resource:org.acme.vehicle_network.Order#1234" +} +``` + +This `UpdateOrderStatus` transaction updates the orderStatus of the `Order` with orderId 1234 in the asset registry and emits an `UpdateOrderStatusEvent` event. + +This Business Network definition has been used to create demo applications that simulate the scenario outlined above. You can find more detail on these at https://github.com/hyperledger/composer-sample-applications/tree/master/packages/vehicle-lifecycle + +## Permissions in this business network for modelled participants +Within this network permissions are outlines for the participants outlining what they can and can't do. The rules in the permissions.acl file explicitly ALLOW participants to perform actions. Actions not written for a participant in that file are blocked. +### Regulator +`RegulatorAdminUser` - Gives the regulator permission to perform ALL actions on ALL resources + +### Manufacturer +`ManufacturerUpdateOrder` - Allows a manufacturer to UPDATE an Order asset's data only using an UpdateOrderStatus transaction. The manufacturer must also be specified as the *vehicleDetails.make* in the Order asset. + +`ManufacturerUpdateOrderStatus` - Allows a manufacturer to CREATE and READ UpdateOrderStatus transactions that refer to an order that they are specified as the *vehicleDetails.make* in. + +`ManufacturerReadOrder` - Allows a manufacturer to READ an Order asset that they are specified as the *vehicleDetails.make* in. + +`ManufacturerCreateVehicle` - Allows a manufacturer to CREATE a vehicle asset only using a UpdateOrderStatus transaction. The transaction must have an *orderStatus* of VIN_ASSIGNED and the Order asset have the manufacturer specified as the *vehicleDetails.make*. + +`ManufacturerReadVehicle` - Allows a manufacturer to READ a Vehicle asset that they are specified as the *vehicleDetails.make* in. + +### Person +`PersonMakeOrder` - Gives the person permission to CREATE an Order asset only using a PlaceOrder transaction. The person must also be specified as the *orderer* in the Order asset. + +`PersonPlaceOrder` - Gives the person permission to CREATE and READ PlaceOrder transactions that refer to an order that they are specified as *orderer* in. + +`PersonReadOrder` - Gives the person permission to READ an Order asset that they are specified as the *orderer* in. \ No newline at end of file diff --git a/packages/vehicle-manufacture-network/config/default.json b/packages/vehicle-manufacture-network/config/default.json new file mode 100644 index 0000000..82cc3f3 --- /dev/null +++ b/packages/vehicle-manufacture-network/config/default.json @@ -0,0 +1,3 @@ +{ + "config":"" +} diff --git a/packages/vehicle-manufacture-network/header.txt b/packages/vehicle-manufacture-network/header.txt new file mode 100644 index 0000000..e1971c4 --- /dev/null +++ b/packages/vehicle-manufacture-network/header.txt @@ -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/vehicle-manufacture-network/index.js b/packages/vehicle-manufacture-network/index.js new file mode 100644 index 0000000..e1971c4 --- /dev/null +++ b/packages/vehicle-manufacture-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/vehicle-manufacture-network/jsdoc.json b/packages/vehicle-manufacture-network/jsdoc.json new file mode 100644 index 0000000..ca000e4 --- /dev/null +++ b/packages/vehicle-manufacture-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/vehicle-manufacture-network/lib/script.js b/packages/vehicle-manufacture-network/lib/script.js new file mode 100644 index 0000000..39e4890 --- /dev/null +++ b/packages/vehicle-manufacture-network/lib/script.js @@ -0,0 +1,266 @@ +/* + * 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. + */ + +// MANUFACTURER FUNCTIONS +/** + * Place an order for a vehicle + * @param {org.acme.vehicle_network.PlaceOrder} placeOrder - the PlaceOrder transaction + * @transaction + */ +function placeOrder(orderRequest) { + console.log('placeOrder') + + var factory = getFactory(); + var namespace = 'org.acme.vehicle_network'; + + var order = factory.newResource(namespace, 'Order', orderRequest.orderId); + order.vehicleDetails = orderRequest.vehicleDetails; + order.orderStatus = 'PLACED'; + order.orderer = factory.newRelationship(namespace, 'Person', orderRequest.orderer.getIdentifier()); + order.options = orderRequest.options; + + // save the order + return getAssetRegistry(order.getFullyQualifiedType()) + .then(function (assetRegistry) { + return assetRegistry.add(order); + }) + .then(function () { + // emit the event + var placeOrderEvent = factory.newEvent(namespace, 'PlaceOrderEvent'); + placeOrderEvent.orderId = order.orderId; + placeOrderEvent.vehicleDetails = order.vehicleDetails; + placeOrderEvent.orderer = order.orderer; + emit(placeOrderEvent); + }); +} + +/** + * Update the status of an order + * @param {org.acme.vehicle_network.UpdateOrderStatus} updateOrderStatus - the UpdateOrderStatus transaction + * @transaction + */ +function updateOrderStatus(updateOrderRequest) { + console.log('updateOrderStatus'); + + var factory = getFactory(); + var namespace = 'org.acme.vehicle_network'; + + // get vehicle registry + return getAssetRegistry(namespace + '.Vehicle') + .then(function (vehicleRegistry) { + if (updateOrderRequest.orderStatus === 'VIN_ASSIGNED') { + if (!updateOrderRequest.vin) { + throw new Error('Value for VIN was expected'); + } + // create a vehicle + var vehicle = factory.newResource(namespace, 'Vehicle', updateOrderRequest.vin ); + vehicle.vehicleDetails = updateOrderRequest.order.vehicleDetails; + vehicle.vehicleStatus = 'OFF_THE_ROAD'; + return vehicleRegistry.add(vehicle); + } else if(updateOrderRequest.orderStatus === 'OWNER_ASSIGNED') { + if (!updateOrderRequest.vin) { + throw new Error('Value for VIN was expected'); + } + + // assign the owner of the vehicle to be the person who placed the order + return vehicleRegistry.get(updateOrderRequest.vin) + .then(function (vehicle) { + vehicle.vehicleStatus = 'ACTIVE'; + vehicle.owner = factory.newRelationship(namespace, 'Person', updateOrderRequest.order.orderer.username); + return vehicleRegistry.update(vehicle); + }); + } + }) + .then(function() { + return getAssetRegistry(namespace + '.Order'); + }) + .then(function(orderRegistry) { + // update the order + var order = updateOrderRequest.order; + order.orderStatus = updateOrderRequest.orderStatus; + return orderRegistry.update(order); + }) + .then(function() { + // emit the event + var updateOrderStatusEvent = factory.newEvent(namespace, 'UpdateOrderStatusEvent'); + updateOrderStatusEvent.orderStatus = updateOrderRequest.order.orderStatus; + updateOrderStatusEvent.order = updateOrderRequest.order; + emit(updateOrderStatusEvent); + }); +} + +// DEMO SETUP FUNCTIONS +/** + * Create the participants to use in the demo + * @param {org.acme.vehicle_network.SetupDemo} setupDemo - the SetupDemo transaction + * @transaction + */ +function setupDemo() { + console.log('setupDemo'); + + var factory = getFactory(); + var namespace = 'org.acme.vehicle_network'; + + var people = ['Paul', 'Andy', 'Hannah', 'Sam', 'Caroline', 'Matt', 'Fenglian', 'Mark', 'James', 'Dave', 'Rob', 'Kai', 'Ellis', 'LesleyAnn']; + var manufacturers; // will use those found in vehicles object + + var vehicles = { + 'Arium': { + 'Nova': [ + { + 'vin': 'ea290d9f5a6833a65', + 'colour': 'Royal Purple', + 'vehicleStatus': 'ACTIVE' + } + ], + 'Nebula': [ + { + 'vin': '39fd242c2bbe80f11', + 'colour': 'Statement Blue', + 'vehicleStatus': 'ACTIVE' + } + ] + }, + 'Morde': { + 'Putt': [ + { + 'vin': '835125e50bca37ca1', + 'colour': 'Black', + 'vehicleStatus': 'ACTIVE', + 'suspiciousMessage': 'Mileage anomaly' + }, + { + 'vin': '0812e6d8d486e0464', + 'colour': 'Red', + 'vehicleStatus': 'ACTIVE' + }, + { + 'vin': 'c4aa418f26d4a0403', + 'colour': 'Silver', + 'vehicleStatus': 'ACTIVE' + } + ], + 'Pluto': [ + { + 'vin': '7382fbfc083f696e5', + 'colour': 'White', + 'vehicleStatus': 'ACTIVE' + }, + { + 'vin': '01a9cd3f8f5db5ef7', + 'colour': 'Green', + 'vehicleStatus': 'ACTIVE', + 'suspiciousMessage': 'Insurance write-off but still active' + }, + { + 'vin': '97f305df4c2881e71', + 'colour': 'Grey', + 'vehicleStatus': 'ACTIVE' + } + ] + }, + 'Ridge': { + 'Cannon': [ + { + 'vin': 'af462063fb901d0e6', + 'colour': 'Red', + 'vehicleStatus': 'ACTIVE' + }, + { + 'vin': '3ff3395ecfd38f787', + 'colour': 'White', + 'vehicleStatus': 'ACTIVE', + 'suspiciousMessage': 'Suspicious ownership sequence' + }, + { + 'vin': 'de701fcf2a78d8086', + 'colour': 'Silver', + 'vehicleStatus': 'ACTIVE', + 'suspiciousMessage': 'Untaxed vehicle' + } + ], + 'Rancher': [ + { + 'vin': '2fcdd7b5131e81fd0', + 'colour': 'Blue', + 'vehicleStatus': 'ACTIVE' + }, + { + 'vin': '79540e5384c970321', + 'colour': 'White', + 'vehicleStatus': 'ACTIVE', + 'suspiciousMessage': 'Uninsured vehicle' + } + ] + } + }; + + // convert array names of people to be array of participant resources of type Person with identifier of that name + people = people.map(function (person) { + return factory.newResource(namespace, 'Person', person); + }); + + // create array of Manufacturer particpant resources identified by the top level keys in vehicles var + manufacturers = Object.keys(vehicles).map(function (manufacturer) { + var manufacturerResource = factory.newResource(namespace, 'Manufacturer', manufacturer); + manufacturerResource.name = manufacturer; + return manufacturerResource; + }); + + // create a Regulator participant resource + var regulator = factory.newResource(namespace, 'Regulator', 'VDA'); + regulator.name = 'VDA'; + + // add the regulator + return getParticipantRegistry(namespace + '.Regulator') + .then(function (regulatorRegistry) { + return regulatorRegistry.add(regulator); + }) + .then(function () { + return getParticipantRegistry(namespace + '.Manufacturer'); + }) + .then(function (manufacturerRegistry) { + return manufacturerRegistry.addAll(manufacturers); + }) + .then(function () { + return getParticipantRegistry(namespace + '.Person'); + }) + .then(function (personRegistry) { + return personRegistry.addAll(people); + }) + .then(function () { + return getAssetRegistry(namespace + '.Vehicle'); + }) + .then(function(vehicleRegistry) { + var vehicleResources = []; + + for (var manufacturer in vehicles) { + for (var model in vehicles[manufacturer]) { + var vehicleTemplatesForModel = vehicles[manufacturer][model]; + vehicleTemplatesForModel.forEach(function(vehicleTemplate) { + var vehicle = factory.newResource(namespace, 'Vehicle', vehicleTemplate.vin); + vehicle.owner = people[vehicleResources.length+1]; + vehicle.vehicleStatus = vehicleTemplate.vehicleStatus; + vehicle.vehicleDetails = factory.newConcept(namespace, 'VehicleDetails'); + vehicle.vehicleDetails.make = factory.newResource(namespace, 'Manufacturer', manufacturer); + vehicle.vehicleDetails.modelType = model; + vehicle.vehicleDetails.colour = vehicleTemplate.colour; + + vehicleResources.push(vehicle); + }); + } + } + return vehicleRegistry.addAll(vehicleResources); + }); +} \ No newline at end of file diff --git a/packages/vehicle-manufacture-network/models/org.vehicle.cto b/packages/vehicle-manufacture-network/models/org.vehicle.cto new file mode 100644 index 0000000..0f6a915 --- /dev/null +++ b/packages/vehicle-manufacture-network/models/org.vehicle.cto @@ -0,0 +1,106 @@ +/* + * 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. + */ + +namespace org.acme.vehicle_network + +// BASE DEFINTIONS + +concept VehicleDetails { + --> Manufacturer make + o String modelType + o String colour +} + +abstract participant Company identified by companyId { + o String companyId + o String name +} + +participant Person identified by username { + o String username + o String email optional +} + + +// MANUFACTURE DEFINITIONS +participant Manufacturer extends Company { +} + +enum OrderStatus { + o PLACED + o SCHEDULED_FOR_MANUFACTURE + o VIN_ASSIGNED + o OWNER_ASSIGNED + o DELIVERED +} + +concept Options { + o String trim + o String interior + o String[] extras +} + +asset Order identified by orderId { + o String orderId + o VehicleDetails vehicleDetails + o OrderStatus orderStatus + o Options options + --> Person orderer +} + +transaction PlaceOrder { + o String orderId + o VehicleDetails vehicleDetails + o Options options + --> Person orderer +} + +event PlaceOrderEvent { + o String orderId + o VehicleDetails vehicleDetails + --> Person orderer +} + +transaction UpdateOrderStatus { + o OrderStatus orderStatus + o String vin optional + --> Order order +} + +event UpdateOrderStatusEvent { + o OrderStatus orderStatus + o Order order +} + + +// REGULATOR DEFINITIONS +participant Regulator extends Company { +} + +enum VehicleStatus { + o ACTIVE + o OFF_THE_ROAD + o SCRAPPED +} + +asset Vehicle identified by vin { + o String vin + o VehicleDetails vehicleDetails + o VehicleStatus vehicleStatus + --> Person owner optional +} + +// DEMO SPECIFIC DEFINITIONS +transaction SetupDemo { +} \ No newline at end of file diff --git a/packages/vehicle-manufacture-network/networkimage.svg b/packages/vehicle-manufacture-network/networkimage.svg new file mode 100644 index 0000000..dc57ef9 --- /dev/null +++ b/packages/vehicle-manufacture-network/networkimage.svg @@ -0,0 +1 @@ +Asset 17 \ No newline at end of file diff --git a/packages/vehicle-manufacture-network/networkimageanimated.svg b/packages/vehicle-manufacture-network/networkimageanimated.svg new file mode 100644 index 0000000..05de0b9 --- /dev/null +++ b/packages/vehicle-manufacture-network/networkimageanimated.svg @@ -0,0 +1 @@ +Asset 17 \ No newline at end of file diff --git a/packages/vehicle-manufacture-network/package.json b/packages/vehicle-manufacture-network/package.json new file mode 100644 index 0000000..e2d996c --- /dev/null +++ b/packages/vehicle-manufacture-network/package.json @@ -0,0 +1,59 @@ +{ + "engines": { + "composer": "^0.16.0" + }, + "name": "vehicle-manufacture-network", + "version": "0.1.14", + "description": "Vehicle manufacture network", + "networkImage": "https://hyperledger.github.io/composer-sample-networks/packages/vehicle-manufacture-network/networkimage.svg", + "networkImageanimated": "https://hyperledger.github.io/composer-sample-networks/packages/vehicle-manufacture-network/networkimageanimated.svg", + "scripts": { + "prepublish": "mkdirp ./dist && composer archive create --sourceType dir --sourceName . -a ./dist/vehicle-manufacture-network.bna", + "pretest": "npm run lint", + "lint": "eslint .", + "postlint": "npm run licchk", + "licchk": "license-check", + "postlicchk": "npm run doc", + "doc": "jsdoc --pedantic --recurse -c jsdoc.json", + "test": "mocha -t 0 --recursive" + }, + "repository": { + "type": "git", + "url": "https://github.com/hyperledger/composer-sample-networks.git" + }, + "keywords": [ + "vehicle", + "manufacture", + "composer", + "composer-network" + ], + "author": "Hyperledger Composer", + "license": "Apache-2.0", + "devDependencies": { + "chai": "^3.5.0", + "composer-admin": "^0.16.0", + "composer-cli": "^0.16.0", + "composer-client": "^0.16.0", + "composer-connector-embedded": "^0.16.0", + "eslint": "^3.6.1", + "istanbul": "^0.4.5", + "jsdoc": "^3.5.5", + "license-check": "^1.1.5", + "mkdirp": "^0.5.1", + "mocha": "^3.2.0", + "moment": "^2.17.1" + }, + "license-check-config": { + "src": [ + "**/*.js", + "!./coverage/**/*", + "!./node_modules/**/*", + "!./out/**/*", + "!./scripts/**/*" + ], + "path": "header.txt", + "blocking": true, + "logInfo": false, + "logError": true + } +} diff --git a/packages/vehicle-manufacture-network/permissions.acl b/packages/vehicle-manufacture-network/permissions.acl new file mode 100644 index 0000000..d9fa7fd --- /dev/null +++ b/packages/vehicle-manufacture-network/permissions.acl @@ -0,0 +1,107 @@ +rule PersonMakeOrder { + description: "Allow Persons to create and view orders" + participant(p): "org.acme.vehicle_network.Person" + operation: CREATE + resource(o): "org.acme.vehicle_network.Order" + transaction(tx): "org.acme.vehicle_network.PlaceOrder" + condition: (o.orderer.getIdentifier() == p.getIdentifier()) + action: ALLOW +} + +rule PersonPlaceOrder { + description: "Allow Persons to place orders and view they've done this" + participant(p): "org.acme.vehicle_network.Person" + operation: CREATE, READ + resource(o): "org.acme.vehicle_network.PlaceOrder" + condition: (o.orderer.getIdentifier() == p.getIdentifier()) + action: ALLOW +} + +rule PersonReadOrder { + description: "Allow Persons to place orders and view they've done this" + participant(p): "org.acme.vehicle_network.Person" + operation: READ + resource(o): "org.acme.vehicle_network.Order" + condition: (o.orderer.getIdentifier() == p.getIdentifier()) + action: ALLOW +} + +rule ManufacturerUpdateOrder { + description: "Allow manufacturers to view and update their own orders" + participant(m): "org.acme.vehicle_network.Manufacturer" + operation: UPDATE + resource(o): "org.acme.vehicle_network.Order" + transaction(tx): "org.acme.vehicle_network.UpdateOrderStatus" + condition: (o.vehicleDetails.make.getIdentifier() == m.getIdentifier()) + action: ALLOW +} + +rule ManufacturerUpdateOrderStatus { + description: "Allow manufacturers to update order statuses and view they've done this" + participant(m): "org.acme.vehicle_network.Manufacturer" + operation: CREATE, READ + resource(o): "org.acme.vehicle_network.UpdateOrderStatus" + condition: (o.order.vehicleDetails.make.getIdentifier() == m.getIdentifier()) + action: ALLOW +} + +rule ManufacturerReadOrder { + description: "Allow manufacturers to view and update their own orders" + participant(m): "org.acme.vehicle_network.Manufacturer" + operation: READ + resource(o): "org.acme.vehicle_network.Order" + condition: (o.vehicleDetails.make.getIdentifier() == m.getIdentifier()) + action: ALLOW +} + +rule ManufacturerCreateVehicles { + description: "Allow manufacturers to create and view their vehicles" + participant(m): "org.acme.vehicle_network.Manufacturer" + operation: CREATE + resource(v): "org.acme.vehicle_network.Vehicle" + transaction(tx): "org.acme.vehicle_network.UpdateOrderStatus" + condition: (v.vehicleDetails.make.getIdentifier() == m.getIdentifier() && tx.orderStatus == "VIN_ASSIGNED") + action: ALLOW +} + +rule ManufacturerReadVehicles { + description: "Allow manufacturers to create and view their vehicles" + participant(m): "org.acme.vehicle_network.Manufacturer" + operation: READ + resource(v): "org.acme.vehicle_network.Vehicle" + condition: (v.vehicleDetails.make.getIdentifier() == m.getIdentifier()) + action: ALLOW +} + +rule RegulatorAdminUser { + description: "Let the regulator do anything" + participant: "org.acme.vehicle_network.Regulator" + operation: ALL + resource: "**" + action: ALLOW +} + +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 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/vehicle-manufacture-network/test/script.js b/packages/vehicle-manufacture-network/test/script.js new file mode 100644 index 0000000..5afdb91 --- /dev/null +++ b/packages/vehicle-manufacture-network/test/script.js @@ -0,0 +1,399 @@ +/* + * 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. + */ + +'use strict'; + +const AdminConnection = require('composer-admin').AdminConnection; +const BusinessNetworkConnection = require('composer-client').BusinessNetworkConnection; +const BusinessNetworkDefinition = require('composer-common').BusinessNetworkDefinition; +const IdCard = require('composer-common').IdCard; +const MemoryCardStore = require('composer-common').MemoryCardStore; +const path = require('path'); + +require('chai').should(); + +const namespace = 'org.acme.vehicle_network'; +const orderId = '1000-1000-1000-1000'; +const vin = '1a2b3c4d5e6f7g8h9'; + +describe('Manufacture network', () => { + const cardStore = new MemoryCardStore(); + let adminConnection; + let businessNetworkConnection; + let factory; + + before(() => { + // Embedded connection used for local testing + const connectionProfile = { + name: 'embedded', + type: 'embedded' + }; + // Embedded connection does not need real credentials + const credentials = { + certificate: 'FAKE CERTIFICATE', + privateKey: 'FAKE PRIVATE KEY' + }; + + // 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 }); + + return adminConnection.importCard(deployerCardName, deployerCard).then(() => { + return adminConnection.connect(deployerCardName); + }); + }); + + beforeEach(() => { + businessNetworkConnection = new BusinessNetworkConnection({ cardStore: cardStore }); + + const adminUserName = 'admin'; + let adminCardName; + let businessNetworkDefinition; + + return BusinessNetworkDefinition.fromDirectory(path.resolve(__dirname, '..')).then(definition => { + businessNetworkDefinition = definition; + // Install the Composer runtime for the new business network + return adminConnection.install(businessNetworkDefinition.getName()); + }).then(() => { + // Start the business network and configure an network admin identity + const startOptions = { + networkAdmins: [ + { + userName: adminUserName, + enrollmentSecret: 'adminpw' + } + ] + }; + return adminConnection.start(businessNetworkDefinition, startOptions); + }).then(adminCards => { + // Import the network admin identity for us to use + adminCardName = `${adminUserName}@${businessNetworkDefinition.getName()}`; + return adminConnection.importCard(adminCardName, adminCards.get(adminUserName)); + }).then(() => { + // Connect to the business network using the network admin identity + return businessNetworkConnection.connect(adminCardName); + }).then(() => { + factory = businessNetworkConnection.getBusinessNetwork().getFactory(); + }); + }); + + describe('PlaceOrder', () => { + it('should be able to place an order', () => { + + // create the orderer + const orderer = factory.newResource(namespace, 'Person', 'Andy'); + + // create the manufacturer + const manufacturer = factory.newResource(namespace, 'Manufacturer', 'Arium'); + manufacturer.name = 'Arium'; + + // create the vehicle details for the order + const vehicleDetails = factory.newConcept(namespace, 'VehicleDetails'); + vehicleDetails.make = factory.newRelationship(namespace, 'Manufacturer', manufacturer.$identifier); + vehicleDetails.modelType = 'nova'; + vehicleDetails.colour = 'statement blue'; + + // create the order options + const options = factory.newConcept(namespace, 'Options'); + options.trim = 'executive'; + options.interior = 'rotor grey'; + options.extras = ['tinted windows', 'extended warranty']; + + // create the transactionObject + + const placeOrderTx = factory.newTransaction(namespace, 'PlaceOrder'); + placeOrderTx.orderId = orderId; + placeOrderTx.vehicleDetails = vehicleDetails; + placeOrderTx.options = options; + placeOrderTx.orderer = factory.newRelationship(namespace, 'Person', orderer.$identifier); + + return businessNetworkConnection.getParticipantRegistry(namespace + '.Person') + .then((personRegistry) => { + return personRegistry.add(orderer); + }) + .then(() => { + return businessNetworkConnection.getParticipantRegistry(namespace + '.Manufacturer'); + }) + .then((manufacturerRegistry) => { + return manufacturerRegistry.add(manufacturer); + }) + .then(() => { + return businessNetworkConnection.submitTransaction(placeOrderTx); + }) + .then(() => { + return businessNetworkConnection.getAssetRegistry(namespace + '.Order'); + }) + .then((orderRegistry) => { + return orderRegistry.get(placeOrderTx.orderId); + }) + .then((order) => { + order.orderId.should.deep.equal(placeOrderTx.orderId); + order.vehicleDetails.should.deep.equal(vehicleDetails); + order.options.should.deep.equal(options); + order.orderStatus.should.deep.equal('PLACED'); + order.orderer.should.deep.equal(placeOrderTx.orderer); + }); + }); + }); + + describe('UpdateOrderStatus', () => { + let order; + let orderer; + let manufacturer; + let vehicleDetails; + beforeEach(() => { + // create the orderer + orderer = factory.newResource(namespace, 'Person', 'Andy'); + + // create the manufacturer + manufacturer = factory.newResource(namespace, 'Manufacturer', 'Arium'); + manufacturer.name = 'Arium'; + + // create the vehicle details for the order + vehicleDetails = factory.newConcept(namespace, 'VehicleDetails'); + vehicleDetails.make = factory.newRelationship(namespace, 'Manufacturer', manufacturer.$identifier); + vehicleDetails.modelType = 'nova'; + vehicleDetails.colour = 'statement blue'; + + // create the order options + const options = factory.newConcept(namespace, 'Options'); + options.trim = 'executive'; + options.interior = 'rotor grey'; + options.extras = ['tinted windows', 'extended warranty']; + + // create the order to update + order = factory.newResource(namespace, 'Order', orderId); + order.vehicleDetails = vehicleDetails; + order.options = options; + order.orderStatus = 'PLACED'; + order.orderer = factory.newRelationship(namespace, 'Person', orderer.$identifier); + + return businessNetworkConnection.getParticipantRegistry(namespace + '.Person') + .then((personRegistry) => { + return personRegistry.add(orderer); + }) + .then(() => { + return businessNetworkConnection.getParticipantRegistry(namespace + '.Manufacturer'); + }) + .then((manufacturerRegistry) => { + return manufacturerRegistry.add(manufacturer); + }) + .then(() => { + return businessNetworkConnection.getAssetRegistry(namespace + '.Order'); + }) + .then((orderRegistry) => { + return orderRegistry.add(order); + }); + + }); + + it('should update the status of the order to SCHEDULED_FOR_MANUFACTURE', () => { + const updateOrderStatusTx = factory.newTransaction(namespace, 'UpdateOrderStatus'); + updateOrderStatusTx.orderStatus = 'SCHEDULED_FOR_MANUFACTURE'; + updateOrderStatusTx.order = factory.newRelationship(namespace, 'Order', order.$identifier); + + return businessNetworkConnection.submitTransaction(updateOrderStatusTx) + .then(() => { + return businessNetworkConnection.getAssetRegistry(namespace + '.Order'); + }) + .then((orderRegistry) => { + return orderRegistry.get(orderId); + }) + .then((order) => { + order.orderStatus.should.deep.equal(updateOrderStatusTx.orderStatus); + }); + }); + + it('should update the status of the order to VIN_ASSIGNED and create a Vehicle', () => { + const updateOrderStatusTx = factory.newTransaction(namespace, 'UpdateOrderStatus'); + updateOrderStatusTx.orderStatus = 'VIN_ASSIGNED'; + updateOrderStatusTx.order = factory.newRelationship(namespace, 'Order', order.$identifier); + updateOrderStatusTx.vin = vin; + + return businessNetworkConnection.submitTransaction(updateOrderStatusTx) + .then(() => { + return businessNetworkConnection.getAssetRegistry(namespace + '.Order'); + }) + .then((orderRegistry) => { + return orderRegistry.get(orderId); + }) + .then((order) => { + order.orderStatus.should.deep.equal(updateOrderStatusTx.orderStatus); + return businessNetworkConnection.getAssetRegistry(namespace + '.Vehicle'); + }) + .then((vehicleRegistry) => { + return vehicleRegistry.get(vin); + }) + .then((vehicle) => { + vehicle.vin.should.deep.equal(vin); + vehicle.vehicleDetails.should.deep.equal(vehicleDetails); + vehicle.vehicleStatus.should.deep.equal('OFF_THE_ROAD'); + }); + }); + + it('should update the status of the order to OWNER_ASSIGNED and update the Vehicle', () => { + const updateOrderStatusTx = factory.newTransaction(namespace, 'UpdateOrderStatus'); + updateOrderStatusTx.orderStatus = 'OWNER_ASSIGNED'; + updateOrderStatusTx.order = factory.newRelationship(namespace, 'Order', order.$identifier); + updateOrderStatusTx.vin = vin; + + const vehicle = factory.newResource(namespace, 'Vehicle', vin); + vehicle.vehicleDetails = vehicleDetails; + vehicle.vehicleStatus = 'OFF_THE_ROAD'; + + return businessNetworkConnection.getAssetRegistry(namespace + '.Vehicle') + .then((vehicleRegistry) => { + return vehicleRegistry.add(vehicle); + }) + .then(() => { + return businessNetworkConnection.submitTransaction(updateOrderStatusTx); + }) + .then(() => { + return businessNetworkConnection.getAssetRegistry(namespace + '.Order'); + }) + .then((orderRegistry) => { + return orderRegistry.get(orderId); + }) + .then((order) => { + order.orderStatus.should.deep.equal(updateOrderStatusTx.orderStatus); + return businessNetworkConnection.getAssetRegistry(namespace + '.Vehicle'); + }) + .then((vehicleRegistry) => { + return vehicleRegistry.get(vin); + }) + .then((vehicle) => { + vehicle.vin.should.deep.equal(vin); + vehicle.vehicleStatus.should.deep.equal('ACTIVE'); + vehicle.owner.should.deep.equal(order.orderer); + }); + }); + + it('should update the status of the order to DELIVERED', () => { + const updateOrderStatusTx = factory.newTransaction(namespace, 'UpdateOrderStatus'); + updateOrderStatusTx.orderStatus = 'DELIVERED'; + updateOrderStatusTx.order = factory.newRelationship(namespace, 'Order', order.$identifier); + + return businessNetworkConnection.submitTransaction(updateOrderStatusTx) + .then(() => { + return businessNetworkConnection.getAssetRegistry(namespace + '.Order'); + }) + .then((orderRegistry) => { + return orderRegistry.get(orderId); + }) + .then((order) => { + order.orderStatus.should.deep.equal(updateOrderStatusTx.orderStatus); + }); + }); + + it('should throw an error if the vin is not passed when orderStatus set to VIN_ASSIGNED', (done) => { + const updateOrderStatusTx = factory.newTransaction(namespace, 'UpdateOrderStatus'); + updateOrderStatusTx.orderStatus = 'VIN_ASSIGNED'; + updateOrderStatusTx.order = factory.newRelationship(namespace, 'Order', order.$identifier); + + businessNetworkConnection.submitTransaction(updateOrderStatusTx) + .then(() => { + done(new Error('Expected method to reject')); + }) + .catch((err) => { + err.message.should.deep.equal('Value for VIN was expected'); + done(); + }) + .catch(done); + }); + + it('should throw an error if the vin is not passed when orderStatus set to OWNER_ASSIGNED', (done) => { + const updateOrderStatusTx = factory.newTransaction(namespace, 'UpdateOrderStatus'); + updateOrderStatusTx.orderStatus = 'OWNER_ASSIGNED'; + updateOrderStatusTx.order = factory.newRelationship(namespace, 'Order', order.$identifier); + + businessNetworkConnection.submitTransaction(updateOrderStatusTx) + .then(() => { + done(new Error('Expected method to reject')); + }) + .catch((err) => { + err.message.should.deep.equal('Value for VIN was expected'); + done(); + }) + .catch(done); + }); + }); + + describe('SetupDemo', () => { + it('should add specified participants and vehicles for the demo scenario', () => { + const setupDemoTx = factory.newTransaction(namespace, 'SetupDemo'); + + var expectedPeopleNames = ['Paul', 'Andy', 'Hannah', 'Sam', 'Caroline', 'Matt', 'Fenglian', 'Mark', 'James', 'Dave', 'Rob', 'Kai', 'Ellis', 'LesleyAnn']; + var expectedPeople = expectedPeopleNames.map((expectedPerson) => { + return factory.newResource(namespace, 'Person', expectedPerson); + }); + + var expectedManufacturerNames = ['Arium', 'Morde', 'Ridge']; + var expectedManufacturers = expectedManufacturerNames.map((expectedManufacturer) => { + let manufacturer = factory.newResource(namespace, 'Manufacturer', expectedManufacturer); + manufacturer.name = expectedManufacturer; + return manufacturer; + }); + + var expectedVins = ['ea290d9f5a6833a65', '39fd242c2bbe80f11', '835125e50bca37ca1', '0812e6d8d486e0464', 'c4aa418f26d4a0403', '7382fbfc083f696e5', '01a9cd3f8f5db5ef7', '97f305df4c2881e71', 'af462063fb901d0e6', '3ff3395ecfd38f787', 'de701fcf2a78d8086', '2fcdd7b5131e81fd0', '79540e5384c970321']; + + return businessNetworkConnection.submitTransaction(setupDemoTx) + .then(() => { + return businessNetworkConnection.getParticipantRegistry(namespace + '.Person'); + }) + .then((personRegistry) => { + return personRegistry.getAll(); + }) + .then((people) => { + people.length.should.deep.equal(14); + people.forEach((person) => { + expectedPeople.should.include(person); + }); + }) + .then(() => { + return businessNetworkConnection.getParticipantRegistry(namespace + '.Manufacturer'); + }) + .then((manufacturerRegistry) => { + return manufacturerRegistry.getAll(); + }) + .then((manufacturers) => { + manufacturers.length.should.deep.equal(3); + manufacturers.forEach((manufacturer) => { + expectedManufacturers.should.include(manufacturer); + }); + }) + .then(() => { + return businessNetworkConnection.getAssetRegistry(namespace + '.Vehicle'); + }) + .then((vehicleRegistry) => { + return vehicleRegistry.getAll(); + }) + .then((vehicles) => { + vehicles.length.should.deep.equal(13); + vehicles.forEach((vehicle) => { + expectedVins.should.include(vehicle.vin); + expectedManufacturerNames.should.include(vehicle.vehicleDetails.make.$identifier); + expectedPeopleNames.slice(1, 14).should.include(vehicle.owner.$identifier); // paul shouldn't have a vehicle + }); + }); + }); + }); +}); \ No newline at end of file