104 lines
3.5 KiB
TypeScript
104 lines
3.5 KiB
TypeScript
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<CompiledCarrierRule[]> };
|
|
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;
|
|
}
|