130 lines
6.3 KiB
Swift
130 lines
6.3 KiB
Swift
import Foundation
|
|
|
|
/// In-memory server substitute. Bookmarks are isolated by sales code.
|
|
/// All error scenarios can be injected without special user-facing search strings.
|
|
@MainActor
|
|
final class StubLeadRemoteService: LeadRemoteService {
|
|
enum Scenario {
|
|
case success, offline, timeout, http(Int), malformedResponse
|
|
}
|
|
|
|
var scenario: Scenario
|
|
var delay: Duration
|
|
var listDelay: Duration?
|
|
private var records: [LeadDTO]
|
|
private var bookmarksByUser: [String: Set<String>] = [:]
|
|
|
|
init(scenario: Scenario = .success, delay: Duration = .milliseconds(350),
|
|
records: [LeadDTO] = LeadSampleData.records, listDelay: Duration? = nil) {
|
|
self.scenario = scenario
|
|
self.delay = delay
|
|
self.listDelay = listDelay
|
|
self.records = records
|
|
}
|
|
|
|
func fetchLeads(salesCode: String, query: LeadQuery) async throws -> [LeadDTO] {
|
|
try await simulate(delay: listDelay)
|
|
let terms = query.normalized.searchText.split(whereSeparator: \.isWhitespace).map(String.init)
|
|
var result = records.map { personalized($0, salesCode: salesCode) }.filter { lead in
|
|
let fields = [lead.id, lead.companyName, lead.contactTitle, lead.firstName,
|
|
lead.lastName, lead.street, lead.postalCode, lead.city]
|
|
.compactMap { $0 }.joined(separator: " ")
|
|
return terms.allSatisfy { fields.localizedStandardContains($0) }
|
|
&& (query.filter == .all || lead.unsignedContractCount > 0)
|
|
}
|
|
result.sort {
|
|
switch query.sort {
|
|
case .newest:
|
|
return $0.createdAt == $1.createdAt ? $0.id < $1.id : $0.createdAt > $1.createdAt
|
|
case .nameAscending:
|
|
let comparison = name($0).localizedStandardCompare(name($1))
|
|
return comparison == .orderedSame ? $0.id < $1.id : comparison == .orderedAscending
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func fetchLead(id: String, salesCode: String) async throws -> LeadDTO {
|
|
try await simulate()
|
|
guard let record = records.first(where: { $0.id == id }) else {
|
|
throw LeadRemoteError.http(statusCode: 404)
|
|
}
|
|
return personalized(record, salesCode: salesCode)
|
|
}
|
|
|
|
func setBookmark(_ bookmarked: Bool, id: String, salesCode: String) async throws -> LeadDTO {
|
|
try await simulate()
|
|
guard let record = records.first(where: { $0.id == id }) else {
|
|
throw LeadRemoteError.http(statusCode: 404)
|
|
}
|
|
var bookmarks = bookmarks(for: salesCode)
|
|
if bookmarked { bookmarks.insert(id) } else { bookmarks.remove(id) }
|
|
bookmarksByUser[salesCode] = bookmarks
|
|
return personalized(record, salesCode: salesCode)
|
|
}
|
|
|
|
private func simulate(delay override: Duration? = nil) async throws {
|
|
try await Task.sleep(for: override ?? delay)
|
|
try Task.checkCancellation()
|
|
switch scenario {
|
|
case .success: break
|
|
case .offline: throw URLError(.notConnectedToInternet)
|
|
case .timeout: throw URLError(.timedOut)
|
|
case .http(let status): throw LeadRemoteError.http(statusCode: status)
|
|
case .malformedResponse: throw LeadRemoteError.malformedResponse
|
|
}
|
|
}
|
|
|
|
private func bookmarks(for salesCode: String) -> Set<String> {
|
|
bookmarksByUser[salesCode] ?? Set(records.filter(\.isBookmarked).map(\.id))
|
|
}
|
|
|
|
private func personalized(_ record: LeadDTO, salesCode: String) -> LeadDTO {
|
|
var value = record
|
|
value.isBookmarked = bookmarks(for: salesCode).contains(record.id)
|
|
return value
|
|
}
|
|
|
|
private func name(_ record: LeadDTO) -> String {
|
|
record.companyName ?? [record.contactTitle, record.firstName, record.lastName]
|
|
.compactMap { $0 }.joined(separator: " ")
|
|
}
|
|
}
|
|
|
|
enum LeadSampleData {
|
|
static let records: [LeadDTO] = [
|
|
make("L-240330", first: "Thomas", last: "Brenner", street: "Am Schlossberg 9",
|
|
postal: "79111", city: "Freiburg", phone: "0160 448 2210", offers: 1, contracts: 2, day: 30),
|
|
make("L-240326", company: "Schreinerei Vogt", street: "Talweg 22",
|
|
postal: "79252", city: "Stegen", phone: "07661 37 12", day: 26),
|
|
make("L-240322", company: "Hotel Schwarzwaldblick", street: "Höhenweg 1",
|
|
postal: "79874", city: "Breitnau", phone: "07652 91 80", offers: 2, contracts: 6, day: 22),
|
|
make("L-240318", company: "Bäckerei Hoffmann GmbH", street: "Marktstraße 14",
|
|
postal: "79098", city: "Freiburg", phone: "0761 204 118", offers: 2, contracts: 5, day: 18, bookmarked: true),
|
|
make("L-240309", company: "Kita Sonnenland e.V.", street: "Schulstraße 5",
|
|
postal: "79224", city: "Umkirch", phone: "07665 50 02 1", offers: 1, contracts: 1, day: 9),
|
|
make("L-240298", company: "Familie Yilmaz", street: "Rosenweg 17",
|
|
postal: "79194", city: "Gundelfingen", phone: "0152 019 33 87", day: 8),
|
|
make("L-240291", title: "Dr.", first: "Sabine", last: "Wendt", street: "Gartenstraße 8",
|
|
postal: "79100", city: "Freiburg", phone: "0171 552 0193", offers: 1, contracts: 3, day: 7, bookmarked: true),
|
|
make("L-240277", company: "Autohaus Kessler KG", street: "Gewerbestraße 3",
|
|
postal: "79199", city: "Kirchzarten", phone: "07661 90 33 0", offers: 3, contracts: 8, day: 6, bookmarked: true),
|
|
make("L-240270", company: "Gärtnerei Weber", street: "Feldweg 4",
|
|
postal: "79206", city: "Breisach", phone: nil, day: 5),
|
|
make("L-240261", first: "Anna", last: "Schneider", street: "Bergstraße 6",
|
|
postal: "79249", city: "Merzhausen", phone: "0761 555 012", offers: 1, day: 4)
|
|
]
|
|
|
|
private static func make(
|
|
_ id: String, company: String? = nil, title: String? = nil,
|
|
first: String? = nil, last: String? = nil, street: String,
|
|
postal: String, city: String, phone: String?, offers: Int = 0,
|
|
contracts: Int = 0, day: Int, bookmarked: Bool = false
|
|
) -> LeadDTO {
|
|
LeadDTO(id: id, companyName: company, contactTitle: title, firstName: first,
|
|
lastName: last, street: street, postalCode: postal, city: city,
|
|
phoneNumber: phone, createdAt: Date(timeIntervalSince1970: 1_710_000_000 + Double(day) * 86_400),
|
|
offerCount: offers, unsignedContractCount: contracts, isBookmarked: bookmarked)
|
|
}
|
|
}
|