Mbonea commited on
Commit
73fedea
·
1 Parent(s): b1fe62a
App/Messages/MessagesRoute.py CHANGED
@@ -1,9 +1,8 @@
1
  # App/Messages/Routes.py
2
- from fastapi import APIRouter, HTTPException, Request
3
  from .Model import Message
4
- from .Schema import MessageCreate, MessageResponse, TransactionSchema
5
- from uuid import UUID
6
- from typing import List, Optional
7
  import re
8
  from datetime import datetime
9
  from decimal import Decimal
@@ -14,71 +13,94 @@ from App.Payments.Schema import PaymentMethod
14
  from App.Payments.Model import Payment
15
  from App.Plans.Model import Plan
16
  from tortoise.contrib.pydantic import pydantic_model_creator
 
 
17
 
18
  message_router = APIRouter(tags=["Messages"], prefix="/messages")
 
19
 
20
 
21
  @message_router.post("/sms_received", response_model=MessageResponse)
22
  async def receive_message(message_data: MessageCreate):
23
- message = Message.get_or_none(message_id=message_data.id)
24
  Message_Pydantic = pydantic_model_creator(Message)
25
  try:
26
  # Extract data from the message content using regex
27
  text = message_data.payload.message
28
  parsed_data = parse_message_content(text)
29
 
30
- # Process Mpesa Payments and prevent double entry from the sms gateway app (message_id must be unique)
31
- if parsed_data and not message:
32
- user: User = User.get_or_none(phoneNumber=parsed_data.phone_number)
 
 
33
 
34
- # if the user sent an exact amount
35
- data_plan: Plan = Plan.get_or_none(
36
- amount=Decimal(parsed_data.amount_received)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  )
38
  payment_details = CreatePaymentRequest(
39
  user_id=user.id,
40
- plan_id=data_plan.id,
41
- amount=Decimal(parsed_data.amount_received),
42
  payment_method=PaymentMethod.MPESA,
43
- transaction_id=parsed_data.transaction_id,
44
  )
45
 
46
- payment: Payment = await create_payment(payment_details)
 
47
  await payment.create_subscription_or_balance()
48
 
49
- # prevent double entries
50
- if not message:
51
  # Create a new message record with parsed_data
52
  message = await Message.create(
53
  device_id=message_data.deviceId,
54
  event=message_data.event,
55
  message_id=message_data.id,
56
  webhook_id=message_data.webhookId,
57
- message_content=text,
58
  phone_number=message_data.payload.phoneNumber,
59
  received_at=message_data.payload.receivedAt,
60
  sim_number=message_data.payload.simNumber,
61
  parsed_data=parsed_data,
62
  )
63
- print(message.__dict__)
64
- if type(message) == Message:
65
- data = await Message_Pydantic.from_tortoise_orm(message)
66
- else:
67
- data = await Message_Pydantic.from_queryset_single(message)
68
 
 
 
69
  return data
 
70
  except Exception as e:
71
- raise HTTPException(status_code=400, detail=str(e))
 
 
 
 
 
 
72
 
73
 
74
- def parse_message_content(text: str) -> Optional[TransactionSchema]:
75
- # Regular expression to capture the data from the message, allowing for different types of spaces
76
  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})"
77
 
78
- # Replace non-breaking spaces and other whitespace characters with regular spaces in the input text
79
  text = re.sub(r"\s+", " ", text)
80
 
81
- matches: TransactionSchema = re.search(pattern, text)
82
  if matches:
83
  data = {
84
  "transaction_id": matches.group(1),
@@ -95,7 +117,7 @@ def parse_message_content(text: str) -> Optional[TransactionSchema]:
95
 
96
 
97
  def parse_decimal(amount_str: str) -> float:
98
- # Remove commas and convert to Decimal
99
  amount_str = amount_str.replace(",", "")
100
  return float(Decimal(amount_str))
101
 
@@ -107,4 +129,7 @@ def parse_date(date_str: str, time_str: str) -> str:
107
  dt = datetime.strptime(datetime_str, "%d/%m/%y %I:%M %p")
108
  return dt.isoformat()
109
  except ValueError:
110
- return datetime_str # Return as-is if parsing fails
 
 
 
 
1
  # App/Messages/Routes.py
2
+ from fastapi import APIRouter, HTTPException
3
  from .Model import Message
4
+ from .Schema import MessageCreate, MessageResponse
5
+ from typing import Optional
 
6
  import re
7
  from datetime import datetime
8
  from decimal import Decimal
 
13
  from App.Payments.Model import Payment
14
  from App.Plans.Model import Plan
15
  from tortoise.contrib.pydantic import pydantic_model_creator
16
+ import traceback
17
+ import logging
18
 
19
  message_router = APIRouter(tags=["Messages"], prefix="/messages")
20
+ logging.basicConfig(level=logging.WARNING)
21
 
22
 
23
  @message_router.post("/sms_received", response_model=MessageResponse)
24
  async def receive_message(message_data: MessageCreate):
 
25
  Message_Pydantic = pydantic_model_creator(Message)
26
  try:
27
  # Extract data from the message content using regex
28
  text = message_data.payload.message
29
  parsed_data = parse_message_content(text)
30
 
31
+ # Validate parsed_data
32
+ if not parsed_data:
33
+ raise HTTPException(
34
+ status_code=400, detail="Failed to parse message content."
35
+ )
36
 
37
+ required_keys = ["phone_number", "amount_received", "transaction_id"]
38
+ missing_keys = [key for key in required_keys if key not in parsed_data]
39
+ if missing_keys:
40
+ raise HTTPException(
41
+ status_code=400, detail=f"Missing keys in parsed data: {missing_keys}"
42
+ )
43
+
44
+ # Prevent double entry from the SMS gateway app
45
+ message = await Message.get_or_none(message_id=message_data.id)
46
+ if not message:
47
+ # Process Mpesa Payments
48
+ user: User = await User.get_or_none(
49
+ phoneNumber="+" + parsed_data["phone_number"]
50
+ )
51
+ if not user:
52
+ raise HTTPException(status_code=404, detail="User not found.")
53
+
54
+ data_plan: Plan = await Plan.get_or_none(
55
+ amount=Decimal(parsed_data["amount_received"])
56
  )
57
  payment_details = CreatePaymentRequest(
58
  user_id=user.id,
59
+ plan_id=str(data_plan.id) if data_plan else None,
60
+ amount=Decimal(parsed_data["amount_received"]),
61
  payment_method=PaymentMethod.MPESA,
62
+ transaction_id=parsed_data["transaction_id"],
63
  )
64
 
65
+ # Ensure 'create_payment' is an async function
66
+ payment: Payment = await create_payment(payment_details, internal=True)
67
  await payment.create_subscription_or_balance()
68
 
 
 
69
  # Create a new message record with parsed_data
70
  message = await Message.create(
71
  device_id=message_data.deviceId,
72
  event=message_data.event,
73
  message_id=message_data.id,
74
  webhook_id=message_data.webhookId,
75
+ message_content=message_data.payload.message,
76
  phone_number=message_data.payload.phoneNumber,
77
  received_at=message_data.payload.receivedAt,
78
  sim_number=message_data.payload.simNumber,
79
  parsed_data=parsed_data,
80
  )
 
 
 
 
 
81
 
82
+ # Serialize the message
83
+ data = await Message_Pydantic.from_tortoise_orm(message)
84
  return data
85
+
86
  except Exception as e:
87
+ # Log the full traceback
88
+ traceback_str = "".join(traceback.format_exception(None, e, e.__traceback__))
89
+ logging.error(f"An error occurred: {traceback_str}")
90
+
91
+ # Provide a default error message if 'e' is empty
92
+ error_message = str(e) if str(e) else "An unexpected error occurred."
93
+ raise HTTPException(status_code=500, detail=error_message)
94
 
95
 
96
+ def parse_message_content(text: str) -> Optional[dict]:
97
+ # Regular expression to capture the data from the message
98
  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})"
99
 
100
+ # Replace non-breaking spaces and other whitespace characters with regular spaces
101
  text = re.sub(r"\s+", " ", text)
102
 
103
+ matches = re.search(pattern, text)
104
  if matches:
105
  data = {
106
  "transaction_id": matches.group(1),
 
117
 
118
 
119
  def parse_decimal(amount_str: str) -> float:
120
+ # Remove commas and convert to float
121
  amount_str = amount_str.replace(",", "")
122
  return float(Decimal(amount_str))
123
 
 
129
  dt = datetime.strptime(datetime_str, "%d/%m/%y %I:%M %p")
130
  return dt.isoformat()
131
  except ValueError:
132
+ # Log the error
133
+ logging.error(f"Failed to parse date: {datetime_str}")
134
+ # Return current time as fallback
135
+ return datetime.now().isoformat()
App/Payments/Model.py CHANGED
@@ -41,23 +41,17 @@ class Payment(Model):
41
  table = "payments"
42
 
43
  async def create_subscription_if_cash(self):
44
- """
45
- Creates a subscription for the user if the payment method is 'cash'
46
- and the user has enough balance for the specified plan.
47
- """
48
  if self.payment_method == PaymentMethod.CASH and self.user and self.plan:
49
  if self.amount >= self.plan.amount:
50
  expiration_time = datetime.datetime.now() + datetime.timedelta(
51
  hours=self.plan.duration
52
  )
53
-
54
  # Create the subscription
55
  await Subscription.create(
56
  user=self.user,
57
  duration=self.plan.duration,
58
- download_mb=self.plan.download_speed
59
- * 1024, # Converting Mbps to MB
60
- upload_mb=self.plan.upload_speed * 1024, # Converting Mbps to MB
61
  created_time=datetime.datetime.now(),
62
  expiration_time=expiration_time,
63
  active=True,
@@ -68,22 +62,18 @@ class Payment(Model):
68
  await self.save()
69
 
70
  async def create_subscription_or_balance(self):
71
- """
72
- Creates a subscription for the user if the payment method is 'cash'
73
- and the user has enough balance for the specified plan.
74
- """
75
  if self.user and self.plan:
76
  if self.amount >= self.plan.amount:
77
  expiration_time = datetime.datetime.now() + datetime.timedelta(
78
  hours=self.plan.duration
79
  )
 
80
  # Create the subscription
81
  await Subscription.create(
82
- user=self.user,
83
  duration=self.plan.duration,
84
- download_mb=self.plan.download_speed
85
- * 1024, # Converting Mbps to MB
86
- upload_mb=self.plan.upload_speed * 1024, # Converting Mbps to MB
87
  created_time=datetime.datetime.now(),
88
  expiration_time=expiration_time,
89
  active=True,
@@ -93,8 +83,10 @@ class Payment(Model):
93
  self.status = "insufficient-funds"
94
  await self.save()
95
 
96
- if not self.plan and self.user:
97
- self.user.balance += self.amount
98
- await self.user.save()
99
- self.status = "subscription-created"
 
 
100
  await self.save()
 
41
  table = "payments"
42
 
43
  async def create_subscription_if_cash(self):
 
 
 
 
44
  if self.payment_method == PaymentMethod.CASH and self.user and self.plan:
45
  if self.amount >= self.plan.amount:
46
  expiration_time = datetime.datetime.now() + datetime.timedelta(
47
  hours=self.plan.duration
48
  )
 
49
  # Create the subscription
50
  await Subscription.create(
51
  user=self.user,
52
  duration=self.plan.duration,
53
+ download_mb=self.plan.download_speed * 1024,
54
+ upload_mb=self.plan.upload_speed * 1024,
 
55
  created_time=datetime.datetime.now(),
56
  expiration_time=expiration_time,
57
  active=True,
 
62
  await self.save()
63
 
64
  async def create_subscription_or_balance(self):
 
 
 
 
65
  if self.user and self.plan:
66
  if self.amount >= self.plan.amount:
67
  expiration_time = datetime.datetime.now() + datetime.timedelta(
68
  hours=self.plan.duration
69
  )
70
+ user = await self.user
71
  # Create the subscription
72
  await Subscription.create(
73
+ user=user,
74
  duration=self.plan.duration,
75
+ download_mb=self.plan.download_speed * 1024,
76
+ upload_mb=self.plan.upload_speed * 1024,
 
77
  created_time=datetime.datetime.now(),
78
  expiration_time=expiration_time,
79
  active=True,
 
83
  self.status = "insufficient-funds"
84
  await self.save()
85
 
86
+ elif not self.plan and self.user:
87
+ # Await the related user object
88
+ user = await self.user
89
+ user.balance += self.amount
90
+ await user.save()
91
+ self.status = "balance-assigned"
92
  await self.save()
App/Payments/PaymentsRoutes.py CHANGED
@@ -17,7 +17,7 @@ payment_router = APIRouter(tags=["Payments"])
17
 
18
 
19
  @payment_router.post("/payment/create", response_model=BaseResponse)
20
- async def create_payment(request: CreatePaymentRequest):
21
  # If payment method is "Lipa Number", transaction_id is required
22
  if (
23
  request.payment_method == PaymentMethod.LIPA_NUMBER
@@ -45,6 +45,9 @@ async def create_payment(request: CreatePaymentRequest):
45
  status="pending", # Default status
46
  )
47
 
 
 
 
48
  # If payment method is "cash", attempt to create a subscription
49
  if payment.payment_method == PaymentMethod.CASH:
50
  await payment.create_subscription_if_cash()
 
17
 
18
 
19
  @payment_router.post("/payment/create", response_model=BaseResponse)
20
+ async def create_payment(request: CreatePaymentRequest, internal=False):
21
  # If payment method is "Lipa Number", transaction_id is required
22
  if (
23
  request.payment_method == PaymentMethod.LIPA_NUMBER
 
45
  status="pending", # Default status
46
  )
47
 
48
+ if internal:
49
+ return payment
50
+
51
  # If payment method is "cash", attempt to create a subscription
52
  if payment.payment_method == PaymentMethod.CASH:
53
  await payment.create_subscription_if_cash()