Skip to content

fix(api): match contacts by name without requiring DOB on import#107

Merged
Systemsaholic merged 24 commits intomainfrom
feature/issue-103-contact-dedup-v2
Mar 26, 2026
Merged

fix(api): match contacts by name without requiring DOB on import#107
Systemsaholic merged 24 commits intomainfrom
feature/issue-103-contact-dedup-v2

Conversation

@Systemsaholic
Copy link
Copy Markdown
Owner

@Systemsaholic Systemsaholic commented Mar 26, 2026

Summary

  • Fix duplicate contact creation during cruise booking import (v2)
  • Previous fix (PR fix(api): case-insensitive contact matching on cruise import #105) made matching case-insensitive but DOB was still a hard WHERE filter
  • If import data had a DOB but existing contact did not, query returned no match → duplicate created
  • Now: match by name first, use DOB only to disambiguate multiple matches
  • Additively updates DOB on matched contacts that are missing it

Test plan

  • TypeScript compilation passes
  • Traced the matching logic to confirm DOB-as-filter was the root cause
  • New logic: name match first, DOB disambiguates, never excludes

fixes #103

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Launched OTA consumer portal with travel deals marketplace, featuring hero showcase and dynamic pricing display.
    • Added advisor directory and profile pages showcasing expertise, curated deals, and trips.
    • Implemented multi-product search across flights, cruises, hotels, tours, and all-inclusive resorts.
    • Added marketing pages: About, Contact, Privacy Policy, and Terms of Service.
    • Introduced Open Graph images for social media sharing on deals and advisor profiles.
  • Infrastructure

    • Enabled server-side rendering with incremental static regeneration for improved performance and SEO.
    • Implemented cache revalidation system for real-time content updates.

Systemsaholic and others added 24 commits March 26, 2026 12:10
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>
@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 26, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
tailfire-client Error Error Mar 26, 2026 8:00pm
tailfire-ota Ready Ready Preview, Comment Mar 26, 2026 8:00pm

Request Review

@Systemsaholic Systemsaholic merged commit 484a05c into main Mar 26, 2026
1 of 3 checks passed
@Systemsaholic Systemsaholic deleted the feature/issue-103-contact-dedup-v2 branch March 26, 2026 19:59
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 26, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 40ca7faa-3ec0-436f-b4ea-f8353dad866b

📥 Commits

Reviewing files that changed from the base of the PR and between f9a952c and 464dbd5.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (134)
  • apps/api/src/advisor-profiles/advisor-profiles.service.ts
  • apps/api/src/common/common.module.ts
  • apps/api/src/common/services/ota-revalidation.service.ts
  • apps/api/src/cruise-booking/services/import-booking.service.ts
  • apps/api/src/deals/deals.service.ts
  • apps/ota/components.json
  • apps/ota/next.config.mjs
  • apps/ota/package.json
  • apps/ota/src/app/(marketing)/about/page.tsx
  • apps/ota/src/app/(marketing)/contact/page.tsx
  • apps/ota/src/app/(marketing)/page.tsx
  • apps/ota/src/app/(marketing)/privacy/page.tsx
  • apps/ota/src/app/(marketing)/terms/page.tsx
  • apps/ota/src/app/about/page.tsx
  • apps/ota/src/app/advisor/[slug]/deals/page.tsx
  • apps/ota/src/app/advisor/[slug]/opengraph-image.tsx
  • apps/ota/src/app/advisor/[slug]/page.tsx
  • apps/ota/src/app/advisor/[slug]/trips/[tripSlug]/page.tsx
  • apps/ota/src/app/advisor/[slug]/trips/page.tsx
  • apps/ota/src/app/advisors/layout.tsx
  • apps/ota/src/app/advisors/loading.tsx
  • apps/ota/src/app/advisors/page.tsx
  • apps/ota/src/app/api/revalidate/route.ts
  • apps/ota/src/app/auth/callback/route.ts
  • apps/ota/src/app/contact/page.tsx
  • apps/ota/src/app/deals/[slug]/opengraph-image.tsx
  • apps/ota/src/app/deals/[slug]/page.tsx
  • apps/ota/src/app/deals/loading.tsx
  • apps/ota/src/app/deals/page.tsx
  • apps/ota/src/app/error.tsx
  • apps/ota/src/app/globals.css
  • apps/ota/src/app/layout.tsx
  • apps/ota/src/app/not-found.tsx
  • apps/ota/src/app/page.tsx
  • apps/ota/src/app/privacy/page.tsx
  • apps/ota/src/app/providers.tsx
  • apps/ota/src/app/search/all-inclusives/page.tsx
  • apps/ota/src/app/search/cruises/loading.tsx
  • apps/ota/src/app/search/cruises/page.tsx
  • apps/ota/src/app/search/flights/loading.tsx
  • apps/ota/src/app/search/flights/page.tsx
  • apps/ota/src/app/search/hotels/loading.tsx
  • apps/ota/src/app/search/hotels/page.tsx
  • apps/ota/src/app/search/page.tsx
  • apps/ota/src/app/search/tours/loading.tsx
  • apps/ota/src/app/search/tours/page.tsx
  • apps/ota/src/app/terms/page.tsx
  • apps/ota/src/app/trips/[slug]/page.tsx
  • apps/ota/src/components/ClientPortalBadge.tsx
  • apps/ota/src/components/ConsultantBadge.tsx
  • apps/ota/src/components/ContactForm.tsx
  • apps/ota/src/components/FeaturedDestinations.tsx
  • apps/ota/src/components/HeroSection.tsx
  • apps/ota/src/components/LayoutWrapper.tsx
  • apps/ota/src/components/Navigation.tsx
  • apps/ota/src/components/PackagesSection.tsx
  • apps/ota/src/components/ServicesSection.tsx
  • apps/ota/src/components/TripInquiryDialog.tsx
  • apps/ota/src/components/advisor/advisor-bio-card.tsx
  • apps/ota/src/components/advisor/advisor-context-bar.tsx
  • apps/ota/src/components/advisor/advisor-destinations.tsx
  • apps/ota/src/components/advisor/advisor-directory-card.tsx
  • apps/ota/src/components/advisor/advisor-directory-filters.tsx
  • apps/ota/src/components/advisor/advisor-profile-header.tsx
  • apps/ota/src/components/advisor/advisor-reviews.tsx
  • apps/ota/src/components/advisor/trip-showcase-card.tsx
  • apps/ota/src/components/deals/deal-card.tsx
  • apps/ota/src/components/deals/deal-cta-section.tsx
  • apps/ota/src/components/deals/deal-filters.tsx
  • apps/ota/src/components/deals/deal-hero.tsx
  • apps/ota/src/components/deals/deal-quick-facts.tsx
  • apps/ota/src/components/deals/featured-deal-card.tsx
  • apps/ota/src/components/home/featured-deal.tsx
  • apps/ota/src/components/home/hero-section.tsx
  • apps/ota/src/components/home/product-grid.tsx
  • apps/ota/src/components/home/trust-bar.tsx
  • apps/ota/src/components/layout/contact-form.tsx
  • apps/ota/src/components/layout/content-page.tsx
  • apps/ota/src/components/layout/footer.tsx
  • apps/ota/src/components/layout/mobile-nav.tsx
  • apps/ota/src/components/layout/nav.tsx
  • apps/ota/src/components/search/cruise-result-card.tsx
  • apps/ota/src/components/search/cruise-search-form.tsx
  • apps/ota/src/components/search/filter-chips.tsx
  • apps/ota/src/components/search/flight-result-card.tsx
  • apps/ota/src/components/search/flight-search-form.tsx
  • apps/ota/src/components/search/hotel-result-card.tsx
  • apps/ota/src/components/search/hotel-search-form.tsx
  • apps/ota/src/components/search/port-pills.tsx
  • apps/ota/src/components/search/search-results-header.tsx
  • apps/ota/src/components/search/tour-result-card.tsx
  • apps/ota/src/components/ui/badge.tsx
  • apps/ota/src/components/ui/button.tsx
  • apps/ota/src/components/ui/card.tsx
  • apps/ota/src/components/ui/dialog.tsx
  • apps/ota/src/components/ui/dropdown-menu.tsx
  • apps/ota/src/components/ui/input.tsx
  • apps/ota/src/components/ui/label.tsx
  • apps/ota/src/components/ui/select.tsx
  • apps/ota/src/components/ui/separator.tsx
  • apps/ota/src/components/ui/sheet.tsx
  • apps/ota/src/components/ui/skeleton.tsx
  • apps/ota/src/components/ui/tabs.tsx
  • apps/ota/src/components/ui/textarea.tsx
  • apps/ota/src/context/consultant-context.tsx
  • apps/ota/src/data/consultants.ts
  • apps/ota/src/data/trips.ts
  • apps/ota/src/lib/api.ts
  • apps/ota/src/lib/config.ts
  • apps/ota/src/lib/consultant.ts
  • apps/ota/src/lib/fonts.ts
  • apps/ota/src/lib/format.ts
  • apps/ota/src/lib/mock-auth.ts
  • apps/ota/src/lib/mock-store.ts
  • apps/ota/src/lib/profile.ts
  • apps/ota/src/lib/requests.ts
  • apps/ota/src/lib/session.ts
  • apps/ota/src/lib/utils.ts
  • apps/ota/src/middleware.ts
  • apps/ota/src/types/advisor.ts
  • apps/ota/src/types/deal.ts
  • apps/ota/src/types/published-trip.ts
  • apps/ota/tailwind.config.ts
  • docs/superpowers/plans/2026-03-26-ota-consumer-portal.md
  • docs/superpowers/specs/2026-03-26-ota-consumer-portal-design.md
  • docs/superpowers/specs/2026-03-26-wordpress-content-inventory.md
  • packages/database/src/migrations/20260326173303_add_ota_tables.sql
  • packages/database/src/migrations/meta/_journal.json
  • packages/database/src/schema/advisor-featured-deals.schema.ts
  • packages/database/src/schema/advisor-profiles.schema.ts
  • packages/database/src/schema/deals.schema.ts
  • packages/database/src/schema/index.ts
  • packages/database/src/schema/ota-published-trips.schema.ts
  • packages/database/src/schema/ota-referrals.schema.ts

📝 Walkthrough

Walkthrough

This 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

Cohort / File(s) Summary
API Services
apps/api/src/advisor-profiles/advisor-profiles.service.ts, apps/api/src/deals/deals.service.ts
Two new NestJS services for managing advisor profiles (public listing/filtering, admin CRUD, TLN sync with OTA revalidation) and deals (public search with pagination/filtering, admin CRUD with upsert, scraper ingestion). Both trigger ISR cache revalidation on mutations.
OTA Revalidation Service
apps/api/src/common/services/ota-revalidation.service.ts, apps/api/src/common/common.module.ts
New fire-and-forget HTTP service for invalidating OTA ISR cache tags and paths; registered in CommonModule for dependency injection.
Contact Matching Fix
apps/api/src/cruise-booking/services/import-booking.service.ts
Updated contact resolution to select up to 10 matches by case-insensitive name, then disambiguate by DOB preference or use first match (addresses bug where existing contacts were created as duplicates).
Database Schema & Migrations
packages/database/src/migrations/20260326173303_add_ota_tables.sql, packages/database/src/schema/*.schema.ts (5 files), packages/database/src/schema/index.ts
New migration and Drizzle ORM schemas for advisor_profiles, deals, advisor_featured_deals, ota_referrals, ota_published_trips tables with relations and TypeScript inference types; migration journal updated.
OTA Home & Marketing Pages
apps/ota/src/app/(marketing)/page.tsx, apps/ota/src/app/(marketing)/{about,contact,privacy,terms}/page.tsx
New Next.js marketing site structure with ISR, async data fetching (featured deals), dynamic metadata, and content layouts replacing old static/client-side implementations.
OTA Advisor Pages
apps/ota/src/app/advisor/[slug]/{page.tsx,opengraph-image.tsx}, apps/ota/src/app/advisor/[slug]/{deals,trips,trips/[tripSlug]}/page.tsx
New advisor profile micro-sites with ISR revalidation, including profile display, curated deals/trips, and dynamic OG images; fetch advisor data from public API endpoints with cache tagging.
OTA Advisors Directory
apps/ota/src/app/advisors/{page.tsx,layout.tsx,loading.tsx}
Server-rendered advisor directory with filter-driven search (specialty/language), replaced old client-side mock-auth approach with real data fetching and ISR.
OTA Deals Pages
apps/ota/src/app/deals/{page.tsx,[slug]/{page.tsx,opengraph-image.tsx},loading.tsx}
New ISR deals listing with filtering by product type, and individual deal landing pages with dynamic OG images; fetch from public deals API.
OTA Search Pages
apps/ota/src/app/search/{cruises,flights,hotels,tours,all-inclusives}/{page.tsx,loading.tsx}
Multiple new server-rendered search experiences for each product category with catalog/service API integration, pagination, and result card components; removed old unified client-side search.
OTA Layout & Navigation
apps/ota/src/app/layout.tsx, apps/ota/src/components/layout/{nav.tsx,footer.tsx,mobile-nav.tsx,content-page.tsx,contact-form.tsx}
New root layout with Nav/Footer composition replacing Providers/LayoutWrapper; added fixed header navigation, mobile drawer, footer link structure, and content/contact form page templates.
OTA Error & 404 Pages
apps/ota/src/app/{error.tsx,not-found.tsx}, apps/ota/src/app/api/revalidate/route.ts, apps/ota/src/app/auth/callback/route.ts
New error boundary and 404 UI; added ISR revalidation webhook endpoint; removed Supabase OAuth callback (auth flow refactored).
OTA Advisor Components
apps/ota/src/components/advisor/{advisor-profile-header,advisor-bio-card,advisor-context-bar,advisor-destinations,advisor-reviews,advisor-directory-card,trip-showcase-card}.tsx
Seven new components for advisor profile UI including header/bio/certifications, destination expertise, review display, directory cards, and trip showcase cards with star ratings and pricing.
OTA Deal Components
apps/ota/src/components/deals/{deal-card,featured-deal-card,deal-hero,deal-quick-facts,deal-cta-section,deal-filters}.tsx
Six new deal UI components including listing cards, featured/hero sections, quick facts grid, filter chips, and CTA sections with conditional pricing display.
OTA Home Section Components
apps/ota/src/components/home/{featured-deal,hero-section,product-grid,trust-bar}.tsx
Four new homepage section components for featured deal display, hero input, product category grid, and trust indicators (non-interactive/presentational).
OTA Search Result Components
apps/ota/src/components/search/{cruise-result-card,flight-result-card,hotel-result-card,tour-result-card,cruise-search-form,flight-search-form,hotel-search-form,filter-chips,port-pills,search-results-header}.tsx
Ten new search-related components for result cards (with pricing/amenities/operator details), search forms (with date/location inputs and URL sync), filter UI, and result headers.
OTA UI Primitives
apps/ota/src/components/ui/{badge,button,card,dialog,dropdown-menu,input,label,select,separator,sheet,skeleton,tabs,textarea}.tsx
Thirteen new Base UI-based and Shadcn-style UI components for buttons, form inputs, dropdowns, dialogs, cards, tabs, and skeleton loaders with Tailwind styling and variants.
OTA API & Config
apps/ota/src/lib/{api.ts,config.ts,fonts.ts,format.ts,utils.ts}, apps/ota/src/types/{advisor,deal,published-trip}.ts, apps/ota/src/middleware.ts
New API client with fetch wrappers (catalogFetch, serviceFetch, publicFetch) and ApiError handling; config constants; font setup; formatting utilities (price, savings); TypeScript types for domain objects; middleware for session/referral cookie management.
OTA Dependencies & Config
apps/ota/package.json, apps/ota/next.config.mjs, apps/ota/components.json, apps/ota/tailwind.config.ts, apps/ota/src/app/globals.css
Updated package.json with AI SDK, Base UI, Shadcn CLI; added remote image domain whitelist in Next config; added Shadcn/components.json; extended Tailwind with font families; replaced global CSS with Tailwind directives and theme variables.
Removed OTA Legacy Code
apps/ota/src/{context,data,lib,components}/* (multiple files)
Removed consultant/auth context and mocks, trip/consultant data, session/profile/request hooks, old search/home/nav/form components to facilitate migration to server-rendered architecture.
Documentation
docs/superpowers/{plans,specs}/*.md
Three new documentation files: implementation roadmap for OTA consumer portal (8 phases with task checklists), design specification with UI decisions and component listings, and WordPress content inventory with migration/redirect strategy.

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
Loading
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 }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

🐰 Advisors now shine on their own little stage,
With deals, destinations, and trips all the page,
The OTA portal springs forth, bright and new,
Where travelers discover their journeys anew!
From TLN sync to the cache's clean sweep,
A rebuilt dream that the database keeps.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/issue-103-contact-dedup-v2

@Systemsaholic Systemsaholic mentioned this pull request Mar 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Contact

1 participant