├── .github ├── CODEOWNERS └── workflows │ ├── daily-build.yml │ └── pull-request.yml ├── .gitignore ├── Highlights.md ├── LICENSE ├── README.md ├── adservice ├── .devcontainer.json ├── .gitignore ├── Ballerina.toml ├── Cloud.toml ├── README.md ├── ads_service.bal ├── docker │ └── Config.toml ├── k8s │ └── Config.toml └── tests │ └── test.bal ├── build-all-docker.sh ├── build-all-k8s.sh ├── cartservice ├── .devcontainer.json ├── .gitignore ├── Ballerina.toml ├── Cloud.toml ├── Config.toml ├── README.md ├── cart_service.bal ├── datastores.bal ├── docker │ └── Config.toml ├── k8s │ └── Config.toml └── tests │ └── test.bal ├── checkoutservice ├── .devcontainer.json ├── .gitignore ├── Ballerina.toml ├── Cloud.toml ├── README.md ├── checkout_service.bal ├── docker │ └── Config.toml ├── k8s │ └── Config.toml └── tests │ └── test.bal ├── client_stubs ├── .devcontainer.json ├── .gitignore ├── Ballerina.toml ├── Package.md ├── demo_pb.bal ├── docker │ └── Config.toml └── k8s │ └── Config.toml ├── currencyservice ├── .devcontainer.json ├── .gitignore ├── Ballerina.toml ├── Cloud.toml ├── README.md ├── currency_service.bal ├── data │ └── currency_conversion.json ├── docker │ └── Config.toml ├── k8s │ └── Config.toml └── tests │ └── test.bal ├── demo.proto ├── docker-compose.yml ├── emailservice ├── .devcontainer.json ├── .gitignore ├── Ballerina.toml ├── Cloud.toml ├── ObserveConfig.toml ├── README.md ├── docker │ └── Config.toml ├── email_service.bal ├── k8s │ └── Config.toml └── tests │ └── test.bal ├── frontend ├── .devcontainer.json ├── .gitignore ├── Ballerina.toml ├── Cloud.toml ├── README.md ├── docker │ └── Config.toml ├── k8s │ └── Config.toml ├── rpc.bal ├── service.bal ├── types.bal └── utils.bal ├── images ├── architecture-diagram.png ├── drop-down-services-spans.png ├── project-design.png ├── shipping-service.png └── tracing-spans.png ├── issue_template.md ├── kustomization.yaml ├── money ├── .devcontainer.json ├── Ballerina.toml ├── Package.md └── money.bal ├── paymentservice ├── .devcontainer.json ├── .gitignore ├── Ballerina.toml ├── Cloud.toml ├── README.md ├── card_validator.bal ├── docker │ └── Config.toml ├── k8s │ └── Config.toml ├── payment_service.bal └── tests │ └── test.bal ├── productcatalogservice ├── .devcontainer.json ├── .gitignore ├── Ballerina.toml ├── Cloud.toml ├── README.md ├── catalog_service.bal ├── docker │ └── Config.toml ├── k8s │ └── Config.toml ├── resources │ └── products.json ├── tests │ └── test.bal └── utils.bal ├── project.code-workspace ├── pull_request_template.md ├── recommendationservice ├── .devcontainer.json ├── .gitignore ├── Ballerina.toml ├── Cloud.toml ├── README.md ├── docker │ └── Config.toml ├── k8s │ └── Config.toml ├── recommendation_service.bal └── tests │ ├── Config.toml │ └── test.bal ├── secret-env-patch.yaml ├── shippingservice ├── .devcontainer.json ├── .gitignore ├── Ballerina.toml ├── Cloud.toml ├── README.md ├── docker │ └── Config.toml ├── k8s │ └── Config.toml ├── shipping_service.bal ├── tests │ └── test.bal └── utils.bal └── ui ├── .eslintrc.json ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json ├── robots.txt └── static │ ├── icons │ ├── Cymbal_NavLogo.svg │ ├── Hipster_Advert2.svg │ ├── Hipster_CartIcon.svg │ ├── Hipster_CheckOutIcon.svg │ ├── Hipster_CurrencyIcon.svg │ ├── Hipster_DownArrow.svg │ ├── Hipster_FacebookIcon.svg │ ├── Hipster_GooglePlayIcon.svg │ ├── Hipster_HelpIcon.svg │ ├── Hipster_HeroLogo.svg │ ├── Hipster_HeroLogoCyan.svg │ ├── Hipster_InstagramIcon.svg │ ├── Hipster_KitchenwareOffer.svg │ ├── Hipster_NavLogo.svg │ ├── Hipster_PinterestIcon.svg │ ├── Hipster_ProfileIcon.svg │ ├── Hipster_SearchIcon.svg │ ├── Hipster_TwitterIcon.svg │ ├── Hipster_UpDownControl.svg │ └── Hipster_YoutubeIcon.svg │ ├── images │ ├── Advert2BannerImage.png │ ├── AdvertBannerImage.png │ ├── HeroBannerImage.png │ ├── HeroBannerImage2.png │ ├── VRHeadsets.png │ ├── credits.txt │ ├── folded-clothes-on-white-chair-wide.jpg │ └── folded-clothes-on-white-chair.jpg │ ├── img │ └── products │ │ ├── bamboo-glass-jar.jpg │ │ ├── candle-holder.jpg │ │ ├── hairdryer.jpg │ │ ├── loafers.jpg │ │ ├── mug.jpg │ │ ├── salt-and-pepper-shakers.jpg │ │ ├── sunglasses.jpg │ │ ├── tank-top.jpg │ │ └── watch.jpg │ └── styles │ ├── cart.css │ ├── order.css │ └── styles.css └── src ├── App.js ├── components ├── UI │ ├── Card.js │ ├── Card.module.css │ ├── LoadingSpinner.js │ └── LoadingSpinner.module.css ├── layout │ ├── Layout.js │ ├── Layout.module.css │ ├── MainNavigation.js │ ├── MainNavigation.module.css │ ├── NoQuotesFound.js │ └── NoQuotesFound.module.css └── products │ ├── Ad.js │ ├── CartItem.js │ ├── CurrencyOption.js │ ├── ExpireOptionPicker.js │ ├── Footer.js │ ├── Header.js │ ├── Order.js │ ├── Product.js │ ├── Recommendation.js │ └── Recommendations.js ├── hooks └── use-http.js ├── index.css ├── index.js ├── lib └── api.js └── pages ├── Cart.js ├── Home.js ├── NotFound.js └── Product.js /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Lines starting with '#' are comments. 2 | # Each line is a file pattern followed by one or more owners. 3 | 4 | # See: https://help.github.com/articles/about-codeowners/ 5 | 6 | # These owners will be the default owners for everything in the repo. 7 | * @xlight05 8 | -------------------------------------------------------------------------------- /.github/workflows/daily-build.yml: -------------------------------------------------------------------------------- 1 | name: Daily build 2 | 3 | on: 4 | schedule: 5 | - cron: '0 */12 * * *' 6 | 7 | env: 8 | BALLERINA_VERSION: 2201.8.4 # Update this with the latest Ballerina version 9 | 10 | jobs: 11 | build: 12 | if: github.repository_owner == 'ballerina-guides' 13 | runs-on: ubuntu-latest 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | test: [adservice, cartservice, checkoutservice, currencyservice, emailservice, frontend, paymentservice, productcatalogservice, recommendationservice, shippingservice] 18 | env: 19 | TEST_NAME: "${{ matrix.test }}" 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Write Test Name to outputs 23 | id: testname 24 | run: | 25 | echo "::set-output name=test-name::${TEST_NAME}" 26 | - uses: ballerina-platform/setup-ballerina@v1 27 | name: Install Ballerina 28 | with: 29 | version: ${{ env.BALLERINA_VERSION }} 30 | - name: Pack gRPC Stub Module 31 | working-directory: "client_stubs" 32 | run: bal pack 33 | - name: Push gRPC Stub Module 34 | working-directory: "client_stubs" 35 | run: bal push --repository=local 36 | - name: Pack Money Utils Module 37 | working-directory: "money" 38 | run: bal pack 39 | - name: Push Money Utils Module 40 | working-directory: "money" 41 | run: bal push --repository=local 42 | - name: Ballerina Build 43 | working-directory: ${{ steps.testname.outputs.test-name }} 44 | run: bal build --cloud=k8s 45 | 46 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yml: -------------------------------------------------------------------------------- 1 | name: GCP Microservice Build 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches-ignore: 7 | - master 8 | 9 | env: 10 | BALLERINA_VERSION: 2201.8.4 # Update this with the latest Ballerina version 11 | 12 | jobs: 13 | build: 14 | if: github.repository_owner == 'ballerina-guides' 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | test: [adservice, cartservice, checkoutservice, currencyservice, emailservice, frontend, paymentservice, productcatalogservice, recommendationservice, shippingservice] 20 | env: 21 | TEST_NAME: "${{ matrix.test }}" 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Write Test Name to outputs 25 | id: testname 26 | run: | 27 | echo "::set-output name=test-name::${TEST_NAME}" 28 | - uses: ballerina-platform/setup-ballerina@v1 29 | name: Install Ballerina 30 | with: 31 | version: ${{ env.BALLERINA_VERSION }} 32 | - name: Pack gRPC Stub Module 33 | working-directory: "client_stubs" 34 | run: bal pack 35 | - name: Push gRPC Stub Module 36 | working-directory: "client_stubs" 37 | run: bal push --repository=local 38 | - name: Pack Money Utils Module 39 | working-directory: "money" 40 | run: bal pack 41 | - name: Push Money Utils Module 42 | working-directory: "money" 43 | run: bal push --repository=local 44 | - name: Ballerina Build 45 | working-directory: ${{ steps.testname.outputs.test-name }} 46 | run: bal build --cloud=k8s 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | **/Dependencies.toml 23 | 24 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 25 | hs_err_pid* 26 | 27 | target 28 | .idea/ 29 | emailservice/GmailConfig.toml 30 | emailservice/tests/Config.toml 31 | node_modules 32 | -------------------------------------------------------------------------------- /adservice/.devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "ballerina/ballerina-devcontainer:2201.0.0", 3 | "extensions": ["WSO2.ballerina"], 4 | } 5 | -------------------------------------------------------------------------------- /adservice/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /adservice/Ballerina.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | org = "wso2" 3 | name = "adservice" 4 | version = "0.1.0" 5 | distribution = "2201.2.3" 6 | 7 | [build-options] 8 | observabilityIncluded = true 9 | 10 | [[dependency]] 11 | org = "wso2" 12 | name = "client_stubs" 13 | version = "0.1.0" 14 | repository = "local" 15 | -------------------------------------------------------------------------------- /adservice/Cloud.toml: -------------------------------------------------------------------------------- 1 | [container.image] 2 | name="ads-service" 3 | repository="wso2inc" 4 | tag="v0.1.0" 5 | 6 | [cloud.deployment] 7 | internal_domain_name="ads-service" 8 | -------------------------------------------------------------------------------- /adservice/README.md: -------------------------------------------------------------------------------- 1 | # Ads Service 2 | 3 | The Ads service loads a set of ads based on the category when the service is initialized and then serves ads based on the products in the cart. 4 | The `adCategories` is a readonly variable populated at the initialization of the service. This allows us to access the ads without lock statements allowing concurrent calls to the service. 5 | 6 | ```bal 7 | type AdCategory record {| 8 | readonly string category; 9 | stubs:Ad[] ads; 10 | |}; 11 | 12 | private final readonly & table key(category) adCategories; 13 | 14 | function init() { 15 | self.adCategories = loadAds().cloneReadOnly(); 16 | 17 | stubs:Ad[] ads = []; 18 | foreach var category in self.adCategories { 19 | ads.push(...category.ads); 20 | } 21 | self.allAds = ads.cloneReadOnly(); 22 | } 23 | 24 | remote function GetAds(stubs:AdRequest request) returns stubs:AdResponse|error { 25 | stubs:Ad[] ads = []; 26 | foreach string category in request.context_keys { 27 | AdCategory? adCategory = self.adCategories[category]; 28 | if adCategory !is () { 29 | ads.push(...adCategory.ads); 30 | } 31 | } 32 | if ads.length() == 0 { 33 | ads = check self.getRandomAds(); 34 | } 35 | return {ads}; 36 | } 37 | ``` 38 | -------------------------------------------------------------------------------- /adservice/ads_service.bal: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 2 | // 3 | // WSO2 LLC. licenses this file to you under the Apache License, 4 | // Version 2.0 (the "License"); you may not use this file except 5 | // in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, 11 | // software distributed under the License is distributed on an 12 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 13 | // KIND, either express or implied. See the License for the 14 | // specific language governing permissions and limitations 15 | // under the License. 16 | 17 | import ballerina/grpc; 18 | import ballerinax/jaeger as _; 19 | import ballerina/log; 20 | import ballerina/random; 21 | import wso2/client_stubs as stubs; 22 | 23 | type AdCategory record {| 24 | readonly string category; 25 | stubs:Ad[] ads; 26 | |}; 27 | 28 | # Provides text advertisements based on the context of the given words. 29 | @display { 30 | label: "Ads", 31 | id: "ads" 32 | } 33 | @grpc:Descriptor {value: stubs:DEMO_DESC} 34 | service "AdService" on new grpc:Listener(9099) { 35 | 36 | private final readonly & table key(category) adCategories; 37 | private final readonly & stubs:Ad[] allAds; 38 | private final int maxAdsToServe = 2; 39 | 40 | function init() { 41 | self.adCategories = loadAds().cloneReadOnly(); 42 | 43 | stubs:Ad[] ads = []; 44 | foreach var category in self.adCategories { 45 | ads.push(...category.ads); 46 | } 47 | self.allAds = ads.cloneReadOnly(); 48 | log:printInfo("Ad service gRPC server started."); 49 | } 50 | 51 | # Retrieves ads based on context provided in the request. 52 | # 53 | # + request - the request containing context 54 | # + return - the related/random ad response or else an error 55 | remote function GetAds(stubs:AdRequest request) returns stubs:AdResponse|error { 56 | log:printInfo(string `Received ad request with context_keys=${request.context_keys.toString()}`); 57 | 58 | stubs:Ad[] ads = []; 59 | foreach string category in request.context_keys { 60 | AdCategory? adCategory = self.adCategories[category]; 61 | if adCategory !is () { 62 | ads.push(...adCategory.ads); 63 | } 64 | } 65 | if ads.length() == 0 { 66 | ads = check self.getRandomAds(); 67 | } 68 | return {ads}; 69 | } 70 | 71 | isolated function getRandomAds() returns stubs:Ad[]|error { 72 | stubs:Ad[] randomAds = []; 73 | foreach int i in 0 ..< self.maxAdsToServe { 74 | int rIndex = check random:createIntInRange(0, self.allAds.length()); 75 | randomAds.push(self.allAds[rIndex]); 76 | } 77 | return randomAds; 78 | } 79 | } 80 | 81 | isolated function loadAds() returns table key(category) { 82 | stubs:Ad hairdryer = { 83 | redirect_url: "/product/2ZYFJ3GM2N", 84 | text: "Hairdryer for sale. 50% off." 85 | }; 86 | stubs:Ad tankTop = { 87 | redirect_url: "/product/66VCHSJNUP", 88 | text: "Tank top for sale. 20% off." 89 | }; 90 | stubs:Ad candleHolder = { 91 | redirect_url: "/product/0PUK6V6EV0", 92 | text: "Candle holder for sale. 30% off." 93 | }; 94 | stubs:Ad bambooGlassJar = { 95 | redirect_url: "/product/9SIQT8TOJO", 96 | text: "Bamboo glass jar for sale. 10% off." 97 | }; 98 | stubs:Ad watch = { 99 | redirect_url: "/product/1YMWWN1N4O", 100 | text: "Watch for sale. Buy one, get second kit for free" 101 | }; 102 | stubs:Ad mug = { 103 | redirect_url: "/product/6E92ZMYYFZ", 104 | text: "Mug for sale. Buy two, get third one for free" 105 | }; 106 | stubs:Ad loafers = { 107 | redirect_url: "/product/L9ECAV7KIM", 108 | text: "Loafers for sale. Buy one, get second one for free" 109 | }; 110 | 111 | return table [ 112 | {category: "clothing", ads: [tankTop]}, 113 | {category: "accessories", ads: [watch]}, 114 | {category: "footwear", ads: [loafers]}, 115 | {category: "hair", ads: [hairdryer]}, 116 | {category: "decor", ads: [candleHolder]}, 117 | {category: "kitchen", ads: [bambooGlassJar, mug]} 118 | ]; 119 | } 120 | -------------------------------------------------------------------------------- /adservice/docker/Config.toml: -------------------------------------------------------------------------------- 1 | [ballerina.observe] 2 | tracingEnabled=true 3 | tracingProvider="jaeger" 4 | 5 | [ballerinax.jaeger] 6 | agentHostname="localhost" 7 | agentPort=4317 8 | -------------------------------------------------------------------------------- /adservice/k8s/Config.toml: -------------------------------------------------------------------------------- 1 | [ballerina.observe] 2 | tracingEnabled=true 3 | tracingProvider="jaeger" 4 | 5 | [ballerinax.jaeger] 6 | agentHostname="localhost" 7 | agentPort=4317 8 | -------------------------------------------------------------------------------- /adservice/tests/test.bal: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 2 | // 3 | // WSO2 LLC. licenses this file to you under the Apache License, 4 | // Version 2.0 (the "License"); you may not use this file except 5 | // in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, 11 | // software distributed under the License is distributed on an 12 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 13 | // KIND, either express or implied. See the License for the 14 | // specific language governing permissions and limitations 15 | // under the License. 16 | 17 | import ballerina/test; 18 | import wso2/client_stubs as stubs; 19 | 20 | @test:Config {} 21 | function listProductsTest() returns error? { 22 | stubs:AdServiceClient adClient = check new ("http://localhost:9099"); 23 | stubs:AdRequest request = { 24 | context_keys: ["accessories"] 25 | }; 26 | 27 | stubs:AdResponse response = check adClient->GetAds(request); 28 | stubs:Ad[] expectedAds = [{ 29 | redirect_url: "/product/1YMWWN1N4O", 30 | text: "Watch for sale. Buy one, get second kit for free" 31 | }]; 32 | stubs:Ad[] receivedAds = []; 33 | response.ads.forEach(function (stubs:Ad ad) { 34 | receivedAds.push(ad); 35 | }); 36 | test:assertEquals(receivedAds, expectedAds); 37 | } 38 | -------------------------------------------------------------------------------- /build-all-docker.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2022 WSO2 LLC. (http://wso2.com) 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | ( cd client_stubs ; bal pack ; bal push --repository local) 17 | ( cd money ; bal pack ; bal push --repository local) 18 | ( cd cartservice ; bal build --cloud=docker) 19 | ( cd currencyservice ; bal build --cloud=docker) 20 | ( cd emailservice ; bal build --cloud=docker) 21 | ( cd paymentservice ; bal build --cloud=docker) 22 | ( cd productcatalogservice ; bal build --cloud=docker) 23 | ( cd recommendationservice ; bal build --cloud=docker) 24 | ( cd shippingservice ; bal build --cloud=docker) 25 | ( cd adservice ; bal build --cloud=docker) 26 | ( cd checkoutservice ; bal build --cloud=docker) 27 | ( cd frontend ; bal build --cloud=docker) 28 | -------------------------------------------------------------------------------- /build-all-k8s.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2022 WSO2 LLC. (http://wso2.com) 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | if [ ! -z "$1" ] 18 | then 19 | eval $(minikube docker-env) 20 | fi 21 | ( cd client_stubs ; bal pack ; bal push --repository local) 22 | ( cd money ; bal pack ; bal push --repository local) 23 | ( cd cartservice ; bal build --cloud=k8s) 24 | ( cd currencyservice ; bal build --cloud=k8s) 25 | ( cd emailservice ; bal build --cloud=k8s) 26 | ( cd paymentservice ; bal build --cloud=k8s) 27 | ( cd productcatalogservice ; bal build --cloud=k8s) 28 | ( cd recommendationservice ; bal build --cloud=k8s) 29 | ( cd shippingservice ; bal build --cloud=k8s) 30 | ( cd adservice ; bal build --cloud=k8s) 31 | ( cd checkoutservice ; bal build --cloud=k8s) 32 | ( cd frontend ; bal build --cloud=k8s) 33 | -------------------------------------------------------------------------------- /cartservice/.devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "ballerina/ballerina-devcontainer:2201.0.0", 3 | "extensions": ["WSO2.ballerina"], 4 | } 5 | -------------------------------------------------------------------------------- /cartservice/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /cartservice/Ballerina.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | org = "wso2" 3 | name = "cartservice" 4 | version = "0.1.0" 5 | distribution = "2201.2.3" 6 | 7 | [build-options] 8 | observabilityIncluded = true 9 | 10 | [[dependency]] 11 | org = "wso2" 12 | name = "client_stubs" 13 | version = "0.1.0" 14 | repository = "local" 15 | -------------------------------------------------------------------------------- /cartservice/Cloud.toml: -------------------------------------------------------------------------------- 1 | [container.image] 2 | name="cart-service" 3 | repository="wso2inc" 4 | tag="v0.1.0" 5 | 6 | [cloud.deployment] 7 | internal_domain_name="cart-service" 8 | -------------------------------------------------------------------------------- /cartservice/Config.toml: -------------------------------------------------------------------------------- 1 | redisHost = "" 2 | redisPassword = "" 3 | -------------------------------------------------------------------------------- /cartservice/README.md: -------------------------------------------------------------------------------- 1 | # Cart Service 2 | 3 | The cart service manages all the details about the shopping card of the user. It implements the Repository pattern to have multiple implementations of DataStore. We have an in-memory data store and redis based data store in the application. 4 | 5 | In the original c# implementation, the repository is defined using an interface. You can find the ballerina representation below. As you notice, the function body is not implemented. This forces the implementer to implement the body of the function. 6 | ```bal 7 | public type DataStore distinct object { 8 | isolated function addItem(string userId, string productId, int quantity) returns error?; 9 | 10 | isolated function emptyCart(string userId) returns error?; 11 | 12 | isolated function getCart(string userId) returns stubs:Cart|error; 13 | }; 14 | ``` 15 | 16 | Then we implement the DataStore using the concrete class `InMemoryStore` and `RedisStore` to provide the actual implementation of the Datastore. 17 | 18 | ```bal 19 | public isolated class InMemoryStore { 20 | *DataStore; 21 | private map store = {}; 22 | 23 | isolated function addItem(string userId, string productId, int quantity) { 24 | } 25 | 26 | isolated function emptyCart(string userId) { 27 | } 28 | 29 | isolated function getCart(string userId) returns stubs:Cart { 30 | } 31 | } 32 | ``` 33 | 34 | You could also observe that we use lock statements to ensure the concurrent safety. 35 | ```bal 36 | isolated function getCart(string userId) returns stubs:Cart { 37 | lock { 38 | if self.store.hasKey(userId) { 39 | return self.store.get(userId).cloneReadOnly(); 40 | } 41 | stubs:Cart newCart = { 42 | user_id: userId, 43 | items: [] 44 | }; 45 | self.store[userId] = newCart; 46 | return newCart.cloneReadOnly(); 47 | } 48 | } 49 | ``` 50 | And finally, we use the appropriate data store based on the config given by the user in the application initialization. 51 | 52 | ```bal 53 | configurable string datastore = ""; 54 | 55 | service "CartService" on new grpc:Listener(9092) { 56 | private final DataStore store; 57 | 58 | function init() returns error? { 59 | if datastore is "redis" { 60 | log:printInfo("Redis datastore is configured for cart service"); 61 | self.store = check new RedisStore(); 62 | } else { 63 | log:printInfo("Redis config is not specified. Starting the cart service with in memory store"); 64 | self.store = new InMemoryStore(); 65 | } 66 | 67 | ... 68 | } 69 | } 70 | ``` 71 | -------------------------------------------------------------------------------- /cartservice/cart_service.bal: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 2 | // 3 | // WSO2 LLC. licenses this file to you under the Apache License, 4 | // Version 2.0 (the "License"); you may not use this file except 5 | // in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, 11 | // software distributed under the License is distributed on an 12 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 13 | // KIND, either express or implied. See the License for the 14 | // specific language governing permissions and limitations 15 | // under the License. 16 | 17 | import ballerina/grpc; 18 | import ballerina/log; 19 | import ballerinax/jaeger as _; 20 | import wso2/client_stubs as stubs; 21 | 22 | configurable string datastore = ""; 23 | configurable string redisHost = ""; 24 | configurable string redisPassword = ""; 25 | 26 | # Stores the product items added to the cart and retrieves them. 27 | @display { 28 | label: "Cart", 29 | id: "cart" 30 | } 31 | @grpc:Descriptor {value: stubs:DEMO_DESC} 32 | service "CartService" on new grpc:Listener(9092) { 33 | private final DataStore store; 34 | 35 | function init() returns error? { 36 | if datastore is "redis" { 37 | log:printInfo("Redis datastore is configured for cart service"); 38 | self.store = check new RedisStore(); 39 | } else { 40 | log:printInfo("Redis config is not specified. Starting the cart service with in memory store"); 41 | self.store = new InMemoryStore(); 42 | } 43 | } 44 | 45 | # Adds an item to the cart. 46 | # 47 | # + request - `AddItemRequest` containing the user id and the `CartItem` 48 | # + return - an `Empty` value or an error 49 | remote function AddItem(stubs:AddItemRequest request) returns stubs:Empty|error { 50 | lock { 51 | check self.store.addItem(request.user_id, request.item.product_id, request.item.quantity); 52 | } 53 | return {}; 54 | } 55 | 56 | # Provides the cart with items. 57 | # 58 | # + request - `GetCartRequest` containing the user id 59 | # + return - `Cart` containing the items or an error 60 | remote function GetCart(stubs:GetCartRequest request) returns stubs:Cart|error { 61 | lock { 62 | return self.store.getCart(request.user_id).cloneReadOnly(); 63 | } 64 | } 65 | 66 | # Clears the cart. 67 | # 68 | # + request - `EmptyCartRequest` containing the user id 69 | # + return - `Empty` value or an error 70 | remote function EmptyCart(stubs:EmptyCartRequest request) returns stubs:Empty|error { 71 | lock { 72 | check self.store.emptyCart(request.user_id); 73 | } 74 | return {}; 75 | } 76 | } 77 | 78 | -------------------------------------------------------------------------------- /cartservice/docker/Config.toml: -------------------------------------------------------------------------------- 1 | [ballerina.observe] 2 | tracingEnabled=true 3 | tracingProvider="jaeger" 4 | 5 | [ballerinax.jaeger] 6 | agentHostname="localhost" 7 | agentPort=4317 8 | -------------------------------------------------------------------------------- /cartservice/k8s/Config.toml: -------------------------------------------------------------------------------- 1 | [ballerina.observe] 2 | tracingEnabled=true 3 | tracingProvider="jaeger" 4 | 5 | [ballerinax.jaeger] 6 | agentHostname="localhost" 7 | agentPort=4317 8 | -------------------------------------------------------------------------------- /cartservice/tests/test.bal: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 2 | // 3 | // WSO2 LLC. licenses this file to you under the Apache License, 4 | // Version 2.0 (the "License"); you may not use this file except 5 | // in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, 11 | // software distributed under the License is distributed on an 12 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 13 | // KIND, either express or implied. See the License for the 14 | // specific language governing permissions and limitations 15 | // under the License. 16 | 17 | import ballerina/test; 18 | import wso2/client_stubs as stubs; 19 | 20 | @test:Config {} 21 | function cartTest() returns error? { 22 | stubs:CartServiceClient ep = check new ("http://localhost:9092"); 23 | //Add Cart 24 | stubs:AddItemRequest item1 = {user_id: "1", item: {product_id: "11", quantity: 1}}; 25 | _ = check ep->AddItem(item1); 26 | 27 | stubs:GetCartRequest user1 = {user_id: "1"}; 28 | stubs:Cart cart = check ep->GetCart(user1); 29 | test:assertEquals(cart.items.length(), 1); 30 | 31 | //Add quantity 32 | stubs:AddItemRequest item2 = {user_id: "1", item: {product_id: "11", quantity: 2}}; 33 | _ = check ep->AddItem(item2); 34 | stubs:Cart cart1 = check ep->GetCart(user1); 35 | test:assertEquals(cart1.items[0].quantity, 3); 36 | 37 | //Add item 38 | stubs:AddItemRequest item3 = {user_id: "1", item: {product_id: "12", quantity: 2}}; 39 | _ = check ep->AddItem(item3); 40 | stubs:Cart cart2 = check ep->GetCart(user1); 41 | test:assertEquals(cart2.items.length(), 2); 42 | } 43 | -------------------------------------------------------------------------------- /checkoutservice/.devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "ballerina/ballerina-devcontainer:2201.0.0", 3 | "extensions": ["WSO2.ballerina"], 4 | } 5 | -------------------------------------------------------------------------------- /checkoutservice/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /checkoutservice/Ballerina.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | org = "wso2" 3 | name = "checkoutservice" 4 | version = "0.1.0" 5 | distribution = "2201.2.3" 6 | 7 | [build-options] 8 | observabilityIncluded = true 9 | 10 | [[dependency]] 11 | org = "wso2" 12 | name = "client_stubs" 13 | version = "0.1.0" 14 | repository = "local" 15 | 16 | [[dependency]] 17 | org = "wso2" 18 | name = "money" 19 | version = "0.1.0" 20 | repository = "local" 21 | -------------------------------------------------------------------------------- /checkoutservice/Cloud.toml: -------------------------------------------------------------------------------- 1 | [container.image] 2 | name="checkout-service" 3 | repository="wso2inc" 4 | tag="v0.1.0" 5 | 6 | [cloud.deployment] 7 | internal_domain_name="checkout-service" 8 | 9 | [[cloud.config.files]] 10 | file="./k8s/Config.toml" 11 | name="checkout-config" 12 | -------------------------------------------------------------------------------- /checkoutservice/README.md: -------------------------------------------------------------------------------- 1 | # Checkout Service 2 | 3 | The checkout service gets called when the user confirms the checkout request. This service represents an intermediate coordination service between several services. 4 | The `PlaceOrder` remote function will be called upon the checkout request. It will call the `Cart Service` to get the products of the cart in the user's account. Then it will match those products with `Catalog Service` and call the `Currency Service` to convert the prices to the user's preferred currency. Then it calls `Shipping Service` to get the shipping quote and it converts the shipping cost to the user's preferred currency using the `Currency Service`. Then the service calculates the total cost and calls the `Payment Service` so it would be charged from the card and it returns a transaction id. Then we ship the order using the `Shipping Service` then clear the cart of the user using the `Cart Service`. Finally, we send an email with all the details to the user's email using `Email Service` and return the order summary to the caller. 5 | 6 | ```bal 7 | configurable string cartHost = "localhost"; 8 | 9 | service "CheckoutService" on new grpc:Listener(9094) { 10 | final CartServiceClient cartClient; 11 | ... 12 | 13 | function init() returns error? { 14 | self.cartClient = check new ("http://" + cartHost + ":9092"); 15 | ... 16 | } 17 | 18 | remote function PlaceOrder(stubs:PlaceOrderRequest request) returns stubs:PlaceOrderResponse|grpc:Error|error { 19 | ... 20 | } 21 | 22 | ... 23 | } 24 | ``` 25 | -------------------------------------------------------------------------------- /checkoutservice/docker/Config.toml: -------------------------------------------------------------------------------- 1 | catalogHost="catalog" 2 | cartHost="cart" 3 | currencyHost="currency" 4 | shippingHost="shipping" 5 | paymentHost="payment" 6 | emailHost="email" 7 | 8 | [ballerina.observe] 9 | tracingEnabled=true 10 | tracingProvider="jaeger" 11 | 12 | [ballerinax.jaeger] 13 | agentHostname="localhost" 14 | agentPort=4317 15 | -------------------------------------------------------------------------------- /checkoutservice/k8s/Config.toml: -------------------------------------------------------------------------------- 1 | catalogHost="catalog-service" 2 | cartHost="cart-service" 3 | currencyHost="currency-service" 4 | shippingHost="shipping-service" 5 | paymentHost="payment-service" 6 | emailHost="email-service" 7 | cartTimeout=3.0 8 | catalogTimeout=3.0 9 | currencyTimeout=3.0 10 | shippingTimeout=3.0 11 | paymentTimeout=3.0 12 | emailTimeout=3.0 13 | 14 | [ballerina.observe] 15 | tracingEnabled=true 16 | tracingProvider="jaeger" 17 | 18 | [ballerinax.jaeger] 19 | agentHostname="localhost" 20 | agentPort=4317 21 | -------------------------------------------------------------------------------- /checkoutservice/tests/test.bal: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 2 | // 3 | // WSO2 LLC. licenses this file to you under the Apache License, 4 | // Version 2.0 (the "License"); you may not use this file except 5 | // in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, 11 | // software distributed under the License is distributed on an 12 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 13 | // KIND, either express or implied. See the License for the 14 | // specific language governing permissions and limitations 15 | // under the License. 16 | 17 | import ballerina/grpc; 18 | import ballerina/test; 19 | import wso2/client_stubs as stubs; 20 | 21 | @grpc:Descriptor {value: stubs:DEMO_DESC} 22 | service "ProductCatalogService" on new grpc:Listener(9091) { 23 | remote function ListProducts(stubs:Empty value) returns stubs:ListProductsResponse { 24 | return { 25 | products: [] 26 | }; 27 | } 28 | 29 | remote function GetProduct(stubs:GetProductRequest value) returns stubs:Product|error { 30 | return { 31 | id: "OLJCESPC7Z", 32 | name: "Sunglasses", 33 | description: "Add a modern touch to your outfits with these sleek aviator sunglasses.", 34 | picture: "/static/img/products/sunglasses.jpg", 35 | price_usd: { 36 | currency_code: "USD", 37 | units: 19, 38 | nanos: 990000000 39 | }, 40 | categories: ["accessories"] 41 | }; 42 | } 43 | 44 | remote function SearchProducts(stubs:SearchProductsRequest value) returns stubs:SearchProductsResponse|error { 45 | return error("method not implemented"); 46 | } 47 | } 48 | 49 | @grpc:Descriptor {value: stubs:DEMO_DESC} 50 | service "CartService" on new grpc:Listener(9092) { 51 | remote function AddItem(stubs:AddItemRequest request) returns stubs:Empty|error { 52 | return error("method not implemented"); 53 | } 54 | 55 | remote function GetCart(stubs:GetCartRequest request) returns stubs:Cart|error { 56 | return {user_id: "3", items: [{product_id: "OLJCESPC7Z", quantity: 1}]}; 57 | } 58 | 59 | remote function EmptyCart(stubs:EmptyCartRequest request) returns stubs:Empty|error { 60 | return {}; 61 | } 62 | } 63 | 64 | @grpc:Descriptor {value: stubs:DEMO_DESC} 65 | service "CurrencyService" on new grpc:Listener(9093) { 66 | remote function GetSupportedCurrencies(stubs:Empty request) returns stubs:GetSupportedCurrenciesResponse|error { 67 | return error("method not implemented"); 68 | } 69 | 70 | remote function Convert(stubs:CurrencyConversionRequest request) returns stubs:Money|error { 71 | return { 72 | currency_code: request.to_code, 73 | units: request.'from.units, 74 | nanos: request.'from.nanos 75 | }; 76 | } 77 | } 78 | 79 | @grpc:Descriptor {value: stubs:DEMO_DESC} 80 | service "ShippingService" on new grpc:Listener(9095) { 81 | remote function GetQuote(stubs:GetQuoteRequest request) returns stubs:GetQuoteResponse|error { 82 | stubs:Money usdCost = {currency_code: "USD", nanos: 99000000, units: 8}; 83 | return { 84 | cost_usd: usdCost 85 | }; 86 | } 87 | 88 | remote function ShipOrder(stubs:ShipOrderRequest request) returns stubs:ShipOrderResponse|error { 89 | return {tracking_id: "AB15A51G1051A"}; 90 | } 91 | } 92 | 93 | @grpc:Descriptor {value: stubs:DEMO_DESC} 94 | service "PaymentService" on new grpc:Listener(9096) { 95 | remote function Charge(stubs:ChargeRequest value) returns stubs:ChargeResponse|error { 96 | return { 97 | transaction_id: "12345678945613254689" 98 | }; 99 | } 100 | } 101 | 102 | @grpc:Descriptor {value: stubs:DEMO_DESC} 103 | service "EmailService" on new grpc:Listener(9097) { 104 | remote function SendOrderConfirmation(stubs:SendOrderConfirmationRequest request) returns stubs:Empty|error { 105 | return {}; 106 | } 107 | } 108 | 109 | @test:Config {} 110 | function intAddTest() returns error? { 111 | stubs:CheckoutServiceClient ep = check new ("http://localhost:9094"); 112 | 113 | stubs:PlaceOrderRequest req = { 114 | user_id: "3", 115 | address: { 116 | country: "Sri lanka", 117 | city: "Colombo", 118 | state: "Western", 119 | street_address: "56,Palm Grove", 120 | zip_code: 10300 121 | }, 122 | credit_card: { 123 | credit_card_number: "4444444444444448", 124 | credit_card_cvv: 123, 125 | credit_card_expiration_year: 2023, 126 | credit_card_expiration_month: 10 127 | 128 | }, 129 | email: "ballerina@wso2.com", 130 | user_currency: "USD" 131 | }; 132 | stubs:PlaceOrderResponse placeOrderResponse = check ep->PlaceOrder(req); 133 | test:assertEquals(placeOrderResponse.'order.shipping_tracking_id, "AB15A51G1051A"); 134 | test:assertEquals(placeOrderResponse.'order.shipping_cost, {currency_code: "USD", nanos: 99000000, units: 8}); 135 | test:assertEquals(placeOrderResponse.'order.shipping_address, { 136 | country: "Sri lanka", 137 | city: "Colombo", 138 | state: "Western", 139 | street_address: "56,Palm Grove", 140 | zip_code: 10300 141 | }); 142 | test:assertEquals(placeOrderResponse.'order.items, [ 143 | { 144 | item: {product_id: "OLJCESPC7Z", quantity: 1}, 145 | cost: {currency_code: "USD", units: 19, nanos: 990000000} 146 | } 147 | ]); 148 | } 149 | -------------------------------------------------------------------------------- /client_stubs/.devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "ballerina/ballerina-devcontainer:2201.2.3", 3 | "extensions": ["WSO2.ballerina"], 4 | } 5 | -------------------------------------------------------------------------------- /client_stubs/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /client_stubs/Ballerina.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | org = "wso2" 3 | name = "client_stubs" 4 | version = "0.1.0" 5 | distribution = "2201.2.3" 6 | 7 | [build-options] 8 | observabilityIncluded = true 9 | -------------------------------------------------------------------------------- /client_stubs/Package.md: -------------------------------------------------------------------------------- 1 | # GCP Micro-Services Demo GRPC Protobuf Stub 2 | 3 | This module contains the protobuf stub files generated for .proto file -------------------------------------------------------------------------------- /client_stubs/docker/Config.toml: -------------------------------------------------------------------------------- 1 | catalogHost="catalog" 2 | cartHost="cart" 3 | currencyHost="currency" 4 | shippingHost="shipping" 5 | paymentHost="payment" 6 | emailHost="email" 7 | 8 | [ballerina.observe] 9 | tracingEnabled=true 10 | tracingProvider="jaeger" 11 | 12 | [ballerinax.jaeger] 13 | agentHostname="localhost" 14 | agentPort=4317 15 | -------------------------------------------------------------------------------- /client_stubs/k8s/Config.toml: -------------------------------------------------------------------------------- 1 | catalogHost="catalog-service" 2 | cartHost="cart-service" 3 | currencyHost="currency-service" 4 | shippingHost="shipping-service" 5 | paymentHost="payment-service" 6 | emailHost="email-service" 7 | cartTimeout=3.0 8 | catalogTimeout=3.0 9 | currencyTimeout=3.0 10 | shippingTimeout=3.0 11 | paymentTimeout=3.0 12 | emailTimeout=3.0 13 | 14 | [ballerina.observe] 15 | tracingEnabled=true 16 | tracingProvider="jaeger" 17 | 18 | [ballerinax.jaeger] 19 | agentHostname="localhost" 20 | agentPort=4317 21 | -------------------------------------------------------------------------------- /currencyservice/.devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "ballerina/ballerina-devcontainer:2201.0.0", 3 | "extensions": ["WSO2.ballerina"], 4 | } 5 | -------------------------------------------------------------------------------- /currencyservice/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /currencyservice/Ballerina.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | org = "wso2" 3 | name = "currencyservice" 4 | version = "0.1.0" 5 | distribution = "2201.2.3" 6 | 7 | [build-options] 8 | observabilityIncluded = true 9 | 10 | [[dependency]] 11 | org = "wso2" 12 | name = "client_stubs" 13 | version = "0.1.0" 14 | repository = "local" 15 | -------------------------------------------------------------------------------- /currencyservice/Cloud.toml: -------------------------------------------------------------------------------- 1 | [container.image] 2 | name="currency-service" 3 | repository="wso2inc" 4 | tag="v0.1.0" 5 | 6 | [[container.copy.files]] 7 | sourceFile="./data/currency_conversion.json" 8 | target="/home/ballerina/data/currency_conversion.json" 9 | 10 | [cloud.deployment] 11 | internal_domain_name="currency-service" 12 | -------------------------------------------------------------------------------- /currencyservice/README.md: -------------------------------------------------------------------------------- 1 | # Currency Service 2 | 3 | The Currency service is responsible for converting a given currency object to another currency. The service contains a JSON file with the conversion rates. When the service is initialized it will read this JSON and store it in a read-only map as it's not getting modified afterward. When the `Convert` remote function is invoked, it will read the rate from the map, convert the value and return the converted currency value to the caller. 4 | 5 | ```bal 6 | configurable string currencyJsonPath = "./data/currency_conversion.json"; 7 | 8 | service "CurrencyService" on new grpc:Listener(9093) { 9 | final map & readonly currencyMap; 10 | 11 | function init() returns error? { 12 | json currencyJson = check io:fileReadJson(currencyJsonPath); 13 | self.currencyMap = check parseCurrencyJson(currencyJson).cloneReadOnly(); 14 | } 15 | 16 | remote function GetSupportedCurrencies(stubs:Empty request) returns stubs:GetSupportedCurrenciesResponse { 17 | return {currency_codes: self.currencyMap.keys()}; 18 | } 19 | remote function Convert(stubs:CurrencyConversionRequest request) returns stubs:Money|error { 20 | ... 21 | } 22 | 23 | ... 24 | } 25 | ``` 26 | 27 | Additionally, since we are reading the Currency rates from the JSON file, therefore we need to copy the JSON into the container. This can be done using having the following codeblock in the `Cloud.toml`. 28 | ```toml 29 | [[container.copy.files]] 30 | sourceFile="./data/currency_conversion.json" 31 | target="/home/ballerina/data/currency_conversion.json" 32 | ``` 33 | -------------------------------------------------------------------------------- /currencyservice/currency_service.bal: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 2 | // 3 | // WSO2 LLC. licenses this file to you under the Apache License, 4 | // Version 2.0 (the "License"); you may not use this file except 5 | // in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, 11 | // software distributed under the License is distributed on an 12 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 13 | // KIND, either express or implied. See the License for the 14 | // specific language governing permissions and limitations 15 | // under the License. 16 | 17 | import ballerina/grpc; 18 | import ballerina/io; 19 | import ballerina/log; 20 | import ballerinax/jaeger as _; 21 | import wso2/client_stubs as stubs; 22 | 23 | const FRACTION_SIZE = 1000000000; 24 | 25 | configurable string currencyJsonPath = "./data/currency_conversion.json"; 26 | 27 | # This service is used read the exchange rates from a JSON and convert one currency value to another. 28 | @display { 29 | label: "Currency", 30 | id: "currency" 31 | } 32 | @grpc:Descriptor {value: stubs:DEMO_DESC} 33 | service "CurrencyService" on new grpc:Listener(9093) { 34 | private final map & readonly currencyMap; 35 | 36 | function init() returns error? { 37 | json currencyJson = check io:fileReadJson(currencyJsonPath); 38 | log:printInfo("Successfully read currency conversion json"); 39 | self.currencyMap = check parseCurrencyJson(currencyJson); 40 | log:printInfo("Currency service gRPC server started."); 41 | } 42 | 43 | # Provides the set of supported currencies. 44 | # 45 | # + request - an empty request 46 | # + return - `GetSupportedCurrenciesResponse` containing supported currencies or else and error 47 | remote function GetSupportedCurrencies(stubs:Empty request) returns stubs:GetSupportedCurrenciesResponse { 48 | log:printInfo("Getting supported currencies."); 49 | return {currency_codes: self.currencyMap.keys()}; 50 | 51 | } 52 | 53 | # Converts a specific `Money` value to a required currency. 54 | # 55 | # + request - `CurrencyConversionRequest` containing the `Money` value and the required currency 56 | # + return - returns the `Money` in the required currency or an error 57 | remote function Convert(stubs:CurrencyConversionRequest request) returns stubs:Money|error { 58 | stubs:Money moneyFrom = request.'from; 59 | //From Unit 60 | decimal pennies = moneyFrom.nanos / FRACTION_SIZE; 61 | decimal totalUSD = moneyFrom.units + pennies; 62 | 63 | //UNIT Euro 64 | decimal rate = self.currencyMap.get(moneyFrom.currency_code); 65 | decimal euroAmount = totalUSD / rate; 66 | 67 | //UNIT to Target 68 | decimal targetRate = self.currencyMap.get(request.to_code); 69 | decimal targetAmount = euroAmount * targetRate; 70 | 71 | int units = targetAmount.floor(); 72 | int nanos = decimal:floor((targetAmount - units) * FRACTION_SIZE); 73 | 74 | return { 75 | currency_code: request.to_code, 76 | nanos, 77 | units 78 | }; 79 | } 80 | } 81 | 82 | isolated function parseCurrencyJson(json currencyJson) returns map & readonly|error { 83 | map currencyValues = check currencyJson.cloneWithType(); 84 | return map from [string, string] [key, value] in currencyValues.entries() 85 | select [key, check decimal:fromString(value)]; 86 | } 87 | -------------------------------------------------------------------------------- /currencyservice/data/currency_conversion.json: -------------------------------------------------------------------------------- 1 | { 2 | "EUR": "1.0", 3 | "USD": "1.1305", 4 | "JPY": "126.40", 5 | "BGN": "1.9558", 6 | "CZK": "25.592", 7 | "DKK": "7.4609", 8 | "GBP": "0.85970", 9 | "HUF": "315.51", 10 | "PLN": "4.2996", 11 | "RON": "4.7463", 12 | "SEK": "10.5375", 13 | "CHF": "1.1360", 14 | "ISK": "136.80", 15 | "NOK": "9.8040", 16 | "HRK": "7.4210", 17 | "RUB": "74.4208", 18 | "TRY": "6.1247", 19 | "AUD": "1.6072", 20 | "BRL": "4.2682", 21 | "CAD": "1.5128", 22 | "CNY": "7.5857", 23 | "HKD": "8.8743", 24 | "IDR": "15999.40", 25 | "ILS": "4.0875", 26 | "INR": "79.4320", 27 | "KRW": "1275.05", 28 | "MXN": "21.7999", 29 | "MYR": "4.6289", 30 | "NZD": "1.6679", 31 | "PHP": "59.083", 32 | "SGD": "1.5349", 33 | "THB": "36.012", 34 | "ZAR": "16.0583" 35 | } -------------------------------------------------------------------------------- /currencyservice/docker/Config.toml: -------------------------------------------------------------------------------- 1 | [ballerina.observe] 2 | tracingEnabled=true 3 | tracingProvider="jaeger" 4 | 5 | [ballerinax.jaeger] 6 | agentHostname="localhost" 7 | agentPort=4317 8 | -------------------------------------------------------------------------------- /currencyservice/k8s/Config.toml: -------------------------------------------------------------------------------- 1 | [ballerina.observe] 2 | tracingEnabled=true 3 | tracingProvider="jaeger" 4 | 5 | [ballerinax.jaeger] 6 | agentHostname="localhost" 7 | agentPort=4317 8 | -------------------------------------------------------------------------------- /currencyservice/tests/test.bal: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 2 | // 3 | // WSO2 LLC. licenses this file to you under the Apache License, 4 | // Version 2.0 (the "License"); you may not use this file except 5 | // in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, 11 | // software distributed under the License is distributed on an 12 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 13 | // KIND, either express or implied. See the License for the 14 | // specific language governing permissions and limitations 15 | // under the License. 16 | 17 | import ballerina/test; 18 | import wso2/client_stubs as stubs; 19 | 20 | @test:Config {} 21 | function currencyExchangeTest() returns error? { 22 | stubs:CurrencyServiceClient ep = check new ("http://localhost:9093"); 23 | stubs:Empty req = {}; 24 | stubs:GetSupportedCurrenciesResponse getSupportedCurrenciesResponse = check ep->GetSupportedCurrencies(req); 25 | test:assertEquals(getSupportedCurrenciesResponse.currency_codes.length(), 33); 26 | 27 | stubs:CurrencyConversionRequest reqq = { 28 | 'from: { 29 | currency_code: "USD", 30 | units: 18, 31 | nanos: 990000000 32 | }, 33 | to_code: "EUR" 34 | }; 35 | stubs:Money money = check ep->Convert(reqq); 36 | test:assertEquals(money.currency_code, "EUR"); 37 | test:assertEquals(money.nanos, 797877045); 38 | test:assertEquals(money.units, 16); 39 | } 40 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | jaeger: 5 | image: jaegertracing/opentelemetry-all-in-one:latest 6 | ports: 7 | - 16686:16686 8 | - 13133:13133 9 | - 4317:4317 10 | ads: 11 | image: wso2inc/ads-service:v0.1.0 12 | volumes: 13 | - type: bind 14 | source: ./adservice/docker/Config.toml 15 | target: /home/ballerina/Config.toml 16 | links: 17 | - jaeger 18 | depends_on: 19 | - jaeger 20 | cart: 21 | image: wso2inc/cart-service:v0.1.0 #add redis 22 | volumes: 23 | - type: bind 24 | source: ./cartservice/docker/Config.toml 25 | target: /home/ballerina/Config.toml 26 | links: 27 | - jaeger 28 | depends_on: 29 | - jaeger 30 | checkout: 31 | image: wso2inc/checkout-service:v0.1.0 32 | volumes: 33 | - type: bind 34 | source: ./checkoutservice/docker/Config.toml 35 | target: /home/ballerina/Config.toml 36 | links: 37 | - catalog 38 | - cart 39 | - currency 40 | - shipping 41 | - payment 42 | - email 43 | - jaeger 44 | depends_on: 45 | - catalog 46 | - cart 47 | - currency 48 | - shipping 49 | - payment 50 | - email 51 | - jaeger 52 | currency: 53 | image: wso2inc/currency-service:v0.1.0 54 | volumes: 55 | - type: bind 56 | source: ./currencyservice/docker/Config.toml 57 | target: /home/ballerina/Config.toml 58 | links: 59 | - jaeger 60 | depends_on: 61 | - jaeger 62 | email: 63 | image: wso2inc/email-service:v0.1.0 64 | volumes: 65 | - type: bind 66 | source: ./emailservice/ObserveConfig.toml 67 | target: /home/ballerina/ObserveConfig.toml 68 | secrets: 69 | - source: email_config 70 | target: /home/ballerina/GmailConfig.toml 71 | environment: 72 | - BAL_CONFIG_FILES=/home/ballerina/ObserveConfig.toml:/home/ballerina/GmailConfig.toml 73 | links: 74 | - jaeger 75 | depends_on: 76 | - jaeger 77 | frontend: 78 | image: wso2inc/frontend-service:v0.1.0 79 | volumes: 80 | - type: bind 81 | source: ./frontend/docker/Config.toml 82 | target: /home/ballerina/Config.toml 83 | ports: 84 | - 9098:9098 85 | links: 86 | - catalog 87 | - cart 88 | - recommendation 89 | - currency 90 | - shipping 91 | - checkout 92 | - ads 93 | - jaeger 94 | depends_on: 95 | - catalog 96 | - cart 97 | - recommendation 98 | - currency 99 | - shipping 100 | - checkout 101 | - ads 102 | - jaeger 103 | payment: 104 | image: wso2inc/payment-service:v0.1.0 105 | volumes: 106 | - type: bind 107 | source: ./paymentservice/docker/Config.toml 108 | target: /home/ballerina/Config.toml 109 | links: 110 | - jaeger 111 | depends_on: 112 | - jaeger 113 | catalog: 114 | image: wso2inc/catalog-service:v0.1.0 115 | volumes: 116 | - type: bind 117 | source: ./productcatalogservice/docker/Config.toml 118 | target: /home/ballerina/Config.toml 119 | links: 120 | - jaeger 121 | depends_on: 122 | - jaeger 123 | recommendation: 124 | image: wso2inc/recommendation-service:v0.1.0 125 | volumes: 126 | - type: bind 127 | source: ./recommendationservice/docker/Config.toml 128 | target: /home/ballerina/Config.toml 129 | links: 130 | - catalog 131 | - jaeger 132 | depends_on: 133 | - catalog 134 | - jaeger 135 | shipping: 136 | image: wso2inc/shipping-service:v0.1.0 137 | volumes: 138 | - type: bind 139 | source: ./shippingservice/docker/Config.toml 140 | target: /home/ballerina/Config.toml 141 | links: 142 | - jaeger 143 | depends_on: 144 | - jaeger 145 | secrets: 146 | email_config: 147 | file: ./emailservice/GmailConfig.toml 148 | -------------------------------------------------------------------------------- /emailservice/.devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "ballerina/ballerina-devcontainer:2201.0.0", 3 | "extensions": ["WSO2.ballerina"], 4 | } 5 | -------------------------------------------------------------------------------- /emailservice/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /emailservice/Ballerina.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | org = "wso2" 3 | name = "emailservice" 4 | version = "0.1.0" 5 | distribution = "2201.2.3" 6 | 7 | [build-options] 8 | observabilityIncluded = true 9 | 10 | [[dependency]] 11 | org = "wso2" 12 | name = "client_stubs" 13 | version = "0.1.0" 14 | repository = "local" 15 | -------------------------------------------------------------------------------- /emailservice/Cloud.toml: -------------------------------------------------------------------------------- 1 | [container.image] 2 | name="email-service" 3 | repository="wso2inc" 4 | tag="v0.1.0" 5 | 6 | [[cloud.secret.files]] 7 | file="./GmailConfig.toml" 8 | mount_path="/home/ballerina/GmailConfig.toml" 9 | 10 | [[cloud.config.files]] 11 | file="./ObserveConfig.toml" 12 | name="observe-config" 13 | 14 | [cloud.deployment] 15 | internal_domain_name="email-service" 16 | -------------------------------------------------------------------------------- /emailservice/ObserveConfig.toml: -------------------------------------------------------------------------------- 1 | [ballerina.observe] 2 | tracingEnabled=true 3 | tracingProvider="jaeger" 4 | 5 | [ballerinax.jaeger] 6 | agentHostname="localhost" 7 | agentPort=4317 8 | -------------------------------------------------------------------------------- /emailservice/README.md: -------------------------------------------------------------------------------- 1 | # Email Service 2 | 3 | The service is responsible for sending an email with the order confirmation after checkout completion. The HTML generation required for the Email formatting is done using ballerina's built-in XML feature. The email-sending part is handled by the Gmail connector. 4 | 5 | ```bal 6 | service "EmailService" on new grpc:Listener(9097) { 7 | remote function SendOrderConfirmation(stubs:SendOrderConfirmationRequest request) returns stubs:Empty|error { 8 | 9 | gmail:MessageRequest messageRequest = { 10 | recipient: request.email, 11 | subject: "Your Confirmation Email", 12 | messageBody: (check getConfirmationHtml(request.'order)).toString(), 13 | contentType: gmail:TEXT_HTML 14 | }; 15 | gmail:Message|error sendMessageResponse = self.gmailClient->sendMessage(messageRequest); 16 | ... 17 | return {}; 18 | } 19 | 20 | function getConfirmationHtml(OrderResult res) returns xml { 21 | ... 22 | xml items = from stubs:OrderItem item in result.items 23 | select xml ` 24 | #${item.item.product_id} 25 | ${item.item.quantity} 26 | ... 27 | `; 28 | 29 | items = xml ` 30 | Item No. 31 | Quantity 32 | Price 33 | ` + items; 34 | 35 | ... 36 | 37 | xml emailContent = ... 38 | 39 | return emailContent; 40 | } 41 | } 42 | ``` 43 | 44 | This service requires sensitive Gmail credentials, to use in the Gmail connector. This also is done with the help of Ballerina's configurable feature. The sample code and the `Config.toml` file can be found below. 45 | 46 | ```bal 47 | type GmailConfig record {| 48 | string refreshToken; 49 | string clientId; 50 | string clientSecret; 51 | |}; 52 | 53 | configurable GmailConfig gmail = ?; 54 | ``` 55 | 56 | ```toml 57 | [gmail] 58 | refreshToken = "" 59 | clientId = "" 60 | clientSecret = "" 61 | ``` 62 | 63 | However, as this `Config.toml` contains sensitive information we need to load this as a secret to the kubernetes cluster. You can do that by adding the following entry to the `Cloud.toml`. 64 | ```toml 65 | [[cloud.secret.files]] 66 | file="./Config.toml" 67 | ``` 68 | -------------------------------------------------------------------------------- /emailservice/docker/Config.toml: -------------------------------------------------------------------------------- 1 | catalogHost="catalog" 2 | cartHost="cart" 3 | currencyHost="currency" 4 | shippingHost="shipping" 5 | paymentHost="payment" 6 | emailHost="email" 7 | 8 | [ballerina.observe] 9 | tracingEnabled=true 10 | tracingProvider="jaeger" 11 | 12 | [ballerinax.jaeger] 13 | agentHostname="localhost" 14 | agentPort=4317 15 | -------------------------------------------------------------------------------- /emailservice/email_service.bal: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 2 | // 3 | // WSO2 LLC. licenses this file to you under the Apache License, 4 | // Version 2.0 (the "License"); you may not use this file except 5 | // in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, 11 | // software distributed under the License is distributed on an 12 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 13 | // KIND, either express or implied. See the License for the 14 | // specific language governing permissions and limitations 15 | // under the License. 16 | 17 | import ballerina/grpc; 18 | import ballerina/log; 19 | import ballerinax/googleapis.gmail as gmail; 20 | import ballerinax/jaeger as _; 21 | import wso2/client_stubs as stubs; 22 | 23 | type GmailConfig record {| 24 | string refreshToken; 25 | string clientId; 26 | string clientSecret; 27 | |}; 28 | 29 | const FONT_URL = 30 | "https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap"; 31 | const FRACTION_SIZE = 1000000000; 32 | 33 | configurable GmailConfig gmailConfig = ?; 34 | 35 | # Used to send an order confirmation email to the user using the `gmail` connector. 36 | @display { 37 | label: "Email", 38 | id: "email" 39 | } 40 | @grpc:Descriptor {value: stubs:DEMO_DESC} 41 | service "EmailService" on new grpc:Listener(9097) { 42 | 43 | private final gmail:Client gmailClient; 44 | 45 | function init() returns error? { 46 | self.gmailClient = check new gmail:Client( 47 | config = { 48 | auth: { 49 | refreshToken: gmailConfig.refreshToken, 50 | clientId: gmailConfig.clientId, 51 | clientSecret: gmailConfig.clientSecret 52 | } 53 | } 54 | ); 55 | 56 | log:printInfo("Email service gRPC server started."); 57 | } 58 | 59 | # Sends the order confirmation email containing details about the order. 60 | # 61 | # + request - `SendOrderConfirmationRequest` which contains the details about the order 62 | # + return - `Empty` or else an error 63 | remote function SendOrderConfirmation(stubs:SendOrderConfirmationRequest request) returns stubs:Empty|error { 64 | log:printInfo(string `Received a request to send order confirmation email to ${request.email}.`); 65 | 66 | gmail:MessageRequest messageRequest = { 67 | to: [request.email], 68 | subject: "Your Confirmation Email", 69 | bodyInHtml: (check getConfirmationHtml(request.'order)).toString() 70 | }; 71 | gmail:Message|error sendMessageResponse = check self.gmailClient->/users/[request.email]/messages/send.post(messageRequest); 72 | if sendMessageResponse is error { 73 | log:printError("An error occurred when sending the order confirmation email ", sendMessageResponse); 74 | return sendMessageResponse; 75 | } 76 | log:printInfo(string `Email sent with Message ID: ${sendMessageResponse.id} and Thread ID: ${sendMessageResponse.threadId}`); 77 | return {}; 78 | } 79 | } 80 | 81 | function getConfirmationHtml(stubs:OrderResult result) returns xml|error { 82 | xml items = from stubs:OrderItem item in result.items 83 | select xml ` 84 | #${item.item.product_id} 85 | ${item.item.quantity} 86 | ${item.cost.units}.${item.cost.nanos / FRACTION_SIZE} ${item.cost.currency_code} 87 | `; 88 | items = xml ` 89 | Item No. 90 | Quantity 91 | Price 92 | ` + items; 93 | 94 | xml body = xml ` 95 |

Your Order Confirmation

96 |

Thanks for shopping with us!

97 |

Order ID

98 |

#${result.order_id}

99 |

Shipping

100 |

#${result.shipping_tracking_id}

101 |

${result.shipping_cost.units}.${result.shipping_cost.nanos / FRACTION_SIZE} 102 | ${result.shipping_cost.currency_code}

103 |

${result.shipping_address.street_address}, ${result.shipping_address.city}, 104 | ${result.shipping_address.country} ${result.shipping_address.zip_code}

105 |

Items

106 | 107 | ${items} 108 |
109 | `; 110 | 111 | xml emailContent = xml ` 112 | 113 | 114 | Your Order Confirmation 115 | 116 | 117 | 122 | ${body} 123 | `; 124 | return emailContent; 125 | } 126 | -------------------------------------------------------------------------------- /emailservice/k8s/Config.toml: -------------------------------------------------------------------------------- 1 | [ballerina.observe] 2 | tracingEnabled=true 3 | tracingProvider="jaeger" 4 | 5 | [ballerinax.jaeger] 6 | agentHostname="localhost" 7 | agentPort=4317 8 | -------------------------------------------------------------------------------- /emailservice/tests/test.bal: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 2 | // 3 | // WSO2 LLC. licenses this file to you under the Apache License, 4 | // Version 2.0 (the "License"); you may not use this file except 5 | // in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, 11 | // software distributed under the License is distributed on an 12 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 13 | // KIND, either express or implied. See the License for the 14 | // specific language governing permissions and limitations 15 | // under the License. 16 | 17 | import ballerina/test; 18 | import wso2/client_stubs as stubs; 19 | 20 | @test:Config {} 21 | function emailSendTest() returns error? { 22 | stubs:EmailServiceClient ep = check new ("http://localhost:9097"); 23 | stubs:Money cost = { 24 | currency_code: "USD", 25 | nanos: 900000000, 26 | units: 5 27 | }; 28 | 29 | stubs:Address address = { 30 | street_address: "56, Palm grove", 31 | city: "Colombo", 32 | country: "Sri Lanka", 33 | state: "Western", 34 | zip_code: 10300 35 | }; 36 | 37 | stubs:OrderItem item1 = { 38 | item: { 39 | product_id: "1", 40 | quantity: 2 41 | }, 42 | cost: cost 43 | }; 44 | 45 | stubs:OrderItem item2 = { 46 | item: { 47 | product_id: "2", 48 | quantity: 1 49 | }, 50 | cost: cost 51 | }; 52 | 53 | stubs:SendOrderConfirmationRequest req = { 54 | email: "anjanasupun05@gmail.com", 55 | 'order: { 56 | order_id: "1", 57 | shipping_tracking_id: "2323", 58 | shipping_cost: cost, 59 | shipping_address: address, 60 | items: [item1, item2] 61 | } 62 | }; 63 | _ = check ep->SendOrderConfirmation(req); 64 | 65 | } 66 | -------------------------------------------------------------------------------- /frontend/.devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "ballerina/ballerina-devcontainer:2201.0.0", 3 | "extensions": ["WSO2.ballerina"], 4 | } 5 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /frontend/Ballerina.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | org = "wso2" 3 | name = "frontend" 4 | version = "0.1.0" 5 | distribution = "2201.2.3" 6 | 7 | [build-options] 8 | observabilityIncluded = true 9 | 10 | [[dependency]] 11 | org = "wso2" 12 | name = "client_stubs" 13 | version = "0.1.0" 14 | repository = "local" 15 | 16 | [[dependency]] 17 | org = "wso2" 18 | name = "money" 19 | version = "0.1.0" 20 | repository = "local" 21 | -------------------------------------------------------------------------------- /frontend/Cloud.toml: -------------------------------------------------------------------------------- 1 | [container.image] 2 | name="frontend-service" 3 | repository="wso2inc" 4 | tag="v0.1.0" 5 | 6 | [cloud.deployment] 7 | internal_domain_name="frontend-service" 8 | 9 | [[cloud.config.files]] 10 | file="./k8s/Config.toml" 11 | name="frontend-config" 12 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | ## Frontend Service 2 | 3 | The HTTP service uses cookies to identify the user details. Since this sample does not have a register capability, if the cookie is not found in the request it will always regenerate a new cookie with return the cookie with the response. Please note that this is not a secure way to do this but for demo purposes only. Anyhow, to implement the feature, since we need to intercept each request, without repeating the code we have implemented an AuthInterceptor and registered into the service. 4 | 5 | 6 | ```bal 7 | service class AuthInterceptor { 8 | *http:RequestInterceptor; 9 | resource function 'default [string... path](http:RequestContext ctx, http:Request request) 10 | returns http:NextService|error? { 11 | http:Cookie[] cookies = request.getCookies(); 12 | http:Cookie[] sessionIDCookies = cookies.filter(cookie => cookie.name == SESSION_ID_COOKIE); 13 | 14 | string sessionId; 15 | if sessionIDCookies.length() == 0 { 16 | ... 17 | http:Cookie sessionIdCookie = new (SESSION_ID_COOKIE, sessionId, path = "/"); 18 | cookies.push(sessionIdCookie); 19 | } else { 20 | sessionId = sessionIDCookies[0].value; 21 | } 22 | 23 | request.addCookies(cookies); 24 | return ctx.next(); 25 | } 26 | } 27 | 28 | AuthInterceptor authInterceptor = new; 29 | 30 | @http:ServiceConfig { 31 | cors: { 32 | allowOrigins: ["http://localhost:3000"], 33 | allowCredentials: true 34 | } 35 | } 36 | service http:InterceptableService / on new http:Listener (9098) { 37 | 38 | public function createInterceptors() returns AuthInterceptor { 39 | return authInterceptor; 40 | } 41 | } 42 | ``` 43 | 44 | In ballerina, an HTTP resource is represented by a resource function. The function definition has all the information about the resource. The following resource function has the resource path of /cart and gets invoked for POST requests. The resource expects a payload with the `AddToCartRequest` record format and the cookie header. It could return responses with different HTTP error codes depending on various reasons. You can find more information about writing a REST service from the [learn page.](https://ballerina.io/learn/write-a-restful-api-with-ballerina/). 45 | 46 | ```bal 47 | resource function post cart(@http:Payload AddToCartRequest request, 48 | @http:Header {name: "Cookie"} string cookieHeader) 49 | returns http:Created|http:Unauthorized|http:BadRequest|error { 50 | http:Cookie|http:Unauthorized sessionIdCookie = getFromCookieHeader(SESSION_ID_COOKIE, cookieHeader); 51 | if sessionIdCookie is http:Unauthorized { 52 | return sessionIdCookie; 53 | } 54 | string userId = sessionIdCookie.value; 55 | stubs:Product|error product = getProduct(request.productId); 56 | if product is error { 57 | http:BadRequest badRequest = { 58 | body: string `invalid request ${product.message()}` 59 | }; 60 | return badRequest; 61 | } 62 | 63 | check insertItemToCart(userId, request.productId, request.quantity); 64 | 65 | http:Created response = { 66 | headers: { 67 | "Set-Cookie": sessionIdCookie.toStringValue() 68 | }, 69 | body: "item added to the cart" 70 | }; 71 | return response; 72 | } 73 | ``` 74 | -------------------------------------------------------------------------------- /frontend/docker/Config.toml: -------------------------------------------------------------------------------- 1 | catalogHost="catalog" 2 | cartHost="cart" 3 | recommendHost="recommendation" 4 | currencyHost="currency" 5 | shippingHost="shipping" 6 | checkoutHost="checkout" 7 | adHost="ads" 8 | 9 | [ballerina.observe] 10 | tracingEnabled=true 11 | tracingProvider="jaeger" 12 | 13 | [ballerinax.jaeger] 14 | agentHostname="localhost" 15 | agentPort=4317 16 | -------------------------------------------------------------------------------- /frontend/k8s/Config.toml: -------------------------------------------------------------------------------- 1 | catalogHost="catalog-service" 2 | cartHost="cart-service" 3 | recommendHost="recommendation-service" 4 | currencyHost="currency-service" 5 | shippingHost="shipping-service" 6 | checkoutHost="checkout-service" 7 | adHost="ads-service" 8 | currencyTimeout=3.0 9 | catalogTimeout=3.0 10 | cartTimeout=3.0 11 | shippingTimeout=3.0 12 | recommendationTimeout=3.0 13 | adTimeout=3.0 14 | checkoutTimeout=3.0 15 | 16 | [ballerina.observe] 17 | tracingEnabled=true 18 | tracingProvider="jaeger" 19 | 20 | [ballerinax.jaeger] 21 | agentHostname="localhost" 22 | agentPort=4317 23 | -------------------------------------------------------------------------------- /frontend/types.bal: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 2 | // 3 | // WSO2 LLC. licenses this file to you under the Apache License, 4 | // Version 2.0 (the "License"); you may not use this file except 5 | // in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, 11 | // software distributed under the License is distributed on an 12 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 13 | // KIND, either express or implied. See the License for the 14 | // specific language governing permissions and limitations 15 | // under the License. 16 | 17 | import ballerina/http; 18 | import wso2/client_stubs as stubs; 19 | 20 | //Request Records 21 | 22 | # Record containing the details of the item. 23 | # 24 | # + productId - product id 25 | # + quantity - product quantity 26 | public type AddToCartRequest record {| 27 | string productId; 28 | int quantity; 29 | |}; 30 | 31 | # Record containing details of the user and the card. 32 | # 33 | # + email - user's email 34 | # + streetAddress - user's street address 35 | # + zipCode - user's zip code 36 | # + city - user's city 37 | # + state - user's state 38 | # + country - user's country 39 | # + creditCardNumber - credit card number 40 | # + creditCardExpirationMonth - expiration month of the card 41 | # + creditCardExpirationYear - expiration year of the card 42 | # + creditCardCvv - cvv of the card 43 | public type CheckoutRequest record {| 44 | string email; 45 | string streetAddress; 46 | int zipCode; 47 | string city; 48 | string state; 49 | string country; 50 | string creditCardNumber; 51 | int creditCardExpirationMonth; 52 | int creditCardExpirationYear; 53 | int creditCardCvv; 54 | |}; 55 | 56 | //Response Records 57 | 58 | type CartItemView record { 59 | stubs:Product product; 60 | int quantity; 61 | string price; 62 | }; 63 | 64 | type MetadataResponse record {| 65 | *http:Ok; 66 | MetadataBody body; 67 | |}; 68 | 69 | type MetadataBody record {| 70 | [string, string] userCurrency; 71 | string[] currencies; 72 | int cartSize; 73 | boolean isCymbalBrand; 74 | |}; 75 | 76 | type ProductLocalized record {| 77 | *stubs:Product; 78 | string price; 79 | |}; 80 | 81 | type HomeResponse record {| 82 | *http:Ok; 83 | HomeBody body; 84 | |}; 85 | 86 | type HomeBody record {| 87 | ProductLocalized[] products; 88 | stubs:Ad ad; 89 | |}; 90 | 91 | type ProductResponse record {| 92 | *http:Ok; 93 | ProductBody body; 94 | |}; 95 | 96 | type ProductBody record {| 97 | ProductLocalized product; 98 | stubs:Product[] recommendations; 99 | stubs:Ad ad; 100 | |}; 101 | 102 | type CartResponse record {| 103 | *http:Ok; 104 | CartBody body; 105 | |}; 106 | 107 | type CartBody record {| 108 | stubs:Product[] recommendations; 109 | string shippingCost; 110 | string totalCost; 111 | CartItemView[] items; 112 | int[] expirationYears; 113 | |}; 114 | 115 | type CheckoutResponse record {| 116 | *http:Created; 117 | CheckoutBody body; 118 | |}; 119 | 120 | type CheckoutBody record {| 121 | stubs:OrderResult 'order; 122 | string totalPaid; 123 | stubs:Product[] recommendations; 124 | |}; 125 | -------------------------------------------------------------------------------- /frontend/utils.bal: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 2 | // 3 | // WSO2 LLC. licenses this file to you under the Apache License, 4 | // Version 2.0 (the "License"); you may not use this file except 5 | // in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, 11 | // software distributed under the License is distributed on an 12 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 13 | // KIND, either express or implied. See the License for the 14 | // specific language governing permissions and limitations 15 | // under the License. 16 | 17 | import ballerina/http; 18 | import ballerina/log; 19 | import ballerina/regex; 20 | import wso2/client_stubs as stubs; 21 | 22 | const COOKIE_PATH = "/"; 23 | const COOKIE_SPLIT_TOKEN = "="; 24 | const COOKIE_DELIMITER = "; "; 25 | 26 | isolated function getFromCookieHeader(string cookieName, string cookieStr) returns http:Cookie|http:Unauthorized { 27 | http:Cookie[] cookies = parseCookieHeader(cookieStr); 28 | http:Cookie[] filteredCookies = cookies.filter(cookie => cookie.name == cookieName); 29 | if filteredCookies.length() == 1 { 30 | return filteredCookies[0]; 31 | } 32 | return { 33 | body: string `${cookieName} cookie is not available.` 34 | }; 35 | } 36 | 37 | isolated function parseCookieHeader(string cookieStringValue) returns http:Cookie[] { 38 | http:Cookie[] cookiesInRequest = []; 39 | string[] nameValuePairs = regex:split(cookieStringValue, COOKIE_DELIMITER); 40 | foreach string pair in nameValuePairs { 41 | if regex:matches(pair, "^([^=]+)=.*$") { 42 | string[] nameValue = regex:split(pair, COOKIE_SPLIT_TOKEN); 43 | http:Cookie cookie = new (nameValue[0], nameValue.length() > 1 ? nameValue[1] : "", path = COOKIE_PATH); 44 | cookiesInRequest.push(cookie); 45 | } else { 46 | log:printError(string `Invalid cookie: ${pair}, which must be in the format as [{name}=].`); 47 | } 48 | } 49 | return cookiesInRequest; 50 | } 51 | 52 | isolated function getCartSize(stubs:Cart cart) returns int { 53 | int cartSize = 0; 54 | foreach stubs:CartItem {quantity} in cart.items { 55 | cartSize += quantity; 56 | } 57 | return cartSize; 58 | } 59 | 60 | isolated function toProductLocalized(stubs:Product product, string price) returns ProductLocalized { 61 | return { 62 | ...product, 63 | price 64 | }; 65 | } 66 | -------------------------------------------------------------------------------- /images/architecture-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ballerina-guides/gcp-microservices-demo/1d803785e9ee93ba134ca1633cb568e3df9890a2/images/architecture-diagram.png -------------------------------------------------------------------------------- /images/drop-down-services-spans.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ballerina-guides/gcp-microservices-demo/1d803785e9ee93ba134ca1633cb568e3df9890a2/images/drop-down-services-spans.png -------------------------------------------------------------------------------- /images/project-design.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ballerina-guides/gcp-microservices-demo/1d803785e9ee93ba134ca1633cb568e3df9890a2/images/project-design.png -------------------------------------------------------------------------------- /images/shipping-service.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ballerina-guides/gcp-microservices-demo/1d803785e9ee93ba134ca1633cb568e3df9890a2/images/shipping-service.png -------------------------------------------------------------------------------- /images/tracing-spans.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ballerina-guides/gcp-microservices-demo/1d803785e9ee93ba134ca1633cb568e3df9890a2/images/tracing-spans.png -------------------------------------------------------------------------------- /issue_template.md: -------------------------------------------------------------------------------- 1 | **Description:** 2 | 3 | 4 | **Suggested Labels:** 5 | 6 | 7 | **Suggested Assignees:** 8 | 9 | 10 | **Affected Product Version:** 11 | 12 | **OS, DB, other environment details and versions:** 13 | 14 | **Steps to reproduce:** 15 | 16 | 17 | **Related Issues:** 18 | -------------------------------------------------------------------------------- /kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - adservice/target/kubernetes/adservice/adservice.yaml 3 | - cartservice/target/kubernetes/cartservice/cartservice.yaml 4 | - checkoutservice/target/kubernetes/checkoutservice/checkoutservice.yaml 5 | - currencyservice/target/kubernetes/currencyservice/currencyservice.yaml 6 | - emailservice/target/kubernetes/emailservice/emailservice.yaml 7 | - frontend/target/kubernetes/frontend/frontend.yaml 8 | - paymentservice/target/kubernetes/paymentservice/paymentservice.yaml 9 | - productcatalogservice/target/kubernetes/productcatalogservice/productcatalogservice.yaml 10 | - recommendationservice/target/kubernetes/recommendationservice/recommendationservice.yaml 11 | - shippingservice/target/kubernetes/shippingservice/shippingservice.yaml 12 | 13 | patchesStrategicMerge: 14 | - secret-env-patch.yaml 15 | -------------------------------------------------------------------------------- /money/.devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "ballerina/ballerina-devcontainer:2201.2.2", 3 | "extensions": ["WSO2.ballerina"], 4 | } 5 | -------------------------------------------------------------------------------- /money/Ballerina.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | org = "wso2" 3 | name = "money" 4 | version = "0.1.0" 5 | distribution = "2201.2.3" 6 | 7 | [build-options] 8 | observabilityIncluded = true 9 | 10 | [[dependency]] 11 | org = "wso2" 12 | name = "client_stubs" 13 | version = "0.1.0" 14 | repository = "local" 15 | -------------------------------------------------------------------------------- /money/Package.md: -------------------------------------------------------------------------------- 1 | # GCP Micro-Services Demo Money module 2 | 3 | This module contains the functions related to `stubs:Money` values. 4 | -------------------------------------------------------------------------------- /paymentservice/.devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "ballerina/ballerina-devcontainer:2201.0.0", 3 | "extensions": ["WSO2.ballerina"], 4 | } 5 | -------------------------------------------------------------------------------- /paymentservice/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /paymentservice/Ballerina.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | org = "wso2" 3 | name = "paymentservice" 4 | version = "0.1.0" 5 | distribution = "2201.2.3" 6 | 7 | [build-options] 8 | observabilityIncluded = true 9 | 10 | [[dependency]] 11 | org = "wso2" 12 | name = "client_stubs" 13 | version = "0.1.0" 14 | repository = "local" 15 | -------------------------------------------------------------------------------- /paymentservice/Cloud.toml: -------------------------------------------------------------------------------- 1 | [container.image] 2 | name="payment-service" 3 | repository="wso2inc" 4 | tag="v0.1.0" 5 | 6 | [cloud.deployment] 7 | internal_domain_name="payment-service" 8 | -------------------------------------------------------------------------------- /paymentservice/README.md: -------------------------------------------------------------------------------- 1 | # Payment Service 2 | 3 | Payment Service is responsible for validating the card details and sending a mock payment id. The validation is done by checking length, performing Luhn algorithm validation, validating the card company, and checking the expiry date. This microservice shows some usage of low-level operations to implement the validation algorithm. 4 | 5 | 6 | ```bal 7 | isolated function isLuhnValid(string cardNumber) returns boolean|error { 8 | int digits = cardNumber.length(); 9 | int oddOrEven = digits & 1; 10 | int sum = 0; 11 | 12 | foreach int count in 0 ..< digits { 13 | int digit = 0; 14 | digit = check int:fromString(cardNumber[count]); 15 | 16 | if ((count & 1) ^ oddOrEven) == 0 { 17 | digit *= 2; 18 | if digit > 9 { 19 | digit -= 9; 20 | } 21 | } 22 | sum += digit; 23 | } 24 | return sum != 0 && (sum % 10 == 0); 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /paymentservice/card_validator.bal: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 2 | // 3 | // WSO2 LLC. licenses this file to you under the Apache License, 4 | // Version 2.0 (the "License"); you may not use this file except 5 | // in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, 11 | // software distributed under the License is distributed on an 12 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 13 | // KIND, either express or implied. See the License for the 14 | // specific language governing permissions and limitations 15 | // under the License. 16 | 17 | import ballerina/regex; 18 | import ballerina/time; 19 | 20 | type CardValidationError distinct error; 21 | 22 | enum CardType { 23 | VISA = "VISA", 24 | MASTER_CARD = "MASTERCARD" 25 | } 26 | 27 | type CardTypeInfo record {| 28 | CardType cardType; 29 | string pattern; 30 | |}; 31 | 32 | final CardTypeInfo[] & readonly cardDetails = [ 33 | { 34 | cardType: VISA, 35 | pattern: "^4[0-9]{12}(?:[0-9]{3})?$" 36 | }, 37 | { 38 | cardType: MASTER_CARD, 39 | pattern: "^5[1-5][0-9]{14}$" 40 | } 41 | ]; 42 | 43 | isolated function getCardType(string cardNumber) returns CardType|error { 44 | string formattedCardNumber = regex:replaceAll(cardNumber, "[^0-9]+", ""); 45 | int cardNumberLength = formattedCardNumber.length(); 46 | if cardNumberLength < 13 || cardNumberLength > 19 { 47 | return error CardValidationError("Credit card info is invalid: failed length check"); 48 | } 49 | 50 | if !check isLuhnValid(formattedCardNumber) { 51 | return error CardValidationError("Credit card info is invalid: failed luhn check"); 52 | } 53 | 54 | CardType[] cardTypes = from var cardTypeInfo in cardDetails 55 | where regex:matches(formattedCardNumber, cardTypeInfo.pattern) 56 | limit 1 57 | select cardTypeInfo.cardType; 58 | 59 | if cardTypes.length() == 0 { 60 | return error CardValidationError("Sorry, we cannot process the credit card. " + 61 | "Only VISA or MasterCard is accepted."); 62 | } 63 | return cardTypes[0]; 64 | } 65 | 66 | isolated function isLuhnValid(string cardNumber) returns boolean|error { 67 | int digits = cardNumber.length(); 68 | int oddOrEven = digits & 1; 69 | int sum = 0; 70 | 71 | foreach int count in 0 ..< digits { 72 | int digit = 0; 73 | digit = check int:fromString(cardNumber[count]); 74 | 75 | if ((count & 1) ^ oddOrEven) == 0 { 76 | digit *= 2; 77 | if digit > 9 { 78 | digit -= 9; 79 | } 80 | } 81 | sum += digit; 82 | } 83 | return sum != 0 && (sum % 10 == 0); 84 | } 85 | 86 | isolated function validateCardExpiration(string cardNumber, int year, int month) returns error? { 87 | if isExpired(year, month) { 88 | return error CardValidationError( 89 | string `Your credit card (ending ${cardNumber.substring(cardNumber.length() - 4)}) 90 | expired on ${month}/${year}`); 91 | } 92 | } 93 | 94 | isolated function isExpired(int expireYear, int expireMonth) returns boolean { 95 | time:Civil currentTime = time:utcToCivil(time:utcNow()); 96 | int year = currentTime.year; 97 | 98 | if year > expireYear { 99 | return true; 100 | } 101 | return year == expireYear && currentTime.month > expireMonth; 102 | } 103 | -------------------------------------------------------------------------------- /paymentservice/docker/Config.toml: -------------------------------------------------------------------------------- 1 | [ballerina.observe] 2 | tracingEnabled=true 3 | tracingProvider="jaeger" 4 | 5 | [ballerinax.jaeger] 6 | agentHostname="localhost" 7 | agentPort=4317 8 | -------------------------------------------------------------------------------- /paymentservice/k8s/Config.toml: -------------------------------------------------------------------------------- 1 | [ballerina.observe] 2 | tracingEnabled=true 3 | tracingProvider="jaeger" 4 | 5 | [ballerinax.jaeger] 6 | agentHostname="localhost" 7 | agentPort=4317 8 | -------------------------------------------------------------------------------- /paymentservice/payment_service.bal: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 2 | // 3 | // WSO2 LLC. licenses this file to you under the Apache License, 4 | // Version 2.0 (the "License"); you may not use this file except 5 | // in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, 11 | // software distributed under the License is distributed on an 12 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 13 | // KIND, either express or implied. See the License for the 14 | // specific language governing permissions and limitations 15 | // under the License. 16 | 17 | import ballerina/grpc; 18 | import ballerina/log; 19 | import ballerina/uuid; 20 | import ballerinax/jaeger as _; 21 | import wso2/client_stubs as stubs; 22 | 23 | # This service validates the card details (using the Luhn algorithm) against the supported card providers and charges the card. 24 | @display { 25 | label: "Payment", 26 | id: "payment" 27 | } 28 | @grpc:Descriptor {value: stubs:DEMO_DESC} 29 | service "PaymentService" on new grpc:Listener(9096) { 30 | 31 | function init() { 32 | log:printInfo("Payment service gRPC server started."); 33 | } 34 | 35 | # Validate and charge the amount from the given card. 36 | # 37 | # + request - `ChargeRequest` containing the card details and the amount to charged 38 | # + return - `ChargeResponse` with the transaction id or an error 39 | remote function Charge(stubs:ChargeRequest request) returns stubs:ChargeResponse|error { 40 | log:printInfo(string `Received card payment request with ${request.toString()}`); 41 | 42 | var {credit_card_number: cardNumber, credit_card_expiration_year: year, 43 | credit_card_expiration_month: month} = request.credit_card; 44 | CardType cardType = check getCardType(cardNumber); 45 | check validateCardExpiration(cardNumber, year, month); 46 | 47 | string lastFourDigits = cardNumber.substring(cardNumber.length() - 4); 48 | string amount = let var {currency_code, units, nanos} = request.amount in 49 | string `${currency_code}${units}.${nanos}`; 50 | log:printInfo(string `Payment transaction processed: ${cardType} ending ${lastFourDigits}, Amount: ${amount}`); 51 | 52 | return {transaction_id: uuid:createType1AsString()}; 53 | } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /paymentservice/tests/test.bal: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 2 | // 3 | // WSO2 LLC. licenses this file to you under the Apache License, 4 | // Version 2.0 (the "License"); you may not use this file except 5 | // in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, 11 | // software distributed under the License is distributed on an 12 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 13 | // KIND, either express or implied. See the License for the 14 | // specific language governing permissions and limitations 15 | // under the License. 16 | 17 | import ballerina/test; 18 | import wso2/client_stubs as stubs; 19 | 20 | @test:Config {} 21 | function paymentTest() returns error? { 22 | stubs:PaymentServiceClient ep = check new ("http://localhost:9096"); 23 | stubs:ChargeRequest req = { 24 | amount: { 25 | currency_code: "USD", 26 | units: 5, 27 | nanos: 990000000 28 | }, 29 | credit_card: { 30 | credit_card_number: "4432-8015-6152-0454", 31 | credit_card_cvv: 123, 32 | credit_card_expiration_year: 2023, 33 | credit_card_expiration_month: 10 34 | } 35 | }; 36 | stubs:ChargeResponse chargeResponse = check ep->Charge(req); 37 | test:assertTrue(chargeResponse.transaction_id.length() > 1); 38 | } 39 | -------------------------------------------------------------------------------- /productcatalogservice/.devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "ballerina/ballerina-devcontainer:2201.0.0", 3 | "extensions": ["WSO2.ballerina"], 4 | } 5 | -------------------------------------------------------------------------------- /productcatalogservice/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /productcatalogservice/Ballerina.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | org = "wso2" 3 | name = "productcatalogservice" 4 | version = "0.1.0" 5 | distribution = "2201.2.3" 6 | 7 | [build-options] 8 | observabilityIncluded = true 9 | 10 | [[dependency]] 11 | org = "wso2" 12 | name = "client_stubs" 13 | version = "0.1.0" 14 | repository = "local" 15 | -------------------------------------------------------------------------------- /productcatalogservice/Cloud.toml: -------------------------------------------------------------------------------- 1 | [container.image] 2 | name="catalog-service" 3 | repository="wso2inc" 4 | tag="v0.1.0" 5 | 6 | [[container.copy.files]] 7 | sourceFile="./resources/products.json" 8 | target="/home/ballerina/resources/products.json" 9 | 10 | [cloud.deployment] 11 | internal_domain_name="catalog-service" 12 | -------------------------------------------------------------------------------- /productcatalogservice/README.md: -------------------------------------------------------------------------------- 1 | # Product Catalog Service 2 | 3 | The Catalog Service maintains a list of products available in the store. The product list will be read from JSON and converted to a readonly array of `Product` when the service is initialized. The `Catalog Service` has the Search Product capability. This feature is implemented using ballerina query expressions. It allows you to write SQL like queries to filter data from the array. You can find more details about query expressions in this [blog](https://dzone.com/articles/language-integrated-queries-in-ballerina). 4 | 5 | ```bal 6 | configurable string productJsonPath = "./resources/products.json"; 7 | 8 | @grpc:Descriptor {value: DEMO_DESC} 9 | service "ProductCatalogService" on new grpc:Listener(9091) { 10 | private final stubs:Product[] & readonly products; 11 | 12 | function init() returns error? { 13 | json|error productsJson = io:fileReadJson(productJsonPath); 14 | self.products = check parseProductJson(productsJson); 15 | } 16 | 17 | remote function ListProducts(stubs:Empty request) returns stubs:ListProductsResponse { 18 | return {products: self.products}; 19 | } 20 | 21 | remote function GetProduct(stubs:GetProductRequest request) returns stubs:Product|grpc:NotFoundError|error { 22 | foreach stubs:Product product in self.products { 23 | if product.id == request.id { 24 | return product; 25 | } 26 | } 27 | return error grpc:NotFoundError(string `no product with ID ${request.id}`); 28 | } 29 | 30 | remote function SearchProducts(stubs:SearchProductsRequest request) returns stubs:SearchProductsResponse { 31 | return { 32 | results: from stubs:Product product in self.products 33 | where isProductRelated(product, request.query) 34 | select product 35 | }; 36 | } 37 | } 38 | ``` 39 | -------------------------------------------------------------------------------- /productcatalogservice/catalog_service.bal: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 2 | // 3 | // WSO2 LLC. licenses this file to you under the Apache License, 4 | // Version 2.0 (the "License"); you may not use this file except 5 | // in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, 11 | // software distributed under the License is distributed on an 12 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 13 | // KIND, either express or implied. See the License for the 14 | // specific language governing permissions and limitations 15 | // under the License. 16 | 17 | import ballerina/grpc; 18 | import ballerina/io; 19 | import ballerina/log; 20 | import ballerinax/jaeger as _; 21 | import wso2/client_stubs as stubs; 22 | 23 | configurable string productJsonPath = "./resources/products.json"; 24 | 25 | # Reads a list of products from a JSON file and provides the ability to search products and get them individually. 26 | @display { 27 | label: "Catalog", 28 | id: "catalog" 29 | } 30 | @grpc:Descriptor {value: stubs:DEMO_DESC} 31 | service "ProductCatalogService" on new grpc:Listener(9091) { 32 | private final stubs:Product[] & readonly products; 33 | 34 | function init() returns error? { 35 | json|error productsJson = io:fileReadJson(productJsonPath); 36 | if productsJson is error { 37 | log:printError("Failed to open product catalog json file: ", productsJson); 38 | return productsJson; 39 | } 40 | log:printInfo("Successfully read product catalog json"); 41 | self.products = check parseProductJson(productsJson); 42 | log:printInfo("Catalog service gRPC server started."); 43 | } 44 | 45 | # Provides a set of products. 46 | # 47 | # + request - an empty request 48 | # + return - `ListProductsResponse` containing a `Product[]` 49 | remote function ListProducts(stubs:Empty request) returns stubs:ListProductsResponse { 50 | return {products: self.products}; 51 | } 52 | 53 | # Provides a specific product based on the product id. 54 | # 55 | # + request - `GetProductRequest` containing the product id 56 | # + return - `Product` related to the required id or an error 57 | remote function GetProduct(stubs:GetProductRequest request) returns stubs:Product|grpc:NotFoundError|error { 58 | foreach stubs:Product product in self.products { 59 | if product.id == request.id { 60 | return product; 61 | } 62 | } 63 | return error grpc:NotFoundError(string `no product with ID ${request.id}`); 64 | } 65 | 66 | # Provides a list of products related to a search query. 67 | # 68 | # + request - `SearchProductsRequest` containing the search query 69 | # + return - `SearchProductsResponse` containing the matching products 70 | remote function SearchProducts(stubs:SearchProductsRequest request) returns stubs:SearchProductsResponse { 71 | return { 72 | results: from stubs:Product product in self.products 73 | where isProductRelated(product, request.query) 74 | select product 75 | }; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /productcatalogservice/docker/Config.toml: -------------------------------------------------------------------------------- 1 | [ballerina.observe] 2 | tracingEnabled=true 3 | tracingProvider="jaeger" 4 | 5 | [ballerinax.jaeger] 6 | agentHostname="localhost" 7 | agentPort=4317 8 | -------------------------------------------------------------------------------- /productcatalogservice/k8s/Config.toml: -------------------------------------------------------------------------------- 1 | [ballerina.observe] 2 | tracingEnabled=true 3 | tracingProvider="jaeger" 4 | 5 | [ballerinax.jaeger] 6 | agentHostname="localhost" 7 | agentPort=4317 8 | -------------------------------------------------------------------------------- /productcatalogservice/resources/products.json: -------------------------------------------------------------------------------- 1 | { 2 | "products": [ 3 | { 4 | "id": "OLJCESPC7Z", 5 | "name": "Sunglasses", 6 | "description": "Add a modern touch to your outfits with these sleek aviator sunglasses.", 7 | "picture": "/static/img/products/sunglasses.jpg", 8 | "priceUsd": { 9 | "currencyCode": "USD", 10 | "units": 19, 11 | "nanos": 990000000 12 | }, 13 | "categories": ["accessories"] 14 | }, 15 | { 16 | "id": "66VCHSJNUP", 17 | "name": "Tank Top", 18 | "description": "Perfectly cropped cotton tank, with a scooped neckline.", 19 | "picture": "/static/img/products/tank-top.jpg", 20 | "priceUsd": { 21 | "currencyCode": "USD", 22 | "units": 18, 23 | "nanos": 990000000 24 | }, 25 | "categories": ["clothing", "tops"] 26 | }, 27 | { 28 | "id": "1YMWWN1N4O", 29 | "name": "Watch", 30 | "description": "This gold-tone stainless steel watch will work with most of your outfits.", 31 | "picture": "/static/img/products/watch.jpg", 32 | "priceUsd": { 33 | "currencyCode": "USD", 34 | "units": 109, 35 | "nanos": 990000000 36 | }, 37 | "categories": ["accessories"] 38 | }, 39 | { 40 | "id": "L9ECAV7KIM", 41 | "name": "Loafers", 42 | "description": "A neat addition to your summer wardrobe.", 43 | "picture": "/static/img/products/loafers.jpg", 44 | "priceUsd": { 45 | "currencyCode": "USD", 46 | "units": 89, 47 | "nanos": 990000000 48 | }, 49 | "categories": ["footwear"] 50 | }, 51 | { 52 | "id": "2ZYFJ3GM2N", 53 | "name": "Hairdryer", 54 | "description": "This lightweight hairdryer has 3 heat and speed settings. It's perfect for travel.", 55 | "picture": "/static/img/products/hairdryer.jpg", 56 | "priceUsd": { 57 | "currencyCode": "USD", 58 | "units": 24, 59 | "nanos": 990000000 60 | }, 61 | "categories": ["hair", "beauty"] 62 | }, 63 | { 64 | "id": "0PUK6V6EV0", 65 | "name": "Candle Holder", 66 | "description": "This small but intricate candle holder is an excellent gift.", 67 | "picture": "/static/img/products/candle-holder.jpg", 68 | "priceUsd": { 69 | "currencyCode": "USD", 70 | "units": 18, 71 | "nanos": 990000000 72 | }, 73 | "categories": ["decor", "home"] 74 | }, 75 | { 76 | "id": "LS4PSXUNUM", 77 | "name": "Salt & Pepper Shakers", 78 | "description": "Add some flavor to your kitchen.", 79 | "picture": "/static/img/products/salt-and-pepper-shakers.jpg", 80 | "priceUsd": { 81 | "currencyCode": "USD", 82 | "units": 18, 83 | "nanos": 490000000 84 | }, 85 | "categories": ["kitchen"] 86 | }, 87 | { 88 | "id": "9SIQT8TOJO", 89 | "name": "Bamboo Glass Jar", 90 | "description": "This bamboo glass jar can hold 57 oz (1.7 l) and is perfect for any kitchen.", 91 | "picture": "/static/img/products/bamboo-glass-jar.jpg", 92 | "priceUsd": { 93 | "currencyCode": "USD", 94 | "units": 5, 95 | "nanos": 490000000 96 | }, 97 | "categories": ["kitchen"] 98 | }, 99 | { 100 | "id": "6E92ZMYYFZ", 101 | "name": "Mug", 102 | "description": "A simple mug with a mustard interior.", 103 | "picture": "/static/img/products/mug.jpg", 104 | "priceUsd": { 105 | "currencyCode": "USD", 106 | "units": 8, 107 | "nanos": 990000000 108 | }, 109 | "categories": ["kitchen"] 110 | } 111 | ] 112 | } 113 | -------------------------------------------------------------------------------- /productcatalogservice/tests/test.bal: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 2 | // 3 | // WSO2 LLC. licenses this file to you under the Apache License, 4 | // Version 2.0 (the "License"); you may not use this file except 5 | // in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, 11 | // software distributed under the License is distributed on an 12 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 13 | // KIND, either express or implied. See the License for the 14 | // specific language governing permissions and limitations 15 | // under the License. 16 | 17 | import ballerina/test; 18 | import wso2/client_stubs as stubs; 19 | 20 | @test:Config {} 21 | function catalogTest() returns error? { 22 | stubs:ProductCatalogServiceClient ep = check new ("http://localhost:9091"); 23 | stubs:Empty req = {}; 24 | stubs:ListProductsResponse listProducts = check ep->ListProducts(req); 25 | test:assertEquals(listProducts.products.length(), 9); 26 | } 27 | -------------------------------------------------------------------------------- /productcatalogservice/utils.bal: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 2 | // 3 | // WSO2 LLC. licenses this file to you under the Apache License, 4 | // Version 2.0 (the "License"); you may not use this file except 5 | // in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, 11 | // software distributed under the License is distributed on an 12 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 13 | // KIND, either express or implied. See the License for the 14 | // specific language governing permissions and limitations 15 | // under the License. 16 | 17 | import ballerina/log; 18 | import wso2/client_stubs as stubs; 19 | 20 | type Price record {| 21 | string currencyCode; 22 | int units; 23 | int nanos; 24 | |}; 25 | 26 | type JsonProduct readonly & record {| 27 | string id; 28 | string name; 29 | string description; 30 | string picture; 31 | Price priceUsd; 32 | string[] categories; 33 | |}; 34 | 35 | isolated function parseProductJson(json jsonContents) returns stubs:Product[] & readonly|error { 36 | json|error productsJson = jsonContents.products; 37 | if productsJson is error { 38 | log:printError("Failed to parse the catalog JSON: ", productsJson); 39 | return productsJson; 40 | } 41 | if productsJson !is json[] { 42 | return error("product array is not found"); 43 | } 44 | 45 | JsonProduct[] jsonProducts = check productsJson.fromJsonWithType(); 46 | return from var {id, name, description, picture, priceUsd, categories} in jsonProducts 47 | select { 48 | id, 49 | name, 50 | description, 51 | picture, 52 | price_usd: parseUsdPrice(priceUsd), 53 | categories 54 | }.cloneReadOnly(); 55 | } 56 | 57 | isolated function parseUsdPrice(Price usdPrice) returns stubs:Money & readonly { 58 | return { 59 | currency_code: usdPrice.currencyCode, 60 | units: usdPrice.units, 61 | nanos: usdPrice.nanos 62 | }; 63 | } 64 | 65 | isolated function isProductRelated(stubs:Product product, string query) returns boolean { 66 | string queryLowercase = query.toLowerAscii(); 67 | return product.name.toLowerAscii().includes(queryLowercase) || 68 | product.description.toLowerAscii().includes(queryLowercase); 69 | } 70 | -------------------------------------------------------------------------------- /project.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "adservice" 5 | }, 6 | { 7 | "path": "cartservice" 8 | }, 9 | { 10 | "path": "checkoutservice" 11 | }, 12 | { 13 | "path": "currencyservice" 14 | }, 15 | { 16 | "path": "emailservice" 17 | }, 18 | { 19 | "path": "frontend" 20 | }, 21 | { 22 | "path": "paymentservice" 23 | }, 24 | { 25 | "path": "productcatalogservice" 26 | }, 27 | { 28 | "path": "recommendationservice" 29 | }, 30 | { 31 | "path": "shippingservice" 32 | }, 33 | { 34 | "path": "client_stubs" 35 | }, 36 | { 37 | "path": "ui" 38 | } 39 | ], 40 | "settings": {} 41 | } 42 | -------------------------------------------------------------------------------- /pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Purpose 2 | > Describe the problems, issues, or needs driving this feature/fix and include links to related issues in the following format: Resolves issue1, issue2, etc. 3 | 4 | ## Goals 5 | > Describe the solutions that this feature/fix will introduce to resolve the problems described above 6 | 7 | ## Approach 8 | > Describe how you are implementing the solutions. Include an animated GIF or screenshot if the change affects the UI (email documentation@wso2.com to review all UI text). Include a link to a Markdown file or Google doc if the feature write-up is too long to paste here. 9 | 10 | ## User stories 11 | > Summary of user stories addressed by this change> 12 | 13 | ## Release note 14 | > Brief description of the new feature or bug fix as it will appear in the release notes 15 | 16 | ## Documentation 17 | > Link(s) to product documentation that addresses the changes of this PR. If no doc impact, enter “N/A” plus brief explanation of why there’s no doc impact 18 | 19 | ## Training 20 | > Link to the PR for changes to the training content in https://github.com/wso2/WSO2-Training, if applicable 21 | 22 | ## Certification 23 | > Type “Sent” when you have provided new/updated certification questions, plus four answers for each question (correct answer highlighted in bold), based on this change. Certification questions/answers should be sent to certification@wso2.com and NOT pasted in this PR. If there is no impact on certification exams, type “N/A” and explain why. 24 | 25 | ## Marketing 26 | > Link to drafts of marketing content that will describe and promote this feature, including product page changes, technical articles, blog posts, videos, etc., if applicable 27 | 28 | ## Automation tests 29 | - Unit tests 30 | > Code coverage information 31 | - Integration tests 32 | > Details about the test cases and coverage 33 | 34 | ## Security checks 35 | - Followed secure coding standards in http://wso2.com/technical-reports/wso2-secure-engineering-guidelines? yes/no 36 | - Ran FindSecurityBugs plugin and verified report? yes/no 37 | - Confirmed that this PR doesn't commit any keys, passwords, tokens, usernames, or other secrets? yes/no 38 | 39 | ## Samples 40 | > Provide high-level details about the samples related to this feature 41 | 42 | ## Related PRs 43 | > List any other related PRs 44 | 45 | ## Migrations (if applicable) 46 | > Describe migration steps and platforms on which migration has been tested 47 | 48 | ## Test environment 49 | > List all JDK versions, operating systems, databases, and browser/versions on which this feature/fix was tested 50 | 51 | ## Learning 52 | > Describe the research phase and any blog posts, patterns, libraries, or add-ons you used to solve the problem. -------------------------------------------------------------------------------- /recommendationservice/.devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "ballerina/ballerina-devcontainer:2201.0.0", 3 | "extensions": ["WSO2.ballerina"], 4 | } 5 | -------------------------------------------------------------------------------- /recommendationservice/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /recommendationservice/Ballerina.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | org = "wso2" 3 | name = "recommendationservice" 4 | version = "0.1.0" 5 | distribution = "2201.2.3" 6 | 7 | [build-options] 8 | observabilityIncluded = true 9 | 10 | [[dependency]] 11 | org = "wso2" 12 | name = "client_stubs" 13 | version = "0.1.0" 14 | repository = "local" 15 | 16 | -------------------------------------------------------------------------------- /recommendationservice/Cloud.toml: -------------------------------------------------------------------------------- 1 | [container.image] 2 | name="recommendation-service" 3 | repository="wso2inc" 4 | tag="v0.1.0" 5 | 6 | [cloud.deployment] 7 | internal_domain_name="recommendation-service" 8 | 9 | [[cloud.config.files]] 10 | file="./k8s/Config.toml" 11 | name="recommend-config" 12 | -------------------------------------------------------------------------------- /recommendationservice/README.md: -------------------------------------------------------------------------------- 1 | # Recommendation Service 2 | 3 | `Recommendation Service` simply calls the `Catalog Service` and returns a set of product which is not included in the user's cart. For this filtration also we make use of query expressions. 4 | 5 | ```bal 6 | remote function ListRecommendations(stubs:ListRecommendationsRequest request) 7 | returns stubs:ListRecommendationsResponse|error { 8 | 9 | stubs:ListProductsResponse|grpc:Error listProducts = self.catalogClient->ListProducts({}); 10 | if listProducts is grpc:Error { 11 | return error grpc:InternalError("Failed to get list of products from catalog service", listProducts); 12 | } 13 | 14 | return { 15 | product_ids: from stubs:Product product in listProducts.products 16 | let string productId = product.id 17 | where request.product_ids.indexOf(productId) is () 18 | limit 5 19 | select productId 20 | }; 21 | } 22 | ``` -------------------------------------------------------------------------------- /recommendationservice/docker/Config.toml: -------------------------------------------------------------------------------- 1 | catalogHost="catalog" 2 | 3 | [ballerina.observe] 4 | tracingEnabled=true 5 | tracingProvider="jaeger" 6 | 7 | [ballerinax.jaeger] 8 | agentHostname="localhost" 9 | agentPort=4317 10 | -------------------------------------------------------------------------------- /recommendationservice/k8s/Config.toml: -------------------------------------------------------------------------------- 1 | catalogHost="catalog-service" 2 | catalogTimeout=3.0 3 | 4 | [ballerina.observe] 5 | tracingEnabled=true 6 | tracingProvider="jaeger" 7 | 8 | [ballerinax.jaeger] 9 | agentHostname="localhost" 10 | agentPort=4317 11 | -------------------------------------------------------------------------------- /recommendationservice/recommendation_service.bal: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 2 | // 3 | // WSO2 LLC. licenses this file to you under the Apache License, 4 | // Version 2.0 (the "License"); you may not use this file except 5 | // in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, 11 | // software distributed under the License is distributed on an 12 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 13 | // KIND, either express or implied. See the License for the 14 | // specific language governing permissions and limitations 15 | // under the License. 16 | 17 | import ballerina/grpc; 18 | import ballerina/log; 19 | import ballerinax/jaeger as _; 20 | import wso2/client_stubs as stubs; 21 | 22 | configurable string catalogHost = "localhost"; 23 | configurable decimal catalogTimeout = 3; 24 | 25 | # Recommends other products based on the items added to the user’s cart. 26 | @display { 27 | label: "Recommendation", 28 | id: "recommendation" 29 | } 30 | @grpc:Descriptor {value: stubs:DEMO_DESC} 31 | service "RecommendationService" on new grpc:Listener(9090) { 32 | @display { 33 | label: "Catalog", 34 | id: "catalog" 35 | } 36 | private final stubs:ProductCatalogServiceClient catalogClient; 37 | 38 | function init() returns error? { 39 | self.catalogClient = check new (string `http://${catalogHost}:9091`, timeout = catalogTimeout); 40 | log:printInfo(string `Product catalog address: http://${catalogHost}:9091`); 41 | log:printInfo("Recommendation service gRPC server started."); 42 | } 43 | 44 | # Provides a product list according to the request. 45 | # 46 | # + request - `ListRecommendationsRequest` containing product ids 47 | # + return - `ListRecommendationsResponse` containing the recommended product ids 48 | remote function ListRecommendations(stubs:ListRecommendationsRequest request) 49 | returns stubs:ListRecommendationsResponse|error { 50 | log:printInfo(string `Received list recommendations request with product_ids=${request.product_ids.toString()}`); 51 | stubs:ListProductsResponse|grpc:Error listProducts = self.catalogClient->ListProducts({}); 52 | if listProducts is grpc:Error { 53 | log:printError("Failed to call ListProducts of catalog service", listProducts); 54 | return error grpc:InternalError("Failed to get list of products from catalog service", listProducts); 55 | } 56 | 57 | return { 58 | product_ids: from stubs:Product product in listProducts.products 59 | let string productId = product.id 60 | where request.product_ids.indexOf(productId) is () 61 | limit 5 62 | select productId 63 | }; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /recommendationservice/tests/Config.toml: -------------------------------------------------------------------------------- 1 | catalogHost="localhost" 2 | -------------------------------------------------------------------------------- /recommendationservice/tests/test.bal: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 2 | // 3 | // WSO2 LLC. licenses this file to you under the Apache License, 4 | // Version 2.0 (the "License"); you may not use this file except 5 | // in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, 11 | // software distributed under the License is distributed on an 12 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 13 | // KIND, either express or implied. See the License for the 14 | // specific language governing permissions and limitations 15 | // under the License. 16 | 17 | import ballerina/test; 18 | import ballerina/grpc; 19 | import wso2/client_stubs as stubs; 20 | 21 | @grpc:Descriptor {value: stubs:DEMO_DESC} 22 | service "ProductCatalogService" on new grpc:Listener(9091) { 23 | remote function ListProducts(stubs:Empty value) returns stubs:ListProductsResponse { 24 | return { 25 | products: [ 26 | { 27 | id: "test id", 28 | categories: ["watch", "clothes"], 29 | description: "Test description", 30 | name: "test name", 31 | picture: "", 32 | price_usd: { 33 | currency_code: "USD", 34 | nanos: 900000000, 35 | units: 5 36 | } 37 | } 38 | ] 39 | }; 40 | } 41 | 42 | remote function GetProduct(stubs:GetProductRequest value) returns stubs:Product|error { 43 | return error("method not implemented"); 44 | } 45 | 46 | remote function SearchProducts(stubs:SearchProductsRequest value) returns stubs:SearchProductsResponse|error { 47 | return error("method not implemented"); 48 | } 49 | } 50 | 51 | @test:Config {} 52 | function recommendTest() returns error? { 53 | stubs:RecommendationServiceClient ep = check new ("http://localhost:9090"); 54 | stubs:ListRecommendationsRequest req = { 55 | user_id: "1", 56 | product_ids: ["2ZYFJ3GM2N", "LS4PSXUNUM"] 57 | }; 58 | stubs:ListRecommendationsResponse listProducts = check ep->ListRecommendations(req); 59 | test:assertEquals(listProducts.product_ids.length(), 1); 60 | } 61 | -------------------------------------------------------------------------------- /secret-env-patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: emailservice-deployment 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: emailservice-deployment 10 | env: 11 | - name: BAL_CONFIG_FILES 12 | value: "/home/ballerina/conf/Config.toml:/home/ballerina/GmailConfig.toml" 13 | -------------------------------------------------------------------------------- /shippingservice/.devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "ballerina/ballerina-devcontainer:2201.0.0", 3 | "extensions": ["WSO2.ballerina"], 4 | } 5 | -------------------------------------------------------------------------------- /shippingservice/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /shippingservice/Ballerina.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | org = "wso2" 3 | name = "shippingservice" 4 | version = "0.1.0" 5 | distribution = "2201.2.3" 6 | 7 | [build-options] 8 | observabilityIncluded = true 9 | 10 | [[dependency]] 11 | org = "wso2" 12 | name = "client_stubs" 13 | version = "0.1.0" 14 | repository = "local" 15 | -------------------------------------------------------------------------------- /shippingservice/Cloud.toml: -------------------------------------------------------------------------------- 1 | [container.image] 2 | name="shipping-service" 3 | repository="wso2inc" 4 | tag="v0.1.0" 5 | 6 | [cloud.deployment] 7 | internal_domain_name="shipping-service" 8 | -------------------------------------------------------------------------------- /shippingservice/README.md: -------------------------------------------------------------------------------- 1 | # Shipping Service 2 | 3 | The `Shipping Service` is a mock service where it returns a constant shipping cost if the cart is not empty. It also has the capability to generate a mock tracking number for the shipment. 4 | 5 | ```bal 6 | remote function GetQuote(stubs:GetQuoteRequest request) returns stubs:GetQuoteResponse { 7 | stubs:CartItem[] items = request.items; 8 | int count = 0; 9 | foreach var {quantity} in items { 10 | count += quantity; 11 | } 12 | float cost = 0.0; 13 | if count != 0 { 14 | cost = 8.99; 15 | } 16 | float cents = cost % 1; 17 | int dollars = (cost - cents); 18 | 19 | return { 20 | cost_usd: {currency_code: "USD", nanos: (cents * FRACTION_SIZE), units: dollars} 21 | }; 22 | } 23 | 24 | remote function ShipOrder(stubs:ShipOrderRequest request) returns stubs:ShipOrderResponse { 25 | stubs:Address address = request.address; 26 | return { 27 | tracking_id: generateTrackingId(string `${address.street_address}, ${address.city}, ${address.state}`) 28 | }; 29 | } 30 | ``` 31 | 32 | ### A Glimpse of comparison 33 | 34 | The below figure provides a glimpse of the comparison of the source codes written in the Go and Ballerina languages. 35 | 36 | 37 | -------------------------------------------------------------------------------- /shippingservice/docker/Config.toml: -------------------------------------------------------------------------------- 1 | [ballerina.observe] 2 | tracingEnabled=true 3 | tracingProvider="jaeger" 4 | 5 | [ballerinax.jaeger] 6 | agentHostname="localhost" 7 | agentPort=4317 8 | -------------------------------------------------------------------------------- /shippingservice/k8s/Config.toml: -------------------------------------------------------------------------------- 1 | [ballerina.observe] 2 | tracingEnabled=true 3 | tracingProvider="jaeger" 4 | 5 | [ballerinax.jaeger] 6 | agentHostname="localhost" 7 | agentPort=4317 8 | -------------------------------------------------------------------------------- /shippingservice/shipping_service.bal: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 2 | // 3 | // WSO2 LLC. licenses this file to you under the Apache License, 4 | // Version 2.0 (the "License"); you may not use this file except 5 | // in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, 11 | // software distributed under the License is distributed on an 12 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 13 | // KIND, either express or implied. See the License for the 14 | // specific language governing permissions and limitations 15 | // under the License. 16 | 17 | import ballerina/grpc; 18 | import ballerina/log; 19 | import ballerinax/jaeger as _; 20 | import wso2/client_stubs as stubs; 21 | 22 | const FRACTION_SIZE = 1000000000; 23 | 24 | # Gives the shipping cost estimates based on the shopping cart. 25 | @display { 26 | label: "Shipping", 27 | id: "shipping" 28 | } 29 | @grpc:Descriptor {value: stubs:DEMO_DESC} 30 | service "ShippingService" on new grpc:Listener(9095) { 31 | 32 | function init() { 33 | log:printInfo("Shipping service gRPC server started."); 34 | } 35 | 36 | # Provides a quote with shipping cost. 37 | # 38 | # + request - `GetQuoteRequest` contaning the user selected items 39 | # + return - `GetQuoteResponse` containing the shipping cost 40 | remote function GetQuote(stubs:GetQuoteRequest request) returns stubs:GetQuoteResponse { 41 | log:printInfo(string `Received a quotation request with ${request.toString()}`); 42 | 43 | stubs:CartItem[] items = request.items; 44 | int count = 0; 45 | foreach var {quantity} in items { 46 | count += quantity; 47 | } 48 | float cost = 0.0; 49 | if count != 0 { 50 | cost = 8.99; 51 | } 52 | float cents = cost % 1; 53 | int dollars = (cost - cents); 54 | 55 | return { 56 | cost_usd: {currency_code: "USD", nanos: (cents * FRACTION_SIZE), units: dollars} 57 | }; 58 | } 59 | 60 | # Ships the order and provides a tracking id. 61 | # 62 | # + request - `ShipOrderRequest` containing the address and the user ordered items 63 | # + return - `ShipOrderResponse` containing the tracking id or an error 64 | remote function ShipOrder(stubs:ShipOrderRequest request) returns stubs:ShipOrderResponse { 65 | log:printInfo(string `Received shipping order request with ${request.toString()}`); 66 | stubs:Address address = request.address; 67 | return { 68 | tracking_id: generateTrackingId(string `${address.street_address}, ${address.city}, ${address.state}`) 69 | }; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /shippingservice/tests/test.bal: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 2 | // 3 | // WSO2 LLC. licenses this file to you under the Apache License, 4 | // Version 2.0 (the "License"); you may not use this file except 5 | // in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, 11 | // software distributed under the License is distributed on an 12 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 13 | // KIND, either express or implied. See the License for the 14 | // specific language governing permissions and limitations 15 | // under the License. 16 | 17 | import ballerina/test; 18 | import wso2/client_stubs as stubs; 19 | 20 | @test:Config {} 21 | function shippingTest() returns error? { 22 | stubs:ShippingServiceClient ep = check new ("http://localhost:9095"); 23 | stubs:GetQuoteRequest req1 = { 24 | address: { 25 | street_address: "Muffin Man", 26 | city: "London", 27 | state: "", 28 | country: "England" 29 | }, 30 | items: [ 31 | { 32 | product_id: "23", 33 | quantity: 1 34 | }, 35 | { 36 | product_id: "46", 37 | quantity: 3 38 | } 39 | ] 40 | }; 41 | stubs:GetQuoteResponse getQuoteResponse = check ep->GetQuote(req1); 42 | int units = getQuoteResponse.cost_usd.units; 43 | int nanos = getQuoteResponse.cost_usd.nanos; 44 | test:assertEquals(units, 8); 45 | test:assertEquals(nanos, 990000000); 46 | 47 | stubs:ShipOrderRequest req = {}; 48 | stubs:ShipOrderResponse getSupportedCurrenciesResponse = check ep->ShipOrder(req); 49 | test:assertEquals(getSupportedCurrenciesResponse.tracking_id.length(), 18); 50 | } 51 | -------------------------------------------------------------------------------- /shippingservice/utils.bal: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 2 | // 3 | // WSO2 LLC. licenses this file to you under the Apache License, 4 | // Version 2.0 (the "License"); you may not use this file except 5 | // in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, 11 | // software distributed under the License is distributed on an 12 | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 13 | // KIND, either express or implied. See the License for the 14 | // specific language governing permissions and limitations 15 | // under the License. 16 | 17 | import ballerina/random; 18 | 19 | isolated function generateRandomLetter() returns string { 20 | //Note - We use checkpanic only because we expect valid input here, and therefore if the input is invalid it is OK for the application to crash. 21 | int randomLetterCodePoint = checkpanic random:createIntInRange(65, 91); 22 | return checkpanic string:fromCodePointInt(randomLetterCodePoint); 23 | } 24 | 25 | isolated function generateRandomNumber(int digit) returns string { 26 | string randomNumber = ""; 27 | foreach int item in 0 ... digit { 28 | //Note - We use checkpanic only because we expect valid input here, and therefore if the input is invalid it is OK for the application to crash. 29 | int randomInt = checkpanic random:createIntInRange(0, 10); 30 | randomNumber += randomInt.toString(); 31 | } 32 | return randomNumber; 33 | } 34 | 35 | isolated function generateTrackingId(string baseAddress) returns string { 36 | return string `${generateRandomLetter()}${generateRandomLetter()}-${baseAddress 37 | .length().toString()}${generateRandomNumber(3)}-${(baseAddress.length() / 2) 38 | .toString()}${generateRandomNumber(7)}`; 39 | } 40 | -------------------------------------------------------------------------------- /ui/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "plugin:react/recommended", 8 | "standard" 9 | ], 10 | "overrides": [], 11 | "parserOptions": { 12 | "ecmaVersion": "latest", 13 | "sourceType": "module" 14 | }, 15 | "plugins": [ 16 | "react" 17 | ], 18 | "ignorePatterns": [ 19 | "**/*/*.css" 20 | ], 21 | "rules": { 22 | "semi": [ 23 | 2, 24 | "always" 25 | ], 26 | "space-before-function-paren": "off", 27 | "react/self-closing-comp": [ 28 | "error", 29 | { 30 | "component": true, 31 | "html": true 32 | } 33 | ], 34 | "indent": [ 35 | "error", 36 | 4 37 | ], 38 | "react/react-in-jsx-scope": "off", 39 | "react/jsx-fragments": [ 40 | "error", 41 | "syntax" 42 | ], 43 | "react/jsx-wrap-multilines": [ 44 | "error", 45 | { 46 | "declaration": "parens", 47 | "assignment": "parens", 48 | "return": "parens", 49 | "arrow": "parens", 50 | "condition": "ignore", 51 | "logical": "ignore", 52 | "prop": "ignore" 53 | } 54 | ] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "online-boutique", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.2", 7 | "@testing-library/react": "^12.1.3", 8 | "@testing-library/user-event": "^13.5.0", 9 | "bootstrap": "^4.1.1", 10 | "jquery": "^3.6.0", 11 | "popper.js": "^1.16.1", 12 | "react": "^17.0.2", 13 | "react-cookie": "^4.1.1", 14 | "react-dom": "^17.0.2", 15 | "react-router-dom": "^6.2.2", 16 | "react-scripts": "5.0.0", 17 | "web-vitals": "^2.1.4" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject" 24 | }, 25 | "eslintConfig": { 26 | "extends": [ 27 | "react-app", 28 | "react-app/jest" 29 | ] 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | ">0.2%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | }, 43 | "devDependencies": { 44 | "eslint": "^8.27.0", 45 | "eslint-config-standard": "^17.0.0", 46 | "eslint-plugin-import": "^2.26.0", 47 | "eslint-plugin-n": "^15.5.1", 48 | "eslint-plugin-promise": "^6.1.1", 49 | "eslint-plugin-react": "^7.31.10" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ballerina-guides/gcp-microservices-demo/1d803785e9ee93ba134ca1633cb568e3df9890a2/ui/public/favicon.ico -------------------------------------------------------------------------------- /ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 22 | 23 | 32 | Online Boutique 33 | 34 | 35 | 36 | 37 |
38 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /ui/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ballerina-guides/gcp-microservices-demo/1d803785e9ee93ba134ca1633cb568e3df9890a2/ui/public/logo192.png -------------------------------------------------------------------------------- /ui/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ballerina-guides/gcp-microservices-demo/1d803785e9ee93ba134ca1633cb568e3df9890a2/ui/public/logo512.png -------------------------------------------------------------------------------- /ui/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Online Boutique", 3 | "name": "Online Boutique Microservice Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /ui/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /ui/public/static/icons/Hipster_CartIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | Hipster 27 | 28 | 29 | 30 | 54 | 56 | 58 | 59 | Hipster 61 | 64 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /ui/public/static/icons/Hipster_CheckOutIcon.svg: -------------------------------------------------------------------------------- 1 | Hipster -------------------------------------------------------------------------------- /ui/public/static/icons/Hipster_CurrencyIcon.svg: -------------------------------------------------------------------------------- 1 | Hipster -------------------------------------------------------------------------------- /ui/public/static/icons/Hipster_DownArrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 47 | 49 | 51 | 52 | Hipster 54 | 57 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /ui/public/static/icons/Hipster_FacebookIcon.svg: -------------------------------------------------------------------------------- 1 | Hipster -------------------------------------------------------------------------------- /ui/public/static/icons/Hipster_GooglePlayIcon.svg: -------------------------------------------------------------------------------- 1 | Hipster -------------------------------------------------------------------------------- /ui/public/static/icons/Hipster_HelpIcon.svg: -------------------------------------------------------------------------------- 1 | Hipster -------------------------------------------------------------------------------- /ui/public/static/icons/Hipster_InstagramIcon.svg: -------------------------------------------------------------------------------- 1 | Hipster -------------------------------------------------------------------------------- /ui/public/static/icons/Hipster_NavLogo.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | Hipster 25 | 26 | 27 | 28 | 48 | 50 | 52 | 53 | Hipster 55 | 57 | 59 | 63 | 67 | 68 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /ui/public/static/icons/Hipster_PinterestIcon.svg: -------------------------------------------------------------------------------- 1 | Hipster -------------------------------------------------------------------------------- /ui/public/static/icons/Hipster_ProfileIcon.svg: -------------------------------------------------------------------------------- 1 | Hipster -------------------------------------------------------------------------------- /ui/public/static/icons/Hipster_SearchIcon.svg: -------------------------------------------------------------------------------- 1 | Hipster -------------------------------------------------------------------------------- /ui/public/static/icons/Hipster_TwitterIcon.svg: -------------------------------------------------------------------------------- 1 | Hipster -------------------------------------------------------------------------------- /ui/public/static/icons/Hipster_UpDownControl.svg: -------------------------------------------------------------------------------- 1 | 2 | Hipster 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ui/public/static/icons/Hipster_YoutubeIcon.svg: -------------------------------------------------------------------------------- 1 | Hipster -------------------------------------------------------------------------------- /ui/public/static/images/Advert2BannerImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ballerina-guides/gcp-microservices-demo/1d803785e9ee93ba134ca1633cb568e3df9890a2/ui/public/static/images/Advert2BannerImage.png -------------------------------------------------------------------------------- /ui/public/static/images/AdvertBannerImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ballerina-guides/gcp-microservices-demo/1d803785e9ee93ba134ca1633cb568e3df9890a2/ui/public/static/images/AdvertBannerImage.png -------------------------------------------------------------------------------- /ui/public/static/images/HeroBannerImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ballerina-guides/gcp-microservices-demo/1d803785e9ee93ba134ca1633cb568e3df9890a2/ui/public/static/images/HeroBannerImage.png -------------------------------------------------------------------------------- /ui/public/static/images/HeroBannerImage2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ballerina-guides/gcp-microservices-demo/1d803785e9ee93ba134ca1633cb568e3df9890a2/ui/public/static/images/HeroBannerImage2.png -------------------------------------------------------------------------------- /ui/public/static/images/VRHeadsets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ballerina-guides/gcp-microservices-demo/1d803785e9ee93ba134ca1633cb568e3df9890a2/ui/public/static/images/VRHeadsets.png -------------------------------------------------------------------------------- /ui/public/static/images/credits.txt: -------------------------------------------------------------------------------- 1 | folded-clothes-on-white-chair.jpg,,https://unsplash.com/photos/fr0J5-GIVyg 2 | folded-clothes-on-white-chair-wide.jpg,,https://unsplash.com/photos/fr0J5-GIVyg 3 | -------------------------------------------------------------------------------- /ui/public/static/images/folded-clothes-on-white-chair-wide.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ballerina-guides/gcp-microservices-demo/1d803785e9ee93ba134ca1633cb568e3df9890a2/ui/public/static/images/folded-clothes-on-white-chair-wide.jpg -------------------------------------------------------------------------------- /ui/public/static/images/folded-clothes-on-white-chair.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ballerina-guides/gcp-microservices-demo/1d803785e9ee93ba134ca1633cb568e3df9890a2/ui/public/static/images/folded-clothes-on-white-chair.jpg -------------------------------------------------------------------------------- /ui/public/static/img/products/bamboo-glass-jar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ballerina-guides/gcp-microservices-demo/1d803785e9ee93ba134ca1633cb568e3df9890a2/ui/public/static/img/products/bamboo-glass-jar.jpg -------------------------------------------------------------------------------- /ui/public/static/img/products/candle-holder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ballerina-guides/gcp-microservices-demo/1d803785e9ee93ba134ca1633cb568e3df9890a2/ui/public/static/img/products/candle-holder.jpg -------------------------------------------------------------------------------- /ui/public/static/img/products/hairdryer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ballerina-guides/gcp-microservices-demo/1d803785e9ee93ba134ca1633cb568e3df9890a2/ui/public/static/img/products/hairdryer.jpg -------------------------------------------------------------------------------- /ui/public/static/img/products/loafers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ballerina-guides/gcp-microservices-demo/1d803785e9ee93ba134ca1633cb568e3df9890a2/ui/public/static/img/products/loafers.jpg -------------------------------------------------------------------------------- /ui/public/static/img/products/mug.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ballerina-guides/gcp-microservices-demo/1d803785e9ee93ba134ca1633cb568e3df9890a2/ui/public/static/img/products/mug.jpg -------------------------------------------------------------------------------- /ui/public/static/img/products/salt-and-pepper-shakers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ballerina-guides/gcp-microservices-demo/1d803785e9ee93ba134ca1633cb568e3df9890a2/ui/public/static/img/products/salt-and-pepper-shakers.jpg -------------------------------------------------------------------------------- /ui/public/static/img/products/sunglasses.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ballerina-guides/gcp-microservices-demo/1d803785e9ee93ba134ca1633cb568e3df9890a2/ui/public/static/img/products/sunglasses.jpg -------------------------------------------------------------------------------- /ui/public/static/img/products/tank-top.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ballerina-guides/gcp-microservices-demo/1d803785e9ee93ba134ca1633cb568e3df9890a2/ui/public/static/img/products/tank-top.jpg -------------------------------------------------------------------------------- /ui/public/static/img/products/watch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ballerina-guides/gcp-microservices-demo/1d803785e9ee93ba134ca1633cb568e3df9890a2/ui/public/static/img/products/watch.jpg -------------------------------------------------------------------------------- /ui/public/static/styles/cart.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | .cart-sections { 18 | padding-bottom: 120px; 19 | padding-top: 56px; 20 | background-color: #F9F9F9; 21 | } 22 | 23 | .cart-sections h3 { 24 | font-size: 36px; 25 | font-weight: normal; 26 | } 27 | 28 | .cart-sections a.cymbal-button-primary:hover { 29 | text-decoration: none; 30 | color: white; 31 | } 32 | 33 | /* Empty Cart Section */ 34 | 35 | .empty-cart-section { 36 | max-width: 458px; 37 | margin: auto; 38 | text-align: center; 39 | } 40 | 41 | .empty-cart-section a { 42 | display: inline-block; /* So margin-top works. */ 43 | margin-top: 32px; 44 | } 45 | 46 | .empty-cart-section a:hover { 47 | color: white; 48 | text-decoration: none; 49 | } 50 | 51 | /* Cart Summary Section */ 52 | 53 | .cart-summary-empty-cart-button { 54 | margin-right: 10px; 55 | } 56 | 57 | .cart-summary-item-row, 58 | .cart-summary-shipping-row, 59 | .cart-summary-total-row { 60 | padding-bottom: 24px; 61 | padding-top: 24px; 62 | border-top: solid 1px rgba(154, 160, 166, 0.5); 63 | } 64 | 65 | .cart-summary-item-row img { 66 | border-radius: 20% 0 20% 20%; 67 | } 68 | 69 | .cart-summary-item-row-item-id-row { 70 | font-size: 12px; 71 | color: #5C6063; 72 | } 73 | 74 | .cart-summary-item-row h4 { 75 | font-size: 18px; 76 | font-weight: normal; 77 | } 78 | 79 | /* Stick item quantity and cost to the bottom (for wider screens). */ 80 | @media (min-width: 768px) { 81 | .cart-summary-item-row .row:last-child { 82 | position: absolute; 83 | bottom: 0px; 84 | width: 100%; 85 | } 86 | } 87 | 88 | /* Item cost (price). */ 89 | .cart-summary-item-row .row:last-child strong { 90 | font-weight: 500; 91 | } 92 | 93 | .cart-summary-total-row { 94 | font-size: 28px; 95 | } 96 | 97 | /* Cart Checkout Form */ 98 | 99 | .cart-checkout-form h3 { 100 | margin-bottom: 0; 101 | } 102 | 103 | .payment-method-heading { 104 | margin-top: 36px; 105 | } 106 | 107 | /* "Place Order" button */ 108 | .cart-checkout-form .cymbal-button-primary { 109 | margin-top: 36px; 110 | } -------------------------------------------------------------------------------- /ui/public/static/styles/order.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | .order { 18 | background: #F9F9F9; 19 | } 20 | 21 | .order-complete-section { 22 | max-width: 487px; 23 | padding-top: 56px; 24 | padding-bottom: 120px; 25 | } 26 | 27 | .order-complete-section h3 { 28 | margin: 0; 29 | font-size: 36px; 30 | font-weight: normal; 31 | } 32 | 33 | .order-complete-section p { 34 | margin-top: 8px; 35 | } 36 | 37 | .order-complete-section .padding-y-24 { 38 | padding-bottom: 24px; 39 | padding-top: 24px; 40 | } 41 | 42 | .order-complete-section .border-bottom-solid { 43 | border-bottom: 1px solid rgba(154, 160, 166, 0.5); 44 | } 45 | 46 | .order-complete-section .cymbal-button-primary { 47 | margin-top: 24px; 48 | } 49 | 50 | .order-complete-section a.cymbal-button-primary:hover { 51 | text-decoration: none; 52 | color: white; 53 | } 54 | -------------------------------------------------------------------------------- /ui/src/App.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 3 | * 4 | * WSO2 LLC. licenses this file to you under the Apache License, 5 | * Version 2.0 (the "License"); you may not use this file except 6 | * in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | import { Routes, Route } from 'react-router-dom'; 19 | import Home from './pages/Home'; 20 | import Product from './pages/Product'; 21 | import Cart from './pages/Cart'; 22 | import NotFound from './pages/NotFound'; 23 | 24 | function App () { 25 | return ( 26 | 27 | } /> 28 | } /> 29 | } /> 30 | } /> 31 | 32 | ); 33 | } 34 | 35 | export default App; 36 | -------------------------------------------------------------------------------- /ui/src/components/UI/Card.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 3 | * 4 | * WSO2 LLC. licenses this file to you under the Apache License, 5 | * Version 2.0 (the "License"); you may not use this file except 6 | * in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | 19 | import PropTypes from 'prop-types'; 20 | import classes from './Card.module.css'; 21 | 22 | const Card = (props) => { 23 | return
{props.children}
; 24 | }; 25 | 26 | Card.propTypes = { 27 | children: PropTypes.oneOfType([ 28 | PropTypes.arrayOf(PropTypes.node), 29 | PropTypes.node 30 | ]) 31 | }; 32 | 33 | export default Card; 34 | -------------------------------------------------------------------------------- /ui/src/components/UI/Card.module.css: -------------------------------------------------------------------------------- 1 | .card { 2 | padding: 1rem; 3 | margin: 1rem; 4 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); 5 | border-radius: 6px; 6 | background-color: white; 7 | } 8 | -------------------------------------------------------------------------------- /ui/src/components/UI/LoadingSpinner.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 3 | * 4 | * WSO2 LLC. licenses this file to you under the Apache License, 5 | * Version 2.0 (the "License"); you may not use this file except 6 | * in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | 19 | import classes from './LoadingSpinner.module.css'; 20 | 21 | const LoadingSpinner = () => { 22 | return
; 23 | }; 24 | 25 | export default LoadingSpinner; 26 | -------------------------------------------------------------------------------- /ui/src/components/UI/LoadingSpinner.module.css: -------------------------------------------------------------------------------- 1 | .spinner { 2 | display: inline-block; 3 | width: 80px; 4 | height: 80px; 5 | } 6 | .spinner:after { 7 | content: ' '; 8 | display: block; 9 | width: 64px; 10 | height: 64px; 11 | margin: 8px; 12 | border-radius: 50%; 13 | border: 6px solid teal; 14 | border-color: teal transparent teal transparent; 15 | animation: spinner 1.2s linear infinite; 16 | } 17 | @keyframes spinner { 18 | 0% { 19 | transform: rotate(0deg); 20 | } 21 | 100% { 22 | transform: rotate(360deg); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ui/src/components/layout/Layout.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 3 | * 4 | * WSO2 LLC. licenses this file to you under the Apache License, 5 | * Version 2.0 (the "License"); you may not use this file except 6 | * in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | 19 | import { Fragment } from 'react'; 20 | import PropTypes from 'prop-types'; 21 | 22 | import classes from './Layout.module.css'; 23 | import MainNavigation from './MainNavigation'; 24 | 25 | function Layout(props) { 26 | return ( 27 | <> 28 | 29 |
{props.children}
30 | 31 | ); 32 | }; 33 | 34 | Layout.propTypes = { 35 | children: PropTypes.oneOfType([ 36 | PropTypes.arrayOf(PropTypes.node), 37 | PropTypes.node 38 | ]) 39 | }; 40 | 41 | export default Layout; 42 | -------------------------------------------------------------------------------- /ui/src/components/layout/Layout.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | margin: 3rem auto; 3 | width: 90%; 4 | max-width: 40rem; 5 | } -------------------------------------------------------------------------------- /ui/src/components/layout/MainNavigation.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 3 | * 4 | * WSO2 LLC. licenses this file to you under the Apache License, 5 | * Version 2.0 (the "License"); you may not use this file except 6 | * in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | 19 | import { NavLink } from 'react-router-dom'; 20 | 21 | import classes from './MainNavigation.module.css'; 22 | 23 | const MainNavigation = () => { 24 | return ( 25 |
26 |
Great Quotes
27 | 41 |
42 | ); 43 | }; 44 | 45 | export default MainNavigation; 46 | -------------------------------------------------------------------------------- /ui/src/components/layout/MainNavigation.module.css: -------------------------------------------------------------------------------- 1 | .header { 2 | width: 100%; 3 | height: 5rem; 4 | display: flex; 5 | padding: 0 10%; 6 | justify-content: space-between; 7 | align-items: center; 8 | background-color: #008080; 9 | } 10 | 11 | .logo { 12 | font-size: 2rem; 13 | color: white; 14 | } 15 | 16 | .nav ul { 17 | list-style: none; 18 | display: flex; 19 | margin: 0; 20 | padding: 0; 21 | } 22 | 23 | .nav li { 24 | margin-left: 1.5rem; 25 | font-size: 1.25rem; 26 | } 27 | 28 | .nav a { 29 | text-decoration: none; 30 | color: #88dfdf; 31 | } 32 | 33 | .nav a:hover, 34 | .nav a:active, 35 | .nav a.active { 36 | color: #e6fcfc; 37 | } 38 | -------------------------------------------------------------------------------- /ui/src/components/layout/NoQuotesFound.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 3 | * 4 | * WSO2 LLC. licenses this file to you under the Apache License, 5 | * Version 2.0 (the "License"); you may not use this file except 6 | * in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | 19 | import { Link } from 'react-router-dom'; 20 | 21 | import classes from './NoQuotesFound.module.css'; 22 | 23 | const NoQuotesFound = () => { 24 | return ( 25 |
26 |

No quotes found!

27 | Add a Quote 28 |
29 | ); 30 | }; 31 | 32 | export default NoQuotesFound; 33 | -------------------------------------------------------------------------------- /ui/src/components/layout/NoQuotesFound.module.css: -------------------------------------------------------------------------------- 1 | .noquotes { 2 | height: 20rem; 3 | margin: auto; 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | display: flex; 8 | flex-direction: column; 9 | align-items: center; 10 | } 11 | 12 | .noquotes p { 13 | color: #262c2c; 14 | font-size: 3rem; 15 | font-weight: bold; 16 | } 17 | 18 | -------------------------------------------------------------------------------- /ui/src/components/products/Ad.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 3 | * 4 | * WSO2 LLC. licenses this file to you under the Apache License, 5 | * Version 2.0 (the "License"); you may not use this file except 6 | * in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | import PropTypes from 'prop-types'; 19 | 20 | const Ad = (props) => { 21 | return ( 22 |
23 |
24 | Ad 25 | 26 | {props.text} 27 | 28 |
29 |
30 | ); 31 | }; 32 | 33 | Ad.propTypes = { 34 | redirect_url: PropTypes.string, 35 | text: PropTypes.string 36 | }; 37 | 38 | export default Ad; 39 | -------------------------------------------------------------------------------- /ui/src/components/products/CartItem.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 3 | * 4 | * WSO2 LLC. licenses this file to you under the Apache License, 5 | * Version 2.0 (the "License"); you may not use this file except 6 | * in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | import PropTypes from 'prop-types'; 19 | 20 | const CartItem = (props) => { 21 | return ( 22 |
23 |
24 | 25 | 26 | 27 |
28 |
29 |
30 |
31 |

{props.name}

32 |
33 |
34 |
35 |
36 | SKU #{props.id} 37 |
38 |
39 |
40 |
41 | Quantity: {props.quantity} 42 |
43 |
44 | 45 | {props.price} 46 | 47 |
48 |
49 |
50 |
51 | ); 52 | }; 53 | 54 | CartItem.propTypes = { 55 | id: PropTypes.string.isRequired, 56 | picture: PropTypes.string.isRequired, 57 | name: PropTypes.string.isRequired, 58 | price: PropTypes.string.isRequired, 59 | quantity: PropTypes.number.isRequired 60 | }; 61 | 62 | export default CartItem; 63 | -------------------------------------------------------------------------------- /ui/src/components/products/CurrencyOption.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 3 | * 4 | * WSO2 LLC. licenses this file to you under the Apache License, 5 | * Version 2.0 (the "License"); you may not use this file except 6 | * in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | import PropTypes from 'prop-types'; 19 | 20 | const CurrencyOption = (props) => { 21 | return ( 22 | 23 | ); 24 | }; 25 | 26 | CurrencyOption.propTypes = { 27 | userCurrency: PropTypes.string.isRequired 28 | }; 29 | 30 | export default CurrencyOption; 31 | -------------------------------------------------------------------------------- /ui/src/components/products/ExpireOptionPicker.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 3 | * 4 | * WSO2 LLC. licenses this file to you under the Apache License, 5 | * Version 2.0 (the "License"); you may not use this file except 6 | * in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | import PropTypes from 'prop-types'; 19 | 20 | const ExpireOption = (props) => { 21 | return ( 22 | 23 | ); 24 | }; 25 | 26 | ExpireOption.propTypes = { 27 | year: PropTypes.number.isRequired 28 | }; 29 | 30 | export default ExpireOption; 31 | -------------------------------------------------------------------------------- /ui/src/components/products/Footer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 3 | * 4 | * WSO2 LLC. licenses this file to you under the Apache License, 5 | * Version 2.0 (the "License"); you may not use this file except 6 | * in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | 19 | import { useCookies } from 'react-cookie'; 20 | 21 | const Footer = (props) => { 22 | const [userIdCookie] = useCookies(['userId']); 23 | const userId = userIdCookie.userId; 24 | 25 | return ( 26 |
27 |
28 |
29 |

This website is hosted for demo purposes only. It is not an actual shop.

30 |

This demo was created based on the GCP microservices sample

31 |

Please find the source code here

32 |

33 | session-id: {userId} 34 |

35 |
36 |
37 |
38 | ); 39 | }; 40 | 41 | export default Footer; 42 | -------------------------------------------------------------------------------- /ui/src/components/products/Header.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 3 | * 4 | * WSO2 LLC. licenses this file to you under the Apache License, 5 | * Version 2.0 (the "License"); you may not use this file except 6 | * in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | 19 | import CurrencyOption from './CurrencyOption'; 20 | import { useState, useEffect, useRef } from 'react'; 21 | import { getMetadata, changeCurrency } from '../../lib/api'; 22 | 23 | const whitelistedCurrencies = ['USD', 'CAD', 'JPY', 'TRY', 'EUR', 'GBP']; 24 | 25 | const Header = () => { 26 | const [myData, setMyData] = useState({ 27 | cartSize: 0, 28 | currencies: whitelistedCurrencies, 29 | isCymbalBrand: false, 30 | userCurrency: ['USD', '$'] 31 | }); 32 | 33 | useEffect(() => { 34 | getMetadata().then((data) => { 35 | setMyData(data); 36 | }); 37 | }, []); 38 | 39 | let image = ; 40 | if (myData.isCymbalBrand) { 41 | image = ; 42 | } 43 | 44 | const items = []; 45 | for (const value of myData.currencies.values()) { 46 | if (whitelistedCurrencies.includes(value)) { 47 | items.push(); 48 | } 49 | } 50 | 51 | const currencyRef = useRef(); 52 | 53 | async function changeCurrencyFormHandler(event) { 54 | event.preventDefault(); 55 | 56 | const currency = currencyRef.current.value; 57 | 58 | const data = { 59 | currency 60 | }; 61 | const response = await changeCurrency(data); 62 | setMyData(response); 63 | window.location.reload(true); 64 | } 65 | 66 | const currencyInfo = ( 67 |
68 |
69 | {myData.userCurrency[1]} 70 |
71 | 74 |
75 | 76 |
77 |
78 | ); 79 | let cartSize; 80 | if (myData.cartSize) { 81 | cartSize = {myData.cartSize}; 82 | } 83 | return ( 84 |
85 |
86 |
87 |
Free shipping with $75 purchase!
88 |
89 |
90 |
91 |
92 | 93 | {image} 94 | 95 |
96 | {currencyInfo} 97 | 98 | Cart icon 99 | {cartSize} 100 | 101 |
102 |
103 |
104 |
105 | ); 106 | }; 107 | 108 | export default Header; 109 | -------------------------------------------------------------------------------- /ui/src/components/products/Order.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 3 | * 4 | * WSO2 LLC. licenses this file to you under the Apache License, 5 | * Version 2.0 (the "License"); you may not use this file except 6 | * in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | import PropTypes from 'prop-types'; 19 | 20 | const Order = (props) => { 21 | const orderId = props.orderId; 22 | const shippingTrackingId = props.shippingTrackingId; 23 | const totalPaid = props.totalPaid; 24 | 25 | return ( 26 | <> 27 |
28 | 29 |
30 |
31 |
32 |

33 | Your order is complete! 34 |

35 |
36 |
37 |

We've sent you a confirmation email.

38 |
39 |
40 |
41 |
42 | Confirmation # 43 |
44 |
45 | {orderId} 46 |
47 |
48 |
49 |
50 | Tracking # 51 |
52 |
53 | {shippingTrackingId} 54 |
55 |
56 |
57 |
58 | Total Paid 59 |
60 |
61 | {totalPaid} 62 |
63 |
64 |
65 | 70 |
71 |
72 | 73 |
74 | 75 | 76 | ); 77 | }; 78 | 79 | Order.propTypes = { 80 | orderId: PropTypes.string.isRequired, 81 | shippingTrackingId: PropTypes.string.isRequired, 82 | totalPaid: PropTypes.string.isRequired 83 | }; 84 | 85 | export default Order; 86 | -------------------------------------------------------------------------------- /ui/src/components/products/Product.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 3 | * 4 | * WSO2 LLC. licenses this file to you under the Apache License, 5 | * Version 2.0 (the "License"); you may not use this file except 6 | * in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | import PropTypes from 'prop-types'; 19 | 20 | const Product = (props) => { 21 | console.log(props); 22 | return ( 23 |
24 | 25 | 26 |
27 | 28 |
29 |
{props.name}
30 |
{props.price}
31 |
32 |
33 | ); 34 | }; 35 | 36 | Product.propTypes = { 37 | id: PropTypes.string.isRequired, 38 | picture: PropTypes.string.isRequired, 39 | name: PropTypes.string.isRequired, 40 | price: PropTypes.string.isRequired 41 | }; 42 | 43 | export default Product; 44 | -------------------------------------------------------------------------------- /ui/src/components/products/Recommendation.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 3 | * 4 | * WSO2 LLC. licenses this file to you under the Apache License, 5 | * Version 2.0 (the "License"); you may not use this file except 6 | * in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | import PropTypes from 'prop-types'; 19 | 20 | const Recommendation = (props) => { 21 | return ( 22 |
23 |
24 | 25 | 26 | 27 |
28 |
29 | {props.name} 30 |
31 |
32 |
33 |
34 | ); 35 | }; 36 | 37 | Recommendation.propTypes = { 38 | id: PropTypes.string.isRequired, 39 | picture: PropTypes.string.isRequired, 40 | name: PropTypes.string.isRequired 41 | }; 42 | 43 | export default Recommendation; 44 | -------------------------------------------------------------------------------- /ui/src/components/products/Recommendations.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 3 | * 4 | * WSO2 LLC. licenses this file to you under the Apache License, 5 | * Version 2.0 (the "License"); you may not use this file except 6 | * in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | import PropTypes from 'prop-types'; 19 | import Recommendation from './Recommendation'; 20 | 21 | const Recommendations = (props) => { 22 | console.log(props); 23 | const values = [props.values]; 24 | const items = []; 25 | 26 | for (let index = 0; index < values[0].length; ++index) { 27 | const value = values[0][index]; 28 | items.push(); 29 | } 30 | 31 | return ( 32 |
33 |
34 |
35 |
36 |

You May Also Like

37 |
38 | {items} 39 |
40 |
41 |
42 |
43 |
44 | ); 45 | }; 46 | 47 | Recommendations.propTypes = { 48 | values: PropTypes.arrayOf(PropTypes.shape({ 49 | categories: PropTypes.arrayOf(PropTypes.string).isRequired, 50 | description: PropTypes.string.isRequired, 51 | id: PropTypes.string.isRequired, 52 | name: PropTypes.string.isRequired, 53 | picture: PropTypes.string.isRequired 54 | })) 55 | }; 56 | 57 | export default Recommendations; 58 | -------------------------------------------------------------------------------- /ui/src/hooks/use-http.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 3 | * 4 | * WSO2 LLC. licenses this file to you under the Apache License, 5 | * Version 2.0 (the "License"); you may not use this file except 6 | * in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | 19 | import { useReducer, useCallback } from 'react'; 20 | 21 | function httpReducer(state, action) { 22 | if (action.type === 'SEND') { 23 | return { 24 | data: null, 25 | error: null, 26 | status: 'pending' 27 | }; 28 | } 29 | 30 | if (action.type === 'SUCCESS') { 31 | return { 32 | data: action.responseData, 33 | error: null, 34 | status: 'completed' 35 | }; 36 | } 37 | 38 | if (action.type === 'ERROR') { 39 | return { 40 | data: null, 41 | error: action.errorMessage, 42 | status: 'completed' 43 | }; 44 | } 45 | 46 | return state; 47 | } 48 | 49 | function useHttp(requestFunction, startWithPending = false) { 50 | const [httpState, dispatch] = useReducer(httpReducer, { 51 | status: startWithPending ? 'pending' : null, 52 | data: null, 53 | error: null 54 | }); 55 | 56 | const sendRequest = useCallback( 57 | async function (requestData) { 58 | dispatch({ type: 'SEND' }); 59 | try { 60 | const responseData = await requestFunction(requestData); 61 | dispatch({ type: 'SUCCESS', responseData }); 62 | } catch (error) { 63 | dispatch({ 64 | type: 'ERROR', 65 | errorMessage: error.message || 'Something went wrong!' 66 | }); 67 | } 68 | }, 69 | [requestFunction] 70 | ); 71 | 72 | return { 73 | sendRequest, 74 | ...httpState 75 | }; 76 | } 77 | 78 | export default useHttp; 79 | -------------------------------------------------------------------------------- /ui/src/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 3 | * 4 | * WSO2 LLC. licenses this file to you under the Apache License, 5 | * Version 2.0 (the "License"); you may not use this file except 6 | * in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | 19 | import React from 'react'; 20 | import ReactDOM from 'react-dom'; 21 | import { BrowserRouter } from 'react-router-dom'; 22 | 23 | import './index.css'; 24 | import App from './App'; 25 | import 'bootstrap/dist/css/bootstrap.min.css'; 26 | import 'bootstrap/dist/js/bootstrap.js'; 27 | import { CookiesProvider } from 'react-cookie'; 28 | 29 | ReactDOM.render( 30 | 31 | 32 | 33 | 34 | , 35 | document.getElementById('root') 36 | ); 37 | -------------------------------------------------------------------------------- /ui/src/lib/api.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 3 | * 4 | * WSO2 LLC. licenses this file to you under the Apache License, 5 | * Version 2.0 (the "License"); you may not use this file except 6 | * in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | 19 | const FRONTEND_SVC_URL = 'http://localhost:9098'; 20 | 21 | export async function getAllQuotes() { 22 | const response = await fetch(`${FRONTEND_SVC_URL}/quotes.json`); 23 | const data = await response.json(); 24 | 25 | if (!response.ok) { 26 | throw new Error(data.message || 'Could not fetch quotes.'); 27 | } 28 | 29 | const transformedQuotes = []; 30 | 31 | for (const key in data) { 32 | const quoteObj = { 33 | id: key, 34 | ...data[key] 35 | }; 36 | 37 | transformedQuotes.push(quoteObj); 38 | } 39 | 40 | return transformedQuotes; 41 | } 42 | 43 | export async function getHomePage() { 44 | const response = await fetch(`${FRONTEND_SVC_URL}`, { credentials: 'include' }); 45 | const data = await response.json(); 46 | 47 | if (!response.ok) { 48 | throw new Error(data.message || 'Could not get home page.'); 49 | } 50 | 51 | return data; 52 | } 53 | 54 | export async function getSingleProduct(productId) { 55 | const response = await fetch(`${FRONTEND_SVC_URL}/product/${productId}`, { credentials: 'include' }); 56 | const data = await response.json(); 57 | 58 | if (!response.ok) { 59 | throw new Error(data.message || 'Could not get the product.'); 60 | } 61 | 62 | return data; 63 | } 64 | 65 | export async function addProductToCart(requestData) { 66 | const response = await fetch(`${FRONTEND_SVC_URL}/cart/`, { 67 | method: 'POST', 68 | body: JSON.stringify(requestData), 69 | headers: { 70 | 'Content-Type': 'application/json' 71 | }, 72 | credentials: 'include' 73 | }); 74 | const data = await response.json(); 75 | 76 | if (!response.ok) { 77 | throw new Error(data.message || 'Could not product to cart.'); 78 | } 79 | 80 | return { }; 81 | } 82 | 83 | export async function getCartPage() { 84 | const response = await fetch(`${FRONTEND_SVC_URL}/cart`, { credentials: 'include' }); 85 | const data = await response.json(); 86 | 87 | if (!response.ok) { 88 | throw new Error(data.message || 'Could not get the cart.'); 89 | } 90 | 91 | return data; 92 | } 93 | 94 | export async function checkout(requestData) { 95 | const response = await fetch(`${FRONTEND_SVC_URL}/cart/checkout`, { 96 | method: 'POST', 97 | body: JSON.stringify(requestData), 98 | headers: { 99 | 'Content-Type': 'application/json' 100 | }, 101 | credentials: 'include' 102 | }); 103 | const data = await response.json(); 104 | 105 | if (!response.ok) { 106 | throw new Error(data.message || 'Could not checkout.'); 107 | } 108 | 109 | return data; 110 | } 111 | 112 | export async function changeCurrency(requestData) { 113 | const response = await fetch(`${FRONTEND_SVC_URL}/setCurrency`, { 114 | method: 'POST', 115 | body: JSON.stringify(requestData), 116 | headers: { 117 | 'Content-Type': 'application/json' 118 | }, 119 | credentials: 'include' 120 | }); 121 | const data = await response.json(); 122 | 123 | if (!response.ok) { 124 | throw new Error(data.message || 'Could not change currency.'); 125 | } 126 | 127 | return data; 128 | } 129 | 130 | export async function getMetadata() { 131 | const response = await fetch(`${FRONTEND_SVC_URL}/metadata`, { credentials: 'include' }); 132 | const data = await response.json(); 133 | 134 | if (!response.ok) { 135 | throw new Error(data.message || 'Could not get metadata.'); 136 | } 137 | 138 | return data; 139 | } 140 | 141 | export async function emptyCart() { 142 | const response = await fetch(`${FRONTEND_SVC_URL}/cart/empty`, { credentials: 'include', method: 'POST' }); 143 | const data = await response.json(); 144 | 145 | if (!response.ok) { 146 | throw new Error(data.message || 'Could not empty the cart.'); 147 | } 148 | 149 | return data; 150 | } 151 | -------------------------------------------------------------------------------- /ui/src/pages/Home.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 3 | * 4 | * WSO2 LLC. licenses this file to you under the Apache License, 5 | * Version 2.0 (the "License"); you may not use this file except 6 | * in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | 19 | import { useEffect } from 'react'; 20 | import Header from '../components/products/Header'; 21 | import Footer from '../components/products/Footer'; 22 | import Product from '../components/products/Product'; 23 | import LoadingSpinner from '../components/UI/LoadingSpinner'; 24 | import useHttp from '../hooks/use-http'; 25 | import { getHomePage } from '../lib/api'; 26 | 27 | const HomePage = () => { 28 | const { sendRequest, status, data: homeData, error } = useHttp( 29 | getHomePage, 30 | true 31 | ); 32 | 33 | useEffect(() => { 34 | sendRequest(); 35 | }, [sendRequest]); 36 | 37 | if (status === 'pending') { 38 | return ( 39 |
40 | 41 |
42 | ); 43 | } 44 | 45 | if (error) { 46 | return

{error}

; 47 | } 48 | 49 | const data = homeData; 50 | 51 | const items = []; 52 | for (const [index, value] of data.products.entries()) { 53 | items.push(); 54 | } 55 | 56 | return ( 57 | <> 58 |
59 |
60 | 61 | local 62 | 63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |

Hot Products

73 |
74 | {items} 75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 | 89 | ); 90 | }; 91 | 92 | export default HomePage; 93 | -------------------------------------------------------------------------------- /ui/src/pages/NotFound.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 3 | * 4 | * WSO2 LLC. licenses this file to you under the Apache License, 5 | * Version 2.0 (the "License"); you may not use this file except 6 | * in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | import React from 'react'; 19 | 20 | const NotFound = () => { 21 | return ( 22 |
23 |

Page not found!

24 |
25 | ); 26 | }; 27 | 28 | export default NotFound; 29 | -------------------------------------------------------------------------------- /ui/src/pages/Product.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, WSO2 LLC. (http://www.wso2.com) All Rights Reserved. 3 | * 4 | * WSO2 LLC. licenses this file to you under the Apache License, 5 | * Version 2.0 (the "License"); you may not use this file except 6 | * in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, 12 | * software distributed under the License is distributed on an 13 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | * KIND, either express or implied. See the License for the 15 | * specific language governing permissions and limitations 16 | * under the License. 17 | */ 18 | import { useEffect, useRef } from 'react'; 19 | import Header from '../components/products//Header'; 20 | import Footer from '../components/products/Footer'; 21 | import LoadingSpinner from '../components/UI/LoadingSpinner'; 22 | import useHttp from '../hooks/use-http'; 23 | import { getSingleProduct, addProductToCart } from '../lib/api'; 24 | import Recommendations from '../components/products/Recommendations'; 25 | import Ad from '../components/products/Ad'; 26 | import { useParams, useNavigate } from 'react-router-dom'; 27 | 28 | const Product = () => { 29 | const params = useParams(); 30 | const quantityRef = useRef(); 31 | const navigate = useNavigate(); 32 | 33 | const { productId } = params; 34 | 35 | function submitFormHandler(event) { 36 | event.preventDefault(); 37 | const quantity = quantityRef.current.value; 38 | addProductToCart({ quantity: parseInt(quantity), productId }).finally(() => { 39 | navigate('/cart'); 40 | }); 41 | } 42 | 43 | const { sendRequest, status, data: loadedProduct, error } = useHttp( 44 | getSingleProduct, 45 | true 46 | ); 47 | 48 | useEffect(() => { 49 | sendRequest(productId); 50 | }, [sendRequest, productId]); 51 | 52 | if (status === 'pending') { 53 | return ( 54 |
55 | 56 |
57 | ); 58 | } 59 | 60 | if (error) { 61 | return

{error}

; 62 | } 63 | 64 | const data = loadedProduct; 65 | 66 | const product = data.product; 67 | const recommendations = data.recommendations; 68 | 69 | return ( 70 | <> 71 |
72 |
73 | 74 | local 75 | 76 |
77 |
78 |
79 |
80 |
81 | 82 |
83 |
84 |
85 | 86 |

{product.id}

87 |

{product.price}

88 |

{product.description}

89 | 90 |
91 | 92 |
93 | 101 | 102 |
103 | 104 |
105 |
106 |
107 |
108 |
109 |
110 | {recommendations.length > 0 && 111 | 112 | } 113 |
114 |
115 | 116 |
117 |
118 | 119 |