Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
32e84a7
refactor(OUT-3543): decompose Item/Account response schemas into row …
SandipBajracharya May 12, 2026
785c6b0
refactor(OUT-3543): rename QBInvoiceResponseSchema to row, add envelo…
SandipBajracharya May 12, 2026
e04676d
feat(OUT-3543): add Payment/Purchase response envelope + row + delete…
SandipBajracharya May 12, 2026
46fc2e0
refactor(OUT-3543): replace inline IntuitAPI interfaces with dto z.in…
SandipBajracharya May 12, 2026
eeb45ff
feat(OUT-3543): zod-parse Item method responses
SandipBajracharya May 12, 2026
405a125
feat(OUT-3543): zod-parse Account method responses
SandipBajracharya May 12, 2026
c286538
feat(OUT-3543): zod-parse Customer create/update responses
SandipBajracharya May 12, 2026
ee80e0e
feat(OUT-3543): zod-parse Invoice create/update/void/delete responses
SandipBajracharya May 12, 2026
201f65e
feat(OUT-3543): zod-parse Payment/Purchase create+delete responses
SandipBajracharya May 12, 2026
b8c5c2f
refactor(OUT-3543): tighten _customQuery return, narrow body type, re…
SandipBajracharya May 12, 2026
8ffe6a3
fix(OUT-3543): align consumers with tightened IntuitAPI return types
SandipBajracharya May 12, 2026
e707a92
fix(OUT-3543): align schemas with SQL projections and consumer reads
SandipBajracharya May 12, 2026
dd8005a
chore(OUT-3543): drop stale CustomerListRowSchema comment
SandipBajracharya May 12, 2026
1505c69
refactor(OUT-3543): centralize Fault handling and tighten envelope sc…
SandipBajracharya May 13, 2026
1103ff5
test(OUT-3543): add unit tests for IntuitAPI response-parse paths
SandipBajracharya May 13, 2026
3726c16
chore(OUT-3543): lint fix
SandipBajracharya May 13, 2026
fa19ce2
fix(OUT-3543): drop misleading cast on APIError.errors in assertNotQB…
SandipBajracharya May 13, 2026
f7fd89f
fix(OUT-3543): address Greptile review — defensive parsing and fixed-…
SandipBajracharya May 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/app/api/quickbooks/product/product.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ export async function getItemsFromQB(req: NextRequest) {
const items = await productService.queryItemsFromQB(
qbTokenInfo,
MAX_PRODUCT_LIST_LIMIT,
['Id', 'Name', 'UnitPrice', 'SyncToken', 'Description'],
)
return NextResponse.json(items)
}
8 changes: 2 additions & 6 deletions src/app/api/quickbooks/product/product.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -646,13 +646,9 @@ export class ProductService extends BaseService {
})
}

async queryItemsFromQB(
qbTokenInfo: IntuitAPITokensType,
limit: number,
columns: string[],
) {
async queryItemsFromQB(qbTokenInfo: IntuitAPITokensType, limit: number) {
const intuitApi = new IntuitAPI(qbTokenInfo)
return await intuitApi.getAllItems(limit, columns)
return await intuitApi.getAllItems(limit)
}

async formatAndSyncProductLogs(payload: ProductChangedItemReferenceType[]) {
Expand Down
8 changes: 1 addition & 7 deletions src/cmd/backfillProductInfo/backfillProductInfo.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,7 @@ export class BackfillProductInfoService extends BaseService {
}

const intuitApi = new IntuitAPI(qbTokenInfo)
const allQbItems = await intuitApi.getAllItems(MAX_PRODUCT_LIST_LIMIT, [
'Id',
'Name',
'UnitPrice',
'Description',
'SyncToken',
])
const allQbItems = await intuitApi.getAllItems(MAX_PRODUCT_LIST_LIMIT)

// 3. update the product info in our mapping table
for (const mappedProduct of mappedProducts) {
Expand Down
2 changes: 1 addition & 1 deletion src/helper/fetch.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ const resolveSignal = (opts: FetcherOptions): AbortSignal | undefined => {
export const postFetcher = async (
url: string,
headers: Record<string, string>,
body: Record<string, any>,
body: Record<string, unknown>,
opts: FetcherOptions = {},
) => {
const response = await fetch(url, {
Expand Down
186 changes: 165 additions & 21 deletions src/type/dto/intuitAPI.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ export const QBNameValueSchema = z.object({
})
export type QBNameValueSchemaType = z.infer<typeof QBNameValueSchema>

// QBO returns Fault on any failed response. Error is loose (object or array)
// across endpoints; we forward whatever shape arrived so callers/log
// consumers can inspect it.
export const QBFaultSchema = z.object({
Fault: z.object({
Error: z.unknown().optional(),
}),
})
export type QBFaultType = z.infer<typeof QBFaultSchema>

export const QBInvoiceLineItemSchema = z.object({
DetailType: z.string(),
Amount: z.number(),
Expand Down Expand Up @@ -96,18 +106,29 @@ export type QBItemFullUpdatePayloadType = z.infer<
typeof QBItemFullUpdatePayloadSchema
>

export const QBItemRowSchema = z.object({
Id: z.string(),
SyncToken: z.string(),
Name: z.string(),
ClassRef: QBNameValueSchema.optional(),
Active: z.boolean().optional(),
UnitPrice: z.number(),
Description: z.string().nullish(),
})
export type QBItemRowType = z.infer<typeof QBItemRowSchema>

export const QBItemResponseSchema = z.object({
Item: z.object({
Id: z.string(),
SyncToken: z.string(),
Name: z.string(),
ClassRef: QBNameValueSchema.optional(),
Active: z.boolean(),
UnitPrice: z.number(),
}),
Item: QBItemRowSchema,
})
export type QBItemResponseType = z.infer<typeof QBItemResponseSchema>

// Envelope returned by `customQuery` for `SELECT ... FROM Item`. Item is
// optional because QBO omits the key when there are zero results.
export const QBItemQueryResponseSchema = z.object({
Item: z.array(QBItemRowSchema).optional(),
})
export type QBItemQueryResponseType = z.infer<typeof QBItemQueryResponseSchema>

export const QBPaymentCreatePayloadSchema = z.object({
TotalAmt: z.number(),
CustomerRef: z.object({
Expand Down Expand Up @@ -164,16 +185,26 @@ export type QBAccountUpdatePayloadType = z.infer<
typeof QBAccountUpdatePayloadSchema
>

export const QBAccountRowSchema = z.object({
Id: z.string(),
Name: z.string(),
SyncToken: z.string(),
Active: z.boolean(),
})
export type QBAccountRowType = z.infer<typeof QBAccountRowSchema>

export const QBAccountResponseSchema = z.object({
Account: z.object({
Id: z.string(),
Name: z.string(),
SyncToken: z.string(),
Active: z.boolean(),
}),
Account: QBAccountRowSchema,
})
export type QBAccountResponseType = z.infer<typeof QBAccountResponseSchema>

export const QBAccountQueryResponseSchema = z.object({
Account: z.array(QBAccountRowSchema).optional(),
})
export type QBAccountQueryResponseType = z.infer<
typeof QBAccountQueryResponseSchema
>

export const QBPurchaseCreatePayloadSchema = z.object({
PaymentType: z.literal('Cash'),
AccountRef: QBNameValueSchema,
Expand Down Expand Up @@ -204,7 +235,7 @@ export type QBDeletePayloadType = z.infer<typeof QBDeletePayloadSchema>
export const CompanyInfoSchema = z.object({
CompanyInfo: z.array(
z.object({
Country: z.string(),
Country: z.string().optional(),
}),
),
})
Expand All @@ -215,31 +246,141 @@ export const CustomerQueryResponseSchema = z.object({
SyncToken: z.string(),
Active: z.boolean(),
CompanyName: z.string().optional(),
FullyQualifiedName: z.string().optional(),
// PrimaryEmailAddr and its Address are .nullish() because a single
// malformed row (null email object, or present-but-null Address) must not
// ZodError the entire paginated walk in _getCustomerByEmail. The find()
// predicate narrows with `typeof addr === 'string'`.
PrimaryEmailAddr: z
.object({
Address: z.string(),
Address: z.string().nullish(),
})
.optional(),
.nullish(),
})

export type CustomerQueryResponseType = z.infer<
typeof CustomerQueryResponseSchema
>

export const QBInvoiceResponseSchema = z.object({
// Envelope returned by createCustomer / customerSparseUpdate.
export const QBCustomerResponseSchema = z.object({
Customer: CustomerQueryResponseSchema,
})
export type QBCustomerResponseType = z.infer<typeof QBCustomerResponseSchema>

export const CustomerListEnvelopeSchema = z.object({
Customer: z.array(CustomerQueryResponseSchema).optional(),
})
export type CustomerListEnvelopeType = z.infer<
typeof CustomerListEnvelopeSchema
>

export const QBInvoiceRowSchema = z.object({
Id: z.string(),
Balance: z.number(),
PrivateNote: z.string().optional(),
SyncToken: z.string(),
DocNumber: z.string().optional(),
Balance: z.number().optional(),
TotalAmt: z.number().optional(),
TxnDate: z.string().optional(),
DueDate: z.string().optional(),
PrivateNote: z.string().optional(),
CustomerRef: QBNameValueSchema.optional(),
})
export type QBInvoiceRowType = z.infer<typeof QBInvoiceRowSchema>

// Envelope returned by createInvoice / invoiceSparseUpdate / voidInvoice.
export const QBInvoiceResponseSchema = z.object({
Invoice: QBInvoiceRowSchema,
})
export type QBInvoiceResponseType = z.infer<typeof QBInvoiceResponseSchema>

export const QBPurchaseResponseSchema = z.object({
// Envelope returned by customQuery for SELECT ... FROM Invoice. Invoice is
// optional because QBO omits the key when there are zero results.
export const QBInvoiceQueryResponseSchema = z.object({
Invoice: z.array(QBInvoiceRowSchema).optional(),
})
export type QBInvoiceQueryResponseType = z.infer<
typeof QBInvoiceQueryResponseSchema
>

// Envelope returned by deleteInvoice (no full row, just deletion confirmation).
export const QBInvoiceDeleteResponseSchema = z.object({
Invoice: z.object({
Id: z.string(),
status: z.string().optional(),
domain: z.string().optional(),
}),
})
export type QBInvoiceDeleteResponseType = z.infer<
typeof QBInvoiceDeleteResponseSchema
>

export const QBPurchaseRowSchema = z.object({
Id: z.string(),
SyncToken: z.string(),
TotalAmt: z.number(),
TxnDate: z.string().optional(),
AccountRef: QBNameValueSchema.optional(),
PaymentType: z.string().optional(),
})
export type QBPurchaseRowType = z.infer<typeof QBPurchaseRowSchema>

export const QBPurchaseResponseSchema = z.object({
Purchase: QBPurchaseRowSchema,
})
export type QBPurchaseResponseType = z.infer<typeof QBPurchaseResponseSchema>

export const QBPurchaseDeleteResponseSchema = z.object({
Purchase: z.object({
Id: z.string(),
status: z.string().optional(),
domain: z.string().optional(),
}),
})
export type QBPurchaseDeleteResponseType = z.infer<
typeof QBPurchaseDeleteResponseSchema
>

export const QBPaymentRowSchema = z.object({
Id: z.string(),
SyncToken: z.string(),
TotalAmt: z.number(),
TxnDate: z.string().optional(),
CustomerRef: QBNameValueSchema.optional(),
Line: z
.array(
z.object({
Amount: z.number().optional(),
LinkedTxn: z
.array(
z.object({
TxnId: z.string(),
TxnType: z.string(),
}),
)
.optional(),
}),
)
.optional(),
})
export type QBPaymentRowType = z.infer<typeof QBPaymentRowSchema>

export const QBPaymentResponseSchema = z.object({
Payment: QBPaymentRowSchema,
})
export type QBPaymentResponseType = z.infer<typeof QBPaymentResponseSchema>

export const QBPaymentDeleteResponseSchema = z.object({
Payment: z.object({
Id: z.string(),
status: z.string().optional(),
domain: z.string().optional(),
}),
})
export type QBPaymentDeleteResponseType = z.infer<
typeof QBPaymentDeleteResponseSchema
>

export const QBItemsResponseSchema = z.array(
z.object({
Id: z.string(),
Expand All @@ -255,3 +396,6 @@ export const SingleIdAndTokenResponseSchema = z.object({
Id: z.string(),
SyncToken: z.string(),
})
export type SingleIdAndTokenResponseType = z.infer<
typeof SingleIdAndTokenResponseSchema
>
Loading
Loading