Anil1515/ipo-automation
0
1import os2import psycopg23import datetime4from playwright.sync_api import sync_playwright5from main import login, apply_ipo, handle_password_reset6from bank_checkers.bank import check_balance7 8# ── Firebase setup ──────────────────────────────────────────────────9import firebase_admin10from firebase_admin import credentials, messaging11 12MIN_BALANCE = 2000.0 # Minimum required balance to apply for IPO (Rs.)13 14def _init_firebase():15 if not firebase_admin._apps:16 cred_path = os.path.join(os.path.dirname(__file__), "config", "firebase_vcc.json")17 if os.path.exists(cred_path):18 cred = credentials.Certificate(cred_path)19 else:20 # Fallback: load from base64-encoded env variable (used on GitHub Actions)21 import base64, json, tempfile22 b64 = os.environ.get("FIREBASE_CREDENTIALS_B64", "")23 if not b64:24 print(" ⚠️ Firebase credentials not found (no file and no FIREBASE_CREDENTIALS_B64 env var). Skipping notifications.")25 return False26 cred_json = json.loads(base64.b64decode(b64).decode())27 cred = credentials.Certificate(cred_json)28 firebase_admin.initialize_app(cred)29 return True30 31def send_push_notification(fcm_tokens: list, title: str, body: str):32 if not fcm_tokens:33 print(" No FCM tokens – skipping notification.")34 return35 if not _init_firebase():36 return37 message = messaging.MulticastMessage(38 notification=messaging.Notification(title=title, body=body),39 tokens=fcm_tokens,40 )41 try:42 response = messaging.send_each_for_multicast(message)43 print(f" FCM: {response.success_count} sent, {response.failure_count} failed.")44 except Exception as e:45 print(f" FCM error: {e}")46 47# ── Database connection ─────────────────────────────────────────────48DB_URL = os.environ.get("DATABASE_URL")49 50# ── Encryption ─────────────────────────────────────────────────────51from cryptography.fernet import Fernet52 53_ENCRYPTION_KEY = os.environ.get("ENCRYPTION_KEY", "").encode()54 55def decrypt(token: str) -> str:56 if not token:57 return token58 if not _ENCRYPTION_KEY:59 if token.startswith('gAAAAA'):60 print(" ⚠️ WARNING: ENCRYPTION_KEY is not set, but token looks encrypted! MeroShare login will likely fail.")61 return token62 63 # If the token doesn't look encrypted (Fernet tokens start with gAAAAA), return as-is64 if not token.startswith('gAAAAA'):65 return token66 67 try:68 return Fernet(_ENCRYPTION_KEY).decrypt(token.encode()).decode()69 except Exception as e:70 print(f" ⚠️ WARNING: Decryption failed for token starting with '{token[:10]}...' (Key mismatch?): {e}")71 return token72 73# ── Main automation ─────────────────────────────────────────────────74 75def run_automation():76 if not DB_URL:77 print("Error: DATABASE_URL not set.")78 return79 80 try:81 conn = psycopg2.connect(DB_URL)82 cur = conn.cursor()83 84 # 1. Fetch active accounts85 cur.execute("""86 SELECT a.id, a.meroshare_user, a.meroshare_pass, a.dp_name,87 a.crn, a.tpin, a.bank_name, a.kitta, a.owner_id,88 b.bank, b.phone_number, b.bank_password89 FROM automation_account a90 LEFT JOIN automation_bankaccount b ON b.linked_account_id = a.id91 WHERE a.is_active = True;92 """)93 columns = [desc[0] for desc in cur.description]94 accounts = [dict(zip(columns, row)) for row in cur.fetchall()]95 96 if not accounts:97 print("No active accounts found.")98 return99 100 print(f"Found {len(accounts)} active accounts. Starting automation...")101 102 with sync_playwright() as p:103 browser = p.chromium.launch(104 headless=True,105 args=[106 '--disable-blink-features=AutomationControlled',107 '--disable-infobars',108 '--no-sandbox',109 '--window-size=1280,720'110 ]111 )112 113 for acc in accounts:114 print(f"\n{'='*50}")115 print(f"Processing: {acc['meroshare_user']}")116 page = browser.new_page()117 status = "Error"118 remark = "Unknown error"119 ipo_name = "Auto-Check" # Always initialise before try/finally120 121 try:122 # 2. Bank balance check (if bank credentials are linked)123 if acc.get('bank') and acc.get('phone_number') and acc.get('bank_password'):124 bank_page = browser.new_page()125 try:126 balance = check_balance(127 bank_code=acc['bank'],128 phone_number=acc['phone_number'],129 password=decrypt(acc['bank_password']),130 page=bank_page,131 )132 except Exception as bank_err:133 print(f" ⚠️ Bank balance check raised exception: {bank_err}. Proceeding with IPO.")134 balance = None135 finally:136 bank_page.close()137 138 if balance is not None and balance < MIN_BALANCE:139 print(f" ⚠️ Balance Rs.{balance:.2f} < Rs.{MIN_BALANCE:.2f} — skipping IPO.")140 status = "Skipped"141 remark = f"Insufficient balance: Rs.{balance:.2f} (min Rs.{MIN_BALANCE:.2f})"142 143 # Notify account holder144 if acc.get('owner_id'):145 cur.execute(146 "SELECT token FROM automation_fcmtoken WHERE user_id = %s",147 (acc['owner_id'],)148 )149 tokens = [row[0] for row in cur.fetchall()]150 send_push_notification(151 tokens,152 acc['meroshare_user'],153 f"⚠️ Low Balance: Rs.{balance:.2f}. Please top up to apply for IPO.",154 )155 156 # Log and move on to next account157 cur.execute("""158 INSERT INTO automation_applicationlog159 (account_id, company_name, status, remark, timestamp, is_read)160 VALUES (%s, %s, %s, %s, %s, %s)161 """, (acc['id'], "Balance Check", status, remark,162 datetime.datetime.now(datetime.timezone.utc), False))163 conn.commit()164 page.close()165 continue166 167 # 3. Apply IPO via MeroShare168 account_data = {169 'MEROSHARE_USER': acc['meroshare_user'],170 'MEROSHARE_PASS': decrypt(acc['meroshare_pass']),171 'DP_NAME': acc['dp_name'],172 'CRN': acc['crn'],173 'TPIN': acc['tpin'],174 'BANK_NAME': acc['bank_name'],175 'KITTA': str(acc['kitta']),176 }177 178 page.goto("https://meroshare.cdsc.com.np", timeout=60000)179 login_result = login(180 page,181 account_data['MEROSHARE_USER'],182 account_data['MEROSHARE_PASS'],183 account_data['DP_NAME'],184 )185 186 if login_result is True:187 print(f" ✅ Login OK. Applying IPO...")188 success, result_detail = apply_ipo(page, account_data)189 if success:190 status = "Success"191 ipo_name = result_detail192 remark = f"{ipo_name} ipo has been applied successfully."193 else:194 status = "Failed"195 remark = result_detail196 elif login_result == "EXPIRED":197 print(f" ⚠️ Password Expired. Attempting automatic reset...")198 if handle_password_reset(page, account_data):199 print(f" ✅ Password successfully reset and logged in.")200 success, result_detail = apply_ipo(page, account_data)201 if success:202 status = "Success"203 ipo_name = result_detail204 remark = f"{ipo_name} (after password reset) ipo has been applied successfully."205 else:206 status = "Failed"207 remark = result_detail208 else:209 print(f" ❌ Password reset failed.")210 status = "Failed"211 remark = "Password expired and automatic reset failed."212 else:213 print(f" ❌ Login failed: {login_result}")214 status = "Failed"215 remark = f"Login failed: {login_result}"216 217 except Exception as e:218 print(f" ❌ Exception: {e}")219 status = "Error"220 remark = str(e)221 222 finally:223 # 4. Write log and send notification — wrapped so a DB error here224 # does NOT prevent the next account from being processed.225 try:226 cur.execute("""227 INSERT INTO automation_applicationlog228 (account_id, company_name, status, remark, timestamp, is_read)229 VALUES (%s, %s, %s, %s, %s, %s)230 """, (acc['id'], ipo_name, status, remark,231 datetime.datetime.now(datetime.timezone.utc), False))232 conn.commit()233 except Exception as db_err:234 print(f" ⚠️ DB log error for {acc['meroshare_user']}: {db_err}")235 try:236 conn.rollback()237 except Exception:238 pass239 240 try:241 if acc.get('owner_id'):242 cur.execute(243 "SELECT token FROM automation_fcmtoken WHERE user_id = %s",244 (acc['owner_id'],)245 )246 tokens = [row[0] for row in cur.fetchall()]247 if status == "Success":248 notif_title = acc['meroshare_user']249 notif_body = f"✅ Success: {ipo_name} applied successfully."250 send_push_notification(tokens, notif_title, notif_body)251 elif remark != "No ordinary shares found":252 notif_title = acc['meroshare_user']253 notif_body = f"⚠️ {status}: {remark}"254 send_push_notification(tokens, notif_title, notif_body)255 except Exception as notif_err:256 print(f" ⚠️ Notification error for {acc['meroshare_user']}: {notif_err}")257 258 try:259 page.close()260 except Exception:261 pass262 263 browser.close()264 265 cur.close()266 conn.close()267 print("\nAutomation run completed.")268 269 except Exception as e:270 print(f"DB Error: {e}")271 272 273if __name__ == "__main__":274 run_automation()275 