Next.js Integration with API v1

This guide explains how to integrate the AirDoo API v1 into a Next.js application securely and efficiently.

Secure Architecture

┌─────────────────┐   1. Client form       ┌─────────────────┐
│   Browser       ├──────────────────────►│  Next.js        │
│   (React)       │                        │  (App Router)   │
└─────────────────┘                        └─────────────────┘
         │                                          │
         │ 6. Result                                │ 2. Route Handler
         │                                          │    (server-side)
         │                                          ▼
         │                                  ┌─────────────────┐
         │                                  │  Route Handler  │
         │                                  │  /api/airdoo/*  │
         │                                  └─────────────────┘
         │                                          │
         │                                          │ 3. Odoo call
         │                                          │    with API Key
         │                                          ▼
         │                                  ┌─────────────────┐
         │                                  │  Odoo API v1    │
         │                                  │  /api/v1/airdoo │
         │                                  └─────────────────┘
         │                                          │
         │                                          │ 4. Response
         │                                          ▼
         │                                  ┌─────────────────┐
         │                                  │  Route Handler  │
         │                                  │  (transforms)   │
         │                                  └─────────────────┘
         │                                          │
         │                                          │ 5. Response to client
         └──────────────────────────────────────────┘

Why This Architecture?

  1. API Key protected: Never exposed to the browser
  2. Server-side validation: Double data validation
  3. Cache and optimization: Ability to cache responses
  4. Transformations: Format adaptation between Odoo and your frontend

File Structure

/app
├── api/
│   └── airdoo/
│       ├── availability/
│       │   └── route.ts      # POST /api/airdoo/availability
│       ├── bookings/
│       │   └── route.ts      # POST /api/airdoo/bookings
│       └── bookings/
│           └── [id]/
│               └── route.ts  # GET /api/airdoo/bookings/[id]
├── lib/
│   └── airdoo-client.ts      # Typed TypeScript client
└── components/
    └── BookingForm.tsx       # Booking component

Configuration

Environment Variables

# .env.local (DO NOT COMMIT)
ODOO_API_URL=https://your-odoo-instance.com
ODOO_API_KEY=your_secret_api_key
NEXT_PUBLIC_ODOO_API_URL=/api/airdoo  # Public URL (proxy)

TypeScript Client

// lib/airdoo-client.ts
export interface PartnerData {
  title?: string;
  first_name: string;
  last_name: string;
  email: string;
  phone?: string;
  address?: {
    street?: string;
    city?: string;
    zip?: string;
    country?: string;
  };
}

export interface BookingData {
  checkin_date: string;
  checkout_date: string;
  adults_count: number;
  children_count: number;
  babies_count: number;
  guest_notes?: string;
  payment_method: 'stripe' | 'bank_transfer';
  expected_amount: number;
  idempotency_key: string;
  payment_intent_id?: string;
}

export interface Extras {
  breakfast?: boolean;
  linen?: boolean;
  late_checkin?: boolean;
}

export interface AvailabilityRequest {
  accommodation_id: number;
  checkin: string;
  checkout: string;
  guest_count?: number;
  extras?: Extras;
}

export interface AvailabilityResponse {
  success: boolean;
  data: {
    available: boolean;
    nights: number;
    pricing?: {
      base_price: number;
      avg_price_per_night: number;
      cleaning_fee: number;
      tax: number;
      extras: Record<string, number>;
      discounts: Record<string, number>;
      total: number;
    };
    restrictions?: {
      min_stay: number;
      max_stay: number;
      max_guests: number;
      checkin_time: string;
      checkout_time: string;
    };
    message: string;
  };
  error?: {
    code: number;
    message: string;
    details?: Record<string, any>;
  };
  metadata: {
    api_version: string;
    timestamp: string;
    endpoint: string;
  };
}

export class AirDooClient {
  private baseUrl: string;

  constructor(baseUrl: string = '/api/airdoo') {
    this.baseUrl = baseUrl.replace(/\/$/, '');
  }

  async request<T>(endpoint: string, method: string = 'GET', data?: any): Promise<T> {
    const url = `${this.baseUrl}${endpoint}`;
    const options: RequestInit = {
      method,
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
      },
    };

    if (data) {
      options.body = JSON.stringify(data);
    }

    const response = await fetch(url, options);

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }

    return response.json();
  }

  async checkAvailability(request: AvailabilityRequest): Promise<AvailabilityResponse> {
    return this.request<AvailabilityResponse>('/availability', 'POST', request);
  }

  async createBooking(
    accommodationId: number,
    partnerData: PartnerData,
    bookingData: BookingData,
    extras?: Extras
  ): Promise<any> {
    return this.request('/bookings', 'POST', {
      accommodation_id: accommodationId,
      partner_data: partnerData,
      booking_data: bookingData,
      extras: extras || {},
    });
  }

  async getBooking(bookingId: number): Promise<any> {
    return this.request(`/bookings/${bookingId}`, 'GET');
  }
}

Route Handlers

1. Availability Route Handler

// app/api/airdoo/availability/route.ts
import { NextRequest, NextResponse } from 'next/server';

const ODOO_API_URL = process.env.ODOO_API_URL;
const ODOO_API_KEY = process.env.ODOO_API_KEY;

export async function POST(request: NextRequest) {
  try {
    if (!ODOO_API_URL || !ODOO_API_KEY) {
      return NextResponse.json(
        { success: false, error: { code: 500, message: 'Missing API configuration' } },
        { status: 500 }
      );
    }

    const body = await request.json();

    if (!body.accommodation_id || !body.checkin || !body.checkout) {
      return NextResponse.json(
        { success: false, error: { code: 400, message: 'Missing: accommodation_id, checkin, checkout required' } },
        { status: 400 }
      );
    }

    const response = await fetch(`${ODOO_API_URL}/api/v1/airdoo/availability`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${ODOO_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(body),
    });

    const data = await response.json();
    return NextResponse.json(data, { status: response.ok ? 200 : response.status });

  } catch (error) {
    return NextResponse.json(
      { success: false, error: { code: 500, message: 'Internal server error' } },
      { status: 500 }
    );
  }
}

2. Bookings Route Handler (Stripe Webhook Flow)

// app/api/airdoo/bookings/route.ts
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';

const ODOO_API_URL = process.env.ODOO_API_URL;
const ODOO_API_KEY = process.env.ODOO_API_KEY;
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY;
const STRIPE_PUBLISHABLE_KEY = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY;

const stripe = STRIPE_SECRET_KEY ? new Stripe(STRIPE_SECRET_KEY) : null;

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const { accommodation_id, partner_data, booking_data, extras } = body;

    if (!accommodation_id || !partner_data || !booking_data) {
      return NextResponse.json(
        { success: false, error: { code: 400, message: 'Missing required fields' } },
        { status: 400 }
      );
    }

    // Create booking in draft in Odoo
    const odooResponse = await fetch(`${ODOO_API_URL}/api/v1/airdoo/bookings`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${ODOO_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ accommodation_id, partner_data, booking_data, extras: extras || {} }),
    });

    const odooData = await odooResponse.json();

    if (!odooResponse.ok) {
      return NextResponse.json(odooData, { status: odooResponse.status });
    }

    // If Stripe payment, create PaymentIntent
    if (booking_data.payment_method === 'stripe' && stripe) {
      const paymentIntent = await stripe.paymentIntents.create({
        amount: Math.round(booking_data.expected_amount * 100),
        currency: 'eur',
        metadata: {
          booking_reference: odooData.data.booking_reference || odooData.data.confirmation_code,
          project: 'airdoo',
        },
        capture_method: 'manual',
        description: `Booking ${odooData.data.confirmation_code}`,
      });

      return NextResponse.json({
        success: true,
        data: {
          ...odooData.data,
          payment: {
            client_secret: paymentIntent.client_secret,
            publishable_key: STRIPE_PUBLISHABLE_KEY,
            payment_intent_id: paymentIntent.id,
            amount: paymentIntent.amount / 100,
            currency: paymentIntent.currency,
          }
        }
      });
    }

    return NextResponse.json(odooData);

  } catch (error) {
    return NextResponse.json(
      { success: false, error: { code: 500, message: 'Internal server error' } },
      { status: 500 }
    );
  }
}

Stripe Integration — Payment Flow

1. Browser → Next.js Route Handler → Odoo
   Odoo creates booking in "draft"
   Odoo returns booking_reference

2. Next.js creates Stripe PaymentIntent
   with metadata: { booking_reference, project: "airdoo" }
   Returns client_secret to browser

3. Browser completes Stripe payment

4. Stripe → Odoo webhook /airdoo/api/stripe/webhook
   Odoo confirms booking + sends email

5. Next.js polls /api/airdoo/bookings/{id}
   until status changes to "confirmed"

Polling for Confirmation

async function pollBookingConfirmation(bookingId: string) {
  const maxAttempts = 30;
  const interval = 2000;

  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    const response = await fetch(`/api/airdoo/bookings/${bookingId}`);
    const data = await response.json();

    if (data.data.booking_status === 'confirmed') {
      return data;
    }

    await new Promise(resolve => setTimeout(resolve, interval));
  }

  throw new Error('Timeout - Booking was not confirmed');
}

Stripe Configuration

# .env.local
STRIPE_SECRET_KEY=sk_test_xxx
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx

← Back: Webhooks | Next: iCal Export →