verify(!$auth->resetPassword($token,$password,$password),'Old-email reset link is rejected after email change');
$db->exec('DELETE FROM auth_attempts');$auth->requestReset('securitymember');$token=$auth->token();$service->setUserActive($id,false);$service->setUserActive($id,true);
verify(!$auth->resetPassword($token,$password,$password),'Disable/reactivate does not revive a reset link');
$db->exec('DELETE FROM auth_attempts');$auth->requestReset('securitymember');$token=$auth->token();$edit['user_role']='Guest';$service->updateUser($id,$edit);
verify(!$auth->resetPassword($auth->token(),$password,$password),'Email change during delivery invalidates an already-created reset token');
$verification=newReviewVerification();$db->exec('DELETE FROM auth_attempts');
verify($verification->request('securitymember'),'Verification link is generated for an unverified account');
$token=$verification->token();verify(!str_contains(json_encode($db->query('SELECT * FROM email_verifications')->fetchAll()),$token),'Verification token is stored only as a hash');
verify(!$verification->confirm($token,'wrong password'),'Mailbox verification also requires the account password');
verify($verification->confirm($token,$password),'Correct verification link and password verify email ownership');
verify(!$verification->confirm($token,$password),'Verification links cannot be reused');
verify((bool)$auth->findUser('securitymember')['user_email_verified_at'],'Verified ownership is stored');
$db->exec('DELETE FROM auth_attempts');$verification->request('securitymember');$token=$verification->token();$db->exec('UPDATE email_verifications SET expires_at=DATE_SUB(UTC_TIMESTAMP(),INTERVAL 1 SECOND)');
verify(!$verification->confirm($token,$password),'Expired email verification link is rejected');
$db->exec('DELETE FROM auth_attempts');$verification->request('securitymember');$token=$verification->token();$edit['user_email']='newmail@example.test';$service->updateUser($id,$edit);
verify(!$verification->confirm($token,$password),'Email edit invalidates old verification links');
$db->exec('DELETE FROM auth_attempts');$verification->mailWorks=false;try{$verification->request('securitymember');thrownewLogicException('Expected mail failure');}catch(RuntimeException$e){verify(str_contains($e->getMessage(),'could not be delivered'),'Verification mail failure is detected');}
verify((int)$db->query('SELECT COUNT(*) FROM email_verifications')->fetchColumn()===0,'Undelivered verification token is removed');
$db->exec('DELETE FROM auth_attempts');$auth->requestReset('securitymember');verify($auth->resetPassword($auth->token(),$password,$password),'Password setup/reset can also prove mailbox ownership');
verify((bool)$auth->findUser('securitymember')['user_email_verified_at'],'Mailbox ownership is verified after successful password reset');
$edit['user_email']='stillunverified@example.test';$service->updateUser($id,$edit);$db->exec('DELETE FROM auth_attempts');ob_start();$auth->login('securitymember',$password);$_SESSION[Base::USER_ROLE]=TenantAccess::SUPER;$_SESSION[Base::USER_SERIAL]=1;$_SESSION['auth_version']=(int)$auth->findUser('securitymember')['user_auth_version'];$auth->changePassword($id,$password,$password,$password);ob_end_clean();
rejects(fn()=>$roster->saveMember($club,0,'securitymember','active','2026-01-01'),'Unverified email cannot receive active club membership');
$db->exec('DELETE FROM auth_attempts');$verification->mailWorks=true;$verification->request('securitymember');$verification->confirm($verification->token(),$password);
$roster->saveMember($club,0,'securitymember','active','2026-01-01');$membership=(int)$db->query('SELECT membership_serial FROM club_memberships')->fetchColumn();$roster->addSeason($club,'Season','2026-01-01','2026-12-31');$season=(int)$db->query('SELECT season_serial FROM club_seasons')->fetchColumn();$roster->charge($club,$membership,$season,'100.00');$due=(int)$db->query('SELECT due_serial FROM club_dues')->fetchColumn();$ledger=newLedgerService();
verify((int)$db->query('SELECT COUNT(*) FROM club_payments')->fetchColumn()===1,'Payment retries preserve exactly one original payment');
$key=bin2hex(random_bytes(32));$ledger->adjust($club,$due,'80.00','Corrected season rate',$key);$ledger->adjust($club,$due,'80.00','Corrected season rate',$key);
$row=$roster->dues($club)[0];verify((int)$row['original_amount_cents']===10000&&(int)$row['amount_cents']===8000&&(int)$row['paid_cents']===3000,'Dues corrections preserve original amount and calculate effective balance');
verify((int)$db->query('SELECT COUNT(*) FROM club_due_adjustments')->fetchColumn()===1,'Dues correction retries are idempotent');
rejects(fn()=>$ledger->adjust($club,$due,'20.00','Too low',bin2hex(random_bytes(32))),'Dues cannot be corrected below received payments');
$payment=(int)$db->query('SELECT payment_serial FROM club_payments')->fetchColumn();$key=bin2hex(random_bytes(32));$ledger->reverse($club,$payment,'Incorrect check recorded',$key);$ledger->reverse($club,$payment,'Incorrect check recorded',$key);
verify((int)$db->query('SELECT COUNT(*) FROM club_payment_reversals')->fetchColumn()===1,'Payment reversal retries are idempotent');
$row=$roster->dues($club)[0];verify((int)$row['paid_cents']===0&&(int)$db->query('SELECT amount_cents FROM club_payments')->fetchColumn()===3000,'Reversal restores balance and preserves the original payment');
rejects(fn()=>$ledger->reverse($club,$payment,'Again',bin2hex(random_bytes(32))),'A payment cannot be reversed twice');
rejects(fn()=>$ledger->adjust($club,$due,'90.00','',bin2hex(random_bytes(32))),'Financial corrections require a reason');
rejects(fn()=>$ledger->payment($club,$due,'0.01','2026-01-02',bin2hex(random_bytes(32))),'Corrected paid dues still reject overpayment');
$history=$ledger->history($club);verify(count($history['payments'])===2&&count($history['adjustments'])===1,'History includes original payments, reversal details, and correction');
verify(str_contains((string)$db->query("SELECT journal_entry FROM journal WHERE journal_entry LIKE '%received payment%' LIMIT 1")->fetchColumn(),'amount_cents'),'Financial audit includes the amount and payment identifier');
verify((int)$db->query("SELECT COUNT(*) FROM journal WHERE journal_entry LIKE '%saved membership%' OR journal_entry LIKE '%created season%'")->fetchColumn()===2,'Membership and season writes have audit events');
rejects(fn()=>(newJournal())->clearJournal(),'Application journal clearing is prohibited');
$db->exec("CREATE TRIGGER reject_audit BEFORE INSERT ON journal FOR EACH ROW SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Deliberate audit failure'");
try{$ledger->adjust($club,$due,'90.00','Audit should fail',bin2hex(random_bytes(32)));thrownewLogicException('Expected audit failure');}catch(PDOException$e){verify((int)$roster->dues($club)[0]['amount_cents']===8000,'Financial write rolls back if its mandatory audit fails');}
$before=$auth->findUser('securitymember')['user_first_name'];try{$service->updateUser($id,array_replace($edit,['user_first_name'=>'Should rollback']));thrownewLogicException('Expected audit failure');}catch(PDOException$e){verify($auth->findUser('securitymember')['user_first_name']===$before,'Account edit rolls back if its audit fails');}
$db->exec('DROP TRIGGER reject_audit');
verify(!Transport::https(['REMOTE_ADDR'=>'203.0.113.1','HTTP_X_FORWARDED_PROTO'=>'https'],'127.0.0.1'),'Untrusted forwarding headers cannot mark a request secure');
verify(Transport::https(['REMOTE_ADDR'=>'127.0.0.1','HTTP_X_FORWARDED_PROTO'=>'https'],'127.0.0.1'),'Explicitly trusted proxy can report HTTPS');
verify(!Transport::https(['REMOTE_ADDR'=>'127.0.0.1','HTTP_X_FORWARDED_PROTO'=>'https,http'],'127.0.0.1'),'Ambiguous forwarded protocol is rejected');
try{(newBase())->connect();thrownewLogicException('Expected transport rejection');}catch(RuntimeException$e){verify(str_contains($e->getMessage(),'verified TLS'),'Remote production database connections fail closed without a trusted CA');}
echo"$checks production security and ledger checks passed.\n";