master 66a863f1b7a2 cached
59 files
108.5 KB
29.2k tokens
31 symbols
1 requests
Download .txt
Repository: aws-samples/aws-serverless-shopping-cart
Branch: master
Commit: 66a863f1b7a2
Files: 59
Total size: 108.5 KB

Directory structure:
gitextract_sass801v/

├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── README.md
├── amplify/
│   └── .gitkeep
├── amplify-ci/
│   └── amplify-template.yaml
├── amplify.yml
├── backend/
│   ├── .gitignore
│   ├── Makefile
│   ├── auth.yaml
│   ├── layers/
│   │   ├── requirements.txt
│   │   └── shared.py
│   ├── product-mock-service/
│   │   ├── __init__.py
│   │   ├── get_product.py
│   │   ├── get_products.py
│   │   ├── product_list.json
│   │   └── requirements.txt
│   ├── product-mock.yaml
│   ├── shopping-cart-service/
│   │   ├── __init__.py
│   │   ├── add_to_cart.py
│   │   ├── checkout_cart.py
│   │   ├── db_stream_handler.py
│   │   ├── delete_from_cart.py
│   │   ├── get_cart_total.py
│   │   ├── list_cart.py
│   │   ├── migrate_cart.py
│   │   ├── requirements.txt
│   │   ├── tests/
│   │   │   ├── __init__.py
│   │   │   └── test_example.py
│   │   ├── update_cart.py
│   │   └── utils.py
│   └── shoppingcart-service.yaml
└── frontend/
    ├── .gitignore
    ├── Makefile
    ├── babel.config.js
    ├── package.json
    ├── public/
    │   └── index.html
    ├── scripts/
    │   └── fetchconfig.js
    ├── src/
    │   ├── App.vue
    │   ├── aws-exports.js
    │   ├── backend/
    │   │   └── api.js
    │   ├── components/
    │   │   ├── CartButton.vue
    │   │   ├── CartDrawer.vue
    │   │   ├── CartQuantityEditor.vue
    │   │   ├── LoadingOverlay.vue
    │   │   └── Product.vue
    │   ├── main.js
    │   ├── plugins/
    │   │   └── vuetify.js
    │   ├── router.js
    │   ├── store/
    │   │   ├── actions.js
    │   │   ├── getters.js
    │   │   ├── mutations.js
    │   │   └── store.js
    │   └── views/
    │       ├── Auth.vue
    │       ├── Home.vue
    │       └── Payment.vue
    └── vue.config.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
.env.local
.idea

================================================
FILE: CODE_OF_CONDUCT.md
================================================
## Code of Conduct
This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
opensource-codeofconduct@amazon.com with any additional questions or comments.


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing Guidelines

Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional
documentation, we greatly value feedback and contributions from our community.

Please read through this document before submitting any issues or pull requests to ensure we have all the necessary
information to effectively respond to your bug report or contribution.


## Reporting Bugs/Feature Requests

We welcome you to use the GitHub issue tracker to report bugs or suggest features.

When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already
reported the issue. Please try to include as much information as you can. Details like these are incredibly useful:

* A reproducible test case or series of steps
* The version of our code being used
* Any modifications you've made relevant to the bug
* Anything unusual about your environment or deployment


## Contributing via Pull Requests
Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that:

1. You are working against the latest source on the *master* branch.
2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already.
3. You open an issue to discuss any significant work - we would hate for your time to be wasted.

To send us a pull request, please:

1. Fork the repository.
2. 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.
3. Ensure local tests pass.
4. Commit to your fork using clear commit messages.
5. Send us a pull request, answering any default questions in the pull request interface.
6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation.

GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and
[creating a pull request](https://help.github.com/articles/creating-a-pull-request/).


## Finding contributions to work on
Looking 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.


## Code of Conduct
This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
opensource-codeofconduct@amazon.com with any additional questions or comments.


## Security issue notifications
If 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.


## Licensing

See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution.

We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes.


================================================
FILE: LICENSE
================================================
Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.



================================================
FILE: Makefile
================================================
all: backend frontend-build

TEMPLATES = auth product-mock shoppingcart-service

REGION := $(shell python3 -c 'import boto3; print(boto3.Session().region_name)')
ifndef S3_BUCKET
ACCOUNT_ID := $(shell aws sts get-caller-identity --query Account --output text)
S3_BUCKET = aws-serverless-shopping-cart-src-$(ACCOUNT_ID)-$(REGION)
endif


backend: create-bucket
	$(MAKE) -C backend TEMPLATE=auth S3_BUCKET=$(S3_BUCKET)
	$(MAKE) -C backend TEMPLATE=product-mock S3_BUCKET=$(S3_BUCKET)
	$(MAKE) -C backend TEMPLATE=shoppingcart-service S3_BUCKET=$(S3_BUCKET)

backend-delete:
	$(MAKE) -C backend delete TEMPLATE=auth
	$(MAKE) -C backend delete TEMPLATE=product-mock
	$(MAKE) -C backend delete TEMPLATE=shoppingcart-service

backend-tests:
	$(MAKE) -C backend tests

create-bucket:
	@echo "Checking if S3 bucket exists s3://$(S3_BUCKET)"
	@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))

amplify-deploy:
	aws cloudformation deploy \
		--template-file ./amplify-ci/amplify-template.yaml \
		--capabilities CAPABILITY_IAM \
		--parameter-overrides \
			OauthToken=$(GITHUB_OAUTH_TOKEN) \
			Repository=$(GITHUB_REPO) \
			BranchName=$(GITHUB_BRANCH) \
			SrcS3Bucket=$(S3_BUCKET) \
		--stack-name CartApp

frontend-serve: 
	$(MAKE) -C frontend serve

frontend-build: 
	$(MAKE) -C frontend build

.PHONY: all backend backend-delete backend-tests create-bucket amplify-deploy frontend-serve frontend-build


================================================
FILE: README.md
================================================
# Serverless Shopping Cart Microservice

This application is a sample application to demonstrate how you could implement a shopping cart microservice using 
serverless 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.

To assist in demonstrating the functionality, a bare bones mock "products" service has also been included. Since the 
authentication parts are likely to be shared between components, there is a separate template for it. The front-end 
doesn't make any real payment integration at this time.

## Architecture & Design

![Architecture Diagram](./images/architecture.png)

## Design Notes

Before building the application, I set some requirements on how the cart should behave:

- Users should be able to add items to the cart without logging in (an "anonymous cart"), and that cart should persist 
across browser restarts etc.
- When logging in, if there were products in an anonymous cart, they should be added to the user's cart from any 
previous logged in sessions.
- When logging out, the anonymous cart should not have products in it any longer.
- Items in an anonymous cart should be removed after a period of time, and items in a logged in cart should persist 
for a longer period.
- Admin users to be able to get an aggregated view of the total number of each product in users' carts at any time.

### Cart Migration

When an item is added to the cart, an item is written in DynamoDB with an identifier which matches a randomly generated 
(uuid) cookie which is set in the browser. This allows a user to add items to cart and come back to the page later 
without losing the items they have added. When the user logs in, these items will be removed, and replaced with items 
with a user id as the pk. If the user already had that product in their cart from a previous logged in session, the 
quantities would be summed. Because we don't need the deletion of old items to happen immediately as part of a 
synchronous workflow, we put messages onto an SQS queue, which triggers a worker function to delete the messages.  

To expire items from users' shopping carts, DynamoDB's native functionality is used where a TTL is written along with 
the item, after which the item should be removed. In this implementation, the TTL defaults to 1 day for anonymous 
carts, and 7 days for logged in carts.  

### Aggregated View of Products in Carts

It would be possible to scan our entire DynamoDB table and sum up the quantities of all the products, but this will be 
expensive past a certain scale. Instead, we can calculate the total as a running process, and keep track of the total 
amount.  

When an item is added, deleted or updated in DynamoDB, an event is put onto DynamoDB Streams, which in turn triggers a 
Lambda function. This function calculates the change in total quantity for each product in users' carts, and writes the 
quantity back to DynamoDB. The Lambda function is configured so that it will run after either 60 seconds pass, or 100 
new events are on the stream. This would enable an admin user to get real time data about the popular products, which 
could in turn help anticipate inventory. In this implementation, the API is exposed without authentication to 
demonstrate the functionality.  


## Api Design

### Shopping Cart Service

GET  
`/cart`  
Retrieves the shopping cart for a user who is either anonymous or logged in.  

POST  
`/cart`  
Accepts a product id and quantity as json. Adds specified quantity of an item to cart.  

`/cart/migrate`  
Called after logging in - migrates items in an anonymous user's cart to belong to their logged in user. If you already 
have a cart on your logged in user, your "anonymous cart" will be merged with it when you log in.

`/cart/checkout`  
Currently just empties cart.

PUT  
`/cart/{product-id}`  
Accepts a product id and quantity as json. Updates quantity of given item to provided quantity.  

GET  
`/cart/{product-id}/total`  
Returns the total amount of a given product across all carts. This API is not used by the frontend but can be manually 
called to test.  

### Product Mock Service

GET  
`/product`  
Returns details for all products.  

`/product/{product_id}`  
Returns details for a single product.  

## Running the Example

### Requirements

python >= 3.8.0
boto3
SAM CLI, >= version 0.50.0  
AWS CLI  
yarn  

### Setup steps

Fork the github repo, then clone your fork locally: 
`git clone https://github.com/<your-github-username>/aws-serverless-shopping-cart && cd aws-serverless-shopping-cart`

If you wish to use a named profile for your AWS credentials, you can set the environment variable `AWS_PROFILE` before 
running the below commands. For a profile named "development": `export AWS_PROFILE=development`.  

You now have 2 options - you can deploy the backend and run the frontend locally, or you can deploy the whole project 
using the AWS Amplify console.

## Option 1 - Deploy backend and run frontend locally
### Deploy the Backend

An S3 bucket will be automatically created for you which will be used for deploying source code to AWS. If you wish to 
use an existing bucket instead, you can manually set the `S3_BUCKET` environment variable to the name of your bucket.  

Build and deploy the resources:  
``` bash
make backend  # Creates S3 bucket if not existing already, then deploys CloudFormation stacks for authentication, a 
product mock service and the shopping cart service.  
```

### Run the Frontend Locally

Start the frontend locally:  
``` bash
make frontend-serve  # Retrieves backend config from ssm parameter store to a .env file, then starts service.  
```

Once the service is running, you can access the frontend on http://localhost:8080/ and start adding items to your cart. 
You can create an account by clicking on "Sign In" then "Create Account". Be sure to use a valid email address as 
you'll need to retrieve the verification code.

**Note:** CORS headers on the backend service default to allowing http://localhost:8080/. You will see CORS errors if 
you access the frontend using the ip (http://127.0.0.1:8080/), or using a port other than 8080.  

### Clean Up
Delete the CloudFormation stacks created by this project:
``` bash
make backend-delete
```

## Option 2 - Automatically deploy backend and frontend using Amplify Console


[![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)

1) Use **1-click deployment** button above, and continue by clicking "Connect to Github"
2) If you don't have an IAM Service Role with admin permissions, select "Create new role". Otherwise proceed to step 5) 
3) Select "Amplify" from the drop-down, and select "Amplify - Backend Deployment", then click "Next".
4) Click "Next" again, then give the role a name and click "Create role"
5) In the Amplify console and select the role you created, then click "Save and deploy"
6) Amplify Console will fork this repository into your GitHub account and deploy it for you
7) You should now be able to see your app being deployed in the [Amplify Console](https://console.aws.amazon.com/amplify/home)
8) Within your new app in Amplify Console, wait for deployment to complete (this should take approximately 12 minutes for the first deploy)


### Clean Up
Delete the CloudFormation stacks created by this project. There are 3 of them, with names starting with "aws-serverless-shopping-cart-".

## License

This library is licensed under the MIT-0 License. See the [LICENSE](LICENSE) file.  


================================================
FILE: amplify/.gitkeep
================================================



================================================
FILE: amplify-ci/amplify-template.yaml
================================================
AWSTemplateFormatVersion: 2010-09-09

Parameters:
  Repository:
    Type: String
    Description: GitHub Repository URL

  BranchName:
    Type: String
    Description: Github branch name
    Default: master

  OauthToken:
    Type: String
    Description: GitHub Personal Access Token
    NoEcho: true

  SrcS3Bucket:
    Type: String
    Description: S3 Bucket for source code storage  

Resources:
  AmplifyRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - amplify.amazonaws.com
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: Amplify
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              -
                Effect: "Allow"
                Action:
                    - "s3:PutObject"
                    - "s3:GetObject"
                    - "s3:CreateMultipartUpload"
                    - "s3:ListBucket"
                    - "s3:CreateBucket"
                Resource:
                    - !Sub "arn:aws:s3:::${SrcS3Bucket}"
                    - !Sub "arn:aws:s3:::${SrcS3Bucket}/*"
              -
                Effect: "Allow"
                Action:
                    - "cloudformation:Describe*"
                    - "cloudformation:Create*"
                    - "cloudformation:Execute*"
                Resource:
                    - "*"
              -
                Effect: Allow
                Action:
                  - lambda:*
                Resource: 
                  - !Sub "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:*"
                  - !Sub "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:layer:*"
              -
                Effect: Allow
                Action:
                  - lambda:GetLayerVersion
                Resource:
                  - !Sub "arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPython*"
              -
                Effect: Allow
                Action:
                  - lambda:*EventSourceMapping
                Resource: 
                  - "*"
              -
                Effect: "Allow"
                Action:
                    - "cloudformation:GetTemplateSummary"
                Resource: "*"
              -
                  Effect: "Allow"
                  Action:
                      - "iam:GetRole"
                      - "iam:Create*"
                      - "iam:Delete*"
                      - "iam:PassRole"
                      - "iam:*RolePolicy"
                  Resource:
                      - "*"
              -
                  Effect: "Allow"
                  Action:
                      - "apigateway:*"
                  Resource:
                      - "*"
              -
                  Effect: "Allow"
                  Action:
                      - "ssm:*"
                  Resource:
                      - "*"       
              -
                  Effect: "Allow"
                  Action:
                      - "dynamodb:List*"
                      - "dynamodb:Describe*"
                      - "dynamodb:TagResource"
                      - "dynamodb:UnTagResource"
                      - "dynamodb:Update*"
                      - "dynamodb:Create*"
                      - "dynamodb:DeleteTable"
                  Resource:
                      - "*"   
              -
                  Effect: "Allow"
                  Action:
                      - "sqs:*"
                  Resource:
                      - "*"
              -
                  Effect: "Allow"
                  Action:
                      - "cognito-idp:*"
                  Resource:
                      - "*"
              -
                  Effect: "Allow"
                  Action:
                      - "sts:GetCallerIdentity"
                  Resource:
                      - "*"

  AmplifyApp:
    Type: AWS::Amplify::App
    Properties:
      Name: CartApp
      Repository: !Ref Repository
      Description: AWS serverless shopping cart
      OauthToken: !Ref OauthToken
      CustomRules:
        -
          Source: </^[^.]+$|\.(?!(css|gif|ico|jpg|js|png|txt|svg|woff|ttf|map|json)$)([^.]+$)/>
          Target: /
          Status: 200
      EnvironmentVariables:
        - Name: S3_BUCKET
          Value: !Ref SrcS3Bucket
      Tags:
        - Key: Name
          Value: ShoppingCart
      IAMServiceRole: !GetAtt AmplifyRole.Arn

  AmplifyBranch:
    Type: AWS::Amplify::Branch
    Properties:
      BranchName: !Ref BranchName
      AppId: !GetAtt AmplifyApp.AppId
      Description: Master Branch
      EnableAutoBuild: true
      Tags:
        - Key: Name
          Value: shoppingcart-master
        - Key: Branch
          Value: master

Outputs:
  DefaultDomain:
    Value: !GetAtt AmplifyApp.DefaultDomain

  MasterBranchUrl:
    Value: !Join [ ".", [ !GetAtt AmplifyBranch.BranchName, !GetAtt AmplifyApp.DefaultDomain ]]

================================================
FILE: amplify.yml
================================================
version: 1
env:
  variables:
      ORIGIN: https://${AWS_BRANCH//\//-}.${AWS_APP_ID}.amplifyapp.com
      STACKNAME: amplify-aws-serverless-shopping-cart
backend:
  phases:
    preBuild:
      commands:
        - curl "https://github.com/aws/aws-sam-cli/releases/latest/download/aws-sam-cli-linux-x86_64.zip" -L -o "aws-sam-cli.zip"
        - unzip aws-sam-cli.zip -d sam-installation
        - ./sam-installation/install
        - pip3 install -U boto3
    build:
      commands:
        - make backend
frontend:
  phases:
    # IMPORTANT - Please verify your build commands
    build:
      commands:
        - make frontend-build
  artifacts:
    # IMPORTANT - Please verify your build output directory
    baseDirectory: /frontend/dist
    files:
      - '**/*'
  cache:
    paths: []


================================================
FILE: backend/.gitignore
================================================

# Created by https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode

### Linux ###
*~

# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*

# KDE directory preferences
.directory

# Linux trash folder which might appear on any partition or disk
.Trash-*

# .nfs files are created when an open file is removed but is still being accessed
.nfs*

### OSX ###
*.DS_Store
.AppleDouble
.LSOverride

# Icon must end with two \r
Icon

# Thumbnails
._*

# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent

# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

### PyCharm ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839

# User-specific stuff:
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/dictionaries

# Sensitive or high-churn files:
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.xml
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml

# Gradle:
.idea/**/gradle.xml
.idea/**/libraries

# CMake
cmake-build-debug/

# Mongo Explorer plugin:
.idea/**/mongoSettings.xml

## File-based project format:
*.iws

## Plugin-specific files:

# IntelliJ
/out/

# mpeltonen/sbt-idea plugin
.idea_modules/

# JIRA plugin
atlassian-ide-plugin.xml

# Cursive Clojure plugin
.idea/replstate.xml

# Ruby plugin and RubyMine
/.rakeTasks

# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties

### PyCharm Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721

# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr

# Sonarlint plugin
.idea/sonarlint

### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
.pytest_cache/
nosetests.xml
coverage.xml
*.cover
.hypothesis/

# Translations
*.mo
*.pot

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# pyenv
.python-version

# celery beat schedule file
celerybeat-schedule.*

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/

### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history

### Windows ###
# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
ehthumbs_vista.db

# Folder config file
Desktop.ini

# Recycle Bin used on file shares
$RECYCLE.BIN/

# Windows Installer files
*.cab
*.msi
*.msm
*.msp

# Windows shortcuts
*.lnk

# Build folder

*/build/*

# End of https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode


# pipenv
#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
#   However, in case of collaboration, if having platform-specific dependencies or dependencies
#   having no cross-platform support, pipenv may install dependencies that don't work, or not
#   install all needed dependencies.
#Pipfile.lock

# celery beat schedule file
celerybeat-schedule


# mypy
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

/packaged-*.yml


.idea
.idea/*

================================================
FILE: backend/Makefile
================================================
all: build deploy
ORIGIN ?= http://localhost:8080
STACKNAME ?= aws-serverless-shopping-cart

build:
	@echo "Building template $(TEMPLATE).yaml..."
	@sam build -t $(TEMPLATE).yaml

deploy:  # s3 bucket still needed for layer
	@echo "Deploying stack $(STACKNAME)-$(TEMPLATE)..."
	@sam deploy --capabilities CAPABILITY_NAMED_IAM --stack-name $(STACKNAME)-$(TEMPLATE) --s3-bucket $(S3_BUCKET) --parameter-overrides AllowedOrigin=$(ORIGIN) --no-fail-on-empty-changeset

tests:
	py.test -v

delete:
	@aws cloudformation delete-stack --stack-name $(STACKNAME)-$(TEMPLATE)
	@echo "Waiting for stack $(STACKNAME)-$(TEMPLATE) to be deleted..."
	@aws cloudformation wait stack-delete-complete --stack-name $(STACKNAME)-$(TEMPLATE)

.PHONY: build package deploy delete


================================================
FILE: backend/auth.yaml
================================================
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  auth-resources

  SAM Template for auth resources


Globals:
  Function:
    Timeout: 3

Resources:
  CognitoUserPool:
    Type: AWS::Cognito::UserPool
    Properties:
      UserPoolName: !Sub ${AWS::StackName}-UserPool
      AutoVerifiedAttributes: 
        - email
  UserPoolClient:
    Type: AWS::Cognito::UserPoolClient
    Properties:
      ClientName: my-app
      GenerateSecret: false
      UserPoolId: !Ref CognitoUserPool
      ExplicitAuthFlows:
        - ADMIN_NO_SRP_AUTH

  UserPoolSSM:
    Type: AWS::SSM::Parameter
    Properties:
      Type: String
      Name: /serverless-shopping-cart-demo/auth/user-pool-id
      Value: !Ref CognitoUserPool

  UserPoolARNSSM:
    Type: AWS::SSM::Parameter
    Properties:
      Type: String
      Name: /serverless-shopping-cart-demo/auth/user-pool-arn
      Value: !GetAtt CognitoUserPool.Arn

  UserPoolAppClientSSM:
    Type: AWS::SSM::Parameter
    Properties:
      Type: String
      Name: /serverless-shopping-cart-demo/auth/user-pool-client-id
      Value: !Ref UserPoolClient

Outputs:
  CognitoUserPoolId:
    Description: "Cognito User Pool ID"
    Value: !Ref CognitoUserPool

  CognitoAppClientId:
    Description: "Cognito App Client ID"
    Value: !Ref UserPoolClient

================================================
FILE: backend/layers/requirements.txt
================================================
requests==2.22.0
cognitojwt==1.1.0
boto3==1.10.34


================================================
FILE: backend/layers/shared.py
================================================
import calendar
import datetime
import os
import uuid
from decimal import Decimal
from http.cookies import SimpleCookie

from aws_lambda_powertools import Tracer

import cognitojwt

tracer = Tracer()

HEADERS = {
    "Access-Control-Allow-Origin": os.environ.get("ALLOWED_ORIGIN"),
    "Access-Control-Allow-Headers": "Content-Type",
    "Access-Control-Allow-Methods": "OPTIONS,POST,GET",
    "Access-Control-Allow-Credentials": True,
}


class NotFoundException(Exception):
    pass


@tracer.capture_method
def handle_decimal_type(obj):
    """
    json serializer which works with Decimal types returned from DynamoDB.
    """
    if isinstance(obj, Decimal):
        if float(obj).is_integer():
            return int(obj)
        else:
            return float(obj)
    raise TypeError


@tracer.capture_method
def generate_ttl(days=1):
    """
    Generate epoch timestamp for number days in future
    """
    future = datetime.datetime.utcnow() + datetime.timedelta(days=days)
    return calendar.timegm(future.utctimetuple())


@tracer.capture_method
def get_user_sub(jwt_token):
    """
    Validate JWT claims & retrieve user identifier
    """
    try:
        verified_claims = cognitojwt.decode(
            jwt_token, os.environ["AWS_REGION"], os.environ["USERPOOL_ID"]
        )
    except (cognitojwt.CognitoJWTException, ValueError):
        verified_claims = {}

    return verified_claims.get("sub")


@tracer.capture_method
def get_cart_id(event_headers):
    """
    Retrieve cart_id from cookies if it exists, otherwise set and return it
    """
    cookie = SimpleCookie()
    try:
        cookie.load(event_headers["cookie"])
        cart_cookie = cookie["cartId"].value
        generated = False
    except KeyError:
        cart_cookie = str(uuid.uuid4())
        generated = True

    return cart_cookie, generated


@tracer.capture_method
def get_headers(cart_id):
    """
    Get the headers to add to response data
    """
    headers = HEADERS
    cookie = SimpleCookie()
    cookie["cartId"] = cart_id
    cookie["cartId"]["max-age"] = (60 * 60) * 24  # 1 day
    cookie["cartId"]["secure"] = True
    cookie["cartId"]["httponly"] = True
    cookie["cartId"]["samesite"] = "None"
    cookie["cartId"]["path"] = "/"
    headers["Set-Cookie"] = cookie["cartId"].OutputString()
    return headers


================================================
FILE: backend/product-mock-service/__init__.py
================================================


================================================
FILE: backend/product-mock-service/get_product.py
================================================
import json
import os

from aws_lambda_powertools import Logger, Tracer

logger = Logger()
tracer = Tracer()

with open("product_list.json", "r") as product_list:
    product_list = json.load(product_list)

HEADERS = {
    "Access-Control-Allow-Origin": os.environ.get("ALLOWED_ORIGIN"),
    "Access-Control-Allow-Headers": "Content-Type",
    "Access-Control-Allow-Methods": "OPTIONS,POST,GET",
}


@logger.inject_lambda_context(log_event=True)
@tracer.capture_lambda_handler
def lambda_handler(event, context):
    """
    Return single product based on path parameter.
    """
    path_params = event["pathParameters"]
    product_id = path_params.get("product_id")
    logger.debug("Retriving product_id: %s", product_id)
    product = next(
        (item for item in product_list if item["productId"] == product_id), None
    )

    return {
        "statusCode": 200,
        "headers": HEADERS,
        "body": json.dumps({"product": product}),
    }


================================================
FILE: backend/product-mock-service/get_products.py
================================================
import json
import os

from aws_lambda_powertools import Logger, Tracer

logger = Logger()
tracer = Tracer()

with open('product_list.json', 'r') as product_list:
    product_list = json.load(product_list)

HEADERS = {
    "Access-Control-Allow-Origin": os.environ.get("ALLOWED_ORIGIN"),
    "Access-Control-Allow-Headers": "Content-Type",
    "Access-Control-Allow-Methods": "OPTIONS,POST,GET",
}


@logger.inject_lambda_context(log_event=True)
@tracer.capture_lambda_handler
def lambda_handler(event, context):
    """
    Return list of all products.
    """
    logger.debug("Fetching product list")

    return {
        "statusCode": 200,
        "headers": HEADERS,
        "body": json.dumps({"products": product_list}),
    }


================================================
FILE: backend/product-mock-service/product_list.json
================================================
[
    {
        "category": "fruit",
        "createdDate": "2017-04-17T01:14:03 -02:00",
        "description": "Culpa non veniam deserunt dolor irure elit cupidatat culpa consequat nulla irure aliqua.",
        "modifiedDate": "2019-03-13T12:18:27 -01:00",
        "name": "packaged strawberries",
        "package": {
            "height": 948,
            "length": 455,
            "weight": 54,
            "width": 905
        },
        "pictures": [
            "http://placehold.it/32x32"
        ],
        "price": 716,
        "productId": "4c1fadaa-213a-4ea8-aa32-58c217604e3c",
        "tags": [
            "mollit",
            "ad",
            "eiusmod",
            "irure",
            "tempor"
        ]
    },
    {
        "category": "sweets",
        "createdDate": "2017-04-06T06:21:36 -02:00",
        "description": "Dolore ipsum eiusmod dolore aliquip laborum laborum aute ipsum commodo id irure duis ipsum.",
        "modifiedDate": "2019-09-21T12:08:48 -02:00",
        "name": "candied prunes",
        "package": {
            "height": 329,
            "length": 179,
            "weight": 293,
            "width": 741
        },
        "pictures": [
            "http://placehold.it/32x32"
        ],
        "price": 35,
        "productId": "d2580eff-d105-45a5-9b21-ba61995bc6da",
        "tags": [
            "laboris",
            "dolor",
            "in",
            "labore",
            "duis"
        ]
    },
    {
        "category": "fruit",
        "createdDate": "2017-03-17T03:06:53 -01:00",
        "description": "Reprehenderit aliquip consequat quis excepteur et et esse exercitation adipisicing dolore nulla consequat.",
        "modifiedDate": "2019-11-25T12:32:49 -01:00",
        "name": "fresh prunes",
        "package": {
            "height": 736,
            "length": 567,
            "weight": 41,
            "width": 487
        },
        "pictures": [
            "http://placehold.it/32x32"
        ],
        "price": 2,
        "productId": "a6dd7187-40b6-4cb5-b73c-aecd655c6d9a",
        "tags": [
            "nisi",
            "quis",
            "sint",
            "adipisicing",
            "pariatur"
        ]
    },
    {
        "category": "vegetable",
        "createdDate": "2018-07-17T02:14:55 -02:00",
        "description": "Minim qui elit dolor est commodo excepteur ea voluptate eu dolor culpa magna.",
        "modifiedDate": "2019-09-05T03:36:34 -02:00",
        "name": "packaged tomatoes",
        "package": {
            "height": 4,
            "length": 756,
            "weight": 607,
            "width": 129
        },
        "pictures": [
            "http://placehold.it/32x32"
        ],
        "price": 97,
        "productId": "c0fbcc6b-7a70-41ac-aac4-f8fd237dc62e",
        "tags": [
            "dolor",
            "officia",
            "fugiat",
            "officia",
            "voluptate"
        ]
    },
    {
        "category": "vegetable",
        "createdDate": "2017-07-25T12:00:11 -02:00",
        "description": "Labore dolore velit mollit aute qui magna elit excepteur officia cupidatat ea ea aliqua.",
        "modifiedDate": "2019-10-04T06:32:14 -02:00",
        "name": "fresh tomatoes",
        "package": {
            "height": 881,
            "length": 252,
            "weight": 66,
            "width": 431
        },
        "pictures": [
            "http://placehold.it/32x32"
        ],
        "price": 144,
        "productId": "cb40c919-033a-47d6-8d00-1d73e2df20fe",
        "tags": [
            "nostrud",
            "elit",
            "Lorem",
            "occaecat",
            "duis"
        ]
    },
    {
        "category": "vegetable",
        "createdDate": "2017-01-07T05:28:03 -01:00",
        "description": "Ad eiusmod cupidatat duis dolor mollit labore mollit eu.",
        "modifiedDate": "2019-04-03T10:36:25 -02:00",
        "name": "fresh lettuce",
        "package": {
            "height": 813,
            "length": 932,
            "weight": 457,
            "width": 436
        },
        "pictures": [
            "http://placehold.it/32x32"
        ],
        "price": 51,
        "productId": "12929eb9-3eb7-4217-99e4-1a39c39217b6",
        "tags": [
            "ad",
            "ipsum",
            "est",
            "eiusmod",
            "duis"
        ]
    },
    {
        "category": "meat",
        "createdDate": "2018-12-03T12:33:44 -01:00",
        "description": "Amet cupidatat anim ipsum pariatur sit eu.",
        "modifiedDate": "2019-04-17T06:31:47 -02:00",
        "name": "packaged steak",
        "package": {
            "height": 707,
            "length": 417,
            "weight": 491,
            "width": 549
        },
        "pictures": [
            "http://placehold.it/32x32"
        ],
        "price": 894,
        "productId": "9fd9ef32-493f-4188-99f5-3aa809aa4fa9",
        "tags": [
            "fugiat",
            "velit",
            "non",
            "magna",
            "laboris"
        ]
    },
    {
        "category": "vegetable",
        "createdDate": "2017-04-27T06:48:08 -02:00",
        "description": "Labore est aliqua laborum ea laboris voluptate cillum aute duis occaecat.",
        "modifiedDate": "2019-11-01T10:23:57 -01:00",
        "name": "fresh lettuce",
        "package": {
            "height": 21,
            "length": 311,
            "weight": 817,
            "width": 964
        },
        "pictures": [
            "http://placehold.it/32x32"
        ],
        "price": 452,
        "productId": "20db6331-1084-48ff-8c4f-c1d98a6a1aa4",
        "tags": [
            "laborum",
            "in",
            "aliquip",
            "sint",
            "quis"
        ]
    },
    {
        "category": "sweet",
        "createdDate": "2017-11-24T04:01:33 -01:00",
        "description": "Fugiat sunt in eu eu occaecat.",
        "modifiedDate": "2019-05-19T05:53:56 -02:00",
        "name": "half-eaten cake",
        "package": {
            "height": 337,
            "length": 375,
            "weight": 336,
            "width": 1
        },
        "pictures": [
            "http://placehold.it/32x32"
        ],
        "price": 322,
        "productId": "8c843a54-27d7-477c-81b3-c21db12ed1c9",
        "tags": [
            "officia",
            "proident",
            "officia",
            "commodo",
            "nisi"
        ]
    },
    {
        "category": "dairy",
        "createdDate": "2018-05-29T11:46:28 -02:00",
        "description": "Aliqua officia magna do ipsum laboris anim magna nulla sit labore nulla qui duis.",
        "modifiedDate": "2019-05-29T05:33:49 -02:00",
        "name": "leftover cheese",
        "package": {
            "height": 267,
            "length": 977,
            "weight": 85,
            "width": 821
        },
        "pictures": [
            "http://placehold.it/32x32"
        ],
        "price": 163,
        "productId": "8d2024c0-6c05-4691-a0ff-dd52959bd1df",
        "tags": [
            "excepteur",
            "ipsum",
            "nulla",
            "nisi",
            "velit"
        ]
    },
    {
        "category": "bakery",
        "createdDate": "2018-09-22T05:22:38 -02:00",
        "description": "Ullamco commodo cupidatat reprehenderit eu sunt.",
        "modifiedDate": "2019-03-11T06:10:38 -01:00",
        "name": "fresh croissants",
        "package": {
            "height": 122,
            "length": 23,
            "weight": 146,
            "width": 694
        },
        "pictures": [
            "http://placehold.it/32x32"
        ],
        "price": 634,
        "productId": "867ecb2b-ef08-446e-8360-b63f60969e3d",
        "tags": [
            "labore",
            "dolor",
            "aliquip",
            "nulla",
            "aute"
        ]
    },
    {
        "category": "meat",
        "createdDate": "2018-09-12T07:24:46 -02:00",
        "description": "Eu ullamco irure qui labore qui duis mollit eiusmod adipisicing fugiat adipisicing nostrud ut non.",
        "modifiedDate": "2019-10-28T01:25:50 -01:00",
        "name": "packaged ham",
        "package": {
            "height": 902,
            "length": 278,
            "weight": 775,
            "width": 31
        },
        "pictures": [
            "http://placehold.it/32x32"
        ],
        "price": 77,
        "productId": "684011fc-ecfd-4557-a6df-9fc977365826",
        "tags": [
            "voluptate",
            "laborum",
            "exercitation",
            "anim",
            "anim"
        ]
    },
    {
        "category": "bakery",
        "createdDate": "2017-06-12T09:15:36 -02:00",
        "description": "Eu culpa nulla est et anim sint amet.",
        "modifiedDate": "2019-08-22T04:22:39 -02:00",
        "name": "fresh bread",
        "package": {
            "height": 551,
            "length": 976,
            "weight": 47,
            "width": 846
        },
        "pictures": [
            "http://placehold.it/32x32"
        ],
        "price": 805,
        "productId": "b027697d-a070-4c8f-8b9a-b8c80b2eb0ba",
        "tags": [
            "nostrud",
            "in",
            "duis",
            "laboris",
            "minim"
        ]
    },
    {
        "category": "sweet",
        "createdDate": "2018-09-06T06:03:43 -02:00",
        "description": "Mollit proident aliquip consectetur irure qui veniam laboris aliqua proident id fugiat esse nulla.",
        "modifiedDate": "2019-10-16T10:53:33 -02:00",
        "name": "candied strawberries",
        "package": {
            "height": 55,
            "length": 32,
            "weight": 661,
            "width": 694
        },
        "pictures": [
            "http://placehold.it/32x32"
        ],
        "price": 283,
        "productId": "7e0dbfa9-a672-4987-a26c-f601d177463a",
        "tags": [
            "minim",
            "irure",
            "in",
            "duis",
            "labore"
        ]
    },
    {
        "category": "bakery",
        "createdDate": "2017-07-23T12:27:34 -02:00",
        "description": "Ex non proident et eiusmod et elit est exercitation anim qui ullamco elit.",
        "modifiedDate": "2019-09-04T08:25:44 -02:00",
        "name": "fresh pie",
        "package": {
            "height": 718,
            "length": 59,
            "weight": 18,
            "width": 962
        },
        "pictures": [
            "http://placehold.it/32x32"
        ],
        "price": 646,
        "productId": "d1d527b8-9cef-4e97-a873-22236f3ee289",
        "tags": [
            "in",
            "ea",
            "excepteur",
            "id",
            "dolore"
        ]
    },
    {
        "category": "vegetable",
        "createdDate": "2018-11-08T04:08:28 -01:00",
        "description": "Pariatur deserunt nostrud cupidatat ut officia voluptate adipisicing mollit sunt cillum quis magna dolore aute.",
        "modifiedDate": "2019-10-11T10:28:49 -02:00",
        "name": "packaged lettuce",
        "package": {
            "height": 81,
            "length": 57,
            "weight": 653,
            "width": 367
        },
        "pictures": [
            "http://placehold.it/32x32"
        ],
        "price": 197,
        "productId": "11663d33-e54d-49da-ba6f-44d016ecde7e",
        "tags": [
            "incididunt",
            "in",
            "adipisicing",
            "eu",
            "tempor"
        ]
    },
    {
        "category": "meat",
        "createdDate": "2018-09-28T04:01:24 -02:00",
        "description": "Dolore nulla laboris incididunt laborum.",
        "modifiedDate": "2019-08-05T01:06:02 -02:00",
        "name": "leftover ham",
        "package": {
            "height": 246,
            "length": 639,
            "weight": 354,
            "width": 953
        },
        "pictures": [
            "http://placehold.it/32x32"
        ],
        "price": 728,
        "productId": "e173d669-b449-4226-af2e-128142abdd30",
        "tags": [
            "exercitation",
            "magna",
            "ex",
            "quis",
            "ad"
        ]
    },
    {
        "category": "dairy",
        "createdDate": "2018-08-23T06:31:47 -02:00",
        "description": "Pariatur mollit voluptate enim qui pariatur deserunt elit.",
        "modifiedDate": "2019-10-02T10:50:16 -02:00",
        "name": "fresh milk",
        "package": {
            "height": 576,
            "length": 948,
            "weight": 535,
            "width": 646
        },
        "pictures": [
            "http://placehold.it/32x32"
        ],
        "price": 164,
        "productId": "2a5b681c-ec7f-4bd4-a51e-57a5b6591f7f",
        "tags": [
            "labore",
            "id",
            "mollit",
            "occaecat",
            "elit"
        ]
    },
    {
        "category": "vegetable",
        "createdDate": "2018-02-21T01:55:54 -01:00",
        "description": "Consectetur laborum ipsum ad laboris.",
        "modifiedDate": "2019-02-23T08:50:01 -01:00",
        "name": "half-eaten lettuce",
        "package": {
            "height": 348,
            "length": 119,
            "weight": 723,
            "width": 44
        },
        "pictures": [
            "http://placehold.it/32x32"
        ],
        "price": 583,
        "productId": "de979b05-9d71-4c7e-b10f-636332ccb6c1",
        "tags": [
            "id",
            "velit",
            "cillum",
            "irure",
            "aute"
        ]
    },
    {
        "category": "meat",
        "createdDate": "2017-05-14T03:39:21 -02:00",
        "description": "Aliqua tempor irure qui consectetur exercitation culpa minim magna laboris ex pariatur elit culpa.",
        "modifiedDate": "2019-11-24T02:23:27 -01:00",
        "name": "fresh steak",
        "package": {
            "height": 328,
            "length": 7,
            "weight": 439,
            "width": 747
        },
        "pictures": [
            "http://placehold.it/32x32"
        ],
        "price": 996,
        "productId": "aa91060a-3601-4cb8-a2cc-025d09c7a9b7",
        "tags": [
            "qui",
            "dolore",
            "culpa",
            "est",
            "duis"
        ]
    }
]

================================================
FILE: backend/product-mock-service/requirements.txt
================================================
aws-lambda-powertools==1.0.0

================================================
FILE: backend/product-mock.yaml
================================================
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  product-service

  SAM Template for mock product-service
Parameters:
  AllowedOrigin:
    Type: 'String'

Globals:
  Function:
    Timeout: 5
    Tracing: Active
    AutoPublishAlias: live
    Runtime: python3.8
    MemorySize: 256
    Environment:
      Variables:
        LOG_LEVEL: "DEBUG"
        ALLOWED_ORIGIN: !Ref AllowedOrigin
        POWERTOOLS_SERVICE_NAME: product-mock
        POWERTOOLS_METRICS_NAMESPACE: ecommerce-app
  Api:
    EndpointConfiguration: REGIONAL
    TracingEnabled: true
    OpenApiVersion: '2.0'
    Cors:
      AllowMethods: "'OPTIONS,POST,GET'"
      AllowHeaders: "'Content-Type'"
      AllowOrigin: !Sub "'${AllowedOrigin}'"

Resources:
  GetProductFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: product-mock-service/
      Handler: get_product.lambda_handler
      Events:
        ListCart:
          Type: Api
          Properties:
            Path: /product/{product_id}
            Method: get

  GetProductsFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: product-mock-service/
      Handler: get_products.lambda_handler
      Events:
        ListCart:
          Type: Api
          Properties:
            Path: /product
            Method: get

  GetProductApiUrl:
    Type: AWS::SSM::Parameter
    Properties:
      Type: String
      Name: /serverless-shopping-cart-demo/products/products-api-url
      Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod"


Outputs:
  ProductApi:
    Description: "API Gateway endpoint URL for Prod stage for Product Mock Service"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod"


================================================
FILE: backend/shopping-cart-service/__init__.py
================================================


================================================
FILE: backend/shopping-cart-service/add_to_cart.py
================================================
import json
import os

import boto3
from aws_lambda_powertools import Logger, Metrics, Tracer

from shared import (
    NotFoundException,
    generate_ttl,
    get_cart_id,
    get_headers,
    get_user_sub,
)
from utils import get_product_from_external_service

logger = Logger()
tracer = Tracer()
metrics = Metrics()

dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(os.environ["TABLE_NAME"])
product_service_url = os.environ["PRODUCT_SERVICE_URL"]


@metrics.log_metrics(capture_cold_start_metric=True)
@logger.inject_lambda_context(log_event=True)
@tracer.capture_lambda_handler
def lambda_handler(event, context):
    """
    Add a the provided quantity of a product to a cart. Where an item already exists in the cart, the quantities will
    be summed.
    """

    try:
        request_payload = json.loads(event["body"])
    except KeyError:
        return {
            "statusCode": 400,
            "headers": get_headers(),
            "body": json.dumps({"message": "No Request payload"}),
        }
    product_id = request_payload["productId"]
    quantity = request_payload.get("quantity", 1)
    cart_id, _ = get_cart_id(event["headers"])

    # Because this method can be called anonymously, we need to check there's a logged in user
    user_sub = None
    jwt_token = event["headers"].get("Authorization")
    if jwt_token:
        user_sub = get_user_sub(jwt_token)

    try:
        product = get_product_from_external_service(product_id)
        logger.info("No product found with product_id: %s", product_id)
    except NotFoundException:
        return {
            "statusCode": 404,
            "headers": get_headers(cart_id=cart_id),
            "body": json.dumps({"message": "product not found"}),
        }

    if user_sub:
        logger.info("Authenticated user")
        pk = f"user#{user_sub}"
        ttl = generate_ttl(
            7
        )  # Set a longer ttl for logged in users - we want to keep their cart for longer.
    else:
        logger.info("Unauthenticated user")
        pk = f"cart#{cart_id}"
        ttl = generate_ttl()

    if int(quantity) < 0:
        table.update_item(
            Key={"pk": pk, "sk": f"product#{product_id}"},
            ExpressionAttributeNames={
                "#quantity": "quantity",
                "#expirationTime": "expirationTime",
                "#productDetail": "productDetail",
            },
            ExpressionAttributeValues={
                ":val": quantity,
                ":ttl": ttl,
                ":productDetail": product,
                ":limit": abs(quantity),
            },
            UpdateExpression="ADD #quantity :val SET #expirationTime = :ttl, #productDetail = :productDetail",
            # Prevent quantity less than 0
            ConditionExpression="quantity >= :limit",
        )
    else:
        table.update_item(
            Key={"pk": pk, "sk": f"product#{product_id}"},
            ExpressionAttributeNames={
                "#quantity": "quantity",
                "#expirationTime": "expirationTime",
                "#productDetail": "productDetail",
            },
            ExpressionAttributeValues={
                ":val": quantity,
                ":ttl": generate_ttl(),
                ":productDetail": product,
            },
            UpdateExpression="ADD #quantity :val SET #expirationTime = :ttl, #productDetail = :productDetail",
        )
    metrics.add_metric(name="CartUpdated", unit="Count", value=1)

    return {
        "statusCode": 200,
        "headers": get_headers(cart_id),
        "body": json.dumps(
            {"productId": product_id, "message": "product added to cart"}
        ),
    }


================================================
FILE: backend/shopping-cart-service/checkout_cart.py
================================================
import json
import os

import boto3
from aws_lambda_powertools import Logger, Metrics, Tracer
from boto3.dynamodb.conditions import Key

from shared import get_cart_id, get_headers, handle_decimal_type

logger = Logger()
tracer = Tracer()
metrics = Metrics()

dynamodb = boto3.resource("dynamodb")

logger.debug("Initializing DDB Table %s", os.environ["TABLE_NAME"])
table = dynamodb.Table(os.environ["TABLE_NAME"])


@metrics.log_metrics(capture_cold_start_metric=True)
@logger.inject_lambda_context(log_event=True)
@tracer.capture_lambda_handler
def lambda_handler(event, context):
    """
    Update cart table to use user identifier instead of anonymous cookie value as a key. This will be called when a user
    is logged in.
    """
    cart_id, _ = get_cart_id(event["headers"])

    try:
        # Because this method is authorized at API gateway layer, we don't need to validate the JWT claims here
        user_id = event["requestContext"]["authorizer"]["claims"]["sub"]
    except KeyError:

        return {
            "statusCode": 400,
            "headers": get_headers(cart_id),
            "body": json.dumps({"message": "Invalid user"}),
        }

    # Get all cart items belonging to the user's identity
    response = table.query(
        KeyConditionExpression=Key("pk").eq(f"user#{user_id}")
        & Key("sk").begins_with("product#"),
        ConsistentRead=True,  # Perform a strongly consistent read here to ensure we get correct and up to date cart
    )

    cart_items = response.get("Items")
    # batch_writer will be used to update status for cart entries belonging to the user
    with table.batch_writer() as batch:
        for item in cart_items:
            # Delete ordered items
            batch.delete_item(Key={"pk": item["pk"], "sk": item["sk"]})

    metrics.add_metric(name="CartCheckedOut", unit="Count", value=1)
    logger.info({"action": "CartCheckedOut", "cartItems": cart_items})

    return {
        "statusCode": 200,
        "headers": get_headers(cart_id),
        "body": json.dumps(
            {"products": response.get("Items")}, default=handle_decimal_type
        ),
    }


================================================
FILE: backend/shopping-cart-service/db_stream_handler.py
================================================
import os
from collections import Counter

import boto3
from aws_lambda_powertools import Logger, Tracer
from boto3.dynamodb import types

logger = Logger()
tracer = Tracer()

dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(os.environ["TABLE_NAME"])

deserializer = types.TypeDeserializer()


@tracer.capture_method
def dynamodb_to_python(dynamodb_item):
    """
    Convert from dynamodb low level format to python dict
    """
    return {k: deserializer.deserialize(v) for k, v in dynamodb_item.items()}


@logger.inject_lambda_context(log_event=True)
@tracer.capture_lambda_handler
def lambda_handler(event, context):
    """
    Handle streams from DynamoDB table
    """

    records = event["Records"]
    quantity_change_counter = Counter()

    for record in records:
        keys = dynamodb_to_python(record["dynamodb"]["Keys"])
        # NewImage record only exists if the event is INSERT or MODIFY
        if record["eventName"] in ("INSERT", "MODIFY"):
            new_image = dynamodb_to_python(record["dynamodb"]["NewImage"])
        else:
            new_image = {}

        old_image_ddb = record["dynamodb"].get("OldImage")

        if old_image_ddb:
            old_image = dynamodb_to_python(
                record["dynamodb"].get("OldImage")
            )  # Won't exist in case event is INSERT
        else:
            old_image = {}

        # We want to record the quantity change the change made to the db rather than absolute values
        if keys["sk"].startswith("product#"):
            quantity_change_counter.update(
                {
                    keys["sk"]: new_image.get("quantity", 0)
                    - old_image.get("quantity", 0)
                }
            )

    for k, v in quantity_change_counter.items():
        table.update_item(
            Key={"pk": k, "sk": "totalquantity"},
            ExpressionAttributeNames={"#quantity": "quantity"},
            ExpressionAttributeValues={":val": v},
            UpdateExpression="ADD #quantity :val",
        )

    return {
        "statusCode": 200,
    }


================================================
FILE: backend/shopping-cart-service/delete_from_cart.py
================================================
import json
import os

import boto3
from aws_lambda_powertools import Logger, Tracer

logger = Logger()
tracer = Tracer()

dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(os.environ["TABLE_NAME"])


@logger.inject_lambda_context(log_event=True)
@tracer.capture_lambda_handler
def lambda_handler(event, context):
    """
    Handle messages from SQS Queue containing cart items, and delete them from DynamoDB.
    """

    records = event["Records"]
    logger.info(f"Deleting {len(records)} records")
    with table.batch_writer() as batch:
        for item in records:
            item_body = json.loads(item["body"])
            batch.delete_item(Key={"pk": item_body["pk"], "sk": item_body["sk"]})

    return {
        "statusCode": 200,
    }


================================================
FILE: backend/shopping-cart-service/get_cart_total.py
================================================
import json
import os

import boto3
from aws_lambda_powertools import Logger, Tracer

from shared import handle_decimal_type

logger = Logger()
tracer = Tracer()

dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(os.environ["TABLE_NAME"])


@logger.inject_lambda_context(log_event=True)
@tracer.capture_lambda_handler
def lambda_handler(event, context):
    """
    List items in shopping cart.
    """
    product_id = event["pathParameters"]["product_id"]
    response = table.get_item(
        Key={"pk": f"product#{product_id}", "sk": "totalquantity"}
    )
    quantity = response["Item"]["quantity"]

    return {
        "statusCode": 200,
        "body": json.dumps(
            {"product": product_id, "quantity": quantity}, default=handle_decimal_type
        ),
    }


================================================
FILE: backend/shopping-cart-service/list_cart.py
================================================
import json
import os

import boto3
from aws_lambda_powertools import Logger, Tracer
from boto3.dynamodb.conditions import Key

from shared import get_cart_id, get_headers, get_user_sub, handle_decimal_type

logger = Logger()
tracer = Tracer()

dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(os.environ["TABLE_NAME"])


@logger.inject_lambda_context(log_event=True)
@tracer.capture_lambda_handler
def lambda_handler(event, context):
    """
    List items in shopping cart.
    """

    cart_id, generated = get_cart_id(event["headers"])

    # Because this method can be called anonymously, we need to check there's a logged in user
    jwt_token = event["headers"].get("Authorization")
    if jwt_token:
        user_sub = get_user_sub(jwt_token)
        key_string = f"user#{user_sub}"
        logger.structure_logs(append=True, cart_id=f"user#{user_sub}")
    else:
        key_string = f"cart#{cart_id}"
        logger.structure_logs(append=True, cart_id=f"cart#{cart_id}")

    # No need to query database if the cart_id was generated rather than passed into the function
    if generated:
        logger.info("cart ID was generated in this request, not fetching cart from DB")
        product_list = []
    else:
        logger.info("Fetching cart from DB")
        response = table.query(
            KeyConditionExpression=Key("pk").eq(key_string)
            & Key("sk").begins_with("product#"),
            ProjectionExpression="sk,quantity,productDetail",
            FilterExpression="quantity > :val",  # Only return items with more than 0 quantity
            ExpressionAttributeValues={":val": 0},
        )
        product_list = response.get("Items", [])

    for product in product_list:
        product.update(
            (k, v.replace("product#", "")) for k, v in product.items() if k == "sk"
        )

    return {
        "statusCode": 200,
        "headers": get_headers(cart_id),
        "body": json.dumps({"products": product_list}, default=handle_decimal_type),
    }


================================================
FILE: backend/shopping-cart-service/migrate_cart.py
================================================
import json
import os
import threading

import boto3
from aws_lambda_powertools import Logger, Metrics, Tracer
from boto3.dynamodb.conditions import Key

from shared import generate_ttl, get_cart_id, get_headers, handle_decimal_type

logger = Logger()
tracer = Tracer()
metrics = Metrics()

dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(os.environ["TABLE_NAME"])
sqs = boto3.resource("sqs")
queue = sqs.Queue(os.environ["DELETE_FROM_CART_SQS_QUEUE"])


@tracer.capture_method
def update_item(user_id, item):
    """
    Update an item in the database, adding the quantity of the passed in item to the quantity of any products already
    existing in the cart.
    """
    table.update_item(
        Key={"pk": f"user#{user_id}", "sk": item["sk"]},
        ExpressionAttributeNames={
            "#quantity": "quantity",
            "#expirationTime": "expirationTime",
            "#productDetail": "productDetail",
        },
        ExpressionAttributeValues={
            ":val": item["quantity"],
            ":ttl": generate_ttl(days=30),
            ":productDetail": item["productDetail"],
        },
        UpdateExpression="ADD #quantity :val SET #expirationTime = :ttl, #productDetail = :productDetail",
    )


@metrics.log_metrics(capture_cold_start_metric=True)
@logger.inject_lambda_context(log_event=True)
@tracer.capture_lambda_handler
def lambda_handler(event, context):
    """
    Update cart table to use user identifier instead of anonymous cookie value as a key. This will be called when a user
    is logged in.
    """

    cart_id, _ = get_cart_id(event["headers"])
    try:
        # Because this method is authorized at API gateway layer, we don't need to validate the JWT claims here
        user_id = event["requestContext"]["authorizer"]["claims"]["sub"]
        logger.info("Migrating cart_id %s to user_id %s", cart_id, user_id)
    except KeyError:

        return {
            "statusCode": 400,
            "headers": get_headers(cart_id),
            "body": json.dumps({"message": "Invalid user"}),
        }

    # Get all cart items belonging to the user's anonymous identity
    response = table.query(
        KeyConditionExpression=Key("pk").eq(f"cart#{cart_id}")
        & Key("sk").begins_with("product#")
    )
    unauth_cart = response["Items"]

    # Since there's no batch operation available for updating items, and there's no dependency between them, we can
    # run them in parallel threads.
    thread_list = []

    for item in unauth_cart:
        # Store items with user identifier as pk instead of "unauthenticated" cart ID
        # Using threading library to perform updates in parallel
        ddb_updateitem_thread = threading.Thread(
            target=update_item, args=(user_id, item)
        )
        thread_list.append(ddb_updateitem_thread)
        ddb_updateitem_thread.start()

        # Delete items with unauthenticated cart ID
        # Rather than deleting directly, push to SQS queue to handle asynchronously
        queue.send_message(MessageBody=json.dumps(item, default=handle_decimal_type))

    for ddb_thread in thread_list:
        ddb_thread.join()  # Block main thread until all updates finished

    if unauth_cart:
        metrics.add_metric(name="CartMigrated", unit="Count", value=1)

    response = table.query(
        KeyConditionExpression=Key("pk").eq(f"user#{user_id}")
        & Key("sk").begins_with("product#"),
        ProjectionExpression="sk,quantity,productDetail",
        ConsistentRead=True,  # Perform a strongly consistent read here to ensure we get correct values after updates
    )

    product_list = response.get("Items", [])
    for product in product_list:
        product.update(
            (k, v.replace("product#", "")) for k, v in product.items() if k == "sk"
        )

    return {
        "statusCode": 200,
        "headers": get_headers(cart_id),
        "body": json.dumps({"products": product_list}, default=handle_decimal_type),
    }


================================================
FILE: backend/shopping-cart-service/requirements.txt
================================================


================================================
FILE: backend/shopping-cart-service/tests/__init__.py
================================================


================================================
FILE: backend/shopping-cart-service/tests/test_example.py
================================================
import sys
import unittest

sys.path.append("..")  # Add application to path
sys.path.append("./layers/")  # Add layer to path

import shared  # noqa: E402  # import from layer


class Tests(unittest.TestCase):
    """
    Example included to demonstrate how to run unit tests when using lambda layers.
    """

    def setUp(self):
        pass

    def test_headers(self):
        self.assertEqual(shared.HEADERS.get("Access-Control-Allow-Credentials"), True)


if __name__ == "__main__":
    unittest.main()


================================================
FILE: backend/shopping-cart-service/update_cart.py
================================================
import json
import os

import boto3
from aws_lambda_powertools import Logger, Metrics, Tracer

from shared import (
    NotFoundException,
    generate_ttl,
    get_cart_id,
    get_headers,
    get_user_sub,
)
from utils import get_product_from_external_service

logger = Logger()
tracer = Tracer()
metrics = Metrics()

dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(os.environ["TABLE_NAME"])
product_service_url = os.environ["PRODUCT_SERVICE_URL"]


@metrics.log_metrics(capture_cold_start_metric=True)
@logger.inject_lambda_context(log_event=True)
@tracer.capture_lambda_handler
def lambda_handler(event, context):
    """
    Idempotent update quantity of products in a cart. Quantity provided will overwrite existing quantity for a
    specific product in cart, rather than adding to it.
    """

    try:
        request_payload = json.loads(event["body"])
    except KeyError:
        return {
            "statusCode": 400,
            "headers": get_headers(),
            "body": json.dumps({"message": "No Request payload"}),
        }

    # retrieve the product_id that was specified in the url
    product_id = event["pathParameters"]["product_id"]

    quantity = int(request_payload["quantity"])
    cart_id, _ = get_cart_id(event["headers"])

    # Because this method can be called anonymously, we need to check if there's a logged in user
    user_sub = None
    jwt_token = event["headers"].get("Authorization")
    if jwt_token:
        user_sub = get_user_sub(jwt_token)

    try:
        product = get_product_from_external_service(product_id)
    except NotFoundException:
        logger.info("No product found with product_id: %s", product_id)
        return {
            "statusCode": 404,
            "headers": get_headers(cart_id=cart_id),
            "body": json.dumps({"message": "product not found"}),
        }

    # Prevent storing negative quantities of things
    if quantity < 0:
        return {
            "statusCode": 400,
            "headers": get_headers(cart_id),
            "body": json.dumps(
                {
                    "productId": product_id,
                    "message": "Quantity must not be lower than 0",
                }
            ),
        }

    # Use logged in user's identifier if it exists, otherwise use the anonymous identifier

    if user_sub:
        pk = f"user#{user_sub}"
        ttl = generate_ttl(
            7
        )  # Set a longer ttl for logged in users - we want to keep their cart for longer.
    else:
        pk = f"cart#{cart_id}"
        ttl = generate_ttl()

    table.put_item(
        Item={
            "pk": pk,
            "sk": f"product#{product_id}",
            "quantity": quantity,
            "expirationTime": ttl,
            "productDetail": product,
        }
    )
    logger.info("about to add metrics...")
    metrics.add_metric(name="CartUpdated", unit="Count", value=1)

    return {
        "statusCode": 200,
        "headers": get_headers(cart_id),
        "body": json.dumps(
            {"productId": product_id, "quantity": quantity, "message": "cart updated"}
        ),
    }


================================================
FILE: backend/shopping-cart-service/utils.py
================================================
import os

import requests
from aws_lambda_powertools import Logger, Tracer

from shared import NotFoundException

product_service_url = os.environ["PRODUCT_SERVICE_URL"]

logger = Logger()
tracer = Tracer()


@tracer.capture_method
def get_product_from_external_service(product_id):
    """
    Call product API to retrieve product details
    """
    response = requests.get(product_service_url + f"/product/{product_id}")
    try:
        response_dict = response.json()["product"]
    except KeyError:
        logger.warn("No product found with id %s", product_id)
        raise NotFoundException

    return response_dict


================================================
FILE: backend/shoppingcart-service.yaml
================================================
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  shoppingcart-service

  SAM Template for shoppingcart-service

Parameters:
  UserPoolArn:
    Type: 'AWS::SSM::Parameter::Value<String>'
    Default: '/serverless-shopping-cart-demo/auth/user-pool-arn'
  UserPoolId:
    Type: 'AWS::SSM::Parameter::Value<String>'
    Default: '/serverless-shopping-cart-demo/auth/user-pool-id'
  ProductServiceUrl:
    Type: 'AWS::SSM::Parameter::Value<String>'
    Default: '/serverless-shopping-cart-demo/products/products-api-url'
  AllowedOrigin:
    Type: 'String'

Globals:
  Function:
    Timeout: 5
    MemorySize: 512
    Tracing: Active
    AutoPublishAlias: live
    Runtime: python3.8
    Layers:
      - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPython:3
    Environment:
      Variables:
        TABLE_NAME: !Ref DynamoDBShoppingCartTable
        LOG_LEVEL: "INFO"
        ALLOWED_ORIGIN: !Ref AllowedOrigin
        POWERTOOLS_SERVICE_NAME: shopping-cart
        POWERTOOLS_METRICS_NAMESPACE: ecommerce-app
  Api:
    EndpointConfiguration: REGIONAL
    TracingEnabled: true
    OpenApiVersion: '2.0'
    Cors:
      AllowMethods: "'OPTIONS,POST,GET,PUT'"
      AllowHeaders: "'Content-Type,Authorization'"
      AllowCredentials: true
      AllowOrigin: !Sub "'${AllowedOrigin}'"

Resources:
  UtilsLayer:
    Type: AWS::Serverless::LayerVersion
    Properties:
      ContentUri: ./layers/
      CompatibleRuntimes:
        - python3.8
    Metadata:
      BuildMethod: python3.8

  CartApi:
    Type: AWS::Serverless::Api
    DependsOn:
      - ApiGWAccount
    Properties:
      StageName: Prod
      MethodSettings:
        - DataTraceEnabled: True
          MetricsEnabled: True
          ResourcePath: "/*"
          HttpMethod: "*"
          LoggingLevel: INFO
      Auth:
        Authorizers:
          CognitoAuthorizer:
            UserPoolArn: !Ref UserPoolArn
            Identity: # OPTIONAL
              Header: Authorization # OPTIONAL; Default: 'Authorization'

  ListCartRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Action:
            - "sts:AssumeRole"
            Effect: "Allow"
            Principal:
              Service:
                - "lambda.amazonaws.com"

  AddToCartRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Action:
            - "sts:AssumeRole"
            Effect: "Allow"
            Principal:
              Service:
                - "lambda.amazonaws.com"

  LambdaLoggingPolicy:
    Type: "AWS::IAM::Policy"
    Properties:
      PolicyName: LambdaXRayPolicy
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          -
            Effect: "Allow"
            Action: [
              "xray:PutTraceSegments",
              "xray:PutTelemetryRecords",
              "logs:CreateLogGroup",
              "logs:CreateLogStream",
              "logs:PutLogEvents"
              ]
            Resource: "*"
      Roles:
        - !Ref ListCartRole
        - !Ref AddToCartRole

  DynamoDBReadPolicy:
    Type: "AWS::IAM::Policy"
    Properties:
      PolicyName: DynamoDBReadPolicy
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          -
            Effect: "Allow"
            Action: [
              "dynamodb:GetItem",
              "dynamodb:Scan",
              "dynamodb:Query",
              "dynamodb:BatchGetItem",
              "dynamodb:DescribeTable"
              ]
            Resource:
              - !GetAtt DynamoDBShoppingCartTable.Arn
      Roles:
        - !Ref ListCartRole
        - !Ref AddToCartRole

  DynamoDBWritePolicy:
    Type: "AWS::IAM::Policy"
    Properties:
      PolicyName: DynamoDBWritePolicy
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          -
            Effect: "Allow"
            Action: [
              "dynamodb:PutItem",
              "dynamodb:UpdateItem",
              "dynamodb:ConditionCheckItem",
              "dynamodb:DeleteItem",
              "dynamodb:BatchWriteItem"
            ]
            Resource: !GetAtt DynamoDBShoppingCartTable.Arn
      Roles:
        - !Ref AddToCartRole

  SQSSendMessagePolicy:
    Type: "AWS::IAM::Policy"
    Properties:
      PolicyName: SQSSendMessagePolicy
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          -
            Effect: "Allow"
            Action: [
              "sqs:SendMessage*"
            ]
            Resource: !GetAtt CartDeleteSQSQueue.Arn
      Roles:
        - !Ref AddToCartRole

  ListCartFunction:
    Type: AWS::Serverless::Function
    DependsOn:
      - LambdaLoggingPolicy
    Properties:
      CodeUri: shopping-cart-service/
      Handler: list_cart.lambda_handler
      Role: !GetAtt ListCartRole.Arn
      Layers:
        - !Ref UtilsLayer
      Environment:
        Variables:
          USERPOOL_ID: !Ref UserPoolId
      Events:
        ListCart:
          Type: Api
          Properties:
            RestApiId: !Ref CartApi
            Path: /cart
            Method: get

  AddToCartFunction:
    Type: AWS::Serverless::Function
    DependsOn:
      - LambdaLoggingPolicy
    Properties:
      CodeUri: shopping-cart-service/
      Handler: add_to_cart.lambda_handler
      Role: !GetAtt AddToCartRole.Arn
      Layers:
        - !Ref UtilsLayer
      Environment:
        Variables:
          PRODUCT_SERVICE_URL: !Ref ProductServiceUrl
          USERPOOL_ID: !Ref UserPoolId
      Events:
        AddToCart:
          Type: Api
          Properties:
            RestApiId: !Ref CartApi
            Path: /cart
            Method: post

  UpdateCartFunction:
    Type: AWS::Serverless::Function
    DependsOn:
      - LambdaLoggingPolicy
    Properties:
      CodeUri: shopping-cart-service/
      Handler: update_cart.lambda_handler
      Role: !GetAtt AddToCartRole.Arn
      Layers:
        - !Ref UtilsLayer
      Environment:
        Variables:
          PRODUCT_SERVICE_URL: !Ref ProductServiceUrl
          USERPOOL_ID: !Ref UserPoolId
      Events:
        AddToCart:
          Type: Api
          Properties:
            RestApiId: !Ref CartApi
            Path: /cart/{product_id}
            Method: put

  MigrateCartFunction:
    Type: AWS::Serverless::Function
    DependsOn:
      - LambdaLoggingPolicy
    Properties:
      CodeUri: shopping-cart-service/
      Handler: migrate_cart.lambda_handler
      Timeout: 30
      Layers:
        - !Ref UtilsLayer
      Environment:
        Variables:
          PRODUCT_SERVICE_URL: !Ref ProductServiceUrl
          USERPOOL_ID: !Ref UserPoolId
          DELETE_FROM_CART_SQS_QUEUE: !Ref CartDeleteSQSQueue
      Role: !GetAtt AddToCartRole.Arn
      Events:
        AddToCart:
          Type: Api
          Properties:
            RestApiId: !Ref CartApi
            Path: /cart/migrate
            Method: post
            Auth:
              Authorizer: CognitoAuthorizer

  CheckoutCartFunction:
    Type: AWS::Serverless::Function
    DependsOn:
      - LambdaLoggingPolicy
    Properties:
      CodeUri: shopping-cart-service/
      Handler: checkout_cart.lambda_handler
      Timeout: 10
      Layers:
        - !Ref UtilsLayer
      Environment:
        Variables:
          PRODUCT_SERVICE_URL: !Ref ProductServiceUrl
          USERPOOL_ID: !Ref UserPoolId
      Role: !GetAtt AddToCartRole.Arn
      Events:
        AddToCart:
          Type: Api
          Properties:
            RestApiId: !Ref CartApi
            Path: /cart/checkout
            Method: post
            Auth:
              Authorizer: CognitoAuthorizer

  GetCartTotalFunction:
    Type: AWS::Serverless::Function
    DependsOn:
      - LambdaLoggingPolicy
    Properties:
      CodeUri: shopping-cart-service/
      Handler: get_cart_total.lambda_handler
      Timeout: 10
      Layers:
        - !Ref UtilsLayer
      Role: !GetAtt ListCartRole.Arn
      Events:
        GetCartTotal:
          Type: Api
          Properties:
            RestApiId: !Ref CartApi
            Path: /cart/{product_id}/total
            Method: get

  DeleteFromCartFunction:
    Type: AWS::Serverless::Function
    DependsOn:
      - LambdaLoggingPolicy
    Properties:
      CodeUri: shopping-cart-service/
      Handler: delete_from_cart.lambda_handler
      ReservedConcurrentExecutions: 25  # Keep the ddb spikes down in case of many deletes at once
      Policies:
        - SQSPollerPolicy:
            QueueName:
              !GetAtt CartDeleteSQSQueue.QueueName
        - Statement:
            - Effect: Allow
              Action:
                - "dynamodb:DeleteItem"
                - "dynamodb:BatchWriteItem"
              Resource:
                - !GetAtt DynamoDBShoppingCartTable.Arn
      Layers:
        - !Ref UtilsLayer
      Environment:
        Variables:
          USERPOOL_ID: !Ref UserPoolId
      Events:
        RetrieveFromSQS:
          Type: SQS
          Properties:
            Queue: !GetAtt CartDeleteSQSQueue.Arn
            BatchSize: 5

  CartDBStreamHandler:
    Type: AWS::Serverless::Function
    DependsOn:
      - LambdaLoggingPolicy
    Properties:
      CodeUri: shopping-cart-service/
      Handler: db_stream_handler.lambda_handler
      Layers:
        - !Ref UtilsLayer
      Policies:
        - AWSLambdaDynamoDBExecutionRole
        - Statement:
            - Effect: Allow
              Action:
                - "dynamodb:UpdateItem"
              Resource:
                - !GetAtt DynamoDBShoppingCartTable.Arn
      Events:
        Stream:
          Type: DynamoDB
          Properties:
            Stream: !GetAtt DynamoDBShoppingCartTable.StreamArn
            BatchSize: 100
            MaximumBatchingWindowInSeconds: 60
            StartingPosition: LATEST

  DynamoDBShoppingCartTable:
    Type: AWS::DynamoDB::Table
    Properties:
      AttributeDefinitions:
        - AttributeName: pk
          AttributeType: S
        - AttributeName: sk
          AttributeType: S
      KeySchema:
        - AttributeName: pk
          KeyType: HASH
        - AttributeName: sk
          KeyType: RANGE
      BillingMode: PAY_PER_REQUEST
      StreamSpecification:
        StreamViewType: 'NEW_AND_OLD_IMAGES'
      TimeToLiveSpecification:
        AttributeName: expirationTime
        Enabled: True

  APIGWCloudWatchRole:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - apigateway.amazonaws.com
            Action: 'sts:AssumeRole'
      Path: /
      ManagedPolicyArns:
        - >-
          arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs
  ApiGWAccount:
    Type: 'AWS::ApiGateway::Account'
    Properties:
      CloudWatchRoleArn: !GetAtt APIGWCloudWatchRole.Arn

  CartDeleteSQSQueue:
    Type: AWS::SQS::Queue
    Properties:
      VisibilityTimeout: 20
      RedrivePolicy:
        deadLetterTargetArn:
          !GetAtt CartDeleteSQSDLQ.Arn
        maxReceiveCount: 5
  CartDeleteSQSDLQ:
    Type: AWS::SQS::Queue

  CartApiUrl:
    Type: AWS::SSM::Parameter
    Properties:
      Type: String
      Name: /serverless-shopping-cart-demo/shopping-cart/cart-api-url
      Value: !Sub "https://${CartApi}.execute-api.${AWS::Region}.amazonaws.com/Prod"

Outputs:
  CartApi:
    Description: "API Gateway endpoint URL for Prod stage for Cart Service"
    Value: !Sub "https://${CartApi}.execute-api.${AWS::Region}.amazonaws.com/Prod"


================================================
FILE: frontend/.gitignore
================================================
# Created by .ignore support plugin (hsz.mobi)
### Vue template
# gitignore template for Vue.js projects
#
# Recommended template: Node.gitignore

# TODO: where does this rule come from?
docs/_book

# TODO: where does this rule come from?
test/

### Node template
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env.test

# parcel-bundler cache (https://parceljs.org/)
.cache

# next.js build output
.next

# nuxt.js build output
.nuxt

# vuepress build output
.vuepress/dist

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

dist/*

.vscode

/amplify/*
/amplify

*.local
.env

================================================
FILE: frontend/Makefile
================================================
all: build

serve:
	yarn install
	yarn fetchConfig local
	yarn serve

build:
	yarn install
	yarn fetchConfig
	yarn build

.PHONY: build serve


================================================
FILE: frontend/babel.config.js
================================================
module.exports = {
  presets: [
    '@vue/cli-plugin-babel/preset'
  ]
}


================================================
FILE: frontend/package.json
================================================
{
  "name": "shoppingcart-service-frontend",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "fetchConfig": "node scripts/fetchconfig.js"
  },
  "dependencies": {
    "aws-amplify": "^1.2.3",
    "aws-amplify-vue": "^0.3.3",
    "core-js": "^3.3.2",
    "decimal.js": "^10.2.0",
    "dotenv": "^8.2.0",
    "v-mask": "^2.0.2",
    "vue": "^2.6.10",
    "vue-router": "^3.1.3",
    "vuelidate": "^0.7.4",
    "vuetify": "^2.1.7",
    "vuex": "^3.1.1"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "^4.0.0",
    "@vue/cli-plugin-eslint": "^4.0.0",
    "@vue/cli-service": "^4.0.0",
    "babel-eslint": "^10.0.3",
    "eslint": "^5.16.0",
    "eslint-plugin-vue": "^5.0.0",
    "sass": "^1.19.0",
    "sass-loader": "^8.0.0",
    "vue-cli-plugin-vuetify": "^2.0.2",
    "vue-template-compiler": "^2.6.10",
    "vuetify-loader": "^1.3.0"
  },
  "resolutions": {
    "websocket-extensions": "^0.1.4",
    "axios": "^0.21.2",
    "ssri": "^8.0.1",
    "is-svg": "^4.2.2",
    "glob-parent": "^5.1.2",
    "set-value": "^4.0.1",
    "ansi-regex": "^5.0.1",
    "nth-check": "^2.0.1",
    "aws-sdk": "^2.814.0",
    "node-forge": "^1.0.0"
  },
  "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": [
      "plugin:vue/essential",
      "eslint:recommended"
    ],
    "rules": {},
    "parserOptions": {
      "parser": "babel-eslint"
    }
  },
  "postcss": {
    "plugins": {
      "autoprefixer": {}
    }
  },
  "browserslist": [
    "> 1%",
    "last 2 versions"
  ]
}


================================================
FILE: frontend/public/index.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title>Serverless Shopping Cart Demo</title>
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css">
    <link href="https://fonts.googleapis.com/css?family=Material+Icons" rel="stylesheet">

  </head>
  <body>
    <noscript>
      <strong>We're sorry but Serverless Shopping Cart Demo doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>


================================================
FILE: frontend/scripts/fetchconfig.js
================================================
process.env.AWS_SDK_LOAD_CONFIG = true;
var fs = require('fs');

var args = process.argv.slice(2);
var envtype = args[0] ? args[0] : ''
var AWS = require('aws-sdk');
var ssm = new AWS.SSM();


const query = {
    "Path": "/serverless-shopping-cart-demo/",
    "WithDecryption": false,
    "Recursive": true
}

const requiredParams = ["CART_API_URL", "PRODUCTS_API_URL", "USER_POOL_ID",
    "USER_POOL_CLIENT_ID"
]

var params = ssm.getParametersByPath(query).promise()

var output = []

function formatParams(data) {
    for (var param of data) {
        const paramName = param.Name.toUpperCase().split("/").pop().replace(/-/g, "_")
        if (requiredParams.includes(paramName)) {
            output.push("VUE_APP_" + paramName + '=' + param.Value)
        }
    }
}

params
    .then(data => {
        formatParams(data.Parameters)
        output.push("VUE_APP_AWS_REGION=" + AWS.config.region)
        var fileName
        if (envtype) {
            fileName = "./.env." + envtype
        }
        else {
            fileName = "./.env"
        }
        fs.writeFile(fileName, output.join('\n'), function (err) {
            if (err) {
                return console.log(err);  // eslint-disable-line no-console
            }
            console.log(`env file ${fileName} populated with config`);  // eslint-disable-line no-console
        });

    })
    .catch(error => {
        console.log('error: ' + error)  // eslint-disable-line no-console
    })


================================================
FILE: frontend/src/App.vue
================================================
<template>
  <v-app>
    <v-app-bar elevate-on-scroll app class="primary">
      <v-toolbar-title>
        <router-link tag="div" to="/">
          <a class="accent--text header font-weight-black">
            DEMO
            <span class="font-weight-thin subheading secondary--text">Store</span>
          </a>
        </router-link>
      </v-toolbar-title>
      <v-toolbar-items>
        <v-btn to="/auth" v-if="!currentUser" text class="ml-2">Sign In</v-btn>
        <cart-button @drawerChange="toggleDrawer" />
        <div class="sign-out">
          <amplify-sign-out v-if="currentUser" class="Form--signout pl-2"></amplify-sign-out>
        </div>
      </v-toolbar-items>
    </v-app-bar>
    <v-content>
      <v-container fluid>
        <loading-overlay />
        <v-fade-transition mode="out-in">
          <router-view></router-view>
        </v-fade-transition>
      </v-container>
      <v-navigation-drawer
        style="position:fixed; overflow-y:scroll;"
        right
        v-model="drawer"
        temporary
        align-space-around
        column
        d-flex
      >
        <cart-drawer />
      </v-navigation-drawer>
    </v-content>
  </v-app>
</template>

<script>
import { mapGetters, mapState } from "vuex";

export default {
  name: "app",
  data() {
    return {
      drawer: null
    };
  },
  mounted() {
    this.$store.dispatch("fetchCart");
  },
  computed: {
    ...mapGetters(["cartSize", "currentUser"]),
    ...mapState(["cartLoading"])
  },
  methods: {
    logout() {
      this.$store.dispatch("logout");
    },
    toggleDrawer() {
      this.drawer = !this.drawer;
    }
  }
};
</script>

<style>
.header {
  font-weight: bold !important;
  font-size: 30px !important;
  text-decoration: none;
}

:root {
  /* Colors */
  --amazonOrange: #e88b01 !important;
}
</style>

================================================
FILE: frontend/src/aws-exports.js
================================================
const awsmobile = {
  Auth: {
    region: process.env.VUE_APP_AWS_REGION,
    userPoolId: process.env.VUE_APP_USER_POOL_ID,
    userPoolWebClientId: process.env.VUE_APP_USER_POOL_CLIENT_ID
  },
  API: {
    endpoints: [{
        name: "CartAPI",
        endpoint: process.env.VUE_APP_CART_API_URL
      },
      {
        name: "ProductAPI",
        endpoint: process.env.VUE_APP_PRODUCTS_API_URL,
      }
    ]
  }
};

export default awsmobile;

================================================
FILE: frontend/src/backend/api.js
================================================
import {
    Auth,
    API
} from 'aws-amplify'

async function getHeaders(includeAuth) {
    const headers = {
        "Content-Type": "application/json"
    }

    if (!includeAuth) {
        return {
            "Content-Type": "application/json"
        }
    }
    let session = null
    try {
        session = await Auth.currentSession()
    } catch (e) {
        e == e
    }
    if (session) {
        let authheader = session.getIdToken().jwtToken
        headers['Authorization'] = authheader
    }
    return headers
}

export async function getCart() {
    return getHeaders(true).then(
        headers => API.get("CartAPI", "/cart", {
            headers: headers,
            withCredentials: true
        }))
}

export async function postCart(obj, quantity = 1) {
    return getHeaders(true).then(
        headers => API.post("CartAPI", "/cart", {
            body: {
                productId: obj.productId,
                quantity: quantity,
            },
            headers: headers,
            withCredentials: true
        })
    )
}

export async function putCart(obj, quantity) {
    return getHeaders(true).then(
        headers => API.put("CartAPI", "/cart/" + obj.productId, {
            body: {
                productId: obj.productId,
                quantity: quantity,
            },
            headers: headers,
            withCredentials: true
        })
    )
}

export async function getProducts() {
    return getHeaders().then(
        headers => API.get("ProductAPI", "/product", {
            headers: headers
        })
    )
}

export async function cartMigrate() {
    return getHeaders(true).then(
        headers => API.post("CartAPI", "/cart/migrate", {
            headers: headers,
            withCredentials: true
        })
    )
}

export async function cartCheckout() {
    return getHeaders(true).then(
        headers => API.post("CartAPI", "/cart/checkout", {
            headers: headers,
            withCredentials: true
        })
    )
}

================================================
FILE: frontend/src/components/CartButton.vue
================================================
<template>
  <v-btn fixed right rounded small @click.stop="toggleDrawer" text icon x-large>
    <v-badge overlap color="accent" v-bind:class="{ 'animated-pulse': cartLoading > 0 }">
      <div slot="badge">{{cartSize}}</div>
      <v-icon color="black" x-large>mdi-cart</v-icon>
    </v-badge>
  </v-btn>
</template>

<script>
import { mapGetters, mapState } from "vuex";

export default {
  name: "cart-button",
  computed: {
    ...mapGetters(["cartSize"]),
    ...mapState(["cartLoading"])
  },
  data() {
    return {
      drawer: false
    };
  },
  methods: {
    toggleDrawer() {
      this.$emit("drawerChange");
    }
  }
};
</script>
<style scoped>
@-webkit-keyframes pulse {
  from {
    -webkit-transform: scale3d(1, 1, 1);
    transform: scale3d(1, 1, 1);
  }

  50% {
    -webkit-transform: scale3d(1.25, 1.25, 1.25);
    transform: scale3d(1.25, 1.25, 1.25);
  }

  to {
    -webkit-transform: scale3d(1, 1, 1);
    transform: scale3d(1, 1, 1);
  }
}

@keyframes pulse {
  from {
    -webkit-transform: scale3d(1, 1, 1);
    transform: scale3d(1, 1, 1);
  }

  50% {
    -webkit-transform: scale3d(1.25, 1.25, 1.25);
    transform: scale3d(1.25, 1.25, 1.25);
  }

  to {
    -webkit-transform: scale3d(1, 1, 1);
    transform: scale3d(1, 1, 1);
  }
}

.animated-pulse {
  animation: pulse 0.5s infinite;
}
</style>

================================================
FILE: frontend/src/components/CartDrawer.vue
================================================
<template>
  <div class="items">
    <v-list-item>
        <v-icon>mdi-cart</v-icon>
      <v-list-item-content>
        <v-list-item-title class="accent--text font-weight-bold">SHOPPING CART</v-list-item-title>
      </v-list-item-content>
    </v-list-item>

    <v-divider></v-divider>

    <v-list dense>
      <v-list-item v-for="item in getCart" :key="item.sk" link>
        <v-list-item-content>
          <v-list-item-content>
            <p>
              {{ item.productDetail.name }}
              <span class="font-weight-light">x {{item.quantity}}</span>
            </p>
            <span class="font-weight-light">${{getTotalPrice(item)}}</span>
          </v-list-item-content>
        </v-list-item-content>
      </v-list-item>
      <v-list-item-content>
        <v-list-item v-if="cartTotalAmount > 0">
          <v-btn to="/checkout" block color="accent">Checkout ${{cartTotalAmount}}</v-btn>
        </v-list-item>
        <v-list-item v-else>Cart Empty</v-list-item>
      </v-list-item-content>
    </v-list>
  </div>
</template>

<script>
import { mapGetters, mapState } from "vuex";
import { Decimal } from "decimal.js";

export default {
  name: "cart-drawer",
  data() {
    return {
      signedIn: false
    };
  },
  computed: {
    ...mapGetters(["cartTotalAmount", "getCart"]),
    ...mapState(["cart"])
  },
  methods: {
    getTotalPrice(item) {
      return new Decimal((item.productDetail.price/100) * item.quantity).toFixed(2);
    }
  }
};
</script>

<style>
.header {
  font-weight: bold !important;
  font-size: 30px !important;
  text-decoration: none;
}
</style>

================================================
FILE: frontend/src/components/CartQuantityEditor.vue
================================================
<template>
  <div>
    <input
      type="text"
      v-if="edit"
      class="cart-quantity-input text-center"
      :class="{ 'input-error': $v.quantity.$error }"
      v-model.trim="$v.quantity.$model"
      @focus="oldquantity=$event.target.value, $event.target.select()"
      @blur="submit($event, product)"
      @keyup.enter="$event.target.blur()"
      v-focus
    />
    <div
      @click="edit = true;"
      v-else
      v-bind:class="{ 'font-weight-light': quantity < 1 }"
      class="pl-2 pr-2 noselect"
    >{{quantity}}</div>
  </div>
</template>

<script>
import { validationMixin } from "vuelidate";
import { required, between, integer } from "vuelidate/lib/validators";

export default {
  props: ["value", "product"],

  data() {
    return {
      edit: false,
      quantity: this.value,
      oldquantity: null
    };
  },
  methods: {
    submit(event, product) {
      this.quantity = event.target.value;
      this.$v.quantity.$touch();
      if (this.$v.$invalid) {
        this.quantity = this.oldquantity
        this.edit = false;
      } else {
        this.edit = false;
        this.$emit("input", { quantity: this.quantity, product });
      }
    }
  },
  validations: {
    quantity: {
      required,
      between: between(0, 500),
      integer
    }
  },
  mixins: [validationMixin],
  watch: {
    value: function() {
      this.quantity = this.value;
    }
  },
  directives: {
    focus: {
      inserted(el) {
        el.focus();
      }
    }
  }
};
</script>

<style scoped>
.cart-quantity-input {
  width: 25px;
}
.noselect {
  -webkit-touch-callout: none; /* iOS Safari */
  -webkit-user-select: none; /* Safari */
  -khtml-user-select: none; /* Konqueror HTML */
  -moz-user-select: none; /* Old versions of Firefox */
  -ms-user-select: none; /* Internet Explorer/Edge */
  user-select: none; /* Non-prefixed version, currently
                                  supported by Chrome, Opera and Firefox */
}
.input-error {
  color: red;
}
</style>

================================================
FILE: frontend/src/components/LoadingOverlay.vue
================================================
<template>
  <v-layout row justify-center>
    <v-dialog v-model="loading" persistent fullscreen content-class="loading-dialog">
      <v-container fill-height justify="center" align="center" style="height: 300px;">
        <v-row justify="center" align="center">
          <v-progress-circular justify-center indeterminate :size="70" :width="7" color="accent"></v-progress-circular>
        </v-row>
        <v-row v-if="loadingText" justify="center" align="center">
          <h1 class="accent--text">{{loadingText}}</h1>
        </v-row>
      </v-container>
    </v-dialog>
  </v-layout>
</template>

<script>
import { mapState } from "vuex";

export default {
  name: "loading-overlay",
  data() {
    return {};
  },
  computed: {
    ...mapState(["loading", "loadingText"])
  }
};
</script>

<style>
.loading-dialog {
  background-color: #303030b2;
}
</style>

================================================
FILE: frontend/src/components/Product.vue
================================================
<template>
  <v-card outlined class="flexcard" height="100%">
    <v-row class="pb-0" dense>
      <v-col :cols="8" class="mb-5">
        <v-card-title primary-title class="pb-0 pt-2">
          <p class="subtitle-2">{{product.name}}</p>
        </v-card-title>
      </v-col>
      <v-col>
        <p class="text-truncate body-2 pt-2 pb-0 pr-2 grow text-right mb-1">{{product.category}}</p>
      </v-col>
    </v-row>
    <v-card-text class="pt-0 pl-4 pb-0">
      <p class="pt-0 pb-0 mb-0 body-2">"{{product.description}}"</p>
      <p class="price pt-0 pb-0 grow accent--text mb-1">${{getPrice(product)}}</p>
    </v-card-text>
    <v-card-actions class="card-actions pa-0 ml-3 mb-2 mt-2 justify-center">
      <v-btn
        icon
        small
        :disabled="cartItemCount(product.productId) < 1"
        @click="removeProductFromCart(product)"
        :loading="product.removeLoading"
      >
        <v-icon>mdi-minus</v-icon>
      </v-btn>
      <cart-quantity-editor @input="updateCart" :product="product" :value="cartItemCount(product.productId)"></cart-quantity-editor>
      <v-btn icon small depressed @click="addProductToCart(product)" :loading="product.addLoading">
        <v-icon>mdi-plus</v-icon>
      </v-btn>
    </v-card-actions>
  </v-card>
</template>

<script>
import { Decimal } from "decimal.js";

export default {
  props: ["product"],
  name: "product",
  methods: {
    cartItemCount(id) {
      let item = this.$store.state.cart.find(obj => obj.sk === id);
      if (item) {
        return item.quantity;
      } else {
        return 0;
      }
    },
    addProductToCart(product) {
      this.$store.dispatch("addToCart", product);
    },
    removeProductFromCart(product) {
      this.$store.dispatch("removeFromCart", product);
    },
    getPrice(product) {
      return new Decimal(product.price/100).toFixed(2);
    },
    updateCart(event) {
      this.$store.dispatch("updateCart", event)
    }
  }
};
</script>

<style scoped>
.flexcard {
  position: relative;
  padding-bottom: 50px;
}
.card-actions {
  position: absolute;
  bottom: 0;
  border: 1px solid;
  border-radius: 15px !important;
  border-color: #dce1e9;
}
</style>

================================================
FILE: frontend/src/main.js
================================================
import Vue from 'vue'
import VueRouter from 'vue-router'

import Amplify from 'aws-amplify'
import Vuelidate from 'vuelidate'
import VueMask from 'v-mask'
import App from './App'
import router from './router'
import config from './aws-exports'
import vuetify from '@/plugins/vuetify'
import store from './store/store'
import {
  components
} from 'aws-amplify-vue';

import CartButton from "@/components/CartButton.vue";
import CartDrawer from "@/components/CartDrawer.vue";
import LoadingOverlay from "@/components/LoadingOverlay.vue";
import Product from "@/components/Product.vue";
import CartQuantityEditor from "@/components/CartQuantityEditor.vue"

Vue.config.productionTip = false

Amplify.configure(config)
Vue.use(VueRouter)
Vue.use(Vuelidate)
Vue.use(VueMask);

Vue.component('cart-button', CartButton)
Vue.component('cart-drawer', CartDrawer)
Vue.component('loading-overlay', LoadingOverlay)
Vue.component('product', Product)
Vue.component('cart-quantity-editor', CartQuantityEditor)


new Vue({
  render: h => h(App),
  router,
  vuetify,
  store,
  components: {
    ...components
  }
}).$mount('#app')


================================================
FILE: frontend/src/plugins/vuetify.js
================================================
import Vue from 'vue'
import Vuetify from 'vuetify/lib'

Vue.use(Vuetify)

const opts = {
    theme: {
      themes: {
        light: {
            primary: '#DCE1E9',
            secondary: '#363732',
            accent: '#e88b01'
        },
      },
    },
  }

export default new Vuetify(opts)

================================================
FILE: frontend/src/router.js
================================================
import VueRouter from 'vue-router'
import Vue from 'vue';
import store from '@/store/store.js'
import Home from '@/views/Home.vue'
import Payment from '@/views/Payment.vue'
import Auth from '@/views/Auth.vue'
import {
  components,
  AmplifyEventBus
} from 'aws-amplify-vue';

import * as AmplifyModules from 'aws-amplify'
import {
  AmplifyPlugin
} from 'aws-amplify-vue'

Vue.use(AmplifyPlugin, AmplifyModules)


getUser().then((user) => {
  if (user) {
    router.push({
      path: '/'
    }).catch(() => {})
  }
})

AmplifyEventBus.$on('authState', async (state) => {
  if (state === 'signedOut') {
    store.commit('setUser', null);
    store.dispatch('fetchCart')
    router.push({
      path: '/'
    }).catch(() => {})
  } else if (state === 'signedIn') {
    getUser().then(() => {
      if (store.state.cart.length > 0) {
        store.dispatch('migrateCart')
      } else {
        store.dispatch('fetchCart')
      }
    })
    router.push({
      path: new URLSearchParams(window.location.search).get('redirect')|| '/'
    }).catch(() => {})
  }
});

function getUser() {
  return Vue.prototype.$Amplify.Auth.currentAuthenticatedUser().then((data) => {
    if (data && data.signInUserSession) {
      store.commit('setUser', data);
      return data;
    }
  }).catch(() => {
    store.commit('setUser', null);
    return null
  });
}
const routes = [{
    path: '/',
    component: Home
  },
  {
    path: '/auth',
    name: 'Authenticator',
    component: Auth
  }, {
    path: '/checkout',
    component: Payment,
    meta: {
      requiresAuth: true
    }
  }
]

const router = new VueRouter({
  mode: 'history',
  routes
})

router.beforeResolve(async (to, from, next) => {
  if (to.matched.some(record => record.meta.requiresAuth)) {
    let user = await getUser();
    if (!user) {
      return next({
        path: '/auth',
        query: {
          redirect: to.fullPath,
        }
      });
    }
    return next()
  }
  return next()
})

export default router

================================================
FILE: frontend/src/store/actions.js
================================================
import {
    postCart,
    getCart,
    getProducts,
    cartMigrate,
    putCart,
    cartCheckout
} from "@/backend/api.js"

import router from '@/router'

const setLoading = ({
    commit
}, payload) => {
    commit("setLoading", {value: payload.value,
    message: payload.message})
}

const fetchProducts = ({
    commit
}) => {
    getProducts().then((response) => {
        commit("setUpProducts", response.products);
    });
}
const fetchCart = ({
    commit
}) => {
    commit("setLoading", {value: true})
    getCart()
        .then((response) => {
            commit("setUpCart", response.products)
            commit("setLoading", {value: false})
        })
}
const addToCart = ({
    commit
}, obj) => {
    commit("setProductLoading", {
        "product": obj,
        "value": true,
        "btn": "add"
    })
    postCart(obj)
        .then((response) => {
            commit("setCartLoading", 1)
            commit("addToCart", response.productId);
            commit("setProductLoading", {
                "product": obj,
                "value": false,
                "btn": "add"
            })
            setTimeout(() => {
                commit("setCartLoading", -1)
            }, 500)
        }).catch(() => {
            commit("setProductLoading", {
                "product": obj,
                "value": false,
                "btn": "add"
            })
        });
}
const removeFromCart = ({
    commit
}, obj) => {
    commit("setProductLoading", {
        "product": obj,
        "value": true,
        "btn": "remove"
    })
    postCart(obj, -1)
        .then((response) => {
            commit("setCartLoading", 1)
            commit("removeFromCart", response.productId)
            commit("setProductLoading", {
                "product": obj,
                "value": false,
                "btn": "remove"
            })
            setTimeout(() => {
                commit("setCartLoading", -1)
            }, 500)
        }).catch(() => {
            commit("setProductLoading", {
                "product": obj,
                "value": false,
                "btn": "remove"
            })
        })
}
const updateCart = ({
    commit
}, obj) => {
    putCart(obj.product, obj.quantity)
        .then((response) => {
            commit("setCartLoading", 1)
            commit("updateCart", response)
            setTimeout(() => {
                commit("setCartLoading", -1)
            }, 500)
        })
}
const migrateCart = ({
    commit
}) => {
    commit("setLoading", {value: true})
    cartMigrate()
        .then((response) => {
            commit("setUpCart", response.products)
            commit("setLoading", {value: false})
        })
}

const checkoutCart = ({
    commit
}) => {
    commit("setLoading", {value: true, message: "This is where we'd handle payment before clearing the cart..."})
    cartCheckout()
        .then(() => {
            commit("setUpCart", [])
            setTimeout(function() {commit("setLoading", {value: false})}, 3000)
            setTimeout(function() {router.push("/")}, 3200)
        })
}

export default {
    setLoading,
    fetchCart,
    fetchProducts,
    migrateCart,
    updateCart,
    removeFromCart,
    addToCart,
    checkoutCart
}

================================================
FILE: frontend/src/store/getters.js
================================================
import {
    Decimal
} from "decimal.js"


const cartSize = (state) => {
    return state.cart.reduce((total, cartProduct) => {
        return total + cartProduct.quantity
    }, 0)
}
const cartTotalAmount = (state) => {
    let val = state.cart.reduce((total, cartProduct) => {
        return new Decimal(total).plus(new Decimal(cartProduct.productDetail.price/100).times(cartProduct.quantity));
    }, 0);
    return new Decimal(val).toFixed(2)
}
const currentUser = (state) => {
    return state.user
}
const getCart = (state) => {
    return state.cart.filter((prod) => prod.quantity > 0)
}

export default {
    cartSize,
    cartTotalAmount,
    currentUser,
    getCart
}

================================================
FILE: frontend/src/store/mutations.js
================================================
const setUser = (state, user) => {
    state.user = user
}

const setUpProducts = (state, productsPayload) => {
    productsPayload.forEach((product) => {
        product.addLoading = false
        product.removeLoading = false
    })
    state.products = productsPayload;
}
const setUpCart = (state, cartPayload) => {
    state.cart = cartPayload;
}
const addToCart = (state, productId) => {
    let product = {}
    product.productDetail = state.products.find((prod) => prod.productId === productId);
    product.sk = productId
    let cartProduct = state.cart.find((prod) => prod.sk === productId);
    if (cartProduct) {
        cartProduct.quantity++;
    } else {
        state.cart.push({
            ...product,
            quantity: 1,
        });
    }
}
const removeFromCart = (state, productId) => {
    let product = {}
    product.productDetail = state.products[productId];
    product.sk = productId
    let cartProduct = state.cart.find((prod) => prod.sk === productId);
    cartProduct.quantity--;
}
const deleteFromCart = (state, productId) => {
    let product = state.products.find((product) => product.productId === productId);
    let cartProductIndex = state.cart.findIndex((product) => product.productId === productId);
    product.quantity = state.cart[cartProductIndex].stock;
    state.cart.splice(cartProductIndex, 1);
}
const setLoading = (state, payload) => {
    state.loading = payload.value
    state.loadingText = payload.message
}
const setCartLoading = (state, value) => {
    state.cartLoading += value
}
const setProductLoading = (state, {
    product,
    btn,
    value
}) => {
    let prod = state.products.find((prod) => prod.productId === product.productId);
    prod[btn + "Loading"] = value
}
const updateCart = (state, obj) => {
    let product = {}
    product.productDetail = state.products.find((prod) => prod.productId === obj.productId);
    product.sk = obj.productId
    let cartProduct = state.cart.find((prod) => prod.sk === obj.productId);
    if (cartProduct) {
        cartProduct.quantity = obj.quantity
    } else {
        state.cart.push({
            ...product,
            quantity: obj.quantity,
        });
    }
}

export default {
    setUser,
    setUpProducts,
    setUpCart,
    addToCart,
    removeFromCart,
    deleteFromCart,
    setLoading,
    setCartLoading,
    setProductLoading,
    updateCart
}

================================================
FILE: frontend/src/store/store.js
================================================
import Vue from 'vue'
import Vuex from 'vuex'
import actions from './actions';
import mutations from './mutations';
import getters from './getters';


Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    products: null,
    cart: [],
    user: null,
    loading: false,
    cartLoading: 0,
    loadingText: ""
  },
  getters,
  mutations,
  actions
})

================================================
FILE: frontend/src/views/Auth.vue
================================================
<template>
    <amplify-authenticator v-bind:authConfig="authConfig"/>
</template>

<script>
export default {
  data() {
      return {
      authConfig: {
          signUpConfig: {
            hideAllDefaults: true,
            defaultCountryCode: '1',
            signUpFields: [
              {
                label: 'Username',
                key: 'username',
                required: true,
                displayOrder: 1,
                type: 'string',
              },
              {
                label: 'Password',
                key: 'password',
                required: true,
                displayOrder: 2,
                type: 'password'
              },
              {
                label: 'Email',
                key: 'email',
                required: true,
                displayOrder: 3,
                type: 'string'
              }
            ]
          }
        }
      }
  }
};
</script>

================================================
FILE: frontend/src/views/Home.vue
================================================
<template>
    <v-container grid-list-md fluid class="mt-0" pt-0>
      <v-layout row wrap>
        <v-flex v-for="product in products" :key="product.productId" xs12 lg4 sm6>
            <product :product="product" :key="product.productId" />
        </v-flex>
      </v-layout>
    </v-container>
</template>

<script>
export default {
  computed: {
    products() {
      return this.$store.state.products;
    }
  },
  created() {
    this.$store.dispatch("fetchProducts");
  }
};
</script>

================================================
FILE: frontend/src/views/Payment.vue
================================================
<template>
  <v-container grid-list-md fluid class="mt-0" pt-0>
    <h1>Example payment form</h1>
    <v-layout row wrap>
      <v-flex xs12 lg4 sm6>
        <v-card>
          <v-container>
            <v-form pa-2 ma-2>
              <v-text-field
                color="secondary"
                outlined
                required
                @input="$v.cardNumber.$touch()"
                @blur="$v.cardNumber.$touch()"
                v-model="cardNumber"
                label="Card Number"
                v-mask="'#### #### #### ####'"
                :error-messages="cardNumberErrors"
              ></v-text-field>
              <v-text-field
                color="secondary"
                outlined
                required
                @input="$v.cardName.$touch()"
                @blur="$v.cardName.$touch()"
                label="Cardholder Name"
                v-model="cardName"
                :error-messages="cardNameErrors"
              ></v-text-field>
              <v-text-field
                color="secondary"
                outlined
                required
                @input="$v.cardExpiry.$touch()"
                @blur="$v.cardExpiry.$touch()"
                label="Card Expiry"
                v-model="cardExpiry"
                :error-messages="cardExpiryErrors"
              ></v-text-field>
              <v-text-field
                color="secondary"
                outlined
                required
                @input="$v.cardCVC.$touch()"
                @blur="$v.cardCVC.$touch()"
                label="Card CVC"
                v-model="cardCVC"
                :error-messages="cardCVCErrors"
              ></v-text-field>
              <v-btn block color="accent" @click="submit">Submit</v-btn>
            </v-form>
          </v-container>
        </v-card>
      </v-flex>
    </v-layout>
  </v-container>
</template>

<script>
import { mapState, mapGetters } from "vuex";
import { validationMixin } from "vuelidate";
import { required, helpers } from "vuelidate/lib/validators";

const ccvalidate = helpers.regex(
  "alpha",
  /(\d{4} *\d{4} *\d{4} *\d{4})/
); /* I know, I know... */

export default {
  data() {
    return {
      cardNumber: null,
      cardExpiry: null,
      cardName: null,
      cardCVC: null
    };
  },
  mixins: [validationMixin],
  validations: {
    cardNumber: {
      required,
      ccvalidate: ccvalidate
    },
    cardExpiry: {
      required
    },
    cardCVC: {
      required
    },
    cardName: {
      required
    }
  },
  methods: {
    submit() {
      this.$v.$touch();
      if (this.$v.$invalid) {
        console.log("invalid form"); // eslint-disable-line no-console
      } else {
        this.$store.dispatch("checkoutCart")
        // TODO: redirect to confirmation
      }
    }
  },
  computed: {
    ...mapGetters(["cartTotalAmount", "getCart"]),
    ...mapState(["cart"]),
    cardNumberErrors() {
      const errors = [];
      if (!this.$v.cardNumber.$dirty) return errors;
      !this.$v.cardNumber.ccvalidate &&
        errors.push("Valid card number is required.");
      !this.$v.cardNumber.required && errors.push("Card number is required.");
      return errors;
    },
    cardNameErrors() {
      const errors = [];
      if (!this.$v.cardName.$dirty) return errors;
      !this.$v.cardName.required && errors.push("Cardholder name is required.");
      return errors;
    },
    cardExpiryErrors() {
      const errors = [];
      if (!this.$v.cardExpiry.$dirty) return errors;
      !this.$v.cardExpiry.required && errors.push("Cart expiry is required.");
      return errors;
    },
    cardCVCErrors() {
      const errors = [];
      if (!this.$v.cardCVC.$dirty) return errors;
      !this.$v.cardCVC.required && errors.push("Card CVC is required.");
      return errors;
    }
  }
};
</script>



================================================
FILE: frontend/vue.config.js
================================================
module.exports = {
  "transpileDependencies": [
    "vuetify"
  ],
  lintOnSave: false

}
Download .txt
gitextract_sass801v/

├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── README.md
├── amplify/
│   └── .gitkeep
├── amplify-ci/
│   └── amplify-template.yaml
├── amplify.yml
├── backend/
│   ├── .gitignore
│   ├── Makefile
│   ├── auth.yaml
│   ├── layers/
│   │   ├── requirements.txt
│   │   └── shared.py
│   ├── product-mock-service/
│   │   ├── __init__.py
│   │   ├── get_product.py
│   │   ├── get_products.py
│   │   ├── product_list.json
│   │   └── requirements.txt
│   ├── product-mock.yaml
│   ├── shopping-cart-service/
│   │   ├── __init__.py
│   │   ├── add_to_cart.py
│   │   ├── checkout_cart.py
│   │   ├── db_stream_handler.py
│   │   ├── delete_from_cart.py
│   │   ├── get_cart_total.py
│   │   ├── list_cart.py
│   │   ├── migrate_cart.py
│   │   ├── requirements.txt
│   │   ├── tests/
│   │   │   ├── __init__.py
│   │   │   └── test_example.py
│   │   ├── update_cart.py
│   │   └── utils.py
│   └── shoppingcart-service.yaml
└── frontend/
    ├── .gitignore
    ├── Makefile
    ├── babel.config.js
    ├── package.json
    ├── public/
    │   └── index.html
    ├── scripts/
    │   └── fetchconfig.js
    ├── src/
    │   ├── App.vue
    │   ├── aws-exports.js
    │   ├── backend/
    │   │   └── api.js
    │   ├── components/
    │   │   ├── CartButton.vue
    │   │   ├── CartDrawer.vue
    │   │   ├── CartQuantityEditor.vue
    │   │   ├── LoadingOverlay.vue
    │   │   └── Product.vue
    │   ├── main.js
    │   ├── plugins/
    │   │   └── vuetify.js
    │   ├── router.js
    │   ├── store/
    │   │   ├── actions.js
    │   │   ├── getters.js
    │   │   ├── mutations.js
    │   │   └── store.js
    │   └── views/
    │       ├── Auth.vue
    │       ├── Home.vue
    │       └── Payment.vue
    └── vue.config.js
Download .txt
SYMBOL INDEX (31 symbols across 16 files)

FILE: backend/layers/shared.py
  class NotFoundException (line 22) | class NotFoundException(Exception):
  function handle_decimal_type (line 27) | def handle_decimal_type(obj):
  function generate_ttl (line 40) | def generate_ttl(days=1):
  function get_user_sub (line 49) | def get_user_sub(jwt_token):
  function get_cart_id (line 64) | def get_cart_id(event_headers):
  function get_headers (line 81) | def get_headers(cart_id):

FILE: backend/product-mock-service/get_product.py
  function lambda_handler (line 21) | def lambda_handler(event, context):

FILE: backend/product-mock-service/get_products.py
  function lambda_handler (line 21) | def lambda_handler(event, context):

FILE: backend/shopping-cart-service/add_to_cart.py
  function lambda_handler (line 28) | def lambda_handler(event, context):

FILE: backend/shopping-cart-service/checkout_cart.py
  function lambda_handler (line 23) | def lambda_handler(event, context):

FILE: backend/shopping-cart-service/db_stream_handler.py
  function dynamodb_to_python (line 18) | def dynamodb_to_python(dynamodb_item):
  function lambda_handler (line 27) | def lambda_handler(event, context):

FILE: backend/shopping-cart-service/delete_from_cart.py
  function lambda_handler (line 16) | def lambda_handler(event, context):

FILE: backend/shopping-cart-service/get_cart_total.py
  function lambda_handler (line 18) | def lambda_handler(event, context):

FILE: backend/shopping-cart-service/list_cart.py
  function lambda_handler (line 19) | def lambda_handler(event, context):

FILE: backend/shopping-cart-service/migrate_cart.py
  function update_item (line 22) | def update_item(user_id, item):
  function lambda_handler (line 46) | def lambda_handler(event, context):

FILE: backend/shopping-cart-service/tests/test_example.py
  class Tests (line 10) | class Tests(unittest.TestCase):
    method setUp (line 15) | def setUp(self):
    method test_headers (line 18) | def test_headers(self):

FILE: backend/shopping-cart-service/update_cart.py
  function lambda_handler (line 28) | def lambda_handler(event, context):

FILE: backend/shopping-cart-service/utils.py
  function get_product_from_external_service (line 15) | def get_product_from_external_service(product_id):

FILE: frontend/scripts/fetchconfig.js
  function formatParams (line 24) | function formatParams(data) {

FILE: frontend/src/backend/api.js
  function getHeaders (line 6) | async function getHeaders(includeAuth) {
  function getCart (line 29) | async function getCart() {
  function postCart (line 37) | async function postCart(obj, quantity = 1) {
  function putCart (line 50) | async function putCart(obj, quantity) {
  function getProducts (line 63) | async function getProducts() {
  function cartMigrate (line 71) | async function cartMigrate() {
  function cartCheckout (line 80) | async function cartCheckout() {

FILE: frontend/src/router.js
  function getUser (line 49) | function getUser() {
Condensed preview — 59 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (122K chars).
[
  {
    "path": ".gitignore",
    "chars": 16,
    "preview": ".env.local\n.idea"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 309,
    "preview": "## Code of Conduct\nThis project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-condu"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 3306,
    "preview": "# Contributing Guidelines\n\nThank you for your interest in contributing to our project. Whether it's a bug report, new fe"
  },
  {
    "path": "LICENSE",
    "chars": 932,
    "preview": "Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.\n\nPermission is hereby granted, free of charge, t"
  },
  {
    "path": "Makefile",
    "chars": 1514,
    "preview": "all: backend frontend-build\n\nTEMPLATES = auth product-mock shoppingcart-service\n\nREGION := $(shell python3 -c 'import bo"
  },
  {
    "path": "README.md",
    "chars": 7966,
    "preview": "# Serverless Shopping Cart Microservice\n\nThis application is a sample application to demonstrate how you could implement"
  },
  {
    "path": "amplify/.gitkeep",
    "chars": 1,
    "preview": "\n"
  },
  {
    "path": "amplify-ci/amplify-template.yaml",
    "chars": 5066,
    "preview": "AWSTemplateFormatVersion: 2010-09-09\n\nParameters:\n  Repository:\n    Type: String\n    Description: GitHub Repository URL\n"
  },
  {
    "path": "amplify.yml",
    "chars": 789,
    "preview": "version: 1\nenv:\n  variables:\n      ORIGIN: https://${AWS_BRANCH//\\//-}.${AWS_APP_ID}.amplifyapp.com\n      STACKNAME: amp"
  },
  {
    "path": "backend/.gitignore",
    "chars": 4220,
    "preview": "\n# Created by https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode\n\n### Linux ###\n*~\n\n# tempor"
  },
  {
    "path": "backend/Makefile",
    "chars": 757,
    "preview": "all: build deploy\nORIGIN ?= http://localhost:8080\nSTACKNAME ?= aws-serverless-shopping-cart\n\nbuild:\n\t@echo \"Building tem"
  },
  {
    "path": "backend/auth.yaml",
    "chars": 1330,
    "preview": "AWSTemplateFormatVersion: '2010-09-09'\nTransform: AWS::Serverless-2016-10-31\nDescription: >\n  auth-resources\n\n  SAM Temp"
  },
  {
    "path": "backend/layers/requirements.txt",
    "chars": 50,
    "preview": "requests==2.22.0\ncognitojwt==1.1.0\nboto3==1.10.34\n"
  },
  {
    "path": "backend/layers/shared.py",
    "chars": 2328,
    "preview": "import calendar\nimport datetime\nimport os\nimport uuid\nfrom decimal import Decimal\nfrom http.cookies import SimpleCookie\n"
  },
  {
    "path": "backend/product-mock-service/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "backend/product-mock-service/get_product.py",
    "chars": 958,
    "preview": "import json\nimport os\n\nfrom aws_lambda_powertools import Logger, Tracer\n\nlogger = Logger()\ntracer = Tracer()\n\nwith open("
  },
  {
    "path": "backend/product-mock-service/get_products.py",
    "chars": 735,
    "preview": "import json\nimport os\n\nfrom aws_lambda_powertools import Logger, Tracer\n\nlogger = Logger()\ntracer = Tracer()\n\nwith open("
  },
  {
    "path": "backend/product-mock-service/product_list.json",
    "chars": 14184,
    "preview": "[\n    {\n        \"category\": \"fruit\",\n        \"createdDate\": \"2017-04-17T01:14:03 -02:00\",\n        \"description\": \"Culpa "
  },
  {
    "path": "backend/product-mock-service/requirements.txt",
    "chars": 28,
    "preview": "aws-lambda-powertools==1.0.0"
  },
  {
    "path": "backend/product-mock.yaml",
    "chars": 1791,
    "preview": "AWSTemplateFormatVersion: '2010-09-09'\nTransform: AWS::Serverless-2016-10-31\nDescription: >\n  product-service\n\n  SAM Tem"
  },
  {
    "path": "backend/shopping-cart-service/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "backend/shopping-cart-service/add_to_cart.py",
    "chars": 3680,
    "preview": "import json\nimport os\n\nimport boto3\nfrom aws_lambda_powertools import Logger, Metrics, Tracer\n\nfrom shared import (\n    "
  },
  {
    "path": "backend/shopping-cart-service/checkout_cart.py",
    "chars": 2137,
    "preview": "import json\nimport os\n\nimport boto3\nfrom aws_lambda_powertools import Logger, Metrics, Tracer\nfrom boto3.dynamodb.condit"
  },
  {
    "path": "backend/shopping-cart-service/db_stream_handler.py",
    "chars": 2076,
    "preview": "import os\nfrom collections import Counter\n\nimport boto3\nfrom aws_lambda_powertools import Logger, Tracer\nfrom boto3.dyna"
  },
  {
    "path": "backend/shopping-cart-service/delete_from_cart.py",
    "chars": 761,
    "preview": "import json\nimport os\n\nimport boto3\nfrom aws_lambda_powertools import Logger, Tracer\n\nlogger = Logger()\ntracer = Tracer("
  },
  {
    "path": "backend/shopping-cart-service/get_cart_total.py",
    "chars": 790,
    "preview": "import json\nimport os\n\nimport boto3\nfrom aws_lambda_powertools import Logger, Tracer\n\nfrom shared import handle_decimal_"
  },
  {
    "path": "backend/shopping-cart-service/list_cart.py",
    "chars": 2012,
    "preview": "import json\nimport os\n\nimport boto3\nfrom aws_lambda_powertools import Logger, Tracer\nfrom boto3.dynamodb.conditions impo"
  },
  {
    "path": "backend/shopping-cart-service/migrate_cart.py",
    "chars": 3979,
    "preview": "import json\nimport os\nimport threading\n\nimport boto3\nfrom aws_lambda_powertools import Logger, Metrics, Tracer\nfrom boto"
  },
  {
    "path": "backend/shopping-cart-service/requirements.txt",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "backend/shopping-cart-service/tests/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "backend/shopping-cart-service/tests/test_example.py",
    "chars": 511,
    "preview": "import sys\nimport unittest\n\nsys.path.append(\"..\")  # Add application to path\nsys.path.append(\"./layers/\")  # Add layer t"
  },
  {
    "path": "backend/shopping-cart-service/update_cart.py",
    "chars": 3125,
    "preview": "import json\nimport os\n\nimport boto3\nfrom aws_lambda_powertools import Logger, Metrics, Tracer\n\nfrom shared import (\n    "
  },
  {
    "path": "backend/shopping-cart-service/utils.py",
    "chars": 627,
    "preview": "import os\n\nimport requests\nfrom aws_lambda_powertools import Logger, Tracer\n\nfrom shared import NotFoundException\n\nprodu"
  },
  {
    "path": "backend/shoppingcart-service.yaml",
    "chars": 11657,
    "preview": "AWSTemplateFormatVersion: '2010-09-09'\nTransform: AWS::Serverless-2016-10-31\nDescription: >\n  shoppingcart-service\n\n  SA"
  },
  {
    "path": "frontend/.gitignore",
    "chars": 1618,
    "preview": "# Created by .ignore support plugin (hsz.mobi)\n### Vue template\n# gitignore template for Vue.js projects\n#\n# Recommended"
  },
  {
    "path": "frontend/Makefile",
    "chars": 142,
    "preview": "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"
  },
  {
    "path": "frontend/babel.config.js",
    "chars": 73,
    "preview": "module.exports = {\n  presets: [\n    '@vue/cli-plugin-babel/preset'\n  ]\n}\n"
  },
  {
    "path": "frontend/package.json",
    "chars": 1639,
    "preview": "{\n  \"name\": \"shoppingcart-service-frontend\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"serve\": \"vue-c"
  },
  {
    "path": "frontend/public/index.html",
    "chars": 905,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE="
  },
  {
    "path": "frontend/scripts/fetchconfig.js",
    "chars": 1462,
    "preview": "process.env.AWS_SDK_LOAD_CONFIG = true;\nvar fs = require('fs');\n\nvar args = process.argv.slice(2);\nvar envtype = args[0]"
  },
  {
    "path": "frontend/src/App.vue",
    "chars": 1825,
    "preview": "<template>\n  <v-app>\n    <v-app-bar elevate-on-scroll app class=\"primary\">\n      <v-toolbar-title>\n        <router-link "
  },
  {
    "path": "frontend/src/aws-exports.js",
    "chars": 445,
    "preview": "const awsmobile = {\n  Auth: {\n    region: process.env.VUE_APP_AWS_REGION,\n    userPoolId: process.env.VUE_APP_USER_POOL_"
  },
  {
    "path": "frontend/src/backend/api.js",
    "chars": 2005,
    "preview": "import {\n    Auth,\n    API\n} from 'aws-amplify'\n\nasync function getHeaders(includeAuth) {\n    const headers = {\n        "
  },
  {
    "path": "frontend/src/components/CartButton.vue",
    "chars": 1330,
    "preview": "<template>\n  <v-btn fixed right rounded small @click.stop=\"toggleDrawer\" text icon x-large>\n    <v-badge overlap color=\""
  },
  {
    "path": "frontend/src/components/CartDrawer.vue",
    "chars": 1605,
    "preview": "<template>\n  <div class=\"items\">\n    <v-list-item>\n        <v-icon>mdi-cart</v-icon>\n      <v-list-item-content>\n       "
  },
  {
    "path": "frontend/src/components/CartQuantityEditor.vue",
    "chars": 1996,
    "preview": "<template>\n  <div>\n    <input\n      type=\"text\"\n      v-if=\"edit\"\n      class=\"cart-quantity-input text-center\"\n      :c"
  },
  {
    "path": "frontend/src/components/LoadingOverlay.vue",
    "chars": 866,
    "preview": "<template>\n  <v-layout row justify-center>\n    <v-dialog v-model=\"loading\" persistent fullscreen content-class=\"loading-"
  },
  {
    "path": "frontend/src/components/Product.vue",
    "chars": 2176,
    "preview": "<template>\n  <v-card outlined class=\"flexcard\" height=\"100%\">\n    <v-row class=\"pb-0\" dense>\n      <v-col :cols=\"8\" clas"
  },
  {
    "path": "frontend/src/main.js",
    "chars": 1116,
    "preview": "import Vue from 'vue'\nimport VueRouter from 'vue-router'\n\nimport Amplify from 'aws-amplify'\nimport Vuelidate from 'vueli"
  },
  {
    "path": "frontend/src/plugins/vuetify.js",
    "chars": 296,
    "preview": "import Vue from 'vue'\nimport Vuetify from 'vuetify/lib'\n\nVue.use(Vuetify)\n\nconst opts = {\n    theme: {\n      themes: {\n "
  },
  {
    "path": "frontend/src/router.js",
    "chars": 1985,
    "preview": "import VueRouter from 'vue-router'\nimport Vue from 'vue';\nimport store from '@/store/store.js'\nimport Home from '@/views"
  },
  {
    "path": "frontend/src/store/actions.js",
    "chars": 3245,
    "preview": "import {\n    postCart,\n    getCart,\n    getProducts,\n    cartMigrate,\n    putCart,\n    cartCheckout\n} from \"@/backend/ap"
  },
  {
    "path": "frontend/src/store/getters.js",
    "chars": 678,
    "preview": "import {\n    Decimal\n} from \"decimal.js\"\n\n\nconst cartSize = (state) => {\n    return state.cart.reduce((total, cartProduc"
  },
  {
    "path": "frontend/src/store/mutations.js",
    "chars": 2377,
    "preview": "const setUser = (state, user) => {\n    state.user = user\n}\n\nconst setUpProducts = (state, productsPayload) => {\n    prod"
  },
  {
    "path": "frontend/src/store/store.js",
    "chars": 361,
    "preview": "import Vue from 'vue'\nimport Vuex from 'vuex'\nimport actions from './actions';\nimport mutations from './mutations';\nimpo"
  },
  {
    "path": "frontend/src/views/Auth.vue",
    "chars": 929,
    "preview": "<template>\n    <amplify-authenticator v-bind:authConfig=\"authConfig\"/>\n</template>\n\n<script>\nexport default {\n  data() {"
  },
  {
    "path": "frontend/src/views/Home.vue",
    "chars": 493,
    "preview": "<template>\n    <v-container grid-list-md fluid class=\"mt-0\" pt-0>\n      <v-layout row wrap>\n        <v-flex v-for=\"produ"
  },
  {
    "path": "frontend/src/views/Payment.vue",
    "chars": 3848,
    "preview": "<template>\n  <v-container grid-list-md fluid class=\"mt-0\" pt-0>\n    <h1>Example payment form</h1>\n    <v-layout row wrap"
  },
  {
    "path": "frontend/vue.config.js",
    "chars": 89,
    "preview": "module.exports = {\n  \"transpileDependencies\": [\n    \"vuetify\"\n  ],\n  lintOnSave: false\n\n}"
  }
]

About this extraction

This page contains the full source code of the aws-samples/aws-serverless-shopping-cart GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 59 files (108.5 KB), approximately 29.2k tokens, and a symbol index with 31 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!