[
  {
    "path": ".gitignore",
    "content": ".env.local\n.idea"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "## Code of Conduct\nThis project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).\nFor more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact\nopensource-codeofconduct@amazon.com with any additional questions or comments.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing Guidelines\n\nThank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional\ndocumentation, we greatly value feedback and contributions from our community.\n\nPlease read through this document before submitting any issues or pull requests to ensure we have all the necessary\ninformation to effectively respond to your bug report or contribution.\n\n\n## Reporting Bugs/Feature Requests\n\nWe welcome you to use the GitHub issue tracker to report bugs or suggest features.\n\nWhen filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already\nreported the issue. Please try to include as much information as you can. Details like these are incredibly useful:\n\n* A reproducible test case or series of steps\n* The version of our code being used\n* Any modifications you've made relevant to the bug\n* Anything unusual about your environment or deployment\n\n\n## Contributing via Pull Requests\nContributions via pull requests are much appreciated. Before sending us a pull request, please ensure that:\n\n1. You are working against the latest source on the *master* branch.\n2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already.\n3. You open an issue to discuss any significant work - we would hate for your time to be wasted.\n\nTo send us a pull request, please:\n\n1. Fork the repository.\n2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change.\n3. Ensure local tests pass.\n4. Commit to your fork using clear commit messages.\n5. Send us a pull request, answering any default questions in the pull request interface.\n6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation.\n\nGitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and\n[creating a pull request](https://help.github.com/articles/creating-a-pull-request/).\n\n\n## Finding contributions to work on\nLooking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start.\n\n\n## Code of Conduct\nThis project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).\nFor more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact\nopensource-codeofconduct@amazon.com with any additional questions or comments.\n\n\n## Security issue notifications\nIf you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue.\n\n\n## Licensing\n\nSee the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution.\n\nWe may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes.\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n"
  },
  {
    "path": "Makefile",
    "content": "all: backend frontend-build\n\nTEMPLATES = auth product-mock shoppingcart-service\n\nREGION := $(shell python3 -c 'import boto3; print(boto3.Session().region_name)')\nifndef S3_BUCKET\nACCOUNT_ID := $(shell aws sts get-caller-identity --query Account --output text)\nS3_BUCKET = aws-serverless-shopping-cart-src-$(ACCOUNT_ID)-$(REGION)\nendif\n\n\nbackend: create-bucket\n\t$(MAKE) -C backend TEMPLATE=auth S3_BUCKET=$(S3_BUCKET)\n\t$(MAKE) -C backend TEMPLATE=product-mock S3_BUCKET=$(S3_BUCKET)\n\t$(MAKE) -C backend TEMPLATE=shoppingcart-service S3_BUCKET=$(S3_BUCKET)\n\nbackend-delete:\n\t$(MAKE) -C backend delete TEMPLATE=auth\n\t$(MAKE) -C backend delete TEMPLATE=product-mock\n\t$(MAKE) -C backend delete TEMPLATE=shoppingcart-service\n\nbackend-tests:\n\t$(MAKE) -C backend tests\n\ncreate-bucket:\n\t@echo \"Checking if S3 bucket exists s3://$(S3_BUCKET)\"\n\t@aws s3api head-bucket --bucket $(S3_BUCKET) || (echo \"bucket does not exist at s3://$(S3_BUCKET), creating it...\" ; aws s3 mb s3://$(S3_BUCKET) --region $(REGION))\n\namplify-deploy:\n\taws cloudformation deploy \\\n\t\t--template-file ./amplify-ci/amplify-template.yaml \\\n\t\t--capabilities CAPABILITY_IAM \\\n\t\t--parameter-overrides \\\n\t\t\tOauthToken=$(GITHUB_OAUTH_TOKEN) \\\n\t\t\tRepository=$(GITHUB_REPO) \\\n\t\t\tBranchName=$(GITHUB_BRANCH) \\\n\t\t\tSrcS3Bucket=$(S3_BUCKET) \\\n\t\t--stack-name CartApp\n\nfrontend-serve: \n\t$(MAKE) -C frontend serve\n\nfrontend-build: \n\t$(MAKE) -C frontend build\n\n.PHONY: all backend backend-delete backend-tests create-bucket amplify-deploy frontend-serve frontend-build\n"
  },
  {
    "path": "README.md",
    "content": "# Serverless Shopping Cart Microservice\n\nThis application is a sample application to demonstrate how you could implement a shopping cart microservice using \nserverless technologies on AWS. The backend is built as a REST API interface, making use of [Amazon API Gateway](https://aws.amazon.com/api-gateway/), [AWS Lambda](https://aws.amazon.com/lambda/), [Amazon Cognito](https://aws.amazon.com/cognito/), and [Amazon DynamoDB](https://aws.amazon.com/dynamodb/). The frontend is a Vue.js application using the [AWS Amplify](https://aws-amplify.github.io/) SDK for authentication and communication with the API.\n\nTo assist in demonstrating the functionality, a bare bones mock \"products\" service has also been included. Since the \nauthentication parts are likely to be shared between components, there is a separate template for it. The front-end \ndoesn't make any real payment integration at this time.\n\n## Architecture & Design\n\n![Architecture Diagram](./images/architecture.png)\n\n## Design Notes\n\nBefore building the application, I set some requirements on how the cart should behave:\n\n- Users should be able to add items to the cart without logging in (an \"anonymous cart\"), and that cart should persist \nacross browser restarts etc.\n- When logging in, if there were products in an anonymous cart, they should be added to the user's cart from any \nprevious logged in sessions.\n- When logging out, the anonymous cart should not have products in it any longer.\n- Items in an anonymous cart should be removed after a period of time, and items in a logged in cart should persist \nfor a longer period.\n- Admin users to be able to get an aggregated view of the total number of each product in users' carts at any time.\n\n### Cart Migration\n\nWhen an item is added to the cart, an item is written in DynamoDB with an identifier which matches a randomly generated \n(uuid) cookie which is set in the browser. This allows a user to add items to cart and come back to the page later \nwithout losing the items they have added. When the user logs in, these items will be removed, and replaced with items \nwith a user id as the pk. If the user already had that product in their cart from a previous logged in session, the \nquantities would be summed. Because we don't need the deletion of old items to happen immediately as part of a \nsynchronous workflow, we put messages onto an SQS queue, which triggers a worker function to delete the messages.  \n\nTo expire items from users' shopping carts, DynamoDB's native functionality is used where a TTL is written along with \nthe item, after which the item should be removed. In this implementation, the TTL defaults to 1 day for anonymous \ncarts, and 7 days for logged in carts.  \n\n### Aggregated View of Products in Carts\n\nIt would be possible to scan our entire DynamoDB table and sum up the quantities of all the products, but this will be \nexpensive past a certain scale. Instead, we can calculate the total as a running process, and keep track of the total \namount.  \n\nWhen an item is added, deleted or updated in DynamoDB, an event is put onto DynamoDB Streams, which in turn triggers a \nLambda function. This function calculates the change in total quantity for each product in users' carts, and writes the \nquantity back to DynamoDB. The Lambda function is configured so that it will run after either 60 seconds pass, or 100 \nnew events are on the stream. This would enable an admin user to get real time data about the popular products, which \ncould in turn help anticipate inventory. In this implementation, the API is exposed without authentication to \ndemonstrate the functionality.  \n\n\n## Api Design\n\n### Shopping Cart Service\n\nGET  \n`/cart`  \nRetrieves the shopping cart for a user who is either anonymous or logged in.  \n\nPOST  \n`/cart`  \nAccepts a product id and quantity as json. Adds specified quantity of an item to cart.  \n\n`/cart/migrate`  \nCalled after logging in - migrates items in an anonymous user's cart to belong to their logged in user. If you already \nhave a cart on your logged in user, your \"anonymous cart\" will be merged with it when you log in.\n\n`/cart/checkout`  \nCurrently just empties cart.\n\nPUT  \n`/cart/{product-id}`  \nAccepts a product id and quantity as json. Updates quantity of given item to provided quantity.  \n\nGET  \n`/cart/{product-id}/total`  \nReturns the total amount of a given product across all carts. This API is not used by the frontend but can be manually \ncalled to test.  \n\n### Product Mock Service\n\nGET  \n`/product`  \nReturns details for all products.  \n\n`/product/{product_id}`  \nReturns details for a single product.  \n\n## Running the Example\n\n### Requirements\n\npython >= 3.8.0\nboto3\nSAM CLI, >= version 0.50.0  \nAWS CLI  \nyarn  \n\n### Setup steps\n\nFork the github repo, then clone your fork locally: \n`git clone https://github.com/<your-github-username>/aws-serverless-shopping-cart && cd aws-serverless-shopping-cart`\n\nIf you wish to use a named profile for your AWS credentials, you can set the environment variable `AWS_PROFILE` before \nrunning the below commands. For a profile named \"development\": `export AWS_PROFILE=development`.  \n\nYou now have 2 options - you can deploy the backend and run the frontend locally, or you can deploy the whole project \nusing the AWS Amplify console.\n\n## Option 1 - Deploy backend and run frontend locally\n### Deploy the Backend\n\nAn S3 bucket will be automatically created for you which will be used for deploying source code to AWS. If you wish to \nuse an existing bucket instead, you can manually set the `S3_BUCKET` environment variable to the name of your bucket.  \n\nBuild and deploy the resources:  \n``` bash\nmake backend  # Creates S3 bucket if not existing already, then deploys CloudFormation stacks for authentication, a \nproduct mock service and the shopping cart service.  \n```\n\n### Run the Frontend Locally\n\nStart the frontend locally:  \n``` bash\nmake frontend-serve  # Retrieves backend config from ssm parameter store to a .env file, then starts service.  \n```\n\nOnce the service is running, you can access the frontend on http://localhost:8080/ and start adding items to your cart. \nYou can create an account by clicking on \"Sign In\" then \"Create Account\". Be sure to use a valid email address as \nyou'll need to retrieve the verification code.\n\n**Note:** CORS headers on the backend service default to allowing http://localhost:8080/. You will see CORS errors if \nyou access the frontend using the ip (http://127.0.0.1:8080/), or using a port other than 8080.  \n\n### Clean Up\nDelete the CloudFormation stacks created by this project:\n``` bash\nmake backend-delete\n```\n\n## Option 2 - Automatically deploy backend and frontend using Amplify Console\n\n\n[![One-click deployment](https://oneclick.amplifyapp.com/button.svg)](https://console.aws.amazon.com/amplify/home#/deploy?repo=https://github.com/aws-samples/aws-serverless-shopping-cart)\n\n1) Use **1-click deployment** button above, and continue by clicking \"Connect to Github\"\n2) If you don't have an IAM Service Role with admin permissions, select \"Create new role\". Otherwise proceed to step 5) \n3) Select \"Amplify\" from the drop-down, and select \"Amplify - Backend Deployment\", then click \"Next\".\n4) Click \"Next\" again, then give the role a name and click \"Create role\"\n5) In the Amplify console and select the role you created, then click \"Save and deploy\"\n6) Amplify Console will fork this repository into your GitHub account and deploy it for you\n7) You should now be able to see your app being deployed in the [Amplify Console](https://console.aws.amazon.com/amplify/home)\n8) Within your new app in Amplify Console, wait for deployment to complete (this should take approximately 12 minutes for the first deploy)\n\n\n### Clean Up\nDelete the CloudFormation stacks created by this project. There are 3 of them, with names starting with \"aws-serverless-shopping-cart-\".\n\n## License\n\nThis library is licensed under the MIT-0 License. See the [LICENSE](LICENSE) file.  \n"
  },
  {
    "path": "amplify/.gitkeep",
    "content": "\n"
  },
  {
    "path": "amplify-ci/amplify-template.yaml",
    "content": "AWSTemplateFormatVersion: 2010-09-09\n\nParameters:\n  Repository:\n    Type: String\n    Description: GitHub Repository URL\n\n  BranchName:\n    Type: String\n    Description: Github branch name\n    Default: master\n\n  OauthToken:\n    Type: String\n    Description: GitHub Personal Access Token\n    NoEcho: true\n\n  SrcS3Bucket:\n    Type: String\n    Description: S3 Bucket for source code storage  \n\nResources:\n  AmplifyRole:\n    Type: AWS::IAM::Role\n    Properties:\n      AssumeRolePolicyDocument:\n        Version: 2012-10-17\n        Statement:\n          - Effect: Allow\n            Principal:\n              Service:\n                - amplify.amazonaws.com\n            Action:\n              - sts:AssumeRole\n      Policies:\n        - PolicyName: Amplify\n          PolicyDocument:\n            Version: 2012-10-17\n            Statement:\n              -\n                Effect: \"Allow\"\n                Action:\n                    - \"s3:PutObject\"\n                    - \"s3:GetObject\"\n                    - \"s3:CreateMultipartUpload\"\n                    - \"s3:ListBucket\"\n                    - \"s3:CreateBucket\"\n                Resource:\n                    - !Sub \"arn:aws:s3:::${SrcS3Bucket}\"\n                    - !Sub \"arn:aws:s3:::${SrcS3Bucket}/*\"\n              -\n                Effect: \"Allow\"\n                Action:\n                    - \"cloudformation:Describe*\"\n                    - \"cloudformation:Create*\"\n                    - \"cloudformation:Execute*\"\n                Resource:\n                    - \"*\"\n              -\n                Effect: Allow\n                Action:\n                  - lambda:*\n                Resource: \n                  - !Sub \"arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:*\"\n                  - !Sub \"arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:layer:*\"\n              -\n                Effect: Allow\n                Action:\n                  - lambda:GetLayerVersion\n                Resource:\n                  - !Sub \"arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPython*\"\n              -\n                Effect: Allow\n                Action:\n                  - lambda:*EventSourceMapping\n                Resource: \n                  - \"*\"\n              -\n                Effect: \"Allow\"\n                Action:\n                    - \"cloudformation:GetTemplateSummary\"\n                Resource: \"*\"\n              -\n                  Effect: \"Allow\"\n                  Action:\n                      - \"iam:GetRole\"\n                      - \"iam:Create*\"\n                      - \"iam:Delete*\"\n                      - \"iam:PassRole\"\n                      - \"iam:*RolePolicy\"\n                  Resource:\n                      - \"*\"\n              -\n                  Effect: \"Allow\"\n                  Action:\n                      - \"apigateway:*\"\n                  Resource:\n                      - \"*\"\n              -\n                  Effect: \"Allow\"\n                  Action:\n                      - \"ssm:*\"\n                  Resource:\n                      - \"*\"       \n              -\n                  Effect: \"Allow\"\n                  Action:\n                      - \"dynamodb:List*\"\n                      - \"dynamodb:Describe*\"\n                      - \"dynamodb:TagResource\"\n                      - \"dynamodb:UnTagResource\"\n                      - \"dynamodb:Update*\"\n                      - \"dynamodb:Create*\"\n                      - \"dynamodb:DeleteTable\"\n                  Resource:\n                      - \"*\"   \n              -\n                  Effect: \"Allow\"\n                  Action:\n                      - \"sqs:*\"\n                  Resource:\n                      - \"*\"\n              -\n                  Effect: \"Allow\"\n                  Action:\n                      - \"cognito-idp:*\"\n                  Resource:\n                      - \"*\"\n              -\n                  Effect: \"Allow\"\n                  Action:\n                      - \"sts:GetCallerIdentity\"\n                  Resource:\n                      - \"*\"\n\n  AmplifyApp:\n    Type: AWS::Amplify::App\n    Properties:\n      Name: CartApp\n      Repository: !Ref Repository\n      Description: AWS serverless shopping cart\n      OauthToken: !Ref OauthToken\n      CustomRules:\n        -\n          Source: </^[^.]+$|\\.(?!(css|gif|ico|jpg|js|png|txt|svg|woff|ttf|map|json)$)([^.]+$)/>\n          Target: /\n          Status: 200\n      EnvironmentVariables:\n        - Name: S3_BUCKET\n          Value: !Ref SrcS3Bucket\n      Tags:\n        - Key: Name\n          Value: ShoppingCart\n      IAMServiceRole: !GetAtt AmplifyRole.Arn\n\n  AmplifyBranch:\n    Type: AWS::Amplify::Branch\n    Properties:\n      BranchName: !Ref BranchName\n      AppId: !GetAtt AmplifyApp.AppId\n      Description: Master Branch\n      EnableAutoBuild: true\n      Tags:\n        - Key: Name\n          Value: shoppingcart-master\n        - Key: Branch\n          Value: master\n\nOutputs:\n  DefaultDomain:\n    Value: !GetAtt AmplifyApp.DefaultDomain\n\n  MasterBranchUrl:\n    Value: !Join [ \".\", [ !GetAtt AmplifyBranch.BranchName, !GetAtt AmplifyApp.DefaultDomain ]]"
  },
  {
    "path": "amplify.yml",
    "content": "version: 1\nenv:\n  variables:\n      ORIGIN: https://${AWS_BRANCH//\\//-}.${AWS_APP_ID}.amplifyapp.com\n      STACKNAME: amplify-aws-serverless-shopping-cart\nbackend:\n  phases:\n    preBuild:\n      commands:\n        - curl \"https://github.com/aws/aws-sam-cli/releases/latest/download/aws-sam-cli-linux-x86_64.zip\" -L -o \"aws-sam-cli.zip\"\n        - unzip aws-sam-cli.zip -d sam-installation\n        - ./sam-installation/install\n        - pip3 install -U boto3\n    build:\n      commands:\n        - make backend\nfrontend:\n  phases:\n    # IMPORTANT - Please verify your build commands\n    build:\n      commands:\n        - make frontend-build\n  artifacts:\n    # IMPORTANT - Please verify your build output directory\n    baseDirectory: /frontend/dist\n    files:\n      - '**/*'\n  cache:\n    paths: []\n"
  },
  {
    "path": "backend/.gitignore",
    "content": "\n# Created by https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode\n\n### Linux ###\n*~\n\n# temporary files which can be created if a process still has a handle open of a deleted file\n.fuse_hidden*\n\n# KDE directory preferences\n.directory\n\n# Linux trash folder which might appear on any partition or disk\n.Trash-*\n\n# .nfs files are created when an open file is removed but is still being accessed\n.nfs*\n\n### OSX ###\n*.DS_Store\n.AppleDouble\n.LSOverride\n\n# Icon must end with two \\r\nIcon\n\n# Thumbnails\n._*\n\n# Files that might appear in the root of a volume\n.DocumentRevisions-V100\n.fseventsd\n.Spotlight-V100\n.TemporaryItems\n.Trashes\n.VolumeIcon.icns\n.com.apple.timemachine.donotpresent\n\n# Directories potentially created on remote AFP share\n.AppleDB\n.AppleDesktop\nNetwork Trash Folder\nTemporary Items\n.apdisk\n\n### PyCharm ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff:\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/dictionaries\n\n# Sensitive or high-churn files:\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.xml\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n\n# Gradle:\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# CMake\ncmake-build-debug/\n\n# Mongo Explorer plugin:\n.idea/**/mongoSettings.xml\n\n## File-based project format:\n*.iws\n\n## Plugin-specific files:\n\n# IntelliJ\n/out/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# Ruby plugin and RubyMine\n/.rakeTasks\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n### PyCharm Patch ###\n# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721\n\n# *.iml\n# modules.xml\n# .idea/misc.xml\n# *.ipr\n\n# Sonarlint plugin\n.idea/sonarlint\n\n### Python ###\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\n*.egg-info/\n.installed.cfg\n*.egg\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.coverage.*\n.cache\n.pytest_cache/\nnosetests.xml\ncoverage.xml\n*.cover\n.hypothesis/\n\n# Translations\n*.mo\n*.pot\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# pyenv\n.python-version\n\n# celery beat schedule file\ncelerybeat-schedule.*\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n\n### VisualStudioCode ###\n.vscode/*\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json\n.history\n\n### Windows ###\n# Windows thumbnail cache files\nThumbs.db\nehthumbs.db\nehthumbs_vista.db\n\n# Folder config file\nDesktop.ini\n\n# Recycle Bin used on file shares\n$RECYCLE.BIN/\n\n# Windows Installer files\n*.cab\n*.msi\n*.msm\n*.msp\n\n# Windows shortcuts\n*.lnk\n\n# Build folder\n\n*/build/*\n\n# End of https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode\n\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# celery beat schedule file\ncelerybeat-schedule\n\n\n# mypy\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n/packaged-*.yml\n\n\n.idea\n.idea/*"
  },
  {
    "path": "backend/Makefile",
    "content": "all: build deploy\nORIGIN ?= http://localhost:8080\nSTACKNAME ?= aws-serverless-shopping-cart\n\nbuild:\n\t@echo \"Building template $(TEMPLATE).yaml...\"\n\t@sam build -t $(TEMPLATE).yaml\n\ndeploy:  # s3 bucket still needed for layer\n\t@echo \"Deploying stack $(STACKNAME)-$(TEMPLATE)...\"\n\t@sam deploy --capabilities CAPABILITY_NAMED_IAM --stack-name $(STACKNAME)-$(TEMPLATE) --s3-bucket $(S3_BUCKET) --parameter-overrides AllowedOrigin=$(ORIGIN) --no-fail-on-empty-changeset\n\ntests:\n\tpy.test -v\n\ndelete:\n\t@aws cloudformation delete-stack --stack-name $(STACKNAME)-$(TEMPLATE)\n\t@echo \"Waiting for stack $(STACKNAME)-$(TEMPLATE) to be deleted...\"\n\t@aws cloudformation wait stack-delete-complete --stack-name $(STACKNAME)-$(TEMPLATE)\n\n.PHONY: build package deploy delete\n"
  },
  {
    "path": "backend/auth.yaml",
    "content": "AWSTemplateFormatVersion: '2010-09-09'\nTransform: AWS::Serverless-2016-10-31\nDescription: >\n  auth-resources\n\n  SAM Template for auth resources\n\n\nGlobals:\n  Function:\n    Timeout: 3\n\nResources:\n  CognitoUserPool:\n    Type: AWS::Cognito::UserPool\n    Properties:\n      UserPoolName: !Sub ${AWS::StackName}-UserPool\n      AutoVerifiedAttributes: \n        - email\n  UserPoolClient:\n    Type: AWS::Cognito::UserPoolClient\n    Properties:\n      ClientName: my-app\n      GenerateSecret: false\n      UserPoolId: !Ref CognitoUserPool\n      ExplicitAuthFlows:\n        - ADMIN_NO_SRP_AUTH\n\n  UserPoolSSM:\n    Type: AWS::SSM::Parameter\n    Properties:\n      Type: String\n      Name: /serverless-shopping-cart-demo/auth/user-pool-id\n      Value: !Ref CognitoUserPool\n\n  UserPoolARNSSM:\n    Type: AWS::SSM::Parameter\n    Properties:\n      Type: String\n      Name: /serverless-shopping-cart-demo/auth/user-pool-arn\n      Value: !GetAtt CognitoUserPool.Arn\n\n  UserPoolAppClientSSM:\n    Type: AWS::SSM::Parameter\n    Properties:\n      Type: String\n      Name: /serverless-shopping-cart-demo/auth/user-pool-client-id\n      Value: !Ref UserPoolClient\n\nOutputs:\n  CognitoUserPoolId:\n    Description: \"Cognito User Pool ID\"\n    Value: !Ref CognitoUserPool\n\n  CognitoAppClientId:\n    Description: \"Cognito App Client ID\"\n    Value: !Ref UserPoolClient"
  },
  {
    "path": "backend/layers/requirements.txt",
    "content": "requests==2.22.0\ncognitojwt==1.1.0\nboto3==1.10.34\n"
  },
  {
    "path": "backend/layers/shared.py",
    "content": "import calendar\nimport datetime\nimport os\nimport uuid\nfrom decimal import Decimal\nfrom http.cookies import SimpleCookie\n\nfrom aws_lambda_powertools import Tracer\n\nimport cognitojwt\n\ntracer = Tracer()\n\nHEADERS = {\n    \"Access-Control-Allow-Origin\": os.environ.get(\"ALLOWED_ORIGIN\"),\n    \"Access-Control-Allow-Headers\": \"Content-Type\",\n    \"Access-Control-Allow-Methods\": \"OPTIONS,POST,GET\",\n    \"Access-Control-Allow-Credentials\": True,\n}\n\n\nclass NotFoundException(Exception):\n    pass\n\n\n@tracer.capture_method\ndef handle_decimal_type(obj):\n    \"\"\"\n    json serializer which works with Decimal types returned from DynamoDB.\n    \"\"\"\n    if isinstance(obj, Decimal):\n        if float(obj).is_integer():\n            return int(obj)\n        else:\n            return float(obj)\n    raise TypeError\n\n\n@tracer.capture_method\ndef generate_ttl(days=1):\n    \"\"\"\n    Generate epoch timestamp for number days in future\n    \"\"\"\n    future = datetime.datetime.utcnow() + datetime.timedelta(days=days)\n    return calendar.timegm(future.utctimetuple())\n\n\n@tracer.capture_method\ndef get_user_sub(jwt_token):\n    \"\"\"\n    Validate JWT claims & retrieve user identifier\n    \"\"\"\n    try:\n        verified_claims = cognitojwt.decode(\n            jwt_token, os.environ[\"AWS_REGION\"], os.environ[\"USERPOOL_ID\"]\n        )\n    except (cognitojwt.CognitoJWTException, ValueError):\n        verified_claims = {}\n\n    return verified_claims.get(\"sub\")\n\n\n@tracer.capture_method\ndef get_cart_id(event_headers):\n    \"\"\"\n    Retrieve cart_id from cookies if it exists, otherwise set and return it\n    \"\"\"\n    cookie = SimpleCookie()\n    try:\n        cookie.load(event_headers[\"cookie\"])\n        cart_cookie = cookie[\"cartId\"].value\n        generated = False\n    except KeyError:\n        cart_cookie = str(uuid.uuid4())\n        generated = True\n\n    return cart_cookie, generated\n\n\n@tracer.capture_method\ndef get_headers(cart_id):\n    \"\"\"\n    Get the headers to add to response data\n    \"\"\"\n    headers = HEADERS\n    cookie = SimpleCookie()\n    cookie[\"cartId\"] = cart_id\n    cookie[\"cartId\"][\"max-age\"] = (60 * 60) * 24  # 1 day\n    cookie[\"cartId\"][\"secure\"] = True\n    cookie[\"cartId\"][\"httponly\"] = True\n    cookie[\"cartId\"][\"samesite\"] = \"None\"\n    cookie[\"cartId\"][\"path\"] = \"/\"\n    headers[\"Set-Cookie\"] = cookie[\"cartId\"].OutputString()\n    return headers\n"
  },
  {
    "path": "backend/product-mock-service/__init__.py",
    "content": ""
  },
  {
    "path": "backend/product-mock-service/get_product.py",
    "content": "import json\nimport os\n\nfrom aws_lambda_powertools import Logger, Tracer\n\nlogger = Logger()\ntracer = Tracer()\n\nwith open(\"product_list.json\", \"r\") as product_list:\n    product_list = json.load(product_list)\n\nHEADERS = {\n    \"Access-Control-Allow-Origin\": os.environ.get(\"ALLOWED_ORIGIN\"),\n    \"Access-Control-Allow-Headers\": \"Content-Type\",\n    \"Access-Control-Allow-Methods\": \"OPTIONS,POST,GET\",\n}\n\n\n@logger.inject_lambda_context(log_event=True)\n@tracer.capture_lambda_handler\ndef lambda_handler(event, context):\n    \"\"\"\n    Return single product based on path parameter.\n    \"\"\"\n    path_params = event[\"pathParameters\"]\n    product_id = path_params.get(\"product_id\")\n    logger.debug(\"Retriving product_id: %s\", product_id)\n    product = next(\n        (item for item in product_list if item[\"productId\"] == product_id), None\n    )\n\n    return {\n        \"statusCode\": 200,\n        \"headers\": HEADERS,\n        \"body\": json.dumps({\"product\": product}),\n    }\n"
  },
  {
    "path": "backend/product-mock-service/get_products.py",
    "content": "import json\nimport os\n\nfrom aws_lambda_powertools import Logger, Tracer\n\nlogger = Logger()\ntracer = Tracer()\n\nwith open('product_list.json', 'r') as product_list:\n    product_list = json.load(product_list)\n\nHEADERS = {\n    \"Access-Control-Allow-Origin\": os.environ.get(\"ALLOWED_ORIGIN\"),\n    \"Access-Control-Allow-Headers\": \"Content-Type\",\n    \"Access-Control-Allow-Methods\": \"OPTIONS,POST,GET\",\n}\n\n\n@logger.inject_lambda_context(log_event=True)\n@tracer.capture_lambda_handler\ndef lambda_handler(event, context):\n    \"\"\"\n    Return list of all products.\n    \"\"\"\n    logger.debug(\"Fetching product list\")\n\n    return {\n        \"statusCode\": 200,\n        \"headers\": HEADERS,\n        \"body\": json.dumps({\"products\": product_list}),\n    }\n"
  },
  {
    "path": "backend/product-mock-service/product_list.json",
    "content": "[\n    {\n        \"category\": \"fruit\",\n        \"createdDate\": \"2017-04-17T01:14:03 -02:00\",\n        \"description\": \"Culpa non veniam deserunt dolor irure elit cupidatat culpa consequat nulla irure aliqua.\",\n        \"modifiedDate\": \"2019-03-13T12:18:27 -01:00\",\n        \"name\": \"packaged strawberries\",\n        \"package\": {\n            \"height\": 948,\n            \"length\": 455,\n            \"weight\": 54,\n            \"width\": 905\n        },\n        \"pictures\": [\n            \"http://placehold.it/32x32\"\n        ],\n        \"price\": 716,\n        \"productId\": \"4c1fadaa-213a-4ea8-aa32-58c217604e3c\",\n        \"tags\": [\n            \"mollit\",\n            \"ad\",\n            \"eiusmod\",\n            \"irure\",\n            \"tempor\"\n        ]\n    },\n    {\n        \"category\": \"sweets\",\n        \"createdDate\": \"2017-04-06T06:21:36 -02:00\",\n        \"description\": \"Dolore ipsum eiusmod dolore aliquip laborum laborum aute ipsum commodo id irure duis ipsum.\",\n        \"modifiedDate\": \"2019-09-21T12:08:48 -02:00\",\n        \"name\": \"candied prunes\",\n        \"package\": {\n            \"height\": 329,\n            \"length\": 179,\n            \"weight\": 293,\n            \"width\": 741\n        },\n        \"pictures\": [\n            \"http://placehold.it/32x32\"\n        ],\n        \"price\": 35,\n        \"productId\": \"d2580eff-d105-45a5-9b21-ba61995bc6da\",\n        \"tags\": [\n            \"laboris\",\n            \"dolor\",\n            \"in\",\n            \"labore\",\n            \"duis\"\n        ]\n    },\n    {\n        \"category\": \"fruit\",\n        \"createdDate\": \"2017-03-17T03:06:53 -01:00\",\n        \"description\": \"Reprehenderit aliquip consequat quis excepteur et et esse exercitation adipisicing dolore nulla consequat.\",\n        \"modifiedDate\": \"2019-11-25T12:32:49 -01:00\",\n        \"name\": \"fresh prunes\",\n        \"package\": {\n            \"height\": 736,\n            \"length\": 567,\n            \"weight\": 41,\n            \"width\": 487\n        },\n        \"pictures\": [\n            \"http://placehold.it/32x32\"\n        ],\n        \"price\": 2,\n        \"productId\": \"a6dd7187-40b6-4cb5-b73c-aecd655c6d9a\",\n        \"tags\": [\n            \"nisi\",\n            \"quis\",\n            \"sint\",\n            \"adipisicing\",\n            \"pariatur\"\n        ]\n    },\n    {\n        \"category\": \"vegetable\",\n        \"createdDate\": \"2018-07-17T02:14:55 -02:00\",\n        \"description\": \"Minim qui elit dolor est commodo excepteur ea voluptate eu dolor culpa magna.\",\n        \"modifiedDate\": \"2019-09-05T03:36:34 -02:00\",\n        \"name\": \"packaged tomatoes\",\n        \"package\": {\n            \"height\": 4,\n            \"length\": 756,\n            \"weight\": 607,\n            \"width\": 129\n        },\n        \"pictures\": [\n            \"http://placehold.it/32x32\"\n        ],\n        \"price\": 97,\n        \"productId\": \"c0fbcc6b-7a70-41ac-aac4-f8fd237dc62e\",\n        \"tags\": [\n            \"dolor\",\n            \"officia\",\n            \"fugiat\",\n            \"officia\",\n            \"voluptate\"\n        ]\n    },\n    {\n        \"category\": \"vegetable\",\n        \"createdDate\": \"2017-07-25T12:00:11 -02:00\",\n        \"description\": \"Labore dolore velit mollit aute qui magna elit excepteur officia cupidatat ea ea aliqua.\",\n        \"modifiedDate\": \"2019-10-04T06:32:14 -02:00\",\n        \"name\": \"fresh tomatoes\",\n        \"package\": {\n            \"height\": 881,\n            \"length\": 252,\n            \"weight\": 66,\n            \"width\": 431\n        },\n        \"pictures\": [\n            \"http://placehold.it/32x32\"\n        ],\n        \"price\": 144,\n        \"productId\": \"cb40c919-033a-47d6-8d00-1d73e2df20fe\",\n        \"tags\": [\n            \"nostrud\",\n            \"elit\",\n            \"Lorem\",\n            \"occaecat\",\n            \"duis\"\n        ]\n    },\n    {\n        \"category\": \"vegetable\",\n        \"createdDate\": \"2017-01-07T05:28:03 -01:00\",\n        \"description\": \"Ad eiusmod cupidatat duis dolor mollit labore mollit eu.\",\n        \"modifiedDate\": \"2019-04-03T10:36:25 -02:00\",\n        \"name\": \"fresh lettuce\",\n        \"package\": {\n            \"height\": 813,\n            \"length\": 932,\n            \"weight\": 457,\n            \"width\": 436\n        },\n        \"pictures\": [\n            \"http://placehold.it/32x32\"\n        ],\n        \"price\": 51,\n        \"productId\": \"12929eb9-3eb7-4217-99e4-1a39c39217b6\",\n        \"tags\": [\n            \"ad\",\n            \"ipsum\",\n            \"est\",\n            \"eiusmod\",\n            \"duis\"\n        ]\n    },\n    {\n        \"category\": \"meat\",\n        \"createdDate\": \"2018-12-03T12:33:44 -01:00\",\n        \"description\": \"Amet cupidatat anim ipsum pariatur sit eu.\",\n        \"modifiedDate\": \"2019-04-17T06:31:47 -02:00\",\n        \"name\": \"packaged steak\",\n        \"package\": {\n            \"height\": 707,\n            \"length\": 417,\n            \"weight\": 491,\n            \"width\": 549\n        },\n        \"pictures\": [\n            \"http://placehold.it/32x32\"\n        ],\n        \"price\": 894,\n        \"productId\": \"9fd9ef32-493f-4188-99f5-3aa809aa4fa9\",\n        \"tags\": [\n            \"fugiat\",\n            \"velit\",\n            \"non\",\n            \"magna\",\n            \"laboris\"\n        ]\n    },\n    {\n        \"category\": \"vegetable\",\n        \"createdDate\": \"2017-04-27T06:48:08 -02:00\",\n        \"description\": \"Labore est aliqua laborum ea laboris voluptate cillum aute duis occaecat.\",\n        \"modifiedDate\": \"2019-11-01T10:23:57 -01:00\",\n        \"name\": \"fresh lettuce\",\n        \"package\": {\n            \"height\": 21,\n            \"length\": 311,\n            \"weight\": 817,\n            \"width\": 964\n        },\n        \"pictures\": [\n            \"http://placehold.it/32x32\"\n        ],\n        \"price\": 452,\n        \"productId\": \"20db6331-1084-48ff-8c4f-c1d98a6a1aa4\",\n        \"tags\": [\n            \"laborum\",\n            \"in\",\n            \"aliquip\",\n            \"sint\",\n            \"quis\"\n        ]\n    },\n    {\n        \"category\": \"sweet\",\n        \"createdDate\": \"2017-11-24T04:01:33 -01:00\",\n        \"description\": \"Fugiat sunt in eu eu occaecat.\",\n        \"modifiedDate\": \"2019-05-19T05:53:56 -02:00\",\n        \"name\": \"half-eaten cake\",\n        \"package\": {\n            \"height\": 337,\n            \"length\": 375,\n            \"weight\": 336,\n            \"width\": 1\n        },\n        \"pictures\": [\n            \"http://placehold.it/32x32\"\n        ],\n        \"price\": 322,\n        \"productId\": \"8c843a54-27d7-477c-81b3-c21db12ed1c9\",\n        \"tags\": [\n            \"officia\",\n            \"proident\",\n            \"officia\",\n            \"commodo\",\n            \"nisi\"\n        ]\n    },\n    {\n        \"category\": \"dairy\",\n        \"createdDate\": \"2018-05-29T11:46:28 -02:00\",\n        \"description\": \"Aliqua officia magna do ipsum laboris anim magna nulla sit labore nulla qui duis.\",\n        \"modifiedDate\": \"2019-05-29T05:33:49 -02:00\",\n        \"name\": \"leftover cheese\",\n        \"package\": {\n            \"height\": 267,\n            \"length\": 977,\n            \"weight\": 85,\n            \"width\": 821\n        },\n        \"pictures\": [\n            \"http://placehold.it/32x32\"\n        ],\n        \"price\": 163,\n        \"productId\": \"8d2024c0-6c05-4691-a0ff-dd52959bd1df\",\n        \"tags\": [\n            \"excepteur\",\n            \"ipsum\",\n            \"nulla\",\n            \"nisi\",\n            \"velit\"\n        ]\n    },\n    {\n        \"category\": \"bakery\",\n        \"createdDate\": \"2018-09-22T05:22:38 -02:00\",\n        \"description\": \"Ullamco commodo cupidatat reprehenderit eu sunt.\",\n        \"modifiedDate\": \"2019-03-11T06:10:38 -01:00\",\n        \"name\": \"fresh croissants\",\n        \"package\": {\n            \"height\": 122,\n            \"length\": 23,\n            \"weight\": 146,\n            \"width\": 694\n        },\n        \"pictures\": [\n            \"http://placehold.it/32x32\"\n        ],\n        \"price\": 634,\n        \"productId\": \"867ecb2b-ef08-446e-8360-b63f60969e3d\",\n        \"tags\": [\n            \"labore\",\n            \"dolor\",\n            \"aliquip\",\n            \"nulla\",\n            \"aute\"\n        ]\n    },\n    {\n        \"category\": \"meat\",\n        \"createdDate\": \"2018-09-12T07:24:46 -02:00\",\n        \"description\": \"Eu ullamco irure qui labore qui duis mollit eiusmod adipisicing fugiat adipisicing nostrud ut non.\",\n        \"modifiedDate\": \"2019-10-28T01:25:50 -01:00\",\n        \"name\": \"packaged ham\",\n        \"package\": {\n            \"height\": 902,\n            \"length\": 278,\n            \"weight\": 775,\n            \"width\": 31\n        },\n        \"pictures\": [\n            \"http://placehold.it/32x32\"\n        ],\n        \"price\": 77,\n        \"productId\": \"684011fc-ecfd-4557-a6df-9fc977365826\",\n        \"tags\": [\n            \"voluptate\",\n            \"laborum\",\n            \"exercitation\",\n            \"anim\",\n            \"anim\"\n        ]\n    },\n    {\n        \"category\": \"bakery\",\n        \"createdDate\": \"2017-06-12T09:15:36 -02:00\",\n        \"description\": \"Eu culpa nulla est et anim sint amet.\",\n        \"modifiedDate\": \"2019-08-22T04:22:39 -02:00\",\n        \"name\": \"fresh bread\",\n        \"package\": {\n            \"height\": 551,\n            \"length\": 976,\n            \"weight\": 47,\n            \"width\": 846\n        },\n        \"pictures\": [\n            \"http://placehold.it/32x32\"\n        ],\n        \"price\": 805,\n        \"productId\": \"b027697d-a070-4c8f-8b9a-b8c80b2eb0ba\",\n        \"tags\": [\n            \"nostrud\",\n            \"in\",\n            \"duis\",\n            \"laboris\",\n            \"minim\"\n        ]\n    },\n    {\n        \"category\": \"sweet\",\n        \"createdDate\": \"2018-09-06T06:03:43 -02:00\",\n        \"description\": \"Mollit proident aliquip consectetur irure qui veniam laboris aliqua proident id fugiat esse nulla.\",\n        \"modifiedDate\": \"2019-10-16T10:53:33 -02:00\",\n        \"name\": \"candied strawberries\",\n        \"package\": {\n            \"height\": 55,\n            \"length\": 32,\n            \"weight\": 661,\n            \"width\": 694\n        },\n        \"pictures\": [\n            \"http://placehold.it/32x32\"\n        ],\n        \"price\": 283,\n        \"productId\": \"7e0dbfa9-a672-4987-a26c-f601d177463a\",\n        \"tags\": [\n            \"minim\",\n            \"irure\",\n            \"in\",\n            \"duis\",\n            \"labore\"\n        ]\n    },\n    {\n        \"category\": \"bakery\",\n        \"createdDate\": \"2017-07-23T12:27:34 -02:00\",\n        \"description\": \"Ex non proident et eiusmod et elit est exercitation anim qui ullamco elit.\",\n        \"modifiedDate\": \"2019-09-04T08:25:44 -02:00\",\n        \"name\": \"fresh pie\",\n        \"package\": {\n            \"height\": 718,\n            \"length\": 59,\n            \"weight\": 18,\n            \"width\": 962\n        },\n        \"pictures\": [\n            \"http://placehold.it/32x32\"\n        ],\n        \"price\": 646,\n        \"productId\": \"d1d527b8-9cef-4e97-a873-22236f3ee289\",\n        \"tags\": [\n            \"in\",\n            \"ea\",\n            \"excepteur\",\n            \"id\",\n            \"dolore\"\n        ]\n    },\n    {\n        \"category\": \"vegetable\",\n        \"createdDate\": \"2018-11-08T04:08:28 -01:00\",\n        \"description\": \"Pariatur deserunt nostrud cupidatat ut officia voluptate adipisicing mollit sunt cillum quis magna dolore aute.\",\n        \"modifiedDate\": \"2019-10-11T10:28:49 -02:00\",\n        \"name\": \"packaged lettuce\",\n        \"package\": {\n            \"height\": 81,\n            \"length\": 57,\n            \"weight\": 653,\n            \"width\": 367\n        },\n        \"pictures\": [\n            \"http://placehold.it/32x32\"\n        ],\n        \"price\": 197,\n        \"productId\": \"11663d33-e54d-49da-ba6f-44d016ecde7e\",\n        \"tags\": [\n            \"incididunt\",\n            \"in\",\n            \"adipisicing\",\n            \"eu\",\n            \"tempor\"\n        ]\n    },\n    {\n        \"category\": \"meat\",\n        \"createdDate\": \"2018-09-28T04:01:24 -02:00\",\n        \"description\": \"Dolore nulla laboris incididunt laborum.\",\n        \"modifiedDate\": \"2019-08-05T01:06:02 -02:00\",\n        \"name\": \"leftover ham\",\n        \"package\": {\n            \"height\": 246,\n            \"length\": 639,\n            \"weight\": 354,\n            \"width\": 953\n        },\n        \"pictures\": [\n            \"http://placehold.it/32x32\"\n        ],\n        \"price\": 728,\n        \"productId\": \"e173d669-b449-4226-af2e-128142abdd30\",\n        \"tags\": [\n            \"exercitation\",\n            \"magna\",\n            \"ex\",\n            \"quis\",\n            \"ad\"\n        ]\n    },\n    {\n        \"category\": \"dairy\",\n        \"createdDate\": \"2018-08-23T06:31:47 -02:00\",\n        \"description\": \"Pariatur mollit voluptate enim qui pariatur deserunt elit.\",\n        \"modifiedDate\": \"2019-10-02T10:50:16 -02:00\",\n        \"name\": \"fresh milk\",\n        \"package\": {\n            \"height\": 576,\n            \"length\": 948,\n            \"weight\": 535,\n            \"width\": 646\n        },\n        \"pictures\": [\n            \"http://placehold.it/32x32\"\n        ],\n        \"price\": 164,\n        \"productId\": \"2a5b681c-ec7f-4bd4-a51e-57a5b6591f7f\",\n        \"tags\": [\n            \"labore\",\n            \"id\",\n            \"mollit\",\n            \"occaecat\",\n            \"elit\"\n        ]\n    },\n    {\n        \"category\": \"vegetable\",\n        \"createdDate\": \"2018-02-21T01:55:54 -01:00\",\n        \"description\": \"Consectetur laborum ipsum ad laboris.\",\n        \"modifiedDate\": \"2019-02-23T08:50:01 -01:00\",\n        \"name\": \"half-eaten lettuce\",\n        \"package\": {\n            \"height\": 348,\n            \"length\": 119,\n            \"weight\": 723,\n            \"width\": 44\n        },\n        \"pictures\": [\n            \"http://placehold.it/32x32\"\n        ],\n        \"price\": 583,\n        \"productId\": \"de979b05-9d71-4c7e-b10f-636332ccb6c1\",\n        \"tags\": [\n            \"id\",\n            \"velit\",\n            \"cillum\",\n            \"irure\",\n            \"aute\"\n        ]\n    },\n    {\n        \"category\": \"meat\",\n        \"createdDate\": \"2017-05-14T03:39:21 -02:00\",\n        \"description\": \"Aliqua tempor irure qui consectetur exercitation culpa minim magna laboris ex pariatur elit culpa.\",\n        \"modifiedDate\": \"2019-11-24T02:23:27 -01:00\",\n        \"name\": \"fresh steak\",\n        \"package\": {\n            \"height\": 328,\n            \"length\": 7,\n            \"weight\": 439,\n            \"width\": 747\n        },\n        \"pictures\": [\n            \"http://placehold.it/32x32\"\n        ],\n        \"price\": 996,\n        \"productId\": \"aa91060a-3601-4cb8-a2cc-025d09c7a9b7\",\n        \"tags\": [\n            \"qui\",\n            \"dolore\",\n            \"culpa\",\n            \"est\",\n            \"duis\"\n        ]\n    }\n]"
  },
  {
    "path": "backend/product-mock-service/requirements.txt",
    "content": "aws-lambda-powertools==1.0.0"
  },
  {
    "path": "backend/product-mock.yaml",
    "content": "AWSTemplateFormatVersion: '2010-09-09'\nTransform: AWS::Serverless-2016-10-31\nDescription: >\n  product-service\n\n  SAM Template for mock product-service\nParameters:\n  AllowedOrigin:\n    Type: 'String'\n\nGlobals:\n  Function:\n    Timeout: 5\n    Tracing: Active\n    AutoPublishAlias: live\n    Runtime: python3.8\n    MemorySize: 256\n    Environment:\n      Variables:\n        LOG_LEVEL: \"DEBUG\"\n        ALLOWED_ORIGIN: !Ref AllowedOrigin\n        POWERTOOLS_SERVICE_NAME: product-mock\n        POWERTOOLS_METRICS_NAMESPACE: ecommerce-app\n  Api:\n    EndpointConfiguration: REGIONAL\n    TracingEnabled: true\n    OpenApiVersion: '2.0'\n    Cors:\n      AllowMethods: \"'OPTIONS,POST,GET'\"\n      AllowHeaders: \"'Content-Type'\"\n      AllowOrigin: !Sub \"'${AllowedOrigin}'\"\n\nResources:\n  GetProductFunction:\n    Type: AWS::Serverless::Function\n    Properties:\n      CodeUri: product-mock-service/\n      Handler: get_product.lambda_handler\n      Events:\n        ListCart:\n          Type: Api\n          Properties:\n            Path: /product/{product_id}\n            Method: get\n\n  GetProductsFunction:\n    Type: AWS::Serverless::Function\n    Properties:\n      CodeUri: product-mock-service/\n      Handler: get_products.lambda_handler\n      Events:\n        ListCart:\n          Type: Api\n          Properties:\n            Path: /product\n            Method: get\n\n  GetProductApiUrl:\n    Type: AWS::SSM::Parameter\n    Properties:\n      Type: String\n      Name: /serverless-shopping-cart-demo/products/products-api-url\n      Value: !Sub \"https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod\"\n\n\nOutputs:\n  ProductApi:\n    Description: \"API Gateway endpoint URL for Prod stage for Product Mock Service\"\n    Value: !Sub \"https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod\"\n"
  },
  {
    "path": "backend/shopping-cart-service/__init__.py",
    "content": ""
  },
  {
    "path": "backend/shopping-cart-service/add_to_cart.py",
    "content": "import json\nimport os\n\nimport boto3\nfrom aws_lambda_powertools import Logger, Metrics, Tracer\n\nfrom shared import (\n    NotFoundException,\n    generate_ttl,\n    get_cart_id,\n    get_headers,\n    get_user_sub,\n)\nfrom utils import get_product_from_external_service\n\nlogger = Logger()\ntracer = Tracer()\nmetrics = Metrics()\n\ndynamodb = boto3.resource(\"dynamodb\")\ntable = dynamodb.Table(os.environ[\"TABLE_NAME\"])\nproduct_service_url = os.environ[\"PRODUCT_SERVICE_URL\"]\n\n\n@metrics.log_metrics(capture_cold_start_metric=True)\n@logger.inject_lambda_context(log_event=True)\n@tracer.capture_lambda_handler\ndef lambda_handler(event, context):\n    \"\"\"\n    Add a the provided quantity of a product to a cart. Where an item already exists in the cart, the quantities will\n    be summed.\n    \"\"\"\n\n    try:\n        request_payload = json.loads(event[\"body\"])\n    except KeyError:\n        return {\n            \"statusCode\": 400,\n            \"headers\": get_headers(),\n            \"body\": json.dumps({\"message\": \"No Request payload\"}),\n        }\n    product_id = request_payload[\"productId\"]\n    quantity = request_payload.get(\"quantity\", 1)\n    cart_id, _ = get_cart_id(event[\"headers\"])\n\n    # Because this method can be called anonymously, we need to check there's a logged in user\n    user_sub = None\n    jwt_token = event[\"headers\"].get(\"Authorization\")\n    if jwt_token:\n        user_sub = get_user_sub(jwt_token)\n\n    try:\n        product = get_product_from_external_service(product_id)\n        logger.info(\"No product found with product_id: %s\", product_id)\n    except NotFoundException:\n        return {\n            \"statusCode\": 404,\n            \"headers\": get_headers(cart_id=cart_id),\n            \"body\": json.dumps({\"message\": \"product not found\"}),\n        }\n\n    if user_sub:\n        logger.info(\"Authenticated user\")\n        pk = f\"user#{user_sub}\"\n        ttl = generate_ttl(\n            7\n        )  # Set a longer ttl for logged in users - we want to keep their cart for longer.\n    else:\n        logger.info(\"Unauthenticated user\")\n        pk = f\"cart#{cart_id}\"\n        ttl = generate_ttl()\n\n    if int(quantity) < 0:\n        table.update_item(\n            Key={\"pk\": pk, \"sk\": f\"product#{product_id}\"},\n            ExpressionAttributeNames={\n                \"#quantity\": \"quantity\",\n                \"#expirationTime\": \"expirationTime\",\n                \"#productDetail\": \"productDetail\",\n            },\n            ExpressionAttributeValues={\n                \":val\": quantity,\n                \":ttl\": ttl,\n                \":productDetail\": product,\n                \":limit\": abs(quantity),\n            },\n            UpdateExpression=\"ADD #quantity :val SET #expirationTime = :ttl, #productDetail = :productDetail\",\n            # Prevent quantity less than 0\n            ConditionExpression=\"quantity >= :limit\",\n        )\n    else:\n        table.update_item(\n            Key={\"pk\": pk, \"sk\": f\"product#{product_id}\"},\n            ExpressionAttributeNames={\n                \"#quantity\": \"quantity\",\n                \"#expirationTime\": \"expirationTime\",\n                \"#productDetail\": \"productDetail\",\n            },\n            ExpressionAttributeValues={\n                \":val\": quantity,\n                \":ttl\": generate_ttl(),\n                \":productDetail\": product,\n            },\n            UpdateExpression=\"ADD #quantity :val SET #expirationTime = :ttl, #productDetail = :productDetail\",\n        )\n    metrics.add_metric(name=\"CartUpdated\", unit=\"Count\", value=1)\n\n    return {\n        \"statusCode\": 200,\n        \"headers\": get_headers(cart_id),\n        \"body\": json.dumps(\n            {\"productId\": product_id, \"message\": \"product added to cart\"}\n        ),\n    }\n"
  },
  {
    "path": "backend/shopping-cart-service/checkout_cart.py",
    "content": "import json\nimport os\n\nimport boto3\nfrom aws_lambda_powertools import Logger, Metrics, Tracer\nfrom boto3.dynamodb.conditions import Key\n\nfrom shared import get_cart_id, get_headers, handle_decimal_type\n\nlogger = Logger()\ntracer = Tracer()\nmetrics = Metrics()\n\ndynamodb = boto3.resource(\"dynamodb\")\n\nlogger.debug(\"Initializing DDB Table %s\", os.environ[\"TABLE_NAME\"])\ntable = dynamodb.Table(os.environ[\"TABLE_NAME\"])\n\n\n@metrics.log_metrics(capture_cold_start_metric=True)\n@logger.inject_lambda_context(log_event=True)\n@tracer.capture_lambda_handler\ndef lambda_handler(event, context):\n    \"\"\"\n    Update cart table to use user identifier instead of anonymous cookie value as a key. This will be called when a user\n    is logged in.\n    \"\"\"\n    cart_id, _ = get_cart_id(event[\"headers\"])\n\n    try:\n        # Because this method is authorized at API gateway layer, we don't need to validate the JWT claims here\n        user_id = event[\"requestContext\"][\"authorizer\"][\"claims\"][\"sub\"]\n    except KeyError:\n\n        return {\n            \"statusCode\": 400,\n            \"headers\": get_headers(cart_id),\n            \"body\": json.dumps({\"message\": \"Invalid user\"}),\n        }\n\n    # Get all cart items belonging to the user's identity\n    response = table.query(\n        KeyConditionExpression=Key(\"pk\").eq(f\"user#{user_id}\")\n        & Key(\"sk\").begins_with(\"product#\"),\n        ConsistentRead=True,  # Perform a strongly consistent read here to ensure we get correct and up to date cart\n    )\n\n    cart_items = response.get(\"Items\")\n    # batch_writer will be used to update status for cart entries belonging to the user\n    with table.batch_writer() as batch:\n        for item in cart_items:\n            # Delete ordered items\n            batch.delete_item(Key={\"pk\": item[\"pk\"], \"sk\": item[\"sk\"]})\n\n    metrics.add_metric(name=\"CartCheckedOut\", unit=\"Count\", value=1)\n    logger.info({\"action\": \"CartCheckedOut\", \"cartItems\": cart_items})\n\n    return {\n        \"statusCode\": 200,\n        \"headers\": get_headers(cart_id),\n        \"body\": json.dumps(\n            {\"products\": response.get(\"Items\")}, default=handle_decimal_type\n        ),\n    }\n"
  },
  {
    "path": "backend/shopping-cart-service/db_stream_handler.py",
    "content": "import os\nfrom collections import Counter\n\nimport boto3\nfrom aws_lambda_powertools import Logger, Tracer\nfrom boto3.dynamodb import types\n\nlogger = Logger()\ntracer = Tracer()\n\ndynamodb = boto3.resource(\"dynamodb\")\ntable = dynamodb.Table(os.environ[\"TABLE_NAME\"])\n\ndeserializer = types.TypeDeserializer()\n\n\n@tracer.capture_method\ndef dynamodb_to_python(dynamodb_item):\n    \"\"\"\n    Convert from dynamodb low level format to python dict\n    \"\"\"\n    return {k: deserializer.deserialize(v) for k, v in dynamodb_item.items()}\n\n\n@logger.inject_lambda_context(log_event=True)\n@tracer.capture_lambda_handler\ndef lambda_handler(event, context):\n    \"\"\"\n    Handle streams from DynamoDB table\n    \"\"\"\n\n    records = event[\"Records\"]\n    quantity_change_counter = Counter()\n\n    for record in records:\n        keys = dynamodb_to_python(record[\"dynamodb\"][\"Keys\"])\n        # NewImage record only exists if the event is INSERT or MODIFY\n        if record[\"eventName\"] in (\"INSERT\", \"MODIFY\"):\n            new_image = dynamodb_to_python(record[\"dynamodb\"][\"NewImage\"])\n        else:\n            new_image = {}\n\n        old_image_ddb = record[\"dynamodb\"].get(\"OldImage\")\n\n        if old_image_ddb:\n            old_image = dynamodb_to_python(\n                record[\"dynamodb\"].get(\"OldImage\")\n            )  # Won't exist in case event is INSERT\n        else:\n            old_image = {}\n\n        # We want to record the quantity change the change made to the db rather than absolute values\n        if keys[\"sk\"].startswith(\"product#\"):\n            quantity_change_counter.update(\n                {\n                    keys[\"sk\"]: new_image.get(\"quantity\", 0)\n                    - old_image.get(\"quantity\", 0)\n                }\n            )\n\n    for k, v in quantity_change_counter.items():\n        table.update_item(\n            Key={\"pk\": k, \"sk\": \"totalquantity\"},\n            ExpressionAttributeNames={\"#quantity\": \"quantity\"},\n            ExpressionAttributeValues={\":val\": v},\n            UpdateExpression=\"ADD #quantity :val\",\n        )\n\n    return {\n        \"statusCode\": 200,\n    }\n"
  },
  {
    "path": "backend/shopping-cart-service/delete_from_cart.py",
    "content": "import json\nimport os\n\nimport boto3\nfrom aws_lambda_powertools import Logger, Tracer\n\nlogger = Logger()\ntracer = Tracer()\n\ndynamodb = boto3.resource(\"dynamodb\")\ntable = dynamodb.Table(os.environ[\"TABLE_NAME\"])\n\n\n@logger.inject_lambda_context(log_event=True)\n@tracer.capture_lambda_handler\ndef lambda_handler(event, context):\n    \"\"\"\n    Handle messages from SQS Queue containing cart items, and delete them from DynamoDB.\n    \"\"\"\n\n    records = event[\"Records\"]\n    logger.info(f\"Deleting {len(records)} records\")\n    with table.batch_writer() as batch:\n        for item in records:\n            item_body = json.loads(item[\"body\"])\n            batch.delete_item(Key={\"pk\": item_body[\"pk\"], \"sk\": item_body[\"sk\"]})\n\n    return {\n        \"statusCode\": 200,\n    }\n"
  },
  {
    "path": "backend/shopping-cart-service/get_cart_total.py",
    "content": "import json\nimport os\n\nimport boto3\nfrom aws_lambda_powertools import Logger, Tracer\n\nfrom shared import handle_decimal_type\n\nlogger = Logger()\ntracer = Tracer()\n\ndynamodb = boto3.resource(\"dynamodb\")\ntable = dynamodb.Table(os.environ[\"TABLE_NAME\"])\n\n\n@logger.inject_lambda_context(log_event=True)\n@tracer.capture_lambda_handler\ndef lambda_handler(event, context):\n    \"\"\"\n    List items in shopping cart.\n    \"\"\"\n    product_id = event[\"pathParameters\"][\"product_id\"]\n    response = table.get_item(\n        Key={\"pk\": f\"product#{product_id}\", \"sk\": \"totalquantity\"}\n    )\n    quantity = response[\"Item\"][\"quantity\"]\n\n    return {\n        \"statusCode\": 200,\n        \"body\": json.dumps(\n            {\"product\": product_id, \"quantity\": quantity}, default=handle_decimal_type\n        ),\n    }\n"
  },
  {
    "path": "backend/shopping-cart-service/list_cart.py",
    "content": "import json\nimport os\n\nimport boto3\nfrom aws_lambda_powertools import Logger, Tracer\nfrom boto3.dynamodb.conditions import Key\n\nfrom shared import get_cart_id, get_headers, get_user_sub, handle_decimal_type\n\nlogger = Logger()\ntracer = Tracer()\n\ndynamodb = boto3.resource(\"dynamodb\")\ntable = dynamodb.Table(os.environ[\"TABLE_NAME\"])\n\n\n@logger.inject_lambda_context(log_event=True)\n@tracer.capture_lambda_handler\ndef lambda_handler(event, context):\n    \"\"\"\n    List items in shopping cart.\n    \"\"\"\n\n    cart_id, generated = get_cart_id(event[\"headers\"])\n\n    # Because this method can be called anonymously, we need to check there's a logged in user\n    jwt_token = event[\"headers\"].get(\"Authorization\")\n    if jwt_token:\n        user_sub = get_user_sub(jwt_token)\n        key_string = f\"user#{user_sub}\"\n        logger.structure_logs(append=True, cart_id=f\"user#{user_sub}\")\n    else:\n        key_string = f\"cart#{cart_id}\"\n        logger.structure_logs(append=True, cart_id=f\"cart#{cart_id}\")\n\n    # No need to query database if the cart_id was generated rather than passed into the function\n    if generated:\n        logger.info(\"cart ID was generated in this request, not fetching cart from DB\")\n        product_list = []\n    else:\n        logger.info(\"Fetching cart from DB\")\n        response = table.query(\n            KeyConditionExpression=Key(\"pk\").eq(key_string)\n            & Key(\"sk\").begins_with(\"product#\"),\n            ProjectionExpression=\"sk,quantity,productDetail\",\n            FilterExpression=\"quantity > :val\",  # Only return items with more than 0 quantity\n            ExpressionAttributeValues={\":val\": 0},\n        )\n        product_list = response.get(\"Items\", [])\n\n    for product in product_list:\n        product.update(\n            (k, v.replace(\"product#\", \"\")) for k, v in product.items() if k == \"sk\"\n        )\n\n    return {\n        \"statusCode\": 200,\n        \"headers\": get_headers(cart_id),\n        \"body\": json.dumps({\"products\": product_list}, default=handle_decimal_type),\n    }\n"
  },
  {
    "path": "backend/shopping-cart-service/migrate_cart.py",
    "content": "import json\nimport os\nimport threading\n\nimport boto3\nfrom aws_lambda_powertools import Logger, Metrics, Tracer\nfrom boto3.dynamodb.conditions import Key\n\nfrom shared import generate_ttl, get_cart_id, get_headers, handle_decimal_type\n\nlogger = Logger()\ntracer = Tracer()\nmetrics = Metrics()\n\ndynamodb = boto3.resource(\"dynamodb\")\ntable = dynamodb.Table(os.environ[\"TABLE_NAME\"])\nsqs = boto3.resource(\"sqs\")\nqueue = sqs.Queue(os.environ[\"DELETE_FROM_CART_SQS_QUEUE\"])\n\n\n@tracer.capture_method\ndef update_item(user_id, item):\n    \"\"\"\n    Update an item in the database, adding the quantity of the passed in item to the quantity of any products already\n    existing in the cart.\n    \"\"\"\n    table.update_item(\n        Key={\"pk\": f\"user#{user_id}\", \"sk\": item[\"sk\"]},\n        ExpressionAttributeNames={\n            \"#quantity\": \"quantity\",\n            \"#expirationTime\": \"expirationTime\",\n            \"#productDetail\": \"productDetail\",\n        },\n        ExpressionAttributeValues={\n            \":val\": item[\"quantity\"],\n            \":ttl\": generate_ttl(days=30),\n            \":productDetail\": item[\"productDetail\"],\n        },\n        UpdateExpression=\"ADD #quantity :val SET #expirationTime = :ttl, #productDetail = :productDetail\",\n    )\n\n\n@metrics.log_metrics(capture_cold_start_metric=True)\n@logger.inject_lambda_context(log_event=True)\n@tracer.capture_lambda_handler\ndef lambda_handler(event, context):\n    \"\"\"\n    Update cart table to use user identifier instead of anonymous cookie value as a key. This will be called when a user\n    is logged in.\n    \"\"\"\n\n    cart_id, _ = get_cart_id(event[\"headers\"])\n    try:\n        # Because this method is authorized at API gateway layer, we don't need to validate the JWT claims here\n        user_id = event[\"requestContext\"][\"authorizer\"][\"claims\"][\"sub\"]\n        logger.info(\"Migrating cart_id %s to user_id %s\", cart_id, user_id)\n    except KeyError:\n\n        return {\n            \"statusCode\": 400,\n            \"headers\": get_headers(cart_id),\n            \"body\": json.dumps({\"message\": \"Invalid user\"}),\n        }\n\n    # Get all cart items belonging to the user's anonymous identity\n    response = table.query(\n        KeyConditionExpression=Key(\"pk\").eq(f\"cart#{cart_id}\")\n        & Key(\"sk\").begins_with(\"product#\")\n    )\n    unauth_cart = response[\"Items\"]\n\n    # Since there's no batch operation available for updating items, and there's no dependency between them, we can\n    # run them in parallel threads.\n    thread_list = []\n\n    for item in unauth_cart:\n        # Store items with user identifier as pk instead of \"unauthenticated\" cart ID\n        # Using threading library to perform updates in parallel\n        ddb_updateitem_thread = threading.Thread(\n            target=update_item, args=(user_id, item)\n        )\n        thread_list.append(ddb_updateitem_thread)\n        ddb_updateitem_thread.start()\n\n        # Delete items with unauthenticated cart ID\n        # Rather than deleting directly, push to SQS queue to handle asynchronously\n        queue.send_message(MessageBody=json.dumps(item, default=handle_decimal_type))\n\n    for ddb_thread in thread_list:\n        ddb_thread.join()  # Block main thread until all updates finished\n\n    if unauth_cart:\n        metrics.add_metric(name=\"CartMigrated\", unit=\"Count\", value=1)\n\n    response = table.query(\n        KeyConditionExpression=Key(\"pk\").eq(f\"user#{user_id}\")\n        & Key(\"sk\").begins_with(\"product#\"),\n        ProjectionExpression=\"sk,quantity,productDetail\",\n        ConsistentRead=True,  # Perform a strongly consistent read here to ensure we get correct values after updates\n    )\n\n    product_list = response.get(\"Items\", [])\n    for product in product_list:\n        product.update(\n            (k, v.replace(\"product#\", \"\")) for k, v in product.items() if k == \"sk\"\n        )\n\n    return {\n        \"statusCode\": 200,\n        \"headers\": get_headers(cart_id),\n        \"body\": json.dumps({\"products\": product_list}, default=handle_decimal_type),\n    }\n"
  },
  {
    "path": "backend/shopping-cart-service/requirements.txt",
    "content": ""
  },
  {
    "path": "backend/shopping-cart-service/tests/__init__.py",
    "content": ""
  },
  {
    "path": "backend/shopping-cart-service/tests/test_example.py",
    "content": "import sys\nimport unittest\n\nsys.path.append(\"..\")  # Add application to path\nsys.path.append(\"./layers/\")  # Add layer to path\n\nimport shared  # noqa: E402  # import from layer\n\n\nclass Tests(unittest.TestCase):\n    \"\"\"\n    Example included to demonstrate how to run unit tests when using lambda layers.\n    \"\"\"\n\n    def setUp(self):\n        pass\n\n    def test_headers(self):\n        self.assertEqual(shared.HEADERS.get(\"Access-Control-Allow-Credentials\"), True)\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "backend/shopping-cart-service/update_cart.py",
    "content": "import json\nimport os\n\nimport boto3\nfrom aws_lambda_powertools import Logger, Metrics, Tracer\n\nfrom shared import (\n    NotFoundException,\n    generate_ttl,\n    get_cart_id,\n    get_headers,\n    get_user_sub,\n)\nfrom utils import get_product_from_external_service\n\nlogger = Logger()\ntracer = Tracer()\nmetrics = Metrics()\n\ndynamodb = boto3.resource(\"dynamodb\")\ntable = dynamodb.Table(os.environ[\"TABLE_NAME\"])\nproduct_service_url = os.environ[\"PRODUCT_SERVICE_URL\"]\n\n\n@metrics.log_metrics(capture_cold_start_metric=True)\n@logger.inject_lambda_context(log_event=True)\n@tracer.capture_lambda_handler\ndef lambda_handler(event, context):\n    \"\"\"\n    Idempotent update quantity of products in a cart. Quantity provided will overwrite existing quantity for a\n    specific product in cart, rather than adding to it.\n    \"\"\"\n\n    try:\n        request_payload = json.loads(event[\"body\"])\n    except KeyError:\n        return {\n            \"statusCode\": 400,\n            \"headers\": get_headers(),\n            \"body\": json.dumps({\"message\": \"No Request payload\"}),\n        }\n\n    # retrieve the product_id that was specified in the url\n    product_id = event[\"pathParameters\"][\"product_id\"]\n\n    quantity = int(request_payload[\"quantity\"])\n    cart_id, _ = get_cart_id(event[\"headers\"])\n\n    # Because this method can be called anonymously, we need to check if there's a logged in user\n    user_sub = None\n    jwt_token = event[\"headers\"].get(\"Authorization\")\n    if jwt_token:\n        user_sub = get_user_sub(jwt_token)\n\n    try:\n        product = get_product_from_external_service(product_id)\n    except NotFoundException:\n        logger.info(\"No product found with product_id: %s\", product_id)\n        return {\n            \"statusCode\": 404,\n            \"headers\": get_headers(cart_id=cart_id),\n            \"body\": json.dumps({\"message\": \"product not found\"}),\n        }\n\n    # Prevent storing negative quantities of things\n    if quantity < 0:\n        return {\n            \"statusCode\": 400,\n            \"headers\": get_headers(cart_id),\n            \"body\": json.dumps(\n                {\n                    \"productId\": product_id,\n                    \"message\": \"Quantity must not be lower than 0\",\n                }\n            ),\n        }\n\n    # Use logged in user's identifier if it exists, otherwise use the anonymous identifier\n\n    if user_sub:\n        pk = f\"user#{user_sub}\"\n        ttl = generate_ttl(\n            7\n        )  # Set a longer ttl for logged in users - we want to keep their cart for longer.\n    else:\n        pk = f\"cart#{cart_id}\"\n        ttl = generate_ttl()\n\n    table.put_item(\n        Item={\n            \"pk\": pk,\n            \"sk\": f\"product#{product_id}\",\n            \"quantity\": quantity,\n            \"expirationTime\": ttl,\n            \"productDetail\": product,\n        }\n    )\n    logger.info(\"about to add metrics...\")\n    metrics.add_metric(name=\"CartUpdated\", unit=\"Count\", value=1)\n\n    return {\n        \"statusCode\": 200,\n        \"headers\": get_headers(cart_id),\n        \"body\": json.dumps(\n            {\"productId\": product_id, \"quantity\": quantity, \"message\": \"cart updated\"}\n        ),\n    }\n"
  },
  {
    "path": "backend/shopping-cart-service/utils.py",
    "content": "import os\n\nimport requests\nfrom aws_lambda_powertools import Logger, Tracer\n\nfrom shared import NotFoundException\n\nproduct_service_url = os.environ[\"PRODUCT_SERVICE_URL\"]\n\nlogger = Logger()\ntracer = Tracer()\n\n\n@tracer.capture_method\ndef get_product_from_external_service(product_id):\n    \"\"\"\n    Call product API to retrieve product details\n    \"\"\"\n    response = requests.get(product_service_url + f\"/product/{product_id}\")\n    try:\n        response_dict = response.json()[\"product\"]\n    except KeyError:\n        logger.warn(\"No product found with id %s\", product_id)\n        raise NotFoundException\n\n    return response_dict\n"
  },
  {
    "path": "backend/shoppingcart-service.yaml",
    "content": "AWSTemplateFormatVersion: '2010-09-09'\nTransform: AWS::Serverless-2016-10-31\nDescription: >\n  shoppingcart-service\n\n  SAM Template for shoppingcart-service\n\nParameters:\n  UserPoolArn:\n    Type: 'AWS::SSM::Parameter::Value<String>'\n    Default: '/serverless-shopping-cart-demo/auth/user-pool-arn'\n  UserPoolId:\n    Type: 'AWS::SSM::Parameter::Value<String>'\n    Default: '/serverless-shopping-cart-demo/auth/user-pool-id'\n  ProductServiceUrl:\n    Type: 'AWS::SSM::Parameter::Value<String>'\n    Default: '/serverless-shopping-cart-demo/products/products-api-url'\n  AllowedOrigin:\n    Type: 'String'\n\nGlobals:\n  Function:\n    Timeout: 5\n    MemorySize: 512\n    Tracing: Active\n    AutoPublishAlias: live\n    Runtime: python3.8\n    Layers:\n      - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPython:3\n    Environment:\n      Variables:\n        TABLE_NAME: !Ref DynamoDBShoppingCartTable\n        LOG_LEVEL: \"INFO\"\n        ALLOWED_ORIGIN: !Ref AllowedOrigin\n        POWERTOOLS_SERVICE_NAME: shopping-cart\n        POWERTOOLS_METRICS_NAMESPACE: ecommerce-app\n  Api:\n    EndpointConfiguration: REGIONAL\n    TracingEnabled: true\n    OpenApiVersion: '2.0'\n    Cors:\n      AllowMethods: \"'OPTIONS,POST,GET,PUT'\"\n      AllowHeaders: \"'Content-Type,Authorization'\"\n      AllowCredentials: true\n      AllowOrigin: !Sub \"'${AllowedOrigin}'\"\n\nResources:\n  UtilsLayer:\n    Type: AWS::Serverless::LayerVersion\n    Properties:\n      ContentUri: ./layers/\n      CompatibleRuntimes:\n        - python3.8\n    Metadata:\n      BuildMethod: python3.8\n\n  CartApi:\n    Type: AWS::Serverless::Api\n    DependsOn:\n      - ApiGWAccount\n    Properties:\n      StageName: Prod\n      MethodSettings:\n        - DataTraceEnabled: True\n          MetricsEnabled: True\n          ResourcePath: \"/*\"\n          HttpMethod: \"*\"\n          LoggingLevel: INFO\n      Auth:\n        Authorizers:\n          CognitoAuthorizer:\n            UserPoolArn: !Ref UserPoolArn\n            Identity: # OPTIONAL\n              Header: Authorization # OPTIONAL; Default: 'Authorization'\n\n  ListCartRole:\n    Type: AWS::IAM::Role\n    Properties:\n      AssumeRolePolicyDocument:\n        Version: \"2012-10-17\"\n        Statement:\n          - Action:\n            - \"sts:AssumeRole\"\n            Effect: \"Allow\"\n            Principal:\n              Service:\n                - \"lambda.amazonaws.com\"\n\n  AddToCartRole:\n    Type: AWS::IAM::Role\n    Properties:\n      AssumeRolePolicyDocument:\n        Version: \"2012-10-17\"\n        Statement:\n          - Action:\n            - \"sts:AssumeRole\"\n            Effect: \"Allow\"\n            Principal:\n              Service:\n                - \"lambda.amazonaws.com\"\n\n  LambdaLoggingPolicy:\n    Type: \"AWS::IAM::Policy\"\n    Properties:\n      PolicyName: LambdaXRayPolicy\n      PolicyDocument:\n        Version: \"2012-10-17\"\n        Statement:\n          -\n            Effect: \"Allow\"\n            Action: [\n              \"xray:PutTraceSegments\",\n              \"xray:PutTelemetryRecords\",\n              \"logs:CreateLogGroup\",\n              \"logs:CreateLogStream\",\n              \"logs:PutLogEvents\"\n              ]\n            Resource: \"*\"\n      Roles:\n        - !Ref ListCartRole\n        - !Ref AddToCartRole\n\n  DynamoDBReadPolicy:\n    Type: \"AWS::IAM::Policy\"\n    Properties:\n      PolicyName: DynamoDBReadPolicy\n      PolicyDocument:\n        Version: \"2012-10-17\"\n        Statement:\n          -\n            Effect: \"Allow\"\n            Action: [\n              \"dynamodb:GetItem\",\n              \"dynamodb:Scan\",\n              \"dynamodb:Query\",\n              \"dynamodb:BatchGetItem\",\n              \"dynamodb:DescribeTable\"\n              ]\n            Resource:\n              - !GetAtt DynamoDBShoppingCartTable.Arn\n      Roles:\n        - !Ref ListCartRole\n        - !Ref AddToCartRole\n\n  DynamoDBWritePolicy:\n    Type: \"AWS::IAM::Policy\"\n    Properties:\n      PolicyName: DynamoDBWritePolicy\n      PolicyDocument:\n        Version: \"2012-10-17\"\n        Statement:\n          -\n            Effect: \"Allow\"\n            Action: [\n              \"dynamodb:PutItem\",\n              \"dynamodb:UpdateItem\",\n              \"dynamodb:ConditionCheckItem\",\n              \"dynamodb:DeleteItem\",\n              \"dynamodb:BatchWriteItem\"\n            ]\n            Resource: !GetAtt DynamoDBShoppingCartTable.Arn\n      Roles:\n        - !Ref AddToCartRole\n\n  SQSSendMessagePolicy:\n    Type: \"AWS::IAM::Policy\"\n    Properties:\n      PolicyName: SQSSendMessagePolicy\n      PolicyDocument:\n        Version: \"2012-10-17\"\n        Statement:\n          -\n            Effect: \"Allow\"\n            Action: [\n              \"sqs:SendMessage*\"\n            ]\n            Resource: !GetAtt CartDeleteSQSQueue.Arn\n      Roles:\n        - !Ref AddToCartRole\n\n  ListCartFunction:\n    Type: AWS::Serverless::Function\n    DependsOn:\n      - LambdaLoggingPolicy\n    Properties:\n      CodeUri: shopping-cart-service/\n      Handler: list_cart.lambda_handler\n      Role: !GetAtt ListCartRole.Arn\n      Layers:\n        - !Ref UtilsLayer\n      Environment:\n        Variables:\n          USERPOOL_ID: !Ref UserPoolId\n      Events:\n        ListCart:\n          Type: Api\n          Properties:\n            RestApiId: !Ref CartApi\n            Path: /cart\n            Method: get\n\n  AddToCartFunction:\n    Type: AWS::Serverless::Function\n    DependsOn:\n      - LambdaLoggingPolicy\n    Properties:\n      CodeUri: shopping-cart-service/\n      Handler: add_to_cart.lambda_handler\n      Role: !GetAtt AddToCartRole.Arn\n      Layers:\n        - !Ref UtilsLayer\n      Environment:\n        Variables:\n          PRODUCT_SERVICE_URL: !Ref ProductServiceUrl\n          USERPOOL_ID: !Ref UserPoolId\n      Events:\n        AddToCart:\n          Type: Api\n          Properties:\n            RestApiId: !Ref CartApi\n            Path: /cart\n            Method: post\n\n  UpdateCartFunction:\n    Type: AWS::Serverless::Function\n    DependsOn:\n      - LambdaLoggingPolicy\n    Properties:\n      CodeUri: shopping-cart-service/\n      Handler: update_cart.lambda_handler\n      Role: !GetAtt AddToCartRole.Arn\n      Layers:\n        - !Ref UtilsLayer\n      Environment:\n        Variables:\n          PRODUCT_SERVICE_URL: !Ref ProductServiceUrl\n          USERPOOL_ID: !Ref UserPoolId\n      Events:\n        AddToCart:\n          Type: Api\n          Properties:\n            RestApiId: !Ref CartApi\n            Path: /cart/{product_id}\n            Method: put\n\n  MigrateCartFunction:\n    Type: AWS::Serverless::Function\n    DependsOn:\n      - LambdaLoggingPolicy\n    Properties:\n      CodeUri: shopping-cart-service/\n      Handler: migrate_cart.lambda_handler\n      Timeout: 30\n      Layers:\n        - !Ref UtilsLayer\n      Environment:\n        Variables:\n          PRODUCT_SERVICE_URL: !Ref ProductServiceUrl\n          USERPOOL_ID: !Ref UserPoolId\n          DELETE_FROM_CART_SQS_QUEUE: !Ref CartDeleteSQSQueue\n      Role: !GetAtt AddToCartRole.Arn\n      Events:\n        AddToCart:\n          Type: Api\n          Properties:\n            RestApiId: !Ref CartApi\n            Path: /cart/migrate\n            Method: post\n            Auth:\n              Authorizer: CognitoAuthorizer\n\n  CheckoutCartFunction:\n    Type: AWS::Serverless::Function\n    DependsOn:\n      - LambdaLoggingPolicy\n    Properties:\n      CodeUri: shopping-cart-service/\n      Handler: checkout_cart.lambda_handler\n      Timeout: 10\n      Layers:\n        - !Ref UtilsLayer\n      Environment:\n        Variables:\n          PRODUCT_SERVICE_URL: !Ref ProductServiceUrl\n          USERPOOL_ID: !Ref UserPoolId\n      Role: !GetAtt AddToCartRole.Arn\n      Events:\n        AddToCart:\n          Type: Api\n          Properties:\n            RestApiId: !Ref CartApi\n            Path: /cart/checkout\n            Method: post\n            Auth:\n              Authorizer: CognitoAuthorizer\n\n  GetCartTotalFunction:\n    Type: AWS::Serverless::Function\n    DependsOn:\n      - LambdaLoggingPolicy\n    Properties:\n      CodeUri: shopping-cart-service/\n      Handler: get_cart_total.lambda_handler\n      Timeout: 10\n      Layers:\n        - !Ref UtilsLayer\n      Role: !GetAtt ListCartRole.Arn\n      Events:\n        GetCartTotal:\n          Type: Api\n          Properties:\n            RestApiId: !Ref CartApi\n            Path: /cart/{product_id}/total\n            Method: get\n\n  DeleteFromCartFunction:\n    Type: AWS::Serverless::Function\n    DependsOn:\n      - LambdaLoggingPolicy\n    Properties:\n      CodeUri: shopping-cart-service/\n      Handler: delete_from_cart.lambda_handler\n      ReservedConcurrentExecutions: 25  # Keep the ddb spikes down in case of many deletes at once\n      Policies:\n        - SQSPollerPolicy:\n            QueueName:\n              !GetAtt CartDeleteSQSQueue.QueueName\n        - Statement:\n            - Effect: Allow\n              Action:\n                - \"dynamodb:DeleteItem\"\n                - \"dynamodb:BatchWriteItem\"\n              Resource:\n                - !GetAtt DynamoDBShoppingCartTable.Arn\n      Layers:\n        - !Ref UtilsLayer\n      Environment:\n        Variables:\n          USERPOOL_ID: !Ref UserPoolId\n      Events:\n        RetrieveFromSQS:\n          Type: SQS\n          Properties:\n            Queue: !GetAtt CartDeleteSQSQueue.Arn\n            BatchSize: 5\n\n  CartDBStreamHandler:\n    Type: AWS::Serverless::Function\n    DependsOn:\n      - LambdaLoggingPolicy\n    Properties:\n      CodeUri: shopping-cart-service/\n      Handler: db_stream_handler.lambda_handler\n      Layers:\n        - !Ref UtilsLayer\n      Policies:\n        - AWSLambdaDynamoDBExecutionRole\n        - Statement:\n            - Effect: Allow\n              Action:\n                - \"dynamodb:UpdateItem\"\n              Resource:\n                - !GetAtt DynamoDBShoppingCartTable.Arn\n      Events:\n        Stream:\n          Type: DynamoDB\n          Properties:\n            Stream: !GetAtt DynamoDBShoppingCartTable.StreamArn\n            BatchSize: 100\n            MaximumBatchingWindowInSeconds: 60\n            StartingPosition: LATEST\n\n  DynamoDBShoppingCartTable:\n    Type: AWS::DynamoDB::Table\n    Properties:\n      AttributeDefinitions:\n        - AttributeName: pk\n          AttributeType: S\n        - AttributeName: sk\n          AttributeType: S\n      KeySchema:\n        - AttributeName: pk\n          KeyType: HASH\n        - AttributeName: sk\n          KeyType: RANGE\n      BillingMode: PAY_PER_REQUEST\n      StreamSpecification:\n        StreamViewType: 'NEW_AND_OLD_IMAGES'\n      TimeToLiveSpecification:\n        AttributeName: expirationTime\n        Enabled: True\n\n  APIGWCloudWatchRole:\n    Type: 'AWS::IAM::Role'\n    Properties:\n      AssumeRolePolicyDocument:\n        Version: 2012-10-17\n        Statement:\n          - Effect: Allow\n            Principal:\n              Service:\n                - apigateway.amazonaws.com\n            Action: 'sts:AssumeRole'\n      Path: /\n      ManagedPolicyArns:\n        - >-\n          arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs\n  ApiGWAccount:\n    Type: 'AWS::ApiGateway::Account'\n    Properties:\n      CloudWatchRoleArn: !GetAtt APIGWCloudWatchRole.Arn\n\n  CartDeleteSQSQueue:\n    Type: AWS::SQS::Queue\n    Properties:\n      VisibilityTimeout: 20\n      RedrivePolicy:\n        deadLetterTargetArn:\n          !GetAtt CartDeleteSQSDLQ.Arn\n        maxReceiveCount: 5\n  CartDeleteSQSDLQ:\n    Type: AWS::SQS::Queue\n\n  CartApiUrl:\n    Type: AWS::SSM::Parameter\n    Properties:\n      Type: String\n      Name: /serverless-shopping-cart-demo/shopping-cart/cart-api-url\n      Value: !Sub \"https://${CartApi}.execute-api.${AWS::Region}.amazonaws.com/Prod\"\n\nOutputs:\n  CartApi:\n    Description: \"API Gateway endpoint URL for Prod stage for Cart Service\"\n    Value: !Sub \"https://${CartApi}.execute-api.${AWS::Region}.amazonaws.com/Prod\"\n"
  },
  {
    "path": "frontend/.gitignore",
    "content": "# Created by .ignore support plugin (hsz.mobi)\n### Vue template\n# gitignore template for Vue.js projects\n#\n# Recommended template: Node.gitignore\n\n# TODO: where does this rule come from?\ndocs/_book\n\n# TODO: where does this rule come from?\ntest/\n\n### Node template\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# TypeScript v1 declaration files\ntypings/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variables file\n.env.test\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n\n# next.js build output\n.next\n\n# nuxt.js build output\n.nuxt\n\n# vuepress build output\n.vuepress/dist\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\ndist/*\n\n.vscode\n\n/amplify/*\n/amplify\n\n*.local\n.env"
  },
  {
    "path": "frontend/Makefile",
    "content": "all: build\n\nserve:\n\tyarn install\n\tyarn fetchConfig local\n\tyarn serve\n\nbuild:\n\tyarn install\n\tyarn fetchConfig\n\tyarn build\n\n.PHONY: build serve\n"
  },
  {
    "path": "frontend/babel.config.js",
    "content": "module.exports = {\n  presets: [\n    '@vue/cli-plugin-babel/preset'\n  ]\n}\n"
  },
  {
    "path": "frontend/package.json",
    "content": "{\n  \"name\": \"shoppingcart-service-frontend\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"serve\": \"vue-cli-service serve\",\n    \"build\": \"vue-cli-service build\",\n    \"lint\": \"vue-cli-service lint\",\n    \"fetchConfig\": \"node scripts/fetchconfig.js\"\n  },\n  \"dependencies\": {\n    \"aws-amplify\": \"^1.2.3\",\n    \"aws-amplify-vue\": \"^0.3.3\",\n    \"core-js\": \"^3.3.2\",\n    \"decimal.js\": \"^10.2.0\",\n    \"dotenv\": \"^8.2.0\",\n    \"v-mask\": \"^2.0.2\",\n    \"vue\": \"^2.6.10\",\n    \"vue-router\": \"^3.1.3\",\n    \"vuelidate\": \"^0.7.4\",\n    \"vuetify\": \"^2.1.7\",\n    \"vuex\": \"^3.1.1\"\n  },\n  \"devDependencies\": {\n    \"@vue/cli-plugin-babel\": \"^4.0.0\",\n    \"@vue/cli-plugin-eslint\": \"^4.0.0\",\n    \"@vue/cli-service\": \"^4.0.0\",\n    \"babel-eslint\": \"^10.0.3\",\n    \"eslint\": \"^5.16.0\",\n    \"eslint-plugin-vue\": \"^5.0.0\",\n    \"sass\": \"^1.19.0\",\n    \"sass-loader\": \"^8.0.0\",\n    \"vue-cli-plugin-vuetify\": \"^2.0.2\",\n    \"vue-template-compiler\": \"^2.6.10\",\n    \"vuetify-loader\": \"^1.3.0\"\n  },\n  \"resolutions\": {\n    \"websocket-extensions\": \"^0.1.4\",\n    \"axios\": \"^0.21.2\",\n    \"ssri\": \"^8.0.1\",\n    \"is-svg\": \"^4.2.2\",\n    \"glob-parent\": \"^5.1.2\",\n    \"set-value\": \"^4.0.1\",\n    \"ansi-regex\": \"^5.0.1\",\n    \"nth-check\": \"^2.0.1\",\n    \"aws-sdk\": \"^2.814.0\",\n    \"node-forge\": \"^1.0.0\"\n  },\n  \"eslintConfig\": {\n    \"root\": true,\n    \"env\": {\n      \"node\": true\n    },\n    \"extends\": [\n      \"plugin:vue/essential\",\n      \"eslint:recommended\"\n    ],\n    \"rules\": {},\n    \"parserOptions\": {\n      \"parser\": \"babel-eslint\"\n    }\n  },\n  \"postcss\": {\n    \"plugins\": {\n      \"autoprefixer\": {}\n    }\n  },\n  \"browserslist\": [\n    \"> 1%\",\n    \"last 2 versions\"\n  ]\n}\n"
  },
  {
    "path": "frontend/public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\">\n    <link rel=\"icon\" href=\"<%= BASE_URL %>favicon.ico\">\n    <title>Serverless Shopping Cart Demo</title>\n    <link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900\">\n    <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css\">\n    <link href=\"https://fonts.googleapis.com/css?family=Material+Icons\" rel=\"stylesheet\">\n\n  </head>\n  <body>\n    <noscript>\n      <strong>We're sorry but Serverless Shopping Cart Demo doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>\n    </noscript>\n    <div id=\"app\"></div>\n    <!-- built files will be auto injected -->\n  </body>\n</html>\n"
  },
  {
    "path": "frontend/scripts/fetchconfig.js",
    "content": "process.env.AWS_SDK_LOAD_CONFIG = true;\nvar fs = require('fs');\n\nvar args = process.argv.slice(2);\nvar envtype = args[0] ? args[0] : ''\nvar AWS = require('aws-sdk');\nvar ssm = new AWS.SSM();\n\n\nconst query = {\n    \"Path\": \"/serverless-shopping-cart-demo/\",\n    \"WithDecryption\": false,\n    \"Recursive\": true\n}\n\nconst requiredParams = [\"CART_API_URL\", \"PRODUCTS_API_URL\", \"USER_POOL_ID\",\n    \"USER_POOL_CLIENT_ID\"\n]\n\nvar params = ssm.getParametersByPath(query).promise()\n\nvar output = []\n\nfunction formatParams(data) {\n    for (var param of data) {\n        const paramName = param.Name.toUpperCase().split(\"/\").pop().replace(/-/g, \"_\")\n        if (requiredParams.includes(paramName)) {\n            output.push(\"VUE_APP_\" + paramName + '=' + param.Value)\n        }\n    }\n}\n\nparams\n    .then(data => {\n        formatParams(data.Parameters)\n        output.push(\"VUE_APP_AWS_REGION=\" + AWS.config.region)\n        var fileName\n        if (envtype) {\n            fileName = \"./.env.\" + envtype\n        }\n        else {\n            fileName = \"./.env\"\n        }\n        fs.writeFile(fileName, output.join('\\n'), function (err) {\n            if (err) {\n                return console.log(err);  // eslint-disable-line no-console\n            }\n            console.log(`env file ${fileName} populated with config`);  // eslint-disable-line no-console\n        });\n\n    })\n    .catch(error => {\n        console.log('error: ' + error)  // eslint-disable-line no-console\n    })\n"
  },
  {
    "path": "frontend/src/App.vue",
    "content": "<template>\n  <v-app>\n    <v-app-bar elevate-on-scroll app class=\"primary\">\n      <v-toolbar-title>\n        <router-link tag=\"div\" to=\"/\">\n          <a class=\"accent--text header font-weight-black\">\n            DEMO\n            <span class=\"font-weight-thin subheading secondary--text\">Store</span>\n          </a>\n        </router-link>\n      </v-toolbar-title>\n      <v-toolbar-items>\n        <v-btn to=\"/auth\" v-if=\"!currentUser\" text class=\"ml-2\">Sign In</v-btn>\n        <cart-button @drawerChange=\"toggleDrawer\" />\n        <div class=\"sign-out\">\n          <amplify-sign-out v-if=\"currentUser\" class=\"Form--signout pl-2\"></amplify-sign-out>\n        </div>\n      </v-toolbar-items>\n    </v-app-bar>\n    <v-content>\n      <v-container fluid>\n        <loading-overlay />\n        <v-fade-transition mode=\"out-in\">\n          <router-view></router-view>\n        </v-fade-transition>\n      </v-container>\n      <v-navigation-drawer\n        style=\"position:fixed; overflow-y:scroll;\"\n        right\n        v-model=\"drawer\"\n        temporary\n        align-space-around\n        column\n        d-flex\n      >\n        <cart-drawer />\n      </v-navigation-drawer>\n    </v-content>\n  </v-app>\n</template>\n\n<script>\nimport { mapGetters, mapState } from \"vuex\";\n\nexport default {\n  name: \"app\",\n  data() {\n    return {\n      drawer: null\n    };\n  },\n  mounted() {\n    this.$store.dispatch(\"fetchCart\");\n  },\n  computed: {\n    ...mapGetters([\"cartSize\", \"currentUser\"]),\n    ...mapState([\"cartLoading\"])\n  },\n  methods: {\n    logout() {\n      this.$store.dispatch(\"logout\");\n    },\n    toggleDrawer() {\n      this.drawer = !this.drawer;\n    }\n  }\n};\n</script>\n\n<style>\n.header {\n  font-weight: bold !important;\n  font-size: 30px !important;\n  text-decoration: none;\n}\n\n:root {\n  /* Colors */\n  --amazonOrange: #e88b01 !important;\n}\n</style>"
  },
  {
    "path": "frontend/src/aws-exports.js",
    "content": "const awsmobile = {\n  Auth: {\n    region: process.env.VUE_APP_AWS_REGION,\n    userPoolId: process.env.VUE_APP_USER_POOL_ID,\n    userPoolWebClientId: process.env.VUE_APP_USER_POOL_CLIENT_ID\n  },\n  API: {\n    endpoints: [{\n        name: \"CartAPI\",\n        endpoint: process.env.VUE_APP_CART_API_URL\n      },\n      {\n        name: \"ProductAPI\",\n        endpoint: process.env.VUE_APP_PRODUCTS_API_URL,\n      }\n    ]\n  }\n};\n\nexport default awsmobile;"
  },
  {
    "path": "frontend/src/backend/api.js",
    "content": "import {\n    Auth,\n    API\n} from 'aws-amplify'\n\nasync function getHeaders(includeAuth) {\n    const headers = {\n        \"Content-Type\": \"application/json\"\n    }\n\n    if (!includeAuth) {\n        return {\n            \"Content-Type\": \"application/json\"\n        }\n    }\n    let session = null\n    try {\n        session = await Auth.currentSession()\n    } catch (e) {\n        e == e\n    }\n    if (session) {\n        let authheader = session.getIdToken().jwtToken\n        headers['Authorization'] = authheader\n    }\n    return headers\n}\n\nexport async function getCart() {\n    return getHeaders(true).then(\n        headers => API.get(\"CartAPI\", \"/cart\", {\n            headers: headers,\n            withCredentials: true\n        }))\n}\n\nexport async function postCart(obj, quantity = 1) {\n    return getHeaders(true).then(\n        headers => API.post(\"CartAPI\", \"/cart\", {\n            body: {\n                productId: obj.productId,\n                quantity: quantity,\n            },\n            headers: headers,\n            withCredentials: true\n        })\n    )\n}\n\nexport async function putCart(obj, quantity) {\n    return getHeaders(true).then(\n        headers => API.put(\"CartAPI\", \"/cart/\" + obj.productId, {\n            body: {\n                productId: obj.productId,\n                quantity: quantity,\n            },\n            headers: headers,\n            withCredentials: true\n        })\n    )\n}\n\nexport async function getProducts() {\n    return getHeaders().then(\n        headers => API.get(\"ProductAPI\", \"/product\", {\n            headers: headers\n        })\n    )\n}\n\nexport async function cartMigrate() {\n    return getHeaders(true).then(\n        headers => API.post(\"CartAPI\", \"/cart/migrate\", {\n            headers: headers,\n            withCredentials: true\n        })\n    )\n}\n\nexport async function cartCheckout() {\n    return getHeaders(true).then(\n        headers => API.post(\"CartAPI\", \"/cart/checkout\", {\n            headers: headers,\n            withCredentials: true\n        })\n    )\n}"
  },
  {
    "path": "frontend/src/components/CartButton.vue",
    "content": "<template>\n  <v-btn fixed right rounded small @click.stop=\"toggleDrawer\" text icon x-large>\n    <v-badge overlap color=\"accent\" v-bind:class=\"{ 'animated-pulse': cartLoading > 0 }\">\n      <div slot=\"badge\">{{cartSize}}</div>\n      <v-icon color=\"black\" x-large>mdi-cart</v-icon>\n    </v-badge>\n  </v-btn>\n</template>\n\n<script>\nimport { mapGetters, mapState } from \"vuex\";\n\nexport default {\n  name: \"cart-button\",\n  computed: {\n    ...mapGetters([\"cartSize\"]),\n    ...mapState([\"cartLoading\"])\n  },\n  data() {\n    return {\n      drawer: false\n    };\n  },\n  methods: {\n    toggleDrawer() {\n      this.$emit(\"drawerChange\");\n    }\n  }\n};\n</script>\n<style scoped>\n@-webkit-keyframes pulse {\n  from {\n    -webkit-transform: scale3d(1, 1, 1);\n    transform: scale3d(1, 1, 1);\n  }\n\n  50% {\n    -webkit-transform: scale3d(1.25, 1.25, 1.25);\n    transform: scale3d(1.25, 1.25, 1.25);\n  }\n\n  to {\n    -webkit-transform: scale3d(1, 1, 1);\n    transform: scale3d(1, 1, 1);\n  }\n}\n\n@keyframes pulse {\n  from {\n    -webkit-transform: scale3d(1, 1, 1);\n    transform: scale3d(1, 1, 1);\n  }\n\n  50% {\n    -webkit-transform: scale3d(1.25, 1.25, 1.25);\n    transform: scale3d(1.25, 1.25, 1.25);\n  }\n\n  to {\n    -webkit-transform: scale3d(1, 1, 1);\n    transform: scale3d(1, 1, 1);\n  }\n}\n\n.animated-pulse {\n  animation: pulse 0.5s infinite;\n}\n</style>"
  },
  {
    "path": "frontend/src/components/CartDrawer.vue",
    "content": "<template>\n  <div class=\"items\">\n    <v-list-item>\n        <v-icon>mdi-cart</v-icon>\n      <v-list-item-content>\n        <v-list-item-title class=\"accent--text font-weight-bold\">SHOPPING CART</v-list-item-title>\n      </v-list-item-content>\n    </v-list-item>\n\n    <v-divider></v-divider>\n\n    <v-list dense>\n      <v-list-item v-for=\"item in getCart\" :key=\"item.sk\" link>\n        <v-list-item-content>\n          <v-list-item-content>\n            <p>\n              {{ item.productDetail.name }}\n              <span class=\"font-weight-light\">x {{item.quantity}}</span>\n            </p>\n            <span class=\"font-weight-light\">${{getTotalPrice(item)}}</span>\n          </v-list-item-content>\n        </v-list-item-content>\n      </v-list-item>\n      <v-list-item-content>\n        <v-list-item v-if=\"cartTotalAmount > 0\">\n          <v-btn to=\"/checkout\" block color=\"accent\">Checkout ${{cartTotalAmount}}</v-btn>\n        </v-list-item>\n        <v-list-item v-else>Cart Empty</v-list-item>\n      </v-list-item-content>\n    </v-list>\n  </div>\n</template>\n\n<script>\nimport { mapGetters, mapState } from \"vuex\";\nimport { Decimal } from \"decimal.js\";\n\nexport default {\n  name: \"cart-drawer\",\n  data() {\n    return {\n      signedIn: false\n    };\n  },\n  computed: {\n    ...mapGetters([\"cartTotalAmount\", \"getCart\"]),\n    ...mapState([\"cart\"])\n  },\n  methods: {\n    getTotalPrice(item) {\n      return new Decimal((item.productDetail.price/100) * item.quantity).toFixed(2);\n    }\n  }\n};\n</script>\n\n<style>\n.header {\n  font-weight: bold !important;\n  font-size: 30px !important;\n  text-decoration: none;\n}\n</style>"
  },
  {
    "path": "frontend/src/components/CartQuantityEditor.vue",
    "content": "<template>\n  <div>\n    <input\n      type=\"text\"\n      v-if=\"edit\"\n      class=\"cart-quantity-input text-center\"\n      :class=\"{ 'input-error': $v.quantity.$error }\"\n      v-model.trim=\"$v.quantity.$model\"\n      @focus=\"oldquantity=$event.target.value, $event.target.select()\"\n      @blur=\"submit($event, product)\"\n      @keyup.enter=\"$event.target.blur()\"\n      v-focus\n    />\n    <div\n      @click=\"edit = true;\"\n      v-else\n      v-bind:class=\"{ 'font-weight-light': quantity < 1 }\"\n      class=\"pl-2 pr-2 noselect\"\n    >{{quantity}}</div>\n  </div>\n</template>\n\n<script>\nimport { validationMixin } from \"vuelidate\";\nimport { required, between, integer } from \"vuelidate/lib/validators\";\n\nexport default {\n  props: [\"value\", \"product\"],\n\n  data() {\n    return {\n      edit: false,\n      quantity: this.value,\n      oldquantity: null\n    };\n  },\n  methods: {\n    submit(event, product) {\n      this.quantity = event.target.value;\n      this.$v.quantity.$touch();\n      if (this.$v.$invalid) {\n        this.quantity = this.oldquantity\n        this.edit = false;\n      } else {\n        this.edit = false;\n        this.$emit(\"input\", { quantity: this.quantity, product });\n      }\n    }\n  },\n  validations: {\n    quantity: {\n      required,\n      between: between(0, 500),\n      integer\n    }\n  },\n  mixins: [validationMixin],\n  watch: {\n    value: function() {\n      this.quantity = this.value;\n    }\n  },\n  directives: {\n    focus: {\n      inserted(el) {\n        el.focus();\n      }\n    }\n  }\n};\n</script>\n\n<style scoped>\n.cart-quantity-input {\n  width: 25px;\n}\n.noselect {\n  -webkit-touch-callout: none; /* iOS Safari */\n  -webkit-user-select: none; /* Safari */\n  -khtml-user-select: none; /* Konqueror HTML */\n  -moz-user-select: none; /* Old versions of Firefox */\n  -ms-user-select: none; /* Internet Explorer/Edge */\n  user-select: none; /* Non-prefixed version, currently\n                                  supported by Chrome, Opera and Firefox */\n}\n.input-error {\n  color: red;\n}\n</style>"
  },
  {
    "path": "frontend/src/components/LoadingOverlay.vue",
    "content": "<template>\n  <v-layout row justify-center>\n    <v-dialog v-model=\"loading\" persistent fullscreen content-class=\"loading-dialog\">\n      <v-container fill-height justify=\"center\" align=\"center\" style=\"height: 300px;\">\n        <v-row justify=\"center\" align=\"center\">\n          <v-progress-circular justify-center indeterminate :size=\"70\" :width=\"7\" color=\"accent\"></v-progress-circular>\n        </v-row>\n        <v-row v-if=\"loadingText\" justify=\"center\" align=\"center\">\n          <h1 class=\"accent--text\">{{loadingText}}</h1>\n        </v-row>\n      </v-container>\n    </v-dialog>\n  </v-layout>\n</template>\n\n<script>\nimport { mapState } from \"vuex\";\n\nexport default {\n  name: \"loading-overlay\",\n  data() {\n    return {};\n  },\n  computed: {\n    ...mapState([\"loading\", \"loadingText\"])\n  }\n};\n</script>\n\n<style>\n.loading-dialog {\n  background-color: #303030b2;\n}\n</style>"
  },
  {
    "path": "frontend/src/components/Product.vue",
    "content": "<template>\n  <v-card outlined class=\"flexcard\" height=\"100%\">\n    <v-row class=\"pb-0\" dense>\n      <v-col :cols=\"8\" class=\"mb-5\">\n        <v-card-title primary-title class=\"pb-0 pt-2\">\n          <p class=\"subtitle-2\">{{product.name}}</p>\n        </v-card-title>\n      </v-col>\n      <v-col>\n        <p class=\"text-truncate body-2 pt-2 pb-0 pr-2 grow text-right mb-1\">{{product.category}}</p>\n      </v-col>\n    </v-row>\n    <v-card-text class=\"pt-0 pl-4 pb-0\">\n      <p class=\"pt-0 pb-0 mb-0 body-2\">\"{{product.description}}\"</p>\n      <p class=\"price pt-0 pb-0 grow accent--text mb-1\">${{getPrice(product)}}</p>\n    </v-card-text>\n    <v-card-actions class=\"card-actions pa-0 ml-3 mb-2 mt-2 justify-center\">\n      <v-btn\n        icon\n        small\n        :disabled=\"cartItemCount(product.productId) < 1\"\n        @click=\"removeProductFromCart(product)\"\n        :loading=\"product.removeLoading\"\n      >\n        <v-icon>mdi-minus</v-icon>\n      </v-btn>\n      <cart-quantity-editor @input=\"updateCart\" :product=\"product\" :value=\"cartItemCount(product.productId)\"></cart-quantity-editor>\n      <v-btn icon small depressed @click=\"addProductToCart(product)\" :loading=\"product.addLoading\">\n        <v-icon>mdi-plus</v-icon>\n      </v-btn>\n    </v-card-actions>\n  </v-card>\n</template>\n\n<script>\nimport { Decimal } from \"decimal.js\";\n\nexport default {\n  props: [\"product\"],\n  name: \"product\",\n  methods: {\n    cartItemCount(id) {\n      let item = this.$store.state.cart.find(obj => obj.sk === id);\n      if (item) {\n        return item.quantity;\n      } else {\n        return 0;\n      }\n    },\n    addProductToCart(product) {\n      this.$store.dispatch(\"addToCart\", product);\n    },\n    removeProductFromCart(product) {\n      this.$store.dispatch(\"removeFromCart\", product);\n    },\n    getPrice(product) {\n      return new Decimal(product.price/100).toFixed(2);\n    },\n    updateCart(event) {\n      this.$store.dispatch(\"updateCart\", event)\n    }\n  }\n};\n</script>\n\n<style scoped>\n.flexcard {\n  position: relative;\n  padding-bottom: 50px;\n}\n.card-actions {\n  position: absolute;\n  bottom: 0;\n  border: 1px solid;\n  border-radius: 15px !important;\n  border-color: #dce1e9;\n}\n</style>"
  },
  {
    "path": "frontend/src/main.js",
    "content": "import Vue from 'vue'\nimport VueRouter from 'vue-router'\n\nimport Amplify from 'aws-amplify'\nimport Vuelidate from 'vuelidate'\nimport VueMask from 'v-mask'\nimport App from './App'\nimport router from './router'\nimport config from './aws-exports'\nimport vuetify from '@/plugins/vuetify'\nimport store from './store/store'\nimport {\n  components\n} from 'aws-amplify-vue';\n\nimport CartButton from \"@/components/CartButton.vue\";\nimport CartDrawer from \"@/components/CartDrawer.vue\";\nimport LoadingOverlay from \"@/components/LoadingOverlay.vue\";\nimport Product from \"@/components/Product.vue\";\nimport CartQuantityEditor from \"@/components/CartQuantityEditor.vue\"\n\nVue.config.productionTip = false\n\nAmplify.configure(config)\nVue.use(VueRouter)\nVue.use(Vuelidate)\nVue.use(VueMask);\n\nVue.component('cart-button', CartButton)\nVue.component('cart-drawer', CartDrawer)\nVue.component('loading-overlay', LoadingOverlay)\nVue.component('product', Product)\nVue.component('cart-quantity-editor', CartQuantityEditor)\n\n\nnew Vue({\n  render: h => h(App),\n  router,\n  vuetify,\n  store,\n  components: {\n    ...components\n  }\n}).$mount('#app')\n"
  },
  {
    "path": "frontend/src/plugins/vuetify.js",
    "content": "import Vue from 'vue'\nimport Vuetify from 'vuetify/lib'\n\nVue.use(Vuetify)\n\nconst opts = {\n    theme: {\n      themes: {\n        light: {\n            primary: '#DCE1E9',\n            secondary: '#363732',\n            accent: '#e88b01'\n        },\n      },\n    },\n  }\n\nexport default new Vuetify(opts)"
  },
  {
    "path": "frontend/src/router.js",
    "content": "import VueRouter from 'vue-router'\nimport Vue from 'vue';\nimport store from '@/store/store.js'\nimport Home from '@/views/Home.vue'\nimport Payment from '@/views/Payment.vue'\nimport Auth from '@/views/Auth.vue'\nimport {\n  components,\n  AmplifyEventBus\n} from 'aws-amplify-vue';\n\nimport * as AmplifyModules from 'aws-amplify'\nimport {\n  AmplifyPlugin\n} from 'aws-amplify-vue'\n\nVue.use(AmplifyPlugin, AmplifyModules)\n\n\ngetUser().then((user) => {\n  if (user) {\n    router.push({\n      path: '/'\n    }).catch(() => {})\n  }\n})\n\nAmplifyEventBus.$on('authState', async (state) => {\n  if (state === 'signedOut') {\n    store.commit('setUser', null);\n    store.dispatch('fetchCart')\n    router.push({\n      path: '/'\n    }).catch(() => {})\n  } else if (state === 'signedIn') {\n    getUser().then(() => {\n      if (store.state.cart.length > 0) {\n        store.dispatch('migrateCart')\n      } else {\n        store.dispatch('fetchCart')\n      }\n    })\n    router.push({\n      path: new URLSearchParams(window.location.search).get('redirect')|| '/'\n    }).catch(() => {})\n  }\n});\n\nfunction getUser() {\n  return Vue.prototype.$Amplify.Auth.currentAuthenticatedUser().then((data) => {\n    if (data && data.signInUserSession) {\n      store.commit('setUser', data);\n      return data;\n    }\n  }).catch(() => {\n    store.commit('setUser', null);\n    return null\n  });\n}\nconst routes = [{\n    path: '/',\n    component: Home\n  },\n  {\n    path: '/auth',\n    name: 'Authenticator',\n    component: Auth\n  }, {\n    path: '/checkout',\n    component: Payment,\n    meta: {\n      requiresAuth: true\n    }\n  }\n]\n\nconst router = new VueRouter({\n  mode: 'history',\n  routes\n})\n\nrouter.beforeResolve(async (to, from, next) => {\n  if (to.matched.some(record => record.meta.requiresAuth)) {\n    let user = await getUser();\n    if (!user) {\n      return next({\n        path: '/auth',\n        query: {\n          redirect: to.fullPath,\n        }\n      });\n    }\n    return next()\n  }\n  return next()\n})\n\nexport default router"
  },
  {
    "path": "frontend/src/store/actions.js",
    "content": "import {\n    postCart,\n    getCart,\n    getProducts,\n    cartMigrate,\n    putCart,\n    cartCheckout\n} from \"@/backend/api.js\"\n\nimport router from '@/router'\n\nconst setLoading = ({\n    commit\n}, payload) => {\n    commit(\"setLoading\", {value: payload.value,\n    message: payload.message})\n}\n\nconst fetchProducts = ({\n    commit\n}) => {\n    getProducts().then((response) => {\n        commit(\"setUpProducts\", response.products);\n    });\n}\nconst fetchCart = ({\n    commit\n}) => {\n    commit(\"setLoading\", {value: true})\n    getCart()\n        .then((response) => {\n            commit(\"setUpCart\", response.products)\n            commit(\"setLoading\", {value: false})\n        })\n}\nconst addToCart = ({\n    commit\n}, obj) => {\n    commit(\"setProductLoading\", {\n        \"product\": obj,\n        \"value\": true,\n        \"btn\": \"add\"\n    })\n    postCart(obj)\n        .then((response) => {\n            commit(\"setCartLoading\", 1)\n            commit(\"addToCart\", response.productId);\n            commit(\"setProductLoading\", {\n                \"product\": obj,\n                \"value\": false,\n                \"btn\": \"add\"\n            })\n            setTimeout(() => {\n                commit(\"setCartLoading\", -1)\n            }, 500)\n        }).catch(() => {\n            commit(\"setProductLoading\", {\n                \"product\": obj,\n                \"value\": false,\n                \"btn\": \"add\"\n            })\n        });\n}\nconst removeFromCart = ({\n    commit\n}, obj) => {\n    commit(\"setProductLoading\", {\n        \"product\": obj,\n        \"value\": true,\n        \"btn\": \"remove\"\n    })\n    postCart(obj, -1)\n        .then((response) => {\n            commit(\"setCartLoading\", 1)\n            commit(\"removeFromCart\", response.productId)\n            commit(\"setProductLoading\", {\n                \"product\": obj,\n                \"value\": false,\n                \"btn\": \"remove\"\n            })\n            setTimeout(() => {\n                commit(\"setCartLoading\", -1)\n            }, 500)\n        }).catch(() => {\n            commit(\"setProductLoading\", {\n                \"product\": obj,\n                \"value\": false,\n                \"btn\": \"remove\"\n            })\n        })\n}\nconst updateCart = ({\n    commit\n}, obj) => {\n    putCart(obj.product, obj.quantity)\n        .then((response) => {\n            commit(\"setCartLoading\", 1)\n            commit(\"updateCart\", response)\n            setTimeout(() => {\n                commit(\"setCartLoading\", -1)\n            }, 500)\n        })\n}\nconst migrateCart = ({\n    commit\n}) => {\n    commit(\"setLoading\", {value: true})\n    cartMigrate()\n        .then((response) => {\n            commit(\"setUpCart\", response.products)\n            commit(\"setLoading\", {value: false})\n        })\n}\n\nconst checkoutCart = ({\n    commit\n}) => {\n    commit(\"setLoading\", {value: true, message: \"This is where we'd handle payment before clearing the cart...\"})\n    cartCheckout()\n        .then(() => {\n            commit(\"setUpCart\", [])\n            setTimeout(function() {commit(\"setLoading\", {value: false})}, 3000)\n            setTimeout(function() {router.push(\"/\")}, 3200)\n        })\n}\n\nexport default {\n    setLoading,\n    fetchCart,\n    fetchProducts,\n    migrateCart,\n    updateCart,\n    removeFromCart,\n    addToCart,\n    checkoutCart\n}"
  },
  {
    "path": "frontend/src/store/getters.js",
    "content": "import {\n    Decimal\n} from \"decimal.js\"\n\n\nconst cartSize = (state) => {\n    return state.cart.reduce((total, cartProduct) => {\n        return total + cartProduct.quantity\n    }, 0)\n}\nconst cartTotalAmount = (state) => {\n    let val = state.cart.reduce((total, cartProduct) => {\n        return new Decimal(total).plus(new Decimal(cartProduct.productDetail.price/100).times(cartProduct.quantity));\n    }, 0);\n    return new Decimal(val).toFixed(2)\n}\nconst currentUser = (state) => {\n    return state.user\n}\nconst getCart = (state) => {\n    return state.cart.filter((prod) => prod.quantity > 0)\n}\n\nexport default {\n    cartSize,\n    cartTotalAmount,\n    currentUser,\n    getCart\n}"
  },
  {
    "path": "frontend/src/store/mutations.js",
    "content": "const setUser = (state, user) => {\n    state.user = user\n}\n\nconst setUpProducts = (state, productsPayload) => {\n    productsPayload.forEach((product) => {\n        product.addLoading = false\n        product.removeLoading = false\n    })\n    state.products = productsPayload;\n}\nconst setUpCart = (state, cartPayload) => {\n    state.cart = cartPayload;\n}\nconst addToCart = (state, productId) => {\n    let product = {}\n    product.productDetail = state.products.find((prod) => prod.productId === productId);\n    product.sk = productId\n    let cartProduct = state.cart.find((prod) => prod.sk === productId);\n    if (cartProduct) {\n        cartProduct.quantity++;\n    } else {\n        state.cart.push({\n            ...product,\n            quantity: 1,\n        });\n    }\n}\nconst removeFromCart = (state, productId) => {\n    let product = {}\n    product.productDetail = state.products[productId];\n    product.sk = productId\n    let cartProduct = state.cart.find((prod) => prod.sk === productId);\n    cartProduct.quantity--;\n}\nconst deleteFromCart = (state, productId) => {\n    let product = state.products.find((product) => product.productId === productId);\n    let cartProductIndex = state.cart.findIndex((product) => product.productId === productId);\n    product.quantity = state.cart[cartProductIndex].stock;\n    state.cart.splice(cartProductIndex, 1);\n}\nconst setLoading = (state, payload) => {\n    state.loading = payload.value\n    state.loadingText = payload.message\n}\nconst setCartLoading = (state, value) => {\n    state.cartLoading += value\n}\nconst setProductLoading = (state, {\n    product,\n    btn,\n    value\n}) => {\n    let prod = state.products.find((prod) => prod.productId === product.productId);\n    prod[btn + \"Loading\"] = value\n}\nconst updateCart = (state, obj) => {\n    let product = {}\n    product.productDetail = state.products.find((prod) => prod.productId === obj.productId);\n    product.sk = obj.productId\n    let cartProduct = state.cart.find((prod) => prod.sk === obj.productId);\n    if (cartProduct) {\n        cartProduct.quantity = obj.quantity\n    } else {\n        state.cart.push({\n            ...product,\n            quantity: obj.quantity,\n        });\n    }\n}\n\nexport default {\n    setUser,\n    setUpProducts,\n    setUpCart,\n    addToCart,\n    removeFromCart,\n    deleteFromCart,\n    setLoading,\n    setCartLoading,\n    setProductLoading,\n    updateCart\n}"
  },
  {
    "path": "frontend/src/store/store.js",
    "content": "import Vue from 'vue'\nimport Vuex from 'vuex'\nimport actions from './actions';\nimport mutations from './mutations';\nimport getters from './getters';\n\n\nVue.use(Vuex);\n\nexport default new Vuex.Store({\n  state: {\n    products: null,\n    cart: [],\n    user: null,\n    loading: false,\n    cartLoading: 0,\n    loadingText: \"\"\n  },\n  getters,\n  mutations,\n  actions\n})"
  },
  {
    "path": "frontend/src/views/Auth.vue",
    "content": "<template>\n    <amplify-authenticator v-bind:authConfig=\"authConfig\"/>\n</template>\n\n<script>\nexport default {\n  data() {\n      return {\n      authConfig: {\n          signUpConfig: {\n            hideAllDefaults: true,\n            defaultCountryCode: '1',\n            signUpFields: [\n              {\n                label: 'Username',\n                key: 'username',\n                required: true,\n                displayOrder: 1,\n                type: 'string',\n              },\n              {\n                label: 'Password',\n                key: 'password',\n                required: true,\n                displayOrder: 2,\n                type: 'password'\n              },\n              {\n                label: 'Email',\n                key: 'email',\n                required: true,\n                displayOrder: 3,\n                type: 'string'\n              }\n            ]\n          }\n        }\n      }\n  }\n};\n</script>"
  },
  {
    "path": "frontend/src/views/Home.vue",
    "content": "<template>\n    <v-container grid-list-md fluid class=\"mt-0\" pt-0>\n      <v-layout row wrap>\n        <v-flex v-for=\"product in products\" :key=\"product.productId\" xs12 lg4 sm6>\n            <product :product=\"product\" :key=\"product.productId\" />\n        </v-flex>\n      </v-layout>\n    </v-container>\n</template>\n\n<script>\nexport default {\n  computed: {\n    products() {\n      return this.$store.state.products;\n    }\n  },\n  created() {\n    this.$store.dispatch(\"fetchProducts\");\n  }\n};\n</script>"
  },
  {
    "path": "frontend/src/views/Payment.vue",
    "content": "<template>\n  <v-container grid-list-md fluid class=\"mt-0\" pt-0>\n    <h1>Example payment form</h1>\n    <v-layout row wrap>\n      <v-flex xs12 lg4 sm6>\n        <v-card>\n          <v-container>\n            <v-form pa-2 ma-2>\n              <v-text-field\n                color=\"secondary\"\n                outlined\n                required\n                @input=\"$v.cardNumber.$touch()\"\n                @blur=\"$v.cardNumber.$touch()\"\n                v-model=\"cardNumber\"\n                label=\"Card Number\"\n                v-mask=\"'#### #### #### ####'\"\n                :error-messages=\"cardNumberErrors\"\n              ></v-text-field>\n              <v-text-field\n                color=\"secondary\"\n                outlined\n                required\n                @input=\"$v.cardName.$touch()\"\n                @blur=\"$v.cardName.$touch()\"\n                label=\"Cardholder Name\"\n                v-model=\"cardName\"\n                :error-messages=\"cardNameErrors\"\n              ></v-text-field>\n              <v-text-field\n                color=\"secondary\"\n                outlined\n                required\n                @input=\"$v.cardExpiry.$touch()\"\n                @blur=\"$v.cardExpiry.$touch()\"\n                label=\"Card Expiry\"\n                v-model=\"cardExpiry\"\n                :error-messages=\"cardExpiryErrors\"\n              ></v-text-field>\n              <v-text-field\n                color=\"secondary\"\n                outlined\n                required\n                @input=\"$v.cardCVC.$touch()\"\n                @blur=\"$v.cardCVC.$touch()\"\n                label=\"Card CVC\"\n                v-model=\"cardCVC\"\n                :error-messages=\"cardCVCErrors\"\n              ></v-text-field>\n              <v-btn block color=\"accent\" @click=\"submit\">Submit</v-btn>\n            </v-form>\n          </v-container>\n        </v-card>\n      </v-flex>\n    </v-layout>\n  </v-container>\n</template>\n\n<script>\nimport { mapState, mapGetters } from \"vuex\";\nimport { validationMixin } from \"vuelidate\";\nimport { required, helpers } from \"vuelidate/lib/validators\";\n\nconst ccvalidate = helpers.regex(\n  \"alpha\",\n  /(\\d{4} *\\d{4} *\\d{4} *\\d{4})/\n); /* I know, I know... */\n\nexport default {\n  data() {\n    return {\n      cardNumber: null,\n      cardExpiry: null,\n      cardName: null,\n      cardCVC: null\n    };\n  },\n  mixins: [validationMixin],\n  validations: {\n    cardNumber: {\n      required,\n      ccvalidate: ccvalidate\n    },\n    cardExpiry: {\n      required\n    },\n    cardCVC: {\n      required\n    },\n    cardName: {\n      required\n    }\n  },\n  methods: {\n    submit() {\n      this.$v.$touch();\n      if (this.$v.$invalid) {\n        console.log(\"invalid form\"); // eslint-disable-line no-console\n      } else {\n        this.$store.dispatch(\"checkoutCart\")\n        // TODO: redirect to confirmation\n      }\n    }\n  },\n  computed: {\n    ...mapGetters([\"cartTotalAmount\", \"getCart\"]),\n    ...mapState([\"cart\"]),\n    cardNumberErrors() {\n      const errors = [];\n      if (!this.$v.cardNumber.$dirty) return errors;\n      !this.$v.cardNumber.ccvalidate &&\n        errors.push(\"Valid card number is required.\");\n      !this.$v.cardNumber.required && errors.push(\"Card number is required.\");\n      return errors;\n    },\n    cardNameErrors() {\n      const errors = [];\n      if (!this.$v.cardName.$dirty) return errors;\n      !this.$v.cardName.required && errors.push(\"Cardholder name is required.\");\n      return errors;\n    },\n    cardExpiryErrors() {\n      const errors = [];\n      if (!this.$v.cardExpiry.$dirty) return errors;\n      !this.$v.cardExpiry.required && errors.push(\"Cart expiry is required.\");\n      return errors;\n    },\n    cardCVCErrors() {\n      const errors = [];\n      if (!this.$v.cardCVC.$dirty) return errors;\n      !this.$v.cardCVC.required && errors.push(\"Card CVC is required.\");\n      return errors;\n    }\n  }\n};\n</script>\n\n"
  },
  {
    "path": "frontend/vue.config.js",
    "content": "module.exports = {\n  \"transpileDependencies\": [\n    \"vuetify\"\n  ],\n  lintOnSave: false\n\n}"
  }
]