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

🤖 Enhancements to Checkout Process and Database Schema #659

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 18 additions & 11 deletions flask/src/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,20 +142,27 @@ def get_inventory(cart):
try:
with sentry_sdk.start_span(op="get_inventory", description="db.connect"):
connection = db.connect()
with sentry_sdk.start_span(op="get_inventory", description="db.query") as span:
inventory = connection.execute(
"SELECT * FROM inventory WHERE productId in %s" % (productIds)

with sentry_sdk.start_span(op="get_inventory.validate_products", description="db.query") as span:
# First verify if products exist
products = connection.execute(
"SELECT id FROM products WHERE id in %s" % (productIds)
).fetchall()
span.set_data("inventory",inventory)

if len(products) != len(productIds):
raise Exception("One or more products not found in catalog")

# Then get inventory with proper join to ensure we get all products
with sentry_sdk.start_span(op="get_inventory.get_inventory", description="db.query") as span:
inventory = connection.execute(
"""SELECT i.*, COALESCE(i.count, 0) as count
FROM products p
LEFT JOIN inventory i ON p.id = i.productId
WHERE p.id in %s""" % (productIds)
).fetchall()
span.set_data("inventory", inventory)
except BrokenPipeError as err:
raise DatabaseConnectionError('get_inventory')
except Exception as err:
err_string = str(err)
if UNPACK_FROM_ERROR in err_string:
raise DatabaseConnectionError('get_inventory')
else:
raise(err)

return inventory


Expand Down
64 changes: 43 additions & 21 deletions flask/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,31 +165,53 @@ def checkout():
form = order["form"]
validate_inventory = True if "validate_inventory" not in order else order["validate_inventory"] == "true"

inventory = []
try:
with sentry_sdk.start_span(op="/checkout.get_inventory", description="function"):
with sentry_sdk.metrics.timing(key="checkout.get_inventory.execution_time"):
inventory = get_inventory(cart)
with sentry_sdk.start_transaction(name="checkout", op="checkout") as transaction:
inventory = []
try:
with sentry_sdk.start_span(op="/checkout.get_inventory", description="function"):
with sentry_sdk.metrics.timing(key="checkout.get_inventory.execution_time"):
inventory = get_inventory(cart)
except Exception as err:
sentry_sdk.metrics.incr(key="checkout.failed", tags={"reason": "inventory_fetch_failed"})
raise err

print("> /checkout inventory", inventory)
print("> validate_inventory", validate_inventory)

with sentry_sdk.start_span(op="process_order", description="function"):
quantities = cart['quantities']
inventory_map = {str(item['productId']): item for item in inventory}

# Validate all quantities against inventory
insufficient_items = []
for product_id, requested_qty in quantities.items():
requested_qty = int(requested_qty)
if product_id not in inventory_map:
insufficient_items.append({"id": product_id, "reason": "not_found"})
continue

available_qty = inventory_map[product_id]['count']
if validate_inventory and (available_qty < requested_qty):
insufficient_items.append({
"id": product_id,
"reason": "insufficient_stock",
"requested": requested_qty,
"available": available_qty
})

if insufficient_items:
sentry_sdk.metrics.incr(key="checkout.failed", tags={"reason": "insufficient_stock"})
return jsonify({
"error": "Inventory check failed",
"details": insufficient_items
}), 400

response = make_response("success")
return response
except Exception as err:
raise (err)

print("> /checkout inventory", inventory)
print("> validate_inventory", validate_inventory)

with sentry_sdk.start_span(op="process_order", description="function"):
quantities = cart['quantities']
for cartItem in quantities:
for inventoryItem in inventory:
print("> inventoryItem.count", inventoryItem['count'])
if (validate_inventory and (inventoryItem.count < quantities[cartItem] or quantities[cartItem] >= inventoryItem.count)):
sentry_sdk.metrics.incr(key="checkout.failed")
raise Exception("Not enough inventory for product")
if len(inventory) == 0 or len(quantities) == 0:
raise Exception("Not enough inventory for product")

response = make_response("success")
return response


@app.route('/success', methods=['GET'])
def success():
Expand Down
48 changes: 48 additions & 0 deletions flask/src/schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
-- Ensure products table has proper constraints
CREATE TABLE IF NOT EXISTS products (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT,
price DECIMAL(10,2) NOT NULL CHECK (price >= 0),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Ensure inventory table has proper constraints and is linked to products
CREATE TABLE IF NOT EXISTS inventory (
id SERIAL PRIMARY KEY,
productId INTEGER NOT NULL REFERENCES products(id),
count INTEGER NOT NULL DEFAULT 0 CHECK (count >= 0),
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique_product UNIQUE (productId)
);

-- Create trigger to auto-create inventory records for new products
CREATE OR REPLACE FUNCTION create_inventory_for_product()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO inventory (productId, count)
VALUES (NEW.id, 0)
ON CONFLICT (productId) DO NOTHING;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- Attach trigger to products table
DROP TRIGGER IF EXISTS ensure_inventory_exists ON products;
CREATE TRIGGER ensure_inventory_exists
AFTER INSERT ON products
FOR EACH ROW
EXECUTE FUNCTION create_inventory_for_product();

-- Create function to fix missing inventory records
CREATE OR REPLACE FUNCTION fix_missing_inventory()
RETURNS void AS $$
BEGIN
INSERT INTO inventory (productId, count)
SELECT id, 0
FROM products p
WHERE NOT EXISTS (
SELECT 1 FROM inventory i WHERE i.productId = p.id
);
END;
$$ LANGUAGE plpgsql;
42 changes: 41 additions & 1 deletion react/src/components/Checkout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { getTag, itemsInCart } from '../utils/utils';
function Checkout({ backend, rageclick, checkout_success, cart }) {
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
let initialFormValues;
let se = sessionStorage.getItem('se');
const seTdaPrefixRegex = /[^-]+-tda-[^-]+-/;
Expand Down Expand Up @@ -114,8 +115,37 @@ function Checkout({ backend, rageclick, checkout_success, cart }) {
setLoading(true);

try {
await checkout(cart);
const response = await fetch(backend + '/checkout?v2=true', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
cart: cart,
form: form,
validate_inventory: checkout_success ? "false" : "true",
}),
});

if (!response.ok) {
const errorData = await response.json();

if (errorData.details) {
const errorMessage = errorData.details.map(item => {
if (item.reason === 'not_found') {
return `Product ${item.id} is no longer available`;
} else if (item.reason === 'insufficient_stock') {
return `Only ${item.available} units available for product ${item.id}, but ${item.requested} were requested`;
}
return `Issue with product ${item.id}`;
}).join('\n');

setError(errorMessage);
throw new Error(errorMessage);
} else {
throw new Error(errorData.error || 'Checkout failed');
}
}
} catch (error) {
setError(error.message || 'An unexpected error occurred');
Sentry.captureException(error);
hadError = true;
}
Expand All @@ -128,9 +158,19 @@ function Checkout({ backend, rageclick, checkout_success, cart }) {
}
})
}

const clearError = () => {
setError(null);
};

return (
<div className="checkout-container">
{error && (
<div className="error-message" role="alert" onClick={clearError}>
<p>{error}</p>
<button className="dismiss-error">Dismiss</button>
</div>
)}
{loading ? (
<Loader
type="ThreeDots"
Expand Down
Loading