- name
- sesion-workflow-clinic
- description
- Mental health practice management platform with AI-powered scheduling, WhatsApp automation, AFIP billing, and video consultations for Argentine clinics
- triggers
- ["how do I integrate Sesión into my psychology clinic","set up WhatsApp automation for patient appointments","configure AFIP electronic invoicing in Sesión","implement Claude AI for clinical note summarization","create appointment scheduling with Sesión","build video consultation feature with Sesión","configure Mercado Pago payment integration","set up patient journey pipeline in Sesión"]
# Sesión Workflow Clinic
> Skill by [ara.so](https://ara.so) — Devtools Skills collection.
Sesión is a comprehensive mental health practice orchestration platform designed for psychology clinics in Argentina. It unifies appointment scheduling, automated WhatsApp messaging, AFIP-compliant billing, secure video consultations, and AI-powered clinical assistance using Claude Opus 4.6 and Sonnet 4.6.
## Tech Stack
- **Frontend**: SvelteKit 5 + Svelte 5, TailwindCSS
- **Backend**: NestJS, Prisma ORM
- **Database**: PostgreSQL (relational), Redis (caching), Elasticsearch (search)
- **AI**: Anthropic Claude Opus 4.6 & Sonnet 4.6
- **Video**: LiveKit (WebRTC)
- **Messaging**: Baileys (WhatsApp automation)
- **Payments**: Stripe, Mercado Pago
- **Language**: TypeScript
## Installation
### Prerequisites
```bash
# Required dependencies
node >= 18.0.0
postgresql >= 14
redis >= 6.2
docker >= 20.10 (optional but recommended)
```
### Clone and Install
```bash
git clone https://github.com/fahad-hamid/psique-workflow-clinic.git
cd psique-workflow-clinic
# Install dependencies
npm install
# Setup environment variables
cp .env.example .env
```
### Environment Configuration
```bash
# .env
DATABASE_URL="postgresql://user:password@localhost:5432/sesion"
REDIS_URL="redis://localhost:6379"
# AI Configuration
ANTHROPIC_API_KEY=your_anthropic_key_here
CLAUDE_OPUS_MODEL="claude-opus-4.6"
CLAUDE_SONNET_MODEL="claude-sonnet-4.6"
# WhatsApp Automation
WHATSAPP_SESSION_PATH="./whatsapp-session"
WHATSAPP_WEBHOOK_SECRET=your_webhook_secret
# Video Consultations
LIVEKIT_API_KEY=your_livekit_key
LIVEKIT_API_SECRET=your_livekit_secret
LIVEKIT_WS_URL="wss://your-livekit-instance.com"
# Payment Processing
STRIPE_SECRET_KEY=your_stripe_key
MERCADOPAGO_ACCESS_TOKEN=your_mercadopago_token
# AFIP Integration (Argentina Tax Authority)
AFIP_CUIT=your_cuit_number
AFIP_CERT_PATH="./certs/afip-cert.pem"
AFIP_KEY_PATH="./certs/afip-key.pem"
```
### Database Setup
```bash
# Run Prisma migrations
npx prisma migrate dev
# Generate Prisma client
npx prisma generate
# Seed initial data
npm run db:seed
```
### Start Development Server
```bash
# Start backend (NestJS)
npm run dev:api
# Start frontend (SvelteKit)
npm run dev:web
# Start all services with Docker Compose
docker-compose up -d
```
## Core Modules
### 1. Appointment Scheduling
**Prisma Schema (appointments)**:
```typescript
// prisma/schema.prisma
model Appointment {
id String @id @default(cuid())
patientId String
practitionerId String
startTime DateTime
endTime DateTime
sessionType SessionType
status AppointmentStatus @default(SCHEDULED)
roomId String?
notes String?
patient Patient @relation(fields: [patientId], references: [id])
practitioner Practitioner @relation(fields: [practitionerId], references: [id])
room Room? @relation(fields: [roomId], references: [id])
@@index([patientId, startTime])
@@index([practitionerId, startTime])
}
enum SessionType {
PRESENCIAL
VIRTUAL
EVALUACION
PAREJA
FAMILIAR
}
enum AppointmentStatus {
SCHEDULED
CONFIRMED
IN_PROGRESS
COMPLETED
CANCELLED
NO_SHOW
}
```
**Creating Appointments (NestJS Service)**:
```typescript
// src/appointments/appointments.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { WhatsAppService } from '../whatsapp/whatsapp.service';
@Injectable()
export class AppointmentsService {
constructor(
private prisma: PrismaService,
private whatsapp: WhatsAppService,
) {}
async createAppointment(data: CreateAppointmentDto) {
// Check for scheduling conflicts
const conflicts = await this.prisma.appointment.findMany({
where: {
practitionerId: data.practitionerId,
status: { not: 'CANCELLED' },
OR: [
{
AND: [
{ startTime: { lte: data.startTime } },
{ endTime: { gt: data.startTime } },
],
},
{
AND: [
{ startTime: { lt: data.endTime } },
{ endTime: { gte: data.endTime } },
],
},
],
},
});
if (conflicts.length > 0) {
throw new Error('Scheduling conflict detected');
}
const appointment = await this.prisma.appointment.create({
data: {
...data,
status: 'SCHEDULED',
},
include: {
patient: true,
practitioner: true,
},
});
// Send WhatsApp confirmation
await this.whatsapp.sendAppointmentConfirmation(appointment);
return appointment;
}
async getAvailableSlots(
practitionerId: string,
date: Date,
duration: number = 45,
) {
const startOfDay = new Date(date);
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date(date);
endOfDay.setHours(23, 59, 59, 999);
const existingAppointments = await this.prisma.appointment.findMany({
where: {
practitionerId,
startTime: { gte: startOfDay, lte: endOfDay },
status: { not: 'CANCELLED' },
},
orderBy: { startTime: 'asc' },
});
// Algorithm to find available slots
const workingHours = { start: 9, end: 20 }; // 9 AM to 8 PM
const slots: Date[] = [];
let currentTime = new Date(date);
currentTime.setHours(workingHours.start, 0, 0, 0);
const endTime = new Date(date);
endTime.setHours(workingHours.end, 0, 0, 0);
while (currentTime < endTime) {
const slotEnd = new Date(currentTime.getTime() + duration * 60000);
const hasConflict = existingAppointments.some(apt => {
return currentTime < apt.endTime && slotEnd > apt.startTime;
});
if (!hasConflict) {
slots.push(new Date(currentTime));
}
currentTime = new Date(currentTime.getTime() + 15 * 60000); // 15-min increments
}
return slots;
}
}
```
**SvelteKit Appointment Scheduler**:
```svelte
<!-- src/routes/appointments/+page.svelte -->
<script lang="ts">
import { onMount } from 'svelte';
import type { Appointment } from '$lib/types';
let selectedDate = $state(new Date());
let selectedPractitioner = $state('');
let availableSlots = $state<Date[]>([]);
let loading = $state(false);
async function loadAvailableSlots() {
loading = true;
try {
const response = await fetch('/api/appointments/available-slots', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
practitionerId: selectedPractitioner,
date: selectedDate.toISOString(),
duration: 45,
}),
});
availableSlots = await response.json();
} finally {
loading = false;
}
}
async function bookAppointment(slot: Date) {
const response = await fetch('/api/appointments', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
practitionerId: selectedPractitioner,
patientId: $page.data.currentPatient.id,
startTime: slot,
endTime: new Date(slot.getTime() + 45 * 60000),
sessionType: 'VIRTUAL',
}),
});
if (response.ok) {
alert('Cita agendada correctamente');
loadAvailableSlots();
}
}
$effect(() => {
if (selectedPractitioner && selectedDate) {
loadAvailableSlots();
}
});
</script>
<div class="appointment-scheduler">
<h2 class="text-2xl font-bold mb-4">Agendar Sesión</h2>
<input
type="date"
bind:value={selectedDate}
class="border rounded px-4 py-2 mb-4"
/>
{#if loading}
<p class="text-gray-500">Cargando horarios disponibles...</p>
{:else if availableSlots.length === 0}
<p class="text-gray-500">No hay horarios disponibles para esta fecha</p>
{:else}
<div class="grid grid-cols-4 gap-2">
{#each availableSlots as slot}
<button
onclick={() => bookAppointment(slot)}
class="border border-blue-500 text-blue-500 hover:bg-blue-500 hover:text-white rounded px-4 py-2 transition"
>
{slot.toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit' })}
</button>
{/each}
</div>
{/if}
</div>
```
### 2. WhatsApp Automation
**WhatsApp Service with Baileys**:
```typescript
// src/whatsapp/whatsapp.service.ts
import { Injectable } from '@nestjs/common';
import makeWASocket, {
useMultiFileAuthState,
DisconnectReason,
MessageType,
} from '@whiskeysockets/baileys';
import { Boom } from '@hapi/boom';
@Injectable()
export class WhatsAppService {
private socket: any;
private sessionPath = process.env.WHATSAPP_SESSION_PATH || './wa-session';
async initialize() {
const { state, saveCreds } = await useMultiFileAuthState(this.sessionPath);
this.socket = makeWASocket({
auth: state,
printQRInTerminal: true,
});
this.socket.ev.on('creds.update', saveCreds);
this.socket.ev.on('connection.update', (update) => {
const { connection, lastDisconnect } = update;
if (connection === 'close') {
const shouldReconnect = (lastDisconnect?.error as Boom)?.output?.statusCode !== DisconnectReason.loggedOut;
if (shouldReconnect) {
this.initialize(); // Reconnect
}
}
});
this.socket.ev.on('messages.upsert', async ({ messages }) => {
await this.handleIncomingMessages(messages);
});
}
async sendAppointmentConfirmation(appointment: any) {
const patient = appointment.patient;
const message = `Hola ${patient.firstName}! 👋\n\n` +
`Tu sesión con ${appointment.practitioner.name} ha sido confirmada:\n\n` +
`📅 Fecha: ${appointment.startTime.toLocaleDateString('es-AR')}\n` +
`🕐 Hora: ${appointment.startTime.toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit' })}\n` +
`💻 Modalidad: ${appointment.sessionType}\n\n` +
`Por favor responde *SI* para confirmar tu asistencia.`;
await this.socket.sendMessage(
`${patient.phoneNumber}@s.whatsapp.net`,
{ text: message },
);
}
async sendReminder24Hours(appointment: any) {
const patient = appointment.patient;
const message = `Recordatorio: Tu sesión es mañana a las ${appointment.startTime.toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit' })} 📆`;
await this.socket.sendMessage(
`${patient.phoneNumber}@s.whatsapp.net`,
{ text: message },
);
}
private async handleIncomingMessages(messages: any[]) {
for (const msg of messages) {
if (msg.key.fromMe) continue;
const text = msg.message?.conversation || msg.message?.extendedTextMessage?.text;
const from = msg.key.remoteJid;
// Crisis keywords detection
const crisisKeywords = ['suicidio', 'morir', 'acabar con todo', 'no puedo más'];
if (crisisKeywords.some(keyword => text?.toLowerCase().includes(keyword))) {
await this.escalateCrisis(from, text);
}
// Confirmation parsing
if (text?.toLowerCase() === 'si' || text?.toLowerCase() === 'sí') {
await this.handleConfirmation(from);
}
}
}
private async escalateCrisis(phoneNumber: string, message: string) {
// Notify practitioner immediately
// Log to crisis management system
console.error(`CRISIS ALERT from ${phoneNumber}: ${message}`);
// Trigger emergency notification workflow
}
Auf GitHub ansehen