refinement phase 4
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import { clientFetch } from '@/lib/api/client';
|
||||
import { unwrap, type ApiEnvelope, type Paginated } from '@/lib/api/types';
|
||||
import type { PriceUnit } from '@/services/catalog/types';
|
||||
import type { TrustBadge } from '@/services/verification/types';
|
||||
import { SEARCH_PAGE_SIZE } from '../constants';
|
||||
import type {
|
||||
NurseGender,
|
||||
NurseProfile,
|
||||
NurseProfileServiceRow,
|
||||
NurseReviewSnippet,
|
||||
NurseSearchFilters,
|
||||
NurseSearchResult,
|
||||
SearchApi,
|
||||
@@ -27,23 +28,44 @@ interface NurseSearchResultDto {
|
||||
totalCompletedBookings: number;
|
||||
cityId: number;
|
||||
districtId: number | null;
|
||||
/** REQ-012 — the C2 card identity, now denormalized into the index row. */
|
||||
nurseName: string | null;
|
||||
avatarUrl: string | null;
|
||||
distanceKm: number | null;
|
||||
}
|
||||
|
||||
/** The INO-membership credential type code (see b6 verification). */
|
||||
const INO_MEMBERSHIP_CODE = 'ino_membership';
|
||||
/** The b6/b7 aggregated `NursePublicProfileDto` (REQ-012) — the C3 profile payload. */
|
||||
interface NursePublicProfileDto {
|
||||
nurseId: number;
|
||||
nurseName: string;
|
||||
avatarUrl: string | null;
|
||||
bio: string;
|
||||
yearsExperience: number;
|
||||
averageRating: number;
|
||||
totalReviews: number;
|
||||
totalCompletedBookings: number;
|
||||
isVerified: boolean;
|
||||
inoMembership: boolean;
|
||||
attributeChips: string[];
|
||||
services: {
|
||||
variantId: number;
|
||||
displayName: string;
|
||||
priceIrr: string;
|
||||
priceUnit: PriceUnit;
|
||||
sessionCount: number | null;
|
||||
}[];
|
||||
latestReview: { rating: number; body: string | null; authorMasked: string | null; createdAt: string } | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Real HTTP implementation of the `SearchApi` seam (b7 `search/nurses`, b6 trust badge). Routes are
|
||||
* Real HTTP implementation of the `SearchApi` seam (b7 `search/nurses`, b6/b7 public profile). Routes are
|
||||
* action-style + snake_case; query params are snake_case per the contract; JSON fields are camelCase and
|
||||
* `clientFetch` returns the raw envelope, so we `unwrap()`.
|
||||
*
|
||||
* NOT the primary implementation this phase (`USE_SEARCH_MOCK = true`): b7's index row omits the nurse
|
||||
* **display name, avatar, and distance** the C2 card renders, and there is **no** aggregated
|
||||
* nurse-profile endpoint (name/bio/specialties/full services list/latest review) for C3 — only the b6
|
||||
* trust badge is public. Both gaps are filed in
|
||||
* `dev/shared-working-context/frontend/requests/for-backend.md`. This client maps everything b7/b6
|
||||
* currently provide (leaving the missing fields blank) so the swap is a single config flip once the
|
||||
* backend lands the join + profile route.
|
||||
* The PRIMARY implementation once `USE_SEARCH_MOCK = false` (refinement-phase-4). REQ-012 delivered the
|
||||
* discovery enrichment the C2 card + C3 profile need: `nurseName`/`avatarUrl`/`distanceKm` are now
|
||||
* denormalized onto `NurseSearchResultDto`, and `GET nurses/{id}/profile` aggregates identity + bio +
|
||||
* specialties + the full services list + the latest review. This client maps both 1:1.
|
||||
*/
|
||||
export const searchClientApi: SearchApi = {
|
||||
searchNurses: async (filters: NurseSearchFilters): Promise<Paginated<NurseSearchResult>> => {
|
||||
@@ -70,16 +92,15 @@ export const searchClientApi: SearchApi = {
|
||||
nurseId: dto.nurseId,
|
||||
variantId: dto.variantId,
|
||||
serviceCategoryId: dto.serviceCategoryId,
|
||||
// Gap (filed): b7 does not yet join the nurse's name/avatar; the card falls back to a label.
|
||||
nurseName: '',
|
||||
avatarUrl: null,
|
||||
// REQ-012 — identity denormalized onto the index row; card falls back to a label only when null.
|
||||
nurseName: dto.nurseName ?? '',
|
||||
avatarUrl: dto.avatarUrl,
|
||||
// Every returned row is searchable by the index invariant.
|
||||
isVerified: true,
|
||||
averageRating: dto.averageRating,
|
||||
totalReviews: dto.totalReviews,
|
||||
totalCompletedBookings: dto.totalCompletedBookings,
|
||||
// Gap (filed): no geo-distance in the index row yet.
|
||||
distanceKm: null,
|
||||
distanceKm: dto.distanceKm,
|
||||
priceFromIrr: dto.price,
|
||||
priceUnit: dto.priceUnit,
|
||||
nurseGender: dto.nurseGender,
|
||||
@@ -90,26 +111,43 @@ export const searchClientApi: SearchApi = {
|
||||
},
|
||||
|
||||
getNurseProfile: async (nurseId: number): Promise<NurseProfile> => {
|
||||
// Only the public trust badge is available today; the aggregated profile (name/bio/specialties/
|
||||
// services list/latest review) is filed for the backend. Compose what b6 exposes; leave the rest blank.
|
||||
const badge = unwrap(
|
||||
await clientFetch<ApiEnvelope<TrustBadge>>(`${NURSES_BASE}/${nurseId}/trust_badge`),
|
||||
const dto = unwrap(
|
||||
await clientFetch<ApiEnvelope<NursePublicProfileDto>>(`${NURSES_BASE}/${nurseId}/profile`),
|
||||
);
|
||||
|
||||
const services: NurseProfileServiceRow[] = dto.services.map((s) => ({
|
||||
variantId: s.variantId,
|
||||
displayName: s.displayName,
|
||||
priceIrr: s.priceIrr,
|
||||
priceUnit: s.priceUnit,
|
||||
sessionCount: s.sessionCount,
|
||||
}));
|
||||
|
||||
const latestReview: NurseReviewSnippet | null = dto.latestReview
|
||||
? {
|
||||
rating: dto.latestReview.rating,
|
||||
body: dto.latestReview.body ?? '',
|
||||
authorMasked: dto.latestReview.authorMasked ?? '',
|
||||
createdAt: dto.latestReview.createdAt,
|
||||
}
|
||||
: null;
|
||||
|
||||
return {
|
||||
nurseId: badge.nurseId,
|
||||
nurseName: '',
|
||||
avatarUrl: null,
|
||||
bio: null,
|
||||
yearsExperience: null,
|
||||
averageRating: 0,
|
||||
totalReviews: 0,
|
||||
totalCompletedBookings: 0,
|
||||
isVerified: badge.isVerified,
|
||||
inoMembership: badge.credentialTypes.includes(INO_MEMBERSHIP_CODE),
|
||||
attributeChips: badge.credentialTypes,
|
||||
services: [],
|
||||
latestReview: null,
|
||||
nurseId: dto.nurseId,
|
||||
nurseName: dto.nurseName,
|
||||
avatarUrl: dto.avatarUrl,
|
||||
bio: dto.bio || null,
|
||||
yearsExperience: dto.yearsExperience,
|
||||
averageRating: dto.averageRating,
|
||||
totalReviews: dto.totalReviews,
|
||||
totalCompletedBookings: dto.totalCompletedBookings,
|
||||
isVerified: dto.isVerified,
|
||||
inoMembership: dto.inoMembership,
|
||||
attributeChips: dto.attributeChips,
|
||||
services,
|
||||
latestReview,
|
||||
// Not carried by the public profile DTO; the same-gender intent for booking comes from the C1
|
||||
// filter carried through the query string, not this field. Unused by the C3 page.
|
||||
nurseGender: 'female',
|
||||
};
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user