fix(api): match contacts by name without requiring DOB on import#107
fix(api): match contacts by name without requiring DOB on import#107Systemsaholic merged 24 commits intomainfrom
Conversation
Mobile-first UI brainstorm results: - Homepage: hybrid AI-first (chat input primary CTA + product grid) - AI Chat: clean minimal, light background, inline product cards - Search Results: compact two-tone cards, no hero images, info-dense - Advisor Profile: full light, scrolling sections, destination carousel - Deals Listing: magazine grid with featured deal + 2-col tiles - Deal Landing: golden hour hero, floating price card, social-optimized - Advisor Deals: same DealCard component with personalization wrapper Brand palette: Phoenix Gold #C59746, Deep Charcoal #1A1A1A, Ember Red #B33939 Typography: Cinzel (titles), Lato (body) Reusable component inventory documented Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Complete audit of phoenixvoyages.ca (~160 pages): - 8 core pages to rewrite for OTA - 15 high-value SEO content pages to migrate - 10 cruise line landing pages (template from DB) - 24 supplier deal pages (auto-generated from deals table) - ~80 promo pages → bulk 301 redirect to /deals - 12 agent recruitment pages → keep on WordPress - 301 redirect map for SEO preservation - OG images already on cdn.tailfire.ca (reusable) - Domain strategy decision needed (same domain vs subdomain) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…d join app - OTA runs on phoenixvoyages.ca (primary domain), WordPress fully retired - join.phoenixvoyages.ca Next.js app retired → moves to /join/* in OTA - Agent recruitment pages (12 WP + 4 competitor comparisons) → /join/[slug] - Registration form with Stripe → /join/register - join.phoenixvoyages.ca/* gets 301 redirected to phoenixvoyages.ca/join/* - Updated route structure in design spec with /join route group - Updated content inventory: nothing stays on WordPress Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comprehensive implementation plan covering: - Phase 1: Foundation (scaffold, brand theming, layout, homepage) - Phase 2: Database schema + NestJS API modules - Phase 3: Deals system (listing, landing pages, VPS scraper) - Phase 4: Advisor micro-sites (TLN sync, attribution, directory) - Phase 5: Search pages (cruises, flights, hotels, tours, all-inclusives) - Phase 6: AI Concierge (chat widget, tools, rate limiting) - Phase 7: Agent recruitment (/join route group) - Phase 8: WordPress migration (301 redirects, sitemap, SEO) Review fixes applied: - Tailwind v3 (not v4) with existing phoenix preset - FusionAPI facade for live cruise pricing - NestJS revalidation triggers for ISR - Supabase middleware removal (no auth in Phase 1) - Error boundaries, loading states, not-found pages - Advisor OG image generation - /join/learn-more page Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove all mock data, placeholder components, and old context from apps/ota. Reset to a minimal Next.js 15 app with: - shadcn/ui initialized (13 baseline components) - AI SDK deps (ai, @ai-sdk/react) - Clean root layout with Cinzel + Lato brand fonts - No-op middleware (Supabase auth disabled for Phase 1) - cn utility in lib/utils.ts - Preserved lib/supabase/ for future auth integration - Preserved api/health route and tailwind phoenixPreset Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace shadcn default oklch color variables with Phoenix Voyages brand HSL palette. Set up Cinzel (display/serif) and Lato (sans-serif) via next/font/google with CSS variable wiring through Tailwind config. - globals.css: all CSS variables now use HSL matching brand guidelines (Phoenix Gold primary, Deep Charcoal foreground, Warm Ivory muted, Golden Hour accent, Ember Red destructive, Ash Gray borders) - lib/fonts.ts: Cinzel (400/700/900) and Lato (300/400/700) exports - layout.tsx: font variable classNames on <html>, font-sans on <body> - tailwind.config.ts: font-family overrides using CSS variables from next/font/google so font-sans and font-display resolve correctly - Removed broken @import "shadcn/tailwind.css" (no such export exists in the shadcn package; it's a Tailwind v4 convention) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add sticky top navigation with Phoenix Voyages branding, desktop nav links (Deals, Cruises, Flights, Hotels, Tours, Advisors, Join Us), and gold "Talk to AI" CTA button. Mobile view collapses to hamburger menu triggering a Sheet slide-out with stacked nav links. Footer with dark charcoal background, brand logo, tagline, link columns (Travel, Company, Legal), TICO registration placeholder, social icons, and copyright notice. Wire Nav and Footer into root layout.tsx around main content area. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace placeholder page with (marketing) route group homepage. Components: HeroSection (AI chat mock), ProductGrid (4 categories), FeaturedDeal (static placeholder), TrustBar (trust signals). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds ContentPage layout wrapper and four marketing routes under (marketing) route group: About (agency copy + core values), Contact (non-functional form with shadcn inputs/select/textarea), Terms (placeholder legal sections), and Privacy (placeholder policy sections with PIPEDA/TICO references). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add root error.tsx (client boundary with Try Again/Go Home), not-found.tsx (404 page with AI concierge CTA), and loading.tsx SSR-streaming skeletons for deals, search/cruises, search/flights, search/hotels, search/tours, and advisors. All files use Phoenix brand colors and Skeleton components; build passes cleanly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds config.ts (API_URL, OTA_SERVICE_KEY, CATALOG_API_KEY constants) and api.ts (catalogFetch, serviceFetch, publicFetch) with typed ApiError, 30s AbortController timeout, and Content-Type defaults. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…a_published_trips Add 5 new Drizzle ORM schemas and migration for the OTA consumer portal: - advisor_profiles: public-facing advisor profiles with TLN sync fields - deals: agency-scoped travel deals with external source dedup - advisor_featured_deals: join table linking advisors to featured deals - ota_referrals: referral session tracking with contact conversion - ota_published_trips: published trip listings from itinerary templates Includes partial unique index on deals(external_source, external_id) for external deal deduplication. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add /deals page with ISR (1-hour revalidation), DealFilters client component for product type filtering via URL search params, DealCard and FeaturedDealCard components matching Phoenix Voyages brand, Deal type definition, and price formatting utilities. Graceful fallback when API is unreachable. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add /deals/[slug] route with: - DealHero: full-width hero with golden hour gradient fallback, back/share buttons, LIMITED TIME badge, supplier name in gold, title overlay - Floating price card: starting-from price, savings percentage, Inquire CTA - DealQuickFacts: 3-column pill cards (type, dates, destination) - DealCtaSection: dual CTAs for AI concierge and advisor contact - Description section, destination pills, validity notice - OG image (1200x630) with golden hour gradient, deal title, price, branding - Server Component with ISR (1hr), generateMetadata, notFound() on miss Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces the static placeholder in FeaturedDeal with real data fetched server-side from /deals?isPublished=true&limit=1. Falls back gracefully to the static placeholder when the API is unavailable. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…dvisors
Add on-demand ISR revalidation so OTA pages update immediately when deal
or advisor data changes in the API, instead of waiting for the 1-hour
time-based revalidation.
OTA side:
- POST /api/revalidate endpoint (secret-protected) accepts tag or path
- publicFetch now supports Next.js `next: { tags }` for cache tagging
- Deal pages tagged with 'deals' / 'deal-{slug}' for targeted invalidation
API side:
- OtaRevalidationService (fire-and-forget, no-op when not configured)
- Registered in @global() CommonModule for use across all feature modules
- DealsService: triggers on create/update/delete/upsertFromScraper
- AdvisorProfilesService: triggers on update/setFeaturedDeals/triggerTlnSync
Doppler config needed: REVALIDATION_SECRET + OTA_REVALIDATION_URL
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add /advisor/[slug] profile page with ISR (1hr), dynamic metadata, and cache tags for on-demand revalidation. Components: profile header with avatar/stars/CTAs, bio card, destination expertise (horizontal scroll), and client reviews. OG image uses edge runtime with golden hour gradient and advisor photo/initials. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds /advisor/[slug]/deals Server Component page with ISR, AdvisorContextBar component (warm ivory bar with avatar, "Jane's Picks" heading, "View Profile" link), and DealCard personalization via optional advisorName prop that renders a gold star badge and "Ask [Name]'s AI" sub-line on each card. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds the published trips listing page (/advisor/[slug]/trips) and detail page (/advisor/[slug]/trips/[tripSlug]) along with the TripShowcaseCard component and PublishedTrip type. Both pages use ISR (revalidate=3600), AdvisorContextBar, graceful empty states, and flexible renderedSnapshot parsing with multiple key fallbacks for title, price, description, destinations, and itinerary days. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add /advisors directory page with specialty chip filters and language dropdown, server-side ISR fetching, and AdvisorDirectoryCard grid. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…image config - featured-deal.tsx: guard destinations[0] with optional chaining to fix string|undefined type error - deals/page.tsx + (marketing)/page.tsx: remove isPublished=true query param (API findPublished() already filters internally) - next.config.mjs: add images.remotePatterns for cdn.tailfire.ca, unsplash, agentprofiler, r2.dev - deal-filters.tsx: change "All-Inclusive"/"all-inclusive" to "Packages"/"package" to match API enum - deal-cta-section.tsx: change /#ai-concierge href to /contact (widget not yet built) - middleware.ts: fix pre-existing string|undefined return type error (pathMatch?.[1] guard) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…gration - Server Component page at /search/cruises with Suspense boundary - CruiseSearchForm: search text, cruise line, region, departure month filters - CruiseResultCard: two-tone card with dark gradient header (cruise line color palette), white body with meta/ports/price tiers - FilterChips: reusable horizontal scroll sort chips (Best Match, Price, Duration, Departure) - SearchResultsHeader: result count + AI placeholder link - PortPills: reusable rounded port/destination pills with +N overflow - Pagination with search param preservation - Error state with retry, empty state messaging - catalogFetch type fix: accept FetchOptions for Next.js revalidation - SEO metadata for the cruises page Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tasks 5.2–5.4: adds /search/flights, /search/hotels, /search/tours, and /search/all-inclusives following the exact cruise search pattern (Server Component + Suspense, serviceFetch/catalogFetch, two-tone result cards, graceful error states, SEO metadata). - flight-search-form: origin/destination (IATA), departure+return date, adults, children, travel class selectors - flight-result-card: dark header with airline/route/price, white body with outbound+return itinerary rows, duration, stops, Inquire CTA - hotel-search-form: city-code destination, check-in/out, guests, rooms - hotel-result-card: dark header with hotel name, star rating, location; white body with board basis badge, room type, Inquire CTA - tour-result-card: operator name, tour name, destinations, duration badge, advisor-only 'Request Quote' CTA (no price displayed) - tours page: keyword search form (plain GET, no client state needed), catalogFetch against /tour-repository/tours, pagination - all-inclusives page: AI concierge CTA + responsive Softvoyage iframe Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous fix made matching case-insensitive but DOB was still a hard WHERE filter. If the import data had a DOB but the existing contact did not, the query returned no match and a duplicate was created. Now: match by name first (case-insensitive), then use DOB only to disambiguate when multiple name matches exist. Also additively updates DOB on existing contacts that are missing it. fixes #103 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (134)
📝 WalkthroughWalkthroughThis PR introduces a comprehensive OTA consumer portal rebuild for phoenixvoyages.ca, including new backend services for advisor profiles and deals management with TLN sync and ISR revalidation, database schema for OTA-specific tables, a modernized Next.js frontend with server-side rendering and ISR, new UI component library, and detailed implementation/specification documentation. Changes
Sequence Diagram(s)sequenceDiagram
participant Admin as Admin Portal
participant API as NestJS API
participant DB as Database
participant TLN as TLN Service
participant OTA as OTA Cache
Admin->>API: triggerTlnSync(advisorId)
API->>DB: Fetch advisor profile
DB-->>API: AdvisorProfile
alt Profile exists & has tlnProfileUrl
API->>TLN: syncFromTln(tlnProfileUrl)
TLN-->>API: TLN synced data
API->>DB: Update profile with TLN fields
DB-->>API: Updated
API->>OTA: revalidateTag("advisors")
API->>OTA: revalidatePath("/advisor/{slug}")
OTA-->>API: Revalidated
API-->>Admin: Success + Updated profile
else
API-->>Admin: NotFoundException
end
sequenceDiagram
participant Client as OTA Client
participant Next as Next.js Server
participant API as API Endpoint
participant DB as Database
Client->>Next: GET /deals/[slug]
Note over Next: ISR revalidate=3600
Next->>API: publicFetch /deals/{slug}
Note over API: next.tags: ["deals", "deal-{slug}"]
API->>DB: SELECT * FROM deals WHERE slug
DB-->>API: Deal record
API-->>Next: Deal JSON
Next->>Next: generateMetadata (from deal)
Next->>Client: HTML + OG metadata
Note over Client,Next: On update via /api/revalidate webhook
Client->>Next: POST /api/revalidate?tag=deals
Next->>Next: revalidateTag("deals")
Next-->>Client: { revalidated: true }
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
Summary
Test plan
fixes #103
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Infrastructure