diff --git a/prisma/migrations/20260808140000_sms_auto_block_below/migration.sql b/prisma/migrations/20260808140000_sms_auto_block_below/migration.sql new file mode 100644 index 0000000..6a52480 --- /dev/null +++ b/prisma/migrations/20260808140000_sms_auto_block_below/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE `sms_accounts` ADD COLUMN `auto_block_below_rials` INTEGER NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index db1596b..1be2c23 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -89,6 +89,8 @@ model SmsAccount { balanceRials Int @default(0) @map("balance_rials") pricePerPartRials Int @default(2500) @map("price_per_part_rials") isBlocked Boolean @default(false) @map("is_blocked") + /// If set: block non-alert SMS when balanceRials < this. Null = only manual isBlocked. + autoBlockBelowRials Int? @map("auto_block_below_rials") /// below this => red/critical alertCriticalRials Int @default(10000) @map("alert_critical_rials") /// at/below this (and above critical) => yellow; above => green diff --git a/src/sms/dto/sms-account.dto.ts b/src/sms/dto/sms-account.dto.ts index bb23125..33bdefa 100644 --- a/src/sms/dto/sms-account.dto.ts +++ b/src/sms/dto/sms-account.dto.ts @@ -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) diff --git a/src/sms/sms.service.ts b/src/sms/sms.service.ts index 97d1059..da8250a 100644 --- a/src/sms/sms.service.ts +++ b/src/sms/sms.service.ts @@ -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,