Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 5 additions & 4 deletions components/expensify/actions/create-expense/create-expense.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
111 changes: 111 additions & 0 deletions components/expensify/actions/create-report/create-report.ts
Original file line number Diff line number Diff line change
@@ -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;
},
});

Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
75 changes: 70 additions & 5 deletions components/expensify/app/expensify.app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -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,
}) {
Expand Down
69 changes: 69 additions & 0 deletions components/expensify/common/utils.ts
Original file line number Diff line number Diff line change
@@ -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,
};
2 changes: 1 addition & 1 deletion components/expensify/package.json
Original file line number Diff line number Diff line change
@@ -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": [
Expand Down
Loading