# 💰 Cost Optimization Strategy - Crowdsourcing System

**Problème:** Chaque requête coûte de l'argent (Google Places + OpenAI)  
**Risque:** Users pourraient ruiner l'app en faisant des milliers de recherches

---

## 🚨 Le Vrai Coût

### Google Places API
- **Text Search:** $32 / 1000 requests
- **Place Details:** $17 / 1000 requests
- **Photos:** $7 / 1000 requests

**Total par POI découvert:** ~$0.056 (5.6 centimes)

### OpenAI API
- **GPT-4o-mini:** $0.15 / 1M tokens input, $0.60 / 1M tokens output
- **Summary moyen:** ~200 tokens (input) + 100 tokens (output) = ~$0.00009

**Total par POI découvert:** ~$0.057 (5.7 centimes)

### Scénario Catastrophe
Si 1000 users font 100 recherches/jour (100k POIs/jour):
- **Coût quotidien:** 100,000 × $0.057 = **$5,700/jour**
- **Coût mensuel:** **$171,000/mois** 💀

---

## ✅ Solutions d'Optimisation

### 1. 🎯 Filtrage Intelligent (PRIORITÉ #1)

**Ne découvrir QUE les POIs de haute qualité**

```typescript
// apps/web/src/modules/poi/services/poiFilterService.ts

interface QualityFilter {
  minRating: number              // Minimum 4.0/5
  minReviews: number             // Minimum 10 avis
  requiresPhotos: boolean        // Doit avoir des photos
  excludeCategories: string[]    // Catégories à exclure
  maxPriceLevel?: number         // Prix max (1-4)
}

const DEFAULT_QUALITY_FILTER: QualityFilter = {
  minRating: 4.0,
  minReviews: 10,
  requiresPhotos: true,
  excludeCategories: ['real_estate_agency', 'car_rental', 'atm', 'bank'],
  maxPriceLevel: 4
}

function shouldDiscoverPOI(place: GooglePlaceResult): boolean {
  const filter = DEFAULT_QUALITY_FILTER
  
  // Rating trop bas
  if (!place.rating || place.rating < filter.minRating) {
    return false
  }
  
  // Pas assez d'avis (pas fiable)
  if (!place.user_ratings_total || place.user_ratings_total < filter.minReviews) {
    return false
  }
  
  // Pas de photos (POI peu intéressant)
  if (filter.requiresPhotos && (!place.photos || place.photos.length === 0)) {
    return false
  }
  
  // Catégorie exclue (banques, agences immobilières, etc.)
  if (place.types?.some(type => filter.excludeCategories.includes(type))) {
    return false
  }
  
  // Prix trop élevé (optionnel)
  if (filter.maxPriceLevel && place.price_level && place.price_level > filter.maxPriceLevel) {
    return false
  }
  
  return true
}

// Résultat: Réduit de 80-90% le nombre de POIs découverts
// Exemple: 100 résultats → 10-20 POIs de qualité découverts
```

**Impact:** **Réduit les coûts de 80-90%** ✅

---

### 2. 🔄 Cache Agressif (PRIORITÉ #1)

**Vérifier d'abord la DB interne AVANT d'appeler les APIs**

```typescript
// apps/web/src/modules/poi/services/poiCacheService.ts

import { collection, query, where, getDocs, limit } from 'firebase/firestore'

interface CachedPOI {
  id: string
  placeId: string
  name: string
  data: POI
  cachedAt: Date
}

const CACHE_TTL_DAYS = 30 // Cache valide 30 jours

async function checkInternalCache(placeId: string): Promise<POI | null> {
  // 1. Check exact placeId match
  const exactMatch = await findByPlaceId(placeId)
  if (exactMatch) {
    console.log('✅ Cache HIT (exact match)')
    return exactMatch
  }
  
  // 2. Check proximity match (même lieu, placeId différent)
  // (Google peut renvoyer différents placeIds pour le même lieu)
  const proximityMatch = await findByProximity(placeId)
  if (proximityMatch) {
    console.log('✅ Cache HIT (proximity match)')
    return proximityMatch
  }
  
  console.log('❌ Cache MISS - API call required')
  return null
}

async function findByPlaceId(placeId: string): Promise<POI | null> {
  const q = query(
    collection(db, 'poi_database'),
    where('placeId', '==', placeId),
    limit(1)
  )
  
  const snapshot = await getDocs(q)
  
  if (snapshot.empty) return null
  
  const poi = { id: snapshot.docs[0].id, ...snapshot.docs[0].data() } as POI
  
  // Vérifier si cache expiré
  const cacheAge = Date.now() - poi.updatedAt.getTime()
  const cacheTTL = CACHE_TTL_DAYS * 24 * 60 * 60 * 1000
  
  if (cacheAge > cacheTTL) {
    console.log('⚠️ Cache expired, refresh needed')
    return null
  }
  
  return poi
}

async function findByProximity(placeId: string): Promise<POI | null> {
  // Fetch basic info from Google (juste le placeId, gratuit)
  const basicInfo = await getBasicPlaceInfo(placeId)
  
  if (!basicInfo?.geometry?.location) return null
  
  // Search in internal DB by coordinates (rayon 50m)
  const q = query(
    collection(db, 'poi_database'),
    where('coordinates.lat', '>=', basicInfo.geometry.location.lat - 0.0005),
    where('coordinates.lat', '<=', basicInfo.geometry.location.lat + 0.0005),
    limit(10)
  )
  
  const snapshot = await getDocs(q)
  
  for (const doc of snapshot.docs) {
    const poi = { id: doc.id, ...doc.data() } as POI
    
    // Calculate distance
    const distance = calculateDistance(
      basicInfo.geometry.location,
      poi.coordinates
    )
    
    // Si < 50m et même nom → c'est le même lieu
    if (distance < 50 && isSimilarName(basicInfo.name, poi.name)) {
      console.log(`✅ Proximity match found (${distance}m away)`)
      return poi
    }
  }
  
  return null
}

function isSimilarName(name1: string, name2: string): boolean {
  const normalize = (str: string) => 
    str.toLowerCase()
       .replace(/[^a-z0-9]/g, '')
  
  const n1 = normalize(name1)
  const n2 = normalize(name2)
  
  // Levenshtein distance ou simple includes
  return n1.includes(n2) || n2.includes(n1) || n1 === n2
}

function calculateDistance(
  coord1: { lat: number, lng: number },
  coord2: { lat: number, lng: number }
): number {
  // Haversine formula
  const R = 6371e3 // Earth radius in meters
  const φ1 = coord1.lat * Math.PI / 180
  const φ2 = coord2.lat * Math.PI / 180
  const Δφ = (coord2.lat - coord1.lat) * Math.PI / 180
  const Δλ = (coord2.lng - coord1.lng) * Math.PI / 180
  
  const a = Math.sin(Δφ/2) * Math.sin(Δφ/2) +
            Math.cos(φ1) * Math.cos(φ2) *
            Math.sin(Δλ/2) * Math.sin(Δλ/2)
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a))
  
  return R * c // Distance in meters
}
```

**Impact:** **Réduit les API calls de 60-80% après quelques semaines** ✅

---

### 3. 📊 Rate Limiting par User (PRIORITÉ #1)

**Limiter le nombre de découvertes par user**

```typescript
// apps/web/src/modules/poi/services/rateLimitService.ts

interface UserRateLimits {
  userId: string
  discoveriesToday: number
  discoveriesThisMonth: number
  lastDiscoveryAt: Date
  tier: 'free' | 'premium'
}

const RATE_LIMITS = {
  free: {
    perDay: 10,          // 10 découvertes/jour max
    perMonth: 100,       // 100 découvertes/mois max
    cooldown: 60_000     // 1 min entre chaque découverte
  },
  premium: {
    perDay: 50,
    perMonth: 500,
    cooldown: 10_000     // 10s entre chaque découverte
  }
}

async function checkRateLimit(userId: string): Promise<RateLimitResult> {
  const limits = await getUserRateLimits(userId)
  const tier = limits.tier || 'free'
  const config = RATE_LIMITS[tier]
  
  // Check cooldown (anti-spam)
  const timeSinceLastDiscovery = Date.now() - limits.lastDiscoveryAt.getTime()
  if (timeSinceLastDiscovery < config.cooldown) {
    return {
      allowed: false,
      reason: 'cooldown',
      message: `Attendez ${Math.ceil((config.cooldown - timeSinceLastDiscovery) / 1000)}s`,
      retryAfter: config.cooldown - timeSinceLastDiscovery
    }
  }
  
  // Check daily limit
  if (limits.discoveriesToday >= config.perDay) {
    return {
      allowed: false,
      reason: 'daily_limit',
      message: `Limite quotidienne atteinte (${config.perDay}/jour)`,
      retryAfter: getTimeUntilMidnight()
    }
  }
  
  // Check monthly limit
  if (limits.discoveriesThisMonth >= config.perMonth) {
    return {
      allowed: false,
      reason: 'monthly_limit',
      message: `Limite mensuelle atteinte (${config.perMonth}/mois)`,
      retryAfter: getTimeUntilNextMonth()
    }
  }
  
  return {
    allowed: true,
    remaining: {
      today: config.perDay - limits.discoveriesToday,
      month: config.perMonth - limits.discoveriesThisMonth
    }
  }
}

async function incrementDiscoveryCount(userId: string): Promise<void> {
  const limitsRef = doc(db, 'user_rate_limits', userId)
  
  await updateDoc(limitsRef, {
    discoveriesToday: increment(1),
    discoveriesThisMonth: increment(1),
    lastDiscoveryAt: new Date()
  })
}

// Reset daily counters (Cloud Function, cron job)
export async function resetDailyLimits() {
  const snapshot = await getDocs(collection(db, 'user_rate_limits'))
  
  const batch = writeBatch(db)
  
  for (const doc of snapshot.docs) {
    batch.update(doc.ref, {
      discoveriesToday: 0
    })
  }
  
  await batch.commit()
}
```

**Impact:** **Bloque les abus, coût prévisible** ✅

---

### 4. 🎯 Discovery Sélectif (PRIORITÉ #2)

**Ne découvrir que sur action volontaire du user**

```typescript
// apps/web/src/modules/search/services/searchService.ts

async function searchPlaces(query: string, location: LatLng) {
  // 1. Search Google Places (toujours fait, léger)
  const results = await googlePlacesTextSearch(query, location)
  
  // 2. Check internal cache for each result
  const enrichedResults = await Promise.all(
    results.map(async (place) => {
      const cached = await checkInternalCache(place.place_id)
      
      return {
        ...place,
        isCached: !!cached,
        cachedData: cached,
        canDiscover: !cached && shouldDiscoverPOI(place)
      }
    })
  )
  
  return enrichedResults
}

// UI affiche un badge "Découvrir" sur les POIs non-cachés
// User clique volontairement pour déclencher la découverte
async function handleDiscoverPOI(placeId: string, userId: string) {
  // Check rate limit
  const rateLimitCheck = await checkRateLimit(userId)
  if (!rateLimitCheck.allowed) {
    toast.warning(rateLimitCheck.message)
    return
  }
  
  // Discover POI (API calls)
  const discovery = await discoverPOI(placeId, basicInfo, userId)
  
  // Increment rate limit counter
  await incrementDiscoveryCount(userId)
  
  // Award points
  toast.success(discovery.message)
}
```

**Impact:** **User contrôle = coût contrôlé** ✅

---

### 5. 💸 Batching & Lazy Loading (PRIORITÉ #2)

**Ne charger que le nécessaire**

```typescript
// apps/web/src/modules/poi/services/batchDiscoveryService.ts

interface BatchDiscoveryOptions {
  maxConcurrent: number    // Max 5 découvertes simultanées
  priority: 'quality' | 'proximity'
  autoDiscover: boolean    // Auto-découvrir ou demander user
}

async function batchDiscoverPOIs(
  places: GooglePlaceResult[], 
  userId: string,
  options: BatchDiscoveryOptions
): Promise<DiscoveryResult[]> {
  
  // 1. Filter par qualité
  const candidates = places.filter(shouldDiscoverPOI)
  
  // 2. Sort by priority
  if (options.priority === 'quality') {
    candidates.sort((a, b) => (b.rating || 0) - (a.rating || 0))
  } else {
    // Sort by proximity to user location
    candidates.sort((a, b) => 
      calculateDistance(userLocation, a.geometry.location) -
      calculateDistance(userLocation, b.geometry.location)
    )
  }
  
  // 3. Take only top N
  const topCandidates = candidates.slice(0, options.maxConcurrent)
  
  // 4. Check rate limit
  const rateLimitCheck = await checkRateLimit(userId)
  if (!rateLimitCheck.allowed) {
    return []
  }
  
  // 5. Discover with concurrency limit
  const results = []
  for (const place of topCandidates) {
    try {
      const discovery = await discoverPOI(place.place_id, place, userId)
      results.push(discovery)
      
      // Increment counter
      await incrementDiscoveryCount(userId)
      
      // Check if limit reached
      const newLimit = await checkRateLimit(userId)
      if (!newLimit.allowed) {
        console.log('Rate limit reached, stopping batch discovery')
        break
      }
      
    } catch (error) {
      console.error('Error discovering POI:', error)
    }
  }
  
  return results
}
```

**Impact:** **Max 5 POIs/recherche = coût maîtrisé** ✅

---

### 6. 🤖 AI Batch Processing (PRIORITÉ #3)

**Générer plusieurs summaries en 1 seul appel OpenAI**

```typescript
// apps/web/src/modules/poi/services/batchAIService.ts

async function batchGenerateAISummaries(pois: Partial<POI>[]): Promise<string[]> {
  // Au lieu de N appels OpenAI, faire 1 seul appel avec N POIs
  
  const prompt = `
Génère un résumé court (2-3 phrases) pour chacun de ces lieux.
Format: JSON array de strings.

Lieux:
${pois.map((poi, i) => `
${i + 1}. ${poi.name}
   Catégorie: ${poi.category}
   Adresse: ${poi.address}
   Note: ${poi.rating}/5
`).join('\n')}

Réponds UNIQUEMENT avec un JSON array de summaries, dans le même ordre.
`

  const response = await openai.chat.completions.create({
    model: 'gpt-4o-mini',
    messages: [{ role: 'user', content: prompt }],
    max_tokens: 500,
    temperature: 0.7,
    response_format: { type: 'json_object' }
  })
  
  const summaries = JSON.parse(response.choices[0].message.content || '[]')
  
  return summaries
}

// Coût: 1 appel pour 10 POIs au lieu de 10 appels
// Économie: 90% sur l'overhead API
```

**Impact:** **Réduit les coûts OpenAI de 70-80%** ✅

---

### 7. 📈 Budget Monitoring (PRIORITÉ #1)

**Alertes automatiques si budget dépassé**

```typescript
// apps/api/src/modules/monitoring/budgetMonitor.ts

interface BudgetConfig {
  dailyLimit: number      // $50/jour max
  monthlyLimit: number    // $1000/mois max
  alertThreshold: number  // 80% du budget
}

const BUDGET_CONFIG: BudgetConfig = {
  dailyLimit: 50,
  monthlyLimit: 1000,
  alertThreshold: 0.8
}

interface APIUsage {
  date: Date
  googlePlacesCalls: number
  openAICalls: number
  estimatedCost: number
}

async function trackAPIUsage(
  service: 'google_places' | 'openai',
  operation: string,
  estimatedCost: number
): Promise<void> {
  
  const today = new Date().toISOString().split('T')[0]
  const usageRef = doc(db, 'api_usage', today)
  
  await updateDoc(usageRef, {
    [`${service}_calls`]: increment(1),
    estimatedCost: increment(estimatedCost),
    updatedAt: new Date()
  })
  
  // Check if budget exceeded
  const usage = (await getDoc(usageRef)).data() as APIUsage
  
  if (usage.estimatedCost >= BUDGET_CONFIG.dailyLimit * BUDGET_CONFIG.alertThreshold) {
    await sendBudgetAlert('daily', usage.estimatedCost, BUDGET_CONFIG.dailyLimit)
  }
  
  // Check monthly budget
  const monthlyUsage = await getMonthlyUsage()
  if (monthlyUsage >= BUDGET_CONFIG.monthlyLimit * BUDGET_CONFIG.alertThreshold) {
    await sendBudgetAlert('monthly', monthlyUsage, BUDGET_CONFIG.monthlyLimit)
  }
}

async function sendBudgetAlert(
  period: 'daily' | 'monthly',
  current: number,
  limit: number
): Promise<void> {
  const percentage = (current / limit) * 100
  
  // Send email to admin
  await sendEmail({
    to: 'admin@myroadtrip.com',
    subject: `⚠️ Budget Alert: ${period} limit at ${percentage.toFixed(0)}%`,
    body: `
Current ${period} spending: $${current.toFixed(2)}
Limit: $${limit}
Percentage: ${percentage.toFixed(0)}%

Action required: Review API usage or increase budget.
    `
  })
  
  // If 100% exceeded, disable API calls
  if (current >= limit) {
    await disableAPIServices(period)
  }
}

async function disableAPIServices(period: 'daily' | 'monthly'): Promise<void> {
  // Set flag in Firestore to disable POI discovery
  await setDoc(doc(db, 'system_config', 'api_limits'), {
    discoveryDisabled: true,
    disabledReason: `${period}_budget_exceeded`,
    disabledAt: new Date()
  })
  
  console.error(`🚨 API services disabled due to ${period} budget exceeded`)
}
```

**Impact:** **Protection contre dépassement budgétaire** ✅

---

## 📊 Architecture Optimisée Finale

```typescript
// apps/web/src/modules/poi/services/optimizedDiscoveryService.ts

async function optimizedDiscoverPOI(
  placeId: string,
  basicInfo: Partial<POI>,
  userId: string
): Promise<DiscoveryResult> {
  
  // ✅ STEP 1: Check internal cache (FREE)
  const cached = await checkInternalCache(placeId)
  if (cached) {
    return {
      poi: cached,
      isNew: false,
      pointsEarned: 2,
      message: '✓ POI vérifié ! +2 points',
      cost: 0  // FREE
    }
  }
  
  // ✅ STEP 2: Check quality filter (FREE)
  if (!shouldDiscoverPOI(basicInfo)) {
    return {
      poi: null,
      isNew: false,
      pointsEarned: 0,
      message: 'POI de faible qualité, non découvert',
      cost: 0  // FREE
    }
  }
  
  // ✅ STEP 3: Check rate limit (FREE)
  const rateLimitCheck = await checkRateLimit(userId)
  if (!rateLimitCheck.allowed) {
    throw new Error(rateLimitCheck.message)
  }
  
  // ✅ STEP 4: Check budget (FREE)
  const budgetCheck = await checkDailyBudget()
  if (!budgetCheck.allowed) {
    throw new Error('Budget quotidien dépassé')
  }
  
  // 💰 STEP 5: Google Places Details API ($0.017)
  const details = await fetchGooglePlaceDetails(placeId)
  await trackAPIUsage('google_places', 'details', 0.017)
  
  // 💰 STEP 6: AI Summary (batched, $0.0001)
  const aiSummary = await generateAISummary(details)
  await trackAPIUsage('openai', 'summary', 0.0001)
  
  // ✅ STEP 7: Save to Firestore (FREE with generous limits)
  const newPOI = await savePOI({
    ...details,
    aiSummary,
    discoveredBy: userId,
    discoveredAt: new Date()
  })
  
  // ✅ STEP 8: Award points (FREE)
  await awardPoints(userId, { type: 'discovery', points: 10, poiId: newPOI.id })
  
  // ✅ STEP 9: Increment rate limit (FREE)
  await incrementDiscoveryCount(userId)
  
  return {
    poi: newPOI,
    isNew: true,
    pointsEarned: 10,
    message: '🎉 Nouveau POI découvert ! +10 points',
    cost: 0.0171  // $0.017 Google + $0.0001 OpenAI
  }
}
```

---

## 💰 Coût Réel Optimisé

### Avec toutes les optimisations

| Étape | Optimisation | Réduction |
|-------|--------------|-----------|
| 1. Filtrage qualité | 80% POIs rejetés | -80% |
| 2. Cache interne | 60% cache hits | -60% |
| 3. Rate limiting | 10 max/jour/user | -90% |
| 4. Batch AI | 10 POIs/call | -70% |
| 5. Discovery sélectif | User triggered | -50% |

**Réduction totale:** ~**95-98%** 🎉

### Coût optimisé

**Sans optimisation:**
- 100 users × 100 recherches/jour = 10,000 POIs/jour
- Coût: 10,000 × $0.057 = **$570/jour** ($17,100/mois)

**Avec optimisations:**
- 100 users × 10 découvertes/jour = 1,000 POIs/jour
- 60% cache hits = 400 API calls/jour
- Coût: 400 × $0.017 = **$6.80/jour** ($204/mois) ✅

**Économie:** **98% de réduction** 🚀

---

## 🎯 Stratégie de Monétisation

### Free Tier
- 10 découvertes/jour
- 100 découvertes/mois
- Cooldown 1 min
- **Coût max:** $2/user/mois

### Premium Tier ($9.99/mois)
- 50 découvertes/jour
- 500 découvertes/mois
- Cooldown 10s
- Accès prioritaire au cache
- **Coût max:** $10/user/mois
- **Marge:** -$0.01/user/mois 😅

### Pro Tier ($29.99/mois)
- Découvertes illimitées*
- Pas de cooldown
- Accès API interne
- **Coût max:** $30/user/mois
- **Marge:** $0/mois

*Avec rate limiting raisonnable (1000/mois)

---

## 🚀 Recommandations Finales

### Phase 1: MVP (Lancement)
1. ✅ Cache agressif (Firestore)
2. ✅ Filtrage qualité strict
3. ✅ Rate limiting 10/jour
4. ✅ Budget monitoring
5. ✅ Discovery volontaire uniquement

**Coût estimé:** $200-500/mois pour 500-1000 users actifs

### Phase 2: Croissance
1. ✅ Batch AI processing
2. ✅ Proximity cache
3. ✅ Premium tier ($9.99/mois)
4. ✅ CDN pour photos

**Coût estimé:** $1000-2000/mois pour 5000-10000 users

### Phase 3: Scale
1. ✅ Dedicated cache server (Redis)
2. ✅ Edge caching (Cloudflare)
3. ✅ AI summary batching avancé
4. ✅ Sponsorships (POIs payants)

**Coût estimé:** $5000/mois pour 50000 users

---

## 🔒 Circuit Breakers

**Protection ultime contre la ruine**

```typescript
// Si budget quotidien dépassé → Disable discovery
if (dailyBudget >= $50) {
  disableDiscovery()
  sendAlert('Budget quotidien dépassé')
}

// Si budget mensuel dépassé → Emergency mode
if (monthlyBudget >= $1000) {
  enableEmergencyMode()  // Cache only, no API calls
  sendAlert('URGENT: Budget mensuel dépassé')
}

// Si spike détecté → Throttle
if (requestsLastHour > 1000) {
  enableThrottling()  // Cooldown 5 min
  sendAlert('Spike détecté, throttling activé')
}
```

---

## ✅ Conclusion

### Avec ces optimisations:

✅ **Coût prévisible:** $200-500/mois au début  
✅ **Protection anti-ruine:** Rate limits + budget monitoring  
✅ **Scalable:** Cache + batching  
✅ **Viable économiquement:** Avec premium tier  

### Sans optimisations:

❌ **Coût imprévisible:** $5,000-$170,000/mois  
❌ **Risque de ruine:** Users illimités  
❌ **Non scalable:** API calls explosifs  
❌ **Non viable:** Impossible à monétiser  

**Verdict:** Système crowdsourcing viable **SI ET SEULEMENT SI** toutes les optimisations sont implémentées dès le début. 🚀

---

**Priorité absolue:**
1. Rate limiting (10/jour)
2. Cache interne (Firestore)
3. Filtrage qualité (rating > 4.0)
4. Budget monitoring ($50/jour max)
5. Discovery volontaire

**Ne jamais faire:**
❌ Auto-discovery sur toutes les recherches  
❌ Pas de rate limits  
❌ Pas de budget monitoring  
❌ Discovery sans filtrage qualité  
