├── portal ├── mpesa │ ├── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── tests.py │ ├── exceptions.py │ ├── __pycache__ │ │ ├── client.cpython-313.pyc │ │ ├── __init__.cpython-313.pyc │ │ └── exceptions.cpython-313.pyc │ ├── apps.py │ ├── admin.py │ ├── models.py │ ├── views.py │ └── client.py ├── portal │ ├── __init__.py │ ├── __pycache__ │ │ ├── urls.cpython-313.pyc │ │ ├── wsgi.cpython-313.pyc │ │ ├── __init__.cpython-313.pyc │ │ └── settings.cpython-313.pyc │ ├── asgi.py │ ├── wsgi.py │ ├── urls.py │ └── settings.py ├── wifi_auth │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ ├── __pycache__ │ │ │ ├── __init__.cpython-313.pyc │ │ │ ├── 0001_initial.cpython-313.pyc │ │ │ ├── 0003_auto_20250402_0708.cpython-313.pyc │ │ │ └── 0002_paymenttransaction_response_data.cpython-313.pyc │ │ ├── 0003_auto_20250402_0708.py │ │ ├── 0002_paymenttransaction_response_data.py │ │ └── 0001_initial.py │ ├── tests.py │ ├── mpesa │ │ ├── __init__.py │ │ ├── __pycache__ │ │ │ ├── client.cpython-313.pyc │ │ │ └── __init__.cpython-313.pyc │ │ └── client.py │ ├── templatetags │ │ ├── __init__.py │ │ ├── __pycache__ │ │ │ ├── __init__.cpython-313.pyc │ │ │ └── wifi_filters.cpython-313.pyc │ │ └── wifi_filters.py │ ├── __pycache__ │ │ ├── apps.cpython-313.pyc │ │ ├── urls.cpython-313.pyc │ │ ├── admin.cpython-313.pyc │ │ ├── models.cpython-313.pyc │ │ ├── views.cpython-313.pyc │ │ ├── __init__.cpython-313.pyc │ │ └── middleware.cpython-313.pyc │ ├── apps.py │ ├── urls.py │ ├── admin.py │ ├── middleware.py │ ├── templates │ │ └── wifi_auth │ │ │ ├── login.html │ │ │ ├── base.html │ │ │ ├── register.html │ │ │ ├── dashboard.html │ │ │ └── purchase_voucher.html │ ├── models.py │ └── views.py ├── db.sqlite3 ├── manage.py ├── test_mpesa.py ├── test_stk_push.py └── mpesa.log ├── .env ├── mpesa.env └── README.md /portal/mpesa/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /portal/portal/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /portal/wifi_auth/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /portal/mpesa/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /portal/wifi_auth/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /portal/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/occupyashanti/SafariFi-/HEAD/portal/db.sqlite3 -------------------------------------------------------------------------------- /portal/mpesa/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /portal/wifi_auth/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /portal/wifi_auth/mpesa/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | MPesa integration package for the WiFi captive portal. 3 | """ 4 | -------------------------------------------------------------------------------- /portal/wifi_auth/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is intentionally empty to make this directory a Python package -------------------------------------------------------------------------------- /portal/mpesa/exceptions.py: -------------------------------------------------------------------------------- 1 | class MpesaError(Exception): 2 | """Custom exception for M-Pesa related errors""" 3 | pass -------------------------------------------------------------------------------- /portal/mpesa/__pycache__/client.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/occupyashanti/SafariFi-/HEAD/portal/mpesa/__pycache__/client.cpython-313.pyc -------------------------------------------------------------------------------- /portal/portal/__pycache__/urls.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/occupyashanti/SafariFi-/HEAD/portal/portal/__pycache__/urls.cpython-313.pyc -------------------------------------------------------------------------------- /portal/portal/__pycache__/wsgi.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/occupyashanti/SafariFi-/HEAD/portal/portal/__pycache__/wsgi.cpython-313.pyc -------------------------------------------------------------------------------- /portal/mpesa/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/occupyashanti/SafariFi-/HEAD/portal/mpesa/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /portal/wifi_auth/__pycache__/apps.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/occupyashanti/SafariFi-/HEAD/portal/wifi_auth/__pycache__/apps.cpython-313.pyc -------------------------------------------------------------------------------- /portal/wifi_auth/__pycache__/urls.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/occupyashanti/SafariFi-/HEAD/portal/wifi_auth/__pycache__/urls.cpython-313.pyc -------------------------------------------------------------------------------- /portal/mpesa/__pycache__/exceptions.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/occupyashanti/SafariFi-/HEAD/portal/mpesa/__pycache__/exceptions.cpython-313.pyc -------------------------------------------------------------------------------- /portal/portal/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/occupyashanti/SafariFi-/HEAD/portal/portal/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /portal/portal/__pycache__/settings.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/occupyashanti/SafariFi-/HEAD/portal/portal/__pycache__/settings.cpython-313.pyc -------------------------------------------------------------------------------- /portal/wifi_auth/__pycache__/admin.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/occupyashanti/SafariFi-/HEAD/portal/wifi_auth/__pycache__/admin.cpython-313.pyc -------------------------------------------------------------------------------- /portal/wifi_auth/__pycache__/models.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/occupyashanti/SafariFi-/HEAD/portal/wifi_auth/__pycache__/models.cpython-313.pyc -------------------------------------------------------------------------------- /portal/wifi_auth/__pycache__/views.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/occupyashanti/SafariFi-/HEAD/portal/wifi_auth/__pycache__/views.cpython-313.pyc -------------------------------------------------------------------------------- /portal/wifi_auth/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/occupyashanti/SafariFi-/HEAD/portal/wifi_auth/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /portal/wifi_auth/__pycache__/middleware.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/occupyashanti/SafariFi-/HEAD/portal/wifi_auth/__pycache__/middleware.cpython-313.pyc -------------------------------------------------------------------------------- /portal/wifi_auth/mpesa/__pycache__/client.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/occupyashanti/SafariFi-/HEAD/portal/wifi_auth/mpesa/__pycache__/client.cpython-313.pyc -------------------------------------------------------------------------------- /portal/wifi_auth/mpesa/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/occupyashanti/SafariFi-/HEAD/portal/wifi_auth/mpesa/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /portal/mpesa/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MpesaConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'mpesa' 7 | -------------------------------------------------------------------------------- /portal/wifi_auth/migrations/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/occupyashanti/SafariFi-/HEAD/portal/wifi_auth/migrations/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /portal/wifi_auth/templatetags/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/occupyashanti/SafariFi-/HEAD/portal/wifi_auth/templatetags/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /portal/wifi_auth/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class WifiAuthConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'wifi_auth' 7 | -------------------------------------------------------------------------------- /portal/wifi_auth/migrations/__pycache__/0001_initial.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/occupyashanti/SafariFi-/HEAD/portal/wifi_auth/migrations/__pycache__/0001_initial.cpython-313.pyc -------------------------------------------------------------------------------- /portal/wifi_auth/templatetags/__pycache__/wifi_filters.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/occupyashanti/SafariFi-/HEAD/portal/wifi_auth/templatetags/__pycache__/wifi_filters.cpython-313.pyc -------------------------------------------------------------------------------- /portal/wifi_auth/migrations/__pycache__/0003_auto_20250402_0708.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/occupyashanti/SafariFi-/HEAD/portal/wifi_auth/migrations/__pycache__/0003_auto_20250402_0708.cpython-313.pyc -------------------------------------------------------------------------------- /portal/wifi_auth/migrations/__pycache__/0002_paymenttransaction_response_data.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/occupyashanti/SafariFi-/HEAD/portal/wifi_auth/migrations/__pycache__/0002_paymenttransaction_response_data.cpython-313.pyc -------------------------------------------------------------------------------- /portal/wifi_auth/migrations/0003_auto_20250402_0708.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.20 on 2025-04-02 07:08 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('wifi_auth', '0002_paymenttransaction_response_data'), 10 | ] 11 | 12 | operations = [ 13 | ] 14 | -------------------------------------------------------------------------------- /portal/mpesa/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import MpesaTransaction 3 | 4 | @admin.register(MpesaTransaction) 5 | class MpesaTransactionAdmin(admin.ModelAdmin): 6 | list_display = ('phone_number', 'amount', 'is_completed', 'transaction_date') 7 | list_filter = ('is_completed', 'transaction_date') 8 | search_fields = ('phone_number', 'receipt_number') 9 | -------------------------------------------------------------------------------- /portal/wifi_auth/templatetags/wifi_filters.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from datetime import datetime, timedelta 3 | 4 | register = template.Library() 5 | 6 | @register.filter 7 | def add_hours(value, hours): 8 | """Add the specified number of hours to a datetime value.""" 9 | if not value: 10 | return value 11 | try: 12 | return value + timedelta(hours=float(hours)) 13 | except (ValueError, TypeError): 14 | return value -------------------------------------------------------------------------------- /portal/portal/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for portal project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'portal.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /portal/portal/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for portal project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'portal.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /portal/wifi_auth/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | 4 | urlpatterns = [ 5 | path('', views.root_view, name='root'), 6 | path('login/', views.login_view, name='login'), 7 | path('logout/', views.logout_view, name='logout'), 8 | path('register/', views.register_view, name='register'), 9 | path('dashboard/', views.wifi_dashboard, name='wifi_dashboard'), 10 | path('purchase/', views.purchase_voucher, name='purchase_voucher'), 11 | path('payment-callback/', views.payment_callback, name='payment_callback'), 12 | ] -------------------------------------------------------------------------------- /portal/wifi_auth/migrations/0002_paymenttransaction_response_data.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.20 on 2025-04-01 08:17 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('wifi_auth', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='paymenttransaction', 15 | name='response_data', 16 | field=models.TextField(blank=True, help_text='Raw response data from M-Pesa API', null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /portal/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'portal.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /portal/mpesa/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.conf import settings 3 | 4 | class MpesaTransaction(models.Model): 5 | user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True) 6 | phone_number = models.CharField(max_length=15) 7 | amount = models.DecimalField(max_digits=10, decimal_places=2) 8 | receipt_number = models.CharField(max_length=50, blank=True) 9 | transaction_date = models.DateTimeField(auto_now_add=True) 10 | is_completed = models.BooleanField(default=False) 11 | merchant_request_id = models.CharField(max_length=100) 12 | checkout_request_id = models.CharField(max_length=100) 13 | result_code = models.IntegerField(null=True) 14 | result_description = models.TextField(blank=True) 15 | -------------------------------------------------------------------------------- /portal/portal/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | URL configuration for portal project. 3 | 4 | The `urlpatterns` list routes URLs to views. For more information please see: 5 | https://docs.djangoproject.com/en/4.2/topics/http/urls/ 6 | Examples: 7 | Function views 8 | 1. Add an import: from my_app import views 9 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 10 | Class-based views 11 | 1. Add an import: from other_app.views import Home 12 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 13 | Including another URLconf 14 | 1. Import the include() function: from django.urls import include, path 15 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 16 | """ 17 | from django.contrib import admin 18 | from django.urls import path, include 19 | 20 | urlpatterns = [ 21 | path('admin/', admin.site.urls), 22 | path('', include('wifi_auth.urls')), 23 | ] 24 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # Django configuration 2 | DJANGO_SECRET_KEY=your_secure_django_secret_key_here 3 | DEBUG=True 4 | ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0 5 | 6 | # MPesa configuration 7 | MPESA_ENVIRONMENT=sandbox 8 | # Replace with your actual Daraja API sandbox credentials 9 | MPESA_CONSUMER_KEY=puCNAENDVh2VGwwcD3gbIyCYxAimCUQfkgj9WK0AoEGmzN2B 10 | MPESA_CONSUMER_SECRET=8qPwRVRu4KqhQZwG10PtIBDE8LEsWA97M5e3hudUZz4ciOozQgAX4l2EVcP9OfqG 11 | MPESA_BUSINESS_SHORT_CODE=174379 12 | MPESA_PASSKEY=bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919 13 | MPESA_MERCHANT_NUMBER=254111304249 14 | 15 | # Callback URL - Use ngrok for local testing 16 | # Run: ngrok http 8000 17 | # Then update this with your ngrok URL 18 | MPESA_CALLBACK_URL=http://localhost:8000/payment-callback/ 19 | 20 | # For local development with ngrok 21 | # MPESA_CALLBACK_URL=https://your-ngrok-url.ngrok.io/payment-callback/ 22 | 23 | # For production 24 | MPESA_CALLBACK_URL=https://yourdomain.com/payment-callback/ -------------------------------------------------------------------------------- /mpesa.env: -------------------------------------------------------------------------------- 1 | 2 | # Django configuration 3 | DJANGO_SECRET_KEY=your_secure_django_secret_key_here 4 | DEBUG=True 5 | ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0 6 | 7 | # MPesa configuration 8 | MPESA_ENVIRONMENT=sandbox 9 | MPESA_BASE_URL=https://sandbox.safaricom.co.ke 10 | # Replace with your actual Daraja API sandbox credentials 11 | MPESA_CONSUMER_KEY=puCNAENDVh2VGwwcD3gbIyCYxAimCUQfkgj9WK0AoEGmzN2B 12 | MPESA_CONSUMER_SECRET=8qPwRVRu4KqhQZwG10PtIBDE8LEsWA97M5e3hudUZz4ciOozQgAX4l2EVcP9OfqG 13 | MPESA_BUSINESS_SHORT_CODE=174379 14 | MPESA_PASSKEY=bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919 15 | MPESA_MERCHANT_NUMBER=254111304249 16 | 17 | # Callback URL - Use ngrok for local testing 18 | # Run: ngrok http 8000 19 | # Then update this with your ngrok URL 20 | MPESA_CALLBACK_URL=http://localhost:8000/payment-callback/ 21 | 22 | # For local development with ngrok 23 | # MPESA_CALLBACK_URL=https://your-ngrok-url.ngrok.io/payment-callback/ 24 | 25 | # For production 26 | # MPESA_CALLBACK_URL=https://yourdomain.com/payment-callback/ -------------------------------------------------------------------------------- /portal/wifi_auth/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Voucher, PaymentTransaction, User 3 | 4 | @admin.register(User) 5 | class UserAdmin(admin.ModelAdmin): 6 | list_display = ('username', 'email', 'phone_number', 'is_premium') 7 | search_fields = ('username', 'email', 'phone_number') 8 | list_filter = ('is_premium', 'is_staff', 'is_active') 9 | 10 | @admin.register(Voucher) 11 | class VoucherAdmin(admin.ModelAdmin): 12 | list_display = ('code', 'duration_hours', 'price', 'is_used', 'created_by', 'created_at') 13 | search_fields = ('code', 'created_by__username') 14 | list_filter = ('is_used', 'created_at') 15 | readonly_fields = ('code', 'created_at') 16 | 17 | @admin.register(PaymentTransaction) 18 | class PaymentTransactionAdmin(admin.ModelAdmin): 19 | list_display = ('user', 'amount', 'phone_number', 'is_completed', 'created_at') 20 | search_fields = ('user__username', 'phone_number', 'mpesa_receipt') 21 | list_filter = ('is_completed', 'created_at') 22 | readonly_fields = ('response_data', 'created_at', 'updated_at') -------------------------------------------------------------------------------- /portal/mpesa/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from django.http import JsonResponse 3 | from django.views.decorators.http import require_http_methods 4 | from .client import MpesaClient, MpesaError 5 | import logging 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | @require_http_methods(["GET", "POST"]) 10 | def initiate_stk(request): 11 | if request.method == 'POST': 12 | try: 13 | phone = request.POST.get('phone') 14 | amount = request.POST.get('amount') 15 | 16 | if not phone or not amount: 17 | return JsonResponse({ 18 | 'success': False, 19 | 'error': 'Phone number and amount are required' 20 | }, status=400) 21 | 22 | client = MpesaClient() 23 | response = client.stk_push( 24 | phone_number=phone, 25 | amount=amount, 26 | account_reference="PAYMENT", 27 | transaction_desc="Payment" 28 | ) 29 | 30 | return JsonResponse({ 31 | 'success': True, 32 | 'data': response 33 | }) 34 | 35 | except MpesaError as e: 36 | logger.error(f"M-Pesa error: {str(e)}") 37 | return JsonResponse({ 38 | 'success': False, 39 | 'error': str(e) 40 | }, status=400) 41 | 42 | except Exception as e: 43 | logger.error(f"Unexpected error: {str(e)}") 44 | return JsonResponse({ 45 | 'success': False, 46 | 'error': 'An unexpected error occurred' 47 | }, status=500) 48 | 49 | return render(request, 'payment.html') 50 | -------------------------------------------------------------------------------- /portal/wifi_auth/middleware.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import redirect 2 | from django.urls import reverse 3 | from django.conf import settings 4 | 5 | class CaptivePortalMiddleware: 6 | def __init__(self, get_response): 7 | self.get_response = get_response 8 | 9 | def __call__(self, request): 10 | # Skip middleware for these paths 11 | if request.path.startswith('/static/') or \ 12 | request.path.startswith('/media/') or \ 13 | request.path.startswith('/login') or \ 14 | request.path.startswith('/register') or \ 15 | request.path.startswith('/payment-callback'): 16 | return self.get_response(request) 17 | 18 | # Check if user is authenticated 19 | if not request.user.is_authenticated: 20 | # Check for captive portal detection requests 21 | captive_portal_urls = [ 22 | '/generate_204', 23 | '/hotspot-detect.html', 24 | '/ncsi.txt', 25 | '/connecttest.txt', 26 | '/redirect', 27 | '/success.txt', 28 | '/check_network_status.txt', 29 | '/fwlink/', 30 | '/generate-204', 31 | '/gen_204', 32 | '/mobile/status.php', 33 | '/library/test/success.html', 34 | '/kindle-wifi/wifistub.html', 35 | '/connectivity-check.html' 36 | ] 37 | 38 | # Check if the request is a captive portal detection 39 | if request.path in captive_portal_urls or request.path.startswith('/generate_204'): 40 | return redirect(reverse('login')) 41 | 42 | return redirect(f"{reverse('login')}?next={request.path}") 43 | 44 | return self.get_response(request) -------------------------------------------------------------------------------- /portal/test_mpesa.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import base64 3 | from django.conf import settings 4 | import os 5 | import django 6 | 7 | # Set up Django environment 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'portal.settings') 9 | django.setup() 10 | 11 | def test_mpesa_credentials(): 12 | """Test M-Pesa API credentials by attempting to get an access token""" 13 | print("\nTesting M-Pesa API Credentials...") 14 | print("-" * 50) 15 | 16 | # Get credentials from settings 17 | consumer_key = settings.MPESA_CONSUMER_KEY 18 | consumer_secret = settings.MPESA_CONSUMER_SECRET 19 | base_url = settings.MPESA_BASE_URL 20 | 21 | print(f"Base URL: {base_url}") 22 | print(f"Consumer Key: {consumer_key}") 23 | print(f"Consumer Secret: {consumer_secret[:8]}...") # Only show first 8 chars for security 24 | 25 | # Prepare the request 26 | url = f"{base_url}/oauth/v1/generate?grant_type=client_credentials" 27 | auth_string = f"{consumer_key}:{consumer_secret}" 28 | encoded_auth = base64.b64encode(auth_string.encode()).decode() 29 | 30 | headers = { 31 | "Authorization": f"Basic {encoded_auth}" 32 | } 33 | 34 | try: 35 | print("\nMaking request to M-Pesa API...") 36 | response = requests.get(url, headers=headers) 37 | 38 | print(f"\nResponse Status Code: {response.status_code}") 39 | print(f"Response Headers: {dict(response.headers)}") 40 | 41 | if response.status_code == 200: 42 | data = response.json() 43 | print("\nSuccess! Access token obtained:") 44 | print(f"Access Token: {data.get('access_token', '')[:20]}...") # Only show first 20 chars 45 | print(f"Expires In: {data.get('expires_in', 'N/A')} seconds") 46 | return True 47 | else: 48 | print("\nError! Failed to get access token") 49 | print(f"Response Body: {response.text}") 50 | return False 51 | 52 | except Exception as e: 53 | print(f"\nException occurred: {str(e)}") 54 | return False 55 | 56 | if __name__ == "__main__": 57 | test_mpesa_credentials() -------------------------------------------------------------------------------- /portal/wifi_auth/templates/wifi_auth/login.html: -------------------------------------------------------------------------------- 1 | {% extends "wifi_auth/base.html" %} 2 | 3 | {% block title %}Login - WiFi Portal{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 |
10 |
11 | 12 |

Welcome Back

13 |

Sign in to access the WiFi network

14 |
15 | 16 | {% if error %} 17 |
18 | 19 | {{ error }} 20 |
21 | {% endif %} 22 | 23 |
24 | {% csrf_token %} 25 |
26 | 27 |
28 | 29 | 30 |
31 |
32 |
33 | 34 |
35 | 36 | 37 |
38 |
39 |
40 | 43 |
44 |
45 | 46 |
47 |

Don't have an account? Register now

48 |
49 |
50 |
51 |
52 |
53 | {% endblock %} -------------------------------------------------------------------------------- /portal/wifi_auth/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import AbstractUser 3 | from django.utils import timezone 4 | 5 | class User(AbstractUser): 6 | phone_number = models.CharField(max_length=15, unique=True) 7 | is_premium = models.BooleanField(default=False) 8 | 9 | # Add related_name attributes to avoid clashes with Django's User model 10 | groups = models.ManyToManyField( 11 | 'auth.Group', 12 | verbose_name='groups', 13 | blank=True, 14 | help_text='The groups this user belongs to.', 15 | related_name='wifi_user_set', 16 | related_query_name='user' 17 | ) 18 | user_permissions = models.ManyToManyField( 19 | 'auth.Permission', 20 | verbose_name='user permissions', 21 | blank=True, 22 | help_text='Specific permissions for this user.', 23 | related_name='wifi_user_set', 24 | related_query_name='user' 25 | ) 26 | 27 | def __str__(self): 28 | return self.username 29 | 30 | class Voucher(models.Model): 31 | code = models.CharField(max_length=20, unique=True) 32 | duration_hours = models.PositiveIntegerField() 33 | price = models.DecimalField(max_digits=10, decimal_places=2) 34 | is_used = models.BooleanField(default=False) 35 | created_by = models.ForeignKey(User, on_delete=models.CASCADE) 36 | created_at = models.DateTimeField(auto_now_add=True) 37 | used_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='used_vouchers') 38 | used_at = models.DateTimeField(null=True, blank=True) 39 | 40 | def __str__(self): 41 | return f"{self.code} ({'used' if self.is_used else 'available'})" 42 | 43 | class PaymentTransaction(models.Model): 44 | user = models.ForeignKey(User, on_delete=models.CASCADE) 45 | voucher = models.OneToOneField('Voucher', on_delete=models.SET_NULL, null=True) 46 | amount = models.DecimalField(max_digits=10, decimal_places=2) 47 | phone_number = models.CharField(max_length=15) 48 | mpesa_receipt = models.CharField(max_length=50, unique=True) 49 | is_completed = models.BooleanField(default=False) 50 | created_at = models.DateTimeField(auto_now_add=True) 51 | updated_at = models.DateTimeField(auto_now=True) 52 | response_data = models.TextField(null=True, blank=True, help_text="Stores the M-Pesa callback response data") 53 | 54 | def __str__(self): 55 | return f"Payment of {self.amount} by {self.user.username} - {'Completed' if self.is_completed else 'Pending'}" 56 | 57 | class Meta: 58 | ordering = ['-created_at'] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SafariFi 2 | 3 | > A Django-based captive portal for WiFi hotspots with M-Pesa payment integration. 4 | 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) 6 | [![Python Version](https://img.shields.io/badge/python-3.8%2B-blue.svg)](https://www.python.org/) 7 | [![Django](https://img.shields.io/badge/Django-4.x-success.svg)](https://www.djangoproject.com/) 8 | [![M-Pesa Integration](https://img.shields.io/badge/M--Pesa-Daraja--API-orange.svg)](https://developer.safaricom.co.ke/) 9 | [![Made in Kenya](https://img.shields.io/badge/Made%20in-Kenya-black.svg?logo=flag&logoColor=white)](https://github.com/occupyashanti/safariFi-) 10 | 11 | --- 12 | 13 | SafariFi lets you monetize WiFi access by redirecting users to a login and payment portal when they connect to your hotspot. Built with Django and integrated with Safaricom’s Daraja API, it provides a simple and secure way to sell internet access using M-Pesa payments. 14 | 15 | 16 | --- 17 | 18 | ## Features 19 | 20 | - **Secure Django backend** with configurable environment settings 21 | - **Captive portal redirection** for Android, iOS, Windows, and Linux 22 | - **M-Pesa integration** for real-time voucher purchase and validation 23 | - **Device-aware** login flows 24 | - Admin dashboard to manage users, vouchers, and payment logs 25 | 26 | --- 27 | 28 | ## Updates Made 29 | 30 | ### Security Enhancements 31 | - Environment-based `SECRET_KEY` and `DEBUG` settings 32 | - Proper `ALLOWED_HOSTS` configuration 33 | - Sample `.env` file for safer secret management 34 | 35 | ### Captive Portal Improvements 36 | - Improved OS/device detection 37 | - Wider support for captive portal triggers (e.g., `http://captive.apple.com`, `connectivitycheck.gstatic.com`) 38 | 39 | ### M-Pesa Integration 40 | - Fully functional M-Pesa client with: 41 | - STK push 42 | - Callback validation 43 | - Transaction status checks 44 | - Environment-variable-driven configuration 45 | 46 | ### Code Fixes 47 | - Added missing `os` import in `settings.py` 48 | - Fixed missing `include()` in `urls.py` 49 | - Modularized and cleaned up M-Pesa client logic 50 | 51 | --- 52 | 53 | ## Getting Started 54 | 55 | Follow these steps to set up and run the project locally. 56 | 57 | ### 1. Clone the Repository 58 | 59 | ```bash 60 | git clone https://github.com/occupyashanti/safariFi.git 61 | cd safariFi 62 | ``` 63 | 64 | ### 2. Create and Activate Virtual Environment 65 | 66 | #### On Linux/macOS: 67 | 68 | ```bash 69 | python -m venv venv 70 | source venv/bin/activate 71 | ``` 72 | 73 | #### On Windows: 74 | 75 | ```bash 76 | python -m venv venv 77 | venv\Scripts\activate 78 | ``` 79 | 80 | ### 3. Install Dependencies 81 | 82 | ```bash 83 | pip install -r requirements.txt 84 | ``` 85 | 86 | ### 4. Set Up Environment Variables 87 | 88 | Create a `.env` file in the project root (same level as `manage.py`) and add the following (adjust values as needed): 89 | 90 | ``` 91 | SECRET_KEY=your_django_secret_key 92 | DEBUG=True 93 | ALLOWED_HOSTS=127.0.0.1,localhost 94 | MPESA_CONSUMER_KEY=your_consumer_key 95 | MPESA_CONSUMER_SECRET=your_consumer_secret 96 | MPESA_SHORTCODE=your_shortcode 97 | MPESA_PASSKEY=your_passkey 98 | MPESA_CALLBACK_URL=https://yourdomain.com/api/payment/callback/ 99 | ``` 100 | 101 | A sample `.env.example` file is provided for reference. 102 | 103 | ### 5. Apply Migrations 104 | 105 | ```bash 106 | python manage.py migrate 107 | ``` 108 | 109 | ### 6. Create a Superuser (Optional but Recommended) 110 | 111 | ```bash 112 | python manage.py createsuperuser 113 | ``` 114 | 115 | Follow the prompts to set up your admin login credentials. 116 | 117 | ### 7. Run the Development Server 118 | 119 | ```bash 120 | python manage.py runserver 121 | ``` 122 | 123 | Visit `http://127.0.0.1:8000/` in your browser. You can access the Django admin panel at `http://127.0.0.1:8000/admin/`. 124 | 125 | --- 126 | 127 | 128 | -------------------------------------------------------------------------------- /portal/wifi_auth/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.20 on 2025-04-01 07:37 2 | 3 | from django.conf import settings 4 | import django.contrib.auth.models 5 | import django.contrib.auth.validators 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | import django.utils.timezone 9 | 10 | 11 | class Migration(migrations.Migration): 12 | 13 | initial = True 14 | 15 | dependencies = [ 16 | ('auth', '0012_alter_user_first_name_max_length'), 17 | ] 18 | 19 | operations = [ 20 | migrations.CreateModel( 21 | name='User', 22 | fields=[ 23 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 24 | ('password', models.CharField(max_length=128, verbose_name='password')), 25 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 26 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 27 | ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), 28 | ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), 29 | ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), 30 | ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), 31 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 32 | ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), 33 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 34 | ('phone_number', models.CharField(max_length=15, unique=True)), 35 | ('is_premium', models.BooleanField(default=False)), 36 | ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to.', related_name='wifi_user_set', related_query_name='user', to='auth.group', verbose_name='groups')), 37 | ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='wifi_user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), 38 | ], 39 | options={ 40 | 'verbose_name': 'user', 41 | 'verbose_name_plural': 'users', 42 | 'abstract': False, 43 | }, 44 | managers=[ 45 | ('objects', django.contrib.auth.models.UserManager()), 46 | ], 47 | ), 48 | migrations.CreateModel( 49 | name='Voucher', 50 | fields=[ 51 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 52 | ('code', models.CharField(max_length=20, unique=True)), 53 | ('duration_hours', models.PositiveIntegerField()), 54 | ('price', models.DecimalField(decimal_places=2, max_digits=10)), 55 | ('is_used', models.BooleanField(default=False)), 56 | ('created_at', models.DateTimeField(auto_now_add=True)), 57 | ('used_at', models.DateTimeField(blank=True, null=True)), 58 | ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 59 | ('used_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='used_vouchers', to=settings.AUTH_USER_MODEL)), 60 | ], 61 | ), 62 | migrations.CreateModel( 63 | name='PaymentTransaction', 64 | fields=[ 65 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 66 | ('amount', models.DecimalField(decimal_places=2, max_digits=10)), 67 | ('mpesa_receipt', models.CharField(blank=True, max_length=50, null=True)), 68 | ('phone_number', models.CharField(max_length=15)), 69 | ('transaction_date', models.DateTimeField(auto_now_add=True)), 70 | ('is_completed', models.BooleanField(default=False)), 71 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 72 | ('voucher', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='wifi_auth.voucher')), 73 | ], 74 | ), 75 | ] 76 | -------------------------------------------------------------------------------- /portal/test_stk_push.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import base64 3 | from datetime import datetime 4 | from django.conf import settings 5 | import os 6 | import django 7 | import json 8 | import socket 9 | 10 | # Set up Django environment 11 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'portal.settings') 12 | django.setup() 13 | 14 | def test_stk_push(): 15 | """Test STK push with a test phone number""" 16 | print("\nTesting M-Pesa STK Push...") 17 | print("-" * 50) 18 | 19 | # Get credentials from settings 20 | consumer_key = settings.MPESA_CONSUMER_KEY 21 | consumer_secret = settings.MPESA_CONSUMER_SECRET 22 | business_short_code = settings.MPESA_BUSINESS_SHORT_CODE 23 | passkey = settings.MPESA_PASSKEY 24 | 25 | # Use sandbox URL 26 | base_url = "https://sandbox.safaricom.co.ke" 27 | callback_url = "https://webhook.site/your-unique-url" # Replace with your webhook.site URL 28 | 29 | print(f"Base URL: {base_url}") 30 | print(f"Business Short Code: {business_short_code}") 31 | print(f"Callback URL: {callback_url}") 32 | 33 | # Test phone number - using standard test number 34 | phone_number = "254708374149" # Standard Safaricom test number 35 | amount = 1 # 1 KES for testing 36 | 37 | try: 38 | # Step 1: Get access token 39 | print("\n1. Getting access token...") 40 | auth_url = f"{base_url}/oauth/v1/generate?grant_type=client_credentials" 41 | auth_string = f"{consumer_key}:{consumer_secret}" 42 | encoded_auth = base64.b64encode(auth_string.encode()).decode() 43 | 44 | print(f"Making request to: {auth_url}") 45 | print(f"Using credentials - Key: {consumer_key}") 46 | 47 | session = requests.Session() 48 | session.verify = False # Skip SSL verification for testing 49 | 50 | auth_response = session.get( 51 | auth_url, 52 | headers={ 53 | "Authorization": f"Basic {encoded_auth}", 54 | "Content-Type": "application/json" 55 | } 56 | ) 57 | 58 | print(f"Auth Response Status: {auth_response.status_code}") 59 | print(f"Auth Response Headers: {dict(auth_response.headers)}") 60 | 61 | auth_response.raise_for_status() 62 | access_token = auth_response.json()['access_token'] 63 | print("✓ Access token obtained successfully") 64 | 65 | # Step 2: Prepare STK push 66 | print("\n2. Preparing STK push...") 67 | stk_url = f"{base_url}/mpesa/stkpush/v1/processrequest" 68 | timestamp = datetime.now().strftime("%Y%m%d%H%M%S") 69 | password = base64.b64encode( 70 | f"{business_short_code}{passkey}{timestamp}".encode() 71 | ).decode() 72 | 73 | payload = { 74 | "BusinessShortCode": business_short_code, 75 | "Password": password, 76 | "Timestamp": timestamp, 77 | "TransactionType": "CustomerPayBillOnline", 78 | "Amount": amount, 79 | "PartyA": phone_number, 80 | "PartyB": business_short_code, 81 | "PhoneNumber": phone_number, 82 | "CallBackURL": callback_url, 83 | "AccountReference": "TEST", 84 | "TransactionDesc": "Test Payment" 85 | } 86 | 87 | print(f"Payload: {json.dumps(payload, indent=2)}") 88 | 89 | # Step 3: Send STK push request 90 | print("\n3. Sending STK push request...") 91 | print(f"Making request to: {stk_url}") 92 | stk_response = session.post( 93 | stk_url, 94 | json=payload, 95 | headers={ 96 | "Authorization": f"Bearer {access_token}", 97 | "Content-Type": "application/json" 98 | } 99 | ) 100 | 101 | print(f"STK Response Status: {stk_response.status_code}") 102 | print(f"STK Response Headers: {dict(stk_response.headers)}") 103 | 104 | stk_response.raise_for_status() 105 | result = stk_response.json() 106 | 107 | print(f"\nResponse: {json.dumps(result, indent=2)}") 108 | 109 | if result.get('ResponseCode') == '0': 110 | print("\n✓ STK push initiated successfully!") 111 | print(f"CheckoutRequestID: {result.get('CheckoutRequestID')}") 112 | return True 113 | else: 114 | print("\n✗ STK push failed!") 115 | print(f"Error Code: {result.get('ResponseCode')}") 116 | print(f"Error Message: {result.get('ResponseDescription')}") 117 | return False 118 | 119 | except requests.exceptions.RequestException as e: 120 | print(f"\n✗ Network error occurred: {str(e)}") 121 | if hasattr(e.response, 'text'): 122 | print(f"Error response: {e.response.text}") 123 | return False 124 | except Exception as e: 125 | print(f"\n✗ Exception occurred: {str(e)}") 126 | return False 127 | 128 | if __name__ == "__main__": 129 | # Disable SSL warnings for testing 130 | import urllib3 131 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 132 | 133 | test_stk_push() -------------------------------------------------------------------------------- /portal/portal/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for portal project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.2.20. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.2/ref/settings/ 11 | """ 12 | 13 | import os 14 | from pathlib import Path 15 | from django.contrib.messages import constants as messages 16 | 17 | MESSAGE_TAGS = { 18 | messages.ERROR: 'danger', 19 | } 20 | 21 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 22 | BASE_DIR = Path(__file__).resolve().parent.parent 23 | 24 | 25 | # Quick-start development settings - unsuitable for production 26 | # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ 27 | 28 | # SECURITY WARNING: keep the secret key used in production secret! 29 | SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', 'django-insecure-zwp@gri4%hov64rp0x!k%_+dt09avd94xa_#bc2#(lspx+=qx_') 30 | 31 | # SECURITY WARNING: don't run with debug turned on in production! 32 | DEBUG = os.getenv('DEBUG', 'True') == 'True' 33 | 34 | ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', 'localhost,127.0.0.1,0.0.0.0').split(',') 35 | 36 | 37 | # Application definition 38 | 39 | INSTALLED_APPS = [ 40 | 'django.contrib.admin', 41 | 'django.contrib.auth', 42 | 'django.contrib.contenttypes', 43 | 'django.contrib.sessions', 44 | 'django.contrib.messages', 45 | 'django.contrib.staticfiles', 46 | 'wifi_auth' 47 | ] 48 | 49 | MIDDLEWARE = [ 50 | 'django.middleware.security.SecurityMiddleware', 51 | 'django.contrib.sessions.middleware.SessionMiddleware', 52 | 'django.middleware.common.CommonMiddleware', 53 | 'django.middleware.csrf.CsrfViewMiddleware', 54 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 55 | 'django.contrib.messages.middleware.MessageMiddleware', 56 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 57 | 'wifi_auth.middleware.CaptivePortalMiddleware', 58 | ] 59 | 60 | ROOT_URLCONF = 'portal.urls' 61 | 62 | TEMPLATES = [ 63 | { 64 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 65 | 'DIRS': [], 66 | 'APP_DIRS': True, 67 | 'OPTIONS': { 68 | 'context_processors': [ 69 | 'django.template.context_processors.debug', 70 | 'django.template.context_processors.request', 71 | 'django.contrib.auth.context_processors.auth', 72 | 'django.contrib.messages.context_processors.messages', 73 | ], 74 | }, 75 | }, 76 | ] 77 | 78 | WSGI_APPLICATION = 'portal.wsgi.application' 79 | 80 | 81 | # Database 82 | # https://docs.djangoproject.com/en/4.2/ref/settings/#databases 83 | 84 | DATABASES = { 85 | 'default': { 86 | 'ENGINE': 'django.db.backends.sqlite3', 87 | 'NAME': BASE_DIR / 'db.sqlite3', 88 | } 89 | } 90 | 91 | 92 | # Password validation 93 | # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators 94 | 95 | AUTH_PASSWORD_VALIDATORS = [ 96 | { 97 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 98 | }, 99 | { 100 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 101 | }, 102 | { 103 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 104 | }, 105 | { 106 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 107 | }, 108 | ] 109 | 110 | 111 | # Internationalization 112 | # https://docs.djangoproject.com/en/4.2/topics/i18n/ 113 | 114 | LANGUAGE_CODE = 'en-us' 115 | 116 | TIME_ZONE = 'UTC' 117 | 118 | USE_I18N = True 119 | 120 | USE_TZ = True 121 | 122 | 123 | # Static files (CSS, JavaScript, Images) 124 | # https://docs.djangoproject.com/en/4.2/howto/static-files/ 125 | 126 | STATIC_URL = 'static/' 127 | 128 | # Default primary key field type 129 | # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field 130 | 131 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 132 | 133 | # Use our custom User model 134 | AUTH_USER_MODEL = 'wifi_auth.User' 135 | # Safaricom Daraja API Configuration 136 | MPESA_ENVIRONMENT = os.getenv('MPESA_ENVIRONMENT', 'sandbox') # 'sandbox' or 'production' 137 | MPESA_CONSUMER_KEY = os.getenv('MPESA_CONSUMER_KEY', 'your_sandbox_consumer_key') 138 | MPESA_CONSUMER_SECRET = os.getenv('MPESA_CONSUMER_SECRET', 'your_sandbox_consumer_secret') 139 | MPESA_MERCHANT_NUMBER = os.getenv('MPESA_MERCHANT_NUMBER', '254111304249') # Merchant number to be credited 140 | 141 | # Sandbox (testing) credentials 142 | if MPESA_ENVIRONMENT == 'sandbox': 143 | MPESA_BASE_URL = 'https://sandbox.safaricom.co.ke' 144 | MPESA_BUSINESS_SHORT_CODE = os.getenv('MPESA_BUSINESS_SHORT_CODE', '174379') 145 | MPESA_PASSKEY = os.getenv('MPESA_PASSKEY', 'bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919') 146 | else: 147 | # Production credentials 148 | MPESA_BASE_URL = 'https://api.safaricom.co.ke' 149 | MPESA_BUSINESS_SHORT_CODE = os.getenv('MPESA_BUSINESS_SHORT_CODE', '') 150 | MPESA_PASSKEY = os.getenv('MPESA_PASSKEY', '') 151 | 152 | # For local development, use ngrok or a similar tool to create a public URL 153 | # Example: ngrok http 8000 154 | # Then update this with your ngrok URL 155 | MPESA_CALLBACK_URL = os.getenv('MPESA_CALLBACK_URL', 'http://localhost:8000/payment-callback/') 156 | # Add to settings.py 157 | LOGIN_URL = '/login/' 158 | LOGIN_REDIRECT_URL = '/dashboard/' 159 | LOGOUT_REDIRECT_URL = '/login/' 160 | 161 | # Logging configuration 162 | LOGGING = { 163 | 'version': 1, 164 | 'disable_existing_loggers': False, 165 | 'formatters': { 166 | 'verbose': { 167 | 'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}', 168 | 'style': '{', 169 | }, 170 | }, 171 | 'handlers': { 172 | 'console': { 173 | 'class': 'logging.StreamHandler', 174 | 'formatter': 'verbose', 175 | }, 176 | 'file': { 177 | 'class': 'logging.FileHandler', 178 | 'filename': 'mpesa.log', 179 | 'formatter': 'verbose', 180 | }, 181 | }, 182 | 'loggers': { 183 | 'wifi_auth': { 184 | 'handlers': ['console', 'file'], 185 | 'level': 'INFO', 186 | 'propagate': True, 187 | }, 188 | 'mpesa': { 189 | 'handlers': ['console', 'file'], 190 | 'level': 'INFO', 191 | 'propagate': True, 192 | }, 193 | }, 194 | } 195 | 196 | -------------------------------------------------------------------------------- /portal/wifi_auth/templates/wifi_auth/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% block title %}WiFi Portal{% endblock %} 7 | 8 | 9 | 142 | 143 | 144 | 175 | 176 |
177 | {% if messages %} 178 | {% for message in messages %} 179 |
180 | 181 | {{ message }} 182 | 183 |
184 | {% endfor %} 185 | {% endif %} 186 | 187 | {% block content %} 188 | {% endblock %} 189 |
190 | 191 | 192 | {% block scripts %}{% endblock %} 193 | 194 | -------------------------------------------------------------------------------- /portal/wifi_auth/templates/wifi_auth/register.html: -------------------------------------------------------------------------------- 1 | {% extends "wifi_auth/base.html" %} 2 | 3 | {% block title %}Register - WiFi Portal{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 |
10 |
11 | 12 |

Create Account

13 |

Join our WiFi network today

14 |
15 | 16 | {% if form.errors %} 17 |
18 | 19 | Please correct the errors below. 20 |
21 | {% endif %} 22 | 23 |
24 | {% csrf_token %} 25 | 26 |
27 | 28 |
29 | 30 | 31 | {% if form.username.errors %} 32 |
33 | {{ form.username.errors.0 }} 34 |
35 | {% endif %} 36 |
37 |
38 | 39 |
40 | 41 |
42 | 43 | 44 | {% if form.email.errors %} 45 |
46 | {{ form.email.errors.0 }} 47 |
48 | {% endif %} 49 |
50 |
51 | 52 |
53 | 54 |
55 | 56 | 57 | {% if form.phone_number.errors %} 58 |
59 | {{ form.phone_number.errors.0 }} 60 |
61 | {% endif %} 62 |
63 | Format: 254XXXXXXXXX (no spaces or dashes) 64 |
65 | 66 |
67 | 68 |
69 | 70 | 71 | {% if form.password1.errors %} 72 |
73 | {{ form.password1.errors.0 }} 74 |
75 | {% endif %} 76 |
77 |
78 | 79 |
80 | 81 |
82 | 83 | 84 | {% if form.password2.errors %} 85 |
86 | {{ form.password2.errors.0 }} 87 |
88 | {% endif %} 89 |
90 |
91 | 92 |
93 | 96 |
97 |
98 | 99 |
100 |

Already have an account? Login here

101 |
102 |
103 |
104 |
105 |
106 | {% endblock %} 107 | 108 | {% block scripts %} 109 | 144 | {% endblock %} 145 | -------------------------------------------------------------------------------- /portal/mpesa/client.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import base64 3 | from datetime import datetime, timedelta 4 | import json 5 | from django.conf import settings 6 | import logging 7 | import re 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | class MpesaError(Exception): 12 | """Custom exception for M-Pesa related errors""" 13 | pass 14 | 15 | class MpesaClient: 16 | def __init__(self): 17 | self.consumer_key = settings.MPESA_CONSUMER_KEY 18 | self.consumer_secret = settings.MPESA_CONSUMER_SECRET 19 | self.base_url = settings.MPESA_BASE_URL 20 | self.passkey = settings.MPESA_PASSKEY 21 | self.business_short_code = settings.MPESA_BUSINESS_SHORT_CODE 22 | self.callback_url = settings.MPESA_CALLBACK_URL 23 | self.access_token = None 24 | self.token_expiry = None 25 | 26 | # Log configuration (without sensitive data) 27 | logger.info(f"Initialized M-Pesa client with base URL: {self.base_url}") 28 | logger.info(f"Business Short Code: {self.business_short_code}") 29 | logger.info(f"Callback URL: {self.callback_url}") 30 | 31 | def validate_phone_number(self, phone_number): 32 | """Validate Kenyan phone number format""" 33 | # Remove any spaces or special characters 34 | phone = re.sub(r'\D', '', phone_number) 35 | # Check if it's a valid Kenyan number (10 digits starting with 254) 36 | if not re.match(r'^254\d{9}$', phone): 37 | raise MpesaError("Invalid phone number format. Use format: 254XXXXXXXXX") 38 | return phone 39 | 40 | def validate_amount(self, amount): 41 | """Validate amount is a positive integer""" 42 | try: 43 | amount = int(float(amount)) # Convert string or float to int 44 | if amount <= 0: 45 | raise MpesaError("Amount must be greater than 0") 46 | return amount 47 | except ValueError: 48 | raise MpesaError("Amount must be a valid number") 49 | 50 | def get_access_token(self): 51 | """Get OAuth access token from M-Pesa""" 52 | if self.access_token and self.token_expiry and self.token_expiry > datetime.now(): 53 | return self.access_token 54 | 55 | url = f"{self.base_url}/oauth/v1/generate?grant_type=client_credentials" 56 | auth_string = f"{self.consumer_key}:{self.consumer_secret}" 57 | encoded_auth = base64.b64encode(auth_string.encode()).decode() 58 | 59 | try: 60 | logger.info("Requesting access token from M-Pesa API") 61 | session = requests.Session() 62 | session.verify = False # Skip SSL verification for testing 63 | 64 | response = session.get( 65 | url, 66 | headers={ 67 | "Authorization": f"Basic {encoded_auth}", 68 | "Content-Type": "application/json" 69 | } 70 | ) 71 | response.raise_for_status() 72 | data = response.json() 73 | self.access_token = data['access_token'] 74 | # Set expiry to 55 minutes from now to be safe 75 | self.token_expiry = datetime.now() + timedelta(minutes=55) 76 | logger.info("Successfully obtained access token") 77 | return self.access_token 78 | except requests.exceptions.RequestException as e: 79 | logger.error(f"Network error getting access token: {e}") 80 | raise MpesaError("Failed to connect to M-Pesa API") 81 | except Exception as e: 82 | logger.error(f"Error getting access token: {e}") 83 | raise MpesaError("Failed to authenticate with M-Pesa API") 84 | 85 | def stk_push(self, phone_number, amount, account_reference, transaction_desc): 86 | """Initiate STK push payment""" 87 | try: 88 | # Validate inputs 89 | phone = self.validate_phone_number(phone_number) 90 | amount = self.validate_amount(amount) 91 | 92 | logger.info(f"Initiating STK push for phone: {phone}, amount: {amount}") 93 | 94 | access_token = self.get_access_token() 95 | url = f"{self.base_url}/mpesa/stkpush/v1/processrequest" 96 | 97 | timestamp = datetime.now().strftime("%Y%m%d%H%M%S") 98 | password = base64.b64encode( 99 | f"{self.business_short_code}{self.passkey}{timestamp}".encode() 100 | ).decode() 101 | 102 | payload = { 103 | "BusinessShortCode": self.business_short_code, 104 | "Password": password, 105 | "Timestamp": timestamp, 106 | "TransactionType": "CustomerPayBillOnline", 107 | "Amount": amount, 108 | "PartyA": phone, 109 | "PartyB": self.business_short_code, 110 | "PhoneNumber": phone, 111 | "CallBackURL": self.callback_url, 112 | "AccountReference": account_reference, 113 | "TransactionDesc": transaction_desc 114 | } 115 | 116 | logger.info(f"Sending STK push request with payload: {json.dumps(payload)}") 117 | 118 | session = requests.Session() 119 | session.verify = False # Skip SSL verification for testing 120 | 121 | response = session.post( 122 | url, 123 | json=payload, 124 | headers={ 125 | "Authorization": f"Bearer {access_token}", 126 | "Content-Type": "application/json" 127 | } 128 | ) 129 | 130 | # Log the raw response for debugging 131 | logger.info(f"Raw response status: {response.status_code}") 132 | logger.info(f"Raw response headers: {dict(response.headers)}") 133 | logger.info(f"Raw response body: {response.text}") 134 | 135 | response.raise_for_status() 136 | result = response.json() 137 | 138 | # Validate response structure 139 | if not isinstance(result, dict): 140 | raise MpesaError("Invalid response format from M-Pesa API") 141 | 142 | if result.get('ResponseCode') == '0': 143 | # Validate required fields 144 | if 'CheckoutRequestID' not in result: 145 | raise MpesaError("Missing CheckoutRequestID in successful response") 146 | 147 | logger.info(f"STK push initiated successfully. CheckoutRequestID: {result['CheckoutRequestID']}") 148 | return result 149 | else: 150 | error_code = result.get('ResponseCode', 'Unknown') 151 | error_message = result.get('ResponseDescription', 'Unknown error') 152 | logger.error(f"STK push failed: Code={error_code}, Message={error_message}") 153 | raise MpesaError(f"Payment failed: {error_message} (Code: {error_code})") 154 | 155 | except requests.exceptions.RequestException as e: 156 | logger.error(f"Network error in STK push: {e}") 157 | raise MpesaError("Failed to connect to M-Pesa API. Please check your internet connection.") 158 | except json.JSONDecodeError as e: 159 | logger.error(f"Invalid JSON response: {e}") 160 | raise MpesaError("Invalid response from M-Pesa API") 161 | except Exception as e: 162 | logger.error(f"Unexpected error in STK push: {e}") 163 | raise MpesaError("Failed to initiate payment. Please try again.") -------------------------------------------------------------------------------- /portal/wifi_auth/mpesa/client.py: -------------------------------------------------------------------------------- 1 | """ 2 | MPesa client implementation for the WiFi captive portal. 3 | """ 4 | import base64 5 | import json 6 | import requests 7 | from datetime import datetime 8 | import logging 9 | from django.conf import settings 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | class MpesaClient: 14 | """ 15 | A client for interacting with the Safaricom M-PESA API. 16 | """ 17 | 18 | def __init__(self): 19 | self.base_url = settings.MPESA_BASE_URL 20 | self.consumer_key = settings.MPESA_CONSUMER_KEY 21 | self.consumer_secret = settings.MPESA_CONSUMER_SECRET 22 | self.business_short_code = settings.MPESA_BUSINESS_SHORT_CODE 23 | self.passkey = settings.MPESA_PASSKEY 24 | self.callback_url = settings.MPESA_CALLBACK_URL 25 | self.merchant_number = settings.MPESA_MERCHANT_NUMBER 26 | self.access_token = self._get_access_token() 27 | 28 | def _get_access_token(self): 29 | """ 30 | Get OAuth access token from Safaricom. 31 | """ 32 | url = f"{self.base_url}/oauth/v1/generate?grant_type=client_credentials" 33 | auth = base64.b64encode(f"{self.consumer_key}:{self.consumer_secret}".encode()).decode("utf-8") 34 | headers = { 35 | "Authorization": f"Basic {auth}" 36 | } 37 | 38 | try: 39 | response = requests.get(url, headers=headers) 40 | response.raise_for_status() 41 | result = response.json() 42 | return result.get("access_token") 43 | except Exception as e: 44 | logger.error(f"Error getting access token: {str(e)}") 45 | raise Exception("Could not connect to M-PESA API. Please try again later.") 46 | 47 | def _generate_password(self): 48 | """ 49 | Generate the M-PESA API password using the provided shortcode and passkey. 50 | """ 51 | timestamp = datetime.now().strftime("%Y%m%d%H%M%S") 52 | password_str = f"{self.business_short_code}{self.passkey}{timestamp}" 53 | password_bytes = base64.b64encode(password_str.encode()) 54 | return { 55 | "password": password_bytes.decode("utf-8"), 56 | "timestamp": timestamp 57 | } 58 | 59 | def stk_push(self, phone_number, amount, account_reference, transaction_desc): 60 | """ 61 | Initiate an STK push request to the customer's phone. 62 | 63 | Args: 64 | phone_number (str): Customer's phone number in format 254XXXXXXXXX 65 | amount (int/float): Amount to charge 66 | account_reference (str): Reference ID for the transaction 67 | transaction_desc (str): Description of the transaction 68 | 69 | Returns: 70 | dict: Response from the M-PESA API 71 | """ 72 | if not self.access_token: 73 | logger.error("No access token available for M-PESA API") 74 | self.access_token = self._get_access_token() # Try to get a new token 75 | if not self.access_token: 76 | raise Exception("Could not authenticate with M-PESA API") 77 | 78 | url = f"{self.base_url}/mpesa/stkpush/v1/processrequest" 79 | headers = { 80 | "Authorization": f"Bearer {self.access_token}", 81 | "Content-Type": "application/json" 82 | } 83 | 84 | password_data = self._generate_password() 85 | 86 | # Make sure amount is at least 1 KES (M-PESA minimum) 87 | amount = max(1, int(amount)) 88 | 89 | # For sandbox testing, use these specific values 90 | if self.base_url == 'https://sandbox.safaricom.co.ke': 91 | # In sandbox, we must use the standard test values 92 | payload = { 93 | "BusinessShortCode": self.business_short_code, # Must be 174379 for sandbox 94 | "Password": password_data["password"], 95 | "Timestamp": password_data["timestamp"], 96 | "TransactionType": "CustomerPayBillOnline", 97 | "Amount": amount, 98 | "PartyA": phone_number, 99 | "PartyB": self.business_short_code, # In sandbox, PartyB must be the shortcode 100 | "PhoneNumber": phone_number, 101 | "CallBackURL": self.callback_url, 102 | "AccountReference": account_reference[:12] if len(account_reference) > 12 else account_reference, 103 | "TransactionDesc": transaction_desc[:13] if len(transaction_desc) > 13 else transaction_desc 104 | } 105 | else: 106 | # Production values 107 | payload = { 108 | "BusinessShortCode": self.business_short_code, 109 | "Password": password_data["password"], 110 | "Timestamp": password_data["timestamp"], 111 | "TransactionType": "CustomerPayBillOnline", 112 | "Amount": amount, 113 | "PartyA": phone_number, 114 | "PartyB": self.merchant_number, # Use the merchant number to be credited 115 | "PhoneNumber": phone_number, 116 | "CallBackURL": self.callback_url, 117 | "AccountReference": account_reference, 118 | "TransactionDesc": transaction_desc 119 | } 120 | 121 | # Log the request payload for debugging (excluding sensitive data) 122 | debug_payload = payload.copy() 123 | debug_payload["Password"] = "[REDACTED]" 124 | logger.info(f"STK Push Request: {json.dumps(debug_payload)}") 125 | 126 | try: 127 | print(f"Making STK push request to: {url}") 128 | print(f"Headers: Authorization: Bearer {self.access_token[:10]}...") 129 | print(f"Payload (partial): BusinessShortCode={payload['BusinessShortCode']}, Amount={payload['Amount']}, PartyA={payload['PartyA']}, PartyB={payload['PartyB']}") 130 | 131 | response = requests.post(url, json=payload, headers=headers) 132 | 133 | # Print raw response for debugging 134 | print(f"Raw response status: {response.status_code}") 135 | print(f"Raw response text: {response.text[:200]}...") 136 | 137 | try: 138 | response_data = response.json() 139 | 140 | # Log the response for debugging 141 | print(f"STK Push Response: {json.dumps(response_data)}") 142 | logger.info(f"STK Push Response: {json.dumps(response_data)}") 143 | 144 | # Check for API errors even if HTTP status is 200 145 | if 'errorCode' in response_data: 146 | error_message = response_data.get('errorMessage', 'Unknown M-PESA API error') 147 | print(f"M-PESA API Error: {error_message} (Code: {response_data.get('errorCode')})") 148 | logger.error(f"M-PESA API Error: {error_message}") 149 | raise Exception(f"M-PESA API Error: {error_message}") 150 | elif response_data.get('ResponseCode') != '0': 151 | error_message = response_data.get('ResponseDescription', 'Unknown M-PESA API error') 152 | print(f"M-PESA API Error: {error_message} (Code: {response_data.get('ResponseCode')})") 153 | logger.error(f"M-PESA API Error: {error_message}") 154 | raise Exception(f"M-PESA API Error: {error_message}") 155 | 156 | return response_data 157 | except json.JSONDecodeError: 158 | print("Failed to parse JSON response") 159 | raise Exception("Invalid response from M-PESA API: Not a valid JSON response") 160 | 161 | except requests.exceptions.HTTPError as http_err: 162 | logger.error(f"HTTP error occurred: {http_err}") 163 | # Try to get more details from the response 164 | error_message = "Payment request failed" 165 | try: 166 | error_data = response.json() 167 | error_message = error_data.get("errorMessage", error_message) 168 | except: 169 | pass 170 | raise Exception(error_message) 171 | except Exception as e: 172 | logger.error(f"Error initiating STK push: {str(e)}") 173 | raise Exception("Could not process payment request. Please try again later.") 174 | 175 | def check_payment_status(self, checkout_request_id): 176 | """ 177 | Check the status of a payment. 178 | 179 | Args: 180 | checkout_request_id (str): The checkout request ID returned by the STK push 181 | 182 | Returns: 183 | dict: Response from the M-PESA API 184 | """ 185 | if not self.access_token: 186 | raise Exception("Could not authenticate with M-PESA API") 187 | 188 | url = f"{self.base_url}/mpesa/stkpushquery/v1/query" 189 | headers = { 190 | "Authorization": f"Bearer {self.access_token}", 191 | "Content-Type": "application/json" 192 | } 193 | 194 | password_data = self._generate_password() 195 | 196 | payload = { 197 | "BusinessShortCode": self.business_short_code, 198 | "Password": password_data["password"], 199 | "Timestamp": password_data["timestamp"], 200 | "CheckoutRequestID": checkout_request_id 201 | } 202 | 203 | try: 204 | response = requests.post(url, json=payload, headers=headers) 205 | response.raise_for_status() 206 | return response.json() 207 | except Exception as e: 208 | logger.error(f"Error checking payment status: {str(e)}") 209 | raise Exception("Could not check payment status. Please try again later.") 210 | -------------------------------------------------------------------------------- /portal/wifi_auth/templates/wifi_auth/dashboard.html: -------------------------------------------------------------------------------- 1 | {% extends "wifi_auth/base.html" %} 2 | {% load static %} 3 | {% load wifi_filters %} 4 | 5 | {% block title %}Dashboard - WiFi Portal{% endblock %} 6 | 7 | {% block content %} 8 |
9 | {% if messages %} 10 |
11 | {% for message in messages %} 12 | 16 | {% endfor %} 17 |
18 | {% endif %} 19 | 20 | 21 |
22 |
23 | 48 |
49 |
50 | 51 | 52 |
53 |
54 |
55 |
56 |

Choose Your Plan

57 |
58 |
59 |
60 |
61 |
62 |
Quick Access
63 |
64 |

KES 5

65 |
66 | 30 Minutes 67 |
68 |

Perfect for quick browsing

69 | Select Plan 70 |
71 |
72 |
73 |
74 |
75 |
Standard
76 |
77 |

KES 9

78 |
79 | 1 Hour 80 |
81 |

Our most popular option

82 | Select Plan 83 |
84 |
85 |
86 |
87 |
88 |
Extended
89 |
90 |

KES 15

91 |
92 | 2 Hours 93 |
94 |

Great value for longer sessions

95 | Select Plan 96 |
97 |
98 |
99 |
100 |
101 |
Day Pass
102 |
103 |

KES 100

104 |
105 | 24 Hours 106 |
107 |

Full day of unlimited access

108 | Select Plan 109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 | 118 | 119 |
120 |
121 |
122 |
123 |

Active Vouchers

124 |
125 |
126 | {% if vouchers %} 127 |
128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | {% for voucher in vouchers %} 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | {% endfor %} 148 | 149 |
CodeDurationPriceUsed AtExpires At
{{ voucher.code }}{{ voucher.duration_hours }} hoursKES {{ voucher.price }}{{ voucher.used_at|date:"Y-m-d H:i" }}{{ voucher.used_at|add_hours:voucher.duration_hours|date:"Y-m-d H:i" }}
150 |
151 | {% else %} 152 |
153 | 154 |

No active vouchers found.

155 |
156 | {% endif %} 157 |
158 |
159 |
160 |
161 |
162 | {% endblock %} 163 | 164 | {% block scripts %} 165 | 190 | 191 | 276 | {% endblock %} -------------------------------------------------------------------------------- /portal/wifi_auth/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, redirect 2 | from django.contrib.auth import authenticate, login, logout 3 | from django.contrib.auth.decorators import login_required 4 | from django.http import JsonResponse 5 | from .models import Voucher, PaymentTransaction, User 6 | from mpesa.client import MpesaClient 7 | from django.conf import settings 8 | from django.contrib import messages 9 | from django.contrib.auth.forms import UserCreationForm 10 | from django import forms 11 | import random 12 | import string 13 | import json 14 | from datetime import timedelta 15 | from django.utils import timezone 16 | import logging 17 | from mpesa.exceptions import MpesaError 18 | from django.views.decorators.http import require_http_methods 19 | 20 | logger = logging.getLogger(__name__) 21 | 22 | def root_view(request): 23 | """Root view that redirects to appropriate page based on auth status""" 24 | if request.user.is_authenticated: 25 | return redirect('wifi_dashboard') 26 | return redirect('login') 27 | 28 | def generate_voucher_code(length=8): 29 | chars = string.ascii_uppercase + string.digits 30 | return ''.join(random.choice(chars) for _ in range(length)) 31 | 32 | def login_view(request): 33 | if request.user.is_authenticated: 34 | return redirect('wifi_dashboard') 35 | 36 | if request.method == 'POST': 37 | username = request.POST.get('username') 38 | password = request.POST.get('password') 39 | user = authenticate(request, username=username, password=password) 40 | 41 | if user is not None: 42 | login(request, user) 43 | next_url = request.GET.get('next', 'wifi_dashboard') 44 | return redirect(next_url) 45 | else: 46 | messages.error(request, 'Invalid username or password') 47 | return render(request, 'wifi_auth/login.html', {'error': True}) 48 | 49 | return render(request, 'wifi_auth/login.html') 50 | 51 | def logout_view(request): 52 | logout(request) 53 | messages.success(request, 'You have been logged out successfully') 54 | return redirect('login') 55 | 56 | class RegistrationForm(UserCreationForm): 57 | phone_number = forms.CharField(max_length=15, required=True, help_text='Required. Format: 254XXXXXXXXX') 58 | email = forms.EmailField(max_length=254, required=False, help_text='Optional.') 59 | 60 | class Meta: 61 | model = User 62 | fields = ('username', 'email', 'phone_number', 'password1', 'password2') 63 | 64 | def clean_phone_number(self): 65 | phone_number = self.cleaned_data.get('phone_number') 66 | 67 | # Basic validation for Kenyan phone numbers 68 | if not phone_number.startswith('254') or len(phone_number) != 12 or not phone_number.isdigit(): 69 | raise forms.ValidationError('Enter a valid phone number in the format 254XXXXXXXXX') 70 | 71 | # Check if phone number is already in use 72 | if User.objects.filter(phone_number=phone_number).exists(): 73 | raise forms.ValidationError('This phone number is already registered') 74 | 75 | return phone_number 76 | 77 | def register_view(request): 78 | if request.user.is_authenticated: 79 | return redirect('wifi_dashboard') 80 | 81 | if request.method == 'POST': 82 | form = RegistrationForm(request.POST) 83 | if form.is_valid(): 84 | user = form.save() 85 | username = form.cleaned_data.get('username') 86 | raw_password = form.cleaned_data.get('password1') 87 | 88 | # Log the user in after registration 89 | user = authenticate(username=username, password=raw_password) 90 | login(request, user) 91 | 92 | messages.success(request, f'Welcome {username}! Your account has been created successfully.') 93 | return redirect('wifi_dashboard') 94 | else: 95 | form = RegistrationForm() 96 | 97 | return render(request, 'wifi_auth/register.html', {'form': form}) 98 | 99 | @login_required 100 | def wifi_dashboard(request): 101 | active_vouchers = Voucher.objects.filter( 102 | used_by=request.user, 103 | is_used=True, 104 | used_at__gte=timezone.now() - timedelta(hours=24) # Show vouchers used in last 24 hours 105 | ) 106 | 107 | # Only show premium message if user has active premium status and no messages exist 108 | if request.user.is_premium and not messages.get_messages(request): 109 | messages.info(request, 'You have unlimited premium access') 110 | 111 | return render(request, 'wifi_auth/dashboard.html', { 112 | 'vouchers': active_vouchers, 113 | 'user': request.user 114 | }) 115 | 116 | @login_required 117 | def purchase_voucher(request): 118 | PRICING = { 119 | '0.5': 5, # 30 minutes - 5sh 120 | '1': 9, # 1 hour - 9sh 121 | '2': 15, # 2 hours - 15sh 122 | '5': 35, # 5 hours - 35sh 123 | '12': 65, # 12 hours - 65sh 124 | '24': 100, # 24 hours - 100sh 125 | '72': 250, # 3 days - 250sh 126 | '168': 500 # 1 week - 500sh 127 | } 128 | 129 | if request.method == 'POST': 130 | duration = request.POST.get('duration') 131 | phone_number = request.POST.get('phone_number', '').strip() 132 | 133 | # Validate duration and get price 134 | try: 135 | price = PRICING[duration] 136 | except KeyError: 137 | messages.error(request, 'Invalid duration selected') 138 | return redirect('purchase_voucher') 139 | 140 | # Clean and validate phone number 141 | phone_number = ''.join(filter(str.isdigit, phone_number)) 142 | if phone_number.startswith('0'): 143 | phone_number = '254' + phone_number[1:] 144 | elif phone_number.startswith('+'): 145 | phone_number = phone_number[1:] 146 | 147 | if not phone_number.startswith('254') or len(phone_number) != 12: 148 | messages.error(request, 'Please enter a valid Safaricom phone number') 149 | return redirect('purchase_voucher') 150 | 151 | # Generate voucher (but don't mark as used yet) 152 | voucher = Voucher.objects.create( 153 | code=generate_voucher_code(), 154 | duration_hours=float(duration), # Convert to float for decimal hours 155 | price=price, 156 | created_by=request.user 157 | ) 158 | 159 | # Initiate STK push 160 | mpesa = MpesaClient() 161 | try: 162 | # Log the payment attempt 163 | logger.info(f"Initiating STK push: Phone={phone_number}, Amount={price}, Duration={duration}hrs") 164 | 165 | response = mpesa.stk_push( 166 | phone_number=phone_number, 167 | amount=price, 168 | account_reference=f"WIFI-{voucher.code}", 169 | transaction_desc=f"WIFI voucher {duration}hrs" 170 | ) 171 | 172 | # Validate response 173 | if not response or 'CheckoutRequestID' not in response: 174 | raise MpesaError("Invalid response from M-Pesa API") 175 | 176 | # Create pending payment transaction 177 | transaction = PaymentTransaction.objects.create( 178 | user=request.user, 179 | voucher=voucher, 180 | amount=price, 181 | phone_number=phone_number, 182 | mpesa_receipt=response.get('CheckoutRequestID') 183 | ) 184 | 185 | # Store additional response data for debugging 186 | transaction.response_data = json.dumps(response) 187 | transaction.save() 188 | 189 | success_message = 'Payment initiated. Check your phone to complete the M-Pesa payment.' 190 | logger.info(f"STK push initiated successfully: {response.get('CheckoutRequestID')}") 191 | 192 | if request.headers.get('X-Requested-With') == 'XMLHttpRequest': 193 | return JsonResponse({ 194 | 'status': 'success', 195 | 'message': success_message, 196 | 'checkout_request_id': response.get('CheckoutRequestID') 197 | }) 198 | 199 | messages.success(request, success_message) 200 | return redirect('wifi_dashboard') 201 | 202 | except MpesaError as e: 203 | # Delete the voucher if payment failed 204 | voucher.delete() 205 | 206 | error_message = str(e) 207 | logger.error(f"M-Pesa error: {error_message}") 208 | 209 | if request.headers.get('X-Requested-With') == 'XMLHttpRequest': 210 | return JsonResponse({ 211 | 'status': 'error', 212 | 'message': error_message 213 | }, status=400) 214 | 215 | messages.error(request, f'Payment failed: {error_message}') 216 | return redirect('purchase_voucher') 217 | 218 | except Exception as e: 219 | # Delete the voucher if payment failed 220 | voucher.delete() 221 | 222 | error_message = f"Unexpected error during payment: {str(e)}" 223 | logger.error(error_message) 224 | 225 | if request.headers.get('X-Requested-With') == 'XMLHttpRequest': 226 | return JsonResponse({ 227 | 'status': 'error', 228 | 'message': 'An unexpected error occurred. Please try again.' 229 | }, status=500) 230 | 231 | messages.error(request, 'An unexpected error occurred. Please try again.') 232 | return redirect('purchase_voucher') 233 | 234 | # GET request - show purchase form 235 | return render(request, 'wifi_auth/purchase_voucher.html', { 236 | 'pricing': PRICING 237 | }) 238 | 239 | @require_http_methods(["POST"]) 240 | def payment_callback(request): 241 | """Handle M-Pesa payment callbacks""" 242 | try: 243 | # Parse the callback data 244 | callback_data = json.loads(request.body) 245 | logger.info(f"Received M-Pesa callback: {json.dumps(callback_data)}") 246 | 247 | # Extract relevant data from callback 248 | result = callback_data.get('Body', {}).get('stkCallback', {}) 249 | result_code = result.get('ResultCode') 250 | checkout_request_id = result.get('CheckoutRequestID') 251 | 252 | if not checkout_request_id: 253 | logger.error("Missing CheckoutRequestID in callback") 254 | return JsonResponse({'ResultCode': 1, 'ResultDesc': 'Invalid callback data'}) 255 | 256 | try: 257 | # Find the transaction 258 | transaction = PaymentTransaction.objects.get(mpesa_receipt=checkout_request_id) 259 | except PaymentTransaction.DoesNotExist: 260 | logger.error(f"Transaction not found for CheckoutRequestID: {checkout_request_id}") 261 | return JsonResponse({'ResultCode': 1, 'ResultDesc': 'Transaction not found'}) 262 | 263 | # Update transaction with callback data 264 | transaction.response_data = json.dumps(callback_data) 265 | 266 | if result_code == 0: # Success 267 | # Extract payment details 268 | items = result.get('CallbackMetadata', {}).get('Item', []) 269 | mpesa_receipt = next((item.get('Value') for item in items if item.get('Name') == 'MpesaReceiptNumber'), None) 270 | 271 | # Update transaction 272 | transaction.is_completed = True 273 | if mpesa_receipt: 274 | transaction.mpesa_receipt = mpesa_receipt 275 | transaction.save() 276 | 277 | # Activate the voucher 278 | voucher = transaction.voucher 279 | if voucher: 280 | voucher.is_used = True 281 | voucher.used_by = transaction.user 282 | voucher.used_at = timezone.now() 283 | voucher.save() 284 | 285 | logger.info(f"Payment successful: Receipt={mpesa_receipt}, Voucher={voucher.code}") 286 | 287 | # Send success message to user (you could implement this) 288 | # notify_user(transaction.user, "Payment successful! Your voucher is now active.") 289 | 290 | return JsonResponse({'ResultCode': 0, 'ResultDesc': 'Success'}) 291 | 292 | else: # Payment failed 293 | error_message = result.get('ResultDesc', 'Payment failed') 294 | logger.error(f"Payment failed: {error_message}") 295 | 296 | # Delete the voucher since payment failed 297 | if transaction.voucher: 298 | transaction.voucher.delete() 299 | 300 | # Update transaction status 301 | transaction.is_completed = False 302 | transaction.save() 303 | 304 | # Send failure message to user (you could implement this) 305 | # notify_user(transaction.user, f"Payment failed: {error_message}") 306 | 307 | return JsonResponse({'ResultCode': 1, 'ResultDesc': error_message}) 308 | 309 | except json.JSONDecodeError: 310 | logger.error("Invalid JSON in callback") 311 | return JsonResponse({'ResultCode': 1, 'ResultDesc': 'Invalid JSON'}, status=400) 312 | 313 | except Exception as e: 314 | logger.error(f"Error processing callback: {str(e)}") 315 | return JsonResponse({'ResultCode': 1, 'ResultDesc': 'Server Error'}, status=500) -------------------------------------------------------------------------------- /portal/wifi_auth/templates/wifi_auth/purchase_voucher.html: -------------------------------------------------------------------------------- 1 | {% extends "wifi_auth/base.html" %} 2 | 3 | {% block title %}Purchase Voucher - WiFi Portal{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 |
10 |

Purchase WiFi Voucher

11 | 12 | Back to Dashboard 13 | 14 |
15 |
16 |
Choose Your Plan
17 | 18 |
19 |
20 |
21 |
22 |
Quick Access
23 |
24 |
25 |

KES 5

26 |

30 Minutes

27 |

Perfect for quick browsing

28 |
29 |
30 |
31 |
32 |
33 |
34 |
Standard
35 |
36 |
37 |

KES 9

38 |

1 Hour

39 |

Our most popular option

40 |
41 |
42 |
43 |
44 |
45 |
46 |
Extended
47 |
48 |
49 |

KES 15

50 |

2 Hours

51 |

Great value for longer sessions

52 |
53 |
54 |
55 |
56 |
57 |
58 |
Day Pass
59 |
60 |
61 |

KES 100

62 |

24 Hours

63 |

Full day of unlimited access

64 |
65 |
66 |
67 |
68 | 69 |
70 |
71 |
72 |
73 |
More Options
74 |
75 |
76 |
77 |
78 |
79 | 80 | 81 |
82 |
83 |
84 |
85 | 86 | 87 |
88 |
89 |
90 |
91 | 92 | 93 |
94 |
95 |
96 |
97 | 98 | 99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | 107 |
108 | {% csrf_token %} 109 | 110 | 111 |
112 | 113 | 115 | We'll send an STK Push to this number for payment 116 |
117 | 118 |
119 | 122 | 123 | Back to Dashboard 124 | 125 |
126 |
127 |
128 |
129 |
130 |
131 | 132 | {% endblock %} 133 | 134 | {% block scripts %} 135 | 296 | {% endblock %} -------------------------------------------------------------------------------- /portal/mpesa.log: -------------------------------------------------------------------------------- 1 | INFO 2025-04-02 07:10:14,385 client 99860 139942917478080 Initialized M-Pesa client with base URL: https://sandbox.safaricom.co.ke 2 | INFO 2025-04-02 07:10:14,477 client 99860 139942917478080 Business Short Code: 174379 3 | INFO 2025-04-02 07:10:14,478 client 99860 139942917478080 Callback URL: https://yourdomain.com/payment-callback/ 4 | INFO 2025-04-02 07:10:14,482 views 99860 139942917478080 Initiating STK push: Phone=254111304249, Amount=5, Duration=0.5hrs 5 | INFO 2025-04-02 07:10:14,484 client 99860 139942917478080 Initiating STK push for phone: 254111304249, amount: 5 6 | INFO 2025-04-02 07:10:14,487 client 99860 139942917478080 Requesting access token from M-Pesa API 7 | INFO 2025-04-02 07:10:16,706 client 99860 139942917478080 Successfully obtained access token 8 | INFO 2025-04-02 07:10:16,709 client 99860 139942917478080 Sending STK push request with payload: {"BusinessShortCode": "174379", "Password": "MTc0Mzc5YmZiMjc5ZjlhYTliZGJjZjE1OGU5N2RkNzFhNDY3Y2QyZTBjODkzMDU5YjEwZjc4ZTZiNzJhZGExZWQyYzkxOTIwMjUwNDAyMDcxMDE2", "Timestamp": "20250402071016", "TransactionType": "CustomerPayBillOnline", "Amount": 5, "PartyA": "254111304249", "PartyB": "174379", "PhoneNumber": "254111304249", "CallBackURL": "https://yourdomain.com/payment-callback/", "AccountReference": "WIFI-2M6Z7120", "TransactionDesc": "WIFI voucher 0.5hrs"} 9 | INFO 2025-04-02 07:10:17,555 client 99860 139942917478080 STK push response: {"MerchantRequestID": "aa10-4361-b7cd-2aa0e34ed7e466714", "CheckoutRequestID": "ws_CO_02042025101016121111304249", "ResponseCode": "0", "ResponseDescription": "Success. Request accepted for processing", "CustomerMessage": "Success. Request accepted for processing"} 10 | INFO 2025-04-02 07:10:17,555 client 99860 139942917478080 STK push initiated successfully 11 | ERROR 2025-04-02 07:10:17,693 views 99860 139942917478080 Unexpected error during payment: table wifi_auth_paymenttransaction has no column named created_at 12 | INFO 2025-04-02 07:13:10,480 client 100213 139817399748288 Initialized M-Pesa client with base URL: https://sandbox.safaricom.co.ke 13 | INFO 2025-04-02 07:13:10,480 client 100213 139817399748288 Business Short Code: 174379 14 | INFO 2025-04-02 07:13:10,480 client 100213 139817399748288 Callback URL: https://yourdomain.com/payment-callback/ 15 | INFO 2025-04-02 07:13:10,481 views 100213 139817399748288 Initiating STK push: Phone=254111304249, Amount=5, Duration=0.5hrs 16 | INFO 2025-04-02 07:13:10,481 client 100213 139817399748288 Initiating STK push for phone: 254111304249, amount: 5 17 | INFO 2025-04-02 07:13:10,482 client 100213 139817399748288 Requesting access token from M-Pesa API 18 | INFO 2025-04-02 07:13:12,376 client 100213 139817399748288 Successfully obtained access token 19 | INFO 2025-04-02 07:13:12,381 client 100213 139817399748288 Sending STK push request with payload: {"BusinessShortCode": "174379", "Password": "MTc0Mzc5YmZiMjc5ZjlhYTliZGJjZjE1OGU5N2RkNzFhNDY3Y2QyZTBjODkzMDU5YjEwZjc4ZTZiNzJhZGExZWQyYzkxOTIwMjUwNDAyMDcxMzEy", "Timestamp": "20250402071312", "TransactionType": "CustomerPayBillOnline", "Amount": 5, "PartyA": "254111304249", "PartyB": "174379", "PhoneNumber": "254111304249", "CallBackURL": "https://yourdomain.com/payment-callback/", "AccountReference": "WIFI-VHZQO5XW", "TransactionDesc": "WIFI voucher 0.5hrs"} 20 | INFO 2025-04-02 07:13:13,312 client 100213 139817399748288 Raw response status: 200 21 | INFO 2025-04-02 07:13:13,312 client 100213 139817399748288 Raw response headers: {'content-type': 'application/json;charset=UTF-8', 'cache-control': 'no-store', 'x-request-id': '975599f4-a369-42a6-a71c-ab6df0dad624', 'date': 'Wed, 02 Apr 2025 07:13:13 GMT', 'Set-Cookie': 'visid_incap_2742146=zB+KZEHtRvKRl0FMMcY7tYjj7GcAAAAAQUIPAAAAAAAqB/SvleV5+/UK/FyTYjR8; expires=Wed, 01 Apr 2026 17:15:19 GMT; HttpOnly; path=/; Domain=.safaricom.co.ke; Secure; SameSite=None, incap_ses_6552_2742146=ifd3L9JD+1o7XpQlemHtWojj7GcAAAAAMBns9R4q7EnS85YSkrRYWA==; path=/; Domain=.safaricom.co.ke; Secure; SameSite=None', 'Strict-Transport-Security': 'max-age=31536000', 'X-CDN': 'Imperva', 'Content-Encoding': 'gzip', 'Transfer-Encoding': 'chunked', 'X-Iinfo': '14-19873893-19873909 NNYN CT(86 87 0) RT(1743577992515 158) q(0 0 1 -1) r(6 6) U6'} 22 | INFO 2025-04-02 07:13:13,312 client 100213 139817399748288 Raw response body: { 23 | "MerchantRequestID":"aa10-4361-b7cd-2aa0e34ed7e466791", 24 | "CheckoutRequestID":"ws_CO_02042025101449512111304249", 25 | "ResponseCode": "0", 26 | "ResponseDescription":"Success. Request accepted for processing", 27 | "CustomerMessage":"Success. Request accepted for processing" 28 | } 29 | 30 | INFO 2025-04-02 07:13:13,313 client 100213 139817399748288 STK push initiated successfully. CheckoutRequestID: ws_CO_02042025101449512111304249 31 | ERROR 2025-04-02 07:13:13,413 views 100213 139817399748288 Unexpected error during payment: table wifi_auth_paymenttransaction has no column named created_at 32 | INFO 2025-04-02 07:14:02,629 client 100213 139817399748288 Initialized M-Pesa client with base URL: https://sandbox.safaricom.co.ke 33 | INFO 2025-04-02 07:14:02,631 client 100213 139817399748288 Business Short Code: 174379 34 | INFO 2025-04-02 07:14:02,631 client 100213 139817399748288 Callback URL: https://yourdomain.com/payment-callback/ 35 | INFO 2025-04-02 07:14:02,631 views 100213 139817399748288 Initiating STK push: Phone=254111304249, Amount=5, Duration=0.5hrs 36 | INFO 2025-04-02 07:14:02,631 client 100213 139817399748288 Initiating STK push for phone: 254111304249, amount: 5 37 | INFO 2025-04-02 07:14:02,631 client 100213 139817399748288 Requesting access token from M-Pesa API 38 | INFO 2025-04-02 07:14:03,629 client 100213 139817399748288 Successfully obtained access token 39 | INFO 2025-04-02 07:14:03,633 client 100213 139817399748288 Sending STK push request with payload: {"BusinessShortCode": "174379", "Password": "MTc0Mzc5YmZiMjc5ZjlhYTliZGJjZjE1OGU5N2RkNzFhNDY3Y2QyZTBjODkzMDU5YjEwZjc4ZTZiNzJhZGExZWQyYzkxOTIwMjUwNDAyMDcxNDAz", "Timestamp": "20250402071403", "TransactionType": "CustomerPayBillOnline", "Amount": 5, "PartyA": "254111304249", "PartyB": "174379", "PhoneNumber": "254111304249", "CallBackURL": "https://yourdomain.com/payment-callback/", "AccountReference": "WIFI-VUVYLUBZ", "TransactionDesc": "WIFI voucher 0.5hrs"} 40 | INFO 2025-04-02 07:14:04,858 client 100213 139817399748288 Raw response status: 200 41 | INFO 2025-04-02 07:14:04,858 client 100213 139817399748288 Raw response headers: {'content-type': 'application/json;charset=UTF-8', 'cache-control': 'no-store', 'x-request-id': '839cd523-ca27-4392-94b7-55a34061722b', 'date': 'Wed, 02 Apr 2025 07:14:04 GMT', 'Set-Cookie': 'visid_incap_2742146=lAg+sYrzRlGMo9RRnYS4dbvj7GcAAAAAQUIPAAAAAAAEzqRAX7/EJ8Jb6El2NDsC; expires=Wed, 01 Apr 2026 17:15:18 GMT; HttpOnly; path=/; Domain=.safaricom.co.ke; Secure; SameSite=None, incap_ses_6552_2742146=a0lXT1aKpjSKtpQlemHtWrvj7GcAAAAAyNc8vabk1Vy1/8upH9gqKA==; path=/; Domain=.safaricom.co.ke; Secure; SameSite=None', 'Strict-Transport-Security': 'max-age=31536000', 'X-CDN': 'Imperva', 'Content-Encoding': 'gzip', 'Transfer-Encoding': 'chunked', 'X-Iinfo': '18-58849965-58849995 NNYN CT(97 87 0) RT(1743578043772 131) q(0 0 2 -1) r(6 6) U6'} 42 | INFO 2025-04-02 07:14:04,859 client 100213 139817399748288 Raw response body: { 43 | "MerchantRequestID":"aa10-4361-b7cd-2aa0e34ed7e466801", 44 | "CheckoutRequestID":"ws_CO_02042025101540749111304249", 45 | "ResponseCode": "0", 46 | "ResponseDescription":"Success. Request accepted for processing", 47 | "CustomerMessage":"Success. Request accepted for processing" 48 | } 49 | 50 | INFO 2025-04-02 07:14:04,859 client 100213 139817399748288 STK push initiated successfully. CheckoutRequestID: ws_CO_02042025101540749111304249 51 | ERROR 2025-04-02 07:14:04,970 views 100213 139817399748288 Unexpected error during payment: table wifi_auth_paymenttransaction has no column named created_at 52 | INFO 2025-04-02 07:17:04,557 client 100213 139817390307008 Initialized M-Pesa client with base URL: https://sandbox.safaricom.co.ke 53 | INFO 2025-04-02 07:17:04,570 client 100213 139817390307008 Business Short Code: 174379 54 | INFO 2025-04-02 07:17:04,571 client 100213 139817390307008 Callback URL: https://yourdomain.com/payment-callback/ 55 | INFO 2025-04-02 07:17:04,571 views 100213 139817390307008 Initiating STK push: Phone=254111304249, Amount=5, Duration=0.5hrs 56 | INFO 2025-04-02 07:17:04,571 client 100213 139817390307008 Initiating STK push for phone: 254111304249, amount: 5 57 | INFO 2025-04-02 07:17:04,571 client 100213 139817390307008 Requesting access token from M-Pesa API 58 | INFO 2025-04-02 07:17:07,022 client 100213 139817390307008 Successfully obtained access token 59 | INFO 2025-04-02 07:17:07,032 client 100213 139817390307008 Sending STK push request with payload: {"BusinessShortCode": "174379", "Password": "MTc0Mzc5YmZiMjc5ZjlhYTliZGJjZjE1OGU5N2RkNzFhNDY3Y2QyZTBjODkzMDU5YjEwZjc4ZTZiNzJhZGExZWQyYzkxOTIwMjUwNDAyMDcxNzA3", "Timestamp": "20250402071707", "TransactionType": "CustomerPayBillOnline", "Amount": 5, "PartyA": "254111304249", "PartyB": "174379", "PhoneNumber": "254111304249", "CallBackURL": "https://yourdomain.com/payment-callback/", "AccountReference": "WIFI-DXU0H3RP", "TransactionDesc": "WIFI voucher 0.5hrs"} 60 | INFO 2025-04-02 07:17:08,280 client 100213 139817390307008 Raw response status: 200 61 | INFO 2025-04-02 07:17:08,281 client 100213 139817390307008 Raw response headers: {'content-type': 'application/json;charset=UTF-8', 'cache-control': 'no-store', 'x-request-id': '31bb852f-dee6-4741-8458-35dc17ef4df6', 'date': 'Wed, 02 Apr 2025 07:17:07 GMT', 'Set-Cookie': 'visid_incap_2742146=ZHbQ+BwqQvKa9PgznVqueHPk7GcAAAAAQUIPAAAAAAByZKAp5J/Bbe8E27VyDSeD; expires=Wed, 01 Apr 2026 17:15:19 GMT; HttpOnly; path=/; Domain=.safaricom.co.ke; Secure; SameSite=None, incap_ses_6552_2742146=CfchEJzGm2+P5JUlemHtWnPk7GcAAAAADPnEX8kV5jdbcf0uQijz4w==; path=/; Domain=.safaricom.co.ke; Secure; SameSite=None', 'Strict-Transport-Security': 'max-age=31536000', 'X-CDN': 'Imperva', 'Content-Encoding': 'gzip', 'Transfer-Encoding': 'chunked', 'X-Iinfo': '7-3643999-3644003 NNYN CT(86 87 0) RT(1743578227179 135) q(0 0 2 -1) r(6 6) U6'} 62 | INFO 2025-04-02 07:17:08,282 client 100213 139817390307008 Raw response body: { 63 | "MerchantRequestID":"c20c-4eb8-8161-40af4f84ee3940015", 64 | "CheckoutRequestID":"ws_CO_02042025101844182111304249", 65 | "ResponseCode": "0", 66 | "ResponseDescription":"Success. Request accepted for processing", 67 | "CustomerMessage":"Success. Request accepted for processing" 68 | } 69 | 70 | INFO 2025-04-02 07:17:08,283 client 100213 139817390307008 STK push initiated successfully. CheckoutRequestID: ws_CO_02042025101844182111304249 71 | ERROR 2025-04-02 07:17:08,407 views 100213 139817390307008 Unexpected error during payment: table wifi_auth_paymenttransaction has no column named created_at 72 | INFO 2025-04-02 07:18:25,155 client 100213 139817390307008 Initialized M-Pesa client with base URL: https://sandbox.safaricom.co.ke 73 | INFO 2025-04-02 07:18:25,155 client 100213 139817390307008 Business Short Code: 174379 74 | INFO 2025-04-02 07:18:25,156 client 100213 139817390307008 Callback URL: https://yourdomain.com/payment-callback/ 75 | INFO 2025-04-02 07:18:25,156 views 100213 139817390307008 Initiating STK push: Phone=254111304249, Amount=5, Duration=0.5hrs 76 | INFO 2025-04-02 07:18:25,156 client 100213 139817390307008 Initiating STK push for phone: 254111304249, amount: 5 77 | INFO 2025-04-02 07:18:25,156 client 100213 139817390307008 Requesting access token from M-Pesa API 78 | INFO 2025-04-02 07:18:26,078 client 100213 139817390307008 Successfully obtained access token 79 | INFO 2025-04-02 07:18:26,105 client 100213 139817390307008 Sending STK push request with payload: {"BusinessShortCode": "174379", "Password": "MTc0Mzc5YmZiMjc5ZjlhYTliZGJjZjE1OGU5N2RkNzFhNDY3Y2QyZTBjODkzMDU5YjEwZjc4ZTZiNzJhZGExZWQyYzkxOTIwMjUwNDAyMDcxODI2", "Timestamp": "20250402071826", "TransactionType": "CustomerPayBillOnline", "Amount": 5, "PartyA": "254111304249", "PartyB": "174379", "PhoneNumber": "254111304249", "CallBackURL": "https://yourdomain.com/payment-callback/", "AccountReference": "WIFI-VCW1KV8A", "TransactionDesc": "WIFI voucher 0.5hrs"} 80 | INFO 2025-04-02 07:18:26,922 client 100213 139817390307008 Raw response status: 200 81 | INFO 2025-04-02 07:18:26,923 client 100213 139817390307008 Raw response headers: {'content-type': 'application/json;charset=UTF-8', 'cache-control': 'no-store', 'x-request-id': '7d6e68b6-2577-41d3-a838-7d877686f0a4', 'date': 'Wed, 02 Apr 2025 07:18:26 GMT', 'Set-Cookie': 'visid_incap_2742146=GncG6wx/TWGMW5vb1LcSa8Lk7GcAAAAAQUIPAAAAAAA2rGnsj33WkwQrU57i/dac; expires=Wed, 01 Apr 2026 17:15:19 GMT; HttpOnly; path=/; Domain=.safaricom.co.ke; Secure; SameSite=None, incap_ses_6552_2742146=+0PQeQvPFSEfZZYlemHtWsLk7GcAAAAA3IUmDN3gOcY6dumxphzyzg==; path=/; Domain=.safaricom.co.ke; Secure; SameSite=None', 'Strict-Transport-Security': 'max-age=31536000', 'X-CDN': 'Imperva', 'Content-Encoding': 'gzip', 'Transfer-Encoding': 'chunked', 'X-Iinfo': '17-50021961-50021984 NNYN CT(85 86 0) RT(1743578306246 151) q(0 0 2 -1) r(5 5) U6'} 82 | INFO 2025-04-02 07:18:26,923 client 100213 139817390307008 Raw response body: { 83 | "MerchantRequestID":"b54f-471d-93d9-f7f3bf3f7c0e3835613", 84 | "CheckoutRequestID":"ws_CO_02042025102003231111304249", 85 | "ResponseCode": "0", 86 | "ResponseDescription":"Success. Request accepted for processing", 87 | "CustomerMessage":"Success. Request accepted for processing" 88 | } 89 | 90 | INFO 2025-04-02 07:18:26,923 client 100213 139817390307008 STK push initiated successfully. CheckoutRequestID: ws_CO_02042025102003231111304249 91 | ERROR 2025-04-02 07:18:27,030 views 100213 139817390307008 Unexpected error during payment: table wifi_auth_paymenttransaction has no column named created_at 92 | INFO 2025-04-02 08:01:21,054 client 111511 139726713284288 Initialized M-Pesa client with base URL: https://sandbox.safaricom.co.ke 93 | INFO 2025-04-02 08:01:21,114 client 111511 139726713284288 Business Short Code: 174379 94 | INFO 2025-04-02 08:01:21,115 client 111511 139726713284288 Callback URL: https://yourdomain.com/payment-callback/ 95 | INFO 2025-04-02 08:01:21,122 views 111511 139726713284288 Initiating STK push: Phone=254111304249, Amount=9, Duration=1hrs 96 | INFO 2025-04-02 08:01:21,123 client 111511 139726713284288 Initiating STK push for phone: 254111304249, amount: 9 97 | INFO 2025-04-02 08:01:21,127 client 111511 139726713284288 Requesting access token from M-Pesa API 98 | INFO 2025-04-02 08:01:23,746 client 111511 139726713284288 Successfully obtained access token 99 | INFO 2025-04-02 08:01:23,788 client 111511 139726713284288 Sending STK push request with payload: {"BusinessShortCode": "174379", "Password": "MTc0Mzc5YmZiMjc5ZjlhYTliZGJjZjE1OGU5N2RkNzFhNDY3Y2QyZTBjODkzMDU5YjEwZjc4ZTZiNzJhZGExZWQyYzkxOTIwMjUwNDAyMDgwMTIz", "Timestamp": "20250402080123", "TransactionType": "CustomerPayBillOnline", "Amount": 9, "PartyA": "254111304249", "PartyB": "174379", "PhoneNumber": "254111304249", "CallBackURL": "https://yourdomain.com/payment-callback/", "AccountReference": "WIFI-TV8Q2DEH", "TransactionDesc": "WIFI voucher 1hrs"} 100 | INFO 2025-04-02 08:01:27,283 client 111511 139726713284288 Raw response status: 200 101 | INFO 2025-04-02 08:01:27,291 client 111511 139726713284288 Raw response headers: {'content-type': 'application/json;charset=UTF-8', 'cache-control': 'no-store', 'x-request-id': '2f0079a4-4d16-481c-8076-eef8a1abe113', 'date': 'Wed, 02 Apr 2025 08:01:26 GMT', 'Set-Cookie': 'visid_incap_2742146=NmYDDeIYSqeupmlXxUJUDdLu7GcAAAAAQUIPAAAAAAA5477W7VwcKScfw7nPbd1p; expires=Wed, 01 Apr 2026 15:29:56 GMT; HttpOnly; path=/; Domain=.safaricom.co.ke; Secure; SameSite=None, incap_ses_6553_2742146=bTk0Kgoejm3h5fMGHu/wWtbu7GcAAAAA6rxExkmAi5cnYRuuQ4nZpA==; path=/; Domain=.safaricom.co.ke; Secure; SameSite=None', 'Strict-Transport-Security': 'max-age=31536000', 'X-CDN': 'Imperva', 'Content-Encoding': 'gzip', 'Transfer-Encoding': 'chunked', 'X-Iinfo': '16-21074961-21075163 NNYN CT(87 86 0) RT(1743580885275 1248) q(0 0 2 3) r(5 5) U6'} 102 | INFO 2025-04-02 08:01:27,291 client 111511 139726713284288 Raw response body: { 103 | "MerchantRequestID":"aa10-4361-b7cd-2aa0e34ed7e467872", 104 | "CheckoutRequestID":"ws_CO_02042025110125581111304249", 105 | "ResponseCode": "0", 106 | "ResponseDescription":"Success. Request accepted for processing", 107 | "CustomerMessage":"Success. Request accepted for processing" 108 | } 109 | 110 | INFO 2025-04-02 08:01:27,292 client 111511 139726713284288 STK push initiated successfully. CheckoutRequestID: ws_CO_02042025110125581111304249 111 | ERROR 2025-04-02 08:01:27,408 views 111511 139726713284288 Unexpected error during payment: table wifi_auth_paymenttransaction has no column named created_at 112 | INFO 2025-04-02 08:03:11,105 client 111511 139726713284288 Initialized M-Pesa client with base URL: https://sandbox.safaricom.co.ke 113 | INFO 2025-04-02 08:03:11,106 client 111511 139726713284288 Business Short Code: 174379 114 | INFO 2025-04-02 08:03:11,106 client 111511 139726713284288 Callback URL: https://yourdomain.com/payment-callback/ 115 | INFO 2025-04-02 08:03:11,108 views 111511 139726713284288 Initiating STK push: Phone=254111304249, Amount=9, Duration=1hrs 116 | INFO 2025-04-02 08:03:11,109 client 111511 139726713284288 Initiating STK push for phone: 254111304249, amount: 9 117 | INFO 2025-04-02 08:03:11,109 client 111511 139726713284288 Requesting access token from M-Pesa API 118 | INFO 2025-04-02 08:03:12,937 client 111511 139726713284288 Successfully obtained access token 119 | INFO 2025-04-02 08:03:12,957 client 111511 139726713284288 Sending STK push request with payload: {"BusinessShortCode": "174379", "Password": "MTc0Mzc5YmZiMjc5ZjlhYTliZGJjZjE1OGU5N2RkNzFhNDY3Y2QyZTBjODkzMDU5YjEwZjc4ZTZiNzJhZGExZWQyYzkxOTIwMjUwNDAyMDgwMzEy", "Timestamp": "20250402080312", "TransactionType": "CustomerPayBillOnline", "Amount": 9, "PartyA": "254111304249", "PartyB": "174379", "PhoneNumber": "254111304249", "CallBackURL": "https://yourdomain.com/payment-callback/", "AccountReference": "WIFI-9J1KIJW3", "TransactionDesc": "WIFI voucher 1hrs"} 120 | INFO 2025-04-02 08:03:14,046 client 111511 139726713284288 Raw response status: 200 121 | INFO 2025-04-02 08:03:14,048 client 111511 139726713284288 Raw response headers: {'content-type': 'application/json;charset=UTF-8', 'cache-control': 'no-store', 'x-request-id': '9785e745-23ff-46cb-99c1-5b36a04e9701', 'date': 'Wed, 02 Apr 2025 08:03:13 GMT', 'Set-Cookie': 'visid_incap_2742146=x4uxFtsGS8iUW8Py11Vap0Hv7GcAAAAAQUIPAAAAAACGGHlaGwIBySnuSfvXbSjW; expires=Wed, 01 Apr 2026 15:29:57 GMT; HttpOnly; path=/; Domain=.safaricom.co.ke; Secure; SameSite=None, incap_ses_6553_2742146=nmBEUoNIaB9Bo/QGHu/wWkHv7GcAAAAAzAJiNkLIfTUA/1KwumDJ4g==; path=/; Domain=.safaricom.co.ke; Secure; SameSite=None', 'Strict-Transport-Security': 'max-age=31536000', 'X-CDN': 'Imperva', 'Content-Encoding': 'gzip', 'Transfer-Encoding': 'chunked', 'X-Iinfo': '13-6546152-6546162 NNYN CT(87 87 0) RT(1743580993179 299) q(0 0 2 -1) r(5 5) U6'} 122 | INFO 2025-04-02 08:03:14,048 client 111511 139726713284288 Raw response body: { 123 | "MerchantRequestID":"b54f-471d-93d9-f7f3bf3f7c0e3836462", 124 | "CheckoutRequestID":"ws_CO_02042025110312486111304249", 125 | "ResponseCode": "0", 126 | "ResponseDescription":"Success. Request accepted for processing", 127 | "CustomerMessage":"Success. Request accepted for processing" 128 | } 129 | 130 | INFO 2025-04-02 08:03:14,050 client 111511 139726713284288 STK push initiated successfully. CheckoutRequestID: ws_CO_02042025110312486111304249 131 | ERROR 2025-04-02 08:03:14,188 views 111511 139726713284288 Unexpected error during payment: table wifi_auth_paymenttransaction has no column named created_at 132 | INFO 2025-04-02 08:04:33,633 client 111511 139726713284288 Initialized M-Pesa client with base URL: https://sandbox.safaricom.co.ke 133 | INFO 2025-04-02 08:04:33,634 client 111511 139726713284288 Business Short Code: 174379 134 | INFO 2025-04-02 08:04:33,635 client 111511 139726713284288 Callback URL: https://yourdomain.com/payment-callback/ 135 | INFO 2025-04-02 08:04:33,636 views 111511 139726713284288 Initiating STK push: Phone=254111304249, Amount=9, Duration=1hrs 136 | INFO 2025-04-02 08:04:33,639 client 111511 139726713284288 Initiating STK push for phone: 254111304249, amount: 9 137 | INFO 2025-04-02 08:04:33,639 client 111511 139726713284288 Requesting access token from M-Pesa API 138 | INFO 2025-04-02 08:04:40,040 client 111511 139726713284288 Successfully obtained access token 139 | INFO 2025-04-02 08:04:40,053 client 111511 139726713284288 Sending STK push request with payload: {"BusinessShortCode": "174379", "Password": "MTc0Mzc5YmZiMjc5ZjlhYTliZGJjZjE1OGU5N2RkNzFhNDY3Y2QyZTBjODkzMDU5YjEwZjc4ZTZiNzJhZGExZWQyYzkxOTIwMjUwNDAyMDgwNDQw", "Timestamp": "20250402080440", "TransactionType": "CustomerPayBillOnline", "Amount": 9, "PartyA": "254111304249", "PartyB": "174379", "PhoneNumber": "254111304249", "CallBackURL": "https://yourdomain.com/payment-callback/", "AccountReference": "WIFI-KC9TEGB5", "TransactionDesc": "WIFI voucher 1hrs"} 140 | INFO 2025-04-02 08:04:41,402 client 111511 139726713284288 Raw response status: 200 141 | INFO 2025-04-02 08:04:41,406 client 111511 139726713284288 Raw response headers: {'content-type': 'application/json;charset=UTF-8', 'cache-control': 'no-store', 'x-request-id': 'bc766b23-5b5c-4258-8283-766d2b40de1f', 'date': 'Wed, 02 Apr 2025 08:04:40 GMT', 'Set-Cookie': 'visid_incap_2742146=vNz3wbwIQ6a5QXGudgXAppjv7GcAAAAAQUIPAAAAAACrh0uClxuVK1nBIQjXJ1AQ; expires=Wed, 01 Apr 2026 15:29:57 GMT; HttpOnly; path=/; Domain=.safaricom.co.ke; Secure; SameSite=None, incap_ses_6553_2742146=vGg8GhIqPn6aPPUGHu/wWpjv7GcAAAAAcnetNivpcZN0mvhNQSWKpA==; path=/; Domain=.safaricom.co.ke; Secure; SameSite=None', 'Strict-Transport-Security': 'max-age=31536000', 'X-CDN': 'Imperva', 'Content-Encoding': 'gzip', 'Transfer-Encoding': 'chunked', 'X-Iinfo': '17-24934342-24934412 NNYN CT(87 87 0) RT(1743581080370 342) q(0 0 1 -1) r(6 6) U6'} 142 | INFO 2025-04-02 08:04:41,414 client 111511 139726713284288 Raw response body: { 143 | "MerchantRequestID":"aa10-4361-b7cd-2aa0e34ed7e467948", 144 | "CheckoutRequestID":"ws_CO_02042025110617581111304249", 145 | "ResponseCode": "0", 146 | "ResponseDescription":"Success. Request accepted for processing", 147 | "CustomerMessage":"Success. Request accepted for processing" 148 | } 149 | 150 | INFO 2025-04-02 08:04:41,421 client 111511 139726713284288 STK push initiated successfully. CheckoutRequestID: ws_CO_02042025110617581111304249 151 | ERROR 2025-04-02 08:04:41,565 views 111511 139726713284288 Unexpected error during payment: table wifi_auth_paymenttransaction has no column named created_at 152 | INFO 2025-04-02 08:05:46,825 client 111511 139726713284288 Initialized M-Pesa client with base URL: https://sandbox.safaricom.co.ke 153 | INFO 2025-04-02 08:05:46,834 client 111511 139726713284288 Business Short Code: 174379 154 | INFO 2025-04-02 08:05:46,837 client 111511 139726713284288 Callback URL: https://yourdomain.com/payment-callback/ 155 | INFO 2025-04-02 08:05:46,837 views 111511 139726713284288 Initiating STK push: Phone=254111304249, Amount=35, Duration=5hrs 156 | INFO 2025-04-02 08:05:46,849 client 111511 139726713284288 Initiating STK push for phone: 254111304249, amount: 35 157 | INFO 2025-04-02 08:05:46,852 client 111511 139726713284288 Requesting access token from M-Pesa API 158 | INFO 2025-04-02 08:05:48,083 client 111511 139726713284288 Successfully obtained access token 159 | INFO 2025-04-02 08:05:48,123 client 111511 139726713284288 Sending STK push request with payload: {"BusinessShortCode": "174379", "Password": "MTc0Mzc5YmZiMjc5ZjlhYTliZGJjZjE1OGU5N2RkNzFhNDY3Y2QyZTBjODkzMDU5YjEwZjc4ZTZiNzJhZGExZWQyYzkxOTIwMjUwNDAyMDgwNTQ4", "Timestamp": "20250402080548", "TransactionType": "CustomerPayBillOnline", "Amount": 35, "PartyA": "254111304249", "PartyB": "174379", "PhoneNumber": "254111304249", "CallBackURL": "https://yourdomain.com/payment-callback/", "AccountReference": "WIFI-OB9ZL42F", "TransactionDesc": "WIFI voucher 5hrs"} 160 | INFO 2025-04-02 08:05:51,101 client 111511 139726713284288 Raw response status: 200 161 | INFO 2025-04-02 08:05:51,103 client 111511 139726713284288 Raw response headers: {'content-type': 'application/json;charset=UTF-8', 'cache-control': 'no-store', 'x-request-id': '623e1529-f216-46e8-b7e5-d47269ba40ec', 'date': 'Wed, 02 Apr 2025 08:05:50 GMT', 'Set-Cookie': 'visid_incap_2742146=dNUln46dTlyPPDvcJVzo2N7v7GcAAAAAQUIPAAAAAACprnHmvTDdmhaQYP1/gK8a; expires=Wed, 01 Apr 2026 15:29:56 GMT; HttpOnly; path=/; Domain=.safaricom.co.ke; Secure; SameSite=None, incap_ses_6553_2742146=qgT2AKrgshWrs/UGHu/wWt7v7GcAAAAAOaJtM0pyOnenJuUiSgpdRA==; path=/; Domain=.safaricom.co.ke; Secure; SameSite=None', 'Strict-Transport-Security': 'max-age=31536000', 'X-CDN': 'Imperva', 'Content-Encoding': 'gzip', 'Transfer-Encoding': 'chunked', 'X-Iinfo': '16-21113196-21113310 NNYN CT(86 87 0) RT(1743581149493 1065) q(0 0 2 -1) r(5 5) U6'} 162 | INFO 2025-04-02 08:05:51,103 client 111511 139726713284288 Raw response body: { 163 | "MerchantRequestID":"b54f-471d-93d9-f7f3bf3f7c0e3836513", 164 | "CheckoutRequestID":"ws_CO_02042025110727398111304249", 165 | "ResponseCode": "0", 166 | "ResponseDescription":"Success. Request accepted for processing", 167 | "CustomerMessage":"Success. Request accepted for processing" 168 | } 169 | 170 | INFO 2025-04-02 08:05:51,104 client 111511 139726713284288 STK push initiated successfully. CheckoutRequestID: ws_CO_02042025110727398111304249 171 | ERROR 2025-04-02 08:05:51,238 views 111511 139726713284288 Unexpected error during payment: table wifi_auth_paymenttransaction has no column named created_at 172 | INFO 2025-04-02 08:18:44,045 client 115371 140406882961088 Initialized M-Pesa client with base URL: https://sandbox.safaricom.co.ke 173 | INFO 2025-04-02 08:18:44,105 client 115371 140406882961088 Business Short Code: 174379 174 | INFO 2025-04-02 08:18:44,105 client 115371 140406882961088 Callback URL: https://yourdomain.com/payment-callback/ 175 | INFO 2025-04-02 08:18:44,105 views 115371 140406882961088 Initiating STK push: Phone=254111304249, Amount=5, Duration=0.5hrs 176 | INFO 2025-04-02 08:18:44,106 client 115371 140406882961088 Initiating STK push for phone: 254111304249, amount: 5 177 | INFO 2025-04-02 08:18:44,106 client 115371 140406882961088 Requesting access token from M-Pesa API 178 | INFO 2025-04-02 08:18:46,013 client 115371 140406882961088 Successfully obtained access token 179 | INFO 2025-04-02 08:18:46,014 client 115371 140406882961088 Sending STK push request with payload: {"BusinessShortCode": "174379", "Password": "MTc0Mzc5YmZiMjc5ZjlhYTliZGJjZjE1OGU5N2RkNzFhNDY3Y2QyZTBjODkzMDU5YjEwZjc4ZTZiNzJhZGExZWQyYzkxOTIwMjUwNDAyMDgxODQ2", "Timestamp": "20250402081846", "TransactionType": "CustomerPayBillOnline", "Amount": 5, "PartyA": "254111304249", "PartyB": "174379", "PhoneNumber": "254111304249", "CallBackURL": "https://yourdomain.com/payment-callback/", "AccountReference": "WIFI-11A5I1BZ", "TransactionDesc": "WIFI voucher 0.5hrs"} 180 | INFO 2025-04-02 08:18:52,338 client 115371 140406882961088 Raw response status: 200 181 | INFO 2025-04-02 08:18:52,339 client 115371 140406882961088 Raw response headers: {'content-type': 'application/json;charset=UTF-8', 'cache-control': 'no-store', 'x-request-id': '37abf942-df00-438b-965f-0c558e6c1d9e', 'date': 'Wed, 02 Apr 2025 08:18:51 GMT', 'Set-Cookie': 'visid_incap_2742146=ulHssCNiRBaRu8AdA6vK7OXy7GcAAAAAQUIPAAAAAABLZJJ/wONafekOvq8jV9nT; expires=Wed, 01 Apr 2026 15:29:56 GMT; HttpOnly; path=/; Domain=.safaricom.co.ke; Secure; SameSite=None, incap_ses_6553_2742146=HXOTSkejeXf60foGHu/wWuvy7GcAAAAAlACbMjnNun+WxUNZcgQEnA==; path=/; Domain=.safaricom.co.ke; Secure; SameSite=None', 'Strict-Transport-Security': 'max-age=31536000', 'X-CDN': 'Imperva', 'Content-Encoding': 'gzip', 'Transfer-Encoding': 'chunked', 'X-Iinfo': '16-21226769-21226786 NNYN CT(87 86 0) RT(1743581931352 324) q(0 0 2 15) r(6 6) U6'} 182 | INFO 2025-04-02 08:18:52,339 client 115371 140406882961088 Raw response body: { 183 | "MerchantRequestID":"f6d6-4b6d-9f75-1a54a0b818ca43744", 184 | "CheckoutRequestID":"ws_CO_02042025111850699111304249", 185 | "ResponseCode": "0", 186 | "ResponseDescription":"Success. Request accepted for processing", 187 | "CustomerMessage":"Success. Request accepted for processing" 188 | } 189 | 190 | INFO 2025-04-02 08:18:52,339 client 115371 140406882961088 STK push initiated successfully. CheckoutRequestID: ws_CO_02042025111850699111304249 191 | ERROR 2025-04-02 08:18:52,439 views 115371 140406882961088 Unexpected error during payment: table wifi_auth_paymenttransaction has no column named created_at 192 | INFO 2025-04-02 08:57:46,228 client 120113 140010024801984 Initialized M-Pesa client with base URL: https://sandbox.safaricom.co.ke 193 | INFO 2025-04-02 08:57:46,292 client 120113 140010024801984 Business Short Code: 174379 194 | INFO 2025-04-02 08:57:46,293 client 120113 140010024801984 Callback URL: https://yourdomain.com/payment-callback/ 195 | INFO 2025-04-02 08:57:46,294 views 120113 140010024801984 Initiating STK push: Phone=254111304249, Amount=5, Duration=0.5hrs 196 | INFO 2025-04-02 08:57:46,294 client 120113 140010024801984 Initiating STK push for phone: 254111304249, amount: 5 197 | INFO 2025-04-02 08:57:46,295 client 120113 140010024801984 Requesting access token from M-Pesa API 198 | INFO 2025-04-02 08:57:48,497 client 120113 140010024801984 Successfully obtained access token 199 | INFO 2025-04-02 08:57:48,499 client 120113 140010024801984 Sending STK push request with payload: {"BusinessShortCode": "174379", "Password": "MTc0Mzc5YmZiMjc5ZjlhYTliZGJjZjE1OGU5N2RkNzFhNDY3Y2QyZTBjODkzMDU5YjEwZjc4ZTZiNzJhZGExZWQyYzkxOTIwMjUwNDAyMDg1NzQ4", "Timestamp": "20250402085748", "TransactionType": "CustomerPayBillOnline", "Amount": 5, "PartyA": "254111304249", "PartyB": "174379", "PhoneNumber": "254111304249", "CallBackURL": "https://yourdomain.com/payment-callback/", "AccountReference": "WIFI-TDQ8TLH7", "TransactionDesc": "WIFI voucher 0.5hrs"} 200 | INFO 2025-04-02 08:57:49,754 client 120113 140010024801984 Raw response status: 200 201 | INFO 2025-04-02 08:57:49,755 client 120113 140010024801984 Raw response headers: {'content-type': 'application/json;charset=UTF-8', 'cache-control': 'no-store', 'x-request-id': '6adbc4ff-e817-48e8-babc-e70fb54df77e', 'date': 'Wed, 02 Apr 2025 08:57:49 GMT', 'Set-Cookie': 'visid_incap_2742146=i94bqurCRS65OjaUuWMZtQv87GcAAAAAQUIPAAAAAAAzFc8kRH1wFTWt9+hx0bUy; expires=Wed, 01 Apr 2026 15:29:58 GMT; HttpOnly; path=/; Domain=.safaricom.co.ke; Secure; SameSite=None, incap_ses_6553_2742146=eJ4qZxkhcgg0MQsHHu/wWg387GcAAAAALKC3X5W0E7sPtuja6EA7Vw==; path=/; Domain=.safaricom.co.ke; Secure; SameSite=None', 'Strict-Transport-Security': 'max-age=31536000', 'X-CDN': 'Imperva', 'Content-Encoding': 'gzip', 'Transfer-Encoding': 'chunked', 'X-Iinfo': '3-1324296-1324298 NNYN CT(86 87 0) RT(1743584268696 117) q(0 0 1 0) r(9 9) U6'} 202 | INFO 2025-04-02 08:57:49,756 client 120113 140010024801984 Raw response body: { 203 | "MerchantRequestID":"8131-4f68-ab7a-5e68879d780e264807", 204 | "CheckoutRequestID":"ws_CO_02042025115925685111304249", 205 | "ResponseCode": "0", 206 | "ResponseDescription":"Success. Request accepted for processing", 207 | "CustomerMessage":"Success. Request accepted for processing" 208 | } 209 | 210 | INFO 2025-04-02 08:57:49,758 client 120113 140010024801984 STK push initiated successfully. CheckoutRequestID: ws_CO_02042025115925685111304249 211 | ERROR 2025-04-02 08:57:49,845 views 120113 140010024801984 Unexpected error during payment: table wifi_auth_paymenttransaction has no column named created_at 212 | INFO 2025-04-02 08:57:59,563 client 120113 140010024801984 Initialized M-Pesa client with base URL: https://sandbox.safaricom.co.ke 213 | INFO 2025-04-02 08:57:59,564 client 120113 140010024801984 Business Short Code: 174379 214 | INFO 2025-04-02 08:57:59,564 client 120113 140010024801984 Callback URL: https://yourdomain.com/payment-callback/ 215 | INFO 2025-04-02 08:57:59,564 views 120113 140010024801984 Initiating STK push: Phone=254111304249, Amount=5, Duration=0.5hrs 216 | INFO 2025-04-02 08:57:59,564 client 120113 140010024801984 Initiating STK push for phone: 254111304249, amount: 5 217 | INFO 2025-04-02 08:57:59,564 client 120113 140010024801984 Requesting access token from M-Pesa API 218 | INFO 2025-04-02 08:58:00,509 client 120113 140010024801984 Successfully obtained access token 219 | INFO 2025-04-02 08:58:00,512 client 120113 140010024801984 Sending STK push request with payload: {"BusinessShortCode": "174379", "Password": "MTc0Mzc5YmZiMjc5ZjlhYTliZGJjZjE1OGU5N2RkNzFhNDY3Y2QyZTBjODkzMDU5YjEwZjc4ZTZiNzJhZGExZWQyYzkxOTIwMjUwNDAyMDg1ODAw", "Timestamp": "20250402085800", "TransactionType": "CustomerPayBillOnline", "Amount": 5, "PartyA": "254111304249", "PartyB": "174379", "PhoneNumber": "254111304249", "CallBackURL": "https://yourdomain.com/payment-callback/", "AccountReference": "WIFI-ITJV42H2", "TransactionDesc": "WIFI voucher 0.5hrs"} 220 | INFO 2025-04-02 08:58:01,214 client 120113 140010024801984 Raw response status: 500 221 | INFO 2025-04-02 08:58:01,215 client 120113 140010024801984 Raw response headers: {'content-type': 'application/json;charset=UTF-8', 'cache-control': 'no-store', 'x-request-id': '165e1c0d-6542-4cda-a4af-f3dd39c3aaa5', 'date': 'Wed, 02 Apr 2025 08:58:00 GMT', 'Set-Cookie': 'visid_incap_2742146=/TCOMYL3TTy8Mai1Lx/+FRj87GcAAAAAQUIPAAAAAADpVNEPVbq2ozElQgoqp1lR; expires=Wed, 01 Apr 2026 15:29:57 GMT; HttpOnly; path=/; Domain=.safaricom.co.ke; Secure; SameSite=None, incap_ses_6553_2742146=I4bqSthCXQdfRwsHHu/wWhj87GcAAAAAQQwEoRnMWoEchj7+LwfMZQ==; path=/; Domain=.safaricom.co.ke; Secure; SameSite=None', 'Strict-Transport-Security': 'max-age=31536000', 'X-CDN': 'Imperva', 'Content-Encoding': 'gzip', 'Transfer-Encoding': 'chunked', 'X-Iinfo': '18-29214539-29214571 NNYN CT(91 87 0) RT(1743584280726 106) q(0 0 2 -1) r(3 3) U6'} 222 | INFO 2025-04-02 08:58:01,215 client 120113 140010024801984 Raw response body: { 223 | "requestId":"8131-4f68-ab7a-5e68879d780e264814", 224 | "errorCode": "500.001.1001", 225 | "errorMessage": "Unable to lock subscriber, a transaction is already in process for the current subscriber" 226 | } 227 | 228 | ERROR 2025-04-02 08:58:01,215 client 120113 140010024801984 Network error in STK push: 500 Server Error: Internal Server Error for url: https://sandbox.safaricom.co.ke/mpesa/stkpush/v1/processrequest 229 | ERROR 2025-04-02 08:58:01,319 views 120113 140010024801984 Unexpected error during payment: Failed to connect to M-Pesa API. Please check your internet connection. 230 | INFO 2025-04-02 09:06:07,902 client 121136 140525340956352 Initialized M-Pesa client with base URL: https://sandbox.safaricom.co.ke 231 | INFO 2025-04-02 09:06:07,949 client 121136 140525340956352 Business Short Code: 174379 232 | INFO 2025-04-02 09:06:07,950 client 121136 140525340956352 Callback URL: https://yourdomain.com/payment-callback/ 233 | INFO 2025-04-02 09:06:07,950 views 121136 140525340956352 Initiating STK push: Phone=254111304249, Amount=5, Duration=0.5hrs 234 | INFO 2025-04-02 09:06:07,950 client 121136 140525340956352 Initiating STK push for phone: 254111304249, amount: 5 235 | INFO 2025-04-02 09:06:07,951 client 121136 140525340956352 Requesting access token from M-Pesa API 236 | INFO 2025-04-02 09:06:16,368 client 121136 140525340956352 Successfully obtained access token 237 | INFO 2025-04-02 09:06:16,369 client 121136 140525340956352 Sending STK push request with payload: {"BusinessShortCode": "174379", "Password": "MTc0Mzc5YmZiMjc5ZjlhYTliZGJjZjE1OGU5N2RkNzFhNDY3Y2QyZTBjODkzMDU5YjEwZjc4ZTZiNzJhZGExZWQyYzkxOTIwMjUwNDAyMDkwNjE2", "Timestamp": "20250402090616", "TransactionType": "CustomerPayBillOnline", "Amount": 5, "PartyA": "254111304249", "PartyB": "174379", "PhoneNumber": "254111304249", "CallBackURL": "https://yourdomain.com/payment-callback/", "AccountReference": "WIFI-Z5EIWW7F", "TransactionDesc": "WIFI voucher 0.5hrs"} 238 | INFO 2025-04-02 09:06:23,316 client 121136 140525340956352 Raw response status: 200 239 | INFO 2025-04-02 09:06:23,316 client 121136 140525340956352 Raw response headers: {'content-type': 'application/json;charset=UTF-8', 'cache-control': 'no-store', 'x-request-id': 'f3bfaa0b-d5ef-4153-bc68-2a2528e48bfc', 'date': 'Wed, 02 Apr 2025 09:06:22 GMT', 'Set-Cookie': 'visid_incap_2742146=rWy4w5c/SESUQAuWozINAw7+7GcAAAAAQUIPAAAAAAAsZsactJdPOoVYxTgM4oRm; expires=Wed, 01 Apr 2026 15:29:57 GMT; HttpOnly; path=/; Domain=.safaricom.co.ke; Secure; SameSite=None, incap_ses_6553_2742146=nfe/JZ0EuTbbxg4HHu/wWg7+7GcAAAAAO4ZNB3Y2FTI0H9diJT7B3g==; path=/; Domain=.safaricom.co.ke; Secure; SameSite=None', 'Strict-Transport-Security': 'max-age=31536000', 'X-CDN': 'Imperva', 'Content-Encoding': 'gzip', 'Transfer-Encoding': 'chunked', 'X-Iinfo': '18-29309368-29310318 NNYN CT(183 191 0) RT(1743584777216 5010) q(0 0 4 -1) r(10 10) U6'} 240 | INFO 2025-04-02 09:06:23,316 client 121136 140525340956352 Raw response body: { 241 | "MerchantRequestID":"aa10-4361-b7cd-2aa0e34ed7e469627", 242 | "CheckoutRequestID":"ws_CO_02042025120621367111304249", 243 | "ResponseCode": "0", 244 | "ResponseDescription":"Success. Request accepted for processing", 245 | "CustomerMessage":"Success. Request accepted for processing" 246 | } 247 | 248 | INFO 2025-04-02 09:06:23,316 client 121136 140525340956352 STK push initiated successfully. CheckoutRequestID: ws_CO_02042025120621367111304249 249 | ERROR 2025-04-02 09:06:23,409 views 121136 140525340956352 Unexpected error during payment: table wifi_auth_paymenttransaction has no column named created_at 250 | INFO 2025-04-02 20:36:24,217 client 161548 140280621385408 Initialized M-Pesa client with base URL: https://sandbox.safaricom.co.ke 251 | INFO 2025-04-02 20:36:24,382 client 161548 140280621385408 Business Short Code: 174379 252 | INFO 2025-04-02 20:36:24,383 client 161548 140280621385408 Callback URL: https://yourdomain.com/payment-callback/ 253 | INFO 2025-04-02 20:36:24,383 views 161548 140280621385408 Initiating STK push: Phone=254111304249, Amount=5, Duration=0.5hrs 254 | INFO 2025-04-02 20:36:24,386 client 161548 140280621385408 Initiating STK push for phone: 254111304249, amount: 5 255 | INFO 2025-04-02 20:36:24,386 client 161548 140280621385408 Requesting access token from M-Pesa API 256 | INFO 2025-04-02 20:36:29,491 client 161548 140280621385408 Successfully obtained access token 257 | INFO 2025-04-02 20:36:29,499 client 161548 140280621385408 Sending STK push request with payload: {"BusinessShortCode": "174379", "Password": "MTc0Mzc5YmZiMjc5ZjlhYTliZGJjZjE1OGU5N2RkNzFhNDY3Y2QyZTBjODkzMDU5YjEwZjc4ZTZiNzJhZGExZWQyYzkxOTIwMjUwNDAyMjAzNjI5", "Timestamp": "20250402203629", "TransactionType": "CustomerPayBillOnline", "Amount": 5, "PartyA": "254111304249", "PartyB": "174379", "PhoneNumber": "254111304249", "CallBackURL": "https://yourdomain.com/payment-callback/", "AccountReference": "WIFI-D6S6TBCI", "TransactionDesc": "WIFI voucher 0.5hrs"} 258 | INFO 2025-04-02 20:36:31,244 client 161548 140280621385408 Raw response status: 200 259 | INFO 2025-04-02 20:36:31,244 client 161548 140280621385408 Raw response headers: {'content-type': 'application/json;charset=UTF-8', 'cache-control': 'no-store', 'x-request-id': '178f7839-760b-4164-aaef-e1ec99299d04', 'date': 'Wed, 02 Apr 2025 20:36:30 GMT', 'Set-Cookie': 'visid_incap_2742146=/0bdOoUWRImsibNULkiUwsyf7WcAAAAAQUIPAAAAAADTRj7YguX8kTeMMpVkezdj; expires=Thu, 02 Apr 2026 17:15:19 GMT; HttpOnly; path=/; Domain=.safaricom.co.ke; Secure; SameSite=None, incap_ses_6552_2742146=esyEdbhRPwSg3ecmemHtWs6f7WcAAAAAxJZJtBjURJmo9N5dFFng8g==; path=/; Domain=.safaricom.co.ke; Secure; SameSite=None', 'Strict-Transport-Security': 'max-age=31536000', 'X-CDN': 'Imperva', 'Content-Encoding': 'gzip', 'Transfer-Encoding': 'chunked', 'X-Iinfo': '15-35243042-35243045 NNYN CT(80 82 0) RT(1743626190168 151) q(0 0 2 0) r(5 5) U6'} 260 | INFO 2025-04-02 20:36:31,245 client 161548 140280621385408 Raw response body: { 261 | "MerchantRequestID":"c20c-4eb8-8161-40af4f84ee3960461", 262 | "CheckoutRequestID":"ws_CO_02042025233807410111304249", 263 | "ResponseCode": "0", 264 | "ResponseDescription":"Success. Request accepted for processing", 265 | "CustomerMessage":"Success. Request accepted for processing" 266 | } 267 | 268 | INFO 2025-04-02 20:36:31,245 client 161548 140280621385408 STK push initiated successfully. CheckoutRequestID: ws_CO_02042025233807410111304249 269 | ERROR 2025-04-02 20:36:31,697 views 161548 140280621385408 Unexpected error during payment: table wifi_auth_paymenttransaction has no column named created_at 270 | INFO 2025-04-02 20:36:45,400 client 161548 140280621385408 Initialized M-Pesa client with base URL: https://sandbox.safaricom.co.ke 271 | INFO 2025-04-02 20:36:45,401 client 161548 140280621385408 Business Short Code: 174379 272 | INFO 2025-04-02 20:36:45,401 client 161548 140280621385408 Callback URL: https://yourdomain.com/payment-callback/ 273 | INFO 2025-04-02 20:36:45,402 views 161548 140280621385408 Initiating STK push: Phone=254111304249, Amount=5, Duration=0.5hrs 274 | INFO 2025-04-02 20:36:45,402 client 161548 140280621385408 Initiating STK push for phone: 254111304249, amount: 5 275 | INFO 2025-04-02 20:36:45,403 client 161548 140280621385408 Requesting access token from M-Pesa API 276 | INFO 2025-04-02 20:36:46,124 client 161548 140280621385408 Successfully obtained access token 277 | INFO 2025-04-02 20:36:46,131 client 161548 140280621385408 Sending STK push request with payload: {"BusinessShortCode": "174379", "Password": "MTc0Mzc5YmZiMjc5ZjlhYTliZGJjZjE1OGU5N2RkNzFhNDY3Y2QyZTBjODkzMDU5YjEwZjc4ZTZiNzJhZGExZWQyYzkxOTIwMjUwNDAyMjAzNjQ2", "Timestamp": "20250402203646", "TransactionType": "CustomerPayBillOnline", "Amount": 5, "PartyA": "254111304249", "PartyB": "174379", "PhoneNumber": "254111304249", "CallBackURL": "https://yourdomain.com/payment-callback/", "AccountReference": "WIFI-QHILSI1D", "TransactionDesc": "WIFI voucher 0.5hrs"} 278 | INFO 2025-04-02 20:36:47,130 client 161548 140280621385408 Raw response status: 200 279 | INFO 2025-04-02 20:36:47,132 client 161548 140280621385408 Raw response headers: {'content-type': 'application/json;charset=UTF-8', 'cache-control': 'no-store', 'x-request-id': 'f2f44512-e68b-433f-8b88-46f2da3ce10e', 'date': 'Wed, 02 Apr 2025 20:36:46 GMT', 'Set-Cookie': 'visid_incap_2742146=7PY5SGniRIi6N9ZXZCdA2t6f7WcAAAAAQUIPAAAAAAATVQhO3gwablGUvdAeC3xJ; expires=Thu, 02 Apr 2026 17:15:19 GMT; HttpOnly; path=/; Domain=.safaricom.co.ke; Secure; SameSite=None, incap_ses_6552_2742146=5NdBIG0AokPy7ecmemHtWt6f7WcAAAAALkuy76rCNS2QpNJMjVPewQ==; path=/; Domain=.safaricom.co.ke; Secure; SameSite=None', 'Strict-Transport-Security': 'max-age=31536000', 'X-CDN': 'Imperva', 'Content-Encoding': 'gzip', 'Transfer-Encoding': 'chunked', 'X-Iinfo': '18-67691510-67691529 NNYN CT(83 84 0) RT(1743626206426 146) q(0 0 1 -1) r(5 5) U6'} 280 | INFO 2025-04-02 20:36:47,133 client 161548 140280621385408 Raw response body: { 281 | "MerchantRequestID":"f6d6-4b6d-9f75-1a54a0b818ca62574", 282 | "CheckoutRequestID":"ws_CO_02042025233646843111304249", 283 | "ResponseCode": "0", 284 | "ResponseDescription":"Success. Request accepted for processing", 285 | "CustomerMessage":"Success. Request accepted for processing" 286 | } 287 | 288 | INFO 2025-04-02 20:36:47,135 client 161548 140280621385408 STK push initiated successfully. CheckoutRequestID: ws_CO_02042025233646843111304249 289 | ERROR 2025-04-02 20:36:47,342 views 161548 140280621385408 Unexpected error during payment: table wifi_auth_paymenttransaction has no column named created_at 290 | --------------------------------------------------------------------------------