Add Verifik RETHUS integration: backend module, settings auth flow, admin query button
- verifik.service.ts: email OTP auth, token storage/refresh, RETHUS query by professional - verifik.controller.ts: send-otp, confirm, refresh, rethus/professional/:id endpoints - settings/page.tsx: Verifik connection section (email→OTP→token) - professionals/[id]/page.tsx: Consultar RETHUS button with inline result display Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
66bc47b6e1
commit
0a67f5d80a
@@ -47,6 +47,8 @@ export default function ProfessionalDetailPage() {
|
|||||||
const [savingPro, setSavingPro] = useState(false);
|
const [savingPro, setSavingPro] = useState(false);
|
||||||
const [savingUser, setSavingUser] = useState(false);
|
const [savingUser, setSavingUser] = useState(false);
|
||||||
const [validatingRethus, setValidatingRethus] = useState(false);
|
const [validatingRethus, setValidatingRethus] = useState(false);
|
||||||
|
const [rethusResult, setRethusResult] = useState<any>(null);
|
||||||
|
const [loadingRethus, setLoadingRethus] = useState(false);
|
||||||
const [proForm, setProForm] = useState<ProForm>({ profession: '', identification: '', address: '', rate: '', rethus_code: '' });
|
const [proForm, setProForm] = useState<ProForm>({ profession: '', identification: '', address: '', rate: '', rethus_code: '' });
|
||||||
const [userForm, setUserForm] = useState<UserForm>({ name: '', email: '', phone: '', city: '' });
|
const [userForm, setUserForm] = useState<UserForm>({ name: '', email: '', phone: '', city: '' });
|
||||||
|
|
||||||
@@ -101,6 +103,19 @@ export default function ProfessionalDetailPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const consultRethus = async () => {
|
||||||
|
setLoadingRethus(true);
|
||||||
|
setRethusResult(null);
|
||||||
|
try {
|
||||||
|
const data = await api.get<any>(`/verifik/rethus/professional/${id}`);
|
||||||
|
setRethusResult(data);
|
||||||
|
} catch (e: any) {
|
||||||
|
toast.error(e?.message || 'Error al consultar RETHUS');
|
||||||
|
} finally {
|
||||||
|
setLoadingRethus(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const validateRethus = async () => {
|
const validateRethus = async () => {
|
||||||
setValidatingRethus(true);
|
setValidatingRethus(true);
|
||||||
try {
|
try {
|
||||||
@@ -349,13 +364,22 @@ export default function ProfessionalDetailPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={consultRethus}
|
||||||
|
disabled={loadingRethus}
|
||||||
|
>
|
||||||
|
<ShieldCheck className="mr-2 h-4 w-4" />
|
||||||
|
{loadingRethus ? 'Consultando...' : 'Consultar en RETHUS'}
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => window.open('https://www.minsalud.gov.co/salud/Paginas/rethus.aspx', '_blank')}
|
onClick={() => window.open('https://www.minsalud.gov.co/salud/Paginas/rethus.aspx', '_blank')}
|
||||||
>
|
>
|
||||||
<ExternalLink className="mr-2 h-4 w-4" />
|
<ExternalLink className="mr-2 h-4 w-4" />
|
||||||
Abrir portal RETHUS (Minsalud)
|
Portal Minsalud
|
||||||
</Button>
|
</Button>
|
||||||
{!professional.rethus_validated && (
|
{!professional.rethus_validated && (
|
||||||
<Button
|
<Button
|
||||||
@@ -369,9 +393,33 @@ export default function ProfessionalDetailPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{rethusResult && (
|
||||||
|
<div className="mt-3 rounded-lg border bg-muted/40 p-4 space-y-2 text-sm">
|
||||||
|
<p className="font-semibold">{rethusResult.fullName || `${rethusResult.firstName} ${rethusResult.lastName}`}</p>
|
||||||
|
<p><span className="text-muted-foreground">Estado: </span>
|
||||||
|
<span className={rethusResult.rethus?.status?.includes('ACTIVO') ? 'text-green-600 font-medium' : 'text-destructive font-medium'}>
|
||||||
|
{rethusResult.rethus?.status || '—'}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
{rethusResult.rethus?.academic?.length > 0 && (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-muted-foreground font-medium">Títulos registrados:</p>
|
||||||
|
{rethusResult.rethus.academic.map((a: any, i: number) => (
|
||||||
|
<div key={i} className="pl-3 border-l-2 border-muted-foreground/30">
|
||||||
|
<p className="font-medium">{a.profession}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">{a.type} · {a.originDegree} · {a.startDate}</p>
|
||||||
|
{a.entity && <p className="text-xs text-muted-foreground">{a.entity}</p>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{!professional.rethus_validated && (
|
{!professional.rethus_validated && (
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Copia el código o cédula, búscalo en el portal de Minsalud y una vez verificado haz clic en "Marcar como validado".
|
Consulta automáticamente con Verifik o búscalo en el portal de Minsalud. Una vez verificado, haz clic en "Marcar como validado".
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Badge } from '@/components/ui/badge';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import {
|
import {
|
||||||
Save, ExternalLink, Copy, FileText, Shield, Map,
|
Save, ExternalLink, Copy, FileText, Shield, Map,
|
||||||
Eye, EyeOff, CheckCircle2, XCircle, Clock, Smartphone, Phone, Mail, Server,
|
Eye, EyeOff, CheckCircle2, XCircle, Clock, Smartphone, Phone, Mail, Server, Link,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api/v1';
|
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api/v1';
|
||||||
@@ -57,6 +57,13 @@ export default function SettingsPage() {
|
|||||||
const [policies, setPolicies] = useState<Record<string, string>>({ privacy: '', terms: '' });
|
const [policies, setPolicies] = useState<Record<string, string>>({ privacy: '', terms: '' });
|
||||||
const [savingPolicy, setSavingPolicy] = useState<string | null>(null);
|
const [savingPolicy, setSavingPolicy] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Verifik
|
||||||
|
const [verifikConnected, setVerifikConnected] = useState(false);
|
||||||
|
const [verifikEmail, setVerifikEmail] = useState('');
|
||||||
|
const [verifikOtp, setVerifikOtp] = useState('');
|
||||||
|
const [verifikOtpSent, setVerifikOtpSent] = useState(false);
|
||||||
|
const [verifikLoading, setVerifikLoading] = useState(false);
|
||||||
|
|
||||||
// SMTP
|
// SMTP
|
||||||
const [smtp, setSmtp] = useState<SmtpConfig>({ host: '', port: '587', user: '', pass: '', from: '' });
|
const [smtp, setSmtp] = useState<SmtpConfig>({ host: '', port: '587', user: '', pass: '', from: '' });
|
||||||
const [showSmtpPass, setShowSmtpPass] = useState(false);
|
const [showSmtpPass, setShowSmtpPass] = useState(false);
|
||||||
@@ -69,12 +76,14 @@ export default function SettingsPage() {
|
|||||||
api.get<{ configured: boolean }>('/settings/maps-key'),
|
api.get<{ configured: boolean }>('/settings/maps-key'),
|
||||||
api.get<{ privacy: string | null; terms: string | null }>('/settings/policies'),
|
api.get<{ privacy: string | null; terms: string | null }>('/settings/policies'),
|
||||||
api.get<SmtpConfig | null>('/settings/smtp').catch(() => null),
|
api.get<SmtpConfig | null>('/settings/smtp').catch(() => null),
|
||||||
|
api.get<{ connected: boolean }>('/verifik/status').catch(() => ({ connected: false })),
|
||||||
])
|
])
|
||||||
.then(([globalData, mapsData, policiesData, smtpData]) => {
|
.then(([globalData, mapsData, policiesData, smtpData, verifikStatus]) => {
|
||||||
setGlobal(globalData || {});
|
setGlobal(globalData || {});
|
||||||
setMapsConfigured(mapsData.configured);
|
setMapsConfigured(mapsData.configured);
|
||||||
setPolicies({ privacy: policiesData.privacy || '', terms: policiesData.terms || '' });
|
setPolicies({ privacy: policiesData.privacy || '', terms: policiesData.terms || '' });
|
||||||
if (smtpData) setSmtp({ host: smtpData.host || '', port: smtpData.port || '587', user: smtpData.user || '', pass: smtpData.pass || '', from: smtpData.from || '' });
|
if (smtpData) setSmtp({ host: smtpData.host || '', port: smtpData.port || '587', user: smtpData.user || '', pass: smtpData.pass || '', from: smtpData.from || '' });
|
||||||
|
setVerifikConnected(verifikStatus.connected);
|
||||||
})
|
})
|
||||||
.catch(() => toast.error('Error al cargar configuración'))
|
.catch(() => toast.error('Error al cargar configuración'))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
@@ -134,6 +143,48 @@ export default function SettingsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sendVerifikOtp = async () => {
|
||||||
|
if (!verifikEmail.trim()) return toast.error('Ingresa tu email de Verifik');
|
||||||
|
setVerifikLoading(true);
|
||||||
|
try {
|
||||||
|
await api.post('/verifik/send-otp', { email: verifikEmail.trim() });
|
||||||
|
setVerifikOtpSent(true);
|
||||||
|
toast.success('OTP enviado a tu correo');
|
||||||
|
} catch (e: any) {
|
||||||
|
toast.error(e?.message || 'Error al enviar OTP');
|
||||||
|
} finally {
|
||||||
|
setVerifikLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmVerifikOtp = async () => {
|
||||||
|
if (!verifikOtp.trim()) return toast.error('Ingresa el código OTP');
|
||||||
|
setVerifikLoading(true);
|
||||||
|
try {
|
||||||
|
await api.post('/verifik/confirm', { email: verifikEmail.trim(), otp: verifikOtp.trim() });
|
||||||
|
setVerifikConnected(true);
|
||||||
|
setVerifikOtpSent(false);
|
||||||
|
setVerifikOtp('');
|
||||||
|
toast.success('Verifik conectado correctamente');
|
||||||
|
} catch (e: any) {
|
||||||
|
toast.error(e?.message || 'Error al confirmar OTP');
|
||||||
|
} finally {
|
||||||
|
setVerifikLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshVerifikToken = async () => {
|
||||||
|
setVerifikLoading(true);
|
||||||
|
try {
|
||||||
|
await api.post('/verifik/refresh', {});
|
||||||
|
toast.success('Token renovado');
|
||||||
|
} catch (e: any) {
|
||||||
|
toast.error(e?.message || 'Error al renovar token');
|
||||||
|
} finally {
|
||||||
|
setVerifikLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const copyLink = (key: string) => {
|
const copyLink = (key: string) => {
|
||||||
navigator.clipboard.writeText(`${API_BASE}/settings/policy/${key}`);
|
navigator.clipboard.writeText(`${API_BASE}/settings/policy/${key}`);
|
||||||
toast.success('Enlace copiado');
|
toast.success('Enlace copiado');
|
||||||
@@ -418,6 +469,66 @@ export default function SettingsPage() {
|
|||||||
</Card>
|
</Card>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* ── Verifik ── */}
|
||||||
|
<section className="space-y-4">
|
||||||
|
<h2 className="text-lg font-semibold">Verifik — Validación de profesionales</h2>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-base">
|
||||||
|
<Link size={18} className="text-muted-foreground" />
|
||||||
|
Cuenta Verifik
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Permite consultar RETHUS (registro de profesionales de salud) directamente desde el perfil de cada profesional.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{verifikConnected
|
||||||
|
? <><CheckCircle2 className="text-green-500 h-5 w-5" /><span className="text-sm font-medium text-green-600">Conectado</span></>
|
||||||
|
: <><XCircle className="text-destructive h-5 w-5" /><span className="text-sm font-medium text-destructive">No conectado</span></>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!verifikOtpSent ? (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
placeholder="tu@email.com (cuenta Verifik)"
|
||||||
|
value={verifikEmail}
|
||||||
|
onChange={(e) => setVerifikEmail(e.target.value)}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<Button onClick={sendVerifikOtp} disabled={verifikLoading || !verifikEmail.trim()}>
|
||||||
|
{verifikLoading ? 'Enviando...' : 'Enviar OTP'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<p className="text-sm text-muted-foreground">Ingresa el código que llegó a <strong>{verifikEmail}</strong></p>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
placeholder="Código OTP"
|
||||||
|
value={verifikOtp}
|
||||||
|
onChange={(e) => setVerifikOtp(e.target.value)}
|
||||||
|
className="w-40 font-mono"
|
||||||
|
/>
|
||||||
|
<Button onClick={confirmVerifikOtp} disabled={verifikLoading}>
|
||||||
|
{verifikLoading ? 'Confirmando...' : 'Confirmar'}
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" onClick={() => setVerifikOtpSent(false)}>Cancelar</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{verifikConnected && (
|
||||||
|
<Button variant="outline" size="sm" onClick={refreshVerifikToken} disabled={verifikLoading}>
|
||||||
|
{verifikLoading ? 'Renovando...' : 'Renovar token (30 días)'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
|
|
||||||
{/* ── Documentos legales ── */}
|
{/* ── Documentos legales ── */}
|
||||||
<section className="space-y-4">
|
<section className="space-y-4">
|
||||||
<h2 className="text-lg font-semibold">Documentos legales</h2>
|
<h2 className="text-lg font-semibold">Documentos legales</h2>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { StorageModule } from './storage/storage.module';
|
|||||||
import { NotificationsModule } from './notifications/notifications.module';
|
import { NotificationsModule } from './notifications/notifications.module';
|
||||||
import { SmsModule } from './sms/sms.module';
|
import { SmsModule } from './sms/sms.module';
|
||||||
import { SuggestionsModule } from './suggestions/suggestions.module';
|
import { SuggestionsModule } from './suggestions/suggestions.module';
|
||||||
|
import { VerifikModule } from './verifik/verifik.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -32,6 +33,7 @@ import { SuggestionsModule } from './suggestions/suggestions.module';
|
|||||||
NotificationsModule,
|
NotificationsModule,
|
||||||
SmsModule,
|
SmsModule,
|
||||||
SuggestionsModule,
|
SuggestionsModule,
|
||||||
|
VerifikModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Controller, Post, Get, Body, Param, Query, UseGuards } from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
||||||
|
import { VerifikService } from './verifik.service';
|
||||||
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||||
|
|
||||||
|
@ApiTags('Verifik')
|
||||||
|
@Controller('verifik')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
export class VerifikController {
|
||||||
|
constructor(private verifik: VerifikService) {}
|
||||||
|
|
||||||
|
@Get('status')
|
||||||
|
status() { return this.verifik.getStatus(); }
|
||||||
|
|
||||||
|
@Post('send-otp')
|
||||||
|
sendOtp(@Body() body: { email: string }) {
|
||||||
|
return this.verifik.sendOtp(body.email);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('confirm')
|
||||||
|
confirm(@Body() body: { email: string; otp: string }) {
|
||||||
|
return this.verifik.confirmOtp(body.email, body.otp);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('refresh')
|
||||||
|
refresh() { return this.verifik.refreshToken(); }
|
||||||
|
|
||||||
|
// Query by professional ID — looks up cedula from DB
|
||||||
|
@Get('rethus/professional/:id')
|
||||||
|
rethusByProfessional(@Param('id') id: string) {
|
||||||
|
return this.verifik.queryRethusByProfessional(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direct query by document
|
||||||
|
@Get('rethus')
|
||||||
|
rethus(
|
||||||
|
@Query('documentType') documentType: string,
|
||||||
|
@Query('documentNumber') documentNumber: string,
|
||||||
|
) {
|
||||||
|
return this.verifik.queryRethus(documentType, documentNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { VerifikService } from './verifik.service';
|
||||||
|
import { VerifikController } from './verifik.controller';
|
||||||
|
import { PrismaModule } from '../prisma/prisma.module';
|
||||||
|
import { AuthModule } from '../auth/auth.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PrismaModule, AuthModule],
|
||||||
|
providers: [VerifikService],
|
||||||
|
controllers: [VerifikController],
|
||||||
|
exports: [VerifikService],
|
||||||
|
})
|
||||||
|
export class VerifikModule {}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
|
||||||
|
const BASE = 'https://api.verifik.co';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class VerifikService {
|
||||||
|
private readonly logger = new Logger(VerifikService.name);
|
||||||
|
|
||||||
|
constructor(private prisma: PrismaService) {}
|
||||||
|
|
||||||
|
// ── token storage ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private async getStoredToken(): Promise<string | null> {
|
||||||
|
const s = await this.prisma.settings.findUnique({ where: { key: 'verifik_config' } });
|
||||||
|
return (s?.value as any)?.token ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async saveToken(token: string) {
|
||||||
|
await this.prisma.settings.upsert({
|
||||||
|
where: { key: 'verifik_config' },
|
||||||
|
create: { key: 'verifik_config', value: { token } },
|
||||||
|
update: { value: { token } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── auth flow ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async sendOtp(email: string) {
|
||||||
|
const res = await fetch(`${BASE}/v2/projects/email-login?email=${encodeURIComponent(email)}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { Accept: 'application/json' },
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.json().catch(() => ({}));
|
||||||
|
throw new Error((body as any).message || `Error ${res.status}`);
|
||||||
|
}
|
||||||
|
return { message: 'OTP enviado al correo' };
|
||||||
|
}
|
||||||
|
|
||||||
|
async confirmOtp(email: string, otp: string) {
|
||||||
|
const res = await fetch(`${BASE}/v2/projects/email-login/confirm`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email, otp }),
|
||||||
|
});
|
||||||
|
const body = await res.json().catch(() => ({})) as any;
|
||||||
|
if (!res.ok) throw new Error(body.message || `Error ${res.status}`);
|
||||||
|
const token: string = body.data?.accessToken ?? body.accessToken;
|
||||||
|
if (!token) throw new Error('No se recibió token');
|
||||||
|
await this.saveToken(token);
|
||||||
|
return { message: 'Conectado correctamente' };
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshToken() {
|
||||||
|
const token = await this.getStoredToken();
|
||||||
|
if (!token) throw new Error('No hay token guardado');
|
||||||
|
const res = await fetch(`${BASE}/v2/auth/session?origin=refresh&expiresIn=1`, {
|
||||||
|
headers: { Accept: 'application/json', Authorization: token },
|
||||||
|
});
|
||||||
|
const body = await res.json().catch(() => ({})) as any;
|
||||||
|
if (!res.ok) throw new Error(body.message || `Error ${res.status}`);
|
||||||
|
const newToken: string = body.accessToken ?? body.data?.accessToken;
|
||||||
|
if (!newToken) throw new Error('No se recibió token renovado');
|
||||||
|
await this.saveToken(newToken);
|
||||||
|
return { message: 'Token renovado' };
|
||||||
|
}
|
||||||
|
|
||||||
|
async getStatus() {
|
||||||
|
const token = await this.getStoredToken();
|
||||||
|
if (!token) return { connected: false };
|
||||||
|
// Quick validation — session endpoint with no origin param just validates
|
||||||
|
const res = await fetch(`${BASE}/v2/auth/session`, {
|
||||||
|
headers: { Accept: 'application/json', Authorization: token },
|
||||||
|
});
|
||||||
|
return { connected: res.ok };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── RETHUS lookup ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async queryRethusByProfessional(professionalId: string) {
|
||||||
|
const prof = await this.prisma.professionals.findUnique({ where: { id: professionalId } });
|
||||||
|
if (!prof) throw new Error('Profesional no encontrado');
|
||||||
|
if (!prof.identification) throw new Error('El profesional no tiene número de cédula registrado');
|
||||||
|
return this.queryRethus('CC', prof.identification);
|
||||||
|
}
|
||||||
|
|
||||||
|
async queryRethus(documentType: string, documentNumber: string) {
|
||||||
|
let token = await this.getStoredToken();
|
||||||
|
if (!token) throw new Error('Verifik no está configurado. Conecta tu cuenta en Configuración.');
|
||||||
|
|
||||||
|
const call = async (t: string) =>
|
||||||
|
fetch(`${BASE}/v2/co/cedula/rethus?documentType=${documentType}&documentNumber=${documentNumber}`, {
|
||||||
|
headers: { Accept: 'application/json', Authorization: `Bearer ${t}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
let res = await call(token);
|
||||||
|
|
||||||
|
// Auto-refresh on 401
|
||||||
|
if (res.status === 401) {
|
||||||
|
this.logger.log('Token Verifik expirado, renovando…');
|
||||||
|
await this.refreshToken();
|
||||||
|
token = await this.getStoredToken();
|
||||||
|
res = await call(token!);
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await res.json().catch(() => ({})) as any;
|
||||||
|
if (!res.ok) throw new Error(body.message || `Error Verifik ${res.status}`);
|
||||||
|
return body.data ?? body;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user