Handle BTCPay __test__ webhooks with Postmark test email
Send a bannered test transactional email when BTCPay UI test events arrive instead of failing on a missing invoice API lookup. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
c2b5d23477
commit
6edd208245
@ -18,6 +18,7 @@ POSTMARK_API_KEY=your_postmark_server_token
|
|||||||
BTCPAY_URL=https://payment.nxtgroup.org
|
BTCPAY_URL=https://payment.nxtgroup.org
|
||||||
FROM_EMAIL=billing@nxtgroup.org
|
FROM_EMAIL=billing@nxtgroup.org
|
||||||
BCC_EMAIL=admin@nxtgroup.org,finance@nxtgroup.org
|
BCC_EMAIL=admin@nxtgroup.org,finance@nxtgroup.org
|
||||||
|
WEBHOOK_TEST_EMAIL=erling@nxtgroup.org
|
||||||
DEBUG=false
|
DEBUG=false
|
||||||
|
|
||||||
# Test Postmark after deploy (docker exec or Portainer console):
|
# Test Postmark after deploy (docker exec or Portainer console):
|
||||||
|
|||||||
2
SETUP.md
2
SETUP.md
@ -96,6 +96,8 @@ The Secret in BTCPay must **exactly match** `BTCPAY_WEBHOOK_SECRET` in Portainer
|
|||||||
|
|
||||||
## 6. Test Postmark
|
## 6. Test Postmark
|
||||||
|
|
||||||
|
Optional: set `WEBHOOK_TEST_EMAIL` (defaults to first `BCC_EMAIL` address). BTCPay’s **Send test** for `InvoiceReceivedPayment` delivers a Postmark test email to that address.
|
||||||
|
|
||||||
Portainer → container console:
|
Portainer → container console:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@ -56,6 +56,10 @@ GET {BTCPAY_URL}/api/v1/stores/{storeId}/invoices/{invoiceId}
|
|||||||
5. `0 < amountPaid < amount`
|
5. `0 < amountPaid < amount`
|
||||||
6. `metadata.buyerEmail` is set
|
6. `metadata.buyerEmail` is set
|
||||||
|
|
||||||
|
## BTCPay UI test events
|
||||||
|
|
||||||
|
Invoice IDs containing `__test__` (BTCPay’s “Send test” button) skip the API fetch and trigger a Postmark test email to `WEBHOOK_TEST_EMAIL`, or the first `BCC_EMAIL` address if unset. The email includes a red **THIS IS A TEST TRANSACTIONAL EMAIL, PLEASE IGNORE.** banner.
|
||||||
|
|
||||||
## Response codes
|
## Response codes
|
||||||
|
|
||||||
| Code | Meaning |
|
| Code | Meaning |
|
||||||
|
|||||||
54
app.py
54
app.py
@ -18,8 +18,11 @@ BTCPAY_API_KEY = os.getenv("BTCPAY_API_KEY")
|
|||||||
POSTMARK_API_KEY = os.getenv("POSTMARK_API_KEY")
|
POSTMARK_API_KEY = os.getenv("POSTMARK_API_KEY")
|
||||||
FROM_EMAIL = os.getenv("FROM_EMAIL", "billing@nxtgroup.org")
|
FROM_EMAIL = os.getenv("FROM_EMAIL", "billing@nxtgroup.org")
|
||||||
BCC_EMAIL = os.getenv("BCC_EMAIL", "admin@nxtgroup.org")
|
BCC_EMAIL = os.getenv("BCC_EMAIL", "admin@nxtgroup.org")
|
||||||
|
WEBHOOK_TEST_EMAIL = os.getenv("WEBHOOK_TEST_EMAIL")
|
||||||
DEBUG = os.getenv("DEBUG", "false").lower() in ("true", "1", "yes")
|
DEBUG = os.getenv("DEBUG", "false").lower() in ("true", "1", "yes")
|
||||||
|
|
||||||
|
TEST_EMAIL_BANNER = "THIS IS A TEST TRANSACTIONAL EMAIL, PLEASE IGNORE."
|
||||||
|
|
||||||
logger = logging.getLogger("btcpaymailer")
|
logger = logging.getLogger("btcpaymailer")
|
||||||
logger.setLevel(logging.DEBUG if DEBUG else logging.INFO)
|
logger.setLevel(logging.DEBUG if DEBUG else logging.INFO)
|
||||||
if not logger.handlers:
|
if not logger.handlers:
|
||||||
@ -57,11 +60,30 @@ def verify_btcpay_webhook(body: bytes, signature: str | None, secret: str | None
|
|||||||
return hmac.compare_digest(expected, signature)
|
return hmac.compare_digest(expected, signature)
|
||||||
|
|
||||||
|
|
||||||
def get_html_template(order_id, invoice_id, amount, currency, amount_paid, remaining):
|
def is_btcpay_test_invoice(invoice_id):
|
||||||
|
return bool(invoice_id and "__test__" in invoice_id)
|
||||||
|
|
||||||
|
|
||||||
|
def get_webhook_test_recipient():
|
||||||
|
if WEBHOOK_TEST_EMAIL:
|
||||||
|
return WEBHOOK_TEST_EMAIL
|
||||||
|
if BCC_EMAIL:
|
||||||
|
return BCC_EMAIL.split(",")[0].strip()
|
||||||
|
return FROM_EMAIL
|
||||||
|
|
||||||
|
|
||||||
|
def get_html_template(order_id, invoice_id, amount, currency, amount_paid, remaining, test_mode=False):
|
||||||
|
test_banner = ""
|
||||||
|
if test_mode:
|
||||||
|
test_banner = f"""
|
||||||
|
<div style="background-color: #dc2626; color: #ffffff; padding: 16px 20px; text-align: center; font-weight: bold; font-size: 16px;">
|
||||||
|
{TEST_EMAIL_BANNER}
|
||||||
|
</div>"""
|
||||||
|
|
||||||
return f"""
|
return f"""
|
||||||
<div style="font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; background-color: #f4f5f7; padding: 40px 20px; color: #333333; line-height: 1.6;">
|
<div style="font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; background-color: #f4f5f7; padding: 40px 20px; color: #333333; line-height: 1.6;">
|
||||||
<div style="max-width: 600px; margin: 0 auto; background-color: #ffffff; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; box-shadow: 0 4px 6px rgba(0,0,0,0.05);">
|
<div style="max-width: 600px; margin: 0 auto; background-color: #ffffff; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; box-shadow: 0 4px 6px rgba(0,0,0,0.05);">
|
||||||
|
{test_banner}
|
||||||
<div style="background-color: #f59e0b; padding: 25px 30px; text-align: center;">
|
<div style="background-color: #f59e0b; padding: 25px 30px; text-align: center;">
|
||||||
<h1 style="margin: 0; color: #ffffff; font-size: 24px; font-weight: 600;">Incomplete Payment Detected</h1>
|
<h1 style="margin: 0; color: #ffffff; font-size: 24px; font-weight: 600;">Incomplete Payment Detected</h1>
|
||||||
</div>
|
</div>
|
||||||
@ -128,11 +150,32 @@ def send_postmark_email(to_email, subject, html_body):
|
|||||||
|
|
||||||
|
|
||||||
def send_test_email(to_email):
|
def send_test_email(to_email):
|
||||||
html_body = get_html_template("TEST-001", "test-invoice-id", 100.0, "USD", 50.0, 50.0)
|
html_body = get_html_template("TEST-001", "test-invoice-id", 100.0, "USD", 50.0, 50.0, test_mode=True)
|
||||||
subject = "BTCPay Mailer test email"
|
subject = f"[TEST] {TEST_EMAIL_BANNER}"
|
||||||
return send_postmark_email(to_email, subject, html_body)
|
return send_postmark_email(to_email, subject, html_body)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_btcpay_test_webhook(invoice_id):
|
||||||
|
recipient = get_webhook_test_recipient()
|
||||||
|
log_evaluation(
|
||||||
|
"BTCPay test invoice detected, sending Postmark test email",
|
||||||
|
invoice_id=invoice_id,
|
||||||
|
to=recipient,
|
||||||
|
)
|
||||||
|
html_body = get_html_template("TEST-001", invoice_id, 100.0, "USD", 50.0, 50.0, test_mode=True)
|
||||||
|
subject = f"[TEST] {TEST_EMAIL_BANNER}"
|
||||||
|
result = send_postmark_email(recipient, subject, html_body)
|
||||||
|
if result["ok"]:
|
||||||
|
log_evaluation("test email sent", to=recipient, invoice_id=invoice_id, postmark_status=result["status_code"])
|
||||||
|
return jsonify({
|
||||||
|
"status": "success",
|
||||||
|
"message": "BTCPay test webhook: Postmark test email sent",
|
||||||
|
"to": recipient,
|
||||||
|
}), 200
|
||||||
|
log_evaluation("test email failed", to=recipient, invoice_id=invoice_id, postmark_result=result)
|
||||||
|
return jsonify({"error": "Failed to send test email via Postmark", "details": result}), 500
|
||||||
|
|
||||||
|
|
||||||
def evaluate_partial_payment(invoice_data, invoice_id):
|
def evaluate_partial_payment(invoice_data, invoice_id):
|
||||||
amount = float(invoice_data.get('amount', 0))
|
amount = float(invoice_data.get('amount', 0))
|
||||||
amount_paid = float(invoice_data.get('amountPaid', 0))
|
amount_paid = float(invoice_data.get('amountPaid', 0))
|
||||||
@ -219,6 +262,9 @@ def btcpay_webhook():
|
|||||||
store_id = data.get('storeId')
|
store_id = data.get('storeId')
|
||||||
invoice_id = data.get('invoiceId')
|
invoice_id = data.get('invoiceId')
|
||||||
|
|
||||||
|
if is_btcpay_test_invoice(invoice_id):
|
||||||
|
return handle_btcpay_test_webhook(invoice_id)
|
||||||
|
|
||||||
log_debug("fetching invoice from BTCPay", store_id=store_id, invoice_id=invoice_id)
|
log_debug("fetching invoice from BTCPay", store_id=store_id, invoice_id=invoice_id)
|
||||||
|
|
||||||
headers = {"Authorization": f"token {BTCPAY_API_KEY}"}
|
headers = {"Authorization": f"token {BTCPAY_API_KEY}"}
|
||||||
|
|||||||
@ -15,4 +15,5 @@ services:
|
|||||||
POSTMARK_API_KEY: ${POSTMARK_API_KEY:?Set POSTMARK_API_KEY in .env or environment}
|
POSTMARK_API_KEY: ${POSTMARK_API_KEY:?Set POSTMARK_API_KEY in .env or environment}
|
||||||
FROM_EMAIL: ${FROM_EMAIL:-billing@nxtgroup.org}
|
FROM_EMAIL: ${FROM_EMAIL:-billing@nxtgroup.org}
|
||||||
BCC_EMAIL: ${BCC_EMAIL:-admin@nxtgroup.org}
|
BCC_EMAIL: ${BCC_EMAIL:-admin@nxtgroup.org}
|
||||||
|
WEBHOOK_TEST_EMAIL: ${WEBHOOK_TEST_EMAIL:-}
|
||||||
DEBUG: ${DEBUG:-false}
|
DEBUG: ${DEBUG:-false}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user