diff --git a/src/db/migrations/index.ts b/src/db/migrations/index.ts index 9b51b87..c182899 100644 --- a/src/db/migrations/index.ts +++ b/src/db/migrations/index.ts @@ -375,5 +375,19 @@ export const migrations: Migration[] = [ } } catch {} } + }, + { + version: 13, + name: 'groups-onboarding-prompted-at', + checksum: 'v13-groups-onboarding-2025-10-17', + up: (db: Database) => { + try { + const cols = db.query(`PRAGMA table_info(groups)`).all() as any[]; + const hasCol = Array.isArray(cols) && cols.some((c: any) => String(c.name) === 'onboarding_prompted_at'); + if (!hasCol) { + db.exec(`ALTER TABLE groups ADD COLUMN onboarding_prompted_at TEXT NULL;`); + } + } catch {} + } } ]; diff --git a/tests/unit/services/group-sync.onboarding.test.ts b/tests/unit/services/group-sync.onboarding.test.ts new file mode 100644 index 0000000..4b38a5b --- /dev/null +++ b/tests/unit/services/group-sync.onboarding.test.ts @@ -0,0 +1,83 @@ +import { describe, it, beforeEach, afterEach, expect } from 'bun:test'; +import Database from 'bun:sqlite'; +import { initializeDatabase } from '../../../src/db'; +import { GroupSyncService } from '../../../src/services/group-sync'; +import { AllowedGroups } from '../../../src/services/allowed-groups'; + +const envBackup = { ...process.env } as NodeJS.ProcessEnv; + +describe('GroupSyncService - onboarding A3', () => { + let memdb: Database; + + beforeEach(() => { + process.env = { + ...envBackup, + NODE_ENV: 'test', + ONBOARDING_ENABLE_IN_TEST: 'true', + ONBOARDING_PROMPTS_ENABLED: 'true', + ONBOARDING_GRACE_SECONDS: '0', + ONBOARDING_COOLDOWN_DAYS: '7', + ONBOARDING_COVERAGE_THRESHOLD: '1', + CHATBOT_PHONE_NUMBER: '555111222' + }; + memdb = new Database(':memory:'); + initializeDatabase(memdb); + (GroupSyncService as any).dbInstance = memdb; + (AllowedGroups as any).dbInstance = memdb; + + // Sembrar grupo activo + memdb.prepare(`INSERT INTO groups (id, community_id, name, active, last_verified) VALUES (?,?,?,?, strftime('%Y-%m-%d %H:%M:%f','now'))`) + .run('g1@g.us', 'comm-1', 'Grupo 1', 1); + }); + + afterEach(() => { + memdb.close(); + process.env = envBackup; + }); + + it('publica prompt cuando coverage < 100, grace cumplido y sin cooldown', () => { + // snapshot con un resoluble (dígitos) y uno no resoluble (alias sin mapeo) + const res = GroupSyncService.reconcileGroupMembers('g1@g.us', [ + { userId: '111', isAdmin: false }, + { userId: 'alias_lid', isAdmin: false } + ], '2025-01-01 00:00:00.000'); + + expect(res).toEqual(expect.objectContaining({ added: 2 })); + const row = memdb.query(`SELECT recipient, message, status FROM response_queue ORDER BY id DESC LIMIT 1`).get() as any; + expect(row).toBeTruthy(); + expect(row.recipient).toBe('g1@g.us'); + expect(String(row.message)).toContain('https://wa.me/555111222'); + + const g = memdb.query(`SELECT onboarding_prompted_at FROM groups WHERE id = 'g1@g.us'`).get() as any; + expect(g).toBeTruthy(); + expect(g.onboarding_prompted_at).toBeTruthy(); + }); + + it('omite prompt cuando coverage = 100', () => { + // Previo: no debe existir prompt para este grupo + const before = memdb.query(`SELECT COUNT(*) AS c FROM response_queue`).get() as any; + expect(Number(before.c)).toBe(0); + + // snapshot totalmente resoluble (dos dígitos) + GroupSyncService.reconcileGroupMembers('g1@g.us', [ + { userId: '111', isAdmin: false }, + { userId: '222', isAdmin: false } + ], '2025-01-01 00:00:00.000'); + + const after = memdb.query(`SELECT COUNT(*) AS c FROM response_queue`).get() as any; + expect(Number(after.c)).toBe(0); + }); + + it('omite prompt en modo enforce si el grupo no está allowed', () => { + process.env.GROUP_GATING_MODE = 'enforce'; + AllowedGroups.setStatus('g1@g.us', 'blocked'); + + GroupSyncService.reconcileGroupMembers('g1@g.us', [ + { userId: '111', isAdmin: false }, + { userId: 'alias_lid', isAdmin: false } + ], '2025-01-01 00:00:00.000'); + + const count = memdb.query(`SELECT COUNT(*) AS c FROM response_queue`).get() as any; + expect(Number(count.c)).toBe(0); + }); +});