ADR-0032: Adapter Pattern for Channel Connectors
Status
Accepted - 2025-01-26
Context
TVL Platform integrates with multiple channels (Hostaway, Airbnb, VRBO) with different APIs, authentication, and data formats.
Decision
Adapter Pattern with shared ChannelConnector interface for all channel integrations.
Rationale
- Polymorphism: Treat all channels uniformly
- Testability: Mock adapters for unit tests
- Extensibility: Add new channels without changing core logic
- Separation of Concerns: Channel-specific logic isolated
Alternatives Considered
Alternative 1: Direct API Calls
Rejected - Tight coupling, hard to test, duplicated logic across channels
Alternative 2: API Gateway Pattern
Rejected - Overkill for MVP, adds latency, complex routing
Alternative 3: Strategy Pattern
Rejected - Similar to Adapter, but less semantic fit for external systems
Implementation
1. Channel Connector Interface
// src/integrations/ChannelConnector.ts
export interface ChannelConnector {
  // Authentication
  authenticate(credentials: ChannelCredentials): Promise<void>;
  refreshToken(): Promise<void>;
  // Property Management
  listProperties(): Promise<Property[]>;
  getProperty(externalId: string): Promise<Property>;
  createProperty(data: PropertyData): Promise<Property>;
  updateProperty(externalId: string, data: Partial<PropertyData>): Promise<Property>;
  // Booking Management
  listBookings(filters: BookingFilters): Promise<Booking[]>;
  getBooking(externalId: string): Promise<Booking>;
  createBooking(data: BookingData): Promise<Booking>;
  cancelBooking(externalId: string): Promise<void>;
  // Availability
  getAvailability(propertyId: string, dateRange: DateRange): Promise<Availability[]>;
  updateAvailability(propertyId: string, availability: Availability[]): Promise<void>;
}
2. Hostaway Adapter
// src/integrations/hostaway/HostawayConnector.ts
import axios from 'axios';
import { ChannelConnector } from '../ChannelConnector';
export class HostawayConnector implements ChannelConnector {
  private apiKey: string;
  private apiSecret: string;
  private baseURL = 'https://api.hostaway.com/v1';
  constructor(credentials: HostawayCredentials) {
    this.apiKey = credentials.apiKey;
    this.apiSecret = credentials.apiSecret;
  }
  async authenticate() {
    const response = await axios.post(`${this.baseURL}/accessTokens`, {
      grant_type: 'client_credentials',
      client_id: this.apiKey,
      client_secret: this.apiSecret,
    });
    this.accessToken = response.data.access_token;
  }
  async listProperties(): Promise<Property[]> {
    const response = await axios.get(`${this.baseURL}/listings`, {
      headers: { Authorization: `Bearer ${this.accessToken}` },
    });
    // Transform Hostaway format → TVL format
    return response.data.result.map((listing: any) => ({
      id: listing.id,
      name: listing.name,
      address: listing.address,
      unitCount: listing.bedrooms,
    }));
  }
  async createBooking(data: BookingData): Promise<Booking> {
    const response = await axios.post(
      `${this.baseURL}/reservations`,
      {
        listingId: data.propertyId,
        guestName: data.guestName,
        checkInDate: data.checkIn,
        checkOutDate: data.checkOut,
        totalPrice: data.totalCents / 100,
      },
      { headers: { Authorization: `Bearer ${this.accessToken}` } }
    );
    return {
      id: response.data.result.id,
      guestName: response.data.result.guestName,
      checkIn: response.data.result.checkInDate,
      checkOut: response.data.result.checkOutDate,
      totalCents: response.data.result.totalPrice * 100,
    };
  }
}
3. Airbnb Adapter
// src/integrations/airbnb/AirbnbConnector.ts
export class AirbnbConnector implements ChannelConnector {
  private accessToken: string;
  private baseURL = 'https://api.airbnb.com/v2';
  async listProperties(): Promise<Property[]> {
    const response = await axios.get(`${this.baseURL}/listings`, {
      headers: { 'X-Airbnb-OAuth-Token': this.accessToken },
    });
    // Transform Airbnb format → TVL format
    return response.data.listings.map((listing: any) => ({
      id: listing.id.toString(),
      name: listing.name,
      address: `${listing.city}, ${listing.state}`,
      unitCount: listing.bedrooms,
    }));
  }
}
4. Connector Factory
// src/integrations/ConnectorFactory.ts
export class ConnectorFactory {
  static create(channel: string, credentials: any): ChannelConnector {
    switch (channel) {
      case 'hostaway':
        return new HostawayConnector(credentials);
      case 'airbnb':
        return new AirbnbConnector(credentials);
      case 'vrbo':
        return new VrboConnector(credentials);
      default:
        throw new Error(`Unknown channel: ${channel}`);
    }
  }
}
// Usage
const connector = ConnectorFactory.create('hostaway', {
  apiKey: process.env.HOSTAWAY_API_KEY,
  apiSecret: process.env.HOSTAWAY_API_SECRET,
});
const properties = await connector.listProperties();
Testing Strategy
Mock Adapter for Tests
// src/integrations/__mocks__/MockConnector.ts
export class MockConnector implements ChannelConnector {
  async listProperties(): Promise<Property[]> {
    return [
      { id: '1', name: 'Test Property', address: '123 Main St', unitCount: 3 },
    ];
  }
  async createBooking(data: BookingData): Promise<Booking> {
    return { id: 'mock-123', ...data };
  }
}
// Test
describe('Booking Service', () => {
  it('should create booking via connector', async () => {
    const connector = new MockConnector();
    const booking = await bookingService.create(connector, bookingData);
    expect(booking.id).toBe('mock-123');
  });
});
Consequences
Positive
- ✅ Uniform Interface: All channels use same API
- ✅ Testability: Easy to mock connectors
- ✅ Extensibility: Add channels without changing core
- ✅ Type Safety: TypeScript enforces interface compliance
Negative
- ❌ Interface Changes: Breaking changes affect all adapters
- ❌ Lowest Common Denominator: Interface limited to shared features
Mitigations
- Version connector interface (v1, v2) for breaking changes
- Use optional methods for channel-specific features
- Document adapter-specific quirks in code comments
Validation Checklist
-  ChannelConnectorinterface defined
- Adapters for Hostaway, Airbnb, VRBO
- Connector factory for instantiation
- Mock connector for tests
- Error handling for API failures