forked from sagnik/Project_Velocity
#24 WebOS Completion Co-authored-by: Sayan Datta <sayan@Sayans-MacBook-Air.local> Reviewed-on: sagnik/Project_Velocity#25
259 lines
8.1 KiB
Swift
259 lines
8.1 KiB
Swift
import Foundation
|
|
|
|
struct VelocityLeadDTO: Decodable, Identifiable {
|
|
let id: String
|
|
let name: String
|
|
let phone: String?
|
|
let source: String
|
|
let qualification: String
|
|
let score: Int
|
|
let kanbanStatus: String
|
|
let budget: String
|
|
let unitInterest: String
|
|
let createdAt: String?
|
|
let updatedAt: String?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case id
|
|
case name
|
|
case phone
|
|
case source
|
|
case qualification
|
|
case score
|
|
case kanbanStatus = "kanban_status"
|
|
case budget
|
|
case unitInterest = "unit_interest"
|
|
case createdAt = "created_at"
|
|
case updatedAt = "updated_at"
|
|
}
|
|
}
|
|
|
|
struct VelocityCommunicationEventDTO: Decodable, Identifiable {
|
|
let eventId: String
|
|
let leadId: String
|
|
let channel: String
|
|
let direction: String
|
|
let provider: String?
|
|
let captureMode: String
|
|
let consentState: String
|
|
let timestamp: String
|
|
let durationSeconds: Int?
|
|
let summary: String?
|
|
let recordingRef: String?
|
|
let createdAt: String
|
|
|
|
var id: String { eventId }
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case eventId = "event_id"
|
|
case leadId = "lead_id"
|
|
case channel
|
|
case direction
|
|
case provider
|
|
case captureMode = "capture_mode"
|
|
case consentState = "consent_state"
|
|
case timestamp
|
|
case durationSeconds = "duration_seconds"
|
|
case summary
|
|
case recordingRef = "recording_ref"
|
|
case createdAt = "created_at"
|
|
}
|
|
}
|
|
|
|
struct VelocityCalendarEventDTO: Decodable, Identifiable {
|
|
let calendarEventId: String
|
|
let leadId: String?
|
|
let title: String
|
|
let description: String?
|
|
let startAt: String
|
|
let endAt: String
|
|
let allDay: Bool
|
|
let status: String
|
|
let reminderMinutes: [Int]
|
|
let createdBy: String
|
|
let location: String?
|
|
let createdAt: String
|
|
|
|
var id: String { calendarEventId }
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case calendarEventId = "calendar_event_id"
|
|
case leadId = "lead_id"
|
|
case title
|
|
case description
|
|
case startAt = "start_at"
|
|
case endAt = "end_at"
|
|
case allDay = "all_day"
|
|
case status
|
|
case reminderMinutes = "reminder_minutes"
|
|
case createdBy = "created_by"
|
|
case location
|
|
case createdAt = "created_at"
|
|
}
|
|
}
|
|
|
|
struct VelocityAlertSnapshotDTO: Decodable {
|
|
let pendingInsights: Int
|
|
let upcomingCalendarEvents24h: Int
|
|
let pendingTranscriptions: Int
|
|
let generatedAt: String
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case pendingInsights = "pending_insights"
|
|
case upcomingCalendarEvents24h = "upcoming_calendar_events_24h"
|
|
case pendingTranscriptions = "pending_transcriptions"
|
|
case generatedAt = "generated_at"
|
|
}
|
|
}
|
|
|
|
enum VelocityAPIError: LocalizedError {
|
|
case notConfigured(String)
|
|
case invalidResponse
|
|
case api(String)
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .notConfigured(let message):
|
|
return message
|
|
case .invalidResponse:
|
|
return "Velocity backend returned an invalid response."
|
|
case .api(let message):
|
|
return message
|
|
}
|
|
}
|
|
}
|
|
|
|
actor VelocityAPIClient {
|
|
static let shared = VelocityAPIClient()
|
|
|
|
private struct LoginBody: Encodable {
|
|
let email: String
|
|
let password: String
|
|
}
|
|
|
|
private struct LoginResponse: Decodable {
|
|
let accessToken: String
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case accessToken = "access_token"
|
|
}
|
|
}
|
|
|
|
private struct LeadsEnvelope: Decodable {
|
|
let data: [VelocityLeadDTO]
|
|
}
|
|
|
|
private struct EventsEnvelope: Decodable {
|
|
let events: [VelocityCommunicationEventDTO]
|
|
}
|
|
|
|
private struct CalendarEnvelope: Decodable {
|
|
let events: [VelocityCalendarEventDTO]
|
|
}
|
|
|
|
private let decoder = JSONDecoder()
|
|
private var cachedToken: String?
|
|
|
|
func fetchLeads() async throws -> [VelocityLeadDTO] {
|
|
let request = try await authorizedRequest(path: "/api/leads")
|
|
let response: LeadsEnvelope = try await perform(request)
|
|
return response.data
|
|
}
|
|
|
|
func fetchEvents(for leadId: String, limit: Int = 5) async throws -> [VelocityCommunicationEventDTO] {
|
|
let query = URLQueryItem(name: "lead_id", value: leadId)
|
|
let limitItem = URLQueryItem(name: "limit", value: String(limit))
|
|
let request = try await authorizedRequest(path: "/api/mobile-edge/events", queryItems: [query, limitItem])
|
|
let response: EventsEnvelope = try await perform(request)
|
|
return response.events
|
|
}
|
|
|
|
func fetchCalendarEvents(limit: Int = 50) async throws -> [VelocityCalendarEventDTO] {
|
|
let request = try await authorizedRequest(
|
|
path: "/api/mobile-edge/calendar",
|
|
queryItems: [URLQueryItem(name: "limit", value: String(limit))]
|
|
)
|
|
let response: CalendarEnvelope = try await perform(request)
|
|
return response.events
|
|
}
|
|
|
|
func fetchAlerts() async throws -> VelocityAlertSnapshotDTO {
|
|
let request = try await authorizedRequest(path: "/api/mobile-edge/alerts")
|
|
return try await perform(request)
|
|
}
|
|
|
|
private func authorizedRequest(path: String, queryItems: [URLQueryItem] = []) async throws -> URLRequest {
|
|
guard let url = buildURL(path: path, queryItems: queryItems) else {
|
|
throw VelocityAPIError.notConfigured("Velocity backend base URL is invalid.")
|
|
}
|
|
let token = try await getToken()
|
|
var request = URLRequest(url: url)
|
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
|
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
|
request.timeoutInterval = 30
|
|
return request
|
|
}
|
|
|
|
private func buildURL(path: String, queryItems: [URLQueryItem]) -> URL? {
|
|
guard var components = URLComponents(string: AppConfig.baseURL) else {
|
|
return nil
|
|
}
|
|
components.path = path
|
|
if !queryItems.isEmpty {
|
|
components.queryItems = queryItems
|
|
}
|
|
return components.url
|
|
}
|
|
|
|
private func getToken() async throws -> String {
|
|
if let token = AppConfig.apiBearerToken {
|
|
return token
|
|
}
|
|
if let token = cachedToken {
|
|
return token
|
|
}
|
|
guard let email = AppConfig.apiEmail, let password = AppConfig.apiPassword else {
|
|
throw VelocityAPIError.notConfigured(
|
|
"Set API_BEARER_TOKEN or API_EMAIL/API_PASSWORD in the app configuration to use live Velocity data."
|
|
)
|
|
}
|
|
|
|
guard let loginURL = buildURL(path: "/api/auth/login", queryItems: []) else {
|
|
throw VelocityAPIError.notConfigured("Velocity backend base URL is invalid.")
|
|
}
|
|
|
|
var request = URLRequest(url: loginURL)
|
|
request.httpMethod = "POST"
|
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
|
request.httpBody = try JSONEncoder().encode(LoginBody(email: email, password: password))
|
|
request.timeoutInterval = 30
|
|
|
|
let response: LoginResponse = try await perform(request)
|
|
cachedToken = response.accessToken
|
|
return response.accessToken
|
|
}
|
|
|
|
private func perform<T: Decodable>(_ request: URLRequest) async throws -> T {
|
|
let (data, response) = try await URLSession.shared.data(for: request)
|
|
guard let http = response as? HTTPURLResponse else {
|
|
throw VelocityAPIError.invalidResponse
|
|
}
|
|
guard 200..<300 ~= http.statusCode else {
|
|
if let apiError = try? decoder.decode(APIErrorPayload.self, from: data), let detail = apiError.detail {
|
|
throw VelocityAPIError.api(detail)
|
|
}
|
|
throw VelocityAPIError.api("Velocity request failed with HTTP \(http.statusCode).")
|
|
}
|
|
do {
|
|
return try decoder.decode(T.self, from: data)
|
|
} catch {
|
|
throw VelocityAPIError.invalidResponse
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct APIErrorPayload: Decodable {
|
|
let detail: String?
|
|
}
|