Karan6124 commited on
Commit
c7ee5b1
·
1 Parent(s): 2f04cf7

feat: implement database CRUD operations for users, watchlists, alerts, history, and payments

Browse files
Files changed (1) hide show
  1. backend/app/database/crud.py +239 -1
backend/app/database/crud.py CHANGED
@@ -1 +1,239 @@
1
- #
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import uuid
2
+ import datetime
3
+ from typing import List, Optional
4
+ from sqlalchemy import select, update, delete, and_
5
+ from sqlalchemy.ext.asyncio import AsyncSession
6
+ from sqlalchemy.dialects.postgresql import insert as pg_insert
7
+ from backend.app.database import models
8
+ from backend.app.schemas import schemas
9
+
10
+ # ==========================================
11
+ # USER OPERATIONS
12
+ # ==========================================
13
+
14
+ async def get_user(db: AsyncSession, user_id: uuid.UUID) -> Optional[models.User]:
15
+ result = await db.execute(select(models.User).where(models.User.id == user_id))
16
+ return result.scalars().first()
17
+
18
+ async def get_user_by_email(db: AsyncSession, email: str) -> Optional[models.User]:
19
+ result = await db.execute(select(models.User).where(models.User.email == email))
20
+ return result.scalars().first()
21
+
22
+ async def get_user_by_google_id(db: AsyncSession, google_id: str) -> Optional[models.User]:
23
+ result = await db.execute(select(models.User).where(models.User.google_id == google_id))
24
+ return result.scalars().first()
25
+
26
+ async def create_user(db: AsyncSession, user_in: schemas.UserBase, google_id: str) -> models.User:
27
+ db_user = models.User(
28
+ email=user_in.email,
29
+ full_name=user_in.full_name,
30
+ picture_url=user_in.picture_url,
31
+ google_id=google_id,
32
+ credits=5, # Starting free credits
33
+ last_credit_refresh=datetime.datetime.now(datetime.timezone.utc)
34
+ )
35
+ db.add(db_user)
36
+ await db.commit()
37
+ await db.refresh(db_user)
38
+ return db_user
39
+
40
+ async def refresh_user_credits(db: AsyncSession, user: models.User) -> models.User:
41
+ """
42
+ Checks if 7 days have passed since the last credit refresh.
43
+ If so, resets credits back to 5.
44
+ """
45
+ now = datetime.datetime.now(datetime.timezone.utc)
46
+ time_elapsed = now - user.last_credit_refresh
47
+
48
+ if time_elapsed >= datetime.timedelta(days=7):
49
+ user.credits = 5
50
+ user.last_credit_refresh = now
51
+ db.add(user)
52
+ await db.commit()
53
+ await db.refresh(user)
54
+ return user
55
+
56
+ async def deduct_user_credit(db: AsyncSession, user_id: uuid.UUID) -> bool:
57
+ """
58
+ Deducts 1 credit from the user's account.
59
+ Returns True if deduction succeeded, False if user has 0 credits.
60
+ """
61
+ user = await get_user(db, user_id)
62
+ if not user or user.credits <= 0:
63
+ return False
64
+
65
+ user.credits -= 1
66
+ db.add(user)
67
+ await db.commit()
68
+ return True
69
+
70
+
71
+ # ==========================================
72
+ # WATCHLIST OPERATIONS
73
+ # ==========================================
74
+
75
+ async def get_user_watchlist(db: AsyncSession, user_id: uuid.UUID) -> List[models.Watchlist]:
76
+ result = await db.execute(
77
+ select(models.Watchlist)
78
+ .where(models.Watchlist.user_id == user_id)
79
+ .order_index(models.Watchlist.created_at.desc() if hasattr(models.Watchlist, 'order_index') else None)
80
+ )
81
+ # Simple order fallback if order_index doesn't apply
82
+ result = await db.execute(
83
+ select(models.Watchlist)
84
+ .where(models.Watchlist.user_id == user_id)
85
+ .order_by(models.Watchlist.created_at.desc())
86
+ )
87
+ return list(result.scalars().all())
88
+
89
+ async def add_to_watchlist(db: AsyncSession, user_id: uuid.UUID, ticker: str) -> models.Watchlist:
90
+ # Check if already exists to prevent duplicate entries
91
+ existing = await db.execute(
92
+ select(models.Watchlist).where(
93
+ and_(models.Watchlist.user_id == user_id, models.Watchlist.ticker == ticker)
94
+ )
95
+ )
96
+ if existing.scalars().first():
97
+ return existing.scalars().first()
98
+
99
+ db_watchlist = models.Watchlist(user_id=user_id, ticker=ticker.upper())
100
+ db.add(db_watchlist)
101
+ await db.commit()
102
+ await db.refresh(db_watchlist)
103
+ return db_watchlist
104
+
105
+ async def remove_from_watchlist(db: AsyncSession, user_id: uuid.UUID, ticker: str) -> bool:
106
+ result = await db.execute(
107
+ delete(models.Watchlist).where(
108
+ and_(models.Watchlist.user_id == user_id, models.Watchlist.ticker == ticker.upper())
109
+ )
110
+ )
111
+ await db.commit()
112
+ return result.rowcount > 0
113
+
114
+
115
+ # ==========================================
116
+ # ALERT OPERATIONS
117
+ # ==========================================
118
+
119
+ async def get_user_alerts(db: AsyncSession, user_id: uuid.UUID) -> List[models.Alert]:
120
+ result = await db.execute(
121
+ select(models.Alert)
122
+ .where(models.Alert.user_id == user_id)
123
+ .order_by(models.Alert.created_at.desc())
124
+ )
125
+ return list(result.scalars().all())
126
+
127
+ async def get_active_alerts(db: AsyncSession) -> List[models.Alert]:
128
+ result = await db.execute(
129
+ select(models.Alert).where(models.Alert.is_active == True)
130
+ )
131
+ return list(result.scalars().all())
132
+
133
+ async def create_alert(db: AsyncSession, user_id: uuid.UUID, alert_in: schemas.AlertCreate) -> models.Alert:
134
+ db_alert = models.Alert(
135
+ user_id=user_id,
136
+ ticker=alert_in.ticker.upper(),
137
+ target_price=alert_in.target_price,
138
+ condition=alert_in.condition.lower(),
139
+ is_active=True
140
+ )
141
+ db.add(db_alert)
142
+ await db.commit()
143
+ await db.refresh(db_alert)
144
+ return db_alert
145
+
146
+ async def deactivate_alert(db: AsyncSession, alert_id: uuid.UUID) -> bool:
147
+ result = await db.execute(
148
+ update(models.Alert)
149
+ .where(models.Alert.id == alert_id)
150
+ .values(is_active=False)
151
+ )
152
+ await db.commit()
153
+ return result.rowcount > 0
154
+
155
+
156
+ # ==========================================
157
+ # STOCK HISTORY OPERATIONS
158
+ # ==========================================
159
+
160
+ async def get_stock_history(db: AsyncSession, ticker: str, limit: int = 100) -> List[models.StockHistory]:
161
+ result = await db.execute(
162
+ select(models.StockHistory)
163
+ .where(models.StockHistory.ticker == ticker.upper())
164
+ .order_by(models.StockHistory.timestamp.desc())
165
+ .limit(limit)
166
+ )
167
+ # Return in chronological order (oldest to newest) for indicators
168
+ history = list(result.scalars().all())
169
+ history.reverse()
170
+ return history
171
+
172
+ async def insert_stock_candle(db: AsyncSession, candle: schemas.StockHistoryBase) -> None:
173
+ """
174
+ Inserts a single candle record. If it already exists, do nothing.
175
+ """
176
+ stmt = pg_insert(models.StockHistory).values(
177
+ ticker=candle.ticker.upper(),
178
+ timestamp=candle.timestamp,
179
+ open=candle.open,
180
+ high=candle.high,
181
+ low=candle.low,
182
+ close=candle.close,
183
+ volume=candle.volume
184
+ )
185
+ # PostgreSQL specific upsert: do nothing on conflict
186
+ stmt = stmt.on_conflict_do_nothing(index_elements=["ticker", "timestamp"])
187
+ await db.execute(stmt)
188
+ await db.commit()
189
+
190
+
191
+ # ==========================================
192
+ # PAYMENT TRANSACTION OPERATIONS
193
+ # ==========================================
194
+
195
+ async def create_payment_transaction(
196
+ db: AsyncSession, user_id: uuid.UUID, order_id: str, amount: int, credits_credited: int
197
+ ) -> models.PaymentTransaction:
198
+ db_tx = models.PaymentTransaction(
199
+ user_id=user_id,
200
+ razorpay_order_id=order_id,
201
+ amount=amount,
202
+ status="created",
203
+ credits_credited=credits_credited
204
+ )
205
+ db.add(db_tx)
206
+ await db.commit()
207
+ await db.refresh(db_tx)
208
+ return db_tx
209
+
210
+ async def capture_payment_transaction(
211
+ db: AsyncSession, order_id: str, payment_id: str
212
+ ) -> Optional[models.PaymentTransaction]:
213
+ """
214
+ Captures a pending transaction, updates status, and credits the user.
215
+ Uses a transaction block to ensure atomic operations.
216
+ """
217
+ # 1. Fetch transaction
218
+ result = await db.execute(
219
+ select(models.PaymentTransaction).where(models.PaymentTransaction.razorpay_order_id == order_id)
220
+ )
221
+ tx = result.scalars().first()
222
+
223
+ if not tx or tx.status == "captured":
224
+ return tx
225
+
226
+ # 2. Update transaction
227
+ tx.razorpay_payment_id = payment_id
228
+ tx.status = "captured"
229
+ db.add(tx)
230
+
231
+ # 3. Credit the user
232
+ user = await get_user(db, tx.user_id)
233
+ if user:
234
+ user.credits += tx.credits_credited
235
+ db.add(user)
236
+
237
+ await db.commit()
238
+ await db.refresh(tx)
239
+ return tx