F&I Products Pricing Documentation
#Complete Guide to Finance & Insurance Product Pricing Flow
#Table of Contents
- Overview
- Product Categories & Pricing Strategies
- Payment Selection & Product Availability
- Architecture Overview
- Frontend Flow (driveway-ui)
- Backend Flow (products-api)
- The FI-Rate Cache Collection
- Quote Submission Flow
- External Services & Integrations
- Pricing Configuration
- API Reference
- Troubleshooting Guide
#1. Overview
The F&I (Finance & Insurance) Products system provides vehicle protection plans and services to Driveway customers. The pricing architecture uses a hybrid approach:
- Static Pricing: Some products have fixed "Starting at" prices displayed immediately
- Dynamic Pricing: Other products require a quote request based on vehicle-specific factors
- Cached Pricing: PEN (ProvenNet) products are cached with pre-calculated markups
#Key Components
+------------------+ +------------------+ +------------------+| | | | | || driveway-ui | -----> | products-api | -----> | PEN API || (Frontend) | | (Backend) | | (Rate Provider) || | | | | |+------------------+ +------------------+ +------------------+| || v| +------------------+| | |+----------------> | dealer-lead-api |(Quote Submit) | (FNI Leads) || |+------------------+
#2. Product Categories & Pricing Strategies
#Products Overview
| Product | Display Price | Pricing Type | Source |
|---|---|---|---|
| Lifetime Oil & Filter | "Starting at $799" | Static | MongoDB (fi-product-v2) |
| Valet Service | "Starting at $599" | Static | MongoDB (fi-product-v2) |
| Vehicle Service Contract | "Price on request" | Dynamic | PEN API + Markup |
| Appearance Protection | "Price on request" | Dynamic | Quote Required |
| Pre-Paid Maintenance | "Price on request" | Dynamic | Quote Required |
#Pricing Strategy Breakdown
Static Pricing (Lithia Products)
Products with known, fixed pricing stored directly in MongoDB:
Lifetime Oil & Filter├── Collection: fi-product-v2├── Price: Stored as totalInCents├── Display: "Starting at $799"└── Filtering: State, vehicle make, fuel type restrictionsValet Service├── Collection: fi-product-v2├── Price: Stored as totalInCents├── Display: "Starting at $599"└── Filtering: Zip code availability check
Dynamic Pricing (PEN Products)
Products requiring real-time or cached rate lookups:
Vehicle Service Contract (WARRANTY)├── Source: PEN API├── Calculation: dealerCost + $1,500 markup (non-FL)├── Florida: Uses PEN retail price (no markup)└── Cache: fi-rate collectionTire & Wheel├── Source: PEN API├── Calculation: dealerCost × 2.2 multiplier (non-FL)├── Florida: Uses PEN retail price└── Cache: fi-rate collectionVehicle Loss Protection├── Source: PEN API (availability check only)├── Calculation: Fixed $1,095 (config-based)├── Availability: FINANCE payment type only└── Cache: fi-rate collection
#3. Payment Selection & Product Availability
#What is paymentSelection?
paymentSelection refers to HOW THE CUSTOMER IS PAYING FOR THE CAR, not how they're paying for the F&I products.
It determines which F&I protection products should be offered based on the vehicle purchase/payment method.
#The Three Payment Types
| Payment Type | Definition | Business Context |
|---|---|---|
| CASH | Customer pays full price upfront for the car | No loan - customer owns vehicle outright immediately |
| FINANCE | Customer takes out a loan to buy the car | Has monthly payments, owes money on the vehicle |
| LEASE | Customer rents the vehicle from the leasing company | Doesn't own the car, returns it at end of lease term |
#Product Availability Matrix
┌─────────────────────────────────────────────────────────────────────────────┐│ PRODUCT AVAILABILITY BY PAYMENT SELECTION │├────────────────────────────┬────────┬─────────┬───────┬────────────────────┤│ Product │ CASH │ FINANCE │ LEASE │ Not Selected │├────────────────────────────┼────────┼─────────┼───────┼────────────────────┤│ Lifetime Oil & Filter │ ✓ │ ✓ │ ✗ │ ✓ ││ Driveway Valet Service │ ✓ │ ✓ │ ✗ │ ✓ ││ Vehicle Service Contract │ ✓ │ ✓ │ ✗ │ ✓ ││ Tire & Wheel Coverage │ ✓ │ ✓ │ ✓ │ ✓ ││ Vehicle Loss Protection │ ✗ │ ✓ │ ✗ │ ✓ │└────────────────────────────┴────────┴─────────┴───────┴────────────────────┘
#Backend Filtering Logic
File: Products-Api/library/src/main/kotlin/com/detroitlabs/products/services/fi/FIService.kt
val productTypes = when (paymentSelection) {CartPaymentSelection.CASH -> listOf(FIProduct.Type.LIFETIME_OIL,FIProduct.Type.DRIVEWAY_VALET_SERVICE,FIProduct.Type.WARRANTY,FIProduct.Type.TIRE_WHEEL// Note: NO Vehicle Loss Protection for CASH)CartPaymentSelection.FINANCE, null -> listOf(FIProduct.Type.LIFETIME_OIL,FIProduct.Type.DRIVEWAY_VALET_SERVICE,FIProduct.Type.WARRANTY,FIProduct.Type.TIRE_WHEEL,FIProduct.Type.VEHICLE_LOSS_PROTECTION // ONLY available for FINANCE)CartPaymentSelection.LEASE -> listOf(FIProduct.Type.TIRE_WHEEL // ONLY Tire & Wheel for LEASE)}
#Business Logic Behind Each Restriction
Vehicle Loss Protection (VLP) - FINANCE ONLY
VLP is exclusively available for FINANCE because:
- Purpose: Covers the "gap" between insurance payout and loan balance if the car is totaled
- CASH buyers: Own the car outright - no loan balance to protect, insurance covers full value
- LEASE customers: The leasing company carries gap protection through their own policies
- FINANCE customers: Have an active loan - if car is totaled, insurance may pay less than what's owed
Example Scenario:┌─────────────────────────────────────────────────────────────────┐│ Car Value: $30,000 ││ Loan Balance: $28,000 ││ Car is totaled in accident ││ Insurance pays: $25,000 (depreciated value) ││ ││ WITHOUT VLP: Customer owes $3,000 on a car they can't drive ││ WITH VLP: Gap coverage pays the $3,000 difference │└─────────────────────────────────────────────────────────────────┘
LEASE - Only Tire & Wheel Available
Lease customers only see Tire & Wheel because:
- Maintenance Coverage: Leases often include maintenance packages from the lessor
- Ownership: Customer doesn't own the vehicle - limited modifications/additions
- Return Condition: Lease agreements specify return condition requirements
- Tire & Wheel Universal: Protects against road hazards regardless of ownership type
CASH - All Products Except VLP
Cash buyers can purchase all products except VLP because:
- Full Ownership: They own the car and can add any protection they want
- No Gap to Protect: Without a loan, there's no "gap" between value and owed amount
- Lifetime Products: LOF and other protection products work regardless of payment method
#Enum Definition
File: Products-Api/library/src/main/kotlin/com/detroitlabs/products/models/CartPaymentSelection.kt
enum class CartPaymentSelection {CASH,FINANCE,LEASE}
#API Parameter
The paymentSelection is passed as a query parameter to the estimator endpoint:
GET /estimator/fi-products?vehicleId=123&userZip=97201&paymentSelection=FINANCE
When paymentSelection is:
- Not provided (null): Defaults to FINANCE behavior (all products returned)
- CASH: Returns all products except VLP
- FINANCE: Returns all products including VLP
- LEASE: Returns only Tire & Wheel
#4. Architecture Overview
#System Architecture Diagram
FRONTEND (driveway-ui)┌─────────────────────────────────────────────────────────────────────────────┐│ ││ Care & Protection Page ││ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ││ │ Product Cards │ │ Get Quote Modal │ │ Quote Form │ ││ │ - Static prices │ │ - Product info │ │ - Mileage │ ││ │ - "Get a Quote" │ │ - Benefits │ │ - Contact info │ ││ └────────┬────────┘ └────────┬────────┘ │ - Address │ ││ │ │ └────────┬────────┘ ││ │ │ │ │└───────────┼────────────────────┼────────────────────┼───────────────────────┘│ │ │v v v┌─────────────────────────────────────────────────────────────────────────────┐│ API CALLS ││ ││ GET /estimator/fi-products POST /fni-leads ││ ├── vehicleId ├── address ││ ├── userZip ├── dealershipId ││ └── userState ├── contact info ││ ├── vehicle info ││ └── productType[] │└─────────────────────────────────────────────────────────────────────────────┘│ │v v┌─────────────────────────────┐ ┌─────────────────────────────────────────┐│ PRODUCTS-API │ │ DEALER-LEAD-API ││ │ │ ││ ┌───────────────────────┐ │ │ Receives quote requests and ││ │ FI Service │ │ │ routes them to dealerships ││ │ - Cache lookup │ │ │ ││ │ - Price calculation │ │ └─────────────────────────────────────────┘│ │ - Markup application │ ││ └───────────┬───────────┘ ││ │ ││ ┌───────────v───────────┐ ││ │ MongoDB │ ││ │ - fi-rate (cache) │ ││ │ - fi-product-v2 │ ││ │ - pen-dealers │ ││ └───────────┬───────────┘ ││ │ │└──────────────┼──────────────┘│v (cache miss only)┌─────────────────────────────┐│ PEN API ││ (ProvenNet SOAP Service) ││ ││ - getASBRates() ││ - getRateBookCodes() ││ - Dealer cost + retail │└─────────────────────────────┘
#5. Frontend Flow (driveway-ui)
#5.1 Care and Protection Page
Location: UI/src/pages/mydriveway/care-and-protection.tsx
The page displays all F&I products using QuickActionCard components.
// Product cards displayed[{ name: "Lifetime Oil & Filter", price: "Starting at $799", type: "LOF" },{ name: "Valet Service", price: "Starting at $599", type: "VALET" },{ name: "Vehicle Service Contract", price: "Price on request", type: "VSC" },{ name: "Appearance Protection", price: "Price on request", type: "APPEARANCE" },{ name: "Pre-Paid Maintenance", price: "Price on request", type: "PPM" }]
#5.2 Get Quote Modal Flow
Component: UI/src/account/components/MyVehicles/GetQuoteModal/GetQuoteWrapper.tsx
Step 1: PRODUCT_INFO┌─────────────────────────────────────────┐│ VEHICLE SERVICE CONTRACT ││ ││ We'll cover repairs big and small... ││ ││ What's included? ││ ✓ Covers qualifying repairs ││ ✓ Emergency roadside assistance ││ ✓ Rental car reimbursement ││ ││ Terms: ││ — Coverage up to 96 months ││ — A small deductible may apply ││ ││ [Get a Quote] │└─────────────────────────────────────────┘│vStep 2: PICK_VEHICLE (if multiple vehicles)┌─────────────────────────────────────────┐│ Select a Vehicle ││ ││ ○ 2022 Toyota Camry ││ ○ 2021 Honda Accord ││ ││ [Continue] │└─────────────────────────────────────────┘│vStep 3: QUOTE_FORM┌─────────────────────────────────────────┐│ What's your current mileage? ││ ┌──────────────────────────────────┐ ││ │ 12,000 │ ││ └──────────────────────────────────┘ ││ ││ How can we reach you? ││ First Name: [Cristian ] ││ Last Name: [Torres ] ││ Email: [email@example.com] ││ Phone: [(323) 799-2985 ] ││ ││ What's your current address? ││ Address: [222 East 41st Street] ││ City: [New York ] ││ State: [NY] Zip: [10017] ││ ││ [Continue] │└─────────────────────────────────────────┘│vStep 4: SERVICE_CENTER┌─────────────────────────────────────────┐│ Select a Service Center ││ ││ ○ Driveway Portland ││ ○ Driveway Seattle ││ ││ [Continue] │└─────────────────────────────────────────┘│vStep 5: SUBMIT_QUOTE┌─────────────────────────────────────────┐│ Review Your Request ││ ││ Product: Vehicle Service Contract ││ Vehicle: 2022 Toyota Camry ││ Mileage: 12,000 ││ Service Center: Driveway Portland ││ ││ [Submit Request] │└─────────────────────────────────────────┘
#5.3 Form Validation Rules
File: UI/src/account/components/MyVehicles/GetQuoteModal/sub-components/QuoteForm/useGetQuoteForm.tsx
| Field | Validation Rules |
|---|---|
| Current Mileage | Positive number, required |
| First Name | Alphanumeric, max 15 chars, required |
| Last Name | Alphanumeric, max 25 chars, required |
| Valid email format, required | |
| Phone | Exactly 10 digits, required |
| Address Line 1 | Alphanumeric, max 100 chars, required |
| Address Line 2 | Alphanumeric, optional |
| City | Alphabetic only, max 20 chars, required |
| State | Exactly 2 letters, required |
| Zip Code | Exactly 5 numeric digits, required |
#5.4 API Endpoints Used
File: UI/src/apis/endpoints.ts
// Products API - Get estimated pricesProductsEndpoints.ESTIMATED_PRODUCTS = "/estimator/fi-products"// Dealer Lead API - Submit quote requestDealerLeadEndpoints.FNI_LEADS = "/fni-leads"
#6. Backend Flow (products-api)
#6.1 Estimator Endpoint
Route: GET /estimator/fi-products
File: Products-Api/routes/src/main/kotlin/com/detroitlabs/products/routes/FIRoutes.kt
Request Flow:┌────────────────┐│ GET /estimator ││ /fi-products ││ ││ ?vehicleId=X ││ &userZip=Y ││ &userState=Z │└───────┬────────┘│v┌───────────────────────────────────────────────────────────────┐│ FIHandler ││ ││ 1. Parse request parameters ││ 2. If no userState, call Geocoding API with zip ││ 3. Call FIService.getEstimatedFIProducts() │└───────────────────────────────────────────────────────────────┘│v┌───────────────────────────────────────────────────────────────┐│ FIService ││ ││ For Lithia Products (LOF, Valet): ││ ├── Query fi-product-v2 collection ││ ├── Filter by state, make, fuel type, zip ││ └── Return totalInCents directly ││ ││ For PEN Products (VSC, T&W, VLP): ││ ├── Check fi-rate cache by VIN ││ ├── If cache hit & not expired (< 60 days): ││ │ └── Return cached product with markup applied ││ ├── If cache miss or expired: ││ │ ├── Estimator mode: Return empty (no PEN call) ││ │ └── Non-estimate: Call PEN API ││ └── Apply markup configuration to dealer cost │└───────────────────────────────────────────────────────────────┘│v┌───────────────────────────────────────────────────────────────┐│ Response ││ ││ [ ││ { ││ "type": "LIFETIME_OIL", ││ "totalInCents": 79900, ││ "termMonths": "lifetime", ││ "deductible": null ││ }, ││ { ││ "type": "VEHICLE_SERVICE_CONTRACT", ││ "totalInCents": 259500, ││ "termMonths": "96", ││ "termMiles": "120000", ││ "deductible": 100 ││ } ││ ] │└───────────────────────────────────────────────────────────────┘
#6.2 Price Calculation Logic
File: Products-Api/library/src/main/kotlin/com/detroitlabs/products/services/fi/FIService.kt
Vehicle Service Contract (WARRANTY)
fun getVehicleServiceContractPenProduct(vehicle: Vehicle): PenFiProduct {val cachedProduct = getCachedProduct(vehicle.vin)if (vehicle.dealershipState in retailPriceOverrideForStates) { // Floridareturn PenFiProduct(totalInCents = cachedProduct.retailPriceInCents // Use PEN retail)} else {val markupAmount = config.vehicleServiceContract.markupAmount // $1,500return PenFiProduct(totalInCents = cachedProduct.dealerCostInCents + markupAmount)}}
Tire & Wheel
fun getTireAndWheelsPenProduct(vehicle: Vehicle): PenFiProduct {val cachedProduct = getCachedProduct(vehicle.vin)if (vehicle.dealershipState in retailPriceOverrideForStates) { // Floridareturn PenFiProduct(totalInCents = cachedProduct.retailPriceInCents // Use PEN retail)} else {val multiplier = config.tireAndWheel.markupMultiplier // 2.2xreturn PenFiProduct(totalInCents = (cachedProduct.dealerCostInCents * multiplier).toInt())}}
Vehicle Loss Protection
fun getVehicleLossProtectionPenProduct(vehicle: Vehicle): PenFiProduct {val cachedProduct = getCachedProduct(vehicle.vin)if (cachedProduct.rateExist) {val fixedAmount = config.vehicleLossProtection.fixedAmount // $1,095return PenFiProduct(totalInCents = fixedAmount // Ignores PEN price, uses config)} else {return PenFiProduct(rateExist = false)}}
#7. The FI-Rate Cache Collection
#7.1 Collection Details
Database: products
Collection: fi-rate
Document Type: CachedFIProducts
File: Products-Api/library/src/main/kotlin/com/detroitlabs/products/models/database/CachedFIProducts.kt
#7.2 Document Schema
@Document("fi-rate")data class CachedFIProducts(@Idval id: String,@Indexed(unique = true)val vin: String,val status: Status, // ACTIVE, DELETED, UPDATE_ERRORval vehicleId: String?,val priceInCents: Int?,val mileage: Int,val year: Int?,val make: String,val model: String?,val dealerState: String,// Cache validityval effectiveDate: Instant, // Cache expiry check (>= 60 days = expired)val nextRefreshDate: Instant?, // Cron scheduler reference// Cached PEN product rates (with markup already applied)val vehicleServiceContractRate: PenFiProduct?,val tireAndWheelRate: PenFiProduct?,val vehicleLossProtectionRate: PenFiProduct?,// Error trackingval updateFailureCount: Int,// Audit timestampsval createdOn: Instant,val updatedOn: Instant?)
#7.3 Cache Behavior Matrix
| Scenario | Cache Action | PEN Call? | Result |
|---|---|---|---|
| Cache hit, not expired (< 60 days) | Read | No | Return cached product |
| Cache hit, expired (>= 60 days) | Skip | Estimator: No | Return empty |
| Cache miss | Skip | Estimator: No | Return empty |
| Inventory ADD event | Create | Yes | New cache doc |
| Inventory UPDATE event | Update | Yes | Refresh cache |
| Cron refresh (due) | Update | Yes | Update rates only |
#7.4 Cache Population Sources
┌─────────────────────────────────────┐│ CACHE POPULATION │└─────────────────────────────────────┘│┌──────────────────────────┼──────────────────────────┐│ │ │v v v┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐│ Inventory Delta │ │ FI Rate Cron │ │ Manual Refresh ││ Receiver │ │ │ │ ││ │ │ Every 5 minutes │ │ Admin endpoints ││ Azure Service Bus │ │ Batch: 100 vehicles│ │ /test/fi-Rate/* ││ - ADD: Create doc │ │ Refresh oldest due │ │ - expire-products ││ - UPDATE: Refresh │ │ first │ │ - update-markup ││ - DELETE: Soft del │ │ │ │ - refresh │└─────────────────────┘ └─────────────────────┘ └─────────────────────┘
#8. Quote Submission Flow
#8.1 Complete Quote Journey
┌─────────────────────────────────────────────────────────────────────────────┐│ USER JOURNEY │└─────────────────────────────────────────────────────────────────────────────┘Step 1: Browse Products┌────────────────────────────────────────────────────────────────────────────┐│ User visits Care & Protection page ││ ↓ ││ System displays product cards with: ││ - Static prices for LOF ($799) and Valet ($599) ││ - "Price on request" for VSC, Appearance, PPM │└────────────────────────────────────────────────────────────────────────────┘│vStep 2: Select Product┌────────────────────────────────────────────────────────────────────────────┐│ User clicks "Get a Quote" on Vehicle Service Contract ││ ↓ ││ ProductInfoModal opens showing: ││ - Product description ││ - What's included (coverage details) ││ - Terms and conditions │└────────────────────────────────────────────────────────────────────────────┘│vStep 3: Vehicle Selection┌────────────────────────────────────────────────────────────────────────────┐│ System checks user's garage ││ ↓ ││ IF multiple vehicles: Show PickVehicle modal ││ IF no vehicles: Show AddVehicle modal ││ IF single vehicle: Auto-select and proceed │└────────────────────────────────────────────────────────────────────────────┘│vStep 4: Quote Form┌────────────────────────────────────────────────────────────────────────────┐│ QuoteForm modal collects: ││ ││ ┌─────────────────────┐ ││ │ Current Mileage │ ← Vehicle-specific pricing factor ││ └─────────────────────┘ ││ ││ ┌─────────────────────┐ ││ │ Contact Information │ ← For quote follow-up ││ │ - First/Last Name │ ││ │ - Email │ ││ │ - Phone │ ││ └─────────────────────┘ ││ ││ ┌─────────────────────┐ ││ │ Address │ ← For state-based pricing/availability ││ │ - Street │ ││ │ - City/State/Zip │ ││ └─────────────────────┘ │└────────────────────────────────────────────────────────────────────────────┘│vStep 5: Service Center Selection┌────────────────────────────────────────────────────────────────────────────┐│ ServiceCenterStep shows nearby dealerships ││ ↓ ││ User selects preferred service location ││ This determines dealershipId for the lead │└────────────────────────────────────────────────────────────────────────────┘│vStep 6: Submit Quote Request┌────────────────────────────────────────────────────────────────────────────┐│ SubmitQuote modal shows summary ││ ↓ ││ User clicks "Submit Request" ││ ↓ ││ POST /fni-leads with payload: ││ { ││ address: { line1, line2, city, state, zip }, ││ dealershipId: 12345, ││ email: "user@email.com", ││ firstName: "Cristian", ││ lastName: "Torres", ││ phone: "3237992985", ││ vehicle: { make, model, modelYear, vin, mileage }, ││ productType: ["VSC"] ││ } │└────────────────────────────────────────────────────────────────────────────┘│vStep 7: Lead Processing┌────────────────────────────────────────────────────────────────────────────┐│ dealer-lead-api receives the request ││ ↓ ││ Lead is created and routed to the selected dealership ││ ↓ ││ Dealership F&I team receives notification ││ ↓ ││ Team contacts customer with personalized quote │└────────────────────────────────────────────────────────────────────────────┘
#8.2 Quote Submission Payload
Endpoint: POST /fni-leads
Service: dealer-lead-api
interface LeadsData {address: {addressLine1: string;addressLine2?: string;city: string;state: string;zipCode: string;};dealershipId: number;email: string;firstName: string;lastName: string;lithiaId?: string;phone: string;vehicle: {make: string;mileage: number;model: string;modelYear: number;vin: string;};productType: ProductType[]; // ["LOF", "VSC", "VALET", "APPEARANCE", "PPM"]}
#9. External Services & Integrations
#9.1 PEN API (ProvenNet)
Purpose: Source of truth for F&I product dealer costs and retail prices
┌─────────────────────────────────────────────────────────────────┐│ PEN API Integration │├─────────────────────────────────────────────────────────────────┤│ ││ Protocol: SOAP/XML ││ Auth: Basic Authentication (PEN_USER / PEN_PASSWORD) ││ Timeout: 60 seconds ││ Rate Limit: 1 call per 50ms (Resilience4j) ││ ││ Key Operations: ││ ┌─────────────────────────────────────────────────────────┐ ││ │ getRateBookCodes() │ ││ │ - Fetches daily rate codes for each product type │ ││ │ - Called during cache refresh │ ││ └─────────────────────────────────────────────────────────┘ ││ ││ ┌─────────────────────────────────────────────────────────┐ ││ │ getASBRates(vehicle, rateBookCode) │ ││ │ - Returns dealer cost and retail price │ ││ │ - Used for VSC, T&W, VLP products │ ││ └─────────────────────────────────────────────────────────┘ ││ │└─────────────────────────────────────────────────────────────────┘
#9.2 Google Geocoding API
Purpose: Resolve state from zip code when not provided
// Used in FIHandler when userState is missingval address = geocodingService.getEstimatedAddressFromPostalCode(userZip)val state = address.state
#9.3 Azure Service Bus
Purpose: Receive vehicle inventory delta messages
Azure Service Bus Topic│v┌─────────────────────────────────────────┐│ VehicleInventoryTopicReceiver ││ ││ Message Actions: ││ - ADD: Create new fi-rate cache doc ││ - UPDATE: Refresh rates if changed ││ - DELETE: Soft delete cache doc │└─────────────────────────────────────────┘
#9.4 Odyssey GraphQL
Purpose: Vehicle inventory queries
query GetVehicleById($id: ID!) {vehicle(id: $id) {vinmakemodelyearmileagedealershipState}}
#10. Pricing Configuration
#10.1 Configuration File
Location: Products-Api/library/src/main/resources/application.yml
products:cost:vehicle-service-contract:markup-amount: 150000 # $1,500 in centsvehicle-loss-protection:fixed-amount: 109500 # $1,095 in centstire-and-wheel:markup-multiplier: 2.2 # 2.2x dealer costretail-price-override-states:- FL # Florida uses PEN retail prices
#10.2 Updating Prices
Option A: Configuration Update (Recommended)
- Modify
application.ymlwith new values - Restart Products Cron service
- Call
GET /test/fi-Rate/update-cached-markup - All cached prices immediately reflect new markup
Option B: Wait for Natural Refresh
- Modify
application.yml - Restart services
- Wait for cron to refresh (~100 vehicles every 5 minutes)
- New vehicles get new prices on ADD event
#10.3 Florida-Specific Pricing
Florida uses PEN retail prices instead of markup-based calculations:
| Product | Non-Florida | Florida |
|---|---|---|
| VSC | dealerCost + $1,500 | PEN retail price |
| Tire & Wheel | dealerCost × 2.2 | PEN retail price |
| VLP | Fixed $1,095 | Fixed $1,095 |
#11. API Reference
#11.1 Products API Endpoints
Get Estimated F&I Products
GET /estimator/fi-products
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| vehicleId | string | Yes | Odyssey vehicle ID |
| userZip | string | Yes | User's zip code |
| userState | string | No | User's state (derived from zip if missing) |
| paymentSelection | enum | No | CASH, FINANCE, LEASE, or null |
Response:
[{"type": "LIFETIME_OIL","totalInCents": 79900,"termMonths": "lifetime","termMiles": null,"deductible": null},{"type": "VEHICLE_SERVICE_CONTRACT","totalInCents": 259500,"termMonths": "96","termMiles": "120000","deductible": 100}]
#11.2 Admin Endpoints (Cron App)
Trigger FI Rate Refresh
GET /test/fi-Rate/refresh
Manually triggers one batch of the FI Rate Refresh cron.
Update Cached Markup
GET /test/fi-Rate/update-cached-markup
Reapplies current configuration markup to ALL cached documents.
Expire Products by State
POST /test/fi-Rate/expire-productsContent-Type: application/json{"dealerState": "FL"}
Sets effectiveDate = 90 days ago for all documents in the specified state.
#11.3 Dealer Lead API Endpoints
Submit F&I Quote Lead
POST /fni-leadsContent-Type: application/jsonAuthorization: Bearer {token}{"address": {"addressLine1": "123 Main St","city": "Portland","state": "OR","zipCode": "97201"},"dealershipId": 12345,"email": "customer@email.com","firstName": "John","lastName": "Doe","phone": "5035551234","vehicle": {"make": "Toyota","model": "Camry","modelYear": 2022,"vin": "1HGBH41JXMN109186","mileage": 12000},"productType": ["VSC"]}
Response:
{"message": "Lead created successfully"}
#12. Troubleshooting Guide
#Common Issues
Issue: Vehicle shows no PEN products
Cause: Cache miss - vehicle not yet processed by inventory-delta or cron
Solution:
- Check if vehicle exists in
fi-ratecollection - If missing, trigger manual refresh:
GET /test/fi-Rate/refresh - Or wait for next inventory-delta message
Issue: Price increase not reflecting
Cause: Config changes only apply on next cache write
Solution:
- Verify config was updated in
application.yml - Confirm cron app was restarted
- Call
GET /test/fi-Rate/update-cached-markupfor immediate application
Issue: Florida prices different than expected
Cause: FL uses PEN retail price override, not markup config
Solution:
- VRC/T&W prices cannot be changed via config for FL
- Would require PEN to update their prices OR code logic change
Issue: Cache showing expired data
Cause: effectiveDate > 60 days old
Solution:
- Check document's
effectiveDatein MongoDB - If expired, estimator returns empty (by design)
- Use
POST /test/fi-Rate/expire-productsto force expiration - Run refresh to repopulate
#Monitoring Queries
// Find all expired cache documentsdb.getCollection('fi-rate').find({status: 'ACTIVE',effectiveDate: { $lt: new Date(Date.now() - 60 * 24 * 60 * 60 * 1000) }})// Find documents with errorsdb.getCollection('fi-rate').find({status: 'UPDATE_ERROR'})// Count documents by statedb.getCollection('fi-rate').aggregate([{ $group: { _id: '$dealerState', count: { $sum: 1 } } },{ $sort: { count: -1 } }])
#Summary
The F&I Products pricing system uses a sophisticated multi-tier approach:
- Static Products (LOF, Valet): Fixed prices stored in MongoDB, displayed immediately
- Dynamic Products (VSC, T&W, VLP): Cached PEN rates with configurable markup
- Quote-Required Products (Appearance, PPM): Require manual quote through dealer-lead-api
The fi-rate cache collection serves as the performance optimization layer, storing pre-calculated prices with markup already applied. This allows the frontend to display accurate pricing without making expensive PEN API calls on every request.
For products requiring personalized quotes, the system collects vehicle details, mileage, and contact information, then routes the lead to the appropriate dealership's F&I team for follow-up.
Last Updated: February 2026 Maintained by: Driveway Engineering Team