diff --git a/components/expensify/actions/create-expense/create-expense.ts b/components/expensify/actions/create-expense/create-expense.ts index bec7e094b7ff1..d3f8cdf1f0ee4 100644 --- a/components/expensify/actions/create-expense/create-expense.ts +++ b/components/expensify/actions/create-expense/create-expense.ts @@ -3,16 +3,17 @@ import expensify from "../../app/expensify.app"; export default defineAction({ key: "expensify-create-expense", - version: "0.0.2", + version: "0.0.3", name: "Create Expense", description: "Creates a new expense. [See docs here](https://integrations.expensify.com/Integration-Server/doc/#expense-creator)", type: "action", props: { expensify, employeeEmail: { - label: "Employee Email", - description: "The expenses will be created in this account.", - type: "string", + propDefinition: [ + expensify, + "employeeEmail", + ], }, currency: { label: "Currency", diff --git a/components/expensify/actions/create-report/create-report.ts b/components/expensify/actions/create-report/create-report.ts new file mode 100644 index 0000000000000..8408b1816b0af --- /dev/null +++ b/components/expensify/actions/create-report/create-report.ts @@ -0,0 +1,111 @@ +import { defineAction } from "@pipedream/types"; +import app from "../../app/expensify.app"; +import utils from "../../common/utils"; + +export default defineAction({ + key: "expensify-create-report", + version: "0.0.1", + name: "Create Report", + description: "Creates a new report with transactions in a user's account. [See docs here](https://integrations.expensify.com/Integration-Server/doc/#report-creator)", + type: "action", + props: { + app, + employeeEmail: { + description: "The report will be created in this account.", + propDefinition: [ + app, + "employeeEmail", + ], + }, + policyId: { + propDefinition: [ + app, + "policyId", + ({ employeeEmail }) => ({ + userEmail: employeeEmail, + }), + ], + }, + reportTitle: { + label: "Report Title", + description: "The title of the report that will be created.", + type: "string", + }, + expenses: { + type: "string[]", + label: "Expenses", + description: `Array of expense objects to include in the report. Each expense should be a JSON object with the following required fields: + +- \`date\`: The date the expense was made (format yyyy-mm-dd) +- \`currency\`: Three-letter currency code (e.g., "USD", "EUR", "CAD") +- \`merchant\`: The name of the merchant +- \`amount\`: The amount in cents (e.g., 2500 for $25.00) + +**Example:** +\`\`\`json +[ + { + "date": "2024-01-15", + "currency": "USD", + "merchant": "Hotel ABC", + "amount": 15000 + }, + { + "date": "2024-01-16", + "currency": "USD", + "merchant": "Restaurant XYZ", + "amount": 5000 + } +] +\`\`\``, + }, + reportFields: { + type: "object", + label: "Report Fields", + description: `Custom fields for the report as a JSON object. Use this to set values for custom report fields in your Expensify policy. + +- \`Key format\`: Field names should have all non-alphanumerical characters replaced with underscores (_) +- \`Value format\`: String values for the corresponding field + +**Example:** +\`\`\`json +{ + "reason_of_trip": "Business meetings with clients", + "employees": "3", + "department": "Sales", + "project_code": "PROJ_2024_001" +} +\`\`\``, + optional: true, + }, + }, + async run({ $ }) { + const { + policyId, + employeeEmail, + reportTitle, + reportFields, + expenses, + } = this; + + const response = await this.app.createReport({ + $, + data: { + employeeEmail, + policyID: policyId, + report: { + title: reportTitle, + ...(reportFields && { + fields: utils.parseJson(reportFields), + }), + }, + expenses: utils.parseArray(expenses), + }, + }); + + $.export("$summary", `Successfully created report \`${response.reportName}\` with ID \`${response.reportID}\``); + + return response; + }, +}); + diff --git a/components/expensify/actions/export-report-to-pdf/export-report-to-pdf.ts b/components/expensify/actions/export-report-to-pdf/export-report-to-pdf.ts index 74359ba4b7cde..a218a4839cb30 100644 --- a/components/expensify/actions/export-report-to-pdf/export-report-to-pdf.ts +++ b/components/expensify/actions/export-report-to-pdf/export-report-to-pdf.ts @@ -4,7 +4,7 @@ import fs from "fs"; export default defineAction({ key: "expensify-export-report-to-pdf", - version: "0.0.2", + version: "0.0.3", name: "Export Report To PDF", description: "Export a report to PDF. [See docs here](https://integrations.expensify.com/Integration-Server/doc/#report-exporter)", type: "action", diff --git a/components/expensify/app/expensify.app.ts b/components/expensify/app/expensify.app.ts index a59e3818d2eab..4593136a9f5ad 100644 --- a/components/expensify/app/expensify.app.ts +++ b/components/expensify/app/expensify.app.ts @@ -5,7 +5,29 @@ import qs from "qs"; export default defineApp({ type: "app", app: "expensify", - propDefinitions: {}, + propDefinitions: { + employeeEmail: { + type: "string", + label: "Employee Email", + description: "The expenses will be created in this account.", + }, + policyId: { + type: "string", + label: "Policy ID", + description: "Select the policy where the report will be created.", + async options({ userEmail }) { + const { policyList } = await this.getPolicyList({ + userEmail, + }); + return policyList?.map(({ + id: value, name: label, + }) => ({ + label, + value, + })) || []; + }, + }, + }, methods: { _partnerUserId() { return this.$auth.partnerUserId; @@ -16,21 +38,33 @@ export default defineApp({ _apiUrl() { return "https://integrations.expensify.com/Integration-Server/ExpensifyIntegrations"; }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any async _makeRequest(options: any = {}, $ = this) { - return axios($, { + const { + extraFormUrlencodedData, + data, + ...rest + } = options; + const response = await axios($, { url: `${this._apiUrl()}`, - ...options, + ...rest, data: qs.stringify({ requestJobDescription: JSON.stringify({ credentials: { partnerUserID: this._partnerUserId(), partnerUserSecret: this._partnerUserSecret(), }, - ...options?.data, + ...data, }), - ...options?.extraFormUrlencodedData, + ...extraFormUrlencodedData, }), }); + + if (response.responseCode < 200 || response.responseCode >= 300) { + throw new Error(JSON.stringify(response, null, 2)); + } + + return response; }, async createExpense({ $, data, @@ -46,6 +80,37 @@ export default defineApp({ }, }, $); }, + async createReport({ + $, data, + }) { + return this._makeRequest({ + method: "post", + data: { + type: "create", + inputSettings: { + type: "report", + ...data, + }, + }, + }, $); + }, + async getPolicyList({ + $, userEmail, adminOnly = true, + }) { + return this._makeRequest({ + method: "post", + data: { + type: "get", + inputSettings: { + type: "policyList", + adminOnly, + ...(userEmail && { + userEmail, + }), + }, + }, + }, $); + }, async updateCustomer({ $, data, }) { diff --git a/components/expensify/common/utils.ts b/components/expensify/common/utils.ts new file mode 100644 index 0000000000000..783e3ff50f101 --- /dev/null +++ b/components/expensify/common/utils.ts @@ -0,0 +1,69 @@ +const parseJson = (input, maxDepth = 100) => { + const seen = new WeakSet(); + const parse = (value) => { + if (maxDepth <= 0) { + return value; + } + if (typeof(value) === "string") { + // Only parse if the string looks like a JSON object or array + const trimmed = value.trim(); + if ( + (trimmed.startsWith("{") && trimmed.endsWith("}")) || + (trimmed.startsWith("[") && trimmed.endsWith("]")) + ) { + try { + return parseJson(JSON.parse(value), maxDepth - 1); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (e) { + return value; + } + } + return value; + } else if (typeof(value) === "object" && value !== null && !Array.isArray(value)) { + if (seen.has(value)) { + return value; + } + seen.add(value); + return Object.entries(value) + .reduce((acc, [ + key, + val, + ]) => Object.assign(acc, { + [key]: parse(val), + }), {}); + } else if (Array.isArray(value)) { + return value.map((item) => parse(item)); + } + return value; + }; + + return parse(input); +}; + +function parseArray (input, maxDepth = 100) { + if (typeof input === "string") { + const trimmed = input.trim(); + if (trimmed.startsWith("[") && trimmed.endsWith("]")) { + try { + const parsed = JSON.parse(trimmed); + if (Array.isArray(parsed)) { + return parsed.map((item) => parseArray(item, maxDepth - 1)); + } + } catch (e) { + throw new Error(`Invalid JSON array format: ${e.message}`); + } + } + return parseJson(input, maxDepth); + } + + if (Array.isArray(input)) { + return input.map((item) => parseArray(item, maxDepth)); + } + + return input; +} + +export default { + parseJson, + parseArray, +}; diff --git a/components/expensify/package.json b/components/expensify/package.json index 3532abb388df2..11fce76551631 100644 --- a/components/expensify/package.json +++ b/components/expensify/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/expensify", - "version": "0.0.4", + "version": "0.1.0", "description": "Pipedream Expensify Components", "main": "dist/app/expensify.app.mjs", "keywords": [