import { Injectable } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; const DEFAULT_CARRIER_RULE_CACHE_TTL_MS = 30_000; interface CompiledCarrierRule { carrier: string; pattern: RegExp; } @Injectable() export class PhoneRoutingLookupService { private carrierRuleCache?: { expiresAt: number; rules: CompiledCarrierRule[] }; private carrierRuleLoad?: { generation: number; promise: Promise }; private carrierRuleGeneration = 0; constructor(private readonly prisma: PrismaService) {} async identifyCarrier(phoneNumber: string) { const rules = await this.getCarrierRules(); return rules.find((rule) => rule.pattern.test(phoneNumber))?.carrier; } async identifyProvince(phoneNumber: string) { return (await this.identifyProvinces([phoneNumber])).get(phoneNumber) ?? null; } async identifyProvinces(phoneNumbers: string[]) { const uniquePhones = [...new Set(phoneNumbers)]; const prefixesByPhone = new Map(uniquePhones.map((phone) => [phone, phonePrefixes(phone)])); const prefixes = [...new Set([...prefixesByPhone.values()].flat())]; if (prefixes.length === 0) return new Map(uniquePhones.map((phone) => [phone, null])); const segments = await this.prisma.phoneSegment.findMany({ where: { prefix: { in: prefixes } }, select: { prefix: true, province: true }, }); const provinceByPrefix = new Map(segments.map((segment) => [segment.prefix, segment.province])); return new Map(uniquePhones.map((phone) => { const province = (prefixesByPhone.get(phone) ?? []) .map((prefix) => provinceByPrefix.get(prefix)) .find((value): value is string => Boolean(value)) ?? null; return [phone, province]; })); } invalidateCarrierRules() { this.carrierRuleGeneration += 1; this.carrierRuleCache = undefined; } private async getCarrierRules() { const now = Date.now(); if (this.carrierRuleCache && this.carrierRuleCache.expiresAt > now) { return this.carrierRuleCache.rules; } const generation = this.carrierRuleGeneration; if (this.carrierRuleLoad?.generation === generation) return this.carrierRuleLoad.promise; const promise = this.loadCarrierRules(generation); this.carrierRuleLoad = { generation, promise }; try { return await promise; } finally { if (this.carrierRuleLoad?.promise === promise) { this.carrierRuleLoad = undefined; } } } private async loadCarrierRules(generation: number) { const rows = await this.prisma.phoneCarrierRule.findMany({ where: { status: 'active' }, orderBy: [{ priority: 'asc' }, { createdAt: 'asc' }], select: { carrier: true, pattern: true }, }); const rules = rows.flatMap((row) => { try { return [{ carrier: row.carrier, pattern: new RegExp(row.pattern) }]; } catch { return []; } }); if (generation === this.carrierRuleGeneration) { this.carrierRuleCache = { expiresAt: Date.now() + carrierRuleCacheTtlMs(), rules, }; } return rules; } } function phonePrefixes(phoneNumber: string) { const maxLength = Math.min(7, phoneNumber.length); return Array.from({ length: Math.max(0, maxLength - 2) }, (_, index) => phoneNumber.slice(0, maxLength - index)); } function carrierRuleCacheTtlMs() { const configured = Number(process.env.PHONE_CARRIER_RULE_CACHE_TTL_MS); return Number.isFinite(configured) && configured > 0 ? Math.floor(configured) : DEFAULT_CARRIER_RULE_CACHE_TTL_MS; }