payment and list

This commit is contained in:
Daniel Arantes Loverde
2026-02-24 10:19:47 -03:00
parent 3a3dc7217b
commit ca83a275ac
51 changed files with 1645 additions and 256 deletions

View File

@@ -21,6 +21,9 @@ struct ApiEnvelope<T: Decodable>: Decodable {
final class ApiService {
private let client: ApiClient
private var tokenStore: TokenStore
private let profileCachePrefix = "api:profile:"
private let ordersCachePrefix = "api:orders:"
private let publicCategoriesCacheKey = "api:public-categories"
init(client: ApiClient = ApiClient(), tokenStore: TokenStore = DefaultTokenStore()) {
self.client = client
@@ -63,9 +66,18 @@ final class ApiService {
private func expireSession(_ message: String?) {
tokenStore.clear()
AppContentCache.shared.invalidate(prefix: profileCachePrefix)
AppContentCache.shared.invalidate(prefix: ordersCachePrefix)
AppContentCache.shared.invalidate(prefix: publicCategoriesCacheKey)
NotificationCenter.default.post(name: .sessionExpired, object: message)
}
private func scopedCacheSuffix() -> String {
let jwt = tokenStore.jwt ?? "anonymous"
if jwt.count <= 16 { return jwt }
return String(jwt.prefix(16))
}
// MARK: - Auth
func registerCustomer(name: String, email: String, phoneNumber: String, birthDate: String? = nil) async throws -> ApiEnvelope<RegistrationResult> {
@@ -121,9 +133,19 @@ final class ApiService {
return try JSONSerialization.data(withJSONObject: payload, options: [])
}
func profile() async throws -> ApiEnvelope<CustomerProfile> {
func profile(forceRefresh: Bool = false) async throws -> ApiEnvelope<CustomerProfile> {
let cacheKey = "\(profileCachePrefix)\(scopedCacheSuffix())"
if forceRefresh == false,
let cached: ApiEnvelope<CustomerProfile> = AppContentCache.shared.value(for: cacheKey, as: ApiEnvelope<CustomerProfile>.self) {
return cached
}
let req = ApiRequest(path: "/api/customer/profile", method: "GET", module: .customer, requiresAuth: true)
return try await sendEnvelope(req)
let envelope: ApiEnvelope<CustomerProfile> = try await sendEnvelope(req)
if envelope.error == false, envelope.result != nil {
AppContentCache.shared.set(envelope, for: cacheKey, ttl: AppCacheTTL.twoHours)
}
return envelope
}
func addCustomerAddress(_ address: CustomerAddress) async throws -> ApiEnvelope<CustomerProfile> {
@@ -131,7 +153,7 @@ final class ApiService {
}
func saveCustomerAddress(_ address: CustomerAddress, replacingAddressId: String?) async throws -> ApiEnvelope<CustomerProfile> {
let currentProfile = try await profile()
let currentProfile = try await profile(forceRefresh: true)
guard currentProfile.error == false, let customer = currentProfile.result else {
throw NetworkError.invalidResponse
}
@@ -151,7 +173,7 @@ final class ApiService {
}
func deleteCustomerAddress(_ address: CustomerAddress) async throws -> ApiEnvelope<CustomerProfile> {
let currentProfile = try await profile()
let currentProfile = try await profile(forceRefresh: true)
guard currentProfile.error == false, let customer = currentProfile.result else {
throw NetworkError.invalidResponse
}
@@ -189,7 +211,14 @@ final class ApiService {
let payload = CustomerProfileUpdatePayload(addressBook: addressBook)
let body = try JSONEncoder().encode(payload)
let req = ApiRequest(path: "/api/customer/\(customerId)", method: "POST", module: .customer, requiresAuth: true, body: body)
return try await sendEnvelope(req)
let envelope: ApiEnvelope<CustomerProfile> = try await sendEnvelope(req)
let scopedProfileKey = "\(profileCachePrefix)\(scopedCacheSuffix())"
if envelope.error == false, envelope.result != nil {
AppContentCache.shared.set(envelope, for: scopedProfileKey, ttl: AppCacheTTL.twoHours)
} else {
AppContentCache.shared.invalidate(prefix: profileCachePrefix)
}
return envelope
}
private func matchesAddress(_ lhs: CustomerAddress, _ rhs: CustomerAddress) -> Bool {
@@ -205,9 +234,18 @@ final class ApiService {
// MARK: - Stores
func listPublicCategories() async throws -> ApiEnvelope<[PublicCategory]> {
func listPublicCategories(forceRefresh: Bool = false) async throws -> ApiEnvelope<[PublicCategory]> {
if forceRefresh == false,
let cached: ApiEnvelope<[PublicCategory]> = AppContentCache.shared.value(for: publicCategoriesCacheKey, as: ApiEnvelope<[PublicCategory]>.self) {
return cached
}
let req = ApiRequest(path: "/api/public/categories", method: "GET", module: .none, requiresAuth: false)
return try await sendEnvelope(req)
let envelope: ApiEnvelope<[PublicCategory]> = try await sendEnvelope(req)
if envelope.error == false {
AppContentCache.shared.set(envelope, for: publicCategoriesCacheKey, ttl: AppCacheTTL.twoHours)
}
return envelope
}
func listStores(lat: Double? = nil, lng: Double? = nil, category: String? = nil, search: String? = nil) async throws -> ApiEnvelope<[StoreSummary]> {
@@ -239,7 +277,11 @@ final class ApiService {
func createOrder(storeId: String, payload: CreateOrderPayload) async throws -> ApiEnvelope<CreateOrderResult> {
let body = try JSONEncoder().encode(payload)
let req = ApiRequest(path: "/api/store/\(storeId)/orders", method: "POST", module: .store, requiresAuth: true, body: body)
return try await sendEnvelope(req)
let envelope: ApiEnvelope<CreateOrderResult> = try await sendEnvelope(req)
if envelope.error == false {
AppContentCache.shared.invalidate(prefix: ordersCachePrefix)
}
return envelope
}
func validateDeliveryAddress(storeId: String, payload: ValidateDeliveryAddressPayload) async throws -> ApiEnvelope<ValidateDeliveryAddressResult> {
@@ -248,9 +290,19 @@ final class ApiService {
return try await sendEnvelope(req)
}
func listOrders() async throws -> ApiEnvelope<[AppOrderSummary]> {
func listOrders(forceRefresh: Bool = false) async throws -> ApiEnvelope<[AppOrderSummary]> {
let cacheKey = "\(ordersCachePrefix)\(scopedCacheSuffix())"
if forceRefresh == false,
let cached: ApiEnvelope<[AppOrderSummary]> = AppContentCache.shared.value(for: cacheKey, as: ApiEnvelope<[AppOrderSummary]>.self) {
return cached
}
let req = ApiRequest(path: "/api/app/orders", method: "GET", module: .app, requiresAuth: true)
return try await sendEnvelope(req)
let envelope: ApiEnvelope<[AppOrderSummary]> = try await sendEnvelope(req)
if envelope.error == false {
AppContentCache.shared.set(envelope, for: cacheKey, ttl: AppCacheTTL.twoHours)
}
return envelope
}
func publicOrder(orderId: String) async throws -> ApiEnvelope<PublicOrderResult> {
@@ -259,6 +311,27 @@ final class ApiService {
}
}
extension ApiService {
static func decodeFlexibleString<K: CodingKey>(from container: KeyedDecodingContainer<K>, keys: [K]) -> String? {
for key in keys {
if let value = try? container.decode(String.self, forKey: key) {
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty == false {
return trimmed
}
}
if let asInt = try? container.decode(Int.self, forKey: key) {
return String(asInt)
}
if let asDouble = try? container.decode(Double.self, forKey: key) {
if asDouble.rounded() == asDouble {
return String(Int(asDouble))
}
return String(asDouble)
}
}
return nil
}
static func decodeFlexibleDouble<K: CodingKey>(from container: KeyedDecodingContainer<K>, keys: [K]) -> Double? {
for key in keys {
if let value = try? container.decode(Double.self, forKey: key) {