feat(260519-0oz-01): add QboPaymentCreatePayload, QboDepositCreatePayload types and createPayment/createDeposit methods to QboClient

- Add QboPaymentCreatePayload + QboDepositCreatePayload interfaces to lib/types/qbo.ts
- Add createPayment(payload) and createDeposit(payload) public methods to QboClient
- Both methods use existing private this.request<T>() with POST + minorversion=65
- Both methods throw descriptively if QBO returns no Id in response
This commit is contained in:
lorentz 2026-05-19 00:36:01 -04:00
parent 5f4ccb9c56
commit ef9b31e7c2
2 changed files with 93 additions and 0 deletions

View file

@ -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<QboDeposit>('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<QboPayment> {
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<QboDeposit> {
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<QboPurchase[]> {
const where = updatedSince
? `MetaData.LastUpdatedTime > '${updatedSince.toISOString()}'`
@ -278,6 +319,26 @@ export class QboClient {
});
return this.request<QboReport>(`/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<QboReport> {
const params = new URLSearchParams({
report_date: asOfDate ?? new Date().toISOString().slice(0, 10),
aging_method: 'Report_Date',
minorversion: '65',
});
return this.request<QboReport>(`/reports/AgedReceivableDetail?${params.toString()}`);
}
async getAgedReceivableSummary(asOfDate?: string): Promise<QboReport> {
const params = new URLSearchParams({
report_date: asOfDate ?? new Date().toISOString().slice(0, 10),
aging_method: 'Report_Date',
minorversion: '65',
});
return this.request<QboReport>(`/reports/AgedReceivables?${params.toString()}`);
}
}
let _instance: QboClient | null = null;

View file

@ -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<string, unknown>;
}>;
}
// 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;
}