-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscheduler.py
More file actions
230 lines (196 loc) · 10.1 KB
/
Copy pathscheduler.py
File metadata and controls
230 lines (196 loc) · 10.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
import os
import httpx
from datetime import datetime, timezone, timedelta
from apscheduler.schedulers.background import BackgroundScheduler
from database import SessionLocal
from models import Case, StatusEnum
import email_service
from gmail_ingestion import process_inbound_emails
import asyncio
from blockchain import transfer_funds
BASE_URL = os.environ.get("BASE_URL", "http://localhost:8000")
from dependencies import ADMIN_KEY
def check_disputed_cases():
print("[Scheduler] Checking for ripe disputed cases (past evidence window)...")
db = SessionLocal()
try:
# User requested 7-day timeout for disputes
seven_days_ago = datetime.now(timezone.utc) - timedelta(days=7)
cases = db.query(Case).filter(
Case.status == StatusEnum.DISPUTED,
Case.dispute_time.isnot(None),
Case.dispute_time <= seven_days_ago
).all()
for case in cases:
print(f"[Scheduler] Triggering adjudication for {case.case_id}")
# Trigger via HTTP to offload the heavy agentic process
try:
# Use httpx post but don't wait for completion if it's long, or run in thread
response = httpx.post(f"{BASE_URL}/adjudication/run", data={"caseId": case.case_id}, headers={"X-Admin-Key": ADMIN_KEY}, timeout=1.0)
response.raise_for_status()
except httpx.ReadTimeout:
# Expected since adjudication takes minutes
pass
except Exception as e:
print(f"[Scheduler] Error triggering {case.case_id}: {e}")
finally:
db.close()
def check_transaction_timeouts():
print("[Scheduler] Checking for 7-day transaction timeouts...")
db = SessionLocal()
try:
seven_days_ago = datetime.now(timezone.utc) - timedelta(days=7)
# Check payment requests
payment_cases = db.query(Case).filter(
Case.payment_request_time.isnot(None),
Case.payment_request_time <= seven_days_ago
).all()
for case in payment_cases:
print(f"[Scheduler] Auto-approving payment for {case.case_id}")
remittance = int(case.requested_payment_amount or 0)
if remittance > 0 and not case.seller_wallet:
print(f"[Scheduler] Cannot auto-approve payment for {case.case_id}: No seller wallet")
continue
try:
if remittance > 0:
asyncio.run(transfer_funds(case.seller_wallet, remittance, case.case_id))
except Exception as e:
print(f"[Scheduler] Blockchain Transfer Failed for {case.case_id}: {e}")
continue
case.payment_to_seller = int(case.payment_to_seller or 0) + remittance
case.payment_request_time = None
case.requested_payment_amount = None
if (int(case.payment_to_seller or 0) + int(case.refund_to_buyer or 0)) >= int(case.escrow_fund or 0):
case.status = StatusEnum.TRANSFERRED_TO_SELLER
db.commit()
email_service.send_payment_released(
case_id=case.case_id,
seller_name=case.seller, seller_email=case.seller_email,
buyer_name=case.buyer, buyer_email=case.buyer_email,
amount_eth=remittance / 1e18, closed=(case.status == StatusEnum.TRANSFERRED_TO_SELLER)
)
# Check refund requests
refund_cases = db.query(Case).filter(
Case.refund_request_time.isnot(None),
Case.refund_request_time <= seven_days_ago
).all()
for case in refund_cases:
print(f"[Scheduler] Auto-approving refund for {case.case_id}")
remittance = int(case.requested_refund_amount or 0)
if remittance > 0 and not case.buyer_wallet:
print(f"[Scheduler] Cannot auto-approve refund for {case.case_id}: No buyer wallet")
continue
try:
if remittance > 0:
asyncio.run(transfer_funds(case.buyer_wallet, remittance, case.case_id))
except Exception as e:
print(f"[Scheduler] Blockchain Transfer Failed for {case.case_id}: {e}")
continue
case.refund_to_buyer = int(case.refund_to_buyer or 0) + remittance
case.refund_request_time = None
case.requested_refund_amount = None
if (int(case.payment_to_seller or 0) + int(case.refund_to_buyer or 0)) >= int(case.escrow_fund or 0):
case.status = StatusEnum.TRANSFERRED_TO_BUYER
db.commit()
finally:
db.close()
def hourly_auto_distribute():
print("[Scheduler] Checking for DISTRIBUTED cases to execute on-chain transfer...")
db = SessionLocal()
try:
# Immediate Terminal States check for DECIDED_LOCKED cases after the 7-day appeal window
seven_days_ago = datetime.now(timezone.utc) - timedelta(days=7)
immediate_close = db.query(Case).filter(
Case.status == StatusEnum.DECIDED_LOCKED,
Case.determination_time.isnot(None),
Case.determination_time <= seven_days_ago
).all()
for case in immediate_close:
escrow_fund = int(case.escrow_fund or 0)
payment_to_seller = int(case.payment_to_seller or 0)
refund_to_buyer = int(case.refund_to_buyer or 0)
available = escrow_fund - payment_to_seller - refund_to_buyer
if available <= 0:
print(f"[Scheduler] Immediate close (No Escrow) for {case.case_id}")
case.status = StatusEnum.CLOSED_NO_ESCROW
db.commit()
elif int(case.seller_award or 0) == 0 and int(case.buyer_award or 0) == 0:
print(f"[Scheduler] Immediate close (No Award) for {case.case_id}")
case.status = StatusEnum.CLOSED_NO_AWARD
db.commit()
# Also check for DECIDED_LOCKED cases where 7-day appeal window has lapsed
seven_days_ago = datetime.now(timezone.utc) - timedelta(days=7)
lapsed_cases = db.query(Case).filter(
Case.status == StatusEnum.DECIDED_LOCKED,
Case.determination_time.isnot(None),
Case.determination_time <= seven_days_ago
).all()
for case in lapsed_cases:
print(f"[Scheduler] 7-day objection window closed for {case.case_id}. Distributing...")
case.status = StatusEnum.DISTRIBUTED
db.commit()
ready_cases = db.query(Case).filter(Case.status == StatusEnum.DISTRIBUTED).all()
for case in ready_cases:
try:
seller_award = int(case.seller_award or 0)
buyer_award = int(case.buyer_award or 0)
# Terminal States: No Escrow vs No Award
escrow_fund = int(case.escrow_fund or 0)
payment_to_seller = int(case.payment_to_seller or 0)
refund_to_buyer = int(case.refund_to_buyer or 0)
available = escrow_fund - payment_to_seller - refund_to_buyer
if available <= 0:
case.status = StatusEnum.CLOSED_NO_ESCROW
db.commit()
continue
if seller_award == 0 and buyer_award == 0:
case.status = StatusEnum.CLOSED_NO_AWARD
db.commit()
continue
# Process Seller Transfer
if seller_award > 0 and int(case.seller_payout or 0) == 0:
if case.seller_wallet:
try:
asyncio.run(transfer_funds(case.seller_wallet, seller_award, case.case_id))
case.seller_payout = seller_award
db.commit()
except Exception as e:
print(f"Seller transfer error for {case.case_id}: {e}")
else:
print(f"No seller wallet for {case.case_id}. Skipping seller transfer.")
# Process Buyer Transfer
if buyer_award > 0 and int(case.buyer_payout or 0) == 0:
if case.buyer_wallet:
try:
asyncio.run(transfer_funds(case.buyer_wallet, buyer_award, case.case_id))
case.buyer_payout = buyer_award
db.commit()
except Exception as e:
print(f"Buyer transfer error for {case.case_id}: {e}")
else:
print(f"No buyer wallet for {case.case_id}. Skipping buyer transfer.")
# Final Status Update & Notification
seller_ok = (seller_award == 0) or (int(case.seller_payout or 0) > 0)
buyer_ok = (buyer_award == 0) or (int(case.buyer_payout or 0) > 0)
if seller_ok and buyer_ok:
case.status = StatusEnum.CLOSED
db.commit()
email_service.send_award_distributed(
case_id=case.case_id,
seller_name=case.seller, seller_email=case.seller_email, seller_award_eth=seller_award/1e18,
buyer_name=case.buyer, buyer_email=case.buyer_email, buyer_award_eth=buyer_award/1e18
)
except Exception as e:
print(f"Error distributing funds for {case.case_id}: {e}")
finally:
db.close()
def start_scheduler():
scheduler = BackgroundScheduler()
# Run checks every 5 minutes
scheduler.add_job(check_disputed_cases, 'interval', minutes=5)
scheduler.add_job(check_transaction_timeouts, 'interval', minutes=5)
scheduler.add_job(process_inbound_emails, 'interval', minutes=5)
# Run distribution hourly
scheduler.add_job(hourly_auto_distribute, 'interval', minutes=60)
scheduler.start()
return scheduler