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): 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) return response.status_code == 200 @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}" email_sent = send_postmark_email(buyer_email, subject, html_body) if email_sent: return jsonify({"status": "success", "message": "Partial payment email sent"}), 200 else: return jsonify({"error": "Failed to send email via Postmark"}), 500 return jsonify({"status": "ignored", "reason": "Payment meets total or invoice not in New state"}), 200 if __name__ == '__main__': app.run(host='0.0.0.0', port=5000)