feat(apps): add mexican phone number apps

This commit is contained in:
devlikepro committed 2026-09-17 13:36:13 +07:00
1 parent 0e73561b8e
commit a48247c2d2
14 files changed
+250 -4

No files matched your search

+3
View File
@@ -4,6 +4,7 @@ import { BrazilianPhoneNumbersAppConfig } from '@waha/apps/brazilian-phone-numbe
import { CallsAppConfig } from '@waha/apps/calls/dto/config.dto';
import { ChatWootAppConfig } from '@waha/apps/chatwoot/dto/config.dto';
import { McpAppConfig } from '@waha/apps/mcp/dto/config.dto';
import { MexicanPhoneNumbersAppConfig } from '@waha/apps/mexican-phone-numbers/dto';
import { PhoneNumbersAppConfig } from '@waha/apps/phone-numbers/dto/config.dto';
export enum AppName {
@@ -12,6 +13,7 @@ export enum AppName {
chatwoot = 'chatwoot',
calls = 'calls',
mcp = 'mcp',
mexicanPhoneNumbers = 'mexican-phone-numbers',
phoneNumbers = 'phone-numbers',
}
@@ -27,6 +29,7 @@ export const AppConfigClasses: Record<AppName, Type<any>> = {
[AppName.calls]: CallsAppConfig,
[AppName.chatwoot]: ChatWootAppConfig,
[AppName.mcp]: McpAppConfig,
[AppName.mexicanPhoneNumbers]: MexicanPhoneNumbersAppConfig,
};
export function GetAppConfigClass(name: AppName): Type<any> {
+2
View File
@@ -6,6 +6,7 @@ import { AppApiTag } from '@waha/apps/app_sdk/apps/openapi';
import CallsAppModule from '@waha/apps/calls/app.module';
import ChatWootAppModule from '@waha/apps/chatwoot/app.module';
import McpAppModule from '@waha/apps/mcp/app.module';
import MexicanPhoneNumbersAppModule from '@waha/apps/mexican-phone-numbers/app.module';
import PhoneNumbersAppModule from '@waha/apps/phone-numbers/app.module';
/**
@@ -18,6 +19,7 @@ const APPS: AppModule[] = [
CallsAppModule,
ChatWootAppModule,
McpAppModule,
MexicanPhoneNumbersAppModule,
PhoneNumbersAppModule,
];
@@ -3,7 +3,7 @@ import { AppName } from '@waha/apps/app_sdk/apps/apps';
import { ArgentinePhoneNumbersController } from '@waha/apps/argentine-phone-numbers/controller';
import { ArgentinePhoneNumbersAppService } from '@waha/apps/argentine-phone-numbers/services/ArgentinePhoneNumbersAppService';
const argentinephonenumbersAppModule: AppModule = {
const ArgentinePhoneNumbersAppModule: AppModule = {
name: AppName.argentinePhoneNumbers,
openapi: {
title: 'Phone Numbers: Argentina',
@@ -25,4 +25,4 @@ const argentinephonenumbersAppModule: AppModule = {
Service: ArgentinePhoneNumbersAppService,
};
export default argentinephonenumbersAppModule;
export default ArgentinePhoneNumbersAppModule;
@@ -3,7 +3,7 @@ import { AppName } from '@waha/apps/app_sdk/apps/apps';
import { BrazilianPhoneNumbersController } from '@waha/apps/brazilian-phone-numbers/controller';
import { BrazilianPhoneNumbersAppService } from '@waha/apps/brazilian-phone-numbers/services/BrazilianPhoneNumbersAppService';
const brazilianphonenumbersAppModule: AppModule = {
const BrazilianPhoneNumbersAppModule: AppModule = {
name: AppName.brazilianPhoneNumbers,
openapi: {
title: 'Phone Numbers: Brazil',
@@ -24,4 +24,4 @@ const brazilianphonenumbersAppModule: AppModule = {
Service: BrazilianPhoneNumbersAppService,
};
export default brazilianphonenumbersAppModule;
export default BrazilianPhoneNumbersAppModule;
@@ -0,0 +1,28 @@
import { AppModule } from '@waha/apps/app_sdk/apps/definition';
import { AppName } from '@waha/apps/app_sdk/apps/apps';
import { MexicanPhoneNumbersController } from '@waha/apps/mexican-phone-numbers/controller';
import { MexicanPhoneNumbersAppService } from '@waha/apps/mexican-phone-numbers/services/MexicanPhoneNumbersAppService';
const MexicanPhoneNumbersAppModule: AppModule = {
name: AppName.mexicanPhoneNumbers,
openapi: {
title: 'Phone Numbers: Mexico',
description:
'Resolve Mexican phone numbers (with and without the 1 after the country code)',
},
definition: {
plainkey: false,
queue: false,
migrations: true,
restartOnChange: true,
unique: true,
},
nestjs: {
imports: [],
controllers: [MexicanPhoneNumbersController],
providers: [MexicanPhoneNumbersAppService],
},
Service: MexicanPhoneNumbersAppService,
};
export default MexicanPhoneNumbersAppModule;
@@ -0,0 +1,21 @@
import { Controller, UseGuards } from '@nestjs/common';
import { ApiSecurity } from '@nestjs/swagger';
import { AppName } from '@waha/apps/app_sdk/apps/apps';
import { UniqueAppResolver } from '@waha/apps/app_sdk/services/UniqueAppResolver';
import { MexicanPhoneNumbersAppService } from '@waha/apps/mexican-phone-numbers/services/MexicanPhoneNumbersAppService';
import { PhoneNumbersCacheController } from '@waha/apps/phone-numbers/api/PhoneNumbersCacheController';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
@ApiSecurity('api_key')
@Controller('api/apps/mexican-phone-numbers/:session')
@UseGuards(PoliciesGuard)
export class MexicanPhoneNumbersController extends PhoneNumbersCacheController {
constructor(
manager: SessionManager,
resolver: UniqueAppResolver,
appService: MexicanPhoneNumbersAppService,
) {
super(manager, resolver, appService, AppName.mexicanPhoneNumbers);
}
}
+3
View File
@@ -0,0 +1,3 @@
import { PhoneNumbersBaseConfig } from '@waha/apps/phone-numbers/dto/config.dto';
export class MexicanPhoneNumbersAppConfig extends PhoneNumbersBaseConfig {}
@@ -0,0 +1,9 @@
import { migrations_001_init_cache } from '@waha/apps/phone-numbers/storage/migrations_001_init_cache';
const migration = migrations_001_init_cache({
table: 'app_mexican_phone_numbers_cache',
index: 'mxphone',
});
exports.up = migration.up;
exports.down = migration.down;
@@ -0,0 +1,106 @@
import { UnprocessableEntityException } from '@nestjs/common';
import { MexicanPhoneNumberRules } from '@waha/apps/mexican-phone-numbers/rules/MexicanPhoneNumberRules';
import { PhoneNumbersBaseConfig } from '@waha/apps/phone-numbers/dto/config.dto';
import {
buildPlugin as buildPhoneNumbersPlugin,
resolveChat,
stubLookup,
} from '@waha/apps/phone-numbers/plugins/testing';
function buildPlugin(config: Partial<PhoneNumbersBaseConfig> = {}) {
return buildPhoneNumbersPlugin({
config: config,
rules: MexicanPhoneNumberRules(),
});
}
describe('MexicanPhoneNumbers', () => {
it('uses the supplied form when it exists, without checking the other one', async () => {
const { session } = buildPlugin();
stubLookup(session, { numberExists: true, chatId: '525512345678@c.us' });
const resolved = await resolveChat(session, '+525512345678');
expect(resolved).toBe('525512345678@c.us');
expect(session.checkNumberStatus).toHaveBeenCalledTimes(1);
expect(session.checkNumberStatus).toHaveBeenCalledWith({
phone: '525512345678',
session: 'test',
});
});
it('tries the form with 1 after the supplied one is absent, keeps a LID answer', async () => {
const { session } = buildPlugin();
session.checkNumberStatus = jest
.fn()
.mockResolvedValueOnce({ numberExists: false })
.mockResolvedValueOnce({ numberExists: true, chatId: '123456@lid' });
const resolved = await resolveChat(session, '525512345678@c.us');
expect(resolved).toBe('123456@lid');
expect(session.checkNumberStatus).toHaveBeenLastCalledWith({
phone: '5215512345678',
session: 'test',
});
// Both forms hit the cache now
expect(await resolveChat(session, '5215512345678@c.us')).toBe('123456@lid');
expect(session.checkNumberStatus).toHaveBeenCalledTimes(2);
});
it('tries the form without 1 as the fallback', async () => {
const { session } = buildPlugin();
session.checkNumberStatus = jest
.fn()
.mockResolvedValueOnce({ numberExists: false })
.mockResolvedValueOnce({
numberExists: true,
chatId: '523312345678@c.us',
});
const resolved = await resolveChat(session, '5213312345678@c.us');
expect(resolved).toBe('523312345678@c.us');
});
it('prefers the pn when the engine answers with both pn and a LID', async () => {
const { session } = buildPlugin();
stubLookup(session, {
numberExists: true,
chatId: '123@lid',
pn: '5215512345678@c.us',
});
expect(await resolveChat(session, '525512345678@c.us')).toBe(
'5215512345678@c.us',
);
});
it('soft mode keeps the supplied form when both are absent, strict rejects', async () => {
const soft = buildPlugin();
stubLookup(soft.session, { numberExists: false });
expect(await resolveChat(soft.session, '525512345678@c.us')).toBe(
'525512345678@c.us',
);
expect(soft.session.checkNumberStatus).toHaveBeenCalledTimes(2);
const strict = buildPlugin({ strict: true });
stubLookup(strict.session, { numberExists: false });
await expect(
resolveChat(strict.session, '525512345678@c.us'),
).rejects.toThrow(UnprocessableEntityException);
});
it('leaves numbers without the country code and other countries untouched', async () => {
const { session } = buildPlugin();
stubLookup(session, { numberExists: false });
expect(await resolveChat(session, '5512345678@c.us')).toBe(
'5512345678@c.us',
);
expect(await resolveChat(session, '5511912345678@c.us')).toBe(
'5511912345678@c.us',
);
expect(session.checkNumberStatus).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,32 @@
import { MexicanPhoneNumberRules } from './MexicanPhoneNumberRules';
describe('MexicanPhoneNumberRules', () => {
const rules = MexicanPhoneNumberRules();
function resolve(digits: string) {
const rule = rules.find((r) => r.matches(digits));
return rule?.resolve(digits) ?? null;
}
it.each([
['525512345678', ['525512345678', '5215512345678']],
['5215512345678', ['5215512345678', '525512345678']],
['523312345678', ['523312345678', '5213312345678']],
['5216641234567', ['5216641234567', '526641234567']],
])('keeps the supplied form first for %s', (digits, expected) => {
expect(resolve(digits)).toEqual({ candidates: expected, fallback: digits });
});
it.each([
'5512345678',
'0445512345678',
'52551234567',
'525512345678901',
'521551234567',
'5491112345678',
'5511912345678',
'',
])('does not match %s', (digits) => {
expect(resolve(digits)).toBeNull();
});
});
@@ -0,0 +1,13 @@
import { PhoneNumberRule } from '@waha/apps/phone-numbers/rules/PhoneNumberRule';
import { RegexpPhoneNumberRule } from '@waha/apps/phone-numbers/rules/RegexpPhoneNumberRule';
/**
* Mexico: since 2019 numbers are dialed as 52 + 10 digits, but accounts registered before that are still stored
* on WhatsApp with the old mobile 1 after the country code - check the supplied form first, then the other one
*/
export function MexicanPhoneNumberRules(): PhoneNumberRule[] {
return [
new RegexpPhoneNumberRule('^52([2-9]\\d{9})$', '521$1'),
new RegexpPhoneNumberRule('^521(\\d{10})$', '52$1'),
];
}
@@ -0,0 +1,21 @@
import { Injectable } from '@nestjs/common';
import { UniqueAppResolver } from '@waha/apps/app_sdk/services/UniqueAppResolver';
import { MexicanPhoneNumbersAppConfig } from '@waha/apps/mexican-phone-numbers/dto';
import { MexicanPhoneNumberRules } from '@waha/apps/mexican-phone-numbers/rules/MexicanPhoneNumberRules';
import { MexicanPhoneNumbersCacheRepository } from '@waha/apps/mexican-phone-numbers/storage/MexicanPhoneNumbersCacheRepository';
import { PhoneNumberRule } from '@waha/apps/phone-numbers/rules/PhoneNumberRule';
import { PhoneNumbersAppServiceBase } from '@waha/apps/phone-numbers/services/PhoneNumbersAppServiceBase';
@Injectable()
export class MexicanPhoneNumbersAppService extends PhoneNumbersAppServiceBase<MexicanPhoneNumbersAppConfig> {
protected readonly Repository = MexicanPhoneNumbersCacheRepository;
constructor(resolver: UniqueAppResolver) {
super(resolver);
}
protected rules(config: MexicanPhoneNumbersAppConfig): PhoneNumberRule[] {
void config;
return MexicanPhoneNumberRules();
}
}
@@ -0,0 +1,5 @@
import { PhoneNumbersCacheRepository } from '@waha/apps/phone-numbers/storage/PhoneNumbersCacheRepository';
export class MexicanPhoneNumbersCacheRepository extends PhoneNumbersCacheRepository {
static tableName = 'app_mexican_phone_numbers_cache';
}
@@ -7,6 +7,8 @@ const phoneNumbers = require('../migrations/001_init_app');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const argentine = require('../../argentine-phone-numbers/migrations/001_init_app');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const mexican = require('../../mexican-phone-numbers/migrations/001_init_app');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const brazilian = require('../../brazilian-phone-numbers/migrations/001_init_brazilian_phone_numbers');
describe('migrations_001_init_cache', () => {
@@ -28,6 +30,7 @@ describe('migrations_001_init_cache', () => {
it('every phone numbers app gets its own table', async () => {
await phoneNumbers.up(db);
await argentine.up(db);
await mexican.up(db);
await brazilian.up(db);
expect(await db.schema.hasTable('app_phone_numbers_cache')).toBe(true);