Skip to content

Commit

Permalink
feat: implement order items model and update order confirmation email…
Browse files Browse the repository at this point in the history
… template
  • Loading branch information
hackerman70000 committed Jan 12, 2025
1 parent 5badbdf commit a6e9a21
Show file tree
Hide file tree
Showing 7 changed files with 196 additions and 127 deletions.
17 changes: 15 additions & 2 deletions backend/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,27 @@ class Cart(db.Model):

class Order(db.Model):
__tablename__ = "orders"

id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
payment_intent_id = db.Column(db.String(255), unique=True, nullable=False)
amount = db.Column(db.Float, nullable=False)
status = db.Column(db.String(50), nullable=False)
items = db.Column(db.Text)
error_message = db.Column(db.Text)
created_at = db.Column(db.DateTime, nullable=False)

user = db.relationship("User", backref=db.backref("orders", lazy=True))
items = db.relationship(
"OrderItem", backref="order", lazy=True, cascade="all, delete-orphan"
)


class OrderItem(db.Model):
__tablename__ = "order_items"
id = db.Column(db.Integer, primary_key=True)
order_id = db.Column(db.Integer, db.ForeignKey("orders.id"), nullable=False)
product_id = db.Column(db.Integer, db.ForeignKey("products.id"), nullable=False)
quantity = db.Column(db.Integer, nullable=False)
unit_price = db.Column(db.Float, nullable=False)
total = db.Column(db.Float, nullable=False)

product = db.relationship("Product", backref=db.backref("order_items", lazy=True))
102 changes: 58 additions & 44 deletions backend/app/routes/payment.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from flask import Blueprint, jsonify, request, current_app
from app.models import Cart, Order, Product, db
import uuid
from datetime import datetime

from flask import Blueprint, current_app, jsonify

from app.models import Cart, Order, OrderItem, Product, db
from app.routes.auth import token_required
from app.utils import send_order_confirmation_email
from datetime import datetime
import json
import uuid

payment_bp = Blueprint("payment", __name__)

Expand All @@ -16,17 +17,6 @@ def generate_order_number():
return f"PH-{timestamp}-{unique_id}"


def parse_order_items(items_str):
"""Safely parse order items JSON string"""
try:
if not items_str:
return []
return json.loads(items_str)
except json.JSONDecodeError:
current_app.logger.error(f"Failed to parse order items: {items_str}")
return []


@payment_bp.route("/checkout", methods=["POST"])
@token_required
def checkout(current_user):
Expand All @@ -38,7 +28,7 @@ def checkout(current_user):
return jsonify({"success": False, "message": "Cart is empty"}), 400

total_amount = 0
items_data = []
order_items = []

for cart_item in cart_items:
product = Product.query.get(cart_item.product_id)
Expand All @@ -53,24 +43,22 @@ def checkout(current_user):
item_total = product.price * cart_item.quantity
total_amount += item_total

items_data.append(
{
"product_id": product.id,
"name": product.name,
"image_url": product.image_url,
"quantity": cart_item.quantity,
"unit_price": product.price,
"total": item_total,
}
order_items.append(
OrderItem(
product_id=product.id,
quantity=cart_item.quantity,
unit_price=product.price,
total=item_total,
)
)

order = Order(
user_id=current_user.id,
payment_intent_id=generate_order_number(),
amount=total_amount,
status="completed",
items=json.dumps(items_data),
created_at=datetime.utcnow(),
items=order_items,
)

Cart.query.filter_by(user_id=current_user.id).delete()
Expand All @@ -89,7 +77,17 @@ def checkout(current_user):
"id": order.id,
"order_number": order.payment_intent_id,
"amount": order.amount,
"items": items_data,
"items": [
{
"product_id": item.product_id,
"name": item.product.name,
"image_url": item.product.image_url,
"quantity": item.quantity,
"unit_price": item.unit_price,
"total": item.total,
}
for item in order.items
],
"created_at": order.created_at.isoformat(),
},
}
Expand All @@ -114,19 +112,27 @@ def get_orders(current_user):
.all()
)

orders_data = []
for order in orders:
items = parse_order_items(order.items)
orders_data.append(
{
"id": order.id,
"order_number": order.payment_intent_id,
"amount": order.amount,
"status": order.status,
"items": items,
"created_at": order.created_at.isoformat(),
}
)
orders_data = [
{
"id": order.id,
"order_number": order.payment_intent_id,
"amount": order.amount,
"status": order.status,
"items": [
{
"product_id": item.product_id,
"name": item.product.name,
"image_url": item.product.image_url,
"quantity": item.quantity,
"unit_price": item.unit_price,
"total": item.total,
}
for item in order.items
],
"created_at": order.created_at.isoformat(),
}
for order in orders
]

return jsonify({"success": True, "orders": orders_data})

Expand Down Expand Up @@ -173,8 +179,6 @@ def get_order(current_user, order_id):
}
), 404

items = parse_order_items(order.items)

return jsonify(
{
"success": True,
Expand All @@ -183,7 +187,17 @@ def get_order(current_user, order_id):
"order_number": order.payment_intent_id,
"amount": order.amount,
"status": order.status,
"items": items,
"items": [
{
"product_id": item.product_id,
"name": item.product.name,
"image_url": item.product.image_url,
"quantity": item.quantity,
"unit_price": item.unit_price,
"total": item.total,
}
for item in order.items
],
"created_at": order.created_at.isoformat(),
},
}
Expand Down
11 changes: 9 additions & 2 deletions backend/app/templates/email/order_confirmation.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
.header { text-align: center; padding: 20px 0; }
.order-details { margin: 20px 0; }
.items-table { width: 100%; border-collapse: collapse; margin: 20px 0; }
.items-table th, .items-table td { padding: 10px; border-bottom: 1px solid #ddd; text-align: left; }
.items-table th, .items-table td { padding: 10px; border-bottom: 1px solid #ddd; text-align: left; vertical-align: middle; }
.total { text-align: right; margin-top: 20px; }
.footer { text-align: center; margin-top: 30px; font-size: 0.9em; color: #666; }
.product-info { display: flex; align-items: center; gap: 10px; }
.product-image { width: 50px; height: 50px; object-fit: cover; border-radius: 4px; }
</style>
</head>
<body>
Expand Down Expand Up @@ -38,7 +40,12 @@ <h1>Order Confirmation</h1>
<tbody>
{% for item in items %}
<tr>
<td>{{ item.name }}</td>
<td>
<div class="product-info">
<img src="{{ item.product.image_url }}" alt="{{ item.product.name }}" class="product-image">
<span>{{ item.product.name }}</span>
</div>
</td>
<td>{{ item.quantity }}</td>
<td>${{ "%.2f"|format(item.unit_price) }}</td>
<td>${{ "%.2f"|format(item.total) }}</td>
Expand Down
19 changes: 7 additions & 12 deletions backend/app/utils.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
from flask import render_template
from flask_mail import Mail, Message
from flask import current_app
import json

mail = Mail()


def send_order_confirmation_email(order):
"""Send order confirmation email with receipt to customer"""
try:
Expand All @@ -15,20 +13,17 @@ def send_order_confirmation_email(order):
recipients=[order.user.email],
)

try:
items = json.loads(order.items) if order.items else []
except:
items = []
current_app.logger.error("Failed to parse order items")

msg.html = render_template(
"email/order_confirmation.html", order=order, user=order.user, items=items
"email/order_confirmation.html",
order=order,
user=order.user,
items=order.items
)

items_text = "\n".join(
[
f"- {item['name']}: {item['quantity']} x ${item['unit_price']:.2f} = ${item['total']:.2f}"
for item in items
f"- {item.product.name}: {item.quantity} x ${item.unit_price:.2f} = ${item.total:.2f}"
for item in order.items
]
)

Expand All @@ -52,4 +47,4 @@ def send_order_confirmation_email(order):
return True
except Exception as e:
current_app.logger.error(f"Failed to send order confirmation email: {str(e)}")
return False
return False
44 changes: 44 additions & 0 deletions backend/migrations/versions/d8c3dab8e779_add_order_items_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""add order items table
Revision ID: d8c3dab8e779
Revises: 3a9b96cc0b45
Create Date: 2025-01-12 14:20:32.924522
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = 'd8c3dab8e779'
down_revision = '3a9b96cc0b45'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('order_items',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('order_id', sa.Integer(), nullable=False),
sa.Column('product_id', sa.Integer(), nullable=False),
sa.Column('quantity', sa.Integer(), nullable=False),
sa.Column('unit_price', sa.Float(), nullable=False),
sa.Column('total', sa.Float(), nullable=False),
sa.ForeignKeyConstraint(['order_id'], ['orders.id'], ),
sa.ForeignKeyConstraint(['product_id'], ['products.id'], ),
sa.PrimaryKeyConstraint('id')
)
with op.batch_alter_table('orders', schema=None) as batch_op:
batch_op.drop_column('items')

# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('orders', schema=None) as batch_op:
batch_op.add_column(sa.Column('items', sa.TEXT(), autoincrement=False, nullable=True))

op.drop_table('order_items')
# ### end Alembic commands ###
Loading

0 comments on commit a6e9a21

Please sign in to comment.