47 Commits

Author SHA1 Message Date
Jose Selesan
e06bc12097 feat(billing): implement billing module with Stripe and Mercado Pago integration
- Add Stripe and Mercado Pago providers for handling subscriptions.
- Create billing repository and access services for managing billing records.
- Implement webhook handlers for processing events from Stripe and Mercado Pago.
- Add routes for checkout, canceling subscriptions, and retrieving billing status.
- Introduce billing status management with appropriate state transitions.
- Create tests for billing functionalities including webhook idempotency and provider resolution.
- Document the billing module architecture, environment variables, and API endpoints.
2026-06-29 15:55:20 -03:00
Jose Selesan
f490eecd11 Replace Playzer logo format from SVG to WEBP for improved loading performance 2026-06-26 15:09:45 -03:00
Jose Selesan
ab69711e87 fix: update recurringGroupId assignment and improve type definitions in helpers 2026-06-26 14:58:40 -03:00
c7e685ea08 Merge pull request 'Refactor to vertical slice' (#20) from refactor/vertical-slice into development
Reviewed-on: #20
2026-06-26 17:11:04 +00:00
Jose Selesan
c8477de5d2 Refactor to vertical slice 2026-06-26 14:05:37 -03:00
3949c9add1 Merge pull request 'feat: add reschedule functionality for admin bookings' (#19) from feat/reschedule into development
Reviewed-on: #19
2026-06-24 11:38:58 +00:00
Jose Selesan
dce312d426 feat: add reschedule functionality for admin bookings
- Implemented rescheduleAdminBooking service to allow users to change court and time for confirmed bookings.
- Added validation for court availability, maintenance status, and overlapping bookings.
- Created reschedule-admin-booking handler to process rescheduling requests and send confirmation emails.
- Updated booking email service to include rescheduling notifications.
- Enhanced frontend components to support booking rescheduling, including a new dialog for selecting new court and time.
- Added tests for rescheduling logic, covering various scenarios including validation errors and successful reschedules.
- Updated Prisma schema to log previous court and time for audit purposes.
2026-06-24 08:38:10 -03:00
fd4d8b7abd Merge pull request 'feat/fixed-bookings' (#18) from feat/fixed-bookings into development
Reviewed-on: #18
2026-06-16 18:27:52 +00:00
Jose Selesan
3b3def94ab feat: add recurring booking management functionality
- Implemented API endpoints for listing and updating recurring booking groups.
- Added handlers for listing and updating recurring groups in the backend.
- Created frontend components for displaying and editing recurring bookings.
- Enhanced booking provider to manage recurring groups and their states.
- Updated API client to include new methods for recurring bookings.
- Introduced new validation schemas for updating recurring groups.
- Added UI elements for managing recurring bookings in the booking interface.
2026-06-16 15:16:38 -03:00
Jose Selesan
1318e3bf57 feat: add recurring bookings feature with related handlers and services
- Introduced recurring booking groups with the ability to create and cancel them.
- Added database migrations for recurring bookings, including new tables and relationships.
- Implemented handlers for creating and canceling recurring bookings in the admin booking module.
- Enhanced existing booking services to support recurring bookings logic.
- Updated API contract to include new schemas for recurring bookings.
- Refactored existing code for improved readability and maintainability.
2026-06-16 11:58:06 -03:00
Jose Selesan
1b5eb253f2 Updated AGENTS.md 2026-06-12 16:22:11 -03:00
ccee416b6f Merge pull request 'feat/admin-portal' (#17) from feat/admin-portal into development
Reviewed-on: #17
2026-06-10 16:08:57 +00:00
Jose Selesan
c3e26f8f0e feat(admin): add maxSports field to plan rules and limits schemas 2026-06-10 13:07:56 -03:00
Jose Selesan
ef926c63a2 feat(admin): implement pricing overrides for plans and update pricing logic 2026-06-10 11:22:57 -03:00
Jose Selesan
dc8a10a612 feat: add geolocation fields to session and implement geo stats endpoint
- Added country, city, countryCode, latitude, and longitude fields to the Session model.
- Updated geoip service to fetch and store geolocation data based on user IP.
- Created a new admin endpoint to retrieve geolocation statistics, including user counts by country and city.
- Enhanced admin page to display geolocation statistics with expandable city details.
- Introduced caching for geolocation data to optimize performance.
2026-06-10 10:18:31 -03:00
Jose Selesan
00f0cf511f feat(admin): add geolocation information to user sessions and implement geo IP fetching 2026-06-10 10:01:36 -03:00
Jose Selesan
93b3a82638 feat(admin): implement user search functionality in admin portal 2026-06-10 09:44:02 -03:00
Jose Selesan
c1c2f18471 Add admin portal 2026-06-10 09:39:20 -03:00
Jose Selesan
c278c78e3d feat(booking): replace filter toggle with dedicated list view for today's bookings 2026-06-08 23:21:35 -03:00
Jose Selesan
d767ac3ac7 fix(booking): prevent past time slots from being selectable for today 2026-06-08 22:50:56 -03:00
Jose Selesan
8c3ca0e7e1 Fix booking cancelation 2026-06-08 16:53:28 -03:00
Jose Selesan
72f273354e fix(public-booking): enhance focus behavior for customer name input on slot selection 2026-06-08 16:47:13 -03:00
Jose Selesan
ef2ded9d64 Fix input focus on time select 2026-06-08 16:41:24 -03:00
Jose Selesan
4c7b825129 refactor(public-booking): remove MobileBottomNav component and adjust layout styles 2026-06-08 16:34:36 -03:00
339e6a70d7 Merge pull request 'refactor(public-booking): remove CourtDetails component and simplify canGoBack logic' (#15) from feat/remove-attributes into development
Reviewed-on: #15
2026-06-08 19:16:04 +00:00
Jose Selesan
c70541b4f5 refactor(public-booking): remove CourtDetails component and simplify canGoBack logic 2026-06-08 16:14:27 -03:00
89673c3844 Merge pull request 'feat/court-maintenace' (#14) from feat/court-maintenace into development
Reviewed-on: #14
2026-06-08 18:47:02 +00:00
Jose Selesan
630dedb507 feat(booking): add validation to prevent past bookings and implement slot validation logic 2026-06-08 15:46:17 -03:00
Jose Selesan
49d2a13672 feat(booking): allow empty customer email in booking forms and schemas 2026-06-08 15:34:53 -03:00
Jose Selesan
b2f9a14b87 refactor(booking): remove maintenance-related metrics and UI components 2026-06-08 15:28:49 -03:00
Jose Selesan
b48dd4f15d feat(public-booking): update logo and improve layout in confirmation page 2026-06-05 17:06:24 -03:00
Jose Selesan
fee0be7e1f feat(booking-ui): add date navigation buttons in MobileFilters component 2026-06-05 15:52:56 -03:00
Jose Selesan
721ebaa775 fix(booking-ui): adjust height units in booking dialog styling 2026-06-05 15:24:02 -03:00
2ccff7dcaa Merge pull request 'feat(booking-ui): enhance mobile booking dialog styling and layout' (#11) from codex/fix-mobile-booking-dlg into development
Reviewed-on: #11
2026-06-05 17:37:52 +00:00
Jose Selesan
4b1b06f00f feat(booking-ui): enhance mobile booking dialog styling and layout 2026-06-05 14:37:27 -03:00
Jose Selesan
af687fe2d8 refactor(tests): simplify prisma mock imports and ensure sport mock is included 2026-06-05 11:36:21 -03:00
9875f22d5a Merge pull request 'feat/improve-booking-ui' (#10) from feat/improve-booking-ui into development
Reviewed-on: #10
2026-06-05 14:25:06 +00:00
Jose Selesan
260d79fc99 feat: implement public booking cancellation feature
- Added cancelPublicBooking handler to manage booking cancellations.
- Updated public booking service to include cancellation logic.
- Created cancelPublicBooking schema for input validation.
- Modified public booking confirmation page to allow users to cancel their bookings.
- Enhanced email service to send cancellation confirmation emails.
- Made customerEmail field required in booking schemas and updated related components.
- Updated API client to support cancellation requests.
- Added migration to enforce non-nullable customer_email fields in the database.
2026-06-05 11:24:02 -03:00
Jose Selesan
1210854c22 feat(booking-ui): add confirmation dialog for cancelling bookings and improve mobile UI 2026-06-04 16:39:45 -03:00
Jose Selesan
7ca784d5f5 feat(booking-ui): conditionally render action buttons based on booking status 2026-06-04 16:33:51 -03:00
Jose Selesan
e441f15ee4 feat(complex-selection): enhance auto-selection logic and error handling for complex selection 2026-06-04 14:06:23 -03:00
Jose Selesan
228004a7e0 feat(emails): update email layout padding and remove min-height for improved design consistency 2026-06-03 10:57:23 -03:00
b20b5c2b8b Merge pull request 'feat/improve-emails' (#8) from feat/improve-emails into development
Reviewed-on: #8
2026-06-03 13:44:12 +00:00
Jose Selesan
457accfbfa feat(emails): add standardized email templates and layout guidelines 2026-06-03 10:43:17 -03:00
Jose Selesan
43287a4baa feat(emails): integrate email verification layout into auth email sending process 2026-06-03 10:41:56 -03:00
Jose Selesan
93fea8ecad feat(emails): enhance booking cancellation and no-show email templates with improved layout and styling 2026-06-03 10:37:07 -03:00
Jose Selesan
ba7b0322ff Refactor code structure for improved readability and maintainability 2026-06-03 10:31:01 -03:00
261 changed files with 15510 additions and 4440 deletions

114
AGENTS.md
View File

@@ -33,6 +33,62 @@ packages/api-contract/ # Shared Zod schemas, types, route definitions
2. Implement handler in `apps/backend`
3. Consume from `apps/frontend` via workspace import `@repo/api-contract`
## Endpoint Pattern
Every new endpoint must follow the **Result pattern** with the **validate helper**.
### Files
| File | Purpose |
|------|---------|
| `apps/backend/src/lib/result.ts` | `Result<T>` type, `ok()`, `err()` |
| `apps/backend/src/lib/errors.ts` | `AppError` discriminated union, `Errors` factory |
| `apps/backend/src/lib/http/handle-result.ts` | `handleResult()` — bridges `Result` to HTTP |
| `apps/backend/src/lib/http/validate.ts` | `validate.json()`, `.query()`, `.param()`, `.header()`, `.form()` |
### Layers
**1. Route** — Use `validate.json(schema)`, `validate.param(schema)`, etc. as Hono middleware.
```typescript
import { validate } from '@/lib/http/validate';
import { z } from 'zod';
const idParams = z.object({ id: z.string() });
router.post('/', validate.json(createSchema), createHandler);
router.get('/:id', validate.param(idParams), getHandler);
```
**2. Service** — Return `Result<T>`. Use `ok(value)` on success, `err(Errors.*(...))` on failure.
```typescript
import { Result, ok, err } from '@/lib/result';
import { Errors } from '@/lib/errors';
export async function doSomething(input: Input): Promise<Result<Output>> {
if (conflict) return err(Errors.conflict('Already exists'));
return ok(result);
}
```
**3. Handler** — Call the service and pass the `Result` to `handleResult`.
```typescript
import { handleResult } from '@/lib/http/handle-result';
import { doSomething } from './service';
export async function myHandler(c: AppContext) {
const payload = c.req.valid('json') as Input;
return handleResult(c, await doSomething(payload), 201);
}
```
### Never
- ❌ Throw custom error classes from services
- ❌ Use try/catch in handlers for business logic errors
- ❌ Use `zValidator` directly — always use `validate.*`
## Authentication (Better Auth)
The project uses **Better Auth** for session management, replacing the legacy Supabase Auth.
@@ -193,3 +249,61 @@ await getAvailability('my-club', { date: '2026-04-20' });
1. Create `lib/api/resources/[resource].ts`
2. Export functions using `http` from `../http`
3. Add exports in `lib/api/index.ts`
## Email Templates
Todos los emails deben usar la misma estética. El layout compartido está en `apps/backend/src/emails/booking-confirmation.ts`.
### Layout (`wrapLayout`)
Exportado como `wrapLayout(content)`. Proporciona:
- Fondo: `#edf7f4`
- Card blanca: `max-width: 520px`, `border-radius: 28px`, `box-shadow: 0 24px 70px rgba(15,23,42,0.12)`, `border: 1px solid rgba(5,9,20,0.1)`
- Playzer favicon: `{APP_BASE_URL}/playzer-favicon-512-transparent.png` (está en `apps/frontend/public/`)
- **Sin `min-height: 100vh`**: la card empieza arriba, no centrada verticalmente
```typescript
import { wrapLayout } from '@/emails/booking-confirmation';
const html = wrapLayout(`<tr>...contenido...</tr>`);
```
### Estructura de cada email
| Sección | Descripción |
|---------|-------------|
| **Header** | Dos columnas: badge pill a la izquierda + Playzer (favicon + texto) a la derecha. El badge usa `border-radius: 999px`, `padding: 4px 12px`, `font-size: 11px`, `font-weight: 700`, `letter-spacing: 0.14em`, `text-transform: uppercase`. |
| **Card fecha/hora** | Fondo `#f0fdf4`, borde `1px solid rgba(5,150,105,0.3)`, `border-radius: 24px`. Siempre verde aunque el email sea de cancelación. |
| **Grilla detalles** | `border: 1px solid #e5e7eb`, `border-radius: 22px`, dos celdas de 50% con `border-right` en la primera. |
| **Botón CTA** | Tabla con fondo `#059669`, `border-radius: 12px`, link blanco con `padding: 14px 32px`. |
| **Pie** | Sin pie de marca. Solo texto secundario opcional centrado si es necesario. |
| **Sin botones de acción** | Los emails de booking **no** incluyen los botones "Compartir por WhatsApp" ni "Hacer otra reserva". |
### Padding estándar
| Ubicación | Valor |
|-----------|-------|
| Wrapper (outer `<td>`) | `padding: 24px 12px` |
| Primer `<td>` del contenido (header) | `padding: 24px 20px 16px` |
| `<td>` intermedios (cards, texto) | `padding: 0 20px 16px` |
| Último `<td>` del contenido | `padding: 0 20px 24px` |
### Colores de badges según estado
| Estado | Fondo badge | Texto badge |
|--------|-------------|-------------|
| Confirmado | `#f0fdf4` | `#15803d` |
| Cancelado | `#fef2f2` | `#dc2626` |
| No concretado | `#fffbeb` | `#d97706` |
| Neutro (verificación, etc.) | `#f4f4f5` | `#71717a` |
### Archivos de templates
| Archivo | Templates |
|---------|-----------|
| `apps/backend/src/emails/booking-confirmation.ts` | `bookingConfirmationHtml`, `bookingCancelledHtml`, `bookingNoShowHtml` + exporta `wrapLayout` |
| `apps/backend/src/lib/auth.ts` | Email de verificación de Better Auth (usa `wrapLayout` inline) |
### Regla general
Para emails nuevos: importar `wrapLayout`, construir el HTML interno con `<tr>`s, usar la misma estructura de cabecera (badge + Playzer), y nunca incluir el pie "Playzer — Reserva de canchas online". Usar `APP_BASE_URL` para construir URLs absolutas al logo.

View File

@@ -16,3 +16,23 @@ SMTP_FROM=Playzer <no-reply@example.com>
# Google OAuth
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
# Billing / Subscriptions
BILLING_ENV=sandbox
FRONTEND_BASE_URL=http://localhost:5173
# Stripe
STRIPE_SECRET_KEY=sk_test_your-stripe-secret-key
STRIPE_WEBHOOK_SECRET=whsec_your-webhook-secret
# Price IDs por plan y moneda (ajustar según tus products/prices en Stripe)
STRIPE_PRICE_ID_USD_BASIC=price_basic_usd
STRIPE_PRICE_ID_USD_ADVANCED=price_advanced_usd
STRIPE_PRICE_ID_USD_ENTERPRISE=price_enterprise_usd
# Mercado Pago (Argentina)
MERCADOPAGO_ACCESS_TOKEN=TEST-your-access-token
MERCADOPAGO_WEBHOOK_SECRET=your-webhook-secret
# Plan IDs por plan (ajustar según tus planes de suscripción en MP)
MERCADOPAGO_PLAN_ID_ARS_BASIC=2c938084...
MERCADOPAGO_PLAN_ID_ARS_ADVANCED=2c938084...
MERCADOPAGO_PLAN_ID_ARS_ENTERPRISE=2c938084...

View File

@@ -1,3 +1,6 @@
# deps
node_modules/
prisma.config.prod.ts
prisma.config.prod.ts
# build output
dist/

View File

@@ -24,12 +24,14 @@
"dotenv": "^17.4.1",
"hono": "4.12.10",
"mailtrap": "^4.5.1",
"mercadopago": "^3.1.0",
"nodemailer": "^8.0.5",
"pg": "^8.20.0",
"pino": "10.3.1",
"pino-pretty": "13.1.3",
"pino-std-serializers": "7.1.0",
"prisma": "^7",
"stripe": "^22.2.2",
"uuid": "^13.0.0"
},
"devDependencies": {

View File

@@ -5,6 +5,9 @@ model User {
emailVerified Boolean @default(false)
image String?
phone String?
banned Boolean @default(false)
bannedAt DateTime?
banReason String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
sessions Session[]
@@ -17,15 +20,20 @@ model User {
}
model Session {
id String @id
expiresAt DateTime
token String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
ipAddress String?
userAgent String?
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
id String @id
expiresAt DateTime
token String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
ipAddress String?
userAgent String?
country String?
city String?
countryCode String?
latitude Float?
longitude Float?
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([token])
@@index([userId])

View File

@@ -0,0 +1,52 @@
enum BillingProvider {
STRIPE
MERCADOPAGO
PAYPAL
}
enum BillingStatus {
TRIAL
ACTIVE
PAST_DUE
CANCELED
SUSPENDED
}
model ComplexBilling {
complexId String @id @map("complex_id") @db.Uuid
status BillingStatus @default(TRIAL)
planCode String @map("plan_code") @db.VarChar(10)
currency String @db.VarChar(3)
provider BillingProvider?
providerCustomerId String? @map("provider_customer_id")
providerSubscriptionId String? @map("provider_subscription_id")
providerPreapprovalId String? @map("provider_preapproval_id")
currentPeriodStart DateTime? @map("current_period_start")
currentPeriodEnd DateTime? @map("current_period_end")
trialEndsAt DateTime? @map("trial_ends_at")
canceledAt DateTime? @map("canceled_at")
suspendedAt DateTime? @map("suspended_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
complex Complex @relation(fields: [complexId], references: [id], onDelete: Cascade)
events BillingEvent[]
@@map("complex_billings")
}
model BillingEvent {
id String @id @db.Uuid
complexId String @map("complex_id") @db.Uuid
eventType String @map("event_type") @db.VarChar(100)
provider BillingProvider
providerEventId String? @unique @map("provider_event_id")
providerData Json? @map("provider_data")
previousStatus BillingStatus? @map("previous_status")
newStatus BillingStatus? @map("new_status")
processedAt DateTime @default(now()) @map("processed_at")
billing ComplexBilling @relation(fields: [complexId], references: [complexId])
@@index([provider, providerEventId])
@@index([complexId])
@@map("billing_events")
}

View File

@@ -14,6 +14,8 @@ model Complex {
users ComplexUser[]
invitations ComplexInvitation[]
courts Court[]
recurringGroups RecurringBookingGroup[]
billing ComplexBilling?
@@index([planCode])
@@index([complexSlug])

View File

@@ -15,6 +15,11 @@ enum CourtBookingStatus {
NOSHOW
}
enum RecurringBookingGroupStatus {
ACTIVE
CANCELLED
}
model Sport {
id String @id @db.Uuid
name String @unique
@@ -34,6 +39,8 @@ model Court {
name String
slotDurationMinutes Int @map("slot_duration_minutes")
basePrice Decimal @map("base_price") @db.Decimal(19, 2)
isUnderMaintenance Boolean @default(false) @map("is_under_maintenance")
maintenanceReason String? @map("maintenance_reason") @db.VarChar(500)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
complex Complex @relation(fields: [complexId], references: [id], onDelete: Cascade)
@@ -41,12 +48,29 @@ model Court {
availabilities CourtAvailability[]
priceRules CourtPriceRule[]
bookings CourtBooking[]
maintenances CourtMaintenance[]
recurringGroups RecurringBookingGroup[]
@@index([complexId])
@@index([sportId])
@@map("courts")
}
model CourtMaintenance {
id String @id @db.Uuid
courtId String @map("court_id") @db.Uuid
startDate DateTime @map("start_date") @db.Date
startTime String? @map("start_time") @db.VarChar(5)
endTime String? @map("end_time") @db.VarChar(5)
reason String? @db.VarChar(500)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
court Court @relation(fields: [courtId], references: [id], onDelete: Cascade)
@@index([courtId, startDate])
@@map("court_maintenances")
}
model CourtAvailability {
id String @id @db.Uuid
courtId String @map("court_id") @db.Uuid
@@ -77,40 +101,71 @@ model CourtPriceRule {
}
model CourtBooking {
id String @id @db.Uuid
bookingCode String @unique @map("booking_code") @db.VarChar(8)
courtId String @map("court_id") @db.Uuid
bookingDate DateTime @map("booking_date") @db.Date
startTime String @map("start_time") @db.VarChar(5)
endTime String @map("end_time") @db.VarChar(5)
customerName String @map("customer_name") @db.VarChar(120)
customerPhone String @map("customer_phone") @db.VarChar(30)
customerEmail String? @map("customer_email") @db.VarChar(254)
status CourtBookingStatus @default(CONFIRMED)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
court Court @relation(fields: [courtId], references: [id], onDelete: Cascade)
id String @id @db.Uuid
bookingCode String @unique @map("booking_code") @db.VarChar(8)
courtId String @map("court_id") @db.Uuid
bookingDate DateTime @map("booking_date") @db.Date
startTime String @map("start_time") @db.VarChar(5)
endTime String @map("end_time") @db.VarChar(5)
customerName String @map("customer_name") @db.VarChar(120)
customerPhone String @map("customer_phone") @db.VarChar(30)
customerEmail String @map("customer_email") @db.VarChar(254)
status CourtBookingStatus @default(CONFIRMED)
recurringGroupId String? @map("recurring_group_id") @db.Uuid
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
court Court @relation(fields: [courtId], references: [id], onDelete: Cascade)
recurringGroup RecurringBookingGroup? @relation(fields: [recurringGroupId], references: [id])
@@unique([courtId, bookingDate, startTime])
@@index([courtId, bookingDate])
@@index([bookingDate])
@@index([recurringGroupId])
@@map("court_bookings")
}
model CourtBookingLog {
id String @id @db.Uuid
bookingCode String @map("booking_code") @db.VarChar(8)
courtId String @map("court_id") @db.Uuid
bookingDate DateTime @map("booking_date") @db.Date
startTime String @map("start_time") @db.VarChar(5)
endTime String @map("end_time") @db.VarChar(5)
previousStatus CourtBookingStatus @map("previous_status")
newStatus CourtBookingStatus @map("new_status")
customerName String @map("customer_name") @db.VarChar(120)
customerPhone String @map("customer_phone") @db.VarChar(30)
customerEmail String? @map("customer_email") @db.VarChar(254)
changedAt DateTime @default(now()) @map("changed_at")
id String @id @db.Uuid
bookingCode String @map("booking_code") @db.VarChar(8)
courtId String @map("court_id") @db.Uuid
bookingDate DateTime @map("booking_date") @db.Date
startTime String @map("start_time") @db.VarChar(5)
endTime String @map("end_time") @db.VarChar(5)
previousStatus CourtBookingStatus @map("previous_status")
newStatus CourtBookingStatus @map("new_status")
previousCourtId String? @map("previous_court_id") @db.Uuid
previousStartTime String? @map("previous_start_time") @db.VarChar(5)
previousEndTime String? @map("previous_end_time") @db.VarChar(5)
customerName String @map("customer_name") @db.VarChar(120)
customerPhone String @map("customer_phone") @db.VarChar(30)
customerEmail String @map("customer_email") @db.VarChar(254)
changedAt DateTime @default(now()) @map("changed_at")
@@map("court_booking_logs")
@@index([courtId, newStatus])
}
model RecurringBookingGroup {
id String @id @db.Uuid
complexId String @map("complex_id") @db.Uuid
courtId String @map("court_id") @db.Uuid
startTime String @map("start_time") @db.VarChar(5)
endTime String @map("end_time") @db.VarChar(5)
dayOfWeek DayOfWeek @map("day_of_week")
startDate DateTime @map("start_date") @db.Date
endDate DateTime? @map("end_date") @db.Date
status RecurringBookingGroupStatus @default(ACTIVE)
customerName String @map("customer_name") @db.VarChar(120)
customerPhone String @map("customer_phone") @db.VarChar(30)
customerEmail String @map("customer_email") @db.VarChar(254)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
court Court @relation(fields: [courtId], references: [id], onDelete: Cascade)
complex Complex @relation(fields: [complexId], references: [id], onDelete: Cascade)
bookings CourtBooking[]
@@index([complexId])
@@index([courtId])
@@index([status])
@@map("recurring_booking_groups")
}

View File

@@ -0,0 +1,16 @@
/*
Warnings:
- Made the column `customer_email` on table `court_booking_logs` required. This step will fail if there are existing NULL values in that column.
- Made the column `customer_email` on table `court_bookings` required. This step will fail if there are existing NULL values in that column.
*/
-- Backfill existing NULL values with placeholder
UPDATE "court_booking_logs" SET "customer_email" = 'missing@playzer.app' WHERE "customer_email" IS NULL;
UPDATE "court_bookings" SET "customer_email" = 'missing@playzer.app' WHERE "customer_email" IS NULL;
-- AlterTable
ALTER TABLE "court_booking_logs" ALTER COLUMN "customer_email" SET NOT NULL;
-- AlterTable
ALTER TABLE "court_bookings" ALTER COLUMN "customer_email" SET NOT NULL;

View File

@@ -0,0 +1,23 @@
-- CreateTable
CREATE TABLE "court_maintenances" (
"id" UUID NOT NULL,
"court_id" UUID NOT NULL,
"start_date" DATE NOT NULL,
"start_time" VARCHAR(5),
"end_time" VARCHAR(5),
"reason" VARCHAR(500),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "court_maintenances_pkey" PRIMARY KEY ("id")
);
-- AlterTable
ALTER TABLE "courts" ADD COLUMN "is_under_maintenance" BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE "courts" ADD COLUMN "maintenance_reason" VARCHAR(500);
-- CreateIndex
CREATE INDEX "court_maintenances_court_id_start_date_idx" ON "court_maintenances"("court_id", "start_date");
-- AddForeignKey
ALTER TABLE "court_maintenances" ADD CONSTRAINT "court_maintenances_court_id_fkey" FOREIGN KEY ("court_id") REFERENCES "courts"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,4 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "banReason" TEXT,
ADD COLUMN "banned" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "bannedAt" TIMESTAMP(3);

View File

@@ -0,0 +1,6 @@
-- AlterTable
ALTER TABLE "sessions" ADD COLUMN "city" TEXT,
ADD COLUMN "country" TEXT,
ADD COLUMN "countryCode" TEXT,
ADD COLUMN "latitude" DOUBLE PRECISION,
ADD COLUMN "longitude" DOUBLE PRECISION;

View File

@@ -0,0 +1,46 @@
-- CreateEnum
CREATE TYPE "RecurringBookingGroupStatus" AS ENUM ('ACTIVE', 'CANCELLED');
-- AlterTable
ALTER TABLE "court_bookings" ADD COLUMN "recurring_group_id" UUID;
-- CreateTable
CREATE TABLE "recurring_booking_groups" (
"id" UUID NOT NULL,
"complex_id" UUID NOT NULL,
"court_id" UUID NOT NULL,
"start_time" VARCHAR(5) NOT NULL,
"end_time" VARCHAR(5) NOT NULL,
"day_of_week" "DayOfWeek" NOT NULL,
"start_date" DATE NOT NULL,
"end_date" DATE,
"status" "RecurringBookingGroupStatus" NOT NULL DEFAULT 'ACTIVE',
"customer_name" VARCHAR(120) NOT NULL,
"customer_phone" VARCHAR(30) NOT NULL,
"customer_email" VARCHAR(254) NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "recurring_booking_groups_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "recurring_booking_groups_complex_id_idx" ON "recurring_booking_groups"("complex_id");
-- CreateIndex
CREATE INDEX "recurring_booking_groups_court_id_idx" ON "recurring_booking_groups"("court_id");
-- CreateIndex
CREATE INDEX "recurring_booking_groups_status_idx" ON "recurring_booking_groups"("status");
-- CreateIndex
CREATE INDEX "court_bookings_recurring_group_id_idx" ON "court_bookings"("recurring_group_id");
-- AddForeignKey
ALTER TABLE "court_bookings" ADD CONSTRAINT "court_bookings_recurring_group_id_fkey" FOREIGN KEY ("recurring_group_id") REFERENCES "recurring_booking_groups"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "recurring_booking_groups" ADD CONSTRAINT "recurring_booking_groups_court_id_fkey" FOREIGN KEY ("court_id") REFERENCES "courts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "recurring_booking_groups" ADD CONSTRAINT "recurring_booking_groups_complex_id_fkey" FOREIGN KEY ("complex_id") REFERENCES "complexes"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,4 @@
-- AlterTable
ALTER TABLE "court_booking_logs" ADD COLUMN "previous_court_id" UUID;
ALTER TABLE "court_booking_logs" ADD COLUMN "previous_start_time" VARCHAR(5);
ALTER TABLE "court_booking_logs" ADD COLUMN "previous_end_time" VARCHAR(5);

View File

@@ -0,0 +1,56 @@
-- CreateEnum
CREATE TYPE "BillingProvider" AS ENUM ('STRIPE', 'MERCADOPAGO', 'PAYPAL');
-- CreateEnum
CREATE TYPE "BillingStatus" AS ENUM ('TRIAL', 'ACTIVE', 'PAST_DUE', 'CANCELED', 'SUSPENDED');
-- CreateTable
CREATE TABLE "complex_billings" (
"complex_id" UUID NOT NULL,
"status" "BillingStatus" NOT NULL DEFAULT 'TRIAL',
"plan_code" VARCHAR(10) NOT NULL,
"currency" VARCHAR(3) NOT NULL,
"provider" "BillingProvider",
"provider_customer_id" TEXT,
"provider_subscription_id" TEXT,
"provider_preapproval_id" TEXT,
"current_period_start" TIMESTAMP(3),
"current_period_end" TIMESTAMP(3),
"trial_ends_at" TIMESTAMP(3),
"canceled_at" TIMESTAMP(3),
"suspended_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "complex_billings_pkey" PRIMARY KEY ("complex_id")
);
-- CreateTable
CREATE TABLE "billing_events" (
"id" UUID NOT NULL,
"complex_id" UUID NOT NULL,
"event_type" VARCHAR(100) NOT NULL,
"provider" "BillingProvider" NOT NULL,
"provider_event_id" TEXT,
"provider_data" JSONB,
"previous_status" "BillingStatus",
"new_status" "BillingStatus",
"processed_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "billing_events_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "billing_events_provider_event_id_key" ON "billing_events"("provider_event_id");
-- CreateIndex
CREATE INDEX "billing_events_provider_provider_event_id_idx" ON "billing_events"("provider", "provider_event_id");
-- CreateIndex
CREATE INDEX "billing_events_complex_id_idx" ON "billing_events"("complex_id");
-- AddForeignKey
ALTER TABLE "complex_billings" ADD CONSTRAINT "complex_billings_complex_id_fkey" FOREIGN KEY ("complex_id") REFERENCES "complexes"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "billing_events" ADD CONSTRAINT "billing_events_complex_id_fkey" FOREIGN KEY ("complex_id") REFERENCES "complex_billings"("complex_id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -4,7 +4,7 @@ export const planSeeds = [
name: 'Basic',
price: '49.00',
rules: {
version: 'v1',
version: 'v2',
limits: {
maxCourts: 2,
maxBookingsPerDay: 80,
@@ -21,6 +21,12 @@ export const planSeeds = [
publicBookingPage: true,
advancedReports: false,
whatsappReminders: false,
fixedSlots: false,
},
pricing: {
overrides: {
AR: { amount: 14999, currency: 'ARS' },
},
},
},
},
@@ -29,7 +35,7 @@ export const planSeeds = [
name: 'Advanced',
price: '99.00',
rules: {
version: 'v1',
version: 'v2',
limits: {
maxCourts: 5,
maxBookingsPerDay: 220,
@@ -46,6 +52,12 @@ export const planSeeds = [
publicBookingPage: true,
advancedReports: true,
whatsappReminders: true,
fixedSlots: true,
},
pricing: {
overrides: {
AR: { amount: 29999, currency: 'ARS' },
},
},
},
},
@@ -54,7 +66,7 @@ export const planSeeds = [
name: 'Enterprise',
price: '199.00',
rules: {
version: 'v1',
version: 'v2',
limits: {
maxCourts: 20,
maxBookingsPerDay: 2000,
@@ -71,6 +83,12 @@ export const planSeeds = [
publicBookingPage: true,
advancedReports: true,
whatsappReminders: true,
fixedSlots: true,
},
pricing: {
overrides: {
AR: { amount: 59999, currency: 'ARS' },
},
},
},
},

View File

@@ -1,5 +1,6 @@
type BookingEmailData = {
bookingCode: string;
complexSlug?: string;
complexName: string;
date: string;
startTime: string;
@@ -10,6 +11,20 @@ type BookingEmailData = {
price?: number;
};
const APP_BASE_URL = process.env.APP_BASE_URL ?? 'http://localhost:5173';
function formatBookingPrice(price: number): string {
if (price === 0) {
return 'Sin cargo';
}
return new Intl.NumberFormat('es-AR', {
style: 'currency',
currency: 'ARS',
maximumFractionDigits: Number.isInteger(price) ? 0 : 2,
}).format(price);
}
function formatFriendlyDate(isoDate: string): string {
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(isoDate);
if (!match) return isoDate;
@@ -27,7 +42,7 @@ const BASE_STYLES = `
body {
margin: 0;
padding: 0;
background-color: #f4f4f5;
background-color: #edf7f4;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
* {
@@ -35,7 +50,7 @@ const BASE_STYLES = `
}
`;
function wrapLayout(content: string) {
export function wrapLayout(content: string) {
return `<!DOCTYPE html>
<html lang="es">
<head>
@@ -44,11 +59,11 @@ function wrapLayout(content: string) {
<title>Playzer</title>
<style>${BASE_STYLES}</style>
</head>
<body style="margin:0;padding:0;background-color:#f4f4f5;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f4f4f5;min-height:100vh;">
<body style="margin:0;padding:0;background-color:#edf7f4;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#edf7f4;">
<tr>
<td align="center" style="padding:32px 16px;">
<table role="presentation" width="100%" style="max-width:520px;background-color:#ffffff;border-radius:12px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,0.08);">
<td align="center" style="padding:24px 12px;">
<table role="presentation" width="100%" style="max-width:520px;background-color:#ffffff;border-radius:28px;overflow:hidden;box-shadow:0 24px 70px rgba(15,23,42,0.12);border:1px solid rgba(5,9,20,0.1);">
${content}
</table>
</td>
@@ -58,231 +73,495 @@ function wrapLayout(content: string) {
</html>`;
}
function headerSection() {
return `
<tr>
<td style="background-color:#0a1628;padding:32px 32px 28px;text-align:center;">
<span style="font-size:24px;font-weight:700;color:#10b981;letter-spacing:-0.3px;">Playzer</span>
</td>
</tr>`;
}
export function bookingConfirmationHtml(data: BookingEmailData): string {
const formattedPrice = formatBookingPrice(data.price ?? 0);
const friendlyDate = formatFriendlyDate(data.date);
function divider() {
return `
const content = `
<tr>
<td style="padding:0 32px;">
<td style="padding:24px 20px 16px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td style="height:1px;background-color:#e5e7eb;line-height:1px;font-size:1px;">&nbsp;</td>
<td valign="top">
<table role="presentation" cellpadding="0" cellspacing="0" style="display:inline-block;background-color:#f0fdf4;border-radius:999px;padding:4px 12px;">
<tr>
<td style="font-size:11px;font-weight:700;letter-spacing:0.14em;color:#15803d;text-transform:uppercase;line-height:1.25rem;">
&#10003; Reserva confirmada
</td>
</tr>
</table>
<h1 style="margin:12px 0 0;font-size:28px;font-weight:700;color:#111827;letter-spacing:-0.025em;">
${data.complexName}
</h1>
</td>
<td valign="top" align="right" style="white-space:nowrap;">
<table role="presentation" cellpadding="0" cellspacing="0">
<tr>
<td valign="middle" style="padding-right:8px;">
<img src="${APP_BASE_URL}/playzer-favicon-512-transparent.png" alt="Playzer" width="32" height="32" style="display:block;" />
</td>
<td valign="middle">
<span style="font-size:18px;font-weight:700;color:#475569;letter-spacing:-0.025em;">Playzer</span>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>`;
}
</tr>
function dateTimeBlock(date: string, startTime: string, endTime: string): string {
const friendlyDate = formatFriendlyDate(date);
return `
<tr>
<td style="padding:0 32px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f0fdf4;border-radius:10px;border:1px solid #bbf7d0;">
<td style="padding:0 20px 16px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f0fdf4;border-radius:24px;border:1px solid rgba(5,150,105,0.3);">
<tr>
<td style="padding:20px 24px;text-align:center;">
<p style="margin:0;font-size:16px;font-weight:700;color:#065f46;line-height:1.4;">
${friendlyDate}
<td style="padding:20px 24px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td valign="bottom">
<p style="margin:0;font-size:13px;font-weight:600;color:#15803d;">
Tu turno
</p>
<p style="margin:12px 0 0;font-size:28px;font-weight:900;color:#111827;line-height:1.1;letter-spacing:-0.025em;">
${friendlyDate}
</p>
<p style="margin:8px 0 0;font-size:24px;font-weight:900;color:#059669;line-height:1;letter-spacing:-0.025em;">
${data.startTime} &mdash; ${data.endTime}
</p>
</td>
<td valign="bottom" align="right" style="padding-left:16px;">
<table role="presentation" cellpadding="0" cellspacing="0" style="background-color:rgba(255,255,255,0.8);border-radius:16px;border:1px solid rgba(5,150,105,0.2);">
<tr>
<td style="padding:12px 16px;text-align:right;min-width:120px;">
<p style="margin:0;font-size:11px;font-weight:500;color:#6b7280;">Precio del turno</p>
<p style="margin:4px 0 0;font-size:20px;font-weight:700;color:#111827;">${formattedPrice}</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="padding:0 20px 16px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e5e7eb;border-radius:22px;overflow:hidden;">
<tr>
<td width="50%" style="padding:16px;border-right:1px solid #e5e7eb;background-color:#ffffff;">
<p style="margin:0;font-size:13px;font-weight:500;color:#6b7280;">
Cancha
</p>
<p style="margin:8px 0 0;font-size:22px;font-weight:800;color:#059669;letter-spacing:-0.3px;">
${startTime} &mdash; ${endTime}
<p style="margin:8px 0 0;font-size:14px;font-weight:600;color:#111827;">
${data.courtName} &mdash; ${data.sportName}
</p>
</td>
<td width="50%" style="padding:16px;background-color:#ffffff;">
<p style="margin:0;font-size:13px;font-weight:500;color:#6b7280;">
C&oacute;digo de reserva
</p>
<p style="margin:8px 0 0;font-family:monospace;font-size:16px;font-weight:700;letter-spacing:0.18em;color:#111827;">
${data.bookingCode}
</p>
</td>
</tr>
</table>
</td>
</tr>`;
}
export function bookingConfirmationHtml(data: BookingEmailData): string {
const formattedPrice =
data.price !== undefined && data.price > 0
? new Intl.NumberFormat('es-AR', {
style: 'currency',
currency: 'ARS',
maximumFractionDigits: Number.isInteger(data.price) ? 0 : 2,
}).format(data.price)
: undefined;
const content = `
${headerSection()}
<tr>
<td style="padding:32px 32px 8px;">
<h1 style="margin:0;font-size:22px;font-weight:700;color:#111827;text-align:center;">
Reserva confirmada
</h1>
<p style="margin:8px 0 0;font-size:14px;color:#6b7280;text-align:center;">
${data.complexName}
</p>
</td>
</tr>
${
data.complexSlug
? `
<tr>
<td style="padding:20px 32px 12px;">
${dateTimeBlock(data.date, data.startTime, data.endTime)}
</td>
</tr>
${divider()}
<tr>
<td style="padding:24px 32px;">
<td style="padding:0 20px 24px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
${detailRow('Código', `<span style="font-family:monospace;font-weight:700;letter-spacing:2px;">${data.bookingCode}</span>`)}
${detailRow('Cancha', `${data.courtName} &mdash; ${data.sportName}`)}
${detailRow('Cliente', data.customerName)}
${formattedPrice ? detailRow('Precio', formattedPrice) : ''}
<tr>
<td align="center">
<table role="presentation" cellpadding="0" cellspacing="0" style="background-color:#059669;border-radius:12px;">
<tr>
<td style="padding:14px 32px;font-size:15px;font-weight:600;">
<a href="${APP_BASE_URL}/${data.complexSlug}/booking/confirmed/${data.bookingCode}" style="color:#ffffff;text-decoration:none;display:inline-block;">
Cancelar reserva
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
${divider()}
<tr>
<td style="padding:24px 32px;">
<p style="margin:0;font-size:13px;color:#6b7280;text-align:center;line-height:1.5;">
Presentá el código de reserva al llegar al complejo.
<br />
Si necesitás cancelar o modificar, contactate directamente con el complejo.
</p>
</td>
</tr>
<tr>
<td style="background-color:#f9fafb;padding:20px 32px;text-align:center;">
<p style="margin:0;font-size:12px;color:#9ca3af;">
Playzer &mdash; Reserva de canchas online
</p>
</td>
</tr>`;
</tr>`
: ''
}`;
return wrapLayout(content);
}
export function bookingCancelledHtml(data: BookingEmailData): string {
const friendlyDate = formatFriendlyDate(data.date);
const content = `
${headerSection()}
<tr>
<td style="padding:32px 32px 8px;">
<h1 style="margin:0;font-size:22px;font-weight:700;color:#dc2626;text-align:center;">
Reserva cancelada
</h1>
<p style="margin:8px 0 0;font-size:14px;color:#6b7280;text-align:center;">
${data.complexName}
</p>
</td>
</tr>
<tr>
<td style="padding:20px 32px 12px;">
${dateTimeBlock(data.date, data.startTime, data.endTime)}
</td>
</tr>
${divider()}
<tr>
<td style="padding:24px 32px;">
<p style="margin:0;font-size:14px;color:#374151;line-height:1.6;">
La reserva <strong style="font-family:monospace;font-weight:700;letter-spacing:2px;">${data.bookingCode}</strong>
fue cancelada. Si tenés dudas, contactate con el complejo.
</p>
</td>
</tr>
<tr>
<td style="padding:0 32px 24px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
${detailRow('Cancha', `${data.courtName} &mdash; ${data.sportName}`)}
${detailRow('Cliente', data.customerName)}
</table>
</td>
</tr>
<tr>
<td style="background-color:#f9fafb;padding:20px 32px;text-align:center;">
<p style="margin:0;font-size:12px;color:#9ca3af;">
Playzer &mdash; Reserva de canchas online
</p>
</td>
</tr>`;
return wrapLayout(content);
}
export function bookingNoShowHtml(data: BookingEmailData): string {
const content = `
${headerSection()}
<tr>
<td style="padding:32px 32px 8px;">
<h1 style="margin:0;font-size:22px;font-weight:700;color:#d97706;text-align:center;">
Reserva no concretada
</h1>
<p style="margin:8px 0 0;font-size:14px;color:#6b7280;text-align:center;">
${data.complexName}
</p>
</td>
</tr>
<tr>
<td style="padding:20px 32px 12px;">
${dateTimeBlock(data.date, data.startTime, data.endTime)}
</td>
</tr>
${divider()}
<tr>
<td style="padding:24px 32px;">
<p style="margin:0;font-size:14px;color:#374151;line-height:1.6;">
La reserva <strong style="font-family:monospace;font-weight:700;letter-spacing:2px;">${data.bookingCode}</strong>
fue registrada como no concretada por falta de asistencia.
</p>
</td>
</tr>
<tr>
<td style="padding:0 32px 24px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
${detailRow('Cancha', `${data.courtName} &mdash; ${data.sportName}`)}
${detailRow('Cliente', data.customerName)}
</table>
</td>
</tr>
<tr>
<td style="background-color:#f9fafb;padding:20px 32px;text-align:center;">
<p style="margin:0;font-size:12px;color:#9ca3af;">
Playzer &mdash; Reserva de canchas online
</p>
</td>
</tr>`;
return wrapLayout(content);
}
function detailRow(label: string, value: string): string {
return `
<tr>
<td style="padding:6px 0;">
<td style="padding:24px 20px 16px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td style="width:100px;font-size:13px;color:#6b7280;vertical-align:top;padding:2px 0;">
${label}
<td valign="top">
<table role="presentation" cellpadding="0" cellspacing="0" style="display:inline-block;background-color:#fef2f2;border-radius:999px;padding:4px 12px;">
<tr>
<td style="font-size:11px;font-weight:700;letter-spacing:0.14em;color:#dc2626;text-transform:uppercase;line-height:1.25rem;">
&#10007; Reserva cancelada
</td>
</tr>
</table>
<h1 style="margin:12px 0 0;font-size:28px;font-weight:700;color:#111827;letter-spacing:-0.025em;">
${data.complexName}
</h1>
</td>
<td style="font-size:13px;font-weight:600;color:#111827;vertical-align:top;padding:2px 0;">
${value}
<td valign="top" align="right" style="white-space:nowrap;">
<table role="presentation" cellpadding="0" cellspacing="0">
<tr>
<td valign="middle" style="padding-right:8px;">
<img src="${APP_BASE_URL}/playzer-favicon-512-transparent.png" alt="Playzer" width="32" height="32" style="display:block;" />
</td>
<td valign="middle">
<span style="font-size:18px;font-weight:700;color:#475569;letter-spacing:-0.025em;">Playzer</span>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="padding:0 20px 16px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f0fdf4;border-radius:24px;border:1px solid rgba(5,150,105,0.3);">
<tr>
<td style="padding:20px 24px;">
<p style="margin:0;font-size:13px;font-weight:600;color:#15803d;">
Tu turno
</p>
<p style="margin:12px 0 0;font-size:28px;font-weight:900;color:#111827;line-height:1.1;letter-spacing:-0.025em;">
${friendlyDate}
</p>
<p style="margin:8px 0 0;font-size:24px;font-weight:900;color:#059669;line-height:1;letter-spacing:-0.025em;">
${data.startTime} &mdash; ${data.endTime}
</p>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="padding:0 20px 16px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#fef2f2;border-radius:24px;border:1px solid rgba(220,38,38,0.3);">
<tr>
<td style="padding:20px 24px;">
<p style="margin:0;font-size:14px;color:#374151;line-height:1.6;">
La reserva <strong style="font-family:monospace;font-weight:700;letter-spacing:2px;">${data.bookingCode}</strong>
fue cancelada. Si ten&eacute;s dudas, contactate con el complejo.
</p>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="padding:0 20px 24px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e5e7eb;border-radius:22px;overflow:hidden;">
<tr>
<td width="50%" style="padding:16px;border-right:1px solid #e5e7eb;background-color:#ffffff;">
<p style="margin:0;font-size:13px;font-weight:500;color:#6b7280;">
Cancha
</p>
<p style="margin:8px 0 0;font-size:14px;font-weight:600;color:#111827;">
${data.courtName} &mdash; ${data.sportName}
</p>
</td>
<td width="50%" style="padding:16px;background-color:#ffffff;">
<p style="margin:0;font-size:13px;font-weight:500;color:#6b7280;">
Cliente
</p>
<p style="margin:8px 0 0;font-size:14px;font-weight:600;color:#111827;">
${data.customerName}
</p>
</td>
</tr>
</table>
</td>
</tr>`;
return wrapLayout(content);
}
type BookingRescheduledEmailData = BookingEmailData & {
previousCourtName: string;
previousStartTime: string;
previousEndTime: string;
};
export function bookingRescheduledHtml(data: BookingRescheduledEmailData): string {
const friendlyDate = formatFriendlyDate(data.date);
const content = `
<tr>
<td style="padding:24px 20px 16px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td valign="top">
<table role="presentation" cellpadding="0" cellspacing="0" style="display:inline-block;background-color:#f4f4f5;border-radius:999px;padding:4px 12px;">
<tr>
<td style="font-size:11px;font-weight:700;letter-spacing:0.14em;color:#71717a;text-transform:uppercase;line-height:1.25rem;">
Reserva reprogramada
</td>
</tr>
</table>
<h1 style="margin:12px 0 0;font-size:28px;font-weight:700;color:#111827;letter-spacing:-0.025em;">
${data.complexName}
</h1>
</td>
<td valign="top" align="right" style="white-space:nowrap;">
<table role="presentation" cellpadding="0" cellspacing="0">
<tr>
<td valign="middle" style="padding-right:8px;">
<img src="${APP_BASE_URL}/playzer-favicon-512-transparent.png" alt="Playzer" width="32" height="32" style="display:block;" />
</td>
<td valign="middle">
<span style="font-size:18px;font-weight:700;color:#475569;letter-spacing:-0.025em;">Playzer</span>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="padding:0 20px 16px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f0fdf4;border-radius:24px;border:1px solid rgba(5,150,105,0.3);">
<tr>
<td style="padding:20px 24px;">
<p style="margin:0;font-size:13px;font-weight:600;color:#15803d;">
Nuevo turno
</p>
<p style="margin:12px 0 0;font-size:28px;font-weight:900;color:#111827;line-height:1.1;letter-spacing:-0.025em;">
${friendlyDate}
</p>
<p style="margin:8px 0 0;font-size:24px;font-weight:900;color:#059669;line-height:1;letter-spacing:-0.025em;">
${data.startTime} &mdash; ${data.endTime}
</p>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="padding:0 20px 16px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e5e7eb;border-radius:22px;overflow:hidden;">
<tr>
<td width="50%" style="padding:16px;border-right:1px solid #e5e7eb;background-color:#ffffff;">
<p style="margin:0;font-size:13px;font-weight:500;color:#6b7280;">
Cancha anterior
</p>
<p style="margin:8px 0 0;font-size:14px;font-weight:600;color:#111827;">
${data.previousCourtName} &mdash; ${data.sportName}
</p>
</td>
<td width="50%" style="padding:16px;background-color:#ffffff;">
<p style="margin:0;font-size:13px;font-weight:500;color:#6b7280;">
Cancha nueva
</p>
<p style="margin:8px 0 0;font-size:14px;font-weight:600;color:#111827;">
${data.courtName} &mdash; ${data.sportName}
</p>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="padding:0 20px 16px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e5e7eb;border-radius:22px;overflow:hidden;">
<tr>
<td width="50%" style="padding:16px;border-right:1px solid #e5e7eb;background-color:#ffffff;">
<p style="margin:0;font-size:13px;font-weight:500;color:#6b7280;">
Horario anterior
</p>
<p style="margin:8px 0 0;font-size:14px;font-weight:600;color:#111827;">
${data.previousStartTime} &mdash; ${data.previousEndTime}
</p>
</td>
<td width="50%" style="padding:16px;background-color:#ffffff;">
<p style="margin:0;font-size:13px;font-weight:500;color:#6b7280;">
Horario nuevo
</p>
<p style="margin:8px 0 0;font-size:14px;font-weight:600;color:#111827;">
${data.startTime} &mdash; ${data.endTime}
</p>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="padding:0 20px 16px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e5e7eb;border-radius:22px;overflow:hidden;">
<tr>
<td width="50%" style="padding:16px;border-right:1px solid #e5e7eb;background-color:#ffffff;">
<p style="margin:0;font-size:13px;font-weight:500;color:#6b7280;">
Cliente
</p>
<p style="margin:8px 0 0;font-size:14px;font-weight:600;color:#111827;">
${data.customerName}
</p>
</td>
<td width="50%" style="padding:16px;background-color:#ffffff;">
<p style="margin:0;font-size:13px;font-weight:500;color:#6b7280;">
C&oacute;digo de reserva
</p>
<p style="margin:8px 0 0;font-family:monospace;font-size:16px;font-weight:700;letter-spacing:0.18em;color:#111827;">
${data.bookingCode}
</p>
</td>
</tr>
</table>
</td>
</tr>
${
data.complexSlug
? `
<tr>
<td style="padding:0 20px 24px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td align="center">
<table role="presentation" cellpadding="0" cellspacing="0" style="background-color:#059669;border-radius:12px;">
<tr>
<td style="padding:14px 32px;font-size:15px;font-weight:600;">
<a href="${APP_BASE_URL}/${data.complexSlug}/booking/confirmed/${data.bookingCode}" style="color:#ffffff;text-decoration:none;display:inline-block;">
Ver reserva
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>`
: ''
}`;
return wrapLayout(content);
}
export function bookingNoShowHtml(data: BookingEmailData): string {
const friendlyDate = formatFriendlyDate(data.date);
const content = `
<tr>
<td style="padding:24px 20px 16px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td valign="top">
<table role="presentation" cellpadding="0" cellspacing="0" style="display:inline-block;background-color:#fffbeb;border-radius:999px;padding:4px 12px;">
<tr>
<td style="font-size:11px;font-weight:700;letter-spacing:0.14em;color:#d97706;text-transform:uppercase;line-height:1.25rem;">
Reserva no concretada
</td>
</tr>
</table>
<h1 style="margin:12px 0 0;font-size:28px;font-weight:700;color:#111827;letter-spacing:-0.025em;">
${data.complexName}
</h1>
</td>
<td valign="top" align="right" style="white-space:nowrap;">
<table role="presentation" cellpadding="0" cellspacing="0">
<tr>
<td valign="middle" style="padding-right:8px;">
<img src="${APP_BASE_URL}/playzer-favicon-512-transparent.png" alt="Playzer" width="32" height="32" style="display:block;" />
</td>
<td valign="middle">
<span style="font-size:18px;font-weight:700;color:#475569;letter-spacing:-0.025em;">Playzer</span>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="padding:0 20px 16px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f0fdf4;border-radius:24px;border:1px solid rgba(5,150,105,0.3);">
<tr>
<td style="padding:20px 24px;">
<p style="margin:0;font-size:13px;font-weight:600;color:#15803d;">
Tu turno
</p>
<p style="margin:12px 0 0;font-size:28px;font-weight:900;color:#111827;line-height:1.1;letter-spacing:-0.025em;">
${friendlyDate}
</p>
<p style="margin:8px 0 0;font-size:24px;font-weight:900;color:#059669;line-height:1;letter-spacing:-0.025em;">
${data.startTime} &mdash; ${data.endTime}
</p>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="padding:0 20px 16px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#fffbeb;border-radius:24px;border:1px solid rgba(217,119,6,0.3);">
<tr>
<td style="padding:20px 24px;">
<p style="margin:0;font-size:14px;color:#374151;line-height:1.6;">
La reserva <strong style="font-family:monospace;font-weight:700;letter-spacing:2px;">${data.bookingCode}</strong>
fue registrada como no concretada por falta de asistencia.
</p>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="padding:0 20px 24px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e5e7eb;border-radius:22px;overflow:hidden;">
<tr>
<td width="50%" style="padding:16px;border-right:1px solid #e5e7eb;background-color:#ffffff;">
<p style="margin:0;font-size:13px;font-weight:500;color:#6b7280;">
Cancha
</p>
<p style="margin:8px 0 0;font-size:14px;font-weight:600;color:#111827;">
${data.courtName} &mdash; ${data.sportName}
</p>
</td>
<td width="50%" style="padding:16px;background-color:#ffffff;">
<p style="margin:0;font-size:13px;font-weight:500;color:#6b7280;">
Cliente
</p>
<p style="margin:8px 0 0;font-size:14px;font-weight:600;color:#111827;">
${data.customerName}
</p>
</td>
</tr>
</table>
</td>
</tr>`;
return wrapLayout(content);
}

View File

@@ -37,6 +37,16 @@ export type Account = Prisma.AccountModel
*
*/
export type Verification = Prisma.VerificationModel
/**
* Model ComplexBilling
*
*/
export type ComplexBilling = Prisma.ComplexBillingModel
/**
* Model BillingEvent
*
*/
export type BillingEvent = Prisma.BillingEventModel
/**
* Model Complex
*
@@ -62,6 +72,11 @@ export type Sport = Prisma.SportModel
*
*/
export type Court = Prisma.CourtModel
/**
* Model CourtMaintenance
*
*/
export type CourtMaintenance = Prisma.CourtMaintenanceModel
/**
* Model CourtAvailability
*
@@ -82,6 +97,11 @@ export type CourtBooking = Prisma.CourtBookingModel
*
*/
export type CourtBookingLog = Prisma.CourtBookingLogModel
/**
* Model RecurringBookingGroup
*
*/
export type RecurringBookingGroup = Prisma.RecurringBookingGroupModel
/**
* Model PasswordResetRequest
*

View File

@@ -61,6 +61,16 @@ export type Account = Prisma.AccountModel
*
*/
export type Verification = Prisma.VerificationModel
/**
* Model ComplexBilling
*
*/
export type ComplexBilling = Prisma.ComplexBillingModel
/**
* Model BillingEvent
*
*/
export type BillingEvent = Prisma.BillingEventModel
/**
* Model Complex
*
@@ -86,6 +96,11 @@ export type Sport = Prisma.SportModel
*
*/
export type Court = Prisma.CourtModel
/**
* Model CourtMaintenance
*
*/
export type CourtMaintenance = Prisma.CourtMaintenanceModel
/**
* Model CourtAvailability
*
@@ -106,6 +121,11 @@ export type CourtBooking = Prisma.CourtBookingModel
*
*/
export type CourtBookingLog = Prisma.CourtBookingLogModel
/**
* Model RecurringBookingGroup
*
*/
export type RecurringBookingGroup = Prisma.RecurringBookingGroupModel
/**
* Model PasswordResetRequest
*

View File

@@ -49,6 +49,17 @@ export type StringNullableFilter<$PrismaModel = never> = {
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
}
export type DateTimeNullableFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
}
export type DateTimeFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
@@ -109,31 +120,6 @@ export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
}
export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
}
export type DateTimeNullableFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
}
export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
@@ -148,6 +134,47 @@ export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
}
export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
}
export type FloatNullableFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedFloatNullableFilter<$PrismaModel> | number | null
}
export type FloatNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedFloatNullableWithAggregatesFilter<$PrismaModel> | number | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_sum?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_min?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_max?: Prisma.NestedFloatNullableFilter<$PrismaModel>
}
export type UuidFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
@@ -160,6 +187,20 @@ export type UuidFilter<$PrismaModel = never> = {
not?: Prisma.NestedUuidFilter<$PrismaModel> | string
}
export type EnumBillingStatusFilter<$PrismaModel = never> = {
equals?: $Enums.BillingStatus | Prisma.EnumBillingStatusFieldRefInput<$PrismaModel>
in?: $Enums.BillingStatus[] | Prisma.ListEnumBillingStatusFieldRefInput<$PrismaModel>
notIn?: $Enums.BillingStatus[] | Prisma.ListEnumBillingStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumBillingStatusFilter<$PrismaModel> | $Enums.BillingStatus
}
export type EnumBillingProviderNullableFilter<$PrismaModel = never> = {
equals?: $Enums.BillingProvider | Prisma.EnumBillingProviderFieldRefInput<$PrismaModel> | null
in?: $Enums.BillingProvider[] | Prisma.ListEnumBillingProviderFieldRefInput<$PrismaModel> | null
notIn?: $Enums.BillingProvider[] | Prisma.ListEnumBillingProviderFieldRefInput<$PrismaModel> | null
not?: Prisma.NestedEnumBillingProviderNullableFilter<$PrismaModel> | $Enums.BillingProvider | null
}
export type UuidWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
@@ -175,6 +216,111 @@ export type UuidWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedStringFilter<$PrismaModel>
}
export type EnumBillingStatusWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.BillingStatus | Prisma.EnumBillingStatusFieldRefInput<$PrismaModel>
in?: $Enums.BillingStatus[] | Prisma.ListEnumBillingStatusFieldRefInput<$PrismaModel>
notIn?: $Enums.BillingStatus[] | Prisma.ListEnumBillingStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumBillingStatusWithAggregatesFilter<$PrismaModel> | $Enums.BillingStatus
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedEnumBillingStatusFilter<$PrismaModel>
_max?: Prisma.NestedEnumBillingStatusFilter<$PrismaModel>
}
export type EnumBillingProviderNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.BillingProvider | Prisma.EnumBillingProviderFieldRefInput<$PrismaModel> | null
in?: $Enums.BillingProvider[] | Prisma.ListEnumBillingProviderFieldRefInput<$PrismaModel> | null
notIn?: $Enums.BillingProvider[] | Prisma.ListEnumBillingProviderFieldRefInput<$PrismaModel> | null
not?: Prisma.NestedEnumBillingProviderNullableWithAggregatesFilter<$PrismaModel> | $Enums.BillingProvider | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedEnumBillingProviderNullableFilter<$PrismaModel>
_max?: Prisma.NestedEnumBillingProviderNullableFilter<$PrismaModel>
}
export type EnumBillingProviderFilter<$PrismaModel = never> = {
equals?: $Enums.BillingProvider | Prisma.EnumBillingProviderFieldRefInput<$PrismaModel>
in?: $Enums.BillingProvider[] | Prisma.ListEnumBillingProviderFieldRefInput<$PrismaModel>
notIn?: $Enums.BillingProvider[] | Prisma.ListEnumBillingProviderFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumBillingProviderFilter<$PrismaModel> | $Enums.BillingProvider
}
export type JsonNullableFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<JsonNullableFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonNullableFilterBase<$PrismaModel>>, 'path'>>,
Required<JsonNullableFilterBase<$PrismaModel>>
>
| Prisma.OptionalFlat<Omit<Required<JsonNullableFilterBase<$PrismaModel>>, 'path'>>
export type JsonNullableFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string[]
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
}
export type EnumBillingStatusNullableFilter<$PrismaModel = never> = {
equals?: $Enums.BillingStatus | Prisma.EnumBillingStatusFieldRefInput<$PrismaModel> | null
in?: $Enums.BillingStatus[] | Prisma.ListEnumBillingStatusFieldRefInput<$PrismaModel> | null
notIn?: $Enums.BillingStatus[] | Prisma.ListEnumBillingStatusFieldRefInput<$PrismaModel> | null
not?: Prisma.NestedEnumBillingStatusNullableFilter<$PrismaModel> | $Enums.BillingStatus | null
}
export type EnumBillingProviderWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.BillingProvider | Prisma.EnumBillingProviderFieldRefInput<$PrismaModel>
in?: $Enums.BillingProvider[] | Prisma.ListEnumBillingProviderFieldRefInput<$PrismaModel>
notIn?: $Enums.BillingProvider[] | Prisma.ListEnumBillingProviderFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumBillingProviderWithAggregatesFilter<$PrismaModel> | $Enums.BillingProvider
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedEnumBillingProviderFilter<$PrismaModel>
_max?: Prisma.NestedEnumBillingProviderFilter<$PrismaModel>
}
export type JsonNullableWithAggregatesFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, 'path'>>,
Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>
>
| Prisma.OptionalFlat<Omit<Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, 'path'>>
export type JsonNullableWithAggregatesFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string[]
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedJsonNullableFilter<$PrismaModel>
_max?: Prisma.NestedJsonNullableFilter<$PrismaModel>
}
export type EnumBillingStatusNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.BillingStatus | Prisma.EnumBillingStatusFieldRefInput<$PrismaModel> | null
in?: $Enums.BillingStatus[] | Prisma.ListEnumBillingStatusFieldRefInput<$PrismaModel> | null
notIn?: $Enums.BillingStatus[] | Prisma.ListEnumBillingStatusFieldRefInput<$PrismaModel> | null
not?: Prisma.NestedEnumBillingStatusNullableWithAggregatesFilter<$PrismaModel> | $Enums.BillingStatus | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedEnumBillingStatusNullableFilter<$PrismaModel>
_max?: Prisma.NestedEnumBillingStatusNullableFilter<$PrismaModel>
}
export type EnumComplexUserRoleFilter<$PrismaModel = never> = {
equals?: $Enums.ComplexUserRole | Prisma.EnumComplexUserRoleFieldRefInput<$PrismaModel>
in?: $Enums.ComplexUserRole[] | Prisma.ListEnumComplexUserRoleFieldRefInput<$PrismaModel>
@@ -287,6 +433,18 @@ export type EnumCourtBookingStatusFilter<$PrismaModel = never> = {
not?: Prisma.NestedEnumCourtBookingStatusFilter<$PrismaModel> | $Enums.CourtBookingStatus
}
export type UuidNullableFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
mode?: Prisma.QueryMode
not?: Prisma.NestedUuidNullableFilter<$PrismaModel> | string | null
}
export type EnumCourtBookingStatusWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.CourtBookingStatus | Prisma.EnumCourtBookingStatusFieldRefInput<$PrismaModel>
in?: $Enums.CourtBookingStatus[] | Prisma.ListEnumCourtBookingStatusFieldRefInput<$PrismaModel>
@@ -297,6 +455,38 @@ export type EnumCourtBookingStatusWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedEnumCourtBookingStatusFilter<$PrismaModel>
}
export type UuidNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
mode?: Prisma.QueryMode
not?: Prisma.NestedUuidNullableWithAggregatesFilter<$PrismaModel> | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
}
export type EnumRecurringBookingGroupStatusFilter<$PrismaModel = never> = {
equals?: $Enums.RecurringBookingGroupStatus | Prisma.EnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
in?: $Enums.RecurringBookingGroupStatus[] | Prisma.ListEnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
notIn?: $Enums.RecurringBookingGroupStatus[] | Prisma.ListEnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumRecurringBookingGroupStatusFilter<$PrismaModel> | $Enums.RecurringBookingGroupStatus
}
export type EnumRecurringBookingGroupStatusWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.RecurringBookingGroupStatus | Prisma.EnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
in?: $Enums.RecurringBookingGroupStatus[] | Prisma.ListEnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
notIn?: $Enums.RecurringBookingGroupStatus[] | Prisma.ListEnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumRecurringBookingGroupStatusWithAggregatesFilter<$PrismaModel> | $Enums.RecurringBookingGroupStatus
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedEnumRecurringBookingGroupStatusFilter<$PrismaModel>
_max?: Prisma.NestedEnumRecurringBookingGroupStatusFilter<$PrismaModel>
}
export type JsonFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<JsonFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonFilterBase<$PrismaModel>>, 'path'>>,
@@ -381,6 +571,17 @@ export type NestedStringNullableFilter<$PrismaModel = never> = {
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
}
export type NestedDateTimeNullableFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
}
export type NestedDateTimeFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
@@ -456,31 +657,6 @@ export type NestedIntNullableFilter<$PrismaModel = never> = {
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
}
export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
}
export type NestedDateTimeNullableFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null
}
export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null
@@ -495,6 +671,47 @@ export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
}
export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
}
export type NestedFloatNullableFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedFloatNullableFilter<$PrismaModel> | number | null
}
export type NestedFloatNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedFloatNullableWithAggregatesFilter<$PrismaModel> | number | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_sum?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_min?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_max?: Prisma.NestedFloatNullableFilter<$PrismaModel>
}
export type NestedUuidFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
@@ -506,6 +723,20 @@ export type NestedUuidFilter<$PrismaModel = never> = {
not?: Prisma.NestedUuidFilter<$PrismaModel> | string
}
export type NestedEnumBillingStatusFilter<$PrismaModel = never> = {
equals?: $Enums.BillingStatus | Prisma.EnumBillingStatusFieldRefInput<$PrismaModel>
in?: $Enums.BillingStatus[] | Prisma.ListEnumBillingStatusFieldRefInput<$PrismaModel>
notIn?: $Enums.BillingStatus[] | Prisma.ListEnumBillingStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumBillingStatusFilter<$PrismaModel> | $Enums.BillingStatus
}
export type NestedEnumBillingProviderNullableFilter<$PrismaModel = never> = {
equals?: $Enums.BillingProvider | Prisma.EnumBillingProviderFieldRefInput<$PrismaModel> | null
in?: $Enums.BillingProvider[] | Prisma.ListEnumBillingProviderFieldRefInput<$PrismaModel> | null
notIn?: $Enums.BillingProvider[] | Prisma.ListEnumBillingProviderFieldRefInput<$PrismaModel> | null
not?: Prisma.NestedEnumBillingProviderNullableFilter<$PrismaModel> | $Enums.BillingProvider | null
}
export type NestedUuidWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
@@ -520,6 +751,84 @@ export type NestedUuidWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedStringFilter<$PrismaModel>
}
export type NestedEnumBillingStatusWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.BillingStatus | Prisma.EnumBillingStatusFieldRefInput<$PrismaModel>
in?: $Enums.BillingStatus[] | Prisma.ListEnumBillingStatusFieldRefInput<$PrismaModel>
notIn?: $Enums.BillingStatus[] | Prisma.ListEnumBillingStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumBillingStatusWithAggregatesFilter<$PrismaModel> | $Enums.BillingStatus
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedEnumBillingStatusFilter<$PrismaModel>
_max?: Prisma.NestedEnumBillingStatusFilter<$PrismaModel>
}
export type NestedEnumBillingProviderNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.BillingProvider | Prisma.EnumBillingProviderFieldRefInput<$PrismaModel> | null
in?: $Enums.BillingProvider[] | Prisma.ListEnumBillingProviderFieldRefInput<$PrismaModel> | null
notIn?: $Enums.BillingProvider[] | Prisma.ListEnumBillingProviderFieldRefInput<$PrismaModel> | null
not?: Prisma.NestedEnumBillingProviderNullableWithAggregatesFilter<$PrismaModel> | $Enums.BillingProvider | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedEnumBillingProviderNullableFilter<$PrismaModel>
_max?: Prisma.NestedEnumBillingProviderNullableFilter<$PrismaModel>
}
export type NestedEnumBillingProviderFilter<$PrismaModel = never> = {
equals?: $Enums.BillingProvider | Prisma.EnumBillingProviderFieldRefInput<$PrismaModel>
in?: $Enums.BillingProvider[] | Prisma.ListEnumBillingProviderFieldRefInput<$PrismaModel>
notIn?: $Enums.BillingProvider[] | Prisma.ListEnumBillingProviderFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumBillingProviderFilter<$PrismaModel> | $Enums.BillingProvider
}
export type NestedEnumBillingStatusNullableFilter<$PrismaModel = never> = {
equals?: $Enums.BillingStatus | Prisma.EnumBillingStatusFieldRefInput<$PrismaModel> | null
in?: $Enums.BillingStatus[] | Prisma.ListEnumBillingStatusFieldRefInput<$PrismaModel> | null
notIn?: $Enums.BillingStatus[] | Prisma.ListEnumBillingStatusFieldRefInput<$PrismaModel> | null
not?: Prisma.NestedEnumBillingStatusNullableFilter<$PrismaModel> | $Enums.BillingStatus | null
}
export type NestedEnumBillingProviderWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.BillingProvider | Prisma.EnumBillingProviderFieldRefInput<$PrismaModel>
in?: $Enums.BillingProvider[] | Prisma.ListEnumBillingProviderFieldRefInput<$PrismaModel>
notIn?: $Enums.BillingProvider[] | Prisma.ListEnumBillingProviderFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumBillingProviderWithAggregatesFilter<$PrismaModel> | $Enums.BillingProvider
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedEnumBillingProviderFilter<$PrismaModel>
_max?: Prisma.NestedEnumBillingProviderFilter<$PrismaModel>
}
export type NestedJsonNullableFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<NestedJsonNullableFilterBase<$PrismaModel>>, Exclude<keyof Required<NestedJsonNullableFilterBase<$PrismaModel>>, 'path'>>,
Required<NestedJsonNullableFilterBase<$PrismaModel>>
>
| Prisma.OptionalFlat<Omit<Required<NestedJsonNullableFilterBase<$PrismaModel>>, 'path'>>
export type NestedJsonNullableFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string[]
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
}
export type NestedEnumBillingStatusNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.BillingStatus | Prisma.EnumBillingStatusFieldRefInput<$PrismaModel> | null
in?: $Enums.BillingStatus[] | Prisma.ListEnumBillingStatusFieldRefInput<$PrismaModel> | null
notIn?: $Enums.BillingStatus[] | Prisma.ListEnumBillingStatusFieldRefInput<$PrismaModel> | null
not?: Prisma.NestedEnumBillingStatusNullableWithAggregatesFilter<$PrismaModel> | $Enums.BillingStatus | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedEnumBillingStatusNullableFilter<$PrismaModel>
_max?: Prisma.NestedEnumBillingStatusNullableFilter<$PrismaModel>
}
export type NestedEnumComplexUserRoleFilter<$PrismaModel = never> = {
equals?: $Enums.ComplexUserRole | Prisma.EnumComplexUserRoleFieldRefInput<$PrismaModel>
in?: $Enums.ComplexUserRole[] | Prisma.ListEnumComplexUserRoleFieldRefInput<$PrismaModel>
@@ -632,6 +941,17 @@ export type NestedEnumCourtBookingStatusFilter<$PrismaModel = never> = {
not?: Prisma.NestedEnumCourtBookingStatusFilter<$PrismaModel> | $Enums.CourtBookingStatus
}
export type NestedUuidNullableFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedUuidNullableFilter<$PrismaModel> | string | null
}
export type NestedEnumCourtBookingStatusWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.CourtBookingStatus | Prisma.EnumCourtBookingStatusFieldRefInput<$PrismaModel>
in?: $Enums.CourtBookingStatus[] | Prisma.ListEnumCourtBookingStatusFieldRefInput<$PrismaModel>
@@ -642,6 +962,37 @@ export type NestedEnumCourtBookingStatusWithAggregatesFilter<$PrismaModel = neve
_max?: Prisma.NestedEnumCourtBookingStatusFilter<$PrismaModel>
}
export type NestedUuidNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedUuidNullableWithAggregatesFilter<$PrismaModel> | string | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
}
export type NestedEnumRecurringBookingGroupStatusFilter<$PrismaModel = never> = {
equals?: $Enums.RecurringBookingGroupStatus | Prisma.EnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
in?: $Enums.RecurringBookingGroupStatus[] | Prisma.ListEnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
notIn?: $Enums.RecurringBookingGroupStatus[] | Prisma.ListEnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumRecurringBookingGroupStatusFilter<$PrismaModel> | $Enums.RecurringBookingGroupStatus
}
export type NestedEnumRecurringBookingGroupStatusWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.RecurringBookingGroupStatus | Prisma.EnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
in?: $Enums.RecurringBookingGroupStatus[] | Prisma.ListEnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
notIn?: $Enums.RecurringBookingGroupStatus[] | Prisma.ListEnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumRecurringBookingGroupStatusWithAggregatesFilter<$PrismaModel> | $Enums.RecurringBookingGroupStatus
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedEnumRecurringBookingGroupStatusFilter<$PrismaModel>
_max?: Prisma.NestedEnumRecurringBookingGroupStatusFilter<$PrismaModel>
}
export type NestedJsonFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<NestedJsonFilterBase<$PrismaModel>>, Exclude<keyof Required<NestedJsonFilterBase<$PrismaModel>>, 'path'>>,

View File

@@ -9,6 +9,26 @@
* 🟢 You can import this file directly.
*/
export const BillingProvider = {
STRIPE: 'STRIPE',
MERCADOPAGO: 'MERCADOPAGO',
PAYPAL: 'PAYPAL'
} as const
export type BillingProvider = (typeof BillingProvider)[keyof typeof BillingProvider]
export const BillingStatus = {
TRIAL: 'TRIAL',
ACTIVE: 'ACTIVE',
PAST_DUE: 'PAST_DUE',
CANCELED: 'CANCELED',
SUSPENDED: 'SUSPENDED'
} as const
export type BillingStatus = (typeof BillingStatus)[keyof typeof BillingStatus]
export const ComplexUserRole = {
ADMIN: 'ADMIN',
EMPLOYEE: 'EMPLOYEE'
@@ -38,3 +58,11 @@ export const CourtBookingStatus = {
} as const
export type CourtBookingStatus = (typeof CourtBookingStatus)[keyof typeof CourtBookingStatus]
export const RecurringBookingGroupStatus = {
ACTIVE: 'ACTIVE',
CANCELLED: 'CANCELLED'
} as const
export type RecurringBookingGroupStatus = (typeof RecurringBookingGroupStatus)[keyof typeof RecurringBookingGroupStatus]

File diff suppressed because one or more lines are too long

View File

@@ -388,15 +388,19 @@ export const ModelName = {
Session: 'Session',
Account: 'Account',
Verification: 'Verification',
ComplexBilling: 'ComplexBilling',
BillingEvent: 'BillingEvent',
Complex: 'Complex',
ComplexUser: 'ComplexUser',
ComplexInvitation: 'ComplexInvitation',
Sport: 'Sport',
Court: 'Court',
CourtMaintenance: 'CourtMaintenance',
CourtAvailability: 'CourtAvailability',
CourtPriceRule: 'CourtPriceRule',
CourtBooking: 'CourtBooking',
CourtBookingLog: 'CourtBookingLog',
RecurringBookingGroup: 'RecurringBookingGroup',
PasswordResetRequest: 'PasswordResetRequest',
Plan: 'Plan'
} as const
@@ -414,7 +418,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
omit: GlobalOmitOptions
}
meta: {
modelProps: "user" | "session" | "account" | "verification" | "complex" | "complexUser" | "complexInvitation" | "sport" | "court" | "courtAvailability" | "courtPriceRule" | "courtBooking" | "courtBookingLog" | "passwordResetRequest" | "plan"
modelProps: "user" | "session" | "account" | "verification" | "complexBilling" | "billingEvent" | "complex" | "complexUser" | "complexInvitation" | "sport" | "court" | "courtMaintenance" | "courtAvailability" | "courtPriceRule" | "courtBooking" | "courtBookingLog" | "recurringBookingGroup" | "passwordResetRequest" | "plan"
txIsolationLevel: TransactionIsolationLevel
}
model: {
@@ -714,6 +718,154 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
}
}
}
ComplexBilling: {
payload: Prisma.$ComplexBillingPayload<ExtArgs>
fields: Prisma.ComplexBillingFieldRefs
operations: {
findUnique: {
args: Prisma.ComplexBillingFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ComplexBillingPayload> | null
}
findUniqueOrThrow: {
args: Prisma.ComplexBillingFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ComplexBillingPayload>
}
findFirst: {
args: Prisma.ComplexBillingFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ComplexBillingPayload> | null
}
findFirstOrThrow: {
args: Prisma.ComplexBillingFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ComplexBillingPayload>
}
findMany: {
args: Prisma.ComplexBillingFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ComplexBillingPayload>[]
}
create: {
args: Prisma.ComplexBillingCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ComplexBillingPayload>
}
createMany: {
args: Prisma.ComplexBillingCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.ComplexBillingCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ComplexBillingPayload>[]
}
delete: {
args: Prisma.ComplexBillingDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ComplexBillingPayload>
}
update: {
args: Prisma.ComplexBillingUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ComplexBillingPayload>
}
deleteMany: {
args: Prisma.ComplexBillingDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.ComplexBillingUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.ComplexBillingUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ComplexBillingPayload>[]
}
upsert: {
args: Prisma.ComplexBillingUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$ComplexBillingPayload>
}
aggregate: {
args: Prisma.ComplexBillingAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateComplexBilling>
}
groupBy: {
args: Prisma.ComplexBillingGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.ComplexBillingGroupByOutputType>[]
}
count: {
args: Prisma.ComplexBillingCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.ComplexBillingCountAggregateOutputType> | number
}
}
}
BillingEvent: {
payload: Prisma.$BillingEventPayload<ExtArgs>
fields: Prisma.BillingEventFieldRefs
operations: {
findUnique: {
args: Prisma.BillingEventFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$BillingEventPayload> | null
}
findUniqueOrThrow: {
args: Prisma.BillingEventFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$BillingEventPayload>
}
findFirst: {
args: Prisma.BillingEventFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$BillingEventPayload> | null
}
findFirstOrThrow: {
args: Prisma.BillingEventFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$BillingEventPayload>
}
findMany: {
args: Prisma.BillingEventFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$BillingEventPayload>[]
}
create: {
args: Prisma.BillingEventCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$BillingEventPayload>
}
createMany: {
args: Prisma.BillingEventCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.BillingEventCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$BillingEventPayload>[]
}
delete: {
args: Prisma.BillingEventDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$BillingEventPayload>
}
update: {
args: Prisma.BillingEventUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$BillingEventPayload>
}
deleteMany: {
args: Prisma.BillingEventDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.BillingEventUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.BillingEventUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$BillingEventPayload>[]
}
upsert: {
args: Prisma.BillingEventUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$BillingEventPayload>
}
aggregate: {
args: Prisma.BillingEventAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateBillingEvent>
}
groupBy: {
args: Prisma.BillingEventGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.BillingEventGroupByOutputType>[]
}
count: {
args: Prisma.BillingEventCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.BillingEventCountAggregateOutputType> | number
}
}
}
Complex: {
payload: Prisma.$ComplexPayload<ExtArgs>
fields: Prisma.ComplexFieldRefs
@@ -1084,6 +1236,80 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
}
}
}
CourtMaintenance: {
payload: Prisma.$CourtMaintenancePayload<ExtArgs>
fields: Prisma.CourtMaintenanceFieldRefs
operations: {
findUnique: {
args: Prisma.CourtMaintenanceFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtMaintenancePayload> | null
}
findUniqueOrThrow: {
args: Prisma.CourtMaintenanceFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtMaintenancePayload>
}
findFirst: {
args: Prisma.CourtMaintenanceFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtMaintenancePayload> | null
}
findFirstOrThrow: {
args: Prisma.CourtMaintenanceFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtMaintenancePayload>
}
findMany: {
args: Prisma.CourtMaintenanceFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtMaintenancePayload>[]
}
create: {
args: Prisma.CourtMaintenanceCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtMaintenancePayload>
}
createMany: {
args: Prisma.CourtMaintenanceCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.CourtMaintenanceCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtMaintenancePayload>[]
}
delete: {
args: Prisma.CourtMaintenanceDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtMaintenancePayload>
}
update: {
args: Prisma.CourtMaintenanceUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtMaintenancePayload>
}
deleteMany: {
args: Prisma.CourtMaintenanceDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.CourtMaintenanceUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.CourtMaintenanceUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtMaintenancePayload>[]
}
upsert: {
args: Prisma.CourtMaintenanceUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtMaintenancePayload>
}
aggregate: {
args: Prisma.CourtMaintenanceAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateCourtMaintenance>
}
groupBy: {
args: Prisma.CourtMaintenanceGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.CourtMaintenanceGroupByOutputType>[]
}
count: {
args: Prisma.CourtMaintenanceCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.CourtMaintenanceCountAggregateOutputType> | number
}
}
}
CourtAvailability: {
payload: Prisma.$CourtAvailabilityPayload<ExtArgs>
fields: Prisma.CourtAvailabilityFieldRefs
@@ -1380,6 +1606,80 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
}
}
}
RecurringBookingGroup: {
payload: Prisma.$RecurringBookingGroupPayload<ExtArgs>
fields: Prisma.RecurringBookingGroupFieldRefs
operations: {
findUnique: {
args: Prisma.RecurringBookingGroupFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RecurringBookingGroupPayload> | null
}
findUniqueOrThrow: {
args: Prisma.RecurringBookingGroupFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RecurringBookingGroupPayload>
}
findFirst: {
args: Prisma.RecurringBookingGroupFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RecurringBookingGroupPayload> | null
}
findFirstOrThrow: {
args: Prisma.RecurringBookingGroupFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RecurringBookingGroupPayload>
}
findMany: {
args: Prisma.RecurringBookingGroupFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RecurringBookingGroupPayload>[]
}
create: {
args: Prisma.RecurringBookingGroupCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RecurringBookingGroupPayload>
}
createMany: {
args: Prisma.RecurringBookingGroupCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.RecurringBookingGroupCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RecurringBookingGroupPayload>[]
}
delete: {
args: Prisma.RecurringBookingGroupDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RecurringBookingGroupPayload>
}
update: {
args: Prisma.RecurringBookingGroupUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RecurringBookingGroupPayload>
}
deleteMany: {
args: Prisma.RecurringBookingGroupDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.RecurringBookingGroupUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.RecurringBookingGroupUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RecurringBookingGroupPayload>[]
}
upsert: {
args: Prisma.RecurringBookingGroupUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RecurringBookingGroupPayload>
}
aggregate: {
args: Prisma.RecurringBookingGroupAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateRecurringBookingGroup>
}
groupBy: {
args: Prisma.RecurringBookingGroupGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.RecurringBookingGroupGroupByOutputType>[]
}
count: {
args: Prisma.RecurringBookingGroupCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.RecurringBookingGroupCountAggregateOutputType> | number
}
}
}
PasswordResetRequest: {
payload: Prisma.$PasswordResetRequestPayload<ExtArgs>
fields: Prisma.PasswordResetRequestFieldRefs
@@ -1574,6 +1874,9 @@ export const UserScalarFieldEnum = {
emailVerified: 'emailVerified',
image: 'image',
phone: 'phone',
banned: 'banned',
bannedAt: 'bannedAt',
banReason: 'banReason',
createdAt: 'createdAt',
updatedAt: 'updatedAt',
role: 'role'
@@ -1590,6 +1893,11 @@ export const SessionScalarFieldEnum = {
updatedAt: 'updatedAt',
ipAddress: 'ipAddress',
userAgent: 'userAgent',
country: 'country',
city: 'city',
countryCode: 'countryCode',
latitude: 'latitude',
longitude: 'longitude',
userId: 'userId'
} as const
@@ -1627,6 +1935,42 @@ export const VerificationScalarFieldEnum = {
export type VerificationScalarFieldEnum = (typeof VerificationScalarFieldEnum)[keyof typeof VerificationScalarFieldEnum]
export const ComplexBillingScalarFieldEnum = {
complexId: 'complexId',
status: 'status',
planCode: 'planCode',
currency: 'currency',
provider: 'provider',
providerCustomerId: 'providerCustomerId',
providerSubscriptionId: 'providerSubscriptionId',
providerPreapprovalId: 'providerPreapprovalId',
currentPeriodStart: 'currentPeriodStart',
currentPeriodEnd: 'currentPeriodEnd',
trialEndsAt: 'trialEndsAt',
canceledAt: 'canceledAt',
suspendedAt: 'suspendedAt',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type ComplexBillingScalarFieldEnum = (typeof ComplexBillingScalarFieldEnum)[keyof typeof ComplexBillingScalarFieldEnum]
export const BillingEventScalarFieldEnum = {
id: 'id',
complexId: 'complexId',
eventType: 'eventType',
provider: 'provider',
providerEventId: 'providerEventId',
providerData: 'providerData',
previousStatus: 'previousStatus',
newStatus: 'newStatus',
processedAt: 'processedAt'
} as const
export type BillingEventScalarFieldEnum = (typeof BillingEventScalarFieldEnum)[keyof typeof BillingEventScalarFieldEnum]
export const ComplexScalarFieldEnum = {
id: 'id',
complexName: 'complexName',
@@ -1688,6 +2032,8 @@ export const CourtScalarFieldEnum = {
name: 'name',
slotDurationMinutes: 'slotDurationMinutes',
basePrice: 'basePrice',
isUnderMaintenance: 'isUnderMaintenance',
maintenanceReason: 'maintenanceReason',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
@@ -1695,6 +2041,20 @@ export const CourtScalarFieldEnum = {
export type CourtScalarFieldEnum = (typeof CourtScalarFieldEnum)[keyof typeof CourtScalarFieldEnum]
export const CourtMaintenanceScalarFieldEnum = {
id: 'id',
courtId: 'courtId',
startDate: 'startDate',
startTime: 'startTime',
endTime: 'endTime',
reason: 'reason',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type CourtMaintenanceScalarFieldEnum = (typeof CourtMaintenanceScalarFieldEnum)[keyof typeof CourtMaintenanceScalarFieldEnum]
export const CourtAvailabilityScalarFieldEnum = {
id: 'id',
courtId: 'courtId',
@@ -1733,6 +2093,7 @@ export const CourtBookingScalarFieldEnum = {
customerPhone: 'customerPhone',
customerEmail: 'customerEmail',
status: 'status',
recurringGroupId: 'recurringGroupId',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
@@ -1749,6 +2110,9 @@ export const CourtBookingLogScalarFieldEnum = {
endTime: 'endTime',
previousStatus: 'previousStatus',
newStatus: 'newStatus',
previousCourtId: 'previousCourtId',
previousStartTime: 'previousStartTime',
previousEndTime: 'previousEndTime',
customerName: 'customerName',
customerPhone: 'customerPhone',
customerEmail: 'customerEmail',
@@ -1758,6 +2122,26 @@ export const CourtBookingLogScalarFieldEnum = {
export type CourtBookingLogScalarFieldEnum = (typeof CourtBookingLogScalarFieldEnum)[keyof typeof CourtBookingLogScalarFieldEnum]
export const RecurringBookingGroupScalarFieldEnum = {
id: 'id',
complexId: 'complexId',
courtId: 'courtId',
startTime: 'startTime',
endTime: 'endTime',
dayOfWeek: 'dayOfWeek',
startDate: 'startDate',
endDate: 'endDate',
status: 'status',
customerName: 'customerName',
customerPhone: 'customerPhone',
customerEmail: 'customerEmail',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type RecurringBookingGroupScalarFieldEnum = (typeof RecurringBookingGroupScalarFieldEnum)[keyof typeof RecurringBookingGroupScalarFieldEnum]
export const PasswordResetRequestScalarFieldEnum = {
id: 'id',
email: 'email',
@@ -1793,6 +2177,14 @@ export const SortOrder = {
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
export const NullableJsonNullValueInput = {
DbNull: DbNull,
JsonNull: JsonNull
} as const
export type NullableJsonNullValueInput = (typeof NullableJsonNullValueInput)[keyof typeof NullableJsonNullValueInput]
export const JsonNullValueInput = {
JsonNull: JsonNull
} as const
@@ -1866,6 +2258,62 @@ export type ListDateTimeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaM
/**
* Reference to a field of type 'Float'
*/
export type FloatFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Float'>
/**
* Reference to a field of type 'Float[]'
*/
export type ListFloatFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Float[]'>
/**
* Reference to a field of type 'BillingStatus'
*/
export type EnumBillingStatusFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'BillingStatus'>
/**
* Reference to a field of type 'BillingStatus[]'
*/
export type ListEnumBillingStatusFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'BillingStatus[]'>
/**
* Reference to a field of type 'BillingProvider'
*/
export type EnumBillingProviderFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'BillingProvider'>
/**
* Reference to a field of type 'BillingProvider[]'
*/
export type ListEnumBillingProviderFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'BillingProvider[]'>
/**
* Reference to a field of type 'Json'
*/
export type JsonFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Json'>
/**
* Reference to a field of type 'QueryMode'
*/
export type EnumQueryModeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'QueryMode'>
/**
* Reference to a field of type 'ComplexUserRole'
*/
@@ -1937,30 +2385,16 @@ export type ListEnumCourtBookingStatusFieldRefInput<$PrismaModel> = FieldRefInpu
/**
* Reference to a field of type 'Json'
* Reference to a field of type 'RecurringBookingGroupStatus'
*/
export type JsonFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Json'>
export type EnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'RecurringBookingGroupStatus'>
/**
* Reference to a field of type 'QueryMode'
* Reference to a field of type 'RecurringBookingGroupStatus[]'
*/
export type EnumQueryModeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'QueryMode'>
/**
* Reference to a field of type 'Float'
*/
export type FloatFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Float'>
/**
* Reference to a field of type 'Float[]'
*/
export type ListFloatFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Float[]'>
export type ListEnumRecurringBookingGroupStatusFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'RecurringBookingGroupStatus[]'>
/**
@@ -2062,15 +2496,19 @@ export type GlobalOmitConfig = {
session?: Prisma.SessionOmit
account?: Prisma.AccountOmit
verification?: Prisma.VerificationOmit
complexBilling?: Prisma.ComplexBillingOmit
billingEvent?: Prisma.BillingEventOmit
complex?: Prisma.ComplexOmit
complexUser?: Prisma.ComplexUserOmit
complexInvitation?: Prisma.ComplexInvitationOmit
sport?: Prisma.SportOmit
court?: Prisma.CourtOmit
courtMaintenance?: Prisma.CourtMaintenanceOmit
courtAvailability?: Prisma.CourtAvailabilityOmit
courtPriceRule?: Prisma.CourtPriceRuleOmit
courtBooking?: Prisma.CourtBookingOmit
courtBookingLog?: Prisma.CourtBookingLogOmit
recurringBookingGroup?: Prisma.RecurringBookingGroupOmit
passwordResetRequest?: Prisma.PasswordResetRequestOmit
plan?: Prisma.PlanOmit
}

View File

@@ -55,15 +55,19 @@ export const ModelName = {
Session: 'Session',
Account: 'Account',
Verification: 'Verification',
ComplexBilling: 'ComplexBilling',
BillingEvent: 'BillingEvent',
Complex: 'Complex',
ComplexUser: 'ComplexUser',
ComplexInvitation: 'ComplexInvitation',
Sport: 'Sport',
Court: 'Court',
CourtMaintenance: 'CourtMaintenance',
CourtAvailability: 'CourtAvailability',
CourtPriceRule: 'CourtPriceRule',
CourtBooking: 'CourtBooking',
CourtBookingLog: 'CourtBookingLog',
RecurringBookingGroup: 'RecurringBookingGroup',
PasswordResetRequest: 'PasswordResetRequest',
Plan: 'Plan'
} as const
@@ -91,6 +95,9 @@ export const UserScalarFieldEnum = {
emailVerified: 'emailVerified',
image: 'image',
phone: 'phone',
banned: 'banned',
bannedAt: 'bannedAt',
banReason: 'banReason',
createdAt: 'createdAt',
updatedAt: 'updatedAt',
role: 'role'
@@ -107,6 +114,11 @@ export const SessionScalarFieldEnum = {
updatedAt: 'updatedAt',
ipAddress: 'ipAddress',
userAgent: 'userAgent',
country: 'country',
city: 'city',
countryCode: 'countryCode',
latitude: 'latitude',
longitude: 'longitude',
userId: 'userId'
} as const
@@ -144,6 +156,42 @@ export const VerificationScalarFieldEnum = {
export type VerificationScalarFieldEnum = (typeof VerificationScalarFieldEnum)[keyof typeof VerificationScalarFieldEnum]
export const ComplexBillingScalarFieldEnum = {
complexId: 'complexId',
status: 'status',
planCode: 'planCode',
currency: 'currency',
provider: 'provider',
providerCustomerId: 'providerCustomerId',
providerSubscriptionId: 'providerSubscriptionId',
providerPreapprovalId: 'providerPreapprovalId',
currentPeriodStart: 'currentPeriodStart',
currentPeriodEnd: 'currentPeriodEnd',
trialEndsAt: 'trialEndsAt',
canceledAt: 'canceledAt',
suspendedAt: 'suspendedAt',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type ComplexBillingScalarFieldEnum = (typeof ComplexBillingScalarFieldEnum)[keyof typeof ComplexBillingScalarFieldEnum]
export const BillingEventScalarFieldEnum = {
id: 'id',
complexId: 'complexId',
eventType: 'eventType',
provider: 'provider',
providerEventId: 'providerEventId',
providerData: 'providerData',
previousStatus: 'previousStatus',
newStatus: 'newStatus',
processedAt: 'processedAt'
} as const
export type BillingEventScalarFieldEnum = (typeof BillingEventScalarFieldEnum)[keyof typeof BillingEventScalarFieldEnum]
export const ComplexScalarFieldEnum = {
id: 'id',
complexName: 'complexName',
@@ -205,6 +253,8 @@ export const CourtScalarFieldEnum = {
name: 'name',
slotDurationMinutes: 'slotDurationMinutes',
basePrice: 'basePrice',
isUnderMaintenance: 'isUnderMaintenance',
maintenanceReason: 'maintenanceReason',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
@@ -212,6 +262,20 @@ export const CourtScalarFieldEnum = {
export type CourtScalarFieldEnum = (typeof CourtScalarFieldEnum)[keyof typeof CourtScalarFieldEnum]
export const CourtMaintenanceScalarFieldEnum = {
id: 'id',
courtId: 'courtId',
startDate: 'startDate',
startTime: 'startTime',
endTime: 'endTime',
reason: 'reason',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type CourtMaintenanceScalarFieldEnum = (typeof CourtMaintenanceScalarFieldEnum)[keyof typeof CourtMaintenanceScalarFieldEnum]
export const CourtAvailabilityScalarFieldEnum = {
id: 'id',
courtId: 'courtId',
@@ -250,6 +314,7 @@ export const CourtBookingScalarFieldEnum = {
customerPhone: 'customerPhone',
customerEmail: 'customerEmail',
status: 'status',
recurringGroupId: 'recurringGroupId',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
@@ -266,6 +331,9 @@ export const CourtBookingLogScalarFieldEnum = {
endTime: 'endTime',
previousStatus: 'previousStatus',
newStatus: 'newStatus',
previousCourtId: 'previousCourtId',
previousStartTime: 'previousStartTime',
previousEndTime: 'previousEndTime',
customerName: 'customerName',
customerPhone: 'customerPhone',
customerEmail: 'customerEmail',
@@ -275,6 +343,26 @@ export const CourtBookingLogScalarFieldEnum = {
export type CourtBookingLogScalarFieldEnum = (typeof CourtBookingLogScalarFieldEnum)[keyof typeof CourtBookingLogScalarFieldEnum]
export const RecurringBookingGroupScalarFieldEnum = {
id: 'id',
complexId: 'complexId',
courtId: 'courtId',
startTime: 'startTime',
endTime: 'endTime',
dayOfWeek: 'dayOfWeek',
startDate: 'startDate',
endDate: 'endDate',
status: 'status',
customerName: 'customerName',
customerPhone: 'customerPhone',
customerEmail: 'customerEmail',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type RecurringBookingGroupScalarFieldEnum = (typeof RecurringBookingGroupScalarFieldEnum)[keyof typeof RecurringBookingGroupScalarFieldEnum]
export const PasswordResetRequestScalarFieldEnum = {
id: 'id',
email: 'email',
@@ -310,6 +398,14 @@ export const SortOrder = {
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
export const NullableJsonNullValueInput = {
DbNull: DbNull,
JsonNull: JsonNull
} as const
export type NullableJsonNullValueInput = (typeof NullableJsonNullValueInput)[keyof typeof NullableJsonNullValueInput]
export const JsonNullValueInput = {
JsonNull: JsonNull
} as const

View File

@@ -12,15 +12,19 @@ export type * from './models/User'
export type * from './models/Session'
export type * from './models/Account'
export type * from './models/Verification'
export type * from './models/ComplexBilling'
export type * from './models/BillingEvent'
export type * from './models/Complex'
export type * from './models/ComplexUser'
export type * from './models/ComplexInvitation'
export type * from './models/Sport'
export type * from './models/Court'
export type * from './models/CourtMaintenance'
export type * from './models/CourtAvailability'
export type * from './models/CourtPriceRule'
export type * from './models/CourtBooking'
export type * from './models/CourtBookingLog'
export type * from './models/RecurringBookingGroup'
export type * from './models/PasswordResetRequest'
export type * from './models/Plan'
export type * from './commonInputTypes'

View File

@@ -234,6 +234,8 @@ export type ComplexWhereInput = {
users?: Prisma.ComplexUserListRelationFilter
invitations?: Prisma.ComplexInvitationListRelationFilter
courts?: Prisma.CourtListRelationFilter
recurringGroups?: Prisma.RecurringBookingGroupListRelationFilter
billing?: Prisma.XOR<Prisma.ComplexBillingNullableScalarRelationFilter, Prisma.ComplexBillingWhereInput> | null
}
export type ComplexOrderByWithRelationInput = {
@@ -252,6 +254,8 @@ export type ComplexOrderByWithRelationInput = {
users?: Prisma.ComplexUserOrderByRelationAggregateInput
invitations?: Prisma.ComplexInvitationOrderByRelationAggregateInput
courts?: Prisma.CourtOrderByRelationAggregateInput
recurringGroups?: Prisma.RecurringBookingGroupOrderByRelationAggregateInput
billing?: Prisma.ComplexBillingOrderByWithRelationInput
}
export type ComplexWhereUniqueInput = Prisma.AtLeast<{
@@ -273,6 +277,8 @@ export type ComplexWhereUniqueInput = Prisma.AtLeast<{
users?: Prisma.ComplexUserListRelationFilter
invitations?: Prisma.ComplexInvitationListRelationFilter
courts?: Prisma.CourtListRelationFilter
recurringGroups?: Prisma.RecurringBookingGroupListRelationFilter
billing?: Prisma.XOR<Prisma.ComplexBillingNullableScalarRelationFilter, Prisma.ComplexBillingWhereInput> | null
}, "id" | "complexSlug">
export type ComplexOrderByWithAggregationInput = {
@@ -324,6 +330,8 @@ export type ComplexCreateInput = {
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutComplexInput
billing?: Prisma.ComplexBillingCreateNestedOneWithoutComplexInput
}
export type ComplexUncheckedCreateInput = {
@@ -341,6 +349,8 @@ export type ComplexUncheckedCreateInput = {
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutComplexInput
billing?: Prisma.ComplexBillingUncheckedCreateNestedOneWithoutComplexInput
}
export type ComplexUpdateInput = {
@@ -358,6 +368,8 @@ export type ComplexUpdateInput = {
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutComplexNestedInput
billing?: Prisma.ComplexBillingUpdateOneWithoutComplexNestedInput
}
export type ComplexUncheckedUpdateInput = {
@@ -375,6 +387,8 @@ export type ComplexUncheckedUpdateInput = {
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutComplexNestedInput
billing?: Prisma.ComplexBillingUncheckedUpdateOneWithoutComplexNestedInput
}
export type ComplexCreateManyInput = {
@@ -418,6 +432,11 @@ export type ComplexUncheckedUpdateManyInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
export type ComplexScalarRelationFilter = {
is?: Prisma.ComplexWhereInput
isNot?: Prisma.ComplexWhereInput
}
export type ComplexCountOrderByAggregateInput = {
id?: Prisma.SortOrder
complexName?: Prisma.SortOrder
@@ -460,11 +479,6 @@ export type ComplexMinOrderByAggregateInput = {
updatedAt?: Prisma.SortOrder
}
export type ComplexScalarRelationFilter = {
is?: Prisma.ComplexWhereInput
isNot?: Prisma.ComplexWhereInput
}
export type ComplexListRelationFilter = {
every?: Prisma.ComplexWhereInput
some?: Prisma.ComplexWhereInput
@@ -475,6 +489,20 @@ export type ComplexOrderByRelationAggregateInput = {
_count?: Prisma.SortOrder
}
export type ComplexCreateNestedOneWithoutBillingInput = {
create?: Prisma.XOR<Prisma.ComplexCreateWithoutBillingInput, Prisma.ComplexUncheckedCreateWithoutBillingInput>
connectOrCreate?: Prisma.ComplexCreateOrConnectWithoutBillingInput
connect?: Prisma.ComplexWhereUniqueInput
}
export type ComplexUpdateOneRequiredWithoutBillingNestedInput = {
create?: Prisma.XOR<Prisma.ComplexCreateWithoutBillingInput, Prisma.ComplexUncheckedCreateWithoutBillingInput>
connectOrCreate?: Prisma.ComplexCreateOrConnectWithoutBillingInput
upsert?: Prisma.ComplexUpsertWithoutBillingInput
connect?: Prisma.ComplexWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.ComplexUpdateToOneWithWhereWithoutBillingInput, Prisma.ComplexUpdateWithoutBillingInput>, Prisma.ComplexUncheckedUpdateWithoutBillingInput>
}
export type ComplexCreateNestedOneWithoutUsersInput = {
create?: Prisma.XOR<Prisma.ComplexCreateWithoutUsersInput, Prisma.ComplexUncheckedCreateWithoutUsersInput>
connectOrCreate?: Prisma.ComplexCreateOrConnectWithoutUsersInput
@@ -517,6 +545,20 @@ export type ComplexUpdateOneRequiredWithoutCourtsNestedInput = {
update?: Prisma.XOR<Prisma.XOR<Prisma.ComplexUpdateToOneWithWhereWithoutCourtsInput, Prisma.ComplexUpdateWithoutCourtsInput>, Prisma.ComplexUncheckedUpdateWithoutCourtsInput>
}
export type ComplexCreateNestedOneWithoutRecurringGroupsInput = {
create?: Prisma.XOR<Prisma.ComplexCreateWithoutRecurringGroupsInput, Prisma.ComplexUncheckedCreateWithoutRecurringGroupsInput>
connectOrCreate?: Prisma.ComplexCreateOrConnectWithoutRecurringGroupsInput
connect?: Prisma.ComplexWhereUniqueInput
}
export type ComplexUpdateOneRequiredWithoutRecurringGroupsNestedInput = {
create?: Prisma.XOR<Prisma.ComplexCreateWithoutRecurringGroupsInput, Prisma.ComplexUncheckedCreateWithoutRecurringGroupsInput>
connectOrCreate?: Prisma.ComplexCreateOrConnectWithoutRecurringGroupsInput
upsert?: Prisma.ComplexUpsertWithoutRecurringGroupsInput
connect?: Prisma.ComplexWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.ComplexUpdateToOneWithWhereWithoutRecurringGroupsInput, Prisma.ComplexUpdateWithoutRecurringGroupsInput>, Prisma.ComplexUncheckedUpdateWithoutRecurringGroupsInput>
}
export type ComplexCreateNestedManyWithoutPlanInput = {
create?: Prisma.XOR<Prisma.ComplexCreateWithoutPlanInput, Prisma.ComplexUncheckedCreateWithoutPlanInput> | Prisma.ComplexCreateWithoutPlanInput[] | Prisma.ComplexUncheckedCreateWithoutPlanInput[]
connectOrCreate?: Prisma.ComplexCreateOrConnectWithoutPlanInput | Prisma.ComplexCreateOrConnectWithoutPlanInput[]
@@ -559,6 +601,94 @@ export type ComplexUncheckedUpdateManyWithoutPlanNestedInput = {
deleteMany?: Prisma.ComplexScalarWhereInput | Prisma.ComplexScalarWhereInput[]
}
export type ComplexCreateWithoutBillingInput = {
id: string
complexName: string
physicalAddress?: string | null
city?: string | null
state?: string | null
country?: string | null
complexSlug: string
adminEmail: string
createdAt?: Date | string
updatedAt?: Date | string
plan?: Prisma.PlanCreateNestedOneWithoutComplexesInput
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutComplexInput
}
export type ComplexUncheckedCreateWithoutBillingInput = {
id: string
complexName: string
physicalAddress?: string | null
city?: string | null
state?: string | null
country?: string | null
complexSlug: string
adminEmail: string
planCode?: string | null
createdAt?: Date | string
updatedAt?: Date | string
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutComplexInput
}
export type ComplexCreateOrConnectWithoutBillingInput = {
where: Prisma.ComplexWhereUniqueInput
create: Prisma.XOR<Prisma.ComplexCreateWithoutBillingInput, Prisma.ComplexUncheckedCreateWithoutBillingInput>
}
export type ComplexUpsertWithoutBillingInput = {
update: Prisma.XOR<Prisma.ComplexUpdateWithoutBillingInput, Prisma.ComplexUncheckedUpdateWithoutBillingInput>
create: Prisma.XOR<Prisma.ComplexCreateWithoutBillingInput, Prisma.ComplexUncheckedCreateWithoutBillingInput>
where?: Prisma.ComplexWhereInput
}
export type ComplexUpdateToOneWithWhereWithoutBillingInput = {
where?: Prisma.ComplexWhereInput
data: Prisma.XOR<Prisma.ComplexUpdateWithoutBillingInput, Prisma.ComplexUncheckedUpdateWithoutBillingInput>
}
export type ComplexUpdateWithoutBillingInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
complexName?: Prisma.StringFieldUpdateOperationsInput | string
physicalAddress?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
city?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
state?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
country?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
complexSlug?: Prisma.StringFieldUpdateOperationsInput | string
adminEmail?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
plan?: Prisma.PlanUpdateOneWithoutComplexesNestedInput
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutComplexNestedInput
}
export type ComplexUncheckedUpdateWithoutBillingInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
complexName?: Prisma.StringFieldUpdateOperationsInput | string
physicalAddress?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
city?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
state?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
country?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
complexSlug?: Prisma.StringFieldUpdateOperationsInput | string
adminEmail?: Prisma.StringFieldUpdateOperationsInput | string
planCode?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutComplexNestedInput
}
export type ComplexCreateWithoutUsersInput = {
id: string
complexName: string
@@ -573,6 +703,8 @@ export type ComplexCreateWithoutUsersInput = {
plan?: Prisma.PlanCreateNestedOneWithoutComplexesInput
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutComplexInput
billing?: Prisma.ComplexBillingCreateNestedOneWithoutComplexInput
}
export type ComplexUncheckedCreateWithoutUsersInput = {
@@ -589,6 +721,8 @@ export type ComplexUncheckedCreateWithoutUsersInput = {
updatedAt?: Date | string
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutComplexInput
billing?: Prisma.ComplexBillingUncheckedCreateNestedOneWithoutComplexInput
}
export type ComplexCreateOrConnectWithoutUsersInput = {
@@ -621,6 +755,8 @@ export type ComplexUpdateWithoutUsersInput = {
plan?: Prisma.PlanUpdateOneWithoutComplexesNestedInput
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutComplexNestedInput
billing?: Prisma.ComplexBillingUpdateOneWithoutComplexNestedInput
}
export type ComplexUncheckedUpdateWithoutUsersInput = {
@@ -637,6 +773,8 @@ export type ComplexUncheckedUpdateWithoutUsersInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutComplexNestedInput
billing?: Prisma.ComplexBillingUncheckedUpdateOneWithoutComplexNestedInput
}
export type ComplexCreateWithoutInvitationsInput = {
@@ -653,6 +791,8 @@ export type ComplexCreateWithoutInvitationsInput = {
plan?: Prisma.PlanCreateNestedOneWithoutComplexesInput
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutComplexInput
billing?: Prisma.ComplexBillingCreateNestedOneWithoutComplexInput
}
export type ComplexUncheckedCreateWithoutInvitationsInput = {
@@ -669,6 +809,8 @@ export type ComplexUncheckedCreateWithoutInvitationsInput = {
updatedAt?: Date | string
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutComplexInput
billing?: Prisma.ComplexBillingUncheckedCreateNestedOneWithoutComplexInput
}
export type ComplexCreateOrConnectWithoutInvitationsInput = {
@@ -701,6 +843,8 @@ export type ComplexUpdateWithoutInvitationsInput = {
plan?: Prisma.PlanUpdateOneWithoutComplexesNestedInput
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutComplexNestedInput
billing?: Prisma.ComplexBillingUpdateOneWithoutComplexNestedInput
}
export type ComplexUncheckedUpdateWithoutInvitationsInput = {
@@ -717,6 +861,8 @@ export type ComplexUncheckedUpdateWithoutInvitationsInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutComplexNestedInput
billing?: Prisma.ComplexBillingUncheckedUpdateOneWithoutComplexNestedInput
}
export type ComplexCreateWithoutCourtsInput = {
@@ -733,6 +879,8 @@ export type ComplexCreateWithoutCourtsInput = {
plan?: Prisma.PlanCreateNestedOneWithoutComplexesInput
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutComplexInput
billing?: Prisma.ComplexBillingCreateNestedOneWithoutComplexInput
}
export type ComplexUncheckedCreateWithoutCourtsInput = {
@@ -749,6 +897,8 @@ export type ComplexUncheckedCreateWithoutCourtsInput = {
updatedAt?: Date | string
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutComplexInput
billing?: Prisma.ComplexBillingUncheckedCreateNestedOneWithoutComplexInput
}
export type ComplexCreateOrConnectWithoutCourtsInput = {
@@ -781,6 +931,8 @@ export type ComplexUpdateWithoutCourtsInput = {
plan?: Prisma.PlanUpdateOneWithoutComplexesNestedInput
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutComplexNestedInput
billing?: Prisma.ComplexBillingUpdateOneWithoutComplexNestedInput
}
export type ComplexUncheckedUpdateWithoutCourtsInput = {
@@ -797,6 +949,96 @@ export type ComplexUncheckedUpdateWithoutCourtsInput = {
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutComplexNestedInput
billing?: Prisma.ComplexBillingUncheckedUpdateOneWithoutComplexNestedInput
}
export type ComplexCreateWithoutRecurringGroupsInput = {
id: string
complexName: string
physicalAddress?: string | null
city?: string | null
state?: string | null
country?: string | null
complexSlug: string
adminEmail: string
createdAt?: Date | string
updatedAt?: Date | string
plan?: Prisma.PlanCreateNestedOneWithoutComplexesInput
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
billing?: Prisma.ComplexBillingCreateNestedOneWithoutComplexInput
}
export type ComplexUncheckedCreateWithoutRecurringGroupsInput = {
id: string
complexName: string
physicalAddress?: string | null
city?: string | null
state?: string | null
country?: string | null
complexSlug: string
adminEmail: string
planCode?: string | null
createdAt?: Date | string
updatedAt?: Date | string
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
billing?: Prisma.ComplexBillingUncheckedCreateNestedOneWithoutComplexInput
}
export type ComplexCreateOrConnectWithoutRecurringGroupsInput = {
where: Prisma.ComplexWhereUniqueInput
create: Prisma.XOR<Prisma.ComplexCreateWithoutRecurringGroupsInput, Prisma.ComplexUncheckedCreateWithoutRecurringGroupsInput>
}
export type ComplexUpsertWithoutRecurringGroupsInput = {
update: Prisma.XOR<Prisma.ComplexUpdateWithoutRecurringGroupsInput, Prisma.ComplexUncheckedUpdateWithoutRecurringGroupsInput>
create: Prisma.XOR<Prisma.ComplexCreateWithoutRecurringGroupsInput, Prisma.ComplexUncheckedCreateWithoutRecurringGroupsInput>
where?: Prisma.ComplexWhereInput
}
export type ComplexUpdateToOneWithWhereWithoutRecurringGroupsInput = {
where?: Prisma.ComplexWhereInput
data: Prisma.XOR<Prisma.ComplexUpdateWithoutRecurringGroupsInput, Prisma.ComplexUncheckedUpdateWithoutRecurringGroupsInput>
}
export type ComplexUpdateWithoutRecurringGroupsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
complexName?: Prisma.StringFieldUpdateOperationsInput | string
physicalAddress?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
city?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
state?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
country?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
complexSlug?: Prisma.StringFieldUpdateOperationsInput | string
adminEmail?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
plan?: Prisma.PlanUpdateOneWithoutComplexesNestedInput
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
billing?: Prisma.ComplexBillingUpdateOneWithoutComplexNestedInput
}
export type ComplexUncheckedUpdateWithoutRecurringGroupsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
complexName?: Prisma.StringFieldUpdateOperationsInput | string
physicalAddress?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
city?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
state?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
country?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
complexSlug?: Prisma.StringFieldUpdateOperationsInput | string
adminEmail?: Prisma.StringFieldUpdateOperationsInput | string
planCode?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
billing?: Prisma.ComplexBillingUncheckedUpdateOneWithoutComplexNestedInput
}
export type ComplexCreateWithoutPlanInput = {
@@ -813,6 +1055,8 @@ export type ComplexCreateWithoutPlanInput = {
users?: Prisma.ComplexUserCreateNestedManyWithoutComplexInput
invitations?: Prisma.ComplexInvitationCreateNestedManyWithoutComplexInput
courts?: Prisma.CourtCreateNestedManyWithoutComplexInput
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutComplexInput
billing?: Prisma.ComplexBillingCreateNestedOneWithoutComplexInput
}
export type ComplexUncheckedCreateWithoutPlanInput = {
@@ -829,6 +1073,8 @@ export type ComplexUncheckedCreateWithoutPlanInput = {
users?: Prisma.ComplexUserUncheckedCreateNestedManyWithoutComplexInput
invitations?: Prisma.ComplexInvitationUncheckedCreateNestedManyWithoutComplexInput
courts?: Prisma.CourtUncheckedCreateNestedManyWithoutComplexInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutComplexInput
billing?: Prisma.ComplexBillingUncheckedCreateNestedOneWithoutComplexInput
}
export type ComplexCreateOrConnectWithoutPlanInput = {
@@ -901,6 +1147,8 @@ export type ComplexUpdateWithoutPlanInput = {
users?: Prisma.ComplexUserUpdateManyWithoutComplexNestedInput
invitations?: Prisma.ComplexInvitationUpdateManyWithoutComplexNestedInput
courts?: Prisma.CourtUpdateManyWithoutComplexNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutComplexNestedInput
billing?: Prisma.ComplexBillingUpdateOneWithoutComplexNestedInput
}
export type ComplexUncheckedUpdateWithoutPlanInput = {
@@ -917,6 +1165,8 @@ export type ComplexUncheckedUpdateWithoutPlanInput = {
users?: Prisma.ComplexUserUncheckedUpdateManyWithoutComplexNestedInput
invitations?: Prisma.ComplexInvitationUncheckedUpdateManyWithoutComplexNestedInput
courts?: Prisma.CourtUncheckedUpdateManyWithoutComplexNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutComplexNestedInput
billing?: Prisma.ComplexBillingUncheckedUpdateOneWithoutComplexNestedInput
}
export type ComplexUncheckedUpdateManyWithoutPlanInput = {
@@ -941,12 +1191,14 @@ export type ComplexCountOutputType = {
users: number
invitations: number
courts: number
recurringGroups: number
}
export type ComplexCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
users?: boolean | ComplexCountOutputTypeCountUsersArgs
invitations?: boolean | ComplexCountOutputTypeCountInvitationsArgs
courts?: boolean | ComplexCountOutputTypeCountCourtsArgs
recurringGroups?: boolean | ComplexCountOutputTypeCountRecurringGroupsArgs
}
/**
@@ -980,6 +1232,13 @@ export type ComplexCountOutputTypeCountCourtsArgs<ExtArgs extends runtime.Types.
where?: Prisma.CourtWhereInput
}
/**
* ComplexCountOutputType without action
*/
export type ComplexCountOutputTypeCountRecurringGroupsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
where?: Prisma.RecurringBookingGroupWhereInput
}
export type ComplexSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
@@ -997,6 +1256,8 @@ export type ComplexSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs
users?: boolean | Prisma.Complex$usersArgs<ExtArgs>
invitations?: boolean | Prisma.Complex$invitationsArgs<ExtArgs>
courts?: boolean | Prisma.Complex$courtsArgs<ExtArgs>
recurringGroups?: boolean | Prisma.Complex$recurringGroupsArgs<ExtArgs>
billing?: boolean | Prisma.Complex$billingArgs<ExtArgs>
_count?: boolean | Prisma.ComplexCountOutputTypeDefaultArgs<ExtArgs>
}, ExtArgs["result"]["complex"]>
@@ -1050,6 +1311,8 @@ export type ComplexInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs
users?: boolean | Prisma.Complex$usersArgs<ExtArgs>
invitations?: boolean | Prisma.Complex$invitationsArgs<ExtArgs>
courts?: boolean | Prisma.Complex$courtsArgs<ExtArgs>
recurringGroups?: boolean | Prisma.Complex$recurringGroupsArgs<ExtArgs>
billing?: boolean | Prisma.Complex$billingArgs<ExtArgs>
_count?: boolean | Prisma.ComplexCountOutputTypeDefaultArgs<ExtArgs>
}
export type ComplexIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
@@ -1066,6 +1329,8 @@ export type $ComplexPayload<ExtArgs extends runtime.Types.Extensions.InternalArg
users: Prisma.$ComplexUserPayload<ExtArgs>[]
invitations: Prisma.$ComplexInvitationPayload<ExtArgs>[]
courts: Prisma.$CourtPayload<ExtArgs>[]
recurringGroups: Prisma.$RecurringBookingGroupPayload<ExtArgs>[]
billing: Prisma.$ComplexBillingPayload<ExtArgs> | null
}
scalars: runtime.Types.Extensions.GetPayloadResult<{
id: string
@@ -1477,6 +1742,8 @@ export interface Prisma__ComplexClient<T, Null = never, ExtArgs extends runtime.
users<T extends Prisma.Complex$usersArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Complex$usersArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ComplexUserPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
invitations<T extends Prisma.Complex$invitationsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Complex$invitationsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$ComplexInvitationPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
courts<T extends Prisma.Complex$courtsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Complex$courtsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CourtPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
recurringGroups<T extends Prisma.Complex$recurringGroupsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Complex$recurringGroupsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$RecurringBookingGroupPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
billing<T extends Prisma.Complex$billingArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Complex$billingArgs<ExtArgs>>): Prisma.Prisma__ComplexBillingClient<runtime.Types.Result.GetResult<Prisma.$ComplexBillingPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
@@ -2008,6 +2275,49 @@ export type Complex$courtsArgs<ExtArgs extends runtime.Types.Extensions.Internal
distinct?: Prisma.CourtScalarFieldEnum | Prisma.CourtScalarFieldEnum[]
}
/**
* Complex.recurringGroups
*/
export type Complex$recurringGroupsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the RecurringBookingGroup
*/
select?: Prisma.RecurringBookingGroupSelect<ExtArgs> | null
/**
* Omit specific fields from the RecurringBookingGroup
*/
omit?: Prisma.RecurringBookingGroupOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.RecurringBookingGroupInclude<ExtArgs> | null
where?: Prisma.RecurringBookingGroupWhereInput
orderBy?: Prisma.RecurringBookingGroupOrderByWithRelationInput | Prisma.RecurringBookingGroupOrderByWithRelationInput[]
cursor?: Prisma.RecurringBookingGroupWhereUniqueInput
take?: number
skip?: number
distinct?: Prisma.RecurringBookingGroupScalarFieldEnum | Prisma.RecurringBookingGroupScalarFieldEnum[]
}
/**
* Complex.billing
*/
export type Complex$billingArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the ComplexBilling
*/
select?: Prisma.ComplexBillingSelect<ExtArgs> | null
/**
* Omit specific fields from the ComplexBilling
*/
omit?: Prisma.ComplexBillingOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.ComplexBillingInclude<ExtArgs> | null
where?: Prisma.ComplexBillingWhereInput
}
/**
* Complex without action
*/

View File

@@ -43,6 +43,8 @@ export type CourtMinAggregateOutputType = {
name: string | null
slotDurationMinutes: number | null
basePrice: runtime.Decimal | null
isUnderMaintenance: boolean | null
maintenanceReason: string | null
createdAt: Date | null
updatedAt: Date | null
}
@@ -54,6 +56,8 @@ export type CourtMaxAggregateOutputType = {
name: string | null
slotDurationMinutes: number | null
basePrice: runtime.Decimal | null
isUnderMaintenance: boolean | null
maintenanceReason: string | null
createdAt: Date | null
updatedAt: Date | null
}
@@ -65,6 +69,8 @@ export type CourtCountAggregateOutputType = {
name: number
slotDurationMinutes: number
basePrice: number
isUnderMaintenance: number
maintenanceReason: number
createdAt: number
updatedAt: number
_all: number
@@ -88,6 +94,8 @@ export type CourtMinAggregateInputType = {
name?: true
slotDurationMinutes?: true
basePrice?: true
isUnderMaintenance?: true
maintenanceReason?: true
createdAt?: true
updatedAt?: true
}
@@ -99,6 +107,8 @@ export type CourtMaxAggregateInputType = {
name?: true
slotDurationMinutes?: true
basePrice?: true
isUnderMaintenance?: true
maintenanceReason?: true
createdAt?: true
updatedAt?: true
}
@@ -110,6 +120,8 @@ export type CourtCountAggregateInputType = {
name?: true
slotDurationMinutes?: true
basePrice?: true
isUnderMaintenance?: true
maintenanceReason?: true
createdAt?: true
updatedAt?: true
_all?: true
@@ -208,6 +220,8 @@ export type CourtGroupByOutputType = {
name: string
slotDurationMinutes: number
basePrice: runtime.Decimal
isUnderMaintenance: boolean
maintenanceReason: string | null
createdAt: Date
updatedAt: Date
_count: CourtCountAggregateOutputType | null
@@ -242,6 +256,8 @@ export type CourtWhereInput = {
name?: Prisma.StringFilter<"Court"> | string
slotDurationMinutes?: Prisma.IntFilter<"Court"> | number
basePrice?: Prisma.DecimalFilter<"Court"> | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFilter<"Court"> | boolean
maintenanceReason?: Prisma.StringNullableFilter<"Court"> | string | null
createdAt?: Prisma.DateTimeFilter<"Court"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"Court"> | Date | string
complex?: Prisma.XOR<Prisma.ComplexScalarRelationFilter, Prisma.ComplexWhereInput>
@@ -249,6 +265,8 @@ export type CourtWhereInput = {
availabilities?: Prisma.CourtAvailabilityListRelationFilter
priceRules?: Prisma.CourtPriceRuleListRelationFilter
bookings?: Prisma.CourtBookingListRelationFilter
maintenances?: Prisma.CourtMaintenanceListRelationFilter
recurringGroups?: Prisma.RecurringBookingGroupListRelationFilter
}
export type CourtOrderByWithRelationInput = {
@@ -258,6 +276,8 @@ export type CourtOrderByWithRelationInput = {
name?: Prisma.SortOrder
slotDurationMinutes?: Prisma.SortOrder
basePrice?: Prisma.SortOrder
isUnderMaintenance?: Prisma.SortOrder
maintenanceReason?: Prisma.SortOrderInput | Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
complex?: Prisma.ComplexOrderByWithRelationInput
@@ -265,6 +285,8 @@ export type CourtOrderByWithRelationInput = {
availabilities?: Prisma.CourtAvailabilityOrderByRelationAggregateInput
priceRules?: Prisma.CourtPriceRuleOrderByRelationAggregateInput
bookings?: Prisma.CourtBookingOrderByRelationAggregateInput
maintenances?: Prisma.CourtMaintenanceOrderByRelationAggregateInput
recurringGroups?: Prisma.RecurringBookingGroupOrderByRelationAggregateInput
}
export type CourtWhereUniqueInput = Prisma.AtLeast<{
@@ -277,6 +299,8 @@ export type CourtWhereUniqueInput = Prisma.AtLeast<{
name?: Prisma.StringFilter<"Court"> | string
slotDurationMinutes?: Prisma.IntFilter<"Court"> | number
basePrice?: Prisma.DecimalFilter<"Court"> | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFilter<"Court"> | boolean
maintenanceReason?: Prisma.StringNullableFilter<"Court"> | string | null
createdAt?: Prisma.DateTimeFilter<"Court"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"Court"> | Date | string
complex?: Prisma.XOR<Prisma.ComplexScalarRelationFilter, Prisma.ComplexWhereInput>
@@ -284,6 +308,8 @@ export type CourtWhereUniqueInput = Prisma.AtLeast<{
availabilities?: Prisma.CourtAvailabilityListRelationFilter
priceRules?: Prisma.CourtPriceRuleListRelationFilter
bookings?: Prisma.CourtBookingListRelationFilter
maintenances?: Prisma.CourtMaintenanceListRelationFilter
recurringGroups?: Prisma.RecurringBookingGroupListRelationFilter
}, "id">
export type CourtOrderByWithAggregationInput = {
@@ -293,6 +319,8 @@ export type CourtOrderByWithAggregationInput = {
name?: Prisma.SortOrder
slotDurationMinutes?: Prisma.SortOrder
basePrice?: Prisma.SortOrder
isUnderMaintenance?: Prisma.SortOrder
maintenanceReason?: Prisma.SortOrderInput | Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
_count?: Prisma.CourtCountOrderByAggregateInput
@@ -312,6 +340,8 @@ export type CourtScalarWhereWithAggregatesInput = {
name?: Prisma.StringWithAggregatesFilter<"Court"> | string
slotDurationMinutes?: Prisma.IntWithAggregatesFilter<"Court"> | number
basePrice?: Prisma.DecimalWithAggregatesFilter<"Court"> | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolWithAggregatesFilter<"Court"> | boolean
maintenanceReason?: Prisma.StringNullableWithAggregatesFilter<"Court"> | string | null
createdAt?: Prisma.DateTimeWithAggregatesFilter<"Court"> | Date | string
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"Court"> | Date | string
}
@@ -321,6 +351,8 @@ export type CourtCreateInput = {
name: string
slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
complex: Prisma.ComplexCreateNestedOneWithoutCourtsInput
@@ -328,6 +360,8 @@ export type CourtCreateInput = {
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
}
export type CourtUncheckedCreateInput = {
@@ -337,11 +371,15 @@ export type CourtUncheckedCreateInput = {
name: string
slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
}
export type CourtUpdateInput = {
@@ -349,6 +387,8 @@ export type CourtUpdateInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
complex?: Prisma.ComplexUpdateOneRequiredWithoutCourtsNestedInput
@@ -356,6 +396,8 @@ export type CourtUpdateInput = {
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
}
export type CourtUncheckedUpdateInput = {
@@ -365,11 +407,15 @@ export type CourtUncheckedUpdateInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
}
export type CourtCreateManyInput = {
@@ -379,6 +425,8 @@ export type CourtCreateManyInput = {
name: string
slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
}
@@ -388,6 +436,8 @@ export type CourtUpdateManyMutationInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
@@ -399,6 +449,8 @@ export type CourtUncheckedUpdateManyInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
@@ -420,6 +472,8 @@ export type CourtCountOrderByAggregateInput = {
name?: Prisma.SortOrder
slotDurationMinutes?: Prisma.SortOrder
basePrice?: Prisma.SortOrder
isUnderMaintenance?: Prisma.SortOrder
maintenanceReason?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
}
@@ -436,6 +490,8 @@ export type CourtMaxOrderByAggregateInput = {
name?: Prisma.SortOrder
slotDurationMinutes?: Prisma.SortOrder
basePrice?: Prisma.SortOrder
isUnderMaintenance?: Prisma.SortOrder
maintenanceReason?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
}
@@ -447,6 +503,8 @@ export type CourtMinOrderByAggregateInput = {
name?: Prisma.SortOrder
slotDurationMinutes?: Prisma.SortOrder
basePrice?: Prisma.SortOrder
isUnderMaintenance?: Prisma.SortOrder
maintenanceReason?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
}
@@ -561,6 +619,20 @@ export type DecimalFieldUpdateOperationsInput = {
divide?: runtime.Decimal | runtime.DecimalJsLike | number | string
}
export type CourtCreateNestedOneWithoutMaintenancesInput = {
create?: Prisma.XOR<Prisma.CourtCreateWithoutMaintenancesInput, Prisma.CourtUncheckedCreateWithoutMaintenancesInput>
connectOrCreate?: Prisma.CourtCreateOrConnectWithoutMaintenancesInput
connect?: Prisma.CourtWhereUniqueInput
}
export type CourtUpdateOneRequiredWithoutMaintenancesNestedInput = {
create?: Prisma.XOR<Prisma.CourtCreateWithoutMaintenancesInput, Prisma.CourtUncheckedCreateWithoutMaintenancesInput>
connectOrCreate?: Prisma.CourtCreateOrConnectWithoutMaintenancesInput
upsert?: Prisma.CourtUpsertWithoutMaintenancesInput
connect?: Prisma.CourtWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.CourtUpdateToOneWithWhereWithoutMaintenancesInput, Prisma.CourtUpdateWithoutMaintenancesInput>, Prisma.CourtUncheckedUpdateWithoutMaintenancesInput>
}
export type CourtCreateNestedOneWithoutAvailabilitiesInput = {
create?: Prisma.XOR<Prisma.CourtCreateWithoutAvailabilitiesInput, Prisma.CourtUncheckedCreateWithoutAvailabilitiesInput>
connectOrCreate?: Prisma.CourtCreateOrConnectWithoutAvailabilitiesInput
@@ -603,17 +675,35 @@ export type CourtUpdateOneRequiredWithoutBookingsNestedInput = {
update?: Prisma.XOR<Prisma.XOR<Prisma.CourtUpdateToOneWithWhereWithoutBookingsInput, Prisma.CourtUpdateWithoutBookingsInput>, Prisma.CourtUncheckedUpdateWithoutBookingsInput>
}
export type CourtCreateNestedOneWithoutRecurringGroupsInput = {
create?: Prisma.XOR<Prisma.CourtCreateWithoutRecurringGroupsInput, Prisma.CourtUncheckedCreateWithoutRecurringGroupsInput>
connectOrCreate?: Prisma.CourtCreateOrConnectWithoutRecurringGroupsInput
connect?: Prisma.CourtWhereUniqueInput
}
export type CourtUpdateOneRequiredWithoutRecurringGroupsNestedInput = {
create?: Prisma.XOR<Prisma.CourtCreateWithoutRecurringGroupsInput, Prisma.CourtUncheckedCreateWithoutRecurringGroupsInput>
connectOrCreate?: Prisma.CourtCreateOrConnectWithoutRecurringGroupsInput
upsert?: Prisma.CourtUpsertWithoutRecurringGroupsInput
connect?: Prisma.CourtWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.CourtUpdateToOneWithWhereWithoutRecurringGroupsInput, Prisma.CourtUpdateWithoutRecurringGroupsInput>, Prisma.CourtUncheckedUpdateWithoutRecurringGroupsInput>
}
export type CourtCreateWithoutComplexInput = {
id: string
name: string
slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
sport: Prisma.SportCreateNestedOneWithoutCourtsInput
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
}
export type CourtUncheckedCreateWithoutComplexInput = {
@@ -622,11 +712,15 @@ export type CourtUncheckedCreateWithoutComplexInput = {
name: string
slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
}
export type CourtCreateOrConnectWithoutComplexInput = {
@@ -665,6 +759,8 @@ export type CourtScalarWhereInput = {
name?: Prisma.StringFilter<"Court"> | string
slotDurationMinutes?: Prisma.IntFilter<"Court"> | number
basePrice?: Prisma.DecimalFilter<"Court"> | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFilter<"Court"> | boolean
maintenanceReason?: Prisma.StringNullableFilter<"Court"> | string | null
createdAt?: Prisma.DateTimeFilter<"Court"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"Court"> | Date | string
}
@@ -674,12 +770,16 @@ export type CourtCreateWithoutSportInput = {
name: string
slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
complex: Prisma.ComplexCreateNestedOneWithoutCourtsInput
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
}
export type CourtUncheckedCreateWithoutSportInput = {
@@ -688,11 +788,15 @@ export type CourtUncheckedCreateWithoutSportInput = {
name: string
slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
}
export type CourtCreateOrConnectWithoutSportInput = {
@@ -721,17 +825,105 @@ export type CourtUpdateManyWithWhereWithoutSportInput = {
data: Prisma.XOR<Prisma.CourtUpdateManyMutationInput, Prisma.CourtUncheckedUpdateManyWithoutSportInput>
}
export type CourtCreateWithoutMaintenancesInput = {
id: string
name: string
slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
complex: Prisma.ComplexCreateNestedOneWithoutCourtsInput
sport: Prisma.SportCreateNestedOneWithoutCourtsInput
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
}
export type CourtUncheckedCreateWithoutMaintenancesInput = {
id: string
complexId: string
sportId: string
name: string
slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
}
export type CourtCreateOrConnectWithoutMaintenancesInput = {
where: Prisma.CourtWhereUniqueInput
create: Prisma.XOR<Prisma.CourtCreateWithoutMaintenancesInput, Prisma.CourtUncheckedCreateWithoutMaintenancesInput>
}
export type CourtUpsertWithoutMaintenancesInput = {
update: Prisma.XOR<Prisma.CourtUpdateWithoutMaintenancesInput, Prisma.CourtUncheckedUpdateWithoutMaintenancesInput>
create: Prisma.XOR<Prisma.CourtCreateWithoutMaintenancesInput, Prisma.CourtUncheckedCreateWithoutMaintenancesInput>
where?: Prisma.CourtWhereInput
}
export type CourtUpdateToOneWithWhereWithoutMaintenancesInput = {
where?: Prisma.CourtWhereInput
data: Prisma.XOR<Prisma.CourtUpdateWithoutMaintenancesInput, Prisma.CourtUncheckedUpdateWithoutMaintenancesInput>
}
export type CourtUpdateWithoutMaintenancesInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
complex?: Prisma.ComplexUpdateOneRequiredWithoutCourtsNestedInput
sport?: Prisma.SportUpdateOneRequiredWithoutCourtsNestedInput
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
}
export type CourtUncheckedUpdateWithoutMaintenancesInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
complexId?: Prisma.StringFieldUpdateOperationsInput | string
sportId?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
}
export type CourtCreateWithoutAvailabilitiesInput = {
id: string
name: string
slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
complex: Prisma.ComplexCreateNestedOneWithoutCourtsInput
sport: Prisma.SportCreateNestedOneWithoutCourtsInput
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
}
export type CourtUncheckedCreateWithoutAvailabilitiesInput = {
@@ -741,10 +933,14 @@ export type CourtUncheckedCreateWithoutAvailabilitiesInput = {
name: string
slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
}
export type CourtCreateOrConnectWithoutAvailabilitiesInput = {
@@ -768,12 +964,16 @@ export type CourtUpdateWithoutAvailabilitiesInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
complex?: Prisma.ComplexUpdateOneRequiredWithoutCourtsNestedInput
sport?: Prisma.SportUpdateOneRequiredWithoutCourtsNestedInput
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
}
export type CourtUncheckedUpdateWithoutAvailabilitiesInput = {
@@ -783,10 +983,14 @@ export type CourtUncheckedUpdateWithoutAvailabilitiesInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
}
export type CourtCreateWithoutPriceRulesInput = {
@@ -794,12 +998,16 @@ export type CourtCreateWithoutPriceRulesInput = {
name: string
slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
complex: Prisma.ComplexCreateNestedOneWithoutCourtsInput
sport: Prisma.SportCreateNestedOneWithoutCourtsInput
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
}
export type CourtUncheckedCreateWithoutPriceRulesInput = {
@@ -809,10 +1017,14 @@ export type CourtUncheckedCreateWithoutPriceRulesInput = {
name: string
slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
}
export type CourtCreateOrConnectWithoutPriceRulesInput = {
@@ -836,12 +1048,16 @@ export type CourtUpdateWithoutPriceRulesInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
complex?: Prisma.ComplexUpdateOneRequiredWithoutCourtsNestedInput
sport?: Prisma.SportUpdateOneRequiredWithoutCourtsNestedInput
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
}
export type CourtUncheckedUpdateWithoutPriceRulesInput = {
@@ -851,10 +1067,14 @@ export type CourtUncheckedUpdateWithoutPriceRulesInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
}
export type CourtCreateWithoutBookingsInput = {
@@ -862,12 +1082,16 @@ export type CourtCreateWithoutBookingsInput = {
name: string
slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
complex: Prisma.ComplexCreateNestedOneWithoutCourtsInput
sport: Prisma.SportCreateNestedOneWithoutCourtsInput
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
recurringGroups?: Prisma.RecurringBookingGroupCreateNestedManyWithoutCourtInput
}
export type CourtUncheckedCreateWithoutBookingsInput = {
@@ -877,10 +1101,14 @@ export type CourtUncheckedCreateWithoutBookingsInput = {
name: string
slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedCreateNestedManyWithoutCourtInput
}
export type CourtCreateOrConnectWithoutBookingsInput = {
@@ -904,12 +1132,16 @@ export type CourtUpdateWithoutBookingsInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
complex?: Prisma.ComplexUpdateOneRequiredWithoutCourtsNestedInput
sport?: Prisma.SportUpdateOneRequiredWithoutCourtsNestedInput
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
}
export type CourtUncheckedUpdateWithoutBookingsInput = {
@@ -919,10 +1151,98 @@ export type CourtUncheckedUpdateWithoutBookingsInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
}
export type CourtCreateWithoutRecurringGroupsInput = {
id: string
name: string
slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
complex: Prisma.ComplexCreateNestedOneWithoutCourtsInput
sport: Prisma.SportCreateNestedOneWithoutCourtsInput
availabilities?: Prisma.CourtAvailabilityCreateNestedManyWithoutCourtInput
priceRules?: Prisma.CourtPriceRuleCreateNestedManyWithoutCourtInput
bookings?: Prisma.CourtBookingCreateNestedManyWithoutCourtInput
maintenances?: Prisma.CourtMaintenanceCreateNestedManyWithoutCourtInput
}
export type CourtUncheckedCreateWithoutRecurringGroupsInput = {
id: string
complexId: string
sportId: string
name: string
slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
availabilities?: Prisma.CourtAvailabilityUncheckedCreateNestedManyWithoutCourtInput
priceRules?: Prisma.CourtPriceRuleUncheckedCreateNestedManyWithoutCourtInput
bookings?: Prisma.CourtBookingUncheckedCreateNestedManyWithoutCourtInput
maintenances?: Prisma.CourtMaintenanceUncheckedCreateNestedManyWithoutCourtInput
}
export type CourtCreateOrConnectWithoutRecurringGroupsInput = {
where: Prisma.CourtWhereUniqueInput
create: Prisma.XOR<Prisma.CourtCreateWithoutRecurringGroupsInput, Prisma.CourtUncheckedCreateWithoutRecurringGroupsInput>
}
export type CourtUpsertWithoutRecurringGroupsInput = {
update: Prisma.XOR<Prisma.CourtUpdateWithoutRecurringGroupsInput, Prisma.CourtUncheckedUpdateWithoutRecurringGroupsInput>
create: Prisma.XOR<Prisma.CourtCreateWithoutRecurringGroupsInput, Prisma.CourtUncheckedCreateWithoutRecurringGroupsInput>
where?: Prisma.CourtWhereInput
}
export type CourtUpdateToOneWithWhereWithoutRecurringGroupsInput = {
where?: Prisma.CourtWhereInput
data: Prisma.XOR<Prisma.CourtUpdateWithoutRecurringGroupsInput, Prisma.CourtUncheckedUpdateWithoutRecurringGroupsInput>
}
export type CourtUpdateWithoutRecurringGroupsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
complex?: Prisma.ComplexUpdateOneRequiredWithoutCourtsNestedInput
sport?: Prisma.SportUpdateOneRequiredWithoutCourtsNestedInput
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
}
export type CourtUncheckedUpdateWithoutRecurringGroupsInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
complexId?: Prisma.StringFieldUpdateOperationsInput | string
sportId?: Prisma.StringFieldUpdateOperationsInput | string
name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
}
export type CourtCreateManyComplexInput = {
@@ -931,6 +1251,8 @@ export type CourtCreateManyComplexInput = {
name: string
slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
}
@@ -940,12 +1262,16 @@ export type CourtUpdateWithoutComplexInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
sport?: Prisma.SportUpdateOneRequiredWithoutCourtsNestedInput
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
}
export type CourtUncheckedUpdateWithoutComplexInput = {
@@ -954,11 +1280,15 @@ export type CourtUncheckedUpdateWithoutComplexInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
}
export type CourtUncheckedUpdateManyWithoutComplexInput = {
@@ -967,6 +1297,8 @@ export type CourtUncheckedUpdateManyWithoutComplexInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
@@ -977,6 +1309,8 @@ export type CourtCreateManySportInput = {
name: string
slotDurationMinutes: number
basePrice: runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: boolean
maintenanceReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
}
@@ -986,12 +1320,16 @@ export type CourtUpdateWithoutSportInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
complex?: Prisma.ComplexUpdateOneRequiredWithoutCourtsNestedInput
availabilities?: Prisma.CourtAvailabilityUpdateManyWithoutCourtNestedInput
priceRules?: Prisma.CourtPriceRuleUpdateManyWithoutCourtNestedInput
bookings?: Prisma.CourtBookingUpdateManyWithoutCourtNestedInput
maintenances?: Prisma.CourtMaintenanceUpdateManyWithoutCourtNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUpdateManyWithoutCourtNestedInput
}
export type CourtUncheckedUpdateWithoutSportInput = {
@@ -1000,11 +1338,15 @@ export type CourtUncheckedUpdateWithoutSportInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
availabilities?: Prisma.CourtAvailabilityUncheckedUpdateManyWithoutCourtNestedInput
priceRules?: Prisma.CourtPriceRuleUncheckedUpdateManyWithoutCourtNestedInput
bookings?: Prisma.CourtBookingUncheckedUpdateManyWithoutCourtNestedInput
maintenances?: Prisma.CourtMaintenanceUncheckedUpdateManyWithoutCourtNestedInput
recurringGroups?: Prisma.RecurringBookingGroupUncheckedUpdateManyWithoutCourtNestedInput
}
export type CourtUncheckedUpdateManyWithoutSportInput = {
@@ -1013,6 +1355,8 @@ export type CourtUncheckedUpdateManyWithoutSportInput = {
name?: Prisma.StringFieldUpdateOperationsInput | string
slotDurationMinutes?: Prisma.IntFieldUpdateOperationsInput | number
basePrice?: Prisma.DecimalFieldUpdateOperationsInput | runtime.Decimal | runtime.DecimalJsLike | number | string
isUnderMaintenance?: Prisma.BoolFieldUpdateOperationsInput | boolean
maintenanceReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
@@ -1026,12 +1370,16 @@ export type CourtCountOutputType = {
availabilities: number
priceRules: number
bookings: number
maintenances: number
recurringGroups: number
}
export type CourtCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
availabilities?: boolean | CourtCountOutputTypeCountAvailabilitiesArgs
priceRules?: boolean | CourtCountOutputTypeCountPriceRulesArgs
bookings?: boolean | CourtCountOutputTypeCountBookingsArgs
maintenances?: boolean | CourtCountOutputTypeCountMaintenancesArgs
recurringGroups?: boolean | CourtCountOutputTypeCountRecurringGroupsArgs
}
/**
@@ -1065,6 +1413,20 @@ export type CourtCountOutputTypeCountBookingsArgs<ExtArgs extends runtime.Types.
where?: Prisma.CourtBookingWhereInput
}
/**
* CourtCountOutputType without action
*/
export type CourtCountOutputTypeCountMaintenancesArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
where?: Prisma.CourtMaintenanceWhereInput
}
/**
* CourtCountOutputType without action
*/
export type CourtCountOutputTypeCountRecurringGroupsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
where?: Prisma.RecurringBookingGroupWhereInput
}
export type CourtSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
@@ -1073,6 +1435,8 @@ export type CourtSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs =
name?: boolean
slotDurationMinutes?: boolean
basePrice?: boolean
isUnderMaintenance?: boolean
maintenanceReason?: boolean
createdAt?: boolean
updatedAt?: boolean
complex?: boolean | Prisma.ComplexDefaultArgs<ExtArgs>
@@ -1080,6 +1444,8 @@ export type CourtSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs =
availabilities?: boolean | Prisma.Court$availabilitiesArgs<ExtArgs>
priceRules?: boolean | Prisma.Court$priceRulesArgs<ExtArgs>
bookings?: boolean | Prisma.Court$bookingsArgs<ExtArgs>
maintenances?: boolean | Prisma.Court$maintenancesArgs<ExtArgs>
recurringGroups?: boolean | Prisma.Court$recurringGroupsArgs<ExtArgs>
_count?: boolean | Prisma.CourtCountOutputTypeDefaultArgs<ExtArgs>
}, ExtArgs["result"]["court"]>
@@ -1090,6 +1456,8 @@ export type CourtSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensi
name?: boolean
slotDurationMinutes?: boolean
basePrice?: boolean
isUnderMaintenance?: boolean
maintenanceReason?: boolean
createdAt?: boolean
updatedAt?: boolean
complex?: boolean | Prisma.ComplexDefaultArgs<ExtArgs>
@@ -1103,6 +1471,8 @@ export type CourtSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensi
name?: boolean
slotDurationMinutes?: boolean
basePrice?: boolean
isUnderMaintenance?: boolean
maintenanceReason?: boolean
createdAt?: boolean
updatedAt?: boolean
complex?: boolean | Prisma.ComplexDefaultArgs<ExtArgs>
@@ -1116,17 +1486,21 @@ export type CourtSelectScalar = {
name?: boolean
slotDurationMinutes?: boolean
basePrice?: boolean
isUnderMaintenance?: boolean
maintenanceReason?: boolean
createdAt?: boolean
updatedAt?: boolean
}
export type CourtOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "complexId" | "sportId" | "name" | "slotDurationMinutes" | "basePrice" | "createdAt" | "updatedAt", ExtArgs["result"]["court"]>
export type CourtOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "complexId" | "sportId" | "name" | "slotDurationMinutes" | "basePrice" | "isUnderMaintenance" | "maintenanceReason" | "createdAt" | "updatedAt", ExtArgs["result"]["court"]>
export type CourtInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
complex?: boolean | Prisma.ComplexDefaultArgs<ExtArgs>
sport?: boolean | Prisma.SportDefaultArgs<ExtArgs>
availabilities?: boolean | Prisma.Court$availabilitiesArgs<ExtArgs>
priceRules?: boolean | Prisma.Court$priceRulesArgs<ExtArgs>
bookings?: boolean | Prisma.Court$bookingsArgs<ExtArgs>
maintenances?: boolean | Prisma.Court$maintenancesArgs<ExtArgs>
recurringGroups?: boolean | Prisma.Court$recurringGroupsArgs<ExtArgs>
_count?: boolean | Prisma.CourtCountOutputTypeDefaultArgs<ExtArgs>
}
export type CourtIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
@@ -1146,6 +1520,8 @@ export type $CourtPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs
availabilities: Prisma.$CourtAvailabilityPayload<ExtArgs>[]
priceRules: Prisma.$CourtPriceRulePayload<ExtArgs>[]
bookings: Prisma.$CourtBookingPayload<ExtArgs>[]
maintenances: Prisma.$CourtMaintenancePayload<ExtArgs>[]
recurringGroups: Prisma.$RecurringBookingGroupPayload<ExtArgs>[]
}
scalars: runtime.Types.Extensions.GetPayloadResult<{
id: string
@@ -1154,6 +1530,8 @@ export type $CourtPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs
name: string
slotDurationMinutes: number
basePrice: runtime.Decimal
isUnderMaintenance: boolean
maintenanceReason: string | null
createdAt: Date
updatedAt: Date
}, ExtArgs["result"]["court"]>
@@ -1555,6 +1933,8 @@ export interface Prisma__CourtClient<T, Null = never, ExtArgs extends runtime.Ty
availabilities<T extends Prisma.Court$availabilitiesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Court$availabilitiesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CourtAvailabilityPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
priceRules<T extends Prisma.Court$priceRulesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Court$priceRulesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CourtPriceRulePayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
bookings<T extends Prisma.Court$bookingsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Court$bookingsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CourtBookingPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
maintenances<T extends Prisma.Court$maintenancesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Court$maintenancesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$CourtMaintenancePayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
recurringGroups<T extends Prisma.Court$recurringGroupsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Court$recurringGroupsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$RecurringBookingGroupPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
@@ -1590,6 +1970,8 @@ export interface CourtFieldRefs {
readonly name: Prisma.FieldRef<"Court", 'String'>
readonly slotDurationMinutes: Prisma.FieldRef<"Court", 'Int'>
readonly basePrice: Prisma.FieldRef<"Court", 'Decimal'>
readonly isUnderMaintenance: Prisma.FieldRef<"Court", 'Boolean'>
readonly maintenanceReason: Prisma.FieldRef<"Court", 'String'>
readonly createdAt: Prisma.FieldRef<"Court", 'DateTime'>
readonly updatedAt: Prisma.FieldRef<"Court", 'DateTime'>
}
@@ -2064,6 +2446,54 @@ export type Court$bookingsArgs<ExtArgs extends runtime.Types.Extensions.Internal
distinct?: Prisma.CourtBookingScalarFieldEnum | Prisma.CourtBookingScalarFieldEnum[]
}
/**
* Court.maintenances
*/
export type Court$maintenancesArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the CourtMaintenance
*/
select?: Prisma.CourtMaintenanceSelect<ExtArgs> | null
/**
* Omit specific fields from the CourtMaintenance
*/
omit?: Prisma.CourtMaintenanceOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.CourtMaintenanceInclude<ExtArgs> | null
where?: Prisma.CourtMaintenanceWhereInput
orderBy?: Prisma.CourtMaintenanceOrderByWithRelationInput | Prisma.CourtMaintenanceOrderByWithRelationInput[]
cursor?: Prisma.CourtMaintenanceWhereUniqueInput
take?: number
skip?: number
distinct?: Prisma.CourtMaintenanceScalarFieldEnum | Prisma.CourtMaintenanceScalarFieldEnum[]
}
/**
* Court.recurringGroups
*/
export type Court$recurringGroupsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the RecurringBookingGroup
*/
select?: Prisma.RecurringBookingGroupSelect<ExtArgs> | null
/**
* Omit specific fields from the RecurringBookingGroup
*/
omit?: Prisma.RecurringBookingGroupOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.RecurringBookingGroupInclude<ExtArgs> | null
where?: Prisma.RecurringBookingGroupWhereInput
orderBy?: Prisma.RecurringBookingGroupOrderByWithRelationInput | Prisma.RecurringBookingGroupOrderByWithRelationInput[]
cursor?: Prisma.RecurringBookingGroupWhereUniqueInput
take?: number
skip?: number
distinct?: Prisma.RecurringBookingGroupScalarFieldEnum | Prisma.RecurringBookingGroupScalarFieldEnum[]
}
/**
* Court without action
*/

View File

@@ -35,6 +35,7 @@ export type CourtBookingMinAggregateOutputType = {
customerPhone: string | null
customerEmail: string | null
status: $Enums.CourtBookingStatus | null
recurringGroupId: string | null
createdAt: Date | null
updatedAt: Date | null
}
@@ -50,6 +51,7 @@ export type CourtBookingMaxAggregateOutputType = {
customerPhone: string | null
customerEmail: string | null
status: $Enums.CourtBookingStatus | null
recurringGroupId: string | null
createdAt: Date | null
updatedAt: Date | null
}
@@ -65,6 +67,7 @@ export type CourtBookingCountAggregateOutputType = {
customerPhone: number
customerEmail: number
status: number
recurringGroupId: number
createdAt: number
updatedAt: number
_all: number
@@ -82,6 +85,7 @@ export type CourtBookingMinAggregateInputType = {
customerPhone?: true
customerEmail?: true
status?: true
recurringGroupId?: true
createdAt?: true
updatedAt?: true
}
@@ -97,6 +101,7 @@ export type CourtBookingMaxAggregateInputType = {
customerPhone?: true
customerEmail?: true
status?: true
recurringGroupId?: true
createdAt?: true
updatedAt?: true
}
@@ -112,6 +117,7 @@ export type CourtBookingCountAggregateInputType = {
customerPhone?: true
customerEmail?: true
status?: true
recurringGroupId?: true
createdAt?: true
updatedAt?: true
_all?: true
@@ -198,8 +204,9 @@ export type CourtBookingGroupByOutputType = {
endTime: string
customerName: string
customerPhone: string
customerEmail: string | null
customerEmail: string
status: $Enums.CourtBookingStatus
recurringGroupId: string | null
createdAt: Date
updatedAt: Date
_count: CourtBookingCountAggregateOutputType | null
@@ -234,11 +241,13 @@ export type CourtBookingWhereInput = {
endTime?: Prisma.StringFilter<"CourtBooking"> | string
customerName?: Prisma.StringFilter<"CourtBooking"> | string
customerPhone?: Prisma.StringFilter<"CourtBooking"> | string
customerEmail?: Prisma.StringNullableFilter<"CourtBooking"> | string | null
customerEmail?: Prisma.StringFilter<"CourtBooking"> | string
status?: Prisma.EnumCourtBookingStatusFilter<"CourtBooking"> | $Enums.CourtBookingStatus
recurringGroupId?: Prisma.UuidNullableFilter<"CourtBooking"> | string | null
createdAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
court?: Prisma.XOR<Prisma.CourtScalarRelationFilter, Prisma.CourtWhereInput>
recurringGroup?: Prisma.XOR<Prisma.RecurringBookingGroupNullableScalarRelationFilter, Prisma.RecurringBookingGroupWhereInput> | null
}
export type CourtBookingOrderByWithRelationInput = {
@@ -250,11 +259,13 @@ export type CourtBookingOrderByWithRelationInput = {
endTime?: Prisma.SortOrder
customerName?: Prisma.SortOrder
customerPhone?: Prisma.SortOrder
customerEmail?: Prisma.SortOrderInput | Prisma.SortOrder
customerEmail?: Prisma.SortOrder
status?: Prisma.SortOrder
recurringGroupId?: Prisma.SortOrderInput | Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
court?: Prisma.CourtOrderByWithRelationInput
recurringGroup?: Prisma.RecurringBookingGroupOrderByWithRelationInput
}
export type CourtBookingWhereUniqueInput = Prisma.AtLeast<{
@@ -270,11 +281,13 @@ export type CourtBookingWhereUniqueInput = Prisma.AtLeast<{
endTime?: Prisma.StringFilter<"CourtBooking"> | string
customerName?: Prisma.StringFilter<"CourtBooking"> | string
customerPhone?: Prisma.StringFilter<"CourtBooking"> | string
customerEmail?: Prisma.StringNullableFilter<"CourtBooking"> | string | null
customerEmail?: Prisma.StringFilter<"CourtBooking"> | string
status?: Prisma.EnumCourtBookingStatusFilter<"CourtBooking"> | $Enums.CourtBookingStatus
recurringGroupId?: Prisma.UuidNullableFilter<"CourtBooking"> | string | null
createdAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
court?: Prisma.XOR<Prisma.CourtScalarRelationFilter, Prisma.CourtWhereInput>
recurringGroup?: Prisma.XOR<Prisma.RecurringBookingGroupNullableScalarRelationFilter, Prisma.RecurringBookingGroupWhereInput> | null
}, "id" | "bookingCode" | "courtId_bookingDate_startTime">
export type CourtBookingOrderByWithAggregationInput = {
@@ -286,8 +299,9 @@ export type CourtBookingOrderByWithAggregationInput = {
endTime?: Prisma.SortOrder
customerName?: Prisma.SortOrder
customerPhone?: Prisma.SortOrder
customerEmail?: Prisma.SortOrderInput | Prisma.SortOrder
customerEmail?: Prisma.SortOrder
status?: Prisma.SortOrder
recurringGroupId?: Prisma.SortOrderInput | Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
_count?: Prisma.CourtBookingCountOrderByAggregateInput
@@ -307,8 +321,9 @@ export type CourtBookingScalarWhereWithAggregatesInput = {
endTime?: Prisma.StringWithAggregatesFilter<"CourtBooking"> | string
customerName?: Prisma.StringWithAggregatesFilter<"CourtBooking"> | string
customerPhone?: Prisma.StringWithAggregatesFilter<"CourtBooking"> | string
customerEmail?: Prisma.StringNullableWithAggregatesFilter<"CourtBooking"> | string | null
customerEmail?: Prisma.StringWithAggregatesFilter<"CourtBooking"> | string
status?: Prisma.EnumCourtBookingStatusWithAggregatesFilter<"CourtBooking"> | $Enums.CourtBookingStatus
recurringGroupId?: Prisma.UuidNullableWithAggregatesFilter<"CourtBooking"> | string | null
createdAt?: Prisma.DateTimeWithAggregatesFilter<"CourtBooking"> | Date | string
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"CourtBooking"> | Date | string
}
@@ -321,11 +336,12 @@ export type CourtBookingCreateInput = {
endTime: string
customerName: string
customerPhone: string
customerEmail?: string | null
customerEmail: string
status?: $Enums.CourtBookingStatus
createdAt?: Date | string
updatedAt?: Date | string
court: Prisma.CourtCreateNestedOneWithoutBookingsInput
recurringGroup?: Prisma.RecurringBookingGroupCreateNestedOneWithoutBookingsInput
}
export type CourtBookingUncheckedCreateInput = {
@@ -337,8 +353,9 @@ export type CourtBookingUncheckedCreateInput = {
endTime: string
customerName: string
customerPhone: string
customerEmail?: string | null
customerEmail: string
status?: $Enums.CourtBookingStatus
recurringGroupId?: string | null
createdAt?: Date | string
updatedAt?: Date | string
}
@@ -351,11 +368,12 @@ export type CourtBookingUpdateInput = {
endTime?: Prisma.StringFieldUpdateOperationsInput | string
customerName?: Prisma.StringFieldUpdateOperationsInput | string
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
customerEmail?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
court?: Prisma.CourtUpdateOneRequiredWithoutBookingsNestedInput
recurringGroup?: Prisma.RecurringBookingGroupUpdateOneWithoutBookingsNestedInput
}
export type CourtBookingUncheckedUpdateInput = {
@@ -367,8 +385,9 @@ export type CourtBookingUncheckedUpdateInput = {
endTime?: Prisma.StringFieldUpdateOperationsInput | string
customerName?: Prisma.StringFieldUpdateOperationsInput | string
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
customerEmail?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
recurringGroupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
@@ -382,8 +401,9 @@ export type CourtBookingCreateManyInput = {
endTime: string
customerName: string
customerPhone: string
customerEmail?: string | null
customerEmail: string
status?: $Enums.CourtBookingStatus
recurringGroupId?: string | null
createdAt?: Date | string
updatedAt?: Date | string
}
@@ -396,7 +416,7 @@ export type CourtBookingUpdateManyMutationInput = {
endTime?: Prisma.StringFieldUpdateOperationsInput | string
customerName?: Prisma.StringFieldUpdateOperationsInput | string
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
customerEmail?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@@ -411,8 +431,9 @@ export type CourtBookingUncheckedUpdateManyInput = {
endTime?: Prisma.StringFieldUpdateOperationsInput | string
customerName?: Prisma.StringFieldUpdateOperationsInput | string
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
customerEmail?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
recurringGroupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
@@ -444,6 +465,7 @@ export type CourtBookingCountOrderByAggregateInput = {
customerPhone?: Prisma.SortOrder
customerEmail?: Prisma.SortOrder
status?: Prisma.SortOrder
recurringGroupId?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
}
@@ -459,6 +481,7 @@ export type CourtBookingMaxOrderByAggregateInput = {
customerPhone?: Prisma.SortOrder
customerEmail?: Prisma.SortOrder
status?: Prisma.SortOrder
recurringGroupId?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
}
@@ -474,6 +497,7 @@ export type CourtBookingMinOrderByAggregateInput = {
customerPhone?: Prisma.SortOrder
customerEmail?: Prisma.SortOrder
status?: Prisma.SortOrder
recurringGroupId?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
}
@@ -524,6 +548,48 @@ export type EnumCourtBookingStatusFieldUpdateOperationsInput = {
set?: $Enums.CourtBookingStatus
}
export type CourtBookingCreateNestedManyWithoutRecurringGroupInput = {
create?: Prisma.XOR<Prisma.CourtBookingCreateWithoutRecurringGroupInput, Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput> | Prisma.CourtBookingCreateWithoutRecurringGroupInput[] | Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput[]
connectOrCreate?: Prisma.CourtBookingCreateOrConnectWithoutRecurringGroupInput | Prisma.CourtBookingCreateOrConnectWithoutRecurringGroupInput[]
createMany?: Prisma.CourtBookingCreateManyRecurringGroupInputEnvelope
connect?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
}
export type CourtBookingUncheckedCreateNestedManyWithoutRecurringGroupInput = {
create?: Prisma.XOR<Prisma.CourtBookingCreateWithoutRecurringGroupInput, Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput> | Prisma.CourtBookingCreateWithoutRecurringGroupInput[] | Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput[]
connectOrCreate?: Prisma.CourtBookingCreateOrConnectWithoutRecurringGroupInput | Prisma.CourtBookingCreateOrConnectWithoutRecurringGroupInput[]
createMany?: Prisma.CourtBookingCreateManyRecurringGroupInputEnvelope
connect?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
}
export type CourtBookingUpdateManyWithoutRecurringGroupNestedInput = {
create?: Prisma.XOR<Prisma.CourtBookingCreateWithoutRecurringGroupInput, Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput> | Prisma.CourtBookingCreateWithoutRecurringGroupInput[] | Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput[]
connectOrCreate?: Prisma.CourtBookingCreateOrConnectWithoutRecurringGroupInput | Prisma.CourtBookingCreateOrConnectWithoutRecurringGroupInput[]
upsert?: Prisma.CourtBookingUpsertWithWhereUniqueWithoutRecurringGroupInput | Prisma.CourtBookingUpsertWithWhereUniqueWithoutRecurringGroupInput[]
createMany?: Prisma.CourtBookingCreateManyRecurringGroupInputEnvelope
set?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
disconnect?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
delete?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
connect?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
update?: Prisma.CourtBookingUpdateWithWhereUniqueWithoutRecurringGroupInput | Prisma.CourtBookingUpdateWithWhereUniqueWithoutRecurringGroupInput[]
updateMany?: Prisma.CourtBookingUpdateManyWithWhereWithoutRecurringGroupInput | Prisma.CourtBookingUpdateManyWithWhereWithoutRecurringGroupInput[]
deleteMany?: Prisma.CourtBookingScalarWhereInput | Prisma.CourtBookingScalarWhereInput[]
}
export type CourtBookingUncheckedUpdateManyWithoutRecurringGroupNestedInput = {
create?: Prisma.XOR<Prisma.CourtBookingCreateWithoutRecurringGroupInput, Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput> | Prisma.CourtBookingCreateWithoutRecurringGroupInput[] | Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput[]
connectOrCreate?: Prisma.CourtBookingCreateOrConnectWithoutRecurringGroupInput | Prisma.CourtBookingCreateOrConnectWithoutRecurringGroupInput[]
upsert?: Prisma.CourtBookingUpsertWithWhereUniqueWithoutRecurringGroupInput | Prisma.CourtBookingUpsertWithWhereUniqueWithoutRecurringGroupInput[]
createMany?: Prisma.CourtBookingCreateManyRecurringGroupInputEnvelope
set?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
disconnect?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
delete?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
connect?: Prisma.CourtBookingWhereUniqueInput | Prisma.CourtBookingWhereUniqueInput[]
update?: Prisma.CourtBookingUpdateWithWhereUniqueWithoutRecurringGroupInput | Prisma.CourtBookingUpdateWithWhereUniqueWithoutRecurringGroupInput[]
updateMany?: Prisma.CourtBookingUpdateManyWithWhereWithoutRecurringGroupInput | Prisma.CourtBookingUpdateManyWithWhereWithoutRecurringGroupInput[]
deleteMany?: Prisma.CourtBookingScalarWhereInput | Prisma.CourtBookingScalarWhereInput[]
}
export type CourtBookingCreateWithoutCourtInput = {
id: string
bookingCode: string
@@ -532,10 +598,11 @@ export type CourtBookingCreateWithoutCourtInput = {
endTime: string
customerName: string
customerPhone: string
customerEmail?: string | null
customerEmail: string
status?: $Enums.CourtBookingStatus
createdAt?: Date | string
updatedAt?: Date | string
recurringGroup?: Prisma.RecurringBookingGroupCreateNestedOneWithoutBookingsInput
}
export type CourtBookingUncheckedCreateWithoutCourtInput = {
@@ -546,8 +613,9 @@ export type CourtBookingUncheckedCreateWithoutCourtInput = {
endTime: string
customerName: string
customerPhone: string
customerEmail?: string | null
customerEmail: string
status?: $Enums.CourtBookingStatus
recurringGroupId?: string | null
createdAt?: Date | string
updatedAt?: Date | string
}
@@ -590,12 +658,69 @@ export type CourtBookingScalarWhereInput = {
endTime?: Prisma.StringFilter<"CourtBooking"> | string
customerName?: Prisma.StringFilter<"CourtBooking"> | string
customerPhone?: Prisma.StringFilter<"CourtBooking"> | string
customerEmail?: Prisma.StringNullableFilter<"CourtBooking"> | string | null
customerEmail?: Prisma.StringFilter<"CourtBooking"> | string
status?: Prisma.EnumCourtBookingStatusFilter<"CourtBooking"> | $Enums.CourtBookingStatus
recurringGroupId?: Prisma.UuidNullableFilter<"CourtBooking"> | string | null
createdAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"CourtBooking"> | Date | string
}
export type CourtBookingCreateWithoutRecurringGroupInput = {
id: string
bookingCode: string
bookingDate: Date | string
startTime: string
endTime: string
customerName: string
customerPhone: string
customerEmail: string
status?: $Enums.CourtBookingStatus
createdAt?: Date | string
updatedAt?: Date | string
court: Prisma.CourtCreateNestedOneWithoutBookingsInput
}
export type CourtBookingUncheckedCreateWithoutRecurringGroupInput = {
id: string
bookingCode: string
courtId: string
bookingDate: Date | string
startTime: string
endTime: string
customerName: string
customerPhone: string
customerEmail: string
status?: $Enums.CourtBookingStatus
createdAt?: Date | string
updatedAt?: Date | string
}
export type CourtBookingCreateOrConnectWithoutRecurringGroupInput = {
where: Prisma.CourtBookingWhereUniqueInput
create: Prisma.XOR<Prisma.CourtBookingCreateWithoutRecurringGroupInput, Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput>
}
export type CourtBookingCreateManyRecurringGroupInputEnvelope = {
data: Prisma.CourtBookingCreateManyRecurringGroupInput | Prisma.CourtBookingCreateManyRecurringGroupInput[]
skipDuplicates?: boolean
}
export type CourtBookingUpsertWithWhereUniqueWithoutRecurringGroupInput = {
where: Prisma.CourtBookingWhereUniqueInput
update: Prisma.XOR<Prisma.CourtBookingUpdateWithoutRecurringGroupInput, Prisma.CourtBookingUncheckedUpdateWithoutRecurringGroupInput>
create: Prisma.XOR<Prisma.CourtBookingCreateWithoutRecurringGroupInput, Prisma.CourtBookingUncheckedCreateWithoutRecurringGroupInput>
}
export type CourtBookingUpdateWithWhereUniqueWithoutRecurringGroupInput = {
where: Prisma.CourtBookingWhereUniqueInput
data: Prisma.XOR<Prisma.CourtBookingUpdateWithoutRecurringGroupInput, Prisma.CourtBookingUncheckedUpdateWithoutRecurringGroupInput>
}
export type CourtBookingUpdateManyWithWhereWithoutRecurringGroupInput = {
where: Prisma.CourtBookingScalarWhereInput
data: Prisma.XOR<Prisma.CourtBookingUpdateManyMutationInput, Prisma.CourtBookingUncheckedUpdateManyWithoutRecurringGroupInput>
}
export type CourtBookingCreateManyCourtInput = {
id: string
bookingCode: string
@@ -604,8 +729,9 @@ export type CourtBookingCreateManyCourtInput = {
endTime: string
customerName: string
customerPhone: string
customerEmail?: string | null
customerEmail: string
status?: $Enums.CourtBookingStatus
recurringGroupId?: string | null
createdAt?: Date | string
updatedAt?: Date | string
}
@@ -618,10 +744,11 @@ export type CourtBookingUpdateWithoutCourtInput = {
endTime?: Prisma.StringFieldUpdateOperationsInput | string
customerName?: Prisma.StringFieldUpdateOperationsInput | string
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
customerEmail?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
recurringGroup?: Prisma.RecurringBookingGroupUpdateOneWithoutBookingsNestedInput
}
export type CourtBookingUncheckedUpdateWithoutCourtInput = {
@@ -632,8 +759,9 @@ export type CourtBookingUncheckedUpdateWithoutCourtInput = {
endTime?: Prisma.StringFieldUpdateOperationsInput | string
customerName?: Prisma.StringFieldUpdateOperationsInput | string
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
customerEmail?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
recurringGroupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
@@ -646,7 +774,68 @@ export type CourtBookingUncheckedUpdateManyWithoutCourtInput = {
endTime?: Prisma.StringFieldUpdateOperationsInput | string
customerName?: Prisma.StringFieldUpdateOperationsInput | string
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
customerEmail?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
recurringGroupId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
export type CourtBookingCreateManyRecurringGroupInput = {
id: string
bookingCode: string
courtId: string
bookingDate: Date | string
startTime: string
endTime: string
customerName: string
customerPhone: string
customerEmail: string
status?: $Enums.CourtBookingStatus
createdAt?: Date | string
updatedAt?: Date | string
}
export type CourtBookingUpdateWithoutRecurringGroupInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
bookingCode?: Prisma.StringFieldUpdateOperationsInput | string
bookingDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
startTime?: Prisma.StringFieldUpdateOperationsInput | string
endTime?: Prisma.StringFieldUpdateOperationsInput | string
customerName?: Prisma.StringFieldUpdateOperationsInput | string
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
court?: Prisma.CourtUpdateOneRequiredWithoutBookingsNestedInput
}
export type CourtBookingUncheckedUpdateWithoutRecurringGroupInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
bookingCode?: Prisma.StringFieldUpdateOperationsInput | string
courtId?: Prisma.StringFieldUpdateOperationsInput | string
bookingDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
startTime?: Prisma.StringFieldUpdateOperationsInput | string
endTime?: Prisma.StringFieldUpdateOperationsInput | string
customerName?: Prisma.StringFieldUpdateOperationsInput | string
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
export type CourtBookingUncheckedUpdateManyWithoutRecurringGroupInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
bookingCode?: Prisma.StringFieldUpdateOperationsInput | string
courtId?: Prisma.StringFieldUpdateOperationsInput | string
bookingDate?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
startTime?: Prisma.StringFieldUpdateOperationsInput | string
endTime?: Prisma.StringFieldUpdateOperationsInput | string
customerName?: Prisma.StringFieldUpdateOperationsInput | string
customerPhone?: Prisma.StringFieldUpdateOperationsInput | string
customerEmail?: Prisma.StringFieldUpdateOperationsInput | string
status?: Prisma.EnumCourtBookingStatusFieldUpdateOperationsInput | $Enums.CourtBookingStatus
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@@ -665,9 +854,11 @@ export type CourtBookingSelect<ExtArgs extends runtime.Types.Extensions.Internal
customerPhone?: boolean
customerEmail?: boolean
status?: boolean
recurringGroupId?: boolean
createdAt?: boolean
updatedAt?: boolean
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
recurringGroup?: boolean | Prisma.CourtBooking$recurringGroupArgs<ExtArgs>
}, ExtArgs["result"]["courtBooking"]>
export type CourtBookingSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
@@ -681,9 +872,11 @@ export type CourtBookingSelectCreateManyAndReturn<ExtArgs extends runtime.Types.
customerPhone?: boolean
customerEmail?: boolean
status?: boolean
recurringGroupId?: boolean
createdAt?: boolean
updatedAt?: boolean
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
recurringGroup?: boolean | Prisma.CourtBooking$recurringGroupArgs<ExtArgs>
}, ExtArgs["result"]["courtBooking"]>
export type CourtBookingSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
@@ -697,9 +890,11 @@ export type CourtBookingSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.
customerPhone?: boolean
customerEmail?: boolean
status?: boolean
recurringGroupId?: boolean
createdAt?: boolean
updatedAt?: boolean
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
recurringGroup?: boolean | Prisma.CourtBooking$recurringGroupArgs<ExtArgs>
}, ExtArgs["result"]["courtBooking"]>
export type CourtBookingSelectScalar = {
@@ -713,25 +908,30 @@ export type CourtBookingSelectScalar = {
customerPhone?: boolean
customerEmail?: boolean
status?: boolean
recurringGroupId?: boolean
createdAt?: boolean
updatedAt?: boolean
}
export type CourtBookingOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "bookingCode" | "courtId" | "bookingDate" | "startTime" | "endTime" | "customerName" | "customerPhone" | "customerEmail" | "status" | "createdAt" | "updatedAt", ExtArgs["result"]["courtBooking"]>
export type CourtBookingOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "bookingCode" | "courtId" | "bookingDate" | "startTime" | "endTime" | "customerName" | "customerPhone" | "customerEmail" | "status" | "recurringGroupId" | "createdAt" | "updatedAt", ExtArgs["result"]["courtBooking"]>
export type CourtBookingInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
recurringGroup?: boolean | Prisma.CourtBooking$recurringGroupArgs<ExtArgs>
}
export type CourtBookingIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
recurringGroup?: boolean | Prisma.CourtBooking$recurringGroupArgs<ExtArgs>
}
export type CourtBookingIncludeUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
court?: boolean | Prisma.CourtDefaultArgs<ExtArgs>
recurringGroup?: boolean | Prisma.CourtBooking$recurringGroupArgs<ExtArgs>
}
export type $CourtBookingPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
name: "CourtBooking"
objects: {
court: Prisma.$CourtPayload<ExtArgs>
recurringGroup: Prisma.$RecurringBookingGroupPayload<ExtArgs> | null
}
scalars: runtime.Types.Extensions.GetPayloadResult<{
id: string
@@ -742,8 +942,9 @@ export type $CourtBookingPayload<ExtArgs extends runtime.Types.Extensions.Intern
endTime: string
customerName: string
customerPhone: string
customerEmail: string | null
customerEmail: string
status: $Enums.CourtBookingStatus
recurringGroupId: string | null
createdAt: Date
updatedAt: Date
}, ExtArgs["result"]["courtBooking"]>
@@ -1141,6 +1342,7 @@ readonly fields: CourtBookingFieldRefs;
export interface Prisma__CourtBookingClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
readonly [Symbol.toStringTag]: "PrismaPromise"
court<T extends Prisma.CourtDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.CourtDefaultArgs<ExtArgs>>): Prisma.Prisma__CourtClient<runtime.Types.Result.GetResult<Prisma.$CourtPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
recurringGroup<T extends Prisma.CourtBooking$recurringGroupArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.CourtBooking$recurringGroupArgs<ExtArgs>>): Prisma.Prisma__RecurringBookingGroupClient<runtime.Types.Result.GetResult<Prisma.$RecurringBookingGroupPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
@@ -1180,6 +1382,7 @@ export interface CourtBookingFieldRefs {
readonly customerPhone: Prisma.FieldRef<"CourtBooking", 'String'>
readonly customerEmail: Prisma.FieldRef<"CourtBooking", 'String'>
readonly status: Prisma.FieldRef<"CourtBooking", 'CourtBookingStatus'>
readonly recurringGroupId: Prisma.FieldRef<"CourtBooking", 'String'>
readonly createdAt: Prisma.FieldRef<"CourtBooking", 'DateTime'>
readonly updatedAt: Prisma.FieldRef<"CourtBooking", 'DateTime'>
}
@@ -1582,6 +1785,25 @@ export type CourtBookingDeleteManyArgs<ExtArgs extends runtime.Types.Extensions.
limit?: number
}
/**
* CourtBooking.recurringGroup
*/
export type CourtBooking$recurringGroupArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the RecurringBookingGroup
*/
select?: Prisma.RecurringBookingGroupSelect<ExtArgs> | null
/**
* Omit specific fields from the RecurringBookingGroup
*/
omit?: Prisma.RecurringBookingGroupOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.RecurringBookingGroupInclude<ExtArgs> | null
where?: Prisma.RecurringBookingGroupWhereInput
}
/**
* CourtBooking without action
*/

View File

@@ -31,6 +31,9 @@ export type UserMinAggregateOutputType = {
emailVerified: boolean | null
image: string | null
phone: string | null
banned: boolean | null
bannedAt: Date | null
banReason: string | null
createdAt: Date | null
updatedAt: Date | null
role: string | null
@@ -43,6 +46,9 @@ export type UserMaxAggregateOutputType = {
emailVerified: boolean | null
image: string | null
phone: string | null
banned: boolean | null
bannedAt: Date | null
banReason: string | null
createdAt: Date | null
updatedAt: Date | null
role: string | null
@@ -55,6 +61,9 @@ export type UserCountAggregateOutputType = {
emailVerified: number
image: number
phone: number
banned: number
bannedAt: number
banReason: number
createdAt: number
updatedAt: number
role: number
@@ -69,6 +78,9 @@ export type UserMinAggregateInputType = {
emailVerified?: true
image?: true
phone?: true
banned?: true
bannedAt?: true
banReason?: true
createdAt?: true
updatedAt?: true
role?: true
@@ -81,6 +93,9 @@ export type UserMaxAggregateInputType = {
emailVerified?: true
image?: true
phone?: true
banned?: true
bannedAt?: true
banReason?: true
createdAt?: true
updatedAt?: true
role?: true
@@ -93,6 +108,9 @@ export type UserCountAggregateInputType = {
emailVerified?: true
image?: true
phone?: true
banned?: true
bannedAt?: true
banReason?: true
createdAt?: true
updatedAt?: true
role?: true
@@ -178,6 +196,9 @@ export type UserGroupByOutputType = {
emailVerified: boolean
image: string | null
phone: string | null
banned: boolean
bannedAt: Date | null
banReason: string | null
createdAt: Date
updatedAt: Date
role: string
@@ -211,6 +232,9 @@ export type UserWhereInput = {
emailVerified?: Prisma.BoolFilter<"User"> | boolean
image?: Prisma.StringNullableFilter<"User"> | string | null
phone?: Prisma.StringNullableFilter<"User"> | string | null
banned?: Prisma.BoolFilter<"User"> | boolean
bannedAt?: Prisma.DateTimeNullableFilter<"User"> | Date | string | null
banReason?: Prisma.StringNullableFilter<"User"> | string | null
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
role?: Prisma.StringFilter<"User"> | string
@@ -226,6 +250,9 @@ export type UserOrderByWithRelationInput = {
emailVerified?: Prisma.SortOrder
image?: Prisma.SortOrderInput | Prisma.SortOrder
phone?: Prisma.SortOrderInput | Prisma.SortOrder
banned?: Prisma.SortOrder
bannedAt?: Prisma.SortOrderInput | Prisma.SortOrder
banReason?: Prisma.SortOrderInput | Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
role?: Prisma.SortOrder
@@ -244,6 +271,9 @@ export type UserWhereUniqueInput = Prisma.AtLeast<{
emailVerified?: Prisma.BoolFilter<"User"> | boolean
image?: Prisma.StringNullableFilter<"User"> | string | null
phone?: Prisma.StringNullableFilter<"User"> | string | null
banned?: Prisma.BoolFilter<"User"> | boolean
bannedAt?: Prisma.DateTimeNullableFilter<"User"> | Date | string | null
banReason?: Prisma.StringNullableFilter<"User"> | string | null
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
role?: Prisma.StringFilter<"User"> | string
@@ -259,6 +289,9 @@ export type UserOrderByWithAggregationInput = {
emailVerified?: Prisma.SortOrder
image?: Prisma.SortOrderInput | Prisma.SortOrder
phone?: Prisma.SortOrderInput | Prisma.SortOrder
banned?: Prisma.SortOrder
bannedAt?: Prisma.SortOrderInput | Prisma.SortOrder
banReason?: Prisma.SortOrderInput | Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
role?: Prisma.SortOrder
@@ -277,6 +310,9 @@ export type UserScalarWhereWithAggregatesInput = {
emailVerified?: Prisma.BoolWithAggregatesFilter<"User"> | boolean
image?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null
phone?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null
banned?: Prisma.BoolWithAggregatesFilter<"User"> | boolean
bannedAt?: Prisma.DateTimeNullableWithAggregatesFilter<"User"> | Date | string | null
banReason?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null
createdAt?: Prisma.DateTimeWithAggregatesFilter<"User"> | Date | string
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"User"> | Date | string
role?: Prisma.StringWithAggregatesFilter<"User"> | string
@@ -289,6 +325,9 @@ export type UserCreateInput = {
emailVerified?: boolean
image?: string | null
phone?: string | null
banned?: boolean
bannedAt?: Date | string | null
banReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
role?: string
@@ -304,6 +343,9 @@ export type UserUncheckedCreateInput = {
emailVerified?: boolean
image?: string | null
phone?: string | null
banned?: boolean
bannedAt?: Date | string | null
banReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
role?: string
@@ -319,6 +361,9 @@ export type UserUpdateInput = {
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
bannedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
banReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string
@@ -334,6 +379,9 @@ export type UserUncheckedUpdateInput = {
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
bannedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
banReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string
@@ -349,6 +397,9 @@ export type UserCreateManyInput = {
emailVerified?: boolean
image?: string | null
phone?: string | null
banned?: boolean
bannedAt?: Date | string | null
banReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
role?: string
@@ -361,6 +412,9 @@ export type UserUpdateManyMutationInput = {
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
bannedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
banReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string
@@ -373,6 +427,9 @@ export type UserUncheckedUpdateManyInput = {
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
bannedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
banReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string
@@ -385,6 +442,9 @@ export type UserCountOrderByAggregateInput = {
emailVerified?: Prisma.SortOrder
image?: Prisma.SortOrder
phone?: Prisma.SortOrder
banned?: Prisma.SortOrder
bannedAt?: Prisma.SortOrder
banReason?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
role?: Prisma.SortOrder
@@ -397,6 +457,9 @@ export type UserMaxOrderByAggregateInput = {
emailVerified?: Prisma.SortOrder
image?: Prisma.SortOrder
phone?: Prisma.SortOrder
banned?: Prisma.SortOrder
bannedAt?: Prisma.SortOrder
banReason?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
role?: Prisma.SortOrder
@@ -409,6 +472,9 @@ export type UserMinOrderByAggregateInput = {
emailVerified?: Prisma.SortOrder
image?: Prisma.SortOrder
phone?: Prisma.SortOrder
banned?: Prisma.SortOrder
bannedAt?: Prisma.SortOrder
banReason?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
role?: Prisma.SortOrder
@@ -431,6 +497,10 @@ export type NullableStringFieldUpdateOperationsInput = {
set?: string | null
}
export type NullableDateTimeFieldUpdateOperationsInput = {
set?: Date | string | null
}
export type DateTimeFieldUpdateOperationsInput = {
set?: Date | string
}
@@ -484,6 +554,9 @@ export type UserCreateWithoutSessionsInput = {
emailVerified?: boolean
image?: string | null
phone?: string | null
banned?: boolean
bannedAt?: Date | string | null
banReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
role?: string
@@ -498,6 +571,9 @@ export type UserUncheckedCreateWithoutSessionsInput = {
emailVerified?: boolean
image?: string | null
phone?: string | null
banned?: boolean
bannedAt?: Date | string | null
banReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
role?: string
@@ -528,6 +604,9 @@ export type UserUpdateWithoutSessionsInput = {
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
bannedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
banReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string
@@ -542,6 +621,9 @@ export type UserUncheckedUpdateWithoutSessionsInput = {
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
bannedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
banReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string
@@ -556,6 +638,9 @@ export type UserCreateWithoutAccountsInput = {
emailVerified?: boolean
image?: string | null
phone?: string | null
banned?: boolean
bannedAt?: Date | string | null
banReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
role?: string
@@ -570,6 +655,9 @@ export type UserUncheckedCreateWithoutAccountsInput = {
emailVerified?: boolean
image?: string | null
phone?: string | null
banned?: boolean
bannedAt?: Date | string | null
banReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
role?: string
@@ -600,6 +688,9 @@ export type UserUpdateWithoutAccountsInput = {
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
bannedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
banReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string
@@ -614,6 +705,9 @@ export type UserUncheckedUpdateWithoutAccountsInput = {
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
bannedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
banReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string
@@ -628,6 +722,9 @@ export type UserCreateWithoutComplexesInput = {
emailVerified?: boolean
image?: string | null
phone?: string | null
banned?: boolean
bannedAt?: Date | string | null
banReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
role?: string
@@ -642,6 +739,9 @@ export type UserUncheckedCreateWithoutComplexesInput = {
emailVerified?: boolean
image?: string | null
phone?: string | null
banned?: boolean
bannedAt?: Date | string | null
banReason?: string | null
createdAt?: Date | string
updatedAt?: Date | string
role?: string
@@ -672,6 +772,9 @@ export type UserUpdateWithoutComplexesInput = {
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
bannedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
banReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string
@@ -686,6 +789,9 @@ export type UserUncheckedUpdateWithoutComplexesInput = {
emailVerified?: Prisma.BoolFieldUpdateOperationsInput | boolean
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
phone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
banned?: Prisma.BoolFieldUpdateOperationsInput | boolean
bannedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
banReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.StringFieldUpdateOperationsInput | string
@@ -749,6 +855,9 @@ export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = r
emailVerified?: boolean
image?: boolean
phone?: boolean
banned?: boolean
bannedAt?: boolean
banReason?: boolean
createdAt?: boolean
updatedAt?: boolean
role?: boolean
@@ -765,6 +874,9 @@ export type UserSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensio
emailVerified?: boolean
image?: boolean
phone?: boolean
banned?: boolean
bannedAt?: boolean
banReason?: boolean
createdAt?: boolean
updatedAt?: boolean
role?: boolean
@@ -777,6 +889,9 @@ export type UserSelectUpdateManyAndReturn<ExtArgs extends runtime.Types.Extensio
emailVerified?: boolean
image?: boolean
phone?: boolean
banned?: boolean
bannedAt?: boolean
banReason?: boolean
createdAt?: boolean
updatedAt?: boolean
role?: boolean
@@ -789,12 +904,15 @@ export type UserSelectScalar = {
emailVerified?: boolean
image?: boolean
phone?: boolean
banned?: boolean
bannedAt?: boolean
banReason?: boolean
createdAt?: boolean
updatedAt?: boolean
role?: boolean
}
export type UserOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "email" | "emailVerified" | "image" | "phone" | "createdAt" | "updatedAt" | "role", ExtArgs["result"]["user"]>
export type UserOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "email" | "emailVerified" | "image" | "phone" | "banned" | "bannedAt" | "banReason" | "createdAt" | "updatedAt" | "role", ExtArgs["result"]["user"]>
export type UserInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
sessions?: boolean | Prisma.User$sessionsArgs<ExtArgs>
accounts?: boolean | Prisma.User$accountsArgs<ExtArgs>
@@ -818,6 +936,9 @@ export type $UserPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs =
emailVerified: boolean
image: string | null
phone: string | null
banned: boolean
bannedAt: Date | null
banReason: string | null
createdAt: Date
updatedAt: Date
role: string
@@ -1253,6 +1374,9 @@ export interface UserFieldRefs {
readonly emailVerified: Prisma.FieldRef<"User", 'Boolean'>
readonly image: Prisma.FieldRef<"User", 'String'>
readonly phone: Prisma.FieldRef<"User", 'String'>
readonly banned: Prisma.FieldRef<"User", 'Boolean'>
readonly bannedAt: Prisma.FieldRef<"User", 'DateTime'>
readonly banReason: Prisma.FieldRef<"User", 'String'>
readonly createdAt: Prisma.FieldRef<"User", 'DateTime'>
readonly updatedAt: Prisma.FieldRef<"User", 'DateTime'>
readonly role: Prisma.FieldRef<"User", 'String'>

View File

@@ -1,3 +1,4 @@
import { wrapLayout } from '@/emails/booking-confirmation';
import { dash } from '@better-auth/infra';
import { betterAuth } from 'better-auth';
import { prismaAdapter } from 'better-auth/adapters/prisma';
@@ -21,10 +22,26 @@ export const auth = betterAuth({
},
user: {
additionalFields: {
role: {
type: 'string',
required: false,
},
phone: {
type: 'string',
required: false,
},
banned: {
type: 'boolean',
required: false,
},
bannedAt: {
type: 'string',
required: false,
},
banReason: {
type: 'string',
required: false,
},
},
},
emailVerification: {
@@ -34,10 +51,73 @@ export const auth = betterAuth({
const verificationUrl = new URL(url);
const appUrl = process.env.APP_BASE_URL ?? 'http://localhost:5173';
verificationUrl.searchParams.set('callbackURL', appUrl);
const content = `
<tr>
<td style="padding:24px 20px 16px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td valign="top">
<table role="presentation" cellpadding="0" cellspacing="0" style="display:inline-block;background-color:#f4f4f5;border-radius:999px;padding:4px 12px;">
<tr>
<td style="font-size:11px;font-weight:700;letter-spacing:0.14em;color:#71717a;text-transform:uppercase;line-height:1.25rem;">
Verificaci&oacute;n de email
</td>
</tr>
</table>
<h1 style="margin:16px 0 0;font-size:24px;font-weight:700;color:#111827;letter-spacing:-0.025em;">
Verific&aacute; tu direcci&oacute;n de correo electr&oacute;nico
</h1>
</td>
<td valign="top" align="right" style="white-space:nowrap;">
<table role="presentation" cellpadding="0" cellspacing="0">
<tr>
<td valign="middle" style="padding-right:8px;">
<img src="${appUrl}/playzer-favicon-512-transparent.png" alt="Playzer" width="32" height="32" style="display:block;" />
</td>
<td valign="middle">
<span style="font-size:18px;font-weight:700;color:#475569;letter-spacing:-0.025em;">Playzer</span>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<tr>
padding:0 20px 16px;
<p style="margin:0;font-size:15px;color:#374151;line-height:1.6;">
Hac&eacute; click en el bot&oacute;n de abajo para verificar tu direcci&oacute;n de correo electr&oacute;nico y empezar a usar Playzer.
</p>
</td>
</tr>
<tr>
<td style="padding:0 20px 24px;">
<table role="presentation" cellpadding="0" cellspacing="0">
<tr>
<td style="background-color:#059669;border-radius:12px;text-align:center;">
<a href="${verificationUrl.toString()}" style="display:block;padding:14px 32px;font-size:15px;font-weight:600;color:#ffffff;text-decoration:none;line-height:1.25rem;">Verificar email</a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="padding:0 20px 24px;">
<p style="margin:0;font-size:13px;color:#9ca3af;text-align:center;line-height:1.5;">
Si no creaste una cuenta en Playzer, ignor&aacute; este mensaje.
</p>
</td>
</tr>`;
await sendMail({
to: user.email,
subject: 'Verificá tu email en Playzer',
html: `Hacé click para verificar tu email: <a href="${verificationUrl.toString()}">${verificationUrl.toString()}</a>`,
html: wrapLayout(content),
text: `Hacé click para verificar tu email: ${verificationUrl.toString()}`,
});
},

View File

@@ -0,0 +1,176 @@
import { randomInt } from 'node:crypto';
import type { DayOfWeek } from '@repo/api-contract';
export type Slot = {
startTime: string;
endTime: string;
};
const BOOKING_CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
const BOOKING_CODE_LENGTH = 6;
export const DAY_OF_WEEK_BY_INDEX: DayOfWeek[] = [
'SUNDAY',
'MONDAY',
'TUESDAY',
'WEDNESDAY',
'THURSDAY',
'FRIDAY',
'SATURDAY',
];
export function toMinutes(value: string): number {
const [hours, minutes] = value.split(':').map((part) => Number(part));
return hours * 60 + minutes;
}
export function minutesToTime(minutes: number): string {
const safeMinutes = Math.max(0, minutes);
const hours = Math.floor(safeMinutes / 60);
const mins = safeMinutes % 60;
return `${String(hours).padStart(2, '0')}:${String(mins).padStart(2, '0')}`;
}
export function parseIsoDate(date: string): { bookingDate: Date; dayOfWeek: DayOfWeek } {
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
if (!match) {
throw new Error('La fecha debe tener formato YYYY-MM-DD.');
}
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
const bookingDate = new Date(Date.UTC(year, month - 1, day));
if (
Number.isNaN(bookingDate.getTime()) ||
bookingDate.getUTCFullYear() !== year ||
bookingDate.getUTCMonth() + 1 !== month ||
bookingDate.getUTCDate() !== day
) {
throw new Error('La fecha enviada no es valida.');
}
const dayOfWeek = DAY_OF_WEEK_BY_INDEX[bookingDate.getUTCDay()];
if (!dayOfWeek) {
throw new Error('No se pudo resolver el dia de la semana.');
}
return { bookingDate, dayOfWeek };
}
export function formatIsoDate(date: Date): string {
return date.toISOString().slice(0, 10);
}
export function getDayOfWeekValue(date: Date): DayOfWeek {
const dayOfWeek = DAY_OF_WEEK_BY_INDEX[date.getUTCDay()];
if (!dayOfWeek) {
throw new Error('No se pudo resolver el dia de la semana.');
}
return dayOfWeek;
}
export function generateBookingCode(): string {
let code = '';
for (let index = 0; index < BOOKING_CODE_LENGTH; index += 1) {
code += BOOKING_CODE_ALPHABET[randomInt(0, BOOKING_CODE_ALPHABET.length)];
}
return code;
}
export function buildSlots(
availability: Array<{ startTime: string; endTime: string }>,
slotDurationMinutes: number
): Slot[] {
const slots: Slot[] = [];
for (const range of availability) {
const start = toMinutes(range.startTime);
const end = toMinutes(range.endTime);
for (
let current = start;
current + slotDurationMinutes <= end;
current += slotDurationMinutes
) {
slots.push({
startTime: minutesToTime(current),
endTime: minutesToTime(current + slotDurationMinutes),
});
}
}
return slots;
}
export function hasOverlap(slot: Slot, existing: Slot): boolean {
return slot.startTime < existing.endTime && slot.endTime > existing.startTime;
}
type PriceableCourt = {
basePrice: unknown;
priceRules: Array<{
dayOfWeek: DayOfWeek | null;
startTime: string | null;
endTime: string | null;
price: unknown;
}>;
};
export function resolveSlotPrice(court: PriceableCourt, dayOfWeek: DayOfWeek, slot: Slot): number {
const slotStart = toMinutes(slot.startTime);
const slotEnd = toMinutes(slot.endTime);
const matchingRules = court.priceRules
.filter((rule) => {
if (rule.dayOfWeek && rule.dayOfWeek !== dayOfWeek) return false;
if (!rule.startTime || !rule.endTime) return true;
return slotStart >= toMinutes(rule.startTime) && slotEnd <= toMinutes(rule.endTime);
})
.sort((first, second) => {
const firstSpecificity =
(first.dayOfWeek ? 2 : 0) + (first.startTime && first.endTime ? 1 : 0);
const secondSpecificity =
(second.dayOfWeek ? 2 : 0) + (second.startTime && second.endTime ? 1 : 0);
return secondSpecificity - firstSpecificity;
});
return Number(matchingRules[0]?.price ?? court.basePrice);
}
export function assertAvailabilityRanges(
availability: Array<{
dayOfWeek: DayOfWeek;
startTime: string;
endTime: string;
}>
) {
const grouped = new Map<DayOfWeek, Array<{ start: number; end: number }>>();
for (const range of availability) {
const start = toMinutes(range.startTime);
const end = toMinutes(range.endTime);
if (start >= end) {
throw new Error(`El rango ${range.startTime}-${range.endTime} es invalido.`);
}
const current = grouped.get(range.dayOfWeek) ?? [];
current.push({ start, end });
grouped.set(range.dayOfWeek, current);
}
for (const ranges of grouped.values()) {
ranges.sort((a, b) => a.start - b.start);
for (let index = 1; index < ranges.length; index += 1) {
const previous = ranges[index - 1];
const current = ranges[index];
if (previous.end > current.start) {
throw new Error('Hay rangos horarios superpuestos para el mismo dia.');
}
}
}
}

View File

@@ -0,0 +1,41 @@
import { db } from '@/lib/prisma';
export class ComplexAccessError extends Error {
status: 403 | 404;
constructor(message: string, status: 403 | 404 = 403) {
super(message);
this.name = 'ComplexAccessError';
this.status = status;
}
}
export async function ensureComplexAccess(complexId: string, userId: string) {
const complexUser = await db.complexUser.findUnique({
where: {
complexId_userId: {
complexId,
userId,
},
},
include: {
complex: {
select: {
id: true,
complexName: true,
plan: {
select: {
rules: true,
},
},
},
},
},
});
if (!complexUser) {
throw new ComplexAccessError('No tienes permisos para administrar este complejo.', 403);
}
return complexUser.complex;
}

View File

@@ -0,0 +1,68 @@
type IpGeoInfo = {
ip: string;
city: string;
country: string;
countryCode: string;
lat: number | null;
lon: number | null;
};
const cache = new Map<string, { data: IpGeoInfo; expiresAt: number }>();
const CACHE_TTL_MS = 60 * 60 * 1000;
function getCached(ip: string): IpGeoInfo | undefined {
const entry = cache.get(ip);
if (!entry) return undefined;
if (Date.now() > entry.expiresAt) {
cache.delete(ip);
return undefined;
}
return entry.data;
}
function setCache(ip: string, info: IpGeoInfo): void {
cache.set(ip, { data: info, expiresAt: Date.now() + CACHE_TTL_MS });
}
async function fetchGeoInfo(ip: string): Promise<IpGeoInfo | null> {
if (ip === '127.0.0.1' || ip === '::1' || ip.startsWith('192.168.') || ip.startsWith('10.')) {
return {
ip,
city: 'Red local',
country: 'Red local',
countryCode: 'LOCAL',
lat: null,
lon: null,
};
}
const cached = getCached(ip);
if (cached) return cached;
try {
const response = await fetch(
`http://ip-api.com/json/${ip}?fields=status,country,countryCode,city,lat,lon,query`
);
if (!response.ok) return null;
const data = await response.json();
if (data.status !== 'success') return null;
const info: IpGeoInfo = {
ip: data.query,
city: data.city || 'Desconocida',
country: data.country || 'Desconocido',
countryCode: data.countryCode || '',
lat: data.lat ?? null,
lon: data.lon ?? null,
};
setCache(ip, info);
return info;
} catch {
return null;
}
}
export type { IpGeoInfo };
export { fetchGeoInfo };

View File

@@ -0,0 +1,34 @@
function toMinutes(value: string): number {
const [hours, minutes] = value.split(':').map((part) => Number(part));
return hours * 60 + minutes;
}
export function isSlotInPast(
bookingDate: Date,
startTime: string,
slotDurationMinutes: number,
now?: Date
): boolean {
const currentTime = now ?? new Date();
const todayStart = new Date(
currentTime.getFullYear(),
currentTime.getMonth(),
currentTime.getDate()
);
const bookingLocalStart = new Date(
bookingDate.getUTCFullYear(),
bookingDate.getUTCMonth(),
bookingDate.getUTCDate()
);
if (bookingLocalStart < todayStart) return true;
if (bookingLocalStart > todayStart) return false;
const currentMinutes = currentTime.getHours() * 60 + currentTime.getMinutes();
const slotStartMinutes = toMinutes(startTime);
const elapsed = currentMinutes - slotStartMinutes;
return elapsed > slotDurationMinutes / 2;
}

View File

@@ -0,0 +1,31 @@
import { v7 as uuidv7 } from 'uuid';
export function slugify(value: string): string {
return value
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase()
.trim()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-');
}
export async function buildUniqueSlug(
source: string,
findExisting: (slug: string) => Promise<{ id: string } | null>
): Promise<string> {
const base = slugify(source);
const fallback = base.length > 0 ? base : `resource-${uuidv7().slice(0, 8)}`;
let candidate = fallback;
let index = 1;
while (true) {
const existing = await findExisting(candidate);
if (!existing) return candidate;
index += 1;
candidate = `${fallback}-${index}`;
}
}

View File

@@ -1,7 +1,20 @@
import { auth } from '@/lib/auth';
import { fetchGeoInfo } from '@/lib/geoip';
import { db } from '@/lib/prisma';
import type { AppEnv } from '@/types/hono';
import { createMiddleware } from 'hono/factory';
type SessionWithGeo = {
id: string;
ipAddress: string | null;
userAgent: string | null;
country: string | null;
city: string | null;
countryCode: string | null;
latitude: number | null;
longitude: number | null;
};
export const requireAuth = createMiddleware<AppEnv>(async (c, next) => {
const session = await auth.api.getSession({
headers: c.req.raw.headers,
@@ -11,8 +24,45 @@ export const requireAuth = createMiddleware<AppEnv>(async (c, next) => {
return c.json({ message: 'Unauthorized' }, 401);
}
const user = session.user as { banned?: boolean; banReason?: string | null };
if (user.banned) {
return c.json(
{
message: 'Tu cuenta ha sido bloqueada.',
...(user.banReason ? { reason: user.banReason } : {}),
},
403
);
}
const s = session.session as unknown as SessionWithGeo;
c.set('user', session.user);
c.set('session', session.session);
if (s.ipAddress && !s.country) {
resolveSessionGeo(s.id, s.ipAddress);
}
await next();
});
async function resolveSessionGeo(sessionId: string, ipAddress: string) {
try {
const geo = await fetchGeoInfo(ipAddress);
if (!geo) return;
await db.session.update({
where: { id: sessionId },
data: {
country: geo.country,
city: geo.city,
countryCode: geo.countryCode,
latitude: geo.lat,
longitude: geo.lon,
},
});
} catch {
// Silently fail — geo enrichment is best-effort
}
}

View File

@@ -1,13 +1,21 @@
import { requireAuth } from '@/middlewares/require-auth.middleware';
import { createAdminBookingHandler } from '@/modules/admin-booking/handlers/create-admin-booking.handler';
import { listAdminBookingsHandler } from '@/modules/admin-booking/handlers/list-admin-bookings.handler';
import { updateAdminBookingStatusHandler } from '@/modules/admin-booking/handlers/update-admin-booking-status.handler';
import { cancelRecurringGroupHandler } from '@/modules/admin-booking/features/cancel-recurring-group/cancel-recurring-group.handler';
import { createAdminBookingHandler } from '@/modules/admin-booking/features/create-admin-booking/create-admin-booking.handler';
import { createAdminRecurringBookingHandler } from '@/modules/admin-booking/features/create-admin-recurring-booking/create-admin-recurring-booking.handler';
import { listAdminBookingsHandler } from '@/modules/admin-booking/features/list-admin-bookings/list-admin-bookings.handler';
import { listRecurringGroupsHandler } from '@/modules/admin-booking/features/list-recurring-groups/list-recurring-groups.handler';
import { rescheduleAdminBookingHandler } from '@/modules/admin-booking/features/reschedule-admin-booking/reschedule-admin-booking.handler';
import { updateAdminBookingStatusHandler } from '@/modules/admin-booking/features/update-admin-booking-status/update-admin-booking-status.handler';
import { updateRecurringGroupHandler } from '@/modules/admin-booking/features/update-recurring-group/update-recurring-group.handler';
import type { AppEnv } from '@/types/hono';
import { zValidator } from '@hono/zod-validator';
import {
createAdminBookingSchema,
createRecurringBookingSchema,
listAdminBookingsQuerySchema,
rescheduleAdminBookingSchema,
updateAdminBookingStatusSchema,
updateRecurringGroupSchema,
} from '@repo/api-contract';
import { Hono } from 'hono';
import { z } from 'zod';
@@ -15,6 +23,7 @@ import { z } from 'zod';
export const adminBookingRoutes = new Hono<AppEnv>();
const complexIdParamsSchema = z.object({ complexId: z.uuid() });
const bookingIdParamsSchema = z.object({ id: z.uuid() });
const groupIdParamsSchema = z.object({ groupId: z.uuid() });
adminBookingRoutes.use('*', requireAuth);
@@ -32,9 +41,42 @@ adminBookingRoutes.post(
createAdminBookingHandler
);
adminBookingRoutes.post(
'/complex/:complexId/recurring',
zValidator('param', complexIdParamsSchema),
zValidator('json', createRecurringBookingSchema),
createAdminRecurringBookingHandler
);
adminBookingRoutes.post(
'/recurring/:groupId/cancel',
zValidator('param', groupIdParamsSchema),
cancelRecurringGroupHandler
);
adminBookingRoutes.get(
'/complex/:complexId/recurring',
zValidator('param', complexIdParamsSchema),
listRecurringGroupsHandler
);
adminBookingRoutes.patch(
'/recurring/:groupId',
zValidator('param', groupIdParamsSchema),
zValidator('json', updateRecurringGroupSchema),
updateRecurringGroupHandler
);
adminBookingRoutes.patch(
'/:id/status',
zValidator('param', bookingIdParamsSchema),
zValidator('json', updateAdminBookingStatusSchema),
updateAdminBookingStatusHandler
);
adminBookingRoutes.patch(
'/:id/reschedule',
zValidator('param', bookingIdParamsSchema),
zValidator('json', rescheduleAdminBookingSchema),
rescheduleAdminBookingHandler
);

View File

@@ -0,0 +1,75 @@
import { Errors } from '@/lib/errors';
import { db } from '@/lib/prisma';
import type { Result } from '@/lib/result';
import { err, ok } from '@/lib/result';
import { v7 as uuidv7 } from 'uuid';
export async function cancelRecurringGroup(
userId: string,
groupId: string
): Promise<Result<{ ok: boolean }>> {
const group = await db.recurringBookingGroup.findUnique({
where: { id: groupId },
include: {
complex: {
select: {
id: true,
users: { where: { userId }, select: { userId: true } },
},
},
},
});
if (!group) {
return err(Errors.notFound('Grupo de reservas no encontrado.'));
}
if (group.complex.users.length === 0) {
return err(Errors.forbidden('No tienes permisos para administrar este complejo.'));
}
if (group.status === 'CANCELLED') {
return err(Errors.conflict('El grupo ya fue cancelado anteriormente.'));
}
const now = new Date();
const todayStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
await db.$transaction(async (tx) => {
await tx.recurringBookingGroup.update({
where: { id: groupId },
data: { status: 'CANCELLED' },
});
const futureBookings = await tx.courtBooking.findMany({
where: {
recurringGroupId: groupId,
bookingDate: { gte: todayStart },
status: 'CONFIRMED',
},
});
for (const booking of futureBookings) {
await tx.courtBookingLog.create({
data: {
id: uuidv7(),
bookingCode: booking.bookingCode,
courtId: booking.courtId,
bookingDate: booking.bookingDate,
startTime: booking.startTime,
endTime: booking.endTime,
customerName: booking.customerName,
customerPhone: booking.customerPhone,
customerEmail: booking.customerEmail,
previousStatus: booking.status,
newStatus: 'CANCELLED',
changedAt: new Date(),
},
});
await tx.courtBooking.delete({ where: { id: booking.id } });
}
});
return ok({ ok: true });
}

View File

@@ -0,0 +1,12 @@
import { handleResult } from '@/lib/http/handle-result';
import type { AppContext } from '@/types/hono';
import { cancelRecurringGroup } from './cancel-recurring-group.business';
type GroupIdParams = { groupId: string };
export async function cancelRecurringGroupHandler(c: AppContext) {
const { groupId } = c.req.valid('param' as never) as GroupIdParams;
const user = c.get('user');
return handleResult(c, await cancelRecurringGroup(user.id, groupId));
}

View File

@@ -0,0 +1,187 @@
import { randomInt } from 'node:crypto';
import { Errors } from '@/lib/errors';
import { db } from '@/lib/prisma';
import type { Result } from '@/lib/result';
import { err, ok } from '@/lib/result';
import { isSlotInPast } from '@/lib/slot-validator';
import { evaluatePlanUsage, parsePlanRules } from '@/modules/plan/services/plan-rules.service';
import type { AdminBooking, CreateAdminBookingInput } from '@repo/api-contract';
import { v7 as uuidv7 } from 'uuid';
import { AdminBookingServiceError } from '../../shared/errors';
import {
buildSlots,
ensureComplexAccess,
getDayOfWeek,
mapBookingResponse,
minutesToTime,
parseIsoDate,
resolvePrice,
toMinutes,
} from '../../shared/helpers';
const BOOKING_CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
const BOOKING_CODE_LENGTH = 6;
function generateBookingCode(): string {
let code = '';
for (let index = 0; index < BOOKING_CODE_LENGTH; index += 1) {
code += BOOKING_CODE_ALPHABET[randomInt(0, BOOKING_CODE_ALPHABET.length)];
}
return code;
}
export async function createAdminBooking(
userId: string,
complexId: string,
input: CreateAdminBookingInput
): Promise<Result<AdminBooking>> {
const complexResult = await ensureComplexAccess(complexId, userId);
if (!complexResult.ok) return err(complexResult.error);
const complex = complexResult.value;
const adminUser = await db.user.findUnique({
where: { id: userId },
select: { emailVerified: true },
});
if (!adminUser?.emailVerified) {
return err(Errors.forbidden('Debés verificar tu email para poder crear reservas.'));
}
const dateResult = parseIsoDate(input.date);
if (!dateResult.ok) return err(dateResult.error);
const bookingDate = dateResult.value;
const dayOfWeekResult = getDayOfWeek(bookingDate);
if (!dayOfWeekResult.ok) return err(dayOfWeekResult.error);
const dayOfWeek = dayOfWeekResult.value;
const court = await db.court.findFirst({
where: { id: input.courtId, complexId },
include: {
availabilities: {
where: { dayOfWeek },
orderBy: { startTime: 'asc' },
},
sport: { select: { id: true, name: true, slug: true } },
priceRules: {
where: { isActive: true },
orderBy: [{ dayOfWeek: 'asc' }, { startTime: 'asc' }],
},
},
});
if (!court) {
return err(Errors.notFound('La cancha seleccionada no existe en el complejo.'));
}
const validSlots = buildSlots(court.availabilities, court.slotDurationMinutes);
const selectedStartMinutes = toMinutes(input.startTime);
const selectedEndMinutes = selectedStartMinutes + court.slotDurationMinutes;
const selectedSlot = { startTime: input.startTime, endTime: minutesToTime(selectedEndMinutes) };
const slotExists = validSlots.some(
(slot) => slot.startTime === selectedSlot.startTime && slot.endTime === selectedSlot.endTime
);
if (!slotExists) {
return err(Errors.conflict('El horario seleccionado no esta disponible para esa cancha.'));
}
if (isSlotInPast(bookingDate, input.startTime, court.slotDurationMinutes)) {
return err(Errors.validation('No se pueden crear reservas en el pasado.'));
}
for (let attempt = 0; attempt < 5; attempt += 1) {
try {
const booking = await db.$transaction(async (tx) => {
if (complex.plan) {
const rules = parsePlanRules(complex.plan.rules);
const bookingsForDate = await tx.courtBooking.count({
where: {
bookingDate,
status: 'CONFIRMED',
court: { complexId },
},
});
const courtsCount = await tx.court.count({ where: { complexId } });
const violations = evaluatePlanUsage(rules, {
courtsCount,
bookingsToday: bookingsForDate,
});
const maxBookingsViolation = violations.find(
(v) => v.code === 'MAX_BOOKINGS_PER_DAY_REACHED'
);
if (maxBookingsViolation) {
throw new AdminBookingServiceError(maxBookingsViolation.message);
}
}
const overlappingBooking = await tx.courtBooking.findFirst({
where: {
courtId: court.id,
bookingDate,
status: 'CONFIRMED',
startTime: { lt: selectedSlot.endTime },
endTime: { gt: selectedSlot.startTime },
},
});
if (overlappingBooking) {
throw new AdminBookingServiceError('El horario seleccionado ya fue reservado.');
}
return tx.courtBooking.create({
data: {
id: uuidv7(),
bookingCode: generateBookingCode(),
courtId: court.id,
bookingDate,
startTime: selectedSlot.startTime,
endTime: selectedSlot.endTime,
customerName: input.customerName.trim(),
customerPhone: input.customerPhone.trim(),
customerEmail: input.customerEmail?.trim() ?? '',
status: 'CONFIRMED',
recurringGroupId: null,
},
include: {
court: {
select: {
id: true,
name: true,
sport: { select: { id: true, name: true, slug: true } },
complex: { select: { id: true, complexName: true } },
},
},
},
});
});
const price = resolvePrice(court, dayOfWeek, selectedSlot.startTime, selectedSlot.endTime);
return ok(mapBookingResponse({ ...booking, price }));
} catch (error) {
if (error instanceof AdminBookingServiceError) {
return err(Errors.conflict(error.message));
}
const prismaError = error as { code?: string; meta?: { target?: string[] | string } };
if (prismaError.code === 'P2002') {
const targets = Array.isArray(prismaError.meta?.target)
? prismaError.meta?.target
: [prismaError.meta?.target];
const isBookingCodeCollision = targets.some((target) =>
String(target).includes('booking_code')
);
if (isBookingCodeCollision) continue;
return err(Errors.conflict('El horario seleccionado ya fue reservado.'));
}
throw error;
}
}
return err(Errors.conflict('No se pudo generar un codigo de reserva unico. Intenta nuevamente.'));
}

View File

@@ -1,10 +1,9 @@
import {
AdminBookingServiceError,
createAdminBooking,
} from '@/modules/admin-booking/services/admin-booking.service';
import { handleResult } from '@/lib/http/handle-result';
import { db } from '@/lib/prisma';
import { sendBookingConfirmation } from '@/services/booking-email.service';
import type { AppContext } from '@/types/hono';
import type { CreateAdminBookingInput } from '@repo/api-contract';
import { createAdminBooking } from './create-admin-booking.business';
type ComplexIdParams = { complexId: string };
@@ -13,11 +12,19 @@ export async function createAdminBookingHandler(c: AppContext) {
const payload = c.req.valid('json' as never) as CreateAdminBookingInput;
const user = c.get('user');
try {
const booking = await createAdminBooking(user.id, complexId, payload);
const result = await createAdminBooking(user.id, complexId, payload);
if (result.ok) {
const booking = result.value;
const complex = await db.complex.findUnique({
where: { id: complexId },
select: { complexSlug: true },
});
void sendBookingConfirmation({
bookingCode: booking.bookingCode,
complexSlug: complex?.complexSlug ?? '',
complexName: booking.complexName,
date: booking.date,
startTime: booking.startTime,
@@ -28,12 +35,7 @@ export async function createAdminBookingHandler(c: AppContext) {
customerEmail: booking.customerEmail,
price: booking.price,
});
return c.json(booking, 201);
} catch (error) {
if (error instanceof AdminBookingServiceError) {
return c.json({ message: error.message }, error.status);
}
throw error;
}
return handleResult(c, result, 201);
}

View File

@@ -0,0 +1,329 @@
import { randomInt } from 'node:crypto';
import { DayOfWeek as DayOfWeekEnum } from '@/generated/prisma/enums';
import { Errors } from '@/lib/errors';
import { db } from '@/lib/prisma';
import type { Result } from '@/lib/result';
import { err, ok } from '@/lib/result';
import { isSlotInPast } from '@/lib/slot-validator';
import {
evaluatePlanUsage,
isFeatureEnabled,
parsePlanRules,
} from '@/modules/plan/services/plan-rules.service';
import type { CreateRecurringBookingInput, RecurringBookingGroup } from '@repo/api-contract';
import { v7 as uuidv7 } from 'uuid';
import { AdminBookingServiceError } from '../../shared/errors';
import {
buildSlots,
ensureComplexAccess,
formatIsoDate,
minutesToTime,
parseIsoDate,
toMinutes,
} from '../../shared/helpers';
const BOOKING_CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
const BOOKING_CODE_LENGTH = 6;
const MAX_RECURRING_WEEKS = 52;
const DAY_OF_WEEK_BY_INDEX = [
DayOfWeekEnum.SUNDAY,
DayOfWeekEnum.MONDAY,
DayOfWeekEnum.TUESDAY,
DayOfWeekEnum.WEDNESDAY,
DayOfWeekEnum.THURSDAY,
DayOfWeekEnum.FRIDAY,
DayOfWeekEnum.SATURDAY,
] as const;
type DayOfWeek = (typeof DAY_OF_WEEK_BY_INDEX)[number];
function generateBookingCode(): string {
let code = '';
for (let index = 0; index < BOOKING_CODE_LENGTH; index += 1) {
code += BOOKING_CODE_ALPHABET[randomInt(0, BOOKING_CODE_ALPHABET.length)];
}
return code;
}
function* generateRecurringDates(
startDate: Date,
endDate: Date | null,
dayOfWeek: number
): Generator<Date> {
const current = new Date(startDate);
current.setUTCDate(current.getUTCDate() + ((dayOfWeek - current.getUTCDay() + 7) % 7));
if (current < startDate) {
current.setUTCDate(current.getUTCDate() + 7);
}
const maxDate = endDate ?? new Date(startDate);
if (!endDate) {
maxDate.setUTCDate(maxDate.getUTCDate() + MAX_RECURRING_WEEKS * 7);
}
while (current <= maxDate) {
yield new Date(current);
current.setUTCDate(current.getUTCDate() + 7);
}
}
function getDayOfWeekValue(date: Date): Result<DayOfWeek> {
const dayOfWeek = DAY_OF_WEEK_BY_INDEX[date.getUTCDay()];
if (!dayOfWeek) {
return err(Errors.validation('No se pudo resolver el dia de la semana.'));
}
return ok(dayOfWeek);
}
export async function createAdminRecurringBooking(
userId: string,
complexId: string,
input: CreateRecurringBookingInput
): Promise<Result<RecurringBookingGroup>> {
const complexResult = await ensureComplexAccess(complexId, userId);
if (!complexResult.ok) return err(complexResult.error);
const complex = complexResult.value;
const adminUser = await db.user.findUnique({
where: { id: userId },
select: { emailVerified: true },
});
if (!adminUser?.emailVerified) {
return err(Errors.forbidden('Debés verificar tu email para poder crear reservas.'));
}
if (!complex.plan) {
return err(Errors.forbidden('El complejo no tiene un plan asignado.'));
}
const rules = parsePlanRules(complex.plan.rules);
if (!isFeatureEnabled(rules, 'fixedSlots')) {
return err(
Errors.forbidden(
'Tu plan no permite la creación de turnos fijos. Comunicate con el administrador.'
)
);
}
const startDateResult = parseIsoDate(input.date);
if (!startDateResult.ok) return err(startDateResult.error);
const startDate = startDateResult.value;
const dayOfWeekResult = getDayOfWeekValue(startDate);
if (!dayOfWeekResult.ok) return err(dayOfWeekResult.error);
const dayOfWeek = dayOfWeekResult.value;
const endDateResult = input.recurringEndDate ? parseIsoDate(input.recurringEndDate) : null;
if (endDateResult && !endDateResult.ok) return err(endDateResult.error);
const endDate = endDateResult?.value ?? null;
if (endDate && endDate <= startDate) {
return err(Errors.validation('La fecha de fin debe ser posterior a la fecha de inicio.'));
}
const court = await db.court.findFirst({
where: { id: input.courtId, complexId },
include: {
availabilities: {
where: { dayOfWeek },
orderBy: { startTime: 'asc' },
},
sport: { select: { id: true, name: true, slug: true } },
priceRules: {
where: { isActive: true },
orderBy: [{ dayOfWeek: 'asc' }, { startTime: 'asc' }],
},
},
});
if (!court) {
return err(Errors.notFound('La cancha seleccionada no existe en el complejo.'));
}
const validSlots = buildSlots(court.availabilities, court.slotDurationMinutes);
const selectedEndMinutes = toMinutes(input.startTime) + court.slotDurationMinutes;
const selectedSlot = { startTime: input.startTime, endTime: minutesToTime(selectedEndMinutes) };
const slotExists = validSlots.some(
(slot) => slot.startTime === selectedSlot.startTime && slot.endTime === selectedSlot.endTime
);
if (!slotExists) {
return err(Errors.conflict('El horario seleccionado no esta disponible para esa cancha.'));
}
if (isSlotInPast(startDate, input.startTime, court.slotDurationMinutes)) {
return err(Errors.validation('La fecha de inicio no puede estar en el pasado.'));
}
const recurringDates = Array.from(
generateRecurringDates(startDate, endDate, startDate.getUTCDay())
);
if (recurringDates.length === 0) {
return err(
Errors.validation(
'No se generaron fechas para la reserva periódica. Verifica las fechas ingresadas.'
)
);
}
for (let attempt = 0; attempt < 5; attempt += 1) {
try {
const result = await db.$transaction(async (tx) => {
const groupId = uuidv7();
const bookingsForDate = await tx.courtBooking.count({
where: {
startTime: selectedSlot.startTime,
bookingDate: startDate,
status: 'CONFIRMED',
court: { complexId },
},
});
const courtsCount = await tx.court.count({ where: { complexId } });
const violations = evaluatePlanUsage(rules, {
courtsCount,
bookingsToday: bookingsForDate,
});
const maxBookingsViolation = violations.find(
(v) => v.code === 'MAX_BOOKINGS_PER_DAY_REACHED'
);
if (maxBookingsViolation) {
throw new AdminBookingServiceError(maxBookingsViolation.message);
}
const group = await tx.recurringBookingGroup.create({
data: {
id: groupId,
complexId,
courtId: court.id,
startTime: selectedSlot.startTime,
endTime: selectedSlot.endTime,
dayOfWeek,
startDate,
endDate,
status: 'ACTIVE',
customerName: input.customerName.trim(),
customerPhone: input.customerPhone.trim(),
customerEmail: input.customerEmail?.trim() ?? '',
},
});
const createdBookings = [];
for (const date of recurringDates) {
if (isSlotInPast(date, input.startTime, court.slotDurationMinutes)) continue;
const overlappingBooking = await tx.courtBooking.findFirst({
where: {
courtId: court.id,
bookingDate: date,
status: 'CONFIRMED',
startTime: { lt: selectedSlot.endTime },
endTime: { gt: selectedSlot.startTime },
},
});
if (overlappingBooking) {
throw new AdminBookingServiceError(
`El horario seleccionado ya fue reservado para el dia ${formatIsoDate(date)}.`
);
}
const booking = await tx.courtBooking.create({
data: {
id: uuidv7(),
bookingCode: generateBookingCode(),
courtId: court.id,
bookingDate: date,
startTime: selectedSlot.startTime,
endTime: selectedSlot.endTime,
customerName: input.customerName.trim(),
customerPhone: input.customerPhone.trim(),
customerEmail: input.customerEmail?.trim() ?? '',
status: 'CONFIRMED',
recurringGroupId: groupId,
},
include: {
court: {
select: {
id: true,
name: true,
sport: { select: { id: true, name: true, slug: true } },
complex: { select: { id: true, complexName: true } },
},
},
},
});
createdBookings.push(booking);
}
return { group, bookings: createdBookings };
});
return ok({
id: result.group.id,
complexId,
courtId: court.id,
startTime: result.group.startTime,
endTime: result.group.endTime,
dayOfWeek: result.group.dayOfWeek,
startDate: formatIsoDate(result.group.startDate),
endDate: result.group.endDate ? formatIsoDate(result.group.endDate) : null,
status: 'ACTIVE' as const,
customerName: result.group.customerName,
customerPhone: result.group.customerPhone,
customerEmail: result.group.customerEmail,
bookings: result.bookings.map((b) => ({
id: b.id,
bookingCode: b.bookingCode,
complexId: b.court.complex.id,
complexName: b.court.complex.complexName,
courtId: b.court.id,
courtName: b.court.name,
sport: { id: b.court.sport.id, name: b.court.sport.name, slug: b.court.sport.slug },
date: formatIsoDate(b.bookingDate),
startTime: b.startTime,
endTime: b.endTime,
customerName: b.customerName,
customerPhone: b.customerPhone,
customerEmail: b.customerEmail,
price: 0,
status: b.status as 'CONFIRMED',
recurringGroupId: result.group.id,
createdAt: b.createdAt.toISOString(),
updatedAt: b.updatedAt.toISOString(),
})),
createdAt: result.group.createdAt.toISOString(),
updatedAt: result.group.updatedAt.toISOString(),
});
} catch (error) {
if (error instanceof AdminBookingServiceError) {
return err(Errors.conflict(error.message));
}
const prismaError = error as { code?: string; meta?: { target?: string[] | string } };
if (prismaError.code === 'P2002') {
const targets = Array.isArray(prismaError.meta?.target)
? prismaError.meta?.target
: [prismaError.meta?.target];
const isBookingCodeCollision = targets.some((target) =>
String(target).includes('booking_code')
);
if (isBookingCodeCollision) continue;
return err(Errors.conflict('El horario seleccionado ya fue reservado.'));
}
throw error;
}
}
return err(Errors.conflict('No se pudo generar un codigo de reserva unico. Intenta nuevamente.'));
}

View File

@@ -0,0 +1,37 @@
import { handleResult } from '@/lib/http/handle-result';
import { sendBookingConfirmation } from '@/services/booking-email.service';
import type { AppContext } from '@/types/hono';
import type { CreateRecurringBookingInput } from '@repo/api-contract';
import { createAdminRecurringBooking } from './create-admin-recurring-booking.business';
type ComplexIdParams = { complexId: string };
export async function createAdminRecurringBookingHandler(c: AppContext) {
const { complexId } = c.req.valid('param' as never) as ComplexIdParams;
const payload = c.req.valid('json' as never) as CreateRecurringBookingInput;
const user = c.get('user');
const result = await createAdminRecurringBooking(user.id, complexId, payload);
if (result.ok) {
const group = result.value;
const firstBooking = group.bookings[0];
if (firstBooking?.customerEmail) {
void sendBookingConfirmation({
bookingCode: firstBooking.bookingCode,
complexName: firstBooking.complexName,
date: firstBooking.date,
startTime: firstBooking.startTime,
endTime: firstBooking.endTime,
courtName: firstBooking.courtName,
sportName: firstBooking.sport.name,
customerName: firstBooking.customerName,
customerEmail: firstBooking.customerEmail,
price: firstBooking.price,
});
}
}
return handleResult(c, result, 201);
}

View File

@@ -0,0 +1,38 @@
import { db } from '@/lib/prisma';
import type { Result } from '@/lib/result';
import { err, ok } from '@/lib/result';
import type { ListAdminBookingsQuery } from '@repo/api-contract';
import { ensureComplexAccess, mapBookingResponse, parseIsoDate } from '../../shared/helpers';
export async function listAdminBookings(
userId: string,
complexId: string,
query: ListAdminBookingsQuery
): Promise<Result<{ bookings: unknown[] }>> {
const accessResult = await ensureComplexAccess(complexId, userId);
if (!accessResult.ok) return err(accessResult.error);
const dateResult = parseIsoDate(query.fromDate);
if (!dateResult.ok) return err(dateResult.error);
const fromDate = dateResult.value;
const bookings = await db.courtBooking.findMany({
where: {
bookingDate: { gte: fromDate },
court: { complexId },
},
include: {
court: {
select: {
id: true,
name: true,
sport: { select: { id: true, name: true, slug: true } },
complex: { select: { id: true, complexName: true } },
},
},
},
orderBy: [{ bookingDate: 'asc' }, { startTime: 'asc' }, { createdAt: 'asc' }],
});
return ok({ bookings: bookings.map((b) => mapBookingResponse(b)) });
}

View File

@@ -1,9 +1,7 @@
import {
AdminBookingServiceError,
listAdminBookings,
} from '@/modules/admin-booking/services/admin-booking.service';
import { handleResult } from '@/lib/http/handle-result';
import type { AppContext } from '@/types/hono';
import type { ListAdminBookingsQuery } from '@repo/api-contract';
import { listAdminBookings } from './list-admin-bookings.business';
type ComplexIdParams = { complexId: string };
@@ -12,13 +10,5 @@ export async function listAdminBookingsHandler(c: AppContext) {
const query = c.req.valid('query' as never) as ListAdminBookingsQuery;
const user = c.get('user');
try {
const response = await listAdminBookings(user.id, complexId, query);
return c.json(response);
} catch (error) {
if (error instanceof AdminBookingServiceError) {
return c.json({ message: error.message }, error.status);
}
throw error;
}
return handleResult(c, await listAdminBookings(user.id, complexId, query));
}

View File

@@ -0,0 +1,112 @@
import { db } from '@/lib/prisma';
import type { Result } from '@/lib/result';
import { ok } from '@/lib/result';
import { formatIsoDate } from '../../shared/helpers';
export async function listRecurringGroups(complexId: string): Promise<
Result<
Array<{
id: string;
complexId: string;
courtId: string;
startTime: string;
endTime: string;
dayOfWeek: string;
startDate: string;
endDate: string | null;
status: string;
customerName: string;
customerPhone: string;
customerEmail: string;
bookings: Array<{
id: string;
bookingCode: string;
complexId: string;
complexName: string;
courtId: string;
courtName: string;
sport: { id: string; name: string; slug: string };
date: string;
startTime: string;
endTime: string;
customerName: string;
customerPhone: string;
customerEmail: string;
price: number;
status: string;
recurringGroupId: string | null;
createdAt: string;
updatedAt: string;
}>;
createdAt: string;
updatedAt: string;
}>
>
> {
const groups = await db.recurringBookingGroup.findMany({
where: { complexId, status: 'ACTIVE' },
include: {
court: {
select: {
id: true,
name: true,
sport: { select: { id: true, name: true, slug: true } },
complex: { select: { id: true, complexName: true } },
},
},
bookings: {
orderBy: { bookingDate: 'asc' },
include: {
court: {
select: {
id: true,
name: true,
sport: { select: { id: true, name: true, slug: true } },
complex: { select: { id: true, complexName: true } },
},
},
},
},
},
orderBy: { createdAt: 'desc' },
});
return ok(
groups.map((group) => ({
id: group.id,
complexId: group.complexId,
courtId: group.courtId,
startTime: group.startTime,
endTime: group.endTime,
dayOfWeek: group.dayOfWeek,
startDate: formatIsoDate(group.startDate),
endDate: group.endDate ? formatIsoDate(group.endDate) : null,
status: group.status,
customerName: group.customerName,
customerPhone: group.customerPhone,
customerEmail: group.customerEmail,
bookings: group.bookings.map((b) => ({
id: b.id,
bookingCode: b.bookingCode,
complexId: b.court.complex.id,
complexName: b.court.complex.complexName,
courtId: b.court.id,
courtName: b.court.name,
sport: { id: b.court.sport.id, name: b.court.sport.name, slug: b.court.sport.slug },
date: formatIsoDate(b.bookingDate),
startTime: b.startTime,
endTime: b.endTime,
customerName: b.customerName,
customerPhone: b.customerPhone,
customerEmail: b.customerEmail,
price: 0,
status: b.status,
recurringGroupId: b.recurringGroupId,
createdAt: b.createdAt.toISOString(),
updatedAt: b.updatedAt.toISOString(),
})),
createdAt: group.createdAt.toISOString(),
updatedAt: group.updatedAt.toISOString(),
}))
);
}

View File

@@ -0,0 +1,18 @@
import { handleResult } from '@/lib/http/handle-result';
import type { Result } from '@/lib/result';
import type { AppContext } from '@/types/hono';
import { listRecurringGroups } from './list-recurring-groups.business';
type ComplexIdParams = { complexId: string };
export async function listRecurringGroupsHandler(c: AppContext) {
const { complexId } = c.req.valid('param' as never) as ComplexIdParams;
const result = await listRecurringGroups(complexId);
return handleResult(
c,
result.ok
? ({ ok: true as const, value: { groups: result.value } } as Result<{ groups: unknown }>)
: result
);
}

View File

@@ -0,0 +1,178 @@
import { Errors } from '@/lib/errors';
import { db } from '@/lib/prisma';
import type { Result } from '@/lib/result';
import { err, ok } from '@/lib/result';
import { isSlotInPast } from '@/lib/slot-validator';
import type { AdminBooking, RescheduleAdminBookingInput } from '@repo/api-contract';
import { v7 as uuidv7 } from 'uuid';
import {
buildSlots,
getDayOfWeek,
mapBookingResponse,
minutesToTime,
resolvePrice,
toMinutes,
} from '../../shared/helpers';
export async function rescheduleAdminBooking(
userId: string,
bookingId: string,
input: RescheduleAdminBookingInput
): Promise<Result<AdminBooking>> {
const booking = await db.courtBooking.findFirst({
where: {
id: bookingId,
court: {
complex: { users: { some: { userId } } },
},
},
include: {
court: {
select: {
id: true,
name: true,
complexId: true,
slotDurationMinutes: true,
sport: { select: { id: true, name: true, slug: true } },
complex: { select: { id: true, complexName: true } },
},
},
},
});
if (!booking) {
return err(Errors.notFound('Reserva no encontrada.'));
}
if (booking.status !== 'CONFIRMED') {
return err(Errors.conflict('Solo se pueden reprogramar reservas en estado confirmada.'));
}
const targetCourtId = input.courtId ?? booking.courtId;
const targetStartTime = input.startTime ?? booking.startTime;
if (targetCourtId === booking.courtId && targetStartTime === booking.startTime) {
return err(Errors.validation('Debe proporcionar al menos una cancha o un horario diferente.'));
}
const dayOfWeekResult = getDayOfWeek(booking.bookingDate);
if (!dayOfWeekResult.ok) return err(dayOfWeekResult.error);
const dayOfWeek = dayOfWeekResult.value;
const targetCourt = await db.court.findFirst({
where: { id: targetCourtId, complexId: booking.court.complexId },
include: {
availabilities: {
where: { dayOfWeek },
orderBy: { startTime: 'asc' },
},
priceRules: {
where: { isActive: true },
orderBy: [{ dayOfWeek: 'asc' }, { startTime: 'asc' }],
},
sport: { select: { id: true, name: true, slug: true } },
},
});
if (!targetCourt) {
return err(Errors.notFound('La cancha seleccionada no existe en el complejo.'));
}
if (targetCourt.sport.id !== booking.court.sport.id) {
return err(
Errors.conflict('La cancha seleccionada no es del mismo deporte que la reserva original.')
);
}
if (targetCourt.isUnderMaintenance) {
return err(Errors.conflict('La cancha seleccionada se encuentra en mantenimiento.'));
}
const validSlots = buildSlots(targetCourt.availabilities, targetCourt.slotDurationMinutes);
const selectedEndMinutes = toMinutes(targetStartTime) + targetCourt.slotDurationMinutes;
const selectedSlot = { startTime: targetStartTime, endTime: minutesToTime(selectedEndMinutes) };
const slotExists = validSlots.some(
(slot) => slot.startTime === selectedSlot.startTime && slot.endTime === selectedSlot.endTime
);
if (!slotExists) {
return err(Errors.conflict('El horario seleccionado no está disponible para esa cancha.'));
}
if (isSlotInPast(booking.bookingDate, targetStartTime, targetCourt.slotDurationMinutes)) {
return err(Errors.validation('No se pueden reprogramar reservas en el pasado.'));
}
const overlappingBooking = await db.courtBooking.findFirst({
where: {
id: { not: bookingId },
courtId: targetCourtId,
bookingDate: booking.bookingDate,
status: 'CONFIRMED',
startTime: { lt: selectedSlot.endTime },
endTime: { gt: selectedSlot.startTime },
},
});
if (overlappingBooking) {
return err(Errors.conflict('El horario seleccionado ya fue reservado por otra reserva.'));
}
const newPrice = resolvePrice(
targetCourt,
dayOfWeek,
selectedSlot.startTime,
selectedSlot.endTime
);
await db.$transaction(async (tx) => {
await tx.courtBookingLog.create({
data: {
id: uuidv7(),
bookingCode: booking.bookingCode,
courtId: booking.court.id,
bookingDate: booking.bookingDate,
startTime: booking.startTime,
endTime: booking.endTime,
customerName: booking.customerName,
customerPhone: booking.customerPhone,
customerEmail: booking.customerEmail,
previousStatus: booking.status,
newStatus: booking.status,
previousCourtId: booking.court.id,
previousStartTime: booking.startTime,
previousEndTime: booking.endTime,
changedAt: new Date(),
},
});
await tx.courtBooking.update({
where: { id: booking.id },
data: {
courtId: targetCourtId,
startTime: selectedSlot.startTime,
endTime: selectedSlot.endTime,
},
});
});
const updatedBooking = {
...booking,
startTime: selectedSlot.startTime,
endTime: selectedSlot.endTime,
price: newPrice,
};
if (targetCourtId !== booking.courtId) {
updatedBooking.court = {
id: targetCourt.id,
name: targetCourt.name,
slotDurationMinutes: targetCourt.slotDurationMinutes,
sport: targetCourt.sport,
complex: booking.court.complex,
} as typeof booking.court;
}
return ok(mapBookingResponse(updatedBooking));
}

View File

@@ -0,0 +1,55 @@
import { handleResult } from '@/lib/http/handle-result';
import { db } from '@/lib/prisma';
import { sseManager } from '@/lib/sse';
import { sendBookingRescheduled } from '@/services/booking-email.service';
import type { AppContext } from '@/types/hono';
import type { RescheduleAdminBookingInput } from '@repo/api-contract';
import { rescheduleAdminBooking } from './reschedule-admin-booking.business';
type BookingIdParams = { id: string };
export async function rescheduleAdminBookingHandler(c: AppContext) {
const { id } = c.req.valid('param' as never) as BookingIdParams;
const payload = c.req.valid('json' as never) as RescheduleAdminBookingInput;
const user = c.get('user');
const previous = await db.courtBooking.findUnique({
where: { id },
select: {
court: { select: { name: true } },
startTime: true,
endTime: true,
customerEmail: true,
},
});
const result = await rescheduleAdminBooking(user.id, id, payload);
if (result.ok) {
const booking = result.value;
if (previous) {
void sendBookingRescheduled({
bookingCode: booking.bookingCode,
complexName: booking.complexName,
date: booking.date,
startTime: booking.startTime,
endTime: booking.endTime,
courtName: booking.courtName,
sportName: booking.sport.name,
customerName: booking.customerName,
customerEmail: previous.customerEmail,
previousCourtName: previous.court.name,
previousStartTime: previous.startTime,
previousEndTime: previous.endTime,
});
}
sseManager.emit(
`complex:${booking.complexId}`,
JSON.stringify({ type: 'reschedule', booking })
);
}
return handleResult(c, result);
}

View File

@@ -0,0 +1,76 @@
import { Errors } from '@/lib/errors';
import { db } from '@/lib/prisma';
import type { Result } from '@/lib/result';
import { err, ok } from '@/lib/result';
import type { UpdateAdminBookingStatusInput } from '@repo/api-contract';
import { v7 as uuidv7 } from 'uuid';
import { mapBookingResponse } from '../../shared/helpers';
export async function updateAdminBookingStatus(
userId: string,
bookingId: string,
input: UpdateAdminBookingStatusInput
): Promise<Result<ReturnType<typeof mapBookingResponse>>> {
const booking = await db.courtBooking.findFirst({
where: {
id: bookingId,
court: {
complex: { users: { some: { userId } } },
},
},
include: {
court: {
select: {
id: true,
name: true,
sport: { select: { id: true, name: true, slug: true } },
complex: { select: { id: true, complexName: true } },
},
},
},
});
if (!booking) {
return err(Errors.notFound('Reserva no encontrada.'));
}
if (input.status === 'CANCELLED' && booking.status !== 'CONFIRMED') {
return err(Errors.conflict('Solo se pueden cancelar reservas en estado confirmada.'));
}
if (input.status === 'COMPLETED' && booking.status !== 'CONFIRMED') {
return err(Errors.conflict('Solo se pueden marcar como cumplidas las reservas confirmadas.'));
}
if (input.status === 'NOSHOW' && booking.status !== 'CONFIRMED') {
return err(Errors.conflict('Solo se pueden marcar como no show las reservas confirmadas.'));
}
await db.courtBookingLog.create({
data: {
id: uuidv7(),
bookingCode: booking.bookingCode,
courtId: booking.court.id,
bookingDate: booking.bookingDate,
startTime: booking.startTime,
endTime: booking.endTime,
customerName: booking.customerName,
customerPhone: booking.customerPhone,
customerEmail: booking.customerEmail,
previousStatus: booking.status,
newStatus: input.status,
changedAt: new Date(),
},
});
if (input.status === 'COMPLETED' || input.status === 'NOSHOW') {
await db.courtBooking.update({
where: { id: booking.id },
data: { status: input.status },
});
} else {
await db.courtBooking.delete({ where: { id: booking.id } });
}
return ok(mapBookingResponse({ ...booking, status: input.status }));
}

View File

@@ -1,10 +1,8 @@
import {
AdminBookingServiceError,
updateAdminBookingStatus,
} from '@/modules/admin-booking/services/admin-booking.service';
import { handleResult } from '@/lib/http/handle-result';
import { sendBookingCancelled, sendBookingNoShow } from '@/services/booking-email.service';
import type { AppContext } from '@/types/hono';
import type { UpdateAdminBookingStatusInput } from '@repo/api-contract';
import { updateAdminBookingStatus } from './update-admin-booking-status.business';
type BookingIdParams = { id: string };
@@ -13,8 +11,10 @@ export async function updateAdminBookingStatusHandler(c: AppContext) {
const payload = c.req.valid('json' as never) as UpdateAdminBookingStatusInput;
const user = c.get('user');
try {
const booking = await updateAdminBookingStatus(user.id, id, payload);
const result = await updateAdminBookingStatus(user.id, id, payload);
if (result.ok) {
const booking = result.value;
if (payload.status === 'CANCELLED') {
void sendBookingCancelled({
@@ -41,12 +41,7 @@ export async function updateAdminBookingStatusHandler(c: AppContext) {
customerEmail: booking.customerEmail,
});
}
return c.json(booking);
} catch (error) {
if (error instanceof AdminBookingServiceError) {
return c.json({ message: error.message }, error.status);
}
throw error;
}
return handleResult(c, result);
}

View File

@@ -0,0 +1,318 @@
import { randomInt } from 'node:crypto';
import { Errors } from '@/lib/errors';
import { db } from '@/lib/prisma';
import type { Result } from '@/lib/result';
import { err, ok } from '@/lib/result';
import { isSlotInPast } from '@/lib/slot-validator';
import type { RecurringBookingGroup, UpdateRecurringGroupInput } from '@repo/api-contract';
import { v7 as uuidv7 } from 'uuid';
import { AdminBookingServiceError } from '../../shared/errors';
import { buildSlots, formatIsoDate, minutesToTime, toMinutes } from '../../shared/helpers';
const DAY_INDEX_BY_VALUE: Record<string, number> = {
SUNDAY: 0,
MONDAY: 1,
TUESDAY: 2,
WEDNESDAY: 3,
THURSDAY: 4,
FRIDAY: 5,
SATURDAY: 6,
};
const MAX_RECURRING_WEEKS = 52;
function generateBookingCode(): string {
const alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
let code = '';
for (let i = 0; i < 6; i++) {
code += alphabet[randomInt(0, alphabet.length)];
}
return code;
}
function* generateRecurringDates(
startDate: Date,
endDate: Date | null,
dayOfWeek: number
): Generator<Date> {
const current = new Date(startDate);
current.setUTCDate(current.getUTCDate() + ((dayOfWeek - current.getUTCDay() + 7) % 7));
if (current < startDate) {
current.setUTCDate(current.getUTCDate() + 7);
}
const maxDate = endDate ?? new Date(startDate);
if (!endDate) {
maxDate.setUTCDate(maxDate.getUTCDate() + MAX_RECURRING_WEEKS * 7);
}
while (current <= maxDate) {
yield new Date(current);
current.setUTCDate(current.getUTCDate() + 7);
}
}
export async function updateRecurringGroup(
userId: string,
groupId: string,
input: UpdateRecurringGroupInput
): Promise<Result<RecurringBookingGroup>> {
const group = await db.recurringBookingGroup.findUnique({
where: { id: groupId },
include: {
complex: {
select: {
id: true,
users: { where: { userId }, select: { userId: true } },
},
},
},
});
if (!group) {
return err(Errors.notFound('Grupo de turnos fijos no encontrado.'));
}
if (group.complex.users.length === 0) {
return err(Errors.forbidden('No tienes permisos para administrar este complejo.'));
}
if (group.status === 'CANCELLED') {
return err(Errors.conflict('No se puede editar un grupo cancelado.'));
}
const courtId = input.courtId ?? group.courtId;
const dayOfWeek = input.dayOfWeek ?? group.dayOfWeek;
const startTime = input.startTime ?? group.startTime;
const scheduleChanged =
input.courtId !== undefined || input.dayOfWeek !== undefined || input.startTime !== undefined;
if (scheduleChanged) {
const court = await db.court.findFirst({
where: { id: courtId, complexId: group.complexId },
include: {
availabilities: {
where: { dayOfWeek: dayOfWeek as typeof group.dayOfWeek },
orderBy: { startTime: 'asc' },
},
},
});
if (!court) {
return err(Errors.notFound('La cancha seleccionada no existe en el complejo.'));
}
const validSlots = buildSlots(court.availabilities, court.slotDurationMinutes);
const selectedEndMinutes = toMinutes(startTime) + court.slotDurationMinutes;
const endTime = minutesToTime(selectedEndMinutes);
const slotExists = validSlots.some(
(slot) => slot.startTime === startTime && slot.endTime === endTime
);
if (!slotExists) {
return err(Errors.conflict('El horario seleccionado no esta disponible para esa cancha.'));
}
const now = new Date();
const todayStart = new Date(
Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())
);
try {
await db.$transaction(async (tx) => {
await tx.recurringBookingGroup.update({
where: { id: groupId },
data: {
courtId,
dayOfWeek: dayOfWeek as typeof group.dayOfWeek,
startTime,
endTime,
customerName: input.customerName?.trim() ?? group.customerName,
customerPhone: input.customerPhone?.trim() ?? group.customerPhone,
customerEmail: input.customerEmail?.trim() ?? group.customerEmail,
},
});
const futureBookings = await tx.courtBooking.findMany({
where: {
recurringGroupId: groupId,
bookingDate: { gte: todayStart },
status: 'CONFIRMED',
},
});
for (const booking of futureBookings) {
await tx.courtBookingLog.create({
data: {
id: uuidv7(),
bookingCode: booking.bookingCode,
courtId: booking.courtId,
bookingDate: booking.bookingDate,
startTime: booking.startTime,
endTime: booking.endTime,
customerName: booking.customerName,
customerPhone: booking.customerPhone,
customerEmail: booking.customerEmail,
previousStatus: booking.status,
newStatus: 'CANCELLED',
changedAt: new Date(),
},
});
await tx.courtBooking.delete({ where: { id: booking.id } });
}
const dayIndex = DAY_INDEX_BY_VALUE[dayOfWeek] ?? 0;
const recurringDates = Array.from(
generateRecurringDates(todayStart, group.endDate, dayIndex)
);
for (const date of recurringDates) {
const overlappingBooking = await tx.courtBooking.findFirst({
where: {
courtId,
bookingDate: date,
status: 'CONFIRMED',
startTime: { lt: endTime },
endTime: { gt: startTime },
},
});
if (overlappingBooking) {
throw new AdminBookingServiceError(
`El horario seleccionado ya fue reservado para el dia ${formatIsoDate(date)}.`
);
}
await tx.courtBooking.create({
data: {
id: uuidv7(),
bookingCode: generateBookingCode(),
courtId,
bookingDate: date,
startTime,
endTime,
customerName: input.customerName?.trim() ?? group.customerName,
customerPhone: input.customerPhone?.trim() ?? group.customerPhone,
customerEmail: input.customerEmail?.trim() ?? group.customerEmail,
status: 'CONFIRMED',
recurringGroupId: groupId,
},
include: {
court: {
select: {
id: true,
name: true,
sport: { select: { id: true, name: true, slug: true } },
complex: { select: { id: true, complexName: true } },
},
},
},
});
}
});
} catch (txError) {
if (txError instanceof AdminBookingServiceError) {
return err(Errors.conflict(txError.message));
}
throw txError;
}
} else {
await db.recurringBookingGroup.update({
where: { id: groupId },
data: {
...(input.customerName !== undefined && { customerName: input.customerName.trim() }),
...(input.customerPhone !== undefined && { customerPhone: input.customerPhone.trim() }),
...(input.customerEmail !== undefined && { customerEmail: input.customerEmail.trim() }),
},
});
const now = new Date();
const todayStart = new Date(
Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())
);
const updateData: Record<string, string> = {};
if (input.customerName !== undefined) updateData.customerName = input.customerName.trim();
if (input.customerPhone !== undefined) updateData.customerPhone = input.customerPhone.trim();
if (input.customerEmail !== undefined) updateData.customerEmail = input.customerEmail.trim();
if (Object.keys(updateData).length > 0) {
await db.courtBooking.updateMany({
where: { recurringGroupId: groupId, bookingDate: { gte: todayStart } },
data: updateData,
});
}
}
const updated = await db.recurringBookingGroup.findUnique({
where: { id: groupId },
include: {
court: {
select: {
id: true,
name: true,
sport: { select: { id: true, name: true, slug: true } },
complex: { select: { id: true, complexName: true } },
},
},
bookings: {
orderBy: { bookingDate: 'asc' },
include: {
court: {
select: {
id: true,
name: true,
sport: { select: { id: true, name: true, slug: true } },
complex: { select: { id: true, complexName: true } },
},
},
},
},
},
});
if (!updated) {
return err(Errors.conflict('Error al actualizar el grupo.'));
}
return ok({
id: updated.id,
complexId: updated.complexId,
courtId: updated.courtId,
startTime: updated.startTime,
endTime: updated.endTime,
dayOfWeek: updated.dayOfWeek,
startDate: formatIsoDate(updated.startDate),
endDate: updated.endDate ? formatIsoDate(updated.endDate) : null,
status: updated.status,
customerName: updated.customerName,
customerPhone: updated.customerPhone,
customerEmail: updated.customerEmail,
bookings: updated.bookings.map((b) => ({
id: b.id,
bookingCode: b.bookingCode,
complexId: b.court.complex.id,
complexName: b.court.complex.complexName,
courtId: b.court.id,
courtName: b.court.name,
sport: { id: b.court.sport.id, name: b.court.sport.name, slug: b.court.sport.slug },
date: formatIsoDate(b.bookingDate),
startTime: b.startTime,
endTime: b.endTime,
customerName: b.customerName,
customerPhone: b.customerPhone,
customerEmail: b.customerEmail,
price: 0,
status: b.status,
recurringGroupId: b.recurringGroupId,
createdAt: b.createdAt.toISOString(),
updatedAt: b.updatedAt.toISOString(),
})),
createdAt: updated.createdAt.toISOString(),
updatedAt: updated.updatedAt.toISOString(),
});
}

View File

@@ -0,0 +1,14 @@
import { handleResult } from '@/lib/http/handle-result';
import type { AppContext } from '@/types/hono';
import type { UpdateRecurringGroupInput } from '@repo/api-contract';
import { updateRecurringGroup } from './update-recurring-group.business';
type GroupIdParams = { groupId: string };
export async function updateRecurringGroupHandler(c: AppContext) {
const { groupId } = c.req.valid('param' as never) as GroupIdParams;
const payload = c.req.valid('json' as never) as UpdateRecurringGroupInput;
const user = c.get('user');
return handleResult(c, await updateRecurringGroup(user.id, groupId, payload));
}

View File

@@ -1,592 +0,0 @@
import { randomInt } from 'node:crypto';
import { CourtBookingStatus } from '@/generated/prisma/enums';
import { db } from '@/lib/prisma';
import { evaluatePlanUsage, parsePlanRules } from '@/modules/plan/services/plan-rules.service';
import type { DayOfWeek } from '@repo/api-contract';
import type {
AdminBooking,
CreateAdminBookingInput,
ListAdminBookingsQuery,
UpdateAdminBookingStatusInput,
} from '@repo/api-contract';
import { v7 as uuidv7 } from 'uuid';
type Slot = {
startTime: string;
endTime: string;
};
const DAY_OF_WEEK_BY_INDEX = [
'SUNDAY',
'MONDAY',
'TUESDAY',
'WEDNESDAY',
'THURSDAY',
'FRIDAY',
'SATURDAY',
] as const;
const BOOKING_CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
const BOOKING_CODE_LENGTH = 6;
export class AdminBookingServiceError extends Error {
status: 400 | 403 | 404 | 409;
constructor(message: string, status: 400 | 403 | 404 | 409) {
super(message);
this.name = 'AdminBookingServiceError';
this.status = status;
}
}
function toMinutes(value: string): number {
const [hours, minutes] = value.split(':').map((part) => Number(part));
return hours * 60 + minutes;
}
function minutesToTime(minutes: number): string {
const safeMinutes = Math.max(0, minutes);
const hours = Math.floor(safeMinutes / 60);
const mins = safeMinutes % 60;
return `${String(hours).padStart(2, '0')}:${String(mins).padStart(2, '0')}`;
}
function parseIsoDate(date: string): Date {
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
if (!match) {
throw new AdminBookingServiceError('La fecha debe tener formato YYYY-MM-DD.', 400);
}
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
const bookingDate = new Date(Date.UTC(year, month - 1, day));
if (
Number.isNaN(bookingDate.getTime()) ||
bookingDate.getUTCFullYear() !== year ||
bookingDate.getUTCMonth() + 1 !== month ||
bookingDate.getUTCDate() !== day
) {
throw new AdminBookingServiceError('La fecha enviada no es valida.', 400);
}
return bookingDate;
}
function getDayOfWeek(date: Date) {
const dayOfWeek = DAY_OF_WEEK_BY_INDEX[date.getUTCDay()];
if (!dayOfWeek) {
throw new AdminBookingServiceError('No se pudo resolver el dia de la semana.', 400);
}
return dayOfWeek;
}
function formatIsoDate(date: Date): string {
return date.toISOString().slice(0, 10);
}
function generateBookingCode(): string {
let code = '';
for (let index = 0; index < BOOKING_CODE_LENGTH; index += 1) {
code += BOOKING_CODE_ALPHABET[randomInt(0, BOOKING_CODE_ALPHABET.length)];
}
return code;
}
function buildSlots(
availability: Array<{
startTime: string;
endTime: string;
}>,
slotDurationMinutes: number
): Slot[] {
const slots: Slot[] = [];
for (const range of availability) {
const start = toMinutes(range.startTime);
const end = toMinutes(range.endTime);
for (
let current = start;
current + slotDurationMinutes <= end;
current += slotDurationMinutes
) {
slots.push({
startTime: minutesToTime(current),
endTime: minutesToTime(current + slotDurationMinutes),
});
}
}
return slots;
}
async function ensureComplexAccess(complexId: string, userId: string) {
const complexUser = await db.complexUser.findUnique({
where: {
complexId_userId: {
complexId,
userId,
},
},
include: {
complex: {
select: {
id: true,
complexName: true,
plan: {
select: {
rules: true,
},
},
},
},
},
});
if (!complexUser) {
throw new AdminBookingServiceError('No tienes permisos para administrar este complejo.', 403);
}
return complexUser.complex;
}
function resolvePrice(
court: {
basePrice: unknown;
priceRules: Array<{
dayOfWeek: string | null;
startTime: string | null;
endTime: string | null;
price: unknown;
}>;
},
dayOfWeek: string,
startTime: string,
endTime: string
): number {
const slotStart = toMinutes(startTime);
const slotEnd = toMinutes(endTime);
const matchingRules = court.priceRules
.filter((rule) => {
if (rule.dayOfWeek && rule.dayOfWeek !== dayOfWeek) return false;
if (!rule.startTime || !rule.endTime) return true;
return slotStart >= toMinutes(rule.startTime) && slotEnd <= toMinutes(rule.endTime);
})
.sort((first, second) => {
const firstSpecificity =
(first.dayOfWeek ? 2 : 0) + (first.startTime && first.endTime ? 1 : 0);
const secondSpecificity =
(second.dayOfWeek ? 2 : 0) + (second.startTime && second.endTime ? 1 : 0);
return secondSpecificity - firstSpecificity;
});
return Number(matchingRules[0]?.price ?? court.basePrice);
}
function mapBookingResponse(booking: {
id: string;
bookingCode: string;
bookingDate: Date;
startTime: string;
endTime: string;
customerName: string;
customerPhone: string;
customerEmail: string | null;
status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED' | 'NOSHOW';
createdAt: Date;
updatedAt: Date;
court: {
id: string;
name: string;
complex: {
id: string;
complexName: string;
};
sport: {
id: string;
name: string;
slug: string;
};
};
price?: number;
}): AdminBooking {
return {
id: booking.id,
bookingCode: booking.bookingCode,
complexId: booking.court.complex.id,
complexName: booking.court.complex.complexName,
courtId: booking.court.id,
courtName: booking.court.name,
sport: {
id: booking.court.sport.id,
name: booking.court.sport.name,
slug: booking.court.sport.slug,
},
date: formatIsoDate(booking.bookingDate),
startTime: booking.startTime,
endTime: booking.endTime,
customerName: booking.customerName,
customerPhone: booking.customerPhone,
customerEmail: booking.customerEmail ?? undefined,
price: booking.price ?? 0,
status: booking.status,
createdAt: booking.createdAt.toISOString(),
updatedAt: booking.updatedAt.toISOString(),
};
}
export async function listAdminBookings(
userId: string,
complexId: string,
query: ListAdminBookingsQuery
) {
await ensureComplexAccess(complexId, userId);
const fromDate = parseIsoDate(query.fromDate);
const bookings = await db.courtBooking.findMany({
where: {
bookingDate: {
gte: fromDate,
},
court: {
complexId,
},
},
include: {
court: {
select: {
id: true,
name: true,
sport: {
select: {
id: true,
name: true,
slug: true,
},
},
complex: {
select: {
id: true,
complexName: true,
},
},
},
},
},
orderBy: [{ bookingDate: 'asc' }, { startTime: 'asc' }, { createdAt: 'asc' }],
});
const response = bookings.map((booking) => mapBookingResponse(booking));
return {
bookings: response,
};
}
export async function createAdminBooking(
userId: string,
complexId: string,
input: CreateAdminBookingInput
) {
const complex = await ensureComplexAccess(complexId, userId);
const adminUser = await db.user.findUnique({
where: { id: userId },
select: { emailVerified: true },
});
if (!adminUser?.emailVerified) {
throw new AdminBookingServiceError('Debés verificar tu email para poder crear reservas.', 403);
}
const bookingDate = parseIsoDate(input.date);
const dayOfWeek = getDayOfWeek(bookingDate);
const court = await db.court.findFirst({
where: {
id: input.courtId,
complexId,
},
include: {
availabilities: {
where: {
dayOfWeek,
},
orderBy: {
startTime: 'asc',
},
},
sport: {
select: {
id: true,
name: true,
slug: true,
},
},
priceRules: {
where: { isActive: true },
orderBy: [{ dayOfWeek: 'asc' }, { startTime: 'asc' }],
},
},
});
if (!court) {
throw new AdminBookingServiceError('La cancha seleccionada no existe en el complejo.', 404);
}
const validSlots = buildSlots(court.availabilities, court.slotDurationMinutes);
const selectedStartMinutes = toMinutes(input.startTime);
const selectedEndMinutes = selectedStartMinutes + court.slotDurationMinutes;
const selectedSlot = {
startTime: input.startTime,
endTime: minutesToTime(selectedEndMinutes),
};
const slotExists = validSlots.some(
(slot) => slot.startTime === selectedSlot.startTime && slot.endTime === selectedSlot.endTime
);
if (!slotExists) {
throw new AdminBookingServiceError(
'El horario seleccionado no esta disponible para esa cancha.',
409
);
}
for (let attempt = 0; attempt < 5; attempt += 1) {
try {
const booking = await db.$transaction(async (tx) => {
if (complex.plan) {
const rules = parsePlanRules(complex.plan.rules);
const bookingsForDate = await tx.courtBooking.count({
where: {
bookingDate,
status: 'CONFIRMED',
court: {
complexId,
},
},
});
const courtsCount = await tx.court.count({
where: {
complexId,
},
});
const violations = evaluatePlanUsage(rules, {
courtsCount,
bookingsToday: bookingsForDate,
});
const maxBookingsViolation = violations.find(
(violation) => violation.code === 'MAX_BOOKINGS_PER_DAY_REACHED'
);
if (maxBookingsViolation) {
throw new AdminBookingServiceError(maxBookingsViolation.message, 409);
}
}
const overlappingBooking = await tx.courtBooking.findFirst({
where: {
courtId: court.id,
bookingDate,
status: 'CONFIRMED',
startTime: {
lt: selectedSlot.endTime,
},
endTime: {
gt: selectedSlot.startTime,
},
},
});
if (overlappingBooking) {
throw new AdminBookingServiceError('El horario seleccionado ya fue reservado.', 409);
}
return tx.courtBooking.create({
data: {
id: uuidv7(),
bookingCode: generateBookingCode(),
courtId: court.id,
bookingDate,
startTime: selectedSlot.startTime,
endTime: selectedSlot.endTime,
customerName: input.customerName.trim(),
customerPhone: input.customerPhone.trim(),
customerEmail: input.customerEmail?.trim() || null,
status: 'CONFIRMED',
},
include: {
court: {
select: {
id: true,
name: true,
sport: {
select: {
id: true,
name: true,
slug: true,
},
},
complex: {
select: {
id: true,
complexName: true,
},
},
},
},
},
});
});
const price = resolvePrice(court, dayOfWeek, selectedSlot.startTime, selectedSlot.endTime);
return mapBookingResponse({ ...booking, price });
} catch (error) {
if (error instanceof AdminBookingServiceError) {
throw error;
}
const prismaError = error as {
code?: string;
meta?: {
target?: string[] | string;
};
};
if (prismaError.code === 'P2002') {
const targets = Array.isArray(prismaError.meta?.target)
? prismaError.meta?.target
: [prismaError.meta?.target];
const isBookingCodeCollision = targets.some((target) =>
String(target).includes('booking_code')
);
if (isBookingCodeCollision) {
continue;
}
throw new AdminBookingServiceError('El horario seleccionado ya fue reservado.', 409);
}
throw error;
}
}
throw new AdminBookingServiceError(
'No se pudo generar un codigo de reserva unico. Intenta nuevamente.',
409
);
}
export async function updateAdminBookingStatus(
userId: string,
bookingId: string,
input: UpdateAdminBookingStatusInput
) {
const booking = await db.courtBooking.findFirst({
where: {
id: bookingId,
court: {
complex: {
users: {
some: {
userId,
},
},
},
},
},
include: {
court: {
select: {
id: true,
name: true,
sport: {
select: {
id: true,
name: true,
slug: true,
},
},
complex: {
select: {
id: true,
complexName: true,
},
},
},
},
},
});
if (!booking) {
throw new AdminBookingServiceError('Reserva no encontrada.', 404);
}
if (input.status === 'CANCELLED' && booking.status !== 'CONFIRMED') {
throw new AdminBookingServiceError(
'Solo se pueden cancelar reservas en estado confirmada.',
409
);
}
if (input.status === 'COMPLETED' && booking.status !== 'CONFIRMED') {
throw new AdminBookingServiceError(
'Solo se pueden marcar como cumplidas las reservas confirmadas.',
409
);
}
if (input.status === 'NOSHOW' && booking.status !== 'CONFIRMED') {
throw new AdminBookingServiceError(
'Solo se pueden marcar como no show las reservas confirmadas.',
409
);
}
await db.courtBookingLog.create({
data: {
id: uuidv7(),
bookingCode: booking.bookingCode,
courtId: booking.court.id,
bookingDate: booking.bookingDate,
startTime: booking.startTime,
endTime: booking.endTime,
customerName: booking.customerName,
customerPhone: booking.customerPhone,
customerEmail: booking.customerEmail,
previousStatus: booking.status,
newStatus: input.status,
changedAt: new Date(),
},
});
if (input.status === 'COMPLETED' || input.status === 'NOSHOW') {
await db.courtBooking.update({
where: { id: booking.id },
data: {
status: input.status,
},
});
} else {
// if the booking is cancelled we delete it to free up the slot, but we keep a log of it with the cancelled status
await db.courtBooking.delete({
where: { id: booking.id },
});
}
return mapBookingResponse({ ...booking, status: input.status });
}

View File

@@ -0,0 +1,6 @@
export class AdminBookingServiceError extends Error {
constructor(message: string) {
super(message);
this.name = 'AdminBookingServiceError';
}
}

View File

@@ -0,0 +1,216 @@
import { randomInt } from 'node:crypto';
import { Errors } from '@/lib/errors';
import { db } from '@/lib/prisma';
import type { Result } from '@/lib/result';
import { err, ok } from '@/lib/result';
import type { AdminBooking, DayOfWeek } from '@repo/api-contract';
export type Slot = { startTime: string; endTime: string };
const DAY_OF_WEEK_BY_INDEX = [
'SUNDAY',
'MONDAY',
'TUESDAY',
'WEDNESDAY',
'THURSDAY',
'FRIDAY',
'SATURDAY',
] as const;
const BOOKING_CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
const BOOKING_CODE_LENGTH = 6;
export function toMinutes(value: string): number {
const [hours, minutes] = value.split(':').map((part) => Number(part));
return hours * 60 + minutes;
}
export function minutesToTime(minutes: number): string {
const safeMinutes = Math.max(0, minutes);
const hours = Math.floor(safeMinutes / 60);
const mins = safeMinutes % 60;
return `${String(hours).padStart(2, '0')}:${String(mins).padStart(2, '0')}`;
}
export function parseIsoDate(date: string): Result<Date> {
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
if (!match) {
return err(Errors.validation('La fecha debe tener formato YYYY-MM-DD.'));
}
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
const bookingDate = new Date(Date.UTC(year, month - 1, day));
if (
Number.isNaN(bookingDate.getTime()) ||
bookingDate.getUTCFullYear() !== year ||
bookingDate.getUTCMonth() + 1 !== month ||
bookingDate.getUTCDate() !== day
) {
return err(Errors.validation('La fecha enviada no es valida.'));
}
return ok(bookingDate);
}
export function formatIsoDate(date: Date): string {
return date.toISOString().slice(0, 10);
}
export function getDayOfWeek(date: Date): Result<DayOfWeek> {
const dayOfWeek = DAY_OF_WEEK_BY_INDEX[date.getUTCDay()];
if (!dayOfWeek) {
return err(Errors.validation('No se pudo resolver el dia de la semana.'));
}
return ok(dayOfWeek);
}
export function generateBookingCode(): string {
let code = '';
for (let index = 0; index < BOOKING_CODE_LENGTH; index += 1) {
code += BOOKING_CODE_ALPHABET[randomInt(0, BOOKING_CODE_ALPHABET.length)];
}
return code;
}
export function buildSlots(
availability: Array<{ startTime: string; endTime: string }>,
slotDurationMinutes: number
): Slot[] {
const slots: Slot[] = [];
for (const range of availability) {
const start = toMinutes(range.startTime);
const end = toMinutes(range.endTime);
for (
let current = start;
current + slotDurationMinutes <= end;
current += slotDurationMinutes
) {
slots.push({
startTime: minutesToTime(current),
endTime: minutesToTime(current + slotDurationMinutes),
});
}
}
return slots;
}
export async function ensureComplexAccess(
complexId: string,
userId: string
): Promise<
Result<{
id: string;
complexName: string;
plan: { rules: unknown } | null;
}>
> {
const complexUser = await db.complexUser.findUnique({
where: {
complexId_userId: { complexId, userId },
},
include: {
complex: {
select: { id: true, complexName: true, plan: { select: { rules: true } } },
},
},
});
if (!complexUser) {
return err(Errors.forbidden('No tienes permisos para administrar este complejo.'));
}
return ok(complexUser.complex);
}
export function resolvePrice(
court: {
basePrice: unknown;
priceRules: Array<{
dayOfWeek: string | null;
startTime: string | null;
endTime: string | null;
price: unknown;
}>;
},
dayOfWeek: string,
startTime: string,
endTime: string
): number {
const slotStart = toMinutes(startTime);
const slotEnd = toMinutes(endTime);
const matchingRules = court.priceRules
.filter((rule) => {
if (rule.dayOfWeek && rule.dayOfWeek !== dayOfWeek) return false;
if (!rule.startTime || !rule.endTime) return true;
return slotStart >= toMinutes(rule.startTime) && slotEnd <= toMinutes(rule.endTime);
})
.sort((first, second) => {
const firstSpecificity =
(first.dayOfWeek ? 2 : 0) + (first.startTime && first.endTime ? 1 : 0);
const secondSpecificity =
(second.dayOfWeek ? 2 : 0) + (second.startTime && second.endTime ? 1 : 0);
return secondSpecificity - firstSpecificity;
});
return Number(matchingRules[0]?.price ?? court.basePrice);
}
export function mapBookingResponse(booking: {
id: string;
bookingCode: string;
bookingDate: Date;
startTime: string;
endTime: string;
customerName: string;
customerPhone: string;
customerEmail: string;
status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED' | 'NOSHOW';
recurringGroupId: string | null;
createdAt: Date;
updatedAt: Date;
court: {
id: string;
name: string;
complex: { id: string; complexName: string };
sport: { id: string; name: string; slug: string };
};
price?: number;
}): AdminBooking {
return {
id: booking.id,
bookingCode: booking.bookingCode,
complexId: booking.court.complex.id,
complexName: booking.court.complex.complexName,
courtId: booking.court.id,
courtName: booking.court.name,
sport: {
id: booking.court.sport.id,
name: booking.court.sport.name,
slug: booking.court.sport.slug,
},
date: formatIsoDate(booking.bookingDate),
startTime: booking.startTime,
endTime: booking.endTime,
customerName: booking.customerName,
customerPhone: booking.customerPhone,
customerEmail: booking.customerEmail,
price: booking.price ?? 0,
status: booking.status,
recurringGroupId: booking.recurringGroupId,
createdAt: booking.createdAt.toISOString(),
updatedAt: booking.updatedAt.toISOString(),
};
}

View File

@@ -0,0 +1,67 @@
import { validate } from '@/lib/http/validate';
import { requireAuth } from '@/middlewares/require-auth.middleware';
import { requireSuperAdmin } from '@/middlewares/require-super-admin.middleware';
import { blockUserHandler } from '@/modules/admin/features/block-user/block-user.handler';
import { createPlanHandler } from '@/modules/admin/features/create-plan/create-plan.handler';
import { deletePlanHandler } from '@/modules/admin/features/delete-plan/delete-plan.handler';
import { getGeoStatsHandler } from '@/modules/admin/features/get-geo-stats/get-geo-stats.handler';
import { getUserSessionsHandler } from '@/modules/admin/features/get-user-sessions/get-user-sessions.handler';
import { listComplexesHandler } from '@/modules/admin/features/list-complexes/list-complexes.handler';
import { listPlansAdminHandler } from '@/modules/admin/features/list-plans-admin/list-plans-admin.handler';
import { listUsersHandler } from '@/modules/admin/features/list-users/list-users.handler';
import { revokeAllSessionsHandler } from '@/modules/admin/features/revoke-all-sessions/revoke-all-sessions.handler';
import { unblockUserHandler } from '@/modules/admin/features/unblock-user/unblock-user.handler';
import { updatePlanHandler } from '@/modules/admin/features/update-plan/update-plan.handler';
import type { AppEnv } from '@/types/hono';
import {
adminBlockUserSchema,
adminCreatePlanSchema,
adminUpdatePlanSchema,
} from '@repo/api-contract';
import { Hono } from 'hono';
import { z } from 'zod';
export const adminRoutes = new Hono<AppEnv>();
adminRoutes.use('*', requireAuth, requireSuperAdmin);
adminRoutes.get('/complexes', listComplexesHandler);
adminRoutes.get('/plans', listPlansAdminHandler);
adminRoutes.post('/plans', validate.json(adminCreatePlanSchema), createPlanHandler);
adminRoutes.patch(
'/plans/:code',
validate.param(z.object({ code: z.string().min(1).max(10) })),
validate.json(adminUpdatePlanSchema),
updatePlanHandler
);
adminRoutes.delete(
'/plans/:code',
validate.param(z.object({ code: z.string().min(1).max(10) })),
deletePlanHandler
);
adminRoutes.get('/geo-stats', getGeoStatsHandler);
adminRoutes.get('/users', listUsersHandler);
adminRoutes.post(
'/users/:id/block',
validate.param(z.object({ id: z.string().min(1) })),
validate.json(adminBlockUserSchema),
blockUserHandler
);
adminRoutes.post(
'/users/:id/unblock',
validate.param(z.object({ id: z.string().min(1) })),
unblockUserHandler
);
adminRoutes.get(
'/users/:id/sessions',
validate.param(z.object({ id: z.string().min(1) })),
getUserSessionsHandler
);
adminRoutes.post(
'/users/:id/sessions/revoke-all',
validate.param(z.object({ id: z.string().min(1) })),
revokeAllSessionsHandler
);

View File

@@ -0,0 +1,26 @@
import { db } from '@/lib/prisma';
import { AdminServiceError } from '../../shared/errors';
export async function blockUser(userId: string, banReason?: string): Promise<void> {
const target = await db.user.findUnique({
where: { id: userId },
select: { role: true },
});
if (!target) {
throw new AdminServiceError('Usuario no encontrado.', 404);
}
if (target.role === 'super_admin') {
throw new AdminServiceError('No se puede bloquear un super_admin.', 403);
}
await db.user.update({
where: { id: userId },
data: {
banned: true,
bannedAt: new Date(),
banReason: banReason ?? null,
},
});
}

View File

@@ -0,0 +1,21 @@
import type { AppContext } from '@/types/hono';
import type { AdminBlockUserInput } from '@repo/api-contract';
import { AdminServiceError } from '../../shared/errors';
import { blockUser } from './block-user.business';
type BlockUserParams = { id: string };
export async function blockUserHandler(c: AppContext) {
const { id } = c.req.valid('param' as never) as BlockUserParams;
const body = c.req.valid('json' as never) as AdminBlockUserInput;
try {
await blockUser(id, body.banReason);
return c.json({ message: 'Usuario bloqueado correctamente.' });
} catch (error) {
if (error instanceof AdminServiceError) {
return c.json({ message: error.message }, error.status);
}
throw error;
}
}

View File

@@ -0,0 +1,32 @@
import { db } from '@/lib/prisma';
import type { AdminCreatePlanInput } from '@repo/api-contract';
import { v7 as uuidv7 } from 'uuid';
export async function createPlan(input: AdminCreatePlanInput) {
const existing = await db.plan.findUnique({
where: { code: input.code },
});
if (existing) {
return { ok: false as const, message: 'Ya existe un plan con ese código.' };
}
const plan = await db.plan.create({
data: {
code: input.code,
name: input.name,
price: input.price,
rules: input.rules,
},
});
return {
ok: true as const,
data: {
code: plan.code,
name: plan.name,
price: Number(plan.price),
rules: plan.rules,
},
};
}

View File

@@ -0,0 +1,15 @@
import type { AppContext } from '@/types/hono';
import type { AdminCreatePlanInput } from '@repo/api-contract';
import { createPlan } from './create-plan.business';
export async function createPlanHandler(c: AppContext) {
const body = c.req.valid('json' as never) as AdminCreatePlanInput;
const result = await createPlan(body);
if (!result.ok) {
return c.json({ message: result.message }, 409);
}
return c.json(result.data, 201);
}

View File

@@ -0,0 +1,25 @@
import { db } from '@/lib/prisma';
export async function deletePlan(code: string) {
const existing = await db.plan.findUnique({
where: { code },
include: {
_count: { select: { complexes: true } },
},
});
if (!existing) {
return { ok: false as const, message: 'Plan no encontrado.' };
}
if (existing._count.complexes > 0) {
return {
ok: false as const,
message: 'No se puede eliminar un plan que tiene complejos asignados.',
};
}
await db.plan.delete({ where: { code } });
return { ok: true as const, message: 'Plan eliminado correctamente.' };
}

View File

@@ -0,0 +1,17 @@
import type { AppContext } from '@/types/hono';
import { deletePlan } from './delete-plan.business';
type DeletePlanParams = { code: string };
export async function deletePlanHandler(c: AppContext) {
const { code } = c.req.valid('param' as never) as DeletePlanParams;
const result = await deletePlan(code);
if (!result.ok) {
const status = result.message === 'Plan no encontrado.' ? 404 : 409;
return c.json({ message: result.message }, status);
}
return c.json({ message: result.message });
}

View File

@@ -0,0 +1,67 @@
import { db } from '@/lib/prisma';
type GeoStatsCountry = {
country: string;
countryCode: string;
totalUsers: number;
cities: { city: string; userCount: number }[];
};
export async function getGeoStats(): Promise<{
countries: GeoStatsCountry[];
totalUniqueCountries: number;
}> {
const sessions = await db.session.findMany({
where: {
country: { not: null },
expiresAt: { gt: new Date() },
},
select: {
country: true,
countryCode: true,
city: true,
userId: true,
},
});
const countryMap = new Map<
string,
{ country: string; countryCode: string; users: Set<string>; cities: Map<string, Set<string>> }
>();
for (const s of sessions) {
const code = s.countryCode || 'XX';
let entry = countryMap.get(code);
if (!entry) {
entry = {
country: s.country || 'Desconocido',
countryCode: code,
users: new Set(),
cities: new Map(),
};
countryMap.set(code, entry);
}
entry.users.add(s.userId);
const cityName = s.city || 'Desconocida';
let cityUsers = entry.cities.get(cityName);
if (!cityUsers) {
cityUsers = new Set();
entry.cities.set(cityName, cityUsers);
}
cityUsers.add(s.userId);
}
const countries = Array.from(countryMap.values())
.map((entry) => ({
country: entry.country,
countryCode: entry.countryCode,
totalUsers: entry.users.size,
cities: Array.from(entry.cities.entries())
.map(([city, users]) => ({ city, userCount: users.size }))
.sort((a, b) => b.userCount - a.userCount),
}))
.sort((a, b) => b.totalUsers - a.totalUsers);
return { countries, totalUniqueCountries: countries.length };
}

View File

@@ -0,0 +1,8 @@
import type { AppEnv } from '@/types/hono';
import type { Handler } from 'hono';
import { getGeoStats } from './get-geo-stats.business';
export const getGeoStatsHandler: Handler<AppEnv> = async (c) => {
const stats = await getGeoStats();
return c.json(stats);
};

View File

@@ -0,0 +1,49 @@
import { fetchGeoInfo } from '@/lib/geoip';
import { db } from '@/lib/prisma';
type AdminUserSession = {
id: string;
createdAt: string;
expiresAt: string;
ipAddress: string | null;
userAgent: string | null;
city: string | null;
country: string | null;
countryCode: string | null;
};
export async function getUserSessions(userId: string): Promise<AdminUserSession[]> {
const sessions = await db.session.findMany({
where: { userId },
orderBy: { createdAt: 'desc' },
});
const uniqueIps = [...new Set(sessions.map((s) => s.ipAddress).filter(Boolean))] as string[];
const geoResults = await Promise.all(uniqueIps.map((ip) => fetchGeoInfo(ip)));
const geoMap = new Map<
string,
{ city: string | null; country: string | null; countryCode: string | null }
>();
for (let i = 0; i < uniqueIps.length; i++) {
const info = geoResults[i];
geoMap.set(uniqueIps[i], {
city: info?.city ?? null,
country: info?.country ?? null,
countryCode: info?.countryCode ?? null,
});
}
return sessions.map((s) => {
const geo = s.ipAddress ? geoMap.get(s.ipAddress) : null;
return {
id: s.id,
createdAt: s.createdAt.toISOString(),
expiresAt: s.expiresAt.toISOString(),
ipAddress: s.ipAddress,
userAgent: s.userAgent,
city: geo?.city ?? null,
country: geo?.country ?? null,
countryCode: geo?.countryCode ?? null,
};
});
}

View File

@@ -0,0 +1,12 @@
import type { AppContext } from '@/types/hono';
import { getUserSessions } from './get-user-sessions.business';
type UserSessionsParams = { id: string };
export async function getUserSessionsHandler(c: AppContext) {
const { id } = c.req.valid('param' as never) as UserSessionsParams;
const sessions = await getUserSessions(id);
return c.json(sessions);
}

View File

@@ -0,0 +1,78 @@
import { db } from '@/lib/prisma';
type ComplexStats = {
id: string;
complexName: string;
complexSlug: string;
city: string | null;
planCode: string | null;
planName: string | null;
userCount: number;
courtCount: number;
totalBookings: number;
avgBookingsPerDay: number;
bookingsByStatus: {
confirmed: number;
cancelled: number;
completed: number;
noshow: number;
};
paymentStatus: 'active' | 'no_plan' | 'expired';
};
export async function getComplexStatsList(): Promise<ComplexStats[]> {
const complexes = await db.complex.findMany({
include: {
plan: true,
users: true,
courts: {
include: { bookings: true },
},
},
});
return complexes.map((complex) => {
const courtCount = complex.courts.length;
const allBookings = complex.courts.flatMap((c) => c.bookings);
const bookingsByStatus = {
confirmed: allBookings.filter((b) => b.status === 'CONFIRMED').length,
cancelled: allBookings.filter((b) => b.status === 'CANCELLED').length,
completed: allBookings.filter((b) => b.status === 'COMPLETED').length,
noshow: allBookings.filter((b) => b.status === 'NOSHOW').length,
};
const totalBookings = allBookings.length;
let avgBookingsPerDay = 0;
if (allBookings.length > 0) {
const dates = allBookings.map((b) => b.bookingDate);
const minDate = new Date(Math.min(...dates.map((d) => d.getTime())));
const daysDiff = Math.max(
1,
Math.ceil((Date.now() - minDate.getTime()) / (1000 * 60 * 60 * 24))
);
avgBookingsPerDay = Math.round((totalBookings / daysDiff) * 100) / 100;
}
let paymentStatus: 'active' | 'no_plan' | 'expired' = 'no_plan';
if (complex.plan) {
paymentStatus = 'active';
}
return {
id: complex.id,
complexName: complex.complexName,
complexSlug: complex.complexSlug,
city: complex.city,
planCode: complex.planCode,
planName: complex.plan?.name ?? null,
userCount: complex.users.length,
courtCount,
totalBookings,
avgBookingsPerDay,
bookingsByStatus,
paymentStatus,
};
});
}

View File

@@ -0,0 +1,7 @@
import type { AppContext } from '@/types/hono';
import { getComplexStatsList } from './list-complexes.business';
export async function listComplexesHandler(c: AppContext) {
const stats = await getComplexStatsList();
return c.json(stats);
}

View File

@@ -0,0 +1,19 @@
import { db } from '@/lib/prisma';
export async function listPlansAdmin() {
const plans = await db.plan.findMany({
orderBy: { price: 'asc' },
include: {
_count: { select: { complexes: true } },
},
});
return plans.map((plan) => ({
code: plan.code,
name: plan.name,
price: Number(plan.price),
rules: plan.rules,
lastUpdatedAt: plan.lastUpdatedAt.toISOString(),
complexCount: plan._count.complexes,
}));
}

View File

@@ -0,0 +1,7 @@
import type { AppContext } from '@/types/hono';
import { listPlansAdmin } from './list-plans-admin.business';
export async function listPlansAdminHandler(c: AppContext) {
const plans = await listPlansAdmin();
return c.json(plans);
}

View File

@@ -0,0 +1,44 @@
import { db } from '@/lib/prisma';
type AdminUser = {
id: string;
name: string;
email: string;
role: string;
banned: boolean;
bannedAt: string | null;
banReason: string | null;
createdAt: string;
complexCount: number;
activeSessions: number;
};
export async function listAdminUsers(search?: string): Promise<AdminUser[]> {
const users = await db.user.findMany({
where: search
? {
OR: [
{ name: { contains: search, mode: 'insensitive' } },
{ email: { contains: search, mode: 'insensitive' } },
],
}
: undefined,
include: {
_count: { select: { complexes: true, sessions: true } },
},
orderBy: { createdAt: 'desc' },
});
return users.map((user) => ({
id: user.id,
name: user.name,
email: user.email,
role: user.role,
banned: user.banned ?? false,
bannedAt: user.bannedAt?.toISOString() ?? null,
banReason: user.banReason ?? null,
createdAt: user.createdAt.toISOString(),
complexCount: user._count.complexes,
activeSessions: user._count.sessions,
}));
}

View File

@@ -0,0 +1,8 @@
import type { AppContext } from '@/types/hono';
import { listAdminUsers } from './list-users.business';
export async function listUsersHandler(c: AppContext) {
const search = c.req.query('search');
const users = await listAdminUsers(search);
return c.json(users);
}

View File

@@ -0,0 +1,9 @@
import { db } from '@/lib/prisma';
export async function revokeAllUserSessions(userId: string): Promise<number> {
const result = await db.session.deleteMany({
where: { userId },
});
return result.count;
}

View File

@@ -0,0 +1,12 @@
import type { AppContext } from '@/types/hono';
import { revokeAllUserSessions } from './revoke-all-sessions.business';
type RevokeSessionsParams = { id: string };
export async function revokeAllSessionsHandler(c: AppContext) {
const { id } = c.req.valid('param' as never) as RevokeSessionsParams;
const count = await revokeAllUserSessions(id);
return c.json({ message: `${count} sesiones cerradas correctamente.` });
}

View File

@@ -0,0 +1,12 @@
import { db } from '@/lib/prisma';
export async function unblockUser(userId: string): Promise<void> {
await db.user.update({
where: { id: userId },
data: {
banned: false,
bannedAt: null,
banReason: null,
},
});
}

View File

@@ -0,0 +1,12 @@
import type { AppContext } from '@/types/hono';
import { unblockUser } from './unblock-user.business';
type UnblockUserParams = { id: string };
export async function unblockUserHandler(c: AppContext) {
const { id } = c.req.valid('param' as never) as UnblockUserParams;
await unblockUser(id);
return c.json({ message: 'Usuario desbloqueado correctamente.' });
}

View File

@@ -0,0 +1,30 @@
import { db } from '@/lib/prisma';
import type { AdminUpdatePlanInput } from '@repo/api-contract';
export async function updatePlan(code: string, input: AdminUpdatePlanInput) {
const existing = await db.plan.findUnique({ where: { code } });
if (!existing) {
return { ok: false as const, message: 'Plan no encontrado.' };
}
const plan = await db.plan.update({
where: { code },
data: {
...(input.name !== undefined && { name: input.name }),
...(input.price !== undefined && { price: input.price }),
...(input.rules !== undefined && { rules: input.rules }),
lastUpdatedAt: new Date(),
},
});
return {
ok: true as const,
data: {
code: plan.code,
name: plan.name,
price: Number(plan.price),
rules: plan.rules,
},
};
}

View File

@@ -0,0 +1,18 @@
import type { AppContext } from '@/types/hono';
import type { AdminUpdatePlanInput } from '@repo/api-contract';
import { updatePlan } from './update-plan.business';
type UpdatePlanParams = { code: string };
export async function updatePlanHandler(c: AppContext) {
const { code } = c.req.valid('param' as never) as UpdatePlanParams;
const body = c.req.valid('json' as never) as AdminUpdatePlanInput;
const result = await updatePlan(code, body);
if (!result.ok) {
return c.json({ message: result.message }, 404);
}
return c.json(result.data);
}

View File

@@ -0,0 +1,9 @@
export class AdminServiceError extends Error {
status: 400 | 403 | 404;
constructor(message: string, status: 400 | 403 | 404 = 400) {
super(message);
this.status = status;
this.name = 'AdminServiceError';
}
}

View File

@@ -0,0 +1,24 @@
import { validate } from '@/lib/http/validate';
import { requireAuth } from '@/middlewares/require-auth.middleware';
import { cancelSubscriptionHandler } from '@/modules/billing/features/cancel-subscription/cancel-subscription.handler';
import { createCheckoutHandler } from '@/modules/billing/features/create-checkout/create-checkout.handler';
import { getBillingStatusHandler } from '@/modules/billing/features/get-billing-status/get-billing-status.handler';
import { mercadopagoWebhookHandler } from '@/modules/billing/features/mercadopago-webhook/mercadopago-webhook.handler';
import { stripeWebhookHandler } from '@/modules/billing/features/stripe-webhook/stripe-webhook.handler';
import type { AppEnv } from '@/types/hono';
import { createCheckoutSchema } from '@repo/api-contract';
import { Hono } from 'hono';
export const billingRoutes = new Hono<AppEnv>();
billingRoutes.post(
'/checkout',
requireAuth,
validate.json(createCheckoutSchema),
createCheckoutHandler
);
billingRoutes.post('/cancel', requireAuth, cancelSubscriptionHandler);
billingRoutes.get('/status', requireAuth, getBillingStatusHandler);
billingRoutes.post('/webhooks/stripe', stripeWebhookHandler);
billingRoutes.post('/webhooks/mercadopago', mercadopagoWebhookHandler);

View File

@@ -0,0 +1,47 @@
import { Errors } from '@/lib/errors';
import { db } from '@/lib/prisma';
import { type Result, err, ok } from '@/lib/result';
import { getBilling } from '@/modules/billing/services/billing-repository.service';
import { getProviderForComplex } from '@/modules/billing/services/provider-resolver.service';
export async function cancelSubscription(
user: { id: string },
complexId: string
): Promise<Result<{ status: string }>> {
const userAccess = await db.complexUser.findUnique({
where: { complexId_userId: { complexId, userId: user.id } },
select: { role: true },
});
if (!userAccess || userAccess.role !== 'ADMIN') {
return err(Errors.forbidden('Only admins can cancel the subscription'));
}
const billingResult = await getBilling(complexId);
if (!billingResult.ok) return err(billingResult.error);
const billing = billingResult.value;
if (billing.status === 'CANCELED' || billing.status === 'SUSPENDED') {
return err(Errors.conflict('La suscripción ya está cancelada o suspendida'));
}
const providerResult = await getProviderForComplex(complexId);
if (!providerResult.ok) return err(providerResult.error);
const { provider } = providerResult.value;
const subId = billing.providerSubscriptionId;
if (subId) {
const cancelResult = await provider.cancelSubscription(subId);
if (!cancelResult.ok) return err(cancelResult.error);
}
await db.complexBilling.update({
where: { complexId },
data: {
status: 'CANCELED',
canceledAt: new Date(),
},
});
return ok({ status: 'CANCELED' });
}

View File

@@ -0,0 +1,17 @@
import { handleResult } from '@/lib/http/handle-result';
import type { AppContext } from '@/types/hono';
import { getCookie } from 'hono/cookie';
import { cancelSubscription } from './cancel-subscription.business';
const SELECTED_COMPLEX_COOKIE = 'selected-complex-id';
export async function cancelSubscriptionHandler(c: AppContext) {
const user = c.get('user');
const complexId = getCookie(c, SELECTED_COMPLEX_COOKIE);
if (!complexId) {
return c.json({ message: 'No complex selected' }, 400);
}
return handleResult(c, await cancelSubscription(user, complexId));
}

View File

@@ -0,0 +1,80 @@
import { Errors } from '@/lib/errors';
import { db } from '@/lib/prisma';
import { type Result, err, ok } from '@/lib/result';
import { hasActiveSubscription } from '@/modules/billing/services/billing-repository.service';
import { getProviderForComplex } from '@/modules/billing/services/provider-resolver.service';
import type { CheckoutResponse, CreateCheckoutInput } from '@repo/api-contract';
import { v7 as uuidv7 } from 'uuid';
export async function createCheckout(
user: { id: string; email: string },
complexId: string,
input: CreateCheckoutInput
): Promise<Result<CheckoutResponse>> {
const complex = await db.complex.findUnique({
where: { id: complexId },
select: { id: true, country: true },
});
if (!complex) {
return err(Errors.notFound('Complex not found'));
}
const userAccess = await db.complexUser.findUnique({
where: { complexId_userId: { complexId, userId: user.id } },
});
if (!userAccess) {
return err(Errors.forbidden('You do not have access to this complex'));
}
const activeResult = await hasActiveSubscription(complexId);
if (!activeResult.ok) return err(activeResult.error);
if (activeResult.value) {
return err(Errors.conflict('Ya existe una suscripción activa para este complejo'));
}
const providerResult = await getProviderForComplex(complexId);
if (!providerResult.ok) return err(providerResult.error);
const { provider, country } = providerResult.value;
const currency = country === 'AR' ? 'ARS' : 'USD';
const baseUrl = Bun.env.FRONTEND_BASE_URL || Bun.env.APP_BASE_URL || 'http://localhost:5173';
const successUrl = `${baseUrl}/billing/success`;
const cancelUrl = `${baseUrl}/billing/cancel`;
const checkoutResult = await provider.createCheckout({
planCode: input.planCode,
complexId,
customerEmail: user.email,
successUrl,
cancelUrl,
});
if (!checkoutResult.ok) return err(checkoutResult.error);
await db.complexBilling.upsert({
where: { complexId },
create: {
complexId,
planCode: input.planCode,
currency,
status: 'TRIAL',
provider: country === 'AR' ? 'MERCADOPAGO' : 'STRIPE',
providerSubscriptionId: checkoutResult.value.providerSubscriptionId,
},
update: {
planCode: input.planCode,
currency,
status: 'TRIAL',
provider: country === 'AR' ? 'MERCADOPAGO' : 'STRIPE',
providerSubscriptionId: checkoutResult.value.providerSubscriptionId,
},
});
return ok({
checkoutUrl: checkoutResult.value.checkoutUrl,
subscriptionId: checkoutResult.value.providerSubscriptionId,
});
}

View File

@@ -0,0 +1,19 @@
import { handleResult } from '@/lib/http/handle-result';
import type { AppContext } from '@/types/hono';
import type { CreateCheckoutInput } from '@repo/api-contract';
import { getCookie } from 'hono/cookie';
import { createCheckout } from './create-checkout.business';
const SELECTED_COMPLEX_COOKIE = 'selected-complex-id';
export async function createCheckoutHandler(c: AppContext) {
const user = c.get('user');
const complexId = getCookie(c, SELECTED_COMPLEX_COOKIE);
const payload = c.req.valid('json' as never) as CreateCheckoutInput;
if (!complexId) {
return c.json({ message: 'No complex selected' }, 400);
}
return handleResult(c, await createCheckout(user, complexId, payload), 201);
}

View File

@@ -0,0 +1,26 @@
import { Errors } from '@/lib/errors';
import { type Result, err, ok } from '@/lib/result';
import { getBilling } from '@/modules/billing/services/billing-repository.service';
import type { BillingStatusResponse } from '@repo/api-contract';
export async function getBillingStatus(
_user: { id: string },
complexId: string
): Promise<Result<BillingStatusResponse>> {
const billingResult = await getBilling(complexId);
if (!billingResult.ok) return err(billingResult.error);
const billing = billingResult.value;
return ok({
complexId: billing.complexId,
status: billing.status,
planCode: billing.planCode,
currency: billing.currency,
provider: billing.provider,
currentPeriodEnd: billing.currentPeriodEnd?.toISOString() ?? null,
currentPeriodStart: billing.currentPeriodStart?.toISOString() ?? null,
trialEndsAt: billing.trialEndsAt?.toISOString() ?? null,
canceledAt: billing.canceledAt?.toISOString() ?? null,
suspendedAt: billing.suspendedAt?.toISOString() ?? null,
});
}

View File

@@ -0,0 +1,17 @@
import { handleResult } from '@/lib/http/handle-result';
import type { AppContext } from '@/types/hono';
import { getCookie } from 'hono/cookie';
import { getBillingStatus } from './get-billing-status.business';
const SELECTED_COMPLEX_COOKIE = 'selected-complex-id';
export async function getBillingStatusHandler(c: AppContext) {
const user = c.get('user');
const complexId = getCookie(c, SELECTED_COMPLEX_COOKIE);
if (!complexId) {
return c.json({ message: 'No complex selected' }, 400);
}
return handleResult(c, await getBillingStatus(user, complexId));
}

View File

@@ -0,0 +1,133 @@
import { Errors } from '@/lib/errors';
import { logger } from '@/lib/logger';
import { db } from '@/lib/prisma';
import { type Result, err, ok } from '@/lib/result';
import type { WebhookEvent } from '@/modules/billing/providers/billing-provider.interface';
import { mercadopagoBillingProvider } from '@/modules/billing/providers/mercadopago.provider';
import {
findEventByProviderId,
recordBillingEvent,
updateBillingStatus,
} from '@/modules/billing/services/billing-repository.service';
import type { BillingStatusEnum } from '@/modules/billing/services/billing-repository.service';
const MP_PROVIDER = 'MERCADOPAGO';
function mapWebhookTypeToStatus(event: WebhookEvent): BillingStatusEnum | null {
switch (event.type) {
case 'activated':
return 'ACTIVE';
case 'updated':
return 'ACTIVE';
case 'payment_failed':
return 'PAST_DUE';
case 'cancelled':
return 'CANCELED';
default:
return null;
}
}
async function getBillingByPreapprovalId(preapprovalId: string) {
return db.complexBilling.findFirst({
where: { providerPreapprovalId: preapprovalId },
}) as Promise<{ complexId: string; status: BillingStatusEnum } | null>;
}
async function getBillingByExternalRef(externalRef: string) {
return db.complexBilling.findUnique({
where: { complexId: externalRef },
}) as Promise<{ complexId: string; status: BillingStatusEnum } | null>;
}
export async function processMercadoPagoWebhook(
body: Record<string, unknown>,
signature: string | undefined,
requestId: string | undefined
): Promise<Result<{ received: boolean }>> {
const parsedResult = await mercadopagoBillingProvider.parseWebhook(body, signature, requestId);
if (!parsedResult.ok) {
if (parsedResult.error.type === 'validation') {
return err(parsedResult.error);
}
logger.error({ error: parsedResult.error }, 'MP webhook parse failed');
return ok({ received: true });
}
const event = parsedResult.value;
const existingResult = await findEventByProviderId(event.providerEventId);
if (!existingResult.ok) return err(existingResult.error);
if (existingResult.value) {
logger.info(
{ providerEventId: event.providerEventId },
'MP webhook: already processed, skipping'
);
return ok({ received: true });
}
const newStatus = mapWebhookTypeToStatus(event);
try {
const billing =
(await getBillingByPreapprovalId(event.providerSubscriptionId)) ??
(await getBillingByExternalRef(event.providerSubscriptionId));
if (!billing) {
logger.info(
{ providerSubscriptionId: event.providerSubscriptionId, eventType: event.type },
'MP webhook: no billing record found'
);
await recordBillingEvent({
complexId: 'unknown',
eventType: `unlinked:${event.type}`,
provider: MP_PROVIDER,
providerEventId: event.providerEventId,
providerData: event.data ?? null,
previousStatus: null,
newStatus: null,
});
return ok({ received: true });
}
const previousStatus = billing.status;
if (newStatus) {
await updateBillingStatus(billing.complexId, newStatus, {
provider: MP_PROVIDER,
providerPreapprovalId: event.providerSubscriptionId,
...(event.type === 'activated' || event.type === 'updated'
? {
currentPeriodStart: new Date(),
currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
}
: {}),
});
}
await recordBillingEvent({
complexId: billing.complexId,
eventType: event.type,
provider: MP_PROVIDER,
providerEventId: event.providerEventId,
providerData: event.data ?? null,
previousStatus,
newStatus,
});
return ok({ received: true });
} catch (error) {
logger.error({ error, providerEventId: event.providerEventId }, 'MP webhook processing failed');
await recordBillingEvent({
complexId: 'unknown',
eventType: `error:${event.type}`,
provider: MP_PROVIDER,
providerEventId: event.providerEventId,
providerData: { error: String(error) },
previousStatus: null,
newStatus: null,
}).catch((e) => logger.error({ error: e }, 'Failed to record error event'));
return ok({ received: true });
}
}

View File

@@ -0,0 +1,15 @@
import { handleResult } from '@/lib/http/handle-result';
import type { AppContext } from '@/types/hono';
import { processMercadoPagoWebhook } from './mercadopago-webhook.business';
export async function mercadopagoWebhookHandler(c: AppContext) {
const body = await c.req.json();
const signature = c.req.header('x-signature');
const requestId = c.req.header('x-request-id');
return handleResult(
c,
await processMercadoPagoWebhook(body, signature ?? undefined, requestId ?? undefined),
200
);
}

View File

@@ -0,0 +1,128 @@
import { Errors } from '@/lib/errors';
import { logger } from '@/lib/logger';
import { db } from '@/lib/prisma';
import { type Result, err, ok } from '@/lib/result';
import type { WebhookEvent } from '@/modules/billing/providers/billing-provider.interface';
import { stripeBillingProvider } from '@/modules/billing/providers/stripe.provider';
import {
findEventByProviderId,
recordBillingEvent,
updateBillingStatus,
} from '@/modules/billing/services/billing-repository.service';
import type { BillingStatusEnum } from '@/modules/billing/services/billing-repository.service';
const STRIPE_PROVIDER = 'STRIPE';
function mapWebhookTypeToStatus(event: WebhookEvent): BillingStatusEnum | null {
switch (event.type) {
case 'activated':
return 'ACTIVE';
case 'updated':
return 'ACTIVE';
case 'payment_failed':
return 'PAST_DUE';
case 'cancelled':
return 'CANCELED';
default:
return null;
}
}
async function getBillingBySubscription(providerSubscriptionId: string) {
return db.complexBilling.findFirst({
where: { providerSubscriptionId },
}) as Promise<{ complexId: string; status: BillingStatusEnum } | null>;
}
export async function processStripeWebhook(
rawBody: string,
signature: string | undefined
): Promise<Result<{ received: boolean }>> {
const parsedResult = await stripeBillingProvider.parseWebhook(rawBody, signature);
if (!parsedResult.ok) {
if (parsedResult.error.type === 'validation') {
return err(parsedResult.error);
}
logger.error({ error: parsedResult.error }, 'Stripe webhook parse failed');
return ok({ received: true });
}
const event = parsedResult.value;
const existingResult = await findEventByProviderId(event.providerEventId);
if (!existingResult.ok) return err(existingResult.error);
if (existingResult.value) {
logger.info(
{ providerEventId: event.providerEventId },
'Stripe webhook: already processed, skipping'
);
return ok({ received: true });
}
const newStatus = mapWebhookTypeToStatus(event);
try {
const billing = await getBillingBySubscription(event.providerSubscriptionId);
if (!billing) {
logger.info(
{ providerSubscriptionId: event.providerSubscriptionId, eventType: event.type },
'Stripe webhook: no billing record found for subscription'
);
await recordBillingEvent({
complexId: 'unknown',
eventType: `unlinked:${event.type}`,
provider: STRIPE_PROVIDER,
providerEventId: event.providerEventId,
providerData: event.data ?? null,
previousStatus: null,
newStatus: null,
});
return ok({ received: true });
}
const previousStatus = billing.status;
if (newStatus) {
await updateBillingStatus(billing.complexId, newStatus, {
provider: STRIPE_PROVIDER,
providerCustomerId: event.providerCustomerId,
providerSubscriptionId: event.providerSubscriptionId,
...(event.type === 'activated' || event.type === 'updated'
? {
currentPeriodStart: new Date(),
currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
}
: {}),
});
}
await recordBillingEvent({
complexId: billing.complexId,
eventType: event.type,
provider: STRIPE_PROVIDER,
providerEventId: event.providerEventId,
providerData: event.data ?? null,
previousStatus,
newStatus,
});
return ok({ received: true });
} catch (error) {
logger.error(
{ error, providerEventId: event.providerEventId },
'Stripe webhook processing failed'
);
await recordBillingEvent({
complexId: 'unknown',
eventType: `error:${event.type}`,
provider: STRIPE_PROVIDER,
providerEventId: event.providerEventId,
providerData: { error: String(error) },
previousStatus: null,
newStatus: null,
}).catch((e) => logger.error({ error: e }, 'Failed to record error event'));
return ok({ received: true });
}
}

View File

@@ -0,0 +1,10 @@
import { handleResult } from '@/lib/http/handle-result';
import type { AppContext } from '@/types/hono';
import { processStripeWebhook } from './stripe-webhook.business';
export async function stripeWebhookHandler(c: AppContext) {
const body = await c.req.raw.text();
const signature = c.req.header('stripe-signature');
return handleResult(c, await processStripeWebhook(body, signature ?? undefined), 200);
}

View File

@@ -0,0 +1,34 @@
import type { Result } from '@/lib/result';
export type WebhookEventType = 'activated' | 'updated' | 'cancelled' | 'payment_failed';
export type WebhookEvent = {
providerEventId: string;
type: WebhookEventType;
providerSubscriptionId: string;
providerCustomerId?: string;
data?: Record<string, unknown>;
};
export type CheckoutParams = {
planCode: string;
complexId: string;
customerEmail: string;
successUrl: string;
cancelUrl: string;
};
export type CheckoutResult = {
checkoutUrl: string;
providerSubscriptionId: string;
};
export interface BillingProvider {
createCheckout(params: CheckoutParams): Promise<Result<CheckoutResult>>;
cancelSubscription(providerSubscriptionId: string): Promise<Result<void>>;
parseWebhook(
payload: unknown,
signature?: string,
requestId?: string
): Promise<Result<WebhookEvent>>;
}

View File

@@ -0,0 +1,164 @@
import { Errors } from '@/lib/errors';
import { logger } from '@/lib/logger';
import type { Result } from '@/lib/result';
import { err, ok } from '@/lib/result';
import MercadoPagoConfig, { PreApproval } from 'mercadopago';
import { WebhookSignatureValidator } from 'mercadopago';
import type {
BillingProvider,
CheckoutParams,
CheckoutResult,
WebhookEvent,
} from './billing-provider.interface';
function getMpConfig(): MercadoPagoConfig {
const accessToken = Bun.env.MERCADOPAGO_ACCESS_TOKEN;
if (!accessToken) {
throw new Error('Missing MERCADOPAGO_ACCESS_TOKEN env var');
}
return new MercadoPagoConfig({ accessToken });
}
let _mpConfig: MercadoPagoConfig | null = null;
let _preApproval: PreApproval | null = null;
function preapproval(): PreApproval {
if (!_preApproval) {
_mpConfig = getMpConfig();
_preApproval = new PreApproval(_mpConfig);
}
return _preApproval;
}
function getMpPlanId(planCode: string, currency: string): string {
const key = `MERCADOPAGO_PLAN_ID_${currency}_${planCode}` as const;
const planId = Bun.env[key];
if (!planId) {
throw new Error(`Missing env var ${key} for Mercado Pago plan ID`);
}
return planId;
}
function isSandbox(): boolean {
return Bun.env.BILLING_ENV !== 'production';
}
export const mercadopagoBillingProvider: BillingProvider = {
async createCheckout(params: CheckoutParams): Promise<Result<CheckoutResult>> {
try {
const { planCode, complexId, customerEmail, successUrl } = params;
const currency = 'ARS';
const planId = getMpPlanId(planCode, currency);
const response = await preapproval().create({
body: {
preapproval_plan_id: planId,
reason: `Suscripción ${planCode} - Playzer`,
payer_email: customerEmail,
external_reference: complexId,
back_url: successUrl,
status: isSandbox() ? 'pending' : undefined,
auto_recurring: undefined,
},
});
if (!response.id || !response.init_point) {
return err(Errors.unexpected('Mercado Pago did not return a preapproval ID or init_point'));
}
return ok({
checkoutUrl: response.init_point,
providerSubscriptionId: response.id,
});
} catch (error) {
logger.error(
{ error, params: { planCode: params.planCode } },
'MercadoPago createCheckout failed'
);
return err(Errors.unexpected('Failed to create Mercado Pago preapproval'));
}
},
async cancelSubscription(providerSubscriptionId: string): Promise<Result<void>> {
try {
await preapproval().update({
id: providerSubscriptionId,
body: { status: 'cancelled' },
});
return ok(undefined);
} catch (error) {
logger.error({ error, providerSubscriptionId }, 'MercadoPago cancelSubscription failed');
return err(Errors.unexpected('Failed to cancel Mercado Pago preapproval'));
}
},
async parseWebhook(
payload: unknown,
signature?: string,
requestId?: string
): Promise<Result<WebhookEvent>> {
try {
const body = payload as Record<string, unknown>;
const action = body?.action as string | undefined;
const dataId = (body?.data as Record<string, unknown>)?.id as string | undefined;
if (!action || !dataId) {
return err(
Errors.validation('Invalid Mercado Pago webhook payload: missing action or data.id')
);
}
if (signature && requestId) {
const secret = Bun.env.MERCADOPAGO_WEBHOOK_SECRET;
if (secret) {
try {
WebhookSignatureValidator.validate({
xSignature: signature,
xRequestId: requestId,
dataId,
secret,
toleranceSeconds: 300,
});
} catch {
return err(Errors.validation('Invalid Mercado Pago webhook signature'));
}
}
}
let webhookType: WebhookEvent['type'];
switch (action) {
case 'subscription_authorized_payment':
webhookType = 'activated';
break;
case 'subscription_cancelled':
webhookType = 'cancelled';
break;
case 'subscription_updated':
webhookType = 'updated';
break;
case 'subscription_charge_payment':
webhookType = 'payment_failed';
break;
default:
logger.info(
{ action },
'Mercado Pago webhook: unhandled action type, treating as updated'
);
webhookType = 'updated';
}
return ok({
providerEventId: `mp-${dataId}-${action}`,
type: webhookType,
providerSubscriptionId: dataId,
providerCustomerId: undefined,
data: body as Record<string, unknown>,
});
} catch (error) {
logger.error({ error }, 'MercadoPago webhook parsing failed');
return err(Errors.validation('Invalid Mercado Pago webhook payload'));
}
},
};

View File

@@ -0,0 +1,165 @@
import { Errors } from '@/lib/errors';
import { logger } from '@/lib/logger';
import type { Result } from '@/lib/result';
import { err, ok } from '@/lib/result';
import Stripe from 'stripe';
import type {
BillingProvider,
CheckoutParams,
CheckoutResult,
WebhookEvent,
} from './billing-provider.interface';
function getStripeClient(): Stripe {
const secretKey = Bun.env.STRIPE_SECRET_KEY;
if (!secretKey) {
throw new Error('Missing STRIPE_SECRET_KEY env var');
}
return new Stripe(secretKey);
}
let _stripeClient: Stripe | null = null;
function stripe(): Stripe {
if (!_stripeClient) {
_stripeClient = getStripeClient();
}
return _stripeClient;
}
function getStripePriceId(planCode: string, currency: string): string {
const key = `STRIPE_PRICE_ID_${currency}_${planCode}` as const;
const priceId = Bun.env[key];
if (!priceId) {
throw new Error(`Missing env var ${key} for Stripe price ID`);
}
return priceId;
}
export const stripeBillingProvider: BillingProvider = {
async createCheckout(params: CheckoutParams): Promise<Result<CheckoutResult>> {
try {
const { planCode, complexId, customerEmail, successUrl, cancelUrl } = params;
const currency = 'USD';
const priceId = getStripePriceId(planCode, currency);
const session = await stripe().checkout.sessions.create({
mode: 'subscription',
customer_email: customerEmail,
line_items: [
{
price: priceId,
quantity: 1,
},
],
metadata: {
complexId,
planCode,
},
subscription_data: {
metadata: {
complexId,
planCode,
},
},
success_url: successUrl,
cancel_url: cancelUrl,
});
if (!session.url || !session.subscription) {
return err(Errors.unexpected('Stripe did not return a checkout URL or subscription ID'));
}
const subscriptionId =
typeof session.subscription === 'string' ? session.subscription : session.subscription.id;
return ok({
checkoutUrl: session.url,
providerSubscriptionId: subscriptionId,
});
} catch (error) {
logger.error(
{ error, params: { planCode: params.planCode } },
'Stripe createCheckout failed'
);
return err(Errors.unexpected('Failed to create Stripe checkout session'));
}
},
async cancelSubscription(providerSubscriptionId: string): Promise<Result<void>> {
try {
await stripe().subscriptions.cancel(providerSubscriptionId);
return ok(undefined);
} catch (error) {
logger.error({ error, providerSubscriptionId }, 'Stripe cancelSubscription failed');
return err(Errors.unexpected('Failed to cancel Stripe subscription'));
}
},
async parseWebhook(payload: unknown, signature?: string): Promise<Result<WebhookEvent>> {
if (!signature) {
return err(Errors.validation('Missing Stripe signature'));
}
const secret = Bun.env.STRIPE_WEBHOOK_SECRET;
if (!secret) {
return err(Errors.unexpected('Missing STRIPE_WEBHOOK_SECRET env var'));
}
try {
const rawBody = typeof payload === 'string' ? payload : JSON.stringify(payload);
const event = stripe().webhooks.constructEvent(rawBody, signature, secret);
const eventType = event.type;
const data = event.data.object as unknown as Record<string, unknown>;
let webhookType: WebhookEvent['type'];
let providerSubscriptionId: string | undefined;
let providerCustomerId: string | undefined;
if (eventType === 'checkout.session.completed') {
webhookType = 'activated';
providerSubscriptionId = data.subscription as string | undefined;
providerCustomerId = data.customer as string | undefined;
} else if (eventType === 'customer.subscription.updated') {
const status = data.status as string | undefined;
if (status === 'past_due') {
webhookType = 'payment_failed';
} else if (status === 'active' || status === 'trialing') {
webhookType = 'updated';
} else if (status === 'canceled' || status === 'unpaid') {
webhookType = 'cancelled';
} else {
webhookType = 'updated';
}
providerSubscriptionId = data.id as string | undefined;
providerCustomerId = data.customer as string | undefined;
} else if (eventType === 'customer.subscription.deleted') {
webhookType = 'cancelled';
providerSubscriptionId = data.id as string | undefined;
providerCustomerId = data.customer as string | undefined;
} else if (eventType === 'invoice.payment_failed') {
webhookType = 'payment_failed';
providerSubscriptionId = data.subscription as string | undefined;
providerCustomerId = data.customer as string | undefined;
} else {
logger.info({ eventType }, 'Stripe webhook: unhandled event type, skipping');
webhookType = 'updated';
providerSubscriptionId =
(data as { subscription?: string })?.subscription ?? `unknown-${event.id}`;
}
return ok({
providerEventId: event.id,
type: webhookType,
providerSubscriptionId: providerSubscriptionId ?? `unknown-${event.id}`,
providerCustomerId,
data,
});
} catch (error) {
logger.error({ error }, 'Stripe webhook parsing failed');
return err(Errors.validation('Invalid Stripe webhook signature or payload'));
}
},
};

View File

@@ -0,0 +1,38 @@
import { Errors } from '@/lib/errors';
import { db } from '@/lib/prisma';
import { type Result, err, ok } from '@/lib/result';
export async function canUseApp(complexId: string): Promise<Result<true>> {
const billing = await db.complexBilling.findUnique({
where: { complexId },
select: { status: true },
});
if (!billing) {
return ok(true);
}
if (billing.status === 'TRIAL' || billing.status === 'ACTIVE') {
return ok(true);
}
if (billing.status === 'CANCELED') {
return err(
Errors.forbidden('Suscripción cancelada. Renueva tu plan para seguir usando la app.')
);
}
if (billing.status === 'PAST_DUE') {
return err(
Errors.forbidden(
'Suscripción con pago pendiente. Regularizá tu deuda para seguir usando la app.'
)
);
}
if (billing.status === 'SUSPENDED') {
return err(Errors.forbidden('Suscripción suspendida. Contactanos para reactivarla.'));
}
return ok(true);
}

View File

@@ -0,0 +1,150 @@
import { Errors } from '@/lib/errors';
import { db } from '@/lib/prisma';
import { type Result, err, ok } from '@/lib/result';
import { v7 as uuidv7 } from 'uuid';
export type BillingProviderEnum = 'STRIPE' | 'MERCADOPAGO' | 'PAYPAL';
export type BillingStatusEnum = 'TRIAL' | 'ACTIVE' | 'PAST_DUE' | 'CANCELED' | 'SUSPENDED';
export type ComplexBillingRow = {
complexId: string;
status: BillingStatusEnum;
planCode: string;
currency: string;
provider: BillingProviderEnum | null;
providerCustomerId: string | null;
providerSubscriptionId: string | null;
providerPreapprovalId: string | null;
currentPeriodStart: Date | null;
currentPeriodEnd: Date | null;
trialEndsAt: Date | null;
canceledAt: Date | null;
suspendedAt: Date | null;
createdAt: Date;
updatedAt: Date;
};
export async function getBilling(complexId: string): Promise<Result<ComplexBillingRow>> {
const billing = await db.complexBilling.findUnique({
where: { complexId },
});
if (!billing) {
return err(Errors.notFound('Billing record not found for this complex'));
}
return ok(billing as unknown as ComplexBillingRow);
}
export async function ensureBilling(
complexId: string,
currency: string
): Promise<Result<ComplexBillingRow>> {
const existing = await db.complexBilling.findUnique({
where: { complexId },
});
if (existing) {
return ok(existing as unknown as ComplexBillingRow);
}
const created = await db.complexBilling.create({
data: {
complexId,
status: 'TRIAL',
planCode: 'BASIC',
currency,
},
});
return ok(created as unknown as ComplexBillingRow);
}
export async function updateBillingStatus(
complexId: string,
status: BillingStatusEnum,
extra?: {
provider?: BillingProviderEnum;
providerCustomerId?: string;
providerSubscriptionId?: string;
providerPreapprovalId?: string;
currentPeriodStart?: Date;
currentPeriodEnd?: Date;
}
): Promise<Result<ComplexBillingRow>> {
try {
const data: Record<string, unknown> = { status };
if (extra?.provider) data.provider = extra.provider;
if (extra?.providerCustomerId) data.providerCustomerId = extra.providerCustomerId;
if (extra?.providerSubscriptionId) data.providerSubscriptionId = extra.providerSubscriptionId;
if (extra?.providerPreapprovalId) data.providerPreapprovalId = extra.providerPreapprovalId;
if (extra?.currentPeriodStart) data.currentPeriodStart = extra.currentPeriodStart;
if (extra?.currentPeriodEnd) data.currentPeriodEnd = extra.currentPeriodEnd;
const updated = await db.complexBilling.update({
where: { complexId },
data: data as never,
});
return ok(updated as unknown as ComplexBillingRow);
} catch {
return err(Errors.unexpected('Failed to update billing status'));
}
}
export async function recordBillingEvent(params: {
complexId: string;
eventType: string;
provider: BillingProviderEnum;
providerEventId: string | null;
providerData: unknown;
previousStatus: BillingStatusEnum | null;
newStatus: BillingStatusEnum | null;
}): Promise<Result<{ id: string }>> {
try {
const event = await db.billingEvent.create({
data: {
id: uuidv7(),
complexId: params.complexId,
eventType: params.eventType,
provider: params.provider,
providerEventId: params.providerEventId,
providerData: params.providerData as never,
previousStatus: params.previousStatus,
newStatus: params.newStatus,
},
select: { id: true },
});
return ok({ id: event.id });
} catch (error: unknown) {
const prismaError = error as { code?: string };
if (prismaError.code === 'P2002') {
return err(Errors.conflict('Webhook event already processed (idempotency)'));
}
return err(Errors.unexpected('Failed to record billing event'));
}
}
export async function findEventByProviderId(
providerEventId: string
): Promise<Result<{ id: string } | null>> {
const event = await db.billingEvent.findUnique({
where: { providerEventId },
select: { id: true },
});
return ok(event ?? null);
}
export async function hasActiveSubscription(complexId: string): Promise<Result<boolean>> {
const billing = await db.complexBilling.findUnique({
where: { complexId },
select: { status: true },
});
if (!billing) return ok(false);
return ok(billing.status === 'ACTIVE' || billing.status === 'TRIAL');
}

View File

@@ -0,0 +1,33 @@
import { Errors } from '@/lib/errors';
import { db } from '@/lib/prisma';
import type { Result } from '@/lib/result';
import { err, ok } from '@/lib/result';
import type { BillingProvider } from '@/modules/billing/providers/billing-provider.interface';
import { mercadopagoBillingProvider } from '@/modules/billing/providers/mercadopago.provider';
import { stripeBillingProvider } from '@/modules/billing/providers/stripe.provider';
export const STRIPE = stripeBillingProvider;
export const MERCADOPAGO = mercadopagoBillingProvider;
export function resolveProviderByCountry(country: string | null): BillingProvider {
if (country === 'AR' || country?.toLowerCase() === 'argentina') {
return MERCADOPAGO;
}
return STRIPE;
}
export async function getProviderForComplex(
complexId: string
): Promise<Result<{ provider: BillingProvider; country: string | null }>> {
const complex = await db.complex.findUnique({
where: { id: complexId },
select: { country: true },
});
if (!complex) {
return err(Errors.notFound('Complex not found'));
}
const provider = resolveProviderByCountry(complex.country);
return ok({ provider, country: complex.country });
}

View File

@@ -1,17 +1,17 @@
import { requireAuth } from '@/middlewares/require-auth.middleware';
import { acceptComplexInvitationsHandler } from '@/modules/complex/handlers/accept-complex-invitations.handler';
import { cancelComplexInvitationHandler } from '@/modules/complex/handlers/cancel-complex-invitation.handler';
import { createComplexHandler } from '@/modules/complex/handlers/create-complex.handler';
import { getComplexByIdHandler } from '@/modules/complex/handlers/get-complex-by-id.handler';
import { getComplexBySlugHandler } from '@/modules/complex/handlers/get-complex-by-slug.handler';
import { getCurrentComplexHandler } from '@/modules/complex/handlers/get-current-complex.handler';
import { inviteComplexUserHandler } from '@/modules/complex/handlers/invite-complex-user.handler';
import { listComplexUsersHandler } from '@/modules/complex/handlers/list-complex-users.handler';
import { listMyComplexesHandler } from '@/modules/complex/handlers/list-my-complexes.handler';
import { resendComplexInvitationHandler } from '@/modules/complex/handlers/resend-complex-invitation.handler';
import { revokeComplexUserHandler } from '@/modules/complex/handlers/revoke-complex-user.handler';
import { selectComplexHandler } from '@/modules/complex/handlers/select-complex.handler';
import { updateComplexHandler } from '@/modules/complex/handlers/update-complex.handler';
import { acceptComplexInvitationsHandler } from '@/modules/complex/features/accept-complex-invitations/accept-complex-invitations.handler';
import { cancelComplexInvitationHandler } from '@/modules/complex/features/cancel-complex-invitation/cancel-complex-invitation.handler';
import { createComplexHandler } from '@/modules/complex/features/create-complex/create-complex.handler';
import { getComplexByIdHandler } from '@/modules/complex/features/get-complex-by-id/get-complex-by-id.handler';
import { getComplexBySlugHandler } from '@/modules/complex/features/get-complex-by-slug/get-complex-by-slug.handler';
import { getCurrentComplexHandler } from '@/modules/complex/features/get-current-complex/get-current-complex.handler';
import { inviteComplexUserHandler } from '@/modules/complex/features/invite-complex-user/invite-complex-user.handler';
import { listComplexUsersHandler } from '@/modules/complex/features/list-complex-users/list-complex-users.handler';
import { listMyComplexesHandler } from '@/modules/complex/features/list-my-complexes/list-my-complexes.handler';
import { resendComplexInvitationHandler } from '@/modules/complex/features/resend-complex-invitation/resend-complex-invitation.handler';
import { revokeComplexUserHandler } from '@/modules/complex/features/revoke-complex-user/revoke-complex-user.handler';
import { selectComplexHandler } from '@/modules/complex/features/select-complex/select-complex.handler';
import { updateComplexHandler } from '@/modules/complex/features/update-complex/update-complex.handler';
import type { AppEnv } from '@/types/hono';
import { zValidator } from '@hono/zod-validator';
import {

View File

@@ -0,0 +1,67 @@
import { Errors } from '@/lib/errors';
import { db } from '@/lib/prisma';
import { type Result, err, ok } from '@/lib/result';
import type {
AcceptComplexInvitationsInput,
AcceptComplexInvitationsResponse,
} from '@repo/api-contract';
import { hashToken, normalizeEmail } from '../../shared/members-helpers';
export async function acceptComplexInvitations(
userId: string,
email: string,
input: AcceptComplexInvitationsInput = {}
): Promise<Result<AcceptComplexInvitationsResponse>> {
const normalizedEmail = normalizeEmail(email);
const tokenHash = input.inviteToken ? hashToken(input.inviteToken) : null;
const invitations = await db.complexInvitation.findMany({
where: {
email: normalizedEmail,
acceptedAt: null,
revokedAt: null,
expiresAt: { gte: new Date() },
...(tokenHash ? { tokenHash } : {}),
},
orderBy: { createdAt: 'asc' },
});
if (tokenHash && invitations.length === 0) {
return err(Errors.notFound('La invitación es inválida o ya venció.'));
}
let acceptedCount = 0;
for (const invitation of invitations) {
await db.$transaction(async (tx) => {
const existingMembership = await tx.complexUser.findUnique({
where: {
complexId_userId: {
complexId: invitation.complexId,
userId,
},
},
select: { userId: true },
});
if (!existingMembership) {
await tx.complexUser.create({
data: {
complexId: invitation.complexId,
userId,
role: 'EMPLOYEE',
},
});
}
await tx.complexInvitation.update({
where: { id: invitation.id },
data: { acceptedAt: new Date() },
});
});
acceptedCount += 1;
}
return ok({ acceptedCount });
}

View File

@@ -0,0 +1,11 @@
import { handleResult } from '@/lib/http/handle-result';
import type { AppContext } from '@/types/hono';
import type { AcceptComplexInvitationsInput } from '@repo/api-contract';
import { acceptComplexInvitations } from './accept-complex-invitations.business';
export async function acceptComplexInvitationsHandler(c: AppContext) {
const user = c.get('user');
const payload = c.req.valid('json' as never) as AcceptComplexInvitationsInput;
return handleResult(c, await acceptComplexInvitations(user.id, user.email, payload));
}

Some files were not shown because too many files have changed in this diff Show More