From 2e8d7ad78c7fd30c32643f13c49b5c0fe1838d6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Ko=C5=82odziejczyk?= Date: Tue, 6 Aug 2024 11:37:23 +0200 Subject: [PATCH] [NU-1739] Add OpenAPI mocks and sample "Customer Offer Based On Activity Event" scenario (#184) --- designer/application-customizations.conf | 12 + docker-compose.yml | 15 + mocks/Dockerfile | 30 ++ mocks/Readme.md | 6 + mocks/http-service/mocks/__files/.gitkeep | 0 .../mocks/__files/openapi/CustomerApi.yaml | 106 +++++ .../customer-api/CustomerProfile.json | 10 + .../customer-api/CustomerTypeOffers0.json | 1 + .../customer-api/CustomerTypeOffers1.json | 9 + .../customer-api/CustomerTypeOffers2.json | 16 + mocks/http-service/mocks/mappings/. gitkeep | 0 .../customer-api/GetCustomerProfile.json | 13 + .../customer-api/GetOffersForCusomerType.json | 26 + mocks/http-service/scripts/run-wiremock.sh | 10 + .../kafka/generate-messages/customerEvents.sh | 10 + quickstart-setup/data/kafka/topics.txt | 1 + quickstart-setup/scripts/utils/lib.sh | 32 +- quickstart-setup/setup/kafka/topics.txt | 2 + quickstart-setup/setup/nu/examples.txt | 1 + ...rCustomerProposalBasedOnActivityEvent.json | 446 ++++++++++++++++++ .../setup/schema-registry/active-schemas.txt | 2 + .../schemas/CustomerEvents.schema.json | 26 + ...ProposalsBasedOnCustomerEvents.schema.json | 49 ++ 23 files changed, 817 insertions(+), 6 deletions(-) create mode 100644 mocks/Dockerfile create mode 100644 mocks/Readme.md create mode 100644 mocks/http-service/mocks/__files/.gitkeep create mode 100644 mocks/http-service/mocks/__files/openapi/CustomerApi.yaml create mode 100644 mocks/http-service/mocks/__files/responses/customer-api/CustomerProfile.json create mode 100644 mocks/http-service/mocks/__files/responses/customer-api/CustomerTypeOffers0.json create mode 100644 mocks/http-service/mocks/__files/responses/customer-api/CustomerTypeOffers1.json create mode 100644 mocks/http-service/mocks/__files/responses/customer-api/CustomerTypeOffers2.json create mode 100644 mocks/http-service/mocks/mappings/. gitkeep create mode 100644 mocks/http-service/mocks/mappings/customer-api/GetCustomerProfile.json create mode 100644 mocks/http-service/mocks/mappings/customer-api/GetOffersForCusomerType.json create mode 100755 mocks/http-service/scripts/run-wiremock.sh create mode 100755 quickstart-setup/data/kafka/generate-messages/customerEvents.sh create mode 100644 quickstart-setup/setup/nu/scenarios/OfferCustomerProposalBasedOnActivityEvent.json create mode 100644 quickstart-setup/setup/schema-registry/schemas/CustomerEvents.schema.json create mode 100644 quickstart-setup/setup/schema-registry/schemas/OfferProposalsBasedOnCustomerEvents.schema.json diff --git a/designer/application-customizations.conf b/designer/application-customizations.conf index 1ab22be0..84f92b49 100644 --- a/designer/application-customizations.conf +++ b/designer/application-customizations.conf @@ -3,6 +3,18 @@ scenarioTypes { "streaming" { # customize Flink streaming scenario type + + modelConfig: { + components { + "customerProfileOffers" { + providerType: "openAPI" + url: "http://mocks:8080/__admin/files/openapi/CustomerApi.yaml" + rootUrl: "http://mocks:8080/" + namePattern: "get.*" + allowedMethods: ["GET"] + } + } + } } "streaming-lite-embedded" { # customize Lite streaming scenario type diff --git a/docker-compose.yml b/docker-compose.yml index 39a4c022..8e0fb57a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,6 +15,19 @@ services: limits: memory: 128M + ### Quickstart mocks + mocks: + build: + context: mocks/ + dockerfile: Dockerfile + # todo added temporary + ports: + - 18080:8080 + deploy: + resources: + limits: + memory: 256M + ### Nussknacker-related services nginx: @@ -71,6 +84,8 @@ services: condition: service_healthy flink-taskmanager: condition: service_started + mocks: + condition: service_healthy expose: - 8181 healthcheck: diff --git a/mocks/Dockerfile b/mocks/Dockerfile new file mode 100644 index 00000000..e9d92cd6 --- /dev/null +++ b/mocks/Dockerfile @@ -0,0 +1,30 @@ +FROM holomekc/wiremock-gui:3.8.1 AS wiremock + +RUN apt-get update && \ + apt-get install -y wget && \ + wget -P /var/wiremock/extensions https://repo1.maven.org/maven2/org/wiremock/extensions/wiremock-faker-extension-standalone/0.2.0/wiremock-faker-extension-standalone-0.2.0.jar + +FROM phusion/baseimage:noble-1.0.0 + +# Use baseimage-docker's init system. +CMD ["/sbin/my_init"] + +# install +USER root + +RUN apt-get update -y && \ + apt -y install openjdk-11-jre-headless && \ + apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +# WIREMOCK +COPY --from=wiremock /var/wiremock /var/wiremock +COPY --from=wiremock /home/wiremock /home/wiremock + +COPY http-service/mocks /home/wiremock/ +COPY http-service/scripts /etc/service/http-service +RUN mv /etc/service/http-service/run-wiremock.sh /etc/service/http-service/run + +EXPOSE 8080 + +HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=30 \ + CMD (curl -f http://localhost:8080/__admin/) || exit 1 \ No newline at end of file diff --git a/mocks/Readme.md b/mocks/Readme.md new file mode 100644 index 00000000..55c3e079 --- /dev/null +++ b/mocks/Readme.md @@ -0,0 +1,6 @@ +# MOCKS + +## Resources: +https://github.com/wiremock/wiremock-faker-extension/blob/main/docs/reference.md +https://docs.wiremock.io/response-templating/basics/ +https://docs.wiremock.io/response-templating/dates-and-times/ \ No newline at end of file diff --git a/mocks/http-service/mocks/__files/.gitkeep b/mocks/http-service/mocks/__files/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/mocks/http-service/mocks/__files/openapi/CustomerApi.yaml b/mocks/http-service/mocks/__files/openapi/CustomerApi.yaml new file mode 100644 index 00000000..2dd082f2 --- /dev/null +++ b/mocks/http-service/mocks/__files/openapi/CustomerApi.yaml @@ -0,0 +1,106 @@ +openapi: 3.0.0 +info: + title: Customer API + version: 1.0.0 + description: API for retrieving customer profiles and offers based on customer type. + +paths: + '/customer/{customerId}/profile': + get: + summary: Get customer profile by customer's ID + description: Retrieve detailed profile information for a customer using their unique customer ID. + operationId: getCustomerProfile + parameters: + - name: customerId + in: path + required: true + description: The unique identifier of the customer. + schema: + type: string + responses: + '200': + description: Customer profile found successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/CustomerProfile' + '404': + description: No customer profile found for the given ID. + + '/customer/{customerType}/offers': + get: + summary: Get offers by customer type + description: Retrieve offers available for a specific type of customer. + operationId: getOffersForCustomerType + parameters: + - name: customerType + in: path + required: true + description: The type/category of the customer. + schema: + type: string + responses: + '200': + description: Offers found for the specified customer type. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/CustomerTypeOffer' + '404': + description: No offer found for the specified customer type. + +components: + schemas: + CustomerProfile: + title: CustomerProfile + type: object + description: Schema representing a customer's profile information. + properties: + id: + type: string + description: The unique identifier of the customer profile. + customerId: + type: string + description: The identifier of customer + customerType: + type: string + description: The type or category of the customer. + customerName: + type: string + description: The customer's name + customerMsisdn: + type: string + description: The customer's phone number + customerAge: + type: integer + description: The customer's age + customerSex: + type: string + description: The customer's sex + isPremiumCustomer: + type: boolean + description: Indicates if the customer is a premium one + + CustomerTypeOffer: + title: CustomerTypeOffer + type: object + description: Schema representing an offer available for a specific type of customer. + properties: + id: + type: string + description: The unique identifier of the offer. + name: + type: string + description: The name or title of the offer. + message: + type: string + description: A human-readable offer description + price: + type: integer + description: Price of the offer + validity: + type: string + format: date-time + description: The validity date of the offer. diff --git a/mocks/http-service/mocks/__files/responses/customer-api/CustomerProfile.json b/mocks/http-service/mocks/__files/responses/customer-api/CustomerProfile.json new file mode 100644 index 00000000..4c43f806 --- /dev/null +++ b/mocks/http-service/mocks/__files/responses/customer-api/CustomerProfile.json @@ -0,0 +1,10 @@ +{ + "id": "{{randomValue length=10 type='NUMERIC'}}", + "customerId": "{{request.path.[1]}}", + "customerType": "{{{pickRandom 'Freemium' 'Regular' 'VIP'}}}", + "customerName": "{{random 'Name.fullName'}}", + "customerMsisdn": "{{random 'PhoneNumber.phoneNumber'}}", + "customerAge": {{randomInt lower=10 upper=99}}, + "customerSex": "{{{pickRandom 'Male' 'Female' 'N/A'}}}", + "isPremiumCustomer": {{random 'Bool.bool'}} +} \ No newline at end of file diff --git a/mocks/http-service/mocks/__files/responses/customer-api/CustomerTypeOffers0.json b/mocks/http-service/mocks/__files/responses/customer-api/CustomerTypeOffers0.json new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/mocks/http-service/mocks/__files/responses/customer-api/CustomerTypeOffers0.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/mocks/http-service/mocks/__files/responses/customer-api/CustomerTypeOffers1.json b/mocks/http-service/mocks/__files/responses/customer-api/CustomerTypeOffers1.json new file mode 100644 index 00000000..704ceea2 --- /dev/null +++ b/mocks/http-service/mocks/__files/responses/customer-api/CustomerTypeOffers1.json @@ -0,0 +1,9 @@ +[ + { + "id": "{{randomValue length=10 type='NUMERIC'}}", + "name": "{{random 'Commerce.productName'}} promotion", + "message": "{{random 'Lorem.sentence'}} {{random 'Lorem.sentence'}} {{random 'Lorem.sentence'}}", + "price": {{randomInt lower=10 upper=50}}, + "validity": "{{dateFormat (now offset='3 days') format='yyyy-MM-dd\'T\'HH:mm:ss\'Z\''}}" + } +] \ No newline at end of file diff --git a/mocks/http-service/mocks/__files/responses/customer-api/CustomerTypeOffers2.json b/mocks/http-service/mocks/__files/responses/customer-api/CustomerTypeOffers2.json new file mode 100644 index 00000000..af6af793 --- /dev/null +++ b/mocks/http-service/mocks/__files/responses/customer-api/CustomerTypeOffers2.json @@ -0,0 +1,16 @@ +[ + { + "id": "{{randomValue length=10 type='NUMERIC'}}", + "name": "{{random 'Commerce.productName'}} promotion", + "message": "{{random 'Lorem.sentence'}} {{random 'Lorem.sentence'}} {{random 'Lorem.sentence'}}", + "price": {{randomInt lower=10 upper=50}}, + "validity": "{{dateFormat (now offset='3 days') format='yyyy-MM-dd\'T\'HH:mm:ss\'Z\''}}" + }, + { + "id": "{{randomValue length=10 type='NUMERIC'}}", + "name": "{{random 'Commerce.productName'}} promotion", + "message": "{{random 'Lorem.sentence'}} {{random 'Lorem.sentence'}} {{random 'Lorem.sentence'}}", + "price": {{randomInt lower=10 upper=50}}, + "validity": "{{dateFormat (now offset='3 days') format='yyyy-MM-dd\'T\'HH:mm:ss\'Z\''}}" + } +] \ No newline at end of file diff --git a/mocks/http-service/mocks/mappings/. gitkeep b/mocks/http-service/mocks/mappings/. gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/mocks/http-service/mocks/mappings/customer-api/GetCustomerProfile.json b/mocks/http-service/mocks/mappings/customer-api/GetCustomerProfile.json new file mode 100644 index 00000000..5907b234 --- /dev/null +++ b/mocks/http-service/mocks/mappings/customer-api/GetCustomerProfile.json @@ -0,0 +1,13 @@ +{ + "request": { + "urlPattern": "/customer/(.+)/profile", + "method": "GET" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "bodyFileName": "responses/customer-api/CustomerProfile.json" + } +} \ No newline at end of file diff --git a/mocks/http-service/mocks/mappings/customer-api/GetOffersForCusomerType.json b/mocks/http-service/mocks/mappings/customer-api/GetOffersForCusomerType.json new file mode 100644 index 00000000..3da0c8a4 --- /dev/null +++ b/mocks/http-service/mocks/mappings/customer-api/GetOffersForCusomerType.json @@ -0,0 +1,26 @@ +{ + "mappings": [ + { + "request": { + "urlPattern": "/customer/(.+)/offers", + "method": "GET" + }, + "response": { + "status": 404 + } + }, + { + "request": { + "urlPattern": "/customer/(?i)(Freemium|Regular|VIP)/offers", + "method": "GET" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "bodyFileName": "responses/customer-api/CustomerTypeOffers{{{pickRandom '0' '1' '2'}}}.json" + } + } + ] +} \ No newline at end of file diff --git a/mocks/http-service/scripts/run-wiremock.sh b/mocks/http-service/scripts/run-wiremock.sh new file mode 100755 index 00000000..a07592a2 --- /dev/null +++ b/mocks/http-service/scripts/run-wiremock.sh @@ -0,0 +1,10 @@ +#!/bin/sh -e + +java $JAVA_OPTS -cp /var/wiremock/lib/*:/var/wiremock/extensions/* wiremock.Run \ + --port=8080 \ + --root-dir=/home/wiremock \ + --max-request-journal=1000 \ + --global-response-templating \ + --extensions=org.wiremock.RandomExtension \ + --async-response-enable=true \ + --async-response-threads=30 \ No newline at end of file diff --git a/quickstart-setup/data/kafka/generate-messages/customerEvents.sh b/quickstart-setup/data/kafka/generate-messages/customerEvents.sh new file mode 100755 index 00000000..93f75408 --- /dev/null +++ b/quickstart-setup/data/kafka/generate-messages/customerEvents.sh @@ -0,0 +1,10 @@ +#!/bin/bash -e + +cd "$(dirname "$0")" + +source ../../../scripts/utils/lib.sh + +ID=$(random_Ndigit_number 10) +EVENT_TYPE="$(pick_randomly "ClientCloseToShowroom" "ClientBrowseOffers" "ClientEndedCallWithCustomerService" "ClientSentTerminationLetter" "Other")" + +echo "{ \"customerId\": \"$ID\", \"eventType\": \"$EVENT_TYPE\" }" \ No newline at end of file diff --git a/quickstart-setup/data/kafka/topics.txt b/quickstart-setup/data/kafka/topics.txt index 05a70cee..ff35d8eb 100644 --- a/quickstart-setup/data/kafka/topics.txt +++ b/quickstart-setup/data/kafka/topics.txt @@ -1,2 +1,3 @@ # Topic name the static and generated messages should be sent to Transactions +CustomerEvents diff --git a/quickstart-setup/scripts/utils/lib.sh b/quickstart-setup/scripts/utils/lib.sh index 00d7e439..c3803427 100755 --- a/quickstart-setup/scripts/utils/lib.sh +++ b/quickstart-setup/scripts/utils/lib.sh @@ -16,12 +16,32 @@ function verifyBashScript() { fi } +function random_Ndigit_number() { + if [ "$#" -ne 1 ]; then + echo "Error: One parameter required: 1) number of digits" + exit 1 + fi + + local LENGTH=$1 + local RESULT="" + + local FIRST_DIGIT=$((RANDOM % 9 + 1)) + RESULT+="$FIRST_DIGIT" + + while [ ${#RESULT} -lt $LENGTH ]; do + local REMAINING=$((LENGTH - ${#RESULT})) + local PART=$(printf "%05d" $((RANDOM % 100000))) + RESULT+=${PART:0:$REMAINING} + done + echo "$RESULT" +} + function random_4digit_number() { - od -An -t d -N 2 /dev/urandom | head -n 1 | tr -d ' ' | head -c 4 + random_Ndigit_number 4 } function random_3digit_number() { - random_4digit_number | head -c 3 + random_Ndigit_number 3 } function now() { @@ -29,8 +49,8 @@ function now() { } function pick_randomly() { - local options=("$@") - local count=${#options[@]} - local random_index=$((RANDOM % count)) - echo "${options[$random_index]}" + local OPTIONS=("$@") + local COUNT=${#OPTIONS[@]} + local RANDOM_INDEX=$((RANDOM % COUNT)) + echo "${OPTIONS[$RANDOM_INDEX]}" } \ No newline at end of file diff --git a/quickstart-setup/setup/kafka/topics.txt b/quickstart-setup/setup/kafka/topics.txt index adc69ddf..eb3bf04b 100644 --- a/quickstart-setup/setup/kafka/topics.txt +++ b/quickstart-setup/setup/kafka/topics.txt @@ -3,3 +3,5 @@ Customers ProcessedTransactions SmsesWithOffer Transactions +CustomerEvents +OfferProposalsBasedOnCustomerEvents diff --git a/quickstart-setup/setup/nu/examples.txt b/quickstart-setup/setup/nu/examples.txt index 3cb0625d..23797658 100644 --- a/quickstart-setup/setup/nu/examples.txt +++ b/quickstart-setup/setup/nu/examples.txt @@ -2,3 +2,4 @@ DetectLargeTransactions.json #DetermineOfferedPlan.json LoanRequest.json +OfferCustomerProposalBasedOnActivityEvent.json diff --git a/quickstart-setup/setup/nu/scenarios/OfferCustomerProposalBasedOnActivityEvent.json b/quickstart-setup/setup/nu/scenarios/OfferCustomerProposalBasedOnActivityEvent.json new file mode 100644 index 00000000..9bf82cd8 --- /dev/null +++ b/quickstart-setup/setup/nu/scenarios/OfferCustomerProposalBasedOnActivityEvent.json @@ -0,0 +1,446 @@ +{ + "metaData" : { + "id" : "OfferCustomerProposalBasedOnActivityEvent", + "additionalFields" : { + "description" : null, + "properties" : { + "parallelism" : "1", + "spillStateToDisk" : "true", + "useAsyncInterpretation" : "", + "checkpointIntervalInSeconds" : "" + }, + "metaDataType" : "StreamMetaData" + } + }, + "nodes" : [ + { + "id" : "Customers-related events", + "ref" : { + "typ" : "kafka", + "parameters" : [ + { + "name" : "Topic", + "expression" : { + "language" : "spel", + "expression" : "'CustomerEvents'" + } + }, + { + "name" : "Schema version", + "expression" : { + "language" : "spel", + "expression" : "'latest'" + } + } + ] + }, + "additionalFields" : { + "description" : null, + "layoutData" : { + "x" : 180, + "y" : 0 + } + }, + "type" : "Source" + }, + { + "nextFalse" : [ + ], + "id" : "May client be interested in the new offer?", + "expression" : { + "language" : "spel", + "expression" : "#input.eventType.toString == \"ClientBrowseOffers\" || \n#input.eventType.toString == \"ClientSentTerminationLetter\"" + }, + "isDisabled" : null, + "additionalFields" : { + "description" : "We are interested only in two types of events: \"ClientSentTerminationLetter\", \"ClientBrowseOffers\"", + "layoutData" : { + "x" : 180, + "y" : 180 + } + }, + "type" : "Filter" + }, + { + "id" : "Get Customer Profile from API", + "service" : { + "id" : "getCustomerProfile", + "parameters" : [ + { + "name" : "customerId", + "expression" : { + "language" : "spel", + "expression" : "#input.customerId" + } + } + ] + }, + "output" : "profile", + "additionalFields" : { + "description" : "HTTP service is called to get customer profile using the \"customerId\" value from the event", + "layoutData" : { + "x" : 180, + "y" : 360 + } + }, + "type" : "Enricher" + }, + { + "nextFalse" : [ + ], + "id" : "Is adult customer?", + "expression" : { + "language" : "spel", + "expression" : "#profile.customerAge > 18" + }, + "isDisabled" : null, + "additionalFields" : { + "description" : "We are not interested in non-adult customers", + "layoutData" : { + "x" : 180, + "y" : 540 + } + }, + "type" : "Filter" + }, + { + "id" : "Get Offers for a given Customer Type from API", + "service" : { + "id" : "getOffersForCustomerType", + "parameters" : [ + { + "name" : "customerType", + "expression" : { + "language" : "spel", + "expression" : "#profile.customerType" + } + } + ] + }, + "output" : "offers", + "additionalFields" : { + "description" : "HTTP service is called to get offers for a passed customer type (taken from the response from the previous HTTP service call)", + "layoutData" : { + "x" : 180, + "y" : 720 + } + }, + "type" : "Enricher" + }, + { + "nextFalse" : [ + ], + "id" : "At least one offer exists?", + "expression" : { + "language" : "spel", + "expression" : "#offers.size > 0" + }, + "isDisabled" : null, + "additionalFields" : { + "description" : "We can continue if at least one offer is returned", + "layoutData" : { + "x" : 180, + "y" : 900 + } + }, + "type" : "Filter" + }, + { + "id" : "Pick the best offer", + "varName" : "offer", + "value" : { + "language" : "spel", + "expression" : "#offers.?[price == #COLLECTION.min(#offers.![price])][0]" + }, + "additionalFields" : { + "description" : "The offer with the best price is taken", + "layoutData" : { + "x" : 180, + "y" : 1080 + } + }, + "type" : "Variable" + }, + { + "nextFalse" : [ + { + "id" : "Random 4% discount", + "varName" : "specialDiscountPercentage", + "value" : { + "language" : "spel", + "expression" : "#RANDOM.nextInt(0,1000) == 0 ? 4 : 0" + }, + "additionalFields" : { + "description" : "1/1000 events will receive a 4% discount", + "layoutData" : { + "x" : 0, + "y" : 1620 + } + }, + "type" : "Variable" + }, + { + "definition" : { + "id" : "Random 4% discount", + "joinId" : "Union" + }, + "type" : "BranchEndData" + } + ], + "id" : "Is premium customer?", + "expression" : { + "language" : "spel", + "expression" : "#profile.isPremiumCustomer" + }, + "isDisabled" : null, + "additionalFields" : { + "description" : "We will give premium customers different discounts than regular customers.", + "layoutData" : { + "x" : 180, + "y" : 1260 + } + }, + "type" : "Filter" + }, + { + "id" : "Special discount depending on day of week (decision table)", + "service" : { + "id" : "decision-table", + "parameters" : [ + { + "name" : "Decision Table", + "expression" : { + "language" : "tabularDataDefinition", + "expression" : "{\n \"rows\": [\n [\n \"MONDAY\",\n \"0\"\n ],\n [\n \"TUESDAY\",\n \"0\"\n ],\n [\n \"WEDNESDAY\",\n \"3\"\n ],\n [\n \"THURSDAY\",\n \"0\"\n ],\n [\n \"FRIDAY\",\n \"5\"\n ],\n [\n \"SATURDAY\",\n \"5\"\n ],\n [\n \"SUNDAY\",\n \"5\"\n ],\n [\n null,\n null\n ]\n ],\n \"columns\": [\n {\n \"name\": \"DayOfWeek\",\n \"type\": \"java.lang.String\"\n },\n {\n \"name\": \"DiscountPercentage\",\n \"type\": \"java.lang.Integer\"\n }\n ]\n}" + } + }, + { + "name" : "Match condition", + "expression" : { + "language" : "spel", + "expression" : "#ROW.DayOfWeek == #DATE.nowAtDefaultTimeZone.getDayOfWeek.toString.toUpperCase" + } + } + ] + }, + "output" : "specialDiscountPercentages", + "additionalFields" : { + "description" : "Customer receives 5% discount on weekends and 3% discount on Wednesdays", + "layoutData" : { + "x" : 360, + "y" : 1440 + } + }, + "type" : "Enricher" + }, + { + "id" : "Extract the special discount", + "varName" : "specialDiscountPercentage", + "value" : { + "language" : "spel", + "expression" : "#specialDiscountPercentages.isEmpty ? \n0 : #specialDiscountPercentages[0].DiscountPercentage" + }, + "additionalFields" : { + "description" : null, + "layoutData" : { + "x" : 360, + "y" : 1620 + } + }, + "type" : "Variable" + }, + { + "definition" : { + "id" : "Extract the special discount", + "joinId" : "Union" + }, + "type" : "BranchEndData" + } + ], + "additionalBranches" : [ + [ + { + "id" : "Union", + "outputVar" : "context", + "nodeType" : "union", + "parameters" : [ + ], + "branchParameters" : [ + { + "branchId" : "Random 4% discount", + "parameters" : [ + { + "name" : "Output expression", + "expression" : { + "language" : "spel", + "expression" : "{ specialDiscountPercentage: #specialDiscountPercentage, offer: #offer, profile: #profile }" + } + } + ] + }, + { + "branchId" : "Extract the special discount", + "parameters" : [ + { + "name" : "Output expression", + "expression" : { + "language" : "spel", + "expression" : "{ specialDiscountPercentage: #specialDiscountPercentage, offer: #offer, profile: #profile }" + } + } + ] + } + ], + "additionalFields" : { + "description" : "it's workaround to unify context after the split: \nhttps://nussknacker.io/documentation/docs/next/scenarios_authoring/DesignerTipsAndTricks/#passing-the-context-after-the-union-node", + "layoutData" : { + "x" : 180, + "y" : 1800 + } + }, + "type" : "Join" + }, + { + "id" : "Calculate final price of the offer", + "varName" : "finalPrice", + "value" : { + "language" : "spel", + "expression" : "#context.offer.price - #context.specialDiscountPercentage * #context.offer.price" + }, + "additionalFields" : { + "description" : "Final price of the offer is calculated here", + "layoutData" : { + "x" : 180, + "y" : 1980 + } + }, + "type" : "Variable" + }, + { + "id" : "Prepare full offer message", + "varName" : "offerMessage", + "value" : { + "language" : "spel", + "expression" : "'Hello ' + #context.profile.customerName + \"! \\n\" + #context.offer.name + \"\\n\\n\" + #context.offer.message + \"\\nValid to \" + #DATE_FORMAT.formatter('yyyy-MM-dd HH:mm').format(#context.offer.validity) + \". Don't miss it!\"" + }, + "additionalFields" : { + "description" : "Offer message is prepared here", + "layoutData" : { + "x" : 180, + "y" : 2160 + } + }, + "type" : "Variable" + }, + { + "id" : "Offer proposal to customer", + "ref" : { + "typ" : "kafka", + "parameters" : [ + { + "name" : "Topic", + "expression" : { + "language" : "spel", + "expression" : "'OfferProposalsBasedOnCustomerEvents'" + } + }, + { + "name" : "Schema version", + "expression" : { + "language" : "spel", + "expression" : "'latest'" + } + }, + { + "name" : "Key", + "expression" : { + "language" : "spel", + "expression" : "" + } + }, + { + "name" : "Raw editor", + "expression" : { + "language" : "spel", + "expression" : "false" + } + }, + { + "name" : "amount", + "expression" : { + "language" : "spel", + "expression" : "#context.offer.price" + } + }, + { + "name" : "offerName", + "expression" : { + "language" : "spel", + "expression" : "#context.offer.name" + } + }, + { + "name" : "clientName", + "expression" : { + "language" : "spel", + "expression" : "#context.profile.customerName" + } + }, + { + "name" : "profileId", + "expression" : { + "language" : "spel", + "expression" : "#context.profile.id" + } + }, + { + "name" : "offerDescription", + "expression" : { + "language" : "spel", + "expression" : "#context.offer.message" + } + }, + { + "name" : "dueDate", + "expression" : { + "language" : "spel", + "expression" : "#context.offer.validity" + } + }, + { + "name" : "customerId", + "expression" : { + "language" : "spel", + "expression" : "#context.profile.customerId" + } + }, + { + "name" : "clientMsisdn", + "expression" : { + "language" : "spel", + "expression" : "#context.profile.customerMsisdn" + } + }, + { + "name" : "preparedMessageReadyToSend", + "expression" : { + "language" : "spel", + "expression" : "#offerMessage" + } + } + ] + }, + "endResult" : null, + "isDisabled" : null, + "additionalFields" : { + "description" : "We put the offer in a Kafka topic. Some other system will take care of sending it.", + "layoutData" : { + "x" : 180, + "y" : 2340 + } + }, + "type" : "Sink" + } + ] + ] +} \ No newline at end of file diff --git a/quickstart-setup/setup/schema-registry/active-schemas.txt b/quickstart-setup/setup/schema-registry/active-schemas.txt index 493e59d5..f9f28041 100644 --- a/quickstart-setup/setup/schema-registry/active-schemas.txt +++ b/quickstart-setup/setup/schema-registry/active-schemas.txt @@ -3,3 +3,5 @@ ProcessedTransactions.schema.json #SmsesWithOffer.schema.json Transactions.schema.json +CustomerEvents.schema.json +OfferProposalsBasedOnCustomerEvents.schema.json diff --git a/quickstart-setup/setup/schema-registry/schemas/CustomerEvents.schema.json b/quickstart-setup/setup/schema-registry/schemas/CustomerEvents.schema.json new file mode 100644 index 00000000..933a3c20 --- /dev/null +++ b/quickstart-setup/setup/schema-registry/schemas/CustomerEvents.schema.json @@ -0,0 +1,26 @@ +{ + "type": "object", + "title": "CustomerEvent", + "$schema": "http://json-schema.org/draft-07/schema#", + "required": [ + "customerId", + "eventType" + ], + "properties": { + "customerId": { + "description": "The customer unique identifier", + "type": "string" + }, + "eventType": { + "description": "Type of event", + "type": "string", + "enum": [ + "ClientCloseToShowroom", + "ClientBrowseOffers", + "ClientEndedCallWithCustomerService", + "ClientSentTerminationLetter", + "Other" + ] + } + } +} diff --git a/quickstart-setup/setup/schema-registry/schemas/OfferProposalsBasedOnCustomerEvents.schema.json b/quickstart-setup/setup/schema-registry/schemas/OfferProposalsBasedOnCustomerEvents.schema.json new file mode 100644 index 00000000..ddcec287 --- /dev/null +++ b/quickstart-setup/setup/schema-registry/schemas/OfferProposalsBasedOnCustomerEvents.schema.json @@ -0,0 +1,49 @@ +{ + "type": "object", + "title": "OfferProposal", + "$schema": "http://json-schema.org/draft-07/schema#", + "required": [ + "offerName", + "offerDescription", + "clientName", + "amount", + "dueDate" + ], + "properties": { + "customerId": { + "type": "string" + }, + "profileId": { + "type": "string" + }, + "clientName": { + "type": "string", + "description": "The person's full name" + }, + "clientMsisdn": { + "type": "string", + "description": "Client's phone number" + }, + "offerName": { + "type": "string", + "description": "The assigned offer for the person" + }, + "offerDescription": { + "type": "string", + "description": "The assigned offer human readable description" + }, + "amount": { + "type": "integer", + "description": "The offer price" + }, + "dueDate": { + "type": "string", + "format": "date-time", + "description": "The expiration date for the offer" + }, + "preparedMessageReadyToSend": { + "type": "string", + "description": "The human-readable message. It should be prepared to be sent without further modifications." + } + } +}