diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index dadfaa7..2a77dfa 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index 139db10..115360c 100644 Binary files a/config/__pycache__/urls.cpython-311.pyc and b/config/__pycache__/urls.cpython-311.pyc differ diff --git a/config/settings.py b/config/settings.py index 001b8c8..6e91bc6 100644 --- a/config/settings.py +++ b/config/settings.py @@ -54,8 +54,20 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', 'core', + 'django_paystack', ] +PAYSTACK_PUBLIC_KEY = os.getenv("PAYSTACK_PUBLIC_KEY", "") +PAYSTACK_SECRET_KEY = os.getenv("PAYSTACK_SECRET_KEY", "") +PAYSTACK_SETTINGS = { + 'BUTTON_ID': 'paystack-button', + 'CURRENCY': 'NGN', + 'BUTTON_CLASS': 'btn btn-primary', +} + +LOGIN_URL = 'login' +LOGIN_REDIRECT_URL = 'index' + MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -78,6 +90,7 @@ TEMPLATES = [ 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', + 'core.context_processors.cart', ], }, }, diff --git a/config/urls.py b/config/urls.py index 5093d47..8ccc2b5 100644 --- a/config/urls.py +++ b/config/urls.py @@ -20,4 +20,5 @@ from django.urls import include, path urlpatterns = [ path("admin/", admin.site.urls), path("", include("core.urls")), + path("", include("core.paystack_urls")), ] diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index cd6f855..4f7d3cf 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/context_processors.cpython-311.pyc b/core/__pycache__/context_processors.cpython-311.pyc new file mode 100644 index 0000000..6b9604b Binary files /dev/null and b/core/__pycache__/context_processors.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 9aa598b..3393192 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/paystack_urls.cpython-311.pyc b/core/__pycache__/paystack_urls.cpython-311.pyc new file mode 100644 index 0000000..76987c1 Binary files /dev/null and b/core/__pycache__/paystack_urls.cpython-311.pyc differ diff --git a/core/__pycache__/paystack_views.cpython-311.pyc b/core/__pycache__/paystack_views.cpython-311.pyc new file mode 100644 index 0000000..bd97d1a Binary files /dev/null and b/core/__pycache__/paystack_views.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 1f807fa..6d3ec39 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 6867ddf..7d2c81d 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 8c38f3f..063fd89 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,3 +1,32 @@ from django.contrib import admin +from .models import Category, Product, Cart, CartItem, Order, OrderItem -# Register your models here. +@admin.register(Category) +class CategoryAdmin(admin.ModelAdmin): + list_display = ('name', 'slug') + prepopulated_fields = {'slug': ('name',)} + +@admin.register(Product) +class ProductAdmin(admin.ModelAdmin): + list_display = ('name', 'category', 'price', 'is_available', 'created_at') + list_filter = ('category', 'is_available') + list_editable = ('price', 'is_available') + +class CartItemInline(admin.TabularInline): + model = CartItem + raw_id_fields = ['product'] + +@admin.register(Cart) +class CartAdmin(admin.ModelAdmin): + list_display = ('user', 'session_key', 'created_at', 'updated_at') + inlines = [CartItemInline] + +class OrderItemInline(admin.TabularInline): + model = OrderItem + raw_id_fields = ['product'] + +@admin.register(Order) +class OrderAdmin(admin.ModelAdmin): + list_display = ('id', 'user', 'is_paid', 'created_at') + list_filter = ('is_paid', 'created_at') + inlines = [OrderItemInline] diff --git a/core/context_processors.py b/core/context_processors.py new file mode 100644 index 0000000..f399958 --- /dev/null +++ b/core/context_processors.py @@ -0,0 +1,4 @@ +from .views import _get_cart + +def cart(request): + return {'cart': _get_cart(request)} diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..8122cc3 --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,39 @@ +# Generated by Django 5.2.7 on 2025-11-13 15:24 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, unique=True)), + ('slug', models.SlugField(blank=True, max_length=100, unique=True)), + ], + options={ + 'verbose_name_plural': 'Categories', + }, + ), + migrations.CreateModel( + name='Product', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('description', models.TextField(blank=True)), + ('price', models.DecimalField(decimal_places=2, max_digits=10)), + ('is_available', models.BooleanField(default=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='products', to='core.category')), + ], + ), + ] diff --git a/core/migrations/0002_cart_cartitem.py b/core/migrations/0002_cart_cartitem.py new file mode 100644 index 0000000..534b673 --- /dev/null +++ b/core/migrations/0002_cart_cartitem.py @@ -0,0 +1,35 @@ +# Generated by Django 5.2.7 on 2025-11-13 15:38 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Cart', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('session_key', models.CharField(blank=True, max_length=40, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='CartItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', models.PositiveIntegerField(default=1)), + ('cart', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='core.cart')), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.product')), + ], + ), + ] diff --git a/core/migrations/0003_order_orderitem.py b/core/migrations/0003_order_orderitem.py new file mode 100644 index 0000000..cbeb29c --- /dev/null +++ b/core/migrations/0003_order_orderitem.py @@ -0,0 +1,35 @@ +# Generated by Django 5.2.7 on 2025-11-13 15:39 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0002_cart_cartitem'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Order', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('is_paid', models.BooleanField(default=False)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='OrderItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', models.PositiveIntegerField(default=1)), + ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='core.order')), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.product')), + ], + ), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000..1d211c6 Binary files /dev/null and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0002_cart_cartitem.cpython-311.pyc b/core/migrations/__pycache__/0002_cart_cartitem.cpython-311.pyc new file mode 100644 index 0000000..56c1714 Binary files /dev/null and b/core/migrations/__pycache__/0002_cart_cartitem.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0003_order_orderitem.cpython-311.pyc b/core/migrations/__pycache__/0003_order_orderitem.cpython-311.pyc new file mode 100644 index 0000000..cf822e4 Binary files /dev/null and b/core/migrations/__pycache__/0003_order_orderitem.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 71a8362..ae45f37 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,82 @@ from django.db import models +from django.utils.text import slugify +from django.conf import settings -# Create your models here. +class Category(models.Model): + name = models.CharField(max_length=100, unique=True) + slug = models.SlugField(max_length=100, unique=True, blank=True) + + def save(self, *args, **kwargs): + if not self.slug: + self.slug = slugify(self.name) + super().save(*args, **kwargs) + + def __str__(self): + return self.name + + class Meta: + verbose_name_plural = "Categories" + + +class Product(models.Model): + category = models.ForeignKey(Category, related_name='products', on_delete=models.CASCADE) + name = models.CharField(max_length=255) + description = models.TextField(blank=True) + price = models.DecimalField(max_digits=10, decimal_places=2) + is_available = models.BooleanField(default=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return self.name + +class Cart(models.Model): + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, blank=True, null=True) + session_key = models.CharField(max_length=40, blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + if self.user: + return f"Cart for {self.user.username}" + return f"Cart (session: {self.session_key})" + + @property + def total_price(self): + return sum(item.total_price for item in self.items.all()) + + @property + def total_price_in_kobo(self): + return int(self.total_price * 100) + +class CartItem(models.Model): + cart = models.ForeignKey(Cart, related_name='items', on_delete=models.CASCADE) + product = models.ForeignKey(Product, on_delete=models.CASCADE) + quantity = models.PositiveIntegerField(default=1) + + @property + def total_price(self): + return self.product.price * self.quantity + + def __str__(self): + return f"{self.quantity} of {self.product.name}" + +class Order(models.Model): + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + is_paid = models.BooleanField(default=False) + + def __str__(self): + return f"Order {self.id} by {self.user.username}" + + def get_total_price(self): + return sum(item.product.price * item.quantity for item in self.items.all()) + +class OrderItem(models.Model): + order = models.ForeignKey(Order, related_name='items', on_delete=models.CASCADE) + product = models.ForeignKey(Product, on_delete=models.CASCADE) + quantity = models.PositiveIntegerField(default=1) + + def __str__(self): + return f"{self.quantity} of {self.product.name}" \ No newline at end of file diff --git a/core/paystack_urls.py b/core/paystack_urls.py new file mode 100644 index 0000000..c6d69f8 --- /dev/null +++ b/core/paystack_urls.py @@ -0,0 +1,10 @@ + +from django.urls import path +from django.shortcuts import render +from . import paystack_views + +urlpatterns = [ + path('payment/success//', paystack_views.payment_success, name='payment_success'), + path('payment/failure/', paystack_views.payment_failure, name='payment_failure'), + path('payment/success_page/', lambda request: render(request, 'core/payment_success.html'), name='payment_success_page'), +] diff --git a/core/paystack_views.py b/core/paystack_views.py new file mode 100644 index 0000000..1388a2f --- /dev/null +++ b/core/paystack_views.py @@ -0,0 +1,37 @@ + +from django.shortcuts import render, redirect +from django.conf import settings +from django.contrib.auth.decorators import login_required +from .models import Cart, Order, OrderItem +from django_paystack.signals import payment_verified + +@login_required +def payment_success(request, reference): + cart = Cart.objects.get(user=request.user) + order = Order.objects.create( + user=request.user, + total_price=cart.total_price, + paid=True, + payment_reference=reference + ) + for item in cart.items.all(): + OrderItem.objects.create( + order=order, + product=item.product, + price=item.product.price, + quantity=item.quantity + ) + cart.items.all().delete() + return redirect('payment_success_page') + +def payment_failure(request): + return render(request, 'core/payment_failure.html') + +@payment_verified.connect +def on_payment_verified(sender, ref, amount, **kwargs): + """ + This signal is called when a payment is successfully verified. + """ + # You can perform actions like updating order status, sending notifications, etc. + print(f"Payment verified for reference: {ref} and amount: {amount}") + diff --git a/core/templates/base.html b/core/templates/base.html index 788576e..21bac82 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -1,11 +1,64 @@ +{% load static %} - + - {% block title %}Knowledge Base{% endblock %} - {% block head %}{% endblock %} - - - {% block content %}{% endblock %} - - + + {% block title %}VTU Nigeria{% endblock %} + + + + + + + + + + + +
+ {% block content %} + {% endblock %} +
+ + + + + + \ No newline at end of file diff --git a/core/templates/core/cart_detail.html b/core/templates/core/cart_detail.html new file mode 100644 index 0000000..7841771 --- /dev/null +++ b/core/templates/core/cart_detail.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} +
+

Your Shopping Cart

+ {% if not cart.items.all %} +

Your cart is empty.

+ {% else %} + + + + + + + + + + + {% for item in cart.items.all %} + + + + + + + {% endfor %} + +
ProductQuantityPriceTotal
{{ item.product.name }}{{ item.quantity }}${{ item.product.price }}${{ item.total_price }}
+
+

Total: ${{ cart.total_price }}

+
+ {% csrf_token %} + +
+
+ {% endif %} +
+{% endblock %} \ No newline at end of file diff --git a/core/templates/core/checkout_complete.html b/core/templates/core/checkout_complete.html new file mode 100644 index 0000000..5c40cdf --- /dev/null +++ b/core/templates/core/checkout_complete.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% block content %} +
+

Checkout Complete

+

Thank you for your order. Your order number is {{ order.id }}.

+

We will process your order shortly.

+ Continue Shopping +
+{% endblock %} \ No newline at end of file diff --git a/core/templates/core/index.html b/core/templates/core/index.html index 0a3f404..8b3be6b 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -1,154 +1,43 @@ -{% extends "base.html" %} -{% block title %}{{ project_name }}{% endblock %} +{% extends 'base.html' %} -{% block head %} -{% if project_description %} - - - -{% endif %} -{% if project_image_url %} - - -{% endif %} - - - - -{% endblock %} +{% block title %}Welcome to VTU Nigeria{% endblock %} {% block content %} -
-
-

Analyzing your requirements and generating your app…

-
- Loading… +
+
+

Fast & Reliable VTU Services

+

Your one-stop platform for airtime, data, cable subscriptions, and more.

+ Get Started Now
-

AppWizzy AI is collecting your requirements and applying the first changes.

-

This page will refresh automatically as the plan is implemented.

-

- Runtime: Django {{ django_version }} · Python {{ python_version }} - — UTC {{ current_time|date:"Y-m-d H:i:s" }} -

-
-
- -{% endblock %} \ No newline at end of file + + +
+
+

Our Products

+
+ {% for product in products %} +
+
+
+
{{ product.name }}
+
{{ product.category }}
+

{{ product.description|truncatewords:20 }}

+

₦{{ product.price }}

+
+ {% csrf_token %} + + +
+
+
+
+ {% empty %} +
+

No products available at the moment. Please check back later.

+
+ {% endfor %} +
+
+
+{% endblock %} diff --git a/core/templates/core/payment_failure.html b/core/templates/core/payment_failure.html new file mode 100644 index 0000000..c8e8bb0 --- /dev/null +++ b/core/templates/core/payment_failure.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% block content %} +
+

Payment Failed

+

There was an error processing your payment. Please try again.

+
+{% endblock %} \ No newline at end of file diff --git a/core/templates/core/payment_success.html b/core/templates/core/payment_success.html new file mode 100644 index 0000000..e50029d --- /dev/null +++ b/core/templates/core/payment_success.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} + +{% block content %} +
+

Payment Successful

+

Your order has been placed successfully.

+

Order ID: {{ order.id }}

+
+{% endblock %} \ No newline at end of file diff --git a/core/templates/registration/signup.html b/core/templates/registration/signup.html new file mode 100644 index 0000000..8f0e0d0 --- /dev/null +++ b/core/templates/registration/signup.html @@ -0,0 +1,22 @@ + +{% extends 'base.html' %} + +{% block title %}Create an Account{% endblock %} + +{% block content %} +
+
+

Create Your Account

+
+ {% csrf_token %} + {{ form.as_p }} +
+ +
+
+

+ Already have an account? Login +

+
+
+{% endblock %} diff --git a/core/templatetags/__init__.py b/core/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/templatetags/__pycache__/__init__.cpython-311.pyc b/core/templatetags/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..860da19 Binary files /dev/null and b/core/templatetags/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/templatetags/__pycache__/cart_tags.cpython-311.pyc b/core/templatetags/__pycache__/cart_tags.cpython-311.pyc new file mode 100644 index 0000000..fe63418 Binary files /dev/null and b/core/templatetags/__pycache__/cart_tags.cpython-311.pyc differ diff --git a/core/templatetags/cart_tags.py b/core/templatetags/cart_tags.py new file mode 100644 index 0000000..304099b --- /dev/null +++ b/core/templatetags/cart_tags.py @@ -0,0 +1,7 @@ +from django import template + +register = template.Library() + +@register.filter +def mul(value, arg): + return value * arg diff --git a/core/urls.py b/core/urls.py index 6299e3d..751c174 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,7 +1,13 @@ -from django.urls import path - -from .views import home +from django.urls import path, include +from .views import index, signup, add_to_cart, cart_detail, checkout, initiate_payment, verify_payment urlpatterns = [ - path("", home, name="home"), -] + path("", index, name="index"), + path("signup/", signup, name="signup"), + path("accounts/", include("django.contrib.auth.urls")), # for login, logout, etc. + path('cart/', cart_detail, name='cart_detail'), + path('cart/add//', add_to_cart, name='add_to_cart'), + path('checkout/', checkout, name='checkout'), + path('initiate-payment/', initiate_payment, name='initiate_payment'), + path('verify-payment/', verify_payment, name='verify_payment'), +] \ No newline at end of file diff --git a/core/views.py b/core/views.py index c9aed12..fa09d91 100644 --- a/core/views.py +++ b/core/views.py @@ -1,25 +1,134 @@ -import os -import platform +from django.shortcuts import render, redirect, get_object_or_404 +from django.contrib.auth.forms import UserCreationForm +from django.contrib.auth import login +from django.views.decorators.http import require_POST +from django.contrib.auth.decorators import login_required +from django.conf import settings +import requests +import json -from django import get_version as django_version -from django.shortcuts import render -from django.utils import timezone - - -def home(request): - """Render the landing screen with loader and environment details.""" - host_name = request.get_host().lower() - agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic" - now = timezone.now() +from .models import Product, Category, Cart, CartItem, Order, OrderItem +def index(request): + """Render the new landing page.""" + products = Product.objects.filter(is_available=True) + categories = Category.objects.all() context = { - "project_name": "New Style", - "agent_brand": agent_brand, - "django_version": django_version(), - "python_version": platform.python_version(), - "current_time": now, - "host_name": host_name, - "project_description": os.getenv("PROJECT_DESCRIPTION", ""), - "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), + 'products': products, + 'categories': categories, } return render(request, "core/index.html", context) + +def signup(request): + """Handle user signup.""" + if request.method == 'POST': + form = UserCreationForm(request.POST) + if form.is_valid(): + user = form.save() + # You can log the user in directly if you want + # login(request, user) + return redirect('login') + else: + form = UserCreationForm() + return render(request, 'registration/signup.html', {'form': form}) + +def _get_cart(request): + if request.user.is_authenticated: + cart, created = Cart.objects.get_or_create(user=request.user) + else: + session_key = request.session.session_key + if not session_key: + request.session.create() + session_key = request.session.session_key + cart, created = Cart.objects.get_or_create(session_key=session_key) + return cart + +@require_POST +def add_to_cart(request, product_id): + cart = _get_cart(request) + product = get_object_or_404(Product, id=product_id) + quantity = int(request.POST.get('quantity', 1)) + + cart_item, created = CartItem.objects.get_or_create(cart=cart, product=product) + if not created: + cart_item.quantity += quantity + else: + cart_item.quantity = quantity + cart_item.save() + + return redirect('cart_detail') + +def cart_detail(request): + cart = _get_cart(request) + return render(request, 'core/cart_detail.html', {'cart': cart}) + +@login_required +def checkout(request): + cart = _get_cart(request) + if not cart.items.all(): + return redirect('index') + return render(request, 'core/cart_detail.html', {'cart': cart}) + +@login_required +def initiate_payment(request): + cart = _get_cart(request) + if not cart.items.all(): + return redirect('index') + + url = 'https://api.paystack.co/transaction/initialize' + headers = { + 'Authorization': f'Bearer {settings.PAYSTACK_SECRET_KEY}', + 'Content-Type': 'application/json', + } + data = { + "email": request.user.email, + "amount": cart.total_price_in_kobo, + "callback_.pyurl": request.build_absolute_uri(f"/verify-payment/"), + } + + try: + response = requests.post(url, headers=headers, data=json.dumps(data)) + response_data = response.json() + if response_data['status']: + # store the reference in the session, so we can retrieve it in the callback + request.session['payment_ref'] = response_data['data']['reference'] + return redirect(response_data['data']['authorization_url']) + else: + return render(request, 'core/payment_failure.html', {'error': response_data['message']}) + except requests.exceptions.RequestException as e: + return render(request, 'core/payment_failure.html', {'error': f"An error occurred: {e}"}) + +@login_required +def verify_payment(request): + ref = request.GET.get('reference') + if not ref: + # if the reference is not in the get request, check the session + ref = request.session.get('payment_ref') + if not ref: + return redirect('payment_failure') + + url = f'https://api.paystack.co/transaction/verify/{ref}' + headers = { + 'Authorization': f'Bearer {settings.PAYSTACK_SECRET_KEY}', + } + + try: + response = requests.get(url, headers=headers) + response_data = response.json() + if response_data['status']: + if response_data['data']['status'] == 'success': + cart = _get_cart(request) + order = Order.objects.create(user=request.user, is_paid=True) + for item in cart.items.all(): + OrderItem.objects.create(order=order, product=item.product, quantity=item.quantity) + cart.items.all().delete() + # clear the payment reference from the session + if 'payment_ref' in request.session: + del request.session['payment_ref'] + return render(request, 'core/payment_success.html', {'order': order}) + else: + return render(request, 'core/payment_failure.html') + else: + return render(request, 'core/payment_failure.html') + except requests.exceptions.RequestException as e: + return render(request, 'core/payment_failure.html', {'error': f"An error occurred: {e}"}) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e22994c..c9b1117 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,8 @@ -Django==5.2.7 +Django +django-environ +gunicorn +psycopg2-binary +whitenoise +django-paystack mysqlclient==2.2.7 python-dotenv==1.1.1 diff --git a/static/css/custom.css b/static/css/custom.css new file mode 100644 index 0000000..073110b --- /dev/null +++ b/static/css/custom.css @@ -0,0 +1,107 @@ + +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&family=Poppins:wght@700&display=swap'); + +:root { + --primary-color: #0D0D2B; + --secondary-color: #3671E9; + --accent-color: #2B076E; + --bg-light: #FFFFFF; + --bg-gray: #F8F9FA; + --text-light: #FFFFFF; + --text-dark: #0D0D2B; + --font-headings: 'Poppins', sans-serif; + --font-body: 'Inter', sans-serif; +} + +body { + font-family: var(--font-body); + background-color: var(--bg-light); + color: var(--text-dark); +} + +h1, h2, h3, h4, h5, h6 { + font-family: var(--font-headings); + font-weight: 700; +} + +.btn-primary { + background-color: var(--secondary-color); + border-color: var(--secondary-color); + padding: 12px 30px; + font-weight: 500; + transition: all 0.3s ease; +} + +.btn-primary:hover { + background-color: #2d62c8; + border-color: #2d62c8; +} + +.navbar-brand { + font-family: var(--font-headings); + font-size: 1.5rem; + color: var(--text-light) !important; +} + +.hero { + background: linear-gradient(90deg, var(--accent-color) 0%, var(--primary-color) 100%); + color: var(--text-light); + padding: 100px 0; + text-align: center; +} + +.hero h1 { + font-size: 3.5rem; + margin-bottom: 20px; +} + +.hero p { + font-size: 1.2rem; + margin-bottom: 40px; +} + +.services-section { + padding: 80px 0; + background-color: var(--bg-gray); +} + +.service-card { + background-color: var(--bg-light); + border: none; + border-radius: 15px; + padding: 30px; + text-align: center; + box-shadow: 0 10px 30px rgba(0,0,0,0.07); + transition: transform 0.3s ease; +} + +.service-card:hover { + transform: translateY(-10px); +} + +.service-card .icon { + font-size: 3rem; + color: var(--secondary-color); + margin-bottom: 20px; +} + +.form-container { + max-width: 500px; + margin: 80px auto; + padding: 40px; + background-color: var(--bg-light); + border-radius: 15px; + box-shadow: 0 10px 30px rgba(0,0,0,0.07); +} + +.form-container h2 { + text-align: center; + margin-bottom: 30px; +} + +.footer { + background-color: var(--primary-color); + color: var(--text-light); + padding: 40px 0; + text-align: center; +}