@@ -232,6 +232,12 @@ def ensure_parent_dir(path_value: str) -> None:
232232NOWPAYMENTS_POSTPAY_NEXT_STEP_HOURS = int (os .getenv ("NOWPAYMENTS_POSTPAY_NEXT_STEP_HOURS" , "24" ))
233233NOWPAYMENTS_POSTPAY_REPLY_TO = os .getenv ("NOWPAYMENTS_POSTPAY_REPLY_TO" , RESEND_TO or "hello@payeeproof.com" ).strip ()
234234NOWPAYMENTS_SUPPORT_EMAIL = os .getenv ("NOWPAYMENTS_SUPPORT_EMAIL" , RESEND_TO or "hello@payeeproof.com" ).strip ()
235+ NOWPAYMENTS_POSTPAY_CRM_ENABLED = str (os .getenv ("NOWPAYMENTS_POSTPAY_CRM_ENABLED" , "1" )).strip ().lower () not in {"0" , "false" , "no" , "off" }
236+ NOWPAYMENTS_POSTPAY_CRM_URL = os .getenv ("NOWPAYMENTS_POSTPAY_CRM_URL" , CRM_INTAKE_URL ).strip ()
237+ NOWPAYMENTS_POSTPAY_CRM_SECRET = os .getenv ("NOWPAYMENTS_POSTPAY_CRM_SECRET" , CRM_INTAKE_SECRET ).strip ()
238+ NOWPAYMENTS_POSTPAY_CRM_AUTH_HEADER = os .getenv ("NOWPAYMENTS_POSTPAY_CRM_AUTH_HEADER" , CRM_INTAKE_AUTH_HEADER or "X-PayeeProof-CRM-Secret" ).strip () or "X-PayeeProof-CRM-Secret"
239+ NOWPAYMENTS_POSTPAY_CRM_TIMEOUT = float (os .getenv ("NOWPAYMENTS_POSTPAY_CRM_TIMEOUT_SEC" , str (CRM_INTAKE_TIMEOUT or 6 )))
240+ NOWPAYMENTS_POSTPAY_CRM_EVENT_NAME = os .getenv ("NOWPAYMENTS_POSTPAY_CRM_EVENT_NAME" , "payment_confirmed" ).strip () or "payment_confirmed"
235241DEFAULT_NOWPAYMENTS_PRODUCTS = {
236242 "pilot_399" : {
237243 "amount_usd" : 399.0 ,
@@ -1933,8 +1939,8 @@ def store_nowpayments_order_created(*, order_id: str, product: Dict[str, Any], c
19331939 order_id, created_at, updated_at, status, payment_status, fulfillment_status,
19341940 customer_email, product_sku, product_title, amount_usd, price_currency, order_description,
19351941 provider_invoice_id, invoice_url, success_url, cancel_url, source_ip, origin, raw_create_json,
1936- payment_notice_status, customer_receipt_status
1937- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1942+ payment_notice_status, customer_receipt_status, crm_status
1943+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
19381944 """ ,
19391945 (
19401946 order_id ,
@@ -1958,6 +1964,7 @@ def store_nowpayments_order_created(*, order_id: str, product: Dict[str, Any], c
19581964 json_dumps_safe (provider_response ),
19591965 "pending" ,
19601966 "pending" if customer_email else "skipped" ,
1967+ "pending" ,
19611968 ),
19621969 )
19631970 conn .commit ()
@@ -1975,6 +1982,18 @@ def _nowpayments_paid_flag(status: str) -> bool:
19751982 return str (status or "" ).strip ().lower () in {"finished" , "confirmed" }
19761983
19771984
1985+ def _nowpayments_terminal_status (status : Any ) -> bool :
1986+ return str (status or "" ).strip ().lower () in {"sent" , "skipped" , "disabled" , "not_configured" }
1987+
1988+
1989+ def _nowpayments_postpay_complete (admin_status : Any , customer_status : Any , crm_status : Any ) -> bool :
1990+ return all ([
1991+ _nowpayments_terminal_status (admin_status ),
1992+ _nowpayments_terminal_status (customer_status ),
1993+ _nowpayments_terminal_status (crm_status ),
1994+ ])
1995+
1996+
19781997def send_nowpayments_internal_paid_email (order : Dict [str , Any ]) -> Dict [str , Optional [str ]]:
19791998 if not NOWPAYMENTS_POSTPAY_EMAILS_ENABLED :
19801999 return {"status" : "disabled" , "email_id" : None , "debug_detail" : None }
@@ -2119,9 +2138,89 @@ def send_nowpayments_customer_paid_email(order: Dict[str, Any]) -> Dict[str, Opt
21192138 return {"status" : "failed" , "email_id" : None , "debug_detail" : f"RESEND_JSON_ERROR: { exc } " }
21202139
21212140
2122- def update_nowpayments_postpay_delivery (order_id : str , admin_result : Dict [str , Optional [str ]], customer_result : Dict [str , Optional [str ]]) -> Dict [str , Any ]:
2141+
2142+ def send_nowpayments_crm_intake (order : Dict [str , Any ]) -> Dict [str , Optional [str ]]:
2143+ if not NOWPAYMENTS_POSTPAY_CRM_ENABLED :
2144+ return {"status" : "disabled" , "delivery_id" : None , "debug_detail" : None , "response_code" : None }
2145+ if not NOWPAYMENTS_POSTPAY_CRM_URL :
2146+ return {"status" : "not_configured" , "delivery_id" : None , "debug_detail" : None , "response_code" : None }
2147+ order_id = normalize_text (order .get ("order_id" ), 120 )
2148+ crm_payload = {
2149+ "event" : NOWPAYMENTS_POSTPAY_CRM_EVENT_NAME ,
2150+ "product" : "payeeproof" ,
2151+ "payment" : {
2152+ "order_id" : order_id ,
2153+ "created_at" : order .get ("created_at" ) or "" ,
2154+ "updated_at" : order .get ("updated_at" ) or "" ,
2155+ "product_sku" : order .get ("product_sku" ) or "" ,
2156+ "product_title" : order .get ("product_title" ) or "" ,
2157+ "amount_usd" : order .get ("amount_usd" ),
2158+ "payment_status" : order .get ("payment_status" ) or "" ,
2159+ "fulfillment_status" : order .get ("fulfillment_status" ) or "" ,
2160+ "provider_invoice_id" : order .get ("provider_invoice_id" ) or "" ,
2161+ "provider_payment_id" : order .get ("provider_payment_id" ) or "" ,
2162+ "invoice_url" : order .get ("invoice_url" ) or "" ,
2163+ "pay_currency" : order .get ("pay_currency" ) or "" ,
2164+ "pay_amount" : order .get ("pay_amount" ) or order .get ("actually_paid" ) or "" ,
2165+ "customer_email" : order .get ("customer_email" ) or "" ,
2166+ },
2167+ "meta" : {
2168+ "origin" : order .get ("origin" ) or "" ,
2169+ "source_ip" : order .get ("source_ip" ) or "" ,
2170+ "support_email" : NOWPAYMENTS_SUPPORT_EMAIL ,
2171+ "next_step_hours" : NOWPAYMENTS_POSTPAY_NEXT_STEP_HOURS ,
2172+ },
2173+ "links" : {
2174+ "site" : "https://payeeproof.com" ,
2175+ "api_base" : PUBLIC_API_BASE ,
2176+ "success_url" : order .get ("success_url" ) or NOWPAYMENTS_SUCCESS_URL ,
2177+ },
2178+ }
2179+ headers = {
2180+ "Content-Type" : "application/json" ,
2181+ "Accept" : "application/json" ,
2182+ "User-Agent" : "PayeeProof/1.0 (+https://payeeproof.com)" ,
2183+ "Idempotency-Key" : f"crm-payment-{ order_id or uuid .uuid4 ().hex [:12 ]} " ,
2184+ }
2185+ if NOWPAYMENTS_POSTPAY_CRM_SECRET :
2186+ headers [NOWPAYMENTS_POSTPAY_CRM_AUTH_HEADER ] = NOWPAYMENTS_POSTPAY_CRM_SECRET
2187+ try :
2188+ response = requests .post (
2189+ NOWPAYMENTS_POSTPAY_CRM_URL ,
2190+ headers = headers ,
2191+ json = crm_payload ,
2192+ timeout = NOWPAYMENTS_POSTPAY_CRM_TIMEOUT ,
2193+ )
2194+ raw_text = response .text [:1000 ]
2195+ if response .status_code < 200 or response .status_code >= 300 :
2196+ return {
2197+ "status" : "failed" ,
2198+ "delivery_id" : order_id ,
2199+ "debug_detail" : f"CRM_HTTP_{ response .status_code } : { raw_text } " ,
2200+ "response_code" : response .status_code ,
2201+ }
2202+ return {
2203+ "status" : "sent" ,
2204+ "delivery_id" : order_id ,
2205+ "debug_detail" : None ,
2206+ "response_code" : response .status_code ,
2207+ }
2208+ except requests .RequestException as exc :
2209+ return {
2210+ "status" : "failed" ,
2211+ "delivery_id" : order_id ,
2212+ "debug_detail" : f"CRM_REQUEST_ERROR: { exc } " ,
2213+ "response_code" : None ,
2214+ }
2215+
2216+
2217+ def update_nowpayments_postpay_delivery (order_id : str , admin_result : Dict [str , Optional [str ]], customer_result : Dict [str , Optional [str ]], crm_result : Dict [str , Optional [str ]]) -> Dict [str , Any ]:
21232218 order_id = normalize_text (order_id , 120 )
21242219 updated_at = utc_now_iso ()
2220+ admin_status = str (admin_result .get ("status" ) or "unknown" )
2221+ customer_status = str (customer_result .get ("status" ) or "unknown" )
2222+ crm_status = str (crm_result .get ("status" ) or "unknown" )
2223+ fulfillment_status = "postpay_completed" if _nowpayments_postpay_complete (admin_status , customer_status , crm_status ) else "payment_confirmed"
21252224 conn = get_db ()
21262225 try :
21272226 db_execute (
@@ -2138,20 +2237,32 @@ def update_nowpayments_postpay_delivery(order_id: str, admin_result: Dict[str, O
21382237 customer_receipt_sent_at = ?,
21392238 customer_receipt_email_id = ?,
21402239 customer_receipt_debug = ?,
2240+ crm_status = ?,
2241+ crm_delivery_id = ?,
2242+ crm_error = ?,
2243+ crm_last_attempt_at = ?,
2244+ crm_response_code = ?,
2245+ crm_recorded_at = ?,
21412246 postpay_processed_at = ?
21422247 WHERE order_id = ?
21432248 """ ,
21442249 (
21452250 updated_at ,
2146- "payment_confirmed" ,
2147- str ( admin_result . get ( "status" ) or "unknown" ) ,
2148- updated_at if str ( admin_result . get ( "status" ) or "" ) == "sent" else None ,
2251+ fulfillment_status ,
2252+ admin_status ,
2253+ updated_at if admin_status == "sent" else None ,
21492254 normalize_text (admin_result .get ("email_id" ), 120 ),
21502255 normalize_text (admin_result .get ("debug_detail" ), 500 ),
2151- str ( customer_result . get ( "status" ) or "unknown" ) ,
2152- updated_at if str ( customer_result . get ( "status" ) or "" ) == "sent" else None ,
2256+ customer_status ,
2257+ updated_at if customer_status == "sent" else None ,
21532258 normalize_text (customer_result .get ("email_id" ), 120 ),
21542259 normalize_text (customer_result .get ("debug_detail" ), 500 ),
2260+ crm_status ,
2261+ normalize_text (crm_result .get ("delivery_id" ), 120 ),
2262+ normalize_text (crm_result .get ("debug_detail" ), 500 ),
2263+ updated_at ,
2264+ crm_result .get ("response_code" ),
2265+ updated_at if crm_status == "sent" else None ,
21552266 updated_at ,
21562267 order_id ,
21572268 ),
@@ -2169,18 +2280,36 @@ def maybe_process_nowpayments_paid_order(order_id: str) -> Dict[str, Any]:
21692280 return order
21702281 admin_status = str (order .get ("payment_notice_status" ) or "" ).strip ().lower ()
21712282 customer_status = str (order .get ("customer_receipt_status" ) or "" ).strip ().lower ()
2283+ crm_status = str (order .get ("crm_status" ) or "" ).strip ().lower ()
21722284 customer_email = normalize_text (order .get ("customer_email" ), 200 ).lower ()
21732285 needs_admin = admin_status not in {"sent" , "not_configured" , "disabled" }
21742286 needs_customer = bool (customer_email ) and customer_status not in {"sent" , "skipped" , "not_configured" , "disabled" }
2175- if not needs_admin and not needs_customer :
2287+ needs_crm = crm_status not in {"sent" , "not_configured" , "disabled" }
2288+ if not needs_admin and not needs_customer and not needs_crm :
21762289 return order
2177- admin_result = {"status" : admin_status or ("disabled" if not NOWPAYMENTS_POSTPAY_EMAILS_ENABLED else "not_configured" if not (RESEND_API_KEY and RESEND_FROM and RESEND_TO ) else "skipped" ), "email_id" : order .get ("payment_notice_email_id" ), "debug_detail" : order .get ("payment_notice_debug" )}
2178- customer_result = {"status" : customer_status or ("skipped" if not customer_email else "disabled" if not NOWPAYMENTS_POSTPAY_EMAILS_ENABLED else "not_configured" if not (RESEND_API_KEY and RESEND_FROM ) else "skipped" ), "email_id" : order .get ("customer_receipt_email_id" ), "debug_detail" : order .get ("customer_receipt_debug" )}
2290+ admin_result = {
2291+ "status" : admin_status or ("disabled" if not NOWPAYMENTS_POSTPAY_EMAILS_ENABLED else "not_configured" if not (RESEND_API_KEY and RESEND_FROM and RESEND_TO ) else "skipped" ),
2292+ "email_id" : order .get ("payment_notice_email_id" ),
2293+ "debug_detail" : order .get ("payment_notice_debug" ),
2294+ }
2295+ customer_result = {
2296+ "status" : customer_status or ("skipped" if not customer_email else "disabled" if not NOWPAYMENTS_POSTPAY_EMAILS_ENABLED else "not_configured" if not (RESEND_API_KEY and RESEND_FROM ) else "skipped" ),
2297+ "email_id" : order .get ("customer_receipt_email_id" ),
2298+ "debug_detail" : order .get ("customer_receipt_debug" ),
2299+ }
2300+ crm_result = {
2301+ "status" : crm_status or ("disabled" if not NOWPAYMENTS_POSTPAY_CRM_ENABLED else "not_configured" if not NOWPAYMENTS_POSTPAY_CRM_URL else "skipped" ),
2302+ "delivery_id" : order .get ("crm_delivery_id" ),
2303+ "debug_detail" : order .get ("crm_error" ),
2304+ "response_code" : order .get ("crm_response_code" ),
2305+ }
21792306 if needs_admin :
21802307 admin_result = send_nowpayments_internal_paid_email (order )
21812308 if needs_customer :
21822309 customer_result = send_nowpayments_customer_paid_email (order )
2183- return update_nowpayments_postpay_delivery (order_id , admin_result , customer_result )
2310+ if needs_crm :
2311+ crm_result = send_nowpayments_crm_intake (order )
2312+ return update_nowpayments_postpay_delivery (order_id , admin_result , customer_result , crm_result )
21842313
21852314
21862315def upsert_nowpayments_order_from_ipn (payload : Dict [str , Any ]) -> Dict [str , Any ]:
@@ -2236,8 +2365,8 @@ def upsert_nowpayments_order_from_ipn(payload: Dict[str, Any]) -> Dict[str, Any]
22362365 order_id, created_at, updated_at, status, payment_status, fulfillment_status,
22372366 provider_invoice_id, provider_payment_id, pay_currency, pay_amount, actually_paid, actually_paid_at_fiat,
22382367 purchase_id, raw_last_ipn_json, last_ipn_at,
2239- payment_notice_status, customer_receipt_status
2240- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2368+ payment_notice_status, customer_receipt_status, crm_status
2369+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
22412370 """ ,
22422371 (
22432372 resolved_order_id ,
@@ -2257,6 +2386,7 @@ def upsert_nowpayments_order_from_ipn(payload: Dict[str, Any]) -> Dict[str, Any]
22572386 updated_at ,
22582387 "pending" ,
22592388 "pending" ,
2389+ "pending" ,
22602390 ),
22612391 )
22622392 conn .commit ()
@@ -2282,11 +2412,21 @@ def nowpayments_public_order_view(order: Dict[str, Any]) -> Dict[str, Any]:
22822412 paid = _nowpayments_paid_flag (str (order .get ("payment_status" ) or "" ))
22832413 customer_email = normalize_text (order .get ("customer_email" ), 200 ).lower ()
22842414 support_email = NOWPAYMENTS_SUPPORT_EMAIL or RESEND_TO or RESEND_FROM
2415+ internal_alert_status = order .get ("payment_notice_status" ) or "pending"
2416+ customer_receipt_status = order .get ("customer_receipt_status" ) or ("skipped" if not customer_email else "pending" )
2417+ crm_status = order .get ("crm_status" ) or "pending"
2418+ postpay_complete = _nowpayments_postpay_complete (internal_alert_status , customer_receipt_status , crm_status )
2419+ if paid and postpay_complete :
2420+ message = "Payment confirmed. Confirmation flow is complete."
2421+ elif paid :
2422+ message = "Payment confirmed. Finalizing confirmation flow now."
2423+ else :
2424+ message = "Payment is not confirmed yet. If you already paid, wait a few seconds and refresh."
22852425 return {
22862426 "order_id" : order .get ("order_id" ),
22872427 "status" : order .get ("status" ) or "created" ,
22882428 "payment_status" : order .get ("payment_status" ) or "waiting" ,
2289- "fulfillment_status" : order .get ("fulfillment_status" ) or ("payment_confirmed" if paid else "pending_payment" ),
2429+ "fulfillment_status" : order .get ("fulfillment_status" ) or ("postpay_completed" if postpay_complete else " payment_confirmed" if paid else "pending_payment" ),
22902430 "paid" : paid ,
22912431 "product" : {
22922432 "sku" : order .get ("product_sku" ) or "" ,
@@ -2297,13 +2437,14 @@ def nowpayments_public_order_view(order: Dict[str, Any]) -> Dict[str, Any]:
22972437 "provider_invoice_id" : order .get ("provider_invoice_id" ) or "" ,
22982438 "updated_at" : order .get ("updated_at" ) or order .get ("created_at" ) or "" ,
22992439 "customer_email_present" : bool (customer_email ),
2300- "payment_notice_status" : order .get ("payment_notice_status" ) or "pending" ,
2301- "customer_receipt_status" : order .get ("customer_receipt_status" ) or ("skipped" if not customer_email else "pending" ),
2440+ "internal_alert_status" : internal_alert_status ,
2441+ "payment_notice_status" : internal_alert_status ,
2442+ "customer_receipt_status" : customer_receipt_status ,
2443+ "crm_status" : crm_status ,
2444+ "postpay_complete" : postpay_complete ,
2445+ "journal_status" : "recorded" if paid else "pending" ,
23022446 "support_email" : support_email ,
2303- "message" : (
2304- f"Payment confirmed. We will follow up within about { NOWPAYMENTS_POSTPAY_NEXT_STEP_HOURS } hours." if paid
2305- else "Payment is not confirmed yet. If you already paid, wait a few seconds and refresh."
2306- ),
2447+ "message" : message ,
23072448 }
23082449
23092450
@@ -4249,6 +4390,12 @@ def ensure_db() -> None:
42494390 customer_receipt_sent_at TEXT,
42504391 customer_receipt_email_id TEXT,
42514392 customer_receipt_debug TEXT,
4393+ crm_status TEXT,
4394+ crm_delivery_id TEXT,
4395+ crm_error TEXT,
4396+ crm_last_attempt_at TEXT,
4397+ crm_response_code INTEGER,
4398+ crm_recorded_at TEXT,
42524399 postpay_processed_at TEXT
42534400 )
42544401 """
@@ -4294,6 +4441,12 @@ def ensure_db() -> None:
42944441 customer_receipt_sent_at TEXT,
42954442 customer_receipt_email_id TEXT,
42964443 customer_receipt_debug TEXT,
4444+ crm_status TEXT,
4445+ crm_delivery_id TEXT,
4446+ crm_error TEXT,
4447+ crm_last_attempt_at TEXT,
4448+ crm_response_code INTEGER,
4449+ crm_recorded_at TEXT,
42974450 postpay_processed_at TEXT
42984451 )
42994452 """
@@ -4333,6 +4486,12 @@ def ensure_db() -> None:
43334486 ensure_column (conn , "nowpayments_orders" , "customer_receipt_sent_at" , "TEXT" )
43344487 ensure_column (conn , "nowpayments_orders" , "customer_receipt_email_id" , "TEXT" )
43354488 ensure_column (conn , "nowpayments_orders" , "customer_receipt_debug" , "TEXT" )
4489+ ensure_column (conn , "nowpayments_orders" , "crm_status" , "TEXT" )
4490+ ensure_column (conn , "nowpayments_orders" , "crm_delivery_id" , "TEXT" )
4491+ ensure_column (conn , "nowpayments_orders" , "crm_error" , "TEXT" )
4492+ ensure_column (conn , "nowpayments_orders" , "crm_last_attempt_at" , "TEXT" )
4493+ ensure_column (conn , "nowpayments_orders" , "crm_response_code" , "INTEGER" )
4494+ ensure_column (conn , "nowpayments_orders" , "crm_recorded_at" , "TEXT" )
43364495 ensure_column (conn , "nowpayments_orders" , "postpay_processed_at" , "TEXT" )
43374496 db_execute (conn , "CREATE UNIQUE INDEX IF NOT EXISTS idx_nowpayments_orders_order_id ON nowpayments_orders(order_id)" )
43384497 db_execute (conn , "CREATE INDEX IF NOT EXISTS idx_nowpayments_orders_payment_id ON nowpayments_orders(provider_payment_id)" )
@@ -4710,6 +4869,7 @@ def nowpayments_ipn_receive():
47104869 "paid" : bool (updated .get ("paid" )),
47114870 "payment_notice_status" : (processed_order or {}).get ("payment_notice_status" ) or "pending" ,
47124871 "customer_receipt_status" : (processed_order or {}).get ("customer_receipt_status" ) or "pending" ,
4872+ "crm_status" : (processed_order or {}).get ("crm_status" ) or "pending" ,
47134873 },
47144874 )
47154875 return jsonify ({
@@ -4720,6 +4880,7 @@ def nowpayments_ipn_receive():
47204880 "paid" : bool (updated .get ("paid" )),
47214881 "payment_notice_status" : (processed_order or {}).get ("payment_notice_status" ) or "pending" ,
47224882 "customer_receipt_status" : (processed_order or {}).get ("customer_receipt_status" ) or "pending" ,
4883+ "crm_status" : (processed_order or {}).get ("crm_status" ) or "pending" ,
47234884 })
47244885
47254886
0 commit comments