MukeshKapoor25 commited on
Commit
1988d77
·
1 Parent(s): 85e4fac

feat(logging): implement structured logging across services

Browse files

Add centralized logging utility with structured logging format
Update all services to use new logger with contextual metadata
Enhance error handling with detailed error logging
Add global exception handlers with proper logging
Include operation context in all log entries

app/appointments/controllers/router.py CHANGED
@@ -9,6 +9,7 @@ from datetime import datetime
9
  from fastapi import APIRouter, HTTPException, status, Depends
10
  from fastapi.responses import JSONResponse
11
 
 
12
  from app.dependencies.auth import TokenUser
13
  from app.dependencies.pos_permissions import require_pos_permission
14
  from app.appointments.schemas.schema import (
@@ -31,7 +32,7 @@ from app.appointments.services.service import (
31
  checkout_appointment,
32
  )
33
 
34
- logger = logging.getLogger(__name__)
35
 
36
  def convert_uuids_to_strings(data):
37
  """Convert UUID objects to strings for JSON serialization"""
@@ -78,10 +79,35 @@ async def create_appointment_endpoint(
78
  created_by=req.created_by,
79
  )
80
  appt = await get_appointment(aid)
 
 
 
 
 
 
 
 
 
 
 
81
  return await _to_appt_response(appt)
 
 
82
  except Exception as e:
83
- logger.error(f"Create appointment failed: {e}")
84
- raise HTTPException(status_code=400, detail=str(e))
 
 
 
 
 
 
 
 
 
 
 
 
85
 
86
  @router.post("/list", response_model=None)
87
  async def list_appointments_endpoint(
@@ -156,9 +182,40 @@ async def update_appointment_endpoint(
156
  ):
157
  try:
158
  appt = await update_appointment(appointment_id, req.start_time, [s.model_dump() for s in req.services], req.notes)
 
 
 
 
 
 
 
 
 
159
  return await _to_appt_response(appt)
 
 
 
 
 
 
 
 
 
 
 
 
160
  except Exception as e:
161
- raise HTTPException(status_code=400, detail=str(e))
 
 
 
 
 
 
 
 
 
 
162
 
163
  @router.patch("/{appointment_id}/status", response_model=AppointmentResponse)
164
  async def update_status_endpoint(
@@ -168,9 +225,41 @@ async def update_status_endpoint(
168
  ):
169
  try:
170
  appt = await update_status(appointment_id, req.status)
 
 
 
 
 
 
 
 
 
 
171
  return await _to_appt_response(appt)
 
 
 
 
 
 
 
 
 
 
 
 
172
  except Exception as e:
173
- raise HTTPException(status_code=400, detail=str(e))
 
 
 
 
 
 
 
 
 
 
174
 
175
  @router.post("/{appointment_id}/cancel", response_model=AppointmentResponse)
176
  async def cancel_appointment_endpoint(
@@ -179,9 +268,40 @@ async def cancel_appointment_endpoint(
179
  ):
180
  try:
181
  appt = await cancel_appointment(appointment_id)
 
 
 
 
 
 
 
 
 
182
  return await _to_appt_response(appt)
 
 
 
 
 
 
 
 
 
 
 
 
183
  except Exception as e:
184
- raise HTTPException(status_code=400, detail=str(e))
 
 
 
 
 
 
 
 
 
 
185
 
186
  @router.post("/{appointment_id}/checkout", response_model=CheckoutResponse)
187
  async def checkout_appointment_endpoint(
@@ -190,9 +310,41 @@ async def checkout_appointment_endpoint(
190
  ):
191
  try:
192
  sale_id, sale = await checkout_appointment(appointment_id)
 
 
 
 
 
 
 
 
 
 
193
  return CheckoutResponse(appointment_id=appointment_id, sale_id=sale_id, status="billed")
 
 
 
 
 
 
 
 
 
 
 
 
194
  except Exception as e:
195
- raise HTTPException(status_code=400, detail=str(e))
 
 
 
 
 
 
 
 
 
 
196
 
197
  async def _to_appt_response(appt: dict) -> AppointmentResponse:
198
  services = [AppointmentServiceResponse(
 
9
  from fastapi import APIRouter, HTTPException, status, Depends
10
  from fastapi.responses import JSONResponse
11
 
12
+ from app.core.logging import get_logger
13
  from app.dependencies.auth import TokenUser
14
  from app.dependencies.pos_permissions import require_pos_permission
15
  from app.appointments.schemas.schema import (
 
32
  checkout_appointment,
33
  )
34
 
35
+ logger = get_logger(__name__)
36
 
37
  def convert_uuids_to_strings(data):
38
  """Convert UUID objects to strings for JSON serialization"""
 
79
  created_by=req.created_by,
80
  )
81
  appt = await get_appointment(aid)
82
+
83
+ logger.info(
84
+ "Appointment created successfully",
85
+ extra={
86
+ "operation": "create_appointment",
87
+ "appointment_id": str(aid),
88
+ "merchant_id": str(merchant_id),
89
+ "user_id": str(current_user.id) if current_user.id else None
90
+ }
91
+ )
92
+
93
  return await _to_appt_response(appt)
94
+ except HTTPException:
95
+ raise
96
  except Exception as e:
97
+ logger.error(
98
+ "Create appointment failed",
99
+ extra={
100
+ "operation": "create_appointment",
101
+ "error": str(e),
102
+ "error_type": type(e).__name__,
103
+ "merchant_id": str(merchant_id) if 'merchant_id' in locals() else None
104
+ },
105
+ exc_info=True
106
+ )
107
+ raise HTTPException(
108
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
109
+ detail="Failed to create appointment"
110
+ )
111
 
112
  @router.post("/list", response_model=None)
113
  async def list_appointments_endpoint(
 
182
  ):
183
  try:
184
  appt = await update_appointment(appointment_id, req.start_time, [s.model_dump() for s in req.services], req.notes)
185
+
186
+ logger.info(
187
+ "Appointment updated",
188
+ extra={
189
+ "operation": "update_appointment",
190
+ "appointment_id": str(appointment_id),
191
+ "user_id": str(current_user.id) if current_user.id else None
192
+ }
193
+ )
194
  return await _to_appt_response(appt)
195
+ except HTTPException:
196
+ raise
197
+ except ValueError as e:
198
+ logger.warning(
199
+ "Update appointment validation failed",
200
+ extra={
201
+ "operation": "update_appointment",
202
+ "appointment_id": str(appointment_id),
203
+ "error": str(e)
204
+ }
205
+ )
206
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
207
  except Exception as e:
208
+ logger.error(
209
+ "Update appointment failed",
210
+ extra={
211
+ "operation": "update_appointment",
212
+ "appointment_id": str(appointment_id),
213
+ "error": str(e),
214
+ "error_type": type(e).__name__
215
+ },
216
+ exc_info=True
217
+ )
218
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to update appointment")
219
 
220
  @router.patch("/{appointment_id}/status", response_model=AppointmentResponse)
221
  async def update_status_endpoint(
 
225
  ):
226
  try:
227
  appt = await update_status(appointment_id, req.status)
228
+
229
+ logger.info(
230
+ "Appointment status updated",
231
+ extra={
232
+ "operation": "update_status",
233
+ "appointment_id": str(appointment_id),
234
+ "status": req.status,
235
+ "user_id": str(current_user.id) if current_user.id else None
236
+ }
237
+ )
238
  return await _to_appt_response(appt)
239
+ except HTTPException:
240
+ raise
241
+ except ValueError as e:
242
+ logger.warning(
243
+ "Update status validation failed",
244
+ extra={
245
+ "operation": "update_status",
246
+ "appointment_id": str(appointment_id),
247
+ "error": str(e)
248
+ }
249
+ )
250
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
251
  except Exception as e:
252
+ logger.error(
253
+ "Update status failed",
254
+ extra={
255
+ "operation": "update_status",
256
+ "appointment_id": str(appointment_id),
257
+ "error": str(e),
258
+ "error_type": type(e).__name__
259
+ },
260
+ exc_info=True
261
+ )
262
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to update status")
263
 
264
  @router.post("/{appointment_id}/cancel", response_model=AppointmentResponse)
265
  async def cancel_appointment_endpoint(
 
268
  ):
269
  try:
270
  appt = await cancel_appointment(appointment_id)
271
+
272
+ logger.info(
273
+ "Appointment cancelled",
274
+ extra={
275
+ "operation": "cancel_appointment",
276
+ "appointment_id": str(appointment_id),
277
+ "user_id": str(current_user.id) if current_user.id else None
278
+ }
279
+ )
280
  return await _to_appt_response(appt)
281
+ except HTTPException:
282
+ raise
283
+ except ValueError as e:
284
+ logger.warning(
285
+ "Cancel appointment validation failed",
286
+ extra={
287
+ "operation": "cancel_appointment",
288
+ "appointment_id": str(appointment_id),
289
+ "error": str(e)
290
+ }
291
+ )
292
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
293
  except Exception as e:
294
+ logger.error(
295
+ "Cancel appointment failed",
296
+ extra={
297
+ "operation": "cancel_appointment",
298
+ "appointment_id": str(appointment_id),
299
+ "error": str(e),
300
+ "error_type": type(e).__name__
301
+ },
302
+ exc_info=True
303
+ )
304
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to cancel appointment")
305
 
306
  @router.post("/{appointment_id}/checkout", response_model=CheckoutResponse)
307
  async def checkout_appointment_endpoint(
 
310
  ):
311
  try:
312
  sale_id, sale = await checkout_appointment(appointment_id)
313
+
314
+ logger.info(
315
+ "Appointment checkout successful",
316
+ extra={
317
+ "operation": "checkout_appointment",
318
+ "appointment_id": str(appointment_id),
319
+ "sale_id": str(sale_id),
320
+ "user_id": str(current_user.id) if current_user.id else None
321
+ }
322
+ )
323
  return CheckoutResponse(appointment_id=appointment_id, sale_id=sale_id, status="billed")
324
+ except HTTPException:
325
+ raise
326
+ except ValueError as e:
327
+ logger.warning(
328
+ "Checkout validation failed",
329
+ extra={
330
+ "operation": "checkout_appointment",
331
+ "appointment_id": str(appointment_id),
332
+ "error": str(e)
333
+ }
334
+ )
335
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
336
  except Exception as e:
337
+ logger.error(
338
+ "Checkout failed",
339
+ extra={
340
+ "operation": "checkout_appointment",
341
+ "appointment_id": str(appointment_id),
342
+ "error": str(e),
343
+ "error_type": type(e).__name__
344
+ },
345
+ exc_info=True
346
+ )
347
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to checkout appointment")
348
 
349
  async def _to_appt_response(appt: dict) -> AppointmentResponse:
350
  services = [AppointmentServiceResponse(
app/appointments/services/service.py CHANGED
@@ -7,9 +7,10 @@ from typing import Optional, List, Tuple
7
  from datetime import timedelta
8
  from sqlalchemy import text
9
 
 
10
  from app.sql import get_postgres_session
11
 
12
- logger = logging.getLogger(__name__)
13
 
14
  def _service_details_from_input(s: dict) -> Tuple[str, int, float]:
15
  """Derive service details from input without Mongo.
@@ -104,11 +105,24 @@ async def create_appointment(
104
  "price": e["service_price"],
105
  })
106
  await session.commit()
107
- logger.info(f"Created appointment {appointment_id} with {len(enriched)} services")
 
 
 
 
 
 
108
  return appointment_id
109
  except Exception as e:
110
  await session.rollback()
111
- logger.error(f"Failed to create appointment: {e}")
 
 
 
 
 
 
 
112
  raise
113
 
114
  async def get_appointment(appointment_id: UUID) -> Optional[dict]:
@@ -247,7 +261,15 @@ async def update_appointment(appointment_id: UUID, start_time, services: List[di
247
  await session.commit()
248
  except Exception as e:
249
  await session.rollback()
250
- logger.error(f"Failed to update appointment: {e}")
 
 
 
 
 
 
 
 
251
  raise
252
  appt = await get_appointment(appointment_id)
253
  return appt
@@ -271,7 +293,16 @@ async def update_status(appointment_id: UUID, status: str) -> dict:
271
  await session.commit()
272
  except Exception as e:
273
  await session.rollback()
274
- logger.error(f"Failed to update status: {e}")
 
 
 
 
 
 
 
 
 
275
  raise
276
  return await get_appointment(appointment_id)
277
 
 
7
  from datetime import timedelta
8
  from sqlalchemy import text
9
 
10
+ from app.core.logging import get_logger
11
  from app.sql import get_postgres_session
12
 
13
+ logger = get_logger(__name__)
14
 
15
  def _service_details_from_input(s: dict) -> Tuple[str, int, float]:
16
  """Derive service details from input without Mongo.
 
105
  "price": e["service_price"],
106
  })
107
  await session.commit()
108
+ logger.info(
109
+ "Created appointment",
110
+ extra={
111
+ "appointment_id": str(appointment_id),
112
+ "service_count": len(enriched)
113
+ }
114
+ )
115
  return appointment_id
116
  except Exception as e:
117
  await session.rollback()
118
+ logger.error(
119
+ "Failed to create appointment",
120
+ extra={
121
+ "error": str(e),
122
+ "error_type": type(e).__name__
123
+ },
124
+ exc_info=True
125
+ )
126
  raise
127
 
128
  async def get_appointment(appointment_id: UUID) -> Optional[dict]:
 
261
  await session.commit()
262
  except Exception as e:
263
  await session.rollback()
264
+ logger.error(
265
+ "Failed to update appointment",
266
+ extra={
267
+ "appointment_id": str(appointment_id),
268
+ "error": str(e),
269
+ "error_type": type(e).__name__
270
+ },
271
+ exc_info=True
272
+ )
273
  raise
274
  appt = await get_appointment(appointment_id)
275
  return appt
 
293
  await session.commit()
294
  except Exception as e:
295
  await session.rollback()
296
+ logger.error(
297
+ "Failed to update status",
298
+ extra={
299
+ "appointment_id": str(appointment_id),
300
+ "status": status,
301
+ "error": str(e),
302
+ "error_type": type(e).__name__
303
+ },
304
+ exc_info=True
305
+ )
306
  raise
307
  return await get_appointment(appointment_id)
308
 
app/catalogue_services/controllers/router.py CHANGED
@@ -5,6 +5,7 @@ import logging
5
  from typing import Optional
6
  from fastapi import APIRouter, HTTPException, Query, status, Depends
7
 
 
8
  from app.dependencies.auth import TokenUser
9
  from app.dependencies.pos_permissions import require_pos_permission
10
  from app.catalogue_services.schemas.schema import (
@@ -24,7 +25,7 @@ from app.catalogue_services.services.service import (
24
  delete_service,
25
  )
26
 
27
- logger = logging.getLogger(__name__)
28
 
29
  router = APIRouter(
30
  prefix="/pos/catalogue/services",
@@ -56,10 +57,42 @@ async def create_service_endpoint(
56
  price=req.price,
57
  gst_rate=req.gst_rate,
58
  )
 
 
 
 
 
 
 
 
 
 
 
59
  return _to_response(doc)
 
 
 
 
 
 
 
 
 
 
 
 
60
  except Exception as e:
61
- logger.error(f"Create service failed: {e}")
62
- raise HTTPException(status_code=400, detail=str(e))
 
 
 
 
 
 
 
 
 
63
 
64
  @router.post("/list", response_model=ListServicesResponse)
65
  async def list_services_endpoint(
@@ -84,23 +117,59 @@ async def list_services_endpoint(
84
  projection_list=req.projection_list
85
  )
86
 
 
 
 
 
 
 
 
 
 
 
87
  return ListServicesResponse(
88
  items=items, # Service layer returns raw dicts
89
  total=total
90
  )
 
 
91
  except Exception as e:
92
- logger.error(f"List services failed: {e}")
93
- raise HTTPException(status_code=400, detail=str(e))
 
 
 
 
 
 
 
 
 
94
 
95
  @router.get("/{service_id}", response_model=ServiceResponse)
96
  async def get_service_endpoint(
97
  service_id: str, # Changed from UUID to str
98
  current_user: TokenUser = Depends(require_pos_permission("retail_catalogue", "view"))
99
  ):
100
- doc = await get_service(service_id)
101
- if not doc:
102
- raise HTTPException(status_code=404, detail="Service not found")
103
- return _to_response(doc)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
 
105
  @router.put("/{service_id}", response_model=ServiceResponse)
106
  async def update_service_endpoint(
@@ -120,9 +189,40 @@ async def update_service_endpoint(
120
  price=req.price,
121
  gst_rate=req.gst_rate,
122
  )
 
 
 
 
 
 
 
 
 
123
  return _to_response(doc)
 
 
 
 
 
 
 
 
 
 
 
 
124
  except Exception as e:
125
- raise HTTPException(status_code=400, detail=str(e))
 
 
 
 
 
 
 
 
 
 
126
 
127
  @router.patch("/{service_id}/status", response_model=ServiceResponse)
128
  async def update_status_endpoint(
@@ -132,9 +232,41 @@ async def update_status_endpoint(
132
  ):
133
  try:
134
  doc = await update_status(service_id, req.status)
 
 
 
 
 
 
 
 
 
 
135
  return _to_response(doc)
 
 
 
 
 
 
 
 
 
 
 
 
136
  except Exception as e:
137
- raise HTTPException(status_code=400, detail=str(e))
 
 
 
 
 
 
 
 
 
 
138
 
139
  @router.delete("/{service_id}", response_model=ServiceResponse)
140
  async def delete_service_endpoint(
@@ -143,9 +275,40 @@ async def delete_service_endpoint(
143
  ):
144
  try:
145
  doc = await delete_service(service_id)
 
 
 
 
 
 
 
 
 
146
  return _to_response(doc)
 
 
 
 
 
 
 
 
 
 
 
 
147
  except Exception as e:
148
- raise HTTPException(status_code=400, detail=str(e))
 
 
 
 
 
 
 
 
 
 
149
 
150
  def _to_response(doc: dict) -> ServiceResponse:
151
  return ServiceResponse(
 
5
  from typing import Optional
6
  from fastapi import APIRouter, HTTPException, Query, status, Depends
7
 
8
+ from app.core.logging import get_logger
9
  from app.dependencies.auth import TokenUser
10
  from app.dependencies.pos_permissions import require_pos_permission
11
  from app.catalogue_services.schemas.schema import (
 
25
  delete_service,
26
  )
27
 
28
+ logger = get_logger(__name__)
29
 
30
  router = APIRouter(
31
  prefix="/pos/catalogue/services",
 
57
  price=req.price,
58
  gst_rate=req.gst_rate,
59
  )
60
+
61
+ logger.info(
62
+ "Service created successfully",
63
+ extra={
64
+ "operation": "create_service",
65
+ "service_id": str(doc["_id"]),
66
+ "merchant_id": str(merchant_id),
67
+ "user_id": str(current_user.id) if current_user.id else None
68
+ }
69
+ )
70
+
71
  return _to_response(doc)
72
+ except HTTPException:
73
+ raise
74
+ except ValueError as e:
75
+ logger.warning(
76
+ "Create service validation failed",
77
+ extra={
78
+ "operation": "create_service",
79
+ "error": str(e),
80
+ "merchant_id": str(merchant_id) if 'merchant_id' in locals() else None
81
+ }
82
+ )
83
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
84
  except Exception as e:
85
+ logger.error(
86
+ "Create service failed",
87
+ extra={
88
+ "operation": "create_service",
89
+ "error": str(e),
90
+ "error_type": type(e).__name__,
91
+ "merchant_id": str(merchant_id) if 'merchant_id' in locals() else None
92
+ },
93
+ exc_info=True
94
+ )
95
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to create service")
96
 
97
  @router.post("/list", response_model=ListServicesResponse)
98
  async def list_services_endpoint(
 
117
  projection_list=req.projection_list
118
  )
119
 
120
+ logger.info(
121
+ "Services listed",
122
+ extra={
123
+ "operation": "list_services",
124
+ "count": total,
125
+ "merchant_id": str(merchant_id),
126
+ "user_id": str(current_user.id) if current_user.id else None
127
+ }
128
+ )
129
+
130
  return ListServicesResponse(
131
  items=items, # Service layer returns raw dicts
132
  total=total
133
  )
134
+ except HTTPException:
135
+ raise
136
  except Exception as e:
137
+ logger.error(
138
+ "List services failed",
139
+ extra={
140
+ "operation": "list_services",
141
+ "error": str(e),
142
+ "error_type": type(e).__name__,
143
+ "merchant_id": str(merchant_id) if 'merchant_id' in locals() else None
144
+ },
145
+ exc_info=True
146
+ )
147
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to list services")
148
 
149
  @router.get("/{service_id}", response_model=ServiceResponse)
150
  async def get_service_endpoint(
151
  service_id: str, # Changed from UUID to str
152
  current_user: TokenUser = Depends(require_pos_permission("retail_catalogue", "view"))
153
  ):
154
+ try:
155
+ doc = await get_service(service_id)
156
+ if not doc:
157
+ raise HTTPException(status_code=404, detail="Service not found")
158
+ return _to_response(doc)
159
+ except HTTPException:
160
+ raise
161
+ except Exception as e:
162
+ logger.error(
163
+ "Get service failed",
164
+ extra={
165
+ "operation": "get_service",
166
+ "service_id": service_id,
167
+ "error": str(e),
168
+ "error_type": type(e).__name__
169
+ },
170
+ exc_info=True
171
+ )
172
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to get service")
173
 
174
  @router.put("/{service_id}", response_model=ServiceResponse)
175
  async def update_service_endpoint(
 
189
  price=req.price,
190
  gst_rate=req.gst_rate,
191
  )
192
+
193
+ logger.info(
194
+ "Service updated",
195
+ extra={
196
+ "operation": "update_service",
197
+ "service_id": service_id,
198
+ "user_id": str(current_user.id) if current_user.id else None
199
+ }
200
+ )
201
  return _to_response(doc)
202
+ except HTTPException:
203
+ raise
204
+ except ValueError as e:
205
+ logger.warning(
206
+ "Update service validation failed",
207
+ extra={
208
+ "operation": "update_service",
209
+ "service_id": service_id,
210
+ "error": str(e)
211
+ }
212
+ )
213
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
214
  except Exception as e:
215
+ logger.error(
216
+ "Update service failed",
217
+ extra={
218
+ "operation": "update_service",
219
+ "service_id": service_id,
220
+ "error": str(e),
221
+ "error_type": type(e).__name__
222
+ },
223
+ exc_info=True
224
+ )
225
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to update service")
226
 
227
  @router.patch("/{service_id}/status", response_model=ServiceResponse)
228
  async def update_status_endpoint(
 
232
  ):
233
  try:
234
  doc = await update_status(service_id, req.status)
235
+
236
+ logger.info(
237
+ "Service status updated",
238
+ extra={
239
+ "operation": "update_status",
240
+ "service_id": service_id,
241
+ "status": req.status,
242
+ "user_id": str(current_user.id) if current_user.id else None
243
+ }
244
+ )
245
  return _to_response(doc)
246
+ except HTTPException:
247
+ raise
248
+ except ValueError as e:
249
+ logger.warning(
250
+ "Update status validation failed",
251
+ extra={
252
+ "operation": "update_status",
253
+ "service_id": service_id,
254
+ "error": str(e)
255
+ }
256
+ )
257
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
258
  except Exception as e:
259
+ logger.error(
260
+ "Update status failed",
261
+ extra={
262
+ "operation": "update_status",
263
+ "service_id": service_id,
264
+ "error": str(e),
265
+ "error_type": type(e).__name__
266
+ },
267
+ exc_info=True
268
+ )
269
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to update status")
270
 
271
  @router.delete("/{service_id}", response_model=ServiceResponse)
272
  async def delete_service_endpoint(
 
275
  ):
276
  try:
277
  doc = await delete_service(service_id)
278
+
279
+ logger.info(
280
+ "Service deleted",
281
+ extra={
282
+ "operation": "delete_service",
283
+ "service_id": service_id,
284
+ "user_id": str(current_user.id) if current_user.id else None
285
+ }
286
+ )
287
  return _to_response(doc)
288
+ except HTTPException:
289
+ raise
290
+ except ValueError as e:
291
+ logger.warning(
292
+ "Delete service validation failed",
293
+ extra={
294
+ "operation": "delete_service",
295
+ "service_id": service_id,
296
+ "error": str(e)
297
+ }
298
+ )
299
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
300
  except Exception as e:
301
+ logger.error(
302
+ "Delete service failed",
303
+ extra={
304
+ "operation": "delete_service",
305
+ "service_id": service_id,
306
+ "error": str(e),
307
+ "error_type": type(e).__name__
308
+ },
309
+ exc_info=True
310
+ )
311
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to delete service")
312
 
313
  def _to_response(doc: dict) -> ServiceResponse:
314
  return ServiceResponse(
app/catalogue_services/services/service.py CHANGED
@@ -7,10 +7,11 @@ from typing import Optional, List, Tuple
7
  from datetime import datetime
8
  from sqlalchemy import text
9
 
 
10
  from app.nosql import get_database
11
  from app.sql import get_postgres_session
12
 
13
- logger = logging.getLogger(__name__)
14
 
15
  CATALOGUE_SERVICES_COLLECTION = "pos_catalogue_services"
16
 
@@ -52,9 +53,25 @@ async def _sync_to_postgres(service_doc: dict):
52
  "status": service_doc.get("status", "active"),
53
  })
54
  await session.commit()
55
- logger.info(f"Synced service {service_doc['_id']} to Postgres")
 
 
 
 
 
 
 
56
  except Exception as e:
57
- logger.error(f"Failed to sync service to Postgres: {e}")
 
 
 
 
 
 
 
 
 
58
  await session.rollback()
59
 
60
  async def create_service(
@@ -104,7 +121,14 @@ async def create_service(
104
  }
105
 
106
  await db[CATALOGUE_SERVICES_COLLECTION].insert_one(doc)
107
- logger.info(f"Created service {service_id} in MongoDB")
 
 
 
 
 
 
 
108
 
109
  # Sync to Postgres
110
  await _sync_to_postgres(doc)
@@ -266,7 +290,13 @@ async def update_service(
266
 
267
  # Get updated doc
268
  updated = await db[CATALOGUE_SERVICES_COLLECTION].find_one({"_id": service_id})
269
- logger.info(f"Updated service {service_id} in MongoDB")
 
 
 
 
 
 
270
 
271
  # Sync to Postgres
272
  await _sync_to_postgres(updated)
@@ -289,7 +319,14 @@ async def update_status(service_id: str, status: str) -> dict: # Changed from U
289
  )
290
 
291
  updated = await db[CATALOGUE_SERVICES_COLLECTION].find_one({"_id": service_id})
292
- logger.info(f"Updated service {service_id} status to {status}")
 
 
 
 
 
 
 
293
 
294
  # Sync to Postgres
295
  await _sync_to_postgres(updated)
 
7
  from datetime import datetime
8
  from sqlalchemy import text
9
 
10
+ from app.core.logging import get_logger
11
  from app.nosql import get_database
12
  from app.sql import get_postgres_session
13
 
14
+ logger = get_logger(__name__)
15
 
16
  CATALOGUE_SERVICES_COLLECTION = "pos_catalogue_services"
17
 
 
53
  "status": service_doc.get("status", "active"),
54
  })
55
  await session.commit()
56
+ logger.info(
57
+ f"Synced service {service_doc['_id']} to Postgres",
58
+ extra={
59
+ "operation": "sync_to_postgres",
60
+ "service_id": service_doc["_id"],
61
+ "merchant_id": service_doc["merchant_id"]
62
+ }
63
+ )
64
  except Exception as e:
65
+ logger.error(
66
+ f"Failed to sync service to Postgres: {e}",
67
+ extra={
68
+ "operation": "sync_to_postgres",
69
+ "service_id": service_doc["_id"],
70
+ "merchant_id": service_doc["merchant_id"],
71
+ "error": str(e)
72
+ },
73
+ exc_info=True
74
+ )
75
  await session.rollback()
76
 
77
  async def create_service(
 
121
  }
122
 
123
  await db[CATALOGUE_SERVICES_COLLECTION].insert_one(doc)
124
+ logger.info(
125
+ f"Created service {service_id} in MongoDB",
126
+ extra={
127
+ "operation": "create_service_mongo",
128
+ "service_id": service_id,
129
+ "merchant_id": str(merchant_id)
130
+ }
131
+ )
132
 
133
  # Sync to Postgres
134
  await _sync_to_postgres(doc)
 
290
 
291
  # Get updated doc
292
  updated = await db[CATALOGUE_SERVICES_COLLECTION].find_one({"_id": service_id})
293
+ logger.info(
294
+ f"Updated service {service_id} in MongoDB",
295
+ extra={
296
+ "operation": "update_service_mongo",
297
+ "service_id": service_id
298
+ }
299
+ )
300
 
301
  # Sync to Postgres
302
  await _sync_to_postgres(updated)
 
319
  )
320
 
321
  updated = await db[CATALOGUE_SERVICES_COLLECTION].find_one({"_id": service_id})
322
+ logger.info(
323
+ f"Updated service {service_id} status to {status}",
324
+ extra={
325
+ "operation": "update_status_mongo",
326
+ "service_id": service_id,
327
+ "status": status
328
+ }
329
+ )
330
 
331
  # Sync to Postgres
332
  await _sync_to_postgres(updated)
app/core/logging.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+
3
+ def get_logger(name: str) -> logging.Logger:
4
+ """
5
+ Get a logger instance with the specified name.
6
+
7
+ Args:
8
+ name: The name of the logger, typically __name__
9
+
10
+ Returns:
11
+ logging.Logger: The configured logger instance
12
+ """
13
+ return logging.getLogger(name)
app/customers/controllers/router.py CHANGED
@@ -4,6 +4,7 @@ Customer API router for POS microservice.
4
  from typing import Optional
5
  from fastapi import APIRouter, HTTPException, Query, status, Depends
6
 
 
7
  from app.dependencies.auth import TokenUser
8
  from app.dependencies.pos_permissions import require_pos_permission
9
  from app.customers.schemas.schema import (
@@ -15,6 +16,8 @@ from app.customers.schemas.schema import (
15
  )
16
  from app.customers.services.service import CustomerService
17
 
 
 
18
  router = APIRouter(
19
  prefix="/customers",
20
  tags=["Customers"],
@@ -33,10 +36,46 @@ async def create_customer(
33
  **Note:** merchant_id from request payload is ignored. The merchant_id from the authentication token is used instead.
34
  This ensures proper merchant isolation and UUID format consistency.
35
  """
36
- if not current_user.merchant_id:
37
- raise HTTPException(status_code=400, detail="merchant_id must be available in token")
38
-
39
- return await CustomerService.create_customer(payload, current_user.merchant_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
 
42
  @router.get("/{customer_id}", response_model=CustomerResponse, summary="Get customer by ID")
@@ -44,14 +83,29 @@ async def get_customer(
44
  customer_id: str,
45
  current_user: TokenUser = Depends(require_pos_permission("customers", "view"))
46
  ) -> CustomerResponse:
47
- customer = await CustomerService.get_customer(
48
- customer_id=customer_id,
49
- merchant_id=current_user.merchant_id,
50
- merchant_type=current_user.merchant_type
51
- )
52
- if not customer:
53
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Customer not found")
54
- return customer
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
 
57
  @router.post("/list", response_model=None, summary="List customers")
@@ -78,40 +132,65 @@ async def list_customers(
78
  - skip: Records to skip (default: 0)
79
  - limit: Max records to return (default: 50, max: 500)
80
  """
81
- # Extract filters and parameters from payload
82
- filters = payload.filters or {}
83
- skip = payload.skip or 0
84
- limit = payload.limit or 50
85
- projection_list = payload.projection_list
86
-
87
- # Always use merchant_id from token
88
- if not current_user.merchant_id:
89
- raise HTTPException(status_code=400, detail="merchant_id must be available in token")
90
-
91
- customers, total = await CustomerService.list_customers(
92
- merchant_id=current_user.merchant_id,
93
- merchant_type=current_user.merchant_type,
94
- filters=filters,
95
- skip=skip,
96
- limit=limit,
97
- projection_list=projection_list
98
- )
99
-
100
- # Return raw dict if projection used, model otherwise
101
- if projection_list:
102
- return CustomerListResponse(
103
- customers=customers, # Raw dicts when projection is used
104
- total=total,
105
  skip=skip,
106
- limit=limit
 
107
  )
108
- else:
109
- return CustomerListResponse(
110
- customers=customers, # CustomerResponse objects when no projection
111
- total=total,
112
- skip=skip,
113
- limit=limit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  )
 
115
 
116
 
117
  @router.put("/{customer_id}", response_model=CustomerResponse, summary="Update customer")
@@ -120,12 +199,47 @@ async def update_customer(
120
  payload: CustomerUpdate,
121
  current_user: TokenUser = Depends(require_pos_permission("customers", "update"))
122
  ) -> CustomerResponse:
123
- return await CustomerService.update_customer(
124
- customer_id=customer_id,
125
- payload=payload,
126
- merchant_id=current_user.merchant_id,
127
- merchant_type=current_user.merchant_type
128
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
 
130
 
131
  @router.delete("/{customer_id}", status_code=status.HTTP_204_NO_CONTENT, summary="Delete customer (soft)")
@@ -133,9 +247,33 @@ async def delete_customer(
133
  customer_id: str,
134
  current_user: TokenUser = Depends(require_pos_permission("customers", "delete"))
135
  ):
136
- await CustomerService.delete_customer(
137
- customer_id=customer_id,
138
- merchant_id=current_user.merchant_id,
139
- merchant_type=current_user.merchant_type
140
- )
141
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  from typing import Optional
5
  from fastapi import APIRouter, HTTPException, Query, status, Depends
6
 
7
+ from app.core.logging import get_logger
8
  from app.dependencies.auth import TokenUser
9
  from app.dependencies.pos_permissions import require_pos_permission
10
  from app.customers.schemas.schema import (
 
16
  )
17
  from app.customers.services.service import CustomerService
18
 
19
+ logger = get_logger(__name__)
20
+
21
  router = APIRouter(
22
  prefix="/customers",
23
  tags=["Customers"],
 
36
  **Note:** merchant_id from request payload is ignored. The merchant_id from the authentication token is used instead.
37
  This ensures proper merchant isolation and UUID format consistency.
38
  """
39
+ try:
40
+ if not current_user.merchant_id:
41
+ raise HTTPException(status_code=400, detail="merchant_id must be available in token")
42
+
43
+ customer = await CustomerService.create_customer(payload, current_user.merchant_id)
44
+
45
+ logger.info(
46
+ "Customer created successfully",
47
+ extra={
48
+ "operation": "create_customer",
49
+ "customer_id": customer.customer_id,
50
+ "merchant_id": str(current_user.merchant_id),
51
+ "user_id": str(current_user.id) if current_user.id else None
52
+ }
53
+ )
54
+ return customer
55
+ except HTTPException:
56
+ raise
57
+ except ValueError as e:
58
+ logger.warning(
59
+ "Create customer validation failed",
60
+ extra={
61
+ "operation": "create_customer",
62
+ "error": str(e),
63
+ "merchant_id": str(current_user.merchant_id) if current_user.merchant_id else None
64
+ }
65
+ )
66
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
67
+ except Exception as e:
68
+ logger.error(
69
+ "Create customer failed",
70
+ extra={
71
+ "operation": "create_customer",
72
+ "error": str(e),
73
+ "error_type": type(e).__name__,
74
+ "merchant_id": str(current_user.merchant_id) if current_user.merchant_id else None
75
+ },
76
+ exc_info=True
77
+ )
78
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to create customer")
79
 
80
 
81
  @router.get("/{customer_id}", response_model=CustomerResponse, summary="Get customer by ID")
 
83
  customer_id: str,
84
  current_user: TokenUser = Depends(require_pos_permission("customers", "view"))
85
  ) -> CustomerResponse:
86
+ try:
87
+ customer = await CustomerService.get_customer(
88
+ customer_id=customer_id,
89
+ merchant_id=current_user.merchant_id,
90
+ merchant_type=current_user.merchant_type
91
+ )
92
+ if not customer:
93
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Customer not found")
94
+ return customer
95
+ except HTTPException:
96
+ raise
97
+ except Exception as e:
98
+ logger.error(
99
+ "Get customer failed",
100
+ extra={
101
+ "operation": "get_customer",
102
+ "customer_id": customer_id,
103
+ "error": str(e),
104
+ "error_type": type(e).__name__
105
+ },
106
+ exc_info=True
107
+ )
108
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to get customer")
109
 
110
 
111
  @router.post("/list", response_model=None, summary="List customers")
 
132
  - skip: Records to skip (default: 0)
133
  - limit: Max records to return (default: 50, max: 500)
134
  """
135
+ try:
136
+ # Extract filters and parameters from payload
137
+ filters = payload.filters or {}
138
+ skip = payload.skip or 0
139
+ limit = payload.limit or 50
140
+ projection_list = payload.projection_list
141
+
142
+ # Always use merchant_id from token
143
+ if not current_user.merchant_id:
144
+ raise HTTPException(status_code=400, detail="merchant_id must be available in token")
145
+
146
+ customers, total = await CustomerService.list_customers(
147
+ merchant_id=current_user.merchant_id,
148
+ merchant_type=current_user.merchant_type,
149
+ filters=filters,
 
 
 
 
 
 
 
 
 
150
  skip=skip,
151
+ limit=limit,
152
+ projection_list=projection_list
153
  )
154
+
155
+ logger.info(
156
+ "Customers listed",
157
+ extra={
158
+ "operation": "list_customers",
159
+ "count": total,
160
+ "merchant_id": str(current_user.merchant_id) if current_user.merchant_id else None,
161
+ "user_id": str(current_user.id) if current_user.id else None
162
+ }
163
+ )
164
+
165
+ # Return raw dict if projection used, model otherwise
166
+ if projection_list:
167
+ return CustomerListResponse(
168
+ customers=customers, # Raw dicts when projection is used
169
+ total=total,
170
+ skip=skip,
171
+ limit=limit
172
+ )
173
+ else:
174
+ return CustomerListResponse(
175
+ customers=customers, # CustomerResponse objects when no projection
176
+ total=total,
177
+ skip=skip,
178
+ limit=limit
179
+ )
180
+ except HTTPException:
181
+ raise
182
+ except Exception as e:
183
+ logger.error(
184
+ "List customers failed",
185
+ extra={
186
+ "operation": "list_customers",
187
+ "error": str(e),
188
+ "error_type": type(e).__name__,
189
+ "merchant_id": str(current_user.merchant_id) if current_user.merchant_id else None
190
+ },
191
+ exc_info=True
192
  )
193
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to list customers")
194
 
195
 
196
  @router.put("/{customer_id}", response_model=CustomerResponse, summary="Update customer")
 
199
  payload: CustomerUpdate,
200
  current_user: TokenUser = Depends(require_pos_permission("customers", "update"))
201
  ) -> CustomerResponse:
202
+ try:
203
+ customer = await CustomerService.update_customer(
204
+ customer_id=customer_id,
205
+ payload=payload,
206
+ merchant_id=current_user.merchant_id,
207
+ merchant_type=current_user.merchant_type
208
+ )
209
+
210
+ logger.info(
211
+ "Customer updated",
212
+ extra={
213
+ "operation": "update_customer",
214
+ "customer_id": customer_id,
215
+ "user_id": str(current_user.id) if current_user.id else None
216
+ }
217
+ )
218
+ return customer
219
+ except HTTPException:
220
+ raise
221
+ except ValueError as e:
222
+ logger.warning(
223
+ "Update customer validation failed",
224
+ extra={
225
+ "operation": "update_customer",
226
+ "customer_id": customer_id,
227
+ "error": str(e)
228
+ }
229
+ )
230
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
231
+ except Exception as e:
232
+ logger.error(
233
+ "Update customer failed",
234
+ extra={
235
+ "operation": "update_customer",
236
+ "customer_id": customer_id,
237
+ "error": str(e),
238
+ "error_type": type(e).__name__
239
+ },
240
+ exc_info=True
241
+ )
242
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to update customer")
243
 
244
 
245
  @router.delete("/{customer_id}", status_code=status.HTTP_204_NO_CONTENT, summary="Delete customer (soft)")
 
247
  customer_id: str,
248
  current_user: TokenUser = Depends(require_pos_permission("customers", "delete"))
249
  ):
250
+ try:
251
+ await CustomerService.delete_customer(
252
+ customer_id=customer_id,
253
+ merchant_id=current_user.merchant_id,
254
+ merchant_type=current_user.merchant_type
255
+ )
256
+
257
+ logger.info(
258
+ "Customer deleted",
259
+ extra={
260
+ "operation": "delete_customer",
261
+ "customer_id": customer_id,
262
+ "user_id": str(current_user.id) if current_user.id else None
263
+ }
264
+ )
265
+ return None
266
+ except HTTPException:
267
+ raise
268
+ except Exception as e:
269
+ logger.error(
270
+ "Delete customer failed",
271
+ extra={
272
+ "operation": "delete_customer",
273
+ "customer_id": customer_id,
274
+ "error": str(e),
275
+ "error_type": type(e).__name__
276
+ },
277
+ exc_info=True
278
+ )
279
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to delete customer")
app/customers/services/service.py CHANGED
@@ -8,6 +8,7 @@ from typing import Optional, List, Tuple
8
  from fastapi import HTTPException, status
9
  from sqlalchemy import text
10
 
 
11
  from app.nosql import get_database
12
  from app.constants.collections import POS_CUSTOMERS_COLLECTION
13
  from app.sql import get_postgres_session
@@ -19,7 +20,7 @@ from app.customers.schemas.schema import (
19
  CustomerListResponse,
20
  )
21
 
22
- logger = logging.getLogger(__name__)
23
 
24
 
25
  class CustomerService:
@@ -38,7 +39,13 @@ class CustomerService:
38
  """Upsert customer into trans.pos_customer_ref using token merchant_id for UUID compatibility."""
39
  async with get_postgres_session() as session:
40
  if session is None:
41
- logger.warning("PostgreSQL session unavailable; skipping customer sync")
 
 
 
 
 
 
42
  return
43
 
44
  # Use token merchant_id if provided (UUID format), otherwise use customer merchant_id
@@ -73,7 +80,15 @@ class CustomerService:
73
  "updated_at": customer.updated_at,
74
  })
75
  await session.commit()
76
- logger.info("Synced customer %s to Postgres with merchant_id %s", customer.customer_id, sync_merchant_id)
 
 
 
 
 
 
 
 
77
 
78
  @classmethod
79
  async def create_customer(cls, payload: CustomerCreate, token_merchant_id: Optional[str] = None) -> CustomerResponse:
@@ -95,7 +110,16 @@ class CustomerService:
95
  try:
96
  await cls._sync_to_postgres(customer, token_merchant_id)
97
  except Exception as e:
98
- logger.error("Postgres sync failed for customer %s: %s", customer_id, e)
 
 
 
 
 
 
 
 
 
99
 
100
  return customer
101
 
@@ -203,7 +227,16 @@ class CustomerService:
203
  try:
204
  await cls._sync_to_postgres(customer, merchant_id)
205
  except Exception as e:
206
- logger.error("Postgres sync failed for customer %s: %s", customer_id, e)
 
 
 
 
 
 
 
 
 
207
 
208
  return customer
209
 
@@ -237,6 +270,15 @@ class CustomerService:
237
  try:
238
  await cls._sync_to_postgres(customer, merchant_id)
239
  except Exception as e:
240
- logger.error("Postgres sync failed for customer %s: %s", customer_id, e)
 
 
 
 
 
 
 
 
 
241
 
242
  return None
 
8
  from fastapi import HTTPException, status
9
  from sqlalchemy import text
10
 
11
+ from app.core.logging import get_logger
12
  from app.nosql import get_database
13
  from app.constants.collections import POS_CUSTOMERS_COLLECTION
14
  from app.sql import get_postgres_session
 
20
  CustomerListResponse,
21
  )
22
 
23
+ logger = get_logger(__name__)
24
 
25
 
26
  class CustomerService:
 
39
  """Upsert customer into trans.pos_customer_ref using token merchant_id for UUID compatibility."""
40
  async with get_postgres_session() as session:
41
  if session is None:
42
+ logger.warning(
43
+ "PostgreSQL session unavailable; skipping customer sync",
44
+ extra={
45
+ "operation": "sync_to_postgres",
46
+ "customer_id": customer.customer_id
47
+ }
48
+ )
49
  return
50
 
51
  # Use token merchant_id if provided (UUID format), otherwise use customer merchant_id
 
80
  "updated_at": customer.updated_at,
81
  })
82
  await session.commit()
83
+
84
+ logger.info(
85
+ f"Synced customer {customer.customer_id} to Postgres",
86
+ extra={
87
+ "operation": "sync_to_postgres",
88
+ "customer_id": customer.customer_id,
89
+ "merchant_id": sync_merchant_id
90
+ }
91
+ )
92
 
93
  @classmethod
94
  async def create_customer(cls, payload: CustomerCreate, token_merchant_id: Optional[str] = None) -> CustomerResponse:
 
110
  try:
111
  await cls._sync_to_postgres(customer, token_merchant_id)
112
  except Exception as e:
113
+ logger.error(
114
+ f"Postgres sync failed for customer {customer_id}",
115
+ extra={
116
+ "operation": "create_customer_sync",
117
+ "customer_id": customer_id,
118
+ "error": str(e),
119
+ "merchant_id": token_merchant_id
120
+ },
121
+ exc_info=True
122
+ )
123
 
124
  return customer
125
 
 
227
  try:
228
  await cls._sync_to_postgres(customer, merchant_id)
229
  except Exception as e:
230
+ logger.error(
231
+ f"Postgres sync failed for customer {customer_id}",
232
+ extra={
233
+ "operation": "update_customer_sync",
234
+ "customer_id": customer_id,
235
+ "error": str(e),
236
+ "merchant_id": merchant_id
237
+ },
238
+ exc_info=True
239
+ )
240
 
241
  return customer
242
 
 
270
  try:
271
  await cls._sync_to_postgres(customer, merchant_id)
272
  except Exception as e:
273
+ logger.error(
274
+ f"Postgres sync failed for customer {customer_id}",
275
+ extra={
276
+ "operation": "delete_customer_sync",
277
+ "customer_id": customer_id,
278
+ "error": str(e),
279
+ "merchant_id": merchant_id
280
+ },
281
+ exc_info=True
282
+ )
283
 
284
  return None
app/main.py CHANGED
@@ -7,10 +7,11 @@ from fastapi.exceptions import RequestValidationError
7
  from starlette.exceptions import HTTPException as StarletteHTTPException
8
  from fastapi.responses import JSONResponse
9
  from fastapi.middleware.cors import CORSMiddleware
10
- import logging # TODO: Uncomment when package is available
11
- from pymongo.errors import PyMongoError, ConnectionFailure
12
 
13
  from app.core.config import settings
 
14
  from app.utils.response import error_response
15
  from app.middleware.logging_middleware import RequestLoggingMiddleware
16
 
@@ -22,8 +23,7 @@ from app.customers.controllers.router import router as customers_router
22
  from app.sales.retail.controllers.router import router as sales_router
23
  from app.appointments.controllers.router import router as appointments_router
24
 
25
- # # logger = get_logger(__name__) # TODO: Uncomment when insightfy_utils is available
26
- logger = logging.getLogger(__name__)
27
  logging.basicConfig(level=logging.INFO)
28
 
29
  # Create FastAPI app
@@ -52,16 +52,25 @@ app.add_middleware(
52
  @app.exception_handler(RequestValidationError)
53
  async def validation_exception_handler(request: Request, exc: RequestValidationError):
54
  """Handle validation errors"""
55
- errors = []
56
- for e in exc.errors():
57
- # Get field name from loc (last element), default to "body" or "unknown"
58
- field = str(e["loc"][-1]) if e["loc"] else "unknown"
59
- errors.append({
60
- "field": field,
61
- "message": e["msg"],
62
- "type": e["type"]
63
- })
64
-
 
 
 
 
 
 
 
 
 
65
  return JSONResponse(
66
  status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
67
  content=error_response(
@@ -73,6 +82,28 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE
73
  )
74
 
75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  @app.exception_handler(status.HTTP_404_NOT_FOUND)
77
  async def not_found_exception_handler(request: Request, exc: Exception):
78
  """Handle 404 errors"""
@@ -113,43 +144,56 @@ async def http_exception_handler(request: Request, exc: StarletteHTTPException):
113
  )
114
 
115
 
116
- @app.exception_handler(ConnectionFailure)
117
- async def mongo_connection_exception_handler(request: Request, exc: ConnectionFailure):
118
- """Handle MongoDB connection errors"""
119
- logger.error(f"Database connection error: {exc}", exc_info=True)
120
- return JSONResponse(
121
- status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
122
- content=error_response(
123
- error="Database Connection Error",
124
- detail="Unable to connect to the database. Please try again later.",
125
- request_id=getattr(request.state, "request_id", None)
126
- )
127
- )
128
-
129
-
130
  @app.exception_handler(PyMongoError)
131
- async def mongo_exception_handler(request: Request, exc: PyMongoError):
132
- """Handle general MongoDB errors"""
133
- logger.error(f"Database error: {exc}", exc_info=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  return JSONResponse(
135
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
136
  content=error_response(
137
  error="Database Error",
138
- detail="An unexpected database error occurred.",
139
  request_id=getattr(request.state, "request_id", None)
140
  )
141
  )
142
 
143
 
144
- # Handle generic HTTPException (catch-all for other HTTP exceptions)
145
  @app.exception_handler(Exception)
146
- async def generic_exception_handler(request: Request, exc: Exception):
147
  """
148
  Handle all unhandled exceptions.
149
-
150
- In production, we should not expose internal error details.
151
  """
152
- logger.error(f"Unhandled exception: {exc}", exc_info=True)
 
 
 
 
 
 
 
 
 
 
 
153
  return JSONResponse(
154
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
155
  content=error_response(
 
7
  from starlette.exceptions import HTTPException as StarletteHTTPException
8
  from fastapi.responses import JSONResponse
9
  from fastapi.middleware.cors import CORSMiddleware
10
+ from pymongo.errors import PyMongoError, ConnectionFailure, OperationFailure
11
+ from jose import JWTError
12
 
13
  from app.core.config import settings
14
+ from app.core.logging import get_logger
15
  from app.utils.response import error_response
16
  from app.middleware.logging_middleware import RequestLoggingMiddleware
17
 
 
23
  from app.sales.retail.controllers.router import router as sales_router
24
  from app.appointments.controllers.router import router as appointments_router
25
 
26
+ logger = get_logger(__name__)
 
27
  logging.basicConfig(level=logging.INFO)
28
 
29
  # Create FastAPI app
 
52
  @app.exception_handler(RequestValidationError)
53
  async def validation_exception_handler(request: Request, exc: RequestValidationError):
54
  """Handle validation errors"""
55
+ errors = [
56
+ {
57
+ "field": " -> ".join(str(loc) for loc in error["loc"]),
58
+ "message": error["msg"],
59
+ "type": error["type"]
60
+ }
61
+ for error in exc.errors()
62
+ ]
63
+
64
+ logger.warning(
65
+ "Validation error",
66
+ extra={
67
+ "path": request.url.path,
68
+ "method": request.method,
69
+ "error_count": len(errors),
70
+ "errors": errors
71
+ }
72
+ )
73
+
74
  return JSONResponse(
75
  status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
76
  content=error_response(
 
82
  )
83
 
84
 
85
+ @app.exception_handler(JWTError)
86
+ async def jwt_exception_handler(request: Request, exc: JWTError):
87
+ """Handle JWT errors"""
88
+ logger.warning(
89
+ "JWT authentication failed",
90
+ extra={
91
+ "path": request.url.path,
92
+ "error": str(exc),
93
+ "client_ip": request.client.host if request.client else None
94
+ }
95
+ )
96
+
97
+ return JSONResponse(
98
+ status_code=status.HTTP_401_UNAUTHORIZED,
99
+ content=error_response(
100
+ error="Unauthorized",
101
+ detail="Invalid or expired token",
102
+ request_id=getattr(request.state, "request_id", None)
103
+ )
104
+ )
105
+
106
+
107
  @app.exception_handler(status.HTTP_404_NOT_FOUND)
108
  async def not_found_exception_handler(request: Request, exc: Exception):
109
  """Handle 404 errors"""
 
144
  )
145
 
146
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  @app.exception_handler(PyMongoError)
148
+ async def mongodb_exception_handler(request: Request, exc: PyMongoError):
149
+ """Handle MongoDB errors"""
150
+ logger.error(
151
+ "Database error",
152
+ extra={
153
+ "path": request.url.path,
154
+ "error": str(exc),
155
+ "error_type": type(exc).__name__
156
+ },
157
+ exc_info=True
158
+ )
159
+
160
+ if isinstance(exc, ConnectionFailure):
161
+ status_code = status.HTTP_503_SERVICE_UNAVAILABLE
162
+ detail = "Database connection failed"
163
+ elif isinstance(exc, OperationFailure):
164
+ status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
165
+ detail = "Database operation failed"
166
+ else:
167
+ status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
168
+ detail = "Database error occurred"
169
+
170
  return JSONResponse(
171
+ status_code=status_code,
172
  content=error_response(
173
  error="Database Error",
174
+ detail=detail,
175
  request_id=getattr(request.state, "request_id", None)
176
  )
177
  )
178
 
179
 
 
180
  @app.exception_handler(Exception)
181
+ async def general_exception_handler(request: Request, exc: Exception):
182
  """
183
  Handle all unhandled exceptions.
 
 
184
  """
185
+ logger.error(
186
+ "Unhandled exception",
187
+ extra={
188
+ "method": request.method,
189
+ "path": request.url.path,
190
+ "error": str(exc),
191
+ "error_type": type(exc).__name__,
192
+ "client_ip": request.client.host if request.client else None
193
+ },
194
+ exc_info=True
195
+ )
196
+
197
  return JSONResponse(
198
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
199
  content=error_response(
app/sales/orders/controllers/router.py CHANGED
@@ -3,8 +3,7 @@ Sales Order API router - FastAPI endpoints for sales order operations.
3
  """
4
  from typing import Optional
5
  from fastapi import APIRouter, HTTPException, Query, Header, status, Depends
6
- # from insightfy_utils.logging import get_logger # TODO: Uncomment when package is available
7
- import logging
8
 
9
  from app.dependencies.auth import TokenUser
10
  from app.dependencies.pos_permissions import require_pos_permission
@@ -20,8 +19,7 @@ from app.sales.orders.schemas.schema import (
20
  from app.sales.orders.services.service import SalesOrderService
21
  from app.utils.response import success_response, paginated_response
22
 
23
- # logger = get_logger(__name__) # TODO: Uncomment when insightfy_utils is available
24
- logger = logging.getLogger(__name__)
25
 
26
  router = APIRouter(
27
  prefix="/sales",
@@ -64,14 +62,32 @@ async def list_sales_orders(
64
  - field: Field to sort by (default: order_date)
65
  - order: Sort order (asc/desc, default: desc)
66
  """
67
- result = await SalesOrderService.list_sales_orders(request)
68
- return paginated_response(
69
- documents=[doc.dict() if hasattr(doc, 'dict') else doc for doc in result["documents"]],
70
- total=result["total"],
71
- page=result["page"],
72
- limit=result["limit"],
73
- correlation_id=x_correlation_id
74
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
 
77
  @router.get(
@@ -96,7 +112,25 @@ async def get_sales_order(
96
  - Shipment information
97
  - Invoice details (if generated)
98
  """
99
- return await SalesOrderService.get_sales_order(sales_order_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
 
101
 
102
  @router.post(
@@ -141,16 +175,45 @@ async def create_sales_order(
141
  - credit/cod → unpaid (amount_paid = 0)
142
  - partial → partial (custom amount_paid)
143
  """
144
- sales_order = await SalesOrderService.create_sales_order(payload, x_merchant_id)
145
- return success_response(
146
- data={
147
- "sales_order_id": sales_order.sales_order_id,
148
- "order_number": sales_order.order_number,
149
- "summary": sales_order.summary.dict()
150
- },
151
- message="Sales order created successfully",
152
- correlation_id=x_correlation_id
153
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
 
155
 
156
  @router.put(
@@ -181,20 +244,50 @@ async def update_sales_order(
181
  - internal_notes: Internal notes
182
  - status: Order status
183
  """
184
- sales_order = await SalesOrderService.update_sales_order(
185
- sales_order_id,
186
- payload,
187
- x_user_id
188
- )
189
- return success_response(
190
- data={
191
- "sales_order_id": sales_order.sales_order_id,
192
- "order_number": sales_order.order_number,
193
- "summary": sales_order.summary.dict()
194
- },
195
- message="Sales order updated successfully",
196
- correlation_id=x_correlation_id
197
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
 
199
 
200
  @router.post(
@@ -223,12 +316,41 @@ async def generate_invoice(
223
  - Invoice not already generated
224
  - Order must be in confirmed or processing status (not enforced)
225
  """
226
- invoice = await SalesOrderService.generate_invoice(sales_order_id, request)
227
- return success_response(
228
- data=invoice.dict(),
229
- message="Invoice generated successfully",
230
- correlation_id=x_correlation_id
231
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
 
233
 
234
  @router.get(
@@ -252,4 +374,21 @@ async def get_sales_widgets(
252
 
253
  All metrics are scoped to the merchant.
254
  """
255
- return await SalesOrderService.get_sales_widgets(x_merchant_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  """
4
  from typing import Optional
5
  from fastapi import APIRouter, HTTPException, Query, Header, status, Depends
6
+ from app.core.logging import get_logger
 
7
 
8
  from app.dependencies.auth import TokenUser
9
  from app.dependencies.pos_permissions import require_pos_permission
 
19
  from app.sales.orders.services.service import SalesOrderService
20
  from app.utils.response import success_response, paginated_response
21
 
22
+ logger = get_logger(__name__)
 
23
 
24
  router = APIRouter(
25
  prefix="/sales",
 
62
  - field: Field to sort by (default: order_date)
63
  - order: Sort order (asc/desc, default: desc)
64
  """
65
+ try:
66
+ result = await SalesOrderService.list_sales_orders(request)
67
+ return paginated_response(
68
+ documents=[doc.dict() if hasattr(doc, 'dict') else doc for doc in result["documents"]],
69
+ total=result["total"],
70
+ page=result["page"],
71
+ limit=result["limit"],
72
+ correlation_id=x_correlation_id
73
+ )
74
+ except HTTPException:
75
+ raise
76
+ except Exception as e:
77
+ logger.error(
78
+ "Error listing sales orders",
79
+ extra={
80
+ "operation": "list_sales_orders",
81
+ "merchant_id": x_merchant_id,
82
+ "error": str(e),
83
+ "filters": str(request.filters) if request.filters else None
84
+ },
85
+ exc_info=True
86
+ )
87
+ raise HTTPException(
88
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
89
+ detail="Error listing sales orders"
90
+ )
91
 
92
 
93
  @router.get(
 
112
  - Shipment information
113
  - Invoice details (if generated)
114
  """
115
+ try:
116
+ return await SalesOrderService.get_sales_order(sales_order_id)
117
+ except HTTPException:
118
+ raise
119
+ except Exception as e:
120
+ logger.error(
121
+ f"Error retrieving sales order {sales_order_id}",
122
+ extra={
123
+ "operation": "get_sales_order",
124
+ "sales_order_id": sales_order_id,
125
+ "merchant_id": x_merchant_id,
126
+ "error": str(e)
127
+ },
128
+ exc_info=True
129
+ )
130
+ raise HTTPException(
131
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
132
+ detail="Error retrieving sales order"
133
+ )
134
 
135
 
136
  @router.post(
 
175
  - credit/cod → unpaid (amount_paid = 0)
176
  - partial → partial (custom amount_paid)
177
  """
178
+ try:
179
+ sales_order = await SalesOrderService.create_sales_order(payload, x_merchant_id)
180
+
181
+ logger.info(
182
+ "Sales order created",
183
+ extra={
184
+ "operation": "create_sales_order",
185
+ "sales_order_id": sales_order.sales_order_id,
186
+ "merchant_id": x_merchant_id,
187
+ "user_id": payload.created_by
188
+ }
189
+ )
190
+
191
+ return success_response(
192
+ data={
193
+ "sales_order_id": sales_order.sales_order_id,
194
+ "order_number": sales_order.order_number,
195
+ "summary": sales_order.summary.dict()
196
+ },
197
+ message="Sales order created successfully",
198
+ correlation_id=x_correlation_id
199
+ )
200
+ except HTTPException:
201
+ raise
202
+ except Exception as e:
203
+ logger.error(
204
+ "Error creating sales order",
205
+ extra={
206
+ "operation": "create_sales_order",
207
+ "merchant_id": x_merchant_id,
208
+ "user_id": payload.created_by,
209
+ "error": str(e)
210
+ },
211
+ exc_info=True
212
+ )
213
+ raise HTTPException(
214
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
215
+ detail="Error creating sales order"
216
+ )
217
 
218
 
219
  @router.put(
 
244
  - internal_notes: Internal notes
245
  - status: Order status
246
  """
247
+ try:
248
+ sales_order = await SalesOrderService.update_sales_order(
249
+ sales_order_id,
250
+ payload,
251
+ x_user_id
252
+ )
253
+
254
+ logger.info(
255
+ f"Sales order {sales_order_id} updated",
256
+ extra={
257
+ "operation": "update_sales_order",
258
+ "sales_order_id": sales_order_id,
259
+ "merchant_id": x_merchant_id,
260
+ "user_id": x_user_id
261
+ }
262
+ )
263
+
264
+ return success_response(
265
+ data={
266
+ "sales_order_id": sales_order.sales_order_id,
267
+ "order_number": sales_order.order_number,
268
+ "summary": sales_order.summary.dict()
269
+ },
270
+ message="Sales order updated successfully",
271
+ correlation_id=x_correlation_id
272
+ )
273
+ except HTTPException:
274
+ raise
275
+ except Exception as e:
276
+ logger.error(
277
+ f"Error updating sales order {sales_order_id}",
278
+ extra={
279
+ "operation": "update_sales_order",
280
+ "sales_order_id": sales_order_id,
281
+ "merchant_id": x_merchant_id,
282
+ "user_id": x_user_id,
283
+ "error": str(e)
284
+ },
285
+ exc_info=True
286
+ )
287
+ raise HTTPException(
288
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
289
+ detail="Error updating sales order"
290
+ )
291
 
292
 
293
  @router.post(
 
316
  - Invoice not already generated
317
  - Order must be in confirmed or processing status (not enforced)
318
  """
319
+ try:
320
+ invoice = await SalesOrderService.generate_invoice(sales_order_id, request)
321
+
322
+ logger.info(
323
+ f"Invoice generated for sales order {sales_order_id}",
324
+ extra={
325
+ "operation": "generate_invoice",
326
+ "sales_order_id": sales_order_id,
327
+ "invoice_id": invoice.invoice_id,
328
+ "merchant_id": x_merchant_id
329
+ }
330
+ )
331
+
332
+ return success_response(
333
+ data=invoice.dict(),
334
+ message="Invoice generated successfully",
335
+ correlation_id=x_correlation_id
336
+ )
337
+ except HTTPException:
338
+ raise
339
+ except Exception as e:
340
+ logger.error(
341
+ f"Error generating invoice for sales order {sales_order_id}",
342
+ extra={
343
+ "operation": "generate_invoice",
344
+ "sales_order_id": sales_order_id,
345
+ "merchant_id": x_merchant_id,
346
+ "error": str(e)
347
+ },
348
+ exc_info=True
349
+ )
350
+ raise HTTPException(
351
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
352
+ detail="Error generating invoice"
353
+ )
354
 
355
 
356
  @router.get(
 
374
 
375
  All metrics are scoped to the merchant.
376
  """
377
+ try:
378
+ return await SalesOrderService.get_sales_widgets(x_merchant_id)
379
+ except HTTPException:
380
+ raise
381
+ except Exception as e:
382
+ logger.error(
383
+ "Error retrieving sales widgets",
384
+ extra={
385
+ "operation": "get_sales_widgets",
386
+ "merchant_id": x_merchant_id,
387
+ "error": str(e)
388
+ },
389
+ exc_info=True
390
+ )
391
+ raise HTTPException(
392
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
393
+ detail="Error retrieving sales widgets"
394
+ )
app/sales/orders/services/service.py CHANGED
@@ -5,8 +5,7 @@ from datetime import datetime
5
  from typing import Optional, List, Dict, Any
6
  from decimal import Decimal
7
  from fastapi import HTTPException, status
8
- # from insightfy_utils.logging import get_logger # TODO: Uncomment when package is available
9
- import logging
10
  import secrets
11
 
12
  from app.nosql import get_database
@@ -30,8 +29,7 @@ from app.sales.orders.schemas.schema import (
30
  FulfillmentStatus
31
  )
32
 
33
- # logger = get_logger(__name__) # TODO: Uncomment when insightfy_utils is available
34
- logger = logging.getLogger(__name__)
35
 
36
 
37
  def generate_sales_order_id() -> str:
@@ -245,6 +243,7 @@ class SalesOrderService:
245
  logger.info(
246
  f"Created sales order {sales_order_id}",
247
  extra={
 
248
  "sales_order_id": sales_order_id,
249
  "order_number": order_number,
250
  "merchant_id": merchant_id,
@@ -255,7 +254,15 @@ class SalesOrderService:
255
  return SalesOrderResponse(**sales_order)
256
 
257
  except Exception as e:
258
- logger.error(f"Error creating sales order", exc_info=e)
 
 
 
 
 
 
 
 
259
  raise HTTPException(
260
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
261
  detail=f"Error creating sales order: {str(e)}"
@@ -264,17 +271,34 @@ class SalesOrderService:
264
  @staticmethod
265
  async def get_sales_order(sales_order_id: str) -> SalesOrderResponse:
266
  """Get sales order by ID."""
267
- sales_order = await get_database()[SCM_SALES_ORDERS_COLLECTION].find_one(
268
- {"sales_order_id": sales_order_id}
269
- )
270
-
271
- if not sales_order:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
  raise HTTPException(
273
- status_code=status.HTTP_404_NOT_FOUND,
274
- detail=f"Sales order {sales_order_id} not found"
275
  )
276
-
277
- return SalesOrderResponse(**sales_order)
278
 
279
  @staticmethod
280
  async def list_sales_orders(request: SalesOrderListRequest) -> Dict[str, Any]:
@@ -344,7 +368,15 @@ class SalesOrderService:
344
  }
345
 
346
  except Exception as e:
347
- logger.error("Error listing sales orders", exc_info=e)
 
 
 
 
 
 
 
 
348
  raise HTTPException(
349
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
350
  detail="Error listing sales orders"
@@ -405,6 +437,7 @@ class SalesOrderService:
405
  logger.info(
406
  f"Updated sales order {sales_order_id}",
407
  extra={
 
408
  "sales_order_id": sales_order_id,
409
  "updated_by": updated_by
410
  }
@@ -415,7 +448,16 @@ class SalesOrderService:
415
  except HTTPException:
416
  raise
417
  except Exception as e:
418
- logger.error(f"Error updating sales order {sales_order_id}", exc_info=e)
 
 
 
 
 
 
 
 
 
419
  raise HTTPException(
420
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
421
  detail=f"Error updating sales order: {str(e)}"
@@ -464,6 +506,7 @@ class SalesOrderService:
464
  logger.info(
465
  f"Generated invoice for sales order {sales_order_id}",
466
  extra={
 
467
  "sales_order_id": sales_order_id,
468
  "invoice_id": invoice_id,
469
  "invoice_number": invoice_number
@@ -478,7 +521,15 @@ class SalesOrderService:
478
  )
479
 
480
  except Exception as e:
481
- logger.error(f"Error generating invoice", exc_info=e)
 
 
 
 
 
 
 
 
482
  raise HTTPException(
483
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
484
  detail="Error generating invoice"
@@ -529,7 +580,15 @@ class SalesOrderService:
529
  )
530
 
531
  except Exception as e:
532
- logger.error("Error getting sales widgets", exc_info=e)
 
 
 
 
 
 
 
 
533
  raise HTTPException(
534
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
535
  detail="Error fetching sales widgets"
 
5
  from typing import Optional, List, Dict, Any
6
  from decimal import Decimal
7
  from fastapi import HTTPException, status
8
+ from app.core.logging import get_logger
 
9
  import secrets
10
 
11
  from app.nosql import get_database
 
29
  FulfillmentStatus
30
  )
31
 
32
+ logger = get_logger(__name__)
 
33
 
34
 
35
  def generate_sales_order_id() -> str:
 
243
  logger.info(
244
  f"Created sales order {sales_order_id}",
245
  extra={
246
+ "operation": "create_sales_order",
247
  "sales_order_id": sales_order_id,
248
  "order_number": order_number,
249
  "merchant_id": merchant_id,
 
254
  return SalesOrderResponse(**sales_order)
255
 
256
  except Exception as e:
257
+ logger.error(
258
+ "Error creating sales order",
259
+ extra={
260
+ "operation": "create_sales_order",
261
+ "error": str(e),
262
+ "merchant_id": merchant_id
263
+ },
264
+ exc_info=True
265
+ )
266
  raise HTTPException(
267
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
268
  detail=f"Error creating sales order: {str(e)}"
 
271
  @staticmethod
272
  async def get_sales_order(sales_order_id: str) -> SalesOrderResponse:
273
  """Get sales order by ID."""
274
+ try:
275
+ sales_order = await get_database()[SCM_SALES_ORDERS_COLLECTION].find_one(
276
+ {"sales_order_id": sales_order_id}
277
+ )
278
+
279
+ if not sales_order:
280
+ raise HTTPException(
281
+ status_code=status.HTTP_404_NOT_FOUND,
282
+ detail=f"Sales order {sales_order_id} not found"
283
+ )
284
+
285
+ return SalesOrderResponse(**sales_order)
286
+ except HTTPException:
287
+ raise
288
+ except Exception as e:
289
+ logger.error(
290
+ f"Error getting sales order {sales_order_id}",
291
+ extra={
292
+ "operation": "get_sales_order",
293
+ "sales_order_id": sales_order_id,
294
+ "error": str(e)
295
+ },
296
+ exc_info=True
297
+ )
298
  raise HTTPException(
299
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
300
+ detail=f"Error getting sales order: {str(e)}"
301
  )
 
 
302
 
303
  @staticmethod
304
  async def list_sales_orders(request: SalesOrderListRequest) -> Dict[str, Any]:
 
368
  }
369
 
370
  except Exception as e:
371
+ logger.error(
372
+ "Error listing sales orders",
373
+ extra={
374
+ "operation": "list_sales_orders",
375
+ "error": str(e),
376
+ "filters": str(request.filters) if request.filters else None
377
+ },
378
+ exc_info=True
379
+ )
380
  raise HTTPException(
381
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
382
  detail="Error listing sales orders"
 
437
  logger.info(
438
  f"Updated sales order {sales_order_id}",
439
  extra={
440
+ "operation": "update_sales_order",
441
  "sales_order_id": sales_order_id,
442
  "updated_by": updated_by
443
  }
 
448
  except HTTPException:
449
  raise
450
  except Exception as e:
451
+ logger.error(
452
+ f"Error updating sales order {sales_order_id}",
453
+ extra={
454
+ "operation": "update_sales_order",
455
+ "sales_order_id": sales_order_id,
456
+ "error": str(e),
457
+ "updated_by": updated_by
458
+ },
459
+ exc_info=True
460
+ )
461
  raise HTTPException(
462
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
463
  detail=f"Error updating sales order: {str(e)}"
 
506
  logger.info(
507
  f"Generated invoice for sales order {sales_order_id}",
508
  extra={
509
+ "operation": "generate_invoice",
510
  "sales_order_id": sales_order_id,
511
  "invoice_id": invoice_id,
512
  "invoice_number": invoice_number
 
521
  )
522
 
523
  except Exception as e:
524
+ logger.error(
525
+ "Error generating invoice",
526
+ extra={
527
+ "operation": "generate_invoice",
528
+ "sales_order_id": sales_order_id,
529
+ "error": str(e)
530
+ },
531
+ exc_info=True
532
+ )
533
  raise HTTPException(
534
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
535
  detail="Error generating invoice"
 
580
  )
581
 
582
  except Exception as e:
583
+ logger.error(
584
+ "Error getting sales widgets",
585
+ extra={
586
+ "operation": "get_sales_widgets",
587
+ "error": str(e),
588
+ "merchant_id": merchant_id
589
+ },
590
+ exc_info=True
591
+ )
592
  raise HTTPException(
593
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
594
  detail="Error fetching sales widgets"
app/sales/retail/controllers/router.py CHANGED
@@ -1,7 +1,7 @@
1
  """
2
  Retail Sales API router.
3
  """
4
- import logging
5
  from uuid import UUID
6
  from fastapi import APIRouter, HTTPException, status, Depends
7
 
@@ -27,7 +27,7 @@ from app.sales.retail.services.service import (
27
  list_sales,
28
  )
29
 
30
- logger = logging.getLogger(__name__)
31
 
32
  router = APIRouter(
33
  prefix="/pos/sales",
@@ -53,20 +53,75 @@ async def create_sale_endpoint(
53
  payment_tenders=[t.model_dump() for t in req.payment_tenders] if req.payment_tenders else None,
54
  )
55
  sale = await get_sale(sid)
 
 
 
 
 
 
 
 
 
 
 
56
  return await _to_sale_response(sale)
 
 
 
 
 
 
 
 
 
 
 
 
57
  except Exception as e:
58
- logger.error(f"Create sale failed: {e}")
59
- raise HTTPException(status_code=400, detail=str(e))
 
 
 
 
 
 
 
 
 
60
 
61
  @router.get("/{sale_id}", response_model=SaleResponse)
62
  async def get_sale_endpoint(
63
  sale_id: UUID,
64
  current_user: TokenUser = Depends(require_pos_permission("sales", "view"))
65
  ):
66
- sale = await get_sale(sale_id)
67
- if not sale:
68
- raise HTTPException(status_code=404, detail="Sale not found")
69
- return await _to_sale_response(sale)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
  @router.post("/list", response_model=SaleListResponse, summary="List sales")
72
  async def list_sales_endpoint(
@@ -93,30 +148,54 @@ async def list_sales_endpoint(
93
  - skip: Records to skip (default: 0)
94
  - limit: Max records to return (default: 50, max: 500)
95
  """
96
- # Extract filters and parameters from payload
97
- filters = payload.filters or {}
98
- skip = payload.skip or 0
99
- limit = payload.limit or 50
100
- projection_list = payload.projection_list
101
-
102
- # Always use merchant_id from token for security
103
- if not current_user.merchant_id:
104
- raise HTTPException(status_code=400, detail="merchant_id must be available in token")
105
-
106
- sales, total = await list_sales(
107
- merchant_id=current_user.merchant_id,
108
- filters=filters,
109
- skip=skip,
110
- limit=limit,
111
- projection_list=projection_list
112
- )
113
-
114
- return SaleListResponse(
115
- sales=sales, # Raw dicts when projection is used, full objects otherwise
116
- total=total,
117
- skip=skip,
118
- limit=limit
119
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
 
121
  @router.put("/{sale_id}/items", response_model=SaleResponse)
122
  async def update_items_endpoint(
@@ -126,8 +205,40 @@ async def update_items_endpoint(
126
  ):
127
  try:
128
  sale = await replace_items(sale_id, [i.model_dump() for i in req.items])
 
 
 
 
 
 
 
 
 
 
 
129
  return await _to_sale_response(sale)
 
 
 
 
 
 
 
 
 
 
 
130
  except Exception as e:
 
 
 
 
 
 
 
 
 
 
131
  raise HTTPException(status_code=400, detail=str(e))
132
 
133
  @router.post("/{sale_id}/payments", response_model=SaleResponse)
@@ -146,8 +257,41 @@ async def capture_payment_endpoint(
146
  req.gst_amount,
147
  req.gst_rate
148
  )
 
 
 
 
 
 
 
 
 
 
 
 
149
  return await _to_sale_response(sale)
 
 
 
 
 
 
 
 
 
 
 
150
  except Exception as e:
 
 
 
 
 
 
 
 
 
 
151
  raise HTTPException(status_code=400, detail=str(e))
152
 
153
  @router.post("/{sale_id}/cancel", response_model=SaleResponse)
@@ -157,8 +301,40 @@ async def cancel_sale_endpoint(
157
  ):
158
  try:
159
  sale = await cancel_sale(sale_id)
 
 
 
 
 
 
 
 
 
 
 
160
  return await _to_sale_response(sale)
 
 
 
 
 
 
 
 
 
 
 
161
  except Exception as e:
 
 
 
 
 
 
 
 
 
 
162
  raise HTTPException(status_code=400, detail=str(e))
163
 
164
  @router.post("/{sale_id}/refund", response_model=SaleResponse)
@@ -169,8 +345,41 @@ async def refund_sale_endpoint(
169
  ):
170
  try:
171
  sale = await refund_sale(sale_id, req.amount, req.reason, req.refunded_by)
 
 
 
 
 
 
 
 
 
 
 
 
172
  return await _to_sale_response(sale)
 
 
 
 
 
 
 
 
 
 
 
173
  except Exception as e:
 
 
 
 
 
 
 
 
 
 
174
  raise HTTPException(status_code=400, detail=str(e))
175
 
176
  async def _to_sale_response(sale: dict) -> SaleResponse:
 
1
  """
2
  Retail Sales API router.
3
  """
4
+ from app.core.logging import get_logger
5
  from uuid import UUID
6
  from fastapi import APIRouter, HTTPException, status, Depends
7
 
 
27
  list_sales,
28
  )
29
 
30
+ logger = get_logger(__name__)
31
 
32
  router = APIRouter(
33
  prefix="/pos/sales",
 
53
  payment_tenders=[t.model_dump() for t in req.payment_tenders] if req.payment_tenders else None,
54
  )
55
  sale = await get_sale(sid)
56
+
57
+ logger.info(
58
+ "Sale created",
59
+ extra={
60
+ "operation": "create_sale",
61
+ "sale_id": str(sid),
62
+ "merchant_id": str(current_user.merchant_id),
63
+ "user_id": str(current_user.id) if current_user.id else None
64
+ }
65
+ )
66
+
67
  return await _to_sale_response(sale)
68
+ except HTTPException:
69
+ raise
70
+ except ValueError as e:
71
+ logger.warning(
72
+ "Create sale validation failed",
73
+ extra={
74
+ "operation": "create_sale",
75
+ "error": str(e),
76
+ "merchant_id": str(current_user.merchant_id) if current_user.merchant_id else None
77
+ }
78
+ )
79
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
80
  except Exception as e:
81
+ logger.error(
82
+ "Create sale failed",
83
+ extra={
84
+ "operation": "create_sale",
85
+ "error": str(e),
86
+ "error_type": type(e).__name__,
87
+ "merchant_id": str(current_user.merchant_id) if current_user.merchant_id else None
88
+ },
89
+ exc_info=True
90
+ )
91
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to create sale")
92
 
93
  @router.get("/{sale_id}", response_model=SaleResponse)
94
  async def get_sale_endpoint(
95
  sale_id: UUID,
96
  current_user: TokenUser = Depends(require_pos_permission("sales", "view"))
97
  ):
98
+ try:
99
+ sale = await get_sale(sale_id)
100
+ if not sale:
101
+ logger.warning(
102
+ "Sale not found",
103
+ extra={
104
+ "operation": "get_sale",
105
+ "sale_id": str(sale_id),
106
+ "merchant_id": str(current_user.merchant_id) if current_user.merchant_id else None
107
+ }
108
+ )
109
+ raise HTTPException(status_code=404, detail="Sale not found")
110
+ return await _to_sale_response(sale)
111
+ except HTTPException:
112
+ raise
113
+ except Exception as e:
114
+ logger.error(
115
+ "Get sale failed",
116
+ extra={
117
+ "operation": "get_sale",
118
+ "sale_id": str(sale_id),
119
+ "error": str(e),
120
+ "merchant_id": str(current_user.merchant_id) if current_user.merchant_id else None
121
+ },
122
+ exc_info=True
123
+ )
124
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to get sale")
125
 
126
  @router.post("/list", response_model=SaleListResponse, summary="List sales")
127
  async def list_sales_endpoint(
 
148
  - skip: Records to skip (default: 0)
149
  - limit: Max records to return (default: 50, max: 500)
150
  """
151
+ try:
152
+ # Extract filters and parameters from payload
153
+ filters = payload.filters or {}
154
+ skip = payload.skip or 0
155
+ limit = payload.limit or 50
156
+ projection_list = payload.projection_list
157
+
158
+ # Always use merchant_id from token for security
159
+ if not current_user.merchant_id:
160
+ raise HTTPException(status_code=400, detail="merchant_id must be available in token")
161
+
162
+ sales, total = await list_sales(
163
+ merchant_id=current_user.merchant_id,
164
+ filters=filters,
165
+ skip=skip,
166
+ limit=limit,
167
+ projection_list=projection_list
168
+ )
169
+
170
+ logger.info(
171
+ "Sales listed",
172
+ extra={
173
+ "operation": "list_sales",
174
+ "count": len(sales),
175
+ "total": total,
176
+ "merchant_id": str(current_user.merchant_id) if current_user.merchant_id else None
177
+ }
178
+ )
179
+
180
+ return SaleListResponse(
181
+ sales=sales, # Raw dicts when projection is used, full objects otherwise
182
+ total=total,
183
+ skip=skip,
184
+ limit=limit
185
+ )
186
+ except HTTPException:
187
+ raise
188
+ except Exception as e:
189
+ logger.error(
190
+ "List sales failed",
191
+ extra={
192
+ "operation": "list_sales",
193
+ "error": str(e),
194
+ "merchant_id": str(current_user.merchant_id) if current_user.merchant_id else None
195
+ },
196
+ exc_info=True
197
+ )
198
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to list sales")
199
 
200
  @router.put("/{sale_id}/items", response_model=SaleResponse)
201
  async def update_items_endpoint(
 
205
  ):
206
  try:
207
  sale = await replace_items(sale_id, [i.model_dump() for i in req.items])
208
+
209
+ logger.info(
210
+ "Sale items updated",
211
+ extra={
212
+ "operation": "update_sale_items",
213
+ "sale_id": str(sale_id),
214
+ "merchant_id": str(current_user.merchant_id) if current_user.merchant_id else None,
215
+ "user_id": str(current_user.id) if current_user.id else None
216
+ }
217
+ )
218
+
219
  return await _to_sale_response(sale)
220
+ except ValueError as e:
221
+ logger.warning(
222
+ "Update sale items validation failed",
223
+ extra={
224
+ "operation": "update_sale_items",
225
+ "sale_id": str(sale_id),
226
+ "error": str(e),
227
+ "merchant_id": str(current_user.merchant_id) if current_user.merchant_id else None
228
+ }
229
+ )
230
+ raise HTTPException(status_code=400, detail=str(e))
231
  except Exception as e:
232
+ logger.error(
233
+ "Update sale items failed",
234
+ extra={
235
+ "operation": "update_sale_items",
236
+ "sale_id": str(sale_id),
237
+ "error": str(e),
238
+ "merchant_id": str(current_user.merchant_id) if current_user.merchant_id else None
239
+ },
240
+ exc_info=True
241
+ )
242
  raise HTTPException(status_code=400, detail=str(e))
243
 
244
  @router.post("/{sale_id}/payments", response_model=SaleResponse)
 
257
  req.gst_amount,
258
  req.gst_rate
259
  )
260
+
261
+ logger.info(
262
+ "Payment captured",
263
+ extra={
264
+ "operation": "capture_payment",
265
+ "sale_id": str(sale_id),
266
+ "amount": req.amount,
267
+ "merchant_id": str(current_user.merchant_id) if current_user.merchant_id else None,
268
+ "user_id": str(current_user.id) if current_user.id else None
269
+ }
270
+ )
271
+
272
  return await _to_sale_response(sale)
273
+ except ValueError as e:
274
+ logger.warning(
275
+ "Capture payment validation failed",
276
+ extra={
277
+ "operation": "capture_payment",
278
+ "sale_id": str(sale_id),
279
+ "error": str(e),
280
+ "merchant_id": str(current_user.merchant_id) if current_user.merchant_id else None
281
+ }
282
+ )
283
+ raise HTTPException(status_code=400, detail=str(e))
284
  except Exception as e:
285
+ logger.error(
286
+ "Capture payment failed",
287
+ extra={
288
+ "operation": "capture_payment",
289
+ "sale_id": str(sale_id),
290
+ "error": str(e),
291
+ "merchant_id": str(current_user.merchant_id) if current_user.merchant_id else None
292
+ },
293
+ exc_info=True
294
+ )
295
  raise HTTPException(status_code=400, detail=str(e))
296
 
297
  @router.post("/{sale_id}/cancel", response_model=SaleResponse)
 
301
  ):
302
  try:
303
  sale = await cancel_sale(sale_id)
304
+
305
+ logger.info(
306
+ "Sale cancelled",
307
+ extra={
308
+ "operation": "cancel_sale",
309
+ "sale_id": str(sale_id),
310
+ "merchant_id": str(current_user.merchant_id) if current_user.merchant_id else None,
311
+ "user_id": str(current_user.id) if current_user.id else None
312
+ }
313
+ )
314
+
315
  return await _to_sale_response(sale)
316
+ except ValueError as e:
317
+ logger.warning(
318
+ "Cancel sale validation failed",
319
+ extra={
320
+ "operation": "cancel_sale",
321
+ "sale_id": str(sale_id),
322
+ "error": str(e),
323
+ "merchant_id": str(current_user.merchant_id) if current_user.merchant_id else None
324
+ }
325
+ )
326
+ raise HTTPException(status_code=400, detail=str(e))
327
  except Exception as e:
328
+ logger.error(
329
+ "Cancel sale failed",
330
+ extra={
331
+ "operation": "cancel_sale",
332
+ "sale_id": str(sale_id),
333
+ "error": str(e),
334
+ "merchant_id": str(current_user.merchant_id) if current_user.merchant_id else None
335
+ },
336
+ exc_info=True
337
+ )
338
  raise HTTPException(status_code=400, detail=str(e))
339
 
340
  @router.post("/{sale_id}/refund", response_model=SaleResponse)
 
345
  ):
346
  try:
347
  sale = await refund_sale(sale_id, req.amount, req.reason, req.refunded_by)
348
+
349
+ logger.info(
350
+ "Sale refunded",
351
+ extra={
352
+ "operation": "refund_sale",
353
+ "sale_id": str(sale_id),
354
+ "amount": req.amount,
355
+ "merchant_id": str(current_user.merchant_id) if current_user.merchant_id else None,
356
+ "user_id": str(current_user.id) if current_user.id else None
357
+ }
358
+ )
359
+
360
  return await _to_sale_response(sale)
361
+ except ValueError as e:
362
+ logger.warning(
363
+ "Refund sale validation failed",
364
+ extra={
365
+ "operation": "refund_sale",
366
+ "sale_id": str(sale_id),
367
+ "error": str(e),
368
+ "merchant_id": str(current_user.merchant_id) if current_user.merchant_id else None
369
+ }
370
+ )
371
+ raise HTTPException(status_code=400, detail=str(e))
372
  except Exception as e:
373
+ logger.error(
374
+ "Refund sale failed",
375
+ extra={
376
+ "operation": "refund_sale",
377
+ "sale_id": str(sale_id),
378
+ "error": str(e),
379
+ "merchant_id": str(current_user.merchant_id) if current_user.merchant_id else None
380
+ },
381
+ exc_info=True
382
+ )
383
  raise HTTPException(status_code=400, detail=str(e))
384
 
385
  async def _to_sale_response(sale: dict) -> SaleResponse:
app/sales/retail/services/service.py CHANGED
@@ -1,14 +1,14 @@
1
  """
2
  Retail Sales service: business logic and Postgres operations.
3
  """
4
- import logging
5
  from uuid import uuid4, UUID
6
  from typing import Optional, List, Tuple
7
  from sqlalchemy import text
8
 
9
  from app.sql import get_postgres_session
10
 
11
- logger = logging.getLogger(__name__)
12
 
13
  # Helper to compute taxes (placeholder: 18% GST)
14
  TAX_RATE = 0.18
@@ -148,11 +148,27 @@ async def create_sale(
148
 
149
  await session.commit()
150
 
151
- logger.info(f"Created sale {sale_id} with {len(enriched_items)} items")
 
 
 
 
 
 
 
 
152
  return sale_id
153
  except Exception as e:
154
  await session.rollback()
155
- logger.error(f"Failed to create sale: {e}")
 
 
 
 
 
 
 
 
156
  raise
157
 
158
  async def get_sale(sale_id: UUID) -> dict:
@@ -250,7 +266,15 @@ async def replace_items(sale_id: UUID, items: List[dict]) -> dict:
250
  await session.commit()
251
  except Exception as e:
252
  await session.rollback()
253
- logger.error(f"Failed to update items: {e}")
 
 
 
 
 
 
 
 
254
  raise
255
  return await get_sale(sale_id)
256
 
@@ -295,7 +319,16 @@ async def capture_payment(sale_id: UUID, mode: str, amount: float, reference_no:
295
  await session.commit()
296
  except Exception as e:
297
  await session.rollback()
298
- logger.error(f"Payment capture failed: {e}")
 
 
 
 
 
 
 
 
 
299
  raise
300
  return await get_sale(sale_id)
301
 
@@ -354,7 +387,16 @@ async def refund_sale(sale_id: UUID, amount: float, reason: Optional[str], refun
354
  await session.commit()
355
  except Exception as e:
356
  await session.rollback()
357
- logger.error(f"Refund failed: {e}")
 
 
 
 
 
 
 
 
 
358
  raise
359
  return await get_sale(sale_id)
360
 
@@ -476,5 +518,5 @@ async def list_sales(
476
  {"sid": str(sale_id)}
477
  )
478
  sale["refunds"] = [dict(r._mapping) for r in ref_rs.fetchall()]
479
-
480
  return sales, total
 
1
  """
2
  Retail Sales service: business logic and Postgres operations.
3
  """
4
+ from app.core.logging import get_logger
5
  from uuid import uuid4, UUID
6
  from typing import Optional, List, Tuple
7
  from sqlalchemy import text
8
 
9
  from app.sql import get_postgres_session
10
 
11
+ logger = get_logger(__name__)
12
 
13
  # Helper to compute taxes (placeholder: 18% GST)
14
  TAX_RATE = 0.18
 
148
 
149
  await session.commit()
150
 
151
+ logger.info(
152
+ f"Created sale {sale_id} with {len(enriched_items)} items",
153
+ extra={
154
+ "operation": "create_sale",
155
+ "sale_id": str(sale_id),
156
+ "merchant_id": str(merchant_id),
157
+ "items_count": len(enriched_items)
158
+ }
159
+ )
160
  return sale_id
161
  except Exception as e:
162
  await session.rollback()
163
+ logger.error(
164
+ f"Failed to create sale: {e}",
165
+ extra={
166
+ "operation": "create_sale",
167
+ "merchant_id": str(merchant_id),
168
+ "error": str(e)
169
+ },
170
+ exc_info=True
171
+ )
172
  raise
173
 
174
  async def get_sale(sale_id: UUID) -> dict:
 
266
  await session.commit()
267
  except Exception as e:
268
  await session.rollback()
269
+ logger.error(
270
+ f"Failed to update items: {e}",
271
+ extra={
272
+ "operation": "replace_items",
273
+ "sale_id": str(sale_id),
274
+ "error": str(e)
275
+ },
276
+ exc_info=True
277
+ )
278
  raise
279
  return await get_sale(sale_id)
280
 
 
319
  await session.commit()
320
  except Exception as e:
321
  await session.rollback()
322
+ logger.error(
323
+ f"Payment capture failed: {e}",
324
+ extra={
325
+ "operation": "capture_payment",
326
+ "sale_id": str(sale_id),
327
+ "amount": amount,
328
+ "error": str(e)
329
+ },
330
+ exc_info=True
331
+ )
332
  raise
333
  return await get_sale(sale_id)
334
 
 
387
  await session.commit()
388
  except Exception as e:
389
  await session.rollback()
390
+ logger.error(
391
+ f"Refund failed: {e}",
392
+ extra={
393
+ "operation": "refund_sale",
394
+ "sale_id": str(sale_id),
395
+ "amount": amount,
396
+ "error": str(e)
397
+ },
398
+ exc_info=True
399
+ )
400
  raise
401
  return await get_sale(sale_id)
402
 
 
518
  {"sid": str(sale_id)}
519
  )
520
  sale["refunds"] = [dict(r._mapping) for r in ref_rs.fetchall()]
521
+
522
  return sales, total
app/sales/returns/controllers/router.py CHANGED
@@ -3,8 +3,7 @@ RMA (Return Merchandise Authorization) API router - FastAPI endpoints for RMA op
3
  """
4
  from typing import Optional, List
5
  from fastapi import APIRouter, HTTPException, Query, status
6
- # from insightfy_utils.logging import get_logger # TODO: Uncomment when package is available
7
- import logging
8
 
9
  from app.sales.returns.schemas.schema import (
10
  RMACreate,
@@ -18,8 +17,7 @@ from app.sales.returns.schemas.schema import (
18
  )
19
  from app.sales.returns.services.service import RMAService
20
 
21
- # logger = get_logger(__name__) # TODO: Uncomment when insightfy_utils is available
22
- logger = logging.getLogger(__name__)
23
 
24
  router = APIRouter(
25
  prefix="/rma",
@@ -47,8 +45,36 @@ async def create_rma(payload: RMACreate):
47
  - Order exists and items are valid
48
  - Return window is within policy
49
  """
50
- rma = await RMAService.create_rma(payload)
51
- return RMAResponse(**rma)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
 
54
  @router.get(
@@ -60,8 +86,25 @@ async def get_rma(rma_id: str):
60
  """
61
  Retrieve an RMA by ID.
62
  """
63
- rma = await RMAService.get_rma(rma_id)
64
- return RMAResponse(**rma)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
 
67
  @router.put(
@@ -74,8 +117,38 @@ async def update_rma(rma_id: str, payload: RMAUpdate):
74
  Update an RMA.
75
  Only allowed for requested or approved RMAs.
76
  """
77
- rma = await RMAService.update_rma(rma_id, payload)
78
- return RMAResponse(**rma)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
 
81
  @router.post(
@@ -92,8 +165,41 @@ async def approve_rma(rma_id: str, payload: RMAApproveRequest):
92
  - **approved_action**: Final approved action (may differ from requested)
93
  - **return_window_days**: Days allowed for return
94
  """
95
- rma = await RMAService.approve_rma(rma_id, payload)
96
- return RMAResponse(**rma)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
 
99
  @router.post(
@@ -110,8 +216,39 @@ async def schedule_pickup(rma_id: str, payload: RMAPickupRequest):
110
  - **pickup_address**: Address for pickup
111
  - **pickup_contact**: Contact person details
112
  """
113
- rma = await RMAService.schedule_pickup(rma_id, payload)
114
- return RMAResponse(**rma)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
 
117
  @router.post(
@@ -134,8 +271,40 @@ async def inspect_rma(rma_id: str, payload: RMAInspectRequest):
134
  - Issue refund/credit note based on inspection
135
  - Close RMA if fully processed
136
  """
137
- rma = await RMAService.inspect_rma(rma_id, payload)
138
- return RMAResponse(**rma)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
 
140
 
141
  @router.get(
@@ -153,14 +322,31 @@ async def list_rmas(
153
  """
154
  List RMAs with optional filters.
155
  """
156
- rmas = await RMAService.list_rmas(
157
- merchant_id=merchant_id,
158
- requestor_id=requestor_id,
159
- status=status.value if status else None,
160
- skip=skip,
161
- limit=limit
162
- )
163
- return [RMAListResponse(**rma) for rma in rmas]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
 
165
 
166
  @router.delete(
@@ -176,5 +362,37 @@ async def cancel_rma(
176
  Cancel an RMA.
177
  Only allowed for requested or approved RMAs.
178
  """
179
- result = await RMAService.cancel_rma(rma_id, cancelled_by, reason)
180
- return {"message": f"RMA {rma_id} cancelled successfully"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  """
4
  from typing import Optional, List
5
  from fastapi import APIRouter, HTTPException, Query, status
6
+ from app.core.logging import get_logger
 
7
 
8
  from app.sales.returns.schemas.schema import (
9
  RMACreate,
 
17
  )
18
  from app.sales.returns.services.service import RMAService
19
 
20
+ logger = get_logger(__name__)
 
21
 
22
  router = APIRouter(
23
  prefix="/rma",
 
45
  - Order exists and items are valid
46
  - Return window is within policy
47
  """
48
+ try:
49
+ rma = await RMAService.create_rma(payload)
50
+
51
+ logger.info(
52
+ "RMA created",
53
+ extra={
54
+ "operation": "create_rma",
55
+ "rma_id": rma.get("rma_id"),
56
+ "merchant_id": payload.merchant_id,
57
+ "user_id": payload.created_by
58
+ }
59
+ )
60
+
61
+ return RMAResponse(**rma)
62
+ except HTTPException:
63
+ raise
64
+ except Exception as e:
65
+ logger.error(
66
+ "Error creating RMA",
67
+ extra={
68
+ "operation": "create_rma",
69
+ "error": str(e),
70
+ "merchant_id": payload.merchant_id
71
+ },
72
+ exc_info=True
73
+ )
74
+ raise HTTPException(
75
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
76
+ detail="Error creating RMA"
77
+ )
78
 
79
 
80
  @router.get(
 
86
  """
87
  Retrieve an RMA by ID.
88
  """
89
+ try:
90
+ rma = await RMAService.get_rma(rma_id)
91
+ return RMAResponse(**rma)
92
+ except HTTPException:
93
+ raise
94
+ except Exception as e:
95
+ logger.error(
96
+ f"Error getting RMA {rma_id}",
97
+ extra={
98
+ "operation": "get_rma",
99
+ "rma_id": rma_id,
100
+ "error": str(e)
101
+ },
102
+ exc_info=True
103
+ )
104
+ raise HTTPException(
105
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
106
+ detail=f"Error getting RMA {rma_id}"
107
+ )
108
 
109
 
110
  @router.put(
 
117
  Update an RMA.
118
  Only allowed for requested or approved RMAs.
119
  """
120
+ try:
121
+ rma = await RMAService.update_rma(rma_id, payload)
122
+
123
+ # Using merchant_id from result if available
124
+ merchant_id = rma.get("merchant_id")
125
+
126
+ logger.info(
127
+ f"RMA {rma_id} updated",
128
+ extra={
129
+ "operation": "update_rma",
130
+ "rma_id": rma_id,
131
+ "merchant_id": merchant_id
132
+ }
133
+ )
134
+
135
+ return RMAResponse(**rma)
136
+ except HTTPException:
137
+ raise
138
+ except Exception as e:
139
+ logger.error(
140
+ f"Error updating RMA {rma_id}",
141
+ extra={
142
+ "operation": "update_rma",
143
+ "rma_id": rma_id,
144
+ "error": str(e)
145
+ },
146
+ exc_info=True
147
+ )
148
+ raise HTTPException(
149
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
150
+ detail=f"Error updating RMA {rma_id}"
151
+ )
152
 
153
 
154
  @router.post(
 
165
  - **approved_action**: Final approved action (may differ from requested)
166
  - **return_window_days**: Days allowed for return
167
  """
168
+ try:
169
+ rma = await RMAService.approve_rma(rma_id, payload)
170
+
171
+ action = "approved" if payload.approved else "rejected"
172
+ merchant_id = rma.get("merchant_id")
173
+
174
+ logger.info(
175
+ f"RMA {rma_id} {action}",
176
+ extra={
177
+ "operation": "approve_rma",
178
+ "rma_id": rma_id,
179
+ "action": action,
180
+ "merchant_id": merchant_id,
181
+ "user_id": payload.approved_by
182
+ }
183
+ )
184
+
185
+ return RMAResponse(**rma)
186
+ except HTTPException:
187
+ raise
188
+ except Exception as e:
189
+ logger.error(
190
+ f"Error approving RMA {rma_id}",
191
+ extra={
192
+ "operation": "approve_rma",
193
+ "rma_id": rma_id,
194
+ "error": str(e),
195
+ "user_id": payload.approved_by
196
+ },
197
+ exc_info=True
198
+ )
199
+ raise HTTPException(
200
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
201
+ detail=f"Error approving RMA {rma_id}"
202
+ )
203
 
204
 
205
  @router.post(
 
216
  - **pickup_address**: Address for pickup
217
  - **pickup_contact**: Contact person details
218
  """
219
+ try:
220
+ rma = await RMAService.schedule_pickup(rma_id, payload)
221
+
222
+ merchant_id = rma.get("merchant_id")
223
+
224
+ logger.info(
225
+ f"Pickup scheduled for RMA {rma_id}",
226
+ extra={
227
+ "operation": "schedule_pickup",
228
+ "rma_id": rma_id,
229
+ "merchant_id": merchant_id,
230
+ "user_id": payload.scheduled_by
231
+ }
232
+ )
233
+
234
+ return RMAResponse(**rma)
235
+ except HTTPException:
236
+ raise
237
+ except Exception as e:
238
+ logger.error(
239
+ f"Error scheduling pickup for RMA {rma_id}",
240
+ extra={
241
+ "operation": "schedule_pickup",
242
+ "rma_id": rma_id,
243
+ "error": str(e),
244
+ "user_id": payload.scheduled_by
245
+ },
246
+ exc_info=True
247
+ )
248
+ raise HTTPException(
249
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
250
+ detail=f"Error scheduling pickup for RMA {rma_id}"
251
+ )
252
 
253
 
254
  @router.post(
 
271
  - Issue refund/credit note based on inspection
272
  - Close RMA if fully processed
273
  """
274
+ try:
275
+ rma = await RMAService.inspect_rma(rma_id, payload)
276
+
277
+ merchant_id = rma.get("merchant_id")
278
+
279
+ logger.info(
280
+ f"RMA {rma_id} inspected",
281
+ extra={
282
+ "operation": "inspect_rma",
283
+ "rma_id": rma_id,
284
+ "result": payload.inspection_result.value,
285
+ "merchant_id": merchant_id,
286
+ "user_id": payload.inspected_by
287
+ }
288
+ )
289
+
290
+ return RMAResponse(**rma)
291
+ except HTTPException:
292
+ raise
293
+ except Exception as e:
294
+ logger.error(
295
+ f"Error inspecting RMA {rma_id}",
296
+ extra={
297
+ "operation": "inspect_rma",
298
+ "rma_id": rma_id,
299
+ "error": str(e),
300
+ "user_id": payload.inspected_by
301
+ },
302
+ exc_info=True
303
+ )
304
+ raise HTTPException(
305
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
306
+ detail=f"Error inspecting RMA {rma_id}"
307
+ )
308
 
309
 
310
  @router.get(
 
322
  """
323
  List RMAs with optional filters.
324
  """
325
+ try:
326
+ rmas = await RMAService.list_rmas(
327
+ merchant_id=merchant_id,
328
+ requestor_id=requestor_id,
329
+ status=status.value if status else None,
330
+ skip=skip,
331
+ limit=limit
332
+ )
333
+ return [RMAListResponse(**rma) for rma in rmas]
334
+ except HTTPException:
335
+ raise
336
+ except Exception as e:
337
+ logger.error(
338
+ "Error listing RMAs",
339
+ extra={
340
+ "operation": "list_rmas",
341
+ "error": str(e),
342
+ "merchant_id": merchant_id
343
+ },
344
+ exc_info=True
345
+ )
346
+ raise HTTPException(
347
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
348
+ detail="Error listing RMAs"
349
+ )
350
 
351
 
352
  @router.delete(
 
362
  Cancel an RMA.
363
  Only allowed for requested or approved RMAs.
364
  """
365
+ try:
366
+ result = await RMAService.cancel_rma(rma_id, cancelled_by, reason)
367
+
368
+ merchant_id = result.get("merchant_id")
369
+
370
+ logger.info(
371
+ f"RMA {rma_id} cancelled",
372
+ extra={
373
+ "operation": "cancel_rma",
374
+ "rma_id": rma_id,
375
+ "merchant_id": merchant_id,
376
+ "user_id": cancelled_by,
377
+ "reason": reason
378
+ }
379
+ )
380
+
381
+ return {"message": f"RMA {rma_id} cancelled successfully"}
382
+ except HTTPException:
383
+ raise
384
+ except Exception as e:
385
+ logger.error(
386
+ f"Error cancelling RMA {rma_id}",
387
+ extra={
388
+ "operation": "cancel_rma",
389
+ "rma_id": rma_id,
390
+ "error": str(e),
391
+ "user_id": cancelled_by
392
+ },
393
+ exc_info=True
394
+ )
395
+ raise HTTPException(
396
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
397
+ detail=f"Error cancelling RMA {rma_id}"
398
+ )
app/sales/returns/services/service.py CHANGED
@@ -5,8 +5,7 @@ from datetime import datetime
5
  from typing import Optional, List, Dict, Any
6
  from decimal import Decimal
7
  from fastapi import HTTPException, status
8
- # from insightfy_utils.logging import get_logger # TODO: Uncomment when package is available
9
- import logging
10
  import secrets
11
 
12
  from app.nosql import get_database
@@ -26,7 +25,7 @@ from app.sales.returns.schemas.schema import (
26
  )
27
 
28
  # logger = get_logger(__name__) # TODO: Uncomment when insightfy_utils is available
29
- logger = logging.getLogger(__name__)
30
 
31
 
32
  def generate_rma_number(merchant_code: str) -> str:
@@ -49,83 +48,101 @@ class RMAService:
49
  """Create a new RMA"""
50
  db = get_database()
51
 
52
- # Validate related order exists
53
- order = await db[SCM_SALES_ORDERS_COLLECTION].find_one({"sales_order_id": payload.related_order_id})
54
- if not order:
55
- raise HTTPException(
56
- status_code=status.HTTP_404_NOT_FOUND,
57
- detail=f"Order {payload.related_order_id} not found"
58
- )
59
-
60
- # Validate items are in the order
61
- order_skus = {item["sku"] for item in order.get("items", [])}
62
- rma_skus = {item.sku for item in payload.items}
63
-
64
- if not rma_skus.issubset(order_skus):
65
- invalid_skus = rma_skus - order_skus
66
- raise HTTPException(
67
- status_code=status.HTTP_400_BAD_REQUEST,
68
- detail=f"Invalid SKUs not in order: {invalid_skus}"
69
- )
70
-
71
- # Generate RMA ID and number
72
- rma_id = generate_rma_id()
73
- merchant_code = payload.merchant_id.split("_")[0] if "_" in payload.merchant_id else "MER"
74
- rma_number = payload.rma_number or generate_rma_number(merchant_code)
75
-
76
- # Create RMA document
77
- now = datetime.utcnow()
78
- items_dict = [item.dict() for item in payload.items]
79
-
80
- rma_doc = {
81
- "rma_id": rma_id,
82
- "rma_number": rma_number,
83
- "related_order_id": payload.related_order_id,
84
- "related_order_type": payload.related_order_type,
85
- "related_order_number": order.get("order_number"),
86
- "requestor_id": payload.requestor_id,
87
- "requestor_type": payload.requestor_type,
88
- "requestor_name": None, # TODO: Fetch from customer/merchant service
89
- "merchant_id": payload.merchant_id,
90
- "status": RMAStatus.REQUESTED.value,
91
- "items": items_dict,
92
- "requested_action": payload.requested_action.value,
93
- "approved_action": None,
94
- "return_reason": payload.return_reason,
95
- "return_address": payload.return_address,
96
- "pickup_required": payload.pickup_required,
97
- "pickup_scheduled": False,
98
- "pickup_details": None,
99
- "shipment_id": None,
100
- "tracking_number": None,
101
- "inspection_result": None,
102
- "inspection_notes": None,
103
- "refund_amount": None,
104
- "store_credit_amount": None,
105
- "credit_note_id": None,
106
- "replacement_order_id": None,
107
- "notes": payload.notes,
108
- "internal_notes": None,
109
- "created_by": payload.created_by,
110
- "created_at": now.isoformat(),
111
- "updated_at": now.isoformat(),
112
- "approved_at": None,
113
- "approved_by": None,
114
- "rejected_at": None,
115
- "rejected_by": None,
116
- "rejection_reason": None,
117
- "inspected_at": None,
118
- "inspected_by": None,
119
- "closed_at": None,
120
- "metadata": {}
121
- }
122
-
123
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  await db[SCM_RMA_COLLECTION].insert_one(rma_doc)
125
- logger.info(f"Created RMA {rma_id}", extra={"rma_id": rma_id, "order_id": payload.related_order_id})
 
 
 
 
 
 
 
 
126
  return rma_doc
 
 
127
  except Exception as e:
128
- logger.error(f"Error creating RMA", exc_info=e)
 
 
 
 
 
 
 
 
129
  raise HTTPException(
130
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
131
  detail="Error creating RMA"
@@ -134,152 +151,245 @@ class RMAService:
134
  @staticmethod
135
  async def get_rma(rma_id: str) -> Dict[str, Any]:
136
  """Get RMA by ID"""
137
- db = get_database()
138
-
139
- rma = await db[SCM_RMA_COLLECTION].find_one({"rma_id": rma_id})
140
- if not rma:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  raise HTTPException(
142
- status_code=status.HTTP_404_NOT_FOUND,
143
- detail=f"RMA {rma_id} not found"
144
  )
145
-
146
- return rma
147
 
148
  @staticmethod
149
  async def approve_rma(rma_id: str, payload: RMAApproveRequest) -> Dict[str, Any]:
150
  """Approve or reject RMA"""
151
- db = get_database()
152
-
153
- rma = await RMAService.get_rma(rma_id)
154
-
155
- if rma["status"] != RMAStatus.REQUESTED.value:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  raise HTTPException(
157
- status_code=status.HTTP_400_BAD_REQUEST,
158
- detail=f"Cannot approve RMA with status {rma['status']}"
159
  )
160
-
161
- now = datetime.utcnow()
162
- update_data = {
163
- "updated_at": now.isoformat()
164
- }
165
-
166
- if payload.approved:
167
- update_data["status"] = RMAStatus.APPROVED.value
168
- update_data["approved_at"] = now.isoformat()
169
- update_data["approved_by"] = payload.approved_by
170
- update_data["approved_action"] = payload.approved_action.value if payload.approved_action else rma["requested_action"]
171
-
172
- # Update items if partial approval
173
- if payload.items:
174
- items_dict = [item.dict() for item in payload.items]
175
- update_data["items"] = items_dict
176
- else:
177
- update_data["status"] = RMAStatus.REJECTED.value
178
- update_data["rejected_at"] = now.isoformat()
179
- update_data["rejected_by"] = payload.approved_by
180
- update_data["rejection_reason"] = payload.rejection_reason
181
-
182
- await db[SCM_RMA_COLLECTION].update_one(
183
- {"rma_id": rma_id},
184
- {"$set": update_data}
185
- )
186
-
187
- logger.info(f"{'Approved' if payload.approved else 'Rejected'} RMA {rma_id}", extra={"rma_id": rma_id})
188
-
189
- return await RMAService.get_rma(rma_id)
190
 
191
  @staticmethod
192
  async def schedule_pickup(rma_id: str, payload: RMAPickupRequest) -> Dict[str, Any]:
193
  """Schedule pickup for RMA"""
194
- db = get_database()
195
-
196
- rma = await RMAService.get_rma(rma_id)
197
-
198
- if rma["status"] != RMAStatus.APPROVED.value:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  raise HTTPException(
200
- status_code=status.HTTP_400_BAD_REQUEST,
201
- detail=f"Cannot schedule pickup for RMA with status {rma['status']}"
202
  )
203
-
204
- pickup_details = {
205
- "carrier": payload.carrier,
206
- "pickup_date": payload.pickup_date.isoformat(),
207
- "pickup_address": payload.pickup_address,
208
- "pickup_contact_name": payload.pickup_contact_name,
209
- "pickup_contact_phone": payload.pickup_contact_phone,
210
- "special_instructions": payload.special_instructions,
211
- "scheduled_by": payload.scheduled_by,
212
- "scheduled_at": datetime.utcnow().isoformat()
213
- }
214
-
215
- update_data = {
216
- "status": RMAStatus.PICKED.value,
217
- "pickup_scheduled": True,
218
- "pickup_details": pickup_details,
219
- "updated_at": datetime.utcnow().isoformat()
220
- }
221
-
222
- await db[SCM_RMA_COLLECTION].update_one(
223
- {"rma_id": rma_id},
224
- {"$set": update_data}
225
- )
226
-
227
- logger.info(f"Scheduled pickup for RMA {rma_id}", extra={"rma_id": rma_id, "carrier": payload.carrier})
228
-
229
- return await RMAService.get_rma(rma_id)
230
 
231
  @staticmethod
232
  async def inspect_rma(rma_id: str, payload: RMAInspectRequest) -> Dict[str, Any]:
233
  """Perform inspection on returned items"""
234
- db = get_database()
235
-
236
- rma = await RMAService.get_rma(rma_id)
237
-
238
- if rma["status"] not in [RMAStatus.PICKED.value, RMAStatus.IN_TRANSIT.value, RMAStatus.RECEIVED.value]:
239
- raise HTTPException(
240
- status_code=status.HTTP_400_BAD_REQUEST,
241
- detail=f"Cannot inspect RMA with status {rma['status']}"
242
- )
243
-
244
- now = datetime.utcnow()
245
- update_data = {
246
- "status": RMAStatus.INSPECTED.value,
247
- "inspection_result": payload.inspection_result.value,
248
- "inspection_notes": payload.inspection_notes,
249
- "inspected_at": payload.inspected_at.isoformat(),
250
- "inspected_by": payload.inspected_by,
251
- "updated_at": now.isoformat()
252
- }
253
-
254
- # Process based on inspection result
255
- if payload.inspection_result.value in ["approved", "partial_approved"]:
256
- if payload.refund_amount:
257
- update_data["refund_amount"] = float(payload.refund_amount)
258
 
259
- if payload.store_credit_amount:
260
- update_data["store_credit_amount"] = float(payload.store_credit_amount)
261
 
262
- if payload.credit_note_id:
263
- update_data["credit_note_id"] = payload.credit_note_id
 
 
 
264
 
265
- if payload.replacement_order_id:
266
- update_data["replacement_order_id"] = payload.replacement_order_id
 
 
 
 
 
 
 
267
 
268
- # Update inventory ledger for returned items
269
- await RMAService._update_inventory_for_return(rma, payload.items)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
 
271
- # Close RMA if fully processed
272
- update_data["status"] = RMAStatus.CLOSED.value
273
- update_data["closed_at"] = now.isoformat()
274
-
275
- await db[SCM_RMA_COLLECTION].update_one(
276
- {"rma_id": rma_id},
277
- {"$set": update_data}
278
- )
279
-
280
- logger.info(f"Inspected RMA {rma_id}", extra={"rma_id": rma_id, "result": payload.inspection_result.value})
281
-
282
- return await RMAService.get_rma(rma_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
 
284
  @staticmethod
285
  async def _update_inventory_for_return(rma: Dict[str, Any], inspected_items: List[Dict[str, Any]]):
@@ -312,37 +422,60 @@ class RMAService:
312
  @staticmethod
313
  async def update_rma(rma_id: str, payload: RMAUpdate) -> Dict[str, Any]:
314
  """Update RMA"""
315
- db = get_database()
316
-
317
- rma = await RMAService.get_rma(rma_id)
318
-
319
- if rma["status"] in [RMAStatus.CLOSED.value, RMAStatus.CANCELLED.value]:
320
- raise HTTPException(
321
- status_code=status.HTTP_400_BAD_REQUEST,
322
- detail=f"Cannot update RMA with status {rma['status']}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
  )
324
-
325
- update_data = payload.dict(exclude_unset=True)
326
- if not update_data:
327
  raise HTTPException(
328
- status_code=status.HTTP_400_BAD_REQUEST,
329
- detail="No update data provided"
330
  )
331
-
332
- if "items" in update_data:
333
- items_dict = [item.dict() for item in update_data["items"]]
334
- update_data["items"] = items_dict
335
-
336
- update_data["updated_at"] = datetime.utcnow().isoformat()
337
-
338
- await db[SCM_RMA_COLLECTION].update_one(
339
- {"rma_id": rma_id},
340
- {"$set": update_data}
341
- )
342
-
343
- logger.info(f"Updated RMA {rma_id}", extra={"rma_id": rma_id})
344
-
345
- return await RMAService.get_rma(rma_id)
346
 
347
  @staticmethod
348
  async def list_rmas(
@@ -368,7 +501,15 @@ class RMAService:
368
  rmas = await cursor.to_list(length=limit)
369
  return rmas
370
  except Exception as e:
371
- logger.error("Error listing RMAs", exc_info=e)
 
 
 
 
 
 
 
 
372
  raise HTTPException(
373
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
374
  detail="Error listing RMAs"
@@ -377,30 +518,55 @@ class RMAService:
377
  @staticmethod
378
  async def cancel_rma(rma_id: str, cancelled_by: str, reason: str) -> Dict[str, Any]:
379
  """Cancel RMA"""
380
- db = get_database()
381
-
382
- rma = await RMAService.get_rma(rma_id)
383
-
384
- if rma["status"] in [RMAStatus.CLOSED.value, RMAStatus.CANCELLED.value]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
385
  raise HTTPException(
386
- status_code=status.HTTP_400_BAD_REQUEST,
387
- detail=f"Cannot cancel RMA with status {rma['status']}"
388
  )
389
-
390
- now = datetime.utcnow()
391
- update_data = {
392
- "status": RMAStatus.CANCELLED.value,
393
- "rejection_reason": reason,
394
- "rejected_by": cancelled_by,
395
- "rejected_at": now.isoformat(),
396
- "updated_at": now.isoformat()
397
- }
398
-
399
- await db[SCM_RMA_COLLECTION].update_one(
400
- {"rma_id": rma_id},
401
- {"$set": update_data}
402
- )
403
-
404
- logger.info(f"Cancelled RMA {rma_id}", extra={"rma_id": rma_id})
405
-
406
- return await RMAService.get_rma(rma_id)
 
5
  from typing import Optional, List, Dict, Any
6
  from decimal import Decimal
7
  from fastapi import HTTPException, status
8
+ from app.core.logging import get_logger
 
9
  import secrets
10
 
11
  from app.nosql import get_database
 
25
  )
26
 
27
  # logger = get_logger(__name__) # TODO: Uncomment when insightfy_utils is available
28
+ logger = get_logger(__name__)
29
 
30
 
31
  def generate_rma_number(merchant_code: str) -> str:
 
48
  """Create a new RMA"""
49
  db = get_database()
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  try:
52
+ # Validate related order exists
53
+ order = await db[SCM_SALES_ORDERS_COLLECTION].find_one({"sales_order_id": payload.related_order_id})
54
+ if not order:
55
+ raise HTTPException(
56
+ status_code=status.HTTP_404_NOT_FOUND,
57
+ detail=f"Order {payload.related_order_id} not found"
58
+ )
59
+
60
+ # Validate items are in the order
61
+ order_skus = {item["sku"] for item in order.get("items", [])}
62
+ rma_skus = {item.sku for item in payload.items}
63
+
64
+ if not rma_skus.issubset(order_skus):
65
+ invalid_skus = rma_skus - order_skus
66
+ raise HTTPException(
67
+ status_code=status.HTTP_400_BAD_REQUEST,
68
+ detail=f"Invalid SKUs not in order: {invalid_skus}"
69
+ )
70
+
71
+ # Generate RMA ID and number
72
+ rma_id = generate_rma_id()
73
+ merchant_code = payload.merchant_id.split("_")[0] if "_" in payload.merchant_id else "MER"
74
+ rma_number = payload.rma_number or generate_rma_number(merchant_code)
75
+
76
+ # Create RMA document
77
+ now = datetime.utcnow()
78
+ items_dict = [item.dict() for item in payload.items]
79
+
80
+ rma_doc = {
81
+ "rma_id": rma_id,
82
+ "rma_number": rma_number,
83
+ "related_order_id": payload.related_order_id,
84
+ "related_order_type": payload.related_order_type,
85
+ "related_order_number": order.get("order_number"),
86
+ "requestor_id": payload.requestor_id,
87
+ "requestor_type": payload.requestor_type,
88
+ "requestor_name": None, # TODO: Fetch from customer/merchant service
89
+ "merchant_id": payload.merchant_id,
90
+ "status": RMAStatus.REQUESTED.value,
91
+ "items": items_dict,
92
+ "requested_action": payload.requested_action.value,
93
+ "approved_action": None,
94
+ "return_reason": payload.return_reason,
95
+ "return_address": payload.return_address,
96
+ "pickup_required": payload.pickup_required,
97
+ "pickup_scheduled": False,
98
+ "pickup_details": None,
99
+ "shipment_id": None,
100
+ "tracking_number": None,
101
+ "inspection_result": None,
102
+ "inspection_notes": None,
103
+ "refund_amount": None,
104
+ "store_credit_amount": None,
105
+ "credit_note_id": None,
106
+ "replacement_order_id": None,
107
+ "notes": payload.notes,
108
+ "internal_notes": None,
109
+ "created_by": payload.created_by,
110
+ "created_at": now.isoformat(),
111
+ "updated_at": now.isoformat(),
112
+ "approved_at": None,
113
+ "approved_by": None,
114
+ "rejected_at": None,
115
+ "rejected_by": None,
116
+ "rejection_reason": None,
117
+ "inspected_at": None,
118
+ "inspected_by": None,
119
+ "closed_at": None,
120
+ "metadata": {}
121
+ }
122
+
123
  await db[SCM_RMA_COLLECTION].insert_one(rma_doc)
124
+ logger.info(
125
+ f"Created RMA {rma_id}",
126
+ extra={
127
+ "operation": "create_rma",
128
+ "rma_id": rma_id,
129
+ "order_id": payload.related_order_id,
130
+ "merchant_id": payload.merchant_id
131
+ }
132
+ )
133
  return rma_doc
134
+ except HTTPException:
135
+ raise
136
  except Exception as e:
137
+ logger.error(
138
+ "Error creating RMA",
139
+ extra={
140
+ "operation": "create_rma",
141
+ "error": str(e),
142
+ "merchant_id": payload.merchant_id
143
+ },
144
+ exc_info=True
145
+ )
146
  raise HTTPException(
147
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
148
  detail="Error creating RMA"
 
151
  @staticmethod
152
  async def get_rma(rma_id: str) -> Dict[str, Any]:
153
  """Get RMA by ID"""
154
+ try:
155
+ db = get_database()
156
+
157
+ rma = await db[SCM_RMA_COLLECTION].find_one({"rma_id": rma_id})
158
+ if not rma:
159
+ raise HTTPException(
160
+ status_code=status.HTTP_404_NOT_FOUND,
161
+ detail=f"RMA {rma_id} not found"
162
+ )
163
+
164
+ return rma
165
+ except HTTPException:
166
+ raise
167
+ except Exception as e:
168
+ logger.error(
169
+ f"Error getting RMA {rma_id}",
170
+ extra={
171
+ "operation": "get_rma",
172
+ "rma_id": rma_id,
173
+ "error": str(e)
174
+ },
175
+ exc_info=True
176
+ )
177
  raise HTTPException(
178
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
179
+ detail=f"Error getting RMA {rma_id}"
180
  )
 
 
181
 
182
  @staticmethod
183
  async def approve_rma(rma_id: str, payload: RMAApproveRequest) -> Dict[str, Any]:
184
  """Approve or reject RMA"""
185
+ try:
186
+ db = get_database()
187
+
188
+ rma = await RMAService.get_rma(rma_id)
189
+
190
+ if rma["status"] != RMAStatus.REQUESTED.value:
191
+ raise HTTPException(
192
+ status_code=status.HTTP_400_BAD_REQUEST,
193
+ detail=f"Cannot approve RMA with status {rma['status']}"
194
+ )
195
+
196
+ now = datetime.utcnow()
197
+ update_data = {
198
+ "updated_at": now.isoformat()
199
+ }
200
+
201
+ if payload.approved:
202
+ update_data["status"] = RMAStatus.APPROVED.value
203
+ update_data["approved_at"] = now.isoformat()
204
+ update_data["approved_by"] = payload.approved_by
205
+ update_data["approved_action"] = payload.approved_action.value if payload.approved_action else rma["requested_action"]
206
+
207
+ # Update items if partial approval
208
+ if payload.items:
209
+ items_dict = [item.dict() for item in payload.items]
210
+ update_data["items"] = items_dict
211
+ else:
212
+ update_data["status"] = RMAStatus.REJECTED.value
213
+ update_data["rejected_at"] = now.isoformat()
214
+ update_data["rejected_by"] = payload.approved_by
215
+ update_data["rejection_reason"] = payload.rejection_reason
216
+
217
+ await db[SCM_RMA_COLLECTION].update_one(
218
+ {"rma_id": rma_id},
219
+ {"$set": update_data}
220
+ )
221
+
222
+ action = "approve" if payload.approved else "reject"
223
+ logger.info(
224
+ f"{'Approved' if payload.approved else 'Rejected'} RMA {rma_id}",
225
+ extra={
226
+ "operation": "approve_rma",
227
+ "rma_id": rma_id,
228
+ "action": action,
229
+ "user_id": payload.approved_by
230
+ }
231
+ )
232
+
233
+ return await RMAService.get_rma(rma_id)
234
+ except HTTPException:
235
+ raise
236
+ except Exception as e:
237
+ logger.error(
238
+ f"Error approving RMA {rma_id}",
239
+ extra={
240
+ "operation": "approve_rma",
241
+ "rma_id": rma_id,
242
+ "error": str(e)
243
+ },
244
+ exc_info=True
245
+ )
246
  raise HTTPException(
247
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
248
+ detail=f"Error approving RMA {rma_id}"
249
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
 
251
  @staticmethod
252
  async def schedule_pickup(rma_id: str, payload: RMAPickupRequest) -> Dict[str, Any]:
253
  """Schedule pickup for RMA"""
254
+ try:
255
+ db = get_database()
256
+
257
+ rma = await RMAService.get_rma(rma_id)
258
+
259
+ if rma["status"] != RMAStatus.APPROVED.value:
260
+ raise HTTPException(
261
+ status_code=status.HTTP_400_BAD_REQUEST,
262
+ detail=f"Cannot schedule pickup for RMA with status {rma['status']}"
263
+ )
264
+
265
+ pickup_details = {
266
+ "carrier": payload.carrier,
267
+ "pickup_date": payload.pickup_date.isoformat(),
268
+ "pickup_address": payload.pickup_address,
269
+ "pickup_contact_name": payload.pickup_contact_name,
270
+ "pickup_contact_phone": payload.pickup_contact_phone,
271
+ "special_instructions": payload.special_instructions,
272
+ "scheduled_by": payload.scheduled_by,
273
+ "scheduled_at": datetime.utcnow().isoformat()
274
+ }
275
+
276
+ update_data = {
277
+ "status": RMAStatus.PICKED.value,
278
+ "pickup_scheduled": True,
279
+ "pickup_details": pickup_details,
280
+ "updated_at": datetime.utcnow().isoformat()
281
+ }
282
+
283
+ await db[SCM_RMA_COLLECTION].update_one(
284
+ {"rma_id": rma_id},
285
+ {"$set": update_data}
286
+ )
287
+
288
+ logger.info(
289
+ f"Scheduled pickup for RMA {rma_id}",
290
+ extra={
291
+ "operation": "schedule_pickup",
292
+ "rma_id": rma_id,
293
+ "carrier": payload.carrier,
294
+ "user_id": payload.scheduled_by
295
+ }
296
+ )
297
+
298
+ return await RMAService.get_rma(rma_id)
299
+ except HTTPException:
300
+ raise
301
+ except Exception as e:
302
+ logger.error(
303
+ f"Error scheduling pickup for RMA {rma_id}",
304
+ extra={
305
+ "operation": "schedule_pickup",
306
+ "rma_id": rma_id,
307
+ "error": str(e)
308
+ },
309
+ exc_info=True
310
+ )
311
  raise HTTPException(
312
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
313
+ detail=f"Error scheduling pickup for RMA {rma_id}"
314
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
 
316
  @staticmethod
317
  async def inspect_rma(rma_id: str, payload: RMAInspectRequest) -> Dict[str, Any]:
318
  """Perform inspection on returned items"""
319
+ try:
320
+ db = get_database()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
 
322
+ rma = await RMAService.get_rma(rma_id)
 
323
 
324
+ if rma["status"] not in [RMAStatus.PICKED.value, RMAStatus.IN_TRANSIT.value, RMAStatus.RECEIVED.value]:
325
+ raise HTTPException(
326
+ status_code=status.HTTP_400_BAD_REQUEST,
327
+ detail=f"Cannot inspect RMA with status {rma['status']}"
328
+ )
329
 
330
+ now = datetime.utcnow()
331
+ update_data = {
332
+ "status": RMAStatus.INSPECTED.value,
333
+ "inspection_result": payload.inspection_result.value,
334
+ "inspection_notes": payload.inspection_notes,
335
+ "inspected_at": payload.inspected_at.isoformat(),
336
+ "inspected_by": payload.inspected_by,
337
+ "updated_at": now.isoformat()
338
+ }
339
 
340
+ # Process based on inspection result
341
+ if payload.inspection_result.value in ["approved", "partial_approved"]:
342
+ if payload.refund_amount:
343
+ update_data["refund_amount"] = float(payload.refund_amount)
344
+
345
+ if payload.store_credit_amount:
346
+ update_data["store_credit_amount"] = float(payload.store_credit_amount)
347
+
348
+ if payload.credit_note_id:
349
+ update_data["credit_note_id"] = payload.credit_note_id
350
+
351
+ if payload.replacement_order_id:
352
+ update_data["replacement_order_id"] = payload.replacement_order_id
353
+
354
+ # Update inventory ledger for returned items
355
+ await RMAService._update_inventory_for_return(rma, payload.items)
356
+
357
+ # Close RMA if fully processed
358
+ update_data["status"] = RMAStatus.CLOSED.value
359
+ update_data["closed_at"] = now.isoformat()
360
 
361
+ await db[SCM_RMA_COLLECTION].update_one(
362
+ {"rma_id": rma_id},
363
+ {"$set": update_data}
364
+ )
365
+
366
+ logger.info(
367
+ f"Inspected RMA {rma_id}",
368
+ extra={
369
+ "operation": "inspect_rma",
370
+ "rma_id": rma_id,
371
+ "result": payload.inspection_result.value,
372
+ "user_id": payload.inspected_by
373
+ }
374
+ )
375
+
376
+ return await RMAService.get_rma(rma_id)
377
+ except HTTPException:
378
+ raise
379
+ except Exception as e:
380
+ logger.error(
381
+ f"Error inspecting RMA {rma_id}",
382
+ extra={
383
+ "operation": "inspect_rma",
384
+ "rma_id": rma_id,
385
+ "error": str(e)
386
+ },
387
+ exc_info=True
388
+ )
389
+ raise HTTPException(
390
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
391
+ detail=f"Error inspecting RMA {rma_id}"
392
+ )
393
 
394
  @staticmethod
395
  async def _update_inventory_for_return(rma: Dict[str, Any], inspected_items: List[Dict[str, Any]]):
 
422
  @staticmethod
423
  async def update_rma(rma_id: str, payload: RMAUpdate) -> Dict[str, Any]:
424
  """Update RMA"""
425
+ try:
426
+ db = get_database()
427
+
428
+ rma = await RMAService.get_rma(rma_id)
429
+
430
+ if rma["status"] in [RMAStatus.CLOSED.value, RMAStatus.CANCELLED.value]:
431
+ raise HTTPException(
432
+ status_code=status.HTTP_400_BAD_REQUEST,
433
+ detail=f"Cannot update RMA with status {rma['status']}"
434
+ )
435
+
436
+ update_data = payload.dict(exclude_unset=True)
437
+ if not update_data:
438
+ raise HTTPException(
439
+ status_code=status.HTTP_400_BAD_REQUEST,
440
+ detail="No update data provided"
441
+ )
442
+
443
+ if "items" in update_data:
444
+ items_dict = [item.dict() for item in update_data["items"]]
445
+ update_data["items"] = items_dict
446
+
447
+ update_data["updated_at"] = datetime.utcnow().isoformat()
448
+
449
+ await db[SCM_RMA_COLLECTION].update_one(
450
+ {"rma_id": rma_id},
451
+ {"$set": update_data}
452
+ )
453
+
454
+ logger.info(
455
+ f"Updated RMA {rma_id}",
456
+ extra={
457
+ "operation": "update_rma",
458
+ "rma_id": rma_id
459
+ }
460
+ )
461
+
462
+ return await RMAService.get_rma(rma_id)
463
+ except HTTPException:
464
+ raise
465
+ except Exception as e:
466
+ logger.error(
467
+ f"Error updating RMA {rma_id}",
468
+ extra={
469
+ "operation": "update_rma",
470
+ "rma_id": rma_id,
471
+ "error": str(e)
472
+ },
473
+ exc_info=True
474
  )
 
 
 
475
  raise HTTPException(
476
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
477
+ detail=f"Error updating RMA {rma_id}"
478
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
479
 
480
  @staticmethod
481
  async def list_rmas(
 
501
  rmas = await cursor.to_list(length=limit)
502
  return rmas
503
  except Exception as e:
504
+ logger.error(
505
+ "Error listing RMAs",
506
+ extra={
507
+ "operation": "list_rmas",
508
+ "error": str(e),
509
+ "merchant_id": merchant_id
510
+ },
511
+ exc_info=True
512
+ )
513
  raise HTTPException(
514
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
515
  detail="Error listing RMAs"
 
518
  @staticmethod
519
  async def cancel_rma(rma_id: str, cancelled_by: str, reason: str) -> Dict[str, Any]:
520
  """Cancel RMA"""
521
+ try:
522
+ db = get_database()
523
+
524
+ rma = await RMAService.get_rma(rma_id)
525
+
526
+ if rma["status"] in [RMAStatus.CLOSED.value, RMAStatus.CANCELLED.value]:
527
+ raise HTTPException(
528
+ status_code=status.HTTP_400_BAD_REQUEST,
529
+ detail=f"Cannot cancel RMA with status {rma['status']}"
530
+ )
531
+
532
+ now = datetime.utcnow()
533
+ update_data = {
534
+ "status": RMAStatus.CANCELLED.value,
535
+ "rejection_reason": reason,
536
+ "rejected_by": cancelled_by,
537
+ "rejected_at": now.isoformat(),
538
+ "updated_at": now.isoformat()
539
+ }
540
+
541
+ await db[SCM_RMA_COLLECTION].update_one(
542
+ {"rma_id": rma_id},
543
+ {"$set": update_data}
544
+ )
545
+
546
+ logger.info(
547
+ f"Cancelled RMA {rma_id}",
548
+ extra={
549
+ "operation": "cancel_rma",
550
+ "rma_id": rma_id,
551
+ "user_id": cancelled_by,
552
+ "reason": reason
553
+ }
554
+ )
555
+
556
+ return await RMAService.get_rma(rma_id)
557
+ except HTTPException:
558
+ raise
559
+ except Exception as e:
560
+ logger.error(
561
+ f"Error cancelling RMA {rma_id}",
562
+ extra={
563
+ "operation": "cancel_rma",
564
+ "rma_id": rma_id,
565
+ "error": str(e)
566
+ },
567
+ exc_info=True
568
+ )
569
  raise HTTPException(
570
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
571
+ detail=f"Error cancelling RMA {rma_id}"
572
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/staff/controllers/router.py CHANGED
@@ -4,8 +4,8 @@ Simplified POS staff management.
4
  """
5
  from typing import Optional, List
6
  from fastapi import APIRouter, HTTPException, Query, status, Header
7
- import logging
8
 
 
9
  from app.constants.staff_types import Designation, stafftatus
10
  from app.staff.schemas.staff_schema import (
11
  StaffCreateSchema,
@@ -18,7 +18,7 @@ from app.staff.schemas.staff_schema import (
18
  )
19
  from app.staff.services.staff_service import StaffService
20
 
21
- logger = logging.getLogger(__name__)
22
 
23
  router = APIRouter(
24
  prefix="/staff",
@@ -50,7 +50,32 @@ async def create_staff(payload: StaffCreateSchema) -> StaffResponseSchema:
50
  - photo_url: Profile photo URL (HTTPS only)
51
  - notes: Additional information
52
  """
53
- return await StaffService.create_staff(payload)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
 
56
 
@@ -65,13 +90,43 @@ async def get_staff(staff_id: str) -> StaffResponseSchema:
65
  """
66
  Get detailed information about a specific staff member.
67
  """
68
- staff = await StaffService.get_staff_by_id(staff_id)
69
- if not staff:
70
- raise HTTPException(
71
- status_code=status.HTTP_404_NOT_FOUND,
72
- detail=f"Staff {staff_id} not found"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  )
74
- return staff
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
 
77
  @router.put(
@@ -85,7 +140,32 @@ async def update_staff(staff_id: str, payload: StaffUpdateSchema) -> StaffRespon
85
 
86
  All fields are optional - only provided fields will be updated.
87
  """
88
- return await StaffService.update_staff(staff_id, payload)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
 
91
  @router.delete(
@@ -96,7 +176,31 @@ async def delete_staff(staff_id: str):
96
  """
97
  Delete a staff member (soft delete - sets status to inactive).
98
  """
99
- return await StaffService.delete_staff(staff_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
 
101
 
102
  @router.get(
@@ -107,18 +211,48 @@ async def get_staff_schedule(staff_id: str):
107
  """
108
  Get working schedule for a staff member.
109
  """
110
- staff = await StaffService.get_staff_by_id(staff_id)
111
- if not staff:
112
- raise HTTPException(
113
- status_code=status.HTTP_404_NOT_FOUND,
114
- detail=f"Staff {staff_id} not found"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  )
116
-
117
- return {
118
- "staff_id": staff.staff_id,
119
- "name": staff.name,
120
- "working_hours": staff.working_hours
121
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
122
 
123
 
124
  @router.get(
@@ -140,7 +274,31 @@ async def get_employee(user_id: str) -> EmployeeResponse:
140
  Raises:
141
  404: Employee not found
142
  """
143
- return await StaffService.get_employee(user_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
 
145
 
146
  @router.get(
@@ -162,7 +320,31 @@ async def get_employee_by_code(employee_code: str) -> EmployeeResponse:
162
  Raises:
163
  404: Employee not found
164
  """
165
- return await StaffService.get_employee_by_code(employee_code)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
 
168
  @router.put(
@@ -196,7 +378,34 @@ async def update_employee(
196
  - Phone uniqueness (if changing phone)
197
  - Manager validation (if changing manager)
198
  """
199
- return await StaffService.update_employee(user_id, payload, x_user_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
 
201
 
202
  @router.post(
@@ -227,22 +436,45 @@ async def list_staff(
227
  Returns:
228
  List of staff matching the filters and total count
229
  """
230
- items, total = await StaffService.list_staff(
231
- designation=payload.designation,
232
- manager_id=payload.manager_id,
233
- status_filter=payload.status,
234
- region=payload.region,
235
- skip=payload.skip,
236
- limit=payload.limit,
237
- projection_list=payload.projection_list
238
- )
239
-
240
- return StaffListResponse(
241
- items=items,
242
- total=total,
243
- skip=payload.skip,
244
- limit=payload.limit
245
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
 
247
 
248
  @router.delete(
@@ -270,7 +502,33 @@ async def delete_employee(
270
  404: Employee not found
271
  400: Employee has active direct reports
272
  """
273
- return await StaffService.delete_employee(user_id, x_user_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
 
275
 
276
  @router.get(
@@ -303,16 +561,39 @@ async def get_employee_reports(
303
  Returns:
304
  List of direct report staff
305
  """
306
- # First verify employee exists
307
- await StaffService.get_employee(user_id)
308
-
309
- items, _ = await StaffService.list_staff(
310
- manager_id=user_id,
311
- status_filter=status_filter,
312
- skip=skip,
313
- limit=limit
314
- )
315
- return items
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
 
317
 
318
  @router.get(
@@ -332,22 +613,45 @@ async def get_employee_hierarchy(user_id: str):
332
  Returns:
333
  List of staff from top manager to current employee
334
  """
335
- hierarchy = []
336
- current_id = user_id
337
- visited = set() # Prevent infinite loops
338
-
339
- # Traverse up the hierarchy
340
- while current_id and current_id not in visited:
341
- visited.add(current_id)
342
- employee_data = await StaffService.get_employee(current_id)
343
- hierarchy.insert(0, employee_data) # Insert at beginning to maintain order
344
- current_id = employee_data.manager_id
345
-
346
- return {
347
- "user_id": user_id,
348
- "depth": len(hierarchy),
349
- "hierarchy": hierarchy
350
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
 
352
 
353
  @router.patch(
@@ -380,8 +684,36 @@ async def update_employee_status(
380
  - active → suspended (disciplinary)
381
  - active/inactive/suspended → terminated (termination)
382
  """
383
- update_payload = EmployeeUpdate(status=new_status)
384
- return await StaffService.update_employee(user_id, update_payload, x_user_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
385
 
386
 
387
  @router.patch(
@@ -419,16 +751,43 @@ async def update_location_consent(
419
  - Background tracking requires location_tracking_consent=True
420
  - Consent timestamp is automatically recorded
421
  """
422
- from datetime import datetime
423
- from app.staff.schemas.staff_schema import LocationSettingsSchema
424
-
425
- location_settings = LocationSettingsSchema(
426
- location_tracking_consent=location_tracking_consent,
427
- consent_given_at=datetime.utcnow() if location_tracking_consent else None,
428
- consent_ip=consent_ip,
429
- consent_device=consent_device,
430
- background_tracking_opt_in=background_tracking_opt_in
431
- )
432
-
433
- update_payload = EmployeeUpdate(location_settings=location_settings)
434
- return await StaffService.update_employee(user_id, update_payload, x_user_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  """
5
  from typing import Optional, List
6
  from fastapi import APIRouter, HTTPException, Query, status, Header
 
7
 
8
+ from app.core.logging import get_logger
9
  from app.constants.staff_types import Designation, stafftatus
10
  from app.staff.schemas.staff_schema import (
11
  StaffCreateSchema,
 
18
  )
19
  from app.staff.services.staff_service import StaffService
20
 
21
+ logger = get_logger(__name__)
22
 
23
  router = APIRouter(
24
  prefix="/staff",
 
50
  - photo_url: Profile photo URL (HTTPS only)
51
  - notes: Additional information
52
  """
53
+ try:
54
+ result = await StaffService.create_staff(payload)
55
+
56
+ logger.info(
57
+ "Staff member created",
58
+ extra={
59
+ "operation": "create_staff",
60
+ "merchant_id": str(payload.merchant_id),
61
+ "staff_name": payload.name
62
+ }
63
+ )
64
+
65
+ return result
66
+ except HTTPException:
67
+ raise
68
+ except Exception as e:
69
+ logger.error(
70
+ "Create staff failed",
71
+ extra={
72
+ "operation": "create_staff",
73
+ "error": str(e),
74
+ "merchant_id": str(payload.merchant_id) if hasattr(payload, "merchant_id") else None
75
+ },
76
+ exc_info=True
77
+ )
78
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to create staff member")
79
 
80
 
81
 
 
90
  """
91
  Get detailed information about a specific staff member.
92
  """
93
+ try:
94
+ staff = await StaffService.get_staff_by_id(staff_id)
95
+ if not staff:
96
+ logger.warning(
97
+ "Staff member not found",
98
+ extra={
99
+ "operation": "get_staff",
100
+ "staff_id": staff_id
101
+ }
102
+ )
103
+ raise HTTPException(
104
+ status_code=status.HTTP_404_NOT_FOUND,
105
+ detail=f"Staff {staff_id} not found"
106
+ )
107
+
108
+ logger.info(
109
+ "Staff member retrieved",
110
+ extra={
111
+ "operation": "get_staff",
112
+ "staff_id": staff_id,
113
+ "merchant_id": str(staff.merchant_id) if hasattr(staff, "merchant_id") else None
114
+ }
115
  )
116
+ return staff
117
+ except HTTPException:
118
+ raise
119
+ except Exception as e:
120
+ logger.error(
121
+ "Get staff failed",
122
+ extra={
123
+ "operation": "get_staff",
124
+ "staff_id": staff_id,
125
+ "error": str(e)
126
+ },
127
+ exc_info=True
128
+ )
129
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to get staff member")
130
 
131
 
132
  @router.put(
 
140
 
141
  All fields are optional - only provided fields will be updated.
142
  """
143
+ try:
144
+ result = await StaffService.update_staff(staff_id, payload)
145
+
146
+ logger.info(
147
+ "Staff member updated",
148
+ extra={
149
+ "operation": "update_staff",
150
+ "staff_id": staff_id,
151
+ "merchant_id": str(result.merchant_id) if hasattr(result, "merchant_id") else None
152
+ }
153
+ )
154
+
155
+ return result
156
+ except HTTPException:
157
+ raise
158
+ except Exception as e:
159
+ logger.error(
160
+ "Update staff failed",
161
+ extra={
162
+ "operation": "update_staff",
163
+ "staff_id": staff_id,
164
+ "error": str(e)
165
+ },
166
+ exc_info=True
167
+ )
168
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to update staff member")
169
 
170
 
171
  @router.delete(
 
176
  """
177
  Delete a staff member (soft delete - sets status to inactive).
178
  """
179
+ try:
180
+ result = await StaffService.delete_staff(staff_id)
181
+
182
+ logger.info(
183
+ "Staff member deleted",
184
+ extra={
185
+ "operation": "delete_staff",
186
+ "staff_id": staff_id
187
+ }
188
+ )
189
+
190
+ return result
191
+ except HTTPException:
192
+ raise
193
+ except Exception as e:
194
+ logger.error(
195
+ "Delete staff failed",
196
+ extra={
197
+ "operation": "delete_staff",
198
+ "staff_id": staff_id,
199
+ "error": str(e)
200
+ },
201
+ exc_info=True
202
+ )
203
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to delete staff member")
204
 
205
 
206
  @router.get(
 
211
  """
212
  Get working schedule for a staff member.
213
  """
214
+ try:
215
+ staff = await StaffService.get_staff_by_id(staff_id)
216
+ if not staff:
217
+ logger.warning(
218
+ "Staff member not found for schedule",
219
+ extra={
220
+ "operation": "get_staff_schedule",
221
+ "staff_id": staff_id
222
+ }
223
+ )
224
+ raise HTTPException(
225
+ status_code=status.HTTP_404_NOT_FOUND,
226
+ detail=f"Staff {staff_id} not found"
227
+ )
228
+
229
+ logger.info(
230
+ "Staff schedule retrieved",
231
+ extra={
232
+ "operation": "get_staff_schedule",
233
+ "staff_id": staff_id,
234
+ "merchant_id": str(staff.merchant_id) if hasattr(staff, "merchant_id") else None
235
+ }
236
  )
237
+
238
+ return {
239
+ "staff_id": staff.staff_id,
240
+ "name": staff.name,
241
+ "working_hours": staff.working_hours
242
+ }
243
+ except HTTPException:
244
+ raise
245
+ except Exception as e:
246
+ logger.error(
247
+ "Get staff schedule failed",
248
+ extra={
249
+ "operation": "get_staff_schedule",
250
+ "staff_id": staff_id,
251
+ "error": str(e)
252
+ },
253
+ exc_info=True
254
+ )
255
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to get staff schedule")
256
 
257
 
258
  @router.get(
 
274
  Raises:
275
  404: Employee not found
276
  """
277
+ try:
278
+ employee = await StaffService.get_employee(user_id)
279
+
280
+ logger.info(
281
+ "Employee retrieved",
282
+ extra={
283
+ "operation": "get_employee",
284
+ "user_id": user_id,
285
+ "merchant_id": str(employee.merchant_id) if hasattr(employee, "merchant_id") else None
286
+ }
287
+ )
288
+ return employee
289
+ except HTTPException:
290
+ raise
291
+ except Exception as e:
292
+ logger.error(
293
+ "Get employee failed",
294
+ extra={
295
+ "operation": "get_employee",
296
+ "user_id": user_id,
297
+ "error": str(e)
298
+ },
299
+ exc_info=True
300
+ )
301
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to get employee")
302
 
303
 
304
  @router.get(
 
320
  Raises:
321
  404: Employee not found
322
  """
323
+ try:
324
+ employee = await StaffService.get_employee_by_code(employee_code)
325
+
326
+ logger.info(
327
+ "Employee retrieved by code",
328
+ extra={
329
+ "operation": "get_employee_by_code",
330
+ "employee_code": employee_code,
331
+ "merchant_id": str(employee.merchant_id) if hasattr(employee, "merchant_id") else None
332
+ }
333
+ )
334
+ return employee
335
+ except HTTPException:
336
+ raise
337
+ except Exception as e:
338
+ logger.error(
339
+ "Get employee by code failed",
340
+ extra={
341
+ "operation": "get_employee_by_code",
342
+ "employee_code": employee_code,
343
+ "error": str(e)
344
+ },
345
+ exc_info=True
346
+ )
347
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to get employee by code")
348
 
349
 
350
  @router.put(
 
378
  - Phone uniqueness (if changing phone)
379
  - Manager validation (if changing manager)
380
  """
381
+ try:
382
+ result = await StaffService.update_employee(user_id, payload, x_user_id)
383
+
384
+ logger.info(
385
+ "Employee updated",
386
+ extra={
387
+ "operation": "update_employee",
388
+ "user_id": user_id,
389
+ "merchant_id": str(result.merchant_id) if hasattr(result, "merchant_id") else None,
390
+ "updated_by": x_user_id
391
+ }
392
+ )
393
+
394
+ return result
395
+ except HTTPException:
396
+ raise
397
+ except Exception as e:
398
+ logger.error(
399
+ "Update employee failed",
400
+ extra={
401
+ "operation": "update_employee",
402
+ "user_id": user_id,
403
+ "error": str(e),
404
+ "updated_by": x_user_id
405
+ },
406
+ exc_info=True
407
+ )
408
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to update employee")
409
 
410
 
411
  @router.post(
 
436
  Returns:
437
  List of staff matching the filters and total count
438
  """
439
+ try:
440
+ items, total = await StaffService.list_staff(
441
+ designation=payload.designation,
442
+ manager_id=payload.manager_id,
443
+ status_filter=payload.status,
444
+ region=payload.region,
445
+ skip=payload.skip,
446
+ limit=payload.limit,
447
+ projection_list=payload.projection_list
448
+ )
449
+
450
+ logger.info(
451
+ "Staff listed",
452
+ extra={
453
+ "operation": "list_staff",
454
+ "count": len(items),
455
+ "total": total,
456
+ "manager_id": payload.manager_id
457
+ }
458
+ )
459
+
460
+ return StaffListResponse(
461
+ items=items,
462
+ total=total,
463
+ skip=payload.skip,
464
+ limit=payload.limit
465
+ )
466
+ except HTTPException:
467
+ raise
468
+ except Exception as e:
469
+ logger.error(
470
+ "List staff failed",
471
+ extra={
472
+ "operation": "list_staff",
473
+ "error": str(e)
474
+ },
475
+ exc_info=True
476
+ )
477
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to list staff")
478
 
479
 
480
  @router.delete(
 
502
  404: Employee not found
503
  400: Employee has active direct reports
504
  """
505
+ try:
506
+ result = await StaffService.delete_employee(user_id, x_user_id)
507
+
508
+ logger.info(
509
+ "Employee deleted",
510
+ extra={
511
+ "operation": "delete_employee",
512
+ "user_id": user_id,
513
+ "deleted_by": x_user_id
514
+ }
515
+ )
516
+
517
+ return result
518
+ except HTTPException:
519
+ raise
520
+ except Exception as e:
521
+ logger.error(
522
+ "Delete employee failed",
523
+ extra={
524
+ "operation": "delete_employee",
525
+ "user_id": user_id,
526
+ "error": str(e),
527
+ "deleted_by": x_user_id
528
+ },
529
+ exc_info=True
530
+ )
531
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to delete employee")
532
 
533
 
534
  @router.get(
 
561
  Returns:
562
  List of direct report staff
563
  """
564
+ try:
565
+ # First verify employee exists
566
+ await StaffService.get_employee(user_id)
567
+
568
+ items, _ = await StaffService.list_staff(
569
+ manager_id=user_id,
570
+ status_filter=status_filter,
571
+ skip=skip,
572
+ limit=limit
573
+ )
574
+
575
+ logger.info(
576
+ "Employee reports listed",
577
+ extra={
578
+ "operation": "get_employee_reports",
579
+ "user_id": user_id,
580
+ "count": len(items)
581
+ }
582
+ )
583
+ return items
584
+ except HTTPException:
585
+ raise
586
+ except Exception as e:
587
+ logger.error(
588
+ "Get employee reports failed",
589
+ extra={
590
+ "operation": "get_employee_reports",
591
+ "user_id": user_id,
592
+ "error": str(e)
593
+ },
594
+ exc_info=True
595
+ )
596
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to get employee reports")
597
 
598
 
599
  @router.get(
 
613
  Returns:
614
  List of staff from top manager to current employee
615
  """
616
+ try:
617
+ hierarchy = []
618
+ current_id = user_id
619
+ visited = set() # Prevent infinite loops
620
+
621
+ # Traverse up the hierarchy
622
+ while current_id and current_id not in visited:
623
+ visited.add(current_id)
624
+ employee_data = await StaffService.get_employee(current_id)
625
+ hierarchy.insert(0, employee_data) # Insert at beginning to maintain order
626
+ current_id = employee_data.manager_id
627
+
628
+ logger.info(
629
+ "Employee hierarchy retrieved",
630
+ extra={
631
+ "operation": "get_employee_hierarchy",
632
+ "user_id": user_id,
633
+ "depth": len(hierarchy)
634
+ }
635
+ )
636
+
637
+ return {
638
+ "user_id": user_id,
639
+ "depth": len(hierarchy),
640
+ "hierarchy": hierarchy
641
+ }
642
+ except HTTPException:
643
+ raise
644
+ except Exception as e:
645
+ logger.error(
646
+ "Get employee hierarchy failed",
647
+ extra={
648
+ "operation": "get_employee_hierarchy",
649
+ "user_id": user_id,
650
+ "error": str(e)
651
+ },
652
+ exc_info=True
653
+ )
654
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to get employee hierarchy")
655
 
656
 
657
  @router.patch(
 
684
  - active → suspended (disciplinary)
685
  - active/inactive/suspended → terminated (termination)
686
  """
687
+ try:
688
+ update_payload = EmployeeUpdate(status=new_status)
689
+ result = await StaffService.update_employee(user_id, update_payload, x_user_id)
690
+
691
+ logger.info(
692
+ "Employee status updated",
693
+ extra={
694
+ "operation": "update_employee_status",
695
+ "user_id": user_id,
696
+ "new_status": new_status,
697
+ "updated_by": x_user_id
698
+ }
699
+ )
700
+
701
+ return result
702
+ except HTTPException:
703
+ raise
704
+ except Exception as e:
705
+ logger.error(
706
+ "Update employee status failed",
707
+ extra={
708
+ "operation": "update_employee_status",
709
+ "user_id": user_id,
710
+ "new_status": new_status,
711
+ "error": str(e),
712
+ "updated_by": x_user_id
713
+ },
714
+ exc_info=True
715
+ )
716
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to update employee status")
717
 
718
 
719
  @router.patch(
 
751
  - Background tracking requires location_tracking_consent=True
752
  - Consent timestamp is automatically recorded
753
  """
754
+ try:
755
+ from datetime import datetime
756
+ from app.staff.schemas.staff_schema import LocationSettingsSchema
757
+
758
+ location_settings = LocationSettingsSchema(
759
+ location_tracking_consent=location_tracking_consent,
760
+ consent_given_at=datetime.utcnow() if location_tracking_consent else None,
761
+ consent_ip=consent_ip,
762
+ consent_device=consent_device,
763
+ background_tracking_opt_in=background_tracking_opt_in
764
+ )
765
+
766
+ update_payload = EmployeeUpdate(location_settings=location_settings)
767
+ result = await StaffService.update_employee(user_id, update_payload, x_user_id)
768
+
769
+ logger.info(
770
+ "Location consent updated",
771
+ extra={
772
+ "operation": "update_location_consent",
773
+ "user_id": user_id,
774
+ "location_tracking_consent": location_tracking_consent,
775
+ "updated_by": x_user_id
776
+ }
777
+ )
778
+
779
+ return result
780
+ except HTTPException:
781
+ raise
782
+ except Exception as e:
783
+ logger.error(
784
+ "Update location consent failed",
785
+ extra={
786
+ "operation": "update_location_consent",
787
+ "user_id": user_id,
788
+ "error": str(e),
789
+ "updated_by": x_user_id
790
+ },
791
+ exc_info=True
792
+ )
793
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to update location consent")
app/staff/services/staff_service.py CHANGED
@@ -5,7 +5,7 @@ Syncs staff data to both MongoDB and PostgreSQL (trans.pos_staff_ref).
5
  from datetime import datetime
6
  from typing import Optional, List, Dict, Any, Union
7
  from fastapi import HTTPException, status
8
- import logging
9
  import secrets
10
  from sqlalchemy import text
11
 
@@ -22,7 +22,7 @@ from app.staff.schemas.staff_schema import (
22
  )
23
  from app.constants.staff_types import Designation, stafftatus
24
 
25
- logger = logging.getLogger(__name__)
26
 
27
 
28
 
@@ -39,7 +39,7 @@ async def sync_staff_to_postgres(staff_id: str, merchant_id: str, staff_name: st
39
  try:
40
  async with get_postgres_session() as session:
41
  if session is None:
42
- logger.warning("PostgreSQL not available, skipping staff sync")
43
  return
44
 
45
  query = text("""
@@ -57,10 +57,25 @@ async def sync_staff_to_postgres(staff_id: str, merchant_id: str, staff_name: st
57
  "staff_name": staff_name
58
  })
59
  await session.commit()
60
- logger.info(f"Synced staff {staff_id} to trans.pos_staff_ref")
 
 
 
 
 
 
 
61
 
62
  except Exception as e:
63
- logger.error(f"Failed to sync staff {staff_id} to PostgreSQL: {e}")
 
 
 
 
 
 
 
 
64
  # Don't raise - PostgreSQL sync is secondary to MongoDB
65
  def generate_staff_id() -> str:
66
  """Generate a unique staff ID."""
@@ -102,13 +117,28 @@ class StaffService:
102
  staff_name=payload.name
103
  )
104
 
105
- logger.info(f"Created staff {staff_id} for merchant {payload.merchant_id}")
 
 
 
 
 
 
 
106
 
107
  # Return response
108
  return StaffResponseSchema(**staff_data)
109
 
110
  except Exception as e:
111
- logger.error(f"Error creating staff: {e}")
 
 
 
 
 
 
 
 
112
  raise HTTPException(
113
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
114
  detail=f"Error creating staff: {str(e)}"
@@ -123,7 +153,15 @@ class StaffService:
123
  return None
124
  return StaffResponseSchema(**staff)
125
  except Exception as e:
126
- logger.error(f"Error fetching staff {staff_id}: {e}")
 
 
 
 
 
 
 
 
127
  raise HTTPException(
128
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
129
  detail="Error retrieving staff"
@@ -177,14 +215,35 @@ class StaffService:
177
  )
178
 
179
  if result.modified_count == 0:
180
- logger.warning(f"No changes made to staff {staff_id}")
 
 
 
 
 
 
181
 
182
- logger.info(f"Updated staff {staff_id}")
 
 
 
 
 
 
 
183
 
184
  return updated_staff
185
 
186
  except Exception as e:
187
- logger.error(f"Error updating staff {staff_id}: {e}")
 
 
 
 
 
 
 
 
188
  raise HTTPException(
189
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
190
  detail=f"Error updating staff: {str(e)}"
@@ -223,11 +282,25 @@ class StaffService:
223
  }
224
  )
225
 
226
- logger.info(f"Deleted staff {staff_id}")
 
 
 
 
 
 
227
  return {"message": f"Staff {staff_id} deleted successfully"}
228
 
229
  except Exception as e:
230
- logger.error(f"Error deleting staff {staff_id}: {e}")
 
 
 
 
 
 
 
 
231
  raise HTTPException(
232
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
233
  detail="Error deleting staff"
@@ -247,7 +320,15 @@ class StaffService:
247
  except HTTPException:
248
  raise
249
  except Exception as e:
250
- logger.error(f"Error fetching employee {user_id}: {e}")
 
 
 
 
 
 
 
 
251
  raise HTTPException(
252
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
253
  detail="Error retrieving employee"
@@ -269,7 +350,15 @@ class StaffService:
269
  except HTTPException:
270
  raise
271
  except Exception as e:
272
- logger.error(f"Error fetching employee by code {employee_code}: {e}")
 
 
 
 
 
 
 
 
273
  raise HTTPException(
274
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
275
  detail="Error retrieving employee"
@@ -412,7 +501,14 @@ class StaffService:
412
  return [EmployeeResponse(**staff) for staff in staff_list], total
413
 
414
  except Exception as e:
415
- logger.error(f"Error listing staff: {e}")
 
 
 
 
 
 
 
416
  raise HTTPException(
417
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
418
  detail="Error listing staff"
 
5
  from datetime import datetime
6
  from typing import Optional, List, Dict, Any, Union
7
  from fastapi import HTTPException, status
8
+ from app.core.logging import get_logger
9
  import secrets
10
  from sqlalchemy import text
11
 
 
22
  )
23
  from app.constants.staff_types import Designation, stafftatus
24
 
25
+ logger = get_logger(__name__)
26
 
27
 
28
 
 
39
  try:
40
  async with get_postgres_session() as session:
41
  if session is None:
42
+ logger.warning("PostgreSQL not available, skipping staff sync", extra={"operation": "sync_staff_to_postgres"})
43
  return
44
 
45
  query = text("""
 
57
  "staff_name": staff_name
58
  })
59
  await session.commit()
60
+ logger.info(
61
+ f"Synced staff {staff_id} to trans.pos_staff_ref",
62
+ extra={
63
+ "operation": "sync_staff_to_postgres",
64
+ "staff_id": staff_id,
65
+ "merchant_id": str(merchant_id)
66
+ }
67
+ )
68
 
69
  except Exception as e:
70
+ logger.error(
71
+ f"Failed to sync staff {staff_id} to PostgreSQL",
72
+ extra={
73
+ "operation": "sync_staff_to_postgres",
74
+ "staff_id": staff_id,
75
+ "error": str(e)
76
+ },
77
+ exc_info=True
78
+ )
79
  # Don't raise - PostgreSQL sync is secondary to MongoDB
80
  def generate_staff_id() -> str:
81
  """Generate a unique staff ID."""
 
117
  staff_name=payload.name
118
  )
119
 
120
+ logger.info(
121
+ f"Created staff {staff_id}",
122
+ extra={
123
+ "operation": "create_staff",
124
+ "staff_id": staff_id,
125
+ "merchant_id": str(payload.merchant_id)
126
+ }
127
+ )
128
 
129
  # Return response
130
  return StaffResponseSchema(**staff_data)
131
 
132
  except Exception as e:
133
+ logger.error(
134
+ "Error creating staff",
135
+ extra={
136
+ "operation": "create_staff",
137
+ "error": str(e),
138
+ "merchant_id": str(payload.merchant_id) if hasattr(payload, "merchant_id") else None
139
+ },
140
+ exc_info=True
141
+ )
142
  raise HTTPException(
143
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
144
  detail=f"Error creating staff: {str(e)}"
 
153
  return None
154
  return StaffResponseSchema(**staff)
155
  except Exception as e:
156
+ logger.error(
157
+ f"Error fetching staff {staff_id}",
158
+ extra={
159
+ "operation": "get_staff_by_id",
160
+ "staff_id": staff_id,
161
+ "error": str(e)
162
+ },
163
+ exc_info=True
164
+ )
165
  raise HTTPException(
166
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
167
  detail="Error retrieving staff"
 
215
  )
216
 
217
  if result.modified_count == 0:
218
+ logger.warning(
219
+ f"No changes made to staff {staff_id}",
220
+ extra={
221
+ "operation": "update_staff",
222
+ "staff_id": staff_id
223
+ }
224
+ )
225
 
226
+ logger.info(
227
+ f"Updated staff {staff_id}",
228
+ extra={
229
+ "operation": "update_staff",
230
+ "staff_id": staff_id,
231
+ "merchant_id": str(updated_staff.merchant_id) if updated_staff else None
232
+ }
233
+ )
234
 
235
  return updated_staff
236
 
237
  except Exception as e:
238
+ logger.error(
239
+ f"Error updating staff {staff_id}",
240
+ extra={
241
+ "operation": "update_staff",
242
+ "staff_id": staff_id,
243
+ "error": str(e)
244
+ },
245
+ exc_info=True
246
+ )
247
  raise HTTPException(
248
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
249
  detail=f"Error updating staff: {str(e)}"
 
282
  }
283
  )
284
 
285
+ logger.info(
286
+ f"Deleted staff {staff_id}",
287
+ extra={
288
+ "operation": "delete_staff",
289
+ "staff_id": staff_id
290
+ }
291
+ )
292
  return {"message": f"Staff {staff_id} deleted successfully"}
293
 
294
  except Exception as e:
295
+ logger.error(
296
+ f"Error deleting staff {staff_id}",
297
+ extra={
298
+ "operation": "delete_staff",
299
+ "staff_id": staff_id,
300
+ "error": str(e)
301
+ },
302
+ exc_info=True
303
+ )
304
  raise HTTPException(
305
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
306
  detail="Error deleting staff"
 
320
  except HTTPException:
321
  raise
322
  except Exception as e:
323
+ logger.error(
324
+ f"Error fetching employee {user_id}",
325
+ extra={
326
+ "operation": "get_employee",
327
+ "user_id": user_id,
328
+ "error": str(e)
329
+ },
330
+ exc_info=True
331
+ )
332
  raise HTTPException(
333
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
334
  detail="Error retrieving employee"
 
350
  except HTTPException:
351
  raise
352
  except Exception as e:
353
+ logger.error(
354
+ f"Error fetching employee by code {employee_code}",
355
+ extra={
356
+ "operation": "get_employee_by_code",
357
+ "employee_code": employee_code,
358
+ "error": str(e)
359
+ },
360
+ exc_info=True
361
+ )
362
  raise HTTPException(
363
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
364
  detail="Error retrieving employee"
 
501
  return [EmployeeResponse(**staff) for staff in staff_list], total
502
 
503
  except Exception as e:
504
+ logger.error(
505
+ "Error listing staff",
506
+ extra={
507
+ "operation": "list_staff",
508
+ "error": str(e)
509
+ },
510
+ exc_info=True
511
+ )
512
  raise HTTPException(
513
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
514
  detail="Error listing staff"
app/sync/catalogue_services/sync_service.py CHANGED
@@ -5,12 +5,12 @@ Handles CRUD operations to keep PostgreSQL catalogue_service_ref table in sync w
5
  from sqlalchemy.ext.asyncio import AsyncSession
6
  from sqlalchemy import select, delete, text
7
  from typing import Optional, Dict, Any, List
8
- import logging
9
  from datetime import datetime
 
10
 
11
  from app.sync.models import CatalogueServiceRef
12
 
13
- logger = logging.getLogger(__name__)
14
 
15
 
16
  class CatalogueServiceSyncService:
@@ -66,10 +66,13 @@ class CatalogueServiceSyncService:
66
  """
67
  await self.pg_session.execute(text(create_table_sql))
68
  await self.pg_session.commit()
69
- logger.info("✅ Created catalogue_service_ref table in trans schema")
70
 
71
  except Exception as e:
72
- logger.warning(f"Schema/table creation failed (may already exist): {e}")
 
 
 
73
 
74
  def _extract_sync_data(self, service_data: Dict[str, Any]) -> Dict[str, Any]:
75
  """Extract relevant data for PostgreSQL sync"""
@@ -116,12 +119,27 @@ class CatalogueServiceSyncService:
116
  await self.pg_session.commit()
117
  await self.pg_session.refresh(service_ref)
118
 
119
- logger.info(f"Created catalogue service reference: {service_data.get('_id') or service_data.get('service_id')}")
 
 
 
 
 
 
 
120
  return service_ref
121
 
122
  except Exception as e:
123
  await self.pg_session.rollback()
124
- logger.error(f"Error creating catalogue service reference: {e}")
 
 
 
 
 
 
 
 
125
  raise
126
 
127
  async def update_catalogue_service_ref(self, service_id: str, service_data: Dict[str, Any]) -> Optional[CatalogueServiceRef]:
@@ -135,7 +153,10 @@ class CatalogueServiceSyncService:
135
  service_ref = result.scalar_one_or_none()
136
 
137
  if not service_ref:
138
- logger.warning(f"Catalogue service reference not found for update: {service_id}")
 
 
 
139
  return None
140
 
141
  # Update with new data
@@ -149,12 +170,19 @@ class CatalogueServiceSyncService:
149
  await self.pg_session.commit()
150
  await self.pg_session.refresh(service_ref)
151
 
152
- logger.info(f"Updated catalogue service reference: {service_id}")
 
 
 
153
  return service_ref
154
 
155
  except Exception as e:
156
  await self.pg_session.rollback()
157
- logger.error(f"Error updating catalogue service reference: {e}")
 
 
 
 
158
  raise
159
 
160
  async def delete_catalogue_service_ref(self, service_id: str) -> bool:
@@ -168,15 +196,25 @@ class CatalogueServiceSyncService:
168
  await self.pg_session.commit()
169
 
170
  if deleted_count > 0:
171
- logger.info(f"Deleted catalogue service reference: {service_id}")
 
 
 
172
  return True
173
  else:
174
- logger.warning(f"Catalogue service reference not found for deletion: {service_id}")
 
 
 
175
  return False
176
 
177
  except Exception as e:
178
  await self.pg_session.rollback()
179
- logger.error(f"Error deleting catalogue service reference: {e}")
 
 
 
 
180
  raise
181
 
182
  async def get_catalogue_service_ref(self, service_id: str) -> Optional[CatalogueServiceRef]:
@@ -188,7 +226,11 @@ class CatalogueServiceSyncService:
188
  return result.scalar_one_or_none()
189
 
190
  except Exception as e:
191
- logger.error(f"Error getting catalogue service reference: {e}")
 
 
 
 
192
  raise
193
 
194
  async def sync_catalogue_service_status(self, service_id: str, status: str) -> bool:
@@ -203,15 +245,25 @@ class CatalogueServiceSyncService:
203
  service_ref.status = status
204
  service_ref.updated_at = datetime.utcnow()
205
  await self.pg_session.commit()
206
- logger.info(f"Updated catalogue service status: {service_id} -> {status}")
 
 
 
207
  return True
208
  else:
209
- logger.warning(f"Catalogue service reference not found for status update: {service_id}")
 
 
 
210
  return False
211
 
212
  except Exception as e:
213
  await self.pg_session.rollback()
214
- logger.error(f"Error updating catalogue service status: {e}")
 
 
 
 
215
  raise
216
 
217
  async def list_catalogue_services_by_merchant(self, merchant_id: str, limit: int = 100, offset: int = 0) -> List[CatalogueServiceRef]:
@@ -227,7 +279,11 @@ class CatalogueServiceSyncService:
227
  return result.scalars().all()
228
 
229
  except Exception as e:
230
- logger.error(f"Error listing catalogue services by merchant: {e}")
 
 
 
 
231
  raise
232
 
233
  async def search_catalogue_services(self, merchant_id: str, search_term: str, limit: int = 50) -> List[CatalogueServiceRef]:
@@ -249,7 +305,11 @@ class CatalogueServiceSyncService:
249
  return result.scalars().all()
250
 
251
  except Exception as e:
252
- logger.error(f"Error searching catalogue services: {e}")
 
 
 
 
253
  raise
254
 
255
  async def list_catalogue_services_by_category(self, merchant_id: str, category_id: str) -> List[CatalogueServiceRef]:
@@ -266,7 +326,11 @@ class CatalogueServiceSyncService:
266
  return result.scalars().all()
267
 
268
  except Exception as e:
269
- logger.error(f"Error listing catalogue services by category: {e}")
 
 
 
 
270
  raise
271
 
272
  async def list_catalogue_services_by_status(self, merchant_id: str, status: str) -> List[CatalogueServiceRef]:
@@ -283,7 +347,11 @@ class CatalogueServiceSyncService:
283
  return result.scalars().all()
284
 
285
  except Exception as e:
286
- logger.error(f"Error listing catalogue services by status: {e}")
 
 
 
 
287
  raise
288
 
289
  async def get_service_pricing_summary(self, merchant_id: str) -> Dict[str, Any]:
@@ -314,5 +382,9 @@ class CatalogueServiceSyncService:
314
  }
315
 
316
  except Exception as e:
317
- logger.error(f"Error getting service pricing summary: {e}")
 
 
 
 
318
  raise
 
5
  from sqlalchemy.ext.asyncio import AsyncSession
6
  from sqlalchemy import select, delete, text
7
  from typing import Optional, Dict, Any, List
 
8
  from datetime import datetime
9
+ from app.core.logging import get_logger
10
 
11
  from app.sync.models import CatalogueServiceRef
12
 
13
+ logger = get_logger(__name__)
14
 
15
 
16
  class CatalogueServiceSyncService:
 
66
  """
67
  await self.pg_session.execute(text(create_table_sql))
68
  await self.pg_session.commit()
69
+ logger.info("✅ Created catalogue_service_ref table in trans schema", extra={"operation": "ensure_schema"})
70
 
71
  except Exception as e:
72
+ logger.warning(
73
+ f"Schema/table creation failed (may already exist): {e}",
74
+ extra={"operation": "ensure_schema", "error": str(e)}
75
+ )
76
 
77
  def _extract_sync_data(self, service_data: Dict[str, Any]) -> Dict[str, Any]:
78
  """Extract relevant data for PostgreSQL sync"""
 
119
  await self.pg_session.commit()
120
  await self.pg_session.refresh(service_ref)
121
 
122
+ logger.info(
123
+ f"Created catalogue service reference: {service_data.get('_id') or service_data.get('service_id')}",
124
+ extra={
125
+ "operation": "create_catalogue_service_ref",
126
+ "service_id": service_data.get('_id') or service_data.get('service_id'),
127
+ "merchant_id": service_data.get('merchant_id')
128
+ }
129
+ )
130
  return service_ref
131
 
132
  except Exception as e:
133
  await self.pg_session.rollback()
134
+ logger.error(
135
+ f"Error creating catalogue service reference: {e}",
136
+ extra={
137
+ "operation": "create_catalogue_service_ref",
138
+ "service_id": service_data.get('_id') or service_data.get('service_id'),
139
+ "error": str(e)
140
+ },
141
+ exc_info=True
142
+ )
143
  raise
144
 
145
  async def update_catalogue_service_ref(self, service_id: str, service_data: Dict[str, Any]) -> Optional[CatalogueServiceRef]:
 
153
  service_ref = result.scalar_one_or_none()
154
 
155
  if not service_ref:
156
+ logger.warning(
157
+ f"Catalogue service reference not found for update: {service_id}",
158
+ extra={"operation": "update_catalogue_service_ref", "service_id": service_id}
159
+ )
160
  return None
161
 
162
  # Update with new data
 
170
  await self.pg_session.commit()
171
  await self.pg_session.refresh(service_ref)
172
 
173
+ logger.info(
174
+ f"Updated catalogue service reference: {service_id}",
175
+ extra={"operation": "update_catalogue_service_ref", "service_id": service_id, "merchant_id": sync_data.get('merchant_id')}
176
+ )
177
  return service_ref
178
 
179
  except Exception as e:
180
  await self.pg_session.rollback()
181
+ logger.error(
182
+ f"Error updating catalogue service reference: {e}",
183
+ extra={"operation": "update_catalogue_service_ref", "service_id": service_id, "error": str(e)},
184
+ exc_info=True
185
+ )
186
  raise
187
 
188
  async def delete_catalogue_service_ref(self, service_id: str) -> bool:
 
196
  await self.pg_session.commit()
197
 
198
  if deleted_count > 0:
199
+ logger.info(
200
+ f"Deleted catalogue service reference: {service_id}",
201
+ extra={"operation": "delete_catalogue_service_ref", "service_id": service_id}
202
+ )
203
  return True
204
  else:
205
+ logger.warning(
206
+ f"Catalogue service reference not found for deletion: {service_id}",
207
+ extra={"operation": "delete_catalogue_service_ref", "service_id": service_id}
208
+ )
209
  return False
210
 
211
  except Exception as e:
212
  await self.pg_session.rollback()
213
+ logger.error(
214
+ f"Error deleting catalogue service reference: {e}",
215
+ extra={"operation": "delete_catalogue_service_ref", "service_id": service_id, "error": str(e)},
216
+ exc_info=True
217
+ )
218
  raise
219
 
220
  async def get_catalogue_service_ref(self, service_id: str) -> Optional[CatalogueServiceRef]:
 
226
  return result.scalar_one_or_none()
227
 
228
  except Exception as e:
229
+ logger.error(
230
+ f"Error getting catalogue service reference: {e}",
231
+ extra={"operation": "get_catalogue_service_ref", "service_id": service_id, "error": str(e)},
232
+ exc_info=True
233
+ )
234
  raise
235
 
236
  async def sync_catalogue_service_status(self, service_id: str, status: str) -> bool:
 
245
  service_ref.status = status
246
  service_ref.updated_at = datetime.utcnow()
247
  await self.pg_session.commit()
248
+ logger.info(
249
+ f"Updated catalogue service status: {service_id} -> {status}",
250
+ extra={"operation": "sync_catalogue_service_status", "service_id": service_id, "status": status}
251
+ )
252
  return True
253
  else:
254
+ logger.warning(
255
+ f"Catalogue service reference not found for status update: {service_id}",
256
+ extra={"operation": "sync_catalogue_service_status", "service_id": service_id}
257
+ )
258
  return False
259
 
260
  except Exception as e:
261
  await self.pg_session.rollback()
262
+ logger.error(
263
+ f"Error updating catalogue service status: {e}",
264
+ extra={"operation": "sync_catalogue_service_status", "service_id": service_id, "status": status, "error": str(e)},
265
+ exc_info=True
266
+ )
267
  raise
268
 
269
  async def list_catalogue_services_by_merchant(self, merchant_id: str, limit: int = 100, offset: int = 0) -> List[CatalogueServiceRef]:
 
279
  return result.scalars().all()
280
 
281
  except Exception as e:
282
+ logger.error(
283
+ f"Error listing catalogue services by merchant: {e}",
284
+ extra={"operation": "list_catalogue_services_by_merchant", "merchant_id": merchant_id, "error": str(e)},
285
+ exc_info=True
286
+ )
287
  raise
288
 
289
  async def search_catalogue_services(self, merchant_id: str, search_term: str, limit: int = 50) -> List[CatalogueServiceRef]:
 
305
  return result.scalars().all()
306
 
307
  except Exception as e:
308
+ logger.error(
309
+ f"Error searching catalogue services: {e}",
310
+ extra={"operation": "search_catalogue_services", "merchant_id": merchant_id, "search_term": search_term, "error": str(e)},
311
+ exc_info=True
312
+ )
313
  raise
314
 
315
  async def list_catalogue_services_by_category(self, merchant_id: str, category_id: str) -> List[CatalogueServiceRef]:
 
326
  return result.scalars().all()
327
 
328
  except Exception as e:
329
+ logger.error(
330
+ f"Error listing catalogue services by category: {e}",
331
+ extra={"operation": "list_catalogue_services_by_category", "merchant_id": merchant_id, "category_id": category_id, "error": str(e)},
332
+ exc_info=True
333
+ )
334
  raise
335
 
336
  async def list_catalogue_services_by_status(self, merchant_id: str, status: str) -> List[CatalogueServiceRef]:
 
347
  return result.scalars().all()
348
 
349
  except Exception as e:
350
+ logger.error(
351
+ f"Error listing catalogue services by status: {e}",
352
+ extra={"operation": "list_catalogue_services_by_status", "merchant_id": merchant_id, "status": status, "error": str(e)},
353
+ exc_info=True
354
+ )
355
  raise
356
 
357
  async def get_service_pricing_summary(self, merchant_id: str) -> Dict[str, Any]:
 
382
  }
383
 
384
  except Exception as e:
385
+ logger.error(
386
+ f"Error getting service pricing summary: {e}",
387
+ extra={"operation": "get_service_pricing_summary", "merchant_id": merchant_id, "error": str(e)},
388
+ exc_info=True
389
+ )
390
  raise
app/sync/customers/sync_service.py CHANGED
@@ -5,13 +5,13 @@ Handles CRUD operations to keep PostgreSQL customer_ref table in sync with Mongo
5
  from sqlalchemy.ext.asyncio import AsyncSession
6
  from sqlalchemy import select, delete, text
7
  from typing import Optional, Dict, Any
8
- import logging
9
  from datetime import datetime
 
10
 
11
  from app.sync.models import CustomerRef
12
  from app.customers.models.model import CustomerModel
13
 
14
- logger = logging.getLogger(__name__)
15
 
16
 
17
  class CustomerSyncService:
@@ -58,10 +58,13 @@ class CustomerSyncService:
58
  """
59
  await self.pg_session.execute(text(create_table_sql))
60
  await self.pg_session.commit()
61
- logger.info("✅ Created customer_ref table in trans schema")
62
 
63
  except Exception as e:
64
- logger.warning(f"Schema/table creation failed (may already exist): {e}")
 
 
 
65
  # Best-effort: if creation fails, proceed; operations may still succeed
66
 
67
  def _extract_sync_data(self, customer: CustomerModel) -> Dict[str, Any]:
@@ -89,12 +92,19 @@ class CustomerSyncService:
89
  await self.pg_session.commit()
90
  await self.pg_session.refresh(customer_ref)
91
 
92
- logger.info(f"Created customer reference: {customer.customer_id}")
 
 
 
93
  return customer_ref
94
 
95
  except Exception as e:
96
  await self.pg_session.rollback()
97
- logger.error(f"Error creating customer reference: {e}")
 
 
 
 
98
  raise
99
 
100
  async def update_customer_ref(self, customer_id: str, customer: CustomerModel) -> Optional[CustomerRef]:
@@ -108,7 +118,10 @@ class CustomerSyncService:
108
  customer_ref = result.scalar_one_or_none()
109
 
110
  if not customer_ref:
111
- logger.warning(f"Customer reference not found for update: {customer_id}")
 
 
 
112
  return None
113
 
114
  # Update with new data
@@ -121,12 +134,19 @@ class CustomerSyncService:
121
  await self.pg_session.commit()
122
  await self.pg_session.refresh(customer_ref)
123
 
124
- logger.info(f"Updated customer reference: {customer_id}")
 
 
 
125
  return customer_ref
126
 
127
  except Exception as e:
128
  await self.pg_session.rollback()
129
- logger.error(f"Error updating customer reference: {e}")
 
 
 
 
130
  raise
131
 
132
  async def delete_customer_ref(self, customer_id: str) -> bool:
@@ -140,15 +160,25 @@ class CustomerSyncService:
140
  await self.pg_session.commit()
141
 
142
  if deleted_count > 0:
143
- logger.info(f"Deleted customer reference: {customer_id}")
 
 
 
144
  return True
145
  else:
146
- logger.warning(f"Customer reference not found for deletion: {customer_id}")
 
 
 
147
  return False
148
 
149
  except Exception as e:
150
  await self.pg_session.rollback()
151
- logger.error(f"Error deleting customer reference: {e}")
 
 
 
 
152
  raise
153
 
154
  async def get_customer_ref(self, customer_id: str) -> Optional[CustomerRef]:
@@ -160,7 +190,11 @@ class CustomerSyncService:
160
  return result.scalar_one_or_none()
161
 
162
  except Exception as e:
163
- logger.error(f"Error getting customer reference: {e}")
 
 
 
 
164
  raise
165
 
166
  async def sync_customer_status(self, customer_id: str, status: str) -> bool:
@@ -175,15 +209,25 @@ class CustomerSyncService:
175
  customer_ref.status = status
176
  customer_ref.updated_at = datetime.utcnow()
177
  await self.pg_session.commit()
178
- logger.info(f"Updated customer status: {customer_id} -> {status}")
 
 
 
179
  return True
180
  else:
181
- logger.warning(f"Customer reference not found for status update: {customer_id}")
 
 
 
182
  return False
183
 
184
  except Exception as e:
185
  await self.pg_session.rollback()
186
- logger.error(f"Error updating customer status: {e}")
 
 
 
 
187
  raise
188
 
189
  async def list_customers_by_merchant(self, merchant_id: str, limit: int = 100, offset: int = 0) -> list:
@@ -199,7 +243,11 @@ class CustomerSyncService:
199
  return result.scalars().all()
200
 
201
  except Exception as e:
202
- logger.error(f"Error listing customers by merchant: {e}")
 
 
 
 
203
  raise
204
 
205
  async def search_customers(self, merchant_id: str, search_term: str, limit: int = 50) -> list:
@@ -222,5 +270,9 @@ class CustomerSyncService:
222
  return result.scalars().all()
223
 
224
  except Exception as e:
225
- logger.error(f"Error searching customers: {e}")
 
 
 
 
226
  raise
 
5
  from sqlalchemy.ext.asyncio import AsyncSession
6
  from sqlalchemy import select, delete, text
7
  from typing import Optional, Dict, Any
 
8
  from datetime import datetime
9
+ from app.core.logging import get_logger
10
 
11
  from app.sync.models import CustomerRef
12
  from app.customers.models.model import CustomerModel
13
 
14
+ logger = get_logger(__name__)
15
 
16
 
17
  class CustomerSyncService:
 
58
  """
59
  await self.pg_session.execute(text(create_table_sql))
60
  await self.pg_session.commit()
61
+ logger.info("✅ Created customer_ref table in trans schema", extra={"operation": "ensure_schema"})
62
 
63
  except Exception as e:
64
+ logger.warning(
65
+ f"Schema/table creation failed (may already exist): {e}",
66
+ extra={"operation": "ensure_schema", "error": str(e)}
67
+ )
68
  # Best-effort: if creation fails, proceed; operations may still succeed
69
 
70
  def _extract_sync_data(self, customer: CustomerModel) -> Dict[str, Any]:
 
92
  await self.pg_session.commit()
93
  await self.pg_session.refresh(customer_ref)
94
 
95
+ logger.info(
96
+ f"Created customer reference: {customer.customer_id}",
97
+ extra={"operation": "create_customer_ref", "customer_id": customer.customer_id, "merchant_id": customer.merchant_id}
98
+ )
99
  return customer_ref
100
 
101
  except Exception as e:
102
  await self.pg_session.rollback()
103
+ logger.error(
104
+ f"Error creating customer reference: {e}",
105
+ extra={"operation": "create_customer_ref", "customer_id": customer.customer_id, "error": str(e)},
106
+ exc_info=True
107
+ )
108
  raise
109
 
110
  async def update_customer_ref(self, customer_id: str, customer: CustomerModel) -> Optional[CustomerRef]:
 
118
  customer_ref = result.scalar_one_or_none()
119
 
120
  if not customer_ref:
121
+ logger.warning(
122
+ f"Customer reference not found for update: {customer_id}",
123
+ extra={"operation": "update_customer_ref", "customer_id": customer_id}
124
+ )
125
  return None
126
 
127
  # Update with new data
 
134
  await self.pg_session.commit()
135
  await self.pg_session.refresh(customer_ref)
136
 
137
+ logger.info(
138
+ f"Updated customer reference: {customer_id}",
139
+ extra={"operation": "update_customer_ref", "customer_id": customer_id, "merchant_id": customer.merchant_id}
140
+ )
141
  return customer_ref
142
 
143
  except Exception as e:
144
  await self.pg_session.rollback()
145
+ logger.error(
146
+ f"Error updating customer reference: {e}",
147
+ extra={"operation": "update_customer_ref", "customer_id": customer_id, "error": str(e)},
148
+ exc_info=True
149
+ )
150
  raise
151
 
152
  async def delete_customer_ref(self, customer_id: str) -> bool:
 
160
  await self.pg_session.commit()
161
 
162
  if deleted_count > 0:
163
+ logger.info(
164
+ f"Deleted customer reference: {customer_id}",
165
+ extra={"operation": "delete_customer_ref", "customer_id": customer_id}
166
+ )
167
  return True
168
  else:
169
+ logger.warning(
170
+ f"Customer reference not found for deletion: {customer_id}",
171
+ extra={"operation": "delete_customer_ref", "customer_id": customer_id}
172
+ )
173
  return False
174
 
175
  except Exception as e:
176
  await self.pg_session.rollback()
177
+ logger.error(
178
+ f"Error deleting customer reference: {e}",
179
+ extra={"operation": "delete_customer_ref", "customer_id": customer_id, "error": str(e)},
180
+ exc_info=True
181
+ )
182
  raise
183
 
184
  async def get_customer_ref(self, customer_id: str) -> Optional[CustomerRef]:
 
190
  return result.scalar_one_or_none()
191
 
192
  except Exception as e:
193
+ logger.error(
194
+ f"Error getting customer reference: {e}",
195
+ extra={"operation": "get_customer_ref", "customer_id": customer_id, "error": str(e)},
196
+ exc_info=True
197
+ )
198
  raise
199
 
200
  async def sync_customer_status(self, customer_id: str, status: str) -> bool:
 
209
  customer_ref.status = status
210
  customer_ref.updated_at = datetime.utcnow()
211
  await self.pg_session.commit()
212
+ logger.info(
213
+ f"Updated customer status: {customer_id} -> {status}",
214
+ extra={"operation": "sync_customer_status", "customer_id": customer_id, "status": status}
215
+ )
216
  return True
217
  else:
218
+ logger.warning(
219
+ f"Customer reference not found for status update: {customer_id}",
220
+ extra={"operation": "sync_customer_status", "customer_id": customer_id}
221
+ )
222
  return False
223
 
224
  except Exception as e:
225
  await self.pg_session.rollback()
226
+ logger.error(
227
+ f"Error updating customer status: {e}",
228
+ extra={"operation": "sync_customer_status", "customer_id": customer_id, "status": status, "error": str(e)},
229
+ exc_info=True
230
+ )
231
  raise
232
 
233
  async def list_customers_by_merchant(self, merchant_id: str, limit: int = 100, offset: int = 0) -> list:
 
243
  return result.scalars().all()
244
 
245
  except Exception as e:
246
+ logger.error(
247
+ f"Error listing customers by merchant: {e}",
248
+ extra={"operation": "list_customers_by_merchant", "merchant_id": merchant_id, "error": str(e)},
249
+ exc_info=True
250
+ )
251
  raise
252
 
253
  async def search_customers(self, merchant_id: str, search_term: str, limit: int = 50) -> list:
 
270
  return result.scalars().all()
271
 
272
  except Exception as e:
273
+ logger.error(
274
+ f"Error searching customers: {e}",
275
+ extra={"operation": "search_customers", "merchant_id": merchant_id, "search_term": search_term, "error": str(e)},
276
+ exc_info=True
277
+ )
278
  raise
app/sync/staff/sync_service.py CHANGED
@@ -5,12 +5,12 @@ Handles CRUD operations to keep PostgreSQL staff_ref table in sync with MongoDB.
5
  from sqlalchemy.ext.asyncio import AsyncSession
6
  from sqlalchemy import select, delete, text
7
  from typing import Optional, Dict, Any, List
8
- import logging
9
  from datetime import datetime
 
10
 
11
  from app.sync.models import StaffRef
12
 
13
- logger = logging.getLogger(__name__)
14
 
15
 
16
  class StaffSyncService:
@@ -57,10 +57,13 @@ class StaffSyncService:
57
  """
58
  await self.pg_session.execute(text(create_table_sql))
59
  await self.pg_session.commit()
60
- logger.info("✅ Created pos_staff_ref table in trans schema")
61
 
62
  except Exception as e:
63
- logger.warning(f"Schema/table creation failed (may already exist): {e}")
 
 
 
64
 
65
  def _extract_sync_data(self, staff_data: Dict[str, Any]) -> Dict[str, Any]:
66
  """Extract relevant data for PostgreSQL sync"""
@@ -88,12 +91,19 @@ class StaffSyncService:
88
  await self.pg_session.commit()
89
  await self.pg_session.refresh(staff_ref)
90
 
91
- logger.info(f"Created staff reference: {staff_data.get('staff_id')}")
 
 
 
92
  return staff_ref
93
 
94
  except Exception as e:
95
  await self.pg_session.rollback()
96
- logger.error(f"Error creating staff reference: {e}")
 
 
 
 
97
  raise
98
 
99
  async def update_staff_ref(self, staff_id: str, staff_data: Dict[str, Any]) -> Optional[StaffRef]:
@@ -107,7 +117,10 @@ class StaffSyncService:
107
  staff_ref = result.scalar_one_or_none()
108
 
109
  if not staff_ref:
110
- logger.warning(f"Staff reference not found for update: {staff_id}")
 
 
 
111
  return None
112
 
113
  # Update with new data
@@ -121,12 +134,19 @@ class StaffSyncService:
121
  await self.pg_session.commit()
122
  await self.pg_session.refresh(staff_ref)
123
 
124
- logger.info(f"Updated staff reference: {staff_id}")
 
 
 
125
  return staff_ref
126
 
127
  except Exception as e:
128
  await self.pg_session.rollback()
129
- logger.error(f"Error updating staff reference: {e}")
 
 
 
 
130
  raise
131
 
132
  async def delete_staff_ref(self, staff_id: str) -> bool:
@@ -140,15 +160,25 @@ class StaffSyncService:
140
  await self.pg_session.commit()
141
 
142
  if deleted_count > 0:
143
- logger.info(f"Deleted staff reference: {staff_id}")
 
 
 
144
  return True
145
  else:
146
- logger.warning(f"Staff reference not found for deletion: {staff_id}")
 
 
 
147
  return False
148
 
149
  except Exception as e:
150
  await self.pg_session.rollback()
151
- logger.error(f"Error deleting staff reference: {e}")
 
 
 
 
152
  raise
153
 
154
  async def get_staff_ref(self, staff_id: str) -> Optional[StaffRef]:
@@ -160,7 +190,11 @@ class StaffSyncService:
160
  return result.scalar_one_or_none()
161
 
162
  except Exception as e:
163
- logger.error(f"Error getting staff reference: {e}")
 
 
 
 
164
  raise
165
 
166
  async def sync_staff_status(self, staff_id: str, status: str) -> bool:
@@ -175,15 +209,25 @@ class StaffSyncService:
175
  staff_ref.status = status
176
  staff_ref.updated_at = datetime.utcnow()
177
  await self.pg_session.commit()
178
- logger.info(f"Updated staff status: {staff_id} -> {status}")
 
 
 
179
  return True
180
  else:
181
- logger.warning(f"Staff reference not found for status update: {staff_id}")
 
 
 
182
  return False
183
 
184
  except Exception as e:
185
  await self.pg_session.rollback()
186
- logger.error(f"Error updating staff status: {e}")
 
 
 
 
187
  raise
188
 
189
  async def list_staff_by_merchant(self, merchant_id: str, limit: int = 100, offset: int = 0) -> List[StaffRef]:
@@ -199,7 +243,11 @@ class StaffSyncService:
199
  return result.scalars().all()
200
 
201
  except Exception as e:
202
- logger.error(f"Error listing staff by merchant: {e}")
 
 
 
 
203
  raise
204
 
205
  async def search_staff(self, merchant_id: str, search_term: str, limit: int = 50) -> List[StaffRef]:
@@ -222,7 +270,11 @@ class StaffSyncService:
222
  return result.scalars().all()
223
 
224
  except Exception as e:
225
- logger.error(f"Error searching staff: {e}")
 
 
 
 
226
  raise
227
 
228
  async def list_staff_by_role(self, merchant_id: str, role: str) -> List[StaffRef]:
@@ -239,7 +291,11 @@ class StaffSyncService:
239
  return result.scalars().all()
240
 
241
  except Exception as e:
242
- logger.error(f"Error listing staff by role: {e}")
 
 
 
 
243
  raise
244
 
245
  async def list_staff_by_specialization(self, merchant_id: str, specialization: str) -> List[StaffRef]:
@@ -256,5 +312,9 @@ class StaffSyncService:
256
  return result.scalars().all()
257
 
258
  except Exception as e:
259
- logger.error(f"Error listing staff by specialization: {e}")
 
 
 
 
260
  raise
 
5
  from sqlalchemy.ext.asyncio import AsyncSession
6
  from sqlalchemy import select, delete, text
7
  from typing import Optional, Dict, Any, List
 
8
  from datetime import datetime
9
+ from app.core.logging import get_logger
10
 
11
  from app.sync.models import StaffRef
12
 
13
+ logger = get_logger(__name__)
14
 
15
 
16
  class StaffSyncService:
 
57
  """
58
  await self.pg_session.execute(text(create_table_sql))
59
  await self.pg_session.commit()
60
+ logger.info("✅ Created pos_staff_ref table in trans schema", extra={"operation": "ensure_schema"})
61
 
62
  except Exception as e:
63
+ logger.warning(
64
+ f"Schema/table creation failed (may already exist): {e}",
65
+ extra={"operation": "ensure_schema", "error": str(e)}
66
+ )
67
 
68
  def _extract_sync_data(self, staff_data: Dict[str, Any]) -> Dict[str, Any]:
69
  """Extract relevant data for PostgreSQL sync"""
 
91
  await self.pg_session.commit()
92
  await self.pg_session.refresh(staff_ref)
93
 
94
+ logger.info(
95
+ f"Created staff reference: {staff_data.get('staff_id')}",
96
+ extra={"operation": "create_staff_ref", "staff_id": staff_data.get('staff_id'), "merchant_id": staff_data.get('merchant_id')}
97
+ )
98
  return staff_ref
99
 
100
  except Exception as e:
101
  await self.pg_session.rollback()
102
+ logger.error(
103
+ f"Error creating staff reference: {e}",
104
+ extra={"operation": "create_staff_ref", "staff_id": staff_data.get('staff_id'), "error": str(e)},
105
+ exc_info=True
106
+ )
107
  raise
108
 
109
  async def update_staff_ref(self, staff_id: str, staff_data: Dict[str, Any]) -> Optional[StaffRef]:
 
117
  staff_ref = result.scalar_one_or_none()
118
 
119
  if not staff_ref:
120
+ logger.warning(
121
+ f"Staff reference not found for update: {staff_id}",
122
+ extra={"operation": "update_staff_ref", "staff_id": staff_id}
123
+ )
124
  return None
125
 
126
  # Update with new data
 
134
  await self.pg_session.commit()
135
  await self.pg_session.refresh(staff_ref)
136
 
137
+ logger.info(
138
+ f"Updated staff reference: {staff_id}",
139
+ extra={"operation": "update_staff_ref", "staff_id": staff_id, "merchant_id": sync_data.get('merchant_id')}
140
+ )
141
  return staff_ref
142
 
143
  except Exception as e:
144
  await self.pg_session.rollback()
145
+ logger.error(
146
+ f"Error updating staff reference: {e}",
147
+ extra={"operation": "update_staff_ref", "staff_id": staff_id, "error": str(e)},
148
+ exc_info=True
149
+ )
150
  raise
151
 
152
  async def delete_staff_ref(self, staff_id: str) -> bool:
 
160
  await self.pg_session.commit()
161
 
162
  if deleted_count > 0:
163
+ logger.info(
164
+ f"Deleted staff reference: {staff_id}",
165
+ extra={"operation": "delete_staff_ref", "staff_id": staff_id}
166
+ )
167
  return True
168
  else:
169
+ logger.warning(
170
+ f"Staff reference not found for deletion: {staff_id}",
171
+ extra={"operation": "delete_staff_ref", "staff_id": staff_id}
172
+ )
173
  return False
174
 
175
  except Exception as e:
176
  await self.pg_session.rollback()
177
+ logger.error(
178
+ f"Error deleting staff reference: {e}",
179
+ extra={"operation": "delete_staff_ref", "staff_id": staff_id, "error": str(e)},
180
+ exc_info=True
181
+ )
182
  raise
183
 
184
  async def get_staff_ref(self, staff_id: str) -> Optional[StaffRef]:
 
190
  return result.scalar_one_or_none()
191
 
192
  except Exception as e:
193
+ logger.error(
194
+ f"Error getting staff reference: {e}",
195
+ extra={"operation": "get_staff_ref", "staff_id": staff_id, "error": str(e)},
196
+ exc_info=True
197
+ )
198
  raise
199
 
200
  async def sync_staff_status(self, staff_id: str, status: str) -> bool:
 
209
  staff_ref.status = status
210
  staff_ref.updated_at = datetime.utcnow()
211
  await self.pg_session.commit()
212
+ logger.info(
213
+ f"Updated staff status: {staff_id} -> {status}",
214
+ extra={"operation": "sync_staff_status", "staff_id": staff_id, "status": status}
215
+ )
216
  return True
217
  else:
218
+ logger.warning(
219
+ f"Staff reference not found for status update: {staff_id}",
220
+ extra={"operation": "sync_staff_status", "staff_id": staff_id}
221
+ )
222
  return False
223
 
224
  except Exception as e:
225
  await self.pg_session.rollback()
226
+ logger.error(
227
+ f"Error updating staff status: {e}",
228
+ extra={"operation": "sync_staff_status", "staff_id": staff_id, "status": status, "error": str(e)},
229
+ exc_info=True
230
+ )
231
  raise
232
 
233
  async def list_staff_by_merchant(self, merchant_id: str, limit: int = 100, offset: int = 0) -> List[StaffRef]:
 
243
  return result.scalars().all()
244
 
245
  except Exception as e:
246
+ logger.error(
247
+ f"Error listing staff by merchant: {e}",
248
+ extra={"operation": "list_staff_by_merchant", "merchant_id": merchant_id, "error": str(e)},
249
+ exc_info=True
250
+ )
251
  raise
252
 
253
  async def search_staff(self, merchant_id: str, search_term: str, limit: int = 50) -> List[StaffRef]:
 
270
  return result.scalars().all()
271
 
272
  except Exception as e:
273
+ logger.error(
274
+ f"Error searching staff: {e}",
275
+ extra={"operation": "search_staff", "merchant_id": merchant_id, "search_term": search_term, "error": str(e)},
276
+ exc_info=True
277
+ )
278
  raise
279
 
280
  async def list_staff_by_role(self, merchant_id: str, role: str) -> List[StaffRef]:
 
291
  return result.scalars().all()
292
 
293
  except Exception as e:
294
+ logger.error(
295
+ f"Error listing staff by role: {e}",
296
+ extra={"operation": "list_staff_by_role", "merchant_id": merchant_id, "role": role, "error": str(e)},
297
+ exc_info=True
298
+ )
299
  raise
300
 
301
  async def list_staff_by_specialization(self, merchant_id: str, specialization: str) -> List[StaffRef]:
 
312
  return result.scalars().all()
313
 
314
  except Exception as e:
315
+ logger.error(
316
+ f"Error listing staff by specialization: {e}",
317
+ extra={"operation": "list_staff_by_specialization", "merchant_id": merchant_id, "specialization": specialization, "error": str(e)},
318
+ exc_info=True
319
+ )
320
  raise
app/sync/sync_service.py CHANGED
@@ -4,14 +4,14 @@ Coordinates all sync operations across different entities.
4
  """
5
  from sqlalchemy.ext.asyncio import AsyncSession
6
  from typing import Dict, Any, Optional
7
- import logging
8
-
9
  from app.sync.customers.sync_service import CustomerSyncService
10
  from app.sync.staff.sync_service import StaffSyncService
11
  from app.sync.catalogue_services.sync_service import CatalogueServiceSyncService
12
  from app.customers.models.model import CustomerModel
13
 
14
- logger = logging.getLogger(__name__)
15
 
16
 
17
  class POSSyncService:
@@ -29,7 +29,16 @@ class POSSyncService:
29
  try:
30
  return await self.customer_sync.create_customer_ref(customer)
31
  except Exception as e:
32
- logger.error(f"Error syncing customer creation: {e}")
 
 
 
 
 
 
 
 
 
33
  raise
34
 
35
  async def sync_customer_update(self, customer_id: str, customer: CustomerModel):
@@ -37,7 +46,15 @@ class POSSyncService:
37
  try:
38
  return await self.customer_sync.update_customer_ref(customer_id, customer)
39
  except Exception as e:
40
- logger.error(f"Error syncing customer update: {e}")
 
 
 
 
 
 
 
 
41
  raise
42
 
43
  async def sync_customer_delete(self, customer_id: str):
@@ -45,7 +62,15 @@ class POSSyncService:
45
  try:
46
  return await self.customer_sync.delete_customer_ref(customer_id)
47
  except Exception as e:
48
- logger.error(f"Error syncing customer deletion: {e}")
 
 
 
 
 
 
 
 
49
  raise
50
 
51
  async def sync_customer_status(self, customer_id: str, status: str):
@@ -53,7 +78,16 @@ class POSSyncService:
53
  try:
54
  return await self.customer_sync.sync_customer_status(customer_id, status)
55
  except Exception as e:
56
- logger.error(f"Error syncing customer status: {e}")
 
 
 
 
 
 
 
 
 
57
  raise
58
 
59
  # Staff sync operations
@@ -62,7 +96,16 @@ class POSSyncService:
62
  try:
63
  return await self.staff_sync.create_staff_ref(staff_data)
64
  except Exception as e:
65
- logger.error(f"Error syncing staff creation: {e}")
 
 
 
 
 
 
 
 
 
66
  raise
67
 
68
  async def sync_staff_update(self, staff_id: str, staff_data: Dict[str, Any]):
@@ -70,7 +113,15 @@ class POSSyncService:
70
  try:
71
  return await self.staff_sync.update_staff_ref(staff_id, staff_data)
72
  except Exception as e:
73
- logger.error(f"Error syncing staff update: {e}")
 
 
 
 
 
 
 
 
74
  raise
75
 
76
  async def sync_staff_delete(self, staff_id: str):
@@ -78,7 +129,15 @@ class POSSyncService:
78
  try:
79
  return await self.staff_sync.delete_staff_ref(staff_id)
80
  except Exception as e:
81
- logger.error(f"Error syncing staff deletion: {e}")
 
 
 
 
 
 
 
 
82
  raise
83
 
84
  async def sync_staff_status(self, staff_id: str, status: str):
@@ -86,7 +145,16 @@ class POSSyncService:
86
  try:
87
  return await self.staff_sync.sync_staff_status(staff_id, status)
88
  except Exception as e:
89
- logger.error(f"Error syncing staff status: {e}")
 
 
 
 
 
 
 
 
 
90
  raise
91
 
92
  # Catalogue service sync operations
@@ -95,7 +163,16 @@ class POSSyncService:
95
  try:
96
  return await self.catalogue_service_sync.create_catalogue_service_ref(service_data)
97
  except Exception as e:
98
- logger.error(f"Error syncing catalogue service creation: {e}")
 
 
 
 
 
 
 
 
 
99
  raise
100
 
101
  async def sync_catalogue_service_update(self, service_id: str, service_data: Dict[str, Any]):
@@ -103,7 +180,15 @@ class POSSyncService:
103
  try:
104
  return await self.catalogue_service_sync.update_catalogue_service_ref(service_id, service_data)
105
  except Exception as e:
106
- logger.error(f"Error syncing catalogue service update: {e}")
 
 
 
 
 
 
 
 
107
  raise
108
 
109
  async def sync_catalogue_service_delete(self, service_id: str):
@@ -111,7 +196,15 @@ class POSSyncService:
111
  try:
112
  return await self.catalogue_service_sync.delete_catalogue_service_ref(service_id)
113
  except Exception as e:
114
- logger.error(f"Error syncing catalogue service deletion: {e}")
 
 
 
 
 
 
 
 
115
  raise
116
 
117
  async def sync_catalogue_service_status(self, service_id: str, status: str):
@@ -119,7 +212,16 @@ class POSSyncService:
119
  try:
120
  return await self.catalogue_service_sync.sync_catalogue_service_status(service_id, status)
121
  except Exception as e:
122
- logger.error(f"Error syncing catalogue service status: {e}")
 
 
 
 
 
 
 
 
 
123
  raise
124
 
125
  # Bulk sync operations
@@ -131,7 +233,16 @@ class POSSyncService:
131
  result = await self.customer_sync.create_customer_ref(customer)
132
  results.append({"customer_id": customer.customer_id, "status": "success", "result": result})
133
  except Exception as e:
134
- logger.error(f"Error bulk syncing customer {customer.customer_id}: {e}")
 
 
 
 
 
 
 
 
 
135
  results.append({"customer_id": customer.customer_id, "status": "error", "error": str(e)})
136
  return results
137
 
@@ -143,7 +254,16 @@ class POSSyncService:
143
  result = await self.staff_sync.create_staff_ref(staff_data)
144
  results.append({"staff_id": staff_data.get('staff_id'), "status": "success", "result": result})
145
  except Exception as e:
146
- logger.error(f"Error bulk syncing staff {staff_data.get('staff_id')}: {e}")
 
 
 
 
 
 
 
 
 
147
  results.append({"staff_id": staff_data.get('staff_id'), "status": "error", "error": str(e)})
148
  return results
149
 
@@ -155,7 +275,16 @@ class POSSyncService:
155
  result = await self.catalogue_service_sync.create_catalogue_service_ref(service_data)
156
  results.append({"service_id": service_data.get('_id') or service_data.get('service_id'), "status": "success", "result": result})
157
  except Exception as e:
158
- logger.error(f"Error bulk syncing catalogue service {service_data.get('_id') or service_data.get('service_id')}: {e}")
 
 
 
 
 
 
 
 
 
159
  results.append({"service_id": service_data.get('_id') or service_data.get('service_id'), "status": "error", "error": str(e)})
160
  return results
161
 
@@ -169,7 +298,11 @@ class POSSyncService:
169
  await self.customer_sync._ensure_customer_ref_schema()
170
  except Exception as e:
171
  customer_health = False
172
- logger.error(f"Customer sync health check failed: {e}")
 
 
 
 
173
 
174
  # Test staff sync
175
  staff_health = True
@@ -177,7 +310,11 @@ class POSSyncService:
177
  await self.staff_sync._ensure_staff_ref_schema()
178
  except Exception as e:
179
  staff_health = False
180
- logger.error(f"Staff sync health check failed: {e}")
 
 
 
 
181
 
182
  # Test catalogue service sync
183
  catalogue_service_health = True
@@ -185,18 +322,27 @@ class POSSyncService:
185
  await self.catalogue_service_sync._ensure_catalogue_service_ref_schema()
186
  except Exception as e:
187
  catalogue_service_health = False
188
- logger.error(f"Catalogue service sync health check failed: {e}")
 
 
 
 
189
 
 
190
  return {
191
  "overall_health": customer_health and staff_health and catalogue_service_health,
192
  "customer_sync": customer_health,
193
  "staff_sync": staff_health,
194
  "catalogue_service_sync": catalogue_service_health,
195
- "timestamp": logger.info("Sync health check completed")
196
  }
197
 
198
  except Exception as e:
199
- logger.error(f"Error checking sync health: {e}")
 
 
 
 
200
  return {
201
  "overall_health": False,
202
  "error": str(e)
 
4
  """
5
  from sqlalchemy.ext.asyncio import AsyncSession
6
  from typing import Dict, Any, Optional
7
+ from datetime import datetime
8
+ from app.core.logging import get_logger
9
  from app.sync.customers.sync_service import CustomerSyncService
10
  from app.sync.staff.sync_service import StaffSyncService
11
  from app.sync.catalogue_services.sync_service import CatalogueServiceSyncService
12
  from app.customers.models.model import CustomerModel
13
 
14
+ logger = get_logger(__name__)
15
 
16
 
17
  class POSSyncService:
 
29
  try:
30
  return await self.customer_sync.create_customer_ref(customer)
31
  except Exception as e:
32
+ logger.error(
33
+ f"Error syncing customer creation: {e}",
34
+ extra={
35
+ "operation": "sync_customer_create",
36
+ "customer_id": customer.customer_id,
37
+ "merchant_id": customer.merchant_id,
38
+ "error": str(e)
39
+ },
40
+ exc_info=True
41
+ )
42
  raise
43
 
44
  async def sync_customer_update(self, customer_id: str, customer: CustomerModel):
 
46
  try:
47
  return await self.customer_sync.update_customer_ref(customer_id, customer)
48
  except Exception as e:
49
+ logger.error(
50
+ f"Error syncing customer update: {e}",
51
+ extra={
52
+ "operation": "sync_customer_update",
53
+ "customer_id": customer_id,
54
+ "error": str(e)
55
+ },
56
+ exc_info=True
57
+ )
58
  raise
59
 
60
  async def sync_customer_delete(self, customer_id: str):
 
62
  try:
63
  return await self.customer_sync.delete_customer_ref(customer_id)
64
  except Exception as e:
65
+ logger.error(
66
+ f"Error syncing customer deletion: {e}",
67
+ extra={
68
+ "operation": "sync_customer_delete",
69
+ "customer_id": customer_id,
70
+ "error": str(e)
71
+ },
72
+ exc_info=True
73
+ )
74
  raise
75
 
76
  async def sync_customer_status(self, customer_id: str, status: str):
 
78
  try:
79
  return await self.customer_sync.sync_customer_status(customer_id, status)
80
  except Exception as e:
81
+ logger.error(
82
+ f"Error syncing customer status: {e}",
83
+ extra={
84
+ "operation": "sync_customer_status",
85
+ "customer_id": customer_id,
86
+ "status": status,
87
+ "error": str(e)
88
+ },
89
+ exc_info=True
90
+ )
91
  raise
92
 
93
  # Staff sync operations
 
96
  try:
97
  return await self.staff_sync.create_staff_ref(staff_data)
98
  except Exception as e:
99
+ logger.error(
100
+ f"Error syncing staff creation: {e}",
101
+ extra={
102
+ "operation": "sync_staff_create",
103
+ "staff_id": staff_data.get('staff_id'),
104
+ "merchant_id": staff_data.get('merchant_id'),
105
+ "error": str(e)
106
+ },
107
+ exc_info=True
108
+ )
109
  raise
110
 
111
  async def sync_staff_update(self, staff_id: str, staff_data: Dict[str, Any]):
 
113
  try:
114
  return await self.staff_sync.update_staff_ref(staff_id, staff_data)
115
  except Exception as e:
116
+ logger.error(
117
+ f"Error syncing staff update: {e}",
118
+ extra={
119
+ "operation": "sync_staff_update",
120
+ "staff_id": staff_id,
121
+ "error": str(e)
122
+ },
123
+ exc_info=True
124
+ )
125
  raise
126
 
127
  async def sync_staff_delete(self, staff_id: str):
 
129
  try:
130
  return await self.staff_sync.delete_staff_ref(staff_id)
131
  except Exception as e:
132
+ logger.error(
133
+ f"Error syncing staff deletion: {e}",
134
+ extra={
135
+ "operation": "sync_staff_delete",
136
+ "staff_id": staff_id,
137
+ "error": str(e)
138
+ },
139
+ exc_info=True
140
+ )
141
  raise
142
 
143
  async def sync_staff_status(self, staff_id: str, status: str):
 
145
  try:
146
  return await self.staff_sync.sync_staff_status(staff_id, status)
147
  except Exception as e:
148
+ logger.error(
149
+ f"Error syncing staff status: {e}",
150
+ extra={
151
+ "operation": "sync_staff_status",
152
+ "staff_id": staff_id,
153
+ "status": status,
154
+ "error": str(e)
155
+ },
156
+ exc_info=True
157
+ )
158
  raise
159
 
160
  # Catalogue service sync operations
 
163
  try:
164
  return await self.catalogue_service_sync.create_catalogue_service_ref(service_data)
165
  except Exception as e:
166
+ logger.error(
167
+ f"Error syncing catalogue service creation: {e}",
168
+ extra={
169
+ "operation": "sync_catalogue_service_create",
170
+ "service_id": service_data.get('_id') or service_data.get('service_id'),
171
+ "merchant_id": service_data.get('merchant_id'),
172
+ "error": str(e)
173
+ },
174
+ exc_info=True
175
+ )
176
  raise
177
 
178
  async def sync_catalogue_service_update(self, service_id: str, service_data: Dict[str, Any]):
 
180
  try:
181
  return await self.catalogue_service_sync.update_catalogue_service_ref(service_id, service_data)
182
  except Exception as e:
183
+ logger.error(
184
+ f"Error syncing catalogue service update: {e}",
185
+ extra={
186
+ "operation": "sync_catalogue_service_update",
187
+ "service_id": service_id,
188
+ "error": str(e)
189
+ },
190
+ exc_info=True
191
+ )
192
  raise
193
 
194
  async def sync_catalogue_service_delete(self, service_id: str):
 
196
  try:
197
  return await self.catalogue_service_sync.delete_catalogue_service_ref(service_id)
198
  except Exception as e:
199
+ logger.error(
200
+ f"Error syncing catalogue service deletion: {e}",
201
+ extra={
202
+ "operation": "sync_catalogue_service_delete",
203
+ "service_id": service_id,
204
+ "error": str(e)
205
+ },
206
+ exc_info=True
207
+ )
208
  raise
209
 
210
  async def sync_catalogue_service_status(self, service_id: str, status: str):
 
212
  try:
213
  return await self.catalogue_service_sync.sync_catalogue_service_status(service_id, status)
214
  except Exception as e:
215
+ logger.error(
216
+ f"Error syncing catalogue service status: {e}",
217
+ extra={
218
+ "operation": "sync_catalogue_service_status",
219
+ "service_id": service_id,
220
+ "status": status,
221
+ "error": str(e)
222
+ },
223
+ exc_info=True
224
+ )
225
  raise
226
 
227
  # Bulk sync operations
 
233
  result = await self.customer_sync.create_customer_ref(customer)
234
  results.append({"customer_id": customer.customer_id, "status": "success", "result": result})
235
  except Exception as e:
236
+ logger.error(
237
+ f"Error bulk syncing customer {customer.customer_id}: {e}",
238
+ extra={
239
+ "operation": "bulk_sync_customers",
240
+ "customer_id": customer.customer_id,
241
+ "merchant_id": customer.merchant_id,
242
+ "error": str(e)
243
+ },
244
+ exc_info=True
245
+ )
246
  results.append({"customer_id": customer.customer_id, "status": "error", "error": str(e)})
247
  return results
248
 
 
254
  result = await self.staff_sync.create_staff_ref(staff_data)
255
  results.append({"staff_id": staff_data.get('staff_id'), "status": "success", "result": result})
256
  except Exception as e:
257
+ logger.error(
258
+ f"Error bulk syncing staff {staff_data.get('staff_id')}: {e}",
259
+ extra={
260
+ "operation": "bulk_sync_staff",
261
+ "staff_id": staff_data.get('staff_id'),
262
+ "merchant_id": staff_data.get('merchant_id'),
263
+ "error": str(e)
264
+ },
265
+ exc_info=True
266
+ )
267
  results.append({"staff_id": staff_data.get('staff_id'), "status": "error", "error": str(e)})
268
  return results
269
 
 
275
  result = await self.catalogue_service_sync.create_catalogue_service_ref(service_data)
276
  results.append({"service_id": service_data.get('_id') or service_data.get('service_id'), "status": "success", "result": result})
277
  except Exception as e:
278
+ logger.error(
279
+ f"Error bulk syncing catalogue service {service_data.get('_id') or service_data.get('service_id')}: {e}",
280
+ extra={
281
+ "operation": "bulk_sync_catalogue_services",
282
+ "service_id": service_data.get('_id') or service_data.get('service_id'),
283
+ "merchant_id": service_data.get('merchant_id'),
284
+ "error": str(e)
285
+ },
286
+ exc_info=True
287
+ )
288
  results.append({"service_id": service_data.get('_id') or service_data.get('service_id'), "status": "error", "error": str(e)})
289
  return results
290
 
 
298
  await self.customer_sync._ensure_customer_ref_schema()
299
  except Exception as e:
300
  customer_health = False
301
+ logger.error(
302
+ f"Customer sync health check failed: {e}",
303
+ extra={"operation": "check_sync_health", "component": "customer_sync", "error": str(e)},
304
+ exc_info=True
305
+ )
306
 
307
  # Test staff sync
308
  staff_health = True
 
310
  await self.staff_sync._ensure_staff_ref_schema()
311
  except Exception as e:
312
  staff_health = False
313
+ logger.error(
314
+ f"Staff sync health check failed: {e}",
315
+ extra={"operation": "check_sync_health", "component": "staff_sync", "error": str(e)},
316
+ exc_info=True
317
+ )
318
 
319
  # Test catalogue service sync
320
  catalogue_service_health = True
 
322
  await self.catalogue_service_sync._ensure_catalogue_service_ref_schema()
323
  except Exception as e:
324
  catalogue_service_health = False
325
+ logger.error(
326
+ f"Catalogue service sync health check failed: {e}",
327
+ extra={"operation": "check_sync_health", "component": "catalogue_service_sync", "error": str(e)},
328
+ exc_info=True
329
+ )
330
 
331
+ logger.info("Sync health check completed", extra={"operation": "check_sync_health"})
332
  return {
333
  "overall_health": customer_health and staff_health and catalogue_service_health,
334
  "customer_sync": customer_health,
335
  "staff_sync": staff_health,
336
  "catalogue_service_sync": catalogue_service_health,
337
+ "timestamp": datetime.utcnow()
338
  }
339
 
340
  except Exception as e:
341
+ logger.error(
342
+ f"Error checking sync health: {e}",
343
+ extra={"operation": "check_sync_health", "error": str(e)},
344
+ exc_info=True
345
+ )
346
  return {
347
  "overall_health": False,
348
  "error": str(e)
logger_expection.md ADDED
@@ -0,0 +1,399 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logger & Error Handling Implementation Summary
2
+
3
+ ## Quick Reference for New Modules
4
+
5
+ ### 1. Logger Setup (One line per module)
6
+ ```python
7
+ from app.core.logging import get_logger
8
+
9
+ logger = get_logger(__name__)
10
+ ```
11
+
12
+ ---
13
+
14
+ ## Error Handling Patterns
15
+
16
+ ### Pattern 1: Simple Error with Context
17
+ ```python
18
+ try:
19
+ result = await operation()
20
+ except SpecificException as e:
21
+ logger.error(
22
+ "Operation failed",
23
+ extra={
24
+ "operation": "operation_name",
25
+ "error": str(e),
26
+ "error_type": type(e).__name__
27
+ },
28
+ exc_info=True
29
+ )
30
+ raise HTTPException(
31
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
32
+ detail="Operation failed"
33
+ )
34
+ ```
35
+
36
+ ### Pattern 2: Validation Error with Details
37
+ ```python
38
+ if not email or "@" not in email:
39
+ logger.warning(
40
+ "Validation failed",
41
+ extra={
42
+ "field": "email",
43
+ "value_provided": bool(email),
44
+ "validation": "email_format",
45
+ "user_id": user_id
46
+ }
47
+ )
48
+ raise HTTPException(
49
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
50
+ detail="Invalid email format"
51
+ )
52
+ ```
53
+
54
+ ### Pattern 3: Authentication Error
55
+ ```python
56
+ if not token:
57
+ logger.warning(
58
+ "Authentication failed",
59
+ extra={
60
+ "reason": "missing_token",
61
+ "endpoint": request.url.path,
62
+ "client_ip": request.client.host if request.client else None
63
+ }
64
+ )
65
+ raise HTTPException(
66
+ status_code=status.HTTP_401_UNAUTHORIZED,
67
+ detail="Missing authentication token"
68
+ )
69
+ ```
70
+
71
+ ### Pattern 4: Permission Denied
72
+ ```python
73
+ if user.role not in required_roles:
74
+ logger.warning(
75
+ "Access denied",
76
+ extra={
77
+ "user_id": str(user.id),
78
+ "user_role": user.role,
79
+ "required_role": required_roles,
80
+ "resource": request.url.path
81
+ }
82
+ )
83
+ raise HTTPException(
84
+ status_code=status.HTTP_403_FORBIDDEN,
85
+ detail="Insufficient permissions"
86
+ )
87
+ ```
88
+
89
+ ### Pattern 5: Database Error
90
+ ```python
91
+ try:
92
+ result = await collection.insert_one(data)
93
+ except PyMongoError as e:
94
+ logger.error(
95
+ "Database operation failed",
96
+ extra={
97
+ "operation": "insert_one",
98
+ "collection": "collection_name",
99
+ "error": str(e),
100
+ "error_type": type(e).__name__
101
+ },
102
+ exc_info=True
103
+ )
104
+ raise HTTPException(
105
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
106
+ detail="Database operation failed"
107
+ )
108
+ ```
109
+
110
+ ### Pattern 6: Success with Context
111
+ ```python
112
+ logger.info(
113
+ "User login successful",
114
+ extra={
115
+ "user_id": user.id,
116
+ "username": user.username,
117
+ "method": "password",
118
+ "ip_address": request.client.host
119
+ }
120
+ )
121
+ ```
122
+
123
+ ---
124
+
125
+ ## Global Exception Handlers (in main.py)
126
+
127
+ ```python
128
+ from fastapi import FastAPI
129
+ from fastapi.exceptions import RequestValidationError
130
+ from pydantic import ValidationError
131
+ from jose import JWTError
132
+ from pymongo.errors import PyMongoError, ConnectionFailure, OperationFailure
133
+
134
+ # 1. Request Validation Errors
135
+ @app.exception_handler(RequestValidationError)
136
+ async def validation_exception_handler(request: Request, exc: RequestValidationError):
137
+ errors = [
138
+ {
139
+ "field": " -> ".join(str(loc) for loc in error["loc"]),
140
+ "message": error["msg"],
141
+ "type": error["type"]
142
+ }
143
+ for error in exc.errors()
144
+ ]
145
+
146
+ logger.warning(
147
+ "Validation error",
148
+ extra={
149
+ "path": request.url.path,
150
+ "method": request.method,
151
+ "error_count": len(errors),
152
+ "errors": errors
153
+ }
154
+ )
155
+
156
+ return JSONResponse(
157
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
158
+ content={
159
+ "success": False,
160
+ "error": "Validation Error",
161
+ "errors": errors
162
+ }
163
+ )
164
+
165
+ # 2. JWT Errors
166
+ @app.exception_handler(JWTError)
167
+ async def jwt_exception_handler(request: Request, exc: JWTError):
168
+ logger.warning(
169
+ "JWT authentication failed",
170
+ extra={
171
+ "path": request.url.path,
172
+ "error": str(exc),
173
+ "client_ip": request.client.host if request.client else None
174
+ }
175
+ )
176
+
177
+ return JSONResponse(
178
+ status_code=status.HTTP_401_UNAUTHORIZED,
179
+ content={
180
+ "success": False,
181
+ "error": "Unauthorized",
182
+ "detail": "Invalid or expired token"
183
+ }
184
+ )
185
+
186
+ # 3. MongoDB Errors
187
+ @app.exception_handler(PyMongoError)
188
+ async def mongodb_exception_handler(request: Request, exc: PyMongoError):
189
+ logger.error(
190
+ "Database error",
191
+ extra={
192
+ "path": request.url.path,
193
+ "error": str(exc),
194
+ "error_type": type(exc).__name__
195
+ },
196
+ exc_info=True
197
+ )
198
+
199
+ if isinstance(exc, ConnectionFailure):
200
+ status_code = status.HTTP_503_SERVICE_UNAVAILABLE
201
+ detail = "Database connection failed"
202
+ elif isinstance(exc, OperationFailure):
203
+ status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
204
+ detail = "Database operation failed"
205
+ else:
206
+ status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
207
+ detail = "Database error occurred"
208
+
209
+ return JSONResponse(
210
+ status_code=status_code,
211
+ content={
212
+ "success": False,
213
+ "error": "Database Error",
214
+ "detail": detail
215
+ }
216
+ )
217
+
218
+ # 4. General Exception Handler
219
+ @app.exception_handler(Exception)
220
+ async def general_exception_handler(request: Request, exc: Exception):
221
+ logger.error(
222
+ "Unhandled exception",
223
+ extra={
224
+ "method": request.method,
225
+ "path": request.url.path,
226
+ "error": str(exc),
227
+ "error_type": type(exc).__name__,
228
+ "client_ip": request.client.host if request.client else None
229
+ },
230
+ exc_info=True
231
+ )
232
+
233
+ return JSONResponse(
234
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
235
+ content={
236
+ "success": False,
237
+ "error": "Internal Server Error",
238
+ "detail": "An unexpected error occurred"
239
+ }
240
+ )
241
+ ```
242
+
243
+ ---
244
+
245
+ ## Context Fields Reference
246
+
247
+ | Field | Usage | Examples |
248
+ |-------|-------|----------|
249
+ | `user_id` | User identifier | "usr_123" |
250
+ | `username` | Username | "john_doe" |
251
+ | `email` | Email address | "john@example.com" |
252
+ | `operation` | Action being performed | "insert_user", "delete_account" |
253
+ | `error` | Error message | str(exception) |
254
+ | `error_type` | Exception class | "ValidationError", "ConnectionFailure" |
255
+ | `status_code` | HTTP status | 400, 401, 403, 500 |
256
+ | `path` | Request path | "/api/users/login" |
257
+ | `method` | HTTP method | "POST", "GET", "PUT" |
258
+ | `client_ip` | Client IP | "192.168.1.1" |
259
+ | `reason` | Why it failed | "invalid_password", "missing_token" |
260
+ | `field` | Field name (validation) | "email", "password" |
261
+ | `required_role` | Expected role | "admin", "super_admin" |
262
+ | `user_role` | User's actual role | "user" |
263
+ | `collection` | MongoDB collection | "system_users" |
264
+
265
+ ---
266
+
267
+ ## Response Format Template
268
+
269
+ ```python
270
+ # Success Response
271
+ {
272
+ "success": True,
273
+ "data": {...},
274
+ "message": "Operation completed"
275
+ }
276
+
277
+ # Error Response
278
+ {
279
+ "success": False,
280
+ "error": "Error Type",
281
+ "detail": "Detailed message",
282
+ "errors": [...] # For validation errors
283
+ }
284
+ ```
285
+
286
+ ---
287
+
288
+ ## Implementation Checklist for New Module
289
+
290
+ - [ ] Import logger: `from app.core.logging import get_logger`
291
+ - [ ] Initialize: `logger = get_logger(__name__)`
292
+ - [ ] Import exceptions: `from fastapi import HTTPException, status`
293
+ - [ ] Wrap operations in try-except
294
+ - [ ] Log warnings for user errors (401, 403, 422)
295
+ - [ ] Log errors for server errors (500, 503)
296
+ - [ ] Include error_type in extras
297
+ - [ ] Use exc_info=True for exceptions
298
+ - [ ] Include user_id for actions
299
+ - [ ] Include operation name for tracking
300
+ - [ ] Use consistent field names (see reference table)
301
+
302
+ ---
303
+
304
+ ## Common HTTP Status Codes
305
+
306
+ | Code | Meaning | Logger Level | Use Case |
307
+ |------|---------|--------------|----------|
308
+ | 400 | Bad Request | `warning` | Invalid input data |
309
+ | 401 | Unauthorized | `warning` | Missing/invalid token |
310
+ | 403 | Forbidden | `warning` | Insufficient permissions |
311
+ | 404 | Not Found | `info` | Resource doesn't exist |
312
+ | 422 | Validation Error | `warning` | Invalid field values |
313
+ | 500 | Server Error | `error` | Unexpected exception |
314
+ | 503 | Service Unavailable | `error` | DB connection failed |
315
+
316
+ ---
317
+
318
+ ## Quick Copy-Paste Template
319
+
320
+ ```python
321
+ """
322
+ Module description.
323
+ """
324
+ from fastapi import APIRouter, Depends, HTTPException, status
325
+ from fastapi.responses import JSONResponse
326
+
327
+ from app.core.logging import get_logger
328
+ from app.system_users.services.service import SystemUserService
329
+ from app.dependencies.auth import get_system_user_service
330
+
331
+ logger = get_logger(__name__)
332
+ router = APIRouter(prefix="/api/path", tags=["Category"])
333
+
334
+
335
+ @router.post("/endpoint")
336
+ async def endpoint_function(
337
+ data: RequestModel,
338
+ service: SystemUserService = Depends(get_system_user_service)
339
+ ):
340
+ """
341
+ Endpoint description.
342
+
343
+ Raises:
344
+ HTTPException: 400 - Invalid input
345
+ HTTPException: 401 - Unauthorized
346
+ HTTPException: 500 - Server error
347
+ """
348
+ try:
349
+ # Validation
350
+ if not data.field:
351
+ logger.warning(
352
+ "Validation failed",
353
+ extra={
354
+ "field": "field_name",
355
+ "reason": "empty_value"
356
+ }
357
+ )
358
+ raise HTTPException(
359
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
360
+ detail="Field is required"
361
+ )
362
+
363
+ # Business logic
364
+ result = await service.operation(data)
365
+
366
+ # Success logging
367
+ logger.info(
368
+ "Operation successful",
369
+ extra={
370
+ "operation": "operation_name",
371
+ "result_id": result.id
372
+ }
373
+ )
374
+
375
+ return {
376
+ "success": True,
377
+ "data": result,
378
+ "message": "Operation completed"
379
+ }
380
+
381
+ except HTTPException:
382
+ raise
383
+ except Exception as e:
384
+ logger.error(
385
+ "Operation failed",
386
+ extra={
387
+ "operation": "operation_name",
388
+ "error": str(e),
389
+ "error_type": type(e).__name__
390
+ },
391
+ exc_info=True
392
+ )
393
+ raise HTTPException(
394
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
395
+ detail="Operation failed"
396
+ )
397
+ ```
398
+
399
+