Files changed (1) hide show
  1. app.py +285 -575
app.py CHANGED
@@ -13,7 +13,7 @@ from werkzeug.exceptions import HTTPException
13
  from sqlalchemy import create_engine, Column, Integer, String, Boolean, Float, DateTime, ForeignKey, Text, and_, or_, text, inspect
14
  from sqlalchemy.orm import relationship, sessionmaker, declarative_base, Session
15
 
16
- # مدیریت ایمن کتابخانه‌های امنیتی و احراز هویت
17
  try:
18
  from jose import JWTError, jwt
19
  except ImportError:
@@ -28,7 +28,7 @@ import bcrypt
28
  logging.basicConfig(level=logging.INFO)
29
  logger = logging.getLogger("production_backend")
30
 
31
- # کلید پردازش‌های ابری هوشمند (Secrets)
32
  AI_CORE_KEY = os.getenv("GEMINI_API_KEY") or os.getenv("SECRET")
33
 
34
  try:
@@ -38,7 +38,7 @@ except ImportError:
38
  logger.warning("The 'google-genai' package is not installed. AI features will fallback.")
39
 
40
  # =========================================================================
41
- # بخش ۲: اتصال پایگاه داده و ساختار بومی دیتابیس
42
  # =========================================================================
43
  DATABASE_URL = os.getenv(
44
  "DATABASE_URL",
@@ -66,11 +66,10 @@ except Exception as db_init_err:
66
  SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
67
  Base = declarative_base()
68
 
69
- # تعریف تابع زمان بومی سرور
70
  def get_now():
71
  return datetime.now(timezone.utc).replace(tzinfo=None)
72
 
73
- # فرمول مبدل تقویم میلادی به شمسی (Jalali Converter) جهت لاگ دقیق تاریخ‌های ایران (بند ۱۱)
74
  def gregorian_to_jalali(g_y, g_m, g_d):
75
  g_days_in_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
76
  j_days_in_month = [31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29]
@@ -127,13 +126,6 @@ KNOWLEDGE_BASE = [
127
  "minimum_rank_r1": 100,
128
  "minimum_rank_r2": 200
129
  },
130
- {
131
- "university": "دانشگاه صنعتی امیرکبیر",
132
- "ranking_tier": "ب",
133
- "fields": ["صنایع", "عمران", "شیمی"],
134
- "minimum_rank_r1": 1000,
135
- "minimum_rank_r2": 1500
136
- },
137
  {
138
  "university": "دانشگاه علوم پزشکی تهران",
139
  "ranking_tier": "الف",
@@ -153,15 +145,15 @@ class User(Base):
153
  email = Column(String, unique=True, index=True, nullable=False)
154
  hashed_password = Column(String, nullable=False)
155
  role = Column(String, default="student") # 'student', 'counselor', 'school_representative'
156
- name = Column(String, nullable=True) # نام و نام خانوادگی کاربر (بند ۶)
157
  is_verified = Column(Boolean, default=False)
158
  balance = Column(Float, default=0.0)
159
- rating = Column(Float, default=5.0)
160
  phone_number = Column(String, nullable=True)
161
- city = Column(String, nullable=True)
162
- grade = Column(String, nullable=True) # مقطع تحصیلی (ششم تا دوازدهم)
163
- major = Column(String, nullable=True) # رشته تحصیلی داوطلب
164
- profile_pic = Column(Text, nullable=True) # عکس پروفایل به فرم Base64
165
 
166
  counselor_profile = relationship("CounselorProfile", back_populates="user", uselist=False, cascade="all, delete-orphan")
167
  school_profile = relationship("SchoolProfile", back_populates="user", uselist=False, cascade="all, delete-orphan")
@@ -172,9 +164,9 @@ class CounselorProfile(Base):
172
  id = Column(Integer, primary_key=True, index=True)
173
  user_id = Column(Integer, ForeignKey("users.id"))
174
  specialty = Column(String, nullable=True)
175
- resume = Column(Text, nullable=True) # آپلود مدرک رزومه (بیس ۶۴ تا سقف ۲۰ مگابایت)
176
  bio = Column(Text, nullable=True)
177
- hourly_rate = Column(Float, default=120000.0)
178
  deactivated_days_json = Column(Text, default="[]") # روزهای غیرفعال شده توسط مشاور (بند ۷)
179
 
180
  user = relationship("User", back_populates="counselor_profile")
@@ -191,8 +183,8 @@ class SchoolProfile(Base):
191
  address = Column(Text, nullable=True)
192
  phone_number = Column(String, nullable=True)
193
  founding_year = Column(Integer, nullable=True)
194
- rating = Column(Float, default=5.0)
195
- profile_pic = Column(Text, nullable=True) # بیس ۶۴ تصویر آموزشگاه
196
 
197
  user = relationship("User", back_populates="school_profile")
198
  courses = relationship("SchoolCourse", back_populates="school", cascade="all, delete-orphan")
@@ -241,16 +233,17 @@ class Booking(Base):
241
  slot_id = Column(Integer, ForeignKey("time_slots.id"))
242
  student_id = Column(Integer, ForeignKey("users.id"))
243
  counselor_id = Column(Integer, ForeignKey("users.id"))
244
- status = Column(String, default="active") # active, cancelled
245
- payment_status = Column(String, default="paid") # paid, pending_payment, expired
246
  payment_due_until = Column(DateTime, nullable=True)
247
  cost = Column(Float, default=0.0)
 
248
 
249
 
250
  class CustomQuiz(Base):
251
  __tablename__ = "custom_quizzes"
252
  id = Column(Integer, primary_key=True, index=True)
253
- counselor_id = Column(Integer, ForeignKey("users.id"), nullable=True) # نال برای کوئیزهای پیش‌فرض سیستم
254
  title = Column(String, nullable=False)
255
  questions_json = Column(Text, nullable=False)
256
 
@@ -298,7 +291,7 @@ class Review(Base):
298
  __tablename__ = "reviews"
299
  id = Column(Integer, primary_key=True, index=True)
300
  student_id = Column(Integer, ForeignKey("users.id"))
301
- booking_id = Column(Integer, ForeignKey("bookings.id"), nullable=True) # مجاز به نال برای امتیازدهی مستقیم به آموزشگاه (بند ۱۴)
302
  counselor_id = Column(Integer, ForeignKey("users.id"), nullable=True)
303
  school_id = Column(Integer, ForeignKey("school_profiles.id"), nullable=True)
304
  rating = Column(Float, default=5.0)
@@ -316,7 +309,7 @@ class Notification(Base):
316
  created_at = Column(DateTime, default=get_now)
317
 
318
 
319
- # مدل جدید جهت مدیریت تسویه حساب‌های پلتفرم (بند ۴ و ۵)
320
  class SettlementRequest(Base):
321
  __tablename__ = "settlement_requests"
322
  id = Column(Integer, primary_key=True, index=True)
@@ -326,7 +319,7 @@ class SettlementRequest(Base):
326
  created_at = Column(DateTime, default=get_now)
327
 
328
 
329
- # مدل جدید جهت ردیابی بازدید آموزشگاه بر اساس حساب کاربری (بند ۹)
330
  class PageView(Base):
331
  __tablename__ = "page_views"
332
  id = Column(Integer, primary_key=True, index=True)
@@ -528,114 +521,7 @@ def seed_initial_data(db: Session):
528
  new_quiz = CustomQuiz(counselor_id=None, title=gq["title"], questions_json=gq["questions"])
529
  db.add(new_quiz)
530
  db.commit()
531
-
532
- seed_users = [
533
- {"email": "student@demo.com", "password": "demo1234", "role": "student", "balance": 950000.0, "phone": "09121111111", "city": "تهران", "name": "امیرحسین رضایی", "grade": "یازدهم", "major": "علوم تجربی"},
534
- {"email": "student@test.com", "password": "student123", "role": "student", "balance": 650000.0, "phone": "09122222222", "city": "تهران", "name": "کیان کریمی", "grade": "دهم", "major": "ریاضی فیزیک"},
535
- {"email": "counselor1@demo.com", "password": "demo1234", "role": "counselor", "rating": 4.8, "city": "تهران", "name": "استاد محمدی"},
536
- {"email": "counselor2@demo.com", "password": "demo1234", "role": "counselor", "rating": 4.5, "city": "شیراز", "name": "استاد کرمی"},
537
- {"email": "counselor@test.com", "password": "counselor123", "role": "counselor", "rating": 2.8, "city": "تهران", "name": "دکتر حسینی"}, # نمونه امتیاز زیر ۳ برای پپ لود اخطار انضباطی
538
- {"email": "school@demo.com", "password": "demo1234", "role": "school_representative", "city": "اصفهان", "name": "مهندس نوری"},
539
- {"email": "school@test.com", "password": "school123", "role": "school_representative", "city": "تهران", "name": "آکادمی اندیشه"}
540
- ]
541
-
542
- for u_data in seed_users:
543
- try:
544
- existing = db.query(User).filter(User.email == u_data["email"]).first()
545
- hashed_pwd = get_password_hash(u_data["password"])
546
-
547
- if existing:
548
- existing.hashed_password = hashed_pwd
549
- existing.role = u_data["role"]
550
- existing.is_verified = True
551
- existing.name = u_data.get("name", existing.name)
552
- existing.grade = u_data.get("grade", existing.grade)
553
- existing.major = u_data.get("major", existing.major)
554
- if "balance" in u_data:
555
- existing.balance = u_data["balance"]
556
- if "rating" in u_data:
557
- existing.rating = u_data["rating"]
558
- if "phone" in u_data:
559
- existing.phone_number = u_data["phone"]
560
- if "city" in u_data:
561
- existing.city = u_data["city"]
562
- else:
563
- new_user = User(
564
- email=u_data["email"],
565
- hashed_password=hashed_pwd,
566
- role=u_data["role"],
567
- name=u_data.get("name"),
568
- is_verified=True,
569
- balance=u_data.get("balance", 0.0),
570
- rating=u_data.get("rating", 5.0),
571
- phone_number=u_data.get("phone"),
572
- city=u_data.get("city"),
573
- grade=u_data.get("grade"),
574
- major=u_data.get("major")
575
- )
576
- db.add(new_user)
577
- db.commit()
578
-
579
- current_user = db.query(User).filter(User.email == u_data["email"]).first()
580
- if not current_user:
581
- continue
582
-
583
- if u_data["role"] == "counselor":
584
- p = db.query(CounselorProfile).filter(CounselorProfile.user_id == current_user.id).first()
585
- if not p:
586
- p = CounselorProfile(
587
- user_id=current_user.id,
588
- specialty="برنامه‌ریزی کنکور و هدایت تحصیلی",
589
- resume="مدرک تایید شده بستر تراز تحصیلی بومی تاپ دانش",
590
- bio="متخصص افزایش راندمان مطالعه داوطلب با تکیه بر تحلیل مستمر سبک‌های یادگیری",
591
- hourly_rate=150000.0,
592
- deactivated_days_json="[]"
593
- )
594
- db.add(p)
595
-
596
- now = get_now()
597
- has_slot = db.query(TimeSlot).filter(TimeSlot.counselor_id == current_user.id, TimeSlot.is_booked == False).first()
598
- if not has_slot:
599
- slot = TimeSlot(
600
- counselor_id=current_user.id,
601
- start_time=now + timedelta(days=1, hours=10),
602
- end_time=now + timedelta(days=1, hours=11),
603
- is_booked=False
604
- )
605
- db.add(slot)
606
-
607
- elif u_data["role"] == "school_representative":
608
- s = db.query(SchoolProfile).filter(SchoolProfile.user_id == current_user.id).first()
609
- if not s:
610
- s = SchoolProfile(
611
- user_id=current_user.id,
612
- name=u_data.get("name"),
613
- subscription_active_until=get_now() + timedelta(days=60),
614
- is_sponsored=True,
615
- bio="برترین آکادمی تستی کنکور با کادر اساتید برجسته",
616
- address="خیابان ولیعصر، پلاک ۱۲",
617
- phone_number="02122222222",
618
- founding_year=1395,
619
- rating=5.0
620
- )
621
- db.add(s)
622
- db.flush()
623
-
624
- course = db.query(SchoolCourse).filter(SchoolCourse.school_id == s.id).first()
625
- if not course:
626
- course = SchoolCourse(
627
- school_id=s.id,
628
- course_name="ریاضی جامع کنکور",
629
- teacher_name="استاد احمدی",
630
- teaching_style="روش‌های محاسباتی تستی و تشریحی"
631
- )
632
- db.add(course)
633
- db.commit()
634
- except Exception as item_err:
635
- db.rollback()
636
- logger.warning(f"Seeding skipped for {u_data['email']}: {item_err}")
637
-
638
- logger.info("Forced Check-Seeding completed.")
639
  except Exception as e:
640
  logger.error(f"Seeding process crashed completely: {e}", exc_info=True)
641
  db.rollback()
@@ -679,11 +565,11 @@ with flask_app.app_context():
679
  try:
680
  seed_initial_data(db)
681
  except Exception as seed_err:
682
- logger.error(f"Error seeding database: {seed_err}", exc_info=True)
683
  finally:
684
  db.close()
685
  except Exception as startup_db_err:
686
- logger.warning(f"Database setup error: {startup_db_err}")
687
 
688
 
689
  @flask_app.errorhandler(Exception)
@@ -694,7 +580,7 @@ def global_exception_handler(exc):
694
  return jsonify({"detail": "سرور موقتاً شلوغ است. لطفاً چند لحظه دیگر مجدداً تلاش کنید."}), 500
695
 
696
  # ==========================================
697
- # بخش ۹: کلیه آدرس‌های APIها
698
  # ==========================================
699
 
700
  @flask_app.route("/api/auth/register", methods=["POST"])
@@ -751,7 +637,7 @@ def auth_register():
751
  elif role == "school_representative":
752
  s_prof = SchoolProfile(
753
  user_id=new_user.id,
754
- name=name or "آموزشگاه جدید",
755
  subscription_active_until=get_now(),
756
  profile_pic=profile_pic
757
  )
@@ -784,16 +670,16 @@ def auth_login():
784
  if not user or not verify_password(password, user.hashed_password):
785
  return jsonify({"detail": "ایمیل یا رمز عبور اشتباه است."}), 400
786
 
787
- # سیستم ثبت اعلان هوشمند یکباره ورود (بند ۲)
788
  existing_login_notif = db.query(Notification).filter(
789
  Notification.user_id == user.id,
790
- Notification.text.like("%ورود موفقیت‌آمیز به سامانه%")
791
  ).first()
792
 
793
  if not existing_login_notif:
794
  notif = Notification(
795
  user_id=user.id,
796
- text=f"ورود موفقیت‌آمیز به سامانه در تاریخ {get_shamsi_date_str(get_now())}",
797
  role=user.role,
798
  is_read=False
799
  )
@@ -817,18 +703,17 @@ def auth_login():
817
  @login_required
818
  def get_user_profile():
819
  current_user = g.current_user
820
- db = get_db()
821
 
822
- # تعیین انضباطی یا تعلیق بر اساس میانگین امتیاز (بند ۱۱)
823
  requires_rating_warning = False
824
- if current_user.role == "counselor" and current_user.rating < 3.0:
825
  requires_rating_warning = True
826
 
827
  return jsonify({
828
  "id": current_user.id,
829
  "email": current_user.email,
830
  "role": current_user.role,
831
- "name": current_user.name or current_user.email.split("@")[0], # برگرداندن نام به عنوان شاخص اصلی (بند ۶)
832
  "balance": current_user.balance,
833
  "rating": current_user.rating,
834
  "phone_number": current_user.phone_number,
@@ -889,7 +774,7 @@ def update_user_profile():
889
  return jsonify({"detail": "سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید."}), 503
890
 
891
  # ==========================================
892
- # دایرکتوری مشاوران با فیلتر بومی هوشمند (بند ۳)
893
  # ==========================================
894
 
895
  @flask_app.route("/api/counselors", methods=["GET"])
@@ -897,20 +782,19 @@ def list_active_counselors():
897
  db = get_db()
898
  name = request.args.get("name")
899
  city = request.args.get("city")
900
- student_city = request.args.get("student_city") # برای اعمال اتوماتیک فیلترینگ منطقه‌ای
901
 
902
  try:
903
  query = db.query(User).filter(User.role == "counselor", User.is_verified == True)
904
 
905
- # ۱. فیلترینگ هوشمند منطقه‌ای (بند ۳)
906
  target_city = city or student_city
907
  is_fallback = False
908
 
 
909
  if target_city:
910
  regional_query = query.filter(User.city.ilike(f"%{target_city}%"))
911
  counselors = regional_query.all()
912
  if not counselors:
913
- # در صورت عدم وجود مشاور در منطقه، سایر مشاوران به صورت فال‌بک برگردانده می‌شوند
914
  is_fallback = True
915
  counselors = query.all()
916
  else:
@@ -965,7 +849,7 @@ def list_counselor_available_slots(counselor_id):
965
  return jsonify({"detail": "سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید."}), 503
966
 
967
  # ==========================================
968
- # رزرو جلسات با کسر ۷ درصد کارمزد پلتفرم (بند ۴)
969
  # ==========================================
970
 
971
  @flask_app.route("/api/bookings", methods=["POST"])
@@ -995,7 +879,7 @@ def book_session():
995
  if current_user.balance < cost:
996
  return jsonify({"detail": "موجودی حساب کافی نیست. لطفاً کیف پول خود را شارژ نمایید."}), 400
997
 
998
- # محاسبه کسر سهم و کارمزد ۷٪ پلتفرم به صورت پویا (بند ۴)
999
  platform_fee = cost * 0.07
1000
  net_to_counselor = cost - platform_fee
1001
 
@@ -1016,7 +900,7 @@ def book_session():
1016
  db.commit()
1017
 
1018
  return jsonify({
1019
- "message": "رزرو جلسه با موفقیت انجام شد.",
1020
  "remaining_balance": current_user.balance,
1021
  "session_start": slot.start_time.isoformat()
1022
  }), 200
@@ -1025,8 +909,95 @@ def book_session():
1025
  db.rollback()
1026
  return jsonify({"detail": "سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید."}), 503
1027
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1028
  # ========================================================
1029
- # سیستم تسویه حساب مشاوران (بند ۴)
1030
  # ========================================================
1031
 
1032
  @flask_app.route("/api/counselor/request-settlement", methods=["POST"])
@@ -1040,7 +1011,7 @@ def counselor_request_settlement():
1040
  return jsonify({"detail": "دسترسی غیرمجاز"}), 403
1041
 
1042
  if current_user.balance <= 0:
1043
- return jsonify({"detail": "موجودی کیف پول شما صفر یا منفی است و امکان تسویه وجود ندارد."}), 400
1044
 
1045
  req = SettlementRequest(
1046
  user_id=current_user.id,
@@ -1049,18 +1020,18 @@ def counselor_request_settlement():
1049
  )
1050
  db.add(req)
1051
 
1052
- # قفل موقت تراز مالی کلاینت تا زمان تایید نهایی توسط ادمین
1053
  current_user.balance = 0.0
1054
  db.commit()
1055
 
1056
- return jsonify({"message": "درخواست تسویه حساب شما با موفقیت ثبت شد و در صف تایید نهایی ادمین قرار گرفت."}), 200
1057
  except Exception as e:
1058
  logger.error(f"Error requesting settlement: {e}")
1059
  db.rollback()
1060
- return jsonify({"detail": "بروز خطا در سرور مالی."}), 500
1061
 
1062
  # ========================================================
1063
- # مدیریت مالی سه‌گانه داوطلبان، مشاوران و آموزشگاه‌ها (بند ۵)
1064
  # ========================================================
1065
 
1066
  @flask_app.route("/api/financial/summary", methods=["GET"])
@@ -1111,9 +1082,8 @@ def get_role_financial_summary():
1111
  }), 200
1112
 
1113
  elif current_user.role == "school_representative":
1114
- # آموزشگاه‌ها هزینه‌های عودت وجه یا پرداخت‌های اسپانسری دارند
1115
  spent_ads = db.query(text("SUM(amount)")).from_statement(
1116
- text("SELECT amount FROM settlement_requests WHERE user_id = :uid") # فرض شبیه‌سازی فاکتورهای پلتفرم
1117
  ).params(uid=current_user.id).scalar() or 0.0
1118
 
1119
  return jsonify({
@@ -1124,10 +1094,10 @@ def get_role_financial_summary():
1124
 
1125
  except Exception as e:
1126
  logger.error(f"Error building financial summary: {e}")
1127
- return jsonify({"detail": "خطا در استخراج آمارهای مالی."}), 500
1128
 
1129
  # ========================================================
1130
- # ردیابی بازدید آموزشگاه بر اساس شناسه کاربری (بند ۹)
1131
  # ========================================================
1132
 
1133
  @flask_app.route("/api/schools/<int:school_id>/visit", methods=["POST"])
@@ -1137,7 +1107,7 @@ def track_school_page_view(school_id):
1137
  current_user = g.current_user
1138
 
1139
  try:
1140
- # جلوگیری از ثبت مکرر بازدید توسط یک کاربر خاص در بازه‌های کوتاه
1141
  recent = db.query(PageView).filter(
1142
  PageView.school_id == school_id,
1143
  PageView.student_id == current_user.id,
@@ -1156,7 +1126,7 @@ def track_school_page_view(school_id):
1156
  except Exception as e:
1157
  logger.error(f"Error tracking visit: {e}")
1158
  db.rollback()
1159
- return jsonify({"detail": "خطا در ثبت ترافیک پلتفرم."}), 500
1160
 
1161
 
1162
  @flask_app.route("/api/schools/<int:school_id>/analytics", methods=["GET"])
@@ -1166,11 +1136,9 @@ def get_school_analytics_metrics(school_id):
1166
 
1167
  try:
1168
  total_views = db.query(PageView).filter(PageView.school_id == school_id).count()
1169
-
1170
- # گرفتن توزیع دقیق امتیازات (بند ۹)
1171
  reviews = db.query(Review).filter(Review.school_id == school_id).all()
1172
- distribution = [0, 0, 0, 0, 0] # ستاره ۱ تا ۵
1173
- avg_rating = 5.0
1174
 
1175
  if reviews:
1176
  for r in reviews:
@@ -1185,11 +1153,11 @@ def get_school_analytics_metrics(school_id):
1185
  "ratings_distribution": distribution
1186
  }), 200
1187
  except Exception as e:
1188
- logger.error(f"Error fetching analytics: {e}")
1189
- return jsonify({"detail": "خطا در لود اطلاعات."}), 500
1190
 
1191
  # ========================================================
1192
- # ماژول بررسی نوبت‌های منقضی نشده ۲۴ ساعته جهت امتیازدهی (بند ۱۰)
1193
  # ========================================================
1194
 
1195
  @flask_app.route("/api/student/pending-reviews", methods=["GET"])
@@ -1200,9 +1168,8 @@ def get_student_pending_reviews_for_counselors():
1200
 
1201
  try:
1202
  if current_user.role != "student":
1203
- return jsonify({"detail": "مخصوص داوطلبان"}), 403
1204
 
1205
- # فیلتر جلساتی که از شروع آن‌ها ۲۴ ساعت گذشته و هنوز فاقد رکورد امتیازدهی هستند
1206
  boundary_time = get_now() - timedelta(hours=24)
1207
 
1208
  bookings = db.query(Booking).join(TimeSlot).filter(
@@ -1229,7 +1196,7 @@ def get_student_pending_reviews_for_counselors():
1229
  return jsonify({"detail": "خطا در ارزیابی نوبت‌های فعال داوطلب."}), 500
1230
 
1231
  # ==========================================
1232
- # دایرکتوری آموزشگاه‌ها با فیلتر بومی هوشمند (بند ۳)
1233
  # ==========================================
1234
 
1235
  @flask_app.route("/api/schools", methods=["GET"])
@@ -1246,6 +1213,7 @@ def list_and_filter_schools():
1246
  target_city = city or student_city
1247
  is_fallback = False
1248
 
 
1249
  if target_city:
1250
  regional_query = query.join(User, User.id == SchoolProfile.user_id).filter(User.city.ilike(f"%{target_city}%"))
1251
  schools = regional_query.all()
@@ -1283,9 +1251,6 @@ def list_and_filter_schools():
1283
  logger.error(f"Error querying schools: {e}")
1284
  return jsonify({"detail": "سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید."}), 503
1285
 
1286
- # ========================================================
1287
- # سیستم امتیازدهی یکپارچه به مشاوران و آموزشگاه‌ها (بند ۱۰ و ۱۴)
1288
- # ========================================================
1289
 
1290
  @flask_app.route("/api/reviews", methods=["POST"])
1291
  @login_required
@@ -1313,394 +1278,23 @@ def create_session_review():
1313
  )
1314
  db.add(review)
1315
 
1316
- # به‌روزرسانی زنده برآیند امتیازات (بند ۱۰ و ۱۴)
1317
  if counselor_id:
1318
  counselor = db.query(User).filter(User.id == counselor_id).first()
1319
  if counselor:
1320
- counselor.rating = (counselor.rating + rating) / 2.0
1321
  elif school_id:
1322
  school = db.query(SchoolProfile).filter(SchoolProfile.id == school_id).first()
1323
  if school:
1324
- school.rating = (school.rating + rating) / 2.0
1325
 
1326
  db.commit()
1327
- return jsonify({"message": "امتیاز و نظر شما با موفقیت ثبت شد."}), 200
1328
  except Exception as e:
1329
  logger.error(f"Error creating review: {e}")
1330
  db.rollback()
1331
  return jsonify({"detail": "سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید."}), 503
1332
 
1333
- # ==========================================
1334
- # کارتابل مشاوران و داوطلبان (بند ۱۲ و ۱۳)
1335
- # ==========================================
1336
-
1337
- @flask_app.route("/api/counselor/students", methods=["GET"])
1338
- @login_required
1339
- def get_counselor_dash_clients():
1340
- db = get_db()
1341
- current_user = g.current_user
1342
- try:
1343
- if current_user.role != "counselor":
1344
- return jsonify({"detail": "دسترسی غیرمجاز"}), 403
1345
-
1346
- booked_slots = db.query(TimeSlot).filter(
1347
- TimeSlot.counselor_id == current_user.id,
1348
- TimeSlot.is_booked == True
1349
- ).all()
1350
-
1351
- student_ids = list(set([s.booked_by_student_id for s in booked_slots if s.booked_by_student_id]))
1352
- results = []
1353
- for s_id in student_ids:
1354
- student_user = db.query(User).filter(User.id == s_id).first()
1355
- if not student_user:
1356
- continue
1357
-
1358
- tests = db.query(TestResult).filter(TestResult.student_id == s_id, TestResult.counselor_id == current_user.id).all()
1359
- selections = db.query(MajorSelection).filter(MajorSelection.student_id == s_id, MajorSelection.counselor_id == current_user.id).all()
1360
-
1361
- parsed_selections = []
1362
- for sel in selections:
1363
- try:
1364
- gen_list = json.loads(sel.generated_list) if sel.generated_list else []
1365
- fin_list = json.loads(sel.finalized_list) if sel.finalized_list else []
1366
- except Exception:
1367
- gen_list, fin_list = sel.generated_list, sel.finalized_list
1368
-
1369
- parsed_selections.append({
1370
- "selection_id": sel.id,
1371
- "status": sel.status,
1372
- "ai_generated_list": gen_list,
1373
- "counselor_finalized_list": fin_list
1374
- })
1375
-
1376
- results.append({
1377
- "student_id": s_id,
1378
- "student_email": student_user.email,
1379
- "student_name": student_user.name or student_user.email.split("@")[0],
1380
- "grade": student_user.grade or "یازدهم", # ارسال مشخصات کامل مقطع (بند ۱۲)
1381
- "major": student_user.major or "علوم تجربی", # ارسال مشخصات کامل رشته (بند ۱۲)
1382
- "tests": [{"test_id": t.id, "answers": t.answers, "ai_analysis": t.ai_analysis_summary} for t in tests],
1383
- "selections": parsed_selections
1384
- })
1385
-
1386
- return jsonify(results), 200
1387
- except Exception as e:
1388
- logger.error(f"Error fetching dashboard clients: {e}", exc_info=True)
1389
- return jsonify({"detail": "سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید."}), 503
1390
-
1391
-
1392
- @flask_app.route("/api/student/my-visited-counselors", methods=["GET"])
1393
- @login_required
1394
- def get_student_visited_counselors():
1395
- db = get_db()
1396
- current_user = g.current_user
1397
-
1398
- try:
1399
- if current_user.role != "student":
1400
- return jsonify({"detail": "دسترسی غیرمجاز"}), 403
1401
-
1402
- bookings = db.query(Booking).filter(
1403
- Booking.student_id == current_user.id,
1404
- Booking.status == "active"
1405
- ).all()
1406
-
1407
- c_ids = list(set([b.counselor_id for b in bookings]))
1408
- results = []
1409
- for cid in c_ids:
1410
- c = db.query(User).filter(User.id == cid).first()
1411
- if c:
1412
- results.append({
1413
- "counselor_id": c.id,
1414
- "name": c.name or c.email.split("@")[0],
1415
- "specialty": c.counselor_profile.specialty if c.counselor_profile else "کنکور سراسری",
1416
- "rating": c.rating,
1417
- "city": c.city or "تهران"
1418
- })
1419
- return jsonify(results), 200
1420
- except Exception as e:
1421
- logger.error(f"Error getting visited counselors: {e}")
1422
- return jsonify({"detail": "خطا در لود مشاوران داوطلب."}), 500
1423
-
1424
- # ==========================================
1425
- # آدرس‌های باقی‌مانده و اصلی وب‌سرویس
1426
- # ==========================================
1427
-
1428
- @flask_app.route("/api/bookings/<int:booking_id>/cancel-by-counselor", methods=["POST"])
1429
- @login_required
1430
- def cancel_session_by_counselor(booking_id):
1431
- db = get_db()
1432
- current_user = g.current_user
1433
- try:
1434
- if current_user.role != "counselor":
1435
- return jsonify({"detail": "تنها مشاوران مجاز به استفاده از این متد هستند."}), 403
1436
-
1437
- booking = db.query(Booking).filter(
1438
- Booking.id == booking_id,
1439
- Booking.counselor_id == current_user.id
1440
- ).first()
1441
-
1442
- if not booking or booking.status == "cancelled":
1443
- return jsonify({"detail": "این رزرو قبلاً لغو شده یا وجود ندارد."}), 404
1444
-
1445
- student = db.query(User).filter(User.id == booking.student_id).first()
1446
- counselor = current_user
1447
-
1448
- if student:
1449
- student.balance += booking.cost
1450
- student_notif = Notification(
1451
- user_id=student.id,
1452
- text=f"پیام سیستم: جلسه مشاوره شما لغو گردید و کل مبلغ {booking.cost:,.0f} ریال به حساب عودت داده شد.",
1453
- role="student"
1454
- )
1455
- db.add(student_notif)
1456
-
1457
- # کسر ۱۰٪ جریمه لغو یک‌طرفه بر مبنای بندهای انضباطی
1458
- penalty_fee = booking.cost * 0.10
1459
- counselor.balance = counselor.balance - (booking.cost + penalty_fee)
1460
-
1461
- counselor_notif = Notification(
1462
- user_id=counselor.id,
1463
- text=f"پیام سیستم: رزرو شماره {booking.id} لغو گردید و جریمه ۱۰ درصدی ({penalty_fee:,.0f} ریال) از اولین واریزی بعدی کسر می‌شود.",
1464
- role="counselor"
1465
- )
1466
- db.add(counselor_notif)
1467
-
1468
- slot = db.query(TimeSlot).filter(TimeSlot.id == booking.slot_id).first()
1469
- if slot:
1470
- slot.is_booked = False
1471
- slot.booked_by_student_id = None
1472
-
1473
- booking.status = "cancelled"
1474
- db.commit()
1475
- return jsonify({"message": "جلسه مشاوره با موفقیت لغو شد، تراز عودت داده شد و جریمه ثبت گردید."}), 200
1476
- except Exception as e:
1477
- logger.error(f"Error cancelling booking by counselor: {e}", exc_info=True)
1478
- db.rollback()
1479
- return jsonify({"detail": "خطا در پردازش سرور."}), 500
1480
-
1481
-
1482
- @flask_app.route("/api/selection/select", methods=["POST"])
1483
- @login_required
1484
- def submit_konkur_rag_data():
1485
- db = get_db()
1486
- current_user = g.current_user
1487
- data = request.get_json() or {}
1488
- rank = data.get("rank")
1489
- quota = data.get("quota")
1490
- favorite_cities = data.get("favorite_cities")
1491
- counselor_id = data.get("counselor_id")
1492
-
1493
- if rank is None or not quota or not favorite_cities:
1494
- return jsonify({"detail": "وارد کردن تمامی فیلدهای رتبه، سهمیه و شهرهای انتخابی الزامی است."}), 400
1495
-
1496
- try:
1497
- if current_user.role != "student":
1498
- return jsonify({"detail": "دسترسی غیرمجاز"}), 403
1499
-
1500
- konkur = db.query(KonkurDetails).filter(KonkurDetails.student_id == current_user.id).first()
1501
- if konkur:
1502
- konkur.rank = rank
1503
- konkur.quota = quota
1504
- konkur.favorite_cities = favorite_cities
1505
- else:
1506
- konkur = KonkurDetails(
1507
- student_id=current_user.id,
1508
- rank=rank,
1509
- quota=quota,
1510
- favorite_cities=favorite_cities
1511
- )
1512
- db.add(konkur)
1513
-
1514
- selection_list_raw = core_selection_engine(rank, quota, favorite_cities)
1515
-
1516
- selection = db.query(MajorSelection).filter(
1517
- MajorSelection.student_id == current_user.id,
1518
- MajorSelection.counselor_id == counselor_id
1519
- ).first()
1520
-
1521
- if selection:
1522
- selection.generated_list = selection_list_raw
1523
- selection.finalized_list = selection_list_raw
1524
- selection.status = "draft"
1525
- else:
1526
- selection = MajorSelection(
1527
- student_id=current_user.id,
1528
- counselor_id=counselor_id,
1529
- generated_list=selection_list_raw,
1530
- finalized_list=selection_list_raw,
1531
- status="draft"
1532
- )
1533
- db.add(selection)
1534
-
1535
- db.commit()
1536
-
1537
- try:
1538
- parsed_selections = json.loads(selection_list_raw)
1539
- except Exception:
1540
- parsed_selections = selection_list_raw
1541
-
1542
- return jsonify({
1543
- "message": "پیش‌نویس اولیه آماده و ارسال شد.",
1544
- "selections": parsed_selections,
1545
- "selection_id": selection.id
1546
- }), 200
1547
- except Exception as e:
1548
- logger.error(f"Error submitting konkur details: {e}", exc_info=True)
1549
- db.rollback()
1550
- return jsonify({"detail": "سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید."}), 503
1551
-
1552
-
1553
- @flask_app.route("/api/selection/<int:selection_id>/approve", methods=["PUT"])
1554
- @login_required
1555
- def edit_and_approve_selection_list(selection_id):
1556
- db = get_db()
1557
- current_user = g.current_user
1558
- data = request.get_json() or {}
1559
- finalized_list_json = data.get("finalized_list_json")
1560
-
1561
- if not finalized_list_json:
1562
- return jsonify({"detail": "ساختار نهایی انتخاب‌ها الزامی است."}), 400
1563
-
1564
- try:
1565
- if current_user.role != "counselor":
1566
- return jsonify({"detail": "تنها مشاوران مجاز به اعمال ویرایش هستند."}), 403
1567
-
1568
- selection = db.query(MajorSelection).filter(
1569
- MajorSelection.id == selection_id,
1570
- MajorSelection.counselor_id == current_user.id
1571
- ).first()
1572
-
1573
- if not selection:
1574
- return jsonify({"detail": "لیست پیش‌نویس مورد نظر یافت نشد."}), 404
1575
-
1576
- selection.finalized_list = finalized_list_json
1577
- selection.status = "approved_by_counselor"
1578
- db.commit()
1579
-
1580
- return jsonify({"message": "لیست نهایی داوطلب ویرایش و تایید گردید."}), 200
1581
- except Exception as e:
1582
- logger.error(f"Error during counselor list approval: {e}", exc_info=True)
1583
- db.rollback()
1584
- return jsonify({"detail": "سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید."}), 503
1585
-
1586
-
1587
- @flask_app.route("/api/student/major-selection", methods=["GET"])
1588
- @login_required
1589
- def get_student_selection_finalized_status():
1590
- db = get_db()
1591
- current_user = g.current_user
1592
- try:
1593
- if current_user.role != "student":
1594
- return jsonify({"detail": "دسترسی غیرمجاز"}), 403
1595
-
1596
- selection = db.query(MajorSelection).filter(MajorSelection.student_id == current_user.id).order_by(MajorSelection.id.desc()).first()
1597
- if not selection:
1598
- return jsonify({"detail": "پیش‌نویسی ثبت نشده است."}), 404
1599
-
1600
- try:
1601
- generated = json.loads(selection.generated_list) if selection.generated_list else []
1602
- finalized = json.loads(selection.finalized_list) if selection.finalized_list else []
1603
- except Exception:
1604
- generated = selection.generated_list
1605
- finalized = selection.finalized_list
1606
-
1607
- return jsonify({
1608
- "selection_id": selection.id,
1609
- "status": selection.status,
1610
- "ai_generated_list": generated,
1611
- "counselor_finalized_list": finalized,
1612
- "finalized_list": finalized
1613
- }), 200
1614
- except Exception as e:
1615
- logger.error(f"Error fetching student selection list: {e}", exc_info=True)
1616
- return jsonify({"detail": "سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید."}), 503
1617
-
1618
-
1619
- @flask_app.route("/api/schools/request-counselor", methods=["POST"])
1620
- @login_required
1621
- def school_request_counselor_link():
1622
- db = get_db()
1623
- current_user = g.current_user
1624
- data = request.get_json() or {}
1625
- counselor_id = data.get("counselor_id")
1626
-
1627
- if not counselor_id:
1628
- return jsonify({"detail": "شناسه مشاور الزامی است."}), 400
1629
-
1630
- try:
1631
- if current_user.role != "school_representative":
1632
- return jsonify({"detail": "دسترسی غیرمجاز"}), 403
1633
-
1634
- school_prof = current_user.school_profile
1635
- if not school_prof:
1636
- return jsonify({"detail": "پروفایل آموزشگاهی ثبت نشده است."}), 404
1637
-
1638
- assoc = SchoolCounselorAssociation(
1639
- school_id=school_prof.id,
1640
- counselor_id=counselor_id,
1641
- status="pending"
1642
- )
1643
- db.add(assoc)
1644
- db.commit()
1645
- return jsonify({"message": "درخواست همبستگی با موفقیت ارسال گردید."}), 200
1646
- except Exception as e:
1647
- logger.error(f"Error requesting link: {e}")
1648
- db.rollback()
1649
- return jsonify({"detail": "سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید."}), 503
1650
-
1651
-
1652
- @flask_app.route("/api/counselor/extract-bio", methods=["POST"])
1653
- @login_required
1654
- def extract_bio_from_resume_text():
1655
- db = get_db()
1656
- current_user = g.current_user
1657
- data = request.get_json() or {}
1658
- resume_text = data.get("resume_text")
1659
-
1660
- if not resume_text:
1661
- return jsonify({"detail": "متن رزومه برای استخراج الزامی است."}), 400
1662
-
1663
- try:
1664
- if current_user.role != "counselor":
1665
- return jsonify({"detail": "مخصوص مشاوران"}), 403
1666
-
1667
- extracted_bio = core_bio_extraction(resume_text)
1668
- profile = current_user.counselor_profile
1669
- if profile:
1670
- profile.bio = extracted_bio
1671
- profile.resume = resume_text
1672
- db.commit()
1673
-
1674
- return jsonify({
1675
- "message": "بیوگرافی بهینه بر اساس رزومه استخراج شد.",
1676
- "extracted_bio": extracted_bio
1677
- }), 200
1678
- except Exception as e:
1679
- logger.error(f"Error extracting bio: {e}")
1680
- db.rollback()
1681
- return jsonify({"detail": "سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید."}), 503
1682
-
1683
-
1684
- @flask_app.route("/api/schools/purchase-ad", methods=["POST"])
1685
- @login_required
1686
- def purchase_school_premium_ad():
1687
- db = get_db()
1688
- current_user = g.current_user
1689
- try:
1690
- if current_user.role != "school_representative":
1691
- return jsonify({"detail": "دسترسی غیرمجاز"}), 403
1692
- profile = current_user.school_profile
1693
- if not profile:
1694
- return jsonify({"detail": "پروفایل آموزشگاه یافت نشد."}), 404
1695
-
1696
- profile.is_sponsored = True
1697
- db.commit()
1698
- return jsonify({"message": "تبلیغات ویژه با موفقیت خریداری شد."}), 200
1699
- except Exception as e:
1700
- logger.error(f"Error purchasing school ad: {e}")
1701
- db.rollback()
1702
- return jsonify({"detail": "سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید."}), 503
1703
-
1704
 
1705
  @flask_app.route("/api/schools/update", methods=["POST"])
1706
  @login_required
@@ -1903,32 +1497,92 @@ def create_time_slots():
1903
  return jsonify({"detail": "سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید."}), 503
1904
 
1905
 
1906
- @flask_app.route("/api/notifications", methods=["GET"])
1907
  @login_required
1908
- def get_user_notifications():
1909
  db = get_db()
1910
  current_user = g.current_user
1911
  try:
1912
- notifs = db.query(Notification).filter(
1913
- or_(
1914
- Notification.user_id == current_user.id,
1915
- Notification.role == "all",
1916
- Notification.role == current_user.role
1917
- )
1918
- ).order_by(Notification.id.desc()).all()
1919
 
1920
- output = []
1921
- for n in notifs:
1922
- output.append({
1923
- "id": n.id,
1924
- "text": n.text,
1925
- "role": n.role,
1926
- "is_read": n.is_read
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1927
  })
1928
- return jsonify(output), 200
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1929
  except Exception as e:
1930
- logger.error(f"Error fetching notifications: {e}")
1931
- return jsonify({"detail": "خطا در دریافت اعلان‌ها."}), 500
1932
 
1933
 
1934
  @flask_app.route("/api/notifications/<int:notif_id>/read", methods=["POST"])
@@ -1951,7 +1605,7 @@ def mark_single_notification_as_read(notif_id):
1951
  return jsonify({"status": "success"}), 200
1952
  return jsonify({"detail": "اعلان یافت نشد."}), 404
1953
  except Exception as e:
1954
- logger.error(f"Error marking single notification as read: {e}")
1955
  db.rollback()
1956
  return jsonify({"detail": "خطا در به‌روزرسانی اعلان."}), 500
1957
 
@@ -1979,11 +1633,67 @@ def mark_notifications_as_read():
1979
  return jsonify({"detail": "خطا در به‌روزرسانی اعلان‌ها."}), 500
1980
 
1981
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1982
  @flask_app.route("/")
1983
  def home_webview():
1984
  return flask_app.send_static_file("index.html")
1985
 
1986
- # تبدیل ساختار فلاسک WSGI به هندلرهای کارآمد ASGI
1987
  app = WsgiToAsgi(flask_app)
1988
 
1989
  if __name__ == "__main__":
 
13
  from sqlalchemy import create_engine, Column, Integer, String, Boolean, Float, DateTime, ForeignKey, Text, and_, or_, text, inspect
14
  from sqlalchemy.orm import relationship, sessionmaker, declarative_base, Session
15
 
16
+ # مدیریت ایمن لود کتابخانه‌های امنیت و توکن JWT
17
  try:
18
  from jose import JWTError, jwt
19
  except ImportError:
 
28
  logging.basicConfig(level=logging.INFO)
29
  logger = logging.getLogger("production_backend")
30
 
31
+ # کلیدهای پردازش ابری هوشمند
32
  AI_CORE_KEY = os.getenv("GEMINI_API_KEY") or os.getenv("SECRET")
33
 
34
  try:
 
38
  logger.warning("The 'google-genai' package is not installed. AI features will fallback.")
39
 
40
  # =========================================================================
41
+ # بخش ۲: اتصال پایگاه داده Neon.tech و ساختار دیتابیس
42
  # =========================================================================
43
  DATABASE_URL = os.getenv(
44
  "DATABASE_URL",
 
66
  SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
67
  Base = declarative_base()
68
 
 
69
  def get_now():
70
  return datetime.now(timezone.utc).replace(tzinfo=None)
71
 
72
+ # فرمول مبدل تقویم میلادی به شمسی بومی (بند ۱۱)
73
  def gregorian_to_jalali(g_y, g_m, g_d):
74
  g_days_in_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
75
  j_days_in_month = [31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29]
 
126
  "minimum_rank_r1": 100,
127
  "minimum_rank_r2": 200
128
  },
 
 
 
 
 
 
 
129
  {
130
  "university": "دانشگاه علوم پزشکی تهران",
131
  "ranking_tier": "الف",
 
145
  email = Column(String, unique=True, index=True, nullable=False)
146
  hashed_password = Column(String, nullable=False)
147
  role = Column(String, default="student") # 'student', 'counselor', 'school_representative'
148
+ name = Column(String, nullable=True) # نام کاربری واقعی (بند ۶)
149
  is_verified = Column(Boolean, default=False)
150
  balance = Column(Float, default=0.0)
151
+ rating = Column(Float, default=0.0) # مقدار اولیه بر اساس صفر است (امتیازی نیست در شروع - بند ۱۰)
152
  phone_number = Column(String, nullable=True)
153
+ city = Column(String, nullable=True) # منطقه ثبت‌نامی (بند ۳)
154
+ grade = Column(String, nullable=True)
155
+ major = Column(String, nullable=True)
156
+ profile_pic = Column(Text, nullable=True)
157
 
158
  counselor_profile = relationship("CounselorProfile", back_populates="user", uselist=False, cascade="all, delete-orphan")
159
  school_profile = relationship("SchoolProfile", back_populates="user", uselist=False, cascade="all, delete-orphan")
 
164
  id = Column(Integer, primary_key=True, index=True)
165
  user_id = Column(Integer, ForeignKey("users.id"))
166
  specialty = Column(String, nullable=True)
167
+ resume = Column(Text, nullable=True)
168
  bio = Column(Text, nullable=True)
169
+ hourly_rate = Column(Float, default=150000.0)
170
  deactivated_days_json = Column(Text, default="[]") # روزهای غیرفعال شده توسط مشاور (بند ۷)
171
 
172
  user = relationship("User", back_populates="counselor_profile")
 
183
  address = Column(Text, nullable=True)
184
  phone_number = Column(String, nullable=True)
185
  founding_year = Column(Integer, nullable=True)
186
+ rating = Column(Float, default=0.0) # مقدار امتیاز آموزشگاه در شروع صفر است (بند ۱۰)
187
+ profile_pic = Column(Text, nullable=True)
188
 
189
  user = relationship("User", back_populates="school_profile")
190
  courses = relationship("SchoolCourse", back_populates="school", cascade="all, delete-orphan")
 
233
  slot_id = Column(Integer, ForeignKey("time_slots.id"))
234
  student_id = Column(Integer, ForeignKey("users.id"))
235
  counselor_id = Column(Integer, ForeignKey("users.id"))
236
+ status = Column(String, default="active") # active, cancelled
237
+ payment_status = Column(String, default="paid") # paid, pending_payment, expired
238
  payment_due_until = Column(DateTime, nullable=True)
239
  cost = Column(Float, default=0.0)
240
+ created_at = Column(DateTime, default=get_now)
241
 
242
 
243
  class CustomQuiz(Base):
244
  __tablename__ = "custom_quizzes"
245
  id = Column(Integer, primary_key=True, index=True)
246
+ counselor_id = Column(Integer, ForeignKey("users.id"), nullable=True)
247
  title = Column(String, nullable=False)
248
  questions_json = Column(Text, nullable=False)
249
 
 
291
  __tablename__ = "reviews"
292
  id = Column(Integer, primary_key=True, index=True)
293
  student_id = Column(Integer, ForeignKey("users.id"))
294
+ booking_id = Column(Integer, ForeignKey("bookings.id"), nullable=True)
295
  counselor_id = Column(Integer, ForeignKey("users.id"), nullable=True)
296
  school_id = Column(Integer, ForeignKey("school_profiles.id"), nullable=True)
297
  rating = Column(Float, default=5.0)
 
309
  created_at = Column(DateTime, default=get_now)
310
 
311
 
312
+ # مدل تسویه حساب‌های پلتفرم (بند ۴ و ۵)
313
  class SettlementRequest(Base):
314
  __tablename__ = "settlement_requests"
315
  id = Column(Integer, primary_key=True, index=True)
 
319
  created_at = Column(DateTime, default=get_now)
320
 
321
 
322
+ # مدل ردیابی بازدید واقعی آموزشگاه بر اساس حساب کاربری (بند ۹)
323
  class PageView(Base):
324
  __tablename__ = "page_views"
325
  id = Column(Integer, primary_key=True, index=True)
 
521
  new_quiz = CustomQuiz(counselor_id=None, title=gq["title"], questions_json=gq["questions"])
522
  db.add(new_quiz)
523
  db.commit()
524
+ logger.info("Database base check completed.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
525
  except Exception as e:
526
  logger.error(f"Seeding process crashed completely: {e}", exc_info=True)
527
  db.rollback()
 
565
  try:
566
  seed_initial_data(db)
567
  except Exception as seed_err:
568
+ logger.error(f"Error seeding database during startup: {seed_err}", exc_info=True)
569
  finally:
570
  db.close()
571
  except Exception as startup_db_err:
572
+ logger.warning(f"Database setup or seeding skipped: {startup_db_err}")
573
 
574
 
575
  @flask_app.errorhandler(Exception)
 
580
  return jsonify({"detail": "سرور موقتاً شلوغ است. لطفاً چند لحظه دیگر مجدداً تلاش کنید."}), 500
581
 
582
  # ==========================================
583
+ # بخش ۹: آدرس‌های API مربوط به احراز هویت و حساب کاربری
584
  # ==========================================
585
 
586
  @flask_app.route("/api/auth/register", methods=["POST"])
 
637
  elif role == "school_representative":
638
  s_prof = SchoolProfile(
639
  user_id=new_user.id,
640
+ name=name or "آموزشگاه جدید ثبت‌شده",
641
  subscription_active_until=get_now(),
642
  profile_pic=profile_pic
643
  )
 
670
  if not user or not verify_password(password, user.hashed_password):
671
  return jsonify({"detail": "ایمیل یا رمز عبور اشتباه است."}), 400
672
 
673
+ # بررسی و ثبت فقط یکباره اعلان ورود موفقیت‌آمیز داوطلب (بند ۲)
674
  existing_login_notif = db.query(Notification).filter(
675
  Notification.user_id == user.id,
676
+ Notification.text.like("%ورود موفقیت‌آمیز به حساب کاربری%")
677
  ).first()
678
 
679
  if not existing_login_notif:
680
  notif = Notification(
681
  user_id=user.id,
682
+ text=f"ورود موفقیت‌آمیز به حساب کاربری در تاریخ {get_shamsi_date_str(get_now())}",
683
  role=user.role,
684
  is_read=False
685
  )
 
703
  @login_required
704
  def get_user_profile():
705
  current_user = g.current_user
 
706
 
707
+ # هشدار انضباطی امتیازات زیر ۳ ستاره (بند ۱۱)
708
  requires_rating_warning = False
709
+ if current_user.role == "counselor" and current_user.rating < 3.0 and current_user.rating > 0.0:
710
  requires_rating_warning = True
711
 
712
  return jsonify({
713
  "id": current_user.id,
714
  "email": current_user.email,
715
  "role": current_user.role,
716
+ "name": current_user.name or current_user.email.split("@")[0], # نمایش اولویت بر اساس نام کاربر (بند ۶)
717
  "balance": current_user.balance,
718
  "rating": current_user.rating,
719
  "phone_number": current_user.phone_number,
 
774
  return jsonify({"detail": "سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید."}), 503
775
 
776
  # ==========================================
777
+ # بخش ۱۰: دایرکتوری مشاوران و فیلتر منطقه‌ای هوشمند (بند ۳)
778
  # ==========================================
779
 
780
  @flask_app.route("/api/counselors", methods=["GET"])
 
782
  db = get_db()
783
  name = request.args.get("name")
784
  city = request.args.get("city")
785
+ student_city = request.args.get("student_city") # موقعیت جغرافیایی کاربر برای اعمال فیلتر بومی
786
 
787
  try:
788
  query = db.query(User).filter(User.role == "counselor", User.is_verified == True)
789
 
 
790
  target_city = city or student_city
791
  is_fallback = False
792
 
793
+ # در صورت نبود مشاور در منطقه داوطلب، فال‌بک به نمایش سایر مناطق اجرا می‌شود (بند ۳)
794
  if target_city:
795
  regional_query = query.filter(User.city.ilike(f"%{target_city}%"))
796
  counselors = regional_query.all()
797
  if not counselors:
 
798
  is_fallback = True
799
  counselors = query.all()
800
  else:
 
849
  return jsonify({"detail": "سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید."}), 503
850
 
851
  # ==========================================
852
+ # بخش ۱۱: فرآیند تراکنش رزرو با کسر ۷٪ کارمزد پلتفرم (بند ۴)
853
  # ==========================================
854
 
855
  @flask_app.route("/api/bookings", methods=["POST"])
 
879
  if current_user.balance < cost:
880
  return jsonify({"detail": "موجودی حساب کافی نیست. لطفاً کیف پول خود را شارژ نمایید."}), 400
881
 
882
+ # کسر کارمزد ۷٪ پلتفرم به صورت خودکار (بند ۴)
883
  platform_fee = cost * 0.07
884
  net_to_counselor = cost - platform_fee
885
 
 
900
  db.commit()
901
 
902
  return jsonify({
903
+ "message": "رزرو جلسه با موفقیت تایید و سهم مشاور به کیف پول واریز شد.",
904
  "remaining_balance": current_user.balance,
905
  "session_start": slot.start_time.isoformat()
906
  }), 200
 
909
  db.rollback()
910
  return jsonify({"detail": "سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید."}), 503
911
 
912
+
913
+ @flask_app.route("/api/student/bookings", methods=["GET"])
914
+ @login_required
915
+ def get_student_bookings():
916
+ db = get_db()
917
+ current_user = g.current_user
918
+
919
+ try:
920
+ if current_user.role != "student":
921
+ return jsonify({"detail": "دسترسی غیرمجاز"}), 403
922
+
923
+ bookings = db.query(Booking).filter(
924
+ Booking.student_id == current_user.id,
925
+ Booking.status == "active"
926
+ ).order_by(Booking.id.desc()).all()
927
+
928
+ output = []
929
+ for b in bookings:
930
+ counselor = db.query(User).filter(User.id == b.counselor_id).first()
931
+ slot = db.query(TimeSlot).filter(TimeSlot.id == b.slot_id).first()
932
+ output.append({
933
+ "id": b.id,
934
+ "counselor_name": counselor.name if counselor else "مشاور تحصیلی",
935
+ "date": get_shamsi_date_str(slot.start_time) if slot else get_shamsi_date_str(b.created_at),
936
+ "time": slot.start_time.strftime("%H:%M") if slot else "۱۶:۰۰",
937
+ "amount": b.cost,
938
+ "status": "موفق"
939
+ })
940
+ return jsonify(output), 200
941
+ except Exception as e:
942
+ logger.error(f"Error fetching student bookings: {e}")
943
+ return jsonify({"detail": "خطا در دریافت لیست نوبت‌های داوطلب."}), 500
944
+
945
+
946
+ @flask_app.route("/api/bookings/<int:booking_id>/cancel-by-counselor", methods=["POST"])
947
+ @login_required
948
+ def cancel_session_by_counselor(booking_id):
949
+ db = get_db()
950
+ current_user = g.current_user
951
+ try:
952
+ if current_user.role != "counselor":
953
+ return jsonify({"detail": "تنها مشاوران مجاز به استفاده از این متد هستند."}), 403
954
+
955
+ booking = db.query(Booking).filter(
956
+ Booking.id == booking_id,
957
+ Booking.counselor_id == current_user.id
958
+ ).first()
959
+
960
+ if not booking or booking.status == "cancelled":
961
+ return jsonify({"detail": "این رزرو قبلاً لغو شده یا وجود ندارد."}), 404
962
+
963
+ student = db.query(User).filter(User.id == booking.student_id).first()
964
+ counselor = current_user
965
+
966
+ if student:
967
+ student.balance += booking.cost
968
+ student_notif = Notification(
969
+ user_id=student.id,
970
+ text=f"پیام سیستم: جلسه مشاوره شما با مشاور {counselor.name} لغو گردید و کل مبلغ {booking.cost:,.0f} ریال به کیف پول شما عودت داده شد.",
971
+ role="student"
972
+ )
973
+ db.add(student_notif)
974
+
975
+ # جریمه انضباطی ۱۰ درصدی لغو یک‌طرفه مشاور بر اساس قوانین پلتفرم (بند ۴)
976
+ penalty_fee = booking.cost * 0.10
977
+ counselor.balance = counselor.balance - (booking.cost + penalty_fee)
978
+
979
+ counselor_notif = Notification(
980
+ user_id=counselor.id,
981
+ text=f"پیام سیستم: رزرو شماره {booking.id} توسط شما لغو گردید و جریمه ۱۰ درصدی ({penalty_fee:,.0f} ریال) در حساب شما ثبت شد.",
982
+ role="counselor"
983
+ )
984
+ db.add(counselor_notif)
985
+
986
+ slot = db.query(TimeSlot).filter(TimeSlot.id == booking.slot_id).first()
987
+ if slot:
988
+ slot.is_booked = False
989
+ slot.booked_by_student_id = None
990
+
991
+ booking.status = "cancelled"
992
+ db.commit()
993
+ return jsonify({"message": "جلسه مشاوره لغو، مبالغ عودت و جریمه کسر گردید."}), 200
994
+ except Exception as e:
995
+ logger.error(f"Error cancelling booking by counselor: {e}", exc_info=True)
996
+ db.rollback()
997
+ return jsonify({"detail": "خطا در فرآیند لغو رزرو مشاور."}), 500
998
+
999
  # ========================================================
1000
+ # بخش ۱۲: درگاه‌های مدیریت تسویه حساب و کیف پول (بند ۴)
1001
  # ========================================================
1002
 
1003
  @flask_app.route("/api/counselor/request-settlement", methods=["POST"])
 
1011
  return jsonify({"detail": "دسترسی غیرمجاز"}), 403
1012
 
1013
  if current_user.balance <= 0:
1014
+ return jsonify({"detail": "موجودی قابل تسویه وجود ندارد."}), 400
1015
 
1016
  req = SettlementRequest(
1017
  user_id=current_user.id,
 
1020
  )
1021
  db.add(req)
1022
 
1023
+ # قفل تراز مالی مشاور تا زمان تایید درخواست توسط ادمین در پایگاه داده
1024
  current_user.balance = 0.0
1025
  db.commit()
1026
 
1027
+ return jsonify({"message": "درخواست تسویه با موفقیت ثبت شد."}), 200
1028
  except Exception as e:
1029
  logger.error(f"Error requesting settlement: {e}")
1030
  db.rollback()
1031
+ return jsonify({"detail": "بروز خطا در ثبت تسویه مالی."}), 500
1032
 
1033
  # ========================================================
1034
+ # بخش ۱۳: گزارش مالی و تفکیک محاسباتی سه‌گانه (بند ۵)
1035
  # ========================================================
1036
 
1037
  @flask_app.route("/api/financial/summary", methods=["GET"])
 
1082
  }), 200
1083
 
1084
  elif current_user.role == "school_representative":
 
1085
  spent_ads = db.query(text("SUM(amount)")).from_statement(
1086
+ text("SELECT amount FROM settlement_requests WHERE user_id = :uid")
1087
  ).params(uid=current_user.id).scalar() or 0.0
1088
 
1089
  return jsonify({
 
1094
 
1095
  except Exception as e:
1096
  logger.error(f"Error building financial summary: {e}")
1097
+ return jsonify({"detail": "خطا در دریافت وضعیت تراز مالی."}), 500
1098
 
1099
  # ========================================================
1100
+ # بخش ۱۴: ردیابی ترافیک و آمار بازدید آموزشگاه (بند ۹)
1101
  # ========================================================
1102
 
1103
  @flask_app.route("/api/schools/<int:school_id>/visit", methods=["POST"])
 
1107
  current_user = g.current_user
1108
 
1109
  try:
1110
+ # جلوگیری از شمارش مکرر بازدید بر اساس اکانت و زمان (نه IP - بند ۹)
1111
  recent = db.query(PageView).filter(
1112
  PageView.school_id == school_id,
1113
  PageView.student_id == current_user.id,
 
1126
  except Exception as e:
1127
  logger.error(f"Error tracking visit: {e}")
1128
  db.rollback()
1129
+ return jsonify({"detail": "خطا در ثبت آمار ترافیک."}), 500
1130
 
1131
 
1132
  @flask_app.route("/api/schools/<int:school_id>/analytics", methods=["GET"])
 
1136
 
1137
  try:
1138
  total_views = db.query(PageView).filter(PageView.school_id == school_id).count()
 
 
1139
  reviews = db.query(Review).filter(Review.school_id == school_id).all()
1140
+ distribution = [0, 0, 0, 0, 0]
1141
+ avg_rating = 0.0
1142
 
1143
  if reviews:
1144
  for r in reviews:
 
1153
  "ratings_distribution": distribution
1154
  }), 200
1155
  except Exception as e:
1156
+ logger.error(f"Error fetching school analytics: {e}")
1157
+ return jsonify({"detail": "خطا در بارگذاری اطلاعات آموزشگاه."}), 500
1158
 
1159
  # ========================================================
1160
+ # بخش ۱۵: امتیازدهی اتوماتیک ۲۴ ساعت پس از نوبت (بند ۱۰)
1161
  # ========================================================
1162
 
1163
  @flask_app.route("/api/student/pending-reviews", methods=["GET"])
 
1168
 
1169
  try:
1170
  if current_user.role != "student":
1171
+ return jsonify({"detail": "دسترسی غیرمجاز"}), 403
1172
 
 
1173
  boundary_time = get_now() - timedelta(hours=24)
1174
 
1175
  bookings = db.query(Booking).join(TimeSlot).filter(
 
1196
  return jsonify({"detail": "خطا در ارزیابی نوبت‌های فعال داوطلب."}), 500
1197
 
1198
  # ==========================================
1199
+ # بخش ۱۶: دایرکتوری آموزشگاه‌های فعال با فیلتر بومی (بند ۳)
1200
  # ==========================================
1201
 
1202
  @flask_app.route("/api/schools", methods=["GET"])
 
1213
  target_city = city or student_city
1214
  is_fallback = False
1215
 
1216
+ # فال‌بک دایرکتوری به سایر شهرها در صورت نبود موسسه در منطقه داوطلب (بند ۳)
1217
  if target_city:
1218
  regional_query = query.join(User, User.id == SchoolProfile.user_id).filter(User.city.ilike(f"%{target_city}%"))
1219
  schools = regional_query.all()
 
1251
  logger.error(f"Error querying schools: {e}")
1252
  return jsonify({"detail": "سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید."}), 503
1253
 
 
 
 
1254
 
1255
  @flask_app.route("/api/reviews", methods=["POST"])
1256
  @login_required
 
1278
  )
1279
  db.add(review)
1280
 
1281
+ # محاسبه پویای میانگین امتیاز پس از درج نظر نهایی (بند ۱۰ و ۱۴)
1282
  if counselor_id:
1283
  counselor = db.query(User).filter(User.id == counselor_id).first()
1284
  if counselor:
1285
+ counselor.rating = (counselor.rating + rating) / 2.0 if counselor.rating > 0.0 else rating
1286
  elif school_id:
1287
  school = db.query(SchoolProfile).filter(SchoolProfile.id == school_id).first()
1288
  if school:
1289
+ school.rating = (school.rating + rating) / 2.0 if school.rating > 0.0 else rating
1290
 
1291
  db.commit()
1292
+ return jsonify({"message": "امتیاز با موفقیت در دیتابیس ثبت شد."}), 200
1293
  except Exception as e:
1294
  logger.error(f"Error creating review: {e}")
1295
  db.rollback()
1296
  return jsonify({"detail": "سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید."}), 503
1297
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1298
 
1299
  @flask_app.route("/api/schools/update", methods=["POST"])
1300
  @login_required
 
1497
  return jsonify({"detail": "سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید."}), 503
1498
 
1499
 
1500
+ @flask_app.route("/api/counselor/students", methods=["GET"])
1501
  @login_required
1502
+ def get_counselor_dash_clients():
1503
  db = get_db()
1504
  current_user = g.current_user
1505
  try:
1506
+ if current_user.role != "counselor":
1507
+ return jsonify({"detail": "دسترسی غیرمجاز"}), 403
1508
+
1509
+ booked_slots = db.query(TimeSlot).filter(
1510
+ TimeSlot.counselor_id == current_user.id,
1511
+ TimeSlot.is_booked == True
1512
+ ).all()
1513
 
1514
+ student_ids = list(set([s.booked_by_student_id for s in booked_slots if s.booked_by_student_id]))
1515
+ results = []
1516
+ for s_id in student_ids:
1517
+ student_user = db.query(User).filter(User.id == s_id).first()
1518
+ if not student_user:
1519
+ continue
1520
+
1521
+ tests = db.query(TestResult).filter(TestResult.student_id == s_id, TestResult.counselor_id == current_user.id).all()
1522
+ selections = db.query(MajorSelection).filter(MajorSelection.student_id == s_id, MajorSelection.counselor_id == current_user.id).all()
1523
+
1524
+ parsed_selections = []
1525
+ for sel in selections:
1526
+ try:
1527
+ gen_list = json.loads(sel.generated_list) if sel.generated_list else []
1528
+ fin_list = json.loads(sel.finalized_list) if sel.finalized_list else []
1529
+ except Exception:
1530
+ gen_list, fin_list = sel.generated_list, sel.finalized_list
1531
+
1532
+ parsed_selections.append({
1533
+ "selection_id": sel.id,
1534
+ "status": sel.status,
1535
+ "ai_generated_list": gen_list,
1536
+ "counselor_finalized_list": fin_list
1537
+ })
1538
+
1539
+ results.append({
1540
+ "student_id": s_id,
1541
+ "student_email": student_user.email,
1542
+ "student_name": student_user.name or student_user.email.split("@")[0],
1543
+ "grade": student_user.grade or "یازدهم", # فیلد مقطع تحصیلی داوطلب (بند ۱۲)
1544
+ "major": student_user.major or "علوم تجربی", # فیلد رشته تحصیلی داوطلب (بند ۱۲)
1545
+ "tests": [{"test_id": t.id, "answers": t.answers, "ai_analysis": t.ai_analysis_summary} for t in tests],
1546
+ "selections": parsed_selections
1547
  })
1548
+
1549
+ return jsonify(results), 200
1550
+ except Exception as e:
1551
+ logger.error(f"Error fetching dashboard clients: {e}", exc_info=True)
1552
+ return jsonify({"detail": "سرور موقتاً شلوغ است. لطفاً بعداً تلاش کنید."}), 503
1553
+
1554
+
1555
+ @flask_app.route("/api/student/my-visited-counselors", methods=["GET"])
1556
+ @login_required
1557
+ def get_student_visited_counselors():
1558
+ db = get_db()
1559
+ current_user = g.current_user
1560
+
1561
+ try:
1562
+ if current_user.role != "student":
1563
+ return jsonify({"detail": "دسترسی غیرمجاز"}), 403
1564
+
1565
+ bookings = db.query(Booking).filter(
1566
+ Booking.student_id == current_user.id,
1567
+ Booking.status == "active"
1568
+ ).all()
1569
+
1570
+ c_ids = list(set([b.counselor_id for b in bookings]))
1571
+ results = []
1572
+ for cid in c_ids:
1573
+ c = db.query(User).filter(User.id == cid).first()
1574
+ if c:
1575
+ results.append({
1576
+ "counselor_id": c.id,
1577
+ "name": c.name or c.email.split("@")[0],
1578
+ "specialty": c.counselor_profile.specialty if c.counselor_profile else "کنکور سراسری",
1579
+ "rating": c.rating,
1580
+ "city": c.city or "تهران"
1581
+ })
1582
+ return jsonify(results), 200
1583
  except Exception as e:
1584
+ logger.error(f"Error getting visited counselors: {e}")
1585
+ return jsonify({"detail": "خطا در دریافت لیست مشاوران من."}), 500
1586
 
1587
 
1588
  @flask_app.route("/api/notifications/<int:notif_id>/read", methods=["POST"])
 
1605
  return jsonify({"status": "success"}), 200
1606
  return jsonify({"detail": "اعلان یافت نشد."}), 404
1607
  except Exception as e:
1608
+ logger.error(f"Error marking notification: {e}")
1609
  db.rollback()
1610
  return jsonify({"detail": "خطا در به‌روزرسانی اعلان."}), 500
1611
 
 
1633
  return jsonify({"detail": "خطا در به‌روزرسانی اعلان‌ها."}), 500
1634
 
1635
 
1636
+ @flask_app.route("/api/counselor/create-manual-booking", methods=["POST"])
1637
+ @login_required
1638
+ def create_manual_booking_by_counselor():
1639
+ db = get_db()
1640
+ current_user = g.current_user
1641
+ data = request.get_json() or {}
1642
+ student_id = data.get("student_id")
1643
+ start_time_str = data.get("start_time")
1644
+ end_time_str = data.get("end_time")
1645
+ cost = data.get("cost")
1646
+
1647
+ if not student_id or not start_time_str or not end_time_str or cost is None:
1648
+ return jsonify({"detail": "تمام فیلدها الزامی است."}), 400
1649
+
1650
+ try:
1651
+ if current_user.role != "counselor":
1652
+ return jsonify({"detail": "تنها مشاوران مجاز به ثبت نوبت هستند."}), 403
1653
+
1654
+ try:
1655
+ start_time = datetime.fromisoformat(start_time_str)
1656
+ end_time = datetime.fromisoformat(end_time_str)
1657
+ except ValueError:
1658
+ return jsonify({"detail": "فرمت تاریخ نامعتبر است."}), 400
1659
+
1660
+ slot = TimeSlot(
1661
+ counselor_id=current_user.id,
1662
+ start_time=start_time,
1663
+ end_time=end_time,
1664
+ is_booked=True,
1665
+ booked_by_student_id=student_id
1666
+ )
1667
+ db.add(slot)
1668
+ db.flush()
1669
+
1670
+ due_time = get_now() + timedelta(hours=24)
1671
+ booking = Booking(
1672
+ slot_id=slot.id,
1673
+ student_id=student_id,
1674
+ counselor_id=current_user.id,
1675
+ status="active",
1676
+ payment_status="pending_payment",
1677
+ payment_due_until=due_time,
1678
+ cost=cost
1679
+ )
1680
+ db.add(booking)
1681
+ db.commit()
1682
+
1683
+ return jsonify({
1684
+ "message": "نوبت صادر و فاکتور ۲۴ ساعته ارسال گردید.",
1685
+ "booking_id": booking.id
1686
+ }), 200
1687
+ except Exception as e:
1688
+ logger.error(f"Error manual booking: {e}")
1689
+ db.rollback()
1690
+ return jsonify({"detail": "خطا در ایجاد نوبت دستی."}), 503
1691
+
1692
+
1693
  @flask_app.route("/")
1694
  def home_webview():
1695
  return flask_app.send_static_file("index.html")
1696
 
 
1697
  app = WsgiToAsgi(flask_app)
1698
 
1699
  if __name__ == "__main__":