# App/Messages/Routes.py from fastapi import APIRouter, HTTPException, Request from .Model import Message from .Schema import MessageCreate, MessageResponse, TransactionSchema from uuid import UUID from typing import List, Optional import re from datetime import datetime from decimal import Decimal from App.Payments.PaymentsRoutes import create_payment from App.Payments.Schema import CreatePaymentRequest from App.Users.Model import User from App.Payments.Schema import PaymentMethod from App.Payments.Model import Payment from App.Plans.Model import Plan from tortoise.contrib.pydantic import pydantic_model_creator message_router = APIRouter(tags=["Messages"], prefix="/messages") @message_router.post("/sms_received", response_model=MessageResponse) async def receive_message(message_data: MessageCreate): message: Message = Message.get_or_none(message_id=message_data.id) Message_Pydantic = pydantic_model_creator(Message) try: # Extract data from the message content using regex text = message_data.payload.message parsed_data = parse_message_content(text) # Process Mpesa Payments and prevent double entry from the sms gateway app (message_id must be unique) if parsed_data and not message: user: User = User.get_or_none(phoneNumber=parsed_data.phone_number) # if the user sent an exact amount data_plan: Plan = Plan.get_or_none( amount=Decimal(parsed_data.amount_received) ) payment_details = CreatePaymentRequest( user_id=user.id, plan_id=data_plan.id, amount=Decimal(parsed_data.amount_received), payment_method=PaymentMethod.MPESA, transaction_id=parsed_data.transaction_id, ) payment: Payment = await create_payment(payment_details) await payment.create_subscription_or_balance() # prevent double entries if not message: # Create a new message record with parsed_data message = await Message.create( device_id=message_data.deviceId, event=message_data.event, message_id=message_data.id, webhook_id=message_data.webhookId, message_content=text, phone_number=message_data.payload.phoneNumber, received_at=message_data.payload.receivedAt, sim_number=message_data.payload.simNumber, parsed_data=parsed_data, ) if type(message) == Message: data = Message_Pydantic.from_tortoise_orm(message) else: data = Message_Pydantic.from_queryset_single(message) return data except Exception as e: raise HTTPException(status_code=400, detail=str(e)) def parse_message_content(text: str) -> Optional[TransactionSchema]: # Regular expression to capture the data from the message, allowing for different types of spaces pattern = r"(\w+)\sConfirmed\.You have received Tsh([\d,]+\.\d{2}) from (\d{12}) - ([A-Z ]+) on (\d{1,2}/\d{1,2}/\d{2}) at ([\d:]+ [APM]+).*?balance\sis\sTsh([\d,]+\.\d{2})" # Replace non-breaking spaces and other whitespace characters with regular spaces in the input text text = re.sub(r"\s+", " ", text) matches: TransactionSchema = re.search(pattern, text) if matches: data = { "transaction_id": matches.group(1), "amount_received": parse_decimal(matches.group(2)), "phone_number": matches.group(3), "name": matches.group(4).strip(), "date": parse_date(matches.group(5), matches.group(6)), "new_balance": parse_decimal(matches.group(7)), } return data else: # Return None if the message doesn't match the expected format return None def parse_decimal(amount_str: str) -> float: # Remove commas and convert to Decimal amount_str = amount_str.replace(",", "") return float(Decimal(amount_str)) def parse_date(date_str: str, time_str: str) -> str: # Combine date and time strings and parse into ISO format datetime_str = f"{date_str} {time_str}" try: dt = datetime.strptime(datetime_str, "%d/%m/%y %I:%M %p") return dt.isoformat() except ValueError: return datetime_str # Return as-is if parsing fails