Show Bitcoin amounts from payment-methods API and add FROM_NAME
Fetch BTCPay payment-methods for BTC totals, log API summaries, and format sender as "Name <email>" via FROM_NAME for Postmark. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
6edd208245
commit
d9a2fcaa0f
@ -16,7 +16,8 @@ POSTMARK_API_KEY=your_postmark_server_token
|
|||||||
|
|
||||||
# Optional
|
# Optional
|
||||||
BTCPAY_URL=https://payment.nxtgroup.org
|
BTCPAY_URL=https://payment.nxtgroup.org
|
||||||
FROM_EMAIL=billing@nxtgroup.org
|
FROM_NAME=NXT Payments
|
||||||
|
FROM_EMAIL=noreply@nxtgroup.org
|
||||||
BCC_EMAIL=admin@nxtgroup.org,finance@nxtgroup.org
|
BCC_EMAIL=admin@nxtgroup.org,finance@nxtgroup.org
|
||||||
WEBHOOK_TEST_EMAIL=erling@nxtgroup.org
|
WEBHOOK_TEST_EMAIL=erling@nxtgroup.org
|
||||||
DEBUG=false
|
DEBUG=false
|
||||||
|
|||||||
6
SETUP.md
6
SETUP.md
@ -7,7 +7,8 @@ One mailer instance handles **multiple BTCPay stores** on the same server (`paym
|
|||||||
## 1. Postmark
|
## 1. Postmark
|
||||||
|
|
||||||
1. Copy **Server API token** → `POSTMARK_API_KEY`
|
1. Copy **Server API token** → `POSTMARK_API_KEY`
|
||||||
2. Verify sender → `FROM_EMAIL` (e.g. `billing@nxtgroup.org`)
|
2. Verify sender → `FROM_EMAIL` (e.g. `noreply@nxtgroup.org`)
|
||||||
|
3. Optional display name → `FROM_NAME` (e.g. `NXT Payments` → sends as `NXT Payments <noreply@nxtgroup.org>`)
|
||||||
3. Set `BCC_EMAIL` (comma-separated, optional)
|
3. Set `BCC_EMAIL` (comma-separated, optional)
|
||||||
|
|
||||||
## 2. BTCPay API key (all stores)
|
## 2. BTCPay API key (all stores)
|
||||||
@ -46,7 +47,8 @@ services:
|
|||||||
BTCPAY_URL: https://payment.nxtgroup.org
|
BTCPAY_URL: https://payment.nxtgroup.org
|
||||||
BTCPAY_API_KEY: PASTE_BTCPAY_API_KEY
|
BTCPAY_API_KEY: PASTE_BTCPAY_API_KEY
|
||||||
POSTMARK_API_KEY: PASTE_POSTMARK_TOKEN
|
POSTMARK_API_KEY: PASTE_POSTMARK_TOKEN
|
||||||
FROM_EMAIL: billing@nxtgroup.org
|
FROM_NAME: NXT Payments
|
||||||
|
FROM_EMAIL: noreply@nxtgroup.org
|
||||||
BCC_EMAIL: admin@nxtgroup.org
|
BCC_EMAIL: admin@nxtgroup.org
|
||||||
DEBUG: "true"
|
DEBUG: "true"
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
177
app.py
177
app.py
@ -16,6 +16,7 @@ TEST_TOKEN = os.getenv("TEST_TOKEN")
|
|||||||
BTCPAY_URL = os.getenv("BTCPAY_URL", "https://payment.nxtgroup.org")
|
BTCPAY_URL = os.getenv("BTCPAY_URL", "https://payment.nxtgroup.org")
|
||||||
BTCPAY_API_KEY = os.getenv("BTCPAY_API_KEY")
|
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_NAME = os.getenv("FROM_NAME", "")
|
||||||
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")
|
WEBHOOK_TEST_EMAIL = os.getenv("WEBHOOK_TEST_EMAIL")
|
||||||
@ -72,7 +73,119 @@ def get_webhook_test_recipient():
|
|||||||
return FROM_EMAIL
|
return FROM_EMAIL
|
||||||
|
|
||||||
|
|
||||||
|
def get_from_address():
|
||||||
|
if FROM_NAME:
|
||||||
|
return f"{FROM_NAME} <{FROM_EMAIL}>"
|
||||||
|
return FROM_EMAIL
|
||||||
|
|
||||||
|
|
||||||
|
def btcpay_headers():
|
||||||
|
return {"Authorization": f"token {BTCPAY_API_KEY}"}
|
||||||
|
|
||||||
|
|
||||||
|
def format_crypto_amount(value):
|
||||||
|
return f"{float(value):.8f}".rstrip("0").rstrip(".")
|
||||||
|
|
||||||
|
|
||||||
|
def pick_btc_payment_method(payment_methods):
|
||||||
|
if not payment_methods:
|
||||||
|
return None
|
||||||
|
|
||||||
|
for method in payment_methods:
|
||||||
|
payment_method_id = (method.get("paymentMethodId") or "").upper()
|
||||||
|
currency = (method.get("currency") or "").upper()
|
||||||
|
if payment_method_id == "BTC" or currency == "BTC":
|
||||||
|
return method
|
||||||
|
|
||||||
|
for method in payment_methods:
|
||||||
|
payment_method_id = (method.get("paymentMethodId") or "").upper()
|
||||||
|
if payment_method_id.startswith("BTC"):
|
||||||
|
return method
|
||||||
|
|
||||||
|
return payment_methods[0]
|
||||||
|
|
||||||
|
|
||||||
|
def extract_payment_amounts(invoice_data, payment_methods):
|
||||||
|
method = pick_btc_payment_method(payment_methods)
|
||||||
|
if method:
|
||||||
|
amount = float(method.get("amount", 0))
|
||||||
|
amount_paid = float(method.get("totalPaid", 0))
|
||||||
|
remaining = float(method.get("due", 0))
|
||||||
|
if remaining <= 0 and amount > amount_paid:
|
||||||
|
remaining = round(amount - amount_paid, 8)
|
||||||
|
currency = method.get("currency") or "BTC"
|
||||||
|
source = "payment_method"
|
||||||
|
else:
|
||||||
|
amount = float(invoice_data.get("amount", 0))
|
||||||
|
amount_paid = float(invoice_data.get("amountPaid", 0))
|
||||||
|
remaining = round(amount - amount_paid, 8) if amount_paid < amount else 0
|
||||||
|
currency = invoice_data.get("currency", "")
|
||||||
|
source = "invoice_fallback"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"amount": amount,
|
||||||
|
"amount_paid": amount_paid,
|
||||||
|
"remaining": remaining,
|
||||||
|
"currency": currency,
|
||||||
|
"source": source,
|
||||||
|
"payment_method_id": method.get("paymentMethodId") if method else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_btcpay_invoice(store_id, invoice_id):
|
||||||
|
response = requests.get(
|
||||||
|
f"{BTCPAY_URL}/api/v1/stores/{store_id}/invoices/{invoice_id}",
|
||||||
|
headers=btcpay_headers(),
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_btcpay_payment_methods(store_id, invoice_id):
|
||||||
|
response = requests.get(
|
||||||
|
f"{BTCPAY_URL}/api/v1/stores/{store_id}/invoices/{invoice_id}/payment-methods",
|
||||||
|
headers=btcpay_headers(),
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def log_invoice_api_response(invoice_id, invoice_data, payment_methods, amounts):
|
||||||
|
invoice_summary = {
|
||||||
|
"invoice_id": invoice_id,
|
||||||
|
"status": invoice_data.get("status"),
|
||||||
|
"additional_status": invoice_data.get("additionalStatus"),
|
||||||
|
"fiat_amount": invoice_data.get("amount"),
|
||||||
|
"fiat_amount_paid": invoice_data.get("amountPaid"),
|
||||||
|
"fiat_currency": invoice_data.get("currency"),
|
||||||
|
}
|
||||||
|
payment_summary = [
|
||||||
|
{
|
||||||
|
"paymentMethodId": method.get("paymentMethodId"),
|
||||||
|
"currency": method.get("currency"),
|
||||||
|
"amount": method.get("amount"),
|
||||||
|
"totalPaid": method.get("totalPaid"),
|
||||||
|
"due": method.get("due"),
|
||||||
|
}
|
||||||
|
for method in (payment_methods or [])
|
||||||
|
]
|
||||||
|
|
||||||
|
log_debug("BTCPay invoice API response", **invoice_summary)
|
||||||
|
log_debug("BTCPay payment-methods API response", invoice_id=invoice_id, payment_methods=payment_summary)
|
||||||
|
log_evaluation(
|
||||||
|
"amounts resolved for email",
|
||||||
|
invoice_id=invoice_id,
|
||||||
|
source=amounts["source"],
|
||||||
|
payment_method_id=amounts["payment_method_id"],
|
||||||
|
total=amounts["amount"],
|
||||||
|
paid=amounts["amount_paid"],
|
||||||
|
remaining=amounts["remaining"],
|
||||||
|
currency=amounts["currency"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_html_template(order_id, invoice_id, amount, currency, amount_paid, remaining, test_mode=False):
|
def get_html_template(order_id, invoice_id, amount, currency, amount_paid, remaining, test_mode=False):
|
||||||
|
total_display = format_crypto_amount(amount)
|
||||||
|
paid_display = format_crypto_amount(amount_paid)
|
||||||
|
remaining_display = format_crypto_amount(remaining)
|
||||||
test_banner = ""
|
test_banner = ""
|
||||||
if test_mode:
|
if test_mode:
|
||||||
test_banner = f"""
|
test_banner = f"""
|
||||||
@ -91,11 +204,12 @@ def get_html_template(order_id, invoice_id, amount, currency, amount_paid, remai
|
|||||||
<div style="padding: 30px;">
|
<div style="padding: 30px;">
|
||||||
<p style="margin-top: 0; font-size: 16px;">Hi,</p>
|
<p style="margin-top: 0; font-size: 16px;">Hi,</p>
|
||||||
<p style="font-size: 16px;">We received a payment for your order <strong>#{order_id}</strong>, but it does not cover the full amount required to complete the purchase.</p>
|
<p style="font-size: 16px;">We received a payment for your order <strong>#{order_id}</strong>, but it does not cover the full amount required to complete the purchase.</p>
|
||||||
|
<p style="font-size: 16px;">This usually happens when you didn't enter the Bitcoin amount on the payment vendor, or didn't use the correct bitcoin amount.</p>
|
||||||
|
|
||||||
<div style="background-color: #fffbeb; border-left: 4px solid #f59e0b; padding: 15px 20px; margin: 25px 0;">
|
<div style="background-color: #fffbeb; border-left: 4px solid #f59e0b; padding: 15px 20px; margin: 25px 0;">
|
||||||
<p style="margin: 0 0 8px 0; font-size: 16px;"><strong>Total Amount:</strong> {amount} {currency}</p>
|
<p style="margin: 0 0 8px 0; font-size: 16px;"><strong>Total Amount (Bitcoin):</strong> {total_display} {currency}</p>
|
||||||
<p style="margin: 0 0 8px 0; font-size: 16px;"><strong>Amount Paid:</strong> {amount_paid} {currency}</p>
|
<p style="margin: 0 0 8px 0; font-size: 16px;"><strong>Amount Paid (Bitcoin):</strong> {paid_display} {currency}</p>
|
||||||
<p style="margin: 0; font-size: 18px; color: #b45309;"><strong>Remaining Balance:</strong> {remaining} {currency}</p>
|
<p style="margin: 0; font-size: 18px; color: #b45309;"><strong>Remaining Balance (Bitcoin):</strong> {remaining_display} {currency}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p style="font-size: 16px;">To ensure your order is processed, please return to the invoice and pay the remaining balance before the timer expires.</p>
|
<p style="font-size: 16px;">To ensure your order is processed, please return to the invoice and pay the remaining balance before the timer expires.</p>
|
||||||
@ -109,6 +223,9 @@ def get_html_template(order_id, invoice_id, amount, currency, amount_paid, remai
|
|||||||
<a href="{BTCPAY_URL}/i/{invoice_id}" style="color: #2563eb; text-decoration: underline;">{BTCPAY_URL}/i/{invoice_id}</a>
|
<a href="{BTCPAY_URL}/i/{invoice_id}" style="color: #2563eb; text-decoration: underline;">{BTCPAY_URL}/i/{invoice_id}</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p style="font-size: 16px;">Alternatively, you can contact us on webchat on the store you placed the order on. We can for instance, manually add a lower time amount to the subscription.</p>
|
||||||
|
|
||||||
|
|
||||||
<div style="background-color: #fef2f2; border: 1px solid #fee2e2; border-left: 4px solid #ef4444; padding: 15px; border-radius: 4px; margin-top: 30px;">
|
<div style="background-color: #fef2f2; border: 1px solid #fee2e2; border-left: 4px solid #ef4444; padding: 15px; border-radius: 4px; margin-top: 30px;">
|
||||||
<p style="margin: 0; font-size: 14px; color: #991b1b;">
|
<p style="margin: 0; font-size: 14px; color: #991b1b;">
|
||||||
<strong>Important:</strong> 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.
|
<strong>Important:</strong> 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.
|
||||||
@ -134,7 +251,7 @@ def send_postmark_email(to_email, subject, html_body):
|
|||||||
"X-Postmark-Server-Token": POSTMARK_API_KEY
|
"X-Postmark-Server-Token": POSTMARK_API_KEY
|
||||||
}
|
}
|
||||||
payload = {
|
payload = {
|
||||||
"From": FROM_EMAIL,
|
"From": get_from_address(),
|
||||||
"To": to_email,
|
"To": to_email,
|
||||||
"Bcc": BCC_EMAIL,
|
"Bcc": BCC_EMAIL,
|
||||||
"Subject": subject,
|
"Subject": subject,
|
||||||
@ -150,7 +267,7 @@ 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, test_mode=True)
|
html_body = get_html_template("TEST-001", "test-invoice-id", 0.001, "BTC", 0.0005, 0.0005, test_mode=True)
|
||||||
subject = f"[TEST] {TEST_EMAIL_BANNER}"
|
subject = f"[TEST] {TEST_EMAIL_BANNER}"
|
||||||
return send_postmark_email(to_email, subject, html_body)
|
return send_postmark_email(to_email, subject, html_body)
|
||||||
|
|
||||||
@ -162,7 +279,7 @@ def handle_btcpay_test_webhook(invoice_id):
|
|||||||
invoice_id=invoice_id,
|
invoice_id=invoice_id,
|
||||||
to=recipient,
|
to=recipient,
|
||||||
)
|
)
|
||||||
html_body = get_html_template("TEST-001", invoice_id, 100.0, "USD", 50.0, 50.0, test_mode=True)
|
html_body = get_html_template("TEST-001", invoice_id, 0.001, "BTC", 0.0005, 0.0005, test_mode=True)
|
||||||
subject = f"[TEST] {TEST_EMAIL_BANNER}"
|
subject = f"[TEST] {TEST_EMAIL_BANNER}"
|
||||||
result = send_postmark_email(recipient, subject, html_body)
|
result = send_postmark_email(recipient, subject, html_body)
|
||||||
if result["ok"]:
|
if result["ok"]:
|
||||||
@ -176,10 +293,11 @@ def handle_btcpay_test_webhook(invoice_id):
|
|||||||
return jsonify({"error": "Failed to send test email via Postmark", "details": result}), 500
|
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, amounts):
|
||||||
amount = float(invoice_data.get('amount', 0))
|
amount = amounts["amount"]
|
||||||
amount_paid = float(invoice_data.get('amountPaid', 0))
|
amount_paid = amounts["amount_paid"]
|
||||||
currency = invoice_data.get('currency', '')
|
currency = amounts["currency"]
|
||||||
|
remaining = amounts["remaining"]
|
||||||
status = invoice_data.get('status', '')
|
status = invoice_data.get('status', '')
|
||||||
metadata = invoice_data.get('metadata', {})
|
metadata = invoice_data.get('metadata', {})
|
||||||
buyer_email = metadata.get('buyerEmail')
|
buyer_email = metadata.get('buyerEmail')
|
||||||
@ -196,7 +314,9 @@ def evaluate_partial_payment(invoice_data, invoice_id):
|
|||||||
"amount": amount,
|
"amount": amount,
|
||||||
"amount_paid": amount_paid,
|
"amount_paid": amount_paid,
|
||||||
"currency": currency,
|
"currency": currency,
|
||||||
"remaining": round(amount - amount_paid, 8) if amount_paid < amount else 0,
|
"remaining": remaining,
|
||||||
|
"amount_source": amounts["source"],
|
||||||
|
"payment_method_id": amounts["payment_method_id"],
|
||||||
"checks": {
|
"checks": {
|
||||||
"status_is_new": is_new,
|
"status_is_new": is_new,
|
||||||
"amount_paid_gt_zero": amount_paid > 0,
|
"amount_paid_gt_zero": amount_paid > 0,
|
||||||
@ -267,16 +387,12 @@ def btcpay_webhook():
|
|||||||
|
|
||||||
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}"}
|
invoice_resp = fetch_btcpay_invoice(store_id, invoice_id)
|
||||||
invoice_resp = requests.get(
|
|
||||||
f"{BTCPAY_URL}/api/v1/stores/{store_id}/invoices/{invoice_id}",
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
|
|
||||||
if invoice_resp.status_code != 200:
|
if invoice_resp.status_code != 200:
|
||||||
logger.error(
|
logger.error(
|
||||||
"BTCPay API error | %s",
|
"BTCPay API error | %s",
|
||||||
json.dumps({
|
json.dumps({
|
||||||
|
"endpoint": "invoice",
|
||||||
"store_id": store_id,
|
"store_id": store_id,
|
||||||
"invoice_id": invoice_id,
|
"invoice_id": invoice_id,
|
||||||
"status_code": invoice_resp.status_code,
|
"status_code": invoice_resp.status_code,
|
||||||
@ -286,9 +402,26 @@ def btcpay_webhook():
|
|||||||
return jsonify({"error": "Failed to fetch invoice"}), 500
|
return jsonify({"error": "Failed to fetch invoice"}), 500
|
||||||
|
|
||||||
invoice_data = invoice_resp.json()
|
invoice_data = invoice_resp.json()
|
||||||
log_debug("invoice fetched", invoice_id=invoice_id, status=invoice_data.get('status'))
|
|
||||||
|
|
||||||
action, evaluation = evaluate_partial_payment(invoice_data, invoice_id)
|
payment_methods_resp = fetch_btcpay_payment_methods(store_id, invoice_id)
|
||||||
|
if payment_methods_resp.status_code != 200:
|
||||||
|
logger.error(
|
||||||
|
"BTCPay API error | %s",
|
||||||
|
json.dumps({
|
||||||
|
"endpoint": "payment-methods",
|
||||||
|
"store_id": store_id,
|
||||||
|
"invoice_id": invoice_id,
|
||||||
|
"status_code": payment_methods_resp.status_code,
|
||||||
|
"body": payment_methods_resp.text,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
return jsonify({"error": "Failed to fetch invoice payment methods"}), 500
|
||||||
|
|
||||||
|
payment_methods = payment_methods_resp.json()
|
||||||
|
amounts = extract_payment_amounts(invoice_data, payment_methods)
|
||||||
|
log_invoice_api_response(invoice_id, invoice_data, payment_methods, amounts)
|
||||||
|
|
||||||
|
action, evaluation = evaluate_partial_payment(invoice_data, invoice_id, amounts)
|
||||||
|
|
||||||
if action is None:
|
if action is None:
|
||||||
return jsonify({"status": "ignored", "reason": "Payment meets total or invoice not in New state"}), 200
|
return jsonify({"status": "ignored", "reason": "Payment meets total or invoice not in New state"}), 200
|
||||||
@ -329,11 +462,11 @@ def test_email():
|
|||||||
|
|
||||||
result = send_test_email(to_email)
|
result = send_test_email(to_email)
|
||||||
if result["ok"]:
|
if result["ok"]:
|
||||||
logger.info("test email sent | %s", json.dumps({"to": to_email, "from": FROM_EMAIL, "bcc": BCC_EMAIL}))
|
logger.info("test email sent | %s", json.dumps({"to": to_email, "from": get_from_address(), "bcc": BCC_EMAIL}))
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"message": f"Test email sent to {to_email}",
|
"message": f"Test email sent to {to_email}",
|
||||||
"from": FROM_EMAIL,
|
"from": get_from_address(),
|
||||||
"bcc": BCC_EMAIL,
|
"bcc": BCC_EMAIL,
|
||||||
}), 200
|
}), 200
|
||||||
logger.error("test email failed | %s", json.dumps({"to": to_email, "result": result}))
|
logger.error("test email failed | %s", json.dumps({"to": to_email, "result": result}))
|
||||||
@ -345,7 +478,7 @@ if __name__ == '__main__':
|
|||||||
to = sys.argv[2]
|
to = sys.argv[2]
|
||||||
outcome = send_test_email(to)
|
outcome = send_test_email(to)
|
||||||
if outcome["ok"]:
|
if outcome["ok"]:
|
||||||
print(f"OK: test email sent to {to} (from {FROM_EMAIL}, bcc {BCC_EMAIL})")
|
print(f"OK: test email sent to {to} (from {get_from_address()}, bcc {BCC_EMAIL})")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
print(f"FAIL: {outcome}", file=sys.stderr)
|
print(f"FAIL: {outcome}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
@ -13,6 +13,7 @@ services:
|
|||||||
BTCPAY_URL: ${BTCPAY_URL:-https://payment.nxtgroup.org}
|
BTCPAY_URL: ${BTCPAY_URL:-https://payment.nxtgroup.org}
|
||||||
BTCPAY_API_KEY: ${BTCPAY_API_KEY:?Set BTCPAY_API_KEY in .env or environment}
|
BTCPAY_API_KEY: ${BTCPAY_API_KEY:?Set BTCPAY_API_KEY in .env or environment}
|
||||||
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_NAME: ${FROM_NAME:-}
|
||||||
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:-}
|
WEBHOOK_TEST_EMAIL: ${WEBHOOK_TEST_EMAIL:-}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user