Mbonea commited on
Commit
7875b25
·
1 Parent(s): 73fedea

Working registration, testing payments

Browse files
App/Android/Android.py CHANGED
@@ -1,5 +1,6 @@
1
  import httpx
2
- from typing import Optional
 
3
  from .Schema import (
4
  RegisterUserRequest,
5
  LoginRequest,
@@ -10,48 +11,90 @@ from App.Portals.PortalRoutes import ANDROID
10
  from App.Portals.Model import Portal
11
 
12
 
 
 
 
 
 
 
 
 
 
 
 
13
  class AndroidClient:
14
  def __init__(self):
15
- self.base_url = "http://127.0.0.1:8080/"
16
- self.client = httpx.AsyncClient(base_url=self.base_url)
 
 
 
 
17
 
 
18
  async def register_user(self, request: RegisterUserRequest) -> APIResponse:
19
  """Register a new user."""
20
  response = await self.client.post("/users/register", json=request.dict())
21
  return APIResponse(**response.json())
22
 
 
23
  async def login_user(self, request: LoginRequest) -> APIResponse:
24
  """Login an existing user."""
25
  response = await self.client.post("/login", json=request.dict())
26
  return APIResponse(**response.json())
27
 
 
28
  async def logout_user(self, phone: str) -> APIResponse:
29
  """Logout a user by phone."""
30
  response = await self.client.post(f"/logout/{phone}")
31
  return APIResponse(**response.json())
32
 
 
33
  async def get_active_users(self) -> APIResponse:
34
  """Retrieve all active users."""
35
  response = await self.client.get("/users/active")
36
  print(response.json())
37
  return APIResponse(**response.json())
38
 
 
39
  async def set_user_status(self, request: SetUserStatusRequest) -> APIResponse:
40
  """Enable or disable a user."""
41
  response = await self.client.patch("/user/status", json=request.dict())
42
  return APIResponse(**response.json())
43
 
 
44
  async def get_users_list(self) -> APIResponse:
45
  """Retrieve all users (active and inactive)."""
46
  response = await self.client.get("/users")
47
  return APIResponse(**response.json())
48
 
 
49
  async def get_user_stats(self, phone: Optional[str] = None) -> APIResponse:
50
  """Retrieve user statistics for a specific user or all users."""
51
  url = f"/user/stats/{phone}" if phone else "/user/stats"
52
  response = await self.client.get(url)
53
  return APIResponse(**response.json())
54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  async def close(self):
56
  """Close the client session."""
57
  await self.client.aclose()
 
1
  import httpx
2
+ from typing import Optional, Union, List
3
+ from functools import wraps
4
  from .Schema import (
5
  RegisterUserRequest,
6
  LoginRequest,
 
11
  from App.Portals.Model import Portal
12
 
13
 
14
+ def require_base_url(func):
15
+ """Decorator to set base_url before making a request."""
16
+
17
+ @wraps(func)
18
+ async def wrapper(self, *args, **kwargs):
19
+ await self.set_base_url()
20
+ return await func(self, *args, **kwargs)
21
+
22
+ return wrapper
23
+
24
+
25
  class AndroidClient:
26
  def __init__(self):
27
+ self.client = httpx.AsyncClient()
28
+
29
+ async def set_base_url(self):
30
+ """Fetch the base_url from the database."""
31
+ portal = await Portal.get(name=ANDROID)
32
+ self.client.base_url = portal.url
33
 
34
+ @require_base_url
35
  async def register_user(self, request: RegisterUserRequest) -> APIResponse:
36
  """Register a new user."""
37
  response = await self.client.post("/users/register", json=request.dict())
38
  return APIResponse(**response.json())
39
 
40
+ @require_base_url
41
  async def login_user(self, request: LoginRequest) -> APIResponse:
42
  """Login an existing user."""
43
  response = await self.client.post("/login", json=request.dict())
44
  return APIResponse(**response.json())
45
 
46
+ @require_base_url
47
  async def logout_user(self, phone: str) -> APIResponse:
48
  """Logout a user by phone."""
49
  response = await self.client.post(f"/logout/{phone}")
50
  return APIResponse(**response.json())
51
 
52
+ @require_base_url
53
  async def get_active_users(self) -> APIResponse:
54
  """Retrieve all active users."""
55
  response = await self.client.get("/users/active")
56
  print(response.json())
57
  return APIResponse(**response.json())
58
 
59
+ @require_base_url
60
  async def set_user_status(self, request: SetUserStatusRequest) -> APIResponse:
61
  """Enable or disable a user."""
62
  response = await self.client.patch("/user/status", json=request.dict())
63
  return APIResponse(**response.json())
64
 
65
+ @require_base_url
66
  async def get_users_list(self) -> APIResponse:
67
  """Retrieve all users (active and inactive)."""
68
  response = await self.client.get("/users")
69
  return APIResponse(**response.json())
70
 
71
+ @require_base_url
72
  async def get_user_stats(self, phone: Optional[str] = None) -> APIResponse:
73
  """Retrieve user statistics for a specific user or all users."""
74
  url = f"/user/stats/{phone}" if phone else "/user/stats"
75
  response = await self.client.get(url)
76
  return APIResponse(**response.json())
77
 
78
+ @require_base_url
79
+ async def send_message(
80
+ self,
81
+ message: str,
82
+ phone_numbers: Union[str, List[str]],
83
+ event: str = "string",
84
+ ) -> APIResponse:
85
+ """Send a message to a single phone number or a list of phone numbers."""
86
+ # Ensure phone_numbers is a list
87
+ if isinstance(phone_numbers, str):
88
+ phone_numbers = [phone_numbers]
89
+ data = {
90
+ "message": message,
91
+ "phoneNumbers": phone_numbers,
92
+ "event": event,
93
+ }
94
+ response = await self.client.post("/send_message", json=data)
95
+ print(response)
96
+ return response
97
+
98
  async def close(self):
99
  """Close the client session."""
100
  await self.client.aclose()
App/Android/Schema.py CHANGED
@@ -6,9 +6,9 @@ from typing import Optional, Dict, Any, List
6
 
7
 
8
  class RegisterUserRequest(BaseModel):
9
- phone: str
10
  password: str
11
- profile: Optional[str] = "default"
12
 
13
 
14
  class LoginRequest(BaseModel):
 
6
 
7
 
8
  class RegisterUserRequest(BaseModel):
9
+ phoneNumber: str
10
  password: str
11
+ profile: Optional[str] = "2mbps_profile"
12
 
13
 
14
  class LoginRequest(BaseModel):
App/Messages/MessagesRoute.py CHANGED
@@ -48,14 +48,11 @@ async def receive_message(message_data: MessageCreate):
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,
 
48
  user: User = await User.get_or_none(
49
  phoneNumber="+" + parsed_data["phone_number"]
50
  )
 
 
 
51
  data_plan: Plan = await Plan.get_or_none(
52
  amount=Decimal(parsed_data["amount_received"])
53
  )
54
  payment_details = CreatePaymentRequest(
55
+ user_id=user.id if user else None,
56
  plan_id=str(data_plan.id) if data_plan else None,
57
  amount=Decimal(parsed_data["amount_received"]),
58
  payment_method=PaymentMethod.MPESA,
App/Payments/Model.py CHANGED
@@ -1,3 +1,5 @@
 
 
1
  from tortoise import fields
2
  from tortoise.models import Model
3
  import datetime
@@ -6,6 +8,17 @@ from App.Users.Model import User
6
  from App.Plans.Model import Plan
7
  from App.Subscriptions.Model import Subscription
8
  from .Schema import PaymentMethod
 
 
 
 
 
 
 
 
 
 
 
9
 
10
 
11
  class Payment(Model):
@@ -37,6 +50,10 @@ class Payment(Model):
37
  created_time = fields.DatetimeField(auto_now_add=True)
38
  updated_time = fields.DatetimeField(auto_now=True)
39
 
 
 
 
 
40
  class Meta:
41
  table = "payments"
42
 
@@ -57,9 +74,18 @@ class Payment(Model):
57
  active=True,
58
  )
59
  self.status = "subscription-created"
 
 
 
 
 
60
  else:
61
  self.status = "insufficient-funds"
62
- await self.save()
 
 
 
 
63
 
64
  async def create_subscription_or_balance(self):
65
  if self.user and self.plan:
@@ -68,6 +94,7 @@ class Payment(Model):
68
  hours=self.plan.duration
69
  )
70
  user = await self.user
 
71
  # Create the subscription
72
  await Subscription.create(
73
  user=user,
@@ -79,10 +106,18 @@ class Payment(Model):
79
  active=True,
80
  )
81
  self.status = "subscription-created"
 
 
 
 
 
82
  else:
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
@@ -90,3 +125,5 @@ class Payment(Model):
90
  await user.save()
91
  self.status = "balance-assigned"
92
  await self.save()
 
 
 
1
+ # App/Payments/Model.py
2
+
3
  from tortoise import fields
4
  from tortoise.models import Model
5
  import datetime
 
8
  from App.Plans.Model import Plan
9
  from App.Subscriptions.Model import Subscription
10
  from .Schema import PaymentMethod
11
+ from App.Android.Android import AndroidClient
12
+ from App.Templates.Templates import MessageTemplate
13
+ import logging
14
+
15
+ # Configure logging
16
+ logger = logging.getLogger(__name__)
17
+ logger.setLevel(logging.INFO)
18
+ handler = logging.StreamHandler()
19
+ formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
20
+ handler.setFormatter(formatter)
21
+ logger.addHandler(handler)
22
 
23
 
24
  class Payment(Model):
 
50
  created_time = fields.DatetimeField(auto_now_add=True)
51
  updated_time = fields.DatetimeField(auto_now=True)
52
 
53
+ # Initialize AndroidClient and MessageTemplate as class variables
54
+ android_client = AndroidClient()
55
+ message_templates = MessageTemplate()
56
+
57
  class Meta:
58
  table = "payments"
59
 
 
74
  active=True,
75
  )
76
  self.status = "subscription-created"
77
+ await self.save()
78
+ # Send payment success message with subscription details
79
+ await self.user.send_payment_success_message(
80
+ amount=self.amount, plan_name=self.plan.name
81
+ )
82
  else:
83
  self.status = "insufficient-funds"
84
+ await self.save()
85
+ # Send insufficient funds message
86
+ await self.user.send_insufficient_funds_message(
87
+ required_amount=self.plan.amount, attempted_amount=self.amount
88
+ )
89
 
90
  async def create_subscription_or_balance(self):
91
  if self.user and self.plan:
 
94
  hours=self.plan.duration
95
  )
96
  user = await self.user
97
+
98
  # Create the subscription
99
  await Subscription.create(
100
  user=user,
 
106
  active=True,
107
  )
108
  self.status = "subscription-created"
109
+ await self.save()
110
+ # Send payment success message with subscription details
111
+ await user.send_payment_success_message(
112
+ amount=self.amount, plan_name=self.plan.name
113
+ )
114
  else:
115
  self.status = "insufficient-funds"
116
+ await self.save()
117
+ # Send insufficient funds message
118
+ await user.send_insufficient_funds_message(
119
+ required_amount=self.plan.amount, attempted_amount=self.amount
120
+ )
121
  elif not self.plan and self.user:
122
  # Await the related user object
123
  user = await self.user
 
125
  await user.save()
126
  self.status = "balance-assigned"
127
  await self.save()
128
+ # Send payment success message with balance assignment
129
+ await user.send_payment_success_message(amount=self.amount)
App/Templates/Payment.md ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ Asante kwa malipo yako ya {{ amount }} TSH kwa huduma ya {{ business_name }}.
2
+ Muunganisho wako upo tayari na utadumu hadi {{ expiry_date }}.
3
+ Furahia mtandao wetu!
App/Templates/Registration.md ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ Karibu {{ user_name }}! Akaunti yako imefanikiwa kusajiliwa.
2
+ Mambo ni buku buku tu!!!
3
+ Asante kwa kuchagua {{ business_name }}!
App/Templates/Templates.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from jinja2 import Environment, FileSystemLoader
3
+ from decimal import Decimal
4
+
5
+ BUSINESS = "Goba Buku Wifi😉"
6
+
7
+
8
+ class MessageTemplate:
9
+ def __init__(self):
10
+ # Set the template directory to the same directory as this file
11
+ template_dir = os.path.dirname(__file__)
12
+ self.env = Environment(loader=FileSystemLoader(template_dir))
13
+
14
+ def render_template(self, template_name: str, **kwargs) -> str:
15
+ """Renders a template with provided variables."""
16
+ template = self.env.get_template(template_name)
17
+ return template.render(**kwargs)
18
+
19
+ def registration_message(self, user_name, business_name=BUSINESS):
20
+ """Generates the registration message for SMS."""
21
+ return self.render_template(
22
+ "registration.md",
23
+ user_name=user_name,
24
+ business_name=business_name,
25
+ )
26
+
27
+ def payment_confirmation_message(self, amount, expiry_date, business_name=BUSINESS):
28
+ """Generates the payment confirmation message for SMS."""
29
+ return self.render_template(
30
+ "payment_confirmation.md",
31
+ amount=amount,
32
+ expiry_date=expiry_date,
33
+ business_name=business_name,
34
+ )
35
+
36
+ def account_disabled_message(self) -> str:
37
+ return "Akaunti yako imezimwa. Tafadhali wasiliana na msaada kwa usaidizi."
38
+
39
+ def account_enabled_message(self) -> str:
40
+ return "Akaunti yako imewashwa. Sasa unaweza kufikia akaunti yako."
41
+
42
+ def reset_token_message(self, token: str) -> str:
43
+ return f"Tokeni yako ya kuweka upya nenosiri ni: {token}. Tafadhali tumia tokeni hii kuweka upya nenosiri lako."
44
+
45
+ def password_reset_confirmation_message(self) -> str:
46
+ return "Nenosiri lako limewekwa upya kwa mafanikio. Ikiwa hukufanya kitendo hiki, tafadhali wasiliana na msaada mara moja."
47
+
48
+ def subscription_created_message(self, plan_name: str, expiration_time: str) -> str:
49
+ return f"Habari! Usajili wako kwenye mpango wa {plan_name} umefanikiwa kuundwa. Utaisha tarehe {expiration_time}."
50
+
51
+ def insufficient_funds_message(
52
+ self, required_amount: Decimal, provided_amount: Decimal
53
+ ) -> str:
54
+ return f"Pesa hazitoshi kwa mpango uliyochagua. Zinazohitajika: {required_amount}, Zilizotolewa: {provided_amount}. Tafadhali ongeza pesa zaidi au chagua mpango tofauti."
55
+
56
+ def balance_assigned_message(self, amount: Decimal, new_balance: Decimal) -> str:
57
+ return f"Habari! Salio la {amount} limeongezwa kwa mafanikio kwenye akaunti yako. Salio lako jipya ni {new_balance}."
App/Users/Model.py CHANGED
@@ -1,21 +1,39 @@
 
 
1
  from tortoise import fields, models
2
  from passlib.context import CryptContext
 
 
 
3
  import datetime
4
  import uuid
5
  import random
6
  import string
 
 
 
 
 
 
 
 
 
 
 
7
 
 
8
  pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
9
 
10
 
11
  def generate_short_uuid() -> str:
 
12
  return "".join(random.choices(string.ascii_letters + string.digits, k=5))
13
 
14
 
15
  class User(models.Model):
16
  id = fields.CharField(primary_key=True, max_length=5, default=generate_short_uuid)
17
  name = fields.CharField(max_length=100)
18
- password = fields.CharField(max_length=100)
19
  phoneNumber = fields.CharField(max_length=15, unique=True)
20
  balance = fields.DecimalField(max_digits=10, decimal_places=2, default=0.00)
21
  mac_address = fields.CharField(max_length=17)
@@ -30,16 +48,27 @@ class User(models.Model):
30
  class Meta:
31
  table = "users"
32
 
 
 
 
 
33
  def hash_password(self, plain_password: str) -> str:
 
34
  return pwd_context.hash(plain_password)
35
 
36
  def verify_password(self, plain_password: str) -> bool:
 
 
 
 
37
  if (
38
  self.account_locked
39
  and datetime.datetime.now()
40
  < self.lastLogin + datetime.timedelta(minutes=15)
41
  ):
42
- print("Account is locked due to too many failed attempts. Try again later.")
 
 
43
  return False
44
 
45
  if pwd_context.verify(plain_password, self.password):
@@ -52,24 +81,180 @@ class User(models.Model):
52
  self.failed_attempts += 1
53
  if self.failed_attempts >= 5:
54
  self.account_locked = True
 
55
  self.save()
56
  return False
57
 
58
  async def initiate_password_reset(self):
 
59
  self.reset_token = f"{random.randint(100000, 999999)}"
60
  self.reset_token_expiration = datetime.datetime.now() + datetime.timedelta(
61
  minutes=15
62
  )
63
  await self.save()
 
64
 
65
- async def reset_password(self, reset_token: str, new_password: str):
 
 
 
 
66
  if (
67
  self.reset_token != reset_token
68
  or datetime.datetime.now() > self.reset_token_expiration
69
  ):
 
70
  return False
71
  self.password = self.hash_password(new_password)
72
  self.reset_token = None
73
  self.reset_token_expiration = None
74
  await self.save()
 
75
  return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # .Model.py
2
+
3
  from tortoise import fields, models
4
  from passlib.context import CryptContext
5
+ from App.Android.Android import AndroidClient
6
+ from App.Android.Schema import RegisterUserRequest as AndroidRegister
7
+ from App.Templates.Templates import MessageTemplate
8
  import datetime
9
  import uuid
10
  import random
11
  import string
12
+ from typing import Optional
13
+ from decimal import Decimal
14
+ import logging
15
+
16
+ # Configure logging
17
+ logger = logging.getLogger(__name__)
18
+ logger.setLevel(logging.INFO)
19
+ handler = logging.StreamHandler()
20
+ formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
21
+ handler.setFormatter(formatter)
22
+ logger.addHandler(handler)
23
 
24
+ # Password hashing context
25
  pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
26
 
27
 
28
  def generate_short_uuid() -> str:
29
+ """Generates a random 5-character alphanumeric ID."""
30
  return "".join(random.choices(string.ascii_letters + string.digits, k=5))
31
 
32
 
33
  class User(models.Model):
34
  id = fields.CharField(primary_key=True, max_length=5, default=generate_short_uuid)
35
  name = fields.CharField(max_length=100)
36
+ password = fields.CharField(max_length=100) # Stores hashed password
37
  phoneNumber = fields.CharField(max_length=15, unique=True)
38
  balance = fields.DecimalField(max_digits=10, decimal_places=2, default=0.00)
39
  mac_address = fields.CharField(max_length=17)
 
48
  class Meta:
49
  table = "users"
50
 
51
+ # Initialize AndroidClient and MessageTemplate as class variables
52
+ android_client = AndroidClient()
53
+ message_templates = MessageTemplate()
54
+
55
  def hash_password(self, plain_password: str) -> str:
56
+ """Hashes a plain password."""
57
  return pwd_context.hash(plain_password)
58
 
59
  def verify_password(self, plain_password: str) -> bool:
60
+ """
61
+ Verifies a plain password against the hashed password.
62
+ Handles account locking after multiple failed attempts.
63
+ """
64
  if (
65
  self.account_locked
66
  and datetime.datetime.now()
67
  < self.lastLogin + datetime.timedelta(minutes=15)
68
  ):
69
+ logger.warning(
70
+ f"Account {self.phoneNumber} is locked due to too many failed attempts."
71
+ )
72
  return False
73
 
74
  if pwd_context.verify(plain_password, self.password):
 
81
  self.failed_attempts += 1
82
  if self.failed_attempts >= 5:
83
  self.account_locked = True
84
+ logger.warning(f"Account {self.phoneNumber} has been locked.")
85
  self.save()
86
  return False
87
 
88
  async def initiate_password_reset(self):
89
+ """Generates a reset token and sends it to the user via message."""
90
  self.reset_token = f"{random.randint(100000, 999999)}"
91
  self.reset_token_expiration = datetime.datetime.now() + datetime.timedelta(
92
  minutes=15
93
  )
94
  await self.save()
95
+ await self.send_reset_token_message()
96
 
97
+ async def reset_password(self, reset_token: str, new_password: str) -> bool:
98
+ """
99
+ Resets the user's password if the provided token is valid and not expired.
100
+ Sends a confirmation message upon successful reset.
101
+ """
102
  if (
103
  self.reset_token != reset_token
104
  or datetime.datetime.now() > self.reset_token_expiration
105
  ):
106
+ logger.error(f"Invalid or expired reset token for user {self.phoneNumber}.")
107
  return False
108
  self.password = self.hash_password(new_password)
109
  self.reset_token = None
110
  self.reset_token_expiration = None
111
  await self.save()
112
+ await self.send_password_reset_confirmation()
113
  return True
114
+
115
+ @classmethod
116
+ async def create_user(cls, user_data: dict) -> models.Model:
117
+ """
118
+ Creates a new user, registers with AndroidClient, and sends a welcome message.
119
+ """
120
+ try:
121
+ # Hash the plain password
122
+ plain_password = user_data.get("password")
123
+ if not plain_password:
124
+ logger.error("Password not provided during user creation.")
125
+ raise ValueError("Password is required.")
126
+ hashed_password = pwd_context.hash(plain_password)
127
+ user_data["password"] = hashed_password
128
+
129
+ # Create the user in the database
130
+ new_user = await cls.create(**user_data)
131
+ logger.info(f"User {new_user.phoneNumber} created successfully.")
132
+
133
+ # Register with AndroidClient
134
+ register_request = AndroidRegister(
135
+ password=plain_password, # Assuming AndroidClient expects plain password
136
+ phoneNumber=new_user.phoneNumber,
137
+ )
138
+ try:
139
+ response = await cls.android_client.register_user(
140
+ request=register_request
141
+ )
142
+ logger.info(
143
+ f"AndroidClient register_user response for {new_user.phoneNumber}: {response}"
144
+ )
145
+ except Exception as e:
146
+ logger.error(
147
+ f"Failed to register user with AndroidClient for {new_user.phoneNumber}: {e}"
148
+ )
149
+ # Decide whether to rollback user creation or proceed
150
+ # For simplicity, we'll proceed
151
+
152
+ # Send welcome message
153
+ await new_user.send_welcome_message()
154
+
155
+ return new_user
156
+ except Exception as e:
157
+ logger.error(f"Error creating user: {e}")
158
+ raise e
159
+
160
+ async def send_welcome_message(self):
161
+ """Sends a welcome message to the user."""
162
+ message = self.message_templates.registration_message(user_name=self.name)
163
+ try:
164
+ await self.android_client.send_message(
165
+ phone_numbers=self.phoneNumber, message=message
166
+ )
167
+ logger.info(f"Welcome message sent to {self.phoneNumber}.")
168
+ except Exception as e:
169
+ logger.error(f"Failed to send welcome message to {self.phoneNumber}: {e}")
170
+
171
+ async def send_reset_token_message(self):
172
+ """Sends the reset token to the user's phone number."""
173
+ message = self.message_templates.reset_token_message(token=self.reset_token)
174
+ try:
175
+ await self.android_client.send_message(
176
+ phone_numbers=self.phoneNumber, message=message
177
+ )
178
+ logger.info(f"Reset token sent to {self.phoneNumber}.")
179
+ except Exception as e:
180
+ logger.error(f"Failed to send reset token to {self.phoneNumber}: {e}")
181
+
182
+ async def send_password_reset_confirmation(self):
183
+ """Sends a confirmation message after a successful password reset."""
184
+ message = self.message_templates.password_reset_confirmation_message()
185
+ try:
186
+ await self.android_client.send_message(
187
+ phone_numbers=self.phoneNumber, message=message
188
+ )
189
+ logger.info(f"Password reset confirmation sent to {self.phoneNumber}.")
190
+ except Exception as e:
191
+ logger.error(
192
+ f"Failed to send password reset confirmation to {self.phoneNumber}: {e}"
193
+ )
194
+
195
+ async def toggle_account_status(self):
196
+ """Toggles the user's account status and sends a notification message."""
197
+ self.account_locked = not self.account_locked
198
+ await self.save()
199
+ if self.account_locked:
200
+ message = self.message_templates.account_disabled_message()
201
+ logger.info(f"Account {self.phoneNumber} has been locked.")
202
+ else:
203
+ message = self.message_templates.account_enabled_message()
204
+ logger.info(f"Account {self.phoneNumber} has been unlocked.")
205
+
206
+ try:
207
+ await self.android_client.send_message(
208
+ phone_numbers=self.phoneNumber, message=message
209
+ )
210
+ logger.info(f"Account status message sent to {self.phoneNumber}.")
211
+ except Exception as e:
212
+ logger.error(
213
+ f"Failed to send account status message to {self.phoneNumber}: {e}"
214
+ )
215
+
216
+ async def send_payment_success_message(
217
+ self, amount: Decimal, plan_name: Optional[str] = None
218
+ ):
219
+ """
220
+ Sends a payment success message to the user.
221
+ If a plan is associated, it includes subscription details.
222
+ """
223
+ if plan_name:
224
+ message = self.message_templates.payment_success_subscription_message(
225
+ user_name=self.name, amount=amount, plan_name=plan_name
226
+ )
227
+ else:
228
+ message = self.message_templates.payment_success_balance_message(
229
+ user_name=self.name, amount=amount
230
+ )
231
+ try:
232
+ await self.android_client.send_message(
233
+ phone_numbers=self.phoneNumber, message=message
234
+ )
235
+ logger.info(f"Payment success message sent to {self.phoneNumber}.")
236
+ except Exception as e:
237
+ logger.error(
238
+ f"Failed to send payment success message to {self.phoneNumber}: {e}"
239
+ )
240
+
241
+ async def send_insufficient_funds_message(
242
+ self, required_amount: Decimal, attempted_amount: Decimal
243
+ ):
244
+ """
245
+ Sends a message to the user indicating insufficient funds for the attempted payment.
246
+ """
247
+ message = self.message_templates.insufficient_funds_message(
248
+ user_name=self.name,
249
+ required_amount=required_amount,
250
+ attempted_amount=attempted_amount,
251
+ )
252
+ try:
253
+ await self.android_client.send_message(
254
+ phone_numbers=self.phoneNumber, message=message
255
+ )
256
+ logger.info(f"Insufficient funds message sent to {self.phoneNumber}.")
257
+ except Exception as e:
258
+ logger.error(
259
+ f"Failed to send insufficient funds message to {self.phoneNumber}: {e}"
260
+ )
App/Users/UserRoutes.py CHANGED
@@ -14,12 +14,16 @@ from jose import jwt
14
  from typing import List
15
  from datetime import datetime, timedelta
16
  from App.Android.Android import AndroidClient
 
 
17
 
18
  # JWT Configurations
19
  SECRET_KEY = "your_secret_key_here"
20
  ALGORITHM = "HS256"
21
  ACCESS_TOKEN_EXPIRE_MINUTES = 30
22
  user_router = APIRouter(tags=["User"])
 
 
23
 
24
 
25
  def create_access_token(data: dict, expires_delta: timedelta = None):
@@ -31,6 +35,11 @@ def create_access_token(data: dict, expires_delta: timedelta = None):
31
  return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
32
 
33
 
 
 
 
 
 
34
  @user_router.post(
35
  "/user/register", response_model=BaseResponse, status_code=status.HTTP_201_CREATED
36
  )
@@ -40,13 +49,8 @@ async def register_user(request: RegisterUserRequest):
40
  raise HTTPException(
41
  status_code=status.HTTP_400_BAD_REQUEST, detail="User already exists."
42
  )
43
- # client part
44
- client = AndroidClient()
45
- print(request.json())
46
- response = await client.register_user(request=request)
47
- print(response)
48
- request.hash_password()
49
- new_user = await User.create(**request.dict())
50
  return BaseResponse(
51
  code=200, message="User created successfully", payload={"user_id": new_user.id}
52
  )
 
14
  from typing import List
15
  from datetime import datetime, timedelta
16
  from App.Android.Android import AndroidClient
17
+ from App.Android.Schema import RegisterUserRequest as AndroidRegister
18
+ from App.Templates.Templates import MessageTemplate
19
 
20
  # JWT Configurations
21
  SECRET_KEY = "your_secret_key_here"
22
  ALGORITHM = "HS256"
23
  ACCESS_TOKEN_EXPIRE_MINUTES = 30
24
  user_router = APIRouter(tags=["User"])
25
+ client = AndroidClient()
26
+ templates = MessageTemplate()
27
 
28
 
29
  def create_access_token(data: dict, expires_delta: timedelta = None):
 
35
  return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
36
 
37
 
38
+ # The user registers
39
+ # It sends userdetails to the router
40
+ # It sends the user a message to welcome the user
41
+
42
+
43
  @user_router.post(
44
  "/user/register", response_model=BaseResponse, status_code=status.HTTP_201_CREATED
45
  )
 
49
  raise HTTPException(
50
  status_code=status.HTTP_400_BAD_REQUEST, detail="User already exists."
51
  )
52
+
53
+ new_user = await User.create_user(request.dict())
 
 
 
 
 
54
  return BaseResponse(
55
  code=200, message="User created successfully", payload={"user_id": new_user.id}
56
  )
requirements.txt CHANGED
@@ -7,4 +7,6 @@ bcrypt
7
  httpx
8
  pytest
9
  asyncpg
10
- tortoise-orm
 
 
 
7
  httpx
8
  pytest
9
  asyncpg
10
+ tortoise-orm
11
+ markdown2
12
+ jinja2