/** * QBO client interface and implementation. * * The real QuickBooks Online API is reached via OAuth 2.0 with a server-held * refresh token (stored in AWS Secrets Manager). The implementation below * stubs the actual HTTP call so no real network I/O happens at import time. * * TODO (DEFERRED — auth layer, phase 3): Replace the stub in QboClientImpl with * a real intuit-oauth + node-quickbooks (or raw fetch) call that: * 1. Reads the refresh token from Secrets Manager at * arn:aws:secretsmanager:us-east-1:328440206208:secret:sh-mcp/qbo-oauth * 2. Exchanges it for a short-lived access token on demand (and rotates the * stored refresh token when QBO returns a new one). * 3. Issues the GET /v3/company/{realmId}/query?query=... request. * 4. Never logs or returns the access/refresh tokens. * The QboClientInterface below is the stable contract; the implementation is * injected, so tests and the real server each provide their own. */ /** A vendor record as returned by QBO's vendor query endpoint. */ export interface QboVendor { /** QBO internal vendor ID */ id: string; /** Display name of the vendor */ displayName: string; /** Primary contact email, if present */ email?: string; /** Primary phone number, if present */ phone?: string; /** Whether the vendor is currently active */ active: boolean; /** Vendor balance (amount owed), expressed as a number */ balance?: number; /** * Tax identification number. Treated as sensitive — callers MUST pass this * through redact() before including it in a tool response. */ taxId?: string; } /** Parameters forwarded to the QBO vendor query. */ export interface SearchVendorsParams { /** Free-text search term matched against vendor display name */ query: string; /** Maximum number of results to return (1–100, default 20) */ maxResults?: number; } /** Result envelope returned by the QBO vendor query. */ export interface SearchVendorsResult { vendors: QboVendor[]; /** Total matching vendors in QBO (may exceed vendors.length if paginated) */ totalCount: number; } /** * The interface every consumer codes against. * The real implementation, a test mock, and any future adapters all satisfy * this shape — nothing in src/ imports a concrete HTTP library directly. */ export interface QboClientInterface { /** * Search QBO vendors by display name. * * @throws {QboThrottleError} when QBO returns HTTP 429. * @throws {QboApiError} for any other non-2xx response. */ searchVendors(params: SearchVendorsParams): Promise; } // --------------------------------------------------------------------------- // Error types // --------------------------------------------------------------------------- export class QboApiError extends Error { constructor( message: string, public readonly statusCode: number, ) { super(message); this.name = 'QboApiError'; } } export class QboThrottleError extends Error { constructor( /** Seconds to wait before retrying, if provided by QBO */ public readonly retryAfterSeconds?: number, ) { super('QBO rate limit exceeded'); this.name = 'QboThrottleError'; } } // --------------------------------------------------------------------------- // Thin implementation (real call stubbed — see TODO above) // --------------------------------------------------------------------------- /** * Thin wrapper around the QuickBooks Online Accounting API. * * STUBBED: the actual HTTP call is replaced with a NotImplementedError so * this class can be imported without triggering real network I/O or requiring * AWS credentials. Inject a mock (see test/qbo.test.ts) in unit tests. */ export class QboClientImpl implements QboClientInterface { // eslint-disable-next-line @typescript-eslint/no-unused-vars async searchVendors(_params: SearchVendorsParams): Promise { // TODO (DEFERRED — auth layer): implement real QBO API call. // Steps: // 1. Retrieve OAuth refresh token from Secrets Manager. // 2. Exchange for QBO access token (rotate stored refresh token if renewed). // 3. Build QBO SQL query: // SELECT * FROM Vendor WHERE DisplayName LIKE '%{query}%' // STARTPOSITION 1 MAXRESULTS {maxResults} // 4. GET https://quickbooks.api.intuit.com/v3/company/{realmId}/query // with Authorization: Bearer {accessToken} // 5. Map QBO QueryResponse.Vendor[] → SearchVendorsResult. // 6. On HTTP 429 → throw QboThrottleError(retryAfterSeconds). // 7. On other non-2xx → throw QboApiError(message, statusCode). throw new Error( 'QboClientImpl.searchVendors is not yet implemented. ' + 'Inject a mock QboClientInterface for tests, or implement the real call (see TODO above).', ); } }