* Sequential, gap-free, atomic to prevent duplicates.
* Only called after payment confirmation.
* Assign the next membership number to a member.
* Called ONLY after membership value payment is confirmed.
* Number is based on payment priority — first to pay gets next number.
*/
publicstaticfunctionnext():string
publicstaticfunctionassign(int$memberId):?string
{
$db=App::getInstance()->db();
// Use a transaction with locking to prevent concurrent duplicates
$db->beginTransaction();
try{
// Get the current maximum membership number
$row=$db->selectOne(
"SELECT MAX(CAST(membership_number AS UNSIGNED)) as max_num FROM members WHERE membership_number IS NOT NULL AND membership_number REGEXP '^[0-9]+$' FOR UPDATE"
);
// Check member exists and doesn't already have a number
$member=$db->selectOne(
"SELECT id, membership_number FROM members WHERE id = ? AND is_archived = 0",