nice
Browse files- App/Messages/MessagesRoute.py +55 -30
- App/Payments/Model.py +12 -20
- App/Payments/PaymentsRoutes.py +4 -1
App/Messages/MessagesRoute.py
CHANGED
@@ -1,9 +1,8 @@
|
|
1 |
# App/Messages/Routes.py
|
2 |
-
from fastapi import APIRouter, HTTPException
|
3 |
from .Model import Message
|
4 |
-
from .Schema import MessageCreate, MessageResponse
|
5 |
-
from
|
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 |
-
#
|
31 |
-
if
|
32 |
-
|
|
|
|
|
33 |
|
34 |
-
|
35 |
-
|
36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
37 |
)
|
38 |
payment_details = CreatePaymentRequest(
|
39 |
user_id=user.id,
|
40 |
-
plan_id=data_plan.id,
|
41 |
-
amount=Decimal(parsed_data
|
42 |
payment_method=PaymentMethod.MPESA,
|
43 |
-
transaction_id=parsed_data
|
44 |
)
|
45 |
|
46 |
-
|
|
|
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=
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
72 |
|
73 |
|
74 |
-
def parse_message_content(text: str) -> Optional[
|
75 |
-
# Regular expression to capture the data from the message
|
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
|
79 |
text = re.sub(r"\s+", " ", text)
|
80 |
|
81 |
-
matches
|
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
|
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 |
-
|
|
|
|
|
|
|
|
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,
|
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=
|
83 |
duration=self.plan.duration,
|
84 |
-
download_mb=self.plan.download_speed
|
85 |
-
* 1024,
|
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 |
-
|
97 |
-
|
98 |
-
await self.user
|
99 |
-
|
|
|
|
|
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()
|