Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vis backend pricing model #1457

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion Agents/VisBackendAgent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ The Vis-Backend Agent is a supporting service to The World Avatar's [visualisati
- [2.6.4 Service commencement route](#264-service-commencement-route)
- [2.6.5 Service order route](#265-service-order-route)
- [2.6.6 Archive contract route](#266-archive-contract-route)
- [2.6.7 Pricing route](#267-pricing-route)
- [3. SHACL Restrictions](#3-shacl-restrictions)
- [3.1 Form Generation](#31-form-generation)
- [3.2 Automated Data Retrieval](#32-automated-data-retrieval)
Expand Down Expand Up @@ -621,6 +622,26 @@ Users must send a `POST` request to terminate an ongoing contract at the `<baseU

A successful request will return `{"message": "Contract has been successfully terminated!", "iri" : "root iri that is instantiated"}`.

#### 2.6.7 Pricing route

Users can send a `PUT` request to the `<baseURL>/vis-backend-agent/contracts/pricing` endpoint to set a new pricing model. Note that this will override any pricing model that was previously set. This route does require the following `JSON` request parameters:

```json
{
/* parameters */
"contract": "The target contract IRI",
"base fee": "The base fee in the decimal format ie 1.00",
"unit price": [{
"rate": "The rate charged per service metric in the decimal format",
"lowerBound": "The minimum value within the range of a service metric where the specified rate applies",
"upperBound": "The maximum value within the range of a service metric where the specified rate applies"
},...]
}
```

> [!IMPORTANT]
> The `unit price` parameter is optional and can hold an array of the same JSON object. The final `upperBound` field can be left empty or null to indicate that the rate applies for any excess of the `lowerBound` service metric.

## 3. SHACL Restrictions

[SHACL](https://www.w3.org/TR/shacl/) is generally a language for validating RDF graphs against a set of conditions. The World Avatar incorporates these restrictions into our workflow to populate form structure and fields, as well as enabling automated data retrieval.
Expand Down Expand Up @@ -799,7 +820,13 @@ It is expected that we should create a new ID and name for the person instance.
}
```

A sample file can be found at `./resources/example.jsonld`. It is recommended for users to first generate a valid schema using the [`JSON-LD` Playground](https://json-ld.org/playground/), and then replace the target literal or IRIs with the replacement object specified above. This validates the `JSON-LD` schema and ensure a consistent schema is adopted. **WARNING:** Please do not use short prefixes and include the full IRI throughout the schema in order for the agent to function as expected. This can be ensured by removing the "@context" field, which defines these prefixes.
A sample file can be found at `./resources/example.jsonld`. It is recommended for users to first generate a valid schema using the [`JSON-LD` Playground](https://json-ld.org/playground/), and then replace the target literal or IRIs with the replacement object specified above. This validates the `JSON-LD` schema and ensure a consistent schema is adopted.

> [!CAUTION]
> Please do not use short prefixes and include the full IRI throughout the schema in order for the agent to function as expected. This can be ensured by removing the "@context" field, which defines these prefixes.

> [!NOTE]
> There are additional `@replace` options such as `schedule` and `pricing` that will be used by default in the agent, but it is not intended for users and should be ignored.

#### 4.1.1 Service lifecycle

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,7 @@ public ResponseEntity<?> commenceContract(@RequestBody Map<String, Object> param
}
this.lifecycleService.addOccurrenceParams(params, LifecycleEventType.APPROVED);
response = this.addService.instantiate(
LifecycleResource.OCCURRENCE_INSTANT_RESOURCE,
params);
LifecycleResource.OCCURRENCE_INSTANT_RESOURCE, params);
if (response.getStatusCode() == HttpStatus.CREATED) {
LOGGER.info("Contract has been approved for service execution!");
return new ResponseEntity<>("Contract has been approved for service execution!", HttpStatus.OK);
Expand Down Expand Up @@ -236,8 +235,7 @@ public ResponseEntity<ApiResponse> cancelService(@RequestBody Map<String, Object
LOGGER.info("Received request to cancel the upcoming service...");
this.lifecycleService.addOccurrenceParams(params, LifecycleEventType.SERVICE_CANCELLATION);
ResponseEntity<ApiResponse> response = this.addService.instantiate(
LifecycleResource.OCCURRENCE_INSTANT_RESOURCE,
params);
LifecycleResource.OCCURRENCE_INSTANT_RESOURCE, params);
if (response.getStatusCode() == HttpStatus.CREATED) {
// If successfully added, remove the active service occurrence
String occurrenceIri = this.lifecycleService
Expand All @@ -264,8 +262,7 @@ public ResponseEntity<ApiResponse> rescindContract(@RequestBody Map<String, Obje
LOGGER.info("Received request to rescind the contract...");
this.lifecycleService.addOccurrenceParams(params, LifecycleEventType.ARCHIVE_RESCINDMENT);
ResponseEntity<ApiResponse> response = this.addService.instantiate(
LifecycleResource.OCCURRENCE_INSTANT_RESOURCE,
params);
LifecycleResource.OCCURRENCE_INSTANT_RESOURCE, params);
if (response.getStatusCode() == HttpStatus.CREATED) {
LOGGER.info("Contract has been successfully rescinded!");
return new ResponseEntity<>(new ApiResponse("Contract has been successfully rescinded!"), HttpStatus.OK);
Expand All @@ -287,8 +284,7 @@ public ResponseEntity<ApiResponse> terminateContract(@RequestBody Map<String, Ob
LOGGER.info("Received request to terminate the contract...");
this.lifecycleService.addOccurrenceParams(params, LifecycleEventType.ARCHIVE_TERMINATION);
ResponseEntity<ApiResponse> response = this.addService.instantiate(
LifecycleResource.OCCURRENCE_INSTANT_RESOURCE,
params);
LifecycleResource.OCCURRENCE_INSTANT_RESOURCE, params);
if (response.getStatusCode() == HttpStatus.CREATED) {
LOGGER.info("Contract has been successfully terminated!");
return new ResponseEntity<>(new ApiResponse("Contract has been successfully terminated!"), HttpStatus.OK);
Expand All @@ -310,8 +306,7 @@ public ResponseEntity<ApiResponse> updateDraftContract(@RequestBody Map<String,
// Add current date into parameters
params.put(LifecycleResource.CURRENT_DATE_KEY, this.dateTimeService.getCurrentDate());
ResponseEntity<ApiResponse> addResponse = this.addService.instantiate(
LifecycleResource.LIFECYCLE_RESOURCE, targetId,
params);
LifecycleResource.LIFECYCLE_RESOURCE, targetId, params);
if (addResponse.getStatusCode() == HttpStatus.CREATED) {
LOGGER.info("The lifecycle of the contract has been successfully updated!");
// Execute request for schedule as well
Expand Down Expand Up @@ -343,8 +338,7 @@ public ResponseEntity<ApiResponse> updateContractSchedule(@RequestBody Map<Strin
targetId);
if (deleteResponse.getStatusCode().equals(HttpStatus.OK)) {
ResponseEntity<ApiResponse> addResponse = this.addService.instantiate(LifecycleResource.SCHEDULE_RESOURCE,
targetId,
params);
targetId, params);
if (addResponse.getStatusCode() == HttpStatus.CREATED) {
LOGGER.info("Draft schedule has been successfully updated!");
return new ResponseEntity<>(
Expand All @@ -358,6 +352,36 @@ public ResponseEntity<ApiResponse> updateContractSchedule(@RequestBody Map<Strin
}
}

/**
* Update the pricing model for the target contract in the knowledge graph.
*/
@PutMapping("/contracts/pricing")
public ResponseEntity<ApiResponse> updatePricingModel(@RequestBody Map<String, Object> params) {
LOGGER.info("Received request to set the pricing model...");
if (this.isInvalidParams(params, LifecycleResource.CONTRACT_KEY)) {
return new ResponseEntity<>(
new ApiResponse(MessageFormat.format(MISSING_FIELD_MSG_TEMPLATE, LifecycleResource.CONTRACT_KEY)),
HttpStatus.BAD_REQUEST);
}
String targetId = params.get("id").toString();
ResponseEntity<ApiResponse> deleteResponse = this.deleteService.delete(LifecycleResource.PRICING_RESOURCE,
targetId);
if (deleteResponse.getStatusCode().equals(HttpStatus.OK)) {
ResponseEntity<ApiResponse> addResponse = this.addService.instantiate(LifecycleResource.PRICING_RESOURCE,
targetId, params);
if (addResponse.getStatusCode() == HttpStatus.CREATED) {
LOGGER.info("Pricing model has been successfully set!");
return new ResponseEntity<>(
new ApiResponse("Pricing model has been successfully set!", addResponse.getBody().getIri()),
HttpStatus.OK);
} else {
return addResponse;
}
} else {
return deleteResponse;
}
}

/**
* Retrieve the status of the contract
*/
Expand Down Expand Up @@ -492,6 +516,16 @@ public ResponseEntity<?> getContractTerminationForm() {
return this.lifecycleService.getForm(LifecycleEventType.ARCHIVE_TERMINATION, null);
}

/**
* Retrieves a status to indicate if the pricing model has been instantiated for
* the specified contract.
*/
@GetMapping("/contracts/pricing/{id}")
public ResponseEntity<?> getPricingStatus(@PathVariable String id) {
LOGGER.info("Received request to get form template to rescind the contract...");
return null;
}

/**
* Validate if the request parameters are invalid or not. Returns true if
* invalid.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,10 @@ private void recursiveReplacePlaceholders(ObjectNode currentNode, ObjectNode par
// Add a different interaction for calculations
} else if (currentNode.path(ShaclResource.REPLACE_KEY).asText().equals("calculation")) {
this.lifecycleReportService.appendCalculationRecord(parentNode, currentNode, replacements);
// Add a different interaction for pricing model
} else if (currentNode.path(ShaclResource.REPLACE_KEY).asText().equals("pricing")) {
ObjectNode pricingModel = this.lifecycleReportService.genPricingModel(replacements);
parentNode.set(parentField, pricingModel);
// Parse literal with data types differently
} else if (currentNode.path(ShaclResource.TYPE_KEY).asText().equals("literal")
&& currentNode.has(ShaclResource.DATA_TYPE_PROPERTY)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.cmclinnovations.agent.service.application;

import java.util.List;
import java.util.Map;

import org.apache.logging.log4j.LogManager;
Expand Down Expand Up @@ -28,6 +29,15 @@ public class LifecycleReportService {
private final LoggingService loggingService;
private final ObjectMapper objectMapper;

private static final String FLAT_FEE_LABEL = "Base Fee";
private static final String UNIT_PRICE_LABEL = "unit price";
private static final String RATE_LABEL = "rate";
private static final String LOWER_BOUND_LABEL = "lowerBound";
private static final String UPPER_BOUND_LABEL = "upperBound";

private static final String PRICING_MODEL_PREFIX = "https://www.theworldavatar.io/kg/agreement/pricing/";
private static final String MONETARY_PRICE_PREFIX = "https://www.theworldavatar.io/kg/agreement/money/";
private static final String VARIABLE_FEE_PREFIX = "https://www.theworldavatar.io/kg/agreement/variable/money/";
private static final String LIFECYCLE_REPORT_PREFIX = "https://www.theworldavatar.io/kg/lifecycle/report/";
private static final String LIFECYCLE_RECORD_PREFIX = "https://www.theworldavatar.io/kg/lifecycle/record/";

Expand Down Expand Up @@ -82,7 +92,8 @@ public void appendCalculationRecord(ObjectNode parentNode, ObjectNode calculatio
params);
parentNode.set(LifecycleResource.SUCCEEDS_RELATIONS, calculationInstance);
// Generate record instance
ObjectNode recordInstance = this.genRecordInstance();
ObjectNode recordInstance = this.jsonLdService.genInstance(LIFECYCLE_RECORD_PREFIX,
LifecycleResource.LIFECYCLE_RECORD);
// Retrieve and associate output value in calculation
recordInstance.set(LifecycleResource.RECORDS_RELATIONS,
calculationInstance.get(LifecycleResource.HAS_QTY_VAL_RELATIONS));
Expand All @@ -98,10 +109,27 @@ public void appendCalculationRecord(ObjectNode parentNode, ObjectNode calculatio
}

/**
* Generates a record instance.
* Generates a pricing model JSON node.
*
* @param params Mappings of the parameters and their values.
*/
private ObjectNode genRecordInstance() {
return this.jsonLdService.genInstance(LIFECYCLE_RECORD_PREFIX, LifecycleResource.LIFECYCLE_RECORD);
public ObjectNode genPricingModel(Map<String, Object> params) {
ArrayNode arguments = this.objectMapper.createArrayNode();
ObjectNode pricingModel = this.jsonLdService.genInstance(PRICING_MODEL_PREFIX, LifecycleResource.PRICING_MODEL);
ObjectNode flatFee = this.jsonLdService.genInstance(MONETARY_PRICE_PREFIX, LifecycleResource.MONETARY_PRICE,
FLAT_FEE_LABEL);
flatFee.put(LifecycleResource.HAS_AMOUNT_RELATIONS,
Double.parseDouble(params.get(FLAT_FEE_LABEL.toLowerCase()).toString()));
arguments.add(flatFee);
if (params.containsKey(UNIT_PRICE_LABEL)) {
List<Map<String, Object>> unitPrices = (List<Map<String, Object>>) params.get(UNIT_PRICE_LABEL);
for (Map<String, Object> unitPrice : unitPrices) {
ObjectNode currentNode = this.genUnitPrice(unitPrice);
arguments.add(currentNode);
}
}
pricingModel.set(LifecycleResource.HAS_ARGUMENT_RELATIONS, arguments);
return pricingModel;
}

/**
Expand Down Expand Up @@ -282,4 +310,23 @@ private ObjectNode genScalarQuantityNode(String prefix, ObjectNode scalarQuantit
this.jsonLdService.genLiteral(String.valueOf(value), "http://www.w3.org/2001/XMLSchema#decimal"));
return quantity;
}

/**
* Generates a unit price instance based on the input parameters.
*
* @param params Mappings of the required parameters and their values.
*/
private ObjectNode genUnitPrice(Map<String, Object> params) {
ObjectNode unitPriceNode = this.jsonLdService.genInstance(VARIABLE_FEE_PREFIX,
LifecycleResource.VARIABLE_FEE);
unitPriceNode.put(LifecycleResource.HAS_AMOUNT_RELATIONS,
Double.parseDouble(params.get(RATE_LABEL).toString()));
unitPriceNode.put(LifecycleResource.HAS_LOWER_BOUND_RELATIONS,
Double.parseDouble(params.get(LOWER_BOUND_LABEL).toString()));
if (params.get(UPPER_BOUND_LABEL) != null) {
unitPriceNode.put(LifecycleResource.HAS_UPPER_BOUND_RELATIONS,
Double.parseDouble(params.get(UPPER_BOUND_LABEL).toString()));
}
return unitPriceNode;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public class FileService {
public static final String LIFECYCLE_JSON_LD_RESOURCE = CLASS_PATH_DIR + "jsonld/lifecycle.jsonld";
public static final String OCCURRENCE_INSTANT_JSON_LD_RESOURCE = CLASS_PATH_DIR + "jsonld/occurrence_instant.jsonld";
public static final String SCHEDULE_JSON_LD_RESOURCE = CLASS_PATH_DIR + "jsonld/schedule.jsonld";
public static final String PRICING_JSON_LD_RESOURCE = CLASS_PATH_DIR + "jsonld/pricing.jsonld";

public static final String REPLACEMENT_TARGET = "[target]";
public static final String REPLACEMENT_SHAPE = "[shape]";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
public class JsonLdService {
private final ObjectMapper objectMapper;

private static final String RDFS_LABEL = "http://www.w3.org/2000/01/rdf-schema#label";
private static final Logger LOGGER = LogManager.getLogger(JsonLdService.class);

/**
Expand Down Expand Up @@ -68,6 +69,18 @@ public String getReplacementValue(ObjectNode replacementNode, Map<String, Object
}
}

/**
* Generates a instance object node with a readable label.
*
* @param prefix IRI prefix.
* @param concept The ontology concept class/type for the instance.
* @param label The label for the instance.
*/
public ObjectNode genInstance(String prefix, String concept, String label) {
return this.genInstance(prefix, concept)
.put(RDFS_LABEL, label);
}

/**
* Generates a instance object node.
*
Expand Down
Loading