diff --git a/components/fynk/actions/create-contract-from-template/create-contract-from-template.mjs b/components/fynk/actions/create-contract-from-template/create-contract-from-template.mjs new file mode 100644 index 0000000000000..9841bd1faf721 --- /dev/null +++ b/components/fynk/actions/create-contract-from-template/create-contract-from-template.mjs @@ -0,0 +1,57 @@ +import fynk from "../../fynk.app.mjs"; + +export default { + key: "fynk-create-contract-from-template", + name: "Create Contract from Template", + description: "Create a new contract in Fynk based on an existing template. [See the documentation](https://app.fynk.com/v1/docs#/operations/v1.documents.create-from-template).", + version: "0.0.1", + annotations: { + destructiveHint: false, + openWorldHint: true, + readOnlyHint: false, + }, + type: "action", + props: { + fynk, + templateUuid: { + propDefinition: [ + fynk, + "templateUuid", + ], + }, + name: { + type: "string", + label: "Name", + description: "The new document's name. If omitted, the name of the template will be used as the new document's name", + optional: true, + }, + ownerEmails: { + type: "string[]", + label: "Owner Emails", + description: "Email addresses of the user(s) from your account who should be given ownership of the new document", + optional: true, + }, + }, + async run({ $ }) { + const { + templateUuid, + name, + ownerEmails, + } = this; + + const data = { + template_uuid: templateUuid, + name, + owner_emails: ownerEmails, + }; + + const response = await this.fynk.createDocumentFromTemplate({ + $, + data, + }); + + $.export("$summary", `Successfully created contract "${response.data.name}" with UUID ${response.data.uuid}`); + return response; + }, +}; + diff --git a/components/fynk/actions/move-contract-stage/move-contract-stage.mjs b/components/fynk/actions/move-contract-stage/move-contract-stage.mjs new file mode 100644 index 0000000000000..38afe9f7f3891 --- /dev/null +++ b/components/fynk/actions/move-contract-stage/move-contract-stage.mjs @@ -0,0 +1,97 @@ +import fynk from "../../fynk.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + key: "fynk-move-contract-stage", + name: "Move Contract Stage", + description: "Move a contract forward in Fynk's lifecycle. See documentation pages [move document to review stage](https://app.fynk.com/v1/docs#/operations/v1.documents.stage-transitions.review) and [move document to signing stage](https://app.fynk.com/v1/docs#/operations/v1.documents.stage-transitions.signing).", + version: "0.0.1", + annotations: { + destructiveHint: true, + openWorldHint: true, + readOnlyHint: false, + }, + type: "action", + props: { + fynk, + documentUuid: { + propDefinition: [ + fynk, + "documentUuid", + ], + }, + targetStage: { + propDefinition: [ + fynk, + "targetStage", + ], + }, + signatureType: { + type: "string", + label: "Signature Type", + description: "The signature type to use when moving to signing stage. Only used when `Target Stage` is set to `signing`", + optional: true, + options: [ + { + label: "Simple Electronic Signature (SES)", + value: "ses", + }, + { + label: "Advanced Electronic Signature (AES)", + value: "aes", + }, + { + label: "Qualified Electronic Signature (QES)", + value: "qes", + }, + ], + }, + sequentialSigning: { + type: "boolean", + label: "Sequential Signing", + description: "If `true`, signatures will be requested in the order defined by the signatories' `signing_order`. Only used when `Target Stage` is set to `signing`", + optional: true, + }, + message: { + type: "string", + label: "Message", + description: "Message to include in the email sent to the document's signatories. This is included in addition to the default email text provided by fynk. Only used when `Target Stage` is set to `signing`", + optional: true, + }, + }, + async run({ $ }) { + const { + documentUuid, + targetStage, + signatureType, + sequentialSigning, + message, + } = this; + + let response; + if (targetStage === "review") { + response = await this.fynk.moveDocumentToReview({ + $, + documentUuid, + }); + } else if (targetStage === "signing") { + const data = { + signature_type: signatureType, + sequential_signing: sequentialSigning, + message, + }; + + response = await this.fynk.moveDocumentToSigning({ + $, + documentUuid, + data, + }); + } else { + throw new ConfigurationError(`Invalid target stage: ${targetStage}`); + } + + $.export("$summary", `Successfully moved contract ${documentUuid} to ${targetStage} stage`); + return response; + }, +}; + diff --git a/components/fynk/actions/update-contract-metadata/update-contract-metadata.mjs b/components/fynk/actions/update-contract-metadata/update-contract-metadata.mjs new file mode 100644 index 0000000000000..efff7613b43c7 --- /dev/null +++ b/components/fynk/actions/update-contract-metadata/update-contract-metadata.mjs @@ -0,0 +1,244 @@ +import fynk from "../../fynk.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + key: "fynk-update-contract-metadata", + name: "Update Contract Metadata", + description: "Update metadata values or dynamic fields associated with a contract in Fynk. [See the documentation](https://app.fynk.com/v1/docs#/operations/v1.documents.metadata-values.update).", + version: "0.0.1", + annotations: { + destructiveHint: true, + openWorldHint: true, + readOnlyHint: false, + }, + type: "action", + props: { + fynk, + documentUuid: { + propDefinition: [ + fynk, + "documentUuid", + ], + }, + metadataValueUuid: { + propDefinition: [ + fynk, + "metadataValueUuid", + (c) => ({ + documentUuid: c.documentUuid, + }), + ], + }, + value: { + type: "string", + label: "Value", + description: "The new value for the metadata field. The format depends on the metadata's `value_type`. [See the documentation](https://app.fynk.com/v1/docs#/operations/v1.documents.metadata-values.store#value-format) for format details.", + }, + }, + methods: { + // Helper function to validate and format the metadata value according to its `value_type` + formatMetadataValue(rawValue, metadata) { + const valueType = metadata.value_type; + + switch (valueType) { + case "email": { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + const emailStr = String(rawValue).trim(); + if (!emailRegex.test(emailStr)) { + throw new ConfigurationError(`Invalid email format: ${rawValue}`); + } + return emailStr; + } + + case "number": { + const num = typeof rawValue === "string" + ? parseFloat(rawValue) + : Number(rawValue); + if (isNaN(num)) { + throw new ConfigurationError(`Invalid number: ${rawValue}`); + } + return num; + } + + case "currency": { + const currencyStr = String(rawValue).trim(); + const parts = currencyStr.split(";"); + if (parts.length !== 2) { + throw new ConfigurationError(`Invalid currency format. Expected "CURRENCY;AMOUNT", got: ${rawValue}`); + } + const [ + currency, + amount, + ] = parts; + const numAmount = parseFloat(amount); + if (isNaN(numAmount)) { + throw new ConfigurationError(`Invalid currency amount: ${amount}`); + } + if (metadata.settings?.currencies && !metadata.settings.currencies.includes(currency)) { + throw new ConfigurationError(`Invalid currency code. Allowed: ${metadata.settings.currencies.join(", ")}, got: ${currency}`); + } + return `${currency};${numAmount}`; + } + + case "currency_duration": { + const currencyDurationStr = String(rawValue).trim(); + const parts = currencyDurationStr.split(";"); + if (parts.length !== 3) { + throw new ConfigurationError(`Invalid currency_duration format. Expected "CURRENCY;AMOUNT;PERIOD", got: ${rawValue}`); + } + const [ + currency, + amount, + period, + ] = parts; + const numAmount = parseFloat(amount); + if (isNaN(numAmount)) { + throw new ConfigurationError(`Invalid currency amount: ${amount}`); + } + if (period !== "monthly" && period !== "yearly") { + throw new ConfigurationError(`Invalid period. Must be "monthly" or "yearly", got: ${period}`); + } + if (metadata.settings?.currencies && !metadata.settings.currencies.includes(currency)) { + throw new ConfigurationError(`Invalid currency code. Allowed: ${metadata.settings.currencies.join(", ")}, got: ${currency}`); + } + return `${currency};${numAmount};${period}`; + } + + case "select": { + const selectStr = String(rawValue).trim(); + if (metadata.select_values && !metadata.select_values.includes(selectStr)) { + throw new ConfigurationError(`Invalid select value. Allowed: ${metadata.select_values.join(", ")}, got: ${selectStr}`); + } + return selectStr; + } + + case "date": { + const dateStr = String(rawValue).trim(); + const dateRegex = /^\d{4}-\d{2}-\d{2}$/; + if (!dateRegex.test(dateStr)) { + throw new ConfigurationError(`Invalid date format. Expected "YYYY-MM-DD", got: ${rawValue}`); + } + const date = new Date(dateStr); + if (isNaN(date.getTime())) { + throw new ConfigurationError(`Invalid date: ${rawValue}`); + } + return dateStr; + } + + case "bool": { + if (typeof rawValue === "boolean") { + return rawValue; + } + const str = String(rawValue).toLowerCase() + .trim(); + if (str === "true" || str === "1" || str === "yes") { + return true; + } + if (str === "false" || str === "0" || str === "no") { + return false; + } + throw new ConfigurationError(`Invalid boolean value. Expected true/false, got: ${rawValue}`); + } + + case "duration": { + const durationStr = String(rawValue).trim(); + const durationRegex = /^P(\d+D)?(\d+W)?(T(\d+H)?(\d+M)?(\d+S)?)?$/; + if (!durationRegex.test(durationStr)) { + throw new ConfigurationError(`Invalid duration format. Expected ISO 8601 duration (e.g., "P6D", "P2W", "PT12H"), got: ${rawValue}`); + } + return durationStr; + } + + case "text": { + const textStr = String(rawValue); + if (textStr.length > 4096) { + throw new ConfigurationError("Text exceeds maximum length of 4096 characters"); + } + return textStr; + } + + case "textarea": { + const textareaStr = String(rawValue); + if (textareaStr.length > 4096) { + throw new ConfigurationError("Textarea exceeds maximum length of 4096 characters"); + } + return textareaStr; + } + + case "timestamp": { + const timestampStr = String(rawValue).trim(); + const timestamp = new Date(timestampStr); + if (isNaN(timestamp.getTime())) { + throw new ConfigurationError(`Invalid timestamp format. Expected ISO 8601 format (e.g., "2025-06-02T14:30:00+00:00"), got: ${rawValue}`); + } + return timestampStr; + } + + case "clause": { + if (typeof rawValue === "boolean") { + return rawValue; + } + const str = String(rawValue).toLowerCase() + .trim(); + if (str === "true" || str === "1" || str === "yes") { + return true; + } + if (str === "false" || str === "0" || str === "no") { + return false; + } + throw new ConfigurationError(`Invalid clause value. Expected true/false, got: ${rawValue}`); + } + + case "id": + case "uuid": { + return String(rawValue).trim(); + } + + default: + return rawValue; + } + }, + }, + async run({ $ }) { + const { + documentUuid, + metadataValueUuid, + value, + } = this; + + // Step 1: Fetch metadata values to get the metadata definition + const metadataResponse = await this.fynk.listDocumentMetadataValues({ + $, + documentUuid, + }); + + const metadataValue = metadataResponse.data?.find( + (mv) => mv.uuid === metadataValueUuid, + ); + + if (!metadataValue) { + throw new ConfigurationError(`Metadata value with UUID ${metadataValueUuid} not found`); + } + + if (!metadataValue.metadata) { + throw new ConfigurationError(`Metadata definition not found for metadata value ${metadataValueUuid}`); + } + + // Step 2: Format the value according to its value_type + const formattedValue = this.formatMetadataValue(value, metadataValue.metadata); + + // Step 3: Update the metadata value + const response = await this.fynk.updateDocumentMetadataValue({ + $, + documentUuid, + metadataValueUuid, + data: { + value: formattedValue, + }, + }); + + $.export("$summary", `Successfully updated metadata value for contract ${documentUuid}`); + return response; + }, +}; + diff --git a/components/fynk/actions/update-contract-party/update-contract-party.mjs b/components/fynk/actions/update-contract-party/update-contract-party.mjs new file mode 100644 index 0000000000000..66e6c1c024df1 --- /dev/null +++ b/components/fynk/actions/update-contract-party/update-contract-party.mjs @@ -0,0 +1,92 @@ +import fynk from "../../fynk.app.mjs"; + +export default { + key: "fynk-update-contract-party", + name: "Update Contract Party", + description: "Update the details of a party associated with a contract in Fynk. [See the documentation](https://app.fynk.com/v1/docs#/operations/v1.documents.parties.update).", + version: "0.0.1", + annotations: { + destructiveHint: true, + openWorldHint: true, + readOnlyHint: false, + }, + type: "action", + props: { + fynk, + documentUuid: { + propDefinition: [ + fynk, + "documentUuid", + ], + }, + partyUuid: { + propDefinition: [ + fynk, + "partyUuid", + (c) => ({ + documentUuid: c.documentUuid, + }), + ], + }, + reference: { + type: "string", + label: "Reference", + description: "A generic reference for the party", + optional: true, + }, + entityType: { + propDefinition: [ + fynk, + "entityType", + ], + }, + entityName: { + type: "string", + label: "Entity Name", + description: "The actual name of the party (i.e., name of the company or person). Must be non-null in order for the document to proceed to the signing stage", + optional: true, + }, + address: { + type: "string", + label: "Address", + description: "The party's address", + optional: true, + }, + scope: { + propDefinition: [ + fynk, + "scope", + ], + }, + }, + async run({ $ }) { + const { + documentUuid, + partyUuid, + reference, + entityType, + entityName, + address, + scope, + } = this; + + const data = { + reference, + entity_type: entityType, + entity_name: entityName, + address, + scope, + }; + + const response = await this.fynk.updateDocumentParty({ + $, + documentUuid, + partyUuid, + data, + }); + + $.export("$summary", `Successfully updated party ${partyUuid} for contract ${documentUuid}`); + return response; + }, +}; + diff --git a/components/fynk/fynk.app.mjs b/components/fynk/fynk.app.mjs index 5b4c2ef06ccbf..69b6a2743db81 100644 --- a/components/fynk/fynk.app.mjs +++ b/components/fynk/fynk.app.mjs @@ -1,11 +1,282 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "fynk", - propDefinitions: {}, + propDefinitions: { + templateUuid: { + type: "string", + label: "Template", + description: "The template to use as a basis for the new document", + async options({ page }) { + const { data } = await this.listTemplates({ + params: { + page: page + 1, + per_page: 100, + }, + }); + return data?.map((template) => ({ + label: template.name, + value: template.uuid, + })) || []; + }, + }, + documentUuid: { + type: "string", + label: "Document", + description: "The document to operate on", + async options({ page }) { + const { data } = await this.listDocuments({ + params: { + page: page + 1, + per_page: 100, + }, + }); + return data?.map((doc) => ({ + label: doc.name, + value: doc.uuid, + })) || []; + }, + }, + partyUuid: { + type: "string", + label: "Party", + description: "The party to operate on", + async options(opts) { + const { documentUuid } = opts; + if (!documentUuid) { + return []; + } + const { data } = await this.listDocumentParties({ + documentUuid, + }); + return data?.map((party) => ({ + label: party.entity_name || party.reference || party.uuid, + value: party.uuid, + })) || []; + }, + }, + metadataValueUuid: { + type: "string", + label: "Metadata Value", + description: "The metadata value to update", + async options(opts) { + const { documentUuid } = opts; + if (!documentUuid) { + return []; + } + const { data } = await this.listDocumentMetadataValues({ + documentUuid, + }); + return data?.map((mv) => ({ + label: `${mv.metadata.display_name}: ${mv.value || "(empty)"}`, + value: mv.uuid, + })) || []; + }, + }, + tagUuids: { + type: "string[]", + label: "Tags", + description: "Tags to assign to the document", + optional: true, + async options({ page }) { + const { data } = await this.listTags({ + params: { + page: page + 1, + per_page: 100, + }, + }); + return data?.map((tag) => ({ + label: tag.name, + value: tag.uuid, + })) || []; + }, + }, + documentType: { + type: "string", + label: "Document Type", + description: "The type of document", + optional: true, + options: [ + { + label: "Contract", + value: "contract", + }, + { + label: "Quote", + value: "quote", + }, + { + label: "Form", + value: "form", + }, + { + label: "Other", + value: "other", + }, + ], + }, + entityType: { + type: "string", + label: "Entity Type", + description: "What kind of entity this party represents", + optional: true, + options: [ + { + label: "Business", + value: "business", + }, + { + label: "Person", + value: "person", + }, + ], + }, + scope: { + type: "string", + label: "Scope", + description: "When this is `internal_and_external`, collaborators from this party may change the party's information", + optional: true, + options: [ + { + label: "Internal", + value: "internal", + }, + { + label: "Internal and External", + value: "internal_and_external", + }, + ], + }, + targetStage: { + type: "string", + label: "Target Stage", + description: "The stage to move the contract to", + options: [ + { + label: "Review", + value: "review", + }, + { + label: "Signing", + value: "signing", + }, + ], + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return "https://app.fynk.com/v1/api"; + }, + _getHeaders() { + return { + "Authorization": `Bearer ${this.$auth.api_token}`, + "Accept": "application/json", + "Content-Type": "application/json", + }; + }, + async _makeRequest({ + $ = this, + path, + method = "GET", + data = null, + params = null, + headers = {}, + ...args + }) { + const config = { + method, + url: `${this._baseUrl()}${path}`, + headers: { + ...this._getHeaders(), + ...headers, + }, + ...(data && { + data, + }), + ...(params && { + params, + }), + ...args, + }; + return axios($, config); + }, + async listTemplates(args = {}) { + return this._makeRequest({ + path: "/templates", + ...args, + }); + }, + async listDocuments(args = {}) { + return this._makeRequest({ + path: "/documents", + ...args, + }); + }, + async listDocumentParties({ + documentUuid, ...args + } = {}) { + return this._makeRequest({ + path: `/documents/${documentUuid}/parties`, + ...args, + }); + }, + async listDocumentMetadataValues({ + documentUuid, ...args + } = {}) { + return this._makeRequest({ + path: `/documents/${documentUuid}/metadata-values`, + ...args, + }); + }, + async listTags(args = {}) { + return this._makeRequest({ + path: "/tags", + ...args, + }); + }, + async createDocumentFromTemplate(args = {}) { + return this._makeRequest({ + method: "POST", + path: "/documents/create-from-template", + ...args, + }); + }, + async updateDocumentMetadataValue({ + documentUuid, metadataValueUuid, ...args + } = {}) { + return this._makeRequest({ + method: "PUT", + path: `/documents/${documentUuid}/metadata-values/${metadataValueUuid}`, + ...args, + }); + }, + async updateDocumentParty({ + documentUuid, partyUuid, ...args + } = {}) { + return this._makeRequest({ + method: "PUT", + path: `/documents/${documentUuid}/parties/${partyUuid}`, + ...args, + }); + }, + async moveDocumentToReview({ + documentUuid, ...args + } = {}) { + return this._makeRequest({ + method: "POST", + path: `/documents/${documentUuid}/stage-transitions/review`, + ...args, + }); + }, + async moveDocumentToSigning({ + documentUuid, ...args + } = {}) { + return this._makeRequest({ + method: "POST", + path: `/documents/${documentUuid}/stage-transitions/signing`, + ...args, + }); }, }, }; diff --git a/components/fynk/package.json b/components/fynk/package.json index 166073185cc22..7b269feeb74cf 100644 --- a/components/fynk/package.json +++ b/components/fynk/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/fynk", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream fynk Components", "main": "fynk.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.1.0" } -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 65dd16770279b..f5ad3a76082df 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5710,7 +5710,11 @@ importers: components/funnelcockpit: {} - components/fynk: {} + components/fynk: + dependencies: + '@pipedream/platform': + specifier: ^3.1.0 + version: 3.1.1 components/gagelist: dependencies: