a440160249
- SALES_STRATEGY.md: $1K/day plan (37 orders @ $27.50 AOV, 1,700 visits) - TRIPWIRE_FUNNEL.md: 7-email Mautic sequence + lead scoring - MONETIZATION.md: 9 monetization options for Nūr PWA (no ads, no registration) - EXECUTABLE_PLAN.md: MCP-executable automation plan - DAILY_OPS.md: daily KPI checklist (visits, orders, sends, recoveries) - scripts/: automation.py, mautic_setup.py, gumroad_setup.py, scout_scrape.py, analytics.py, crontab - quality_leads.json/.csv: 12 enriched Islamic-tech leads for Mautic import - content_queue.json: 10 SEO blog posts for Ghost - PWA: icons, sw.js, e2e + vitest coverage, generator scripts
305 lines
11 KiB
Python
Executable File
305 lines
11 KiB
Python
Executable File
#!/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() |