From ad9bca8e6cd5d64318e8f6b43e054bdf187148e5 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Thu, 9 Jul 2026 20:17:58 +0330 Subject: [PATCH] count character --- src/campaigns/campaign-automation.service.ts | 9 ++++- src/campaigns/campaigns.service.ts | 36 +++++++++++++++++++- src/customers/customers.service.ts | 4 ++- src/sms/helpers/sms-segment.helper.ts | 19 +++++++++++ 4 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 src/sms/helpers/sms-segment.helper.ts diff --git a/src/campaigns/campaign-automation.service.ts b/src/campaigns/campaign-automation.service.ts index 042fbe3..2d23ee0 100644 --- a/src/campaigns/campaign-automation.service.ts +++ b/src/campaigns/campaign-automation.service.ts @@ -9,6 +9,7 @@ import { CampaignTargetingService } from './campaign-targeting.service'; import { CampaignsService } from './campaigns.service'; import { CampaignTrigger } from './enums/campaign-trigger.enum'; import { CampaignType } from './enums/campaign-type.enum'; +import { getCampaignTypeDefinition } from './constants/campaign-types.constants'; @Injectable() export class CampaignAutomationService { @@ -114,7 +115,13 @@ export class CampaignAutomationService { } try { - await this.salonSubscriptionsService.consumeSms(salonId, targets.length); + const definition = getCampaignTypeDefinition(type); + const smsParts = await this.campaignsService.calculateCampaignSmsParts( + definition.scenarioKey, + salon, + targets, + ); + await this.salonSubscriptionsService.consumeSms(salonId, smsParts); } catch (error) { this.logger.warn( `Skipping ${type} campaign for salon ${salonId}: ${(error as Error).message}`, diff --git a/src/campaigns/campaigns.service.ts b/src/campaigns/campaigns.service.ts index acee9a9..e2a832b 100644 --- a/src/campaigns/campaigns.service.ts +++ b/src/campaigns/campaigns.service.ts @@ -12,6 +12,7 @@ import { Salon } from '../salons/entities/salon.entity'; import { SentMessage } from '../sent-messages/entities/sent-message.entity'; import { SentMessageStatus } from '../sent-messages/enums/sent-message-status.enum'; import { SmsDispatchService } from '../sms-settings/sms-dispatch.service'; +import { calculateSmsParts } from '../sms/helpers/sms-segment.helper'; import { SmsService } from '../sms/sms.service'; import { SalonSubscriptionsService } from '../subscriptions/salon-subscriptions.service'; import { UserRole } from '../users/enums/user-role.enum'; @@ -133,7 +134,12 @@ export class CampaignsService { ); } - await this.salonSubscriptionsService.consumeSms(salon.id, targets.length); + const smsParts = await this.calculateCampaignSmsParts( + definition.scenarioKey, + salon, + targets, + ); + await this.salonSubscriptionsService.consumeSms(salon.id, smsParts); const run = await this.runCampaignForSalon({ salon, @@ -211,6 +217,34 @@ export class CampaignsService { return targets.reduce((sum, target) => sum + target.estimatedRevenue, 0); } + async calculateCampaignSmsParts( + scenarioKey: (typeof CAMPAIGN_TYPES)[number]['scenarioKey'], + salon: Salon, + targets: CampaignTargetCustomer[], + ): Promise { + let totalParts = 0; + + for (const target of targets) { + const variables: Record = { + customerName: + `${target.firstName} ${target.lastName}`.trim() || 'مشتری', + salonName: salon.name, + ...target.meta, + }; + + const message = await this.smsDispatchService.resolveMessage( + scenarioKey, + variables, + ); + + if (message) { + totalParts += calculateSmsParts(message); + } + } + + return totalParts; + } + async findRuns( user: AuthUser, query: FindCampaignRunsQueryDto, diff --git a/src/customers/customers.service.ts b/src/customers/customers.service.ts index 87007d6..ae57e3e 100644 --- a/src/customers/customers.service.ts +++ b/src/customers/customers.service.ts @@ -13,6 +13,7 @@ import { Salon } from '../salons/entities/salon.entity'; import { SentMessage } from '../sent-messages/entities/sent-message.entity'; import { SentMessageStatus } from '../sent-messages/enums/sent-message-status.enum'; import { SmsService } from '../sms/sms.service'; +import { calculateSmsParts } from '../sms/helpers/sms-segment.helper'; import { SalonSubscriptionsService } from '../subscriptions/salon-subscriptions.service'; import { UserRole } from '../users/enums/user-role.enum'; import { UsersService } from '../users/users.service'; @@ -123,8 +124,9 @@ export class CustomersService { ): Promise { const salon = await this.resolveSalonForUser(user); const detail = await this.findOneForSalon(id, user); + const smsParts = calculateSmsParts(dto.body); - await this.salonSubscriptionsService.consumeSms(salon.id, 1); + await this.salonSubscriptionsService.consumeSms(salon.id, smsParts); const sent = await this.smsService.sendSms(detail.mobile, dto.body); const status = sent ? SentMessageStatus.SENT : SentMessageStatus.FAILED; diff --git a/src/sms/helpers/sms-segment.helper.ts b/src/sms/helpers/sms-segment.helper.ts new file mode 100644 index 0000000..2739a19 --- /dev/null +++ b/src/sms/helpers/sms-segment.helper.ts @@ -0,0 +1,19 @@ +const GSM_7_REGEX = + /^[\n\r !"#$%&'()*+,\-./0-9:;<=>?@A-Z\[\\\]^_`a-z{|}~€£¥èéùìòÇØøÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ]*$/; + +const ENGLISH_CHARS_PER_PART = 154; +const PERSIAN_CHARS_PER_PART = 64; + +/** Returns how many SMS units a message consumes (GSM-7 or Unicode/Persian rules). */ +export function calculateSmsParts(text: string): number { + const length = [...text].length; + if (length === 0) { + return 0; + } + + if (GSM_7_REGEX.test(text)) { + return Math.ceil(length / ENGLISH_CHARS_PER_PART); + } + + return Math.ceil(length / PERSIAN_CHARS_PER_PART); +}