88import base64
99import gzip
1010import json
11+ import math
1112import os
1213import sys
1314import sqlite3
@@ -1103,7 +1104,7 @@ def send_welcome_email(email, name):
11031104 <div style="text-align:center;margin-bottom:24px">
11041105 <a href="https://www.gohirehumans.com" style="display:inline-block;background:#0d7377;color:white;padding:12px 28px;border-radius:6px;text-decoration:none;font-weight:600;font-size:15px">Get Started →</a>
11051106 </div>
1106- <p style="font-size:13px;color:#6b6963;margin-bottom:8px">Every transaction is protected by escrow. Funds only release when you approve the work .</p>
1107+ <p style="font-size:13px;color:#6b6963;margin-bottom:8px">Where payment processing is configured, paid work uses platform payment records and review steps before funds are released or refunded .</p>
11071108 <p style="font-size:13px;color:#6b6963">Questions? Reply to this email or check our <a href="https://www.gohirehumans.com/faq.html" style="color:#0d7377">FAQ</a>.</p>
11081109 <hr style="border:none;border-top:1px solid #dddbd6;margin:24px 0 16px">
11091110 <p style="font-size:11px;color:#a8a6a0;text-align:center">© 2026 GoHireHumans · <a href="https://www.gohirehumans.com" style="color:#a8a6a0">gohirehumans.com</a></p>
@@ -1154,7 +1155,12 @@ def worker_has_payout_setup(db, user_id):
11541155 wp = db .execute ("SELECT payout_account_id, payout_method FROM worker_profiles WHERE user_id = ?" , [user_id ]).fetchone ()
11551156 if not wp :
11561157 return False
1157- return bool (wp ['payout_account_id' ]) and wp ['payout_method' ] not in ('pending_setup' , None , '' )
1158+ payout_account_id = wp ['payout_account_id' ] or ''
1159+ if PRODUCTION_MODE and payout_account_id .startswith ('acct_sim_' ):
1160+ return False
1161+ if (stripe_configured () or PRODUCTION_MODE ) and not payout_account_id :
1162+ return False
1163+ return bool (payout_account_id ) and wp ['payout_method' ] not in ('pending_setup' , None , '' )
11581164
11591165
11601166def employer_has_payment_setup (db , user_id ):
@@ -1170,40 +1176,39 @@ def employer_has_payment_setup(db, user_id):
11701176
11711177def release_escrow_to_worker (db , order_id , milestone_id , amount , worker_id ):
11721178 """Release escrow hold, transfer to worker via Stripe or simulation."""
1173- # Mark escrow released
1174- db .execute (
1175- "UPDATE escrow_holds SET status='released', released_at=datetime('now') WHERE order_id=? AND (milestone_id=? OR milestone_id IS NULL) AND status='held'" ,
1176- [order_id , milestone_id ]
1177- )
11781179 # Employer pays the 1% platform margin on top of the listed amount.
11791180 # Workers receive the listed amount unless Enzo explicitly changes the model.
11801181 fee = round (amount * SERVICE_FEE_RATE , 2 )
11811182 worker_payout = round (amount , 2 )
11821183
1184+ if stripe_configured () or PRODUCTION_MODE :
1185+ wp = db .execute ("SELECT payout_account_id FROM worker_profiles WHERE user_id = ?" , [worker_id ]).fetchone ()
1186+ payout_account_id = (wp ['payout_account_id' ] if wp else '' ) or ''
1187+ if not payout_account_id or payout_account_id .startswith ('acct_sim_' ):
1188+ raise ValueError ("A live worker Stripe Connect payout account is required before release." )
1189+ if not stripe_configured ():
1190+ raise ValueError ("Stripe is not configured; live payout release is disabled in production." )
1191+ try :
1192+ stripe .Transfer .create (
1193+ amount = int (worker_payout * 100 ),
1194+ currency = "usd" ,
1195+ destination = payout_account_id ,
1196+ metadata = {"order_id" : str (order_id ), "milestone_id" : str (milestone_id or "" )},
1197+ description = f"GoHireHumans escrow release order #{ order_id } " ,
1198+ idempotency_key = f"escrow-release:{ order_id } :{ milestone_id or 'full' } :{ int (worker_payout * 100 )} "
1199+ )
1200+ except stripe .error .StripeError as e :
1201+ raise ValueError (f"Stripe transfer failed: { str (e )} " )
1202+
1203+ # Only mark escrow/revenue after live transfer succeeds or non-production simulation is allowed.
1204+ db .execute (
1205+ "UPDATE escrow_holds SET status='released', released_at=datetime('now') WHERE order_id=? AND (milestone_id=? OR milestone_id IS NULL) AND status='held'" ,
1206+ [order_id , milestone_id ]
1207+ )
11831208 db .execute (
11841209 "INSERT INTO platform_revenue (order_id, fee_amount, fee_type) VALUES (?,?,?)" ,
11851210 [order_id , fee , 'service_fee' ]
11861211 )
1187-
1188- # Attempt Stripe transfer if configured
1189- if stripe_configured ():
1190- wp = db .execute ("SELECT payout_account_id FROM worker_profiles WHERE user_id = ?" , [worker_id ]).fetchone ()
1191- if wp and wp ['payout_account_id' ] and not wp ['payout_account_id' ].startswith ('acct_sim_' ):
1192- try :
1193- stripe .Transfer .create (
1194- amount = int (worker_payout * 100 ),
1195- currency = "usd" ,
1196- destination = wp ['payout_account_id' ],
1197- metadata = {"order_id" : str (order_id ), "milestone_id" : str (milestone_id or "" )},
1198- description = f"GoHireHumans escrow release order #{ order_id } "
1199- )
1200- except stripe .error .StripeError as e :
1201- db .execute (
1202- "UPDATE escrow_holds SET status='held' WHERE order_id=? AND (milestone_id=? OR milestone_id IS NULL)" ,
1203- [order_id , milestone_id ]
1204- )
1205- raise ValueError (f"Stripe transfer failed: { str (e )} " )
1206-
12071212 return worker_payout , fee
12081213
12091214
@@ -2744,7 +2749,11 @@ def _handle_routes(db):
27442749 ms_amount = float (current_ms ['amount' ])
27452750
27462751 # Release escrow for this milestone
2747- worker_payout , fee = release_escrow_to_worker (db , order_id , ms_id , ms_amount , order ['worker_id' ])
2752+ try :
2753+ worker_payout , fee = release_escrow_to_worker (db , order_id , ms_id , ms_amount , order ['worker_id' ])
2754+ except ValueError as e :
2755+ db .rollback ()
2756+ return error_response (str (e ), 502 )
27482757
27492758 db .execute (
27502759 "UPDATE milestones SET status='approved', released_at=datetime('now') WHERE id=?" ,
@@ -3056,15 +3065,34 @@ def _handle_routes(db):
30563065 total_hours = sum (float (e ['hours' ]) for e in entries )
30573066 total_pay = round (total_hours * float (hc ['hourly_rate' ]), 2 )
30583067 fee = round (total_pay * SERVICE_FEE_RATE , 2 )
3059- worker_pay = round (total_pay - fee , 2 )
3068+ worker_pay = round (total_pay , 2 )
3069+
3070+ if stripe_configured () or PRODUCTION_MODE :
3071+ wp = db .execute ("SELECT payout_account_id FROM worker_profiles WHERE user_id=?" , [order ['worker_id' ]]).fetchone ()
3072+ payout_account_id = (wp ['payout_account_id' ] if wp else '' ) or ''
3073+ if not payout_account_id or payout_account_id .startswith ('acct_sim_' ):
3074+ return error_response ("A live worker Stripe Connect payout account is required before approving paid hours." , 409 )
3075+ if not stripe_configured ():
3076+ return error_response ("Stripe is not configured; live hourly payout release is disabled in production." , 503 )
3077+ try :
3078+ stripe .Transfer .create (
3079+ amount = int (worker_pay * 100 ),
3080+ currency = "usd" ,
3081+ destination = payout_account_id ,
3082+ metadata = {"order_id" : str (order_id ), "week_of" : week_of },
3083+ idempotency_key = f"hourly-release:{ order_id } :{ week_of } :{ int (worker_pay * 100 )} "
3084+ )
3085+ except stripe .error .StripeError as e :
3086+ db .rollback ()
3087+ return error_response (f"Stripe transfer failed: { str (e )} " , 502 )
30603088
3061- # Mark entries approved
3089+ # Mark entries approved only after live transfer succeeds or non-production simulation is allowed.
30623090 db .execute (
30633091 "UPDATE time_entries SET status='approved' WHERE contract_id=? AND week_of=? AND status='pending'" ,
30643092 [hc ['id' ], week_of ]
30653093 )
30663094
3067- # Release escrow for these hours
3095+ # Release escrow for these hours after transfer/fail-closed checks above.
30683096 db .execute (
30693097 "UPDATE escrow_holds SET status='released', released_at=datetime('now') WHERE order_id=? AND status='held'" ,
30703098 [order_id ]
@@ -3074,20 +3102,6 @@ def _handle_routes(db):
30743102 [order_id , fee ]
30753103 )
30763104
3077- # Transfer to worker if Stripe configured
3078- if stripe_configured ():
3079- wp = db .execute ("SELECT payout_account_id FROM worker_profiles WHERE user_id=?" , [order ['worker_id' ]]).fetchone ()
3080- if wp and wp ['payout_account_id' ] and not wp ['payout_account_id' ].startswith ('acct_sim_' ):
3081- try :
3082- stripe .Transfer .create (
3083- amount = int (worker_pay * 100 ),
3084- currency = "usd" ,
3085- destination = wp ['payout_account_id' ],
3086- metadata = {"order_id" : str (order_id ), "week_of" : week_of }
3087- )
3088- except stripe .error .StripeError :
3089- pass
3090-
30913105 # Refund unused escrow and fund next week
30923106 escrow_held = float (hc ['current_week_escrow_amount' ] or 0 )
30933107 unused = max (0 , round (escrow_held - total_pay , 2 ))
@@ -3416,6 +3430,8 @@ def _handle_routes(db):
34163430 except stripe .error .StripeError as e :
34173431 return error_response (f"Stripe error: { str (e )} " , 502 )
34183432 else :
3433+ if PRODUCTION_MODE :
3434+ return error_response ("Stripe is not configured; simulated worker payout setup is disabled in production." , 503 )
34193435 # Simulation
34203436 body = get_body ()
34213437 payout_account_id = f"acct_sim_{ secrets .token_hex (10 )} "
@@ -3456,7 +3472,10 @@ def _handle_routes(db):
34563472 except stripe .error .StripeError :
34573473 worker_status = {"connected" : False , "account_id" : wp ['payout_account_id' ], "mode" : "live" }
34583474 else :
3459- worker_status = {"connected" : True , "account_id" : wp ['payout_account_id' ], "mode" : "simulated" }
3475+ if PRODUCTION_MODE :
3476+ worker_status = {"connected" : False , "account_id" : None , "mode" : "disabled" , "message" : "Simulated worker payout is disabled in production." }
3477+ else :
3478+ worker_status = {"connected" : True , "account_id" : wp ['payout_account_id' ], "mode" : "simulated" }
34603479 else :
34613480 worker_status = {"connected" : False , "account_id" : None }
34623481
@@ -4107,85 +4126,79 @@ def _handle_routes(db):
41074126 return error_response ("Order must be disputed to resolve" , 409 )
41084127
41094128 admin_notes = body .get ("notes" , "" )
4129+ step_error , step_status = require_admin_step_up (db , user , body , "admin_resolve_dispute" )
4130+ if step_error :
4131+ return error_response (step_error , step_status )
41104132
4111- if resolution == 'release_to_worker' :
4112- # Release all held escrow to worker
4113- holds = db .execute (
4114- "SELECT * FROM escrow_holds WHERE order_id=? AND status='held'" ,
4115- [int (order_id )]
4116- ).fetchall ()
4117- total_released = 0
4118- for hold in holds :
4119- db .execute (
4120- "UPDATE escrow_holds SET status='released', released_at=datetime('now') WHERE id=?" ,
4121- [hold ['id' ]]
4122- )
4123- total_released += float (hold ['amount' ])
4124-
4125- fee = round (total_released * SERVICE_FEE_RATE , 2 )
4126- worker_pay = round (total_released - fee , 2 )
4127- db .execute (
4128- "INSERT INTO platform_revenue (order_id, fee_amount, fee_type) VALUES (?,?,'dispute_resolution')" ,
4129- [order_id , fee ]
4133+ if body .get ("manual_money_movement_confirmed" ) is not True :
4134+ return error_response (
4135+ "Manual money movement confirmation required. Complete and verify any Stripe refund/transfer outside this admin action before recording the dispute resolution." ,
4136+ 409
41304137 )
4138+ processor_reference = str (body .get ("processor_reference" ) or "" ).strip ()
4139+ if not processor_reference :
4140+ return error_response ("processor_reference required for manual dispute settlement audit trail" , 400 )
41314141
4132- push_notification (db , order ['worker_id' ], "dispute_resolved" ,
4133- "Dispute resolved in your favor" ,
4134- f"${ worker_pay :.2f} has been released to you." ,
4135- f"/orders/{ order_id } " )
4136- push_notification (db , order ['employer_id' ], "dispute_resolved" ,
4137- "Dispute resolved" ,
4138- f"The dispute for order #{ order_id } was resolved in the worker's favor." ,
4139- f"/orders/{ order_id } " )
4142+ holds = db .execute (
4143+ "SELECT * FROM escrow_holds WHERE order_id=? AND status='held'" ,
4144+ [int (order_id )]
4145+ ).fetchall ()
4146+ if not holds :
4147+ return error_response ("No held payment record available to resolve" , 409 )
41404148
4141- elif resolution == 'refund_to_employer' :
4149+ total_held = round (sum (float (hold ['amount' ]) for hold in holds ), 2 )
4150+ worker_percent = 100.0 if resolution == 'release_to_worker' else 0.0
4151+ if resolution == 'split' :
4152+ try :
4153+ worker_percent = float (body .get ("worker_percent" , 50 ))
4154+ except (TypeError , ValueError ):
4155+ return error_response ("worker_percent must be a number between 0 and 100" , 400 )
4156+ if not math .isfinite (worker_percent ) or worker_percent < 0 or worker_percent > 100 :
4157+ return error_response ("worker_percent must be a finite number between 0 and 100" , 400 )
4158+ worker_portion = round (total_held * (worker_percent / 100.0 ), 2 )
4159+ employer_portion = round (total_held - worker_portion , 2 )
4160+
4161+ escrow_status = 'released' if resolution == 'release_to_worker' else 'refunded' if resolution == 'refund_to_employer' else 'partial'
4162+ db .execute (
4163+ "UPDATE escrow_holds SET status=?, released_at=datetime('now') WHERE order_id=? AND status='held'" ,
4164+ [escrow_status , int (order_id )]
4165+ )
4166+ if worker_portion > 0 :
41424167 db .execute (
4143- "UPDATE escrow_holds SET status='refunded', released_at=datetime('now') WHERE order_id=? AND status='held' " ,
4144- [int ( order_id ) ]
4168+ "INSERT INTO platform_revenue (order_id, fee_amount, fee_type) VALUES (?,?,?) " ,
4169+ [order_id , round ( worker_portion * SERVICE_FEE_RATE , 2 ), 'manual_dispute_resolution' ]
41454170 )
4146- push_notification (db , order ['employer_id' ], "dispute_resolved" ,
4147- "Dispute resolved — refund issued" ,
4148- f"Your payment for order #{ order_id } has been refunded." ,
4149- f"/orders/{ order_id } " )
4150- push_notification (db , order ['worker_id' ], "dispute_resolved" ,
4151- "Dispute resolved" ,
4152- f"The dispute for order #{ order_id } was resolved in the employer's favor." ,
4153- f"/orders/{ order_id } " )
41544171
4155- elif resolution == 'split' :
4156- split_pct = float (body .get ("worker_percent" , 50 )) / 100
4157- holds = db .execute (
4158- "SELECT * FROM escrow_holds WHERE order_id=? AND status='held'" ,
4159- [int (order_id )]
4160- ).fetchall ()
4161- for hold in holds :
4162- amount = float (hold ['amount' ])
4163- worker_portion = round (amount * split_pct , 2 )
4164- employer_portion = round (amount - worker_portion , 2 )
4165- db .execute (
4166- "UPDATE escrow_holds SET status='partial', released_at=datetime('now') WHERE id=?" ,
4167- [hold ['id' ]]
4168- )
4169- db .execute (
4170- "INSERT INTO platform_revenue (order_id, fee_amount, fee_type) VALUES (?,?,'dispute_split')" ,
4171- [order_id , round (worker_portion * SERVICE_FEE_RATE , 2 )]
4172- )
4173- push_notification (db , order ['worker_id' ], "dispute_resolved" ,
4174- "Dispute resolved — split decision" ,
4175- f"The dispute for order #{ order_id } was resolved with a split decision." ,
4176- f"/orders/{ order_id } " )
4177- push_notification (db , order ['employer_id' ], "dispute_resolved" ,
4178- "Dispute resolved — split decision" ,
4179- f"The dispute for order #{ order_id } was resolved with a split decision." ,
4180- f"/orders/{ order_id } " )
4172+ push_notification (db , order ['worker_id' ], "dispute_resolved" ,
4173+ "Dispute resolution recorded" ,
4174+ f"The dispute for order #{ order_id } was resolved after manual settlement was verified by an admin." ,
4175+ f"/orders/{ order_id } " )
4176+ push_notification (db , order ['employer_id' ], "dispute_resolved" ,
4177+ "Dispute resolution recorded" ,
4178+ f"The dispute for order #{ order_id } was resolved after manual settlement was verified by an admin." ,
4179+ f"/orders/{ order_id } " )
41814180
41824181 db .execute (
41834182 "UPDATE orders SET status='completed', completed_at=datetime('now'), updated_at=datetime('now') WHERE id=?" ,
41844183 [int (order_id )]
41854184 )
4186- audit (db , user ['id' ], "resolve_dispute" , "order" , int (order_id ), {"resolution" : resolution , "notes" : admin_notes })
4185+ audit (db , user ['id' ], "resolve_dispute_manual_settlement" , "order" , int (order_id ), {
4186+ "resolution" : resolution ,
4187+ "notes" : admin_notes ,
4188+ "manual_money_movement_confirmed" : True ,
4189+ "processor_reference" : processor_reference ,
4190+ "worker_portion" : worker_portion ,
4191+ "employer_portion" : employer_portion ,
4192+ })
41874193 db .commit ()
4188- return json_response ({"ok" : True , "resolution" : resolution })
4194+ return json_response ({
4195+ "ok" : True ,
4196+ "resolution" : resolution ,
4197+ "mode" : "manual_settlement_recorded" ,
4198+ "processor_reference" : processor_reference ,
4199+ "worker_portion" : worker_portion ,
4200+ "employer_portion" : employer_portion ,
4201+ })
41894202
41904203 elif path == "/admin/audit-log" and method == "GET" :
41914204 user = authenticate (db )
0 commit comments