Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[release] Add stock counts, fix bugs #71

Open
wants to merge 35 commits into
base: production
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
68c501d
Update cd-dev.yml
aryans1204 Jan 31, 2023
d768e1e
Added stock count feature
ykcyberarts Feb 5, 2023
7f52fcc
Merge branch 'main' into feature/stock-count
ykcyberarts Feb 5, 2023
33e0287
Fixed tests
ykcyberarts Feb 5, 2023
b3a980b
Validate qty of products
ykcyberarts Feb 5, 2023
7857c54
Change to Union
ykcyberarts Feb 5, 2023
94d3a05
Consider stocks with different sizes and colorway
ykcyberarts Feb 12, 2023
0637386
Merge branch 'main' into feature/stock-count
ykcyberarts Feb 13, 2023
f2a2272
Fix minor bug
ykcyberarts Feb 13, 2023
7c8b9e3
Fix minor bugs
ykcyberarts Feb 13, 2023
80229e8
Merge pull request #53 from ntuscse/feature/stock-count
RealDyllon Feb 18, 2023
78eebb1
feat: Add size chart to products endpoint
chanbakjsd Feb 18, 2023
a74ffca
fix: Regenerate poetry.lock
chanbakjsd Feb 19, 2023
e55333e
fix: Add error reporting for GetProducts endpoint
chanbakjsd Feb 19, 2023
1b0d72b
fix: Use stock instead of current_qty
chanbakjsd Feb 20, 2023
7075ffa
fix: Copy dict on read product
chanbakjsd Feb 20, 2023
a82ce42
fix: Use boto3 deserializer
chanbakjsd Feb 20, 2023
5a99682
Fixed get products endpoint
ykcyberarts Feb 20, 2023
88af57a
Merge pull request #73 from ntuscse/fix/products
RealDyllon Feb 20, 2023
d667fa2
Fix bug
ykcyberarts Feb 20, 2023
a07b225
Merge branch 'main' into fix/products
yankai14 Feb 20, 2023
18fd301
Merge pull request #74 from ntuscse/fix/products
RealDyllon Feb 20, 2023
36e7e6c
Fix dal_read_product
ykcyberarts Feb 20, 2023
6430715
add ci .env file generation
RealDyllon Feb 20, 2023
2999ca4
add ORDER_HOLD_TABLE_NAME to ci .env
RealDyllon Feb 20, 2023
804ebcf
skip failing test
RealDyllon Feb 20, 2023
89bbc0d
remove dynamodb offline from ci
RealDyllon Feb 20, 2023
1be4fe1
fix lint job
RealDyllon Feb 20, 2023
6f85409
remove serverless offline dynamodb
RealDyllon Feb 23, 2023
5268693
add coverage
RealDyllon Feb 23, 2023
0ae250e
add coverage to ci
RealDyllon Feb 23, 2023
0c83c24
bump ci test node version
RealDyllon Feb 23, 2023
ac81097
Merge pull request #72 from ntuscse/task/SCSE-210
RealDyllon Feb 23, 2023
4229df7
Merge branch 'main' into update-cd-dev
RealDyllon Feb 23, 2023
a8f8b79
Merge pull request #50 from ntuscse/update-cd-dev
RealDyllon Feb 23, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
2 changes: 1 addition & 1 deletion .github/workflows/cd-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,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 Down
33 changes: 30 additions & 3 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,6 +38,17 @@ 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
Expand All @@ -47,7 +58,7 @@ jobs:
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 }}
Expand All @@ -63,6 +74,11 @@ jobs:
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
Expand All @@ -78,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
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,15 @@ node_modules/
# generated
out/
docs_server/swagger/openapi.json

#env vars
.env.*

# pytest
/.pytest_cache/

# test coverage
/.coverage
/coverage_html/
/coverage.lcov
/coverage.xml
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()
26 changes: 22 additions & 4 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 @@ -49,8 +55,6 @@ async def post_checkout(req: CheckoutRequestBodyModel):
price = calc_cart_value(items_products)
description = describe_cart(items_products, orderID)

# todo: create "pending" order here - in db

payment_intent = stripe.PaymentIntent.create(
payment_method_types=["paynow"],
payment_method_data={"type": "paynow"},
Expand All @@ -65,6 +69,7 @@ async def post_checkout(req: CheckoutRequestBodyModel):
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 @@ -76,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 @@ -86,9 +98,15 @@ 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)
Expand Down
13 changes: 7 additions & 6 deletions be/api/v1/endpoints/orders/get_test.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from unittest.mock import patch
import pytest
from datetime import datetime
from utils.test_app import createTestClient
Expand Down Expand Up @@ -25,9 +26,9 @@
'transactionID': ''}


def test_get_order(mocker):
mocker.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
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
19 changes: 16 additions & 3 deletions be/api/v1/endpoints/products/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,22 @@
sizes=["xs", "s", "m", "l"],
colorways=["Black", "White"],
productCategory="t-shirt",
stock=[
[0, 1, 2, 3], [1, 2, 3, 4],
],
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
13 changes: 13 additions & 0 deletions be/api/v1/models/order_hold_entry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from pydantic import BaseModel
from datetime import datetime
from typing import List

class ReservedProduct(BaseModel):
productID: str
qty: int


class OrderHoldEntry(BaseModel):
transactionID: str
expiry: int
reservedProducts: List[ReservedProduct]
3 changes: 2 additions & 1 deletion be/api/v1/models/product.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ class Product(BaseModel):
price: int
images: list[str]
sizes: list[str]
sizeChart: str
colorways: list[str]
productCategory: str # todo: change to productCat enum model
stock: dict[str, dict[str, int]]
isAvailable: bool = True
stock: list[list[int]]
1 change: 1 addition & 0 deletions be/api/v1/utils/cart_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from be.api.v1.models.orders import OrderItem
from be.api.v1.models.cart import Cart, PriceModel
from utils.dal.products import dal_all_read_products, dal_read_product
from typing import List


frontend_url = os.environ["FRONTEND_HOST"]
Expand Down
Loading