Почему мой генератор не работает?
str_replace — Заменяет все вхождения строки поиска на строку замены

легче всего переделать так.
по всей длине маски
взять текущий символ.
если он Х то к результату добавить случайный символ иначе текущий
str_replace на preg_replace с доп. параметром заменить...

$promo_mask = 'XXXX-5555-XXXX-5555';
$mask_count = substr_count($promo_mask, 'X');
$promo = $promo_mask;

  $chars = '12345ABCDEFGHIJKLMNOPQRSTUVWXYZ67890';
  $hashpromo = '';
  for($ichars = 1; $ichars <= $mask_count; ++$ichars) {
    	$random = str_shuffle($chars);
    	$promo = preg_replace('/X/', $random[$ichars], $promo, 1);
  }
  echo $promo;