Mock API Pattern¶
The Mock API Pattern allows AccessALI to develop independently without waiting for external API availability.
Problem Statement¶
AccessALI integrates with multiple external systems:
- SAP (contracts, buyer data)
- Salesforce (appointments, CRM)
- SpringCM (documents)
- ELedger (payments)
- Data Warehouse (construction progress)
Challenges:
- External APIs may not be ready during development
- Testing with real APIs is slow and expensive
- Development blocked waiting for API access
- Hard to reproduce edge cases
Solution: Adapter Pattern¶
// Environment variable controls behavior
USE_MOCK_SAP="true" // Development
USE_MOCK_SAP="false" // Production
// Usage is identical - adapter handles switching
import { sapAdapter } from '@/lib/adapters/sap-adapter'
const contract = await sapAdapter.getContract(id)
Architecture¶
graph LR
UC[Use Case] --> ADAPTER[Adapter]
ADAPTER --> |USE_MOCK=true| MOCK[Mock Implementation]
ADAPTER --> |USE_MOCK=false| REAL[Real API Client]
MOCK --> DB[(Mock Data<br/>in Database)]
REAL --> EXT[External API]
Implementation¶
1. Base Adapter¶
// src/lib/adapters/base-adapter.ts
export function createAdapter<T>(
MockClass: new () => T,
RealClass: new () => T | null,
envVar: string
): T {
const useMock = process.env[envVar] === 'true'
if (useMock) {
return new MockClass()
}
if (!RealClass) {
throw new Error(`Real implementation not available for ${envVar}`)
}
return new RealClass()
}
2. SAP Adapter¶
// src/lib/adapters/sap-adapter.ts
import { createAdapter } from './base-adapter'
import { SAPMock } from '@/lib/mocks/sap-mock'
import { SAPClient } from '@/lib/integrations/sap-client'
export const sapAdapter = createAdapter(
SAPMock,
SAPClient,
'USE_MOCK_SAP'
)
3. Mock Implementation¶
// src/lib/mocks/sap-mock.ts
import { prisma } from '@/lib/prisma'
export class SAPMock {
async getContract(contractId: string) {
const mock = await prisma.mockSAPContract.findUnique({
where: { contractId }
})
if (!mock) {
throw new Error(`Contract ${contractId} not found`)
}
return {
contractId: mock.contractId,
buyerId: mock.buyerId,
propertyCode: mock.propertyCode,
totalAmount: mock.totalAmount,
// ... map all fields
}
}
async getBuyerProfile(buyerId: string) {
const mock = await prisma.mockSAPBuyer.findUnique({
where: { buyerId }
})
return {
buyerId: mock.buyerId,
firstName: mock.firstName,
lastName: mock.lastName,
email: mock.email,
// ... map all fields
}
}
}
4. Real Implementation¶
// src/lib/integrations/sap-client.ts
export class SAPClient {
private baseUrl = process.env.SAP_API_URL
private apiKey = process.env.SAP_API_KEY
async getContract(contractId: string) {
const response = await fetch(
`${this.baseUrl}/contracts/${contractId}`,
{
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
}
}
)
if (!response.ok) {
throw new Error(`SAP API error: ${response.statusText}`)
}
return response.json()
}
async getBuyerProfile(buyerId: string) {
const response = await fetch(
`${this.baseUrl}/buyers/${buyerId}`,
{
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
}
}
)
return response.json()
}
}
Usage in Use Cases¶
// src/lib/use-cases/dashboard.ts
import { sapAdapter } from '@/lib/adapters/sap-adapter'
import { getUserProperties } from '@/lib/repositories/property-repository'
export async function getDashboardDataUseCase(userId: string) {
const properties = await getUserProperties(userId)
// Works with both mock and real SAP
const contracts = await Promise.all(
properties.map(p => sapAdapter.getContract(p.sapContractId))
)
return {
properties,
contracts
}
}
Migration Path¶
Phase 1: Mock Only (Weeks 1-16)¶
Focus: Build all features with predictable mock data
Phase 2: Real API Integration (Weeks 17-24)¶
# Switch one API at a time
USE_MOCK_SAP="false" # Real SAP API
USE_MOCK_SALESFORCE="true"
USE_MOCK_SPRINGCM="true"
USE_MOCK_ELEDGER="true"
# Configure real API credentials
SAP_API_URL="https://sap.ayalaland.com.ph/api"
SAP_API_KEY="production-key"
Phase 3: Production (Week 25+)¶
# All real APIs
USE_MOCK_SAP="false"
USE_MOCK_SALESFORCE="false"
USE_MOCK_SPRINGCM="false"
USE_MOCK_ELEDGER="false"
Benefits¶
-
Independent Development
- Don't wait for external APIs
- Develop at your own pace
- No external dependencies
-
Predictable Testing
- Consistent test data
- Reproducible results
- Easy edge case testing
-
Fast Iteration
- No network latency
- Instant responses
- Local development
-
Seamless Migration
- No code changes
- Environment variable switch
- Gradual rollout
-
Resilience
- Fallback to mocks if API fails
- Graceful degradation
- Always functional
Mock Data Management¶
Database Schema¶
// prisma/schema.prisma
model MockSAPContract {
contractId String @id
buyerId String
propertyCode String
totalAmount Decimal
status String
createdAt DateTime @default(now())
}
model MockSAPBuyer {
buyerId String @id
firstName String
lastName String
email String
phone String
}
Seeding Mock Data¶
// prisma/seed.ts
await prisma.mockSAPContract.createMany({
data: [
{
contractId: 'SAP-001',
buyerId: 'BUYER-001',
propertyCode: 'ALV-T1-101',
totalAmount: 5000000,
status: 'ACTIVE'
},
// ... more mock contracts
]
})
Best Practices¶
Do's¶
- ✅ Mirror real API response structure
- ✅ Include realistic data
- ✅ Store mocks in database
- ✅ Version mock data
- ✅ Document API contracts
Don'ts¶
- ❌ Hardcode mock responses
- ❌ Use different structure than real API
- ❌ Skip error scenarios
- ❌ Forget to update mocks when API changes
Testing with Mocks¶
// tests/use-cases/dashboard.test.ts
describe('getDashboardDataUseCase', () => {
beforeEach(() => {
process.env.USE_MOCK_SAP = 'true'
})
it('fetches dashboard data', async () => {
const data = await getDashboardDataUseCase('user-123')
expect(data.contracts).toHaveLength(2)
expect(data.contracts[0].contractId).toBe('SAP-001')
})
})