Refactor SMS sending to use KavehNegar VerifyLookup templates
- Removed SmsTextResolver dependency from multiple services and controllers. - Introduced dispatchTemplate method in SmsService to handle SMS sending with templates. - Updated existing SMS sending logic across various services (OtpService, PreRegistrationController, ClinicInvitationService, PaymentManager, SecretaryController, RepresentationActionController) to utilize the new dispatchTemplate method. - Enhanced SmsMessageTemplate entity to include kavenegar_template and token_map fields. - Created migration to add new fields to the sms_message_templates table and populate them with existing data. - Updated SeedSmsMessageTemplatesCommand to handle new template structure. - Added documentation for the new SMS template structure and usage.
This commit is contained in:
@@ -23,19 +23,33 @@ class SeedSmsMessageTemplatesCommand extends Command
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$created = 0;
|
||||
$backfilled = 0;
|
||||
foreach (SmsMessageTemplate::DEFAULTS as $tag => $def) {
|
||||
if ($this->repo->findByTag($tag) !== null) {
|
||||
$existing = $this->repo->findByTag($tag);
|
||||
if ($existing !== null) {
|
||||
if ($existing->getKavenegarTemplate() === null) {
|
||||
$existing->setKavenegarTemplate($def['kavenegar_template']);
|
||||
$existing->setTokenMap($def['token_map']);
|
||||
$backfilled++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$this->repo->save(
|
||||
new SmsMessageTemplate($tag, $def['title'], $def['body'], $def['variables']),
|
||||
new SmsMessageTemplate(
|
||||
$tag,
|
||||
$def['title'],
|
||||
$def['body'],
|
||||
$def['variables'],
|
||||
$def['kavenegar_template'],
|
||||
$def['token_map'],
|
||||
),
|
||||
false,
|
||||
);
|
||||
$created++;
|
||||
}
|
||||
$this->repo->getEntityManager()->flush();
|
||||
|
||||
$output->writeln(sprintf('Seeded %d sms message templates.', $created));
|
||||
$output->writeln(sprintf('Seeded %d, backfilled %d sms message templates.', $created, $backfilled));
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,11 +41,13 @@ class SmsMessageController extends BaseController
|
||||
$items = [];
|
||||
foreach (SmsMessageTemplate::DEFAULTS as $tag => $def) {
|
||||
$items[] = $existing[$tag] ?? [
|
||||
'tag' => $tag,
|
||||
'title' => $def['title'],
|
||||
'body' => $def['body'],
|
||||
'variables' => $def['variables'],
|
||||
'updated_at' => null,
|
||||
'tag' => $tag,
|
||||
'title' => $def['title'],
|
||||
'body' => $def['body'],
|
||||
'variables' => $def['variables'],
|
||||
'kavenegar_template' => $def['kavenegar_template'],
|
||||
'token_map' => $def['token_map'],
|
||||
'updated_at' => null,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -95,13 +97,25 @@ class SmsMessageController extends BaseController
|
||||
);
|
||||
}
|
||||
|
||||
$def = SmsMessageTemplate::DEFAULTS[$tag];
|
||||
$tpl = $this->repo->findByTag($tag);
|
||||
if ($tpl === null) {
|
||||
$def = SmsMessageTemplate::DEFAULTS[$tag];
|
||||
$tpl = new SmsMessageTemplate($tag, $def['title'], $body, $def['variables']);
|
||||
$tpl = new SmsMessageTemplate(
|
||||
$tag,
|
||||
$def['title'],
|
||||
$body,
|
||||
$def['variables'],
|
||||
$def['kavenegar_template'],
|
||||
$def['token_map'],
|
||||
);
|
||||
} else {
|
||||
$tpl->setBody($body);
|
||||
}
|
||||
|
||||
if (array_key_exists('kavenegar_template', $data)) {
|
||||
$name = trim((string) $data['kavenegar_template']);
|
||||
$tpl->setKavenegarTemplate($name !== '' ? $name : null);
|
||||
}
|
||||
$this->repo->save($tpl);
|
||||
|
||||
return $this->success(['data' => $tpl->toArray()]);
|
||||
|
||||
@@ -15,43 +15,66 @@ class SmsMessageTemplate
|
||||
{
|
||||
/**
|
||||
* متن و متادیتای پیشفرض هر تگ — مرجع برای seed و fallback.
|
||||
* @var array<string, array{title: string, body: string, variables: string[]}>
|
||||
* kavenegar_template: نام تمپلت ساختهشده در پنل کاوهنگار (VerifyLookup).
|
||||
* token_map: نگاشت متغیر منطقی → جایگاه کاوهنگار (token/token2/token3 بدون فاصله، token10/token20 با فاصله).
|
||||
* @var array<string, array{title: string, body: string, variables: string[], kavenegar_template: string, token_map: array<string,string>}>
|
||||
*/
|
||||
public const DEFAULTS = [
|
||||
SmsLog::TAG_OTP => [
|
||||
'title' => 'کد تأیید ورود',
|
||||
'body' => 'کد تأیید شما: {code}',
|
||||
'variables' => ['code', 'site'],
|
||||
'kavenegar_template' => 'clinicpro-otp',
|
||||
'token_map' => ['code' => 'token', 'site' => 'token10'],
|
||||
],
|
||||
SmsLog::TAG_PAYMENT => [
|
||||
'title' => 'تأیید پرداخت و نوبت',
|
||||
'body' => 'نوبت شما با {doctor} در تاریخ {date} ثبت و تأیید شد.',
|
||||
'variables' => ['doctor', 'date'],
|
||||
'kavenegar_template' => 'clinicpro-payment',
|
||||
'token_map' => ['doctor' => 'token10', 'date' => 'token20'],
|
||||
],
|
||||
SmsLog::TAG_CLINIC_INVITATION => [
|
||||
'title' => 'دعوت پزشک به کلینیک',
|
||||
'body' => "دکتر گرامی، کلینیک {clinic} شما را برای همکاری دعوت کرده است.\nبرای بررسی: {link}\nاین لینک تا ۷۲ ساعت معتبر است.",
|
||||
'variables' => ['clinic', 'link'],
|
||||
'kavenegar_template' => 'clinicpro-clinic-invite',
|
||||
'token_map' => ['clinic' => 'token10', 'link' => 'token'],
|
||||
],
|
||||
SmsLog::TAG_PRE_REGISTRATION => [
|
||||
'title' => 'پیشثبتنام',
|
||||
'body' => 'به کلینیک پرو خوش آمدید! شمارهکاربری: {username} | رمز عبور: {password} | لینک ورود: {link}',
|
||||
'variables' => ['username', 'password', 'link'],
|
||||
'kavenegar_template' => 'clinicpro-pre-register',
|
||||
'token_map' => ['username' => 'token', 'password' => 'token2', 'link' => 'token3'],
|
||||
],
|
||||
SmsLog::TAG_NOTIFICATION_MOBILE => [
|
||||
'title' => 'تأیید شماره اعلان',
|
||||
'body' => "کد تأیید شماره اعلان شما: {code}\nاعتبار: ۵ دقیقه",
|
||||
'variables' => ['code'],
|
||||
'kavenegar_template' => 'clinicpro-notify-code',
|
||||
'token_map' => ['code' => 'token'],
|
||||
],
|
||||
SmsLog::TAG_SECRETARY => [
|
||||
'title' => 'دعوت بهعنوان منشی',
|
||||
'body' => "شما بهعنوان منشیِ {owner} در کلینیکپرو تعریف شدید.\nشمارهکاربری: {username}\nلینک ورود: {link}",
|
||||
'variables' => ['owner', 'username', 'link'],
|
||||
'kavenegar_template' => 'clinicpro-secretary',
|
||||
'token_map' => ['owner' => 'token10', 'username' => 'token', 'link' => 'token3'],
|
||||
],
|
||||
SmsLog::TAG_DOCTOR_APPOINTMENT => [
|
||||
'title' => 'نوبت جدید (اعلان به پزشک)',
|
||||
'body' => "نوبت جدید ثبت شد.\nبیمار: {patient}\nتاریخ: {date} ساعت {time}\n\nبرای مدیریت نوبتهای خود بهصورت رایگان به سایت clinic-pro.ir مراجعه کنید.\nکلینیک پرو",
|
||||
'variables' => ['patient', 'date', 'time'],
|
||||
'kavenegar_template' => 'clinicpro-doctor-appt',
|
||||
'token_map' => ['patient' => 'token10', 'date' => 'token2', 'time' => 'token3'],
|
||||
],
|
||||
SmsLog::TAG_WELCOME => [
|
||||
'title' => 'خوشآمدگویی',
|
||||
'body' => '{name} عزیز، به {site} خوش آمدید. پروفایل شما توسط نماینده ثبت شد.',
|
||||
'variables' => ['name', 'site'],
|
||||
'kavenegar_template' => 'clinicpro-welcome',
|
||||
'token_map' => ['name' => 'token10', 'site' => 'token20'],
|
||||
],
|
||||
];
|
||||
|
||||
@@ -72,36 +95,56 @@ class SmsMessageTemplate
|
||||
#[ORM\Column(type: 'json')]
|
||||
private array $variables = [];
|
||||
|
||||
#[ORM\Column(name: 'kavenegar_template', type: 'string', length: 100, nullable: true)]
|
||||
private ?string $kavenegarTemplate = null;
|
||||
|
||||
#[ORM\Column(name: 'token_map', type: 'json')]
|
||||
private array $tokenMap = [];
|
||||
|
||||
#[ORM\Column(name: 'updated_at', type: 'integer')]
|
||||
private int $updatedAt;
|
||||
|
||||
public function __construct(string $tag, string $title, string $body, array $variables = [])
|
||||
{
|
||||
$this->tag = $tag;
|
||||
$this->title = $title;
|
||||
$this->body = $body;
|
||||
$this->variables = $variables;
|
||||
$this->updatedAt = time();
|
||||
public function __construct(
|
||||
string $tag,
|
||||
string $title,
|
||||
string $body,
|
||||
array $variables = [],
|
||||
?string $kavenegarTemplate = null,
|
||||
array $tokenMap = [],
|
||||
) {
|
||||
$this->tag = $tag;
|
||||
$this->title = $title;
|
||||
$this->body = $body;
|
||||
$this->variables = $variables;
|
||||
$this->kavenegarTemplate = $kavenegarTemplate;
|
||||
$this->tokenMap = $tokenMap;
|
||||
$this->updatedAt = time();
|
||||
}
|
||||
|
||||
public function getId(): ?int { return $this->id; }
|
||||
public function getTag(): string { return $this->tag; }
|
||||
public function getTitle(): string { return $this->title; }
|
||||
public function getBody(): string { return $this->body; }
|
||||
public function getVariables(): array { return $this->variables; }
|
||||
public function getUpdatedAt(): int { return $this->updatedAt; }
|
||||
public function getId(): ?int { return $this->id; }
|
||||
public function getTag(): string { return $this->tag; }
|
||||
public function getTitle(): string { return $this->title; }
|
||||
public function getBody(): string { return $this->body; }
|
||||
public function getVariables(): array { return $this->variables; }
|
||||
public function getKavenegarTemplate(): ?string { return $this->kavenegarTemplate; }
|
||||
public function getTokenMap(): array { return $this->tokenMap; }
|
||||
public function getUpdatedAt(): int { return $this->updatedAt; }
|
||||
|
||||
public function setTitle(string $v): self { $this->title = $v; $this->updatedAt = time(); return $this; }
|
||||
public function setBody(string $v): self { $this->body = $v; $this->updatedAt = time(); return $this; }
|
||||
public function setKavenegarTemplate(?string $v): self { $this->kavenegarTemplate = $v; $this->updatedAt = time(); return $this; }
|
||||
public function setTokenMap(array $v): self { $this->tokenMap = $v; $this->updatedAt = time(); return $this; }
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'tag' => $this->tag,
|
||||
'title' => $this->title,
|
||||
'body' => $this->body,
|
||||
'variables' => $this->variables,
|
||||
'updated_at' => $this->updatedAt,
|
||||
'tag' => $this->tag,
|
||||
'title' => $this->title,
|
||||
'body' => $this->body,
|
||||
'variables' => $this->variables,
|
||||
'kavenegar_template' => $this->kavenegarTemplate,
|
||||
'token_map' => $this->tokenMap,
|
||||
'updated_at' => $this->updatedAt,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
namespace App\Sms\Service;
|
||||
|
||||
use App\Sms\Entity\SmsLog;
|
||||
use App\Sms\Entity\SmsMessageTemplate;
|
||||
use App\Sms\Message\SendSmsMessage;
|
||||
use App\Sms\Provider\KavehNegarProvider;
|
||||
use App\Sms\Provider\RanginehProvider;
|
||||
use App\Sms\Provider\SmsProviderInterface;
|
||||
use App\Sms\Repository\SmsLogRepository;
|
||||
use App\Sms\Repository\SmsMessageTemplateRepository;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
|
||||
class SmsService
|
||||
@@ -15,10 +17,12 @@ class SmsService
|
||||
private array $providers;
|
||||
|
||||
public function __construct(
|
||||
private readonly KavehNegarProvider $kavenegar,
|
||||
private readonly RanginehProvider $rangineh,
|
||||
private readonly SmsLogRepository $logRepo,
|
||||
private readonly MessageBusInterface $bus,
|
||||
private readonly KavehNegarProvider $kavenegar,
|
||||
private readonly RanginehProvider $rangineh,
|
||||
private readonly SmsLogRepository $logRepo,
|
||||
private readonly MessageBusInterface $bus,
|
||||
private readonly SmsMessageTemplateRepository $messageTemplateRepo,
|
||||
private readonly SmsTextResolver $textResolver,
|
||||
) {
|
||||
$this->providers = [
|
||||
'kavenegar' => $kavenegar,
|
||||
@@ -26,6 +30,37 @@ class SmsService
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* ارسال یک پیامک سیستمی از طریق تمپلت VerifyLookup کاوهنگار.
|
||||
* فقط تگ + متغیرهای منطقی میگیرد؛ نام تمپلت و نگاشت token از SmsMessageTemplate (یا DEFAULTS) خوانده میشود.
|
||||
*
|
||||
* @param array<string,string|int> $vars متغیرهای منطقی (کلیدهای {...} بدنه)
|
||||
*/
|
||||
public function dispatchTemplate(string $tag, string $mobile, array $vars = [], string $provider = 'kavenegar'): void
|
||||
{
|
||||
$tpl = $this->messageTemplateRepo->findByTag($tag);
|
||||
$kaveTemplate = $tpl?->getKavenegarTemplate()
|
||||
?? (SmsMessageTemplate::DEFAULTS[$tag]['kavenegar_template'] ?? null);
|
||||
$tokenMap = $tpl?->getTokenMap()
|
||||
?: (SmsMessageTemplate::DEFAULTS[$tag]['token_map'] ?? []);
|
||||
|
||||
$message = $this->textResolver->resolve($tag, $vars);
|
||||
|
||||
if ($kaveTemplate === null || $tokenMap === []) {
|
||||
$this->dispatchAsync($mobile, $message, $provider, tag: $tag);
|
||||
return;
|
||||
}
|
||||
|
||||
$slotVars = [];
|
||||
foreach ($tokenMap as $logicalKey => $slot) {
|
||||
if (array_key_exists($logicalKey, $vars)) {
|
||||
$slotVars[$slot] = (string) $vars[$logicalKey];
|
||||
}
|
||||
}
|
||||
|
||||
$this->dispatchAsync($mobile, $message, $provider, templateVars: $slotVars, templateCode: $kaveTemplate, tag: $tag);
|
||||
}
|
||||
|
||||
public function dispatchAsync(
|
||||
string $mobile,
|
||||
string $message,
|
||||
|
||||
Reference in New Issue
Block a user