Order Processing Lambda - sastry-25/unboard GitHub Wiki
import json
import urllib.request
import random
API_URL = "https://------.execute-api.us-east-2.amazonaws.com/dev/inventory-management/inventory/items/"
def lambda_handler(event, context):
order_info = {}
# Parse request body
try:
body = json.loads(event['body'])
except Exception as e:
return {
'statusCode': 400,
'headers': {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'POST,OPTIONS'
},
'body': json.dumps({'message': 'Invalid request body'})
}
# Validate required fields exist
if 'items' not in body or 'shipping' not in body or 'payment' not in body:
return {
'statusCode': 400,
'headers': {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'POST,OPTIONS'
},
'body': json.dumps({'message': 'Missing required fields: items, shipping, or payment'})
}
# Process order items - check inventory availability
try:
order_info['items'] = []
for item in body['items']:
r = urllib.request.urlopen(urllib.request.Request(
url=API_URL + str(item['id']),
method='GET'
))
data = json.loads(r.read())
# Check if sufficient quantity available
if data['quantity'] < item['quantity']:
return {
'statusCode': 400,
'headers': {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'POST,OPTIONS'
},
'body': json.dumps({
'message': 'Item ' + str(item['id']) + ' not in stock',
'availableQuantity': data['quantity']
})
}
# Add item details to order
order_info['items'].append({
'id': item['id'],
'name': data.get('name', 'Unknown'),
'quantity': item['quantity'],
'price': data.get('price', 0)
})
except urllib.error.HTTPError as e:
return {
'statusCode': 404,
'headers': {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'POST,OPTIONS'
},
'body': json.dumps({'message': 'Item not found'})
}
except Exception as e:
print(e)
return {
'statusCode': 500,
'headers': {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'POST,OPTIONS'
},
'body': json.dumps({'message': 'Error processing order items'})
}
# Process order shipping
try:
order_info['shipping'] = {
'name': body['shipping']['name'],
'address': body['shipping']['address'],
'city': body['shipping']['city'],
'state': body['shipping']['state'],
'zip': body['shipping']['zip']
}
except KeyError as e:
return {
'statusCode': 400,
'headers': {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'POST,OPTIONS'
},
'body': json.dumps({'message': f'Missing shipping field: {str(e)}'})
}
# Process order payment
try:
order_info['payment'] = {
'cardNumber': '****' + body['payment']['cardNumber'][-4:], # Only last 4 digits
'cardHolder': body['payment']['cardHolder'],
'expirationDate': body['payment']['expirationDate']
}
except KeyError as e:
return {
'statusCode': 400,
'headers': {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'POST,OPTIONS'
},
'body': json.dumps({'message': f'Missing payment field: {str(e)}'})
}
# Generate confirmation number
confirmation_number = f"ORD-{random.randint(100000, 999999)}"
# Calculate order total
total = sum(item['quantity'] * item['price'] for item in order_info['items'])
# Return success response
return {
'statusCode': 200,
'headers': {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'POST,OPTIONS'
},
'body': json.dumps({
'confirmationNumber': confirmation_number,
'message': 'Order processed successfully',
'orderTotal': round(total, 2),
'orderDetails': order_info
})
}
DB implementation
import json
import urllib.request
import urllib.error
import pymysql
import pymysql.cursors
import os
import datetime
DB_HOST = os.environ["DB_HOST"]
DB_USER = os.environ["DB_USER"]
DB_PASSWORD = os.environ["DB_PASSWORD"]
DB_SCHEMA = os.environ["DB_SCHEMA"]
INVENTORY_API_BASE = os.environ.get("INVENTORY_API_BASE", "")
SQL_INSERT_SHIPPING = """
INSERT INTO SHIPPING_INFO (ADDRESS1, ADDRESS2, CITY, STATE, COUNTRY, POSTAL_CODE, EMAIL)
VALUES (%s, %s, %s, %s, %s, %s, %s)
"""
SQL_INSERT_PAYMENT = """
INSERT INTO PAYMENT_INFO (HOLDER_NAME, CARD_NUM, EXP_DATE, CVV)
VALUES (%s, %s, %s, %s)
"""
SQL_INSERT_ORDER = """
INSERT INTO CUSTOMER_ORDER (CUSTOMER_NAME, CUSTOMER_EMAIL, SHIPPING_INFO_ID_FK, PAYMENT_INFO_ID_FK, STATUS)
VALUES (%s, %s, %s, %s, 'New')
"""
SQL_INSERT_LINE_ITEM = """
INSERT INTO CUSTOMER_ORDER_LINE_ITEM (ITEM_NUMBER, ITEM_NAME, QUANTITY, CUSTOMER_ORDER_ID_FK)
VALUES (%s, %s, %s, %s)
"""
SQL_UPDATE_STOCK = """
UPDATE ITEM
SET AVAILABLE_QUANTITY = AVAILABLE_QUANTITY - %s
WHERE ITEM_NUMBER = %s
"""
PATH_ORDER = "/order-processing/order"
def lambda_handler(event, context):
if isinstance(event, dict) and "requestContext" in event:
path = event.get("requestContext", {}).get("resourcePath") or event.get("rawPath", "")
method = (
event.get("requestContext", {}).get("httpMethod")
or event.get("requestContext", {}).get("http", {}).get("method", "")
)
body_raw = event.get("body")
try:
body = json.loads(body_raw) if isinstance(body_raw, str) else body_raw
except Exception:
body = {}
else:
print("INFO: Direct JSON event detected — using default path/method")
path = PATH_ORDER
method = "POST"
body = event
if path != PATH_ORDER or method != "POST":
return _response(400, {"error": "Request not recognized", "path": path, "method": method}, "POST")
if not isinstance(body, dict):
return _response(400, {"message": "Invalid request body"}, "POST")
required = ["items", "shipping", "payment"]
if not all(k in body for k in required):
return _response(400, {"message": "Missing required fields: items, shipping, or payment"}, "POST")
order_items = body["items"]
shipping = body["shipping"]
payment = body["payment"]
conn = None
try:
conn = pymysql.connect(
host=DB_HOST,
user=DB_USER,
password=DB_PASSWORD,
db=DB_SCHEMA,
cursorclass=pymysql.cursors.DictCursor,
autocommit=False,
)
validated_items = []
insufficient_stock = []
total = 0.0
for item in order_items:
try:
url = f"{INVENTORY_API_BASE}/inventory-management/inventory/items/{item['id']}"
r = urllib.request.urlopen(urllib.request.Request(url=url, method="GET"))
data = json.loads(r.read())
available_qty = data["AVAILABLE_QUANTITY"]
requested_qty = item["quantity"]
if available_qty < requested_qty:
insufficient_stock.append({
"itemId": item["id"],
"itemName": data["NAME"],
"requested": requested_qty,
"available": available_qty,
})
else:
validated_items.append({
"ITEM_NUMBER": data["ITEM_NUMBER"],
"ITEM_NAME": data["NAME"],
"QUANTITY": requested_qty,
"PRICE": data["UNIT_PRICE"],
})
total += requested_qty * data["UNIT_PRICE"]
except urllib.error.HTTPError:
return _response(404, {"message": f"Item {item['id']} not found"}, "POST")
except Exception as e:
return _response(500, {"message": "Error checking inventory", "error": str(e)}, "POST")
if insufficient_stock:
return _response(
400,
{
"message": "Insufficient stock to fulfill your order.",
"items": insufficient_stock,
},
"POST",
)
# Shipping info
with conn.cursor() as cursor:
cursor.execute(
SQL_INSERT_SHIPPING,
(
shipping.get("address1", ""),
shipping.get("address2", ""),
shipping.get("city", ""),
shipping.get("state", ""),
shipping.get("country", ""),
shipping.get("postalCode", ""),
shipping.get("email", ""),
),
)
shipping_id = cursor.lastrowid
# Payment info
with conn.cursor() as cursor:
cursor.execute(
SQL_INSERT_PAYMENT,
(
payment.get("cardHolder", ""),
payment.get("cardNumber", ""),
payment.get("expirationDate", ""),
payment.get("cvv", ""),
),
)
payment_id = cursor.lastrowid
# Order
with conn.cursor() as cursor:
cursor.execute(
SQL_INSERT_ORDER,
(
body.get("customerName", "Guest"),
body.get("customerEmail", shipping.get("email", "")),
shipping_id,
payment_id,
),
)
order_id = cursor.lastrowid
# Line items + stock updates
with conn.cursor() as cursor:
for item in validated_items:
cursor.execute(
SQL_INSERT_LINE_ITEM,
(item["ITEM_NUMBER"], item["ITEM_NAME"], item["QUANTITY"], order_id),
)
cursor.execute(SQL_UPDATE_STOCK, (item["QUANTITY"], item["ITEM_NUMBER"]))
conn.commit()
confirmation_number = f"ORD-{order_id:06d}"
return _response(
200,
{
"confirmationNumber": confirmation_number,
"message": "Order processed successfully",
"orderTotal": round(total, 2),
"orderId": order_id,
"shippingId": shipping_id,
"paymentId": payment_id,
},
"POST",
)
except Exception as e:
if conn:
conn.rollback()
return _response(500, {"error": "Internal server error", "message": str(e)}, "POST")
finally:
if conn and conn.open:
conn.close()
def _response(status, body_dict, method):
return {
"statusCode": status,
"headers": {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type",
"Access-Control-Allow-Methods": f"{method},OPTIONS",
},
"body": json.dumps(body_dict),
}
After adding shipping and payment lambdas
import json
import urllib.request
import urllib.error
import pymysql
import pymysql.cursors
import os
import datetime
import boto3
DB_HOST = os.environ["DB_HOST"]
DB_USER = os.environ["DB_USER"]
DB_PASSWORD = os.environ["DB_PASSWORD"]
DB_SCHEMA = os.environ["DB_SCHEMA"]
INVENTORY_API_BASE = os.environ.get("INVENTORY_API_BASE", "")
PAYMENT_API_BASE = os.environ.get("PAYMENT_API_BASE", "")
EVENT_BUS_NAME = "default"
BUSINESS_REGISTRATION_ID = "UNBOARD-001"
PER_ITEM_WEIGHT = 1.0
SQL_INSERT_ORDER = """
INSERT INTO CUSTOMER_ORDER (CUSTOMER_NAME, CUSTOMER_EMAIL, PAYMENT_TOKEN, ORDER_TOTAL, STATUS)
VALUES (%s, %s, %s, %s, 'New')
"""
SQL_INSERT_LINE_ITEM = """
INSERT INTO CUSTOMER_ORDER_LINE_ITEM (ITEM_NUMBER, ITEM_NAME, QUANTITY, CUSTOMER_ORDER_ID_FK)
VALUES (%s, %s, %s, %s)
"""
SQL_UPDATE_STOCK = """
UPDATE ITEM
SET AVAILABLE_QUANTITY = AVAILABLE_QUANTITY - %s
WHERE ITEM_NUMBER = %s
"""
PATH_ORDER = "/order-processing/order"
events_client = boto3.client("events")
def lambda_handler(event, context):
if isinstance(event, dict) and "requestContext" in event:
path = event.get("requestContext", {}).get("resourcePath") or event.get("rawPath", "")
method = (
event.get("requestContext", {}).get("httpMethod")
or event.get("requestContext", {}).get("http", {}).get("method", "")
)
body_raw = event.get("body")
try:
body = json.loads(body_raw) if isinstance(body_raw, str) else body_raw
except Exception:
body = {}
else:
print("INFO: Direct JSON event detected — using default path/method")
path = PATH_ORDER
method = "POST"
body = event
if path != PATH_ORDER or method != "POST":
return _response(400, {"error": "Request not recognized", "path": path, "method": method}, "POST")
if not isinstance(body, dict):
return _response(400, {"message": "Invalid request body"}, "POST")
required = ["items", "shipping", "payment"]
if not all(k in body for k in required):
return _response(400, {"message": "Missing required fields: items, shipping, or payment"}, "POST")
order_items = body["items"]
shipping = body["shipping"]
payment = body["payment"]
if not PAYMENT_API_BASE:
return _response(500, {"error": "PAYMENT_API_BASE is not configured"}, "POST")
if not INVENTORY_API_BASE:
return _response(500, {"error": "INVENTORY_API_BASE is not configured"}, "POST")
conn = None
try:
conn = pymysql.connect(
host=DB_HOST,
user=DB_USER,
password=DB_PASSWORD,
db=DB_SCHEMA,
cursorclass=pymysql.cursors.DictCursor,
autocommit=False,
)
validated_items = []
insufficient_stock = []
total = 0.0
for item in order_items:
try:
url = f"{INVENTORY_API_BASE}/inventory-management/inventory/items/{item['id']}"
r = urllib.request.urlopen(urllib.request.Request(url=url, method="GET"))
data = json.loads(r.read())
available_qty = data["AVAILABLE_QUANTITY"]
requested_qty = item["quantity"]
if available_qty < requested_qty:
insufficient_stock.append({
"itemId": item["id"],
"itemName": data["NAME"],
"requested": requested_qty,
"available": available_qty,
})
else:
validated_items.append({
"ITEM_NUMBER": data["ITEM_NUMBER"],
"ITEM_NAME": data["NAME"],
"QUANTITY": requested_qty,
"PRICE": data["UNIT_PRICE"],
})
total += requested_qty * data["UNIT_PRICE"]
except urllib.error.HTTPError:
return _response(404, {"message": f"Item {item['id']} not found"}, "POST")
except Exception as e:
return _response(500, {"message": "Error checking inventory", "error": str(e)}, "POST")
if insufficient_stock:
return _response(
400,
{
"message": "Insufficient stock to fulfill your order.",
"items": insufficient_stock,
},
"POST",
)
payment_token = _process_payment(payment, total)
if not payment_token:
return _response(502, {"message": "Payment processing failed"}, "POST")
with conn.cursor() as cursor:
cursor.execute(
SQL_INSERT_ORDER,
(
body.get("customerName", "Guest"),
body.get("customerEmail", shipping.get("email", "")),
payment_token,
round(total, 2),
),
)
order_id = cursor.lastrowid
with conn.cursor() as cursor:
for item in validated_items:
cursor.execute(
SQL_INSERT_LINE_ITEM,
(item["ITEM_NUMBER"], item["ITEM_NAME"], item["QUANTITY"], order_id),
)
cursor.execute(SQL_UPDATE_STOCK, (item["QUANTITY"], item["ITEM_NUMBER"]))
conn.commit()
confirmation_number = f"ORD-{order_id:06d}"
try:
_publish_shipping_event(order_id, shipping, order_items)
except Exception as e:
print(f"WARNING: Failed to publish shipping event for order {order_id}: {e}")
return _response(
200,
{
"confirmationNumber": confirmation_number,
"message": "Order processed successfully",
"orderTotal": round(total, 2),
"orderId": order_id,
"paymentToken": payment_token,
},
"POST",
)
except Exception as e:
if conn:
conn.rollback()
return _response(500, {"error": "Internal server error", "message": str(e)}, "POST")
finally:
if conn and conn.open:
conn.close()
def _process_payment(payment, amount):
try:
url = f"{PAYMENT_API_BASE}/payment"
payload = {
"amount": round(amount, 2),
"cardHolder": payment.get("cardHolder", ""),
"cardNumber": payment.get("cardNumber", ""),
"expirationDate": payment.get("expirationDate", ""),
"cvv": payment.get("cvv", ""),
}
data_bytes = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(
url=url,
method="POST",
data=data_bytes,
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(req) as r:
raw = r.read()
resp_json = json.loads(raw)
if isinstance(resp_json, dict) and "body" in resp_json:
try:
inner = json.loads(resp_json["body"])
return inner.get("paymentToken")
except:
return None
return resp_json.get("paymentToken")
except urllib.error.HTTPError as e:
print(f"ERROR: Payment HTTPError: {e.code} {e.reason}")
return None
except Exception as e:
print(f"ERROR: Payment exception: {e}")
return None
def _publish_shipping_event(order_id, shipping, order_items):
total_qty = sum(item.get("quantity", 0) for item in order_items)
packets = len(order_items) if order_items else 0
if packets <= 0:
print(f"INFO: No order items for order {order_id}, skipping shipping event")
return
total_weight = total_qty * PER_ITEM_WEIGHT
weight_each = total_weight / packets if packets > 0 else 0.0
detail = {
"orderId": order_id,
"registrationId": BUSINESS_REGISTRATION_ID,
"address": {
"address1": shipping.get("address1", ""),
"address2": shipping.get("address2", ""),
"city": shipping.get("city", ""),
"state": shipping.get("state", ""),
"country": shipping.get("country", ""),
"postalCode": shipping.get("postalCode", ""),
},
"packets": packets,
"weightEach": weight_each,
}
events_client.put_events(
Entries=[
{
"Source": "order.processing",
"DetailType": "OrderCreated",
"Detail": json.dumps(detail),
"EventBusName": EVENT_BUS_NAME,
}
]
)
def _response(status, body_dict, method):
return {
"statusCode": status,
"headers": {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type",
"Access-Control-Allow-Methods": f"{method},OPTIONS",
},
"body": json.dumps(body_dict),
}