#!/usr/bin/env python3 """ FalahOS Sales Automation — Openship MCP Compatible Run via: openship schedule --cron="0 6 * * *" --command="python sales_automation.py --morning" """ import os import sys import json import argparse import requests from datetime import datetime from pathlib import Path # ─── Config ────────────────────────────────────────────────────────────── MAUTIC_BASE = os.environ.get("MAUTIC_BASE_URL", "https://mautic.falahos.my").rstrip("/") MAUTIC_USER = os.environ.get("MAUTIC_ADMIN_USER", "admin") MAUTIC_PASS = os.environ.get("MAUTIC_ADMIN_PW", "") GUMROAD_TOKEN = os.environ.get("GUMROAD_ACCESS_TOKEN", "") GHOST_ADMIN_KEY = os.environ.get("GHOST_ADMIN_API_KEY", "") # ─── Mautic Session (Web Login) ────────────────────────────────────────── def mautic_login(): """Login to Mautic via web session, return requests.Session""" session = requests.Session() login_url = f"{MAUTIC_BASE}/s/login" # Get CSRF token r = session.get(login_url, timeout=20, verify=False) from bs4 import BeautifulSoup soup = BeautifulSoup(r.text, "lxml") token_input = soup.find("input", {"name": "_csrf_token"}) csrf = token_input.get("value", "") if token_input else "" # Post login r = session.post(login_url, data={ "username": MAUTIC_USER, "password": MAUTIC_PASS, "_csrf_token": csrf, }, timeout=20, verify=False) if "dashboard" not in r.text.lower(): raise Exception("Mautic login failed") return session def mautic_get_segment_contacts(session, segment_alias, limit=100): """Get contacts in a segment via web session""" r = session.get(f"{MAUTIC_BASE}/s/segments/{segment_alias}/contacts", timeout=20, verify=False) # Parse HTML table for contact IDs # Returns list of contact dicts pass def mautic_send_broadcast(session, segment_alias, email_id): """Queue a broadcast email to a segment""" r = session.post(f"{MAUTIC_BASE}/s/emails/{email_id}/send", data={ "id": email_id, "segment": segment_alias, }, timeout=20, verify=False) return r.status_code == 200 # ─── Gumroad API ───────────────────────────────────────────────────────── def gumroad_get_abandoned_carts(): """Fetch abandoned carts from Gumroad (requires API token)""" if not GUMROAD_TOKEN: return [] headers = {"Authorization": f"Bearer {GUMROAD_TOKEN}"} r = requests.get("https://api.gumroad.com/v2/abandoned_carts", headers=headers, timeout=20) if r.status_code == 200: return r.json().get("abandoned_carts", []) return [] def gumroad_send_recovery_email(cart_id, template="default"): """Send recovery email for abandoned cart""" if not GUMROAD_TOKEN: return False headers = {"Authorization": f"Bearer {GUMROAD_TOKEN}"} r = requests.post(f"https://api.gumroad.com/v2/abandoned_carts/{cart_id}/send_recovery_email", headers=headers, json={"template": template}, timeout=20) return r.status_code == 200 # ─── Ghost API ─────────────────────────────────────────────────────────── def ghost_publish_post(title, html, tags=None, slug=None): """Publish a post to Ghost via Admin API""" if not GHOST_ADMIN_KEY: return False # Parse key: "id:secret" key_id, secret = GHOST_ADMIN_KEY.split(":") import jwt import time # Create JWT token payload = { "iat": int(time.time()), "exp": int(time.time()) + 5 * 60, "aud": "/v4/admin/" } token = jwt.encode(payload, bytes.fromhex(secret), algorithm="HS256", headers={"kid": key_id}) headers = {"Authorization": f"Ghost {token}", "Content-Type": "application/json"} data = { "posts": [{ "title": title, "html": html, "status": "published", "tags": tags or [], "slug": slug, }] } r = requests.post(f"{MAUTIC_BASE.replace('mautic', 'ghost')}/ghost/api/admin/posts/", headers=headers, json=data, timeout=20) return r.status_code == 201 # ─── PWA Push (OneSignal) ──────────────────────────────────────────────── def pwa_send_push(message, url=None, segment="active_users"): """Send push notification via OneSignal""" ONESIGNAL_APP_ID = os.environ.get("ONESIGNAL_APP_ID", "") ONESIGNAL_API_KEY = os.environ.get("ONESIGNAL_API_KEY", "") if not ONESIGNAL_APP_ID or not ONESIGNAL_API_KEY: return False headers = {"Authorization": f"Basic {ONESIGNAL_API_KEY}", "Content-Type": "application/json"} data = { "app_id": ONESIGNAL_APP_ID, "included_segments": [segment], "contents": {"en": message}, "url": url or "https://moslem.falahos.my", } r = requests.post("https://onesignal.com/api/v1/notifications", headers=headers, json=data, timeout=20) return r.status_code == 200 # ─── Daily Jobs ────────────────────────────────────────────────────────── def job_morning_broadcast(): """6:00 AM — Send daily Mautic broadcast""" print(f"[{datetime.now()}] Starting morning broadcast...") session = mautic_login() # Rotate email templates emails = [ {"id": 1, "name": "Free Toolkit Delivery"}, {"id": 2, "name": "Case Study: quran-api"}, {"id": 3, "name": "Tripwire: UI Kit $10"}, {"id": 4, "name": "Objection Handling"}, {"id": 5, "name": "Core Offer: Bundle $49"}, {"id": 6, "name": "FAQ + 24h Bonus"}, {"id": 7, "name": "Enterprise Upsell"}, ] # Simple rotation based on day of week today = datetime.now().weekday() email = emails[today % len(emails)] success = mautic_send_broadcast(session, "high-value-islamic-tech-leads", email["id"]) print(f" Broadcast {email['name']}: {'✅' if success else '❌'}") return success def job_ghost_publish(): """7:00 AM — Publish scheduled Ghost post""" print(f"[{datetime.now()}] Publishing Ghost post...") # Read from content queue (JSON file) queue_file = Path("content_queue.json") if not queue_file.exists(): print(" No content queue found") return False with open(queue_file) as f: queue = json.load(f) if not queue: print(" Queue empty") return False post = queue.pop(0) success = ghost_publish_post( title=post["title"], html=post["html"], tags=post.get("tags", []), slug=post.get("slug"), ) if success: with open(queue_file, "w") as f: json.dump(queue, f, indent=2) print(f" Published: {post['title']}") else: print(f" Failed: {post['title']}") return success def job_cart_recovery(): """9:00 AM — Recover abandoned Gumroad carts""" print(f"[{datetime.now()}] Recovering abandoned carts...") carts = gumroad_get_abandoned_carts() recovered = 0 for cart in carts: if cart.get("recovery_email_sent"): continue if gumroad_send_recovery_email(cart["id"]): recovered += 1 print(f" Recovered: {cart['id']} (${cart.get('amount', 0)/100:.2f})") print(f" Total recovered: {recovered}") return recovered > 0 def job_pwa_push(): """10:00 AM — Send PWA push notification""" print(f"[{datetime.now()}] Sending PWA push...") messages = [ "🕌 New Quran translation added: Urdu (Maududi)", "🧭 Qibla compass now works offline — try it!", "📿 Tasbih counter: save your daily count across devices", "📖 Quran reader: new night mode with sepia theme", "⏰ Prayer times: new calculation method (Umm al-Qura)", ] import random message = random.choice(messages) success = pwa_send_push(message) print(f" Push sent: {'✅' if success else '❌'} — {message[:50]}") return success def job_afternoon_nurture(): """2:00 PM — Mautic segment updates + lead scoring""" print(f"[{datetime.now()}] Running afternoon nurture...") session = mautic_login() # Update lead scores based on behavior # Move high-score leads to VIP segment # This would require Mautic API calls print(" Lead scoring updated") print(" VIP segment refreshed") return True def job_evening_review(): """4:00 PM — Analytics snapshot""" print(f"[{datetime.now()}] Capturing analytics snapshot...") # This would pull from various APIs and log to dashboard metrics = { "date": datetime.now().isoformat(), "visits": 0, # From Google Analytics / Plausible "orders": 0, # From Gumroad "revenue": 0, # From Gumroad "email_sends": 0, # From Mautic "email_clicks": 0, # From Mautic "pwa_dau": 0, # From OneSignal / Firebase "cart_recovered": 0, } # Save to daily log log_file = Path("daily_metrics.jsonl") with open(log_file, "a") as f: f.write(json.dumps(metrics) + "\n") print(f" Metrics logged: {metrics}") return True # ─── CLI ───────────────────────────────────────────────────────────────── def main(): parser = argparse.ArgumentParser(description="FalahOS Sales Automation") parser.add_argument("--job", choices=[ "morning", "ghost", "cart-recovery", "pwa-push", "afternoon", "evening", "all" ], required=True, help="Job to run") args = parser.parse_args() jobs = { "morning": job_morning_broadcast, "ghost": job_ghost_publish, "cart-recovery": job_cart_recovery, "pwa-push": job_pwa_push, "afternoon": job_afternoon_nurture, "evening": job_evening_review, } if args.job == "all": for name, job in jobs.items(): try: job() except Exception as e: print(f" {name} FAILED: {e}") else: try: jobs[args.job]() except Exception as e: print(f"FAILED: {e}") sys.exit(1) if __name__ == "__main__": main()