feat(portal): lead module, WHMCS sync report/contacts/mapping, service lifecycle email templates

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
baodev0
2026-10-21 16:55:19 +07:00
co-authored by Claude Sonnet 5.5
parent 3c7710f1de
commit a443ec3bf6
66 changed files with 2032 additions and 124 deletions
+17 -1
View File
@@ -8,6 +8,8 @@ final class MailPlaceholders
{
public const PATTERN = '/\{\{\s*\$?([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/';
private const CONDITIONAL = '/\{\{\s*#if\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}(.*?)(?:\{\{\s*else\s*\}\}(.*?))?\{\{\s*\/if\s*\}\}/s';
private const LAYOUT_SLOT = 'content';
public const RECIPIENT_FIELDS = [
@@ -29,7 +31,19 @@ final class MailPlaceholders
{
preg_match_all(self::PATTERN, implode("\n", array_map('strval', $contents)), $matches);
return array_values(array_diff(array_unique($matches[1]), [self::LAYOUT_SLOT]));
return array_values(array_diff(array_unique($matches[1]), [self::LAYOUT_SLOT, 'else']));
}
/**
* @param array<string, string|null> $values
*/
public static function resolveConditionals(string $content, array $values): string
{
return preg_replace_callback(
self::CONDITIONAL,
fn (array $match) => trim((string) ($values[$match[1]] ?? '')) !== '' ? $match[2] : ($match[3] ?? ''),
$content,
) ?? $content;
}
/**
@@ -37,6 +51,8 @@ final class MailPlaceholders
*/
public static function fill(string $content, array $values, bool $escapeHtml = true): string
{
$content = self::resolveConditionals($content, $values);
return preg_replace_callback(self::PATTERN, function (array $match) use ($values, $escapeHtml) {
if ($match[1] === self::LAYOUT_SLOT) {
return $match[0];