Bill SMS in credits and add IRT top-up in Super Admin.
Show credit balance in business settings; Super Admin tops up by IRT (adds credits) and tracks SMS sent per business. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
a8676854fb
commit
6bc57f26ed
@@ -8,7 +8,6 @@ import { Tooltip } from './Tooltip'
|
|||||||
import { useToast } from '../context/ToastContext'
|
import { useToast } from '../context/ToastContext'
|
||||||
import { useT } from '../i18n/useT'
|
import { useT } from '../i18n/useT'
|
||||||
import { ApiError, isAbortError } from '../lib/api'
|
import { ApiError, isAbortError } from '../lib/api'
|
||||||
import { formatIrtPrice } from '../utils/irtPrice'
|
|
||||||
import {
|
import {
|
||||||
createSmsTemplate,
|
createSmsTemplate,
|
||||||
deleteSmsTemplate,
|
deleteSmsTemplate,
|
||||||
@@ -26,7 +25,7 @@ import settingsStyles from '../pages/SettingsPage.module.css'
|
|||||||
import modalStyles from './VariationsModal.module.css'
|
import modalStyles from './VariationsModal.module.css'
|
||||||
|
|
||||||
const SMS_TEMPLATE_VARS = ['{{user.name}}', '{{user.lastname}}', '{{user.title}}'] as const
|
const SMS_TEMPLATE_VARS = ['{{user.name}}', '{{user.lastname}}', '{{user.title}}'] as const
|
||||||
const LOW_BALANCE_IRT = 200_000
|
const LOW_BALANCE_CREDITS = 400
|
||||||
const ANIMATION_MS = 220
|
const ANIMATION_MS = 220
|
||||||
const MAX_BODY_LEN = 700
|
const MAX_BODY_LEN = 700
|
||||||
|
|
||||||
@@ -131,7 +130,7 @@ export function SmsSettingsSection() {
|
|||||||
}, [listMounted, editorMounted, editorOpen, listOpen, submitting, removeTarget])
|
}, [listMounted, editorMounted, editorOpen, listOpen, submitting, removeTarget])
|
||||||
|
|
||||||
const verifiedCount = templates.filter((item) => item.isVerified).length
|
const verifiedCount = templates.filter((item) => item.isVerified).length
|
||||||
const isLowBalance = balance < LOW_BALANCE_IRT
|
const isLowBalance = balance < LOW_BALANCE_CREDITS
|
||||||
|
|
||||||
function openCreate() {
|
function openCreate() {
|
||||||
setEditing(null)
|
setEditing(null)
|
||||||
@@ -265,7 +264,11 @@ export function SmsSettingsSection() {
|
|||||||
>
|
>
|
||||||
<span className={settingsStyles.smsCreditLabel}>{t('settings.sms.balance')}</span>
|
<span className={settingsStyles.smsCreditLabel}>{t('settings.sms.balance')}</span>
|
||||||
<strong className={settingsStyles.smsCreditValue}>
|
<strong className={settingsStyles.smsCreditValue}>
|
||||||
{loading ? '…' : formatIrtPrice(balance)}
|
{loading
|
||||||
|
? '…'
|
||||||
|
: t('settings.sms.balanceValue', {
|
||||||
|
count: balance.toLocaleString(isFa ? 'fa-IR' : 'en-US'),
|
||||||
|
})}
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1694,9 +1694,11 @@ const en = {
|
|||||||
'settings.sms.title': 'SMS settings',
|
'settings.sms.title': 'SMS settings',
|
||||||
'settings.sms.desc':
|
'settings.sms.desc':
|
||||||
'View your prepaid SMS balance and manage message templates. Sender numbers are managed by Meshkee.',
|
'View your prepaid SMS balance and manage message templates. Sender numbers are managed by Meshkee.',
|
||||||
'settings.sms.balance': 'SMS balance',
|
'settings.sms.balance': 'SMS credits',
|
||||||
|
'settings.sms.balanceValue': '{count} SMS',
|
||||||
'settings.sms.balanceHint':
|
'settings.sms.balanceHint':
|
||||||
'Balance is in IRT. Each 64 characters costs 400 IRT per recipient (login, orders, customer SMS, etc.). Contact Meshkee to top up.',
|
'Balance is SMS credits. Each 64 characters costs 1 credit per recipient (login, orders, customer SMS, etc.). Contact Meshkee to top up.',
|
||||||
|
'settings.sms.sentCount': 'SMS sent',
|
||||||
'settings.sms.credit': 'Account credit',
|
'settings.sms.credit': 'Account credit',
|
||||||
'settings.sms.creditUnavailable': 'Unavailable',
|
'settings.sms.creditUnavailable': 'Unavailable',
|
||||||
'settings.sms.templates': 'Templates',
|
'settings.sms.templates': 'Templates',
|
||||||
@@ -4157,8 +4159,10 @@ const fa: Record<MessageKey, string> = {
|
|||||||
'settings.sms.desc':
|
'settings.sms.desc':
|
||||||
'اعتبار پیشپرداخت پیامک و قالبهای پیام را ببینید. شماره فرستنده توسط مشکی مدیریت میشود.',
|
'اعتبار پیشپرداخت پیامک و قالبهای پیام را ببینید. شماره فرستنده توسط مشکی مدیریت میشود.',
|
||||||
'settings.sms.balance': 'اعتبار پیامک',
|
'settings.sms.balance': 'اعتبار پیامک',
|
||||||
|
'settings.sms.balanceValue': '{count} پیامک',
|
||||||
'settings.sms.balanceHint':
|
'settings.sms.balanceHint':
|
||||||
'اعتبار به تومان (IRT) است. هر ۶۴ کاراکتر برای هر گیرنده ۴۰۰ تومان هزینه دارد (ورود، سفارش، پیامک مشتری و غیره). برای افزایش با مشکی تماس بگیرید.',
|
'اعتبار به واحد پیامک است. هر ۶۴ کاراکتر برای هر گیرنده ۱ اعتبار مصرف میکند (ورود، سفارش، پیامک مشتری و غیره). برای افزایش با مشکی تماس بگیرید.',
|
||||||
|
'settings.sms.sentCount': 'پیامک ارسالشده',
|
||||||
'settings.sms.credit': 'اعتبار حساب',
|
'settings.sms.credit': 'اعتبار حساب',
|
||||||
'settings.sms.creditUnavailable': 'در دسترس نیست',
|
'settings.sms.creditUnavailable': 'در دسترس نیست',
|
||||||
'settings.sms.templates': 'قالبها',
|
'settings.sms.templates': 'قالبها',
|
||||||
|
|||||||
@@ -69,6 +69,9 @@ export async function getSmsCredit(signal?: AbortSignal) {
|
|||||||
balance: number
|
balance: number
|
||||||
/** @deprecated use balance */
|
/** @deprecated use balance */
|
||||||
credit: number | null
|
credit: number | null
|
||||||
|
/** Lifetime SMS messages billed (one per recipient). */
|
||||||
|
sentCount: number
|
||||||
|
unit: 'credits'
|
||||||
currency: string | null
|
currency: string | null
|
||||||
message: string | null
|
message: string | null
|
||||||
}>(smsPath('/credit'), { auth: true, signal })
|
}>(smsPath('/credit'), { auth: true, signal })
|
||||||
|
|||||||
@@ -30,9 +30,11 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Equal width fields; same 12px gap as field→action buttons */
|
/* Equal width fields; same 12px gap as field→action buttons.
|
||||||
|
Cap growth so a single filter (e.g. Websites Name) stays compact. */
|
||||||
.filtersInputs > .field {
|
.filtersInputs > .field {
|
||||||
flex: 1 1 160px;
|
flex: 1 1 160px;
|
||||||
|
max-width: 280px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,6 +80,14 @@
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.smsTopUpCredits {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
.entityGroupLabel {
|
.entityGroupLabel {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -440,6 +450,21 @@
|
|||||||
padding-right: 8px;
|
padding-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.thSms,
|
||||||
|
.tdSms {
|
||||||
|
width: 1%;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: center;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tdSms {
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 8px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
.thModules,
|
.thModules,
|
||||||
.tdModules {
|
.tdModules {
|
||||||
width: 1%;
|
width: 1%;
|
||||||
@@ -1051,6 +1076,7 @@
|
|||||||
|
|
||||||
.filtersInputs > .field {
|
.filtersInputs > .field {
|
||||||
flex-basis: 100%;
|
flex-basis: 100%;
|
||||||
|
max-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filterActions {
|
.filterActions {
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ import {
|
|||||||
type HomeChartId,
|
type HomeChartId,
|
||||||
} from '../utils/businessModules'
|
} from '../utils/businessModules'
|
||||||
import { flattenBusinessCategories } from '../utils/categories'
|
import { flattenBusinessCategories } from '../utils/categories'
|
||||||
|
import { formatIrtInput, parseIrtInput } from '../utils/irtPrice'
|
||||||
import { Pagination } from '@meshkee/dashboard-ui'
|
import { Pagination } from '@meshkee/dashboard-ui'
|
||||||
import pageStyles from '../components/PageContent.module.css'
|
import pageStyles from '../components/PageContent.module.css'
|
||||||
import styles from './BusinessesPage.module.css'
|
import styles from './BusinessesPage.module.css'
|
||||||
@@ -345,7 +346,7 @@ export function BusinessesPage() {
|
|||||||
const [smsSettingsOpen, setSmsSettingsOpen] = useState(false)
|
const [smsSettingsOpen, setSmsSettingsOpen] = useState(false)
|
||||||
const [smsSettingsBusiness, setSmsSettingsBusiness] = useState<BusinessListItem | null>(null)
|
const [smsSettingsBusiness, setSmsSettingsBusiness] = useState<BusinessListItem | null>(null)
|
||||||
const [smsSenderNumber, setSmsSenderNumber] = useState('')
|
const [smsSenderNumber, setSmsSenderNumber] = useState('')
|
||||||
const [smsBalance, setSmsBalance] = useState('0')
|
const [smsTopUpIrt, setSmsTopUpIrt] = useState('')
|
||||||
const [smsSettingsSubmitting, setSmsSettingsSubmitting] = useState(false)
|
const [smsSettingsSubmitting, setSmsSettingsSubmitting] = useState(false)
|
||||||
const [smsSettingsError, setSmsSettingsError] = useState('')
|
const [smsSettingsError, setSmsSettingsError] = useState('')
|
||||||
|
|
||||||
@@ -534,7 +535,7 @@ export function BusinessesPage() {
|
|||||||
function openSmsSettings(b: BusinessListItem) {
|
function openSmsSettings(b: BusinessListItem) {
|
||||||
setSmsSettingsBusiness(b)
|
setSmsSettingsBusiness(b)
|
||||||
setSmsSenderNumber(b.smsSenderNumber ?? '')
|
setSmsSenderNumber(b.smsSenderNumber ?? '')
|
||||||
setSmsBalance(String(b.smsBalance ?? 0))
|
setSmsTopUpIrt('')
|
||||||
setSmsSettingsError('')
|
setSmsSettingsError('')
|
||||||
setSmsSettingsOpen(true)
|
setSmsSettingsOpen(true)
|
||||||
}
|
}
|
||||||
@@ -917,18 +918,28 @@ export function BusinessesPage() {
|
|||||||
if (!smsSettingsBusiness) return
|
if (!smsSettingsBusiness) return
|
||||||
|
|
||||||
const sender = smsSenderNumber.replace(/\D/g, '')
|
const sender = smsSenderNumber.replace(/\D/g, '')
|
||||||
const balanceParsed = Number.parseInt(smsBalance.replace(/\D/g, '') || '0', 10)
|
const topUpParsed = parseIrtInput(smsTopUpIrt) ?? undefined
|
||||||
if (!Number.isFinite(balanceParsed) || balanceParsed < 0) {
|
if (
|
||||||
setSmsSettingsError('SMS balance must be a non-negative integer.')
|
topUpParsed !== undefined &&
|
||||||
|
(!Number.isFinite(topUpParsed) || topUpParsed < 1)
|
||||||
|
) {
|
||||||
|
setSmsSettingsError('Top-up amount must be a positive integer (IRT).')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (topUpParsed !== undefined && topUpParsed < 500) {
|
||||||
|
setSmsSettingsError('Minimum top-up is 500 IRT (1 SMS credit).')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const creditsAdded =
|
||||||
|
topUpParsed !== undefined ? Math.floor(topUpParsed / 500) : 0
|
||||||
|
|
||||||
setSmsSettingsSubmitting(true)
|
setSmsSettingsSubmitting(true)
|
||||||
setSmsSettingsError('')
|
setSmsSettingsError('')
|
||||||
try {
|
try {
|
||||||
await updateBusiness(smsSettingsBusiness.id, {
|
await updateBusiness(smsSettingsBusiness.id, {
|
||||||
smsSenderNumber: sender || null,
|
smsSenderNumber: sender || null,
|
||||||
smsBalance: balanceParsed,
|
...(topUpParsed !== undefined ? { smsTopUpIrt: topUpParsed } : {}),
|
||||||
})
|
})
|
||||||
setData((prev) => {
|
setData((prev) => {
|
||||||
if (!prev) return prev
|
if (!prev) return prev
|
||||||
@@ -939,7 +950,7 @@ export function BusinessesPage() {
|
|||||||
? {
|
? {
|
||||||
...item,
|
...item,
|
||||||
smsSenderNumber: sender || null,
|
smsSenderNumber: sender || null,
|
||||||
smsBalance: balanceParsed,
|
smsBalance: (item.smsBalance ?? 0) + creditsAdded,
|
||||||
}
|
}
|
||||||
: item,
|
: item,
|
||||||
),
|
),
|
||||||
@@ -947,7 +958,12 @@ export function BusinessesPage() {
|
|||||||
})
|
})
|
||||||
setSmsSettingsOpen(false)
|
setSmsSettingsOpen(false)
|
||||||
setSmsSettingsBusiness(null)
|
setSmsSettingsBusiness(null)
|
||||||
showToast('SMS settings saved.', 'success')
|
showToast(
|
||||||
|
creditsAdded > 0
|
||||||
|
? `SMS settings saved. Added ${creditsAdded.toLocaleString('en-US')} credits.`
|
||||||
|
: 'SMS settings saved.',
|
||||||
|
'success',
|
||||||
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setSmsSettingsError(
|
setSmsSettingsError(
|
||||||
err instanceof ApiError ? err.message : 'Unable to save SMS settings.',
|
err instanceof ApiError ? err.message : 'Unable to save SMS settings.',
|
||||||
@@ -1546,6 +1562,7 @@ export function BusinessesPage() {
|
|||||||
<th className={styles.th}>Contract Data / Renewal</th>
|
<th className={styles.th}>Contract Data / Renewal</th>
|
||||||
<th className={styles.th}>Domain</th>
|
<th className={styles.th}>Domain</th>
|
||||||
<th className={styles.th}>Owner</th>
|
<th className={styles.th}>Owner</th>
|
||||||
|
<th className={`${styles.th} ${styles.thSms}`}>SMS sent</th>
|
||||||
<th className={`${styles.th} ${styles.thTheme}`}>Theme</th>
|
<th className={`${styles.th} ${styles.thTheme}`}>Theme</th>
|
||||||
<th className={`${styles.th} ${styles.thLocale}`}>Lang</th>
|
<th className={`${styles.th} ${styles.thLocale}`}>Lang</th>
|
||||||
<th className={`${styles.th} ${styles.thModules}`}>Modules</th>
|
<th className={`${styles.th} ${styles.thModules}`}>Modules</th>
|
||||||
@@ -1555,7 +1572,7 @@ export function BusinessesPage() {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{loading && (
|
{loading && (
|
||||||
<tr>
|
<tr>
|
||||||
<td className={styles.td} colSpan={8}>
|
<td className={styles.td} colSpan={9}>
|
||||||
Loading...
|
Loading...
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1563,7 +1580,7 @@ export function BusinessesPage() {
|
|||||||
|
|
||||||
{!loading && data?.items?.length === 0 && (
|
{!loading && data?.items?.length === 0 && (
|
||||||
<tr>
|
<tr>
|
||||||
<td className={styles.td} colSpan={8}>
|
<td className={styles.td} colSpan={9}>
|
||||||
No results found.
|
No results found.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1654,6 +1671,9 @@ export function BusinessesPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td className={`${styles.td} ${styles.tdSms}`}>
|
||||||
|
{(b.smsSentCount ?? 0).toLocaleString('en-US')}
|
||||||
|
</td>
|
||||||
<td className={`${styles.td} ${styles.tdTheme}`}>
|
<td className={`${styles.td} ${styles.tdTheme}`}>
|
||||||
<PrimaryColorSwatchControl
|
<PrimaryColorSwatchControl
|
||||||
value={normalizeBusinessPrimaryColorId(b.primaryColor)}
|
value={normalizeBusinessPrimaryColorId(b.primaryColor)}
|
||||||
@@ -2088,20 +2108,64 @@ export function BusinessesPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.field}>
|
<div className={styles.field}>
|
||||||
<label htmlFor="sms-settings-balance">SMS balance (IRT)</label>
|
<label>Current SMS balance</label>
|
||||||
<input
|
<input
|
||||||
id="sms-settings-balance"
|
value={`${(smsSettingsBusiness?.smsBalance ?? 0).toLocaleString('en-US')} credits`}
|
||||||
|
readOnly
|
||||||
|
disabled
|
||||||
|
aria-label="Current SMS credit balance"
|
||||||
|
/>
|
||||||
|
<p className={styles.helperText}>
|
||||||
|
Prepaid SMS credits. 1 credit = one 64-character segment per recipient (OTP,
|
||||||
|
customer SMS, partner SMS). New businesses start with 1,000 credits. Values below
|
||||||
|
400 credits show as low balance in the business dashboard.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.field}>
|
||||||
|
<label htmlFor="sms-settings-topup">Add top-up (IRT)</label>
|
||||||
|
<input
|
||||||
|
id="sms-settings-topup"
|
||||||
|
type="text"
|
||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
value={smsBalance}
|
value={smsTopUpIrt}
|
||||||
onChange={(e) => setSmsBalance(e.target.value.replace(/\D/g, '').slice(0, 12))}
|
onChange={(e) => {
|
||||||
placeholder="0"
|
const digits = e.target.value.replace(/\D/g, '').slice(0, 12)
|
||||||
|
setSmsTopUpIrt(formatIrtInput(digits))
|
||||||
|
}}
|
||||||
|
placeholder="e.g. 1,000,000"
|
||||||
disabled={smsSettingsSubmitting}
|
disabled={smsSettingsSubmitting}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
|
{(() => {
|
||||||
|
const irt = parseIrtInput(smsTopUpIrt) ?? 0
|
||||||
|
const credits = Math.floor(irt / 500)
|
||||||
|
const current = smsSettingsBusiness?.smsBalance ?? 0
|
||||||
|
const next = current + credits
|
||||||
|
return (
|
||||||
|
<p className={styles.smsTopUpCredits} aria-live="polite">
|
||||||
|
{irt > 0
|
||||||
|
? `SMS credit charge: +${credits.toLocaleString('en-US')} → new balance ${next.toLocaleString('en-US')} (was ${current.toLocaleString('en-US')})`
|
||||||
|
: 'SMS credit charge: —'}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
<p className={styles.helperText}>
|
<p className={styles.helperText}>
|
||||||
Prepaid SMS balance in IRT. Billing: 400 IRT per 64 characters per recipient
|
Each SMS credit costs 500 IRT (1,000,000 → +2,000 credits). Top-ups are added to
|
||||||
(OTP, customer SMS, partner SMS). New businesses start with 500,000 IRT. Values
|
the current balance — they never reset it. Leave empty to only update the sender
|
||||||
below 200,000 IRT show as low balance in the business dashboard.
|
number.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.field}>
|
||||||
|
<label>SMS sent</label>
|
||||||
|
<input
|
||||||
|
value={(smsSettingsBusiness?.smsSentCount ?? 0).toLocaleString('en-US')}
|
||||||
|
readOnly
|
||||||
|
disabled
|
||||||
|
aria-label="SMS messages sent"
|
||||||
|
/>
|
||||||
|
<p className={styles.helperText}>
|
||||||
|
Lifetime messages billed for this business (one per recipient). Updated automatically
|
||||||
|
when SMS is charged or refunded.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ height: 12 }} />
|
<div style={{ height: 12 }} />
|
||||||
@@ -2607,8 +2671,8 @@ export function BusinessesPage() {
|
|||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
<p className={styles.helperText} style={{ marginTop: 0, marginBottom: 12 }}>
|
<p className={styles.helperText} style={{ marginTop: 0, marginBottom: 12 }}>
|
||||||
New businesses receive an initial SMS balance of 500,000 IRT (400 IRT per 64 characters
|
New businesses receive an initial SMS balance of 1,000 credits (1 credit = 64 characters
|
||||||
per recipient).
|
per recipient; top-ups cost 500 IRT per credit).
|
||||||
</p>
|
</p>
|
||||||
<div className={styles.formGrid}>
|
<div className={styles.formGrid}>
|
||||||
<div className={styles.field}>
|
<div className={styles.field}>
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ export interface UpdateBusinessPayload {
|
|||||||
oldBusinessId?: number | null
|
oldBusinessId?: number | null
|
||||||
annualRenewalAt?: string | null
|
annualRenewalAt?: string | null
|
||||||
smsSenderNumber?: string | null
|
smsSenderNumber?: string | null
|
||||||
smsBalance?: number
|
/** IRT amount to convert into SMS credits and add (floor(irt / 500)). */
|
||||||
|
smsTopUpIrt?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AddDomainPayload {
|
export interface AddDomainPayload {
|
||||||
|
|||||||
@@ -40,8 +40,10 @@ export interface BusinessListItem {
|
|||||||
oldBusinessId: string | null
|
oldBusinessId: string | null
|
||||||
/** Gama shortcode for customer SMS; null → Meshkee advertising number. */
|
/** Gama shortcode for customer SMS; null → Meshkee advertising number. */
|
||||||
smsSenderNumber: string | null
|
smsSenderNumber: string | null
|
||||||
/** Prepaid SMS balance in IRT (400 IRT per 64 chars / recipient). */
|
/** Prepaid SMS credit units (1 = 64 chars × recipient). Top-up at 500 IRT/credit. */
|
||||||
smsBalance: number
|
smsBalance: number
|
||||||
|
/** Lifetime SMS messages billed (one per recipient; decremented on refund). */
|
||||||
|
smsSentCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MigrateFromOldEntity =
|
export type MigrateFromOldEntity =
|
||||||
|
|||||||
@@ -319,11 +319,13 @@ Products accept optional `brandId` on create/update.
|
|||||||
- `POST /businesses/:businessId/customers/sms` — bulk SMS via Gama **SendBulk** (or per-recipient when template/vars); body `{ message, userIds?, templateId? }` — empty/omitted `userIds` → all enabled customers
|
- `POST /businesses/:businessId/customers/sms` — bulk SMS via Gama **SendBulk** (or per-recipient when template/vars); body `{ message, userIds?, templateId? }` — empty/omitted `userIds` → all enabled customers
|
||||||
- Free-text SMS source: `settings.sms.senderNumber` (Super Admin only) or advertising shortcode `5000990009`
|
- Free-text SMS source: `settings.sms.senderNumber` (Super Admin only) or advertising shortcode `5000990009`
|
||||||
- Verified SMS template → service shortcode `5000110005` (fixed); unverified template → `5000990009`
|
- Verified SMS template → service shortcode `5000110005` (fixed); unverified template → `5000990009`
|
||||||
- Per-business prepaid `businesses.sms_balance` (IRT) — all Meshkee SMS (OTP login/forgot-password, customer SMS, partner/public SMS for linked domains) costs **400 IRT per 64 characters** per recipient (`ceil(len/64)*400`). Insufficient balance blocks send.
|
- Per-business prepaid `businesses.sms_balance` (**SMS credits**) — all Meshkee SMS (OTP login/forgot-password, customer SMS, partner/public SMS for linked domains) costs **1 credit per 64 characters** per recipient (`ceil(len/64)`). Insufficient balance blocks send.
|
||||||
- New businesses created in Super Admin start with **500,000 IRT** SMS balance
|
- Top-up (Super Admin): enter IRT amount → adds `floor(IRT / 500)` credits (e.g. 1,000,000 IRT → 2,000 credits)
|
||||||
|
- New businesses created in Super Admin start with **1,000** SMS credits
|
||||||
|
- `businesses.sms_sent_count` — lifetime messages billed (one per recipient)
|
||||||
- External domains (no Meshkee website) still bill via their business row once the domain is linked
|
- External domains (no Meshkee website) still bill via their business row once the domain is linked
|
||||||
- Business Settings → **SMS settings**: balance (read-only) + template CRUD
|
- Business Settings → **SMS settings**: credit balance (read-only) + template CRUD
|
||||||
- Super Admin business overflow → **SMS settings** modal: sender number + SMS balance top-up
|
- Super Admin business overflow → **SMS settings** modal: sender number + current credits + IRT top-up
|
||||||
- Super Admin → **SMS Templates** (`/sms-templates`): verify / unverify
|
- Super Admin → **SMS Templates** (`/sms-templates`): verify / unverify
|
||||||
- Online payment gateways live under **Website → Settings** (not main Settings)
|
- Online payment gateways live under **Website → Settings** (not main Settings)
|
||||||
- Template vars: `{{user.name}}`, `{{user.lastname}}`, `{{user.title}}`
|
- Template vars: `{{user.name}}`, `{{user.lastname}}`, `{{user.title}}`
|
||||||
|
|||||||
Reference in New Issue
Block a user