diff --git a/.env.example b/.env.example index 8162f7a..74b212d 100644 --- a/.env.example +++ b/.env.example @@ -18,6 +18,7 @@ POSTMARK_API_KEY=your_postmark_server_token BTCPAY_URL=https://payment.nxtgroup.org FROM_EMAIL=billing@nxtgroup.org BCC_EMAIL=admin@nxtgroup.org,finance@nxtgroup.org +WEBHOOK_TEST_EMAIL=erling@nxtgroup.org DEBUG=false # Test Postmark after deploy (docker exec or Portainer console): diff --git a/SETUP.md b/SETUP.md index 0d24cd8..3ff23eb 100644 --- a/SETUP.md +++ b/SETUP.md @@ -96,6 +96,8 @@ The Secret in BTCPay must **exactly match** `BTCPAY_WEBHOOK_SECRET` in Portainer ## 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: ```bash diff --git a/WEBHOOK.md b/WEBHOOK.md index 4af126f..c84d1e5 100644 --- a/WEBHOOK.md +++ b/WEBHOOK.md @@ -56,6 +56,10 @@ GET {BTCPAY_URL}/api/v1/stores/{storeId}/invoices/{invoiceId} 5. `0 < amountPaid < amount` 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 | Code | Meaning | diff --git a/app.py b/app.py index 59a83b8..f99e46e 100644 --- a/app.py +++ b/app.py @@ -18,8 +18,11 @@ BTCPAY_API_KEY = os.getenv("BTCPAY_API_KEY") POSTMARK_API_KEY = os.getenv("POSTMARK_API_KEY") FROM_EMAIL = os.getenv("FROM_EMAIL", "billing@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") +TEST_EMAIL_BANNER = "THIS IS A TEST TRANSACTIONAL EMAIL, PLEASE IGNORE." + logger = logging.getLogger("btcpaymailer") logger.setLevel(logging.DEBUG if DEBUG else logging.INFO) 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) -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""" +
+ {TEST_EMAIL_BANNER} +
""" + return f"""
- + {test_banner}

Incomplete Payment Detected

@@ -128,11 +150,32 @@ def send_postmark_email(to_email, subject, html_body): def send_test_email(to_email): - html_body = get_html_template("TEST-001", "test-invoice-id", 100.0, "USD", 50.0, 50.0) - subject = "BTCPay Mailer test email" + html_body = get_html_template("TEST-001", "test-invoice-id", 100.0, "USD", 50.0, 50.0, test_mode=True) + subject = f"[TEST] {TEST_EMAIL_BANNER}" 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): amount = float(invoice_data.get('amount', 0)) amount_paid = float(invoice_data.get('amountPaid', 0)) @@ -219,6 +262,9 @@ def btcpay_webhook(): store_id = data.get('storeId') 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) headers = {"Authorization": f"token {BTCPAY_API_KEY}"} diff --git a/docker-compose.yml b/docker-compose.yml index 2179dc0..9474749 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,4 +15,5 @@ services: POSTMARK_API_KEY: ${POSTMARK_API_KEY:?Set POSTMARK_API_KEY in .env or environment} FROM_EMAIL: ${FROM_EMAIL:-billing@nxtgroup.org} BCC_EMAIL: ${BCC_EMAIL:-admin@nxtgroup.org} + WEBHOOK_TEST_EMAIL: ${WEBHOOK_TEST_EMAIL:-} DEBUG: ${DEBUG:-false}