Mbonea commited on
Commit
1a92f7e
·
1 Parent(s): a18c333

expiring subs

Browse files
App/Android/Android.py CHANGED
@@ -46,7 +46,7 @@ class AndroidClient:
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
@@ -67,7 +67,7 @@ class AndroidClient:
67
  @require_base_url
68
  async def get_active_users(self) -> APIResponse:
69
  """Retrieve all active users."""
70
- response = await self.client.get("/users/active")
71
  print(response.json())
72
  return APIResponse(**response.json())
73
 
@@ -76,7 +76,7 @@ class AndroidClient:
76
  """Enable or disable a user."""
77
  path = f"/users/{phone_number}/remove-session"
78
  try:
79
- response = await self.client.post(path, json=None)
80
  return APIResponse(**response.json())
81
  except:
82
  pass
@@ -85,7 +85,7 @@ class AndroidClient:
85
  async def _disable_request(self, request: SetUserStatusRequest) -> APIResponse:
86
  """disable a user."""
87
  path = f"/users/{request.phone}/disable"
88
- response = await self.client.post(path, json=None)
89
 
90
  return APIResponse(**response.json())
91
 
@@ -93,20 +93,20 @@ class AndroidClient:
93
  async def enable_user(self, request: SetUserStatusRequest) -> APIResponse:
94
  """Enable or disable a user."""
95
  path = f"/users/{request.phone}/enable"
96
- response = await self.client.post(path, json=None)
97
  return APIResponse(**response.json())
98
 
99
  @require_base_url
100
  async def get_users_list(self) -> APIResponse:
101
  """Retrieve all users (active and inactive)."""
102
- response = await self.client.get("/users")
103
  return APIResponse(**response.json())
104
 
105
  @require_base_url
106
  async def get_user_stats(self, phone: Optional[str] = None) -> APIResponse:
107
  """Retrieve user statistics for a specific user or all users."""
108
  url = f"/user/stats/{phone}" if phone else "/user/stats"
109
- response = await self.client.get(url)
110
  return APIResponse(**response.json())
111
 
112
  @require_base_url
@@ -126,15 +126,33 @@ class AndroidClient:
126
  "phoneNumbers": phone_numbers,
127
  "event": event,
128
  }
129
- print(data)
130
- response = await self.client.post("/send_message", json=data)
131
  x = response.content
132
- print(x)
133
- if response.status_code < 200 or response.status_code >= 300:
134
  return response
135
  else:
136
  raise Exception(f"Error sending message : {x}")
137
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  async def close(self):
139
  """Close the client session."""
140
  await self.client.aclose()
 
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}", timeout=60)
50
  return APIResponse(**response.json())
51
 
52
  @require_base_url
 
67
  @require_base_url
68
  async def get_active_users(self) -> APIResponse:
69
  """Retrieve all active users."""
70
+ response = await self.client.get("/users/active", timeout=60)
71
  print(response.json())
72
  return APIResponse(**response.json())
73
 
 
76
  """Enable or disable a user."""
77
  path = f"/users/{phone_number}/remove-session"
78
  try:
79
+ response = await self.client.post(path, json=None, timeout=60)
80
  return APIResponse(**response.json())
81
  except:
82
  pass
 
85
  async def _disable_request(self, request: SetUserStatusRequest) -> APIResponse:
86
  """disable a user."""
87
  path = f"/users/{request.phone}/disable"
88
+ response = await self.client.post(path, json=None, timeout=60)
89
 
90
  return APIResponse(**response.json())
91
 
 
93
  async def enable_user(self, request: SetUserStatusRequest) -> APIResponse:
94
  """Enable or disable a user."""
95
  path = f"/users/{request.phone}/enable"
96
+ response = await self.client.post(path, json=None, timeout=60)
97
  return APIResponse(**response.json())
98
 
99
  @require_base_url
100
  async def get_users_list(self) -> APIResponse:
101
  """Retrieve all users (active and inactive)."""
102
+ response = await self.client.get("/users", timeout=60)
103
  return APIResponse(**response.json())
104
 
105
  @require_base_url
106
  async def get_user_stats(self, phone: Optional[str] = None) -> APIResponse:
107
  """Retrieve user statistics for a specific user or all users."""
108
  url = f"/user/stats/{phone}" if phone else "/user/stats"
109
+ response = await self.client.get(url, timeout=60)
110
  return APIResponse(**response.json())
111
 
112
  @require_base_url
 
126
  "phoneNumbers": phone_numbers,
127
  "event": event,
128
  }
129
+ response = await self.client.post("/send_message", json=data, timeout=60)
 
130
  x = response.content
131
+ print(response.status_code)
132
+ if response.status_code >= 200 or response.status_code <= 300:
133
  return response
134
  else:
135
  raise Exception(f"Error sending message : {x}")
136
 
137
+ @require_base_url
138
+ async def change_password(self, phone: str, new_password: str) -> APIResponse:
139
+ """
140
+ Change the password for a user.
141
+ Args:
142
+ phone (str): The phone number of the user.
143
+ new_password (str): The new password for the user.
144
+ Returns:
145
+ APIResponse: The response from the API.
146
+ """
147
+ path = f"/users/{phone}/change-password"
148
+ payload = {"new_password": new_password}
149
+
150
+ try:
151
+ response = await self.client.post(path, json=payload, timeout=60)
152
+ return APIResponse(**response.json())
153
+ except Exception as e:
154
+ raise Exception(f"Error changing password: {e}")
155
+
156
  async def close(self):
157
  """Close the client session."""
158
  await self.client.aclose()
App/Payments/PaymentsRoutes.py CHANGED
@@ -1,6 +1,6 @@
1
  from fastapi import APIRouter, HTTPException, status, Query, Depends
2
  from typing import List
3
- from datetime import datetime
4
  from .Model import Payment
5
  from App.Users.Model import User
6
  from App.Plans.Model import Plan
@@ -13,6 +13,7 @@ from .Schema import (
13
  PaymentListResponse,
14
  )
15
  from .Schema import PaymentMethod
 
16
  from App.Users.dependencies import (
17
  get_current_active_user,
18
  UserType,
@@ -40,7 +41,7 @@ async def create_payment(request: CreatePaymentRequest, internal: bool = False):
40
  raise HTTPException(
41
  status_code=status.HTTP_404_NOT_FOUND, detail="Plan not found"
42
  )
43
-
44
  payment = await Payment.get_or_none(transaction_id=request.transaction_id)
45
 
46
  if payment:
@@ -166,8 +167,14 @@ async def link_user_to_payment(payment_id: str, request: UpdatePaymentUserReques
166
 
167
  @payment_router.get("/payments/date-range", response_model=PaymentListResponse)
168
  async def get_payments_by_date_range(
169
- start_date: datetime = Query(..., description="Start date in ISO format"),
170
- end_date: datetime = Query(..., description="End date in ISO format"),
 
 
 
 
 
 
171
  ):
172
  if start_date > end_date:
173
  raise HTTPException(
 
1
  from fastapi import APIRouter, HTTPException, status, Query, Depends
2
  from typing import List
3
+ from datetime import datetime, timedelta
4
  from .Model import Payment
5
  from App.Users.Model import User
6
  from App.Plans.Model import Plan
 
13
  PaymentListResponse,
14
  )
15
  from .Schema import PaymentMethod
16
+ from .utils import get_current_month_range
17
  from App.Users.dependencies import (
18
  get_current_active_user,
19
  UserType,
 
41
  raise HTTPException(
42
  status_code=status.HTTP_404_NOT_FOUND, detail="Plan not found"
43
  )
44
+ print(request.transaction_id)
45
  payment = await Payment.get_or_none(transaction_id=request.transaction_id)
46
 
47
  if payment:
 
167
 
168
  @payment_router.get("/payments/date-range", response_model=PaymentListResponse)
169
  async def get_payments_by_date_range(
170
+ start_date: datetime = Query(
171
+ default_factory=lambda: get_current_month_range()[0],
172
+ description="Start date in ISO format",
173
+ ),
174
+ end_date: datetime = Query(
175
+ default_factory=lambda: get_current_month_range()[1],
176
+ description="End date in ISO format",
177
+ ),
178
  ):
179
  if start_date > end_date:
180
  raise HTTPException(
App/Payments/Schema.py CHANGED
@@ -1,7 +1,10 @@
1
  from pydantic import BaseModel, Field
2
- from typing import Optional, List
3
  from decimal import Decimal
4
  from datetime import datetime
 
 
 
5
 
6
 
7
  class BaseResponse(BaseModel):
@@ -38,7 +41,8 @@ class CreatePaymentRequest(BaseModel):
38
  ..., description="Method of payment", example=PaymentMethod.CASH
39
  )
40
  transaction_id: Optional[str] = Field(
41
- None, description="Unique transaction ID (for methods like Lipa Number)"
 
42
  )
43
 
44
 
@@ -49,13 +53,13 @@ class UpdatePaymentStatusRequest(BaseModel):
49
 
50
 
51
  class PaymentResponse(BaseModel):
52
- id: str
53
- user_id: Optional[str]
54
- plan_id: Optional[str]
55
  amount: Decimal
56
  payment_method: str
57
  status: str
58
- transaction_id: Optional[str]
59
  created_time: datetime
60
  updated_time: datetime
61
 
 
1
  from pydantic import BaseModel, Field
2
+ from typing import Optional, List, Union
3
  from decimal import Decimal
4
  from datetime import datetime
5
+ from uuid import UUID
6
+
7
+ from uuid import UUID, uuid4 # Ensure you have this import at the top
8
 
9
 
10
  class BaseResponse(BaseModel):
 
41
  ..., description="Method of payment", example=PaymentMethod.CASH
42
  )
43
  transaction_id: Optional[str] = Field(
44
+ default_factory=lambda: str(uuid4()), # Generate a UUID and convert to string
45
+ description="Unique transaction ID (for methods like Lipa Number)",
46
  )
47
 
48
 
 
53
 
54
 
55
  class PaymentResponse(BaseModel):
56
+ id: Union[UUID, str]
57
+ user_id: Optional[Union[UUID, str]]
58
+ plan_id: Optional[Union[UUID, str]]
59
  amount: Decimal
60
  payment_method: str
61
  status: str
62
+ transaction_id: Optional[Union[UUID, str]]
63
  created_time: datetime
64
  updated_time: datetime
65
 
App/Payments/utils.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime, timedelta
2
+ import calendar
3
+
4
+
5
+ def get_current_month_range():
6
+ """Helper function to get the first and last dates of the current month."""
7
+ now = datetime.now()
8
+ start_of_month = datetime(now.year, now.month, 1)
9
+ _, last_day = calendar.monthrange(now.year, now.month)
10
+ end_of_month = datetime(now.year, now.month, last_day, 23, 59, 59)
11
+ return start_of_month, end_of_month
App/Subscriptions/background_tasks.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from tortoise import Tortoise
2
+ from datetime import datetime, timedelta
3
+ from App.Subscriptions.Model import Subscription
4
+ import logging
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ async def check_expiring_subscriptions():
10
+ """
11
+ Background task to check for subscriptions that are about to expire
12
+ and update their status accordingly.
13
+ """
14
+ try:
15
+ # Get the current time
16
+ current_time = datetime.now()
17
+
18
+ # Define the threshold for expiring subscriptions (e.g., 1 hour)
19
+ threshold = current_time + timedelta(hours=1)
20
+
21
+ # Fetch subscriptions that are about to expire
22
+ expiring_subscriptions = await Subscription.filter(
23
+ active=True, expiration_time__lt=threshold
24
+ ).all()
25
+
26
+ for subscription in expiring_subscriptions:
27
+ # Deactivate the subscription
28
+ subscription.active = False
29
+ await subscription.save()
30
+ logger.info(
31
+ f"Subscription {subscription.id} has been deactivated due to expiration."
32
+ )
33
+
34
+ except Exception as e:
35
+ logger.error(f"Error checking expiring subscriptions: {str(e)}")
App/Templates/Templates.py CHANGED
@@ -1,8 +1,10 @@
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:
@@ -26,10 +28,13 @@ class MessageTemplate:
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.md",
31
  amount=amount,
32
- expiry_date=expiry_date,
33
  business_name=business_name,
34
  )
35
 
@@ -59,10 +64,10 @@ class MessageTemplate:
59
  def insufficient_funds_message(
60
  self, required_amount: Decimal, provided_amount: Decimal
61
  ) -> str:
62
- return f"Pesa hazitoshi kwa mpango uliyochagua. Zinazohitajika: {required_amount}, Zilizotolewa: {provided_amount}. Tafadhali ongeza pesa zaidi au chagua mpango tofauti."
63
 
64
  def balance_assigned_message(self, amount: Decimal, new_balance: Decimal) -> str:
65
- return f"Habari! Salio la {amount} limeongezwa kwa mafanikio kwenye akaunti yako. Salio lako jipya ni {new_balance}."
66
 
67
  def subscription_expired_message(
68
  self, user_name: str, plan_name: str, business_name=BUSINESS
 
1
  import os
2
  from jinja2 import Environment, FileSystemLoader
3
  from decimal import Decimal
4
+ from datetime import datetime
5
 
6
+
7
+ BUSINESS = "Buku Mbili Wifi"
8
 
9
 
10
  class MessageTemplate:
 
28
 
29
  def payment_confirmation_message(self, amount, expiry_date, business_name=BUSINESS):
30
  """Generates the payment confirmation message for SMS."""
31
+ formatted_expiry_date = expiry_date.strftime(
32
+ "%Y-%m-%d %H:%M:%S"
33
+ ) # Format the date and time
34
  return self.render_template(
35
  "Payment.md",
36
  amount=amount,
37
+ expiry_date=formatted_expiry_date,
38
  business_name=business_name,
39
  )
40
 
 
64
  def insufficient_funds_message(
65
  self, required_amount: Decimal, provided_amount: Decimal
66
  ) -> str:
67
+ return f"Pesa hazitoshi kwa mpango uliyochagua. Zinazohitajika: {required_amount:,.2f}, Zilizotolewa: {provided_amount:,.2f}. Tafadhali ongeza pesa zaidi au chagua mpango tofauti."
68
 
69
  def balance_assigned_message(self, amount: Decimal, new_balance: Decimal) -> str:
70
+ return f"Habari! Salio la {amount:,.2f} limeongezwa kwa mafanikio kwenye akaunti yako. Salio lako jipya ni {new_balance:,.2f}."
71
 
72
  def subscription_expired_message(
73
  self, user_name: str, plan_name: str, business_name=BUSINESS
App/Users/Schema.py CHANGED
@@ -19,6 +19,10 @@ class UserResponse(BaseModel):
19
  account_locked: bool
20
  model_config = ConfigDict(from_attributes=True)
21
 
 
 
 
 
22
  # class Config:
23
  # orm_mode = True
24
 
 
19
  account_locked: bool
20
  model_config = ConfigDict(from_attributes=True)
21
 
22
+
23
+ class UserResponseSub(UserResponse):
24
+ expiration_time: str
25
+
26
  # class Config:
27
  # orm_mode = True
28
 
App/Users/UserRoutes.py CHANGED
@@ -9,6 +9,7 @@ from .Schema import (
9
  ResetPasswordRequest,
10
  BaseResponse,
11
  UserResponse,
 
12
  )
13
  from .Model import User
14
  from jose import jwt
@@ -79,7 +80,11 @@ async def login_user(request: LoginUserRequest):
79
  if db_user and valid_password:
80
 
81
  # Fetch active subscription if it exists
82
- subscription = await Subscription.filter(user=db_user, active=True).first()
 
 
 
 
83
 
84
  # Handle case when no active subscription is found
85
  subscription_end = (
@@ -196,12 +201,31 @@ async def toggle_user_status(user_id: str):
196
 
197
 
198
  @user_router.get(
199
- "/user/me", response_model=UserResponse, status_code=status.HTTP_200_OK
200
  )
201
  async def get_user_details(current_user: User = Depends(get_current_active_user)):
202
  """
203
  Get the current user's details and balance.
204
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  return UserResponse.from_orm(current_user)
206
 
207
 
 
9
  ResetPasswordRequest,
10
  BaseResponse,
11
  UserResponse,
12
+ UserResponseSub,
13
  )
14
  from .Model import User
15
  from jose import jwt
 
80
  if db_user and valid_password:
81
 
82
  # Fetch active subscription if it exists
83
+ subscription = (
84
+ await Subscription.filter(user=db_user, expiration_time__isnull=False)
85
+ .order_by("-expiration_time")
86
+ .first()
87
+ )
88
 
89
  # Handle case when no active subscription is found
90
  subscription_end = (
 
201
 
202
 
203
  @user_router.get(
204
+ "/user/me", response_model=UserResponseSub, status_code=status.HTTP_200_OK
205
  )
206
  async def get_user_details(current_user: User = Depends(get_current_active_user)):
207
  """
208
  Get the current user's details and balance.
209
  """
210
+ subscription = (
211
+ await Subscription.filter(user=current_user, expiration_time__isnull=False)
212
+ .order_by("-expiration_time")
213
+ .first()
214
+ )
215
+
216
+ data = UserResponse.from_orm(current_user).model_dump()
217
+ data["expiration_time"] = subscription.expiration_time.isoformat()
218
+ return UserResponseSub(**data)
219
+
220
+
221
+ @user_router.get(
222
+ "/user/{user_id}", response_model=UserResponse, status_code=status.HTTP_200_OK
223
+ )
224
+ async def get_user_details(user_id: str, admin: User = Depends(get_admin_user)):
225
+ """
226
+ user's details and balance.
227
+ """
228
+ current_user = await User.get_or_none(id=user_id)
229
  return UserResponse.from_orm(current_user)
230
 
231
 
App/app.py CHANGED
@@ -10,12 +10,23 @@ from .Plans.PlanRoutes import plan_router
10
  from .Portals.PortalRoutes import portal_router
11
  from .Metrics.MetricsRoutes import metrics_router
12
  from .Messages.MessagesRoute import message_router
 
13
 
14
  # from .Subscriptions.background_tasks import check_expiring_subscriptions
15
  import asyncio, os
16
  import logging
17
  import subprocess
18
 
 
 
 
 
 
 
 
 
 
 
19
  logging.basicConfig(level=logging.INFO)
20
 
21
  app = FastAPI()
@@ -34,6 +45,9 @@ app.add_middleware(
34
  async def startup_event():
35
  await Tortoise.init(config=TORTOISE_ORM)
36
  await Tortoise.generate_schemas()
 
 
 
37
 
38
 
39
  @app.get("/")
 
10
  from .Portals.PortalRoutes import portal_router
11
  from .Metrics.MetricsRoutes import metrics_router
12
  from .Messages.MessagesRoute import message_router
13
+ from .Subscriptions.background_tasks import check_expiring_subscriptions
14
 
15
  # from .Subscriptions.background_tasks import check_expiring_subscriptions
16
  import asyncio, os
17
  import logging
18
  import subprocess
19
 
20
+
21
+ async def periodic_task():
22
+ """
23
+ Periodically run the background task to check for expiring subscriptions.
24
+ """
25
+ while True:
26
+ await check_expiring_subscriptions()
27
+ await asyncio.sleep(100) # Run every hour (adjust the interval as needed)
28
+
29
+
30
  logging.basicConfig(level=logging.INFO)
31
 
32
  app = FastAPI()
 
45
  async def startup_event():
46
  await Tortoise.init(config=TORTOISE_ORM)
47
  await Tortoise.generate_schemas()
48
+ print("check subs")
49
+ asyncio.create_task(periodic_task())
50
+ print("checked subs")
51
 
52
 
53
  @app.get("/")