Add optional SMS auto-block threshold below balance.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-08 14:12:29 +03:30
co-authored by Cursor
parent f9b77ceaac
commit 8eba9a9b8f
4 changed files with 47 additions and 4 deletions
+13
View File
@@ -6,6 +6,7 @@ import {
IsString,
Max,
Min,
ValidateIf,
} from 'class-validator';
import { Type, Transform } from 'class-transformer';
@@ -19,6 +20,18 @@ export class UpdateSmsAccountDto {
@IsBoolean()
isBlocked?: boolean;
/** Null clears auto-block; omit leaves unchanged. UI sends tomans×10 as rials. */
@IsOptional()
@Transform(({ value }) => {
if (value === null || value === '' || value === undefined) return value === undefined ? undefined : null;
const n = Number(value);
return Number.isFinite(n) ? Math.trunc(n) : value;
})
@ValidateIf((_, v) => v !== null && v !== undefined)
@IsInt()
@Min(0)
autoBlockBelowRials?: number | null;
@IsOptional()
@IsInt()
@Min(0)
+30 -4
View File
@@ -146,6 +146,10 @@ export class SmsService {
data: {
pricePerPartRials: dto.pricePerPartRials ?? undefined,
isBlocked: dto.isBlocked ?? undefined,
autoBlockBelowRials:
dto.autoBlockBelowRials === undefined
? undefined
: dto.autoBlockBelowRials,
alertCriticalRials: dto.alertCriticalRials ?? undefined,
alertWarningRials: dto.alertWarningRials ?? undefined,
gatewayProvider: dto.gatewayProvider ?? undefined,
@@ -254,21 +258,25 @@ export class SmsService {
let reason: string | null = null;
let allowed = true;
// Only the explicit block switch stops sending (parity with Hamkar/Khanegi panels).
// Negative / low balance is allowed; UI can still show balanceLevel as warning.
// Manual block switch, then optional auto-block threshold (empty = no auto block).
// Alert SMS (isAlert) always bypasses both. Negative balance alone does not block.
if (account.isBlocked && !isAlert) {
allowed = false;
reason = 'blocked';
} else if (this.isAutoBlocked(account) && !isAlert) {
allowed = false;
reason = 'auto_blocked';
} else if (!isAlert && account.balanceRials < estimatedCostRials) {
reason = 'insufficient_balance';
}
return {
allowed,
blocked: account.isBlocked,
blocked: account.isBlocked || this.isAutoBlocked(account),
reason,
balanceRials: account.balanceRials,
pricePerPartRials: account.pricePerPartRials,
autoBlockBelowRials: account.autoBlockBelowRials,
parts: safeParts,
estimatedCostRials,
balanceLevel,
@@ -303,9 +311,16 @@ export class SmsService {
});
}
if (this.isAutoBlocked(account) && !isAlert) {
throw new ForbiddenException({
message: 'ارسال پیامک به‌خاطر کمتر بودن موجودی از آستانه خودکار مسدود است',
reason: 'auto_blocked',
});
}
const costRials = parts * account.pricePerPartRials;
// Allow negative wallet (same as product panels): only isBlocked stops send/consume.
// Negative wallet still allowed unless manual/auto block applies.
const nextBalance = account.balanceRials - costRials;
const updated = await tx.smsAccount.update({
@@ -869,6 +884,16 @@ export class SmsService {
return `${new Intl.NumberFormat('fa-IR').format(toman)} تومان`;
}
private isAutoBlocked(account: {
balanceRials: number;
autoBlockBelowRials: number | null;
}) {
return (
account.autoBlockBelowRials != null &&
account.balanceRials < account.autoBlockBelowRials
);
}
private chargeInfoOf(account: SmsAccount) {
return {
card: account.chargeCard || '',
@@ -884,6 +909,7 @@ export class SmsService {
balanceRials: account.balanceRials,
pricePerPartRials: account.pricePerPartRials,
isBlocked: account.isBlocked,
autoBlockBelowRials: account.autoBlockBelowRials,
alertCriticalRials: account.alertCriticalRials,
alertWarningRials: account.alertWarningRials,
chargeCard: account.chargeCard,