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),
    }