Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,9 @@ __pycache__/
env/
__pycache__/
*.pyc
<<<<<<< HEAD
db.sqlite3

.env
=======
>>>>>>> 27f594c97619937ede2206d41e9d6f64e2e6c72b
Empty file added =0.21.0
Empty file.
63 changes: 63 additions & 0 deletions api/daraja.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import requests
from django.conf import settings
from requests.auth import HTTPBasicAuth
import base64
import datetime


class DarajaAPI:
def __init__(self):
self.consumer_key = settings.DARAJA_CONSUMER_KEY
self.consumer_secret = settings.DARAJA_CONSUMER_SECRET
self.base_url = settings.DARAJA_BASE_URL
self.callback_url = settings.DARAJA_CALLBACK_URL
self.passkey = settings.DARAJA_PASSKEY
self.business_shortcode = settings.DARAJA_BUSINESS_SHORTCODE
self.merchant_request_id = settings.DARAJA_MERCHANT_REQUEST_ID

def get_access_token(self):
url = f"{self.base_url}oauth/v1/generate?grant_type=client_credentials"
response = requests.get(
url, auth=HTTPBasicAuth(self.consumer_key, self.consumer_secret)
)
response.raise_for_status()
access_token = response.json().get("access_token")
return access_token

def ussd_push(self, amount, phone_number, account_reference, transaction_desc):
timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
data_to_encode = (
str(self.business_shortcode) + str(self.passkey) + str(timestamp)
)
password = base64.b64encode(data_to_encode.encode()).decode("utf-8")
access_token = self.get_access_token()
url = f"{self.base_url}mpesa/stkpush/v1/processrequest"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
}
data = {
"BusinessShortCode": self.business_shortcode,
"Password": password,
"Timestamp": timestamp,
"TransactionType": "CustomerPayBillOnline",
"Amount": amount,
"PartyA": phone_number,
"PartyB": self.business_shortcode,
"PhoneNumber": phone_number,
"CallBackURL": self.callback_url,
"AccountReference": account_reference,
"TransactionDesc": transaction_desc,
}
response = requests.post(url, headers=headers, json=data)
if response.status_code != 200:
print(f"--- Daraja API Error Details ---")
print(f"Status Code: {response.status_code}")
print(f"Response Body: {response.text}")
try:
print(f"Response JSON: {response.json()}")
except requests.exceptions.JSONDecodeError:
print("Response body is not valid JSON.")
print(f"--- End of Error Details ---")
response.raise_for_status()
return response.json()
37 changes: 31 additions & 6 deletions api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,37 @@
from rest_framework import serializers
from payment.models import Payment
from rest_framework import serializers
from orders.models import Order, OrderItem, WasteClaim
from inventory.models import Listing
from user.models import User
from django.contrib.auth import get_user_model, authenticate
from location.models import UserLocation
from location.utils import geocode_address


class PaymentSerializer(serializers.ModelSerializer):
class Meta:
model = Payment
fields = "__all__"
read_only_fields = (
'transaction_id',
'mpesa_receipt_number',
'checkout_request_id',
'merchant_request_id',
'result_code',
'result_desc',
'phone_number',
'created_at'

)

class USSDPUSHSerializer(serializers.Serializer):
phone_number = serializers.CharField()
amount = serializers.DecimalField(max_digits=10, decimal_places=2)
account_reference = serializers.CharField()
transaction_desc = serializers.CharField()


class ListingSerializer(serializers.ModelSerializer):
class Meta:
model = Listing
Expand Down Expand Up @@ -64,12 +95,6 @@ class Meta:
fields = ['waste_id', 'listing_id', 'claim_time', 'claim_status', 'pin', 'created_at', 'updated_at']
read_only_fields = ['waste_id','listing_id', 'pin', 'created_at', 'updated_at']


from user.models import User
from django.contrib.auth import get_user_model, authenticate
from location.models import UserLocation
from location.utils import geocode_address
from inventory.models import Listing


class UserLoginSerializer(serializers.Serializer):
Expand Down
5 changes: 4 additions & 1 deletion api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from django.urls import path,include
from rest_framework.routers import DefaultRouter
from .views import OrderViewSet, WasteClaimViewSet, OrderItemViewSet,ListingViewSet, ListingCSVUploadView
from .views import OrderViewSet, WasteClaimViewSet, OrderItemViewSet,ListingViewSet, ListingCSVUploadView, USSDPUSHView, PaymentViewSet, mpesa_ussd_callback

from .views import (
UserViewSet,
Expand All @@ -20,6 +20,7 @@
router.register(r'wasteclaims', WasteClaimViewSet)
router.register(r'users', UserViewSet, basename='user')
router.register(r'listings', ListingViewSet, basename='listings')
router.register(r"payments", PaymentViewSet, basename='payments')


urlpatterns = [
Expand All @@ -30,6 +31,8 @@
path('verification/', VerifyCodeView.as_view(), name='verify-code'),
path('reset/', ResetPasswordView.as_view(), name='reset-password'),
path('listings/upload-csv/', ListingCSVUploadView.as_view(), name='listing-csv-upload'),
path("ussdpush", USSDPUSHView.as_view(), name="ussdpush"),
path("mpesa/callback", mpesa_ussd_callback, name="mpesa_callback"),
path('', include(router.urls)),

]
Expand Down
89 changes: 79 additions & 10 deletions api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from rest_framework.response import Response
from inventory.models import Listing
from rest_framework import generics, status

import csv
from io import TextIOWrapper
import random
Expand All @@ -19,7 +18,6 @@
from rest_framework.authtoken.models import Token
from location.models import UserLocation
from user.models import User

from .serializers import (
UserSerializer,
UserSignupSerializer,
Expand All @@ -28,6 +26,12 @@
ResetPasswordSerializer,
ListingSerializer
)
from django.shortcuts import render
from .serializers import USSDPUSHSerializer, PaymentSerializer
from .daraja import DarajaAPI
from rest_framework.decorators import api_view, APIView
from payment.models import Payment
import datetime


otp_storage = {}
Expand Down Expand Up @@ -135,10 +139,7 @@ def get_queryset(self):
if order_id:
return self.queryset.filter(order_id=order_id)
return self.queryset





class WasteClaimViewSet(viewsets.ModelViewSet):
queryset = WasteClaim.objects.all()
serializer_class = WasteClaimSerializer
Expand All @@ -153,9 +154,6 @@ def perform_create(self, serializer):
serializer.save(claim_time=timezone.now())





class ListingViewSet(viewsets.ModelViewSet):
queryset = Listing.objects.all()
serializer_class = ListingSerializer
Expand Down Expand Up @@ -191,4 +189,75 @@ def post(self, request):
return Response({
"listings": listings_created
}, status=status.HTTP_201_CREATED)



class PaymentViewSet(viewsets.ModelViewSet):
queryset = Payment.objects.all()
serializer_class = PaymentSerializer

class USSDPUSHView(APIView):
def post(self, request):
serializer = USSDPUSHSerializer(data=request.data)
if serializer.is_valid():
data = serializer.validated_data
daraja = DarajaAPI()
ussd_response = daraja.ussd_push(
phone_number=data["phone_number"],
amount=float(data["amount"]),
account_reference=data["account_reference"],
transaction_desc=data["transaction_desc"],
)
merchant_request_id = ussd_response.get("MerchantRequestID")
checkout_request_id = ussd_response.get("CheckoutRequestID")
print("Saved checkout requestID:", checkout_request_id)
Payment.objects.create(
amount=float(data["amount"]),
merchant_request_id=merchant_request_id,
checkout_request_id=checkout_request_id,
status="PENDING"
)
return Response(
{
"message": "USSD Push initiated, check your phone to complete the payment.",
"response": ussd_response,
},
status=status.HTTP_200_OK,
)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

@api_view(["POST"])
def mpesa_ussd_callback(request):
print("Daraja Callback Data:", request.data)
body = request.data.get("Body", {})
stk_callback = body.get("stkCallback", {})
merchant_request_id = stk_callback.get("MerchantRequestID")
checkout_request_id = stk_callback.get("CheckoutRequestID")
result_code = int(stk_callback.get("ResultCode", 1))
result_desc = stk_callback.get("ResultDesc")
try:
payment = Payment.objects.get(checkout_request_id=checkout_request_id)
except Payment.DoesNotExist:
return Response({"error": "Payment not found"}, status=404)
payment.merchant_request_id = merchant_request_id
payment.result_code = result_code
payment.result_desc = result_desc
receipt_url = None
if result_code == 0:
metadata = stk_callback.get("CallbackMetadata", {}).get("Item", [])
parsed_metadata = {item["Name"]: item.get("Value") for item in metadata}
payment.mpesa_receipt_number = parsed_metadata.get("MpesaReceiptNumber")
payment.amount = parsed_metadata.get("Amount", payment.amount)
txn_date = parsed_metadata.get("TransactionDate")
if txn_date:
txn_date_str = str(txn_date)
payment.payment_date = datetime.strptime(txn_date_str, "%Y%m%d%H%M%S")
payment.status = "SUCCESS"
receipt_url = payment.generate_receipt()
else:
payment.status = "FAILED"
payment.save()
return Response({
"ResultCode": 0,
"ResultDesc": "Callback processed successfully",
"receipt_url": receipt_url,
})
Loading