Skip to content

Commit

Permalink
Merge branch 'main' into update-cd-dev
Browse files Browse the repository at this point in the history
  • Loading branch information
RealDyllon authored Feb 23, 2023
2 parents 68c501d + ac81097 commit 4229df7
Show file tree
Hide file tree
Showing 30 changed files with 1,565 additions and 3,378 deletions.
2 changes: 2 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[run]
omit = **/__init__.py
1 change: 0 additions & 1 deletion .env.dev

This file was deleted.

14 changes: 0 additions & 14 deletions .env.test

This file was deleted.

2 changes: 1 addition & 1 deletion .github/workflows/cd-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,4 @@ jobs:
- name: Deploy to AWS
run: |
npm run deploy -- --stage production --aws-profile github_actions
npm run deploy -- --stage prod --aws-profile github_actions
52 changes: 47 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: "14"
node-version: "16"

- name: Setup Python
uses: actions/setup-python@v2
Expand All @@ -38,17 +38,48 @@ jobs:
virtualenvs-path: ./poetry_virtualenv
installer-parallel: true

- name: Create dummy .env file for tests
run: |
touch .env
echo "
STRIPE_SECRET_KEY=key
FRONTEND_HOST='http://localhost:3000'
PRODUCTS_TABLE_NAME=testing-products
PRODUCT_CATEGORIES_TABLE_NAME=testing-products-categories
ORDER_HOLD_TABLE_NAME=testing-order-hold
" >> .env
- name: Setup aws dummy credentials
run: |
mkdir ~/.aws
touch ~/.aws/credentials
- name: Install dependencies
run: npm run setup

- name: Pytest
run: npm run test

run: npm run test:py # TODO: change this to `npm run test`

env:
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
IS_OFFLINE: "true"
AWS_ACCESS_KEY_ID: 'testing'
AWS_SECRET_ACCESS_KEY: 'testing'
AWS_SECURITY_TOKEN: 'testing'
AWS_SESSION_TOKEN: 'testing'
AWS_DEFAULT_REGION: 'ap-southeast-1'

PRODUCT_CATEGORIES_TABLE_NAME: 'be-dev-product_categories'
PRODUCTS_TABLE_NAME: 'be-dev-products'
FRONTEND_HOST: 'https://dev.merch.ntuscse.com'

BASE_API_SERVER_URL: 'https://api.dev.ntuscse.com'

- name: Pytest coverage comment
uses: MishaKav/pytest-coverage-comment@main
with:
pytest-xml-coverage-path: ./coverage.xml

lint:
runs-on: ubuntu-latest
steps:
Expand All @@ -63,9 +94,20 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: "3.9"
python-version: "3.9.16"
architecture: "x64"

- name: Create dummy .env file for lint
run: |
touch .env
echo "
STRIPE_SECRET_KEY=key
FRONTEND_HOST='http://localhost:3000'
PRODUCTS_TABLE_NAME=testing-products
PRODUCT_CATEGORIES_TABLE_NAME=testing-products-categories
ORDER_HOLD_TABLE_NAME=testing-order-hold
" >> .env
- name: Install and configure Poetry
uses: snok/install-poetry@v1
with:
Expand Down
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,20 @@ node_modules/
# python local
.dynamodb/
.env
.env.dev

# generated
out/
docs_server/swagger/openapi.json

#env vars
.env.*

# pytest
/.pytest_cache/

# test coverage
/.coverage
/coverage_html/
/coverage.lcov
/coverage.xml
2 changes: 2 additions & 0 deletions .sample.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
STRIPE_SECRET_KEY='<redacted>'
FRONTEND_HOST='http://localhost:3000'
27 changes: 27 additions & 0 deletions be/api/v1/cron/expire_unpaid_orders.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import boto3
from datetime import datetime
import os
from utils.aws.dynamodb import dynamodb
import sys


table_name = os.environ["ORDER_HOLD_TABLE_NAME"]

def handler(event, context):
table = dynamodb.Table(table_name)

# Get the current epoch time
now = int(datetime.now().timestamp())

# Scan the table to find all expired documents
result = table.scan(
FilterExpression="expiry < :now",
ExpressionAttributeValues={":now": now}
)

# Delete the expired documents
with table.batch_writer() as batch:
for item in result["Items"]:
batch.delete_item(Key={"transactionID": item["transactionID"]})

return dict()
32 changes: 25 additions & 7 deletions be/api/v1/endpoints/cart/checkout/post.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@
import uuid
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from datetime import datetime
from botocore.exceptions import ClientError
from datetime import datetime, timedelta
import stripe
from be.api.v1.templates.non_auth_route import create_non_auth_router
from be.api.v1.models.cart import PriceModel, Cart
from be.api.v1.models.order_hold_entry import OrderHoldEntry, ReservedProduct
from be.api.v1.models.orders import OrderItem, Order, OrderStatus
from be.api.v1.utils.cart_utils import calc_cart_value, describe_cart, generate_order_items_from_cart
from utils.dal.order import dal_create_order
from utils.dal.products import dal_increment_stock_count
from utils.dal.order_hold import dal_create_order_hold_entry

stripe.api_key = os.environ["STRIPE_SECRET_KEY"]
DEFAULT_ORDER_EXPIRY_TIME = 1

router = APIRouter(prefix="/cart/checkout", tags=["cart"])

Expand All @@ -30,6 +35,7 @@ class PostCheckoutResponseModel(BaseModel):
items: list[OrderItem]
price: PriceModel
payment: PaymentModel
expiry: int


@router.post("", response_model=PostCheckoutResponseModel)
Expand All @@ -45,10 +51,9 @@ async def post_checkout(req: CheckoutRequestBodyModel):
# calculate subtotal
items_products = generate_order_items_from_cart(req)

orderID = uuid.uuid4().__str__()
price = calc_cart_value(items_products)
description = describe_cart(items_products)

# todo: create "pending" order here - in db
description = describe_cart(items_products, orderID)

payment_intent = stripe.PaymentIntent.create(
payment_method_types=["paynow"],
Expand All @@ -58,14 +63,13 @@ async def post_checkout(req: CheckoutRequestBodyModel):
receipt_email=req.email,
description=f"SCSE Merch Purchase:\n{description}"
)

orderID = uuid.uuid4().__str__()
orderDateTime = datetime.now().__str__()
customerEmail = req.email
transactionID = payment_intent.id
paymentPlatform = "stripe"
orderItems = items_products
status = OrderStatus.PENDING_PAYMENT
expiry = datetime.now() + timedelta(hours=int(os.environ.get("ORDER_EXPIRY_TIME", DEFAULT_ORDER_EXPIRY_TIME)))

order = Order(
orderID = orderID,
Expand All @@ -77,7 +81,14 @@ async def post_checkout(req: CheckoutRequestBodyModel):
status = status
)

for orderItem in orderItems:
dal_increment_stock_count(orderItem.id, -orderItem.quantity, orderItem.size, orderItem.colorway)

reservedProducts = [ReservedProduct(productID=item.productId, qty=item.quantity) for item in req.items]
orderHoldEntry = OrderHoldEntry(transactionID=transactionID, expiry=int(expiry.timestamp()), reservedProducts=reservedProducts)

dal_create_order(order)
dal_create_order_hold_entry(orderHoldEntry)

return {
"orderId": orderID,
Expand All @@ -87,11 +98,18 @@ async def post_checkout(req: CheckoutRequestBodyModel):
"paymentGateway": "stripe",
"clientSecret": payment_intent.client_secret
},
"email": req.email
"email": req.email,
"expiry": int(expiry.timestamp())
}

except ClientError as e:
if e.response['Error']['Code'] == 'ConditionalCheckFailedException':
raise HTTPException(status_code=400, detail="Current quantity cannot be less than 0 and must be available for sale")
else:
raise HTTPException(status_code=500, detail=e)

except Exception as e:
print("Error checking out:", e)
raise HTTPException(status_code=500, detail=e)


Expand Down
14 changes: 12 additions & 2 deletions be/api/v1/endpoints/orders/get.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,18 @@
# gets a single order
async def get_order(order_id: str):
try:
return dal_read_order(order_id)
except Exception:
order = dal_read_order(order_id)
# Censor the email associated with the order.
split = order.customerEmail.split('@')
username = split[0]
if len(username) > 2:
username = username[:2] + '*' * (len(username)-2)
split[0] = username
# order.customerEmail = split.join('@')
order.customerEmail = '@'.join(split)
return order
except Exception as e:
print("Error reading order:", e)
raise HTTPException(status_code=404, detail="Order not found")


Expand Down
34 changes: 34 additions & 0 deletions be/api/v1/endpoints/orders/get_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from unittest.mock import patch
import pytest
from datetime import datetime
from utils.test_app import createTestClient
from be.api.v1.endpoints.orders.get import router

from be.api.v1.models.orders import Order, OrderStatus

client = createTestClient(router)

mock_order_data = Order(
orderID="123",
orderDateTime=datetime.fromisoformat("2023-02-10 10:26:43.387520"),
customerEmail="test@example.com",
transactionID="",
paymentGateway="",
orderItems=[],
status=OrderStatus(1))

expected_api_res = {'customerEmail': 'te**@example.com',
'orderDateTime': '2023-02-10T10:26:43.387520',
'orderID': '123',
'orderItems': [],
'paymentGateway': '',
'status': 1,
'transactionID': ''}


def test_get_order():
with patch("be.api.v1.endpoints.orders.get.dal_read_order", return_value=mock_order_data):
response = client.get("/orders/123")
assert response.status_code == 200
print(response.json())
assert response.json() == expected_api_res
5 changes: 5 additions & 0 deletions be/api/v1/endpoints/payments/intent/post_test.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import pytest
from utils.test_app import createTestClient
from be.api.v1.endpoints.payments.intent.post import router

client = createTestClient(router)


@pytest.mark.skip(
reason="test fails in ci, stripe key is not valid. we should mock the stripe library. DO NOT USE A LIVE OR TEST "
"STRIPE KEY IN CI!")
def test_post_payment_intent():
req_body = {"amount": 200}
response = client.post("/payments/intent", json=req_body)
Expand Down
18 changes: 17 additions & 1 deletion be/api/v1/endpoints/products/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,24 @@
"sggoods_09_451406.jpg?width=1008&impolicy=quality_75")
],
sizes=["xs", "s", "m", "l"],
colorways=["Black, White"],
colorways=["Black", "White"],
productCategory="t-shirt",
sizeChart="https://cdn.ntuscse.com/merch/size-chart/trendlink.png",
stock={
"Black": {
"xs": 5,
"s": 10,
"m": 7,
"l": 12,
},
"White": {
"xs": 5,
"s": 10,
"m": 7,
"l": 12,
},
},
isAvailable=True,
),
# Product(
# id="2",
Expand Down
16 changes: 10 additions & 6 deletions be/api/v1/endpoints/products/get.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,23 @@ class GetProductsResponseModel(BaseModel):
@router.get("", response_model=GetProductsResponseModel)
# gets all products
async def get_products():
# table_name = os.environ.get("PRODUCTS_TABLE_NAME")
products_list = dal_all_read_products()
print('products', products_list)

return {"products": products_list}
try:
# table_name = os.environ.get("PRODUCTS_TABLE_NAME")
products_list = dal_all_read_products()
print('products', products_list)
return {"products": products_list}
except Exception as e:
print("Error reading products:", e)
raise HTTPException(status_code=500, detail="Internal Server Error")


@router.get("/{item_id}", response_model=Product)
# gets a single product
async def get_product(item_id: str):
try:
return dal_read_product(item_id)
except Exception:
except Exception as e:
print("Error reading product:", e)
raise HTTPException(status_code=404, detail="Product not found")


Expand Down
2 changes: 1 addition & 1 deletion be/api/v1/models/cart.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

class CartItem(BaseModel):
productId: str
size: Optional[str]
size: str
quantity: int
colorway: str

Expand Down
Loading

1 comment on commit 4229df7

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCoverMissing
docs.py32320%1–6, 8–9, 11, 14, 17, 20, 24–26, 28, 38, 45, 47–49, 51–53, 56–59, 72–73, 76, 78
docs_generate.py10100%1–5, 7–8, 10, 12–13
root.py770%1–2, 4, 6–8, 11
api/v1/endpoints/cart/checkout
   post.py653250%44–45, 47–48, 50, 52, 54–56, 58, 66–72, 74, 84–85, 87–88, 90–91, 93, 105–107, 109, 111–113
   post_test.py14750%10, 20–22, 33–35
api/v1/endpoints/cart/quotation
   post.py19668%18–19, 21, 23, 28–29
   post_test.py10460%9, 18–20
api/v1/endpoints/orders
   get.py20385%22–24
   get_test.py150100% 
api/v1/endpoints/payments/intent
   post.py17570%18–19, 26, 29–30
   post_test.py10460%12–15
api/v1/endpoints/product_categories
   conftest.py9544%10–14
   delete.py12466%12–14, 17
   delete_test.py261157%16–19, 23, 32–37
   get.py17947%12–19, 21
   get_test.py11372%12–14
   post.py15380%19–21
   post_test.py15660%13–18
   test_utils.py181138%12–13, 19–20, 24–26, 33, 40–41, 45
api/v1/endpoints/products
   data.py20100% 
   get.py25772%19, 21–26
   get_test.py22672%13–15, 19–21
api/v1/endpoints/users
   get.py100100% 
   get_test.py110100% 
api/v1/models
   cart.py150100% 
   order_hold_entry.py100100% 
   orders.py250100% 
   product.py120100% 
api/v1/templates
   non_auth_route.py17194%12
api/v1/utils
   cart_utils.py292031%11–16, 27, 31–33, 55, 57, 65, 68–69, 71–74, 76
TOTAL52019662% 

Please sign in to comment.