diff --git a/lib/services/qbo-client.ts b/lib/services/qbo-client.ts index 3352668..2c642a8 100644 --- a/lib/services/qbo-client.ts +++ b/lib/services/qbo-client.ts @@ -14,6 +14,8 @@ import { QboJournalEntry, QboReport, QboQueryResponse, + QboPaymentCreatePayload, + QboDepositCreatePayload, } from '@/lib/types/qbo'; const QBO_PRODUCTION_URL = 'https://quickbooks.api.intuit.com'; @@ -225,6 +227,45 @@ export class QboClient { return this.queryAll('Deposit', where); } + // ─── Entity Creators ───────────────────────────────────────────────────────── + + /** + * Create a Receive Payment in QBO and apply it to one or more invoices. + * Returns the created Payment as QBO echoes it back (includes Id, SyncToken). + */ + async createPayment(payload: QboPaymentCreatePayload): Promise { + const data = await this.request<{ Payment: QboPayment }>( + `/payment?minorversion=65`, + { + method: 'POST', + body: JSON.stringify(payload), + }, + ); + if (!data?.Payment?.Id) { + throw new Error(`QBO createPayment returned no Payment: ${JSON.stringify(data).slice(0, 500)}`); + } + return data.Payment; + } + + /** + * Create a Deposit in QBO that groups one or more existing Payments into a + * single bank-deposit line (so QBO's bank-feed reconciliation matches the + * bank's actual deposit slip). + */ + async createDeposit(payload: QboDepositCreatePayload): Promise { + const data = await this.request<{ Deposit: QboDeposit }>( + `/deposit?minorversion=65`, + { + method: 'POST', + body: JSON.stringify(payload), + }, + ); + if (!data?.Deposit?.Id) { + throw new Error(`QBO createDeposit returned no Deposit: ${JSON.stringify(data).slice(0, 500)}`); + } + return data.Deposit; + } + async getPurchases(updatedSince?: Date): Promise { const where = updatedSince ? `MetaData.LastUpdatedTime > '${updatedSince.toISOString()}'` @@ -278,6 +319,26 @@ export class QboClient { }); return this.request(`/reports/CashFlow?${params.toString()}`); } + + // A/R Aging Detail — one row per open invoice, with customer + balance + days + // past due. Used to reconcile Pulse's qbo_invoices against QBO's view of A/R. + async getAgedReceivableDetail(asOfDate?: string): Promise { + const params = new URLSearchParams({ + report_date: asOfDate ?? new Date().toISOString().slice(0, 10), + aging_method: 'Report_Date', + minorversion: '65', + }); + return this.request(`/reports/AgedReceivableDetail?${params.toString()}`); + } + + async getAgedReceivableSummary(asOfDate?: string): Promise { + const params = new URLSearchParams({ + report_date: asOfDate ?? new Date().toISOString().slice(0, 10), + aging_method: 'Report_Date', + minorversion: '65', + }); + return this.request(`/reports/AgedReceivables?${params.toString()}`); + } } let _instance: QboClient | null = null; diff --git a/lib/types/qbo.ts b/lib/types/qbo.ts index 6ff611e..bd40cf0 100644 --- a/lib/types/qbo.ts +++ b/lib/types/qbo.ts @@ -87,6 +87,37 @@ export interface QboDeposit { MetaData?: QboMetaData; } +// Payload for creating a Payment via POST /v3/company/{realmId}/payment +// Only the fields we actually send — QBO accepts many more but we keep the +// surface area small and explicit. +export interface QboPaymentCreatePayload { + CustomerRef: QboRef; // { value: customer_ref_id } + TotalAmt: number; + TxnDate?: string; // YYYY-MM-DD + DepositToAccountRef?: QboRef; // { value: account id } + PaymentRefNum?: string; // check number (max 21 chars per QBO) + PrivateNote?: string; + Line?: Array<{ + Amount: number; + LinkedTxn?: QboLinkedTxn[]; // one entry per invoice being paid + }>; +} + +// Payload for creating a Deposit via POST /v3/company/{realmId}/deposit +// Each Line links to a previously-created Payment so QBO groups them as a +// single bank deposit (matches the bank slip). +export interface QboDepositCreatePayload { + TxnDate?: string; + DepositToAccountRef: QboRef; // required for deposit + PrivateNote?: string; + Line: Array<{ + Amount: number; + DetailType: 'DepositLineDetail'; + LinkedTxn?: QboLinkedTxn[]; // [{ TxnId: payment_id, TxnType: 'Payment' }] + DepositLineDetail?: Record; + }>; +} + // Purchase (expense/credit card) export interface QboPurchase { Id: string; @@ -156,6 +187,7 @@ export interface QboEntitySyncResult { entity: string; success: boolean; recordsUpserted: number; + recordsDeleted?: number; duration: number; error?: string; }