- name
- sesion-clinic-workflow-platform
- description
- Mental health practice management platform with intelligent scheduling, WhatsApp automation, AFIP billing, video consultations, and Claude AI integration for Argentine psychologists
- triggers
- ["set up sesion clinic workflow platform","configure sesion appointment scheduling and whatsapp automation","integrate claude ai with sesion mental health platform","implement afip compliant invoicing with sesion","configure livekit video consultations in sesion","work with sesion psychology practice management","set up mercadopago billing in sesion","build sesion whatsapp patient communication workflows"]
# Sesión Clinic Workflow Platform
> Skill by [ara.so](https://ara.so) — Devtools Skills collection.
Sesión is a comprehensive SaaS platform for psychology clinics and independent practitioners in Argentina. It orchestrates appointment scheduling, automated WhatsApp patient communication, AFIP-compliant electronic invoicing, secure video consultations, and AI-powered clinical assistance using Claude Opus 4.6 and Sonnet 4.6 models.
## Architecture Overview
Sesión is built as a microservices ecosystem with:
- **Frontend**: SvelteKit 5 with TypeScript and TailwindCSS
- **Backend**: NestJS microservices
- **Database**: PostgreSQL with Prisma ORM
- **Cache/Sessions**: Redis
- **Search**: Elasticsearch
- **Storage**: MinIO (S3-compatible)
- **Messaging**: Apache Kafka for event-driven workflows
- **Video**: LiveKit WebRTC infrastructure
- **WhatsApp**: Baileys library for WhatsApp Web API
- **AI**: Anthropic Claude (Opus 4.6 for deep analysis, Sonnet 4.6 for real-time assistance)
- **Payments**: Stripe (international), Mercado Pago (Argentina)
## Installation and Setup
### Prerequisites
```bash
# Required tools
node >= 20.x
pnpm >= 8.x
docker >= 24.x
docker-compose >= 2.x
postgresql >= 15.x
redis >= 7.x
```
### Clone and Install Dependencies
```bash
git clone https://github.com/fahad-hamid/psique-workflow-clinic.git
cd psique-workflow-clinic
# Install dependencies
pnpm install
# Set up environment variables
cp .env.example .env
```
### Environment Configuration
Create a `.env` file with the following variables:
```env
# Database
DATABASE_URL="postgresql://user:password@localhost:5432/sesion_db"
REDIS_URL="redis://localhost:6379"
ELASTICSEARCH_URL="http://localhost:9200"
# MinIO Storage
MINIO_ENDPOINT="localhost"
MINIO_PORT=9000
MINIO_ACCESS_KEY="${MINIO_ACCESS_KEY}"
MINIO_SECRET_KEY="${MINIO_SECRET_KEY}"
MINIO_BUCKET="sesion-documents"
# Authentication
JWT_SECRET="${JWT_SECRET}"
JWT_EXPIRES_IN="15m"
REFRESH_TOKEN_SECRET="${REFRESH_TOKEN_SECRET}"
REFRESH_TOKEN_EXPIRES_IN="7d"
# Anthropic Claude AI
ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY}"
CLAUDE_OPUS_MODEL="claude-opus-4.6"
CLAUDE_SONNET_MODEL="claude-sonnet-4.6"
CLAUDE_MAX_TOKENS=4096
# LiveKit Video
LIVEKIT_API_KEY="${LIVEKIT_API_KEY}"
LIVEKIT_API_SECRET="${LIVEKIT_API_SECRET}"
LIVEKIT_WS_URL="wss://your-livekit-instance.com"
# WhatsApp (Baileys)
WHATSAPP_SESSION_PATH="./whatsapp-sessions"
WHATSAPP_WEBHOOK_SECRET="${WHATSAPP_WEBHOOK_SECRET}"
# Payment Gateways
STRIPE_SECRET_KEY="${STRIPE_SECRET_KEY}"
STRIPE_WEBHOOK_SECRET="${STRIPE_WEBHOOK_SECRET}"
MERCADOPAGO_ACCESS_TOKEN="${MERCADOPAGO_ACCESS_TOKEN}"
MERCADOPAGO_PUBLIC_KEY="${MERCADOPAGO_PUBLIC_KEY}"
# AFIP (Argentina Tax Authority)
AFIP_CUIT="${AFIP_CUIT}"
AFIP_CERT_PATH="./certs/afip.crt"
AFIP_KEY_PATH="./certs/afip.key"
AFIP_PRODUCTION=false
# Kafka
KAFKA_BROKERS="localhost:9092"
KAFKA_CLIENT_ID="sesion-clinic"
KAFKA_CONSUMER_GROUP="sesion-workers"
# Application
NODE_ENV="development"
PORT=3000
API_PORT=4000
FRONTEND_URL="http://localhost:3000"
BACKEND_URL="http://localhost:4000"
```
### Database Setup
```bash
# Generate Prisma client
pnpm prisma generate
# Run migrations
pnpm prisma migrate deploy
# Seed initial data (optional)
pnpm prisma db seed
```
### Start Services
```bash
# Start infrastructure (Docker Compose)
docker-compose up -d postgres redis elasticsearch kafka minio
# Start backend services
cd backend
pnpm run start:dev
# Start frontend (separate terminal)
cd frontend
pnpm run dev
```
## Key Components and Usage
### 1. Agenda Management (Appointment Scheduling)
#### Prisma Schema for Appointments
```typescript
// prisma/schema.prisma
model Appointment {
id String @id @default(cuid())
patientId String
practitionerId String
sessionType SessionType
startTime DateTime
endTime DateTime
status AppointmentStatus @default(SCHEDULED)
roomId String?
notes String?
reminderSent24h Boolean @default(false)
reminderSent2h Boolean @default(false)
confirmationToken String? @unique
patient Patient @relation(fields: [patientId], references: [id])
practitioner Practitioner @relation(fields: [practitionerId], references: [id])
room Room? @relation(fields: [roomId], references: [id])
invoice Invoice?
videoSession VideoSession?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([practitionerId, startTime])
@@index([patientId, startTime])
}
enum SessionType {
PRESENCIAL
VIRTUAL
EVALUACION
COUPLES
FAMILY
}
enum AppointmentStatus {
SCHEDULED
CONFIRMED
IN_PROGRESS
COMPLETED
CANCELLED
NO_SHOW
}
```
#### Appointment Service (NestJS)
```typescript
// backend/src/appointments/appointments.service.ts
import { Injectable, ConflictException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Appointment, SessionType } from '@prisma/client';
import { addMinutes, isBefore, isAfter } from 'date-fns';
@Injectable()
export class AppointmentsService {
constructor(
private prisma: PrismaService,
private eventEmitter: EventEmitter2,
) {}
async createAppointment(data: {
patientId: string;
practitionerId: string;
sessionType: SessionType;
startTime: Date;
roomId?: string;
}): Promise<Appointment> {
// Determine session duration based on type
const duration = this.getSessionDuration(data.sessionType);
const endTime = addMinutes(data.startTime, duration);
// Check for conflicts
await this.checkAvailability(
data.practitionerId,
data.startTime,
endTime,
data.roomId,
);
const appointment = await this.prisma.appointment.create({
data: {
...data,
endTime,
confirmationToken: this.generateConfirmationToken(),
},
include: {
patient: true,
practitioner: true,
},
});
// Emit event for downstream processing (WhatsApp, calendar sync)
this.eventEmitter.emit('appointment.created', appointment);
return appointment;
}
private getSessionDuration(sessionType: SessionType): number {
const durations = {
PRESENCIAL: 45,
VIRTUAL: 45,
EVALUACION: 60,
COUPLES: 90,
FAMILY: 90,
};
return durations[sessionType] || 45;
}
private async checkAvailability(
practitionerId: string,
startTime: Date,
endTime: Date,
roomId?: string,
): Promise<void> {
// Check practitioner availability
const practitionerConflicts = await this.prisma.appointment.findMany({
where: {
practitionerId,
status: { notIn: ['CANCELLED', 'NO_SHOW'] },
OR: [
{
startTime: { lte: startTime },
endTime: { gt: startTime },
},
{
startTime: { lt: endTime },
endTime: { gte: endTime },
},
],
},
});
if (practitionerConflicts.length > 0) {
throw new ConflictException(
'Practitioner has a conflicting appointment',
);
}
// Check room availability if specified
if (roomId) {
const roomConflicts = await this.prisma.appointment.findMany({
where: {
roomId,
status: { notIn: ['CANCELLED', 'NO_SHOW'] },
OR: [
{
startTime: { lte: startTime },
endTime: { gt: startTime },
},
{
startTime: { lt: endTime },
endTime: { gte: endTime },
},
],
},
});
if (roomConflicts.length > 0) {
throw new ConflictException('Room is not available at this time');
}
}
}
private generateConfirmationToken(): string {
return Math.random().toString(36).substring(2, 15);
}
async suggestAlternativeSlots(
practitionerId: string,
preferredDate: Date,
sessionType: SessionType,
): Promise<Date[]> {
const duration = this.getSessionDuration(sessionType);
const suggestions: Date[] = [];
// Get practitioner's working hours
const workingHours = await this.prisma.practitionerSchedule.findMany({
where: { practitionerId },
});
// Find available slots within 7 days
// Implementation details for slot suggestion algorithm
return suggestions;
}
}
```
### 2. WhatsApp Automation with Baileys
#### WhatsApp Service
```typescript
// backend/src/whatsapp/whatsapp.service.ts
import { Injectable, Logger } from '@nestjs/common';
import makeWASocket, {
DisconnectReason,
useMultiFileAuthState,
WAMessage,
} from '@whiskeysockets/baileys';
import { Boom } from '@hapi/boom';
import { OnEvent } from '@nestjs/event-emitter';
import { Appointment } from '@prisma/client';
import { format, subHours } from 'date-fns';
import { es } from 'date-fns/locale';
@Injectable()
export class WhatsAppService {
private sock: any;
private readonly logger = new Logger(WhatsAppService.name);
async initialize() {
const { state, saveCreds } = await useMultiFileAuthState(
process.env.WHATSAPP_SESSION_PATH,
);
this.sock = makeWASocket({
auth: state,
printQRInTerminal: true,
});
this.sock.ev.on('creds.update', saveCreds);
this.sock.ev.on('connection.update', (update: any) => {
const { connection, lastDisconnect } = update;
if (connection === 'close') {
const shouldReconnect =
(lastDisconnect?.error as Boom)?.output?.statusCode !==
DisconnectReason.loggedOut;
if (shouldReconnect) {
this.initialize();
}
} else if (connection === 'open') {
this.logger.log('WhatsApp connection opened successfully');
}
});
this.sock.ev.on('messages.upsert', async ({ messages }: any) => {
await this.handleIncomingMessage(messages[0]);
});
}
private async handleIncomingMessage(message: WAMessage) {
const messageText = message.message?.conversation || '';
const from = message.key.remoteJid;
// Crisis keyword detection
const crisisKeywords = [
'suicidio',
'hacerme daño',
'no aguanto más',
'crisis',
'emergencia',
];
const isCrisis = crisisKeywords.some((keyword) =>
messageText.toLowerCase().includes(keyword),
);
if (isCrisis) {
// Alert practitioner immediately
this.logger.warn(`Crisis message detected from ${from}`);
// Trigger emergency protocol
}
// Parse confirmation responses
if (messageText.toLowerCase().includes('confirmo')) {
await this.handleAppointmentConfirmation(from, message);
}
}
@OnEvent('appointment.created')
async handleAppointmentCreated(appointment: Appointment & any) {
// Schedule reminder 24 hours before
const reminderTime24h = subHours(appointment.startTime, 24);
// Use job scheduler (e.g., Bull) to send reminder at reminderTime24h
}
async sendAppointmentReminder(appointment: Appointment & any) {
const formattedDate = format(appointment.startTime, "EEEE d 'de' MMMM", {
locale: es,
});
const formattedTime = format(appointment.startTime, 'HH:mm');
const message = `Hola ${appointment.patient.firstName},
Te recordamos tu sesión ${appointment.sessionType.toLowerCase()} con ${appointment.practitioner.firstName} ${appointment.practitioner.lastName}.
📅 Fecha: ${formattedDate}
🕐 Hora: ${formattedTime}
${appointment.sessionType === 'VIRTUAL' ? '💻 Tipo: Videollamada' : '🏥 Tipo: Presencial'}
Por favor, confirmá tu asistencia respondiendo "CONFIRMO" a este mensaje.
Si necesitás reprogramar, comunicate con nosotros.
Equipo Sesión`;
await this.sendMessage(appointment.patient.phone, message);
}
async sendInvoice(invoiceData: any) {
const message = `Hola ${invoiceData.patient.firstName},
Adjunto encontrarás tu comprobante de sesión:
📄 Factura: ${invoiceData.invoiceNumber}
💰 Monto: $${invoiceData.amount}
📅 Fecha: ${format(invoiceData.date, 'dd/MM/yyyy')}
Gracias por confiar en nosotros.
Equipo Sesión`;
await this.sendMessage(invoiceData.patient.phone, message);
// Send PDF invoice
if (invoiceData.pdfBuffer) {
await this.sock.sendMessage(invoiceData.patient.phone, {
document: invoiceData.pdfBuffer,
mimetype: 'application/pdf',
fileName: `factura-${invoiceData.invoiceNumber}.pdf`,
});
Ver en GitHub