import os import requests from flask import Flask, request, jsonify app = Flask(__name__) # --- Configuration --- # Ensure these are passed as environment variables WEBHOOK_SECRET = os.getenv("WEBHOOK_SECRET", "your_secret_token_here") BTCPAY_URL = os.getenv("BTCPAY_URL", "https://payment.nxtgroup.org") BTCPAY_API_KEY = os.getenv("BTCPAY_API_KEY") # Needs 'btcpay.store.canviewinvoices' permission 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") def get_html_template(order_id, invoice_id, amount, currency, amount_paid, remaining): return f"""

Incomplete Payment Detected

Hi,

We received a payment for your order #{order_id}, but it does not cover the full amount required to complete the purchase.

Total Amount: {amount} {currency}

Amount Paid: {amount_paid} {currency}

Remaining Balance: {remaining} {currency}

To ensure your order is processed, please return to the invoice and pay the remaining balance before the timer expires.

Pay Remaining Balance

Or copy and paste this link into your browser:
{BTCPAY_URL}/i/{invoice_id}

Important: If the full balance is not received before the invoice expires, the order will be cancelled and you will need to contact support for a refund of the partial amount.

Best regards,
NXT Group

""" def send_postmark_email(to_email, subject, html_body): if not POSTMARK_API_KEY: return {"ok": False, "status_code": None, "error": "POSTMARK_API_KEY is not set"} headers = { "Accept": "application/json", "Content-Type": "application/json", "X-Postmark-Server-Token": POSTMARK_API_KEY } payload = { "From": FROM_EMAIL, "To": to_email, "Bcc": BCC_EMAIL, "Subject": subject, "HtmlBody": html_body, "MessageStream": "outbound" } response = requests.post("https://api.postmarkapp.com/email", json=payload, headers=headers) result = {"ok": response.status_code == 200, "status_code": response.status_code} if not result["ok"]: result["error"] = response.text return result 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" return send_postmark_email(to_email, subject, html_body) @app.route('/btcpay-webhook', methods=['POST']) def btcpay_webhook(): # 1. Verify Secret Token token = request.args.get('token') if token != WEBHOOK_SECRET: return jsonify({"error": "Unauthorized"}), 401 data = request.json # 2. Only process 'InvoiceReceivedPayment' events if data.get('type') != 'InvoiceReceivedPayment': return jsonify({"status": "ignored", "reason": "Not a payment event"}), 200 store_id = data.get('storeId') invoice_id = data.get('invoiceId') # 3. Fetch full invoice details from BTCPay API headers = {"Authorization": f"token {BTCPAY_API_KEY}"} invoice_resp = requests.get(f"{BTCPAY_URL}/api/v1/stores/{store_id}/invoices/{invoice_id}", headers=headers) if invoice_resp.status_code != 200: return jsonify({"error": "Failed to fetch invoice"}), 500 invoice_data = invoice_resp.json() amount = float(invoice_data.get('amount', 0)) amount_paid = float(invoice_data.get('amountPaid', 0)) currency = invoice_data.get('currency', '') status = invoice_data.get('status', '') metadata = invoice_data.get('metadata', {}) buyer_email = metadata.get('buyerEmail') order_id = metadata.get('orderId', invoice_id) # 4. Check if it's a partial payment on an active invoice if status == 'New' and 0 < amount_paid < amount: if not buyer_email: return jsonify({"error": "No buyer email associated with invoice"}), 400 remaining = round(amount - amount_paid, 8) # Generate HTML and Send Email html_body = get_html_template(order_id, invoice_id, amount, currency, amount_paid, remaining) subject = f"Action Required: Partial payment received for Order #{order_id}" result = send_postmark_email(buyer_email, subject, html_body) if result["ok"]: return jsonify({"status": "success", "message": "Partial payment email sent"}), 200 return jsonify({"error": "Failed to send email via Postmark", "details": result}), 500 return jsonify({"status": "ignored", "reason": "Payment meets total or invoice not in New state"}), 200 @app.route('/test-email', methods=['POST']) def test_email(): token = request.args.get('token') if token != WEBHOOK_SECRET: return jsonify({"error": "Unauthorized"}), 401 to_email = request.args.get('to') or request.json.get('to') if request.is_json else None if not to_email: return jsonify({"error": "Missing 'to' email address"}), 400 result = send_test_email(to_email) if result["ok"]: return jsonify({ "status": "success", "message": f"Test email sent to {to_email}", "from": FROM_EMAIL, "bcc": BCC_EMAIL, }), 200 return jsonify({"error": "Failed to send test email via Postmark", "details": result}), 500 if __name__ == '__main__': import sys if len(sys.argv) >= 3 and sys.argv[1] == 'test-email': to = sys.argv[2] outcome = send_test_email(to) if outcome["ok"]: print(f"OK: test email sent to {to} (from {FROM_EMAIL}, bcc {BCC_EMAIL})") sys.exit(0) print(f"FAIL: {outcome}", file=sys.stderr) sys.exit(1) app.run(host='0.0.0.0', port=5000)