sivarajbookmyservice commited on
Commit
9e1c5c2
·
1 Parent(s): 0c31b33

appointment new changes based on new table format

Browse files
app/auth/auth.py CHANGED
@@ -58,12 +58,13 @@ async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(s
58
 
59
  # Extract user information from token
60
  customer_id = payload.get("sub")
61
-
62
 
63
 
64
  return {
65
  "sub": customer_id,
66
- "payload": payload
 
67
  }
68
 
69
 
 
58
 
59
  # Extract user information from token
60
  customer_id = payload.get("sub")
61
+ merchant_id = payload.get("merchant_id")
62
 
63
 
64
  return {
65
  "sub": customer_id,
66
+ "payload": payload,
67
+ "merchant_id": merchant_id
68
  }
69
 
70
 
app/models/appointment.py CHANGED
@@ -1,5 +1,10 @@
 
 
 
 
 
1
  from pydantic import BaseModel, Field
2
- from datetime import datetime, date
3
  from typing import List, Dict, Optional, Any
4
  import enum
5
  import sqlalchemy
@@ -29,158 +34,310 @@ class PaymentMode(str, enum.Enum):
29
 
30
 
31
  # Model for associate assigned to the appointment
32
- class AssociateDetails(BaseModel):
33
- associate_id: str = Field(..., example="STAFF1")
34
- name: str = Field(..., example="John Doe")
35
-
36
- class Guestdetails(BaseModel):
37
- guest_id:str = Field(...,description="guest id")
38
- first_name: str =Field(..., description="first name")
39
- last_name: str = Field(..., description="Last name")
40
-
41
- class Petdetails(BaseModel):
42
- pet_id:str = Field(...,description="pet id")
43
- pet_name: str = Field(..., description="pet name")
44
-
45
-
46
- # Model for services booked in the appointment
47
- class ServiceDetails(BaseModel):
48
- service_id: str = Field(..., example="SERV1")
49
- name: str = Field(..., example="Hair Cut")
50
- price: float = Field(..., ge=0, example=500.00)
51
- duration: str = Field(..., example="30 minutes")
52
- quantity: int = Field(..., ge=1, example=1) # Quantity must be at least 1
53
 
54
 
55
 
56
  # Model for Appointment
57
- class Appointment(BaseModel):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  appointment_id: Optional[str] = None
59
- merchant_id: str = Field(..., description="Merchant ID is required") # ✅ Mandatory
60
- merchant_name: str = Field(..., description="Merchant name is required") # ✅ Mandatory
61
- city: str = Field(..., description="City is required") # ✅ Mandatory
62
- merchant_address: Dict[str, Any] = Field(..., description="Merchant address is required") # ✅ Mandatory
63
  location_id: str
64
- customer_id: Optional[str] = Field(None, description="Customer ID is optional") # ✅ Optional
65
- appointment_date: Optional[date] = Field(None, description="Appointment date (YYYY-MM-DD)")
66
- appointment_time: str = Field(..., pattern=r"^\d{2}:\d{2}$", description="Appointment time in HH:MM format")
67
- associates: List[AssociateDetails] # ✅ Using `AssociateDetails` model
68
- guest: Optional[List[Guestdetails]] = Field(None, description="Guest details")
69
- pet: Optional[List[Petdetails]] = Field(None, description="Pet details")
70
- status: AppointmentStatus # ✅ Using Enum
71
- services: List[ServiceDetails] # Using `ServiceDetails` model
72
- notes: Optional[str] = None
73
- total_amount: float = Field(..., gt=0, description="Total amount must be positive") # ✅ Mandatory
74
- discount: float = Field(default=0.0, ge=0, description="Discount cannot be negative")
75
- payment_mode: PaymentMode = Field(..., description="Payment mode must be ONLINE or OFFLINE")
76
- payment_status: PaymentStatus = PaymentStatus.PENDING # ✅ Default to "pending"
 
 
77
  payment_id: Optional[str] = None
78
- cleared_amount: float = Field(default=0.0, ge=0, description="Cleared amount must be non-negative")
79
- order_id: Optional[str] = Field(None, description="Order ID is required if payment mode is ONLINE")
80
- business_url: Optional[str] = Field(None, description="Business URL")
81
- merchant_category: Optional[str] = Field(None, description="Merchant category")
82
 
83
- class Config:
84
- json_encoders = {
85
- datetime: lambda v: v.strftime('%Y-%m-%d %H:%M:%S'),
86
- date: lambda v: v.strftime('%Y-%m-%d')
87
- }
88
- json_schema_extra = {
89
- "example": {
90
- "merchant_id": "MERCHANT123",
91
- "merchant_name": "Elegant Salon",
92
- "city": "Mumbai",
93
- "merchant_address": {
94
- "line1": "12 High Street",
95
- "area": "Andheri East",
96
- "city": "Mumbai",
97
- "state": "MH",
98
- "zip_code": "400093"
99
- },
100
- "location_id": "LOC001",
101
- "customer_id": "CUST567",
102
- "appointment_date": "2025-02-25",
103
- "appointment_time": "14:30",
104
- "associates": [
105
- {
106
- "associate_id": "STAFF101",
107
- "name": "John Doe"
108
- }
109
- ],
110
- "guest": [
111
- {
112
- "guest_id":"FGYY01",
113
- "first_name":"John",
114
- "last_name":"Doe"
115
- }
116
- ],
117
- "pet":[
118
- {
119
- "pet_id":"GHUU01",
120
- "pet_name":"Buddy"
121
- }
122
- ],
123
- "status": "confirmed",
124
- "services": [
125
- {
126
- "service_id": "SERV001",
127
- "name": "Haircut",
128
- "price": 50.0,
129
- "quantity": 1,
130
- "duration": "30 minutes"
131
- }
132
- ],
133
- "notes": "Customer prefers short haircut",
134
- "total_amount": 50.0,
135
- "discount": 5.0,
136
- "payment_mode": "online",
137
- "payment_status": "paid",
138
- "payment_id": "pay_789xyz",
139
- "cleared_amount": 45.0,
140
- "order_id": "order_789xyz",
141
- "business_url":"johnson-gomez-and-fleming",
142
- "merchant_category": "spa"
143
-
144
- }
145
- }
146
 
147
 
 
 
 
 
 
 
148
 
149
- # SQLAlchemy Table Definition for Appointments
150
- appointment_table = sqlalchemy.Table(
151
- "appointments",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  metadata,
153
- sqlalchemy.Column("appointment_id", sqlalchemy.String, primary_key=True),
 
 
 
 
154
  sqlalchemy.Column("merchant_name", sqlalchemy.String, nullable=False),
155
- sqlalchemy.Column("city", sqlalchemy.String, nullable=False),
156
- sqlalchemy.Column("merchant_address", sqlalchemy.JSON, nullable=False),
157
- sqlalchemy.Column("merchant_id", sqlalchemy.String, nullable=False),
158
- sqlalchemy.Column("location_id", sqlalchemy.String, nullable=False),
159
- sqlalchemy.Column("customer_id", sqlalchemy.String, nullable=False),
 
 
 
 
 
 
 
 
160
  sqlalchemy.Column("appointment_date", sqlalchemy.Date, nullable=False),
161
- sqlalchemy.Column("appointment_time", sqlalchemy.String(20), nullable=False), # HH:MM format
162
- sqlalchemy.Column("associates", sqlalchemy.JSON, nullable=False), # JSON array
163
- sqlalchemy.Column("guest", sqlalchemy.JSON, nullable=False), # JSON array
164
- sqlalchemy.Column("pet", sqlalchemy.JSON, nullable=False), # JSON array
165
- sqlalchemy.Column("status", sqlalchemy.Enum(AppointmentStatus, native_enum=False, values_callable=lambda enum: [e.value for e in enum]), nullable=False),
166
- sqlalchemy.Column("services", sqlalchemy.JSON, nullable=False), # JSON array
 
 
 
 
 
167
  sqlalchemy.Column("notes", sqlalchemy.Text, nullable=True),
168
- sqlalchemy.Column("total_amount", sqlalchemy.Numeric(10, 2), nullable=False), # Numeric with 2 decimal places
169
- sqlalchemy.Column("discount", sqlalchemy.Numeric(10, 2), nullable=True, default=0.0), # Default discount is 0.0
170
- sqlalchemy.Column("payment_mode", sqlalchemy.Enum(PaymentMode, native_enum=False, values_callable=lambda enum: [e.value for e in enum]), nullable=False),
171
- sqlalchemy.Column("payment_status", sqlalchemy.Enum(PaymentStatus, native_enum=False, values_callable=lambda enum: [e.value for e in enum]), default=PaymentStatus.PENDING.value, nullable=False),
172
- #sqlalchemy.Column("payment_status", sqlalchemy.Enum(PaymentStatus), nullable=False, default="pending"),
173
- sqlalchemy.Column("payment_id", sqlalchemy.String, nullable=True), # ✅ Added payment_id column
174
- sqlalchemy.Column("cleared_amount", sqlalchemy.Numeric(10, 2), nullable=False, default=0.0),
175
- sqlalchemy.Column("order_id", sqlalchemy.String, nullable=True), # ✅ Added order_id column
176
- #sqlalchemy.Column("created_at", sqlalchemy.DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False),
177
- #sqlalchemy.Column("modified_at", sqlalchemy.DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc), nullable=False),
178
- sqlalchemy.Column("cancel_reason", sqlalchemy.String, nullable=True), # ✅ Added cancel_reason column
179
- sqlalchemy.Column("business_url", sqlalchemy.String, nullable=True),
180
- sqlalchemy.Column("merchant_category", sqlalchemy.String, nullable=True)
 
 
 
 
 
 
 
 
 
 
 
 
181
  )
182
 
183
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  # Response Model for Appointments
185
  class AppointmentResponse(BaseModel):
186
  appointment_id: str
@@ -193,21 +350,13 @@ class AppointmentResponse(BaseModel):
193
  appointment_date: str
194
  appointment_time: str
195
  status: str
196
- associates: List[Dict[str, Any]]
197
- guest: List[Dict[str, Any]]
198
- pet: List[Dict[str, Any]]
199
  services: List[Dict[str, Any]]
200
  notes: Optional[str]
201
  total_amount: float
202
  discount: float
203
  payment_mode: str
204
  payment_status: str
205
- payment_id: Optional[str] # ✅ Included payment_id
206
  cleared_amount: float
207
- order_id: Optional[str] = None # ✅ Included order as optional with default
208
- cancel_reason: Optional[str] # ✅ Included cancel_reason
209
- business_url: Optional[str]
210
- merchant_category: Optional[str]
211
 
212
 
213
 
 
1
+ from decimal import Decimal
2
+ from sqlalchemy.dialects.postgresql import UUID as pgUUID
3
+ import uuid
4
+ from geoalchemy2 import Geography
5
+ from sqlalchemy import func
6
  from pydantic import BaseModel, Field
7
+ from datetime import datetime, date, time
8
  from typing import List, Dict, Optional, Any
9
  import enum
10
  import sqlalchemy
 
34
 
35
 
36
  # Model for associate assigned to the appointment
37
+ # class AssociateDetails(BaseModel):
38
+ # associate_id: str = Field(..., example="STAFF1")
39
+ # name: str = Field(..., example="John Doe")
40
+
41
+ # class Guestdetails(BaseModel):
42
+ # guest_id:str = Field(...,description="guest id")
43
+ # first_name: str =Field(..., description="first name")
44
+ # last_name: str = Field(..., description="Last name")
45
+
46
+ # class Petdetails(BaseModel):
47
+ # pet_id:str = Field(...,description="pet id")
48
+ # pet_name: str = Field(..., description="pet name")
49
+
50
+
51
+ # # Model for services booked in the appointment
52
+ # class ServiceDetails(BaseModel):
53
+ # service_id: str = Field(..., example="SERV1")
54
+ # name: str = Field(..., example="Hair Cut")
55
+ # price: float = Field(..., ge=0, example=500.00)
56
+ # duration: str = Field(..., example="30 minutes")
57
+ # quantity: int = Field(..., ge=1, example=1) # Quantity must be at least 1
58
 
59
 
60
 
61
  # Model for Appointment
62
+ # class Appointment(BaseModel):
63
+ # appointment_id: Optional[str] = None
64
+ # merchant_id: str = Field(..., description="Merchant ID is required") # ✅ Mandatory
65
+ # merchant_name: str = Field(..., description="Merchant name is required") # ✅ Mandatory
66
+ # city: str = Field(..., description="City is required") # ✅ Mandatory
67
+ # merchant_address: Dict[str, Any] = Field(..., description="Merchant address is required") # ✅ Mandatory
68
+ # location_id: str
69
+ # customer_id: Optional[str] = Field(None, description="Customer ID is optional") # ✅ Optional
70
+ # appointment_date: Optional[date] = Field(None, description="Appointment date (YYYY-MM-DD)")
71
+ # appointment_time: str = Field(..., pattern=r"^\d{2}:\d{2}$", description="Appointment time in HH:MM format")
72
+ # associates: List[AssociateDetails] # ✅ Using `AssociateDetails` model
73
+ # guest: Optional[List[Guestdetails]] = Field(None, description="Guest details")
74
+ # pet: Optional[List[Petdetails]] = Field(None, description="Pet details")
75
+ # status: AppointmentStatus # ✅ Using Enum
76
+ # services: List[ServiceDetails] # ✅ Using `ServiceDetails` model
77
+ # notes: Optional[str] = None
78
+ # total_amount: float = Field(..., gt=0, description="Total amount must be positive") # ✅ Mandatory
79
+ # discount: float = Field(default=0.0, ge=0, description="Discount cannot be negative")
80
+ # payment_mode: PaymentMode = Field(..., description="Payment mode must be ONLINE or OFFLINE")
81
+ # payment_status: PaymentStatus = PaymentStatus.PENDING # ✅ Default to "pending"
82
+ # payment_id: Optional[str] = None
83
+ # cleared_amount: float = Field(default=0.0, ge=0, description="Cleared amount must be non-negative")
84
+ # order_id: Optional[str] = Field(None, description="Order ID is required if payment mode is ONLINE")
85
+ # business_url: Optional[str] = Field(None, description="Business URL")
86
+ # merchant_category: Optional[str] = Field(None, description="Merchant category")
87
+
88
+ # class Config:
89
+ # json_encoders = {
90
+ # datetime: lambda v: v.strftime('%Y-%m-%d %H:%M:%S'),
91
+ # date: lambda v: v.strftime('%Y-%m-%d')
92
+ # }
93
+ # json_schema_extra = {
94
+ # "example": {
95
+ # "merchant_id": "MERCHANT123",
96
+ # "merchant_name": "Elegant Salon",
97
+ # "city": "Mumbai",
98
+ # "merchant_address": {
99
+ # "line1": "12 High Street",
100
+ # "area": "Andheri East",
101
+ # "city": "Mumbai",
102
+ # "state": "MH",
103
+ # "zip_code": "400093"
104
+ # },
105
+ # "location_id": "LOC001",
106
+ # "customer_id": "CUST567",
107
+ # "appointment_date": "2025-02-25",
108
+ # "appointment_time": "14:30",
109
+ # "associates": [
110
+ # {
111
+ # "associate_id": "STAFF101",
112
+ # "name": "John Doe"
113
+ # }
114
+ # ],
115
+ # "guest": [
116
+ # {
117
+ # "guest_id":"FGYY01",
118
+ # "first_name":"John",
119
+ # "last_name":"Doe"
120
+ # }
121
+ # ],
122
+ # "pet":[
123
+ # {
124
+ # "pet_id":"GHUU01",
125
+ # "pet_name":"Buddy"
126
+ # }
127
+ # ],
128
+ # "status": "confirmed",
129
+ # "services": [
130
+ # {
131
+ # "service_id": "SERV001",
132
+ # "name": "Haircut",
133
+ # "price": 50.0,
134
+ # "quantity": 1,
135
+ # "duration": "30 minutes"
136
+ # }
137
+ # ],
138
+ # "notes": "Customer prefers short haircut",
139
+ # "total_amount": 50.0,
140
+ # "discount": 5.0,
141
+ # "payment_mode": "online",
142
+ # "payment_status": "paid",
143
+ # "payment_id": "pay_789xyz",
144
+ # "cleared_amount": 45.0,
145
+ # "order_id": "order_789xyz",
146
+ # "business_url":"johnson-gomez-and-fleming",
147
+ # "merchant_category": "spa"
148
+
149
+ # }
150
+ # }
151
+
152
+ class GeoCoordinates(BaseModel):
153
+ type: str = Field(..., example="Point")
154
+ coordinates: List[float] = Field(..., min_items=2, max_items=2)
155
+
156
+ class MerchantAddress(BaseModel):
157
+ street: str
158
+ postcode: str
159
+ state: str
160
+ location: GeoCoordinates
161
+ area: Optional[str] = None
162
+
163
+
164
+ class ServiceCreate(BaseModel):
165
+ service_id: str
166
+ name: str
167
+ duration: str # Example: "50 minutes"
168
+ price: Decimal
169
+ quantity: int
170
+ associate_name: str
171
+ associate_id: str
172
+
173
+
174
+ class AppointmentCreateRequest(BaseModel):
175
+ merchant_id: Optional[str] = None
176
+ customer_id: Optional[str] = None
177
  appointment_id: Optional[str] = None
178
+ merchant_name: str
179
+ merchant_address: MerchantAddress
180
+
181
+ city: str
182
  location_id: str
183
+
184
+ appointment_date: date
185
+ appointment_time: str # HH:MM format
186
+
187
+ services: List[ServiceCreate]
188
+
189
+ status: str
190
+ notes: Optional[str] = ""
191
+
192
+ total_amount: Decimal
193
+ discount: Decimal
194
+ cleared_amount: Decimal
195
+
196
+ payment_mode: str
197
+ payment_status: str
198
  payment_id: Optional[str] = None
199
+ order_id: Optional[str] = None
 
 
 
200
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
 
202
 
203
+ class AppointmentModel(BaseModel):
204
+ appointment_id: str
205
+ customer_id: str
206
+
207
+ merchant_id: str
208
+ merchant_name: str
209
 
210
+ city: Optional[str] = None
211
+ location_id: Optional[str] = None
212
+
213
+ # Address Snapshot
214
+ address_street: Optional[str] = None
215
+ address_area: Optional[str] = None
216
+ address_in_tcode: Optional[str] = None
217
+ address_state: Optional[str] = None
218
+
219
+ # Geo location (GeoJSON format)
220
+ geo_location: Optional[Dict[str, Any]] = None
221
+
222
+ appointment_date: date
223
+ appointment_time: time
224
+
225
+ status: str
226
+
227
+ total_amount: Decimal
228
+ discount_amount: Optional[Decimal] = Decimal("0.00")
229
+ cleared_amount: Decimal
230
+
231
+ payment_status: Optional[str] = None
232
+ payment_mode: Optional[str] = None
233
+
234
+ notes: Optional[str] = None
235
+
236
+ created_at: Optional[datetime] = None
237
+ updated_at: Optional[datetime] = None
238
+
239
+ class Config:
240
+ orm_mode = True
241
+
242
+ # NEW UPDATED SCHEMA - REPLACES `appointment_table`
243
+ in_appointments_table = sqlalchemy.Table(
244
+ "in_appointments",
245
  metadata,
246
+ sqlalchemy.Column("appointment_id", pgUUID, primary_key=True,),
247
+
248
+ sqlalchemy.Column("customer_id", pgUUID, nullable=False),
249
+
250
+ sqlalchemy.Column("merchant_id", pgUUID, nullable=False),
251
  sqlalchemy.Column("merchant_name", sqlalchemy.String, nullable=False),
252
+
253
+ sqlalchemy.Column("city", sqlalchemy.String, nullable=True),
254
+ sqlalchemy.Column("location_id", sqlalchemy.String, nullable=True),
255
+
256
+ # Address Snapshot Columns
257
+ sqlalchemy.Column("address_street", sqlalchemy.String, nullable=True),
258
+ sqlalchemy.Column("address_area", sqlalchemy.String, nullable=True),
259
+ sqlalchemy.Column("address_in_tcode", sqlalchemy.String, nullable=True),
260
+ sqlalchemy.Column("address_state", sqlalchemy.String, nullable=True),
261
+
262
+ # Geo Location (PostGIS Geography Point)
263
+ sqlalchemy.Column("geo_location", Geography(geometry_type="POINT", srid=4326), nullable=True),
264
+
265
  sqlalchemy.Column("appointment_date", sqlalchemy.Date, nullable=False),
266
+ sqlalchemy.Column("appointment_time", sqlalchemy.Time, nullable=False),
267
+
268
+ sqlalchemy.Column("status", sqlalchemy.String, nullable=False),
269
+
270
+ sqlalchemy.Column("total_amount", sqlalchemy.Numeric(12, 2), nullable=False),
271
+ sqlalchemy.Column("discount_amount", sqlalchemy.Numeric(12, 2), nullable=True, default=0),
272
+ sqlalchemy.Column("cleared_amount", sqlalchemy.Numeric(12, 2), nullable=False),
273
+
274
+ sqlalchemy.Column("payment_status", sqlalchemy.String, nullable=True),
275
+ sqlalchemy.Column("payment_mode", sqlalchemy.String, nullable=True),
276
+
277
  sqlalchemy.Column("notes", sqlalchemy.Text, nullable=True),
278
+ sqlalchemy.Column("reason", sqlalchemy.Text, nullable=True),
279
+
280
+ sqlalchemy.Column("created_at", sqlalchemy.DateTime(timezone=True),
281
+ server_default=func.now(), nullable=False),
282
+
283
+ sqlalchemy.Column("updated_at", sqlalchemy.DateTime(timezone=True),
284
+ server_default=func.now(),
285
+ onupdate=func.now(), nullable=False)
286
+
287
+ )
288
+
289
+ in_appointment_services_table = sqlalchemy.Table(
290
+ "in_appointment_services",
291
+ metadata,
292
+ sqlalchemy.Column("appointment_service_id", pgUUID, primary_key=True, default=uuid.uuid4),
293
+ sqlalchemy.Column("appointment_id", pgUUID, nullable=False),
294
+ sqlalchemy.Column("service_id", pgUUID, nullable=False),
295
+ sqlalchemy.Column("service_name", sqlalchemy.String, nullable=False),
296
+ sqlalchemy.Column("duration_minutes", sqlalchemy.String, nullable=False), # e.g., "50 minutes"
297
+ sqlalchemy.Column("unit_price", sqlalchemy.Numeric(12, 2), nullable=False),
298
+ sqlalchemy.Column("quantity", sqlalchemy.Integer, nullable=False),
299
+ sqlalchemy.Column("line_total", sqlalchemy.Numeric(12, 2), nullable=False),
300
+ sqlalchemy.Column("associate_name", sqlalchemy.String, nullable=False),
301
+ sqlalchemy.Column("associate_id", pgUUID, nullable=False),
302
+ sqlalchemy.Column("created_at", sqlalchemy.DateTime(timezone=True), server_default=func.now(), nullable=False),
303
  )
304
 
305
 
306
+ # SQLAlchemy Table Definition for Appointments
307
+ # appointment_table = sqlalchemy.Table(
308
+ # "appointments",
309
+ # metadata,
310
+ # sqlalchemy.Column("appointment_id", sqlalchemy.String, primary_key=True),
311
+ # sqlalchemy.Column("merchant_name", sqlalchemy.String, nullable=False),
312
+ # sqlalchemy.Column("city", sqlalchemy.String, nullable=False),
313
+ # sqlalchemy.Column("merchant_address", sqlalchemy.JSON, nullable=False),
314
+ # sqlalchemy.Column("merchant_id", sqlalchemy.String, nullable=False),
315
+ # sqlalchemy.Column("location_id", sqlalchemy.String, nullable=False),
316
+ # sqlalchemy.Column("customer_id", sqlalchemy.String, nullable=False),
317
+ # sqlalchemy.Column("appointment_date", sqlalchemy.Date, nullable=False),
318
+ # sqlalchemy.Column("appointment_time", sqlalchemy.String(20), nullable=False), # HH:MM format
319
+ # sqlalchemy.Column("associates", sqlalchemy.JSON, nullable=False), # JSON array
320
+ # sqlalchemy.Column("guest", sqlalchemy.JSON, nullable=False), # JSON array
321
+ # sqlalchemy.Column("pet", sqlalchemy.JSON, nullable=False), # JSON array
322
+ # sqlalchemy.Column("status", sqlalchemy.Enum(AppointmentStatus, native_enum=False, values_callable=lambda enum: [e.value for e in enum]), nullable=False),
323
+ # sqlalchemy.Column("services", sqlalchemy.JSON, nullable=False), # JSON array
324
+ # sqlalchemy.Column("notes", sqlalchemy.Text, nullable=True),
325
+ # sqlalchemy.Column("total_amount", sqlalchemy.Numeric(10, 2), nullable=False), # Numeric with 2 decimal places
326
+ # sqlalchemy.Column("discount", sqlalchemy.Numeric(10, 2), nullable=True, default=0.0), # Default discount is 0.0
327
+ # sqlalchemy.Column("payment_mode", sqlalchemy.Enum(PaymentMode, native_enum=False, values_callable=lambda enum: [e.value for e in enum]), nullable=False),
328
+ # sqlalchemy.Column("payment_status", sqlalchemy.Enum(PaymentStatus, native_enum=False, values_callable=lambda enum: [e.value for e in enum]), default=PaymentStatus.PENDING.value, nullable=False),
329
+ # #sqlalchemy.Column("payment_status", sqlalchemy.Enum(PaymentStatus), nullable=False, default="pending"),
330
+ # sqlalchemy.Column("payment_id", sqlalchemy.String, nullable=True), # ✅ Added payment_id column
331
+ # sqlalchemy.Column("cleared_amount", sqlalchemy.Numeric(10, 2), nullable=False, default=0.0),
332
+ # sqlalchemy.Column("order_id", sqlalchemy.String, nullable=True), # ✅ Added order_id column
333
+ # #sqlalchemy.Column("created_at", sqlalchemy.DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False),
334
+ # #sqlalchemy.Column("modified_at", sqlalchemy.DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc), nullable=False),
335
+ # sqlalchemy.Column("cancel_reason", sqlalchemy.String, nullable=True), # ✅ Added cancel_reason column
336
+ # sqlalchemy.Column("business_url", sqlalchemy.String, nullable=True),
337
+ # sqlalchemy.Column("merchant_category", sqlalchemy.String, nullable=True)
338
+ # )
339
+
340
+
341
  # Response Model for Appointments
342
  class AppointmentResponse(BaseModel):
343
  appointment_id: str
 
350
  appointment_date: str
351
  appointment_time: str
352
  status: str
 
 
 
353
  services: List[Dict[str, Any]]
354
  notes: Optional[str]
355
  total_amount: float
356
  discount: float
357
  payment_mode: str
358
  payment_status: str
 
359
  cleared_amount: float
 
 
 
 
360
 
361
 
362
 
app/repositories/appointment.py CHANGED
@@ -1,9 +1,11 @@
1
  from typing import Tuple, Optional, List, Dict, Union
2
- from app.models.appointment import appointment_table, Appointment
3
  from app.core.sql_config import database
4
  from app.utils.performance_metrics import monitor_db_operation
5
  from app.utils.database import (
6
  serialize_appointment,
 
 
7
  validate_query_result,
8
  validate_existing_appointment,
9
  )
@@ -16,30 +18,52 @@ from sqlalchemy import func, insert
16
  logging.basicConfig(level=logging.INFO)
17
  logger = logging.getLogger(__name__)
18
 
19
- async def create_appointment(appointment: Appointment):
20
- """Creates an appointment, handling both online and offline payments."""
 
21
  try:
22
- # Convert appointment object to dictionary before inserting into DB
23
- appointment_data = appointment.dict()
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
- logger.info(f"🛠️ Pre-insert status: {appointment_data['status']} (Type: {type(appointment_data['status'])})")
26
- logger.info(f"📌 Creating appointment: {appointment_data}")
 
 
 
27
 
28
- # ✅ Insert into DB correctly
29
- result = await database.execute(insert(appointment_table).values(**appointment_data))
 
 
 
 
 
30
 
31
- logger.info(f"✅ Appointment stored in DB: {appointment.appointment_id}")
32
 
33
  return {
 
34
  "appointment_id": appointment.appointment_id,
35
- "appointment_details": appointment.dict(exclude_none=True),
36
  }
37
 
38
- except HTTPException as e:
39
- logger.error(f"❌ HTTP error: {e.detail}")
40
- raise e
41
  except Exception as e:
42
- logger.error(f"❌ Unexpected error while creating appointment: {str(e)}")
43
  raise HTTPException(status_code=500, detail="Failed to create appointment")
44
 
45
 
@@ -61,8 +85,8 @@ async def update_appointment(appointment_id: str, update_data: dict):
61
  validate_existing_appointment(existing_appointment)
62
 
63
  query = (
64
- appointment_table.update()
65
- .where(appointment_table.c.appointment_id == appointment_id)
66
  .values(**update_data)
67
  )
68
 
@@ -91,11 +115,11 @@ async def get_appointment_by_id(appointment_id: str):
91
  dict: The serialized appointment details.
92
  """
93
  try:
94
- query = appointment_table.select().where(appointment_table.c.appointment_id == appointment_id)
95
 
96
  logger.info(f"Fetching appointment: {query}")
97
 
98
- async with monitor_db_operation("SELECT", "appointment"):
99
  result = await database.fetch_one(query)
100
 
101
  validate_query_result(result, "Appointment not found.")
@@ -118,9 +142,9 @@ async def get_appointments_by_customer(customer_id: str):
118
  list: List of serialized appointments.
119
  """
120
  try:
121
- query = appointment_table.select().where(appointment_table.c.customer_id == customer_id)
122
-
123
- async with monitor_db_operation("SELECT", "appointment"):
124
  results = await database.fetch_all(query)
125
 
126
  if not results:
@@ -146,7 +170,7 @@ async def get_appointments_by_merchant(merchant_id: str):
146
  list: List of serialized appointments.
147
  """
148
  try:
149
- query = appointment_table.select().where(appointment_table.c.merchant_id == merchant_id)
150
  results = await database.fetch_all(query)
151
 
152
  if not results:
@@ -173,12 +197,12 @@ async def get_appointments_with_filters(filters: dict):
173
  """
174
  try:
175
  # Build secure query using SQLAlchemy query builder
176
- query = appointment_table.select()
177
 
178
  # Apply filters safely using SQLAlchemy's where clauses
179
  for key, value in filters.items():
180
- if value and hasattr(appointment_table.c, key):
181
- query = query.where(getattr(appointment_table.c, key) == value)
182
 
183
  results = await database.fetch_all(query)
184
 
@@ -218,8 +242,8 @@ async def cancel_appointment(appointment_id: str, update_data: dict):
218
  raise HTTPException(status_code=400, detail="Cannot cancel a completed appointment.")
219
 
220
  query = (
221
- appointment_table.update()
222
- .where(appointment_table.c.appointment_id == appointment_id)
223
  .values(**update_data)
224
  )
225
 
@@ -255,36 +279,80 @@ async def fetch_appointments_from_db(
255
  logger.info(f"Fetching appointments for customer ID {customer_id} with status {status}")
256
 
257
  # Base query for total count
258
- count_query = select(func.count()).select_from(appointment_table).where(
259
- appointment_table.c.customer_id == customer_id
260
  )
261
 
262
  if status:
263
- count_query = count_query.where(appointment_table.c.status.in_(status))
264
 
265
  total_count_row = await database.fetch_one(count_query)
266
  total_count = total_count_row[0] if total_count_row else 0
267
 
268
 
269
  # ✅ Always initialize query first
 
270
  query = (
271
- select(appointment_table)
272
- .where(appointment_table.c.customer_id == customer_id)
273
- .order_by(appointment_table.c.appointment_date.desc())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  .limit(limit)
275
  .offset(offset)
276
  )
277
 
278
-
279
  # ✅ Apply status filter correctly
280
  if status:
281
- query = query.where(appointment_table.c.status.in_(status))
282
 
283
  appointments = await database.fetch_all(query)
284
 
285
- appointments_dicts = [dict(appointment) for appointment in appointments]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
 
287
- return appointments_dicts, total_count
288
 
289
  except Exception as e:
290
  logger.error(f"Database error in fetch_appointments_from_db: {str(e)}")
 
1
  from typing import Tuple, Optional, List, Dict, Union
2
+ from app.models.appointment import AppointmentModel, AppointmentCreateRequest, in_appointments_table, in_appointment_services_table
3
  from app.core.sql_config import database
4
  from app.utils.performance_metrics import monitor_db_operation
5
  from app.utils.database import (
6
  serialize_appointment,
7
+ to_in_appointments_db,
8
+ to_in_appointments_services_db,
9
  validate_query_result,
10
  validate_existing_appointment,
11
  )
 
18
  logging.basicConfig(level=logging.INFO)
19
  logger = logging.getLogger(__name__)
20
 
21
+ async def create_appointment(appointment: AppointmentCreateRequest):
22
+ """Creates an appointment with services in a single safe transaction."""
23
+
24
  try:
25
+
26
+ # Extract services
27
+ appointment_services = appointment.services
28
+
29
+ # Convert appointment to DB payload
30
+ appointment_data = to_in_appointments_db(appointment)
31
+
32
+ logger.info(f"📌 Appointment Insert Data: {appointment_data}")
33
+
34
+ # Begin transaction
35
+ async with database.transaction():
36
+
37
+ # Insert Appointment (Parent)
38
+ await database.execute(insert(in_appointments_table).values(**appointment_data))
39
+ logger.info(f"🟢 Appointment inserted: {appointment.appointment_id}")
40
 
41
+ # Prepare & Insert Service Line Items (Child)
42
+ service_rows = to_in_appointments_services_db(
43
+ appointment_services,
44
+ appointment.appointment_id
45
+ )
46
 
47
+ if service_rows:
48
+ if len(service_rows) == 1:
49
+ logger.info("🟡 Inserting one service row...")
50
+ await database.execute(insert(in_appointment_services_table).values(service_rows[0]))
51
+ else:
52
+ logger.info(f"🟡 Bulk inserting {len(service_rows)} service rows...")
53
+ await database.execute_many(insert(in_appointment_services_table), service_rows)
54
 
55
+ logger.info(f"✅ Appointment created successfully: {appointment.appointment_id}")
56
 
57
  return {
58
+ "success": True,
59
  "appointment_id": appointment.appointment_id,
60
+ "message": "Appointment created successfully"
61
  }
62
 
63
+ except HTTPException:
64
+ raise
 
65
  except Exception as e:
66
+ logger.error(f"❌ Unexpected error in create_appointment: {str(e)}")
67
  raise HTTPException(status_code=500, detail="Failed to create appointment")
68
 
69
 
 
85
  validate_existing_appointment(existing_appointment)
86
 
87
  query = (
88
+ in_appointments_table.update()
89
+ .where(in_appointments_table.c.appointment_id == appointment_id)
90
  .values(**update_data)
91
  )
92
 
 
115
  dict: The serialized appointment details.
116
  """
117
  try:
118
+ query = in_appointments_table.select().where(in_appointments_table.c.appointment_id == appointment_id)
119
 
120
  logger.info(f"Fetching appointment: {query}")
121
 
122
+ async with monitor_db_operation("SELECT", "in_appointments_table"):
123
  result = await database.fetch_one(query)
124
 
125
  validate_query_result(result, "Appointment not found.")
 
142
  list: List of serialized appointments.
143
  """
144
  try:
145
+ query = in_appointments_table.select().where(in_appointments_table.c.customer_id == customer_id)
146
+
147
+ async with monitor_db_operation("SELECT", "in_appointments_table"):
148
  results = await database.fetch_all(query)
149
 
150
  if not results:
 
170
  list: List of serialized appointments.
171
  """
172
  try:
173
+ query = in_appointments_table.select().where(in_appointments_table.c.merchant_id == merchant_id)
174
  results = await database.fetch_all(query)
175
 
176
  if not results:
 
197
  """
198
  try:
199
  # Build secure query using SQLAlchemy query builder
200
+ query = in_appointments_table.select()
201
 
202
  # Apply filters safely using SQLAlchemy's where clauses
203
  for key, value in filters.items():
204
+ if value and hasattr(in_appointments_table.c, key):
205
+ query = query.where(getattr(in_appointments_table.c, key) == value)
206
 
207
  results = await database.fetch_all(query)
208
 
 
242
  raise HTTPException(status_code=400, detail="Cannot cancel a completed appointment.")
243
 
244
  query = (
245
+ in_appointments_table.update()
246
+ .where(in_appointments_table.c.appointment_id == appointment_id)
247
  .values(**update_data)
248
  )
249
 
 
279
  logger.info(f"Fetching appointments for customer ID {customer_id} with status {status}")
280
 
281
  # Base query for total count
282
+ count_query = select(func.count()).select_from(in_appointments_table).where(
283
+ in_appointments_table.c.customer_id == customer_id
284
  )
285
 
286
  if status:
287
+ count_query = count_query.where(in_appointments_table.c.status.in_(status))
288
 
289
  total_count_row = await database.fetch_one(count_query)
290
  total_count = total_count_row[0] if total_count_row else 0
291
 
292
 
293
  # ✅ Always initialize query first
294
+ # Main Query: JOIN appointment_services
295
  query = (
296
+ select(
297
+ in_appointments_table,
298
+ in_appointment_services_table.c.service_id,
299
+ in_appointment_services_table.c.service_name,
300
+ in_appointment_services_table.c.duration_minutes,
301
+ in_appointment_services_table.c.unit_price,
302
+ in_appointment_services_table.c.quantity,
303
+ in_appointment_services_table.c.line_total,
304
+ in_appointment_services_table.c.associate_id,
305
+ in_appointment_services_table.c.associate_name,
306
+ )
307
+ .join(
308
+ in_appointment_services_table,
309
+ in_appointments_table.c.appointment_id == in_appointment_services_table.c.appointment_id,
310
+ isouter=True
311
+ )
312
+ .where(in_appointments_table.c.customer_id == customer_id)
313
+ .order_by(
314
+ in_appointments_table.c.appointment_date.desc(),
315
+ in_appointments_table.c.appointment_time.desc(),
316
+ )
317
  .limit(limit)
318
  .offset(offset)
319
  )
320
 
 
321
  # ✅ Apply status filter correctly
322
  if status:
323
+ query = query.where(in_appointments_table.c.status.in_(status))
324
 
325
  appointments = await database.fetch_all(query)
326
 
327
+
328
+ # Group by appointment_id
329
+ appointments_map = {}
330
+
331
+ for row in appointments:
332
+ row = dict(row)
333
+ aid = row["appointment_id"]
334
+
335
+ if aid not in appointments_map:
336
+ base = {k: row[k] for k in in_appointments_table.columns.keys()}
337
+ base["services"] = []
338
+ appointments_map[aid] = base
339
+
340
+ if row.get("service_id"):
341
+ appointments_map[aid]["services"].append(
342
+ {
343
+ "service_id": row["service_id"],
344
+ "service_name": row["service_name"],
345
+ "duration": row["duration_minutes"],
346
+ "unit_price": float(row["unit_price"]),
347
+ "quantity": row["quantity"],
348
+ "line_total": float(row["line_total"]),
349
+ "associate_id": row["associate_id"],
350
+ "associate_name": row["associate_name"],
351
+ }
352
+ )
353
+
354
+ return list(appointments_map.values()), total_count
355
 
 
356
 
357
  except Exception as e:
358
  logger.error(f"Database error in fetch_appointments_from_db: {str(e)}")
app/routers/appointment.py CHANGED
@@ -6,7 +6,7 @@ from app.services.appointment import (
6
  cancel_appointment_service,
7
  get_appointments_by_customer_id
8
  )
9
- from app.models.appointment import Appointment, AppointmentListResponse
10
  from app.services.order import OrderController
11
  from app.auth import get_current_user
12
 
@@ -22,7 +22,7 @@ router = APIRouter()
22
  logger = logging.getLogger(__name__)
23
 
24
  @router.post("/appointment")
25
- async def create_appointment(appointment: Appointment, current_user: dict = Depends(get_current_user)):
26
  """
27
  API endpoint to create a new appointment and generate a Razorpay order.
28
 
@@ -36,11 +36,14 @@ async def create_appointment(appointment: Appointment, current_user: dict = Depe
36
  try:
37
  # Extract customer_id from current_user token
38
  customer_id = current_user.get("sub")
 
 
39
  if not customer_id:
40
  raise HTTPException(status_code=401, detail="Invalid token: missing customer ID")
41
 
42
  # Set the customer_id in the appointment object
43
  appointment.customer_id = customer_id
 
44
 
45
  logger.info(f"Creating a new appointment for customer: {customer_id}")
46
  return await create_new_appointment(appointment)
 
6
  cancel_appointment_service,
7
  get_appointments_by_customer_id
8
  )
9
+ from app.models.appointment import AppointmentCreateRequest, AppointmentListResponse
10
  from app.services.order import OrderController
11
  from app.auth import get_current_user
12
 
 
22
  logger = logging.getLogger(__name__)
23
 
24
  @router.post("/appointment")
25
+ async def create_appointment(appointment: AppointmentCreateRequest, current_user: dict = Depends(get_current_user)):
26
  """
27
  API endpoint to create a new appointment and generate a Razorpay order.
28
 
 
36
  try:
37
  # Extract customer_id from current_user token
38
  customer_id = current_user.get("sub")
39
+ merchant_id = current_user.get("merchant_id")
40
+
41
  if not customer_id:
42
  raise HTTPException(status_code=401, detail="Invalid token: missing customer ID")
43
 
44
  # Set the customer_id in the appointment object
45
  appointment.customer_id = customer_id
46
+ appointment.merchant_id = merchant_id if appointment.merchant_id is None else appointment.merchant_id
47
 
48
  logger.info(f"Creating a new appointment for customer: {customer_id}")
49
  return await create_new_appointment(appointment)
app/services/appointment.py CHANGED
@@ -13,9 +13,11 @@ from app.repositories.appointment import (
13
  )
14
  from app.repositories.payment import get_order_by_id
15
  from app.repositories.cache import invalidate_appointment_cache
16
- from app.models.appointment import Appointment, AppointmentStatus, PaymentMode, AppointmentListResponse, PaginationMeta, AppointmentResponse, PaymentStatus
17
  import string, random
18
 
 
 
19
  # Initialize logger
20
  logger = getLogger(__name__)
21
 
@@ -70,7 +72,7 @@ def generate_appointment_id(business_name: str, city: str, customer_id: str, app
70
 
71
 
72
 
73
- async def create_new_appointment(appointment: Appointment):
74
  """Creates an appointment, handling both online and offline payments."""
75
  try:
76
  # ✅ Generate unique appointment ID
@@ -78,7 +80,6 @@ async def create_new_appointment(appointment: Appointment):
78
  appointment.appointment_id = generate_appointment_id(appointment.merchant_name, appointment.city,
79
  appointment.customer_id, appointment.appointment_date.strftime("%y%m%d"))
80
 
81
- now = datetime.now(timezone.utc)
82
 
83
  # ✅ Ensure appointment_date is valid
84
  if isinstance(appointment.appointment_date, str):
@@ -110,22 +111,32 @@ async def create_new_appointment(appointment: Appointment):
110
  logger.error("❌ Missing `payment_mode` in request")
111
  raise HTTPException(status_code=400, detail="Missing payment mode")
112
 
113
- appointment.payment_mode = str(appointment.payment_mode.value).lower()
 
 
 
 
 
 
 
 
 
 
 
114
 
115
- # Handle online payments
116
- if appointment.payment_mode == PaymentMode.ONLINE:
117
- if not appointment.order_id:
118
- logger.error("Missing order_id for online payment")
119
- raise HTTPException(status_code=400, detail="Missing Razorpay Order ID")
120
 
121
- try:
122
- razorpay_order = await validate_razorpay_order(appointment.order_id)
123
- appointment.payment_status = PaymentStatus.PAID
124
- except Exception as e:
125
- logger.error(f"❌ Razorpay order validation failed: {str(e)}")
126
- raise HTTPException(status_code=400, detail="Invalid or expired Razorpay Order ID")
127
- else:
128
- appointment.payment_status = PaymentStatus.PENDING
129
 
130
 
131
  # ✅ Create appointment in DB
@@ -290,7 +301,7 @@ async def cancel_appointment_service(appointment_id: str, cancel_reason: str, cu
290
  # Update appointment status and add cancellation details
291
  update_data = {
292
  "status": AppointmentStatus.CANCELED.value,
293
- "cancel_reason": cancel_reason,
294
  #"modified_at": datetime.now(timezone.utc),
295
  }
296
 
@@ -353,44 +364,60 @@ async def get_appointments_by_customer_id(
353
  status=status_list
354
  )
355
 
356
-
357
  formatted_appointments = [
358
  AppointmentResponse(
359
  appointment_id=str(appointment.get("appointment_id", "")),
360
  merchant_id=str(appointment.get("merchant_id", "")),
361
- city=appointment.get("city", ""),
362
  customer_id=str(appointment.get("customer_id", "")),
 
363
  merchant_name=appointment.get("merchant_name", "Unknown"),
364
- merchant_address=appointment.get("merchant_address", "Unknown"),
365
- location_id=str(appointment.get("location_id", "")),
 
 
 
 
 
 
 
366
 
367
- # Ensure date fields are strings
 
368
  appointment_date=(
369
  appointment["appointment_date"].strftime("%Y-%m-%d")
370
  if appointment.get("appointment_date") else ""
371
  ),
372
  appointment_time=str(appointment.get("appointment_time", "")),
373
 
374
- status=appointment.get("status", "unknown").lower(),
375
- associates=appointment.get("associates", []) or [],
376
- guest=appointment.get("guest",[]) or [],
377
- pet = appointment.get("pet",[]) or [],
378
- services=appointment.get("services", []) or [],
379
  notes=appointment.get("notes", ""),
 
380
  total_amount=float(appointment.get("total_amount", 0.0)),
381
- discount=float(appointment.get("discount", 0.0)),
382
- payment_mode=appointment.get("payment_mode", "unknown").lower(),
383
- payment_status=appointment.get("payment_status", "Pending"),
384
- payment_id=str(appointment.get("payment_id", "")) if appointment.get("payment_id") else None,
385
- cleared_amount=float(appointment.get("cleared_amount", 0.0)),
386
- order_id=str(appointment.get("order_id", "")) if appointment.get("order_id") else None,
387
- cancel_reason=appointment.get("cancel_reason", ""),
388
- business_url=str(appointment.get("business_url","")),
389
- merchant_category=str(appointment.get("merchant_category",""))
390
- )
391
- for appointment in appointments
 
 
 
 
 
 
 
 
 
 
 
392
  ]
393
 
 
394
  response_data = AppointmentListResponse(
395
  customer_id=customer_id,
396
  appointments=formatted_appointments,
 
13
  )
14
  from app.repositories.payment import get_order_by_id
15
  from app.repositories.cache import invalidate_appointment_cache
16
+ from app.models.appointment import AppointmentCreateRequest, AppointmentStatus, PaymentMode, AppointmentListResponse, PaginationMeta, AppointmentResponse, PaymentStatus
17
  import string, random
18
 
19
+ from app.utils.json_utils import format_geo_location
20
+
21
  # Initialize logger
22
  logger = getLogger(__name__)
23
 
 
72
 
73
 
74
 
75
+ async def create_new_appointment(appointment: AppointmentCreateRequest):
76
  """Creates an appointment, handling both online and offline payments."""
77
  try:
78
  # ✅ Generate unique appointment ID
 
80
  appointment.appointment_id = generate_appointment_id(appointment.merchant_name, appointment.city,
81
  appointment.customer_id, appointment.appointment_date.strftime("%y%m%d"))
82
 
 
83
 
84
  # ✅ Ensure appointment_date is valid
85
  if isinstance(appointment.appointment_date, str):
 
111
  logger.error("❌ Missing `payment_mode` in request")
112
  raise HTTPException(status_code=400, detail="Missing payment mode")
113
 
114
+ appointment.payment_mode = str(appointment.payment_mode).lower()
115
+
116
+ # Online Payments:
117
+ # When payment_mode is ONLINE, ensure a valid order_id is present.
118
+ # The Razorpay order is validated before marking the payment as PAID.
119
+ #
120
+ # Offline Payments:
121
+ # When payment_mode is OFFLINE, the default payment status remains PENDING.
122
+ #
123
+ # Note: Payment gateway integration will be enabled once finalized.
124
+ # The below logic will be uncommented when Razorpay is fully configured.
125
+
126
 
127
+ # if appointment.payment_mode == PaymentMode.ONLINE:
128
+ # if not appointment.order_id:
129
+ # logger.error("❌ Missing order_id for online payment")
130
+ # raise HTTPException(status_code=400, detail="Missing Razorpay Order ID")
 
131
 
132
+ # try:
133
+ # razorpay_order = await validate_razorpay_order(appointment.order_id)
134
+ # appointment.payment_status = PaymentStatus.PAID
135
+ # except Exception as e:
136
+ # logger.error(f"❌ Razorpay order validation failed: {str(e)}")
137
+ # raise HTTPException(status_code=400, detail="Invalid or expired Razorpay Order ID")
138
+ # else:
139
+ # appointment.payment_status = PaymentStatus.PENDING
140
 
141
 
142
  # ✅ Create appointment in DB
 
301
  # Update appointment status and add cancellation details
302
  update_data = {
303
  "status": AppointmentStatus.CANCELED.value,
304
+ "reason": cancel_reason,
305
  #"modified_at": datetime.now(timezone.utc),
306
  }
307
 
 
364
  status=status_list
365
  )
366
 
 
367
  formatted_appointments = [
368
  AppointmentResponse(
369
  appointment_id=str(appointment.get("appointment_id", "")),
370
  merchant_id=str(appointment.get("merchant_id", "")),
 
371
  customer_id=str(appointment.get("customer_id", "")),
372
+
373
  merchant_name=appointment.get("merchant_name", "Unknown"),
374
+
375
+ # Reconstruct merchant address into nested object
376
+ merchant_address={
377
+ "street": appointment.get("address_street"),
378
+ "area": appointment.get("address_area"),
379
+ "postcode": appointment.get("address_in_tcode"),
380
+ "state": appointment.get("address_state"),
381
+ "location": format_geo_location(appointment.get("geo_location"))
382
+ },
383
 
384
+ city=appointment.get("city", ""),
385
+ location_id=str(appointment.get("location_id", "")),
386
  appointment_date=(
387
  appointment["appointment_date"].strftime("%Y-%m-%d")
388
  if appointment.get("appointment_date") else ""
389
  ),
390
  appointment_time=str(appointment.get("appointment_time", "")),
391
 
392
+ status=str(appointment.get("status", "unknown")).lower(),
 
 
 
 
393
  notes=appointment.get("notes", ""),
394
+
395
  total_amount=float(appointment.get("total_amount", 0.0)),
396
+ discount=float(appointment.get("discount_amount", 0.0)),
397
+ cleared_amount=float(appointment.get("cleared_amount", 0.0)),
398
+
399
+ payment_mode=str(appointment.get("payment_mode", "unknown")).lower(),
400
+ payment_status=str(appointment.get("payment_status", "pending")).lower(),
401
+
402
+ created_at=str(appointment.get("created_at", "")),
403
+ updated_at=str(appointment.get("updated_at", "")),
404
+ services=[
405
+ {
406
+ "service_id": service.get("service_id"),
407
+ "service_name": service.get("service_name"),
408
+ "duration": service.get("duration"),
409
+ "unit_price": float(service.get("unit_price", 0)),
410
+ "quantity": service.get("quantity"),
411
+ "line_total": float(service.get("line_total", 0)),
412
+ "associate_id": service.get("associate_id"),
413
+ "associate_name": service.get("associate_name"),
414
+ }
415
+ for service in appointment.get("services", [])
416
+ ]
417
+ ) for appointment in appointments
418
  ]
419
 
420
+
421
  response_data = AppointmentListResponse(
422
  customer_id=customer_id,
423
  appointments=formatted_appointments,
app/utils/database.py CHANGED
@@ -1,7 +1,10 @@
1
  from datetime import datetime, timezone
 
2
  from sqlalchemy.sql import text
3
  from fastapi import HTTPException
4
 
 
 
5
  def build_filter_query(filters: dict):
6
  """
7
  Builds a dynamic SQL filter query based on the provided filter parameters.
@@ -60,34 +63,54 @@ def serialize_appointment(appointment):
60
  if appointment is None:
61
  return None
62
 
63
- print("in seralize appointment", appointment)
64
-
65
  return {
66
- "appointment_id": appointment["appointment_id"],
67
- "merchant_id": appointment["merchant_id"],
 
68
  "merchant_name": appointment["merchant_name"],
 
69
  "city": appointment["city"],
70
- "merchant_address": appointment["merchant_address"],
71
  "location_id": appointment["location_id"],
72
- "customer_id": appointment["customer_id"],
73
- "appointment_date": appointment["appointment_date"].isoformat() if appointment["appointment_date"] is not None else None,
74
- "appointment_time": appointment["appointment_time"].isoformat() if hasattr(appointment["appointment_time"], 'isoformat') else str(appointment["appointment_time"]),
75
- "associates": appointment["associates"],
76
- "guest":appointment["guest"],
77
- "pet":appointment["pet"],
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  "status": appointment["status"],
79
- "services": appointment["services"],
80
  "notes": appointment["notes"],
 
81
  "total_amount": float(appointment["total_amount"]) if appointment["total_amount"] else 0.0,
82
- "discount": float(appointment["discount"]) if appointment["discount"] else 0.0,
 
 
83
  "payment_mode": appointment["payment_mode"],
84
  "payment_status": appointment["payment_status"],
85
- "payment_id": appointment["payment_id"],
86
- "cleared_amount": float(appointment["cleared_amount"]) if appointment["cleared_amount"] else 0.0,
87
- "order_id": appointment["order_id"],
88
- "cancel_reason": appointment["cancel_reason"],
 
 
 
 
 
89
  }
90
 
 
91
  def validate_existing_appointment(appointment):
92
  """
93
  Validates the existing appointment's status for operations.
@@ -114,4 +137,55 @@ def calculate_appointment_duration(services):
114
  Returns:
115
  int: Total duration in minutes.
116
  """
117
- return sum(service.get("duration", 0) for service in services)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from datetime import datetime, timezone
2
+ import uuid
3
  from sqlalchemy.sql import text
4
  from fastapi import HTTPException
5
 
6
+ from app.models.appointment import AppointmentCreateRequest
7
+
8
  def build_filter_query(filters: dict):
9
  """
10
  Builds a dynamic SQL filter query based on the provided filter parameters.
 
63
  if appointment is None:
64
  return None
65
 
 
 
66
  return {
67
+ "appointment_id": str(appointment["appointment_id"]),
68
+ "customer_id": str(appointment["customer_id"]),
69
+ "merchant_id": str(appointment["merchant_id"]),
70
  "merchant_name": appointment["merchant_name"],
71
+
72
  "city": appointment["city"],
 
73
  "location_id": appointment["location_id"],
74
+
75
+ "merchant_address": {
76
+ "street": appointment["address_street"],
77
+ "area": appointment["address_area"],
78
+ "postcode": appointment["address_in_tcode"],
79
+ "state": appointment["address_state"]
80
+ },
81
+
82
+ "appointment_date": (
83
+ appointment["appointment_date"].isoformat()
84
+ if appointment["appointment_date"] else None
85
+ ),
86
+
87
+ "appointment_time": (
88
+ appointment["appointment_time"].isoformat()
89
+ if hasattr(appointment["appointment_time"], "isoformat")
90
+ else str(appointment["appointment_time"])
91
+ ),
92
+
93
  "status": appointment["status"],
 
94
  "notes": appointment["notes"],
95
+
96
  "total_amount": float(appointment["total_amount"]) if appointment["total_amount"] else 0.0,
97
+ "discount_amount": float(appointment["discount_amount"]) if appointment["discount_amount"] else 0.0,
98
+ "cleared_amount": float(appointment["cleared_amount"]) if appointment["cleared_amount"] else 0.0,
99
+
100
  "payment_mode": appointment["payment_mode"],
101
  "payment_status": appointment["payment_status"],
102
+
103
+ "created_at": (
104
+ appointment["created_at"].isoformat()
105
+ if appointment["created_at"] else None
106
+ ),
107
+ "updated_at": (
108
+ appointment["updated_at"].isoformat()
109
+ if appointment["updated_at"] else None
110
+ ),
111
  }
112
 
113
+
114
  def validate_existing_appointment(appointment):
115
  """
116
  Validates the existing appointment's status for operations.
 
137
  Returns:
138
  int: Total duration in minutes.
139
  """
140
+ return sum(service.get("duration", 0) for service in services)
141
+
142
+ def to_in_appointments_db(appointment: AppointmentCreateRequest):
143
+ return {
144
+ "appointment_id": appointment.appointment_id,
145
+ "customer_id": appointment.customer_id, # You will update later when customer is added
146
+ "merchant_id": appointment.merchant_id,
147
+ "merchant_name": appointment.merchant_name,
148
+ "city": appointment.city,
149
+ "location_id": appointment.location_id,
150
+
151
+ # Extracted from merchant_address
152
+ "address_street": appointment.merchant_address.street,
153
+ "address_area": appointment.merchant_address.area,
154
+ "address_in_tcode": appointment.merchant_address.postcode,
155
+ "address_state": appointment.merchant_address.state,
156
+ "geo_location": (
157
+ f"POINT({appointment.merchant_address.location.coordinates[0]} "
158
+ f"{appointment.merchant_address.location.coordinates[1]})"
159
+ if appointment.merchant_address.location else None
160
+ ),
161
+
162
+ "appointment_date": appointment.appointment_date,
163
+ "appointment_time": appointment.appointment_time,
164
+
165
+ "status": appointment.status,
166
+ "total_amount": appointment.total_amount,
167
+ "discount_amount": appointment.discount,
168
+ "cleared_amount": appointment.cleared_amount,
169
+
170
+ "payment_status": appointment.payment_status,
171
+ "payment_mode": appointment.payment_mode,
172
+
173
+ "notes": appointment.notes,
174
+ }
175
+
176
+ def to_in_appointments_services_db(appointment_services, appointment_id):
177
+ services_data = []
178
+ for service in appointment_services:
179
+ services_data.append({
180
+ "appointment_service_id":uuid.uuid4(),
181
+ "appointment_id": appointment_id,
182
+ "service_id": service.service_id,
183
+ "service_name": service.name,
184
+ "duration_minutes": service.duration,
185
+ "unit_price": service.price,
186
+ "quantity": service.quantity,
187
+ "line_total": service.price * service.quantity,
188
+ "associate_id": service.associate_id,
189
+ "associate_name": service.associate_name,
190
+ })
191
+ return services_data
app/utils/json_utils.py CHANGED
@@ -4,6 +4,8 @@ Falls back to standard json if orjson is not available.
4
  """
5
  import logging
6
  from typing import Any, Union
 
 
7
 
8
  logger = logging.getLogger(__name__)
9
 
@@ -98,4 +100,17 @@ async def async_fast_loads(json_str: Union[str, bytes]) -> Any:
98
 
99
  async def async_fast_dumps_bytes(obj: Any) -> bytes:
100
  """Async wrapper for fast_dumps_bytes."""
101
- return fast_dumps_bytes(obj)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  """
5
  import logging
6
  from typing import Any, Union
7
+ from geoalchemy2.shape import to_shape
8
+
9
 
10
  logger = logging.getLogger(__name__)
11
 
 
100
 
101
  async def async_fast_dumps_bytes(obj: Any) -> bytes:
102
  """Async wrapper for fast_dumps_bytes."""
103
+ return fast_dumps_bytes(obj)
104
+
105
+
106
+
107
+ def format_geo_location(wkb_element):
108
+ if not wkb_element:
109
+ return None
110
+
111
+ point = to_shape(wkb_element) # Convert WKB → Shapely object
112
+
113
+ return {
114
+ "type": "Point",
115
+ "coordinates": [point.x, point.y]
116
+ }
requirements.txt CHANGED
@@ -10,4 +10,5 @@ python-jose[cryptography]==3.3.0
10
  passlib[bcrypt]==1.7.4
11
  razorpay==1.4.2
12
  orjson==3.11.3
13
- python-dotenv==1.0.0
 
 
10
  passlib[bcrypt]==1.7.4
11
  razorpay==1.4.2
12
  orjson==3.11.3
13
+ python-dotenv==1.0.0
14
+ GeoAlchemy2==0.18.1