mirror of
https://github.com/Sea-Haven-Industries/sh-mcp.git
synced 2026-10-04 20:42:04 +00:00
127 lines
4.8 KiB
TypeScript
127 lines
4.8 KiB
TypeScript
|
|
/**
|
|||
|
|
* 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<SearchVendorsResult>;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
// 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<SearchVendorsResult> {
|
|||
|
|
// 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).',
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|