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>
291 lines
11 KiB
Python
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
|