[public-store-distance] Consume the public distance contract, stop labelling it as unavailable

Guest browsing showed 'Distância indisponível' on every store card and a
'--' tile on store detail, because the public locator response carries no
distance and StoreSummary.init(publicItem:) hardcoded nil.

Backend contract (docs/plans/public-store-distance-consumer.md): distance
is a number, never null, 0 means unavailable. Implemented app-side ahead of
the backend - every change is forward-compatible, and the part that removes
the broken label works with no backend at all.

- PublicStoreListItem / PublicStoreDetail: + distance (optional, so today's
  responses without the field still decode).
- StoreSummary.init(publicItem:): normalize the 0 sentinel to nil, so one
  representation of 'unknown' reaches the label and the max-distance filter.
- fetchStoreDetail: send state/city from GuestLocationStore by default -
  the server needs them to resolve the city centroid.
- formatDistance: empty string for nil/0/negative. It previously returned
  'Distância indisponível' for nil, and - found by the new test - '0 m' for
  0 and '-1000 m' for a negative.
- StoreCard: drop the distance segment and its '·' separator together,
  otherwise the row ended in a dangling separator.
- StoreDetailView: drop the DISTÂNCIA tile and its divider instead of '--'.

Also fixes the same label in the authenticated flow when the user declined
location and has no address coordinates.

Tests: PublicStoreDistanceTests (decode with/without the field, passthrough,
0 normalization) + the empty case in HomeViewFilteringTests.
This commit is contained in:
Daniel Arantes Loverde
2026-08-27 16:33:01 -03:00
parent 0f0672f18d
commit a0e4375914
10 changed files with 154 additions and 33 deletions

View File

@@ -63,5 +63,17 @@ func formatDistanceSwitchesUnitsAtOneKm() {
#expect(view.formatDistance(0.5) == "500 m")
#expect(view.formatDistance(1.0) == "1.0 km")
#expect(view.formatDistance(2.3) == "2.3 km")
#expect(view.formatDistance(nil) == "Distância indisponível")
}
/// Contract (docs/plans/public-store-distance-consumer.md): the API never
/// sends null - `0` means "no distance available". Both that and a missing
/// value render as an empty string so the caller omits the segment
/// entirely, instead of showing "Distância indisponível" or "0 m".
@Test("formatDistance renders nothing when the distance is missing or zero")
@MainActor
func formatDistanceIsEmptyWhenUnavailable() {
let view = makeHomeView()
#expect(view.formatDistance(nil) == "")
#expect(view.formatDistance(0) == "")
#expect(view.formatDistance(-1) == "")
}

View File

@@ -0,0 +1,49 @@
import Foundation
import Testing
@testable import PediFoods
/// Covers the public-locator distance contract documented in
/// docs/plans/public-store-distance-consumer.md: the server sends
/// `distance` as a number (never null) where `0` means "unavailable", and
/// the app collapses that to `nil` so a single representation of "unknown"
/// reaches the card label and the max-distance filter.
private func decode<T: Decodable>(_ type: T.Type, _ json: String) throws -> T {
try JSONDecoder().decode(T.self, from: Data(json.utf8))
}
@Test("PublicStoreListItem decodes the distance field")
func publicStoreListItemDecodesDistance() throws {
let item = try decode(PublicStoreListItem.self, #"{"id":"1","distance":3.4}"#)
#expect(item.distance == 3.4)
}
/// The field ships on the backend after the app - a response without it
/// must still decode rather than throwing.
@Test("PublicStoreListItem still decodes a response with no distance field")
func publicStoreListItemDecodesWithoutDistance() throws {
let item = try decode(PublicStoreListItem.self, #"{"id":"1"}"#)
#expect(item.distance == nil)
}
@Test("PublicStoreDetail decodes the distance field")
func publicStoreDetailDecodesDistance() throws {
let detail = try decode(PublicStoreDetail.self, #"{"id":"1","distance":1.2}"#)
#expect(detail.distance == 1.2)
let without = try decode(PublicStoreDetail.self, #"{"id":"1"}"#)
#expect(without.distance == nil)
}
@Test("StoreSummary carries a real public distance through")
func storeSummaryKeepsPublicDistance() throws {
let item = try decode(PublicStoreListItem.self, #"{"id":"1","name":"A","distance":2.5}"#)
#expect(StoreSummary(publicItem: item).distance == 2.5)
}
@Test("StoreSummary maps the unavailable sentinel and a missing distance to nil")
func storeSummaryNormalizesUnavailableDistance() throws {
let zero = try decode(PublicStoreListItem.self, #"{"id":"1","name":"A","distance":0}"#)
#expect(StoreSummary(publicItem: zero).distance == nil)
let absent = try decode(PublicStoreListItem.self, #"{"id":"1","name":"A"}"#)
#expect(StoreSummary(publicItem: absent).distance == nil)
}