├── 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 |
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)
6 | [](https://www.python.org/)
7 | [](https://www.djangoproject.com/)
8 | [](https://developer.safaricom.co.ke/)
9 | [](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 |
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 |
13 | {{ message }}
14 |
15 |
16 | {% endfor %}
17 |
18 | {% endif %}
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
{{ user.username }}
31 |
32 | {{ user.phone_number }}
33 | {% if user.is_premium %}
34 | Premium
35 | {% else %}
36 | Standard
37 | {% endif %}
38 |
39 |
40 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
KES 5
65 |
66 | 30 Minutes
67 |
68 |
Perfect for quick browsing
69 |
Select Plan
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
KES 9
78 |
79 | 1 Hour
80 |
81 |
Our most popular option
82 |
Select Plan
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
KES 15
91 |
92 | 2 Hours
93 |
94 |
Great value for longer sessions
95 |
Select Plan
96 |
97 |
98 |
99 |
100 |
101 |
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 |
125 |
126 | {% if vouchers %}
127 |
128 |
129 |
130 |
131 | | Code |
132 | Duration |
133 | Price |
134 | Used At |
135 | Expires At |
136 |
137 |
138 |
139 | {% for voucher in vouchers %}
140 |
141 | {{ voucher.code }} |
142 | {{ voucher.duration_hours }} hours |
143 | KES {{ voucher.price }} |
144 | {{ voucher.used_at|date:"Y-m-d H:i" }} |
145 | {{ voucher.used_at|add_hours:voucher.duration_hours|date:"Y-m-d H:i" }} |
146 |
147 | {% endfor %}
148 |
149 |
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 |
15 |
16 |
Choose Your Plan
17 |
18 |
19 |
20 |
21 |
24 |
25 |
KES 5
26 |
30 Minutes
27 |
Perfect for quick browsing
28 |
29 |
30 |
31 |
32 |
33 |
36 |
37 |
KES 9
38 |
1 Hour
39 |
Our most popular option
40 |
41 |
42 |
43 |
44 |
45 |
48 |
49 |
KES 15
50 |
2 Hours
51 |
Great value for longer sessions
52 |
53 |
54 |
55 |
56 |
57 |
60 |
61 |
KES 100
62 |
24 Hours
63 |
Full day of unlimited access
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
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 |
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 |
--------------------------------------------------------------------------------