import Foundation /// Transport boundary. A future URLSession implementation must enforce a finite /// request timeout and throw HTTP errors before decoding the response body. @MainActor protocol LeadRemoteService { func fetchLeads(salesCode: String, query: LeadQuery) async throws -> [LeadDTO] func fetchLead(id: String, salesCode: String) async throws -> LeadDTO func setBookmark(_ bookmarked: Bool, id: String, salesCode: String) async throws -> LeadDTO } enum LeadRemoteError: Error { case http(statusCode: Int) case malformedResponse } /// Provisional transport shape, independent of the UI and domain model. struct LeadDTO: Codable, Sendable { let id: String let companyName: String? let contactTitle: String? let firstName: String? let lastName: String? let street: String? let postalCode: String let city: String let phoneNumber: String? let createdAt: Date let offerCount: Int let unsignedContractCount: Int var isBookmarked: Bool func toDomain() throws -> Lead { func clean(_ value: String?) -> String? { guard let text = value?.trimmingCharacters(in: .whitespacesAndNewlines), !text.isEmpty else { return nil } return text } let company = clean(companyName) let first = clean(firstName) let last = clean(lastName) guard clean(id) != nil, company != nil || first != nil || last != nil, clean(postalCode) != nil, clean(city) != nil, offerCount >= 0, unsignedContractCount >= 0 else { throw LeadError.invalidResponse } let contact = first != nil || last != nil ? LeadContact(title: clean(contactTitle), firstName: first ?? "", lastName: last ?? "") : nil return Lead( id: id, companyName: company, contact: contact, address: LeadAddress(street: clean(street), postalCode: postalCode, city: city), phoneNumber: clean(phoneNumber), createdAt: createdAt, offerCount: offerCount, unsignedContractCount: unsignedContractCount, isBookmarked: isBookmarked ) } }