Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
207 changes: 205 additions & 2 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,14 @@ model User {
transactionHistory TransactionHistory[]
favorites PropertyFavorite[]
propertyViews PropertyView[]
openHouseRsvps OpenHouseRsvp[]
openHouseRsvps OpenHouseRsvp[]
webhooks Webhook[]
tourRequests TourRequest[] @relation("TourRequestRequester")
tourAgentAssignments TourRequest[] @relation("TourRequestAgent")
agentAvailabilities AgentAvailability[]
supportTickets SupportTicket[] @relation("SupportTicketUser")
assignedTickets SupportTicket[] @relation("SupportTicketAgent")
supportTicketNotes SupportTicketNote[]
transactionNotes TransactionNote[] @relation("TransactionNoteAuthor")
deletedProperties Property[] @relation("DeletedProperties")
priceChanges PropertyPriceHistory[] @relation("PriceChangeAuthor")
Expand Down Expand Up @@ -458,6 +465,7 @@ model Property {
favorites PropertyFavorite[]
views PropertyView[]
openHouses OpenHouse[]
tourRequests TourRequest[]
neighborhood Neighborhood? @relation(fields: [neighborhoodId], references: [id], onDelete: SetNull)
amenities PropertyAmenity[]
deletedBy User? @relation("DeletedProperties", fields: [deletedById], references: [id], onDelete: SetNull)
Expand Down Expand Up @@ -1328,4 +1336,199 @@ enum JobStatus {
PROCESSING
COMPLETED
FAILED
}
}

// ─── Webhook Event System (#958) ──────────────────────────────────────────────

enum WebhookStatus {
ACTIVE
INACTIVE
VERIFYING
}

enum WebhookDeliveryStatus {
PENDING
SUCCESS
FAILED
RETRYING
}

model Webhook {
id String @id @default(uuid())
userId String @map("user_id")
url String
secret String
events String[]
description String?
status WebhookStatus @default(ACTIVE)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

user User @relation(fields: [userId], references: [id], onDelete: Cascade)
deliveries WebhookDeliveryLog[]

@@index([userId])
@@index([status])
@@map("webhooks")
}

model WebhookDeliveryLog {
id String @id @default(uuid())
webhookId String @map("webhook_id")
eventType String @map("event_type")
payload Json
status WebhookDeliveryStatus @default(PENDING)
responseCode Int? @map("response_code")
responseBody String? @map("response_body") @db.Text
attempts Int @default(0)
maxAttempts Int @default(5)
nextRetryAt DateTime? @map("next_retry_at")
error String? @db.Text
deliveredAt DateTime? @map("delivered_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

webhook Webhook @relation(fields: [webhookId], references: [id], onDelete: Cascade)

@@index([webhookId, status])
@@index([status, nextRetryAt])
@@index([eventType])
@@map("webhook_delivery_logs")
}

// ─── Property Tour Scheduling (#957) ──────────────────────────────────────────

enum TourRequestStatus {
PENDING
CONFIRMED
CANCELLED
COMPLETED
DECLINED
}

enum TourType {
PRIVATE
OPEN_HOUSE
}

model TourRequest {
id String @id @default(uuid())
propertyId String @map("property_id")
requesterId String @map("requester_id")
agentId String? @map("agent_id")
tourType TourType @default(PRIVATE) @map("tour_type")
status TourRequestStatus @default(PENDING)
requestedAt DateTime @map("requested_at")
confirmedAt DateTime? @map("confirmed_at")
notes String? @db.Text
timezone String @default("UTC")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

property Property @relation(fields: [propertyId], references: [id], onDelete: Cascade)
requester User @relation("TourRequestRequester", fields: [requesterId], references: [id], onDelete: Cascade)
agent User? @relation("TourRequestAgent", fields: [agentId], references: [id], onDelete: SetNull)

@@index([propertyId])
@@index([requesterId])
@@index([agentId])
@@index([status])
@@index([requestedAt])
@@map("tour_requests")
}

model AgentAvailability {
id String @id @default(uuid())
agentId String @map("agent_id")
dayOfWeek Int @map("day_of_week") // 0=Sun, 6=Sat
startTime String @map("start_time") // "HH:MM"
endTime String @map("end_time") // "HH:MM"
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

agent User @relation(fields: [agentId], references: [id], onDelete: Cascade)

@@index([agentId, dayOfWeek])
@@map("agent_availabilities")
}

// ─── Support Tickets with SLAs (#955) ────────────────────────────────────────

enum TicketCategory {
GENERAL
BILLING
TECHNICAL
PROPERTY_LISTING
TRANSACTION
ACCOUNT
FRAUD_REPORT
OTHER
}

enum TicketPriority {
LOW
MEDIUM
HIGH
CRITICAL
}

enum TicketStatus {
NEW
IN_PROGRESS
WAITING_ON_CUSTOMER
RESOLVED
CLOSED
}

model SupportTicket {
id String @id @default(uuid())
userId String @map("user_id")
assignedToId String? @map("assigned_to_id")
category TicketCategory @default(GENERAL)
priority TicketPriority @default(MEDIUM)
status TicketStatus @default(NEW)
subject String
description String @db.Text
transactionId String? @map("transaction_id")
propertyId String? @map("property_id")
slaDeadline DateTime? @map("sla_deadline")
slaBreached Boolean @default(false) @map("sla_breached")
firstResponseAt DateTime? @map("first_response_at")
resolvedAt DateTime? @map("resolved_at")
closedAt DateTime? @map("closed_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

user User? @relation("SupportTicketUser", fields: [userId], references: [id], onDelete: Cascade)
assignedTo User? @relation("SupportTicketAgent", fields: [assignedToId], references: [id], onDelete: SetNull)
notes SupportTicketNote[]

@@index([userId])
@@index([assignedToId])
@@index([status])
@@index([priority, status])
@@index([slaDeadline, slaBreached])
@@map("support_tickets")
}

model SupportTicketNote {
id String @id @default(uuid())
ticketId String @map("ticket_id")
authorId String @map("author_id")
content String @db.Text
isPublic Boolean @default(false) @map("is_public")
createdAt DateTime @default(now()) @map("created_at")

ticket SupportTicket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)

@@index([ticketId, createdAt])
@@map("support_ticket_notes")
}

// ─── User relation additions ──────────────────────────────────────────────────

// Note: Webhook, TourRequest, AgentAvailability, SupportTicket relations
// are defined on the User model through @relation directives above.
// Additional User relations needed:
33 changes: 33 additions & 0 deletions src/blockchain/blockchain.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,37 @@ export class BlockchainController {
getStatus(): Record<string, any> {
return this.blockchainService.getStatus();
}

// ─── RPC Health Monitoring Endpoints ──────────────────────────────────────

@Get('rpc/health')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Get RPC provider health status',
description: 'Check health of all configured RPC providers with latency and block info',
})
async getRpcHealth() {
return this.blockchainService.getRpcHealthSummary();
}

@Post('rpc/health/check')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Trigger RPC health check',
description: 'Manually trigger an RPC health check across all providers',
})
async triggerRpcHealthCheck() {
await this.blockchainService.checkRpcHealth();
return this.blockchainService.getRpcHealthSummary();
}

@Get('rpc/gas-price')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Get current gas price',
description: 'Fetch current gas price estimates from the active RPC provider',
})
async getGasPrice() {
return this.blockchainService.getGasPrice();
}
}
3 changes: 2 additions & 1 deletion src/blockchain/blockchain.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ScheduleModule } from '@nestjs/schedule';
import { BlockchainService } from './blockchain.service';
import { BlockchainController } from './blockchain.controller';
import { PrismaModule } from '../database/prisma.module';

@Module({
imports: [ConfigModule, PrismaModule],
imports: [ConfigModule, PrismaModule, ScheduleModule.forRoot()],
providers: [BlockchainService],
controllers: [BlockchainController],
exports: [BlockchainService],
Expand Down
Loading
Loading