/* eslint-disable n8n-nodes-base/node-filename-against-convention */
import type {
	IExecuteFunctions,
	IDataObject,
	ILoadOptionsFunctions,
	INodeExecutionData,
	INodeType,
	INodeTypeDescription,
} from 'n8n-workflow';
import { NodeConnectionType, NodeOperationError } from 'n8n-workflow';

import { capitalCase } from 'change-case';
import isEmpty from 'lodash/isEmpty';
import {
	billFields,
	billOperations,
	customerFields,
	customerOperations,
	employeeFields,
	employeeOperations,
	estimateFields,
	estimateOperations,
	invoiceFields,
	invoiceOperations,
	itemFields,
	itemOperations,
	paymentFields,
	paymentOperations,
	purchaseFields,
	purchaseOperations,
	transactionFields,
	transactionOperations,
	vendorFields,
	vendorOperations,
} from './descriptions';

import {
	adjustTransactionDates,
	getRefAndSyncToken,
	getSyncToken,
	handleBinaryData,
	handleListing,
	loadResource,
	populateFields,
	processLines,
	quickBooksApiRequest,
	simplifyTransactionReport,
} from './GenericFunctions';

import type { QuickBooksOAuth2Credentials, TransactionFields, TransactionReport } from './types';

export class QuickBooks implements INodeType {
	description: INodeTypeDescription = {
		displayName: 'QuickBooks Online',
		name: 'quickbooks',
		icon: 'file:quickbooks.svg',
		group: ['transform'],
		version: 1,
		subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
		description: 'Consume the QuickBooks Online API',
		defaults: {
			name: 'QuickBooks Online',
		},
		inputs: [NodeConnectionType.Main],
		outputs: [NodeConnectionType.Main],
		credentials: [
			{
				name: 'quickBooksOAuth2Api',
				required: true,
			},
		],
		properties: [
			{
				displayName: 'Resource',
				name: 'resource',
				type: 'options',
				noDataExpression: true,
				options: [
					{
						name: 'Bill',
						value: 'bill',
					},
					{
						name: 'Customer',
						value: 'customer',
					},
					{
						name: 'Employee',
						value: 'employee',
					},
					{
						name: 'Estimate',
						value: 'estimate',
					},
					{
						name: 'Invoice',
						value: 'invoice',
					},
					{
						name: 'Item',
						value: 'item',
					},
					{
						name: 'Payment',
						value: 'payment',
					},
					{
						name: 'Purchase',
						value: 'purchase',
					},
					{
						name: 'Transaction',
						value: 'transaction',
					},
					{
						name: 'Vendor',
						value: 'vendor',
					},
				],
				default: 'customer',
			},
			...billOperations,
			...billFields,
			...customerOperations,
			...customerFields,
			...employeeOperations,
			...employeeFields,
			...estimateOperations,
			...estimateFields,
			...invoiceOperations,
			...invoiceFields,
			...itemOperations,
			...itemFields,
			...paymentOperations,
			...paymentFields,
			...purchaseOperations,
			...purchaseFields,
			...transactionOperations,
			...transactionFields,
			...vendorOperations,
			...vendorFields,
		],
	};

	methods = {
		loadOptions: {
			async getCustomers(this: ILoadOptionsFunctions) {
				return await loadResource.call(this, 'customer');
			},

			async getCustomFields(this: ILoadOptionsFunctions) {
				return await loadResource.call(this, 'preferences');
			},

			async getDepartments(this: ILoadOptionsFunctions) {
				return await loadResource.call(this, 'department');
			},

			async getItems(this: ILoadOptionsFunctions) {
				return await loadResource.call(this, 'item');
			},

			async getMemos(this: ILoadOptionsFunctions) {
				return await loadResource.call(this, 'CreditMemo');
			},

			async getPurchases(this: ILoadOptionsFunctions) {
				return await loadResource.call(this, 'purchase');
			},

			async getTaxCodeRefs(this: ILoadOptionsFunctions) {
				return await loadResource.call(this, 'TaxCode');
			},

			async getTerms(this: ILoadOptionsFunctions) {
				return await loadResource.call(this, 'Term');
			},

			async getVendors(this: ILoadOptionsFunctions) {
				return await loadResource.call(this, 'vendor');
			},
		},
	};

	async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
		const items = this.getInputData();

		const resource = this.getNodeParameter('resource', 0);
		const operation = this.getNodeParameter('operation', 0);

		let responseData;
		const returnData: INodeExecutionData[] = [];

		const { oauthTokenData } = (await this.getCredentials(
			'quickBooksOAuth2Api',
		)) as QuickBooksOAuth2Credentials;
		const companyId = oauthTokenData.callbackQueryString.realmId;

		for (let i = 0; i < items.length; i++) {
			try {
				if (resource === 'bill') {
					// *********************************************************************
					//                            bill
					// *********************************************************************

					// https://developer.intuit.com/app/developer/qbo/docs/api/accounting/most-commonly-used/bill

					if (operation === 'create') {
						// ----------------------------------
						//         bill: create
						// ----------------------------------

						const lines = this.getNodeParameter('Line', i) as IDataObject[];

						if (!lines.length) {
							throw new NodeOperationError(
								this.getNode(),
								`Please enter at least one line for the ${resource}.`,
								{ itemIndex: i },
							);
						}

						if (
							lines.some(
								(line) =>
									line.DetailType === undefined ||
									line.Amount === undefined ||
									line.Description === undefined,
							)
						) {
							throw new NodeOperationError(
								this.getNode(),
								'Please enter detail type, amount and description for every line.',
								{ itemIndex: i },
							);
						}

						lines.forEach((line) => {
							if (
								line.DetailType === 'AccountBasedExpenseLineDetail' &&
								line.accountId === undefined
							) {
								throw new NodeOperationError(
									this.getNode(),
									'Please enter an account ID for the associated line.',
									{ itemIndex: i },
								);
							} else if (
								line.DetailType === 'ItemBasedExpenseLineDetail' &&
								line.itemId === undefined
							) {
								throw new NodeOperationError(
									this.getNode(),
									'Please enter an item ID for the associated line.',
									{ itemIndex: i },
								);
							}
						});

						let body = {
							VendorRef: {
								value: this.getNodeParameter('VendorRef', i),
							},
						} as IDataObject;

						body.Line = processLines.call(this, lines, resource);

						const additionalFields = this.getNodeParameter('additionalFields', i);

						body = populateFields.call(this, body, additionalFields, resource);

						const endpoint = `/v3/company/${companyId}/${resource}`;
						responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body);
						responseData = responseData[capitalCase(resource)];
					} else if (operation === 'delete') {
						// ----------------------------------
						//         bill: delete
						// ----------------------------------

						const qs = {
							operation: 'delete',
						} as IDataObject;

						const body = {
							Id: this.getNodeParameter('billId', i),
							SyncToken: await getSyncToken.call(this, i, companyId, resource),
						} as IDataObject;

						const endpoint = `/v3/company/${companyId}/${resource}`;
						responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, qs, body);
						responseData = responseData[capitalCase(resource)];
					} else if (operation === 'get') {
						// ----------------------------------
						//         bill: get
						// ----------------------------------

						const billId = this.getNodeParameter('billId', i);
						const endpoint = `/v3/company/${companyId}/${resource}/${billId}`;
						responseData = await quickBooksApiRequest.call(this, 'GET', endpoint, {}, {});
						responseData = responseData[capitalCase(resource)];
					} else if (operation === 'getAll') {
						// ----------------------------------
						//         bill: getAll
						// ----------------------------------

						const endpoint = `/v3/company/${companyId}/query`;
						responseData = await handleListing.call(this, i, endpoint, resource);
					} else if (operation === 'update') {
						// ----------------------------------
						//         bill: update
						// ----------------------------------

						const { ref, syncToken } = await getRefAndSyncToken.call(
							this,
							i,
							companyId,
							resource,
							'VendorRef',
						);

						let body = {
							Id: this.getNodeParameter('billId', i),
							SyncToken: syncToken,
							sparse: true,
							VendorRef: {
								name: ref.name,
								value: ref.value,
							},
						} as IDataObject;

						const updateFields = this.getNodeParameter('updateFields', i);

						if (isEmpty(updateFields)) {
							throw new NodeOperationError(
								this.getNode(),
								`Please enter at least one field to update for the ${resource}.`,
								{ itemIndex: i },
							);
						}

						body = populateFields.call(this, body, updateFields, resource);

						const endpoint = `/v3/company/${companyId}/${resource}`;
						responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body);
						responseData = responseData[capitalCase(resource)];
					}
				} else if (resource === 'customer') {
					// *********************************************************************
					//                            customer
					// *********************************************************************

					// https://developer.intuit.com/app/developer/qbo/docs/api/accounting/most-commonly-used/customer

					if (operation === 'create') {
						// ----------------------------------
						//         customer: create
						// ----------------------------------

						let body = {
							DisplayName: this.getNodeParameter('displayName', i),
						} as IDataObject;

						const additionalFields = this.getNodeParameter('additionalFields', i);

						body = populateFields.call(this, body, additionalFields, resource);

						const endpoint = `/v3/company/${companyId}/${resource}`;
						responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body);
						responseData = responseData[capitalCase(resource)];
					} else if (operation === 'get') {
						// ----------------------------------
						//         customer: get
						// ----------------------------------

						const customerId = this.getNodeParameter('customerId', i);
						const endpoint = `/v3/company/${companyId}/${resource}/${customerId}`;
						responseData = await quickBooksApiRequest.call(this, 'GET', endpoint, {}, {});
						responseData = responseData[capitalCase(resource)];
					} else if (operation === 'getAll') {
						// ----------------------------------
						//         customer: getAll
						// ----------------------------------

						const endpoint = `/v3/company/${companyId}/query`;
						responseData = await handleListing.call(this, i, endpoint, resource);
					} else if (operation === 'update') {
						// ----------------------------------
						//         customer: update
						// ----------------------------------

						let body = {
							Id: this.getNodeParameter('customerId', i),
							SyncToken: await getSyncToken.call(this, i, companyId, resource),
							sparse: true,
						} as IDataObject;

						const updateFields = this.getNodeParameter('updateFields', i);

						if (isEmpty(updateFields)) {
							throw new NodeOperationError(
								this.getNode(),
								`Please enter at least one field to update for the ${resource}.`,
								{ itemIndex: i },
							);
						}

						body = populateFields.call(this, body, updateFields, resource);

						const endpoint = `/v3/company/${companyId}/${resource}`;
						responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body);
						responseData = responseData[capitalCase(resource)];
					}
				} else if (resource === 'employee') {
					// *********************************************************************
					//                            employee
					// *********************************************************************

					if (operation === 'create') {
						// ----------------------------------
						//         employee: create
						// ----------------------------------

						let body = {
							FamilyName: this.getNodeParameter('FamilyName', i),
							GivenName: this.getNodeParameter('GivenName', i),
						} as IDataObject;

						const additionalFields = this.getNodeParameter('additionalFields', i);

						body = populateFields.call(this, body, additionalFields, resource);

						const endpoint = `/v3/company/${companyId}/${resource}`;
						responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body);
						responseData = responseData[capitalCase(resource)];
					} else if (operation === 'get') {
						// ----------------------------------
						//         employee: get
						// ----------------------------------

						const employeeId = this.getNodeParameter('employeeId', i);
						const endpoint = `/v3/company/${companyId}/${resource}/${employeeId}`;
						responseData = await quickBooksApiRequest.call(this, 'GET', endpoint, {}, {});
						responseData = responseData[capitalCase(resource)];
					} else if (operation === 'getAll') {
						// ----------------------------------
						//         employee: getAll
						// ----------------------------------

						const endpoint = `/v3/company/${companyId}/query`;
						responseData = await handleListing.call(this, i, endpoint, resource);
					} else if (operation === 'update') {
						// ----------------------------------
						//         employee: update
						// ----------------------------------

						let body = {
							Id: this.getNodeParameter('employeeId', i),
							SyncToken: await getSyncToken.call(this, i, companyId, resource),
							sparse: true,
						} as IDataObject;

						const updateFields = this.getNodeParameter('updateFields', i);

						if (isEmpty(updateFields)) {
							throw new NodeOperationError(
								this.getNode(),
								`Please enter at least one field to update for the ${resource}.`,
								{ itemIndex: i },
							);
						}

						body = populateFields.call(this, body, updateFields, resource);

						const endpoint = `/v3/company/${companyId}/${resource}`;
						responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body);
						responseData = responseData[capitalCase(resource)];
					}
				} else if (resource === 'estimate') {
					// *********************************************************************
					//                            estimate
					// *********************************************************************

					// https://developer.intuit.com/app/developer/qbo/docs/api/accounting/most-commonly-used/estimate

					if (operation === 'create') {
						// ----------------------------------
						//         estimate: create
						// ----------------------------------

						const lines = this.getNodeParameter('Line', i) as IDataObject[];

						if (!lines.length) {
							throw new NodeOperationError(
								this.getNode(),
								`Please enter at least one line for the ${resource}.`,
								{ itemIndex: i },
							);
						}

						if (
							lines.some(
								(line) =>
									line.DetailType === undefined ||
									line.Amount === undefined ||
									line.Description === undefined,
							)
						) {
							throw new NodeOperationError(
								this.getNode(),
								'Please enter detail type, amount and description for every line.',
								{ itemIndex: i },
							);
						}

						lines.forEach((line) => {
							if (line.DetailType === 'SalesItemLineDetail' && line.itemId === undefined) {
								throw new NodeOperationError(
									this.getNode(),
									'Please enter an item ID for the associated line.',
									{ itemIndex: i },
								);
							}
						});

						let body = {
							CustomerRef: {
								value: this.getNodeParameter('CustomerRef', i),
							},
						} as IDataObject;

						body.Line = processLines.call(this, lines, resource);
						const additionalFields = this.getNodeParameter('additionalFields', i);

						body = populateFields.call(this, body, additionalFields, resource);

						const endpoint = `/v3/company/${companyId}/${resource}`;
						responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body);
						responseData = responseData[capitalCase(resource)];
					} else if (operation === 'delete') {
						// ----------------------------------
						//         estimate: delete
						// ----------------------------------

						const qs = {
							operation: 'delete',
						} as IDataObject;

						const body = {
							Id: this.getNodeParameter('estimateId', i),
							SyncToken: await getSyncToken.call(this, i, companyId, resource),
						} as IDataObject;

						const endpoint = `/v3/company/${companyId}/${resource}`;
						responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, qs, body);
						responseData = responseData[capitalCase(resource)];
					} else if (operation === 'get') {
						// ----------------------------------
						//         estimate: get
						// ----------------------------------

						const estimateId = this.getNodeParameter('estimateId', i) as string;
						const download = this.getNodeParameter('download', i);

						if (download) {
							responseData = await handleBinaryData.call(
								this,
								items,
								i,
								companyId,
								resource,
								estimateId,
							);
						} else {
							const endpoint = `/v3/company/${companyId}/${resource}/${estimateId}`;
							responseData = await quickBooksApiRequest.call(this, 'GET', endpoint, {}, {});
							responseData = responseData[capitalCase(resource)];
						}
					} else if (operation === 'getAll') {
						// ----------------------------------
						//         estimate: getAll
						// ----------------------------------

						const endpoint = `/v3/company/${companyId}/query`;
						responseData = await handleListing.call(this, i, endpoint, resource);
					} else if (operation === 'send') {
						// ----------------------------------
						//         estimate: send
						// ----------------------------------

						const estimateId = this.getNodeParameter('estimateId', i) as string;

						const qs = {
							sendTo: this.getNodeParameter('email', i) as string,
						} as IDataObject;

						const endpoint = `/v3/company/${companyId}/${resource}/${estimateId}/send`;
						responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, qs, {});
						responseData = responseData[capitalCase(resource)];
					} else if (operation === 'update') {
						// ----------------------------------
						//         estimate: update
						// ----------------------------------

						const { ref, syncToken } = await getRefAndSyncToken.call(
							this,
							i,
							companyId,
							resource,
							'CustomerRef',
						);

						let body = {
							Id: this.getNodeParameter('estimateId', i),
							SyncToken: syncToken,
							sparse: true,
							CustomerRef: {
								name: ref.name,
								value: ref.value,
							},
						} as IDataObject;

						const updateFields = this.getNodeParameter('updateFields', i);

						if (isEmpty(updateFields)) {
							throw new NodeOperationError(
								this.getNode(),
								`Please enter at least one field to update for the ${resource}.`,
								{ itemIndex: i },
							);
						}

						body = populateFields.call(this, body, updateFields, resource);

						const endpoint = `/v3/company/${companyId}/${resource}`;
						responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body);
						responseData = responseData[capitalCase(resource)];
					}
				} else if (resource === 'invoice') {
					// *********************************************************************
					//                            invoice
					// *********************************************************************

					// https://developer.intuit.com/app/developer/qbo/docs/api/accounting/most-commonly-used/invoice

					if (operation === 'create') {
						// ----------------------------------
						//         invoice: create
						// ----------------------------------

						const lines = this.getNodeParameter('Line', i) as IDataObject[];

						if (!lines.length) {
							throw new NodeOperationError(
								this.getNode(),
								`Please enter at least one line for the ${resource}.`,
								{ itemIndex: i },
							);
						}

						if (
							lines.some(
								(line) =>
									line.DetailType === undefined ||
									line.Amount === undefined ||
									line.Description === undefined,
							)
						) {
							throw new NodeOperationError(
								this.getNode(),
								'Please enter detail type, amount and description for every line.',
								{ itemIndex: i },
							);
						}

						lines.forEach((line) => {
							if (line.DetailType === 'SalesItemLineDetail' && line.itemId === undefined) {
								throw new NodeOperationError(
									this.getNode(),
									'Please enter an item ID for the associated line.',
									{ itemIndex: i },
								);
							}
						});

						let body = {
							CustomerRef: {
								value: this.getNodeParameter('CustomerRef', i),
							},
						} as IDataObject;

						body.Line = processLines.call(this, lines, resource);

						const additionalFields = this.getNodeParameter('additionalFields', i);

						body = populateFields.call(this, body, additionalFields, resource);

						const endpoint = `/v3/company/${companyId}/${resource}`;
						responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body);
						responseData = responseData[capitalCase(resource)];
					} else if (operation === 'delete') {
						// ----------------------------------
						//         invoice: delete
						// ----------------------------------

						const qs = {
							operation: 'delete',
						} as IDataObject;

						const body = {
							Id: this.getNodeParameter('invoiceId', i),
							SyncToken: await getSyncToken.call(this, i, companyId, resource),
						} as IDataObject;

						const endpoint = `/v3/company/${companyId}/${resource}`;
						responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, qs, body);
						responseData = responseData[capitalCase(resource)];
					} else if (operation === 'get') {
						// ----------------------------------
						//         invoice: get
						// ----------------------------------

						const invoiceId = this.getNodeParameter('invoiceId', i) as string;
						const download = this.getNodeParameter('download', i);

						if (download) {
							responseData = await handleBinaryData.call(
								this,
								items,
								i,
								companyId,
								resource,
								invoiceId,
							);
						} else {
							const endpoint = `/v3/company/${companyId}/${resource}/${invoiceId}`;
							responseData = await quickBooksApiRequest.call(this, 'GET', endpoint, {}, {});
							responseData = responseData[capitalCase(resource)];
						}
					} else if (operation === 'getAll') {
						// ----------------------------------
						//         invoice: getAll
						// ----------------------------------

						const endpoint = `/v3/company/${companyId}/query`;
						responseData = await handleListing.call(this, i, endpoint, resource);
					} else if (operation === 'send') {
						// ----------------------------------
						//         invoice: send
						// ----------------------------------

						const invoiceId = this.getNodeParameter('invoiceId', i) as string;

						const qs = {
							sendTo: this.getNodeParameter('email', i) as string,
						} as IDataObject;

						const endpoint = `/v3/company/${companyId}/${resource}/${invoiceId}/send`;
						responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, qs, {});
						responseData = responseData[capitalCase(resource)];
					} else if (operation === 'update') {
						// ----------------------------------
						//         invoice: update
						// ----------------------------------

						const { ref, syncToken } = await getRefAndSyncToken.call(
							this,
							i,
							companyId,
							resource,
							'CustomerRef',
						);

						let body = {
							Id: this.getNodeParameter('invoiceId', i),
							SyncToken: syncToken,
							sparse: true,
							CustomerRef: {
								name: ref.name,
								value: ref.value,
							},
						} as IDataObject;

						const updateFields = this.getNodeParameter('updateFields', i);

						if (isEmpty(updateFields)) {
							throw new NodeOperationError(
								this.getNode(),
								`Please enter at least one field to update for the ${resource}.`,
								{ itemIndex: i },
							);
						}

						body = populateFields.call(this, body, updateFields, resource);

						const endpoint = `/v3/company/${companyId}/${resource}`;
						responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body);
						responseData = responseData[capitalCase(resource)];
					} else if (operation === 'void') {
						// ----------------------------------
						//         invoice: void
						// ----------------------------------

						const qs = {
							Id: this.getNodeParameter('invoiceId', i),
							SyncToken: await getSyncToken.call(this, i, companyId, resource),
							operation: 'void',
						} as IDataObject;

						const endpoint = `/v3/company/${companyId}/${resource}`;
						responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, qs, {});
						responseData = responseData[capitalCase(resource)];
					}
				} else if (resource === 'item') {
					// *********************************************************************
					//                            item
					// *********************************************************************

					// https://developer.intuit.com/app/developer/qbo/docs/api/accounting/most-commonly-used/item

					if (operation === 'get') {
						// ----------------------------------
						//         item: get
						// ----------------------------------

						const item = this.getNodeParameter('itemId', i);
						const endpoint = `/v3/company/${companyId}/${resource}/${item}`;
						responseData = await quickBooksApiRequest.call(this, 'GET', endpoint, {}, {});
						responseData = responseData[capitalCase(resource)];
					} else if (operation === 'getAll') {
						// ----------------------------------
						//         item: getAll
						// ----------------------------------

						const endpoint = `/v3/company/${companyId}/query`;
						responseData = await handleListing.call(this, i, endpoint, resource);
					}
				} else if (resource === 'payment') {
					// *********************************************************************
					//                            payment
					// *********************************************************************

					// https://developer.intuit.com/app/developer/qbo/docs/api/accounting/most-commonly-used/payment

					if (operation === 'create') {
						// ----------------------------------
						//         payment: create
						// ----------------------------------

						let body = {
							CustomerRef: {
								value: this.getNodeParameter('CustomerRef', i),
							},
							TotalAmt: this.getNodeParameter('TotalAmt', i),
						} as IDataObject;

						const additionalFields = this.getNodeParameter('additionalFields', i);

						body = populateFields.call(this, body, additionalFields, resource);

						const endpoint = `/v3/company/${companyId}/${resource}`;
						responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body);
						responseData = responseData[capitalCase(resource)];
					} else if (operation === 'delete') {
						// ----------------------------------
						//         payment: delete
						// ----------------------------------

						const qs = {
							operation: 'delete',
						} as IDataObject;

						const body = {
							Id: this.getNodeParameter('paymentId', i),
							SyncToken: await getSyncToken.call(this, i, companyId, resource),
						} as IDataObject;

						const endpoint = `/v3/company/${companyId}/${resource}`;
						responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, qs, body);
						responseData = responseData[capitalCase(resource)];
					} else if (operation === 'get') {
						// ----------------------------------
						//         payment: get
						// ----------------------------------

						const paymentId = this.getNodeParameter('paymentId', i) as string;
						const download = this.getNodeParameter('download', i);

						if (download) {
							responseData = await handleBinaryData.call(
								this,
								items,
								i,
								companyId,
								resource,
								paymentId,
							);
						} else {
							const endpoint = `/v3/company/${companyId}/${resource}/${paymentId}`;
							responseData = await quickBooksApiRequest.call(this, 'GET', endpoint, {}, {});
							responseData = responseData[capitalCase(resource)];
						}
					} else if (operation === 'getAll') {
						// ----------------------------------
						//         payment: getAll
						// ----------------------------------

						const endpoint = `/v3/company/${companyId}/query`;
						responseData = await handleListing.call(this, i, endpoint, resource);
					} else if (operation === 'send') {
						// ----------------------------------
						//         payment: send
						// ----------------------------------

						const paymentId = this.getNodeParameter('paymentId', i) as string;

						const qs = {
							sendTo: this.getNodeParameter('email', i) as string,
						} as IDataObject;

						const endpoint = `/v3/company/${companyId}/${resource}/${paymentId}/send`;
						responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, qs, {});
						responseData = responseData[capitalCase(resource)];
					} else if (operation === 'update') {
						// ----------------------------------
						//         payment: update
						// ----------------------------------

						const { ref, syncToken } = await getRefAndSyncToken.call(
							this,
							i,
							companyId,
							resource,
							'CustomerRef',
						);

						let body = {
							Id: this.getNodeParameter('paymentId', i),
							SyncToken: syncToken,
							sparse: true,
							CustomerRef: {
								name: ref.name,
								value: ref.value,
							},
						} as IDataObject;

						const updateFields = this.getNodeParameter('updateFields', i);

						if (isEmpty(updateFields)) {
							throw new NodeOperationError(
								this.getNode(),
								`Please enter at least one field to update for the ${resource}.`,
								{ itemIndex: i },
							);
						}

						body = populateFields.call(this, body, updateFields, resource);

						const endpoint = `/v3/company/${companyId}/${resource}`;
						responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body);
						responseData = responseData[capitalCase(resource)];
					} else if (operation === 'void') {
						// ----------------------------------
						//         payment: void
						// ----------------------------------

						const qs = {
							Id: this.getNodeParameter('paymentId', i),
							SyncToken: await getSyncToken.call(this, i, companyId, resource),
							operation: 'void',
						} as IDataObject;

						const endpoint = `/v3/company/${companyId}/${resource}`;
						responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, qs, {});
						responseData = responseData[capitalCase(resource)];
					}
				} else if (resource === 'purchase') {
					// *********************************************************************
					//                            purchase
					// *********************************************************************

					// https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/purchase

					if (operation === 'get') {
						// ----------------------------------
						//         purchase: get
						// ----------------------------------

						const purchaseId = this.getNodeParameter('purchaseId', i);
						const endpoint = `/v3/company/${companyId}/${resource}/${purchaseId}`;
						responseData = await quickBooksApiRequest.call(this, 'GET', endpoint, {}, {});
						responseData = responseData[capitalCase(resource)];
					} else if (operation === 'getAll') {
						// ----------------------------------
						//         purchase: getAll
						// ----------------------------------

						const endpoint = `/v3/company/${companyId}/query`;
						responseData = await handleListing.call(this, i, endpoint, resource);
					}
				} else if (resource === 'transaction') {
					// *********************************************************************
					//                            transaction
					// *********************************************************************

					// https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/transactionlist

					if (operation === 'getReport') {
						// ----------------------------------
						//        transaction: getReport
						// ----------------------------------

						const { columns, memo, term, customer, vendor, ...rest } = this.getNodeParameter(
							'filters',
							i,
						) as TransactionFields;

						let qs = { ...rest };

						if (columns?.length) {
							qs.columns = columns.join(',');
						}

						if (memo?.length) {
							qs.memo = memo.join(',');
						}

						if (term?.length) {
							qs.term = term.join(',');
						}

						if (customer?.length) {
							qs.customer = customer.join(',');
						}

						if (vendor?.length) {
							qs.vendor = vendor.join(',');
						}

						qs = adjustTransactionDates(qs);

						const endpoint = `/v3/company/${companyId}/reports/TransactionList`;
						responseData = await quickBooksApiRequest.call(this, 'GET', endpoint, qs, {});

						const simplifyResponse = this.getNodeParameter('simple', i, true) as boolean;

						if (!Object.keys(responseData?.Rows as IDataObject).length) {
							responseData = [];
						}

						if (simplifyResponse && !Array.isArray(responseData)) {
							responseData = simplifyTransactionReport(responseData as TransactionReport);
						}
					}
				} else if (resource === 'vendor') {
					// *********************************************************************
					//                            vendor
					// *********************************************************************

					// https://developer.intuit.com/app/developer/qbo/docs/api/accounting/most-commonly-used/vendor

					if (operation === 'create') {
						// ----------------------------------
						//         vendor: create
						// ----------------------------------

						let body = {
							DisplayName: this.getNodeParameter('displayName', i),
						} as IDataObject;

						const additionalFields = this.getNodeParameter('additionalFields', i);

						body = populateFields.call(this, body, additionalFields, resource);

						const endpoint = `/v3/company/${companyId}/${resource}`;
						responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body);
						responseData = responseData[capitalCase(resource)];
					} else if (operation === 'get') {
						// ----------------------------------
						//         vendor: get
						// ----------------------------------

						const vendorId = this.getNodeParameter('vendorId', i);
						const endpoint = `/v3/company/${companyId}/${resource}/${vendorId}`;
						responseData = await quickBooksApiRequest.call(this, 'GET', endpoint, {}, {});
						responseData = responseData[capitalCase(resource)];
					} else if (operation === 'getAll') {
						// ----------------------------------
						//         vendor: getAll
						// ----------------------------------

						const endpoint = `/v3/company/${companyId}/query`;
						responseData = await handleListing.call(this, i, endpoint, resource);
					} else if (operation === 'update') {
						// ----------------------------------
						//         vendor: update
						// ----------------------------------

						let body = {
							Id: this.getNodeParameter('vendorId', i),
							SyncToken: await getSyncToken.call(this, i, companyId, resource),
							sparse: true,
						} as IDataObject;

						const updateFields = this.getNodeParameter('updateFields', i);

						if (isEmpty(updateFields)) {
							throw new NodeOperationError(
								this.getNode(),
								`Please enter at least one field to update for the ${resource}.`,
								{ itemIndex: i },
							);
						}

						body = populateFields.call(this, body, updateFields, resource);

						const endpoint = `/v3/company/${companyId}/${resource}`;
						responseData = await quickBooksApiRequest.call(this, 'POST', endpoint, {}, body);
						responseData = responseData[capitalCase(resource)];
					}
				}
			} catch (error) {
				if (this.continueOnFail()) {
					const download = this.getNodeParameter('download', 0, false);
					if (
						['invoice', 'estimate', 'payment'].includes(resource) &&
						['get'].includes(operation) &&
						download
					) {
						// in this case responseDate? === items
						if (!responseData) {
							items[i].json = { error: error.message };
							responseData = items;
						} else {
							responseData[i].json = { error: error.message };
						}
					} else {
						const executionErrorData = this.helpers.constructExecutionMetaData(
							this.helpers.returnJsonArray({ error: error.message }),
							{ itemData: { item: i } },
						);
						returnData.push(...executionErrorData);
					}
					continue;
				}
				throw error;
			}
			const executionData = this.helpers.constructExecutionMetaData(
				this.helpers.returnJsonArray(responseData as IDataObject),
				{ itemData: { item: i } },
			);

			returnData.push(...executionData);
		}

		const download = this.getNodeParameter('download', 0, false);

		if (
			['invoice', 'estimate', 'payment'].includes(resource) &&
			['get'].includes(operation) &&
			download
		) {
			return [responseData as INodeExecutionData[]];
		} else {
			return [returnData];
		}
	}
}