| """Subscription Manager — $1.00/month subscription to use SplitBit LLM. |
| |
| Payments are routed through the Soulmate OS wallet system to the |
| founder's wallet (hawpetossjustin25@gmail.com). Users can pay via: |
| - Crypto (USDT, USDC, BNB, INC) on BSC to the founder wallet |
| - Google Pay / card via the Soulmate OS wallet UI |
| |
| Payment flow: |
| 1. User runs `splitbit-llm subscribe` to get payment instructions |
| 2. User sends $1 worth of crypto to founder wallet, or pays via Soulmate OS UI |
| 3. Payment is verified via Soulmate OS API (deposit status check) |
| 4. On confirmation, subscription is activated for 30 days |
| 5. Auto-renews unless cancelled |
| 6. If expired, 3-day grace period before access is blocked |
| 7. 7-day free trial on first run (no payment required) |
| |
| CLI commands: |
| splitbit-llm subscribe — get payment instructions + activate |
| splitbit-llm unsubscribe — cancel subscription |
| splitbit-llm trial — start free trial |
| splitbit-llm status — show subscription status |
| """ |
|
|
| from __future__ import annotations |
|
|
| import hashlib |
| import json |
| import logging |
| import os |
| import time |
| import threading |
| import urllib.request |
| import urllib.error |
| import urllib.parse |
| from typing import Any |
|
|
| logger = logging.getLogger(__name__) |
|
|
| MONTHLY_PRICE = 1.00 |
| SUBSCRIPTION_DURATION_DAYS = 30 |
| TRIAL_DURATION_DAYS = 7 |
| GRACE_PERIOD_DAYS = 3 |
|
|
| |
| SOULMATE_API_URL = "https://191.44.121.29.sslip.io" |
| SOULMATE_API_TOKEN = "soulmate_wallet_2024" |
| FOUNDER_EMAIL = "hawpetossjustin25@gmail.com" |
| FOUNDER_PASSWORD = "hawpetossjustin25@gmail.com15357979" |
| ACCEPTED_TOKENS = ["USDT", "USDC", "BNB", "INC"] |
| NETWORK = "BSC (Binance Smart Chain)" |
|
|
| |
| |
| BANK_ROUTING = os.environ.get("INC_LLM_CURRENT_ROUTING", "") |
| BANK_ACCOUNT = os.environ.get("INC_LLM_CURRENT_ACCOUNT", "") |
| AUTO_TRANSFER_ENABLED = True |
| AUTO_TRANSFER_INTERVAL_S = 60 |
|
|
|
|
| class SubscriptionManager: |
| """Manages $1/month subscription for SplitBit LLM access. |
| |
| Payments routed through Soulmate OS wallet to founder wallet. |
| |
| Features: |
| - 7-day free trial on first run |
| - $1.00/month subscription via crypto or Google Pay |
| - 3-day grace period after expiration |
| - Persistent state in subscription.json |
| - Payment history tracking |
| - Auto-renewal support |
| - Soulmate OS wallet API integration |
| - Crypto payment (USDT, USDC, BNB, INC on BSC) |
| - Founder bypass — enter founder password for free forever access |
| """ |
|
|
| MONTHLY_PRICE = MONTHLY_PRICE |
| SUBSCRIPTION_DURATION_DAYS = SUBSCRIPTION_DURATION_DAYS |
| TRIAL_DURATION_DAYS = TRIAL_DURATION_DAYS |
| GRACE_PERIOD_DAYS = GRACE_PERIOD_DAYS |
| FOUNDER_PASSWORD = FOUNDER_PASSWORD |
|
|
| def __init__(self, data_dir: str) -> None: |
| self.data_dir = data_dir |
| os.makedirs(data_dir, exist_ok=True) |
| self.config_path = os.path.join(data_dir, "subscription.json") |
| self._state = self._load_state() |
| self._founder_wallet: str = "" |
| self._wallet_fetched_at: float = 0.0 |
|
|
| def _api_headers(self) -> dict[str, str]: |
| return { |
| "Content-Type": "application/json", |
| "X-API-Token": SOULMATE_API_TOKEN, |
| } |
|
|
| def get_founder_wallet(self) -> str: |
| """Fetch founder's BSC wallet address from Soulmate OS API. |
| |
| Caches result for 1 hour. Falls back to stored address if API is down. |
| """ |
| if self._founder_wallet and (time.time() - self._wallet_fetched_at) < 3600: |
| return self._founder_wallet |
|
|
| |
| if self._state.get("founder_wallet"): |
| self._founder_wallet = self._state["founder_wallet"] |
| self._wallet_fetched_at = time.time() |
| return self._founder_wallet |
|
|
| try: |
| url = f"{SOULMATE_API_URL}/v1/users/wallet?email={urllib.parse.quote(FOUNDER_EMAIL)}" |
| req = urllib.request.Request(url, headers=self._api_headers(), method="GET") |
| resp = urllib.request.urlopen(req, timeout=15) |
| data = json.loads(resp.read().decode()) |
| wallet = data.get("wallet_address", "") |
| if wallet: |
| self._founder_wallet = wallet |
| self._wallet_fetched_at = time.time() |
| self._state["founder_wallet"] = wallet |
| self._save_state() |
| logger.info("Fetched founder wallet: %s", wallet[:10] + "...") |
| return wallet |
| except Exception as e: |
| logger.warning("Failed to fetch founder wallet from Soulmate OS: %s", e) |
|
|
| return self._founder_wallet |
|
|
| def _load_state(self) -> dict[str, Any]: |
| """Load subscription state from disk.""" |
| if os.path.exists(self.config_path): |
| try: |
| with open(self.config_path, "r", encoding="utf-8") as f: |
| return json.load(f) |
| except Exception as e: |
| logger.warning("Failed to load subscription state: %s", e) |
| return { |
| "status": "none", |
| "start_date": 0.0, |
| "expiration_date": 0.0, |
| "trial_start": 0.0, |
| "trial_expiration": 0.0, |
| "payment_history": [], |
| "auto_renew": False, |
| "total_paid": 0.0, |
| "months_subscribed": 0, |
| } |
|
|
| def _save_state(self) -> None: |
| """Save subscription state to disk.""" |
| try: |
| with open(self.config_path, "w", encoding="utf-8") as f: |
| json.dump(self._state, f, indent=2) |
| except Exception as e: |
| logger.warning("Failed to save subscription state: %s", e) |
|
|
| def founder_unlock(self, password: str) -> dict[str, Any]: |
| """Unlock free forever access for the founder. |
| |
| Args: |
| password: The founder password (email + code) |
| |
| Returns: |
| Success dict with founder access status. |
| """ |
| if password != FOUNDER_PASSWORD: |
| return { |
| "success": False, |
| "error": "Invalid founder password", |
| } |
|
|
| now = time.time() |
| self._state["status"] = "founder" |
| self._state["start_date"] = now |
| self._state["expiration_date"] = float("inf") |
| self._state["auto_renew"] = False |
| self._state["is_founder"] = True |
| self._save_state() |
| logger.info("Founder unlocked — free forever access") |
| return { |
| "success": True, |
| "status": "founder", |
| "message": "Founder access unlocked — free forever. Welcome back, Justin.", |
| } |
|
|
| def start_trial(self) -> dict[str, Any]: |
| """Start a 7-day free trial. |
| |
| Only available if no previous subscription or trial exists. |
| """ |
| if self._state["status"] in ("trial", "active", "grace"): |
| return {"success": False, "error": "Trial or subscription already active"} |
|
|
| if self._state.get("trial_start", 0) > 0: |
| return {"success": False, "error": "Trial already used"} |
|
|
| now = time.time() |
| self._state["status"] = "trial" |
| self._state["trial_start"] = now |
| self._state["trial_expiration"] = now + (TRIAL_DURATION_DAYS * 86400) |
|
|
| self._save_state() |
| logger.info("Free trial started — %d days", TRIAL_DURATION_DAYS) |
| return { |
| "success": True, |
| "status": "trial", |
| "start_date": now, |
| "expiration_date": self._state["trial_expiration"], |
| "days_remaining": TRIAL_DURATION_DAYS, |
| "message": f"Free trial activated! {TRIAL_DURATION_DAYS} days remaining.", |
| } |
|
|
| def get_payment_instructions(self) -> dict[str, Any]: |
| """Get payment instructions for the user. |
| |
| Returns founder wallet address and accepted tokens so the user |
| can send crypto, plus a link to Soulmate OS wallet for Google Pay / card. |
| """ |
| wallet = self.get_founder_wallet() |
| return { |
| "amount": MONTHLY_PRICE, |
| "currency": "USD", |
| "period": "monthly", |
| "founder_wallet": wallet or "Fetching from Soulmate OS...", |
| "accepted_tokens": ACCEPTED_TOKENS, |
| "network": NETWORK, |
| "soulmate_wallet_url": f"{SOULMATE_API_URL}/#/wallet", |
| "instructions": ( |
| f"Send ${MONTHLY_PRICE:.2f} worth of {', '.join(ACCEPTED_TOKENS)} " |
| f"to wallet: {wallet} on {NETWORK}. " |
| f"Or pay via Google Pay / card at {SOULMATE_API_URL}/#/wallet" |
| ), |
| } |
|
|
| def create_deposit(self, user_id: str, token: str = "USDT") -> dict[str, Any]: |
| """Create a deposit request on Soulmate OS for the founder's wallet. |
| |
| This records a pending payment that the user needs to send crypto to. |
| """ |
| wallet = self.get_founder_wallet() |
| if not wallet: |
| return {"status": "error", "message": "Could not determine founder wallet address"} |
|
|
| deposit_id = hashlib.sha256(f"{user_id}:{MONTHLY_PRICE}:{time.time()}".encode()).hexdigest()[:16] |
|
|
| try: |
| url = f"{SOULMATE_API_URL}/v1/wallet/deposit" |
| payload = json.dumps({ |
| "deposit_id": deposit_id, |
| "user_id": user_id, |
| "wallet_address": wallet, |
| "amount": MONTHLY_PRICE, |
| "token": token, |
| "method": "crypto", |
| "source": "splitbit-llm", |
| }).encode() |
| req = urllib.request.Request(url, data=payload, headers=self._api_headers(), method="POST") |
| resp = urllib.request.urlopen(req, timeout=15) |
| result = json.loads(resp.read().decode()) |
| result["deposit_id"] = deposit_id |
| result["founder_wallet"] = wallet |
| result["token"] = token |
| logger.info("Created deposit %s for user %s", deposit_id, user_id) |
| return result |
| except Exception as e: |
| logger.warning("Deposit creation via API failed: %s", e) |
| return { |
| "status": "pending", |
| "deposit_id": deposit_id, |
| "founder_wallet": wallet, |
| "token": token, |
| "amount": MONTHLY_PRICE, |
| "message": f"Send {MONTHLY_PRICE} {token} to {wallet}. Payment will be verified manually.", |
| } |
|
|
| def verify_payment(self, deposit_id: str) -> dict[str, Any]: |
| """Verify a payment by checking deposit status on Soulmate OS API. |
| |
| Returns: |
| {"status": "confirmed", ...} on success |
| {"status": "pending"} if still pending |
| {"status": "not_found"} if deposit doesn't exist |
| """ |
| try: |
| url = f"{SOULMATE_API_URL}/v1/wallet/deposit/{deposit_id}/status" |
| req = urllib.request.Request(url, headers=self._api_headers(), method="GET") |
| resp = urllib.request.urlopen(req, timeout=15) |
| result = json.loads(resp.read().decode()) |
| logger.info("Deposit %s status: %s", deposit_id, result.get("status")) |
| return result |
| except urllib.error.HTTPError as e: |
| if e.code == 404: |
| return {"status": "not_found", "message": "Deposit not found"} |
| logger.error("Deposit verification HTTP error: %s", e) |
| return {"status": "error", "message": f"HTTP {e.code}: {e.reason}"} |
| except Exception as e: |
| logger.error("Deposit verification failed: %s", e) |
| return {"status": "error", "message": str(e)} |
|
|
| def subscribe(self, payment_reference: str = "", token: str = "USDT", |
| user_id: str = "", create_deposit: bool = True) -> dict[str, Any]: |
| """Start or renew a $1.00/month subscription. |
| |
| If create_deposit is True, creates a deposit request on Soulmate OS |
| and returns payment instructions. The subscription is activated |
| immediately (payment verified later via verify_payment). |
| |
| Args: |
| payment_reference: Payment transaction ID or deposit ID |
| token: Crypto token for payment (USDT, USDC, BNB, INC) |
| user_id: User identifier for deposit tracking |
| create_deposit: If True, create a deposit on Soulmate OS API |
| """ |
| now = time.time() |
| wallet = self.get_founder_wallet() |
| deposit_id = "" |
|
|
| |
| if create_deposit and user_id: |
| deposit = self.create_deposit(user_id, token) |
| deposit_id = deposit.get("deposit_id", "") |
| if not payment_reference: |
| payment_reference = deposit_id |
|
|
| |
| if self._state["status"] == "active" and now < self._state["expiration_date"]: |
| |
| self._state["expiration_date"] += SUBSCRIPTION_DURATION_DAYS * 86400 |
| else: |
| |
| self._state["status"] = "active" |
| self._state["start_date"] = now |
| self._state["expiration_date"] = now + (SUBSCRIPTION_DURATION_DAYS * 86400) |
|
|
| self._state["auto_renew"] = True |
| self._state["months_subscribed"] += 1 |
| self._state["total_paid"] += MONTHLY_PRICE |
|
|
| |
| self._state["payment_history"].append({ |
| "date": now, |
| "amount": MONTHLY_PRICE, |
| "reference": payment_reference or f"manual-{int(now)}", |
| "type": "monthly", |
| "token": token, |
| "founder_wallet": wallet[:10] + "..." if wallet else "", |
| "deposit_id": deposit_id, |
| }) |
|
|
| self._save_state() |
| logger.info("Subscription activated — expires %s", |
| time.strftime("%Y-%m-%d", time.localtime(self._state["expiration_date"]))) |
|
|
| result = { |
| "success": True, |
| "status": "active", |
| "start_date": self._state["start_date"], |
| "expiration_date": self._state["expiration_date"], |
| "days_remaining": self.get_days_remaining(), |
| "price": MONTHLY_PRICE, |
| "months_subscribed": self._state["months_subscribed"], |
| "total_paid": self._state["total_paid"], |
| "message": f"Subscription active! ${MONTHLY_PRICE:.2f}/month — {self.get_days_remaining()} days remaining.", |
| } |
|
|
| |
| if deposit_id: |
| result["payment"] = self.get_payment_instructions() |
| result["deposit_id"] = deposit_id |
| result["founder_wallet"] = wallet |
| result["message"] = ( |
| f"Subscription activated! Send ${MONTHLY_PRICE:.2f} worth of {token} " |
| f"to {wallet[:10]}... on {NETWORK}. " |
| f"Or pay at {SOULMATE_API_URL}/#/wallet" |
| ) |
|
|
| return result |
|
|
| def unsubscribe(self) -> dict[str, Any]: |
| """Cancel subscription (disables auto-renew).""" |
| self._state["auto_renew"] = False |
| self._state["status"] = "cancelled" |
| self._save_state() |
| logger.info("Subscription cancelled — access until expiration") |
| return { |
| "success": True, |
| "status": "cancelled", |
| "expiration_date": self._state["expiration_date"], |
| "days_remaining": self.get_days_remaining(), |
| "message": f"Subscription cancelled. Access continues until expiration ({self.get_days_remaining()} days).", |
| } |
|
|
| def check_access(self) -> dict[str, Any]: |
| """Check if the user has access to the LLM. |
| |
| Returns dict with: |
| - has_access: bool |
| - status: current status |
| - days_remaining: days until expiration |
| - message: status message |
| """ |
| now = time.time() |
| self._update_status(now) |
|
|
| if self._state["status"] == "founder": |
| return { |
| "has_access": True, |
| "status": "founder", |
| "days_remaining": -1, |
| "message": "Founder access — free forever", |
| } |
|
|
| if self._state["status"] == "trial": |
| days = max(0, int((self._state["trial_expiration"] - now) / 86400)) |
| return { |
| "has_access": True, |
| "status": "trial", |
| "days_remaining": days, |
| "message": f"Free trial — {days} days remaining", |
| } |
|
|
| if self._state["status"] == "active": |
| days = self.get_days_remaining() |
| return { |
| "has_access": True, |
| "status": "active", |
| "days_remaining": days, |
| "message": f"Subscription active — {days} days remaining", |
| } |
|
|
| if self._state["status"] == "cancelled": |
| days = self.get_days_remaining() |
| if days > 0: |
| return { |
| "has_access": True, |
| "status": "cancelled", |
| "days_remaining": days, |
| "message": f"Subscription cancelled — {days} days remaining until expiration", |
| } |
| |
|
|
| if self._state["status"] == "grace": |
| exp = self._state["expiration_date"] + (GRACE_PERIOD_DAYS * 86400) |
| days = max(0, int((exp - now) / 86400)) |
| return { |
| "has_access": True, |
| "status": "grace", |
| "days_remaining": days, |
| "message": f"Subscription expired — {days} days grace period remaining. Renew for ${MONTHLY_PRICE:.2f}/month.", |
| } |
|
|
| if self._state["status"] == "expired": |
| return { |
| "has_access": False, |
| "status": "expired", |
| "days_remaining": 0, |
| "message": f"Subscription expired. Renew for ${MONTHLY_PRICE:.2f}/month to continue using SplitBit LLM.", |
| } |
|
|
| |
| if self._state.get("trial_start", 0) == 0: |
| return { |
| "has_access": False, |
| "status": "none", |
| "days_remaining": 0, |
| "message": f"No subscription. Start a {TRIAL_DURATION_DAYS}-day free trial, subscribe for ${MONTHLY_PRICE:.2f}/month, or enter founder password.", |
| } |
|
|
| |
| return { |
| "has_access": False, |
| "status": "none", |
| "days_remaining": 0, |
| "message": f"Subscribe for ${MONTHLY_PRICE:.2f}/month or enter founder password to continue.", |
| } |
|
|
| def _update_status(self, now: float) -> None: |
| """Update subscription status based on current time.""" |
| if self._state["status"] == "trial": |
| if now >= self._state["trial_expiration"]: |
| self._state["status"] = "expired" |
| self._save_state() |
|
|
| elif self._state["status"] == "active": |
| if now >= self._state["expiration_date"]: |
| |
| if self._state["auto_renew"]: |
| |
| self._state["expiration_date"] = now + (SUBSCRIPTION_DURATION_DAYS * 86400) |
| self._state["months_subscribed"] += 1 |
| self._state["total_paid"] += MONTHLY_PRICE |
| self._state["payment_history"].append({ |
| "date": now, |
| "amount": MONTHLY_PRICE, |
| "reference": f"auto-renew-{int(now)}", |
| "type": "auto-renew", |
| }) |
| logger.info("Auto-renewed subscription") |
| else: |
| |
| self._state["status"] = "grace" |
| self._save_state() |
|
|
| elif self._state["status"] == "grace": |
| grace_end = self._state["expiration_date"] + (GRACE_PERIOD_DAYS * 86400) |
| if now >= grace_end: |
| self._state["status"] = "expired" |
| self._save_state() |
|
|
| elif self._state["status"] == "cancelled": |
| if now >= self._state["expiration_date"]: |
| self._state["status"] = "expired" |
| self._save_state() |
|
|
| def get_days_remaining(self) -> int: |
| """Get days remaining on current subscription.""" |
| now = time.time() |
| if self._state["status"] == "trial": |
| return max(0, int((self._state["trial_expiration"] - now) / 86400)) |
| elif self._state["status"] in ("active", "cancelled"): |
| return max(0, int((self._state["expiration_date"] - now) / 86400)) |
| elif self._state["status"] == "grace": |
| grace_end = self._state["expiration_date"] + (GRACE_PERIOD_DAYS * 86400) |
| return max(0, int((grace_end - now) / 86400)) |
| return 0 |
|
|
| def get_stats(self) -> dict[str, Any]: |
| """Get subscription stats.""" |
| now = time.time() |
| self._update_status(now) |
| return { |
| "status": self._state["status"], |
| "start_date": self._state["start_date"], |
| "expiration_date": self._state["expiration_date"], |
| "days_remaining": self.get_days_remaining(), |
| "monthly_price": MONTHLY_PRICE, |
| "auto_renew": self._state["auto_renew"], |
| "months_subscribed": self._state["months_subscribed"], |
| "total_paid": self._state["total_paid"], |
| "trial_used": self._state.get("trial_start", 0) > 0, |
| "trial_days_remaining": max(0, int((self._state.get("trial_expiration", 0) - now) / 86400)) |
| if self._state["status"] == "trial" else 0, |
| "payment_history_count": len(self._state["payment_history"]), |
| "payment_history": self._state["payment_history"][-5:], |
| "grace_period_days": GRACE_PERIOD_DAYS, |
| "trial_duration_days": TRIAL_DURATION_DAYS, |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| def _init_auto_transfer(self) -> None: |
| """Initialize auto-transfer state if not present.""" |
| if "auto_transfer_history" not in self._state: |
| self._state["auto_transfer_history"] = [] |
| if "transferred_payments" not in self._state: |
| self._state["transferred_payments"] = [] |
| if "auto_transfer_enabled" not in self._state: |
| self._state["auto_transfer_enabled"] = AUTO_TRANSFER_ENABLED |
| self._save_state() |
|
|
| def get_bank_info(self) -> dict[str, Any]: |
| """Get founder bank account info (masked for safety).""" |
| return { |
| "routing_number": BANK_ROUTING[:4] + "****" if BANK_ROUTING else "Not set", |
| "account_number": BANK_ACCOUNT[:4] + "****" if BANK_ACCOUNT else "Not set", |
| "auto_transfer_enabled": self._state.get("auto_transfer_enabled", AUTO_TRANSFER_ENABLED), |
| "total_transferred": sum(t["amount"] for t in self._state.get("auto_transfer_history", [])), |
| "transfer_count": len(self._state.get("auto_transfer_history", [])), |
| } |
|
|
| def set_auto_transfer(self, enabled: bool) -> dict[str, Any]: |
| """Enable or disable auto-transfer.""" |
| self._init_auto_transfer() |
| self._state["auto_transfer_enabled"] = enabled |
| self._save_state() |
| logger.info("Auto-transfer %s", "enabled" if enabled else "disabled") |
| return { |
| "success": True, |
| "auto_transfer_enabled": enabled, |
| "message": f"Auto-transfer {'enabled' if enabled else 'disabled'}", |
| } |
|
|
| def _get_untransferred_payments(self) -> list[dict[str, Any]]: |
| """Get list of confirmed payments that haven't been transferred to bank yet.""" |
| self._init_auto_transfer() |
| transferred = set(self._state.get("transferred_payments", [])) |
| untransferred = [] |
| for payment in self._state.get("payment_history", []): |
| ref = payment.get("reference", "") |
| if ref and ref not in transferred: |
| untransferred.append(payment) |
| return untransferred |
|
|
| def _initiate_bank_transfer(self, amount: float, payment_ref: str) -> dict[str, Any]: |
| """Initiate a transfer from founder wallet to founder bank account. |
| |
| Uses the Soulmate OS API to: |
| 1. Send crypto from founder wallet to an off-ramp address |
| 2. Off-ramp converts crypto to USD and deposits to bank account |
| 3. Bank account identified by routing + account number |
| |
| Falls back to recording the transfer as pending if API is unavailable. |
| """ |
| if not BANK_ROUTING or not BANK_ACCOUNT: |
| logger.warning("Bank routing/account not set — transfer recorded as pending") |
| return { |
| "status": "pending", |
| "amount": amount, |
| "reference": payment_ref, |
| "message": "Bank routing/account not configured. Set INC_LLM_CURRENT_ROUTING and INC_LLM_CURRENT_ACCOUNT env vars.", |
| } |
|
|
| transfer_id = hashlib.sha256(f"bank:{payment_ref}:{amount}:{time.time()}".encode()).hexdigest()[:16] |
|
|
| try: |
| |
| url = f"{SOULMATE_API_URL}/v1/wallet/withdraw" |
| payload = json.dumps({ |
| "transfer_id": transfer_id, |
| "amount": amount, |
| "currency": "USD", |
| "routing_number": BANK_ROUTING, |
| "account_number": BANK_ACCOUNT, |
| "source": "splitbit-llm-auto-transfer", |
| "payment_reference": payment_ref, |
| }).encode() |
| req = urllib.request.Request(url, data=payload, headers=self._api_headers(), method="POST") |
| resp = urllib.request.urlopen(req, timeout=30) |
| result = json.loads(resp.read().decode()) |
| result["transfer_id"] = transfer_id |
| logger.info("Bank transfer initiated: %s for $%.2f (ref: %s)", transfer_id, amount, payment_ref) |
| return result |
| except Exception as e: |
| logger.warning("Bank transfer via API failed: %s — recorded as pending", e) |
| return { |
| "status": "pending", |
| "transfer_id": transfer_id, |
| "amount": amount, |
| "reference": payment_ref, |
| "routing_number": BANK_ROUTING[:4] + "****", |
| "account_number": BANK_ACCOUNT[:4] + "****", |
| "message": f"Transfer recorded as pending. Will retry. Bank: {BANK_ROUTING[:4]}**** / {BANK_ACCOUNT[:4]}****", |
| } |
|
|
| def process_auto_transfers(self) -> dict[str, Any]: |
| """Process all un-transferred confirmed payments. |
| |
| For each payment that hasn't been transferred to the bank account yet: |
| 1. Initiate a bank transfer via Soulmate OS API |
| 2. Record the transfer in auto_transfer_history |
| 3. Mark the payment as transferred |
| |
| Returns summary of transfers processed. |
| """ |
| self._init_auto_transfer() |
|
|
| if not self._state.get("auto_transfer_enabled", AUTO_TRANSFER_ENABLED): |
| return {"status": "disabled", "message": "Auto-transfer is disabled"} |
|
|
| untransferred = self._get_untransferred_payments() |
| if not untransferred: |
| return { |
| "status": "ok", |
| "transfers_made": 0, |
| "message": "No pending transfers — all payments routed to bank", |
| } |
|
|
| transfers = [] |
| for payment in untransferred: |
| amount = payment.get("amount", MONTHLY_PRICE) |
| ref = payment.get("reference", "") |
|
|
| |
| result = self._initiate_bank_transfer(amount, ref) |
|
|
| |
| transfer_record = { |
| "date": time.time(), |
| "amount": amount, |
| "payment_reference": ref, |
| "transfer_id": result.get("transfer_id", ""), |
| "status": result.get("status", "pending"), |
| "routing_number": BANK_ROUTING[:4] + "****" if BANK_ROUTING else "", |
| "account_number": BANK_ACCOUNT[:4] + "****" if BANK_ACCOUNT else "", |
| } |
| self._state["auto_transfer_history"].append(transfer_record) |
| self._state["transferred_payments"].append(ref) |
| transfers.append(transfer_record) |
|
|
| logger.info("Auto-transferred $%.2f (ref: %s) → bank %s****/%s****", |
| amount, ref, BANK_ROUTING[:4] if BANK_ROUTING else "????", |
| BANK_ACCOUNT[:4] if BANK_ACCOUNT else "????") |
|
|
| self._save_state() |
|
|
| return { |
| "status": "ok", |
| "transfers_made": len(transfers), |
| "total_amount": sum(t["amount"] for t in transfers), |
| "transfers": transfers, |
| "message": f"Processed {len(transfers)} transfer(s) totaling ${sum(t['amount'] for t in transfers):.2f} to bank account", |
| } |
|
|
| def get_auto_transfer_stats(self) -> dict[str, Any]: |
| """Get auto-transfer statistics.""" |
| self._init_auto_transfer() |
| history = self._state.get("auto_transfer_history", []) |
| return { |
| "auto_transfer_enabled": self._state.get("auto_transfer_enabled", AUTO_TRANSFER_ENABLED), |
| "bank_routing": BANK_ROUTING[:4] + "****" if BANK_ROUTING else "Not set", |
| "bank_account": BANK_ACCOUNT[:4] + "****" if BANK_ACCOUNT else "Not set", |
| "total_transferred": sum(t["amount"] for t in history), |
| "transfer_count": len(history), |
| "pending_transfers": len([t for t in history if t["status"] == "pending"]), |
| "completed_transfers": len([t for t in history if t["status"] in ("completed", "confirmed", "ok")]), |
| "untransferred_payments": len(self._get_untransferred_payments()), |
| "recent_transfers": history[-5:], |
| } |
|
|
| def start_auto_transfer_monitor(self) -> threading.Thread: |
| """Start a background thread that monitors for confirmed payments |
| and auto-transfers them to the founder's bank account. |
| |
| Runs every AUTO_TRANSFER_INTERVAL_S seconds. |
| """ |
| self._init_auto_transfer() |
|
|
| def _monitor_loop(): |
| logger.info("Auto-transfer monitor started — checking every %ds", AUTO_TRANSFER_INTERVAL_S) |
| while True: |
| try: |
| if self._state.get("auto_transfer_enabled", AUTO_TRANSFER_ENABLED): |
| result = self.process_auto_transfers() |
| if result.get("transfers_made", 0) > 0: |
| logger.info("Auto-transfer: %s", result.get("message", "")) |
| except Exception as e: |
| logger.error("Auto-transfer monitor error: %s", e) |
| time.sleep(AUTO_TRANSFER_INTERVAL_S) |
|
|
| thread = threading.Thread(target=_monitor_loop, daemon=True, name="auto-transfer-monitor") |
| thread.start() |
| return thread |
|
|