Add lead workflows, PDF signing, AR projects and photo gallery integration

This commit is contained in:
Dennis Nemec
2026-11-08 17:49:39 +01:00
parent a343bde741
commit 8d18873412
82 changed files with 6401 additions and 34 deletions

View File

@ -0,0 +1,91 @@
import Foundation
import PDFKit
import UIKit
@MainActor
final class DefaultLeadDetailRepository: LeadDetailRepository {
private struct FileKey: Hashable {
let reference: LeadFileReference
let leadID: String
let salesCode: String
}
private let remoteService: any LeadDetailRemoteService
private let cacheDirectory: URL
private var cachedFiles: [FileKey: URL] = [:]
private var downloads: [FileKey: Task<URL, Error>] = [:]
init(remoteService: any LeadDetailRemoteService,
cacheDirectory: URL = FileManager.default.temporaryDirectory.appendingPathComponent("LeadFiles")) {
self.remoteService = remoteService
self.cacheDirectory = cacheDirectory
}
func detail(id: String, salesCode: String) async throws -> LeadDetail {
do {
return try await remoteService.fetchDetail(id: id, salesCode: salesCode).toDomain(expectedID: id)
} catch {
throw DefaultLeadRepository.map(error, leadID: id)
}
}
func file(_ reference: LeadFileReference, leadID: String, salesCode: String) async throws -> URL {
try Task.checkCancellation()
let key = FileKey(reference: reference, leadID: leadID, salesCode: salesCode)
if let url = cachedFiles[key], FileManager.default.fileExists(atPath: url.path) {
return url
}
if let download = downloads[key] {
let url = try await download.value
try Task.checkCancellation()
return url
}
let directory = cacheDirectory
let service = remoteService
// A shared download survives one viewer closing; that viewer checks cancellation below.
let download = Task {
let data: Data
do {
data = try await service.download(reference, leadID: leadID, salesCode: salesCode)
} catch LeadRemoteError.http(404) {
throw LeadFileError.notFound
} catch {
throw DefaultLeadRepository.map(error)
}
return try await Task.detached {
try Self.persist(data, kind: reference.kind, directory: directory)
}.value
}
downloads[key] = download
do {
let url = try await download.value
downloads[key] = nil
cachedFiles[key] = url
try Task.checkCancellation()
return url
} catch {
downloads[key] = nil
throw error
}
}
nonisolated private static func persist(_ data: Data, kind: LeadFileKind, directory: URL) throws -> URL {
switch kind {
case .pdf:
guard let document = PDFDocument(data: data), document.pageCount > 0, !document.isLocked else {
throw LeadFileError.invalidFile
}
case .image:
guard UIImage(data: data) != nil else { throw LeadFileError.invalidFile }
}
do {
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
let url = directory.appendingPathComponent(UUID().uuidString)
.appendingPathExtension(kind == .pdf ? "pdf" : "png")
try data.write(to: url, options: .atomic)
return url
} catch {
throw LeadFileError.storageUnavailable
}
}
}