From 9e8d0838cd84a5a45605160372184f8cca4af42d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Mondaini=20Calv=C3=A3o?= Date: Mon, 22 Dec 2025 17:59:22 -0300 Subject: [PATCH 1/3] Add inspector_image_url support to createInspection method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update the createInspection method to accept an optional inspectorImageUrl parameter and include it in the API request when provided. This allows the gcp-fixle Cloud Function to use the SDK instead of making direct HTTP requests. Closes #1 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- src/client.ts | 13 +++++++--- tests/client.test.ts | 56 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/src/client.ts b/src/client.ts index c3561fa..2ce53e3 100644 --- a/src/client.ts +++ b/src/client.ts @@ -59,6 +59,7 @@ export interface InspectionRequest { external_id: string; inspection_date?: string; inspector_name?: string; + inspector_image_url?: string; notes?: string; }; } @@ -245,20 +246,26 @@ export class FixleClient { /** * Creates an inspection record for a property - * + * * @param propertyId - ID of the property to associate the inspection with * @param inspectionId - External inspection ID (from the source system) + * @param inspectorImageUrl - Optional URL to the inspector's profile image * @returns Promise that resolves when the inspection is created * @throws Error if the API request fails or the property doesn't exist - * + * * @example * await client.createInspection(123, 45678); * console.log('Inspection created successfully'); + * + * @example + * // With inspector image + * await client.createInspection(123, 45678, 'https://example.com/inspector.jpg'); */ - async createInspection(propertyId: number, inspectionId: number): Promise { + async createInspection(propertyId: number, inspectionId: number, inspectorImageUrl?: string): Promise { const inspectionData: InspectionRequest = { inspection: { external_id: inspectionId.toString(), + ...(inspectorImageUrl && { inspector_image_url: inspectorImageUrl }), }, }; diff --git a/tests/client.test.ts b/tests/client.test.ts index 46d8a1d..f6c4064 100644 --- a/tests/client.test.ts +++ b/tests/client.test.ts @@ -46,5 +46,61 @@ describe('FixleClient', () => { expect(propertyId).toBe(123); }); }); + + describe('createInspection', () => { + it('should create inspection without inspectorImageUrl', async () => { + const mockResponse = { + statusCode: 201, + on: jest.fn((event, handler) => { + if (event === 'data') handler('{"data":{"id":"456"}}'); + if (event === 'end') handler(); + }), + }; + + const http = require('http'); + let capturedBody = ''; + http.request = jest.fn((options, callback) => { + callback(mockResponse); + return { + on: jest.fn(), + write: jest.fn((data: string) => { capturedBody = data; }), + end: jest.fn(), + }; + }); + + await client.createInspection(123, 45678); + + const parsedBody = JSON.parse(capturedBody); + expect(parsedBody.inspection.external_id).toBe('45678'); + expect(parsedBody.inspection.inspector_image_url).toBeUndefined(); + }); + + it('should create inspection with inspectorImageUrl', async () => { + const mockResponse = { + statusCode: 201, + on: jest.fn((event, handler) => { + if (event === 'data') handler('{"data":{"id":"456"}}'); + if (event === 'end') handler(); + }), + }; + + const http = require('http'); + let capturedBody = ''; + http.request = jest.fn((options, callback) => { + callback(mockResponse); + return { + on: jest.fn(), + write: jest.fn((data: string) => { capturedBody = data; }), + end: jest.fn(), + }; + }); + + await client.createInspection(123, 45678, 'https://example.com/inspector.jpg'); + + const parsedBody = JSON.parse(capturedBody); + expect(parsedBody.inspection.external_id).toBe('45678'); + expect(parsedBody.inspection.inspector_image_url).toBe('https://example.com/inspector.jpg'); + }); + }); }); From cac3761047c0e279afa1eab6e6e1c1d01d70d2d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Mondaini=20Calv=C3=A3o?= Date: Fri, 26 Dec 2025 13:50:18 -0300 Subject: [PATCH 2/3] Address PR review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix test mock isolation: use jest.mock with proper reset in beforeEach - Extract mock setup into reusable helper function - Fix empty string behavior: use !== undefined check so empty strings can be explicitly sent to clear inspector images - Add test case for empty string inspectorImageUrl 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/client.ts | 2 +- tests/client.test.ts | 103 ++++++++++++++++++------------------------- 2 files changed, 45 insertions(+), 60 deletions(-) diff --git a/src/client.ts b/src/client.ts index 2ce53e3..7ea3857 100644 --- a/src/client.ts +++ b/src/client.ts @@ -265,7 +265,7 @@ export class FixleClient { const inspectionData: InspectionRequest = { inspection: { external_id: inspectionId.toString(), - ...(inspectorImageUrl && { inspector_image_url: inspectorImageUrl }), + ...(inspectorImageUrl !== undefined && { inspector_image_url: inspectorImageUrl }), }, }; diff --git a/tests/client.test.ts b/tests/client.test.ts index f6c4064..850552d 100644 --- a/tests/client.test.ts +++ b/tests/client.test.ts @@ -1,46 +1,56 @@ import { FixleClient } from '../src/client'; -// Mock http and https modules -jest.mock('http'); -jest.mock('https'); +// Mock http module +jest.mock('http', () => ({ + request: jest.fn(), +})); describe('FixleClient', () => { let client: FixleClient; + let mockRequest: jest.Mock; + let capturedBody: string; beforeEach(() => { - jest.clearAllMocks(); + capturedBody = ''; client = new FixleClient({ apiUrl: 'http://localhost:3000', apiKey: 'test-api-key', }); + + // Reset the mock before each test + const http = require('http'); + mockRequest = http.request as jest.Mock; + mockRequest.mockReset(); }); + const setupHttpMock = (responseBody: string, statusCode = 201) => { + const mockResponse = { + statusCode, + on: jest.fn((event: string, handler: (data?: string) => void) => { + if (event === 'data') handler(responseBody); + if (event === 'end') handler(); + }), + }; + + mockRequest.mockImplementation((options: unknown, callback: (res: typeof mockResponse) => void) => { + callback(mockResponse); + return { + on: jest.fn(), + write: jest.fn((data: string) => { capturedBody = data; }), + end: jest.fn(), + }; + }); + }; + describe('constructor', () => { it('should create a client with config', () => { expect(client).toBeInstanceOf(FixleClient); }); }); - describe('findOrCreateProperty', () => { it('should parse address correctly', async () => { - const mockResponse = { - statusCode: 201, - on: jest.fn((event, handler) => { - if (event === 'data') handler('{"data":{"id":"123"}}'); - if (event === 'end') handler(); - }), - }; - - const http = require('http'); - http.request = jest.fn((options, callback) => { - callback(mockResponse); - return { - on: jest.fn(), - write: jest.fn(), - end: jest.fn(), - }; - }); + setupHttpMock('{"data":{"id":"123"}}'); const propertyId = await client.findOrCreateProperty('123 Main St, Portland, OR 97201'); expect(propertyId).toBe(123); @@ -49,24 +59,7 @@ describe('FixleClient', () => { describe('createInspection', () => { it('should create inspection without inspectorImageUrl', async () => { - const mockResponse = { - statusCode: 201, - on: jest.fn((event, handler) => { - if (event === 'data') handler('{"data":{"id":"456"}}'); - if (event === 'end') handler(); - }), - }; - - const http = require('http'); - let capturedBody = ''; - http.request = jest.fn((options, callback) => { - callback(mockResponse); - return { - on: jest.fn(), - write: jest.fn((data: string) => { capturedBody = data; }), - end: jest.fn(), - }; - }); + setupHttpMock('{"data":{"id":"456"}}'); await client.createInspection(123, 45678); @@ -76,24 +69,7 @@ describe('FixleClient', () => { }); it('should create inspection with inspectorImageUrl', async () => { - const mockResponse = { - statusCode: 201, - on: jest.fn((event, handler) => { - if (event === 'data') handler('{"data":{"id":"456"}}'); - if (event === 'end') handler(); - }), - }; - - const http = require('http'); - let capturedBody = ''; - http.request = jest.fn((options, callback) => { - callback(mockResponse); - return { - on: jest.fn(), - write: jest.fn((data: string) => { capturedBody = data; }), - end: jest.fn(), - }; - }); + setupHttpMock('{"data":{"id":"456"}}'); await client.createInspection(123, 45678, 'https://example.com/inspector.jpg'); @@ -101,6 +77,15 @@ describe('FixleClient', () => { expect(parsedBody.inspection.external_id).toBe('45678'); expect(parsedBody.inspection.inspector_image_url).toBe('https://example.com/inspector.jpg'); }); + + it('should include empty string inspectorImageUrl when explicitly provided', async () => { + setupHttpMock('{"data":{"id":"456"}}'); + + await client.createInspection(123, 45678, ''); + + const parsedBody = JSON.parse(capturedBody); + expect(parsedBody.inspection.external_id).toBe('45678'); + expect(parsedBody.inspection.inspector_image_url).toBe(''); + }); }); }); - From 3e1583061cb89e5a3fcb719c38569d8d3059a088 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Mondaini=20Calv=C3=A3o?= Date: Fri, 26 Dec 2025 13:56:37 -0300 Subject: [PATCH 3/3] Add README with installation and API documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- README.md | 166 +++++++++++++++++++++++++++++------------------------- 1 file changed, 90 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index e949af6..529a155 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,65 @@ -# Fixle SDK +# @spectorasoftware/fixle-sdk -[![CI](https://github.com/spectorasoftware/fixle-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/spectorasoftware/fixle-sdk/actions/workflows/ci.yml) -[![Docs](https://img.shields.io/badge/docs-JSDoc-blue)](https://spectorasoftware.github.io/fixle-sdk/index.html) - -TypeScript SDK for interacting with the Fixle API. +TypeScript SDK for the Fixle API. Manage properties, inspections, and appliances. ## Installation ```bash -npm install @spectora/fixle-sdk +npm install @spectorasoftware/fixle-sdk +``` + +### Installing from GitHub Packages + +Add to your `.npmrc`: + +``` +@spectorasoftware:registry=https://npm.pkg.github.com ``` -Or for local development: +Then install: + ```bash -npm install ../fixle-sdk +npm install @spectorasoftware/fixle-sdk ``` -## Usage +### Installing from Git + +```bash +npm install git+https://github.com/spectorasoftware/fixle-sdk.git +``` + +## Quick Start ```typescript -import { FixleClient } from '@spectora/fixle-sdk'; +import { FixleClient } from '@spectorasoftware/fixle-sdk'; -// Initialize the client const client = new FixleClient({ - apiUrl: 'https://fixle-api.example.com', - apiKey: 'your-api-key-here', + apiUrl: 'https://api.fixle.com', + apiKey: 'your-api-key' }); -// Send inspection data to Fixle -const result = await client.sendToFixleApi({ - inspection_id: 12345, - address: '123 Main Street, Portland, OR 97201', - appliances: [ - { - item_name: 'Water Heater', - section_name: 'Plumbing', - brand: 'Rheem', - model: 'XE50M06ST45U1', - serial_number: 'ABC123456', - manufacturer: 'Rheem Manufacturing', - year: '2020', - }, - ], -}); +// Create a property +const propertyId = await client.findOrCreateProperty('123 Main St, Portland, OR 97201'); -console.log(`Created ${result} appliances`); +// Create an inspection +await client.createInspection(propertyId, 12345); + +// Create an inspection with inspector image +await client.createInspection(propertyId, 12345, 'https://example.com/inspector.jpg'); + +// Add an appliance +await client.createAppliance(propertyId, { + item_name: 'Water Heater', + section_name: 'Basement', + brand: 'Rheem', + model: 'XE50M06ST45U1', + serial_number: 'ABC123', + manufacturer: 'Rheem Manufacturing', + year: '2020' +}); ``` -## API +## API Reference ### `FixleClient` @@ -57,63 +69,66 @@ console.log(`Created ${result} appliances`); new FixleClient(config: FixleClientConfig) ``` -**Parameters:** -- `config.apiUrl` (string): Base URL of the Fixle API -- `config.apiKey` (string): API key for authentication +| Parameter | Type | Description | +|-----------|------|-------------| +| `config.apiUrl` | `string` | Base URL of the Fixle API | +| `config.apiKey` | `string` | API key for authentication | #### Methods -##### `sendToFixleApi(data: ExtractedData): Promise` - -Sends inspection data to Fixle API. Creates property, inspection, and appliances. - -**Returns:** Number of appliances successfully created - ##### `findOrCreateProperty(address: string): Promise` -Finds or creates a property by address. - -**Returns:** Property ID +Creates a new property from an address string. -##### `createInspection(propertyId: number, inspectionId: number): Promise` - -Creates an inspection for a property. - -##### `createPropertyAppliance(propertyId: number, appliance: Appliance): Promise` +```typescript +const propertyId = await client.findOrCreateProperty('123 Main St, Portland, OR 97201'); +``` -Creates a property appliance. +##### `createInspection(propertyId: number, inspectionId: number, inspectorImageUrl?: string): Promise` -## Types +Creates an inspection record for a property. ```typescript -interface ExtractedData { - inspection_id: number; - address: string; - appliances: Appliance[]; -} - -interface Appliance { - item_name: string; - section_name: string; - brand: string | null; - model: string | null; - serial_number: string | null; - manufacturer: string | null; - year: string | null; -} +// Without inspector image +await client.createInspection(123, 45678); + +// With inspector image +await client.createInspection(123, 45678, 'https://example.com/inspector.jpg'); ``` -## Documentation +| Parameter | Type | Description | +|-----------|------|-------------| +| `propertyId` | `number` | ID of the property | +| `inspectionId` | `number` | External inspection ID | +| `inspectorImageUrl` | `string` (optional) | URL to inspector's profile image | -Full API documentation is available at: https://spectorasoftware.github.io/fixle-sdk/index.html +##### `createAppliance(propertyId: number, appliance: Appliance): Promise` -To generate docs locally: +Creates an appliance record for a property. -```bash -npm run docs +```typescript +await client.createAppliance(123, { + item_name: 'Water Heater', + section_name: 'Basement', + brand: 'Rheem', + model: 'XE50M06ST45U1', + serial_number: 'ABC123', + manufacturer: 'Rheem Manufacturing', + year: '2020' +}); ``` -Then open `docs/index.html` in your browser. +### `Appliance` + +| Field | Type | Description | +|-------|------|-------------| +| `item_name` | `string` | Name of the appliance | +| `section_name` | `string` | Location/section where found | +| `brand` | `string \| null` | Brand name | +| `model` | `string \| null` | Model number | +| `serial_number` | `string \| null` | Serial number | +| `manufacturer` | `string \| null` | Manufacturer name | +| `year` | `string \| null` | Year of manufacture | ## Development @@ -127,14 +142,13 @@ npm run build # Run tests npm test -# Watch mode -npm run test:watch +# Run unit tests only +npm run test:unit -# Generate docs -npm run docs +# Run tests with coverage +npm run test:coverage ``` ## License MIT -