MukeshKapoor25 commited on
Commit
68d2ebc
·
1 Parent(s): b0cef76

refactor: Change merchant_id and service_id types from UUID to str for consistency across endpoints and schemas

Browse files
app/appointments/controllers/router.py CHANGED
@@ -2,7 +2,6 @@
2
  Appointments API router.
3
  """
4
  import logging
5
- import hashlib
6
  from uuid import UUID
7
  from typing import Optional
8
  from datetime import datetime
@@ -48,12 +47,8 @@ async def create_appointment_endpoint(
48
  if merchant_id is None:
49
  if not current_user.merchant_id:
50
  raise HTTPException(status_code=400, detail="merchant_id must be provided in request or token")
51
- # merchant_id from JWT token is a string, convert to UUID for database operations
52
- try:
53
- merchant_id = UUID(current_user.merchant_id)
54
- except ValueError:
55
- # If merchant_id is not a valid UUID, create a deterministic UUID from the string
56
- merchant_id = UUID(bytes=hashlib.md5(current_user.merchant_id.encode()).digest()[:16])
57
 
58
  aid = await create_appointment(
59
  merchant_id=merchant_id,
@@ -75,7 +70,7 @@ async def create_appointment_endpoint(
75
 
76
  @router.post("/list", response_model=ListAppointmentsResponse)
77
  async def list_appointments_endpoint(
78
- merchant_id: Optional[UUID] = None,
79
  start_from: Optional[datetime] = Query(None),
80
  start_to: Optional[datetime] = Query(None),
81
  staff_id: Optional[UUID] = Query(None),
@@ -89,12 +84,8 @@ async def list_appointments_endpoint(
89
  if merchant_id is None:
90
  if not current_user.merchant_id:
91
  raise HTTPException(status_code=400, detail="merchant_id must be provided in request or token")
92
- # merchant_id from JWT token is a string, convert to UUID for database operations
93
- try:
94
- merchant_id = UUID(current_user.merchant_id)
95
- except ValueError:
96
- # If merchant_id is not a valid UUID, create a deterministic UUID from the string
97
- merchant_id = UUID(bytes=hashlib.md5(current_user.merchant_id.encode()).digest()[:16])
98
 
99
  items, total = await list_appointments(
100
  merchant_id=merchant_id,
 
2
  Appointments API router.
3
  """
4
  import logging
 
5
  from uuid import UUID
6
  from typing import Optional
7
  from datetime import datetime
 
47
  if merchant_id is None:
48
  if not current_user.merchant_id:
49
  raise HTTPException(status_code=400, detail="merchant_id must be provided in request or token")
50
+ # Use merchant_id directly as string from JWT token
51
+ merchant_id = current_user.merchant_id
 
 
 
 
52
 
53
  aid = await create_appointment(
54
  merchant_id=merchant_id,
 
70
 
71
  @router.post("/list", response_model=ListAppointmentsResponse)
72
  async def list_appointments_endpoint(
73
+ merchant_id: Optional[str] = None,
74
  start_from: Optional[datetime] = Query(None),
75
  start_to: Optional[datetime] = Query(None),
76
  staff_id: Optional[UUID] = Query(None),
 
84
  if merchant_id is None:
85
  if not current_user.merchant_id:
86
  raise HTTPException(status_code=400, detail="merchant_id must be provided in request or token")
87
+ # Use merchant_id directly as string from JWT token
88
+ merchant_id = current_user.merchant_id
 
 
 
 
89
 
90
  items, total = await list_appointments(
91
  merchant_id=merchant_id,
app/appointments/schemas/schema.py CHANGED
@@ -15,7 +15,7 @@ class AppointmentServiceInput(BaseModel):
15
  service_price: Optional[float] = Field(default=None, ge=0)
16
 
17
  class CreateAppointmentRequest(BaseModel):
18
- merchant_id: Optional[UUID] = None
19
  source: str = Field(..., pattern="^(online|offline)$")
20
  booking_channel: Optional[str] = Field(None, pattern="^(app|web|walkin|phone|pos)$")
21
  customer_id: Optional[UUID] = None
@@ -45,7 +45,7 @@ class AppointmentServiceResponse(BaseModel):
45
 
46
  class AppointmentResponse(BaseModel):
47
  appointment_id: UUID
48
- merchant_id: UUID
49
  source: str
50
  booking_channel: Optional[str]
51
  customer_id: Optional[UUID]
 
15
  service_price: Optional[float] = Field(default=None, ge=0)
16
 
17
  class CreateAppointmentRequest(BaseModel):
18
+ merchant_id: Optional[str] = None
19
  source: str = Field(..., pattern="^(online|offline)$")
20
  booking_channel: Optional[str] = Field(None, pattern="^(app|web|walkin|phone|pos)$")
21
  customer_id: Optional[UUID] = None
 
45
 
46
  class AppointmentResponse(BaseModel):
47
  appointment_id: UUID
48
+ merchant_id: str
49
  source: str
50
  booking_channel: Optional[str]
51
  customer_id: Optional[UUID]
app/catalogue_services/controllers/router.py CHANGED
@@ -2,9 +2,7 @@
2
  Catalogue Services API router - FastAPI endpoints for service catalogue CRUD.
3
  """
4
  import logging
5
- import hashlib
6
  from typing import Optional
7
- from uuid import UUID
8
  from fastapi import APIRouter, HTTPException, Query, status, Depends
9
 
10
  from app.dependencies.auth import TokenUser
@@ -44,12 +42,8 @@ async def create_service_endpoint(
44
  if merchant_id is None:
45
  if not current_user.merchant_id:
46
  raise HTTPException(status_code=400, detail="merchant_id must be provided in request or token")
47
- # merchant_id from JWT token is a string, convert to UUID for database operations
48
- try:
49
- merchant_id = UUID(current_user.merchant_id)
50
- except ValueError:
51
- # If merchant_id is not a valid UUID, create a deterministic UUID from the string
52
- merchant_id = UUID(bytes=hashlib.md5(current_user.merchant_id.encode()).digest()[:16])
53
 
54
  doc = await create_service(
55
  merchant_id=merchant_id,
@@ -78,12 +72,8 @@ async def list_services_endpoint(
78
  if merchant_id is None:
79
  if not current_user.merchant_id:
80
  raise HTTPException(status_code=400, detail="merchant_id must be provided in request or token")
81
- # merchant_id from JWT token is a string, convert to UUID for database operations
82
- try:
83
- merchant_id = UUID(current_user.merchant_id)
84
- except ValueError:
85
- # If merchant_id is not a valid UUID, create a deterministic UUID from the string
86
- merchant_id = UUID(bytes=hashlib.md5(current_user.merchant_id.encode()).digest()[:16])
87
 
88
  items, total = await list_services(
89
  merchant_id=merchant_id,
@@ -111,7 +101,7 @@ async def list_services_endpoint(
111
 
112
  @router.get("/{service_id}", response_model=ServiceResponse)
113
  async def get_service_endpoint(
114
- service_id: UUID,
115
  current_user: TokenUser = Depends(require_pos_permission("retail_catalogue", "view"))
116
  ):
117
  doc = await get_service(service_id)
@@ -121,7 +111,7 @@ async def get_service_endpoint(
121
 
122
  @router.put("/{service_id}", response_model=ServiceResponse)
123
  async def update_service_endpoint(
124
- service_id: UUID,
125
  req: UpdateServiceRequest,
126
  current_user: TokenUser = Depends(require_pos_permission("retail_catalogue", "update"))
127
  ):
@@ -143,7 +133,7 @@ async def update_service_endpoint(
143
 
144
  @router.patch("/{service_id}/status", response_model=ServiceResponse)
145
  async def update_status_endpoint(
146
- service_id: UUID,
147
  req: UpdateStatusRequest,
148
  current_user: TokenUser = Depends(require_pos_permission("retail_catalogue", "update"))
149
  ):
@@ -155,7 +145,7 @@ async def update_status_endpoint(
155
 
156
  @router.delete("/{service_id}", response_model=ServiceResponse)
157
  async def delete_service_endpoint(
158
- service_id: UUID,
159
  current_user: TokenUser = Depends(require_pos_permission("retail_catalogue", "delete"))
160
  ):
161
  try:
@@ -166,8 +156,8 @@ async def delete_service_endpoint(
166
 
167
  def _to_response(doc: dict) -> ServiceResponse:
168
  return ServiceResponse(
169
- service_id=UUID(doc["_id"]) if isinstance(doc["_id"], str) else doc["_id"],
170
- merchant_id=UUID(doc["merchant_id"]) if isinstance(doc["merchant_id"], str) else doc["merchant_id"],
171
  name=doc["name"],
172
  code=doc["code"],
173
  category=doc.get("category"),
 
2
  Catalogue Services API router - FastAPI endpoints for service catalogue CRUD.
3
  """
4
  import logging
 
5
  from typing import Optional
 
6
  from fastapi import APIRouter, HTTPException, Query, status, Depends
7
 
8
  from app.dependencies.auth import TokenUser
 
42
  if merchant_id is None:
43
  if not current_user.merchant_id:
44
  raise HTTPException(status_code=400, detail="merchant_id must be provided in request or token")
45
+ # Use merchant_id directly as string from JWT token
46
+ merchant_id = current_user.merchant_id
 
 
 
 
47
 
48
  doc = await create_service(
49
  merchant_id=merchant_id,
 
72
  if merchant_id is None:
73
  if not current_user.merchant_id:
74
  raise HTTPException(status_code=400, detail="merchant_id must be provided in request or token")
75
+ # Use merchant_id directly as string from JWT token
76
+ merchant_id = current_user.merchant_id
 
 
 
 
77
 
78
  items, total = await list_services(
79
  merchant_id=merchant_id,
 
101
 
102
  @router.get("/{service_id}", response_model=ServiceResponse)
103
  async def get_service_endpoint(
104
+ service_id: str, # Changed from UUID to str
105
  current_user: TokenUser = Depends(require_pos_permission("retail_catalogue", "view"))
106
  ):
107
  doc = await get_service(service_id)
 
111
 
112
  @router.put("/{service_id}", response_model=ServiceResponse)
113
  async def update_service_endpoint(
114
+ service_id: str, # Changed from UUID to str
115
  req: UpdateServiceRequest,
116
  current_user: TokenUser = Depends(require_pos_permission("retail_catalogue", "update"))
117
  ):
 
133
 
134
  @router.patch("/{service_id}/status", response_model=ServiceResponse)
135
  async def update_status_endpoint(
136
+ service_id: str, # Changed from UUID to str
137
  req: UpdateStatusRequest,
138
  current_user: TokenUser = Depends(require_pos_permission("retail_catalogue", "update"))
139
  ):
 
145
 
146
  @router.delete("/{service_id}", response_model=ServiceResponse)
147
  async def delete_service_endpoint(
148
+ service_id: str, # Changed from UUID to str
149
  current_user: TokenUser = Depends(require_pos_permission("retail_catalogue", "delete"))
150
  ):
151
  try:
 
156
 
157
  def _to_response(doc: dict) -> ServiceResponse:
158
  return ServiceResponse(
159
+ service_id=doc["_id"], # Keep as string, don't convert to UUID
160
+ merchant_id=doc["merchant_id"], # Keep as string, don't convert to UUID
161
  name=doc["name"],
162
  code=doc["code"],
163
  category=doc.get("category"),
app/catalogue_services/schemas/schema.py CHANGED
@@ -3,7 +3,6 @@ Pydantic schemas for Catalogue Services API.
3
  """
4
  from typing import Optional, List
5
  from pydantic import BaseModel, Field
6
- from uuid import UUID
7
  from datetime import datetime
8
 
9
  class CategoryInput(BaseModel):
@@ -11,7 +10,7 @@ class CategoryInput(BaseModel):
11
  name: str
12
 
13
  class CreateServiceRequest(BaseModel):
14
- merchant_id: Optional[UUID] = None
15
  name: str = Field(..., min_length=1, max_length=200)
16
  code: str = Field(..., min_length=1, max_length=50)
17
  category_id: Optional[str] = None
@@ -35,8 +34,8 @@ class UpdateStatusRequest(BaseModel):
35
  status: str = Field(..., pattern="^(active|inactive|archived)$")
36
 
37
  class ServiceResponse(BaseModel):
38
- service_id: UUID
39
- merchant_id: UUID
40
  name: str
41
  code: str
42
  category: Optional[dict] = None
@@ -49,7 +48,7 @@ class ServiceResponse(BaseModel):
49
  updated_at: datetime
50
 
51
  class ListServicesRequest(BaseModel):
52
- merchant_id: Optional[UUID] = None
53
  status: Optional[str] = Field(None, pattern="^(active|inactive|archived)$")
54
  category: Optional[str] = None
55
  skip: int = Field(0, ge=0)
 
3
  """
4
  from typing import Optional, List
5
  from pydantic import BaseModel, Field
 
6
  from datetime import datetime
7
 
8
  class CategoryInput(BaseModel):
 
10
  name: str
11
 
12
  class CreateServiceRequest(BaseModel):
13
+ merchant_id: Optional[str] = None
14
  name: str = Field(..., min_length=1, max_length=200)
15
  code: str = Field(..., min_length=1, max_length=50)
16
  category_id: Optional[str] = None
 
34
  status: str = Field(..., pattern="^(active|inactive|archived)$")
35
 
36
  class ServiceResponse(BaseModel):
37
+ service_id: str # Changed from UUID to str to handle string IDs
38
+ merchant_id: str
39
  name: str
40
  code: str
41
  category: Optional[dict] = None
 
48
  updated_at: datetime
49
 
50
  class ListServicesRequest(BaseModel):
51
+ merchant_id: Optional[str] = None
52
  status: Optional[str] = Field(None, pattern="^(active|inactive|archived)$")
53
  category: Optional[str] = None
54
  skip: int = Field(0, ge=0)
app/catalogue_services/services/service.py CHANGED
@@ -2,7 +2,7 @@
2
  Catalogue Services business logic: Mongo master + Postgres sync.
3
  """
4
  import logging
5
- from uuid import uuid4, UUID
6
  from typing import Optional, List, Tuple
7
  from datetime import datetime
8
  from sqlalchemy import text
@@ -58,7 +58,7 @@ async def _sync_to_postgres(service_doc: dict):
58
  await session.rollback()
59
 
60
  async def create_service(
61
- merchant_id: UUID,
62
  name: str,
63
  code: str,
64
  category_id: Optional[str],
@@ -111,15 +111,15 @@ async def create_service(
111
 
112
  return doc
113
 
114
- async def get_service(service_id: UUID) -> Optional[dict]:
115
  """Get service by ID from Mongo."""
116
  db = get_database()
117
  if db is None:
118
  raise RuntimeError("MongoDB not available")
119
- return await db[CATALOGUE_SERVICES_COLLECTION].find_one({"_id": str(service_id)})
120
 
121
  async def list_services(
122
- merchant_id: UUID,
123
  status: Optional[str] = None,
124
  category: Optional[str] = None,
125
  skip: int = 0,
@@ -131,7 +131,9 @@ async def list_services(
131
  if db is None:
132
  raise RuntimeError("MongoDB not available")
133
 
134
- query = {"merchant_id": str(merchant_id)}
 
 
135
  if status:
136
  query["status"] = status
137
  if category:
@@ -150,7 +152,7 @@ async def list_services(
150
  return items, total
151
 
152
  async def update_service(
153
- service_id: UUID,
154
  name: Optional[str],
155
  code: Optional[str],
156
  category_id: Optional[str],
@@ -166,7 +168,7 @@ async def update_service(
166
  raise RuntimeError("MongoDB not available")
167
 
168
  # Check service exists and not archived
169
- existing = await db[CATALOGUE_SERVICES_COLLECTION].find_one({"_id": str(service_id)})
170
  if not existing:
171
  raise ValueError("Service not found")
172
  if existing.get("status") == "archived":
@@ -177,7 +179,7 @@ async def update_service(
177
  dup = await db[CATALOGUE_SERVICES_COLLECTION].find_one({
178
  "merchant_id": existing["merchant_id"],
179
  "code": code,
180
- "_id": {"$ne": str(service_id)}
181
  })
182
  if dup:
183
  raise ValueError(f"Service code '{code}' already exists")
@@ -208,12 +210,12 @@ async def update_service(
208
  update_data["pricing"] = pricing
209
 
210
  await db[CATALOGUE_SERVICES_COLLECTION].update_one(
211
- {"_id": str(service_id)},
212
  {"$set": update_data}
213
  )
214
 
215
  # Get updated doc
216
- updated = await db[CATALOGUE_SERVICES_COLLECTION].find_one({"_id": str(service_id)})
217
  logger.info(f"Updated service {service_id} in MongoDB")
218
 
219
  # Sync to Postgres
@@ -221,22 +223,22 @@ async def update_service(
221
 
222
  return updated
223
 
224
- async def update_status(service_id: UUID, status: str) -> dict:
225
  """Update service status and sync."""
226
  db = get_database()
227
  if db is None:
228
  raise RuntimeError("MongoDB not available")
229
 
230
- existing = await db[CATALOGUE_SERVICES_COLLECTION].find_one({"_id": str(service_id)})
231
  if not existing:
232
  raise ValueError("Service not found")
233
 
234
  await db[CATALOGUE_SERVICES_COLLECTION].update_one(
235
- {"_id": str(service_id)},
236
  {"$set": {"status": status, "updated_at": datetime.utcnow()}}
237
  )
238
 
239
- updated = await db[CATALOGUE_SERVICES_COLLECTION].find_one({"_id": str(service_id)})
240
  logger.info(f"Updated service {service_id} status to {status}")
241
 
242
  # Sync to Postgres
@@ -244,6 +246,6 @@ async def update_status(service_id: UUID, status: str) -> dict:
244
 
245
  return updated
246
 
247
- async def delete_service(service_id: UUID) -> dict:
248
  """Soft delete service (archive) and sync."""
249
  return await update_status(service_id, "archived")
 
2
  Catalogue Services business logic: Mongo master + Postgres sync.
3
  """
4
  import logging
5
+ from uuid import uuid4
6
  from typing import Optional, List, Tuple
7
  from datetime import datetime
8
  from sqlalchemy import text
 
58
  await session.rollback()
59
 
60
  async def create_service(
61
+ merchant_id, # Can be UUID or str
62
  name: str,
63
  code: str,
64
  category_id: Optional[str],
 
111
 
112
  return doc
113
 
114
+ async def get_service(service_id: str) -> Optional[dict]: # Changed from UUID to str
115
  """Get service by ID from Mongo."""
116
  db = get_database()
117
  if db is None:
118
  raise RuntimeError("MongoDB not available")
119
+ return await db[CATALOGUE_SERVICES_COLLECTION].find_one({"_id": service_id})
120
 
121
  async def list_services(
122
+ merchant_id, # Can be UUID or str
123
  status: Optional[str] = None,
124
  category: Optional[str] = None,
125
  skip: int = 0,
 
131
  if db is None:
132
  raise RuntimeError("MongoDB not available")
133
 
134
+ # Convert merchant_id to string for consistent querying
135
+ merchant_id_str = str(merchant_id)
136
+ query = {"merchant_id": merchant_id_str}
137
  if status:
138
  query["status"] = status
139
  if category:
 
152
  return items, total
153
 
154
  async def update_service(
155
+ service_id: str, # Changed from UUID to str
156
  name: Optional[str],
157
  code: Optional[str],
158
  category_id: Optional[str],
 
168
  raise RuntimeError("MongoDB not available")
169
 
170
  # Check service exists and not archived
171
+ existing = await db[CATALOGUE_SERVICES_COLLECTION].find_one({"_id": service_id})
172
  if not existing:
173
  raise ValueError("Service not found")
174
  if existing.get("status") == "archived":
 
179
  dup = await db[CATALOGUE_SERVICES_COLLECTION].find_one({
180
  "merchant_id": existing["merchant_id"],
181
  "code": code,
182
+ "_id": {"$ne": service_id}
183
  })
184
  if dup:
185
  raise ValueError(f"Service code '{code}' already exists")
 
210
  update_data["pricing"] = pricing
211
 
212
  await db[CATALOGUE_SERVICES_COLLECTION].update_one(
213
+ {"_id": service_id},
214
  {"$set": update_data}
215
  )
216
 
217
  # Get updated doc
218
+ updated = await db[CATALOGUE_SERVICES_COLLECTION].find_one({"_id": service_id})
219
  logger.info(f"Updated service {service_id} in MongoDB")
220
 
221
  # Sync to Postgres
 
223
 
224
  return updated
225
 
226
+ async def update_status(service_id: str, status: str) -> dict: # Changed from UUID to str
227
  """Update service status and sync."""
228
  db = get_database()
229
  if db is None:
230
  raise RuntimeError("MongoDB not available")
231
 
232
+ existing = await db[CATALOGUE_SERVICES_COLLECTION].find_one({"_id": service_id})
233
  if not existing:
234
  raise ValueError("Service not found")
235
 
236
  await db[CATALOGUE_SERVICES_COLLECTION].update_one(
237
+ {"_id": service_id},
238
  {"$set": {"status": status, "updated_at": datetime.utcnow()}}
239
  )
240
 
241
+ updated = await db[CATALOGUE_SERVICES_COLLECTION].find_one({"_id": service_id})
242
  logger.info(f"Updated service {service_id} status to {status}")
243
 
244
  # Sync to Postgres
 
246
 
247
  return updated
248
 
249
+ async def delete_service(service_id: str) -> dict: # Changed from UUID to str
250
  """Soft delete service (archive) and sync."""
251
  return await update_status(service_id, "archived")