Skip to content

Commit 4229df7

Browse files
authored
Merge branch 'main' into update-cd-dev
2 parents 68c501d + ac81097 commit 4229df7

File tree

30 files changed

+1565
-3378
lines changed

30 files changed

+1565
-3378
lines changed

.coveragerc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[run]
2+
omit = **/__init__.py

.env.dev

Lines changed: 0 additions & 1 deletion
This file was deleted.

.env.test

Lines changed: 0 additions & 14 deletions
This file was deleted.

.github/workflows/cd-prod.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,4 @@ jobs:
5353
5454
- name: Deploy to AWS
5555
run: |
56-
npm run deploy -- --stage production --aws-profile github_actions
56+
npm run deploy -- --stage prod --aws-profile github_actions

.github/workflows/ci.yml

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
- name: Setup Node
2323
uses: actions/setup-node@v2
2424
with:
25-
node-version: "14"
25+
node-version: "16"
2626

2727
- name: Setup Python
2828
uses: actions/setup-python@v2
@@ -38,17 +38,48 @@ jobs:
3838
virtualenvs-path: ./poetry_virtualenv
3939
installer-parallel: true
4040

41+
- name: Create dummy .env file for tests
42+
run: |
43+
touch .env
44+
echo "
45+
STRIPE_SECRET_KEY=key
46+
FRONTEND_HOST='http://localhost:3000'
47+
PRODUCTS_TABLE_NAME=testing-products
48+
PRODUCT_CATEGORIES_TABLE_NAME=testing-products-categories
49+
ORDER_HOLD_TABLE_NAME=testing-order-hold
50+
" >> .env
51+
4152
- name: Setup aws dummy credentials
4253
run: |
4354
mkdir ~/.aws
4455
touch ~/.aws/credentials
4556
4657
- name: Install dependencies
4758
run: npm run setup
48-
59+
4960
- name: Pytest
50-
run: npm run test
51-
61+
run: npm run test:py # TODO: change this to `npm run test`
62+
63+
env:
64+
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
65+
IS_OFFLINE: "true"
66+
AWS_ACCESS_KEY_ID: 'testing'
67+
AWS_SECRET_ACCESS_KEY: 'testing'
68+
AWS_SECURITY_TOKEN: 'testing'
69+
AWS_SESSION_TOKEN: 'testing'
70+
AWS_DEFAULT_REGION: 'ap-southeast-1'
71+
72+
PRODUCT_CATEGORIES_TABLE_NAME: 'be-dev-product_categories'
73+
PRODUCTS_TABLE_NAME: 'be-dev-products'
74+
FRONTEND_HOST: 'https://dev.merch.ntuscse.com'
75+
76+
BASE_API_SERVER_URL: 'https://api.dev.ntuscse.com'
77+
78+
- name: Pytest coverage comment
79+
uses: MishaKav/pytest-coverage-comment@main
80+
with:
81+
pytest-xml-coverage-path: ./coverage.xml
82+
5283
lint:
5384
runs-on: ubuntu-latest
5485
steps:
@@ -63,9 +94,20 @@ jobs:
6394
- name: Setup Python
6495
uses: actions/setup-python@v2
6596
with:
66-
python-version: "3.9"
97+
python-version: "3.9.16"
6798
architecture: "x64"
6899

100+
- name: Create dummy .env file for lint
101+
run: |
102+
touch .env
103+
echo "
104+
STRIPE_SECRET_KEY=key
105+
FRONTEND_HOST='http://localhost:3000'
106+
PRODUCTS_TABLE_NAME=testing-products
107+
PRODUCT_CATEGORIES_TABLE_NAME=testing-products-categories
108+
ORDER_HOLD_TABLE_NAME=testing-order-hold
109+
" >> .env
110+
69111
- name: Install and configure Poetry
70112
uses: snok/install-poetry@v1
71113
with:

.gitignore

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,20 @@ node_modules/
88
# python local
99
.dynamodb/
1010
.env
11+
.env.dev
1112

1213
# generated
1314
out/
1415
docs_server/swagger/openapi.json
16+
17+
#env vars
18+
.env.*
19+
20+
# pytest
21+
/.pytest_cache/
22+
23+
# test coverage
24+
/.coverage
25+
/coverage_html/
26+
/coverage.lcov
27+
/coverage.xml

.sample.env

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
STRIPE_SECRET_KEY='<redacted>'
2+
FRONTEND_HOST='http://localhost:3000'
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import boto3
2+
from datetime import datetime
3+
import os
4+
from utils.aws.dynamodb import dynamodb
5+
import sys
6+
7+
8+
table_name = os.environ["ORDER_HOLD_TABLE_NAME"]
9+
10+
def handler(event, context):
11+
table = dynamodb.Table(table_name)
12+
13+
# Get the current epoch time
14+
now = int(datetime.now().timestamp())
15+
16+
# Scan the table to find all expired documents
17+
result = table.scan(
18+
FilterExpression="expiry < :now",
19+
ExpressionAttributeValues={":now": now}
20+
)
21+
22+
# Delete the expired documents
23+
with table.batch_writer() as batch:
24+
for item in result["Items"]:
25+
batch.delete_item(Key={"transactionID": item["transactionID"]})
26+
27+
return dict()

be/api/v1/endpoints/cart/checkout/post.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,20 @@
22
import uuid
33
from fastapi import APIRouter, HTTPException
44
from pydantic import BaseModel
5-
from datetime import datetime
5+
from botocore.exceptions import ClientError
6+
from datetime import datetime, timedelta
67
import stripe
78
from be.api.v1.templates.non_auth_route import create_non_auth_router
89
from be.api.v1.models.cart import PriceModel, Cart
10+
from be.api.v1.models.order_hold_entry import OrderHoldEntry, ReservedProduct
911
from be.api.v1.models.orders import OrderItem, Order, OrderStatus
1012
from be.api.v1.utils.cart_utils import calc_cart_value, describe_cart, generate_order_items_from_cart
1113
from utils.dal.order import dal_create_order
14+
from utils.dal.products import dal_increment_stock_count
15+
from utils.dal.order_hold import dal_create_order_hold_entry
1216

1317
stripe.api_key = os.environ["STRIPE_SECRET_KEY"]
18+
DEFAULT_ORDER_EXPIRY_TIME = 1
1419

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

@@ -30,6 +35,7 @@ class PostCheckoutResponseModel(BaseModel):
3035
items: list[OrderItem]
3136
price: PriceModel
3237
payment: PaymentModel
38+
expiry: int
3339

3440

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

54+
orderID = uuid.uuid4().__str__()
4855
price = calc_cart_value(items_products)
49-
description = describe_cart(items_products)
50-
51-
# todo: create "pending" order here - in db
56+
description = describe_cart(items_products, orderID)
5257

5358
payment_intent = stripe.PaymentIntent.create(
5459
payment_method_types=["paynow"],
@@ -58,14 +63,13 @@ async def post_checkout(req: CheckoutRequestBodyModel):
5863
receipt_email=req.email,
5964
description=f"SCSE Merch Purchase:\n{description}"
6065
)
61-
62-
orderID = uuid.uuid4().__str__()
6366
orderDateTime = datetime.now().__str__()
6467
customerEmail = req.email
6568
transactionID = payment_intent.id
6669
paymentPlatform = "stripe"
6770
orderItems = items_products
6871
status = OrderStatus.PENDING_PAYMENT
72+
expiry = datetime.now() + timedelta(hours=int(os.environ.get("ORDER_EXPIRY_TIME", DEFAULT_ORDER_EXPIRY_TIME)))
6973

7074
order = Order(
7175
orderID = orderID,
@@ -77,7 +81,14 @@ async def post_checkout(req: CheckoutRequestBodyModel):
7781
status = status
7882
)
7983

84+
for orderItem in orderItems:
85+
dal_increment_stock_count(orderItem.id, -orderItem.quantity, orderItem.size, orderItem.colorway)
86+
87+
reservedProducts = [ReservedProduct(productID=item.productId, qty=item.quantity) for item in req.items]
88+
orderHoldEntry = OrderHoldEntry(transactionID=transactionID, expiry=int(expiry.timestamp()), reservedProducts=reservedProducts)
89+
8090
dal_create_order(order)
91+
dal_create_order_hold_entry(orderHoldEntry)
8192

8293
return {
8394
"orderId": orderID,
@@ -87,11 +98,18 @@ async def post_checkout(req: CheckoutRequestBodyModel):
8798
"paymentGateway": "stripe",
8899
"clientSecret": payment_intent.client_secret
89100
},
90-
"email": req.email
101+
"email": req.email,
102+
"expiry": int(expiry.timestamp())
91103
}
92104

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

94111
except Exception as e:
112+
print("Error checking out:", e)
95113
raise HTTPException(status_code=500, detail=e)
96114

97115

be/api/v1/endpoints/orders/get.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,18 @@
99
# gets a single order
1010
async def get_order(order_id: str):
1111
try:
12-
return dal_read_order(order_id)
13-
except Exception:
12+
order = dal_read_order(order_id)
13+
# Censor the email associated with the order.
14+
split = order.customerEmail.split('@')
15+
username = split[0]
16+
if len(username) > 2:
17+
username = username[:2] + '*' * (len(username)-2)
18+
split[0] = username
19+
# order.customerEmail = split.join('@')
20+
order.customerEmail = '@'.join(split)
21+
return order
22+
except Exception as e:
23+
print("Error reading order:", e)
1424
raise HTTPException(status_code=404, detail="Order not found")
1525

1626

0 commit comments

Comments
 (0)