create-expense.ts•7.04 kB
import { Property, createAction, OAuth2PropertyValue } from '@activepieces/pieces-framework';
import { HttpMethod, httpClient } from '@activepieces/pieces-common';
import { quickbooksAuth } from '../index';
import { quickbooksCommon, QuickbooksEntityResponse } from '../lib/common';
import {
	QuickbooksAccount,
	QuickbooksVendor,
	QuickbooksPurchase,
	QuickbooksRef,
} from '../lib/types';
export const createExpenseAction = createAction({
	auth: quickbooksAuth,
	name: 'create_expense',
	displayName: 'Create Expense',
	description: 'Creates an expense transaction (purchase) in QuickBooks.',
	props: {
		accountRef: Property.Dropdown({
			displayName: 'Bank/Credit Card Account',
			description: 'The account from which the expense was paid.',
			required: true,
			refreshers: [],
			options: async ({ auth }) => {
				if (!auth) {
					return { disabled: true, placeholder: 'Connect account', options: [] };
				}
				const { access_token, props } = auth as OAuth2PropertyValue;
				const companyId = props?.['companyId'];
				const apiUrl = quickbooksCommon.getApiUrl(companyId);
				const query = `SELECT Id, Name, AccountType FROM Account STARTPOSITION 1 MAXRESULTS 1000`;
				const response = await httpClient.sendRequest<QuickbooksEntityResponse<QuickbooksAccount>>({
					method: HttpMethod.GET,
					url: `${apiUrl}/query`,
					queryParams: { query: query, minorversion: '70' },
					headers: {
						Authorization: `Bearer ${access_token}`,
						Accept: 'application/json',
					},
				});
				if (response.body.Fault) {
					throw new Error(
						`QuickBooks API Error fetching accounts: ${response.body.Fault.Error.map(
							(e: { Message: string }) => e.Message,
						).join(', ')}`,
					);
				}
				const accounts = response.body.QueryResponse?.['Account'] ?? [];
				return {
					disabled: false,
					options: accounts.map((account) => ({
						label: `${account.Name} (${account.AccountType})`,
						value: account.Id,
					})),
				};
			},
		}),
		paymentType: Property.StaticDropdown({
			displayName: 'Payment Type',
			required: true,
			options: {
				options: [
					{ label: 'Cash', value: 'Cash' },
					{ label: 'Check', value: 'Check' },
					{ label: 'Credit Card', value: 'CreditCard' },
				],
			},
			defaultValue: 'Cash',
		}),
		entityRef: Property.Dropdown({
			displayName: 'Payee (Vendor)',
			description: 'Optional - The vendor the expense was paid to.',
			required: false,
			refreshers: [],
			options: async ({ auth }) => {
				if (!auth) {
					return { disabled: true, placeholder: 'Connect account', options: [] };
				}
				const { access_token, props } = auth as OAuth2PropertyValue;
				const companyId = props?.['companyId'];
				const apiUrl = quickbooksCommon.getApiUrl(companyId);
				const query = `SELECT Id, DisplayName FROM Vendor STARTPOSITION 1 MAXRESULTS 1000`;
				const response = await httpClient.sendRequest<QuickbooksEntityResponse<QuickbooksVendor>>({
					method: HttpMethod.GET,
					url: `${apiUrl}/query`,
					queryParams: { query: query, minorversion: '70' },
					headers: {
						Authorization: `Bearer ${access_token}`,
						Accept: 'application/json',
					},
				});
				if (response.body.Fault) {
					throw new Error(
						`QuickBooks API Error fetching vendors: ${response.body.Fault.Error.map(
							(e: { Message: string }) => e.Message,
						).join(', ')}`,
					);
				}
				const vendors = response.body.QueryResponse?.['Vendor'] ?? [];
				return {
					disabled: false,
					options: vendors.map((vendor) => ({
						label: vendor.DisplayName,
						value: vendor.Id,
					})),
				};
			},
		}),
		txnDate: Property.DateTime({
			displayName: 'Payment Date',
			description: 'The date the expense occurred.',
			required: false, // Defaults to today if empty
		}),
		// Line items for the expense details
		lineItems: Property.Array({
			displayName: 'Line Items',
			description:
				'Details of the expense (e.g., categories or items purchased). At least one line is required.',
			required: true,
			properties: {
				amount: Property.Number({
					displayName: 'Amount',
					required: true,
				}),
				description: Property.ShortText({
					displayName: 'Description',
					required: false,
				}),
				detailType: Property.StaticDropdown({
					displayName: 'Detail Type',
					required: true,
					options: {
						options: [
							{
								label: 'Account Based Expense Line Detail',
								value: 'AccountBasedExpenseLineDetail',
							},
						],
					},
					defaultValue: 'AccountBasedExpenseLineDetail',
				}),
				expenseAccountId: Property.ShortText({
					displayName: 'Expense Category/Account ID',
					description:
						'Enter the ID of the Expense Account. Required for AccountBasedExpenseLineDetail.',
					required: true,
				}),
			},
		}),
		privateNote: Property.LongText({
			displayName: 'Memo (Private Note)',
			description: 'Internal note about the expense.',
			required: false,
		}),
	},
	async run(context) {
		const { access_token } = context.auth;
		const companyId = context.auth.props?.['companyId'];
		const apiUrl = quickbooksCommon.getApiUrl(companyId);
		const props = context.propsValue;
		const lines = (props['lineItems'] as any[]).map((line) => {
			const detail: any = {
				Amount: line['amount'],
				Description: line['description'],
				DetailType: line['detailType'],
			};
			if (line['detailType'] === 'AccountBasedExpenseLineDetail') {
				if (!line['expenseAccountId']) {
					throw new Error(
						'Expense Category/Account ID is required for Account Based Expense Line Detail.',
					);
				}
				detail.AccountBasedExpenseLineDetail = {
					AccountRef: { value: line['expenseAccountId'] },
				};
			}
			return detail;
		});
		if (lines.length === 0) {
			throw new Error('At least one line item is required.');
		}
		const expensePayload: Partial<QuickbooksPurchase> = {
			AccountRef: { value: props['accountRef'] },
			PaymentType: props['paymentType'] as 'Cash' | 'Check' | 'CreditCard',
			Line: lines,
			...(props['entityRef'] && { EntityRef: { value: props['entityRef'] } as QuickbooksRef }),
			...(props['txnDate'] && { TxnDate: props['txnDate'].split('T')[0] }),
			...(props['privateNote'] && { PrivateNote: props['privateNote'] }),
		};
		const endpoint = 'purchase';
		const response = await httpClient.sendRequest<{
			Purchase: QuickbooksPurchase;
			time: string;
			Fault?: { Error: { Message: string; Detail?: string; code: string }[]; type: string };
		}>({
			method: HttpMethod.POST,
			url: `${apiUrl}/${endpoint}`,
			queryParams: { minorversion: '70' },
			headers: {
				Authorization: `Bearer ${access_token}`,
				Accept: 'application/json',
				'Content-Type': 'application/json',
			},
			body: expensePayload,
		});
		if (response.body.Fault) {
			throw new Error(
				`QuickBooks API Error creating expense: ${response.body.Fault.Error.map(
					(e: any) => e.Message,
				).join(', ')} - Detail: ${response.body.Fault.Error.map((e: any) => e.Detail).join(', ')}`,
			);
		}
		return response.body.Purchase;
	},
});