From 93c610d7ae0574dfd3fba9edfe1be8b99327d201 Mon Sep 17 00:00:00 2001 From: Luan Cazarine Date: Fri, 13 Dec 2024 10:06:55 -0300 Subject: [PATCH 1/5] pennylane init --- .../create-billing-subscription.mjs | 53 ++++ .../create-customer-invoice.mjs | 51 ++++ .../create-customer/create-customer.mjs | 48 +++ components/pennylane/package.json | 2 +- components/pennylane/pennylane.app.mjs | 285 +++++++++++++++++- .../new-billing-subscription.mjs | 124 ++++++++ .../new-customer-invoice.mjs | 104 +++++++ .../sources/new-customer/new-customer.mjs | 115 +++++++ 8 files changed, 779 insertions(+), 3 deletions(-) create mode 100644 components/pennylane/actions/create-billing-subscription/create-billing-subscription.mjs create mode 100644 components/pennylane/actions/create-customer-invoice/create-customer-invoice.mjs create mode 100644 components/pennylane/actions/create-customer/create-customer.mjs create mode 100644 components/pennylane/sources/new-billing-subscription/new-billing-subscription.mjs create mode 100644 components/pennylane/sources/new-customer-invoice/new-customer-invoice.mjs create mode 100644 components/pennylane/sources/new-customer/new-customer.mjs diff --git a/components/pennylane/actions/create-billing-subscription/create-billing-subscription.mjs b/components/pennylane/actions/create-billing-subscription/create-billing-subscription.mjs new file mode 100644 index 0000000000000..ed138ba775c78 --- /dev/null +++ b/components/pennylane/actions/create-billing-subscription/create-billing-subscription.mjs @@ -0,0 +1,53 @@ +import pennylane from "../../pennylane.app.mjs"; +import { axios } from "@pipedream/platform"; + +export default { + key: "pennylane-create-billing-subscription", + name: "Create Billing Subscription", + description: "Creates a billing subscription for a customer. [See the documentation]().", + version: "0.0.{{ts}}", + type: "action", + props: { + pennylane: { + type: "app", + app: "pennylane", + }, + customerId: { + propDefinition: [ + pennylane, + "customerId", + ], + }, + subscriptionPlanId: { + propDefinition: [ + pennylane, + "subscriptionPlanId", + ], + }, + billingFrequency: { + propDefinition: [ + pennylane, + "billingFrequency", + ], + }, + subscriptionDiscounts: { + propDefinition: [ + pennylane, + "subscriptionDiscounts", + ], + optional: true, + }, + subscriptionCustomNotes: { + propDefinition: [ + pennylane, + "subscriptionCustomNotes", + ], + optional: true, + }, + }, + async run({ $ }) { + const response = await this.pennylane.createBillingSubscription(); + $.export("$summary", `Created billing subscription with ID ${response.billing_subscription.id}`); + return response; + }, +}; diff --git a/components/pennylane/actions/create-customer-invoice/create-customer-invoice.mjs b/components/pennylane/actions/create-customer-invoice/create-customer-invoice.mjs new file mode 100644 index 0000000000000..dc8fab7debbbe --- /dev/null +++ b/components/pennylane/actions/create-customer-invoice/create-customer-invoice.mjs @@ -0,0 +1,51 @@ +import pennylane from "../../pennylane.app.mjs"; +import { axios } from "@pipedream/platform"; + +export default { + key: "pennylane-create-customer-invoice", + name: "Create Customer Invoice", + description: "Generates a new invoice for a customer using Pennylane. [See the documentation](https://pennylane.readme.io/reference/customer_invoices-post-1)", + version: "0.0.{{ts}}", + type: "action", + props: { + pennylane, + invoiceCustomerId: { + propDefinition: [ + pennylane, + "invoiceCustomerId", + ], + }, + invoiceItems: { + propDefinition: [ + pennylane, + "invoiceItems", + ], + }, + paymentTerms: { + propDefinition: [ + pennylane, + "paymentTerms", + ], + }, + invoiceTaxDetails: { + propDefinition: [ + pennylane, + "invoiceTaxDetails", + ], + optional: true, + }, + invoiceFooterCustomization: { + propDefinition: [ + pennylane, + "invoiceFooterCustomization", + ], + optional: true, + }, + }, + async run({ $ }) { + const invoice = await this.pennylane.generateInvoice(); + + $.export("$summary", `Created invoice with ID ${invoice.invoice.id}`); + return invoice; + }, +}; diff --git a/components/pennylane/actions/create-customer/create-customer.mjs b/components/pennylane/actions/create-customer/create-customer.mjs new file mode 100644 index 0000000000000..8f6089c049734 --- /dev/null +++ b/components/pennylane/actions/create-customer/create-customer.mjs @@ -0,0 +1,48 @@ +import pennylane from "../../pennylane.app.mjs"; +import { axios } from "@pipedream/platform"; + +export default { + key: "pennylane-create-customer", + name: "Create Customer", + description: "Creates a new customer in Pennylane. [See the documentation](https://pennylane.readme.io/reference/customers-post-1)", + version: "0.0.{{ts}}", + type: "action", + props: { + pennylane, + customerName: { + propDefinition: [ + pennylane, + "customerName", + ], + }, + customerEmail: { + propDefinition: [ + pennylane, + "customerEmail", + ], + }, + customerContactInfo: { + propDefinition: [ + pennylane, + "customerContactInfo", + ], + }, + customerAddress: { + propDefinition: [ + pennylane, + "customerAddress", + ], + }, + customerMetadata: { + propDefinition: [ + pennylane, + "customerMetadata", + ], + }, + }, + async run({ $ }) { + const response = await this.pennylane.createCustomer(); + $.export("$summary", `Created customer ${response.customer.name}`); + return response; + }, +}; diff --git a/components/pennylane/package.json b/components/pennylane/package.json index 4a288e4b4b0f1..6f4b9a3f150a9 100644 --- a/components/pennylane/package.json +++ b/components/pennylane/package.json @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/components/pennylane/pennylane.app.mjs b/components/pennylane/pennylane.app.mjs index 91b88847f1af9..0c6913bb970e1 100644 --- a/components/pennylane/pennylane.app.mjs +++ b/components/pennylane/pennylane.app.mjs @@ -1,11 +1,292 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "pennylane", - propDefinitions: {}, + propDefinitions: { + // Emit event when a billing subscription is created + billingSubscriptionFilters: { + type: "string[]", + label: "Billing Subscription Filters", + description: "Filters to narrow down billing subscriptions for event triggers.", + }, + // Emit event when a new invoice is created or imported + invoiceFilters: { + type: "string[]", + label: "Invoice Filters", + description: "Filters to specify criteria for invoice events.", + }, + invoiceDateRangeFilters: { + type: "string[]", + label: "Invoice Date Range Filters", + description: "Optional date range filters for invoice events.", + optional: true, + }, + // Emit event when a new customer is created + customerTagsOrMetadataFilters: { + type: "string[]", + label: "Customer Tags or Metadata Filters", + description: "Optional tags or metadata to filter specific types of customers.", + optional: true, + }, + // Action: Create a new customer + customerName: { + type: "string", + label: "Customer Name", + description: "The name of the customer.", + }, + customerEmail: { + type: "string", + label: "Customer Email", + description: "The email address of the customer.", + }, + customerContactInfo: { + type: "string", + label: "Customer Contact Information", + description: "Contact information for the customer.", + }, + customerAddress: { + type: "string", + label: "Customer Address", + description: "The address of the customer.", + optional: true, + }, + customerMetadata: { + type: "string", + label: "Customer Metadata", + description: "Additional metadata for the customer.", + optional: true, + }, + // Action: Create a billing subscription + customerId: { + type: "string", + label: "Customer ID", + description: "The ID of the customer for the subscription.", + }, + subscriptionPlanId: { + type: "string", + label: "Subscription Plan ID", + description: "The ID of the subscription plan.", + async options() { + const plans = await this.listSubscriptionPlans(); + return plans.map((plan) => ({ + label: plan.name, + value: plan.id, + })); + }, + }, + billingFrequency: { + type: "string", + label: "Billing Frequency", + description: "The billing frequency of the subscription.", + options: [ + { + label: "Monthly", + value: "monthly", + }, + { + label: "Yearly", + value: "yearly", + }, + // Add more frequencies as needed + ], + }, + subscriptionDiscounts: { + type: "string", + label: "Subscription Discounts", + description: "Optional discounts for the subscription.", + optional: true, + }, + subscriptionCustomNotes: { + type: "string", + label: "Subscription Custom Notes", + description: "Optional custom notes for the subscription.", + optional: true, + }, + // Action: Generate a new invoice + invoiceCustomerId: { + type: "string", + label: "Invoice Customer ID", + description: "The ID of the customer for the invoice.", + }, + invoiceItems: { + type: "string[]", + label: "Invoice Items", + description: "An array of invoice items as JSON strings.", + helperText: "Each item should be a valid JSON string representing an invoice item.", + }, + paymentTerms: { + type: "string", + label: "Payment Terms", + description: "Payment terms for the invoice.", + }, + invoiceTaxDetails: { + type: "string", + label: "Invoice Tax Details", + description: "Optional tax details for the invoice.", + optional: true, + }, + invoiceFooterCustomization: { + type: "string", + label: "Invoice Footer Customization", + description: "Optional footer customization for the invoice.", + optional: true, + }, + }, methods: { - // this.$auth contains connected account data authKeys() { console.log(Object.keys(this.$auth)); }, + _baseUrl() { + return "https://app.pennylane.com/api/external/v1"; + }, + async _makeRequest(opts = {}) { + const { + $ = this, method = "GET", path = "/", headers, ...otherOpts + } = opts; + return axios($, { + ...otherOpts, + method, + url: `${this._baseUrl()}${path}`, + headers: { + ...headers, + "Authorization": `Bearer ${this.$auth.api_token}`, + "Content-Type": "application/json", + }, + }); + }, + // List billing subscriptions + async listBillingSubscriptions(opts = {}) { + return this._makeRequest({ + method: "GET", + path: "/billing_subscriptions", + params: opts.params, + }); + }, + // List invoices + async listInvoices(opts = {}) { + return this._makeRequest({ + method: "GET", + path: "/customer_invoices", + params: opts.params, + }); + }, + // List customers + async listCustomers(opts = {}) { + return this._makeRequest({ + method: "GET", + path: "/customers", + params: opts.params, + }); + }, + // Create a new customer + async createCustomer() { + const data = { + customer: { + name: this.customerName, + email: this.customerEmail, + contact_information: this.customerContactInfo, + }, + }; + if (this.customerAddress) { + data.customer.billing_address = JSON.parse(this.customerAddress); + } + if (this.customerMetadata) { + data.customer.metadata = JSON.parse(this.customerMetadata); + } + return this._makeRequest({ + method: "POST", + path: "/customers", + data, + }); + }, + // Create a billing subscription + async createBillingSubscription() { + const data = { + billing_subscription: { + customer_id: this.customerId, + subscription_plan_id: this.subscriptionPlanId, + billing_frequency: this.billingFrequency, + }, + }; + if (this.subscriptionDiscounts) { + data.billing_subscription.discounts = JSON.parse(this.subscriptionDiscounts); + } + if (this.subscriptionCustomNotes) { + data.billing_subscription.custom_notes = this.subscriptionCustomNotes; + } + return this._makeRequest({ + method: "POST", + path: "/billing_subscriptions", + data, + }); + }, + // Generate a new invoice + async generateInvoice() { + const data = { + invoice: { + customer_id: this.invoiceCustomerId, + invoice_items: this.invoiceItems.map((item) => JSON.parse(item)), + payment_terms: this.paymentTerms, + }, + }; + if (this.invoiceTaxDetails) { + data.invoice.tax_details = JSON.parse(this.invoiceTaxDetails); + } + if (this.invoiceFooterCustomization) { + data.invoice.footer_customization = this.invoiceFooterCustomization; + } + return this._makeRequest({ + method: "POST", + path: "/customer_invoices", + data, + }); + }, + // List subscription plans + async listSubscriptionPlans(opts = {}) { + return this._makeRequest({ + method: "GET", + path: "/plan_items", + params: opts.params, + }); + }, + // Pagination method + async paginate(fn, ...opts) { + let results = []; + let currentPage = 1; + let totalPages = 1; + + while (currentPage <= totalPages) { + const response = await fn({ + ...opts, + params: { + page: currentPage, + ...opts.params, + }, + }); + if (!response) break; + let key = ""; + if (response.billing_subscriptions) key = "billing_subscriptions"; + else if (response.invoices) key = "invoices"; + else if (response.customers) key = "customers"; + else break; + results = results.concat(response[key]); + totalPages = response.total_pages || response.totalPages || 1; + currentPage += 1; + } + + return results; + }, + // Emit events + async emitNewBillingSubscription(subscription) { + // Implementation to emit the billing subscription event + }, + async emitNewInvoice(invoice) { + // Implementation to emit the invoice event + }, + async emitNewCustomer(customer) { + // Implementation to emit the customer event + }, }, + version: "0.0.{{ts}}", }; diff --git a/components/pennylane/sources/new-billing-subscription/new-billing-subscription.mjs b/components/pennylane/sources/new-billing-subscription/new-billing-subscription.mjs new file mode 100644 index 0000000000000..a68505e79f283 --- /dev/null +++ b/components/pennylane/sources/new-billing-subscription/new-billing-subscription.mjs @@ -0,0 +1,124 @@ +import { + axios, DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; +import pennylane from "../../pennylane.app.mjs"; + +export default { + key: "pennylane-new-billing-subscription", + name: "New Billing Subscription Created", + description: "Emit new event when a billing subscription is created. [See the documentation]()", + version: "0.0.{{ts}}", + type: "source", + dedupe: "unique", + props: { + pennylane: { + type: "app", + app: "pennylane", + }, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + billingSubscriptionFilters: { + propDefinition: [ + "pennylane", + "billingSubscriptionFilters", + ], + }, + }, + hooks: { + async deploy() { + const filters = this.billingSubscriptionFilters.length + ? JSON.stringify(this.billingSubscriptionFilters.map((filter) => JSON.parse(filter))) + : undefined; + + const params = {}; + if (filters) params.filter = filters; + + const subscriptions = await this.pennylane.paginate( + this.pennylane.listBillingSubscriptions, + { + params, + }, + ); + + const latestSubscriptions = subscriptions.slice(-50).reverse(); + + for (const sub of latestSubscriptions) { + this.$emit( + sub, + { + id: sub.id.toString(), + summary: `New Billing Subscription: ${sub.id}`, + ts: sub.start + ? new Date(sub.start).getTime() + : Date.now(), + }, + ); + } + + const lastTimestamp = latestSubscriptions.length > 0 + ? Math.max(...latestSubscriptions.map((sub) => sub.start + ? new Date(sub.start).getTime() + : 0)) + : Date.now(); + + await this.db.set("lastTimestamp", lastTimestamp); + }, + async activate() { + // No webhook setup required for polling source + }, + async deactivate() { + // No webhook teardown required for polling source + }, + }, + async run() { + const lastTimestamp = (await this.db.get("lastTimestamp")) || 0; + + const filters = this.billingSubscriptionFilters.length + ? JSON.stringify(this.billingSubscriptionFilters.map((filter) => JSON.parse(filter))) + : undefined; + + const params = {}; + if (filters) params.filter = filters; + + const subscriptions = await this.pennylane.paginate( + this.pennylane.listBillingSubscriptions, + { + params, + }, + ); + + const newSubscriptions = subscriptions.filter((sub) => { + const ts = sub.start + ? new Date(sub.start).getTime() + : Date.now(); + return ts > lastTimestamp; + }); + + for (const sub of newSubscriptions) { + this.$emit( + sub, + { + id: sub.id.toString(), + summary: `New Billing Subscription: ${sub.id}`, + ts: sub.start + ? new Date(sub.start).getTime() + : Date.now(), + }, + ); + } + + if (newSubscriptions.length > 0) { + const newLastTimestamp = Math.max( + ...newSubscriptions.map((sub) => sub.start + ? new Date(sub.start).getTime() + : 0), + ); + await this.db.set("lastTimestamp", newLastTimestamp); + } + }, +}; diff --git a/components/pennylane/sources/new-customer-invoice/new-customer-invoice.mjs b/components/pennylane/sources/new-customer-invoice/new-customer-invoice.mjs new file mode 100644 index 0000000000000..890d5d36deecf --- /dev/null +++ b/components/pennylane/sources/new-customer-invoice/new-customer-invoice.mjs @@ -0,0 +1,104 @@ +import { + axios, DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; +import pennylane from "../../pennylane.app.mjs"; + +export default { + key: "pennylane-new-customer-invoice", + name: "New Customer Invoice Created or Imported", + description: "Emit a new event when a new invoice is created or imported. [See the documentation]()", + version: "0.0.{{ts}}", + type: "source", + dedupe: "unique", + props: { + pennylane: { + type: "app", + app: "pennylane", + }, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + invoiceFilters: { + propDefinition: [ + "pennylane", + "invoiceFilters", + ], + }, + invoiceDateRangeFilters: { + propDefinition: [ + "pennylane", + "invoiceDateRangeFilters", + ], + optional: true, + }, + }, + hooks: { + async deploy() { + const filter = this.invoiceFilters.map(JSON.parse); + if (this.invoiceDateRangeFilters) { + filter.push(...this.invoiceDateRangeFilters.map(JSON.parse)); + } + + const allInvoices = await this.pennylane.paginate(this.pennylane.listInvoices, { + filter, + date_range: this.invoiceDateRangeFilters, + }); + + const recentInvoices = allInvoices.slice(-50).reverse(); + + for (const invoice of recentInvoices) { + this.$emit(invoice, { + id: invoice.id, + summary: `New Invoice: ${invoice.invoice_number || invoice.label}`, + ts: invoice.created_at + ? Date.parse(invoice.created_at) + : Date.now(), + }); + } + + if (recentInvoices.length > 0) { + const latestInvoice = recentInvoices[recentInvoices.length - 1]; + this.db.set("lastTimestamp", Date.parse(latestInvoice.created_at || Date.now())); + } + }, + async activate() { + // No webhook subscription needed for polling source + }, + async deactivate() { + // No webhook subscription to delete for polling source + }, + }, + async run() { + const lastTimestamp = (await this.db.get("lastTimestamp")) || 0; + + const filter = this.invoiceFilters.map(JSON.parse); + if (this.invoiceDateRangeFilters) { + filter.push(...this.invoiceDateRangeFilters.map(JSON.parse)); + } + + const newInvoices = await this.pennylane.listInvoices({ + filter, + date_range: this.invoiceDateRangeFilters, + since: new Date(lastTimestamp).toISOString(), + }); + + for (const invoice of newInvoices) { + const invoiceTimestamp = Date.parse(invoice.created_at || Date.now()); + if (invoiceTimestamp > lastTimestamp) { + this.$emit(invoice, { + id: invoice.id, + summary: `New Invoice: ${invoice.invoice_number || invoice.label}`, + ts: invoiceTimestamp, + }); + + if (invoiceTimestamp > lastTimestamp) { + await this.db.set("lastTimestamp", invoiceTimestamp); + } + } + } + }, +}; diff --git a/components/pennylane/sources/new-customer/new-customer.mjs b/components/pennylane/sources/new-customer/new-customer.mjs new file mode 100644 index 0000000000000..017ffd3c01e11 --- /dev/null +++ b/components/pennylane/sources/new-customer/new-customer.mjs @@ -0,0 +1,115 @@ +import pennylane from "../../pennylane.app.mjs"; +import { + axios, DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, +} from "@pipedream/platform"; + +export default { + key: "pennylane-new-customer", + name: "New Customer Created", + description: "Emit a new event when a customer is created. [See the documentation]()", + version: "0.0.{{ts}}", + type: "source", + dedupe: "unique", + props: { + pennylane, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + customerTagsOrMetadataFilters: { + propDefinition: [ + pennylane, + "customerTagsOrMetadataFilters", + ], + }, + }, + methods: { + async _fetchCustomers(params) { + return this.pennylane.paginate(this.pennylane.listCustomers, params); + }, + _getLastTimestamp() { + return this.db.get("lastRunTs") || 0; + }, + _setLastTimestamp(timestamp) { + this.db.set("lastRunTs", timestamp); + }, + _emitCustomer(customer) { + const timestamp = Date.parse(customer.created_at) || Date.now(); + const id = customer.id || customer.source_id || timestamp; + this.$emit(customer, { + id, + summary: `New Customer: ${customer.name}`, + ts: timestamp, + }); + }, + }, + hooks: { + async deploy() { + const params = { + per_page: 50, + }; + + if (this.customerTagsOrMetadataFilters) { + params.filter = JSON.stringify(this.customerTagsOrMetadataFilters); + } + + const customers = await this._fetchCustomers(params); + + // Emit customers in order from oldest to newest + const sortedCustomers = customers.sort( + (a, b) => new Date(a.created_at) - new Date(b.created_at), + ); + + sortedCustomers.forEach((customer) => this._emitCustomer(customer)); + + if (customers.length > 0) { + const latestCustomer = customers.reduce((latest, customer) => { + const customerTs = Date.parse(customer.created_at); + return customerTs > latest + ? customerTs + : latest; + }, 0); + this._setLastTimestamp(latestCustomer); + } + }, + async activate() { + // No webhook subscription needed for polling source + }, + async deactivate() { + // No webhook subscription to remove for polling source + }, + }, + async run() { + const lastRunTs = this._getLastTimestamp(); + const params = { + per_page: 50, + sort: "created_at", + order: "asc", + }; + + if (this.customerTagsOrMetadataFilters) { + params.filter = JSON.stringify(this.customerTagsOrMetadataFilters); + } + + const customers = await this._fetchCustomers(params); + + const newCustomers = customers.filter( + (customer) => Date.parse(customer.created_at) > lastRunTs, + ); + + newCustomers.forEach((customer) => this._emitCustomer(customer)); + + if (newCustomers.length > 0) { + const latestCustomerTs = newCustomers.reduce((latest, customer) => { + const customerTs = Date.parse(customer.created_at); + return customerTs > latest + ? customerTs + : latest; + }, lastRunTs); + this._setLastTimestamp(latestCustomerTs); + } + }, +}; From 6cbb3876ff73691cdc61af1130940b2467c50551 Mon Sep 17 00:00:00 2001 From: Luan Cazarine Date: Mon, 16 Dec 2024 09:58:44 -0300 Subject: [PATCH 2/5] [Components] pennylane #14952 Sources - New Billing Subscription - New Customer Invoice - New Customer Actions - Create Customer - Create Billing Subscription - Create Customer Invoice --- .../create-billing-subscription.mjs | 158 ++++++--- .../create-customer-invoice.mjs | 158 +++++++-- .../create-customer/create-customer.mjs | 239 ++++++++++++-- components/pennylane/common/constants.mjs | 23 ++ components/pennylane/common/utils.mjs | 32 ++ components/pennylane/package.json | 5 +- components/pennylane/pennylane.app.mjs | 307 ++++-------------- components/pennylane/sources/common/base.mjs | 60 ++++ .../new-billing-subscription.mjs | 127 +------- .../new-billing-subscription/test-event.mjs | 106 ++++++ .../new-customer-invoice.mjs | 107 +----- .../new-customer-invoice/test-event.mjs | 114 +++++++ .../sources/new-customer/new-customer.mjs | 116 +------ .../sources/new-customer/test-event.mjs | 38 +++ 14 files changed, 941 insertions(+), 649 deletions(-) create mode 100644 components/pennylane/common/constants.mjs create mode 100644 components/pennylane/common/utils.mjs create mode 100644 components/pennylane/sources/common/base.mjs create mode 100644 components/pennylane/sources/new-billing-subscription/test-event.mjs create mode 100644 components/pennylane/sources/new-customer-invoice/test-event.mjs create mode 100644 components/pennylane/sources/new-customer/test-event.mjs diff --git a/components/pennylane/actions/create-billing-subscription/create-billing-subscription.mjs b/components/pennylane/actions/create-billing-subscription/create-billing-subscription.mjs index ed138ba775c78..b430b3fa594e8 100644 --- a/components/pennylane/actions/create-billing-subscription/create-billing-subscription.mjs +++ b/components/pennylane/actions/create-billing-subscription/create-billing-subscription.mjs @@ -1,52 +1,134 @@ +import { parseObject } from "../../common/utils.mjs"; import pennylane from "../../pennylane.app.mjs"; -import { axios } from "@pipedream/platform"; export default { key: "pennylane-create-billing-subscription", name: "Create Billing Subscription", - description: "Creates a billing subscription for a customer. [See the documentation]().", - version: "0.0.{{ts}}", + description: "Creates a billing subscription for a customer. [See the documentation](https://pennylane.readme.io/reference/billing_subscriptions-post-1).", + version: "0.0.1", type: "action", props: { - pennylane: { - type: "app", - app: "pennylane", - }, - customerId: { - propDefinition: [ - pennylane, - "customerId", - ], - }, - subscriptionPlanId: { - propDefinition: [ - pennylane, - "subscriptionPlanId", - ], - }, - billingFrequency: { - propDefinition: [ - pennylane, - "billingFrequency", - ], - }, - subscriptionDiscounts: { - propDefinition: [ - pennylane, - "subscriptionDiscounts", - ], - optional: true, - }, - subscriptionCustomNotes: { - propDefinition: [ - pennylane, - "subscriptionCustomNotes", - ], + pennylane, + mode: { + type: "string", + label: "Mode", + description: "Mode", + optional: true, + }, + paymentConditions: { + type: "string", + label: "Payment Conditions", + description: "PaymentConditions", + optional: true, + }, + paymentMethod: { + type: "string", + label: "Payment Method", + description: "PaymentMethod", + optional: true, + }, + type: { + type: "string", + label: "Type", + description: "Type", + optional: true, + }, + dayOfMonth: { + type: "string", + label: "Day Of Month", + description: "DayOfMonth", + optional: true, + }, + dayOfWeek: { + type: "string", + label: "Day Of Week", + description: "DayOfWeek", + optional: true, + }, + interval: { + type: "string", + label: "Interval", + description: "Interval", + optional: true, + }, + count: { + type: "string", + label: "Count", + description: "Count", + optional: true, + }, + sourceId: { + type: "string", + label: "Source Id", + description: "SourceId", + optional: true, + }, + currency: { + type: "string", + label: "Currency", + description: "Currency", + optional: true, + }, + start: { + type: "string", + label: "Start", + description: "Start", + optional: true, + }, + specialMention: { + type: "string", + label: "Special Mention", + description: "Special Mention", + optional: true, + }, + discount: { + type: "integer", + label: "Discount", + description: "Discount", + optional: true, + }, + lineItemsSectionsAttributes: { + type: "string", + label: "Line Items Sections Attributes", + description: "Line Items Sections Attributes", + optional: true, + }, + invoiceLines: { + type: "string", + label: "Invoice Lines", + description: "Invoice Lines", optional: true, }, }, async run({ $ }) { - const response = await this.pennylane.createBillingSubscription(); + const response = await this.pennylane.createBillingSubscription({ + $, + data: { + create_customer: false, + create_products: false, + billing_subscription: { + currency: this.currency, + mode: this.mode, + start: this.start, + payment_conditions: this.paymentConditions, + payment_method: this.paymentMethod, + recurring_rule: { + type: this.type, + day_of_month: this.dayOfMonth, + day_of_week: this.dayOfWeek, + interval: this.interval, + count: this.count, + }, + special_mention: this.specialMention, + discount: this.discount, + customer: { + source_id: this.sourceId, + }, + line_items_sections_attributes: parseObject(this.lineItemsSectionsAttributes), + invoice_lines: parseObject(this.invoiceLines), + }, + }, + }); $.export("$summary", `Created billing subscription with ID ${response.billing_subscription.id}`); return response; }, diff --git a/components/pennylane/actions/create-customer-invoice/create-customer-invoice.mjs b/components/pennylane/actions/create-customer-invoice/create-customer-invoice.mjs index dc8fab7debbbe..60608d7db57e4 100644 --- a/components/pennylane/actions/create-customer-invoice/create-customer-invoice.mjs +++ b/components/pennylane/actions/create-customer-invoice/create-customer-invoice.mjs @@ -1,49 +1,157 @@ +import { LANGUAGE_OPTIONS } from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; import pennylane from "../../pennylane.app.mjs"; -import { axios } from "@pipedream/platform"; export default { key: "pennylane-create-customer-invoice", name: "Create Customer Invoice", description: "Generates a new invoice for a customer using Pennylane. [See the documentation](https://pennylane.readme.io/reference/customer_invoices-post-1)", - version: "0.0.{{ts}}", + version: "0.0.1", type: "action", props: { pennylane, - invoiceCustomerId: { - propDefinition: [ - pennylane, - "invoiceCustomerId", - ], + date: { + type: "string", + label: "Date", + description: "Invoice date (ISO 8601)", }, - invoiceItems: { - propDefinition: [ - pennylane, - "invoiceItems", - ], + deadline: { + type: "string", + label: "Deadline", + description: "Invoice payment deadline (ISO 8601)", }, - paymentTerms: { - propDefinition: [ - pennylane, - "paymentTerms", - ], + externalId: { + type: "string", + label: "External Id", + description: "An id you can use to refer to the invoice from outside of Pennylane", + optional: true, }, - invoiceTaxDetails: { - propDefinition: [ - pennylane, - "invoiceTaxDetails", - ], + pdfInvoiceFreeText: { + type: "string", + label: "PDF Invoice Free Text", + description: "For example, the contact details of the person to contact", optional: true, }, - invoiceFooterCustomization: { + pdfInvoiceSubject: { + type: "string", + label: "PDF Invoice Subject", + description: "Invoice title", + optional: true, + }, + draft: { + type: "boolean", + label: "Draft", + description: "Do you wish to create a draft invoice (otherwise it is a finalized invoice) ? Reminder, once created, a finalized invoice cannot be edited !", + }, + currency: { + type: "string", + label: "Currency", + description: "Invoice Currency (ISO 4217). Default is EUR.", + optional: true, + }, + specialMention: { + type: "string", + label: "Special Mention", + description: "Additional details", + optional: true, + }, + discount: { + type: "integer", + label: "Discount", + description: "Invoice discount (in percent)", + optional: true, + }, + language: { + type: "string", + label: "Language", + description: "invoice pdf language", + options: LANGUAGE_OPTIONS, + optional: true, + }, + bankingProvider: { + type: "string", + label: "Banking Provider", + description: "The banking provider for the transaction", + }, + providerFieldName: { + type: "string", + label: "Provider Field Name", + description: "Name of the field that you want to match", + }, + providerFieldValue: { + type: "string", + label: "Provider Field Value", + description: "Value that you want to match", + }, + customerId: { propDefinition: [ pennylane, - "invoiceFooterCustomization", + "customerId", ], + }, + lineItemsSectionsAttributes: { + type: "string[]", + label: "Line Items Sections Attributes", + description: "A list of objects of items sections to be listed on the invoice", + optional: true, + }, + lineItems: { + type: "string[]", + label: "Line Items", + description: "A list of objects of items to be listed on the invoice", + }, + categories: { + type: "string[]", + label: "Categories", + description: "A list of objects of categories", optional: true, }, + startDate: { + type: "string", + label: "Start Date", + description: "Start date of the imputation period (ISO 8601)", + }, + endDate: { + type: "string", + label: "End Date", + description: "End date of the imputation period (ISO 8601)", + }, }, async run({ $ }) { - const invoice = await this.pennylane.generateInvoice(); + const invoice = await this.pennylane.createInvoice({ + $, + data: { + create_custome: false, + create_products: false, + invoice: { + date: this.date, + deadline: this.deadline, + external_id: this.externalId, + pdf_invoice_free_text: this.pdfInvoiceFreeText, + pdf_invoice_subject: this.pdfInvoiceSubject, + draft: this.draft, + currency: this.currency, + special_mention: this.specialMention, + discount: this.discount, + language: this.language, + transactions_reference: { + banking_provider: this.bankingProvider, + provider_field_name: this.providerFieldName, + provider_field_value: this.providerFieldValue, + }, + customer: { + source_id: this.customerId, + }, + line_items_sections_attributes: parseObject(this.lineItemsSectionsAttributes), + line_items: parseObject(this.lineItems), + categories: parseObject(this.categories), + imputation_dates: { + start_date: this.startDate, + end_date: this.endDate, + }, + }, + }, + }); $.export("$summary", `Created invoice with ID ${invoice.invoice.id}`); return invoice; diff --git a/components/pennylane/actions/create-customer/create-customer.mjs b/components/pennylane/actions/create-customer/create-customer.mjs index 8f6089c049734..3d1ada49a8531 100644 --- a/components/pennylane/actions/create-customer/create-customer.mjs +++ b/components/pennylane/actions/create-customer/create-customer.mjs @@ -1,48 +1,221 @@ +import { + CUSTOMER_GENDER_OPTIONS, + CUSTOMER_TYPE_OPTIONS, PAYMENT_CONDITIONS_OPTIONS, +} from "../../common/constants.mjs"; import pennylane from "../../pennylane.app.mjs"; -import { axios } from "@pipedream/platform"; export default { key: "pennylane-create-customer", name: "Create Customer", description: "Creates a new customer in Pennylane. [See the documentation](https://pennylane.readme.io/reference/customers-post-1)", - version: "0.0.{{ts}}", + version: "0.0.1", type: "action", props: { pennylane, - customerName: { - propDefinition: [ - pennylane, - "customerName", - ], - }, - customerEmail: { - propDefinition: [ - pennylane, - "customerEmail", - ], - }, - customerContactInfo: { - propDefinition: [ - pennylane, - "customerContactInfo", - ], - }, - customerAddress: { - propDefinition: [ - pennylane, - "customerAddress", - ], - }, - customerMetadata: { - propDefinition: [ - pennylane, - "customerMetadata", - ], + customerType: { + type: "string", + label: "Customer Type", + description: "The type of the customer you want to create.", + options: CUSTOMER_TYPE_OPTIONS, + reloadProps: true, }, + firstName: { + type: "string", + label: "First Name", + description: "Customer first name.", + hidden: true, + }, + lastName: { + type: "string", + label: "Last Name", + description: "Customer last name.", + hidden: true, + }, + gender: { + type: "string", + label: "Gender", + description: "Customer Gender", + options: CUSTOMER_GENDER_OPTIONS, + hidden: true, + optional: true, + }, + name: { + type: "string", + label: "Name", + description: "The name of the company.", + hidden: true, + }, + regNo: { + type: "string", + label: "Reg No", + description: "Customer registration number (SIREN).", + hidden: true, + optional: true, + }, + address: { + type: "string", + label: "Address", + description: "Customer address (billing address).", + }, + postalCode: { + type: "string", + label: "Postal Code", + description: "Postal code (billing address).", + }, + city: { + type: "string", + label: "City", + description: "City (billing address).", + }, + country: { + type: "string", + label: "Country", + description: "Any ISO 3166 Alpha-2 country code (billing address).", + }, + recipient: { + type: "string", + label: "Recipient", + description: "Recipient displayed in the invoice.", + optional: true, + }, + vatNumber: { + type: "string", + label: "VAT Number", + description: "Customer's VAT number.", + hidden: true, + optional: true, + }, + sourceId: { + type: "string", + label: "Source Id", + description: "You can use your own id when creating the customer. If not provided, Pennylane will pick one for you. Id must be unique.", + optional: true, + }, + emails: { + type: "string[]", + label: "Emails", + description: "A list of customer emails.", + optional: true, + }, + billingIban: { + type: "string", + label: "Billing IBAN", + description: "The billing IBAN of the customer. This is the iban on which you wish to receive payment from this customer.", + optional: true, + }, + deliveryAddress: { + type: "string", + label: "Delivery Address", + description: "Address (shipping address).", + optional: true, + }, + deliveryPostalCode: { + type: "string", + label: "Delivery Postal Code", + description: "Postal code (shipping address).", + optional: true, + }, + deliveryCity: { + type: "string", + label: "Delivery City", + description: "City (shipping address).", + optional: true, + }, + deliveryCountry: { + type: "string", + label: "Delivery Country", + description: "Any ISO 3166 Alpha-2 country code (shipping address).", + optional: true, + }, + paymentConditions: { + type: "string", + label: "Payment Conditions", + description: "Customer payment conditions", + options: PAYMENT_CONDITIONS_OPTIONS, + optional: true, + }, + phone: { + type: "string", + label: "Phone", + description: "Customer phone number.", + optional: true, + }, + reference: { + type: "string", + label: "Reference", + description: "This reference doesn't appear on the invoice.", + optional: true, + }, + notes: { + type: "string", + label: "Notes", + description: "Notes about the customer.", + optional: true, + }, + mandate: { + type: "object", + label: "Mandate", + description: "The mandate object. **Example: {\"provider\": \"gocardless\",\"source_id\": \"MD001H23WP8E7XN\"}**.", + optional: true, + }, + planItem: { + type: "object", + label: "Plan Item", + description: "The plan item object. **Example: {\"enabled\": true,\"number\": \"123\",\"label\": \"label\",\"vat_rate\": \"123\",\"country_alpha2\": \"US\",\"description\": \"description\"}**.", + optional: true, + }, + }, + async additionalProps(props) { + const typeCompany = (this.customerType === "company"); + props.name.hidden = !typeCompany; + props.regNo.hidden = !typeCompany; + props.vatNumber.hidden = !typeCompany; + props.firstName.hidden = typeCompany; + props.lastName.hidden = typeCompany; + props.gender.hidden = typeCompany; + return {}; }, async run({ $ }) { - const response = await this.pennylane.createCustomer(); - $.export("$summary", `Created customer ${response.customer.name}`); + const additionalData = this.customerType === "company" + ? { + name: this.name, + reg_no: this.regNo, + vat_number: this.vatNumber, + } + : { + first_name: this.firstName, + last_name: this.lastName, + gender: this.gender, + }; + + const response = await this.pennylane.createCustomer({ + $, + data: { + customer: { + customer_type: this.customerType, + address: this.address, + postal_code: this.postalCode, + city: this.city, + country: this.country, + recipient: this.recipient, + source_id: this.sourceId, + emails: this.emails, + billing_iban: this.billingIban, + delivery_address: this.deliveryAddress, + delivery_postal_code: this.deliveryPostalCode, + delivery_city: this.deliveryCity, + delivery_country: this.deliveryCountry, + payment_conditions: this.paymentConditions, + phone: this.phone, + reference: this.reference, + notes: this.notes, + mandate: this.mandate, + plan_item: this.planItem, + ...additionalData, + }, + }, + }); + $.export("$summary", `Successfully created customer with Id: ${response.customer.source_id}`); return response; }, }; diff --git a/components/pennylane/common/constants.mjs b/components/pennylane/common/constants.mjs new file mode 100644 index 0000000000000..993d4ed405c13 --- /dev/null +++ b/components/pennylane/common/constants.mjs @@ -0,0 +1,23 @@ +export const CUSTOMER_TYPE_OPTIONS = [ + "company", + "individual", +]; + +export const PAYMENT_CONDITIONS_OPTIONS = [ + "upon_receipt", + "custom", + "15_days", + "30_days", + "45_days", + "60_days", +]; + +export const CUSTOMER_GENDER_OPTIONS = [ + "mister", + "madam", +]; + +export const LANGUAGE_OPTIONS = [ + "en_GB", + "fr_FR", +]; diff --git a/components/pennylane/common/utils.mjs b/components/pennylane/common/utils.mjs new file mode 100644 index 0000000000000..9050833bcda0b --- /dev/null +++ b/components/pennylane/common/utils.mjs @@ -0,0 +1,32 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return parseObject(item); + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + if (typeof obj === "object") { + for (const [ + key, + value, + ] of Object.entries(obj)) { + obj[key] = parseObject(value); + } + } + return obj; +}; diff --git a/components/pennylane/package.json b/components/pennylane/package.json index 6f4b9a3f150a9..0acf42d2418cb 100644 --- a/components/pennylane/package.json +++ b/components/pennylane/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/pennylane", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Pennylane Components", "main": "pennylane.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } } diff --git a/components/pennylane/pennylane.app.mjs b/components/pennylane/pennylane.app.mjs index 0c6913bb970e1..0fc04bfc7baa2 100644 --- a/components/pennylane/pennylane.app.mjs +++ b/components/pennylane/pennylane.app.mjs @@ -4,289 +4,110 @@ export default { type: "app", app: "pennylane", propDefinitions: { - // Emit event when a billing subscription is created - billingSubscriptionFilters: { - type: "string[]", - label: "Billing Subscription Filters", - description: "Filters to narrow down billing subscriptions for event triggers.", - }, - // Emit event when a new invoice is created or imported - invoiceFilters: { - type: "string[]", - label: "Invoice Filters", - description: "Filters to specify criteria for invoice events.", - }, - invoiceDateRangeFilters: { - type: "string[]", - label: "Invoice Date Range Filters", - description: "Optional date range filters for invoice events.", - optional: true, - }, - // Emit event when a new customer is created - customerTagsOrMetadataFilters: { - type: "string[]", - label: "Customer Tags or Metadata Filters", - description: "Optional tags or metadata to filter specific types of customers.", - optional: true, - }, - // Action: Create a new customer - customerName: { - type: "string", - label: "Customer Name", - description: "The name of the customer.", - }, - customerEmail: { - type: "string", - label: "Customer Email", - description: "The email address of the customer.", - }, - customerContactInfo: { - type: "string", - label: "Customer Contact Information", - description: "Contact information for the customer.", - }, - customerAddress: { - type: "string", - label: "Customer Address", - description: "The address of the customer.", - optional: true, - }, - customerMetadata: { - type: "string", - label: "Customer Metadata", - description: "Additional metadata for the customer.", - optional: true, - }, - // Action: Create a billing subscription customerId: { type: "string", - label: "Customer ID", - description: "The ID of the customer for the subscription.", - }, - subscriptionPlanId: { - type: "string", - label: "Subscription Plan ID", - description: "The ID of the subscription plan.", - async options() { - const plans = await this.listSubscriptionPlans(); - return plans.map((plan) => ({ - label: plan.name, - value: plan.id, + label: "Customer Id", + description: "Existing customer identifier (source_id)", + async options({ page }) { + const { data } = await this.listCustomers({ + params: { + page: page + 1, + }, + }); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, })); }, }, - billingFrequency: { - type: "string", - label: "Billing Frequency", - description: "The billing frequency of the subscription.", - options: [ - { - label: "Monthly", - value: "monthly", - }, - { - label: "Yearly", - value: "yearly", - }, - // Add more frequencies as needed - ], - }, - subscriptionDiscounts: { - type: "string", - label: "Subscription Discounts", - description: "Optional discounts for the subscription.", - optional: true, - }, - subscriptionCustomNotes: { - type: "string", - label: "Subscription Custom Notes", - description: "Optional custom notes for the subscription.", - optional: true, - }, - // Action: Generate a new invoice - invoiceCustomerId: { - type: "string", - label: "Invoice Customer ID", - description: "The ID of the customer for the invoice.", - }, - invoiceItems: { - type: "string[]", - label: "Invoice Items", - description: "An array of invoice items as JSON strings.", - helperText: "Each item should be a valid JSON string representing an invoice item.", - }, - paymentTerms: { - type: "string", - label: "Payment Terms", - description: "Payment terms for the invoice.", - }, - invoiceTaxDetails: { - type: "string", - label: "Invoice Tax Details", - description: "Optional tax details for the invoice.", - optional: true, - }, - invoiceFooterCustomization: { - type: "string", - label: "Invoice Footer Customization", - description: "Optional footer customization for the invoice.", - optional: true, - }, }, methods: { - authKeys() { - console.log(Object.keys(this.$auth)); - }, _baseUrl() { return "https://app.pennylane.com/api/external/v1"; }, - async _makeRequest(opts = {}) { - const { - $ = this, method = "GET", path = "/", headers, ...otherOpts - } = opts; + _headers() { + return { + "Authorization": `Bearer ${this.$auth.api_token}`, + "Content-Type": "application/json", + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { return axios($, { - ...otherOpts, - method, - url: `${this._baseUrl()}${path}`, - headers: { - ...headers, - "Authorization": `Bearer ${this.$auth.api_token}`, - "Content-Type": "application/json", - }, + url: this._baseUrl() + path, + headers: this._headers(), + ...opts, }); }, - // List billing subscriptions - async listBillingSubscriptions(opts = {}) { + createCustomer(opts = {}) { return this._makeRequest({ - method: "GET", - path: "/billing_subscriptions", - params: opts.params, + method: "POST", + path: "/customers", + ...opts, }); }, - // List invoices - async listInvoices(opts = {}) { + createInvoice(opts = {}) { return this._makeRequest({ - method: "GET", + method: "POST", path: "/customer_invoices", - params: opts.params, - }); - }, - // List customers - async listCustomers(opts = {}) { - return this._makeRequest({ - method: "GET", - path: "/customers", - params: opts.params, + ...opts, }); }, - // Create a new customer - async createCustomer() { - const data = { - customer: { - name: this.customerName, - email: this.customerEmail, - contact_information: this.customerContactInfo, - }, - }; - if (this.customerAddress) { - data.customer.billing_address = JSON.parse(this.customerAddress); - } - if (this.customerMetadata) { - data.customer.metadata = JSON.parse(this.customerMetadata); - } + listCustomers(opts = {}) { return this._makeRequest({ - method: "POST", path: "/customers", - data, + ...opts, }); }, - // Create a billing subscription - async createBillingSubscription() { - const data = { - billing_subscription: { - customer_id: this.customerId, - subscription_plan_id: this.subscriptionPlanId, - billing_frequency: this.billingFrequency, - }, - }; - if (this.subscriptionDiscounts) { - data.billing_subscription.discounts = JSON.parse(this.subscriptionDiscounts); - } - if (this.subscriptionCustomNotes) { - data.billing_subscription.custom_notes = this.subscriptionCustomNotes; - } + createBillingSubscription(opts = {}) { return this._makeRequest({ method: "POST", path: "/billing_subscriptions", - data, + ...opts, }); }, - // Generate a new invoice - async generateInvoice() { - const data = { - invoice: { - customer_id: this.invoiceCustomerId, - invoice_items: this.invoiceItems.map((item) => JSON.parse(item)), - payment_terms: this.paymentTerms, - }, - }; - if (this.invoiceTaxDetails) { - data.invoice.tax_details = JSON.parse(this.invoiceTaxDetails); - } - if (this.invoiceFooterCustomization) { - data.invoice.footer_customization = this.invoiceFooterCustomization; - } + listBillingSubscriptions(opts = {}) { return this._makeRequest({ - method: "POST", - path: "/customer_invoices", - data, + path: "/billing_subscriptions", + params: opts.params, }); }, - // List subscription plans - async listSubscriptionPlans(opts = {}) { + listInvoices(opts = {}) { return this._makeRequest({ method: "GET", - path: "/plan_items", - params: opts.params, + path: "/customer_invoices", + ...opts, }); }, - // Pagination method - async paginate(fn, ...opts) { - let results = []; - let currentPage = 1; - let totalPages = 1; + async *paginate({ + fn, params = {}, fieldName, maxResults = null, ...opts + }) { + let hasMore = false; + let count = 0; + let page = 0; - while (currentPage <= totalPages) { - const response = await fn({ + do { + params.page = ++page; + const data = await fn({ + params, ...opts, - params: { - page: currentPage, - ...opts.params, - }, }); - if (!response) break; - let key = ""; - if (response.billing_subscriptions) key = "billing_subscriptions"; - else if (response.invoices) key = "invoices"; - else if (response.customers) key = "customers"; - else break; - results = results.concat(response[key]); - totalPages = response.total_pages || response.totalPages || 1; - currentPage += 1; - } + const items = data[fieldName]; + for (const d of items) { + yield d; - return results; - }, - // Emit events - async emitNewBillingSubscription(subscription) { - // Implementation to emit the billing subscription event - }, - async emitNewInvoice(invoice) { - // Implementation to emit the invoice event - }, - async emitNewCustomer(customer) { - // Implementation to emit the customer event + if (maxResults && ++count === maxResults) { + return count; + } + } + + hasMore = items.length; + + } while (hasMore); }, }, - version: "0.0.{{ts}}", }; diff --git a/components/pennylane/sources/common/base.mjs b/components/pennylane/sources/common/base.mjs new file mode 100644 index 0000000000000..5d6763beaf42e --- /dev/null +++ b/components/pennylane/sources/common/base.mjs @@ -0,0 +1,60 @@ +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; +import pennylane from "../../pennylane.app.mjs"; + +export default { + props: { + pennylane, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastId() { + return this.db.get("lastId") || 0; + }, + _setLastId(lastId) { + this.db.set("lastId", lastId); + }, + async emitEvent(maxResults = false) { + const lastId = this._getLastId(); + + const response = this.pennylane.paginate({ + fn: this.getFunction(), + fieldName: this.getFieldName(), + }); + + let responseArray = []; + for await (const item of response) { + if (item.source_id <= lastId) break; + responseArray.push(item); + } + + if (responseArray.length) { + if (maxResults && (responseArray.length > maxResults)) { + responseArray.length = maxResults; + } + this._setLastId(responseArray[0].source_id); + } + + for (const item of responseArray.reverse()) { + this.$emit(item, { + id: item.source_id, + summary: this.getSummary(item), + ts: Date.parse(item.updated_at || item.activated_at || new Date()), + }); + } + }, + }, + hooks: { + async deploy() { + await this.emitEvent(25); + }, + }, + async run() { + await this.emitEvent(); + }, +}; diff --git a/components/pennylane/sources/new-billing-subscription/new-billing-subscription.mjs b/components/pennylane/sources/new-billing-subscription/new-billing-subscription.mjs index a68505e79f283..7b331da212156 100644 --- a/components/pennylane/sources/new-billing-subscription/new-billing-subscription.mjs +++ b/components/pennylane/sources/new-billing-subscription/new-billing-subscription.mjs @@ -1,124 +1,25 @@ -import { - axios, DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, -} from "@pipedream/platform"; -import pennylane from "../../pennylane.app.mjs"; +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; export default { + ...common, key: "pennylane-new-billing-subscription", name: "New Billing Subscription Created", - description: "Emit new event when a billing subscription is created. [See the documentation]()", - version: "0.0.{{ts}}", + description: "Emit new event when a billing subscription is created.", + version: "0.0.1", type: "source", dedupe: "unique", - props: { - pennylane: { - type: "app", - app: "pennylane", + methods: { + ...common.methods, + getFunction() { + return this.pennylane.listBillingSubscriptions; }, - db: "$.service.db", - timer: { - type: "$.interface.timer", - default: { - intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, - }, + getFieldName() { + return "billing_subscriptions"; }, - billingSubscriptionFilters: { - propDefinition: [ - "pennylane", - "billingSubscriptionFilters", - ], + getSummary(item) { + return `New Billing Subscription: ${item.id}`; }, }, - hooks: { - async deploy() { - const filters = this.billingSubscriptionFilters.length - ? JSON.stringify(this.billingSubscriptionFilters.map((filter) => JSON.parse(filter))) - : undefined; - - const params = {}; - if (filters) params.filter = filters; - - const subscriptions = await this.pennylane.paginate( - this.pennylane.listBillingSubscriptions, - { - params, - }, - ); - - const latestSubscriptions = subscriptions.slice(-50).reverse(); - - for (const sub of latestSubscriptions) { - this.$emit( - sub, - { - id: sub.id.toString(), - summary: `New Billing Subscription: ${sub.id}`, - ts: sub.start - ? new Date(sub.start).getTime() - : Date.now(), - }, - ); - } - - const lastTimestamp = latestSubscriptions.length > 0 - ? Math.max(...latestSubscriptions.map((sub) => sub.start - ? new Date(sub.start).getTime() - : 0)) - : Date.now(); - - await this.db.set("lastTimestamp", lastTimestamp); - }, - async activate() { - // No webhook setup required for polling source - }, - async deactivate() { - // No webhook teardown required for polling source - }, - }, - async run() { - const lastTimestamp = (await this.db.get("lastTimestamp")) || 0; - - const filters = this.billingSubscriptionFilters.length - ? JSON.stringify(this.billingSubscriptionFilters.map((filter) => JSON.parse(filter))) - : undefined; - - const params = {}; - if (filters) params.filter = filters; - - const subscriptions = await this.pennylane.paginate( - this.pennylane.listBillingSubscriptions, - { - params, - }, - ); - - const newSubscriptions = subscriptions.filter((sub) => { - const ts = sub.start - ? new Date(sub.start).getTime() - : Date.now(); - return ts > lastTimestamp; - }); - - for (const sub of newSubscriptions) { - this.$emit( - sub, - { - id: sub.id.toString(), - summary: `New Billing Subscription: ${sub.id}`, - ts: sub.start - ? new Date(sub.start).getTime() - : Date.now(), - }, - ); - } - - if (newSubscriptions.length > 0) { - const newLastTimestamp = Math.max( - ...newSubscriptions.map((sub) => sub.start - ? new Date(sub.start).getTime() - : 0), - ); - await this.db.set("lastTimestamp", newLastTimestamp); - } - }, + sampleEmit, }; diff --git a/components/pennylane/sources/new-billing-subscription/test-event.mjs b/components/pennylane/sources/new-billing-subscription/test-event.mjs new file mode 100644 index 0000000000000..a9dd205c14c45 --- /dev/null +++ b/components/pennylane/sources/new-billing-subscription/test-event.mjs @@ -0,0 +1,106 @@ +export default { + "id": 0, + "next_occurrence": "string", + "prev_occurrence": "string", + "stopped_at": "2023-08-30T10:08:08.146343Z", + "start": "2023-01-01", + "finish": "2023-12-31", + "status": "string", + "mode": "string", + "email_settings": {}, + "activated_at": "2023-08-30T10:08:08.146343Z", + "payment_method": "string", + "recurring_rule": { + "day_of_month": [ + 0 + ], + "week_start": 0, + "day": [ + 0 + ], + "rule_type": "weekly", + "interval": 0, + "count": 12, + "until": "string" + }, + "customer": { + "first_name": "John", + "last_name": "Doe", + "gender": "mister", + "name": "Pennylane", + "reg_no": "XXXXXXXXX", + "vat_number": "FR12345678910", + "updated_at": "2023-08-30T10:08:08.146343Z", + "source_id": "38a1f19a-256d-4692-a8fe-0a16403f59ff", + "emails": [ + "hello@example.org" + ], + "billing_iban": "FRXX XXXX XXXX XXXX XXXX XXXX XXX", + "customer_type": "company", + "address": "4 rue du faubourg saint martin", + "postal_code": "75010", + "city": "Paris", + "country_alpha2": "FR", + "recipient": "string", + "billing_address": { + "address": "string", + "postal_code": "string", + "city": "string", + "country_alpha2": "string" + }, + "delivery_address": { + "address": "105 Rue Mondenard", + "postal_code": "33100", + "city": "Bordeaux", + "country_alpha2": "FR" + }, + "payment_conditions": "upon_receipt", + "phone": "+33123232323", + "reference": "This is a custom reference", + "notes": "This is a note", + "v2_id": 1234 + }, + "invoice_template": { + "label": "Invoice label", + "currency": "EUR", + "amount": "230.32", + "currency_amount": "230.32", + "currency_amount_before_tax": "196.32", + "exchange_rate": "1.0", + "payment_condition": "15_days", + "currency_tax": "34.0", + "language": "fr_FR", + "discount": "50.1", + "discount_type": "relative", + "special_mention": "Additional details", + "updated_at": "2023-08-30T10:08:08.146343Z", + "line_items_sections_attributes": [ + { + "title": "Line items section title", + "description": "Description of the line items section", + "rank": 1 + } + ], + "line_items": [ + { + "id": 444, + "label": "Demo label", + "unit": "piece", + "quantity": 12, + "amount": "50.4", + "currency_amount": "50.4", + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor", + "product_id": "9bca2815-217a-4210-bed8-73139977b9a8", + "product_v2_id": 1234, + "vat_rate": "FR_200", + "currency_price_before_tax": "30", + "currency_tax": "10", + "discount": "25", + "discount_type": "relative", + "section_rank": 1, + "v2_id": 1234 + } + ] + }, + "v2_id": 1234 +} \ No newline at end of file diff --git a/components/pennylane/sources/new-customer-invoice/new-customer-invoice.mjs b/components/pennylane/sources/new-customer-invoice/new-customer-invoice.mjs index 890d5d36deecf..0bb509b1f85c4 100644 --- a/components/pennylane/sources/new-customer-invoice/new-customer-invoice.mjs +++ b/components/pennylane/sources/new-customer-invoice/new-customer-invoice.mjs @@ -1,104 +1,25 @@ -import { - axios, DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, -} from "@pipedream/platform"; -import pennylane from "../../pennylane.app.mjs"; +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; export default { + ...common, key: "pennylane-new-customer-invoice", name: "New Customer Invoice Created or Imported", - description: "Emit a new event when a new invoice is created or imported. [See the documentation]()", - version: "0.0.{{ts}}", + description: "Emit new event when a new invoice is created or imported.", + version: "0.0.1", type: "source", dedupe: "unique", - props: { - pennylane: { - type: "app", - app: "pennylane", + methods: { + ...common.methods, + getFunction() { + return this.pennylane.listInvoices; }, - db: "$.service.db", - timer: { - type: "$.interface.timer", - default: { - intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, - }, + getFieldName() { + return "invoices"; }, - invoiceFilters: { - propDefinition: [ - "pennylane", - "invoiceFilters", - ], + getSummary(item) { + return `New Invoice: ${item.invoice_number || item.label}`; }, - invoiceDateRangeFilters: { - propDefinition: [ - "pennylane", - "invoiceDateRangeFilters", - ], - optional: true, - }, - }, - hooks: { - async deploy() { - const filter = this.invoiceFilters.map(JSON.parse); - if (this.invoiceDateRangeFilters) { - filter.push(...this.invoiceDateRangeFilters.map(JSON.parse)); - } - - const allInvoices = await this.pennylane.paginate(this.pennylane.listInvoices, { - filter, - date_range: this.invoiceDateRangeFilters, - }); - - const recentInvoices = allInvoices.slice(-50).reverse(); - - for (const invoice of recentInvoices) { - this.$emit(invoice, { - id: invoice.id, - summary: `New Invoice: ${invoice.invoice_number || invoice.label}`, - ts: invoice.created_at - ? Date.parse(invoice.created_at) - : Date.now(), - }); - } - - if (recentInvoices.length > 0) { - const latestInvoice = recentInvoices[recentInvoices.length - 1]; - this.db.set("lastTimestamp", Date.parse(latestInvoice.created_at || Date.now())); - } - }, - async activate() { - // No webhook subscription needed for polling source - }, - async deactivate() { - // No webhook subscription to delete for polling source - }, - }, - async run() { - const lastTimestamp = (await this.db.get("lastTimestamp")) || 0; - - const filter = this.invoiceFilters.map(JSON.parse); - if (this.invoiceDateRangeFilters) { - filter.push(...this.invoiceDateRangeFilters.map(JSON.parse)); - } - - const newInvoices = await this.pennylane.listInvoices({ - filter, - date_range: this.invoiceDateRangeFilters, - since: new Date(lastTimestamp).toISOString(), - }); - - for (const invoice of newInvoices) { - const invoiceTimestamp = Date.parse(invoice.created_at || Date.now()); - if (invoiceTimestamp > lastTimestamp) { - this.$emit(invoice, { - id: invoice.id, - summary: `New Invoice: ${invoice.invoice_number || invoice.label}`, - ts: invoiceTimestamp, - }); - - if (invoiceTimestamp > lastTimestamp) { - await this.db.set("lastTimestamp", invoiceTimestamp); - } - } - } }, + sampleEmit, }; diff --git a/components/pennylane/sources/new-customer-invoice/test-event.mjs b/components/pennylane/sources/new-customer-invoice/test-event.mjs new file mode 100644 index 0000000000000..a12654d228b09 --- /dev/null +++ b/components/pennylane/sources/new-customer-invoice/test-event.mjs @@ -0,0 +1,114 @@ +export default { + "id": "wMoOACctiA", + "label": "Invoice label", + "invoice_number": "Invoice number", + "quote_group_uuid": "b50b8fe6-48ce-4380-b9b3-52eec688fc04", + "is_draft": true, + "is_estimate": true, + "currency": "EUR", + "amount": "230.32", + "currency_amount": "230.32", + "currency_amount_before_tax": "196.32", + "exchange_rate": 1, + "date": "2023-08-30", + "deadline": "2020-09-02", + "currency_tax": "34.0", + "language": "fr_FR", + "paid": false, + "status": "upcoming", + "discount": "50.1", + "discount_type": "relative", + "public_url": "https://app.pennylane.com/...", + "file_url": "https://app.pennylane.com/.../file.pdf", + "filename": "my_file.pdf", + "remaining_amount": "20.0", + "source": "email", + "special_mention": "Additional details", + "updated_at": "2023-08-30T10:08:08.146343Z", + "imputation_dates": { + "start_date": "2020-06-30", + "end_date": "2021-06-30" + }, + "line_items_sections_attributes": [ + { + "title": "Line items section title", + "description": "Description of the line items section", + "rank": 1 + } + ], + "line_items": [ + { + "id": 444, + "label": "Demo label", + "unit": "piece", + "quantity": 12, + "amount": "50.4", + "currency_amount": "50.4", + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor", + "product_id": "9bca2815-217a-4210-bed8-73139977b9a8", + "product_v2_id": 1234, + "vat_rate": "FR_200", + "currency_price_before_tax": "30", + "currency_tax": "10", + "raw_currency_unit_price": "5", + "discount": "25", + "discount_type": "relative", + "section_rank": 1, + "v2_id": 1234 + } + ], + "categories": [ + { + "source_id": "38a1f19a-256d-4692-a8fe-0a16403f59ff", + "weight": 0.8, + "label": "Alimentaire", + "direction": "cash_in", + "created_at": "2023-08-30T10:08:08.146343Z", + "updated_at": "2023-08-30T10:08:08.146343Z", + "v2_id": 1234 + } + ], + "transactions_reference": { + "banking_provider": "bank", + "provider_field_name": "label", + "provider_field_value": "invoice_number" + }, + "payments": [ + { + "label": "string", + "created_at": "2023-08-30", + "currency_amount": "string" + } + ], + "matched_transactions": [ + { + "label": "string", + "amount": "string", + "group_uuid": "string", + "date": "2023-08-30", + "fee": "string", + "currency": "string" + } + ], + "pdf_invoice_free_text": "string", + "pdf_invoice_subject": "string", + "billing_subscription": { + "id": 0, + "v2_id": 1234 + }, + "credit_notes": [ + { + "id": "BCVPZQJ17V", + "amount": "230.32", + "tax": "34.0", + "currency": "EUR", + "currency_amount": "230.32", + "currency_tax": "230.32", + "currency_price_before_tax": "196.32", + "invoice_number": "Credit note number", + "draft": false, + "v2_id": 1234 + } + ], + "v2_id": 1234 +} \ No newline at end of file diff --git a/components/pennylane/sources/new-customer/new-customer.mjs b/components/pennylane/sources/new-customer/new-customer.mjs index 017ffd3c01e11..bd3dc384edbed 100644 --- a/components/pennylane/sources/new-customer/new-customer.mjs +++ b/components/pennylane/sources/new-customer/new-customer.mjs @@ -1,115 +1,25 @@ -import pennylane from "../../pennylane.app.mjs"; -import { - axios, DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, -} from "@pipedream/platform"; +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; export default { + ...common, key: "pennylane-new-customer", name: "New Customer Created", - description: "Emit a new event when a customer is created. [See the documentation]()", - version: "0.0.{{ts}}", + description: "Emit new event when a customer is created.", + version: "0.0.1", type: "source", dedupe: "unique", - props: { - pennylane, - db: "$.service.db", - timer: { - type: "$.interface.timer", - default: { - intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, - }, - }, - customerTagsOrMetadataFilters: { - propDefinition: [ - pennylane, - "customerTagsOrMetadataFilters", - ], - }, - }, methods: { - async _fetchCustomers(params) { - return this.pennylane.paginate(this.pennylane.listCustomers, params); + ...common.methods, + getFunction() { + return this.pennylane.listCustomers; }, - _getLastTimestamp() { - return this.db.get("lastRunTs") || 0; + getFieldName() { + return "customers"; }, - _setLastTimestamp(timestamp) { - this.db.set("lastRunTs", timestamp); + getSummary(item) { + return `New Customer: ${item.name}`; }, - _emitCustomer(customer) { - const timestamp = Date.parse(customer.created_at) || Date.now(); - const id = customer.id || customer.source_id || timestamp; - this.$emit(customer, { - id, - summary: `New Customer: ${customer.name}`, - ts: timestamp, - }); - }, - }, - hooks: { - async deploy() { - const params = { - per_page: 50, - }; - - if (this.customerTagsOrMetadataFilters) { - params.filter = JSON.stringify(this.customerTagsOrMetadataFilters); - } - - const customers = await this._fetchCustomers(params); - - // Emit customers in order from oldest to newest - const sortedCustomers = customers.sort( - (a, b) => new Date(a.created_at) - new Date(b.created_at), - ); - - sortedCustomers.forEach((customer) => this._emitCustomer(customer)); - - if (customers.length > 0) { - const latestCustomer = customers.reduce((latest, customer) => { - const customerTs = Date.parse(customer.created_at); - return customerTs > latest - ? customerTs - : latest; - }, 0); - this._setLastTimestamp(latestCustomer); - } - }, - async activate() { - // No webhook subscription needed for polling source - }, - async deactivate() { - // No webhook subscription to remove for polling source - }, - }, - async run() { - const lastRunTs = this._getLastTimestamp(); - const params = { - per_page: 50, - sort: "created_at", - order: "asc", - }; - - if (this.customerTagsOrMetadataFilters) { - params.filter = JSON.stringify(this.customerTagsOrMetadataFilters); - } - - const customers = await this._fetchCustomers(params); - - const newCustomers = customers.filter( - (customer) => Date.parse(customer.created_at) > lastRunTs, - ); - - newCustomers.forEach((customer) => this._emitCustomer(customer)); - - if (newCustomers.length > 0) { - const latestCustomerTs = newCustomers.reduce((latest, customer) => { - const customerTs = Date.parse(customer.created_at); - return customerTs > latest - ? customerTs - : latest; - }, lastRunTs); - this._setLastTimestamp(latestCustomerTs); - } }, + sampleEmit, }; diff --git a/components/pennylane/sources/new-customer/test-event.mjs b/components/pennylane/sources/new-customer/test-event.mjs new file mode 100644 index 0000000000000..89b75ac2b2e00 --- /dev/null +++ b/components/pennylane/sources/new-customer/test-event.mjs @@ -0,0 +1,38 @@ +export default { + "name": "Pennylane", + "reg_no": "XXXXXXXXX", + "vat_number": "FR12345678910", + "updated_at": "2021-06-30T07:44:37.545Z", + "source_id": "38a1f19a-256d-4692-a8fe-0a16403f59ff", + "emails": [ + "hello@example.org" + ], + "billing_iban": "FRXX XXXX XXXX XXXX XXXX XXXX XXX", + "customer_type": "company", + "recipient": "On the behalf of John", + "billing_address": { + "address": "33 rue du mail", + "postal_code": "75010", + "city": "Paris", + "country_alpha2": "FR" + }, + "delivery_address": { + "address": "33 rue du mail", + "postal_code": "75010", + "city": "Paris", + "country_alpha2": "FR" + }, + "payment_conditions": "upon_receipt", + "phone": "+33123232323", + "reference": "This is a custom reference", + "notes": "This is a note", + "plan_item": { + "number": "string", + "label": "string", + "enabled": true, + "vat_rate": "string", + "country_alpha2": "string", + "description": "string" + }, + "v2_id": 1234 +} \ No newline at end of file From 56f01903a7670069118b2c76902043078d4ce43d Mon Sep 17 00:00:00 2001 From: Luan Cazarine Date: Mon, 16 Dec 2024 10:02:55 -0300 Subject: [PATCH 3/5] pnpm update --- pnpm-lock.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4a33aa6f1955d..b767108a6b82a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7506,7 +7506,11 @@ importers: components/pendo: {} - components/pennylane: {} + components/pennylane: + dependencies: + '@pipedream/platform': + specifier: ^3.0.3 + version: 3.0.3 components/people_data_labs: dependencies: From 827a81dc570223e661b2e62c7fb3638be2a85827 Mon Sep 17 00:00:00 2001 From: Luan Cazarine Date: Fri, 20 Dec 2024 18:07:18 -0300 Subject: [PATCH 4/5] [Components] pennylane #14952 Sources - New Billing Subscription - New Customer Invoice - New Customer Actions - Create Customer - Create Billing Subscription - Create Customer Invoice --- .../create-billing-subscription.mjs | 117 +++++++++++------- .../create-customer-invoice.mjs | 107 +++++++++------- .../create-customer/create-customer.mjs | 86 +++++++------ components/pennylane/common/constants.mjs | 52 ++++++++ components/pennylane/pennylane.app.mjs | 18 ++- components/pennylane/sources/common/base.mjs | 16 +-- 6 files changed, 258 insertions(+), 138 deletions(-) diff --git a/components/pennylane/actions/create-billing-subscription/create-billing-subscription.mjs b/components/pennylane/actions/create-billing-subscription/create-billing-subscription.mjs index b430b3fa594e8..4835da3aa8a83 100644 --- a/components/pennylane/actions/create-billing-subscription/create-billing-subscription.mjs +++ b/components/pennylane/actions/create-billing-subscription/create-billing-subscription.mjs @@ -1,3 +1,10 @@ +import { + DAY_OF_WEEK_OPTIONS, + MODE_OPTIONS, + PAYMENT_CONDITIONS_OPTIONS, + PAYMENT_METHOD_OPTIONS, + RECURRING_RULE_TYPE, +} from "../../common/constants.mjs"; import { parseObject } from "../../common/utils.mjs"; import pennylane from "../../pennylane.app.mjs"; @@ -9,97 +16,117 @@ export default { type: "action", props: { pennylane, + currency: { + type: "string", + label: "Currency", + description: "Invoice Currency (ISO 4217). Default is EUR.", + optional: true, + }, mode: { type: "string", label: "Mode", - description: "Mode", - optional: true, + description: "Mode in which the new invoices will be created.", + options: MODE_OPTIONS, + }, + start: { + type: "string", + label: "Start", + description: "Start date (ISO 8601)", }, paymentConditions: { type: "string", label: "Payment Conditions", - description: "PaymentConditions", - optional: true, + description: "Customer payment conditions", + options: PAYMENT_CONDITIONS_OPTIONS, }, paymentMethod: { type: "string", label: "Payment Method", description: "PaymentMethod", - optional: true, + options: PAYMENT_METHOD_OPTIONS, }, - type: { + recurringRuleType: { type: "string", - label: "Type", - description: "Type", - optional: true, + label: "Recurring Rule Type", + description: "Type of the billing subscription's recurrence", + options: RECURRING_RULE_TYPE, + reloadProps: true, }, dayOfMonth: { - type: "string", + type: "integer", label: "Day Of Month", - description: "DayOfMonth", - optional: true, + description: "The day of occurrences of the recurring rule", + hidden: true, }, dayOfWeek: { type: "string", label: "Day Of Week", - description: "DayOfWeek", - optional: true, + description: "The day of occurrences of the recurring rule", + options: DAY_OF_WEEK_OPTIONS, + hidden: true, }, interval: { type: "string", label: "Interval", - description: "Interval", + description: "The interval of occurrences of the recurring rule", optional: true, }, count: { - type: "string", + type: "integer", label: "Count", - description: "Count", - optional: true, - }, - sourceId: { - type: "string", - label: "Source Id", - description: "SourceId", - optional: true, - }, - currency: { - type: "string", - label: "Currency", - description: "Currency", - optional: true, - }, - start: { - type: "string", - label: "Start", - description: "Start", + description: "Number of occurrences of the recurring rule", optional: true, }, specialMention: { type: "string", label: "Special Mention", - description: "Special Mention", + description: "Additional details", optional: true, }, discount: { type: "integer", label: "Discount", - description: "Discount", + description: "Invoice discount (in percent)", optional: true, }, + customerId: { + propDefinition: [ + pennylane, + "customerId", + ], + }, lineItemsSectionsAttributes: { - type: "string", - label: "Line Items Sections Attributes", - description: "Line Items Sections Attributes", + propDefinition: [ + pennylane, + "lineItemsSectionsAttributes", + ], optional: true, }, invoiceLines: { - type: "string", + propDefinition: [ + pennylane, + "lineItems", + ], label: "Invoice Lines", - description: "Invoice Lines", - optional: true, }, }, + async additionalProps(props) { + switch (this.recurringRuleType) { + case "monthly": + props.dayOfMonth.hidden = false; + props.dayOfWeek.hidden = true; + break; + case "weekly": + props.dayOfMonth.hidden = true; + props.dayOfWeek.hidden = false; + break; + case "yearly": + props.dayOfMonth.hidden = true; + props.dayOfWeek.hidden = true; + break; + } + return {}; + }, async run({ $ }) { const response = await this.pennylane.createBillingSubscription({ $, @@ -113,7 +140,7 @@ export default { payment_conditions: this.paymentConditions, payment_method: this.paymentMethod, recurring_rule: { - type: this.type, + type: this.recurringRuleType, day_of_month: this.dayOfMonth, day_of_week: this.dayOfWeek, interval: this.interval, @@ -122,7 +149,7 @@ export default { special_mention: this.specialMention, discount: this.discount, customer: { - source_id: this.sourceId, + source_id: this.customerId, }, line_items_sections_attributes: parseObject(this.lineItemsSectionsAttributes), invoice_lines: parseObject(this.invoiceLines), diff --git a/components/pennylane/actions/create-customer-invoice/create-customer-invoice.mjs b/components/pennylane/actions/create-customer-invoice/create-customer-invoice.mjs index 60608d7db57e4..1e568b8339f73 100644 --- a/components/pennylane/actions/create-customer-invoice/create-customer-invoice.mjs +++ b/components/pennylane/actions/create-customer-invoice/create-customer-invoice.mjs @@ -1,4 +1,9 @@ -import { LANGUAGE_OPTIONS } from "../../common/constants.mjs"; +import { ConfigurationError } from "@pipedream/platform"; +import { + BANKING_PROVIDER_OPTIONS, + LANGUAGE_OPTIONS, + PROVIDER_FIELD_NAMES_OPTIONS, +} from "../../common/constants.mjs"; import { parseObject } from "../../common/utils.mjs"; import pennylane from "../../pennylane.app.mjs"; @@ -41,7 +46,7 @@ export default { draft: { type: "boolean", label: "Draft", - description: "Do you wish to create a draft invoice (otherwise it is a finalized invoice) ? Reminder, once created, a finalized invoice cannot be edited !", + description: "Do you wish to create a draft invoice (otherwise it is a finalized invoice)? Reminder, once created, a finalized invoice cannot be edited!", }, currency: { type: "string", @@ -72,11 +77,15 @@ export default { type: "string", label: "Banking Provider", description: "The banking provider for the transaction", + options: BANKING_PROVIDER_OPTIONS, + reloadProps: true, }, providerFieldName: { type: "string", label: "Provider Field Name", description: "Name of the field that you want to match", + options: PROVIDER_FIELD_NAMES_OPTIONS, + hidden: true, }, providerFieldValue: { type: "string", @@ -90,15 +99,17 @@ export default { ], }, lineItemsSectionsAttributes: { - type: "string[]", - label: "Line Items Sections Attributes", - description: "A list of objects of items sections to be listed on the invoice", + propDefinition: [ + pennylane, + "lineItemsSectionsAttributes", + ], optional: true, }, lineItems: { - type: "string[]", - label: "Line Items", - description: "A list of objects of items to be listed on the invoice", + propDefinition: [ + pennylane, + "lineItems", + ], }, categories: { type: "string[]", @@ -117,43 +128,55 @@ export default { description: "End date of the imputation period (ISO 8601)", }, }, + async additionalProps(props) { + if (this.bankingProvider === "stripe") { + props.providerFieldName.hidden = false; + } + return {}; + }, async run({ $ }) { - const invoice = await this.pennylane.createInvoice({ - $, - data: { - create_custome: false, - create_products: false, - invoice: { - date: this.date, - deadline: this.deadline, - external_id: this.externalId, - pdf_invoice_free_text: this.pdfInvoiceFreeText, - pdf_invoice_subject: this.pdfInvoiceSubject, - draft: this.draft, - currency: this.currency, - special_mention: this.specialMention, - discount: this.discount, - language: this.language, - transactions_reference: { - banking_provider: this.bankingProvider, - provider_field_name: this.providerFieldName, - provider_field_value: this.providerFieldValue, - }, - customer: { - source_id: this.customerId, - }, - line_items_sections_attributes: parseObject(this.lineItemsSectionsAttributes), - line_items: parseObject(this.lineItems), - categories: parseObject(this.categories), - imputation_dates: { - start_date: this.startDate, - end_date: this.endDate, + try { + const invoice = await this.pennylane.createInvoice({ + $, + data: { + create_customer: false, + create_products: false, + invoice: { + date: this.date, + deadline: this.deadline, + external_id: this.externalId, + pdf_invoice_free_text: this.pdfInvoiceFreeText, + pdf_invoice_subject: this.pdfInvoiceSubject, + draft: this.draft, + currency: this.currency, + special_mention: this.specialMention, + discount: this.discount, + language: this.language, + transactions_reference: { + banking_provider: this.bankingProvider, + provider_field_name: (this.bankingProvider === "gocardless") + ? "payment_id" + : this.providerFieldName, + provider_field_value: this.providerFieldValue, + }, + customer: { + source_id: this.customerId, + }, + line_items_sections_attributes: parseObject(this.lineItemsSectionsAttributes), + line_items: parseObject(this.lineItems), + categories: parseObject(this.categories), + imputation_dates: { + start_date: this.startDate, + end_date: this.endDate, + }, }, }, - }, - }); + }); - $.export("$summary", `Created invoice with ID ${invoice.invoice.id}`); - return invoice; + $.export("$summary", `Created invoice with ID ${invoice.invoice.id}`); + return invoice; + } catch ({ response }) { + throw new ConfigurationError(response?.data?.message || response?.data?.error); + } }, }; diff --git a/components/pennylane/actions/create-customer/create-customer.mjs b/components/pennylane/actions/create-customer/create-customer.mjs index 3d1ada49a8531..38054afd4905c 100644 --- a/components/pennylane/actions/create-customer/create-customer.mjs +++ b/components/pennylane/actions/create-customer/create-customer.mjs @@ -1,7 +1,9 @@ +import { ConfigurationError } from "@pipedream/platform"; import { CUSTOMER_GENDER_OPTIONS, CUSTOMER_TYPE_OPTIONS, PAYMENT_CONDITIONS_OPTIONS, } from "../../common/constants.mjs"; +import { parseObject } from "../../common/utils.mjs"; import pennylane from "../../pennylane.app.mjs"; export default { @@ -76,6 +78,7 @@ export default { type: "string", label: "Recipient", description: "Recipient displayed in the invoice.", + hidden: true, optional: true, }, vatNumber: { @@ -169,6 +172,7 @@ export default { const typeCompany = (this.customerType === "company"); props.name.hidden = !typeCompany; props.regNo.hidden = !typeCompany; + props.recipient.hidden = !typeCompany; props.vatNumber.hidden = !typeCompany; props.firstName.hidden = typeCompany; props.lastName.hidden = typeCompany; @@ -176,46 +180,50 @@ export default { return {}; }, async run({ $ }) { - const additionalData = this.customerType === "company" - ? { - name: this.name, - reg_no: this.regNo, - vat_number: this.vatNumber, - } - : { - first_name: this.firstName, - last_name: this.lastName, - gender: this.gender, - }; - - const response = await this.pennylane.createCustomer({ - $, - data: { - customer: { - customer_type: this.customerType, - address: this.address, - postal_code: this.postalCode, - city: this.city, - country: this.country, + try { + const additionalData = this.customerType === "company" + ? { + name: this.name, + reg_no: this.regNo, + vat_number: this.vatNumber, recipient: this.recipient, - source_id: this.sourceId, - emails: this.emails, - billing_iban: this.billingIban, - delivery_address: this.deliveryAddress, - delivery_postal_code: this.deliveryPostalCode, - delivery_city: this.deliveryCity, - delivery_country: this.deliveryCountry, - payment_conditions: this.paymentConditions, - phone: this.phone, - reference: this.reference, - notes: this.notes, - mandate: this.mandate, - plan_item: this.planItem, - ...additionalData, + } + : { + first_name: this.firstName, + last_name: this.lastName, + gender: this.gender, + }; + + const response = await this.pennylane.createCustomer({ + $, + data: { + customer: { + customer_type: this.customerType, + address: this.address, + postal_code: this.postalCode, + city: this.city, + country_alpha2: this.country, + source_id: this.sourceId, + emails: parseObject(this.emails), + billing_iban: this.billingIban, + delivery_address: this.deliveryAddress, + delivery_postal_code: this.deliveryPostalCode, + delivery_city: this.deliveryCity, + delivery_country_alpha2: this.deliveryCountry, + payment_conditions: this.paymentConditions, + phone: this.phone, + reference: this.reference, + notes: this.notes, + mandate: parseObject(this.mandate), + plan_item: parseObject(this.planItem), + ...additionalData, + }, }, - }, - }); - $.export("$summary", `Successfully created customer with Id: ${response.customer.source_id}`); - return response; + }); + $.export("$summary", `Successfully created customer with Id: ${response.customer.source_id}`); + return response; + } catch ({ response }) { + throw new ConfigurationError(response?.data?.message || response?.data?.error); + } }, }; diff --git a/components/pennylane/common/constants.mjs b/components/pennylane/common/constants.mjs index 993d4ed405c13..3cdf83a14f219 100644 --- a/components/pennylane/common/constants.mjs +++ b/components/pennylane/common/constants.mjs @@ -21,3 +21,55 @@ export const LANGUAGE_OPTIONS = [ "en_GB", "fr_FR", ]; + +export const BANKING_PROVIDER_OPTIONS = [ + "gocardless", + "stripe", +]; + +export const PROVIDER_FIELD_NAMES_OPTIONS = [ + "payment_id", + "charge_id", +]; + +export const RECURRING_RULE_TYPE = [ + "monthly", + "weekly", + "yearly", +]; + +export const MODE_OPTIONS = [ + { + label: "Draft invoices will be created", + value: "awaiting_validation", + }, + { + label: "Finalized invoices will be created", + value: "finalized", + }, + { + label: "Finalized invoices will be created and autimatically sent to the client at each new occurrence", + value: "email", + }, +]; + +export const PAYMENT_METHOD_OPTIONS = [ + { + label: "Offline - The subscription is not linked to a payment method", + value: "offline", + }, + { + label: "Gocardless Direct Debit - At each new occurrence the client will be automatically debited thanks to GoCardless", + value: "gocardless_direct_debit", + }, +]; + +export const DAY_OF_WEEK_OPTIONS = [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday", +]; diff --git a/components/pennylane/pennylane.app.mjs b/components/pennylane/pennylane.app.mjs index 0fc04bfc7baa2..a163a81424d8b 100644 --- a/components/pennylane/pennylane.app.mjs +++ b/components/pennylane/pennylane.app.mjs @@ -9,20 +9,30 @@ export default { label: "Customer Id", description: "Existing customer identifier (source_id)", async options({ page }) { - const { data } = await this.listCustomers({ + const { customers } = await this.listCustomers({ params: { page: page + 1, }, }); - return data.map(({ - id: value, name: label, + return customers.map(({ + source_id: value, name: label, }) => ({ label, value, })); }, }, + lineItemsSectionsAttributes: { + type: "string[]", + label: "Line Items Sections Attributes", + description: "A list of objects of items sections to be listed on the invoice. **Example: [{\"title\": \"Title\",\"description\": \"description\",\"rank\": 1}]** [See the documentation](https://pennylane.readme.io/reference/customer_invoices-post-1) from further information.", + }, + lineItems: { + type: "string[]", + label: "Line Items", + description: "A list of objects of items to be listed on the invoice. **Example: [{\"product\": {\"source_id\": \"0e67fc3c-c632-4feb-ad34-e18ed5fbf66a\",\"unit\": \"20\",\"price\": \"10.00\",\"label\": \"Product label\",\"vat_rate\": \"FR_09\",\"currency\": \"EUR\"},\"quantity\": 2,\"label\": \"Line Item Label\"}]** [See the documentation](https://pennylane.readme.io/reference/customer_invoices-post-1) from further information.", + }, }, methods: { _baseUrl() { @@ -30,7 +40,7 @@ export default { }, _headers() { return { - "Authorization": `Bearer ${this.$auth.api_token}`, + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, "Content-Type": "application/json", }; }, diff --git a/components/pennylane/sources/common/base.mjs b/components/pennylane/sources/common/base.mjs index 5d6763beaf42e..43611e79f381e 100644 --- a/components/pennylane/sources/common/base.mjs +++ b/components/pennylane/sources/common/base.mjs @@ -13,14 +13,14 @@ export default { }, }, methods: { - _getLastId() { - return this.db.get("lastId") || 0; + _getLastDate() { + return this.db.get("lastDate") || 0; }, - _setLastId(lastId) { - this.db.set("lastId", lastId); + _setLastDate(lastDate) { + this.db.set("lastDate", lastDate); }, async emitEvent(maxResults = false) { - const lastId = this._getLastId(); + const lastDate = this._getLastDate(); const response = this.pennylane.paginate({ fn: this.getFunction(), @@ -29,7 +29,7 @@ export default { let responseArray = []; for await (const item of response) { - if (item.source_id <= lastId) break; + if (Date.parse(item.updated_at) <= lastDate) break; responseArray.push(item); } @@ -37,12 +37,12 @@ export default { if (maxResults && (responseArray.length > maxResults)) { responseArray.length = maxResults; } - this._setLastId(responseArray[0].source_id); + this._setLastDate(responseArray[0].updated_at); } for (const item of responseArray.reverse()) { this.$emit(item, { - id: item.source_id, + id: item.source_id || item.id, summary: this.getSummary(item), ts: Date.parse(item.updated_at || item.activated_at || new Date()), }); From 487d20e0bc9292cc47a5a40533072f8e2ca5fa47 Mon Sep 17 00:00:00 2001 From: Luan Cazarine Date: Fri, 20 Dec 2024 18:08:08 -0300 Subject: [PATCH 5/5] pnpm update --- pnpm-lock.yaml | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b767108a6b82a..bdd7501f145bd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12168,10 +12168,10 @@ importers: version: 14.2.19(@babel/core@8.0.0-alpha.13)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) nextra: specifier: latest - version: 3.2.5(@types/react@18.3.12)(acorn@8.14.0)(next@14.2.19(@babel/core@8.0.0-alpha.13)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) + version: 3.3.0(@types/react@18.3.12)(acorn@8.14.0)(next@14.2.19(@babel/core@8.0.0-alpha.13)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) nextra-theme-docs: specifier: latest - version: 3.2.5(next@14.2.19(@babel/core@8.0.0-alpha.13)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@3.2.5(@types/react@18.3.12)(acorn@8.14.0)(next@14.2.19(@babel/core@8.0.0-alpha.13)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 3.3.0(next@14.2.19(@babel/core@8.0.0-alpha.13)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@3.3.0(@types/react@18.3.12)(acorn@8.14.0)(next@14.2.19(@babel/core@8.0.0-alpha.13)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: ^18.3.1 version: 18.3.1 @@ -22363,16 +22363,16 @@ packages: sass: optional: true - nextra-theme-docs@3.2.5: - resolution: {integrity: sha512-eF0j1VNNS1rFjZOfYqlrXISaCU3+MhZ9hhXY+TUydRlfELrFWpGzrpW6MiL7hq4JvUR7OBtHHs8+A+8AYcETBQ==} + nextra-theme-docs@3.3.0: + resolution: {integrity: sha512-4JSbDmsbtaYa2eKHsNymWy6So4/fAAXuNPSkjgQ3S+aLRiC730mR9djdkTd1iRca4+czetzBWaqxu+HwTVSSCA==} peerDependencies: next: '>=13' - nextra: 3.2.5 + nextra: 3.3.0 react: '>=18' react-dom: '>=18' - nextra@3.2.5: - resolution: {integrity: sha512-n665DRpI/brjHXM83G5LdlbYA2nOtjaLcWJs7mZS3gkuRDmEXpJj4XJ860xrhkYZW2iYoUMu32zzhPuFByU7VA==} + nextra@3.3.0: + resolution: {integrity: sha512-//+bQW3oKrpLrrIFD5HJow310+YcNYhGIgdM4y+EjYuIXScJcgp6Q4Upsq8c2s2fQKEJjeuf+hXKusx9fvH/2w==} engines: {node: '>=18'} peerDependencies: next: '>=13' @@ -23424,6 +23424,12 @@ packages: '@types/react': '>=18' react: '>=18' + react-medium-image-zoom@5.2.12: + resolution: {integrity: sha512-BbQ9jLBFxu6z+viH5tzQzAGqHOJQoYUM7iT1KUkamWKOO6vR1pC33os7LGLrHvOcyySMw74rUdoUCXFdeglwCQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-select@5.8.3: resolution: {integrity: sha512-lVswnIq8/iTj1db7XCG74M/3fbGB6ZaluCzvwPGT5ZOjCdL/k0CLWhEK0vCBLuU5bHTEf6Gj8jtSvi+3v+tO1w==} peerDependencies: @@ -39701,7 +39707,7 @@ snapshots: - '@babel/core' - babel-plugin-macros - nextra-theme-docs@3.2.5(next@14.2.19(@babel/core@8.0.0-alpha.13)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@3.2.5(@types/react@18.3.12)(acorn@8.14.0)(next@14.2.19(@babel/core@8.0.0-alpha.13)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + nextra-theme-docs@3.3.0(next@14.2.19(@babel/core@8.0.0-alpha.13)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@3.3.0(@types/react@18.3.12)(acorn@8.14.0)(next@14.2.19(@babel/core@8.0.0-alpha.13)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@headlessui/react': 2.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) clsx: 2.1.1 @@ -39709,13 +39715,13 @@ snapshots: flexsearch: 0.7.43 next: 14.2.19(@babel/core@8.0.0-alpha.13)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-themes: 0.4.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - nextra: 3.2.5(@types/react@18.3.12)(acorn@8.14.0)(next@14.2.19(@babel/core@8.0.0-alpha.13)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) + nextra: 3.3.0(@types/react@18.3.12)(acorn@8.14.0)(next@14.2.19(@babel/core@8.0.0-alpha.13)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) scroll-into-view-if-needed: 3.1.0 zod: 3.23.8 - nextra@3.2.5(@types/react@18.3.12)(acorn@8.14.0)(next@14.2.19(@babel/core@8.0.0-alpha.13)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3): + nextra@3.3.0(@types/react@18.3.12)(acorn@8.14.0)(next@14.2.19(@babel/core@8.0.0-alpha.13)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3): dependencies: '@formatjs/intl-localematcher': 0.5.8 '@headlessui/react': 2.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -39742,6 +39748,7 @@ snapshots: p-limit: 6.1.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + react-medium-image-zoom: 5.2.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1) rehype-katex: 7.0.1 rehype-pretty-code: 0.14.0(shiki@1.24.0) rehype-raw: 7.0.0 @@ -41179,6 +41186,11 @@ snapshots: transitivePeerDependencies: - supports-color + react-medium-image-zoom@5.2.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-select@5.8.3(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.26.0