[ 'Заказ №{order_id} принят.', 'Мы приняли ваш заказ №{order_id}.', 'Ваш заказ №{order_id} принят в работу.', 'Получили ваш заказ №{order_id} и приняли его в работу.', ], 'order_ready_for_pickup' => [ 'Заказ №{order_id} готов к выдаче.', 'Ваш заказ №{order_id} можно забрать.', 'Можно получить заказ №{order_id}.', 'Заказ №{order_id} ожидает получения.', ], 'order_tracking_assigned' => [ 'Заказу №{order_id} присвоен трек {tracking_no}.', 'Трек заказа №{order_id}: {tracking_no}.', 'Номер отслеживания заказа №{order_id}: {tracking_no}.', 'Для заказа №{order_id} доступен трек {tracking_no}.', ], ]; /** * history: variants already used for this event/channel since its last cycle. * Persist next_history in a locked transaction; freeze text for all retries of the same event. * @return array{text:string,variant:string,next_history:list} */ public static function build(string $kind, array $payload, array $history = [], string $brand = 'iFarfor', ?callable $random = null): array { $templates = self::VARIANTS[$kind] ?? throw new \InvalidArgumentException('Неизвестный статус заказа'); $order = $payload['order_id'] ?? null; if ((!is_int($order) && !is_string($order)) || preg_match('/^[1-9][0-9]{0,18}$/D', (string)$order) !== 1) { throw new \InvalidArgumentException('Нужен положительный номер заказа'); } $brand = self::field($brand, 40); $fields = ['{order_id}' => (string)$order]; if ($kind === 'order_tracking_assigned') { $fields['{tracking_no}'] = self::field($payload['tracking_no'] ?? null, 60); } $keys = array_map(static fn(int $i): string => $kind . ':' . $i, array_keys($templates)); $history = array_values(array_unique(array_filter($history, static fn($key): bool => is_string($key) && in_array($key, $keys, true)))); $available = array_values(array_diff($keys, $history)); $last = $history === [] ? null : $history[count($history) - 1]; if ($available === []) { $history = []; $available = array_values(array_diff($keys, [$last])); // No immediate repeat across cycle boundaries. } $position = $random === null ? random_int(0, count($available) - 1) : $random(0, count($available) - 1); if (!is_int($position) || $position < 0 || $position >= count($available)) throw new \InvalidArgumentException('Неверный генератор случайных чисел'); $variant = $available[$position]; $index = (int)substr($variant, strrpos($variant, ':') + 1); $text = $brand . ': ' . strtr($templates[$index], $fields); // All optional details are facts from the event; never invent a pickup location or dispatch date. if ($kind === 'order_ready_for_pickup' && isset($payload['pickup_point']) && $payload['pickup_point'] !== '') { $text .= ' Место получения: ' . self::field($payload['pickup_point'], 100) . '.'; } $units = intdiv(strlen(mb_convert_encoding($text, 'UTF-16LE', 'UTF-8')), 2); if ($units > 268) throw new \LengthException('Текст длиннее четырёх частей SMS; сократите данные'); return ['text' => $text, 'variant' => $variant, 'next_history' => [...$history, $variant]]; } private static function field(mixed $value, int $max): string { if (!is_string($value) || !mb_check_encoding($value, 'UTF-8')) throw new \InvalidArgumentException('Неверное текстовое поле'); $value = trim($value); if ($value === '' || mb_strlen($value) > $max || preg_match('/[\x00-\x1f\x7f]/u', $value)) throw new \InvalidArgumentException('Неверное текстовое поле'); return $value; } }