new-site/scripts/workers/relay_integration.py
justin f8cd37ac8c Initial commit — Performance West telecom compliance platform
Includes: API (Express/TypeScript), Astro site, Python workers,
document generators, FCC compliance tools, Canada CRTC formation,
Ansible infrastructure, and deployment scripts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 06:54:22 -05:00

291 lines
11 KiB
Python

"""
Relay Financial (relayfi.com) integration for Performance West.
Relay is our business banking platform. No public API exists, so integration
is via:
1. Encrypted card details stored in ERPNext Sensitive ID doctype
2. Playwright automation to enter card on state portal payment forms
3. Relay deposit email monitoring (relay_deposit_monitor.py) for funds gate
4. Manual ACH for commission payouts (via Relay dashboard)
Multi-card support:
- SID-0002 (Stripe Issuing): Virtual card funded by Stripe balance — used for
card/ACH/Klarna orders. Funds available as soon as Stripe
settlement clears (typically T+2 for cards, T+4 for ACH).
- SID-0001 (PayPal MC): PayPal Mastercard — used for PayPal-funded orders.
Funds available immediately in PayPal balance.
- crypto-filing-card: Secondary card — used for crypto-funded orders (SHKeeper).
Admin manually advances "Awaiting Funds" after confirming
SHKeeper wallet balance.
- relay-filing-card: Legacy Relay virtual debit card — fallback only.
IMPORTANT: Card details are stored encrypted in ERPNext using the Password
fieldtype (AES). They are decrypted only in memory during the filing
automation, never written to disk or logs.
"""
from __future__ import annotations
import logging
import os
from dataclasses import dataclass
from typing import Optional
LOG = logging.getenv("RELAY_LOG_LEVEL", "workers.relay")
LOG = logging.getLogger("workers.relay")
# ERPNext Sensitive ID names for each card
RELAY_FILING_CARD_ID = os.getenv("RELAY_FILING_CARD_ID", "relay-filing-card")
STRIPE_FILING_CARD_ID = os.getenv("STRIPE_FILING_CARD_ID", "SID-0002")
CRYPTO_FILING_CARD_ID = os.getenv("CRYPTO_FILING_CARD_ID", "crypto-filing-card")
PAYPAL_FILING_CARD_ID = os.getenv("PAYPAL_FILING_CARD_ID", "SID-0001")
# Legacy alias — defaults to Relay card for backward compatibility
FILING_CARD_ID = RELAY_FILING_CARD_ID
@dataclass
class CardDetails:
"""Debit card details for state filing payments."""
card_number: str
exp_month: str
exp_year: str
cvv: str
cardholder_name: str = "Performance West Inc"
billing_address_line1: str = ""
billing_address_line2: str = ""
billing_city: str = ""
billing_state: str = ""
billing_zip: str = "82001"
@property
def exp_mmyy(self) -> str:
return f"{self.exp_month}/{self.exp_year}"
@property
def exp_mmslashyyyy(self) -> str:
yr = self.exp_year if len(self.exp_year) == 4 else f"20{self.exp_year}"
return f"{self.exp_month}/{yr}"
def masked(self) -> str:
"""Return masked card number for logging (last 4 only)."""
return f"****{self.card_number[-4:]}" if len(self.card_number) >= 4 else "****"
def _load_card_by_id(sensitive_id_name: str) -> Optional[CardDetails]:
"""
Load a debit card from ERPNext Sensitive ID by record name.
Uses the whitelisted API endpoint performancewest_erpnext.api.get_filing_card
which decrypts the Password field server-side. Frappe's standard REST API
masks Password fields with ********, so get_list/get_doc cannot be used.
"""
from scripts.workers.erpnext_client import ERPNextClient
try:
client = ERPNextClient()
data = client.call_method(
"performancewest_erpnext.api.get_filing_card",
{"card_name": sensitive_id_name},
)
if not data:
LOG.error("Filing card not found or empty (name=%s)", sensitive_id_name)
return None
card = CardDetails(
card_number=data["number"],
exp_month=data["exp_month"],
exp_year=data["exp_year"],
cvv=data["cvv"],
cardholder_name=data.get("name", "Performance West Inc"),
billing_address_line1=data.get("address_line1", ""),
billing_address_line2=data.get("address_line2", ""),
billing_city=data.get("city", ""),
billing_state=data.get("state", ""),
billing_zip=data.get("zip", "82001"),
)
LOG.info("Loaded card %s (%s) for filing", card.masked(), sensitive_id_name)
return card
except Exception as e:
LOG.error("Failed to load card %s from ERPNext: %s", sensitive_id_name, e)
return None
def load_card_from_erpnext(payment_method: str = "card") -> Optional[CardDetails]:
"""
Load the appropriate filing card based on the order's payment method.
- Stripe-funded orders (card, ach, klarna): use STRIPE_FILING_CARD_ID (Stripe Issuing virtual card)
- PayPal-funded orders (paypal): use PAYPAL_FILING_CARD_ID (PayPal Mastercard)
- Crypto-funded orders (crypto via SHKeeper): use CRYPTO_FILING_CARD_ID
- Fallback (legacy Relay): use RELAY_FILING_CARD_ID
Args:
payment_method: The payment method used for the customer order.
Returns:
CardDetails or None if not found/error.
"""
if payment_method == "crypto":
card_id = CRYPTO_FILING_CARD_ID
elif payment_method == "paypal":
card_id = PAYPAL_FILING_CARD_ID
elif payment_method in ("card", "ach", "klarna"):
card_id = STRIPE_FILING_CARD_ID
else:
card_id = RELAY_FILING_CARD_ID
return _load_card_by_id(card_id)
def populate_order_payment(order, payment_method: str = "card") -> bool:
"""
Load the correct filing card and populate it on the FormationOrder.
Selects card based on payment_method:
- card/ach/klarna → Relay virtual debit card (funded by Stripe payouts)
- crypto → Crypto.com card (funded by crypto proceeds)
Card details are held in memory only — never logged or written to disk.
"""
# Try to infer payment_method from the order itself if not passed
if hasattr(order, "payment_method") and order.payment_method:
payment_method = order.payment_method
card = load_card_from_erpnext(payment_method)
if not card:
return False
order.payment_card_number = card.card_number
order.payment_card_exp = card.exp_mmyy
order.payment_card_cvv = card.cvv
order.payment_card_name = card.cardholder_name
order.payment_card_zip = card.billing_zip
LOG.info(
"Payment card %s (%s) loaded onto order %s",
card.masked(),
"Crypto.com" if payment_method == "crypto" else "Relay",
getattr(order, "order_id", "?"),
)
return True
def mark_reservation_spent(order_id: str, amount_cents: int) -> None:
"""Mark a filing fee reservation as spent after Playwright confirms the charge."""
try:
import psycopg2
DATABASE_URL = os.getenv("DATABASE_URL", "")
if not DATABASE_URL:
return
with psycopg2.connect(DATABASE_URL) as conn:
with conn.cursor() as cur:
cur.execute(
"""
UPDATE filing_fee_reservations
SET status = 'spent', spent_at = NOW()
WHERE order_id = %s AND status = 'reserved'
""",
(order_id,),
)
conn.commit()
LOG.info("Marked reservation spent: %s ($%.2f)", order_id, amount_cents / 100)
except Exception as exc:
LOG.error("Failed to mark reservation spent for %s: %s", order_id, exc)
def release_reservation(order_id: str) -> None:
"""Release a reserved filing fee back to the available pool (on filing failure)."""
try:
import psycopg2
DATABASE_URL = os.getenv("DATABASE_URL", "")
if not DATABASE_URL:
return
with psycopg2.connect(DATABASE_URL) as conn:
with conn.cursor() as cur:
cur.execute(
"""
UPDATE filing_fee_reservations
SET status = 'released', released_at = NOW()
WHERE order_id = %s AND status = 'reserved'
""",
(order_id,),
)
conn.commit()
LOG.info("Released filing fee reservation for %s", order_id)
except Exception as exc:
LOG.error("Failed to release reservation for %s: %s", order_id, exc)
def record_filing_payment(
order_name: str,
state_code: str,
amount_cents: int,
card_last4: str,
confirmation: str = "",
) -> bool:
"""Record a filing payment in ERPNext for reconciliation with Relay transactions.
Creates a Journal Entry or custom Payment record that can be matched
against the Relay bank statement when imported via Plaid/CSV.
"""
from scripts.workers.erpnext_client import ERPNextClient
try:
client = ERPNextClient()
# Add a comment to the Formation Order with payment details
client.update_resource("Formation Order", order_name, {
"admin_notes": (
f"Payment: ${amount_cents/100:.2f} charged to Relay card ****{card_last4} "
f"for {state_code} filing. "
f"{'Confirmation: ' + confirmation if confirmation else 'Pending confirmation.'}"
),
})
LOG.info("Recorded payment of $%.2f for %s on Relay card ****%s",
amount_cents / 100, order_name, card_last4)
return True
except Exception as e:
LOG.error("Failed to record filing payment: %s", e)
return False
def track_commission_payout(
partner_name: str,
partner_email: str,
amount_cents: int,
order_numbers: list[str],
) -> bool:
"""Track a commission payout owed to a referral partner.
Since Relay has no ACH API, this creates an ERPNext record that
an admin uses to manually send the ACH via the Relay dashboard.
The ERPNext record serves as:
- A reminder to send the payout
- An audit trail of what was paid
- Reconciliation when the Relay transaction appears
"""
from scripts.workers.erpnext_client import ERPNextClient
try:
client = ERPNextClient()
client.create_resource("Issue", {
"subject": f"Commission Payout: ${amount_cents/100:.2f} to {partner_name}",
"description": (
f"Referral commission payout due:\n\n"
f"Partner: {partner_name}\n"
f"Email: {partner_email}\n"
f"Amount: ${amount_cents/100:.2f}\n"
f"Orders: {', '.join(order_numbers)}\n\n"
f"Action: Send ACH payment via Relay dashboard to {partner_email}.\n"
f"Mark this issue as resolved after payment is sent."
),
"issue_type": "Feature Request", # Using as a task type
"priority": "Medium",
})
LOG.info("Created commission payout task: $%.2f to %s", amount_cents / 100, partner_name)
return True
except Exception as e:
LOG.error("Failed to create commission payout task: %s", e)
return False