Files
Project_Velocity/iOS/Core/Networking/VelocityAPIClient.swift
sayan 84e439712c feat/#24 WebOS Completion (#25)
#24 WebOS Completion

Co-authored-by: Sayan Datta <sayan@Sayans-MacBook-Air.local>
Reviewed-on: #25
2026-04-18 18:59:04 +05:30

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?
}