diff --git a/components/oto/actions/create-product/create-product.mjs b/components/oto/actions/create-product/create-product.mjs new file mode 100644 index 0000000000000..dfc21e0807bc2 --- /dev/null +++ b/components/oto/actions/create-product/create-product.mjs @@ -0,0 +1,111 @@ +import oto from "../../oto.app.mjs"; +import { parseObject } from "../../common/utils.mjs"; + +export default { + key: "oto-create-product", + name: "Create Product", + description: "Creates a new product. [See the documentation](https://apis.tryoto.com/#21b289bc-04c1-49b1-993e-23e928d57f56)", + version: "0.0.1", + type: "action", + props: { + oto, + sku: { + type: "string", + label: "Sku", + description: "SKU of the product", + }, + productName: { + type: "string", + label: "Product Name", + description: "Name of the product", + }, + price: { + type: "string", + label: "Price", + description: "Price of the product", + }, + taxAmount: { + type: "string", + label: "Tax Amount", + description: "Tax Amount of the product", + }, + brandId: { + propDefinition: [ + oto, + "brandId", + ], + }, + description: { + type: "string", + label: "Description", + description: "Description of the product", + optional: true, + }, + barcode: { + type: "string", + label: "Barcode", + description: "Barcode of the product", + optional: true, + }, + secondBarcode: { + type: "string", + label: "Second Barcode", + description: "Second Barcode of the product", + optional: true, + }, + productImage: { + type: "string", + label: "Product Image", + description: "Image Link of the product", + optional: true, + }, + category: { + type: "string", + label: "Category", + description: "Category of the product", + optional: true, + }, + hsCode: { + type: "string", + label: "HS Code", + description: "A standardized numerical method of classifying traded products", + optional: true, + }, + itemOrigin: { + type: "string", + label: "Item Origin", + description: "Origin of the product", + optional: true, + }, + customAttributes: { + type: "object", + label: "Custom Attributes", + description: "Custom attributes of the product specified as a JSON Array of objects with keys `attributeName` and `attributeValue`. Example: `[{ \"attributeName\": \"112\", \"attributeValue\": \"test product\"}]`", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.oto.createProduct({ + $, + data: { + sku: this.sku, + productName: this.productName, + price: this.price, + taxAmount: this.taxAmount, + brandId: this.brandId, + description: this.description, + barcode: this.barcode, + secondBarcode: this.secondBarcode, + productImage: this.productImage, + category: this.category, + hsCode: this.hsCode, + itemOrigin: this.itemOrigin, + customAttributes: parseObject(this.customAttributes), + }, + }); + if (response.productId) { + $.export("$summary", `Successfully created product with ID: ${response.productId}`); + } + return response; + }, +}; diff --git a/components/oto/actions/get-order-details/get-order-details.mjs b/components/oto/actions/get-order-details/get-order-details.mjs new file mode 100644 index 0000000000000..f7f5d7cfe2707 --- /dev/null +++ b/components/oto/actions/get-order-details/get-order-details.mjs @@ -0,0 +1,28 @@ +import oto from "../../oto.app.mjs"; + +export default { + key: "oto-get-order-details", + name: "Get Order Details", + description: "Provides detailed information about a specific order. [See the documentation](https://apis.tryoto.com/#53964419-2d64-4c07-b39d-b26a92b379c9)", + version: "0.0.1", + type: "action", + props: { + oto, + orderId: { + propDefinition: [ + oto, + "orderId", + ], + }, + }, + async run({ $ }) { + const response = await this.oto.getOrderDetails({ + $, + params: { + orderId: this.orderId, + }, + }); + $.export("$summary", `Successfully retrieved details for order with ID: ${this.orderId}`); + return response; + }, +}; diff --git a/components/oto/actions/list-orders/list-orders.mjs b/components/oto/actions/list-orders/list-orders.mjs new file mode 100644 index 0000000000000..5710406c59ef5 --- /dev/null +++ b/components/oto/actions/list-orders/list-orders.mjs @@ -0,0 +1,66 @@ +import oto from "../../oto.app.mjs"; + +export default { + key: "oto-list-orders", + name: "List Orders", + description: "Retrieves a list of orders. [See the documentation](https://apis.tryoto.com/#c2e94027-5214-456d-b653-0a66c038e3a4)", + version: "0.0.1", + type: "action", + props: { + oto, + status: { + propDefinition: [ + oto, + "status", + ], + }, + minDate: { + type: "string", + label: "Min Date", + description: "Starting \"Order Creation Date\" of your orders in \"yyyy-mm-dd\" format", + optional: true, + }, + maxDate: { + type: "string", + label: "Max Date", + description: "Ending \"Order Creation Date\" of your orders in \"yyyy-mm-dd\" format", + optional: true, + }, + maxResults: { + type: "integer", + label: "Max Results", + description: "The maximum number of orders to return", + default: 100, + optional: true, + }, + }, + async run({ $ }) { + const results = this.oto.paginate({ + fn: this.oto.listOrders, + args: { + $, + params: { + minDate: this.minDate, + maxDate: this.maxDate, + status: this.status, + }, + }, + resourceKey: "orders", + max: this.maxResults, + }); + + const orders = []; + for await (const order of results) { + orders.push(order); + } + + if (orders[0].items?.length) { + $.export("$summary", `Successfully retrieved ${orders.length} order${orders.length === 1 + ? "" + : "s"}`); + } else { + $.export("$summary", "No orders found"); + } + return orders; + }, +}; diff --git a/components/oto/common/constants.mjs b/components/oto/common/constants.mjs new file mode 100644 index 0000000000000..3bf20ef23847b --- /dev/null +++ b/components/oto/common/constants.mjs @@ -0,0 +1,68 @@ +const DEFAULT_LIMIT = 100; + +const STATUSES = [ + "new", + "paymentConfirmed", + "waitingAddressConfirmation", + "addressConfirmed", + "needConfirmation", + "paymentTypeConfirmed", + "codOrderConfirmed", + "orderConfirmed", + "pickupFromStore", + "interDepotTransfer", + "canceled", + "deleted", + "readyForCollection", + "branchAssigned", + "assignedToWarehouse", + "shipmentOnHoldWarehouse", + "shipmentOnHoldToCancel", + "notAvailableBR", + "notAvailableWH", + "picked", + "packed", + "searchingDriver", + "shipmentCreated", + "goingToPickup", + "arrivedPickup", + "pickedUp", + "arrivedDestinationTerminal", + "arrivedTerminal", + "departedTerminal", + "inTransit", + "arrivedOriginTerminal", + "outForDelivery", + "arrivedDestination", + "shipmentInProgress", + "undeliveredAttempt", + "shipmentOnHold", + "delivered", + "returned", + "returnProcessing", + "returnShipmentProcessing", + "reverseShipmentProcessing", + "reverseShipmentCreated", + "reverseShipmentCanceled", + "reverseGoingToPickup", + "reversePickedUp", + "reverseOutForDelivery", + "reverseArrivedTerminal", + "reverseDepartedTerminal", + "reverseArrivedDestinationTerminal", + "reverseUndeliveredAttempt", + "reverseShipmentOnHold", + "reverseReturned", + "reverseConfirmReturn", + "shipmentCanceled", + "lostOrDamaged", + "confirmedReturn", + "approved", + "rejected", + "returnReverseComment", +]; + +export default { + DEFAULT_LIMIT, + STATUSES, +}; diff --git a/components/oto/common/utils.mjs b/components/oto/common/utils.mjs new file mode 100644 index 0000000000000..78ef2a6bb6834 --- /dev/null +++ b/components/oto/common/utils.mjs @@ -0,0 +1,24 @@ +export function 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 item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/oto/oto.app.mjs b/components/oto/oto.app.mjs index 41dbdb91de0be..5b809da7096c7 100644 --- a/components/oto/oto.app.mjs +++ b/components/oto/oto.app.mjs @@ -1,11 +1,134 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + export default { type: "app", app: "oto", - propDefinitions: {}, + propDefinitions: { + brandName: { + type: "string", + label: "Brand Name", + description: "The brand name associated with the shipment", + optional: true, + async options() { + const { clientStores } = await this.listBrands(); + return clientStores?.map(({ storeName }) => storeName) || []; + }, + }, + brandId: { + type: "string", + label: "Brand ID", + description: "The brand ID of the product", + optional: true, + async options() { + const { clientStores } = await this.listBrands(); + return clientStores?.map(({ + ID: value, storeName: label, + }) => ({ + label, + value, + })) || []; + }, + }, + orderId: { + type: "string", + label: "Order ID", + description: "The ID of an order", + async options({ page }) { + const { orders } = await this.listOrders({ + params: { + page, + }, + }); + return orders?.map(({ orderId }) => orderId ) || []; + }, + }, + status: { + type: "string", + label: "Status", + description: "The status of an order", + options: constants.STATUSES, + optional: true, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _baseUrl() { + return `${this.$auth.api_url}/rest/v2`; + }, + _makeRequest({ + $ = this, path, ...otherOpts + }) { + return axios($, { + ...otherOpts, + url: `${this._baseUrl()}${path}`, + headers: { + Authorization: `Bearer ${this.$auth.oauth_access_token}`, + }, + }); + }, + createWebhook(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/webhook", + ...opts, + }); + }, + deleteWebhook(opts = {}) { + return this._makeRequest({ + method: "DELETE", + path: "/webhook", + ...opts, + }); + }, + getOrderDetails(opts = {}) { + return this._makeRequest({ + path: "/orderDetails", + ...opts, + }); + }, + listOrders(opts = {}) { + return this._makeRequest({ + path: "/orders", + ...opts, + }); + }, + listBrands(opts = {}) { + return this._makeRequest({ + path: "/getBrandList", + ...opts, + }); + }, + createProduct(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/createProduct", + ...opts, + }); + }, + async *paginate({ + fn, args, resourceKey, max, + }) { + args = { + ...args, + params: { + ...args?.params, + perPage: constants.DEFAULT_LIMIT, + page: 1, + }, + }; + let total, count = 0; + do { + const response = await fn(args); + const items = response[resourceKey] ?? []; + for (const item of items) { + yield item; + if (max && ++count >= max) { + return; + } + } + total = items.length; + args.params.page++; + } while (total === args.params.perPage); }, }, }; diff --git a/components/oto/package.json b/components/oto/package.json index 58cd469742de9..190bf83996cf2 100644 --- a/components/oto/package.json +++ b/components/oto/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/oto", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream OTO Components", "main": "oto.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } -} \ No newline at end of file +} diff --git a/components/oto/sources/common/base.mjs b/components/oto/sources/common/base.mjs new file mode 100644 index 0000000000000..79e4bc9831882 --- /dev/null +++ b/components/oto/sources/common/base.mjs @@ -0,0 +1,53 @@ +import oto from "../../oto.app.mjs"; + +export default { + props: { + oto, + db: "$.service.db", + http: "$.interface.http", + }, + hooks: { + async activate() { + const { id } = await this.oto.createWebhook({ + data: { + url: this.http.endpoint, + method: "POST", + webhookType: this.getWebhookType(), + }, + }); + this._setHookId(id); + }, + async deactivate() { + const id = this._getHookId(); + if (id) { + await this.oto.deleteWebhook({ + params: { + id, + }, + }); + } + }, + }, + methods: { + _getHookId() { + return this.db.get("hookId"); + }, + _setHookId(hookId) { + this.db.set("hookId", hookId); + }, + getWebhookType() { + throw new Error("getWebhookType is not implemented"); + }, + generateMeta() { + throw new Error ("generateMeta is not implemented"); + }, + }, + async run(event) { + const { body } = event; + if (!body) { + return; + } + const meta = this.generateMeta(body); + this.$emit(body, meta); + }, +}; diff --git a/components/oto/sources/order-status-updated-instant/order-status-updated-instant.mjs b/components/oto/sources/order-status-updated-instant/order-status-updated-instant.mjs new file mode 100644 index 0000000000000..decb991f65f0a --- /dev/null +++ b/components/oto/sources/order-status-updated-instant/order-status-updated-instant.mjs @@ -0,0 +1,26 @@ +import common from "../common/base.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "oto-order-status-updated-instant", + name: "Order Status Updated (Instant)", + description: "Emit new event when the status of an order changes. [See the documentation](https://apis.tryoto.com/#9671ca1f-7d06-43fc-8ee9-cf9c336b088d)", + version: "0.0.1", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getWebhookType() { + return "orderStatus"; + }, + generateMeta(event) { + return { + id: event.orderId, + summary: `Status Updated for Order with ID: ${event.orderId}`, + ts: event.timestamp, + }; + }, + }, + sampleEmit, +}; diff --git a/components/oto/sources/order-status-updated-instant/test-event.mjs b/components/oto/sources/order-status-updated-instant/test-event.mjs new file mode 100644 index 0000000000000..b4fb9d547b92f --- /dev/null +++ b/components/oto/sources/order-status-updated-instant/test-event.mjs @@ -0,0 +1,13 @@ +export default { + "dcStatus": "", + "note": "", + "feedbackLink": "", + "shipmentWeight": 1, + "orderId": "123", + "reverseShipment": false, + "brandedTrackingURL": "", + "pickupLocationCode": "DefaultWH", + "trackingNumber": "", + "status": "delivered", + "timestamp": 1746721659000, +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4dc8f6929af50..57ebaf4c9cab9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9228,7 +9228,11 @@ importers: components/osu: {} - components/oto: {} + components/oto: + dependencies: + '@pipedream/platform': + specifier: ^3.0.3 + version: 3.0.3 components/otter_waiver: dependencies: @@ -15313,14 +15317,6 @@ importers: specifier: ^6.0.0 version: 6.2.0 - modelcontextprotocol/node_modules2/@modelcontextprotocol/sdk/dist/cjs: {} - - modelcontextprotocol/node_modules2/@modelcontextprotocol/sdk/dist/esm: {} - - modelcontextprotocol/node_modules2/zod-to-json-schema/dist/cjs: {} - - modelcontextprotocol/node_modules2/zod-to-json-schema/dist/esm: {} - packages/browsers: dependencies: '@sparticuz/chromium':