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.
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.
"""
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)