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:
hamed
2026-07-05 11:20:53 +03:30
parent 828e3552c0
commit 969dc9651f
17 changed files with 480 additions and 102 deletions
+23 -7
View File
@@ -60,6 +60,7 @@ export default function SmsPage() {
const [viewLog, setViewLog] = useState<SmsLog | null>(null);
const [editMsg, setEditMsg] = useState<SmsMessageText | null>(null);
const [editBody, setEditBody] = useState('');
const [editKaveName, setEditKaveName] = useState('');
const [editTemplate, setEditTemplate] = useState<SmsTemplate | null>(null);
const [editTplName, setEditTplName] = useState('');
const [editTplBody, setEditTplBody] = useState('');
@@ -98,8 +99,8 @@ export default function SmsPage() {
});
const updateMessageMut = useMutation({
mutationFn: ({ tag, body }: { tag: string; body: string }) =>
api.patch<ApiResponse<SmsMessageText>>(`/api/v1/admin/sms/messages/${tag}`, { body }),
mutationFn: ({ tag, body, kavenegarTemplate }: { tag: string; body: string; kavenegarTemplate: string }) =>
api.patch<ApiResponse<SmsMessageText>>(`/api/v1/admin/sms/messages/${tag}`, { body, kavenegar_template: kavenegarTemplate }),
onSuccess: () => {
toast.success('متن پیامک به‌روزرسانی شد');
setEditMsg(null);
@@ -485,11 +486,17 @@ export default function SmsPage() {
<div key={m.tag} className="card card-pad" style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<b style={{ fontSize: 14 }}>{m.title}</b>
<button className="btn ghost sm" onClick={() => { setEditMsg(m); setEditBody(m.body); }}>
<button className="btn ghost sm" onClick={() => { setEditMsg(m); setEditBody(m.body); setEditKaveName(m.kavenegar_template ?? ''); }}>
<PencilIcon style={{ width: 14, height: 14 }} /> ویرایش
</button>
</div>
<div className="muted" style={{ fontSize: 13, lineHeight: 1.9, whiteSpace: 'pre-wrap', direction: 'rtl' }}>{m.body}</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span className="muted" style={{ fontSize: 12 }}>تمپلت کاوهنگار:</span>
<span className={`badge ${m.kavenegar_template ? 'green' : 'gray'}`} dir="ltr" style={{ fontSize: 11 }}>
{m.kavenegar_template || 'تعریف‌نشده'}
</span>
</div>
{m.variables.length > 0 && (
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
{m.variables.map((v) => <span key={v} className="chip" dir="ltr">{`{${v}}`}</span>)}
@@ -513,7 +520,7 @@ export default function SmsPage() {
<button
className="btn primary sm"
disabled={updateMessageMut.isPending || !editBody.trim()}
onClick={() => editMsg && updateMessageMut.mutate({ tag: editMsg.tag, body: editBody })}
onClick={() => editMsg && updateMessageMut.mutate({ tag: editMsg.tag, body: editBody, kavenegarTemplate: editKaveName })}
>
{updateMessageMut.isPending ? 'در حال ذخیره...' : 'ذخیره'}
</button>
@@ -521,13 +528,22 @@ export default function SmsPage() {
}
>
<div className="form-row">
<label>متن پیامک</label>
<label>متن پیامک (پیشنمایش/لاگ)</label>
<textarea className="input" rows={5} dir="rtl" value={editBody} onChange={(e) => setEditBody(e.target.value)} />
</div>
<div className="form-row">
<label>نام تمپلت کاوهنگار (VerifyLookup)</label>
<input className="input" dir="ltr" value={editKaveName} onChange={(e) => setEditKaveName(e.target.value)} placeholder="clinicpro-otp" />
<span className="muted" style={{ fontSize: 11, marginTop: 4 }}>
این نام باید دقیقاً با تمپلت تأییدشده در پنل کاوهنگار یکی باشد. متن واقعی پیامک از پنل کاوهنگار ارسال میشود، نه از این متن.
</span>
</div>
{editMsg && editMsg.variables.length > 0 && (
<div style={{ marginTop: 10, display: 'flex', gap: 6, flexWrap: 'wrap', alignItems: 'center' }}>
<span className="muted" style={{ fontSize: 12 }}>متغیرهای مجاز:</span>
{editMsg.variables.map((v) => <span key={v} className="chip" dir="ltr">{`{${v}}`}</span>)}
<span className="muted" style={{ fontSize: 12 }}>متغیرها token:</span>
{editMsg.variables.map((v) => (
<span key={v} className="chip" dir="ltr">{`{${v}}`}{editMsg.token_map?.[v] ? ` → %${editMsg.token_map[v]}` : ''}</span>
))}
</div>
)}
</Modal>
+2
View File
@@ -220,6 +220,8 @@ export interface SmsMessageText {
title: string;
body: string;
variables: string[];
kavenegar_template: string | null;
token_map: Record<string, string>;
updated_at: number | null;
}