MukeshKapoor25 commited on
Commit
82a6cc5
·
1 Parent(s): d846c9a

feat: add wallet, address, pet and guest management features

Browse files

- Add new routers for wallet, address, pet and guest management
- Implement models and schemas for each feature
- Add CRUD operations for wallet transactions, addresses, pets and guests
- Include validation and error handling for all new features

app/app.py CHANGED
@@ -2,7 +2,7 @@
2
 
3
  from fastapi import FastAPI
4
  from fastapi.middleware.cors import CORSMiddleware
5
- from app.routers import user_router, profile_router, account_router
6
  from app.middleware.rate_limiter import RateLimitMiddleware
7
  from app.middleware.security_middleware import SecurityMiddleware
8
  import logging
@@ -48,6 +48,10 @@ app.add_middleware(
48
  app.include_router(user_router.router, prefix="/auth", tags=["user_auth"])
49
  app.include_router(profile_router.router, prefix="/profile", tags=["profile"])
50
  app.include_router(account_router.router, prefix="/account", tags=["account_management"])
 
 
 
 
51
 
52
  @app.get("/")
53
  def root():
 
2
 
3
  from fastapi import FastAPI
4
  from fastapi.middleware.cors import CORSMiddleware
5
+ from app.routers import user_router, profile_router, account_router, wallet_router, address_router, pet_router, guest_router
6
  from app.middleware.rate_limiter import RateLimitMiddleware
7
  from app.middleware.security_middleware import SecurityMiddleware
8
  import logging
 
48
  app.include_router(user_router.router, prefix="/auth", tags=["user_auth"])
49
  app.include_router(profile_router.router, prefix="/profile", tags=["profile"])
50
  app.include_router(account_router.router, prefix="/account", tags=["account_management"])
51
+ app.include_router(wallet_router.router, prefix="/wallet", tags=["wallet_management"])
52
+ app.include_router(address_router.router, prefix="/addresses", tags=["address_management"])
53
+ app.include_router(pet_router.router, prefix="/api/v1/users", tags=["pet_management"])
54
+ app.include_router(guest_router.router, prefix="/api/v1/users", tags=["guest_management"])
55
 
56
  @app.get("/")
57
  def root():
app/models/address_model.py ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+ from typing import Optional, List, Dict, Any
3
+ from bson import ObjectId
4
+ import logging
5
+
6
+ from app.core.nosql_client import db
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ class AddressModel:
11
+ """Model for managing user delivery addresses"""
12
+
13
+ collection = db["user_addresses"]
14
+
15
+ @staticmethod
16
+ async def create_address(user_id: str, address_data: Dict[str, Any]) -> Optional[str]:
17
+ """Create a new address for a user"""
18
+ try:
19
+ address_doc = {
20
+ "user_id": user_id,
21
+ "address_line_1": address_data.get("address_line_1"),
22
+ "address_line_2": address_data.get("address_line_2", ""),
23
+ "city": address_data.get("city"),
24
+ "state": address_data.get("state"),
25
+ "postal_code": address_data.get("postal_code"),
26
+ "country": address_data.get("country", "India"),
27
+ "address_type": address_data.get("address_type", "home"), # home, work, other
28
+ "is_default": address_data.get("is_default", False),
29
+ "landmark": address_data.get("landmark", ""),
30
+ "contact_name": address_data.get("contact_name", ""),
31
+ "contact_phone": address_data.get("contact_phone", ""),
32
+ "created_at": datetime.utcnow(),
33
+ "updated_at": datetime.utcnow()
34
+ }
35
+
36
+ # If this is set as default, unset other default addresses
37
+ if address_doc["is_default"]:
38
+ await AddressModel.collection.update_many(
39
+ {"user_id": user_id, "is_default": True},
40
+ {"$set": {"is_default": False, "updated_at": datetime.utcnow()}}
41
+ )
42
+
43
+ result = await AddressModel.collection.insert_one(address_doc)
44
+ logger.info(f"Created address for user {user_id}")
45
+ return str(result.inserted_id)
46
+
47
+ except Exception as e:
48
+ logger.error(f"Error creating address for user {user_id}: {str(e)}")
49
+ return None
50
+
51
+ @staticmethod
52
+ async def get_user_addresses(user_id: str) -> List[Dict[str, Any]]:
53
+ """Get all addresses for a user"""
54
+ try:
55
+ cursor = AddressModel.collection.find({"user_id": user_id}).sort("created_at", -1)
56
+ addresses = []
57
+
58
+ async for address in cursor:
59
+ address["_id"] = str(address["_id"])
60
+ addresses.append(address)
61
+
62
+ return addresses
63
+
64
+ except Exception as e:
65
+ logger.error(f"Error getting addresses for user {user_id}: {str(e)}")
66
+ return []
67
+
68
+ @staticmethod
69
+ async def get_address_by_id(user_id: str, address_id: str) -> Optional[Dict[str, Any]]:
70
+ """Get a specific address by ID for a user"""
71
+ try:
72
+ address = await AddressModel.collection.find_one({
73
+ "_id": ObjectId(address_id),
74
+ "user_id": user_id
75
+ })
76
+
77
+ if address:
78
+ address["_id"] = str(address["_id"])
79
+
80
+ return address
81
+
82
+ except Exception as e:
83
+ logger.error(f"Error getting address {address_id} for user {user_id}: {str(e)}")
84
+ return None
85
+
86
+ @staticmethod
87
+ async def update_address(user_id: str, address_id: str, update_data: Dict[str, Any]) -> bool:
88
+ """Update an existing address"""
89
+ try:
90
+ # Prepare update data
91
+ update_fields = {}
92
+ allowed_fields = [
93
+ "address_line_1", "address_line_2", "city", "state", "postal_code",
94
+ "country", "address_type", "is_default", "landmark", "contact_name", "contact_phone"
95
+ ]
96
+
97
+ for field in allowed_fields:
98
+ if field in update_data:
99
+ update_fields[field] = update_data[field]
100
+
101
+ update_fields["updated_at"] = datetime.utcnow()
102
+
103
+ # If setting as default, unset other default addresses
104
+ if update_fields.get("is_default"):
105
+ await AddressModel.collection.update_many(
106
+ {"user_id": user_id, "is_default": True, "_id": {"$ne": ObjectId(address_id)}},
107
+ {"$set": {"is_default": False, "updated_at": datetime.utcnow()}}
108
+ )
109
+
110
+ result = await AddressModel.collection.update_one(
111
+ {"_id": ObjectId(address_id), "user_id": user_id},
112
+ {"$set": update_fields}
113
+ )
114
+
115
+ return result.modified_count > 0
116
+
117
+ except Exception as e:
118
+ logger.error(f"Error updating address {address_id} for user {user_id}: {str(e)}")
119
+ return False
120
+
121
+ @staticmethod
122
+ async def delete_address(user_id: str, address_id: str) -> bool:
123
+ """Delete an address"""
124
+ try:
125
+ result = await AddressModel.collection.delete_one({
126
+ "_id": ObjectId(address_id),
127
+ "user_id": user_id
128
+ })
129
+
130
+ logger.info(f"Deleted address {address_id} for user {user_id}")
131
+ return result.deleted_count > 0
132
+
133
+ except Exception as e:
134
+ logger.error(f"Error deleting address {address_id} for user {user_id}: {str(e)}")
135
+ return False
136
+
137
+ @staticmethod
138
+ async def get_default_address(user_id: str) -> Optional[Dict[str, Any]]:
139
+ """Get the default address for a user"""
140
+ try:
141
+ address = await AddressModel.collection.find_one({
142
+ "user_id": user_id,
143
+ "is_default": True
144
+ })
145
+
146
+ if address:
147
+ address["_id"] = str(address["_id"])
148
+
149
+ return address
150
+
151
+ except Exception as e:
152
+ logger.error(f"Error getting default address for user {user_id}: {str(e)}")
153
+ return None
154
+
155
+ @staticmethod
156
+ async def set_default_address(user_id: str, address_id: str) -> bool:
157
+ """Set an address as default"""
158
+ try:
159
+ # First, unset all default addresses for the user
160
+ await AddressModel.collection.update_many(
161
+ {"user_id": user_id, "is_default": True},
162
+ {"$set": {"is_default": False, "updated_at": datetime.utcnow()}}
163
+ )
164
+
165
+ # Then set the specified address as default
166
+ result = await AddressModel.collection.update_one(
167
+ {"_id": ObjectId(address_id), "user_id": user_id},
168
+ {"$set": {"is_default": True, "updated_at": datetime.utcnow()}}
169
+ )
170
+
171
+ return result.modified_count > 0
172
+
173
+ except Exception as e:
174
+ logger.error(f"Error setting default address {address_id} for user {user_id}: {str(e)}")
175
+ return False
app/models/guest_model.py ADDED
@@ -0,0 +1,237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app.core.nosql_client import db
2
+ from datetime import datetime
3
+ from typing import List, Optional, Dict, Any
4
+ import uuid
5
+ import logging
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ class GuestModel:
10
+ """Model for managing guest profiles in the database"""
11
+
12
+ @staticmethod
13
+ async def create_guest(
14
+ user_id: str,
15
+ first_name: str,
16
+ last_name: Optional[str] = None,
17
+ email: Optional[str] = None,
18
+ phone_number: Optional[str] = None,
19
+ gender: Optional[str] = None,
20
+ date_of_birth: Optional[datetime] = None,
21
+ relationship: Optional[str] = None,
22
+ notes: Optional[str] = None
23
+ ) -> Optional[str]:
24
+ """
25
+ Create a new guest profile for a user.
26
+
27
+ Args:
28
+ user_id: ID of the user creating the guest profile
29
+ first_name: Guest's first name
30
+ last_name: Guest's last name
31
+ email: Guest's email address
32
+ phone_number: Guest's phone number
33
+ gender: Guest's gender
34
+ date_of_birth: Guest's date of birth
35
+ relationship: Relationship to the user
36
+ notes: Additional notes about the guest
37
+
38
+ Returns:
39
+ Guest ID if successful, None otherwise
40
+ """
41
+ try:
42
+ guests_collection = db.guests
43
+
44
+ guest_id = str(uuid.uuid4())
45
+ current_time = datetime.utcnow()
46
+
47
+ guest_data = {
48
+ "guest_id": guest_id,
49
+ "user_id": user_id,
50
+ "first_name": first_name,
51
+ "last_name": last_name,
52
+ "email": email,
53
+ "phone_number": phone_number,
54
+ "gender": gender,
55
+ "date_of_birth": date_of_birth,
56
+ "relationship": relationship,
57
+ "notes": notes,
58
+ "created_at": current_time,
59
+ "updated_at": current_time
60
+ }
61
+
62
+ result = await guests_collection.insert_one(guest_data)
63
+
64
+ if result.inserted_id:
65
+ logger.info(f"Guest created successfully: {guest_id} for user: {user_id}")
66
+ return guest_id
67
+ else:
68
+ logger.error(f"Failed to create guest for user: {user_id}")
69
+ return None
70
+
71
+ except Exception as e:
72
+ logger.error(f"Error creating guest for user {user_id}: {str(e)}")
73
+ return None
74
+
75
+ @staticmethod
76
+ async def get_user_guests(user_id: str) -> List[Dict[str, Any]]:
77
+ """
78
+ Get all guests for a specific user.
79
+
80
+ Args:
81
+ user_id: ID of the user
82
+
83
+ Returns:
84
+ List of guest documents
85
+ """
86
+ try:
87
+ guests_collection = db.guests
88
+
89
+ cursor = guests_collection.find({"user_id": user_id})
90
+ guests = await cursor.to_list(length=None)
91
+
92
+ # Remove MongoDB's _id field
93
+ for guest in guests:
94
+ guest.pop("_id", None)
95
+
96
+ logger.info(f"Retrieved {len(guests)} guests for user: {user_id}")
97
+ return guests
98
+
99
+ except Exception as e:
100
+ logger.error(f"Error getting guests for user {user_id}: {str(e)}")
101
+ return []
102
+
103
+ @staticmethod
104
+ async def get_guest_by_id(guest_id: str) -> Optional[Dict[str, Any]]:
105
+ """
106
+ Get a specific guest by ID.
107
+
108
+ Args:
109
+ guest_id: ID of the guest
110
+
111
+ Returns:
112
+ Guest document if found, None otherwise
113
+ """
114
+ try:
115
+ guests_collection = db.guests
116
+
117
+ guest = await guests_collection.find_one({"guest_id": guest_id})
118
+
119
+ if guest:
120
+ guest.pop("_id", None)
121
+ logger.info(f"Guest found: {guest_id}")
122
+ return guest
123
+ else:
124
+ logger.warning(f"Guest not found: {guest_id}")
125
+ return None
126
+
127
+ except Exception as e:
128
+ logger.error(f"Error getting guest {guest_id}: {str(e)}")
129
+ return None
130
+
131
+ @staticmethod
132
+ async def update_guest(guest_id: str, update_fields: Dict[str, Any]) -> bool:
133
+ """
134
+ Update a guest's information.
135
+
136
+ Args:
137
+ guest_id: ID of the guest to update
138
+ update_fields: Dictionary of fields to update
139
+
140
+ Returns:
141
+ True if successful, False otherwise
142
+ """
143
+ try:
144
+ guests_collection = db.guests
145
+
146
+ # Add updated timestamp
147
+ update_fields["updated_at"] = datetime.utcnow()
148
+
149
+ result = await guests_collection.update_one(
150
+ {"guest_id": guest_id},
151
+ {"$set": update_fields}
152
+ )
153
+
154
+ if result.modified_count > 0:
155
+ logger.info(f"Guest updated successfully: {guest_id}")
156
+ return True
157
+ else:
158
+ logger.warning(f"No changes made to guest: {guest_id}")
159
+ return False
160
+
161
+ except Exception as e:
162
+ logger.error(f"Error updating guest {guest_id}: {str(e)}")
163
+ return False
164
+
165
+ @staticmethod
166
+ async def delete_guest(guest_id: str) -> bool:
167
+ """
168
+ Delete a guest profile.
169
+
170
+ Args:
171
+ guest_id: ID of the guest to delete
172
+
173
+ Returns:
174
+ True if successful, False otherwise
175
+ """
176
+ try:
177
+ guests_collection = db.guests
178
+
179
+ result = await guests_collection.delete_one({"guest_id": guest_id})
180
+
181
+ if result.deleted_count > 0:
182
+ logger.info(f"Guest deleted successfully: {guest_id}")
183
+ return True
184
+ else:
185
+ logger.warning(f"Guest not found for deletion: {guest_id}")
186
+ return False
187
+
188
+ except Exception as e:
189
+ logger.error(f"Error deleting guest {guest_id}: {str(e)}")
190
+ return False
191
+
192
+ @staticmethod
193
+ async def get_guest_count_for_user(user_id: str) -> int:
194
+ """
195
+ Get the total number of guests for a user.
196
+
197
+ Args:
198
+ user_id: ID of the user
199
+
200
+ Returns:
201
+ Number of guests
202
+ """
203
+ try:
204
+ guests_collection = db.guests
205
+
206
+ count = await guests_collection.count_documents({"user_id": user_id})
207
+ return count
208
+
209
+ except Exception as e:
210
+ logger.error(f"Error counting guests for user {user_id}: {str(e)}")
211
+ return 0
212
+
213
+ @staticmethod
214
+ async def check_guest_ownership(guest_id: str, user_id: str) -> bool:
215
+ """
216
+ Check if a guest belongs to a specific user.
217
+
218
+ Args:
219
+ guest_id: ID of the guest
220
+ user_id: ID of the user
221
+
222
+ Returns:
223
+ True if guest belongs to user, False otherwise
224
+ """
225
+ try:
226
+ guests_collection = db.guests
227
+
228
+ guest = await guests_collection.find_one({
229
+ "guest_id": guest_id,
230
+ "user_id": user_id
231
+ })
232
+
233
+ return guest is not None
234
+
235
+ except Exception as e:
236
+ logger.error(f"Error checking guest ownership {guest_id} for user {user_id}: {str(e)}")
237
+ return False
app/models/pet_model.py ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app.core.nosql_client import db
2
+ from datetime import datetime
3
+ from typing import List, Optional, Dict, Any
4
+ import uuid
5
+ import logging
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ class PetModel:
10
+ """Model for managing pet profiles in the database"""
11
+
12
+ @staticmethod
13
+ async def create_pet(
14
+ user_id: str,
15
+ pet_name: str,
16
+ species: str,
17
+ breed: Optional[str] = None,
18
+ date_of_birth: Optional[datetime] = None,
19
+ age: Optional[int] = None,
20
+ weight: Optional[float] = None,
21
+ gender: Optional[str] = None,
22
+ temperament: Optional[str] = None,
23
+ health_notes: Optional[str] = None,
24
+ is_vaccinated: bool = False,
25
+ pet_photo_url: Optional[str] = None
26
+ ) -> Optional[str]:
27
+ """
28
+ Create a new pet profile for a user.
29
+
30
+ Args:
31
+ user_id: ID of the pet owner
32
+ pet_name: Name of the pet
33
+ species: Species (Dog, Cat, Other)
34
+ breed: Breed of the pet
35
+ date_of_birth: Pet's date of birth
36
+ age: Pet's age (if DOB not provided)
37
+ weight: Pet's weight
38
+ gender: Pet's gender
39
+ temperament: Pet's temperament
40
+ health_notes: Health notes including allergies/medications
41
+ is_vaccinated: Vaccination status
42
+ pet_photo_url: URL to pet's photo
43
+
44
+ Returns:
45
+ Pet ID if successful, None otherwise
46
+ """
47
+ try:
48
+ pets_collection = db.pets
49
+
50
+ pet_id = str(uuid.uuid4())
51
+ current_time = datetime.utcnow()
52
+
53
+ pet_data = {
54
+ "pet_id": pet_id,
55
+ "user_id": user_id,
56
+ "pet_name": pet_name,
57
+ "species": species,
58
+ "breed": breed,
59
+ "date_of_birth": date_of_birth,
60
+ "age": age,
61
+ "weight": weight,
62
+ "gender": gender,
63
+ "temperament": temperament,
64
+ "health_notes": health_notes,
65
+ "is_vaccinated": is_vaccinated,
66
+ "pet_photo_url": pet_photo_url,
67
+ "created_at": current_time,
68
+ "updated_at": current_time
69
+ }
70
+
71
+ result = await pets_collection.insert_one(pet_data)
72
+
73
+ if result.inserted_id:
74
+ logger.info(f"Pet created successfully: {pet_id} for user: {user_id}")
75
+ return pet_id
76
+ else:
77
+ logger.error(f"Failed to create pet for user: {user_id}")
78
+ return None
79
+
80
+ except Exception as e:
81
+ logger.error(f"Error creating pet for user {user_id}: {str(e)}")
82
+ return None
83
+
84
+ @staticmethod
85
+ async def get_user_pets(user_id: str) -> List[Dict[str, Any]]:
86
+ """
87
+ Get all pets for a specific user.
88
+
89
+ Args:
90
+ user_id: ID of the pet owner
91
+
92
+ Returns:
93
+ List of pet documents
94
+ """
95
+ try:
96
+ pets_collection = db.pets
97
+
98
+ cursor = pets_collection.find({"user_id": user_id})
99
+ pets = await cursor.to_list(length=None)
100
+
101
+ # Remove MongoDB's _id field
102
+ for pet in pets:
103
+ pet.pop("_id", None)
104
+
105
+ logger.info(f"Retrieved {len(pets)} pets for user: {user_id}")
106
+ return pets
107
+
108
+ except Exception as e:
109
+ logger.error(f"Error getting pets for user {user_id}: {str(e)}")
110
+ return []
111
+
112
+ @staticmethod
113
+ async def get_pet_by_id(pet_id: str) -> Optional[Dict[str, Any]]:
114
+ """
115
+ Get a specific pet by ID.
116
+
117
+ Args:
118
+ pet_id: ID of the pet
119
+
120
+ Returns:
121
+ Pet document if found, None otherwise
122
+ """
123
+ try:
124
+ pets_collection = db.pets
125
+
126
+ pet = await pets_collection.find_one({"pet_id": pet_id})
127
+
128
+ if pet:
129
+ pet.pop("_id", None)
130
+ logger.info(f"Pet found: {pet_id}")
131
+ return pet
132
+ else:
133
+ logger.warning(f"Pet not found: {pet_id}")
134
+ return None
135
+
136
+ except Exception as e:
137
+ logger.error(f"Error getting pet {pet_id}: {str(e)}")
138
+ return None
139
+
140
+ @staticmethod
141
+ async def update_pet(pet_id: str, update_fields: Dict[str, Any]) -> bool:
142
+ """
143
+ Update a pet's information.
144
+
145
+ Args:
146
+ pet_id: ID of the pet to update
147
+ update_fields: Dictionary of fields to update
148
+
149
+ Returns:
150
+ True if successful, False otherwise
151
+ """
152
+ try:
153
+ pets_collection = db.pets
154
+
155
+ # Add updated timestamp
156
+ update_fields["updated_at"] = datetime.utcnow()
157
+
158
+ result = await pets_collection.update_one(
159
+ {"pet_id": pet_id},
160
+ {"$set": update_fields}
161
+ )
162
+
163
+ if result.modified_count > 0:
164
+ logger.info(f"Pet updated successfully: {pet_id}")
165
+ return True
166
+ else:
167
+ logger.warning(f"No changes made to pet: {pet_id}")
168
+ return False
169
+
170
+ except Exception as e:
171
+ logger.error(f"Error updating pet {pet_id}: {str(e)}")
172
+ return False
173
+
174
+ @staticmethod
175
+ async def delete_pet(pet_id: str) -> bool:
176
+ """
177
+ Delete a pet profile.
178
+
179
+ Args:
180
+ pet_id: ID of the pet to delete
181
+
182
+ Returns:
183
+ True if successful, False otherwise
184
+ """
185
+ try:
186
+ pets_collection = db.pets
187
+
188
+ result = await pets_collection.delete_one({"pet_id": pet_id})
189
+
190
+ if result.deleted_count > 0:
191
+ logger.info(f"Pet deleted successfully: {pet_id}")
192
+ return True
193
+ else:
194
+ logger.warning(f"Pet not found for deletion: {pet_id}")
195
+ return False
196
+
197
+ except Exception as e:
198
+ logger.error(f"Error deleting pet {pet_id}: {str(e)}")
199
+ return False
200
+
201
+ @staticmethod
202
+ async def get_pet_count_for_user(user_id: str) -> int:
203
+ """
204
+ Get the total number of pets for a user.
205
+
206
+ Args:
207
+ user_id: ID of the pet owner
208
+
209
+ Returns:
210
+ Number of pets
211
+ """
212
+ try:
213
+ pets_collection = db.pets
214
+
215
+ count = await pets_collection.count_documents({"user_id": user_id})
216
+ return count
217
+
218
+ except Exception as e:
219
+ logger.error(f"Error counting pets for user {user_id}: {str(e)}")
220
+ return 0
app/models/user_model.py CHANGED
@@ -123,4 +123,37 @@ class BookMyServiceUserModel:
123
 
124
  except ValueError as ve:
125
  logger.error(f"Validation error for identifier {identifier}: {str(ve)}")
126
- raise HTTPException(status_code=400, detail=str(ve))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
124
  except ValueError as ve:
125
  logger.error(f"Validation error for identifier {identifier}: {str(ve)}")
126
+ raise HTTPException(status_code=400, detail=str(ve))
127
+
128
+ @staticmethod
129
+ async def update_profile(user_id: str, update_fields: dict):
130
+ """Update user profile by user_id"""
131
+ try:
132
+ from datetime import datetime
133
+
134
+ # Add updated_at timestamp
135
+ update_fields["updated_at"] = datetime.utcnow()
136
+
137
+ result = await BookMyServiceUserModel.collection.update_one(
138
+ {"user_id": user_id},
139
+ {"$set": update_fields}
140
+ )
141
+
142
+ if result.matched_count == 0:
143
+ raise HTTPException(status_code=404, detail="User not found")
144
+
145
+ return result.modified_count > 0
146
+
147
+ except Exception as e:
148
+ logger.error(f"Error updating profile for user {user_id}: {str(e)}")
149
+ raise HTTPException(status_code=500, detail="Failed to update profile")
150
+
151
+ @staticmethod
152
+ async def find_by_id(user_id: str):
153
+ """Find user by user_id"""
154
+ try:
155
+ user = await BookMyServiceUserModel.collection.find_one({"user_id": user_id})
156
+ return user
157
+ except Exception as e:
158
+ logger.error(f"Error finding user by ID {user_id}: {str(e)}")
159
+ return None
app/models/wallet_model.py ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+ from typing import Optional, List, Dict, Any
3
+ from bson import ObjectId
4
+ import logging
5
+
6
+ from app.core.nosql_client import db
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ class WalletModel:
11
+ """Model for managing user wallet operations"""
12
+
13
+ wallet_collection = db["user_wallets"]
14
+ transaction_collection = db["wallet_transactions"]
15
+
16
+ @staticmethod
17
+ async def get_wallet_balance(user_id: str) -> float:
18
+ """Get current wallet balance for a user"""
19
+ try:
20
+ wallet = await WalletModel.wallet_collection.find_one({"user_id": user_id})
21
+ if wallet:
22
+ return wallet.get("balance", 0.0)
23
+ else:
24
+ # Create wallet if doesn't exist
25
+ await WalletModel.create_wallet(user_id)
26
+ return 0.0
27
+ except Exception as e:
28
+ logger.error(f"Error getting wallet balance for user {user_id}: {str(e)}")
29
+ return 0.0
30
+
31
+ @staticmethod
32
+ async def create_wallet(user_id: str, initial_balance: float = 0.0) -> bool:
33
+ """Create a new wallet for a user"""
34
+ try:
35
+ wallet_doc = {
36
+ "user_id": user_id,
37
+ "balance": initial_balance,
38
+ "created_at": datetime.utcnow(),
39
+ "updated_at": datetime.utcnow()
40
+ }
41
+
42
+ result = await WalletModel.wallet_collection.insert_one(wallet_doc)
43
+ logger.info(f"Created wallet for user {user_id} with balance {initial_balance}")
44
+ return result.inserted_id is not None
45
+ except Exception as e:
46
+ logger.error(f"Error creating wallet for user {user_id}: {str(e)}")
47
+ return False
48
+
49
+ @staticmethod
50
+ async def update_balance(user_id: str, amount: float, transaction_type: str,
51
+ description: str = "", reference_id: str = None) -> bool:
52
+ """Update wallet balance and create transaction record"""
53
+ try:
54
+ # Get current balance
55
+ current_balance = await WalletModel.get_wallet_balance(user_id)
56
+
57
+ # Calculate new balance
58
+ if transaction_type in ["credit", "refund", "cashback"]:
59
+ new_balance = current_balance + amount
60
+ elif transaction_type in ["debit", "payment", "withdrawal"]:
61
+ if current_balance < amount:
62
+ logger.warning(f"Insufficient balance for user {user_id}. Current: {current_balance}, Required: {amount}")
63
+ return False
64
+ new_balance = current_balance - amount
65
+ else:
66
+ logger.error(f"Invalid transaction type: {transaction_type}")
67
+ return False
68
+
69
+ # Update wallet balance
70
+ update_result = await WalletModel.wallet_collection.update_one(
71
+ {"user_id": user_id},
72
+ {
73
+ "$set": {
74
+ "balance": new_balance,
75
+ "updated_at": datetime.utcnow()
76
+ }
77
+ },
78
+ upsert=True
79
+ )
80
+
81
+ # Create transaction record
82
+ transaction_doc = {
83
+ "user_id": user_id,
84
+ "amount": amount,
85
+ "transaction_type": transaction_type,
86
+ "description": description,
87
+ "reference_id": reference_id,
88
+ "balance_before": current_balance,
89
+ "balance_after": new_balance,
90
+ "timestamp": datetime.utcnow(),
91
+ "status": "completed"
92
+ }
93
+
94
+ await WalletModel.transaction_collection.insert_one(transaction_doc)
95
+
96
+ logger.info(f"Updated wallet for user {user_id}: {transaction_type} of {amount}, new balance: {new_balance}")
97
+ return True
98
+
99
+ except Exception as e:
100
+ logger.error(f"Error updating wallet balance for user {user_id}: {str(e)}")
101
+ return False
102
+
103
+ @staticmethod
104
+ async def get_transaction_history(user_id: str, page: int = 1, per_page: int = 20) -> Dict[str, Any]:
105
+ """Get paginated transaction history for a user"""
106
+ try:
107
+ skip = (page - 1) * per_page
108
+
109
+ # Get transactions with pagination
110
+ cursor = WalletModel.transaction_collection.find(
111
+ {"user_id": user_id}
112
+ ).sort("timestamp", -1).skip(skip).limit(per_page)
113
+
114
+ transactions = []
115
+ async for transaction in cursor:
116
+ # Convert ObjectId to string for JSON serialization
117
+ transaction["_id"] = str(transaction["_id"])
118
+ transactions.append(transaction)
119
+
120
+ # Get total count
121
+ total_count = await WalletModel.transaction_collection.count_documents({"user_id": user_id})
122
+
123
+ return {
124
+ "transactions": transactions,
125
+ "total_count": total_count,
126
+ "page": page,
127
+ "per_page": per_page,
128
+ "total_pages": (total_count + per_page - 1) // per_page
129
+ }
130
+
131
+ except Exception as e:
132
+ logger.error(f"Error getting transaction history for user {user_id}: {str(e)}")
133
+ return {
134
+ "transactions": [],
135
+ "total_count": 0,
136
+ "page": page,
137
+ "per_page": per_page,
138
+ "total_pages": 0
139
+ }
140
+
141
+ @staticmethod
142
+ async def get_wallet_summary(user_id: str) -> Dict[str, Any]:
143
+ """Get wallet summary including balance and recent transactions"""
144
+ try:
145
+ balance = await WalletModel.get_wallet_balance(user_id)
146
+ recent_transactions = await WalletModel.get_transaction_history(user_id, page=1, per_page=5)
147
+
148
+ return {
149
+ "balance": balance,
150
+ "recent_transactions": recent_transactions["transactions"]
151
+ }
152
+
153
+ except Exception as e:
154
+ logger.error(f"Error getting wallet summary for user {user_id}: {str(e)}")
155
+ return {
156
+ "balance": 0.0,
157
+ "recent_transactions": []
158
+ }
app/routers/address_router.py ADDED
@@ -0,0 +1,337 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, status
2
+ from typing import List
3
+ import logging
4
+
5
+ from app.utils.jwt import get_current_user_id
6
+ from app.models.address_model import AddressModel
7
+ from app.schemas.address_schema import (
8
+ AddressCreateRequest, AddressUpdateRequest, AddressResponse,
9
+ AddressListResponse, SetDefaultAddressRequest, AddressOperationResponse
10
+ )
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ router = APIRouter()
15
+
16
+ @router.get("/", response_model=AddressListResponse)
17
+ async def get_user_addresses(current_user_id: str = Depends(get_current_user_id)):
18
+ """
19
+ Get all delivery addresses for the current user.
20
+
21
+ This endpoint is JWT protected and requires a valid Bearer token.
22
+ """
23
+ try:
24
+ logger.info(f"Get addresses request for user: {current_user_id}")
25
+
26
+ addresses = await AddressModel.get_user_addresses(current_user_id)
27
+
28
+ address_responses = []
29
+ for addr in addresses:
30
+ address_responses.append(AddressResponse(
31
+ address_id=addr["address_id"],
32
+ user_id=addr["user_id"],
33
+ address_type=addr["address_type"],
34
+ full_name=addr["full_name"],
35
+ phone=addr["phone"],
36
+ address_line_1=addr["address_line_1"],
37
+ address_line_2=addr.get("address_line_2"),
38
+ city=addr["city"],
39
+ state=addr["state"],
40
+ postal_code=addr["postal_code"],
41
+ country=addr["country"],
42
+ is_default=addr.get("is_default", False),
43
+ created_at=addr.get("created_at"),
44
+ updated_at=addr.get("updated_at")
45
+ ))
46
+
47
+ return AddressListResponse(
48
+ success=True,
49
+ message="Addresses retrieved successfully",
50
+ addresses=address_responses,
51
+ total_count=len(address_responses)
52
+ )
53
+
54
+ except Exception as e:
55
+ logger.error(f"Error getting addresses for user {current_user_id}: {str(e)}")
56
+ raise HTTPException(
57
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
58
+ detail="Failed to retrieve addresses"
59
+ )
60
+
61
+ @router.post("/", response_model=AddressOperationResponse)
62
+ async def create_address(
63
+ address_data: AddressCreateRequest,
64
+ current_user_id: str = Depends(get_current_user_id)
65
+ ):
66
+ """
67
+ Create a new delivery address for the current user.
68
+ """
69
+ try:
70
+ logger.info(f"Create address request for user: {current_user_id}")
71
+
72
+ # Check if user already has 5 addresses (limit)
73
+ existing_addresses = await AddressModel.get_user_addresses(current_user_id)
74
+ if len(existing_addresses) >= 5:
75
+ raise HTTPException(
76
+ status_code=400,
77
+ detail="Maximum of 5 addresses allowed per user"
78
+ )
79
+
80
+ # If this is the first address, make it default
81
+ is_default = len(existing_addresses) == 0 or address_data.is_default
82
+
83
+ address_id = await AddressModel.create_address(
84
+ user_id=current_user_id,
85
+ address_type=address_data.address_type,
86
+ full_name=address_data.full_name,
87
+ phone=address_data.phone,
88
+ address_line_1=address_data.address_line_1,
89
+ address_line_2=address_data.address_line_2,
90
+ city=address_data.city,
91
+ state=address_data.state,
92
+ postal_code=address_data.postal_code,
93
+ country=address_data.country,
94
+ is_default=is_default
95
+ )
96
+
97
+ if address_id:
98
+ # Get the created address
99
+ created_address = await AddressModel.get_address_by_id(address_id)
100
+
101
+ address_response = AddressResponse(
102
+ address_id=created_address["address_id"],
103
+ user_id=created_address["user_id"],
104
+ address_type=created_address["address_type"],
105
+ full_name=created_address["full_name"],
106
+ phone=created_address["phone"],
107
+ address_line_1=created_address["address_line_1"],
108
+ address_line_2=created_address.get("address_line_2"),
109
+ city=created_address["city"],
110
+ state=created_address["state"],
111
+ postal_code=created_address["postal_code"],
112
+ country=created_address["country"],
113
+ is_default=created_address.get("is_default", False),
114
+ created_at=created_address.get("created_at"),
115
+ updated_at=created_address.get("updated_at")
116
+ )
117
+
118
+ return AddressOperationResponse(
119
+ success=True,
120
+ message="Address created successfully",
121
+ address=address_response
122
+ )
123
+ else:
124
+ return AddressOperationResponse(
125
+ success=False,
126
+ message="Failed to create address"
127
+ )
128
+
129
+ except HTTPException:
130
+ raise
131
+ except Exception as e:
132
+ logger.error(f"Error creating address for user {current_user_id}: {str(e)}")
133
+ raise HTTPException(
134
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
135
+ detail="Failed to create address"
136
+ )
137
+
138
+ @router.put("/{address_id}", response_model=AddressOperationResponse)
139
+ async def update_address(
140
+ address_id: str,
141
+ address_data: AddressUpdateRequest,
142
+ current_user_id: str = Depends(get_current_user_id)
143
+ ):
144
+ """
145
+ Update an existing delivery address.
146
+ """
147
+ try:
148
+ logger.info(f"Update address request for user: {current_user_id}, address: {address_id}")
149
+
150
+ # Check if address exists and belongs to user
151
+ existing_address = await AddressModel.get_address_by_id(address_id)
152
+ if not existing_address:
153
+ raise HTTPException(status_code=404, detail="Address not found")
154
+
155
+ if existing_address["user_id"] != current_user_id:
156
+ raise HTTPException(status_code=403, detail="Access denied")
157
+
158
+ # Prepare update fields
159
+ update_fields = {}
160
+
161
+ if address_data.address_type is not None:
162
+ update_fields["address_type"] = address_data.address_type
163
+ if address_data.full_name is not None:
164
+ update_fields["full_name"] = address_data.full_name
165
+ if address_data.phone is not None:
166
+ update_fields["phone"] = address_data.phone
167
+ if address_data.address_line_1 is not None:
168
+ update_fields["address_line_1"] = address_data.address_line_1
169
+ if address_data.address_line_2 is not None:
170
+ update_fields["address_line_2"] = address_data.address_line_2
171
+ if address_data.city is not None:
172
+ update_fields["city"] = address_data.city
173
+ if address_data.state is not None:
174
+ update_fields["state"] = address_data.state
175
+ if address_data.postal_code is not None:
176
+ update_fields["postal_code"] = address_data.postal_code
177
+ if address_data.country is not None:
178
+ update_fields["country"] = address_data.country
179
+
180
+ if not update_fields:
181
+ raise HTTPException(status_code=400, detail="No fields to update")
182
+
183
+ success = await AddressModel.update_address(address_id, update_fields)
184
+
185
+ if success:
186
+ # Get updated address
187
+ updated_address = await AddressModel.get_address_by_id(address_id)
188
+
189
+ address_response = AddressResponse(
190
+ address_id=updated_address["address_id"],
191
+ user_id=updated_address["user_id"],
192
+ address_type=updated_address["address_type"],
193
+ full_name=updated_address["full_name"],
194
+ phone=updated_address["phone"],
195
+ address_line_1=updated_address["address_line_1"],
196
+ address_line_2=updated_address.get("address_line_2"),
197
+ city=updated_address["city"],
198
+ state=updated_address["state"],
199
+ postal_code=updated_address["postal_code"],
200
+ country=updated_address["country"],
201
+ is_default=updated_address.get("is_default", False),
202
+ created_at=updated_address.get("created_at"),
203
+ updated_at=updated_address.get("updated_at")
204
+ )
205
+
206
+ return AddressOperationResponse(
207
+ success=True,
208
+ message="Address updated successfully",
209
+ address=address_response
210
+ )
211
+ else:
212
+ return AddressOperationResponse(
213
+ success=False,
214
+ message="Failed to update address"
215
+ )
216
+
217
+ except HTTPException:
218
+ raise
219
+ except Exception as e:
220
+ logger.error(f"Error updating address {address_id} for user {current_user_id}: {str(e)}")
221
+ raise HTTPException(
222
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
223
+ detail="Failed to update address"
224
+ )
225
+
226
+ @router.delete("/{address_id}", response_model=AddressOperationResponse)
227
+ async def delete_address(
228
+ address_id: str,
229
+ current_user_id: str = Depends(get_current_user_id)
230
+ ):
231
+ """
232
+ Delete a delivery address.
233
+ """
234
+ try:
235
+ logger.info(f"Delete address request for user: {current_user_id}, address: {address_id}")
236
+
237
+ # Check if address exists and belongs to user
238
+ existing_address = await AddressModel.get_address_by_id(address_id)
239
+ if not existing_address:
240
+ raise HTTPException(status_code=404, detail="Address not found")
241
+
242
+ if existing_address["user_id"] != current_user_id:
243
+ raise HTTPException(status_code=403, detail="Access denied")
244
+
245
+ # Check if this is the default address
246
+ if existing_address.get("is_default", False):
247
+ # Get other addresses to potentially set a new default
248
+ user_addresses = await AddressModel.get_user_addresses(current_user_id)
249
+ other_addresses = [addr for addr in user_addresses if addr["address_id"] != address_id]
250
+
251
+ if other_addresses:
252
+ # Set the first other address as default
253
+ await AddressModel.set_default_address(current_user_id, other_addresses[0]["address_id"])
254
+
255
+ success = await AddressModel.delete_address(address_id)
256
+
257
+ if success:
258
+ return AddressOperationResponse(
259
+ success=True,
260
+ message="Address deleted successfully"
261
+ )
262
+ else:
263
+ return AddressOperationResponse(
264
+ success=False,
265
+ message="Failed to delete address"
266
+ )
267
+
268
+ except HTTPException:
269
+ raise
270
+ except Exception as e:
271
+ logger.error(f"Error deleting address {address_id} for user {current_user_id}: {str(e)}")
272
+ raise HTTPException(
273
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
274
+ detail="Failed to delete address"
275
+ )
276
+
277
+ @router.post("/set-default", response_model=AddressOperationResponse)
278
+ async def set_default_address(
279
+ request: SetDefaultAddressRequest,
280
+ current_user_id: str = Depends(get_current_user_id)
281
+ ):
282
+ """
283
+ Set an address as the default delivery address.
284
+ """
285
+ try:
286
+ logger.info(f"Set default address request for user: {current_user_id}, address: {request.address_id}")
287
+
288
+ # Check if address exists and belongs to user
289
+ existing_address = await AddressModel.get_address_by_id(request.address_id)
290
+ if not existing_address:
291
+ raise HTTPException(status_code=404, detail="Address not found")
292
+
293
+ if existing_address["user_id"] != current_user_id:
294
+ raise HTTPException(status_code=403, detail="Access denied")
295
+
296
+ success = await AddressModel.set_default_address(current_user_id, request.address_id)
297
+
298
+ if success:
299
+ # Get updated address
300
+ updated_address = await AddressModel.get_address_by_id(request.address_id)
301
+
302
+ address_response = AddressResponse(
303
+ address_id=updated_address["address_id"],
304
+ user_id=updated_address["user_id"],
305
+ address_type=updated_address["address_type"],
306
+ full_name=updated_address["full_name"],
307
+ phone=updated_address["phone"],
308
+ address_line_1=updated_address["address_line_1"],
309
+ address_line_2=updated_address.get("address_line_2"),
310
+ city=updated_address["city"],
311
+ state=updated_address["state"],
312
+ postal_code=updated_address["postal_code"],
313
+ country=updated_address["country"],
314
+ is_default=updated_address.get("is_default", False),
315
+ created_at=updated_address.get("created_at"),
316
+ updated_at=updated_address.get("updated_at")
317
+ )
318
+
319
+ return AddressOperationResponse(
320
+ success=True,
321
+ message="Default address set successfully",
322
+ address=address_response
323
+ )
324
+ else:
325
+ return AddressOperationResponse(
326
+ success=False,
327
+ message="Failed to set default address"
328
+ )
329
+
330
+ except HTTPException:
331
+ raise
332
+ except Exception as e:
333
+ logger.error(f"Error setting default address for user {current_user_id}: {str(e)}")
334
+ raise HTTPException(
335
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
336
+ detail="Failed to set default address"
337
+ )
app/routers/guest_router.py ADDED
@@ -0,0 +1,273 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException, Depends, status
2
+ from fastapi.security import HTTPBearer
3
+ from app.models.guest_model import GuestModel
4
+ from app.schemas.guest_schema import (
5
+ GuestCreateRequest,
6
+ GuestUpdateRequest,
7
+ GuestResponse,
8
+ GuestListResponse,
9
+ GuestDeleteResponse
10
+ )
11
+ from app.utils.jwt import verify_token
12
+ from typing import Dict, Any
13
+ import logging
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ router = APIRouter()
18
+ security = HTTPBearer()
19
+
20
+ async def get_current_user(token: str = Depends(security)) -> Dict[str, Any]:
21
+ """
22
+ Dependency to get current authenticated user from JWT token
23
+ """
24
+ try:
25
+ payload = verify_token(token.credentials)
26
+ if not payload:
27
+ raise HTTPException(
28
+ status_code=status.HTTP_401_UNAUTHORIZED,
29
+ detail="Invalid or expired token"
30
+ )
31
+ return payload
32
+ except Exception as e:
33
+ logger.error(f"Token verification failed: {str(e)}")
34
+ raise HTTPException(
35
+ status_code=status.HTTP_401_UNAUTHORIZED,
36
+ detail="Invalid or expired token"
37
+ )
38
+
39
+ @router.get("/{user_id}/guests", response_model=GuestListResponse)
40
+ async def get_user_guests(
41
+ user_id: str,
42
+ current_user: Dict[str, Any] = Depends(get_current_user)
43
+ ):
44
+ """
45
+ Get all guests for a specific user.
46
+
47
+ - **user_id**: ID of the user
48
+ - Returns list of guests with total count
49
+ """
50
+ try:
51
+ # Verify user can only access their own guests
52
+ if current_user.get("user_id") != user_id:
53
+ raise HTTPException(
54
+ status_code=status.HTTP_403_FORBIDDEN,
55
+ detail="Access denied. You can only view your own guests."
56
+ )
57
+
58
+ guests_data = await GuestModel.get_user_guests(user_id)
59
+
60
+ guests = [GuestResponse(**guest) for guest in guests_data]
61
+
62
+ return GuestListResponse(
63
+ guests=guests,
64
+ total_count=len(guests)
65
+ )
66
+
67
+ except HTTPException:
68
+ raise
69
+ except Exception as e:
70
+ logger.error(f"Error getting guests for user {user_id}: {str(e)}")
71
+ raise HTTPException(
72
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
73
+ detail="Failed to retrieve guests"
74
+ )
75
+
76
+ @router.post("/{user_id}/guests", response_model=GuestResponse, status_code=status.HTTP_201_CREATED)
77
+ async def create_guest(
78
+ user_id: str,
79
+ guest_data: GuestCreateRequest,
80
+ current_user: Dict[str, Any] = Depends(get_current_user)
81
+ ):
82
+ """
83
+ Create a new guest profile for a user.
84
+
85
+ - **user_id**: ID of the user creating the guest profile
86
+ - **guest_data**: Guest information including name, contact details, etc.
87
+ - Returns the created guest profile
88
+ """
89
+ try:
90
+ # Verify user can only create guests for themselves
91
+ if current_user.get("user_id") != user_id:
92
+ raise HTTPException(
93
+ status_code=status.HTTP_403_FORBIDDEN,
94
+ detail="Access denied. You can only create guests for yourself."
95
+ )
96
+
97
+ # Create guest in database
98
+ guest_id = await GuestModel.create_guest(
99
+ user_id=user_id,
100
+ first_name=guest_data.first_name,
101
+ last_name=guest_data.last_name,
102
+ email=guest_data.email,
103
+ phone_number=guest_data.phone_number,
104
+ gender=guest_data.gender.value if guest_data.gender else None,
105
+ date_of_birth=guest_data.date_of_birth,
106
+ relationship=guest_data.relationship.value if guest_data.relationship else None,
107
+ notes=guest_data.notes
108
+ )
109
+
110
+ if not guest_id:
111
+ raise HTTPException(
112
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
113
+ detail="Failed to create guest profile"
114
+ )
115
+
116
+ # Retrieve and return the created guest
117
+ created_guest = await GuestModel.get_guest_by_id(guest_id)
118
+ if not created_guest:
119
+ raise HTTPException(
120
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
121
+ detail="Guest created but failed to retrieve"
122
+ )
123
+
124
+ return GuestResponse(**created_guest)
125
+
126
+ except HTTPException:
127
+ raise
128
+ except Exception as e:
129
+ logger.error(f"Error creating guest for user {user_id}: {str(e)}")
130
+ raise HTTPException(
131
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
132
+ detail="Failed to create guest profile"
133
+ )
134
+
135
+ @router.put("/{user_id}/guests/{guest_id}", response_model=GuestResponse)
136
+ async def update_guest(
137
+ user_id: str,
138
+ guest_id: str,
139
+ guest_data: GuestUpdateRequest,
140
+ current_user: Dict[str, Any] = Depends(get_current_user)
141
+ ):
142
+ """
143
+ Update an existing guest profile.
144
+
145
+ - **user_id**: ID of the user who owns the guest profile
146
+ - **guest_id**: ID of the guest to update
147
+ - **guest_data**: Updated guest information
148
+ - Returns the updated guest profile
149
+ """
150
+ try:
151
+ # Verify user can only update their own guests
152
+ if current_user.get("user_id") != user_id:
153
+ raise HTTPException(
154
+ status_code=status.HTTP_403_FORBIDDEN,
155
+ detail="Access denied. You can only update your own guests."
156
+ )
157
+
158
+ # Check if guest exists and belongs to user
159
+ existing_guest = await GuestModel.get_guest_by_id(guest_id)
160
+ if not existing_guest:
161
+ raise HTTPException(
162
+ status_code=status.HTTP_404_NOT_FOUND,
163
+ detail="Guest not found"
164
+ )
165
+
166
+ if existing_guest.get("user_id") != user_id:
167
+ raise HTTPException(
168
+ status_code=status.HTTP_403_FORBIDDEN,
169
+ detail="Access denied. This guest doesn't belong to you."
170
+ )
171
+
172
+ # Prepare update fields (only include non-None values)
173
+ update_fields = {}
174
+ for field, value in guest_data.dict(exclude_unset=True).items():
175
+ if value is not None:
176
+ if hasattr(value, 'value'): # Handle enum values
177
+ update_fields[field] = value.value
178
+ else:
179
+ update_fields[field] = value
180
+
181
+ if not update_fields:
182
+ raise HTTPException(
183
+ status_code=status.HTTP_400_BAD_REQUEST,
184
+ detail="No valid fields provided for update"
185
+ )
186
+
187
+ # Update guest in database
188
+ success = await GuestModel.update_guest(guest_id, update_fields)
189
+ if not success:
190
+ raise HTTPException(
191
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
192
+ detail="Failed to update guest profile"
193
+ )
194
+
195
+ # Retrieve and return updated guest
196
+ updated_guest = await GuestModel.get_guest_by_id(guest_id)
197
+ if not updated_guest:
198
+ raise HTTPException(
199
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
200
+ detail="Guest updated but failed to retrieve"
201
+ )
202
+
203
+ return GuestResponse(**updated_guest)
204
+
205
+ except HTTPException:
206
+ raise
207
+ except Exception as e:
208
+ logger.error(f"Error updating guest {guest_id} for user {user_id}: {str(e)}")
209
+ raise HTTPException(
210
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
211
+ detail="Failed to update guest profile"
212
+ )
213
+
214
+ @router.delete("/{user_id}/guests/{guest_id}", response_model=GuestDeleteResponse)
215
+ async def delete_guest(
216
+ user_id: str,
217
+ guest_id: str,
218
+ current_user: Dict[str, Any] = Depends(get_current_user)
219
+ ):
220
+ """
221
+ Delete a guest profile.
222
+
223
+ - **user_id**: ID of the user who owns the guest profile
224
+ - **guest_id**: ID of the guest to delete
225
+ - Returns confirmation of deletion
226
+ """
227
+ try:
228
+ # Verify user can only delete their own guests
229
+ if current_user.get("user_id") != user_id:
230
+ raise HTTPException(
231
+ status_code=status.HTTP_403_FORBIDDEN,
232
+ detail="Access denied. You can only delete your own guests."
233
+ )
234
+
235
+ # Check if guest exists and belongs to user
236
+ existing_guest = await GuestModel.get_guest_by_id(guest_id)
237
+ if not existing_guest:
238
+ raise HTTPException(
239
+ status_code=status.HTTP_404_NOT_FOUND,
240
+ detail="Guest not found"
241
+ )
242
+
243
+ if existing_guest.get("user_id") != user_id:
244
+ raise HTTPException(
245
+ status_code=status.HTTP_403_FORBIDDEN,
246
+ detail="Access denied. This guest doesn't belong to you."
247
+ )
248
+
249
+ # Delete guest from database
250
+ success = await GuestModel.delete_guest(guest_id)
251
+ if not success:
252
+ raise HTTPException(
253
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
254
+ detail="Failed to delete guest profile"
255
+ )
256
+
257
+ guest_name = existing_guest.get('first_name', 'Guest')
258
+ if existing_guest.get('last_name'):
259
+ guest_name += f" {existing_guest.get('last_name')}"
260
+
261
+ return GuestDeleteResponse(
262
+ message=f"Guest '{guest_name}' has been successfully deleted",
263
+ guest_id=guest_id
264
+ )
265
+
266
+ except HTTPException:
267
+ raise
268
+ except Exception as e:
269
+ logger.error(f"Error deleting guest {guest_id} for user {user_id}: {str(e)}")
270
+ raise HTTPException(
271
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
272
+ detail="Failed to delete guest profile"
273
+ )
app/routers/pet_router.py ADDED
@@ -0,0 +1,272 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException, Depends, status
2
+ from fastapi.security import HTTPBearer
3
+ from app.models.pet_model import PetModel
4
+ from app.schemas.pet_schema import (
5
+ PetCreateRequest,
6
+ PetUpdateRequest,
7
+ PetResponse,
8
+ PetListResponse,
9
+ PetDeleteResponse
10
+ )
11
+ from app.utils.jwt import verify_token
12
+ from typing import Dict, Any
13
+ import logging
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ router = APIRouter()
18
+ security = HTTPBearer()
19
+
20
+ async def get_current_user(token: str = Depends(security)) -> Dict[str, Any]:
21
+ """
22
+ Dependency to get current authenticated user from JWT token
23
+ """
24
+ try:
25
+ payload = verify_token(token.credentials)
26
+ if not payload:
27
+ raise HTTPException(
28
+ status_code=status.HTTP_401_UNAUTHORIZED,
29
+ detail="Invalid or expired token"
30
+ )
31
+ return payload
32
+ except Exception as e:
33
+ logger.error(f"Token verification failed: {str(e)}")
34
+ raise HTTPException(
35
+ status_code=status.HTTP_401_UNAUTHORIZED,
36
+ detail="Invalid or expired token"
37
+ )
38
+
39
+ @router.get("/{user_id}/pets", response_model=PetListResponse)
40
+ async def get_user_pets(
41
+ user_id: str,
42
+ current_user: Dict[str, Any] = Depends(get_current_user)
43
+ ):
44
+ """
45
+ Get all pets for a specific user.
46
+
47
+ - **user_id**: ID of the pet owner
48
+ - Returns list of pets with total count
49
+ """
50
+ try:
51
+ # Verify user can only access their own pets
52
+ if current_user.get("user_id") != user_id:
53
+ raise HTTPException(
54
+ status_code=status.HTTP_403_FORBIDDEN,
55
+ detail="Access denied. You can only view your own pets."
56
+ )
57
+
58
+ pets_data = await PetModel.get_user_pets(user_id)
59
+
60
+ pets = [PetResponse(**pet) for pet in pets_data]
61
+
62
+ return PetListResponse(
63
+ pets=pets,
64
+ total_count=len(pets)
65
+ )
66
+
67
+ except HTTPException:
68
+ raise
69
+ except Exception as e:
70
+ logger.error(f"Error getting pets for user {user_id}: {str(e)}")
71
+ raise HTTPException(
72
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
73
+ detail="Failed to retrieve pets"
74
+ )
75
+
76
+ @router.post("/{user_id}/pets", response_model=PetResponse, status_code=status.HTTP_201_CREATED)
77
+ async def create_pet(
78
+ user_id: str,
79
+ pet_data: PetCreateRequest,
80
+ current_user: Dict[str, Any] = Depends(get_current_user)
81
+ ):
82
+ """
83
+ Create a new pet profile for a user.
84
+
85
+ - **user_id**: ID of the pet owner
86
+ - **pet_data**: Pet information including name, species, breed, etc.
87
+ - Returns the created pet profile
88
+ """
89
+ try:
90
+ # Verify user can only create pets for themselves
91
+ if current_user.get("user_id") != user_id:
92
+ raise HTTPException(
93
+ status_code=status.HTTP_403_FORBIDDEN,
94
+ detail="Access denied. You can only create pets for yourself."
95
+ )
96
+
97
+ # Create pet in database
98
+ pet_id = await PetModel.create_pet(
99
+ user_id=user_id,
100
+ pet_name=pet_data.pet_name,
101
+ species=pet_data.species.value,
102
+ breed=pet_data.breed,
103
+ date_of_birth=pet_data.date_of_birth,
104
+ age=pet_data.age,
105
+ weight=pet_data.weight,
106
+ gender=pet_data.gender.value if pet_data.gender else None,
107
+ temperament=pet_data.temperament.value if pet_data.temperament else None,
108
+ health_notes=pet_data.health_notes,
109
+ is_vaccinated=pet_data.is_vaccinated,
110
+ pet_photo_url=pet_data.pet_photo_url
111
+ )
112
+
113
+ if not pet_id:
114
+ raise HTTPException(
115
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
116
+ detail="Failed to create pet profile"
117
+ )
118
+
119
+ # Retrieve and return the created pet
120
+ created_pet = await PetModel.get_pet_by_id(pet_id)
121
+ if not created_pet:
122
+ raise HTTPException(
123
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
124
+ detail="Pet created but failed to retrieve"
125
+ )
126
+
127
+ return PetResponse(**created_pet)
128
+
129
+ except HTTPException:
130
+ raise
131
+ except Exception as e:
132
+ logger.error(f"Error creating pet for user {user_id}: {str(e)}")
133
+ raise HTTPException(
134
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
135
+ detail="Failed to create pet profile"
136
+ )
137
+
138
+ @router.put("/{user_id}/pets/{pet_id}", response_model=PetResponse)
139
+ async def update_pet(
140
+ user_id: str,
141
+ pet_id: str,
142
+ pet_data: PetUpdateRequest,
143
+ current_user: Dict[str, Any] = Depends(get_current_user)
144
+ ):
145
+ """
146
+ Update an existing pet profile.
147
+
148
+ - **user_id**: ID of the pet owner
149
+ - **pet_id**: ID of the pet to update
150
+ - **pet_data**: Updated pet information
151
+ - Returns the updated pet profile
152
+ """
153
+ try:
154
+ # Verify user can only update their own pets
155
+ if current_user.get("user_id") != user_id:
156
+ raise HTTPException(
157
+ status_code=status.HTTP_403_FORBIDDEN,
158
+ detail="Access denied. You can only update your own pets."
159
+ )
160
+
161
+ # Check if pet exists and belongs to user
162
+ existing_pet = await PetModel.get_pet_by_id(pet_id)
163
+ if not existing_pet:
164
+ raise HTTPException(
165
+ status_code=status.HTTP_404_NOT_FOUND,
166
+ detail="Pet not found"
167
+ )
168
+
169
+ if existing_pet.get("user_id") != user_id:
170
+ raise HTTPException(
171
+ status_code=status.HTTP_403_FORBIDDEN,
172
+ detail="Access denied. This pet doesn't belong to you."
173
+ )
174
+
175
+ # Prepare update fields (only include non-None values)
176
+ update_fields = {}
177
+ for field, value in pet_data.dict(exclude_unset=True).items():
178
+ if value is not None:
179
+ if hasattr(value, 'value'): # Handle enum values
180
+ update_fields[field] = value.value
181
+ else:
182
+ update_fields[field] = value
183
+
184
+ if not update_fields:
185
+ raise HTTPException(
186
+ status_code=status.HTTP_400_BAD_REQUEST,
187
+ detail="No valid fields provided for update"
188
+ )
189
+
190
+ # Update pet in database
191
+ success = await PetModel.update_pet(pet_id, update_fields)
192
+ if not success:
193
+ raise HTTPException(
194
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
195
+ detail="Failed to update pet profile"
196
+ )
197
+
198
+ # Retrieve and return updated pet
199
+ updated_pet = await PetModel.get_pet_by_id(pet_id)
200
+ if not updated_pet:
201
+ raise HTTPException(
202
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
203
+ detail="Pet updated but failed to retrieve"
204
+ )
205
+
206
+ return PetResponse(**updated_pet)
207
+
208
+ except HTTPException:
209
+ raise
210
+ except Exception as e:
211
+ logger.error(f"Error updating pet {pet_id} for user {user_id}: {str(e)}")
212
+ raise HTTPException(
213
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
214
+ detail="Failed to update pet profile"
215
+ )
216
+
217
+ @router.delete("/{user_id}/pets/{pet_id}", response_model=PetDeleteResponse)
218
+ async def delete_pet(
219
+ user_id: str,
220
+ pet_id: str,
221
+ current_user: Dict[str, Any] = Depends(get_current_user)
222
+ ):
223
+ """
224
+ Delete a pet profile.
225
+
226
+ - **user_id**: ID of the pet owner
227
+ - **pet_id**: ID of the pet to delete
228
+ - Returns confirmation of deletion
229
+ """
230
+ try:
231
+ # Verify user can only delete their own pets
232
+ if current_user.get("user_id") != user_id:
233
+ raise HTTPException(
234
+ status_code=status.HTTP_403_FORBIDDEN,
235
+ detail="Access denied. You can only delete your own pets."
236
+ )
237
+
238
+ # Check if pet exists and belongs to user
239
+ existing_pet = await PetModel.get_pet_by_id(pet_id)
240
+ if not existing_pet:
241
+ raise HTTPException(
242
+ status_code=status.HTTP_404_NOT_FOUND,
243
+ detail="Pet not found"
244
+ )
245
+
246
+ if existing_pet.get("user_id") != user_id:
247
+ raise HTTPException(
248
+ status_code=status.HTTP_403_FORBIDDEN,
249
+ detail="Access denied. This pet doesn't belong to you."
250
+ )
251
+
252
+ # Delete pet from database
253
+ success = await PetModel.delete_pet(pet_id)
254
+ if not success:
255
+ raise HTTPException(
256
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
257
+ detail="Failed to delete pet profile"
258
+ )
259
+
260
+ return PetDeleteResponse(
261
+ message=f"Pet '{existing_pet.get('pet_name')}' has been successfully deleted",
262
+ pet_id=pet_id
263
+ )
264
+
265
+ except HTTPException:
266
+ raise
267
+ except Exception as e:
268
+ logger.error(f"Error deleting pet {pet_id} for user {user_id}: {str(e)}")
269
+ raise HTTPException(
270
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
271
+ detail="Failed to delete pet profile"
272
+ )
app/routers/profile_router.py CHANGED
@@ -7,6 +7,13 @@ import logging
7
 
8
  from app.utils.jwt import get_current_user_id
9
  from app.services.profile_service import profile_service
 
 
 
 
 
 
 
10
 
11
  logger = logging.getLogger(__name__)
12
 
@@ -50,3 +57,144 @@ async def get_profile(current_user_id: str = Depends(get_current_user_id)):
50
  detail="Internal server error"
51
  )
52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
  from app.utils.jwt import get_current_user_id
9
  from app.services.profile_service import profile_service
10
+ from app.services.wallet_service import WalletService
11
+ from app.models.user_model import BookMyServiceUserModel
12
+ from app.models.address_model import AddressModel
13
+ from app.schemas.profile_schema import (
14
+ ProfileUpdateRequest, ProfileResponse, ProfileOperationResponse,
15
+ PersonalDetailsResponse, WalletDisplayResponse, ProfileDashboardResponse
16
+ )
17
 
18
  logger = logging.getLogger(__name__)
19
 
 
57
  detail="Internal server error"
58
  )
59
 
60
+ @router.get("/dashboard", response_model=ProfileDashboardResponse)
61
+ async def get_profile_dashboard(current_user_id: str = Depends(get_current_user_id)):
62
+ """
63
+ Get complete profile dashboard with personal details, wallet, and address info.
64
+
65
+ This endpoint matches the screenshot requirements showing:
66
+ - Personal details (name, email, phone, DOB)
67
+ - Wallet balance
68
+ - Address management info
69
+ """
70
+ try:
71
+ logger.info(f"Dashboard request for user: {current_user_id}")
72
+
73
+ # Get user profile
74
+ user = await BookMyServiceUserModel.find_by_id(current_user_id)
75
+ if not user:
76
+ raise HTTPException(status_code=404, detail="User not found")
77
+
78
+ # Parse name into first and last name
79
+ name_parts = user.get("name", "").split(" ", 1)
80
+ first_name = name_parts[0] if name_parts else ""
81
+ last_name = name_parts[1] if len(name_parts) > 1 else ""
82
+
83
+ # Get wallet balance
84
+ wallet_balance = await WalletService.get_wallet_balance(current_user_id)
85
+
86
+ # Get address count and default address status
87
+ addresses = await AddressModel.get_user_addresses(current_user_id)
88
+ address_count = len(addresses)
89
+ has_default_address = any(addr.get("is_default", False) for addr in addresses)
90
+
91
+ # Build response
92
+ personal_details = PersonalDetailsResponse(
93
+ first_name=first_name,
94
+ last_name=last_name,
95
+ email=user.get("email", ""),
96
+ phone=user.get("phone", ""),
97
+ date_of_birth=user.get("date_of_birth")
98
+ )
99
+
100
+ wallet_display = WalletDisplayResponse(
101
+ balance=wallet_balance.balance,
102
+ formatted_balance=wallet_balance.formatted_balance,
103
+ currency=wallet_balance.currency
104
+ )
105
+
106
+ return ProfileDashboardResponse(
107
+ personal_details=personal_details,
108
+ wallet=wallet_display,
109
+ address_count=address_count,
110
+ has_default_address=has_default_address
111
+ )
112
+
113
+ except HTTPException:
114
+ raise
115
+ except Exception as e:
116
+ logger.error(f"Error getting profile dashboard for user {current_user_id}: {str(e)}")
117
+ raise HTTPException(
118
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
119
+ detail="Internal server error"
120
+ )
121
+
122
+ @router.put("/update", response_model=ProfileOperationResponse)
123
+ async def update_profile(
124
+ profile_data: ProfileUpdateRequest,
125
+ current_user_id: str = Depends(get_current_user_id)
126
+ ):
127
+ """
128
+ Update user profile information including personal details and DOB.
129
+ """
130
+ try:
131
+ logger.info(f"Profile update request for user: {current_user_id}")
132
+
133
+ # Prepare update fields
134
+ update_fields = {}
135
+
136
+ if profile_data.name is not None:
137
+ update_fields["name"] = profile_data.name
138
+
139
+ if profile_data.email is not None:
140
+ # Check if email is already used by another user
141
+ existing_user = await BookMyServiceUserModel.find_by_email(str(profile_data.email))
142
+ if existing_user and existing_user.get("user_id") != current_user_id:
143
+ raise HTTPException(status_code=409, detail="Email already in use by another account")
144
+ update_fields["email"] = str(profile_data.email)
145
+
146
+ if profile_data.phone is not None:
147
+ # Check if phone is already used by another user
148
+ existing_user = await BookMyServiceUserModel.find_by_phone(profile_data.phone)
149
+ if existing_user and existing_user.get("user_id") != current_user_id:
150
+ raise HTTPException(status_code=409, detail="Phone number already in use by another account")
151
+ update_fields["phone"] = profile_data.phone
152
+
153
+ if profile_data.date_of_birth is not None:
154
+ update_fields["date_of_birth"] = profile_data.date_of_birth
155
+
156
+ if profile_data.profile_picture is not None:
157
+ update_fields["profile_picture"] = profile_data.profile_picture
158
+
159
+ if not update_fields:
160
+ raise HTTPException(status_code=400, detail="No fields to update")
161
+
162
+ # Update profile
163
+ success = await BookMyServiceUserModel.update_profile(current_user_id, update_fields)
164
+
165
+ if success:
166
+ # Get updated profile
167
+ updated_user = await BookMyServiceUserModel.find_by_id(current_user_id)
168
+
169
+ profile_response = ProfileResponse(
170
+ user_id=updated_user["user_id"],
171
+ name=updated_user["name"],
172
+ email=updated_user.get("email"),
173
+ phone=updated_user.get("phone"),
174
+ date_of_birth=updated_user.get("date_of_birth"),
175
+ profile_picture=updated_user.get("profile_picture"),
176
+ auth_method=updated_user.get("auth_mode", "unknown"),
177
+ created_at=updated_user.get("created_at"),
178
+ updated_at=updated_user.get("updated_at")
179
+ )
180
+
181
+ return ProfileOperationResponse(
182
+ success=True,
183
+ message="Profile updated successfully",
184
+ profile=profile_response
185
+ )
186
+ else:
187
+ return ProfileOperationResponse(
188
+ success=False,
189
+ message="Failed to update profile"
190
+ )
191
+
192
+ except HTTPException:
193
+ raise
194
+ except Exception as e:
195
+ logger.error(f"Error updating profile for user {current_user_id}: {str(e)}")
196
+ raise HTTPException(
197
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
198
+ detail="Internal server error"
199
+ )
200
+
app/routers/wallet_router.py ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, status, Query
2
+ from typing import List, Optional
3
+ import logging
4
+
5
+ from app.utils.jwt import get_current_user_id
6
+ from app.services.wallet_service import WalletService
7
+ from app.schemas.wallet_schema import (
8
+ WalletBalanceResponse, TransactionHistoryResponse, WalletSummaryResponse,
9
+ AddMoneyRequest, WithdrawMoneyRequest, TransactionRequest, TransactionResponse
10
+ )
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ router = APIRouter()
15
+
16
+ @router.get("/balance", response_model=WalletBalanceResponse)
17
+ async def get_wallet_balance(current_user_id: str = Depends(get_current_user_id)):
18
+ """
19
+ Get current user's wallet balance.
20
+
21
+ This endpoint is JWT protected and requires a valid Bearer token.
22
+ """
23
+ try:
24
+ logger.info(f"Wallet balance request for user: {current_user_id}")
25
+
26
+ balance_info = await WalletService.get_wallet_balance(current_user_id)
27
+ return balance_info
28
+
29
+ except Exception as e:
30
+ logger.error(f"Error getting wallet balance for user {current_user_id}: {str(e)}")
31
+ raise HTTPException(
32
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
33
+ detail="Failed to retrieve wallet balance"
34
+ )
35
+
36
+ @router.get("/summary", response_model=WalletSummaryResponse)
37
+ async def get_wallet_summary(current_user_id: str = Depends(get_current_user_id)):
38
+ """
39
+ Get wallet summary including balance and recent transaction stats.
40
+ """
41
+ try:
42
+ logger.info(f"Wallet summary request for user: {current_user_id}")
43
+
44
+ summary = await WalletService.get_wallet_summary(current_user_id)
45
+ return summary
46
+
47
+ except Exception as e:
48
+ logger.error(f"Error getting wallet summary for user {current_user_id}: {str(e)}")
49
+ raise HTTPException(
50
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
51
+ detail="Failed to retrieve wallet summary"
52
+ )
53
+
54
+ @router.get("/transactions", response_model=TransactionHistoryResponse)
55
+ async def get_transaction_history(
56
+ current_user_id: str = Depends(get_current_user_id),
57
+ page: int = Query(1, ge=1, description="Page number"),
58
+ limit: int = Query(10, ge=1, le=100, description="Number of transactions per page"),
59
+ transaction_type: Optional[str] = Query(None, description="Filter by transaction type")
60
+ ):
61
+ """
62
+ Get paginated transaction history for the current user.
63
+
64
+ Query parameters:
65
+ - page: Page number (default: 1)
66
+ - limit: Number of transactions per page (default: 10, max: 100)
67
+ - transaction_type: Filter by type (credit, debit, refund, etc.)
68
+ """
69
+ try:
70
+ logger.info(f"Transaction history request for user: {current_user_id}, page: {page}, limit: {limit}")
71
+
72
+ history = await WalletService.get_transaction_history(
73
+ current_user_id,
74
+ page=page,
75
+ limit=limit,
76
+ transaction_type=transaction_type
77
+ )
78
+ return history
79
+
80
+ except Exception as e:
81
+ logger.error(f"Error getting transaction history for user {current_user_id}: {str(e)}")
82
+ raise HTTPException(
83
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
84
+ detail="Failed to retrieve transaction history"
85
+ )
86
+
87
+ @router.post("/add-money", response_model=TransactionResponse)
88
+ async def add_money_to_wallet(
89
+ request: AddMoneyRequest,
90
+ current_user_id: str = Depends(get_current_user_id)
91
+ ):
92
+ """
93
+ Add money to user's wallet.
94
+
95
+ This would typically integrate with a payment gateway.
96
+ For now, it simulates adding money to the wallet.
97
+ """
98
+ try:
99
+ logger.info(f"Add money request for user: {current_user_id}, amount: {request.amount}")
100
+
101
+ if request.amount <= 0:
102
+ raise HTTPException(status_code=400, detail="Amount must be greater than zero")
103
+
104
+ transaction = await WalletService.add_money(
105
+ current_user_id,
106
+ request.amount,
107
+ request.payment_method,
108
+ request.reference_id,
109
+ request.description
110
+ )
111
+
112
+ return TransactionResponse(
113
+ success=True,
114
+ message="Money added successfully",
115
+ transaction=transaction,
116
+ new_balance=transaction.balance_after
117
+ )
118
+
119
+ except HTTPException:
120
+ raise
121
+ except Exception as e:
122
+ logger.error(f"Error adding money for user {current_user_id}: {str(e)}")
123
+ raise HTTPException(
124
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
125
+ detail="Failed to add money to wallet"
126
+ )
127
+
128
+ @router.post("/withdraw", response_model=TransactionResponse)
129
+ async def withdraw_money(
130
+ request: WithdrawMoneyRequest,
131
+ current_user_id: str = Depends(get_current_user_id)
132
+ ):
133
+ """
134
+ Withdraw money from user's wallet.
135
+
136
+ This would typically integrate with a payment gateway for bank transfers.
137
+ """
138
+ try:
139
+ logger.info(f"Withdraw request for user: {current_user_id}, amount: {request.amount}")
140
+
141
+ if request.amount <= 0:
142
+ raise HTTPException(status_code=400, detail="Amount must be greater than zero")
143
+
144
+ # Check if user has sufficient balance
145
+ balance_info = await WalletService.get_wallet_balance(current_user_id)
146
+ if balance_info.balance < request.amount:
147
+ raise HTTPException(status_code=400, detail="Insufficient wallet balance")
148
+
149
+ transaction = await WalletService.deduct_money(
150
+ current_user_id,
151
+ request.amount,
152
+ "withdrawal",
153
+ request.bank_account_id,
154
+ request.description or "Wallet withdrawal"
155
+ )
156
+
157
+ return TransactionResponse(
158
+ success=True,
159
+ message="Withdrawal processed successfully",
160
+ transaction=transaction,
161
+ new_balance=transaction.balance_after
162
+ )
163
+
164
+ except HTTPException:
165
+ raise
166
+ except Exception as e:
167
+ logger.error(f"Error processing withdrawal for user {current_user_id}: {str(e)}")
168
+ raise HTTPException(
169
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
170
+ detail="Failed to process withdrawal"
171
+ )
172
+
173
+ @router.post("/transaction", response_model=TransactionResponse)
174
+ async def create_transaction(
175
+ request: TransactionRequest,
176
+ current_user_id: str = Depends(get_current_user_id)
177
+ ):
178
+ """
179
+ Create a generic transaction (for internal use, service bookings, etc.).
180
+ """
181
+ try:
182
+ logger.info(f"Transaction request for user: {current_user_id}, type: {request.transaction_type}, amount: {request.amount}")
183
+
184
+ if request.amount <= 0:
185
+ raise HTTPException(status_code=400, detail="Amount must be greater than zero")
186
+
187
+ if request.transaction_type == "debit":
188
+ # Check if user has sufficient balance for debit transactions
189
+ balance_info = await WalletService.get_wallet_balance(current_user_id)
190
+ if balance_info.balance < request.amount:
191
+ raise HTTPException(status_code=400, detail="Insufficient wallet balance")
192
+
193
+ transaction = await WalletService.deduct_money(
194
+ current_user_id,
195
+ request.amount,
196
+ request.category or "service",
197
+ request.reference_id,
198
+ request.description
199
+ )
200
+ else: # credit or refund
201
+ transaction = await WalletService.add_money(
202
+ current_user_id,
203
+ request.amount,
204
+ request.category or "refund",
205
+ request.reference_id,
206
+ request.description
207
+ )
208
+
209
+ return TransactionResponse(
210
+ success=True,
211
+ message=f"Transaction processed successfully",
212
+ transaction=transaction,
213
+ new_balance=transaction.balance_after
214
+ )
215
+
216
+ except HTTPException:
217
+ raise
218
+ except Exception as e:
219
+ logger.error(f"Error creating transaction for user {current_user_id}: {str(e)}")
220
+ raise HTTPException(
221
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
222
+ detail="Failed to process transaction"
223
+ )
app/schemas/address_schema.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field, validator
2
+ from datetime import datetime
3
+ from typing import List, Optional, Literal
4
+
5
+ class AddressCreateRequest(BaseModel):
6
+ """Request model for creating a new address"""
7
+ address_line_1: str = Field(..., min_length=5, max_length=200, description="Primary address line")
8
+ address_line_2: Optional[str] = Field("", max_length=200, description="Secondary address line")
9
+ city: str = Field(..., min_length=2, max_length=100, description="City name")
10
+ state: str = Field(..., min_length=2, max_length=100, description="State name")
11
+ postal_code: str = Field(..., min_length=5, max_length=10, description="Postal/ZIP code")
12
+ country: str = Field(default="India", max_length=100, description="Country name")
13
+ address_type: Literal["home", "work", "other"] = Field(default="home", description="Type of address")
14
+ is_default: bool = Field(default=False, description="Set as default address")
15
+ landmark: Optional[str] = Field("", max_length=200, description="Nearby landmark")
16
+ contact_name: Optional[str] = Field("", max_length=100, description="Contact person name")
17
+ contact_phone: Optional[str] = Field("", max_length=15, description="Contact phone number")
18
+
19
+ @validator('postal_code')
20
+ def validate_postal_code(cls, v):
21
+ if not v.isdigit():
22
+ raise ValueError('Postal code must contain only digits')
23
+ return v
24
+
25
+ @validator('contact_phone')
26
+ def validate_contact_phone(cls, v):
27
+ if v and not v.isdigit():
28
+ raise ValueError('Contact phone must contain only digits')
29
+ return v
30
+
31
+ class AddressUpdateRequest(BaseModel):
32
+ """Request model for updating an existing address"""
33
+ address_line_1: Optional[str] = Field(None, min_length=5, max_length=200, description="Primary address line")
34
+ address_line_2: Optional[str] = Field(None, max_length=200, description="Secondary address line")
35
+ city: Optional[str] = Field(None, min_length=2, max_length=100, description="City name")
36
+ state: Optional[str] = Field(None, min_length=2, max_length=100, description="State name")
37
+ postal_code: Optional[str] = Field(None, min_length=5, max_length=10, description="Postal/ZIP code")
38
+ country: Optional[str] = Field(None, max_length=100, description="Country name")
39
+ address_type: Optional[Literal["home", "work", "other"]] = Field(None, description="Type of address")
40
+ is_default: Optional[bool] = Field(None, description="Set as default address")
41
+ landmark: Optional[str] = Field(None, max_length=200, description="Nearby landmark")
42
+ contact_name: Optional[str] = Field(None, max_length=100, description="Contact person name")
43
+ contact_phone: Optional[str] = Field(None, max_length=15, description="Contact phone number")
44
+
45
+ @validator('postal_code')
46
+ def validate_postal_code(cls, v):
47
+ if v and not v.isdigit():
48
+ raise ValueError('Postal code must contain only digits')
49
+ return v
50
+
51
+ @validator('contact_phone')
52
+ def validate_contact_phone(cls, v):
53
+ if v and not v.isdigit():
54
+ raise ValueError('Contact phone must contain only digits')
55
+ return v
56
+
57
+ class AddressResponse(BaseModel):
58
+ """Response model for address data"""
59
+ address_id: str = Field(..., description="Unique address ID")
60
+ address_line_1: str = Field(..., description="Primary address line")
61
+ address_line_2: str = Field(..., description="Secondary address line")
62
+ city: str = Field(..., description="City name")
63
+ state: str = Field(..., description="State name")
64
+ postal_code: str = Field(..., description="Postal/ZIP code")
65
+ country: str = Field(..., description="Country name")
66
+ address_type: str = Field(..., description="Type of address")
67
+ is_default: bool = Field(..., description="Is default address")
68
+ landmark: str = Field(..., description="Nearby landmark")
69
+ contact_name: str = Field(..., description="Contact person name")
70
+ contact_phone: str = Field(..., description="Contact phone number")
71
+ created_at: datetime = Field(..., description="Address creation timestamp")
72
+ updated_at: datetime = Field(..., description="Address last update timestamp")
73
+
74
+ class Config:
75
+ from_attributes = True
76
+
77
+ class AddressListResponse(BaseModel):
78
+ """Response model for list of addresses"""
79
+ addresses: List[AddressResponse] = Field(..., description="List of user addresses")
80
+ total_count: int = Field(..., description="Total number of addresses")
81
+ default_address_id: Optional[str] = Field(None, description="ID of default address")
82
+
83
+ class SetDefaultAddressRequest(BaseModel):
84
+ """Request model for setting default address"""
85
+ address_id: str = Field(..., description="Address ID to set as default")
86
+
87
+ class AddressOperationResponse(BaseModel):
88
+ """Response model for address operations"""
89
+ success: bool = Field(..., description="Operation success status")
90
+ message: str = Field(..., description="Response message")
91
+ address_id: Optional[str] = Field(None, description="Address ID if applicable")
app/schemas/guest_schema.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field, validator, EmailStr
2
+ from typing import Optional, List
3
+ from datetime import datetime
4
+ from enum import Enum
5
+
6
+ class GenderEnum(str, Enum):
7
+ MALE = "Male"
8
+ FEMALE = "Female"
9
+ OTHER = "Other"
10
+
11
+ class RelationshipEnum(str, Enum):
12
+ FAMILY = "Family"
13
+ FRIEND = "Friend"
14
+ COLLEAGUE = "Colleague"
15
+ OTHER = "Other"
16
+
17
+ class GuestCreateRequest(BaseModel):
18
+ """Schema for creating a new guest profile"""
19
+ first_name: str = Field(..., min_length=1, max_length=100, description="Guest's first name")
20
+ last_name: Optional[str] = Field(None, max_length=100, description="Guest's last name")
21
+ email: Optional[EmailStr] = Field(None, description="Guest's email address")
22
+ phone_number: Optional[str] = Field(None, max_length=20, description="Guest's phone number")
23
+ gender: Optional[GenderEnum] = Field(None, description="Guest's gender")
24
+ date_of_birth: Optional[datetime] = Field(None, description="Guest's date of birth for age calculation")
25
+ relationship: Optional[RelationshipEnum] = Field(None, description="Relationship to the user")
26
+ notes: Optional[str] = Field(None, max_length=500, description="Additional notes about the guest")
27
+
28
+ @validator('first_name')
29
+ def validate_first_name(cls, v):
30
+ if not v or not v.strip():
31
+ raise ValueError('First name cannot be empty')
32
+ return v.strip()
33
+
34
+ @validator('last_name')
35
+ def validate_last_name(cls, v):
36
+ if v is not None and v.strip() == '':
37
+ return None
38
+ return v.strip() if v else v
39
+
40
+ @validator('phone_number')
41
+ def validate_phone_number(cls, v):
42
+ if v is not None:
43
+ # Remove spaces and special characters for validation
44
+ cleaned = ''.join(filter(str.isdigit, v))
45
+ if len(cleaned) < 10 or len(cleaned) > 15:
46
+ raise ValueError('Phone number must be between 10 and 15 digits')
47
+ return v
48
+
49
+ @validator('date_of_birth')
50
+ def validate_date_of_birth(cls, v):
51
+ if v is not None:
52
+ if v > datetime.now():
53
+ raise ValueError('Date of birth cannot be in the future')
54
+ # Check if age would be reasonable (not more than 120 years old)
55
+ age = (datetime.now() - v).days // 365
56
+ if age > 120:
57
+ raise ValueError('Date of birth indicates unrealistic age')
58
+ return v
59
+
60
+ class GuestUpdateRequest(BaseModel):
61
+ """Schema for updating a guest profile"""
62
+ first_name: Optional[str] = Field(None, min_length=1, max_length=100, description="Guest's first name")
63
+ last_name: Optional[str] = Field(None, max_length=100, description="Guest's last name")
64
+ email: Optional[EmailStr] = Field(None, description="Guest's email address")
65
+ phone_number: Optional[str] = Field(None, max_length=20, description="Guest's phone number")
66
+ gender: Optional[GenderEnum] = Field(None, description="Guest's gender")
67
+ date_of_birth: Optional[datetime] = Field(None, description="Guest's date of birth for age calculation")
68
+ relationship: Optional[RelationshipEnum] = Field(None, description="Relationship to the user")
69
+ notes: Optional[str] = Field(None, max_length=500, description="Additional notes about the guest")
70
+
71
+ @validator('first_name')
72
+ def validate_first_name(cls, v):
73
+ if v is not None and (not v or not v.strip()):
74
+ raise ValueError('First name cannot be empty')
75
+ return v.strip() if v else v
76
+
77
+ @validator('last_name')
78
+ def validate_last_name(cls, v):
79
+ if v is not None and v.strip() == '':
80
+ return None
81
+ return v.strip() if v else v
82
+
83
+ @validator('phone_number')
84
+ def validate_phone_number(cls, v):
85
+ if v is not None:
86
+ # Remove spaces and special characters for validation
87
+ cleaned = ''.join(filter(str.isdigit, v))
88
+ if len(cleaned) < 10 or len(cleaned) > 15:
89
+ raise ValueError('Phone number must be between 10 and 15 digits')
90
+ return v
91
+
92
+ @validator('date_of_birth')
93
+ def validate_date_of_birth(cls, v):
94
+ if v is not None:
95
+ if v > datetime.now():
96
+ raise ValueError('Date of birth cannot be in the future')
97
+ # Check if age would be reasonable (not more than 120 years old)
98
+ age = (datetime.now() - v).days // 365
99
+ if age > 120:
100
+ raise ValueError('Date of birth indicates unrealistic age')
101
+ return v
102
+
103
+ class GuestResponse(BaseModel):
104
+ """Schema for guest profile response"""
105
+ guest_id: str = Field(..., description="Unique guest identifier")
106
+ user_id: str = Field(..., description="User ID who created this guest profile")
107
+ first_name: str = Field(..., description="Guest's first name")
108
+ last_name: Optional[str] = Field(None, description="Guest's last name")
109
+ email: Optional[str] = Field(None, description="Guest's email address")
110
+ phone_number: Optional[str] = Field(None, description="Guest's phone number")
111
+ gender: Optional[str] = Field(None, description="Guest's gender")
112
+ date_of_birth: Optional[datetime] = Field(None, description="Guest's date of birth")
113
+ relationship: Optional[str] = Field(None, description="Relationship to the user")
114
+ notes: Optional[str] = Field(None, description="Additional notes about the guest")
115
+ created_at: datetime = Field(..., description="Guest profile creation timestamp")
116
+ updated_at: datetime = Field(..., description="Guest profile last update timestamp")
117
+
118
+ @property
119
+ def full_name(self) -> str:
120
+ """Get the full name of the guest"""
121
+ if self.last_name:
122
+ return f"{self.first_name} {self.last_name}"
123
+ return self.first_name
124
+
125
+ @property
126
+ def age(self) -> Optional[int]:
127
+ """Calculate age from date of birth"""
128
+ if self.date_of_birth:
129
+ today = datetime.now()
130
+ return today.year - self.date_of_birth.year - (
131
+ (today.month, today.day) < (self.date_of_birth.month, self.date_of_birth.day)
132
+ )
133
+ return None
134
+
135
+ class Config:
136
+ from_attributes = True
137
+ json_encoders = {
138
+ datetime: lambda v: v.isoformat()
139
+ }
140
+
141
+ class GuestListResponse(BaseModel):
142
+ """Schema for list of guests response"""
143
+ guests: List[GuestResponse] = Field(..., description="List of user's guests")
144
+ total_count: int = Field(..., description="Total number of guests")
145
+
146
+ class Config:
147
+ from_attributes = True
148
+
149
+ class GuestDeleteResponse(BaseModel):
150
+ """Schema for guest deletion response"""
151
+ message: str = Field(..., description="Deletion confirmation message")
152
+ guest_id: str = Field(..., description="ID of the deleted guest")
153
+
154
+ class Config:
155
+ from_attributes = True
app/schemas/pet_schema.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field, validator
2
+ from typing import Optional, List
3
+ from datetime import datetime
4
+ from enum import Enum
5
+
6
+ class SpeciesEnum(str, Enum):
7
+ DOG = "Dog"
8
+ CAT = "Cat"
9
+ OTHER = "Other"
10
+
11
+ class GenderEnum(str, Enum):
12
+ MALE = "Male"
13
+ FEMALE = "Female"
14
+ OTHER = "Other"
15
+
16
+ class TemperamentEnum(str, Enum):
17
+ CALM = "Calm"
18
+ NERVOUS = "Nervous"
19
+ AGGRESSIVE = "Aggressive"
20
+ SOCIAL = "Social"
21
+
22
+ class PetCreateRequest(BaseModel):
23
+ """Schema for creating a new pet profile"""
24
+ pet_name: str = Field(..., min_length=1, max_length=100, description="Name of the pet")
25
+ species: SpeciesEnum = Field(..., description="Species of the pet")
26
+ breed: Optional[str] = Field(None, max_length=100, description="Breed of the pet")
27
+ date_of_birth: Optional[datetime] = Field(None, description="Pet's date of birth")
28
+ age: Optional[int] = Field(None, ge=0, le=50, description="Pet's age in years")
29
+ weight: Optional[float] = Field(None, ge=0, le=200, description="Pet's weight in kg")
30
+ gender: Optional[GenderEnum] = Field(None, description="Pet's gender")
31
+ temperament: Optional[TemperamentEnum] = Field(None, description="Pet's temperament")
32
+ health_notes: Optional[str] = Field(None, max_length=1000, description="Health notes, allergies, medications")
33
+ is_vaccinated: bool = Field(False, description="Vaccination status")
34
+ pet_photo_url: Optional[str] = Field(None, max_length=500, description="URL to pet's photo")
35
+
36
+ @validator('pet_name')
37
+ def validate_pet_name(cls, v):
38
+ if not v or not v.strip():
39
+ raise ValueError('Pet name cannot be empty')
40
+ return v.strip()
41
+
42
+ @validator('age', 'date_of_birth')
43
+ def validate_age_or_dob(cls, v, values):
44
+ # At least one of age or date_of_birth should be provided
45
+ if 'age' in values and 'date_of_birth' in values:
46
+ if not values.get('age') and not values.get('date_of_birth'):
47
+ raise ValueError('Either age or date of birth must be provided')
48
+ return v
49
+
50
+ class PetUpdateRequest(BaseModel):
51
+ """Schema for updating a pet profile"""
52
+ pet_name: Optional[str] = Field(None, min_length=1, max_length=100, description="Name of the pet")
53
+ species: Optional[SpeciesEnum] = Field(None, description="Species of the pet")
54
+ breed: Optional[str] = Field(None, max_length=100, description="Breed of the pet")
55
+ date_of_birth: Optional[datetime] = Field(None, description="Pet's date of birth")
56
+ age: Optional[int] = Field(None, ge=0, le=50, description="Pet's age in years")
57
+ weight: Optional[float] = Field(None, ge=0, le=200, description="Pet's weight in kg")
58
+ gender: Optional[GenderEnum] = Field(None, description="Pet's gender")
59
+ temperament: Optional[TemperamentEnum] = Field(None, description="Pet's temperament")
60
+ health_notes: Optional[str] = Field(None, max_length=1000, description="Health notes, allergies, medications")
61
+ is_vaccinated: Optional[bool] = Field(None, description="Vaccination status")
62
+ pet_photo_url: Optional[str] = Field(None, max_length=500, description="URL to pet's photo")
63
+
64
+ @validator('pet_name')
65
+ def validate_pet_name(cls, v):
66
+ if v is not None and (not v or not v.strip()):
67
+ raise ValueError('Pet name cannot be empty')
68
+ return v.strip() if v else v
69
+
70
+ class PetResponse(BaseModel):
71
+ """Schema for pet profile response"""
72
+ pet_id: str = Field(..., description="Unique pet identifier")
73
+ user_id: str = Field(..., description="Owner's user ID")
74
+ pet_name: str = Field(..., description="Name of the pet")
75
+ species: str = Field(..., description="Species of the pet")
76
+ breed: Optional[str] = Field(None, description="Breed of the pet")
77
+ date_of_birth: Optional[datetime] = Field(None, description="Pet's date of birth")
78
+ age: Optional[int] = Field(None, description="Pet's age in years")
79
+ weight: Optional[float] = Field(None, description="Pet's weight in kg")
80
+ gender: Optional[str] = Field(None, description="Pet's gender")
81
+ temperament: Optional[str] = Field(None, description="Pet's temperament")
82
+ health_notes: Optional[str] = Field(None, description="Health notes, allergies, medications")
83
+ is_vaccinated: bool = Field(..., description="Vaccination status")
84
+ pet_photo_url: Optional[str] = Field(None, description="URL to pet's photo")
85
+ created_at: datetime = Field(..., description="Pet profile creation timestamp")
86
+ updated_at: datetime = Field(..., description="Pet profile last update timestamp")
87
+
88
+ class Config:
89
+ from_attributes = True
90
+ json_encoders = {
91
+ datetime: lambda v: v.isoformat()
92
+ }
93
+
94
+ class PetListResponse(BaseModel):
95
+ """Schema for list of pets response"""
96
+ pets: List[PetResponse] = Field(..., description="List of user's pets")
97
+ total_count: int = Field(..., description="Total number of pets")
98
+
99
+ class Config:
100
+ from_attributes = True
101
+
102
+ class PetDeleteResponse(BaseModel):
103
+ """Schema for pet deletion response"""
104
+ message: str = Field(..., description="Deletion confirmation message")
105
+ pet_id: str = Field(..., description="ID of the deleted pet")
106
+
107
+ class Config:
108
+ from_attributes = True
app/schemas/profile_schema.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, EmailStr, Field, validator
2
+ from datetime import datetime, date
3
+ from typing import Optional, Dict, Any
4
+ import re
5
+
6
+ class ProfileUpdateRequest(BaseModel):
7
+ """Request model for updating user profile"""
8
+ name: Optional[str] = Field(None, min_length=2, max_length=100, description="User's full name")
9
+ email: Optional[EmailStr] = Field(None, description="User's email address")
10
+ phone: Optional[str] = Field(None, description="User's phone number")
11
+ date_of_birth: Optional[str] = Field(None, description="Date of birth in DD/MM/YYYY format")
12
+ profile_picture: Optional[str] = Field(None, description="Profile picture URL")
13
+
14
+ @validator('phone')
15
+ def validate_phone(cls, v):
16
+ if v is not None:
17
+ # Remove any non-digit characters
18
+ phone_digits = re.sub(r'\D', '', v)
19
+ if len(phone_digits) != 10:
20
+ raise ValueError('Phone number must be exactly 10 digits')
21
+ return phone_digits
22
+ return v
23
+
24
+ @validator('date_of_birth')
25
+ def validate_date_of_birth(cls, v):
26
+ if v is not None:
27
+ try:
28
+ # Parse DD/MM/YYYY format
29
+ day, month, year = map(int, v.split('/'))
30
+ birth_date = date(year, month, day)
31
+
32
+ # Check if date is not in the future
33
+ if birth_date > date.today():
34
+ raise ValueError('Date of birth cannot be in the future')
35
+
36
+ # Check if age is reasonable (not more than 120 years)
37
+ age = (date.today() - birth_date).days // 365
38
+ if age > 120:
39
+ raise ValueError('Invalid date of birth')
40
+
41
+ return v
42
+ except ValueError as e:
43
+ if "Invalid date of birth" in str(e) or "Date of birth cannot be in the future" in str(e):
44
+ raise e
45
+ raise ValueError('Date of birth must be in DD/MM/YYYY format')
46
+ return v
47
+
48
+ class ProfileResponse(BaseModel):
49
+ """Response model for user profile"""
50
+ user_id: str = Field(..., description="Unique user identifier")
51
+ name: str = Field(..., description="User's full name")
52
+ email: Optional[str] = Field(None, description="User's email address")
53
+ phone: Optional[str] = Field(None, description="User's phone number")
54
+ date_of_birth: Optional[str] = Field(None, description="Date of birth in DD/MM/YYYY format")
55
+ profile_picture: Optional[str] = Field(None, description="Profile picture URL")
56
+ auth_method: str = Field(..., description="Authentication method used")
57
+ created_at: datetime = Field(..., description="Account creation timestamp")
58
+ updated_at: Optional[datetime] = Field(None, description="Last profile update timestamp")
59
+
60
+ class Config:
61
+ from_attributes = True
62
+
63
+ class PersonalDetailsResponse(BaseModel):
64
+ """Response model for personal details section"""
65
+ first_name: str = Field(..., description="User's first name")
66
+ last_name: str = Field(..., description="User's last name")
67
+ email: str = Field(..., description="User's email address")
68
+ phone: str = Field(..., description="User's phone number")
69
+ date_of_birth: Optional[str] = Field(None, description="Date of birth in DD/MM/YYYY format")
70
+
71
+ class Config:
72
+ from_attributes = True
73
+
74
+ class ProfileOperationResponse(BaseModel):
75
+ """Response model for profile operations"""
76
+ success: bool = Field(..., description="Operation success status")
77
+ message: str = Field(..., description="Response message")
78
+ profile: Optional[ProfileResponse] = Field(None, description="Updated profile data")
79
+
80
+ class WalletDisplayResponse(BaseModel):
81
+ """Response model for wallet display in profile"""
82
+ balance: float = Field(..., description="Current wallet balance")
83
+ formatted_balance: str = Field(..., description="Formatted balance with currency symbol")
84
+ currency: str = Field(default="INR", description="Currency code")
85
+
86
+ class ProfileDashboardResponse(BaseModel):
87
+ """Complete response model for profile dashboard"""
88
+ personal_details: PersonalDetailsResponse = Field(..., description="Personal details")
89
+ wallet: WalletDisplayResponse = Field(..., description="Wallet information")
90
+ address_count: int = Field(..., description="Number of saved addresses")
91
+ has_default_address: bool = Field(..., description="Whether user has a default address set")
app/schemas/wallet_schema.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from datetime import datetime
3
+ from typing import List, Optional, Literal
4
+ from decimal import Decimal
5
+
6
+ class WalletBalanceResponse(BaseModel):
7
+ """Response model for wallet balance"""
8
+ balance: float = Field(..., description="Current wallet balance")
9
+ currency: str = Field(default="INR", description="Currency code")
10
+ formatted_balance: str = Field(..., description="Formatted balance with currency symbol")
11
+
12
+ class TransactionEntry(BaseModel):
13
+ """Model for individual transaction entry"""
14
+ transaction_id: str = Field(..., description="Unique transaction ID")
15
+ amount: float = Field(..., description="Transaction amount")
16
+ transaction_type: Literal["credit", "debit", "refund", "cashback", "payment", "withdrawal"] = Field(..., description="Type of transaction")
17
+ description: str = Field(..., description="Transaction description")
18
+ reference_id: Optional[str] = Field(None, description="Reference ID for the transaction")
19
+ balance_before: float = Field(..., description="Balance before transaction")
20
+ balance_after: float = Field(..., description="Balance after transaction")
21
+ timestamp: datetime = Field(..., description="Transaction timestamp")
22
+ status: Literal["completed", "pending", "failed"] = Field(default="completed", description="Transaction status")
23
+
24
+ class TransactionHistoryResponse(BaseModel):
25
+ """Response model for transaction history"""
26
+ transactions: List[TransactionEntry] = Field(..., description="List of transactions")
27
+ total_count: int = Field(..., description="Total number of transactions")
28
+ page: int = Field(..., description="Current page number")
29
+ per_page: int = Field(..., description="Number of items per page")
30
+ total_pages: int = Field(..., description="Total number of pages")
31
+
32
+ class WalletSummaryResponse(BaseModel):
33
+ """Response model for wallet summary"""
34
+ balance: float = Field(..., description="Current wallet balance")
35
+ formatted_balance: str = Field(..., description="Formatted balance with currency symbol")
36
+ recent_transactions: List[TransactionEntry] = Field(..., description="Recent transactions")
37
+
38
+ class AddMoneyRequest(BaseModel):
39
+ """Request model for adding money to wallet"""
40
+ amount: float = Field(..., gt=0, description="Amount to add (must be positive)")
41
+ payment_method: Literal["card", "upi", "netbanking"] = Field(..., description="Payment method")
42
+ reference_id: Optional[str] = Field(None, description="Payment reference ID")
43
+ description: Optional[str] = Field("Wallet top-up", description="Transaction description")
44
+
45
+ class WithdrawMoneyRequest(BaseModel):
46
+ """Request model for withdrawing money from wallet"""
47
+ amount: float = Field(..., gt=0, description="Amount to withdraw (must be positive)")
48
+ bank_account_id: str = Field(..., description="Bank account ID for withdrawal")
49
+ description: Optional[str] = Field("Wallet withdrawal", description="Transaction description")
50
+
51
+ class TransactionRequest(BaseModel):
52
+ """Generic transaction request model"""
53
+ amount: float = Field(..., gt=0, description="Transaction amount")
54
+ transaction_type: Literal["credit", "debit", "refund", "cashback", "payment"] = Field(..., description="Transaction type")
55
+ description: str = Field(..., description="Transaction description")
56
+ reference_id: Optional[str] = Field(None, description="Reference ID")
57
+ category: Optional[str] = Field(None, description="Transaction category")
58
+
59
+ class TransactionResponse(BaseModel):
60
+ """Response model for transaction operations"""
61
+ success: bool = Field(..., description="Transaction success status")
62
+ message: str = Field(..., description="Response message")
63
+ transaction: Optional[TransactionEntry] = Field(None, description="Transaction details")
64
+ new_balance: Optional[float] = Field(None, description="New wallet balance after transaction")
65
+
66
+ class WalletTransactionResponse(BaseModel):
67
+ """Response model for wallet transaction operations"""
68
+ success: bool = Field(..., description="Transaction success status")
69
+ message: str = Field(..., description="Response message")
70
+ transaction_id: Optional[str] = Field(None, description="Transaction ID if successful")
71
+ new_balance: Optional[float] = Field(None, description="New wallet balance after transaction")
app/services/wallet_service.py ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, Any, Optional
2
+ import logging
3
+ from datetime import datetime
4
+
5
+ from app.models.wallet_model import WalletModel
6
+ from app.schemas.wallet_schema import (
7
+ WalletBalanceResponse, WalletSummaryResponse, TransactionHistoryResponse,
8
+ TransactionEntry, WalletTransactionResponse
9
+ )
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class WalletService:
14
+ """Service for wallet operations"""
15
+
16
+ @staticmethod
17
+ async def get_wallet_balance(user_id: str) -> WalletBalanceResponse:
18
+ """Get formatted wallet balance for user"""
19
+ try:
20
+ balance = await WalletModel.get_wallet_balance(user_id)
21
+
22
+ return WalletBalanceResponse(
23
+ balance=balance,
24
+ currency="INR",
25
+ formatted_balance=f"₹{balance:,.2f}"
26
+ )
27
+
28
+ except Exception as e:
29
+ logger.error(f"Error getting wallet balance for user {user_id}: {str(e)}")
30
+ return WalletBalanceResponse(
31
+ balance=0.0,
32
+ currency="INR",
33
+ formatted_balance="₹0.00"
34
+ )
35
+
36
+ @staticmethod
37
+ async def get_wallet_summary(user_id: str) -> WalletSummaryResponse:
38
+ """Get wallet summary with balance and recent transactions"""
39
+ try:
40
+ summary_data = await WalletModel.get_wallet_summary(user_id)
41
+
42
+ # Convert transactions to schema format
43
+ recent_transactions = []
44
+ for transaction in summary_data.get("recent_transactions", []):
45
+ recent_transactions.append(TransactionEntry(
46
+ transaction_id=transaction["_id"],
47
+ amount=transaction["amount"],
48
+ transaction_type=transaction["transaction_type"],
49
+ description=transaction["description"],
50
+ reference_id=transaction.get("reference_id"),
51
+ balance_before=transaction["balance_before"],
52
+ balance_after=transaction["balance_after"],
53
+ timestamp=transaction["timestamp"],
54
+ status=transaction["status"]
55
+ ))
56
+
57
+ balance = summary_data.get("balance", 0.0)
58
+
59
+ return WalletSummaryResponse(
60
+ balance=balance,
61
+ formatted_balance=f"₹{balance:,.2f}",
62
+ recent_transactions=recent_transactions
63
+ )
64
+
65
+ except Exception as e:
66
+ logger.error(f"Error getting wallet summary for user {user_id}: {str(e)}")
67
+ return WalletSummaryResponse(
68
+ balance=0.0,
69
+ formatted_balance="₹0.00",
70
+ recent_transactions=[]
71
+ )
72
+
73
+ @staticmethod
74
+ async def get_transaction_history(user_id: str, page: int = 1, per_page: int = 20) -> TransactionHistoryResponse:
75
+ """Get paginated transaction history"""
76
+ try:
77
+ history_data = await WalletModel.get_transaction_history(user_id, page, per_page)
78
+
79
+ # Convert transactions to schema format
80
+ transactions = []
81
+ for transaction in history_data.get("transactions", []):
82
+ transactions.append(TransactionEntry(
83
+ transaction_id=transaction["_id"],
84
+ amount=transaction["amount"],
85
+ transaction_type=transaction["transaction_type"],
86
+ description=transaction["description"],
87
+ reference_id=transaction.get("reference_id"),
88
+ balance_before=transaction["balance_before"],
89
+ balance_after=transaction["balance_after"],
90
+ timestamp=transaction["timestamp"],
91
+ status=transaction["status"]
92
+ ))
93
+
94
+ return TransactionHistoryResponse(
95
+ transactions=transactions,
96
+ total_count=history_data.get("total_count", 0),
97
+ page=page,
98
+ per_page=per_page,
99
+ total_pages=history_data.get("total_pages", 0)
100
+ )
101
+
102
+ except Exception as e:
103
+ logger.error(f"Error getting transaction history for user {user_id}: {str(e)}")
104
+ return TransactionHistoryResponse(
105
+ transactions=[],
106
+ total_count=0,
107
+ page=page,
108
+ per_page=per_page,
109
+ total_pages=0
110
+ )
111
+
112
+ @staticmethod
113
+ async def add_money(user_id: str, amount: float, payment_method: str,
114
+ description: str = "Wallet top-up", reference_id: str = None) -> WalletTransactionResponse:
115
+ """Add money to wallet"""
116
+ try:
117
+ success = await WalletModel.update_balance(
118
+ user_id=user_id,
119
+ amount=amount,
120
+ transaction_type="credit",
121
+ description=f"{description} via {payment_method}",
122
+ reference_id=reference_id
123
+ )
124
+
125
+ if success:
126
+ new_balance = await WalletModel.get_wallet_balance(user_id)
127
+ return WalletTransactionResponse(
128
+ success=True,
129
+ message=f"Successfully added ₹{amount:,.2f} to wallet",
130
+ transaction_id=reference_id,
131
+ new_balance=new_balance
132
+ )
133
+ else:
134
+ return WalletTransactionResponse(
135
+ success=False,
136
+ message="Failed to add money to wallet"
137
+ )
138
+
139
+ except Exception as e:
140
+ logger.error(f"Error adding money to wallet for user {user_id}: {str(e)}")
141
+ return WalletTransactionResponse(
142
+ success=False,
143
+ message="Internal error occurred while adding money"
144
+ )
145
+
146
+ @staticmethod
147
+ async def deduct_money(user_id: str, amount: float, description: str,
148
+ reference_id: str = None) -> WalletTransactionResponse:
149
+ """Deduct money from wallet (for payments)"""
150
+ try:
151
+ success = await WalletModel.update_balance(
152
+ user_id=user_id,
153
+ amount=amount,
154
+ transaction_type="debit",
155
+ description=description,
156
+ reference_id=reference_id
157
+ )
158
+
159
+ if success:
160
+ new_balance = await WalletModel.get_wallet_balance(user_id)
161
+ return WalletTransactionResponse(
162
+ success=True,
163
+ message=f"Successfully deducted ₹{amount:,.2f} from wallet",
164
+ transaction_id=reference_id,
165
+ new_balance=new_balance
166
+ )
167
+ else:
168
+ return WalletTransactionResponse(
169
+ success=False,
170
+ message="Insufficient balance or transaction failed"
171
+ )
172
+
173
+ except Exception as e:
174
+ logger.error(f"Error deducting money from wallet for user {user_id}: {str(e)}")
175
+ return WalletTransactionResponse(
176
+ success=False,
177
+ message="Internal error occurred while processing payment"
178
+ )
179
+
180
+ @staticmethod
181
+ async def process_refund(user_id: str, amount: float, description: str,
182
+ reference_id: str = None) -> WalletTransactionResponse:
183
+ """Process refund to wallet"""
184
+ try:
185
+ success = await WalletModel.update_balance(
186
+ user_id=user_id,
187
+ amount=amount,
188
+ transaction_type="refund",
189
+ description=description,
190
+ reference_id=reference_id
191
+ )
192
+
193
+ if success:
194
+ new_balance = await WalletModel.get_wallet_balance(user_id)
195
+ return WalletTransactionResponse(
196
+ success=True,
197
+ message=f"Refund of ₹{amount:,.2f} processed successfully",
198
+ transaction_id=reference_id,
199
+ new_balance=new_balance
200
+ )
201
+ else:
202
+ return WalletTransactionResponse(
203
+ success=False,
204
+ message="Failed to process refund"
205
+ )
206
+
207
+ except Exception as e:
208
+ logger.error(f"Error processing refund for user {user_id}: {str(e)}")
209
+ return WalletTransactionResponse(
210
+ success=False,
211
+ message="Internal error occurred while processing refund"
212
+ )