migration

This commit is contained in:
Daniel Arantes Loverde
2026-08-11 13:22:02 -03:00
parent c4d6998595
commit 7702836fe7
231 changed files with 4329 additions and 1958 deletions

View File

@@ -0,0 +1,110 @@
import Foundation
/// Pre-login public store locator states/cities/stores-by-location, and
/// store detail. Always talks to `pedifoods.com.br` (the BFF), never
/// `atomenta.com.br` directly. See
/// docs/plans/app-migrate-atomenta-calls-to-pedifoods-bff.md.
///
/// Deliberately does not go through ApiService.sendEnvelope that treats
/// any session-expired-shaped response as the *customer* session expiring
/// (clears TokenStore, forces logout). A guest-session 401 here means the
/// short-lived guest JWT expired and must be silently refreshed instead.
final class PublicLocationService: @unchecked Sendable {
static let shared = PublicLocationService()
private let client: ApiClient
private let guestSession: GuestSessionService
init(client: ApiClient = ApiClient(), guestSession: GuestSessionService = .shared) {
self.client = client
self.guestSession = guestSession
}
/// States + their cities in one call keyed by UF, e.g. `{"SP": [...]}`.
func fetchLocations() async throws -> PublicLocationsResult {
try await sendGuestAuthed(path: "/api/public/locations")
}
func fetchStores(state: String, city: String) async throws -> [PublicStoreListItem] {
let query = [
URLQueryItem(name: "state", value: state),
URLQueryItem(name: "city", value: city)
]
return try await sendGuestAuthed(path: "/api/public/stores/by-location", query: query)
}
/// No guest token this route is fully public/unauthenticated per the doc.
func fetchStoreDetail(identifier: String) async throws -> PublicStoreDetail {
let encodedIdentifier = identifier.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? identifier
let req = ApiRequest(
path: "/api/public/store/\(encodedIdentifier)",
method: "GET",
module: .none,
requiresAuth: false,
baseURLOverride: ApiConfig.pediFoodsBFFURL
)
let envelope: ApiEnvelope<PublicStoreDetail> = try await client.send(req)
guard envelope.error == false, let result = envelope.result else {
throw NetworkError.invalidResponse
}
return result
}
/// No guest token same open pattern as fetchStoreDetail. Reuses the
/// existing StoreCatalogCategory/StoreCatalogProduct models directly:
/// the public response is the same category+products shape (with extra
/// computed inventory/stockStatus fields the decoder just ignores).
func fetchStoreProducts(storeId: String) async throws -> [StoreCatalogCategory] {
let encodedStoreId = storeId.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? storeId
let req = ApiRequest(
path: "/api/public/store/\(encodedStoreId)/products",
method: "GET",
module: .none,
requiresAuth: false,
baseURLOverride: ApiConfig.pediFoodsBFFURL
)
let envelope: ApiEnvelope<[StoreCatalogCategory]> = try await client.send(req)
guard envelope.error == false, let result = envelope.result else {
throw NetworkError.invalidResponse
}
return result
}
private func sendGuestAuthed<T: Decodable & Sendable>(path: String, query: [URLQueryItem] = []) async throws -> T {
let token = try await guestSession.validToken()
do {
return try await performGuestRequest(path: path, query: query, token: token)
} catch let error as NetworkError where isGuestSessionExpired(error) {
let refreshed = try await guestSession.invalidateAndRefresh()
return try await performGuestRequest(path: path, query: query, token: refreshed)
}
}
private func performGuestRequest<T: Decodable & Sendable>(path: String, query: [URLQueryItem], token: String) async throws -> T {
let req = ApiRequest(
path: path,
method: "GET",
module: .none,
requiresAuth: false,
queryItems: query,
baseURLOverride: ApiConfig.pediFoodsBFFURL,
customBearerToken: token
)
let envelope: ApiEnvelope<T> = try await client.send(req)
guard envelope.error == false, let result = envelope.result else {
throw NetworkError.invalidResponse
}
return result
}
private func isGuestSessionExpired(_ error: NetworkError) -> Bool {
switch error {
case .unauthorized:
return true
case .httpError(let statusCode, _):
return statusCode == 401
default:
return false
}
}
}