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>
This commit is contained in:
commit
f8cd37ac8c
1823 changed files with 145167 additions and 0 deletions
291
scripts/workers/relay_integration.py
Normal file
291
scripts/workers/relay_integration.py
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
"""
|
||||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue