FASE 4 COMPLETADA: Pagos y Monetización con MercadoPago

Implementados 4 módulos con agent swarm:

1. MERCADOPAGO INTEGRADO
   - SDK oficial de MercadoPago
   - Crear preferencias de pago
   - Webhooks para notificaciones
   - Reembolsos y cancelaciones
   - Estados: PENDING, PROCESSING, COMPLETED, REFUNDED

2. SISTEMA DE BONOS Y PACKS
   - Pack 5, Pack 10, Pack Mensual
   - Compra online con MP
   - Uso FIFO automático
   - Control de expiración
   - Aplicación en reservas

3. SUSCRIPCIONES/MEMBRESÍAS
   - Planes: Básico, Premium, Anual VIP
   - Beneficios: descuentos, reservas gratis, prioridad
   - Cobro recurrente vía MP
   - Estados: ACTIVE, PAUSED, CANCELLED
   - Aplicación automática en reservas

4. CLASES CON PROFESORES
   - Registro de coaches con verificación
   - Tipos: Individual, Grupal, Clínica
   - Horarios y disponibilidad
   - Reservas con pago integrado
   - Sistema de reseñas

Endpoints nuevos:
- /payments/* - Pagos MercadoPago
- /bonus-packs/*, /bonuses/* - Bonos
- /subscription-plans/*, /subscriptions/* - Suscripciones
- /coaches/* - Profesores
- /classes/*, /class-enrollments/* - Clases

Variables de entorno:
- MERCADOPAGO_ACCESS_TOKEN
- MERCADOPAGO_PUBLIC_KEY
- MERCADOPAGO_WEBHOOK_SECRET

Datos de prueba:
- 3 Bonus Packs
- 3 Planes de suscripción
- 1 Coach verificado (admin)
- 3 Clases disponibles
This commit is contained in:
2026-01-31 09:02:25 +00:00
parent 6494e2b38b
commit b8a964dc2c
44 changed files with 7084 additions and 9 deletions

Binary file not shown.

View File

@@ -0,0 +1,166 @@
-- CreateTable
CREATE TABLE "payments" (
"id" TEXT NOT NULL PRIMARY KEY,
"userId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"referenceId" TEXT NOT NULL,
"amount" INTEGER NOT NULL,
"currency" TEXT NOT NULL DEFAULT 'ARS',
"provider" TEXT NOT NULL DEFAULT 'MERCADOPAGO',
"providerPaymentId" TEXT,
"providerPreferenceId" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'PENDING',
"paymentMethod" TEXT,
"installments" INTEGER,
"metadata" TEXT,
"paidAt" DATETIME,
"refundedAt" DATETIME,
"refundAmount" INTEGER,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "payments_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "bonus_packs" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"description" TEXT,
"numberOfBookings" INTEGER NOT NULL,
"price" INTEGER NOT NULL,
"validityDays" INTEGER NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
-- CreateTable
CREATE TABLE "user_bonuses" (
"id" TEXT NOT NULL PRIMARY KEY,
"userId" TEXT NOT NULL,
"bonusPackId" TEXT NOT NULL,
"totalBookings" INTEGER NOT NULL,
"usedBookings" INTEGER NOT NULL DEFAULT 0,
"remainingBookings" INTEGER NOT NULL,
"purchaseDate" DATETIME NOT NULL,
"expirationDate" DATETIME NOT NULL,
"status" TEXT NOT NULL DEFAULT 'ACTIVE',
"paymentId" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "user_bonuses_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "user_bonuses_bonusPackId_fkey" FOREIGN KEY ("bonusPackId") REFERENCES "bonus_packs" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "bonus_usages" (
"id" TEXT NOT NULL PRIMARY KEY,
"userBonusId" TEXT NOT NULL,
"bookingId" TEXT NOT NULL,
"usedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "bonus_usages_userBonusId_fkey" FOREIGN KEY ("userBonusId") REFERENCES "user_bonuses" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "bonus_usages_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "bookings" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "subscription_plans" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"description" TEXT,
"type" TEXT NOT NULL,
"price" INTEGER NOT NULL,
"features" TEXT,
"benefits" TEXT NOT NULL,
"mercadoPagoPlanId" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
-- CreateTable
CREATE TABLE "user_subscriptions" (
"id" TEXT NOT NULL PRIMARY KEY,
"userId" TEXT NOT NULL,
"planId" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'PENDING',
"startDate" DATETIME,
"endDate" DATETIME,
"currentPeriodStart" DATETIME,
"currentPeriodEnd" DATETIME,
"cancelAtPeriodEnd" BOOLEAN NOT NULL DEFAULT false,
"mercadoPagoSubscriptionId" TEXT,
"paymentMethodId" TEXT,
"lastPaymentDate" DATETIME,
"nextPaymentDate" DATETIME,
"freeBookingsUsed" INTEGER NOT NULL DEFAULT 0,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "user_subscriptions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "user_subscriptions_planId_fkey" FOREIGN KEY ("planId") REFERENCES "subscription_plans" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "payments_providerPreferenceId_key" ON "payments"("providerPreferenceId");
-- CreateIndex
CREATE INDEX "payments_userId_idx" ON "payments"("userId");
-- CreateIndex
CREATE INDEX "payments_status_idx" ON "payments"("status");
-- CreateIndex
CREATE INDEX "payments_type_referenceId_idx" ON "payments"("type", "referenceId");
-- CreateIndex
CREATE INDEX "payments_providerPaymentId_idx" ON "payments"("providerPaymentId");
-- CreateIndex
CREATE INDEX "payments_providerPreferenceId_idx" ON "payments"("providerPreferenceId");
-- CreateIndex
CREATE INDEX "payments_createdAt_idx" ON "payments"("createdAt");
-- CreateIndex
CREATE INDEX "bonus_packs_isActive_idx" ON "bonus_packs"("isActive");
-- CreateIndex
CREATE INDEX "user_bonuses_userId_idx" ON "user_bonuses"("userId");
-- CreateIndex
CREATE INDEX "user_bonuses_status_idx" ON "user_bonuses"("status");
-- CreateIndex
CREATE INDEX "user_bonuses_expirationDate_idx" ON "user_bonuses"("expirationDate");
-- CreateIndex
CREATE INDEX "user_bonuses_userId_status_idx" ON "user_bonuses"("userId", "status");
-- CreateIndex
CREATE INDEX "bonus_usages_userBonusId_idx" ON "bonus_usages"("userBonusId");
-- CreateIndex
CREATE INDEX "bonus_usages_usedAt_idx" ON "bonus_usages"("usedAt");
-- CreateIndex
CREATE UNIQUE INDEX "bonus_usages_bookingId_key" ON "bonus_usages"("bookingId");
-- CreateIndex
CREATE INDEX "subscription_plans_type_idx" ON "subscription_plans"("type");
-- CreateIndex
CREATE INDEX "subscription_plans_isActive_idx" ON "subscription_plans"("isActive");
-- CreateIndex
CREATE INDEX "user_subscriptions_userId_idx" ON "user_subscriptions"("userId");
-- CreateIndex
CREATE INDEX "user_subscriptions_planId_idx" ON "user_subscriptions"("planId");
-- CreateIndex
CREATE INDEX "user_subscriptions_status_idx" ON "user_subscriptions"("status");
-- CreateIndex
CREATE INDEX "user_subscriptions_mercadoPagoSubscriptionId_idx" ON "user_subscriptions"("mercadoPagoSubscriptionId");
-- CreateIndex
CREATE UNIQUE INDEX "user_subscriptions_userId_status_key" ON "user_subscriptions"("userId", "status");

View File

@@ -0,0 +1,178 @@
/*
Warnings:
- You are about to drop the `subscription_plans` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `user_subscriptions` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropTable
PRAGMA foreign_keys=off;
DROP TABLE "subscription_plans";
PRAGMA foreign_keys=on;
-- DropTable
PRAGMA foreign_keys=off;
DROP TABLE "user_subscriptions";
PRAGMA foreign_keys=on;
-- CreateTable
CREATE TABLE "coaches" (
"id" TEXT NOT NULL PRIMARY KEY,
"userId" TEXT NOT NULL,
"bio" TEXT,
"specialties" TEXT,
"certifications" TEXT,
"yearsExperience" INTEGER NOT NULL DEFAULT 0,
"hourlyRate" INTEGER NOT NULL DEFAULT 0,
"photoUrl" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"isVerified" BOOLEAN NOT NULL DEFAULT false,
"rating" REAL,
"reviewCount" INTEGER NOT NULL DEFAULT 0,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "coaches_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "coach_availabilities" (
"id" TEXT NOT NULL PRIMARY KEY,
"coachId" TEXT NOT NULL,
"dayOfWeek" INTEGER NOT NULL,
"startTime" TEXT NOT NULL,
"endTime" TEXT NOT NULL,
"isAvailable" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "coach_availabilities_coachId_fkey" FOREIGN KEY ("coachId") REFERENCES "coaches" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "classes" (
"id" TEXT NOT NULL PRIMARY KEY,
"coachId" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT,
"type" TEXT NOT NULL DEFAULT 'INDIVIDUAL',
"maxStudents" INTEGER NOT NULL DEFAULT 1,
"price" INTEGER NOT NULL DEFAULT 0,
"duration" INTEGER NOT NULL DEFAULT 60,
"levelRequired" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "classes_coachId_fkey" FOREIGN KEY ("coachId") REFERENCES "coaches" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "class_bookings" (
"id" TEXT NOT NULL PRIMARY KEY,
"classId" TEXT NOT NULL,
"coachId" TEXT NOT NULL,
"courtId" TEXT,
"date" DATETIME NOT NULL,
"startTime" TEXT NOT NULL,
"students" TEXT NOT NULL DEFAULT '[]',
"maxStudents" INTEGER NOT NULL DEFAULT 1,
"enrolledStudents" INTEGER NOT NULL DEFAULT 0,
"status" TEXT NOT NULL DEFAULT 'AVAILABLE',
"price" INTEGER NOT NULL DEFAULT 0,
"paymentId" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "class_bookings_classId_fkey" FOREIGN KEY ("classId") REFERENCES "classes" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "class_bookings_coachId_fkey" FOREIGN KEY ("coachId") REFERENCES "coaches" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "class_bookings_courtId_fkey" FOREIGN KEY ("courtId") REFERENCES "courts" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "student_enrollments" (
"id" TEXT NOT NULL PRIMARY KEY,
"classBookingId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"paymentId" TEXT,
"status" TEXT NOT NULL DEFAULT 'PENDING',
"enrolledAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"cancelledAt" DATETIME,
CONSTRAINT "student_enrollments_classBookingId_fkey" FOREIGN KEY ("classBookingId") REFERENCES "class_bookings" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "student_enrollments_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "coach_reviews" (
"id" TEXT NOT NULL PRIMARY KEY,
"coachId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"rating" INTEGER NOT NULL,
"comment" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "coach_reviews_coachId_fkey" FOREIGN KEY ("coachId") REFERENCES "coaches" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "coach_reviews_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "coaches_userId_key" ON "coaches"("userId");
-- CreateIndex
CREATE INDEX "coaches_isActive_idx" ON "coaches"("isActive");
-- CreateIndex
CREATE INDEX "coaches_isVerified_idx" ON "coaches"("isVerified");
-- CreateIndex
CREATE INDEX "coaches_userId_idx" ON "coaches"("userId");
-- CreateIndex
CREATE INDEX "coach_availabilities_coachId_idx" ON "coach_availabilities"("coachId");
-- CreateIndex
CREATE INDEX "coach_availabilities_coachId_dayOfWeek_idx" ON "coach_availabilities"("coachId", "dayOfWeek");
-- CreateIndex
CREATE INDEX "coach_availabilities_dayOfWeek_idx" ON "coach_availabilities"("dayOfWeek");
-- CreateIndex
CREATE INDEX "classes_coachId_idx" ON "classes"("coachId");
-- CreateIndex
CREATE INDEX "classes_type_idx" ON "classes"("type");
-- CreateIndex
CREATE INDEX "classes_isActive_idx" ON "classes"("isActive");
-- CreateIndex
CREATE INDEX "class_bookings_classId_idx" ON "class_bookings"("classId");
-- CreateIndex
CREATE INDEX "class_bookings_coachId_idx" ON "class_bookings"("coachId");
-- CreateIndex
CREATE INDEX "class_bookings_courtId_idx" ON "class_bookings"("courtId");
-- CreateIndex
CREATE INDEX "class_bookings_date_idx" ON "class_bookings"("date");
-- CreateIndex
CREATE INDEX "class_bookings_status_idx" ON "class_bookings"("status");
-- CreateIndex
CREATE INDEX "student_enrollments_userId_idx" ON "student_enrollments"("userId");
-- CreateIndex
CREATE INDEX "student_enrollments_status_idx" ON "student_enrollments"("status");
-- CreateIndex
CREATE INDEX "student_enrollments_classBookingId_idx" ON "student_enrollments"("classBookingId");
-- CreateIndex
CREATE UNIQUE INDEX "student_enrollments_classBookingId_userId_key" ON "student_enrollments"("classBookingId", "userId");
-- CreateIndex
CREATE INDEX "coach_reviews_coachId_idx" ON "coach_reviews"("coachId");
-- CreateIndex
CREATE INDEX "coach_reviews_userId_idx" ON "coach_reviews"("userId");
-- CreateIndex
CREATE INDEX "coach_reviews_rating_idx" ON "coach_reviews"("rating");
-- CreateIndex
CREATE UNIQUE INDEX "coach_reviews_coachId_userId_key" ON "coach_reviews"("coachId", "userId");

View File

@@ -0,0 +1,57 @@
-- CreateTable
CREATE TABLE "subscription_plans" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"description" TEXT,
"type" TEXT NOT NULL,
"price" INTEGER NOT NULL,
"features" TEXT,
"benefits" TEXT NOT NULL,
"mercadoPagoPlanId" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
-- CreateTable
CREATE TABLE "user_subscriptions" (
"id" TEXT NOT NULL PRIMARY KEY,
"userId" TEXT NOT NULL,
"planId" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'PENDING',
"startDate" DATETIME,
"endDate" DATETIME,
"currentPeriodStart" DATETIME,
"currentPeriodEnd" DATETIME,
"cancelAtPeriodEnd" BOOLEAN NOT NULL DEFAULT false,
"mercadoPagoSubscriptionId" TEXT,
"paymentMethodId" TEXT,
"lastPaymentDate" DATETIME,
"nextPaymentDate" DATETIME,
"freeBookingsUsed" INTEGER NOT NULL DEFAULT 0,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "user_subscriptions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "user_subscriptions_planId_fkey" FOREIGN KEY ("planId") REFERENCES "subscription_plans" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE INDEX "subscription_plans_type_idx" ON "subscription_plans"("type");
-- CreateIndex
CREATE INDEX "subscription_plans_isActive_idx" ON "subscription_plans"("isActive");
-- CreateIndex
CREATE INDEX "user_subscriptions_userId_idx" ON "user_subscriptions"("userId");
-- CreateIndex
CREATE INDEX "user_subscriptions_planId_idx" ON "user_subscriptions"("planId");
-- CreateIndex
CREATE INDEX "user_subscriptions_status_idx" ON "user_subscriptions"("status");
-- CreateIndex
CREATE INDEX "user_subscriptions_mercadoPagoSubscriptionId_idx" ON "user_subscriptions"("mercadoPagoSubscriptionId");
-- CreateIndex
CREATE UNIQUE INDEX "user_subscriptions_userId_status_key" ON "user_subscriptions"("userId", "status");

View File

@@ -76,6 +76,20 @@ model User {
tournamentsCreated Tournament[] @relation("TournamentsCreated")
tournamentParticipations TournamentParticipant[]
// Bonos (Fase 4.2)
userBonuses UserBonus[]
// Pagos (Fase 4.1)
payments Payment[]
// Suscripciones (Fase 4.3)
subscriptions UserSubscription[]
// Clases con profesores (Fase 4.4)
coach Coach?
studentEnrollments StudentEnrollment[]
coachReviews CoachReview[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -132,6 +146,7 @@ model Court {
recurringBookings RecurringBooking[]
leagueMatches LeagueMatch[]
tournamentMatches TournamentMatch[]
classBookings ClassBooking[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -193,6 +208,9 @@ model Booking {
recurringBooking RecurringBooking? @relation(fields: [recurringBookingId], references: [id])
recurringBookingId String?
// Uso de bonos
bonusUsages BonusUsage[]
// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -757,3 +775,449 @@ model LeagueStanding {
@@index([points])
@@map("league_standings")
}
// ============================================
// Modelo de Pagos (Fase 4.1)
// ============================================
model Payment {
id String @id @default(uuid())
// Usuario que realiza el pago
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String
// Tipo de pago: BOOKING, TOURNAMENT, BONUS, SUBSCRIPTION, CLASS
type String
// ID de la entidad relacionada (booking, tournament, etc.)
referenceId String
// Monto en centavos (para evitar decimales)
amount Int
// Moneda (ARS, MXN, etc.)
currency String @default("ARS")
// Proveedor de pago
provider String @default("MERCADOPAGO")
// IDs de MercadoPago
providerPaymentId String? // ID del pago en MP (cuando se confirma)
providerPreferenceId String @unique // ID de la preferencia MP
// Estado del pago
status String @default("PENDING") // PENDING, PROCESSING, COMPLETED, FAILED, REFUNDED, CANCELLED
// Información del método de pago
paymentMethod String?
installments Int? // Cantidad de cuotas
// Metadata adicional (JSON)
metadata String?
// Fechas
paidAt DateTime?
refundedAt DateTime?
refundAmount Int? // Monto reembolsado en centavos
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@index([status])
@@index([type, referenceId])
@@index([providerPaymentId])
@@index([providerPreferenceId])
@@index([createdAt])
@@map("payments")
}
// ============================================
// Modelos de Sistema de Bonos (Fase 4.2)
// ============================================
// Modelo de Pack de Bonos (tipos de bonos disponibles)
model BonusPack {
id String @id @default(uuid())
name String
description String?
// Configuración del bono
numberOfBookings Int // Cantidad de reservas incluidas
price Int // Precio del bono en centavos
validityDays Int // Días de validez desde la compra
// Estado
isActive Boolean @default(true)
// Relaciones
userBonuses UserBonus[]
// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([isActive])
@@map("bonus_packs")
}
// Modelo de Bono de Usuario (bonos comprados)
model UserBonus {
id String @id @default(uuid())
// Relaciones
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String
bonusPack BonusPack @relation(fields: [bonusPackId], references: [id])
bonusPackId String
// Uso del bono
totalBookings Int
usedBookings Int @default(0)
remainingBookings Int
// Fechas
purchaseDate DateTime
expirationDate DateTime
// Estado: ACTIVE, EXPIRED, DEPLETED
status String @default("ACTIVE")
// Referencia al pago
paymentId String?
// Relaciones
usages BonusUsage[]
// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@index([status])
@@index([expirationDate])
@@index([userId, status])
@@map("user_bonuses")
}
// Modelo de Uso de Bono (registro de usos)
model BonusUsage {
id String @id @default(uuid())
// Relaciones
userBonus UserBonus @relation(fields: [userBonusId], references: [id], onDelete: Cascade)
userBonusId String
// Reserva asociada
booking Booking @relation(fields: [bookingId], references: [id])
bookingId String
// Fecha de uso
usedAt DateTime @default(now())
@@unique([bookingId])
@@index([userBonusId])
@@index([usedAt])
@@map("bonus_usages")
}
// ============================================
// Modelos de Sistema de Suscripciones (Fase 4.3)
// ============================================
// Modelo de Plan de Suscripción (planes disponibles)
model SubscriptionPlan {
id String @id @default(uuid())
name String
description String?
// Tipo de plan
type String // MONTHLY, QUARTERLY, YEARLY
// Precio en centavos
price Int
// Características (JSON array de strings)
features String? // Ej: ["Reservas ilimitadas", "Prioridad en reservas"]
// Beneficios del plan (almacenados como JSON)
// discountPercentage: porcentaje de descuento en reservas
// freeBookingsPerMonth: cantidad de reservas gratis por mes
// priorityBooking: prioridad en reservas
// tournamentDiscount: descuento en torneos
benefits String // JSON: { discountPercentage, freeBookingsPerMonth, priorityBooking, tournamentDiscount }
// ID del plan en MercadoPago
mercadoPagoPlanId String?
// Estado
isActive Boolean @default(true)
// Relaciones
subscriptions UserSubscription[]
// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([type])
@@index([isActive])
@@map("subscription_plans")
}
// Modelo de Suscripción de Usuario
model UserSubscription {
id String @id @default(uuid())
// Usuario
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String
// Plan
plan SubscriptionPlan @relation(fields: [planId], references: [id])
planId String
// Estado: PENDING, ACTIVE, PAUSED, CANCELLED, EXPIRED
status String @default("PENDING")
// Fechas de suscripción
startDate DateTime?
endDate DateTime?
// Período actual
currentPeriodStart DateTime?
currentPeriodEnd DateTime?
// Cancelar al final del período
cancelAtPeriodEnd Boolean @default(false)
// Referencia a MercadoPago
mercadoPagoSubscriptionId String?
// Método de pago vinculado
paymentMethodId String?
// Fechas de pagos
lastPaymentDate DateTime?
nextPaymentDate DateTime?
// Contador de reservas gratis usadas en el período actual
freeBookingsUsed Int @default(0)
// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([userId, status])
@@index([userId])
@@index([planId])
@@index([status])
@@index([mercadoPagoSubscriptionId])
@@map("user_subscriptions")
}
// ============================================
// Modelos de Clases con Profesores (Fase 4.4)
// ============================================
// Modelo de Profesor (Coach)
model Coach {
id String @id @default(uuid())
// Relación con usuario
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String @unique
// Perfil profesional
bio String?
specialties String? // JSON array de especialidades
certifications String? // JSON array de certificaciones
yearsExperience Int @default(0)
hourlyRate Int @default(0) // en centavos
photoUrl String?
// Estado
isActive Boolean @default(true)
isVerified Boolean @default(false)
// Calificaciones
rating Float?
reviewCount Int @default(0)
// Relaciones
availabilities CoachAvailability[]
classes Class[]
classBookings ClassBooking[]
coachReviews CoachReview[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([isActive])
@@index([isVerified])
@@index([userId])
@@map("coaches")
}
// Modelo de Disponibilidad del Coach
model CoachAvailability {
id String @id @default(uuid())
// Relación con coach
coach Coach @relation(fields: [coachId], references: [id], onDelete: Cascade)
coachId String
// Día de la semana (0=Domingo, 1=Lunes, ..., 6=Sábado)
dayOfWeek Int
// Horario
startTime String
endTime String
// Estado
isAvailable Boolean @default(true)
@@index([coachId])
@@index([coachId, dayOfWeek])
@@index([dayOfWeek])
@@map("coach_availabilities")
}
// Modelo de Clase (programa/tipo de clase)
model Class {
id String @id @default(uuid())
// Relación con coach
coach Coach @relation(fields: [coachId], references: [id], onDelete: Cascade)
coachId String
// Información de la clase
title String
description String?
// Tipo: INDIVIDUAL, GROUP, CLINIC
type String @default("INDIVIDUAL")
// Configuración
maxStudents Int @default(1) // Máximo de alumnos
price Int @default(0) // Precio por persona en centavos
duration Int @default(60) // Duración en minutos
// Nivel mínimo requerido
levelRequired String?
// Estado
isActive Boolean @default(true)
// Relaciones
sessions ClassBooking[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([coachId])
@@index([type])
@@index([isActive])
@@map("classes")
}
// Modelo de Sesión de Clase (instancia específica de una clase)
model ClassBooking {
id String @id @default(uuid())
// Relaciones
class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
classId String
coach Coach @relation(fields: [coachId], references: [id])
coachId String
court Court? @relation(fields: [courtId], references: [id], onDelete: SetNull)
courtId String?
// Fecha y hora
date DateTime
startTime String
// Estudiantes (JSON array de userIds)
students String @default("[]")
// Cupo
maxStudents Int @default(1)
enrolledStudents Int @default(0)
// Estado: AVAILABLE, FULL, COMPLETED, CANCELLED
status String @default("AVAILABLE")
// Precio
price Int @default(0)
// Pago
paymentId String?
// Relaciones
enrollments StudentEnrollment[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([classId])
@@index([coachId])
@@index([courtId])
@@index([date])
@@index([status])
@@map("class_bookings")
}
// Modelo de Inscripción de Estudiante
model StudentEnrollment {
id String @id @default(uuid())
// Relaciones
classBooking ClassBooking @relation(fields: [classBookingId], references: [id], onDelete: Cascade)
classBookingId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String
// Referencia al pago
paymentId String?
// Estado: PENDING, CONFIRMED, CANCELLED, ATTENDED
status String @default("PENDING")
// Timestamps
enrolledAt DateTime @default(now())
cancelledAt DateTime?
@@unique([classBookingId, userId])
@@index([userId])
@@index([status])
@@index([classBookingId])
@@map("student_enrollments")
}
// Modelo de Reseña de Coach
model CoachReview {
id String @id @default(uuid())
// Relaciones
coach Coach @relation(fields: [coachId], references: [id], onDelete: Cascade)
coachId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String
// Calificación (1-5)
rating Int
comment String?
createdAt DateTime @default(now())
@@unique([coachId, userId])
@@index([coachId])
@@index([userId])
@@index([rating])
@@map("coach_reviews")
}

View File

@@ -0,0 +1,159 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
console.log('🌱 Seeding Fase 4 - Pagos y Monetización...\n');
const admin = await prisma.user.findUnique({ where: { email: 'admin@padel.com' } });
const user = await prisma.user.findUnique({ where: { email: 'user@padel.com' } });
if (!admin || !user) {
console.log('❌ Usuarios no encontrados. Ejecuta seed.ts primero.');
return;
}
// Crear Bonus Packs
const bonusPacks = [
{ name: 'Pack 5 Clases', bookings: 5, price: 9000, validity: 90 },
{ name: 'Pack 10 Clases', bookings: 10, price: 16000, validity: 180 },
{ name: 'Pack Mensual', bookings: 30, price: 40000, validity: 30 },
];
for (const pack of bonusPacks) {
await prisma.bonusPack.upsert({
where: { id: `bonus-${pack.bookings}` },
update: {},
create: {
id: `bonus-${pack.bookings}`,
name: pack.name,
description: `${pack.bookings} reservas de 1 hora cada una`,
numberOfBookings: pack.bookings,
price: pack.price,
validityDays: pack.validity,
isActive: true,
},
});
console.log(`✅ Bonus Pack creado: ${pack.name}`);
}
// Crear planes de suscripción
const plans = [
{
name: 'Básico',
type: 'MONTHLY',
price: 15000,
benefits: { discountPercentage: 10, freeBookingsPerMonth: 2, priorityBooking: false, tournamentDiscount: 5 },
desc: '10% off en reservas, 2 reservas gratis/mes'
},
{
name: 'Premium',
type: 'MONTHLY',
price: 25000,
benefits: { discountPercentage: 20, freeBookingsPerMonth: 5, priorityBooking: true, tournamentDiscount: 10 },
desc: '20% off en reservas, 5 reservas gratis/mes, prioridad'
},
{
name: 'Anual VIP',
type: 'YEARLY',
price: 250000,
benefits: { discountPercentage: 30, freeBookingsPerMonth: 10, priorityBooking: true, tournamentDiscount: 15 },
desc: '30% off en reservas, 10 reservas gratis/mes'
},
];
for (const plan of plans) {
await prisma.subscriptionPlan.upsert({
where: { id: `plan-${plan.name.toLowerCase().replace(/ /g, '-')}` },
update: {},
create: {
id: `plan-${plan.name.toLowerCase().replace(/ /g, '-')}`,
name: plan.name,
description: plan.desc,
type: plan.type,
price: plan.price,
benefits: JSON.stringify(plan.benefits),
features: JSON.stringify([`${plan.benefits.discountPercentage}% descuento`, `${plan.benefits.freeBookingsPerMonth} reservas gratis`, plan.benefits.priorityBooking ? 'Prioridad de reserva' : 'Sin prioridad']),
isActive: true,
},
});
console.log(`✅ Plan de suscripción creado: ${plan.name}`);
}
// Registrar usuario como coach
const coach = await prisma.coach.upsert({
where: { id: 'coach-1' },
update: {},
create: {
id: 'coach-1',
userId: admin.id,
bio: 'Profesor de pádel con 10 años de experiencia. Especialista en técnica y táctica.',
specialties: JSON.stringify(['Técnica', 'Táctica', 'Volea', 'Smash']),
certifications: 'Entrenador Nacional Nivel 3',
yearsExperience: 10,
hourlyRate: 5000,
isActive: true,
isVerified: true,
},
});
console.log(`✅ Coach creado: ${coach.id}`);
// Crear disponibilidad del coach
for (let day = 1; day <= 5; day++) { // Lunes a Viernes
await prisma.coachAvailability.upsert({
where: { id: `avail-${coach.id}-${day}` },
update: {},
create: {
id: `avail-${coach.id}-${day}`,
coachId: coach.id,
dayOfWeek: day,
startTime: '09:00',
endTime: '18:00',
isAvailable: true,
},
});
}
console.log('✅ Disponibilidad del coach creada');
// Crear clases
const classes = [
{ name: 'Clase Individual', type: 'INDIVIDUAL', max: 1, price: 5000, duration: 60 },
{ name: 'Clase en Pareja', type: 'GROUP', max: 2, price: 3500, duration: 60 },
{ name: 'Clínica de Volea', type: 'CLINIC', max: 8, price: 2000, duration: 90 },
];
for (const cls of classes) {
await prisma.class.upsert({
where: { id: `class-${cls.name.toLowerCase().replace(/ /g, '-')}` },
update: {},
create: {
id: `class-${cls.name.toLowerCase().replace(/ /g, '-')}`,
coachId: coach.id,
title: cls.name,
description: `Clase especializada de ${cls.name.toLowerCase()}`,
type: cls.type,
maxStudents: cls.max,
price: cls.price,
duration: cls.duration,
isActive: true,
},
});
console.log(`✅ Clase creada: ${cls.name}`);
}
console.log('\n🎾 Fase 4 seed completado!');
console.log('\nDatos creados:');
console.log(` - 3 Bonus Packs`);
console.log(` - 3 Planes de suscripción`);
console.log(` - 1 Coach verificado`);
console.log(` - 3 Clases disponibles`);
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});