Add optional SMS auto-block threshold below balance.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `sms_accounts` ADD COLUMN `auto_block_below_rials` INTEGER NULL;
|
||||||
@@ -89,6 +89,8 @@ model SmsAccount {
|
|||||||
balanceRials Int @default(0) @map("balance_rials")
|
balanceRials Int @default(0) @map("balance_rials")
|
||||||
pricePerPartRials Int @default(2500) @map("price_per_part_rials")
|
pricePerPartRials Int @default(2500) @map("price_per_part_rials")
|
||||||
isBlocked Boolean @default(false) @map("is_blocked")
|
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
|
/// below this => red/critical
|
||||||
alertCriticalRials Int @default(10000) @map("alert_critical_rials")
|
alertCriticalRials Int @default(10000) @map("alert_critical_rials")
|
||||||
/// at/below this (and above critical) => yellow; above => green
|
/// at/below this (and above critical) => yellow; above => green
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
IsString,
|
IsString,
|
||||||
Max,
|
Max,
|
||||||
Min,
|
Min,
|
||||||
|
ValidateIf,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
import { Type, Transform } from 'class-transformer';
|
import { Type, Transform } from 'class-transformer';
|
||||||
|
|
||||||
@@ -19,6 +20,18 @@ export class UpdateSmsAccountDto {
|
|||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
isBlocked?: boolean;
|
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()
|
@IsOptional()
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@Min(0)
|
@Min(0)
|
||||||
|
|||||||
+30
-4
@@ -146,6 +146,10 @@ export class SmsService {
|
|||||||
data: {
|
data: {
|
||||||
pricePerPartRials: dto.pricePerPartRials ?? undefined,
|
pricePerPartRials: dto.pricePerPartRials ?? undefined,
|
||||||
isBlocked: dto.isBlocked ?? undefined,
|
isBlocked: dto.isBlocked ?? undefined,
|
||||||
|
autoBlockBelowRials:
|
||||||
|
dto.autoBlockBelowRials === undefined
|
||||||
|
? undefined
|
||||||
|
: dto.autoBlockBelowRials,
|
||||||
alertCriticalRials: dto.alertCriticalRials ?? undefined,
|
alertCriticalRials: dto.alertCriticalRials ?? undefined,
|
||||||
alertWarningRials: dto.alertWarningRials ?? undefined,
|
alertWarningRials: dto.alertWarningRials ?? undefined,
|
||||||
gatewayProvider: dto.gatewayProvider ?? undefined,
|
gatewayProvider: dto.gatewayProvider ?? undefined,
|
||||||
@@ -254,21 +258,25 @@ export class SmsService {
|
|||||||
let reason: string | null = null;
|
let reason: string | null = null;
|
||||||
let allowed = true;
|
let allowed = true;
|
||||||
|
|
||||||
// Only the explicit block switch stops sending (parity with Hamkar/Khanegi panels).
|
// Manual block switch, then optional auto-block threshold (empty = no auto block).
|
||||||
// Negative / low balance is allowed; UI can still show balanceLevel as warning.
|
// Alert SMS (isAlert) always bypasses both. Negative balance alone does not block.
|
||||||
if (account.isBlocked && !isAlert) {
|
if (account.isBlocked && !isAlert) {
|
||||||
allowed = false;
|
allowed = false;
|
||||||
reason = 'blocked';
|
reason = 'blocked';
|
||||||
|
} else if (this.isAutoBlocked(account) && !isAlert) {
|
||||||
|
allowed = false;
|
||||||
|
reason = 'auto_blocked';
|
||||||
} else if (!isAlert && account.balanceRials < estimatedCostRials) {
|
} else if (!isAlert && account.balanceRials < estimatedCostRials) {
|
||||||
reason = 'insufficient_balance';
|
reason = 'insufficient_balance';
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
allowed,
|
allowed,
|
||||||
blocked: account.isBlocked,
|
blocked: account.isBlocked || this.isAutoBlocked(account),
|
||||||
reason,
|
reason,
|
||||||
balanceRials: account.balanceRials,
|
balanceRials: account.balanceRials,
|
||||||
pricePerPartRials: account.pricePerPartRials,
|
pricePerPartRials: account.pricePerPartRials,
|
||||||
|
autoBlockBelowRials: account.autoBlockBelowRials,
|
||||||
parts: safeParts,
|
parts: safeParts,
|
||||||
estimatedCostRials,
|
estimatedCostRials,
|
||||||
balanceLevel,
|
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;
|
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 nextBalance = account.balanceRials - costRials;
|
||||||
|
|
||||||
const updated = await tx.smsAccount.update({
|
const updated = await tx.smsAccount.update({
|
||||||
@@ -869,6 +884,16 @@ export class SmsService {
|
|||||||
return `${new Intl.NumberFormat('fa-IR').format(toman)} تومان`;
|
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) {
|
private chargeInfoOf(account: SmsAccount) {
|
||||||
return {
|
return {
|
||||||
card: account.chargeCard || '',
|
card: account.chargeCard || '',
|
||||||
@@ -884,6 +909,7 @@ export class SmsService {
|
|||||||
balanceRials: account.balanceRials,
|
balanceRials: account.balanceRials,
|
||||||
pricePerPartRials: account.pricePerPartRials,
|
pricePerPartRials: account.pricePerPartRials,
|
||||||
isBlocked: account.isBlocked,
|
isBlocked: account.isBlocked,
|
||||||
|
autoBlockBelowRials: account.autoBlockBelowRials,
|
||||||
alertCriticalRials: account.alertCriticalRials,
|
alertCriticalRials: account.alertCriticalRials,
|
||||||
alertWarningRials: account.alertWarningRials,
|
alertWarningRials: account.alertWarningRials,
|
||||||
chargeCard: account.chargeCard,
|
chargeCard: account.chargeCard,
|
||||||
|
|||||||
Reference in New Issue
Block a user