Files
2026-05-06 22:58:23 -04:00

49 KiB
Raw Permalink Blame History


name: rental-car-advanced-features description: Build advanced rental operations features for RentalDriveGo. Use this skill when the user asks about: vehicle damage diagrams/inspection (contract car layout), insurance policies and charges, second driver (additional driver) with optional charge, driver license validation (expiry check, 3-month rule, flagging), age-based pricing rules (25+ discount, license+5 surcharge), gasoline/fuel policies (FULL_TO_FULL etc.), billing period reporting (weekly/monthly/yearly export for accountants), or the platform admin panel (managing tenants, staff, roles, permissions). This skill works alongside document-service.md and schema.md.

Skill: RentalDriveGo Advanced Features

Features Covered

  1. Vehicle Damage Inspection Map (contract car diagram)
  2. Insurance Policies & Per-Reservation Charges
  3. Second / Additional Driver
  4. Driver License Validation (expiry + 3-month rule + flagging)
  5. Age-Based Pricing Rules (25+ discount, license+5 surcharge)
  6. Fuel/Gasoline Policy (structured types)
  7. Billing Period Reporting (accountant export)
  8. Platform Admin Panel (tenant + staff + role management)

1. Vehicle Damage Inspection Map

What it is

An interactive SVG car diagram embedded in the check-in/check-out workflow (dashboard + printable on contract). Staff marks pre-existing damage zones before handing over the vehicle. At return, the same diagram shows original vs new damage.

Data model

// Damage report: one per check-in, one per check-out
model DamageReport {
  id             String          @id @default(cuid())
  reservationId  String
  reservation    Reservation     @relation(fields: [reservationId], references: [id], onDelete: Cascade)
  companyId      String          // denormalized for tenant scoping
  type           DamageReportType  // CHECKIN | CHECKOUT

  // Array of marked damage zones
  // Each zone: { zone: string, severity: string, note: string }
  // zone values: "hood", "roof", "trunk", "front-left-door", "front-right-door",
  //   "rear-left-door", "rear-right-door", "front-left-fender", "front-right-fender",
  //   "rear-left-fender", "rear-right-fender", "front-bumper", "rear-bumper",
  //   "windshield", "rear-window", "left-mirror", "right-mirror",
  //   "front-left-wheel", "front-right-wheel", "rear-left-wheel", "rear-right-wheel"
  // severity: "SCRATCH" | "DENT" | "CRACK" | "MISSING" | "OTHER"
  damages        Json            // DamageZone[]

  // Photos of damage (Cloudinary URLs, optional)
  photos         String[]

  // Fuel level at this inspection point
  fuelLevel      FuelLevel

  // Odometer reading
  mileage        Int?

  // Captured at
  inspectedAt    DateTime        @default(now())
  inspectedBy    String?         // Employee name who did the inspection

  // Customer acknowledgement
  customerSignedAt  DateTime?
  customerName      String?       // printed name of who acknowledged

  createdAt DateTime @default(now())

  @@unique([reservationId, type])
  @@index([companyId])
  @@map("damage_reports")
}

enum DamageReportType { CHECKIN  CHECKOUT }
enum FuelLevel        { FULL  THREE_QUARTERS  HALF  QUARTER  EMPTY }

Add to Reservation model:

  damageReports     DamageReport[]
  // Remove old checkInFuelLevel/checkOutFuelLevel String? — replaced by DamageReport.fuelLevel

Damage zone TypeScript type

// types/damage.ts
export interface DamageZone {
  zone: VehicleZone
  severity: 'SCRATCH' | 'DENT' | 'CRACK' | 'MISSING' | 'OTHER'
  note?: string
}

export type VehicleZone =
  | 'hood' | 'roof' | 'trunk'
  | 'front-left-door' | 'front-right-door'
  | 'rear-left-door' | 'rear-right-door'
  | 'front-left-fender' | 'front-right-fender'
  | 'rear-left-fender' | 'rear-right-fender'
  | 'front-bumper' | 'rear-bumper'
  | 'windshield' | 'rear-window'
  | 'left-mirror' | 'right-mirror'
  | 'front-left-wheel' | 'front-right-wheel'
  | 'rear-left-wheel' | 'rear-right-wheel'
  | 'interior' | 'under-hood' | 'undercarriage'

// Coordinates on the SVG diagram for each zone
// Used by the interactive damage map UI and the PDF render
export const DAMAGE_ZONE_COORDS: Record<VehicleZone, { x: number; y: number; w: number; h: number }> = {
  'hood':               { x: 48,  y: 8,   w: 64, h: 28 },
  'roof':               { x: 48,  y: 52,  w: 64, h: 56 },
  'trunk':              { x: 48,  y: 124, w: 64, h: 28 },
  'front-bumper':       { x: 48,  y: 0,   w: 64, h: 10 },
  'rear-bumper':        { x: 48,  y: 150, w: 64, h: 10 },
  'front-left-fender':  { x: 20,  y: 10,  w: 30, h: 35 },
  'front-right-fender': { x: 110, y: 10,  w: 30, h: 35 },
  'front-left-door':    { x: 20,  y: 52,  w: 30, h: 30 },
  'front-right-door':   { x: 110, y: 52,  w: 30, h: 30 },
  'rear-left-door':     { x: 20,  y: 82,  w: 30, h: 30 },
  'rear-right-door':    { x: 110, y: 82,  w: 30, h: 30 },
  'rear-left-fender':   { x: 20,  y: 112, w: 30, h: 35 },
  'rear-right-fender':  { x: 110, y: 112, w: 30, h: 35 },
  'windshield':         { x: 52,  y: 38,  w: 56, h: 18 },
  'rear-window':        { x: 52,  y: 104, w: 56, h: 18 },
  'left-mirror':        { x: 8,   y: 50,  w: 14, h: 10 },
  'right-mirror':       { x: 138, y: 50,  w: 14, h: 10 },
  'front-left-wheel':   { x: 10,  y: 24,  w: 22, h: 26 },
  'front-right-wheel':  { x: 128, y: 24,  w: 22, h: 26 },
  'rear-left-wheel':    { x: 10,  y: 110, w: 22, h: 26 },
  'rear-right-wheel':   { x: 128, y: 110, w: 22, h: 26 },
  'interior':           { x: 55,  y: 58,  w: 50, h: 44 },
  'under-hood':         { x: 52,  y: 10,  w: 56, h: 20 },
  'undercarriage':      { x: 52,  y: 130, w: 56, h: 20 },
}

// Severity colors for the SVG overlay
export const SEVERITY_COLORS = {
  SCRATCH:  '#F59E0B',   // amber
  DENT:     '#EF4444',   // red
  CRACK:    '#8B5CF6',   // purple
  MISSING:  '#374151',   // dark gray
  OTHER:    '#6B7280',   // gray
}

Interactive Damage Map UI (React Component)

// components/reservations/DamageMap.tsx
import { useState } from 'react'
import { DAMAGE_ZONE_COORDS, SEVERITY_COLORS, VehicleZone, DamageZone } from '@/types/damage'

interface DamageMapProps {
  damages: DamageZone[]
  onChange?: (damages: DamageZone[]) => void  // undefined = read-only
  readonly?: boolean
  primaryColor?: string
}

export function DamageMap({ damages, onChange, readonly, primaryColor = '#1A56DB' }: DamageMapProps) {
  const [selected, setSelected] = useState<VehicleZone | null>(null)
  const [severity, setSeverity] = useState<DamageZone['severity']>('SCRATCH')
  const [note, setNote] = useState('')

  const getDamageForZone = (zone: VehicleZone) =>
    damages.find(d => d.zone === zone)

  const handleZoneClick = (zone: VehicleZone) => {
    if (readonly || !onChange) return
    setSelected(zone)
    const existing = getDamageForZone(zone)
    if (existing) {
      setSeverity(existing.severity)
      setNote(existing.note ?? '')
    } else {
      setSeverity('SCRATCH')
      setNote('')
    }
  }

  const applyDamage = () => {
    if (!selected || !onChange) return
    const updated = damages.filter(d => d.zone !== selected)
    updated.push({ zone: selected, severity, note })
    onChange(updated)
    setSelected(null)
  }

  const clearZone = (zone: VehicleZone) => {
    if (!onChange) return
    onChange(damages.filter(d => d.zone !== zone))
  }

  return (
    <div className="flex flex-col items-center gap-4">
      {/* SVG Car Diagram — top-down view */}
      <div className="relative">
        <svg viewBox="0 0 160 165" width={280} height={290} className="select-none">

          {/* Car body — top-down silhouette */}
          <CarTopDownSVG primaryColor={primaryColor} />

          {/* Damage zone hit areas — transparent clickable rects */}
          {(Object.entries(DAMAGE_ZONE_COORDS) as [VehicleZone, any][]).map(([zone, coords]) => {
            const damage = getDamageForZone(zone)
            return (
              <g key={zone} onClick={() => handleZoneClick(zone)} className={readonly ? '' : 'cursor-pointer'}>
                <rect
                  x={coords.x} y={coords.y} width={coords.w} height={coords.h}
                  rx={3}
                  fill={damage ? SEVERITY_COLORS[damage.severity] : 'transparent'}
                  fillOpacity={damage ? 0.45 : 0}
                  stroke={selected === zone ? '#1A56DB' : damage ? SEVERITY_COLORS[damage.severity] : 'transparent'}
                  strokeWidth={selected === zone ? 2 : 1}
                  className={readonly ? '' : 'hover:fill-blue-500/20 hover:stroke-blue-500'}
                />
                {/* Damage indicator dot */}
                {damage && (
                  <circle
                    cx={coords.x + coords.w / 2}
                    cy={coords.y + coords.h / 2}
                    r={4}
                    fill={SEVERITY_COLORS[damage.severity]}
                    stroke="white"
                    strokeWidth={1}
                  />
                )}
              </g>
            )
          })}
        </svg>

        {/* Direction labels */}
        <div className="absolute top-1 left-1/2 -translate-x-1/2 text-[10px] text-gray-400 font-medium">FRONT</div>
        <div className="absolute bottom-1 left-1/2 -translate-x-1/2 text-[10px] text-gray-400 font-medium">REAR</div>
      </div>

      {/* Severity legend */}
      <div className="flex flex-wrap gap-2 justify-center">
        {(Object.entries(SEVERITY_COLORS) as [DamageZone['severity'], string][]).map(([s, color]) => (
          <span key={s} className="flex items-center gap-1 text-xs text-gray-600">
            <span className="w-3 h-3 rounded-full inline-block" style={{ backgroundColor: color }} />
            {s}
          </span>
        ))}
      </div>

      {/* Zone editor (appears when zone is clicked, in edit mode) */}
      {selected && !readonly && (
        <div className="w-full border border-blue-200 bg-blue-50 rounded-lg p-3">
          <p className="text-sm font-medium text-blue-900 mb-2 capitalize">
            {selected.replace(/-/g, ' ')}
          </p>
          <div className="flex gap-2 flex-wrap mb-2">
            {(['SCRATCH', 'DENT', 'CRACK', 'MISSING', 'OTHER'] as const).map(s => (
              <button key={s} onClick={() => setSeverity(s)}
                className={`px-2 py-1 text-xs rounded font-medium border
                  ${severity === s ? 'text-white border-transparent' : 'bg-white border-gray-200 text-gray-600'}`}
                style={severity === s ? { backgroundColor: SEVERITY_COLORS[s], borderColor: SEVERITY_COLORS[s] } : {}}>
                {s}
              </button>
            ))}
          </div>
          <input type="text" placeholder="Note (optional)" value={note}
            onChange={e => setNote(e.target.value)}
            className="w-full text-sm border border-gray-200 rounded px-2 py-1 mb-2" />
          <div className="flex gap-2">
            <button onClick={applyDamage}
              className="btn-primary text-xs flex-1">Mark Damage</button>
            <button onClick={() => { clearZone(selected); setSelected(null) }}
              className="btn-ghost text-xs">Clear Zone</button>
            <button onClick={() => setSelected(null)}
              className="btn-ghost text-xs text-gray-400">Cancel</button>
          </div>
        </div>
      )}

      {/* Damage list */}
      {damages.length > 0 && (
        <div className="w-full">
          <p className="text-xs font-medium text-gray-500 mb-1">Marked damages ({damages.length})</p>
          <div className="space-y-1">
            {damages.map(d => (
              <div key={d.zone} className="flex items-center justify-between text-xs bg-gray-50 rounded px-2 py-1">
                <div className="flex items-center gap-2">
                  <span className="w-2 h-2 rounded-full" style={{ backgroundColor: SEVERITY_COLORS[d.severity] }} />
                  <span className="capitalize font-medium">{d.zone.replace(/-/g, ' ')}</span>
                  <span className="text-gray-400"> {d.severity}</span>
                  {d.note && <span className="text-gray-400 italic">"{d.note}"</span>}
                </div>
                {!readonly && (
                  <button onClick={() => clearZone(d.zone)} className="text-red-400 hover:text-red-600 ml-2"></button>
                )}
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  )
}

// Simplified SVG path for top-down car silhouette
function CarTopDownSVG({ primaryColor }: { primaryColor: string }) {
  return (
    <g>
      {/* Outer car body */}
      <rect x={30} y={4} width={100} height={157} rx={18} fill="#F3F4F6" stroke="#D1D5DB" strokeWidth={1.5} />
      {/* Hood section */}
      <rect x={38} y={8} width={84} height={32} rx={8} fill="#E5E7EB" />
      {/* Windshield */}
      <rect x={44} y={38} width={72} height={18} rx={4} fill="#BFDBFE" fillOpacity={0.8} />
      {/* Roof / cabin */}
      <rect x={36} y={54} width={88} height={58} rx={4} fill={primaryColor} fillOpacity={0.08} stroke={primaryColor} strokeWidth={0.5} strokeDasharray="3,2" />
      {/* Rear window */}
      <rect x={44} y={108} width={72} height={18} rx={4} fill="#BFDBFE" fillOpacity={0.8} />
      {/* Trunk */}
      <rect x={38} y={124} width={84} height={30} rx={8} fill="#E5E7EB" />
      {/* Rear bumper */}
      <rect x={42} y={152} width={76} height={9} rx={4} fill="#D1D5DB" />
      {/* Front bumper */}
      <rect x={42} y={4} width={76} height={9} rx={4} fill="#D1D5DB" />
      {/* Mirrors */}
      <rect x={16} y={52} width={16} height={10} rx={3} fill="#E5E7EB" stroke="#D1D5DB" strokeWidth={1} />
      <rect x={128} y={52} width={16} height={10} rx={3} fill="#E5E7EB" stroke="#D1D5DB" strokeWidth={1} />
      {/* Wheels */}
      <rect x={10} y={24} width={22} height={28} rx={5} fill="#374151" />
      <rect x={128} y={24} width={22} height={28} rx={5} fill="#374151" />
      <rect x={10} y={110} width={22} height={28} rx={5} fill="#374151" />
      <rect x={128} y={110} width={22} height={28} rx={5} fill="#374151" />
      {/* Center line */}
      <line x1={80} y1={54} x2={80} y2={112} stroke="#E5E7EB" strokeWidth={1} strokeDasharray="4,3" />
    </g>
  )
}

Damage Map in the PDF Contract

In RentalContractPDF.tsx, add a damage section after the vehicle block using @react-pdf/renderer SVG primitives (the interactive React SVG won't work in the PDF renderer — use static Svg, Rect, Circle, Line elements from @react-pdf/renderer):

// In RentalContractPDF.tsx — add after vehicle section
import { Svg, Rect, Circle, Line, Path, G, Text as SvgText } from '@react-pdf/renderer'

function DamageMapPDF({ damages, label }: { damages: DamageZone[]; label: string }) {
  return (
    <View>
      <Text style={styles.sectionLabel}>{label}</Text>
      <View style={{ flexDirection: 'row', gap: 20 }}>
        {/* SVG car diagram — static top-down view */}
        <Svg viewBox="0 0 160 165" width={100} height={105}>
          {/* Repeat the CarTopDownSVG shapes using Svg primitives */}
          <Rect x={30} y={4} width={100} height={157} rx={18} fill="#F3F4F6" stroke="#D1D5DB" strokeWidth={1.5} />
          <Rect x={38} y={8} width={84} height={32} rx={8} fill="#E5E7EB" />
          <Rect x={44} y={38} width={72} height={18} rx={4} fill="#BFDBFE" fillOpacity={0.6} />
          <Rect x={38} y={54} width={84} height={58} rx={4} fill="#F9FAFB" stroke="#E5E7EB" strokeWidth={0.5} />
          <Rect x={44} y={108} width={72} height={18} rx={4} fill="#BFDBFE" fillOpacity={0.6} />
          <Rect x={38} y={124} width={84} height={30} rx={8} fill="#E5E7EB" />
          <Rect x={10} y={24} width={22} height={28} rx={5} fill="#374151" />
          <Rect x={128} y={24} width={22} height={28} rx={5} fill="#374151" />
          <Rect x={10} y={110} width={22} height={28} rx={5} fill="#374151" />
          <Rect x={128} y={110} width={22} height={28} rx={5} fill="#374151" />
          {/* Damage markers */}
          {damages.map((d, i) => {
            const coords = DAMAGE_ZONE_COORDS[d.zone]
            return (
              <G key={i}>
                <Rect x={coords.x} y={coords.y} width={coords.w} height={coords.h}
                  fill={SEVERITY_COLORS[d.severity]} fillOpacity={0.35}
                  stroke={SEVERITY_COLORS[d.severity]} strokeWidth={0.8} />
                <Circle cx={coords.x + coords.w / 2} cy={coords.y + coords.h / 2}
                  r={3.5} fill={SEVERITY_COLORS[d.severity]} stroke="white" strokeWidth={0.7} />
              </G>
            )
          })}
        </Svg>

        {/* Damage list */}
        <View style={{ flex: 1 }}>
          {damages.length === 0
            ? <Text style={{ fontSize: 8, color: '#10B981' }}> No damage recorded</Text>
            : damages.map((d, i) => (
              <View key={i} style={{ flexDirection: 'row', marginBottom: 2.5 }}>
                <View style={{
                  width: 7, height: 7, borderRadius: 3.5, marginRight: 4, marginTop: 1,
                  backgroundColor: SEVERITY_COLORS[d.severity]
                }} />
                <Text style={{ fontSize: 7.5, flex: 1, color: '#374151' }}>
                  <Text style={{ fontWeight: 700 }}>{d.zone.replace(/-/g, ' ')}</Text>
                  {` — ${d.severity}${d.note ? ` (${d.note})` : ''}`}
                </Text>
              </View>
            ))
          }
        </View>
      </View>
    </View>
  )
}

// Usage in the contract — two damage sections:
// 1. At check-in: <DamageMapPDF damages={checkInReport?.damages ?? []} label="Vehicle Condition at Pickup" />
// 2. At checkout: <DamageMapPDF damages={checkOutReport?.damages ?? []} label="Vehicle Condition at Return" />
// Both are shown on the contract, side by side if space, or stacked.

2. Insurance Policies

Data model

// ─── Insurance Policy ─────────────────────────────────────────
// Company configures insurance options available to renters.
// At booking, renter selects one (or NONE if all are optional).

model InsurancePolicy {
  id          String   @id @default(cuid())
  companyId   String
  company     Company  @relation(fields: [companyId], references: [id], onDelete: Cascade)

  name        String   // e.g. "Basic Coverage", "Full Coverage", "CDW", "SCDW"
  description String?  // what it covers (shown to customer at booking)
  type        InsuranceType

  // Pricing — one of the following applies:
  chargeType  InsuranceChargeType
  chargeValue Int      // in smallest currency unit
  // Examples:
  //   chargeType=PER_DAY, chargeValue=3000 → 30 MAD/day
  //   chargeType=PER_RENTAL, chargeValue=15000 → 150 MAD flat
  //   chargeType=PERCENTAGE_OF_RENTAL, chargeValue=10 → 10% of base rental

  isRequired  Boolean  @default(false)  // if true, auto-applied, customer cannot opt out
  isActive    Boolean  @default(true)
  sortOrder   Int      @default(0)      // display order at booking

  reservations ReservationInsurance[]

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@index([companyId])
  @@index([companyId, isActive])
  @@map("insurance_policies")
}

enum InsuranceType {
  CDW           // Collision Damage Waiver
  SCDW          // Super CDW (lower excess)
  THEFT         // Theft protection
  THIRD_PARTY   // Third-party liability
  FULL          // Full coverage bundle
  BASIC         // Basic minimal coverage
  ROADSIDE      // Roadside assistance
  PERSONAL      // Personal accident insurance
  CUSTOM        // Company-defined name
}

enum InsuranceChargeType {
  PER_DAY
  PER_RENTAL       // flat fee per reservation
  PERCENTAGE_OF_RENTAL  // % of base rental amount
}

// ─── Reservation Insurance ────────────────────────────────────
// Junction: which insurance(s) were selected for a reservation

model ReservationInsurance {
  id                 String           @id @default(cuid())
  reservationId      String
  reservation        Reservation      @relation(fields: [reservationId], references: [id], onDelete: Cascade)
  insurancePolicyId  String
  insurancePolicy    InsurancePolicy  @relation(fields: [insurancePolicyId], references: [id])

  // Snapshot of policy at time of booking (prices may change later)
  policyName         String
  policyType         InsuranceType
  chargeType         InsuranceChargeType
  chargeValue        Int              // locked at booking time
  totalCharge        Int              // calculated total for this reservation (in smallest unit)

  @@unique([reservationId, insurancePolicyId])
  @@map("reservation_insurances")
}

Add to Reservation model:

  insurances        ReservationInsurance[]
  insuranceTotal    Int   @default(0)   // sum of all insurance charges (cached)

Insurance charge calculation

// services/insuranceService.ts

export function calculateInsuranceCharge(
  policy: InsurancePolicy,
  totalDays: number,
  baseRentalAmount: number   // dailyRate × totalDays, in smallest unit
): number {
  switch (policy.chargeType) {
    case 'PER_DAY':
      return policy.chargeValue * totalDays
    case 'PER_RENTAL':
      return policy.chargeValue
    case 'PERCENTAGE_OF_RENTAL':
      return Math.round(baseRentalAmount * policy.chargeValue / 100)
    default:
      return 0
  }
}

export async function applyInsurancesToReservation(
  reservationId: string,
  companyId: string,
  selectedPolicyIds: string[],
  totalDays: number,
  baseRentalAmount: number
) {
  // Always include required policies
  const allPolicies = await prisma.insurancePolicy.findMany({
    where: { companyId, isActive: true }
  })

  const required = allPolicies.filter(p => p.isRequired)
  const selected = allPolicies.filter(p => selectedPolicyIds.includes(p.id) && !p.isRequired)
  const toApply = [...required, ...selected]

  const records = toApply.map(policy => ({
    reservationId,
    insurancePolicyId: policy.id,
    policyName: policy.name,
    policyType: policy.type,
    chargeType: policy.chargeType,
    chargeValue: policy.chargeValue,
    totalCharge: calculateInsuranceCharge(policy, totalDays, baseRentalAmount)
  }))

  const insuranceTotal = records.reduce((sum, r) => sum + r.totalCharge, 0)

  await prisma.$transaction([
    prisma.reservationInsurance.createMany({ data: records }),
    prisma.reservation.update({
      where: { id: reservationId },
      data: { insuranceTotal }
    })
  ])

  return { records, insuranceTotal }
}

3. Second / Additional Driver

Data model

model AdditionalDriver {
  id              String      @id @default(cuid())
  reservationId   String
  reservation     Reservation @relation(fields: [reservationId], references: [id], onDelete: Cascade)
  companyId       String      // tenant scoping

  firstName       String
  lastName        String
  email           String?
  phone           String?
  driverLicense   String
  licenseExpiry   Date?
  dateOfBirth     DateTime?
  nationality     String?

  // Charge for adding this driver
  chargeType      AdditionalDriverCharge   @default(FREE)
  chargeValue     Int                      @default(0)   // flat or per-day in smallest unit
  totalCharge     Int                      @default(0)

  // Validation flags
  licenseExpired  Boolean     @default(false)
  licenseExpiringSoon Boolean @default(false)   // < 3 months validity remaining
  requiresApproval Boolean    @default(false)   // flagged for manual approval
  approvedBy      String?     // Employee who approved a flagged license
  approvedAt      DateTime?
  approvalNote    String?

  createdAt DateTime @default(now())

  @@index([reservationId])
  @@index([companyId])
  @@map("additional_drivers")
}

enum AdditionalDriverCharge {
  FREE
  PER_DAY    // charge per rental day
  FLAT       // one-time flat charge per reservation
}

Add to company ContractSettings:

  // Additional driver pricing (company sets these)
  additionalDriverCharge      AdditionalDriverCharge @default(FREE)
  additionalDriverDailyRate   Int                    @default(0)
  additionalDriverFlatRate    Int                    @default(0)

Add to Reservation:

  additionalDrivers     AdditionalDriver[]
  additionalDriverTotal Int   @default(0)

4. Driver License Validation

Fields on Customer model (additions)

// Add to model Customer { ... }
  licenseExpiry       DateTime?    // expiry date of driver's license
  licenseIssuedAt     DateTime?    // issue date
  licenseCountry      String?      // issuing country
  licenseNumber       String?      // license number (may differ from driverLicense ID)
  licenseCategory     String?      // e.g. "B", "B+E", "A" (for motorcycle)

  // Validation flags
  licenseExpired           Boolean  @default(false)
  licenseExpiringSoon      Boolean  @default(false)   // < 3 months validity
  licenseValidationStatus  LicenseStatus @default(PENDING)
  licenseApprovedBy        String?       // Employee who approved/denied
  licenseApprovedAt        DateTime?
  licenseApprovalNote      String?
enum LicenseStatus {
  PENDING      // not yet checked
  VALID        // checked and valid (>3 months remaining)
  EXPIRING     // flagged: <3 months remaining — awaiting employee decision
  APPROVED     // expired/expiring but manually approved by employee
  DENIED       // employee denied the booking due to license
  EXPIRED      // license has already expired
}

License validation service

// services/licenseValidationService.ts

const THREE_MONTHS_MS = 3 * 30 * 24 * 60 * 60 * 1000

export interface LicenseValidationResult {
  status: 'VALID' | 'EXPIRING' | 'EXPIRED'
  daysUntilExpiry: number | null
  requiresApproval: boolean
  message: string
}

export function validateLicense(licenseExpiry: Date | null): LicenseValidationResult {
  if (!licenseExpiry) {
    return {
      status: 'VALID',        // no expiry stored — cannot determine
      daysUntilExpiry: null,
      requiresApproval: false,
      message: 'No expiry date on record'
    }
  }

  const now = new Date()
  const expiryMs = licenseExpiry.getTime()
  const daysUntilExpiry = Math.ceil((expiryMs - now.getTime()) / (1000 * 60 * 60 * 24))

  if (expiryMs <= now.getTime()) {
    return {
      status: 'EXPIRED',
      daysUntilExpiry: daysUntilExpiry,   // negative number
      requiresApproval: true,
      message: `License expired ${Math.abs(daysUntilExpiry)} day(s) ago`
    }
  }

  if (expiryMs - now.getTime() < THREE_MONTHS_MS) {
    return {
      status: 'EXPIRING',
      daysUntilExpiry,
      requiresApproval: true,
      message: `License expires in ${daysUntilExpiry} day(s) — approval required`
    }
  }

  return {
    status: 'VALID',
    daysUntilExpiry,
    requiresApproval: false,
    message: `Valid — expires in ${daysUntilExpiry} day(s)`
  }
}

// Called when creating/updating a customer or adding an additional driver
export async function validateAndFlagLicense(customerId: string) {
  const customer = await prisma.customer.findUniqueOrThrow({
    where: { id: customerId }
  })

  const result = validateLicense(customer.licenseExpiry)

  const status: LicenseStatus =
    result.status === 'EXPIRED' ? 'EXPIRED' :
    result.status === 'EXPIRING' ? 'EXPIRING' : 'VALID'

  await prisma.customer.update({
    where: { id: customerId },
    data: {
      licenseExpired:      result.status === 'EXPIRED',
      licenseExpiringSoon: result.status === 'EXPIRING',
      licenseValidationStatus: status,
    }
  })

  return result
}

License approval UI (dashboard)

When a reservation is created for a customer with licenseValidationStatus: EXPIRING | EXPIRED:

  • Show a yellow (EXPIRING) or red (EXPIRED) banner in the reservation detail page
  • Banner text: "⚠️ Customer's license expires in X days — please verify before confirming"
  • Two action buttons: [Approve and Continue] / [Deny Reservation]
  • Clicking Approve: PATCH /customers/:id/license-approval { decision: 'APPROVE', note: '...' }
  • Clicking Deny: opens cancel reservation flow

5. Age-Based Pricing Rules

Data model

model PricingRule {
  id          String          @id @default(cuid())
  companyId   String
  company     Company         @relation(fields: [companyId], references: [id], onDelete: Cascade)

  name        String          // e.g. "Young Driver Surcharge", "Senior Discount"
  type        PricingRuleType
  condition   PricingCondition
  conditionValue Int          // e.g. 25 for "age < 25", 70 for "age > 70"

  adjustmentType   PriceAdjustmentType
  adjustmentValue  Int        // percentage or flat in smallest unit
  // For LICENSE_YEARS condition: conditionValue = minimum years since license issue date

  isActive    Boolean         @default(true)
  description String?         // shown to customer at booking

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@index([companyId])
  @@map("pricing_rules")
}

enum PricingRuleType {
  SURCHARGE   // adds cost
  DISCOUNT    // reduces cost
}

enum PricingCondition {
  AGE_LESS_THAN     // customer age (years) < conditionValue
  AGE_GREATER_THAN  // customer age > conditionValue
  LICENSE_YEARS_LESS_THAN   // years since license issue < conditionValue
  LICENSE_YEARS_GREATER_THAN
}

enum PriceAdjustmentType {
  PERCENTAGE    // % of base daily rate
  FLAT_PER_DAY  // flat amount per day in smallest unit
  FLAT_TOTAL    // flat amount per reservation
}

Add to Reservation:

  pricingRulesApplied   Json?    // Array of { ruleId, name, type, amount } — snapshot at booking
  pricingRulesTotal     Int      @default(0)   // net surcharges minus discounts

Built-in rules (pre-configured defaults companies can edit)

// The system ships with these as suggested defaults (companies can modify or delete):
const DEFAULT_PRICING_RULES = [
  {
    name: 'Young Driver Surcharge (Under 25)',
    type: 'SURCHARGE',
    condition: 'AGE_LESS_THAN',
    conditionValue: 25,
    adjustmentType: 'PERCENTAGE',
    adjustmentValue: 15,   // +15% of base rate
    description: 'Additional charge for drivers under 25 years of age'
  },
  {
    name: 'Experienced License Discount (5+ Years)',
    type: 'DISCOUNT',
    condition: 'LICENSE_YEARS_GREATER_THAN',
    conditionValue: 5,
    adjustmentType: 'PERCENTAGE',
    adjustmentValue: 5,    // -5% of base rate
    description: 'Discount for drivers with 5+ years of driving experience'
  },
]

Rule application service

// services/pricingRuleService.ts

export async function applyPricingRules(
  companyId: string,
  customerId: string,
  additionalDrivers: { dateOfBirth?: Date | null; licenseIssuedAt?: Date | null }[],
  dailyRate: number,
  totalDays: number
): Promise<{ applied: any[]; total: number }> {

  const rules = await prisma.pricingRule.findMany({
    where: { companyId, isActive: true }
  })

  const customer = await prisma.customer.findUniqueOrThrow({ where: { id: customerId } })
  const allDrivers = [customer, ...additionalDrivers]

  const applied: any[] = []
  const baseAmount = dailyRate * totalDays

  for (const driver of allDrivers) {
    for (const rule of rules) {
      const driverAge = driver.dateOfBirth
        ? Math.floor((Date.now() - driver.dateOfBirth.getTime()) / (365.25 * 24 * 60 * 60 * 1000))
        : null

      const licenseYears = (driver as any).licenseIssuedAt
        ? Math.floor((Date.now() - (driver as any).licenseIssuedAt.getTime()) / (365.25 * 24 * 60 * 60 * 1000))
        : null

      let conditionMet = false
      if (rule.condition === 'AGE_LESS_THAN' && driverAge !== null)
        conditionMet = driverAge < rule.conditionValue
      else if (rule.condition === 'AGE_GREATER_THAN' && driverAge !== null)
        conditionMet = driverAge > rule.conditionValue
      else if (rule.condition === 'LICENSE_YEARS_LESS_THAN' && licenseYears !== null)
        conditionMet = licenseYears < rule.conditionValue
      else if (rule.condition === 'LICENSE_YEARS_GREATER_THAN' && licenseYears !== null)
        conditionMet = licenseYears > rule.conditionValue

      if (!conditionMet) continue

      let amount = 0
      if (rule.adjustmentType === 'PERCENTAGE')
        amount = Math.round(baseAmount * rule.adjustmentValue / 100)
      else if (rule.adjustmentType === 'FLAT_PER_DAY')
        amount = rule.adjustmentValue * totalDays
      else
        amount = rule.adjustmentValue

      if (rule.type === 'DISCOUNT') amount = -amount

      applied.push({ ruleId: rule.id, name: rule.name, type: rule.type, amount })
    }
  }

  // Deduplicate: each rule type applied once even if multiple drivers trigger it
  const deduped = applied.reduce((acc: any[], item) => {
    if (!acc.find(a => a.ruleId === item.ruleId)) acc.push(item)
    return acc
  }, [])

  const total = deduped.reduce((sum, r) => sum + r.amount, 0)
  return { applied: deduped, total }
}

6. Fuel / Gasoline Policy (Structured)

Replace the free-text fuelPolicy string in ContractSettings with a structured enum.

// In ContractSettings — replace fuelPolicy String with:
  fuelPolicyType      FuelPolicyType  @default(FULL_TO_FULL)
  fuelPolicyNote      String?         // optional extra note
  prepaidFuelPrice    Int?            // price per liter in smallest unit (for PREPAID type)
enum FuelPolicyType {
  FULL_TO_FULL      // Customer receives full, must return full. Extra charges if returned low.
  FULL_TO_EMPTY     // Customer receives full, keeps all fuel (no refund for unused fuel).
  SAME_TO_SAME      // Returns vehicle with same level as received.
  PREPAID           // Customer pays for a full tank upfront, returns at any level.
  FREE              // Fuel included in rental price.
}
// lib/fuelPolicyLabels.ts
export const FUEL_POLICY_LABELS: Record<FuelPolicyType, Record<'en'|'fr'|'ar', string>> = {
  FULL_TO_FULL: {
    en: 'Full-to-Full: Return vehicle with a full tank. A refueling fee applies if returned with less fuel.',
    fr: 'Plein-à-Plein: Retournez le véhicule avec le plein. Des frais de carburant s\'appliquent sinon.',
    ar: 'ممتلئ إلى ممتلئ: يجب إعادة السيارة بخزان ممتلئ. رسوم إضافية في حال الإعادة بمستوى أقل.'
  },
  FULL_TO_EMPTY: {
    en: 'Full-to-Empty: Vehicle is provided with a full tank. No refund for unused fuel upon return.',
    fr: 'Plein-à-Vide: Le véhicule est fourni avec un plein. Aucun remboursement pour carburant non utilisé.',
    ar: 'ممتلئ إلى فارغ: تُسلَّم السيارة بخزان ممتلئ. لا استرداد للوقود غير المستخدم.'
  },
  SAME_TO_SAME: {
    en: 'Same-to-Same: Return vehicle with the same fuel level as at pickup.',
    fr: 'Même niveau: Retournez avec le même niveau de carburant qu\'au départ.',
    ar: 'نفس المستوى: أعد السيارة بنفس مستوى الوقود عند الاستلام.'
  },
  PREPAID: {
    en: 'Prepaid Fuel: You pre-purchase a full tank at a fixed rate. Return at any fuel level.',
    fr: 'Carburant prépayé: Vous prépayez un plein à tarif fixe. Retour à n\'importe quel niveau.',
    ar: 'الوقود المدفوع مسبقاً: تدفع مقدماً لخزان كامل بسعر ثابت. العودة بأي مستوى.'
  },
  FREE: {
    en: 'Fuel Included: Fuel cost is included in the rental price.',
    fr: 'Carburant inclus: Le coût du carburant est inclus dans le tarif de location.',
    ar: 'الوقود مشمول: تكلفة الوقود مشمولة في سعر الإيجار.'
  }
}

7. Billing Period Reporting (Accountant Export)

Companies need to export their financial data by week, month, or year.

Report model (optional — or generate entirely on-demand)

// No model needed — reports are generated on-demand from Reservation + RentalPayment data
// The API query uses date range filters

Report API

// GET /api/v1/reports/financial
// Query params: period=WEEK|MONTH|YEAR, startDate, endDate, format=JSON|CSV

export async function generateFinancialReport(
  companyId: string,
  startDate: Date,
  endDate: Date
) {
  const reservations = await prisma.reservation.findMany({
    where: {
      companyId,
      status: { in: ['CONFIRMED', 'ACTIVE', 'COMPLETED'] },
      startDate: { gte: startDate },
      endDate:   { lte: endDate }
    },
    include: {
      vehicle:    { select: { make: true, model: true, year: true, licensePlate: true } },
      customer:   { select: { firstName: true, lastName: true, email: true } },
      rentalPayments: { where: { status: 'SUCCEEDED' } },
      insurances: true,
      additionalDrivers: true,
    },
    orderBy: { startDate: 'asc' }
  })

  // Aggregate totals
  const summary = {
    totalReservations:  reservations.length,
    totalRentalRevenue: reservations.reduce((s, r) => s + r.dailyRate * r.totalDays, 0),
    totalDiscounts:     reservations.reduce((s, r) => s + r.discountAmount, 0),
    totalInsurance:     reservations.reduce((s, r) => s + r.insuranceTotal, 0),
    totalAdditionalDrivers: reservations.reduce((s, r) => s + r.additionalDriverTotal, 0),
    totalPricingRulesAdjustments: reservations.reduce((s, r) => s + r.pricingRulesTotal, 0),
    totalDeposits:      reservations.reduce((s, r) => s + r.depositAmount, 0),
    totalCollected:     reservations.reduce((s, r) => s + r.totalAmount, 0),
    averageRentalDays:  reservations.length > 0
      ? reservations.reduce((s, r) => s + r.totalDays, 0) / reservations.length : 0,
  }

  // Per-reservation rows for the table / CSV
  const rows = reservations.map(r => ({
    reservationId:    r.id,
    contractNumber:   r.contractNumber ?? '—',
    invoiceNumber:    r.invoiceNumber ?? '—',
    customerName:     `${r.customer.firstName} ${r.customer.lastName}`,
    vehicle:          `${r.vehicle.year} ${r.vehicle.make} ${r.vehicle.model}`,
    plate:            r.vehicle.licensePlate,
    startDate:        r.startDate.toISOString().split('T')[0],
    endDate:          r.endDate.toISOString().split('T')[0],
    days:             r.totalDays,
    dailyRate:        r.dailyRate,
    baseAmount:       r.dailyRate * r.totalDays,
    discount:         r.discountAmount,
    insurance:        r.insuranceTotal,
    additionalDriver: r.additionalDriverTotal,
    pricingAdj:       r.pricingRulesTotal,
    deposit:          r.depositAmount,
    totalAmount:      r.totalAmount,
    paymentStatus:    r.rentalPayments[0]?.status ?? 'UNPAID',
    paymentMethod:    r.rentalPayments[0]?.paymentMethod ?? '—',
    source:           r.source,
  }))

  return { summary, rows, period: { startDate, endDate } }
}

CSV export

// lib/csvExport.ts
export function toCsv(rows: Record<string, any>[]): string {
  if (rows.length === 0) return ''
  const headers = Object.keys(rows[0])
  const lines = [
    headers.join(','),
    ...rows.map(row =>
      headers.map(h => {
        const val = row[h] ?? ''
        return typeof val === 'string' && val.includes(',') ? `"${val}"` : String(val)
      }).join(',')
    )
  ]
  return lines.join('\n')
}

Dashboard: Reports Page

Located at /dashboard/reports (new page).

┌─────────────────────────────────────────────────────┐
│ Financial Reports                          [Export CSV] │
│                                                      │
│ Period:  [This Week ▼]   [This Month ▼]  [This Year ▼] │
│          Or custom: [From date] → [To date] [Apply] │
│                                                      │
│ ┌─────────────────────────────────────────────────┐ │
│ │  Summary Cards (6 cards in a row)               │ │
│ │  Total Bookings · Revenue · Discounts · Insurance│ │
│ │  Additional Drivers · Net Collected             │ │
│ └─────────────────────────────────────────────────┘ │
│                                                      │
│  Reservations Table (all in period)                  │
│  Contract# · Invoice# · Customer · Vehicle · Dates  │
│  Days · Base · Discount · Insurance · AddDriver      │
│  PricingAdj · Total · Payment Status · Source       │
└─────────────────────────────────────────────────────┘

8. Platform Admin Panel

The platform administrator (RentalDriveGo team) manages all companies and their staff.

Admin model

model AdminUser {
  id            String    @id @default(cuid())
  email         String    @unique
  firstName     String
  lastName      String
  passwordHash  String
  role          AdminRole @default(SUPPORT)
  isActive      Boolean   @default(true)
  lastLoginAt   DateTime?

  auditLogs     AdminAuditLog[]

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@map("admin_users")
}

enum AdminRole {
  SUPER_ADMIN   // full access: create/edit/delete companies, manage other admins
  ADMIN         // create/edit companies, manage staff, cannot delete or manage other admins
  SUPPORT       // read-only access to companies + reservations, can impersonate
}

model AdminAuditLog {
  id          String    @id @default(cuid())
  adminId     String
  admin       AdminUser @relation(fields: [adminId], references: [id])
  action      String    // e.g. "CREATE_COMPANY", "SUSPEND_COMPANY", "EDIT_EMPLOYEE"
  targetType  String    // "Company" | "Employee" | "Reservation"
  targetId    String
  before      Json?     // state before change
  after       Json?     // state after change
  ip          String?
  createdAt   DateTime  @default(now())

  @@index([adminId])
  @@index([targetType, targetId])
  @@map("admin_audit_logs")
}

Admin API routes

Base: https://admin.RentalDriveGo.com/api/v1/ (separate app)
OR:   https://api.RentalDriveGo.com/api/v1/admin/ (preferred — same API, admin middleware)

Middleware: requireAdminAuth → attaches req.admin (AdminUser)
Method Path Role Description
POST /admin/auth/login Admin login (email + password)
GET /admin/auth/me Any admin Current admin profile
Companies
GET /admin/companies Any List all companies + search/filter
POST /admin/companies ADMIN+ Create company (name, slug, email, plan)
GET /admin/companies/:id Any Full company detail
PATCH /admin/companies/:id ADMIN+ Edit company profile
PATCH /admin/companies/:id/status ADMIN+ Change status (ACTIVE/SUSPENDED)
PATCH /admin/companies/:id/plan ADMIN+ Change plan without payment
DELETE /admin/companies/:id SUPER_ADMIN Hard delete company (irreversible)
POST /admin/companies/:id/impersonate ADMIN+ Generate impersonation token
Staff
GET /admin/companies/:id/employees Any List employees
POST /admin/companies/:id/employees ADMIN+ Add employee (name, email, role)
PATCH /admin/companies/:id/employees/:eid ADMIN+ Edit employee
PATCH /admin/companies/:id/employees/:eid/role ADMIN+ Change role
DELETE /admin/companies/:id/employees/:eid ADMIN+ Deactivate employee
POST /admin/companies/:id/employees/:eid/reset-password ADMIN+ Trigger password reset email
Platform
GET /admin/metrics Any MRR, ARR, churn, signups, active
GET /admin/admins SUPER_ADMIN List admin users
POST /admin/admins SUPER_ADMIN Create admin user
PATCH /admin/admins/:id SUPER_ADMIN Edit admin
PATCH /admin/admins/:id/role SUPER_ADMIN Change admin role
DELETE /admin/admins/:id SUPER_ADMIN Deactivate admin
GET /admin/audit-log SUPER_ADMIN Full audit log

Impersonation

Admins can impersonate any company employee for support purposes:

// POST /admin/companies/:id/impersonate
// Returns a short-lived JWT that grants dashboard access as the company OWNER
// Duration: 30 minutes max
// All actions logged in AdminAuditLog
// Dashboard shows "⚠️ Impersonation Mode — RentalDriveGo Admin" banner

async function impersonateCompany(req, res) {
  const company = await prisma.company.findUniqueOrThrow({
    where: { id: req.params.id },
    include: { employees: { where: { role: 'OWNER' } } }
  })

  await prisma.adminAuditLog.create({
    data: {
      adminId: req.admin.id,
      action: 'IMPERSONATE_COMPANY',
      targetType: 'Company',
      targetId: company.id,
      ip: req.ip
    }
  })

  // Generate a short-lived JWT signed with JWT_SECRET
  const token = jwt.sign(
    { companyId: company.id, employeeId: company.employees[0]?.id, isImpersonation: true },
    process.env.JWT_SECRET!,
    { expiresIn: '30m' }
  )

  res.json({ data: { token, expiresIn: 1800 } })
}

Admin UI (admin.RentalDriveGo.com)

Separate Next.js app with its own auth (email + password, NO Clerk).

Page Description
/ → redirect to /companies
/login Admin login
/companies Searchable table: name, slug, plan, status, employees count, vehicles count, MRR, created. Actions: Edit, Suspend, Impersonate
/companies/new Create company form
/companies/:id Company profile: overview, subscription, employees, vehicles, reservations, audit log
/companies/:id/employees Employee list + add/edit/deactivate/change role
/metrics Platform KPIs: MRR, ARR, churn rate, new signups chart, top companies
/admins Admin user management (SUPER_ADMIN only)
/audit-log Full audit log with filters

Invoice: Full Price Breakdown

The CustomerInvoicePDF line items now must include all charge types:

// Full line items array for invoice generation
const lineItems = [
  // 1. Base rental
  {
    description: `${vehicle.year} ${vehicle.make} ${vehicle.model}${totalDays} day(s) × ${fmt(dailyRate)}/day`,
    qty: totalDays, unitPrice: dailyRate, total: dailyRate * totalDays,
    category: 'RENTAL'
  },

  // 2. Insurance lines (one per policy selected)
  ...insurances.map(ins => ({
    description: `${ins.policyName} (${insuranceChargeLabel(ins.chargeType, ins.chargeValue, totalDays, locale)})`,
    qty: ins.chargeType === 'PER_DAY' ? totalDays : 1,
    unitPrice: ins.chargeType === 'PER_DAY' ? ins.chargeValue : ins.totalCharge,
    total: ins.totalCharge,
    category: 'INSURANCE'
  })),

  // 3. Additional driver(s)
  ...additionalDrivers.filter(d => d.totalCharge > 0).map(d => ({
    description: `Additional Driver — ${d.firstName} ${d.lastName}`,
    qty: d.chargeType === 'PER_DAY' ? totalDays : 1,
    unitPrice: d.chargeType === 'PER_DAY' ? d.chargeValue : d.totalCharge,
    total: d.totalCharge,
    category: 'ADDITIONAL_DRIVER'
  })),

  // 4. Pricing rule adjustments (surcharges as positive, discounts as negative)
  ...(pricingRulesApplied ?? []).map((rule: any) => ({
    description: rule.name,
    qty: 1, unitPrice: rule.amount, total: rule.amount,
    category: rule.amount > 0 ? 'SURCHARGE' : 'DISCOUNT'
  })),

  // 5. Deposit (always last, marked as refundable)
  ...(depositAmount > 0 ? [{
    description: locale === 'ar' ? 'تأمين (قابل للاسترداد)' : locale === 'fr' ? 'Caution (remboursable)' : 'Security Deposit (refundable)',
    qty: 1, unitPrice: depositAmount, total: depositAmount,
    category: 'DEPOSIT'
  }] : []),
]

// Totals
const subtotal = dailyRate * totalDays
const discountTotal = -Math.abs(discountAmount + pricingDiscountsTotal)
const surchargeTotal = pricingSurchargesTotal + pricingRulesTotal
const insuranceTotal = insurances.reduce((s, i) => s + i.totalCharge, 0)
const additionalDriverTotal = additionalDrivers.reduce((s, d) => s + d.totalCharge, 0)
const taxAmount = showTax ? Math.round(subtotal * (taxRate ?? 0)) : 0
const grandTotal = subtotal + discountTotal + surchargeTotal + insuranceTotal + additionalDriverTotal + taxAmount + depositAmount

Updated ContractSettings model (additions to existing)

// ADD to model ContractSettings — replacing fuelPolicy String:
  fuelPolicyType        FuelPolicyType @default(FULL_TO_FULL)
  fuelPolicyNote        String?

  // Additional driver config
  additionalDriverCharge       AdditionalDriverCharge @default(FREE)
  additionalDriverDailyRate    Int     @default(0)
  additionalDriverFlatRate     Int     @default(0)

Updated Reservation model (additional fields)

// ADD to model Reservation:
  insurances              ReservationInsurance[]
  insuranceTotal          Int        @default(0)
  additionalDrivers       AdditionalDriver[]
  additionalDriverTotal   Int        @default(0)
  pricingRulesApplied     Json?      // snapshot of applied pricing rules
  pricingRulesTotal       Int        @default(0)
  damageReports           DamageReport[]

New Company relations (add to model Company)

  insurancePolicies     InsurancePolicy[]
  pricingRules          PricingRule[]