- name
- psique-workflow-clinic
- description
- Sesión - Mental health practice management platform with AI orchestration, WhatsApp automation, AFIP billing, and video consultations for Argentine psychology clinics
- triggers
- ["how do I set up Sesión for a psychology clinic","configure WhatsApp automation for patient reminders","integrate AFIP electronic invoicing in Sesión","set up Claude AI for clinical note summarization","implement video consultation workflow","configure patient journey pipeline stages","automate appointment scheduling with conflict detection","integrate Mercado Pago payment processing"]
# Sesión Workflow Clinic
> Skill by [ara.so](https://ara.so) — Devtools Skills collection.
Sesión is a comprehensive SaaS platform for psychology clinics in Argentina that unifies appointment scheduling, WhatsApp automation, AFIP-compliant billing, secure video consultations, and AI-powered clinical assistance using Claude Opus 4.6 and Sonnet 4.6.
## Architecture Overview
Sesión uses a microservices architecture with:
- **Frontend**: SvelteKit 5 with TypeScript and TailwindCSS
- **Backend**: NestJS microservices
- **Database**: PostgreSQL with Prisma ORM
- **Cache**: Redis for sessions and real-time presence
- **Search**: Elasticsearch for patient records
- **Storage**: MinIO for encrypted documents
- **Messaging**: Apache Kafka for event-driven workflows
- **WhatsApp**: Baileys library for WhatsApp Business integration
- **Video**: LiveKit for WebRTC video consultations
- **AI**: Anthropic Claude Opus 4.6 and Sonnet 4.6
- **Payments**: Stripe and Mercado Pago integration
## Installation
### Prerequisites
```bash
# Required versions
node --version # v20+
npm --version # v10+
docker --version # v24+
```
### Clone and Install
```bash
git clone https://github.com/fahad-hamid/psique-workflow-clinic.git
cd psique-workflow-clinic
npm install
```
### Environment Configuration
Create `.env` file in project root:
```env
# Database
DATABASE_URL="postgresql://user:password@localhost:5432/sesion"
# Redis
REDIS_URL="redis://localhost:6379"
# Anthropic AI
ANTHROPIC_API_KEY="sk-ant-..."
CLAUDE_OPUS_MODEL="claude-opus-4.6"
CLAUDE_SONNET_MODEL="claude-sonnet-4.6"
# WhatsApp (Baileys)
WHATSAPP_SESSION_PATH="./whatsapp-sessions"
WHATSAPP_WEBHOOK_SECRET="your-webhook-secret"
# LiveKit Video
LIVEKIT_API_KEY="your-livekit-api-key"
LIVEKIT_API_SECRET="your-livekit-api-secret"
LIVEKIT_URL="wss://your-livekit-server.com"
# AFIP (Argentina Tax Authority)
AFIP_CUIT="20123456789"
AFIP_CERTIFICATE_PATH="./certificates/afip-cert.pem"
AFIP_PRIVATE_KEY_PATH="./certificates/afip-key.pem"
AFIP_ENVIRONMENT="production" # or "homologacion" for testing
# Payment Gateways
MERCADOPAGO_ACCESS_TOKEN="APP_USR-..."
STRIPE_SECRET_KEY="sk_live_..."
STRIPE_WEBHOOK_SECRET="whsec_..."
# Application
JWT_SECRET="your-jwt-secret"
FRONTEND_URL="https://app.sesion.com.ar"
BACKEND_URL="https://api.sesion.com.ar"
```
### Database Setup
```bash
# Generate Prisma client
npx prisma generate
# Run migrations
npx prisma migrate deploy
# Seed initial data (optional)
npx prisma db seed
```
### Start Services
```bash
# Development mode with hot reload
npm run dev
# Production build
npm run build
npm run start
# Docker Compose (all services)
docker-compose up -d
```
## Core Module Usage
### 1. Appointment Scheduling Engine
**Schema Definition** (Prisma):
```prisma
model Appointment {
id String @id @default(cuid())
patientId String
practitionerId String
sessionType SessionType
startTime DateTime
endTime DateTime
status AppointmentStatus
roomId String?
isVirtual Boolean @default(false)
notes String?
patient Patient @relation(fields: [patientId], references: [id])
practitioner Practitioner @relation(fields: [practitionerId], references: [id])
room Room? @relation(fields: [roomId], references: [id])
@@index([practitionerId, startTime])
@@index([patientId])
}
enum SessionType {
INDIVIDUAL
COUPLES
FAMILY
EVALUATION
FOLLOWUP
}
enum AppointmentStatus {
SCHEDULED
CONFIRMED
CANCELLED
COMPLETED
NO_SHOW
}
```
**Creating Appointments with Conflict Detection**:
```typescript
// services/appointment.service.ts
import { Injectable, ConflictException } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
@Injectable()
export class AppointmentService {
constructor(
private prisma: PrismaService,
private eventEmitter: EventEmitter2
) {}
async createAppointment(data: CreateAppointmentDto) {
// Check for scheduling conflicts
const conflicts = await this.findConflicts(
data.practitionerId,
data.startTime,
data.endTime,
data.roomId
);
if (conflicts.length > 0) {
throw new ConflictException({
message: 'Scheduling conflict detected',
conflicts: conflicts,
suggestions: await this.suggestAlternatives(data)
});
}
const appointment = await this.prisma.appointment.create({
data: {
patientId: data.patientId,
practitionerId: data.practitionerId,
sessionType: data.sessionType,
startTime: data.startTime,
endTime: data.endTime,
isVirtual: data.isVirtual,
roomId: data.roomId,
status: 'SCHEDULED'
},
include: {
patient: true,
practitioner: true
}
});
// Emit event for WhatsApp automation
this.eventEmitter.emit('appointment.created', appointment);
return appointment;
}
private async findConflicts(
practitionerId: string,
startTime: Date,
endTime: Date,
roomId?: string
) {
return this.prisma.appointment.findMany({
where: {
AND: [
{
OR: [
{ practitionerId },
{ roomId: roomId || undefined }
]
},
{
status: { in: ['SCHEDULED', 'CONFIRMED'] }
},
{
OR: [
{
AND: [
{ startTime: { lte: startTime } },
{ endTime: { gt: startTime } }
]
},
{
AND: [
{ startTime: { lt: endTime } },
{ endTime: { gte: endTime } }
]
},
{
AND: [
{ startTime: { gte: startTime } },
{ endTime: { lte: endTime } }
]
}
]
}
]
}
});
}
private async suggestAlternatives(data: CreateAppointmentDto) {
const duration = data.endTime.getTime() - data.startTime.getTime();
const searchStart = new Date(data.startTime);
searchStart.setDate(searchStart.getDate() - 1);
const searchEnd = new Date(data.startTime);
searchEnd.setDate(searchEnd.getDate() + 7);
// Find free slots in the next week
const allSlots = await this.generateTimeSlots(
data.practitionerId,
searchStart,
searchEnd,
duration
);
return allSlots.slice(0, 5);
}
}
```
### 2. WhatsApp Automation with Baileys
**WhatsApp Service Implementation**:
```typescript
// services/whatsapp.service.ts
import makeWASocket, {
DisconnectReason,
useMultiFileAuthState,
WAMessage
} from '@whiskeysockets/baileys';
import { Boom } from '@hapi/boom';
import { OnEvent } from '@nestjs/event-emitter';
@Injectable()
export class WhatsAppService {
private sock: any;
private qrCode: string;
async initialize() {
const { state, saveCreds } = await useMultiFileAuthState(
process.env.WHATSAPP_SESSION_PATH
);
this.sock = makeWASocket({
auth: state,
printQRInTerminal: true
});
this.sock.ev.on('connection.update', (update) => {
const { connection, lastDisconnect, qr } = update;
if (qr) {
this.qrCode = qr;
}
if (connection === 'close') {
const shouldReconnect = (lastDisconnect?.error as Boom)?.output
?.statusCode !== DisconnectReason.loggedOut;
if (shouldReconnect) {
this.initialize();
}
}
});
this.sock.ev.on('creds.update', saveCreds);
this.sock.ev.on('messages.upsert', this.handleIncomingMessage.bind(this));
}
@OnEvent('appointment.created')
async sendAppointmentConfirmation(appointment: any) {
const patient = appointment.patient;
const practitioner = appointment.practitioner;
const message = this.formatConfirmationMessage(appointment);
await this.sendMessage(patient.phoneNumber, message, {
buttons: [
{ buttonId: `confirm:${appointment.id}`, buttonText: { displayText: 'Confirmar ✅' } },
{ buttonId: `cancel:${appointment.id}`, buttonText: { displayText: 'Cancelar ❌' } }
]
});
}
@OnEvent('appointment.reminder.24h')
async send24HourReminder(appointment: any) {
const message = `
Hola ${appointment.patient.firstName}! 👋
Recordatorio: tenés turno mañana a las ${this.formatTime(appointment.startTime)} con ${appointment.practitioner.fullName}.
${appointment.isVirtual ? '📹 Sesión virtual - El link se enviará 15 minutos antes.' : '📍 Dirección: ' + appointment.practitioner.address}
¿Confirmás tu asistencia?
`.trim();
await this.sendMessage(appointment.patient.phoneNumber, message);
}
private async sendMessage(
phoneNumber: string,
text: string,
options?: any
) {
const jid = `${phoneNumber}@s.whatsapp.net`;
if (options?.buttons) {
await this.sock.sendMessage(jid, {
text,
footer: 'Sesión - Tu agenda inteligente',
buttons: options.buttons,
headerType: 1
});
} else {
await this.sock.sendMessage(jid, { text });
}
}
private async handleIncomingMessage(m: { messages: WAMessage[] }) {
const message = m.messages[0];
if (!message.message) return;
const text = message.message.conversation ||
message.message.extendedTextMessage?.text || '';
// Crisis detection keywords
const crisisKeywords = [
'suicidio', 'quitarme la vida', 'no puedo más',
'voy a hacerme daño', 'crisis', 'emergencia'
];
const isCrisis = crisisKeywords.some(keyword =>
text.toLowerCase().includes(keyword)
);
if (isCrisis) {
await this.escalateCrisis(message);
}
// Button response handling
if (message.message.buttonsResponseMessage) {
const buttonId = message.message.buttonsResponseMessage.selectedButtonId;
await this.handleButtonResponse(buttonId, message);
}
}
private formatTime(date: Date): string {
return new Intl.DateTimeFormat('es-AR', {
hour: '2-digit',
minute: '2-digit',
hour12: false
}).format(date);
}
}
```
### 3. AFIP Electronic Invoicing
**Invoice Generation Service**:
```typescript
// services/afip-billing.service.ts
import { Injectable } from '@nestjs/common';
import { readFileSync } from 'fs';
import { Afip } from '@afipsdk/afip.js';
@Injectable()
export class AfipBillingService {
private afip: Afip;
constructor() {
this.afip = new Afip({
CUIT: process.env.AFIP_CUIT,
cert: readFileSync(process.env.AFIP_CERTIFICATE_PATH, 'utf8'),
key: readFileSync(process.env.AFIP_PRIVATE_KEY_PATH, 'utf8'),
production: process.env.AFIP_ENVIRONMENT === 'production'
});
}
async generateInvoice(sessionData: GenerateInvoiceDto) {
// Determine invoice type based on practitioner's fiscal category
const invoiceType = this.determineInvoiceType(sessionData.practitionerFiscalCategory);
const lastVoucher = await this.afip.ElectronicBilling.getLastVoucher(
sessionData.salePoint,
invoiceType
);
const invoiceNumber = lastVoucher + 1;
const invoiceData = {
'CantReg': 1,
'PtoVta': sessionData.salePoint,
'CbteTipo': invoiceType,
'Concepto': 2, // Services
'DocTipo': 96, // DNI
'DocNro': sessionData.patientDni,
'CbteDesde': invoiceNumber,
'CbteHasta': invoiceNumber,
'CbteFch': this.formatAfipDate(new Date()),
'FchServDesde': this.formatAfipDate(sessionData.sessionDate),
'FchServHasta': this.formatAfipDate(sessionData.sessionDate),
'FchVtoPago': this.formatAfipDate(sessionData.dueDate),
'ImpTotal': sessionData.amount,
'ImpTotConc': 0,
'ImpNeto': sessionData.amount,
'ImpOpEx': 0,
'ImpIVA': 0,
'ImpTrib': 0,
'MonId': 'PES',
'MonCotiz': 1
};
const result = await this.afip.ElectronicBilling.createVoucher(invoiceData);
// Store invoice in database
const invoice = await this.prisma.invoice.create({
data: {
cae: result.CAE,
caeExpirationDate: result.CAEFchVto,
invoiceNumber: invoiceNumber,
invoiceType: invoiceType,
salePoint: sessionData.salePoint,
amount: sessionData.amount,
patientId: sessionData.patientId,
sessionId: sessionData.sessionId,
afipResponse: result
}
});
Ver no GitHub